这里使用了 WPF(译者注:Windows Presentation Foundation) 的 3D 展示功能来对一个文档集合进行了可视化,这些文档是根据 AAAI 2014(一个人工智能会议)所接受的论文列表获取的。
潜在语义分析(LSA,Latent Semantic Analysis)使用文档词频矩阵(Document-term Matrix)的奇异值分解(SVD,Singular Value Decomposition)将文档集合投影到三维潜在空间(3D Latent Space)中。这个空间就被可视化在一个可以通过拖动鼠标进行操控的 3D 场景中。
本文中的程序使用了 Bright Wire 的开源机器学习库 来创建和归一化文档词频矩阵,并导入其相关的线性代数库以进行 SVD 和 LSA。
详情请参阅我之前的 CodeProject 文章,以了解关于向量空间模型技术的简单知识。这其中最主要的一点是,它们以每个文档的每个单词的计数为中心进行归一化,然后存储在一个矩阵中。于是我们就可以使用向量乘法来比较代表文档的列或行的相似性。
我们可以用一个简单的比喻来描述 SVD 的用途:
· 假设你有数以千计的热带鱼在一个大鱼缸里游来游去。而你则想拍摄这样一张照片:照片中展示了鱼缸中各种各样的鱼,同时保留了鱼之间的相对距离。这时候 SVD 就可以告诉我们,在任意给定的时刻,能让我们拍摄到最佳照片的定位相机的最好的位置与角度。
应用程序启动后,首先进行如下操作:
DataTable
中DataTable
中创建强类型的 AAAIDocument
var uri = new Uri("https://archive.ics.uci.edu/ml/machine-learning-databases/00307/%5bUCI%5d%20AAAI-14%20Accepted%20Papers%20-%20Papers.csv");
var KEYWORD_SPLIT = " \n".ToCharArray();
var TOPIC_SPLIT = "\n".ToCharArray();
// download the document list
var docList = new List<AAAIDocument>();
using (var client = new WebClient()) {
var data = client.DownloadData(uri);
Dispatcher.Invoke(() => {
_statusMessage.Add("Building data table...");
});
// parse the file CSV
var dataTable = new StreamReader(new MemoryStream(data)).ParseCSV(',');
// create strongly typed documents from the data table
dataTable.ForEach(row => docList.Add(new AAAIDocument {
Abstract = row.GetField<string>(5),
Keyword = row.GetField<string>(3).Split(KEYWORD_SPLIT, StringSplitOptions.RemoveEmptyEntries).Select(str => str.ToLower()).ToArray(),
Topic = row.GetField<string>(4).Split(TOPIC_SPLIT, StringSplitOptions.RemoveEmptyEntries),
Group = row.GetField<string>(2).Split(TOPIC_SPLIT, StringSplitOptions.RemoveEmptyEntries),
Title = row.GetField<string>(0)
}));
}
// create a document lookup table
var docTable = docList.ToDictionary(d => d.Title, d => d);
// extract features from the document's metadata
var stringTable = new StringTableBuilder();
var classificationSet = new SparseVectorClassificationSet {
Classification = docList.Select(d => d.AsClassification(stringTable)).ToArray()
};
// create dense feature vectors and normalise along the way
var encodings = classificationSet.Vectorise(true);
接下来的操作就是将这些密集特征向量组合成文档词频矩阵,并计算其 SVD。
找到前三个奇异值和 VT(V 矩阵的转置) 矩阵相应的行,然后相乘以产生潜在空间并投影到已经构建的文档词频矩阵中。
在潜在空间上运行 K 均值聚类来查找类似文档的组,以及为每个聚类关联不同颜色。
// create a term/document matrix with terms as columns and documents as rows
var matrix = lap.CreateMatrix(vectorList.Select(d => d.Data).ToList());
const int K = 3;
var kIndices = Enumerable.Range(0, K).ToList();
var matrixT = matrix.Transpose();
var svd = matrixT.Svd();
var s = lap.CreateDiagonal(svd.S.AsIndexable().Values.Take(K).ToList());
var v2 = svd.VT.GetNewMatrixFromRows(kIndices);
using (var sv2 = s.Multiply(v2)) {
var vectorList2 = sv2.AsIndexable().Columns.ToList();
var lookupTable2 = vectorList2.Select((v, i) => Tuple.Create(v, vectorList[i])).ToDictionary(d => (IVector)d.Item1, d => lookupTable[d.Item2]);
var clusters = vectorList2.KMeans(COLOUR_LIST.Length);
var clusterTable = clusters
.Select((l, i) => Tuple.Create(l, i))
.SelectMany(d => d.Item1.Select(v => Tuple.Create(v, d.Item2)))
.ToDictionary(d => d.Item1, d => COLOUR_LIST[d.Item2])
;
然后通过相关的AAAIDocument
,3D 投影以及簇色彩来创建Document
模型。然后对文档位置进行归一化以将其可视化。
var documentList = new List<Document>();
int index = 0;
double maxX = double.MinValue, minX = double.MaxValue, maxY = double.MinValue, minY = double.MaxValue, maxZ = double.MinValue, minZ = double.MaxValue;
foreach (var item in vectorList2) {
float x = item[0];
float y = item[1];
float z = item[2];
documentList.Add(new Document(x, y, z, index++, lookupTable2[item], clusterTable[item]));
if (x > maxX)
maxX = x;
if (x < minX)
minX = x;
if (y > maxY)
maxY = y;
if (y < minY)
minY = y;
if (z > maxZ)
maxZ = z;
if (z < minZ)
minZ = z;
}
double rangeX = maxX - minX;
double rangeY = maxY - minY;
double rangeZ = maxZ - minZ;
foreach (var document in documentList)
document.Normalise(minX, rangeX, minY, rangeY, minZ, rangeZ);
最后,每个都Document
被转换成一个Cube
并添加到 3D 视窗。
var numDocs = documentList.Count;
_cube = new Cube[numDocs];
var SCALE = 10;
for(var i = 0; i < numDocs; i++) {
var document = documentList[i];
var cube = _cube[i] = new Cube(SCALE * document.X, SCALE * document.Y, SCALE * document.Z, i);
cube.Colour = document.Colour;
viewPort.Children.Add(cube);
3D 场景中包含了:一个方向灯 —— 它能给予立方体一些额外的深度;以及一个PerspectiveCamera
—— 其位置都通过响应鼠标输入进行轨迹球代码转换而得来。
我们可以使用下述代码来测试 3D 立方体:
Cube foundCube = null;
SearchResult correspondingSearchResult = null;
HitTestResult result =
VisualTreeHelper.HitTest(viewPort, e.GetPosition(viewPort));
RayHitTestResult rayResult = result as RayHitTestResult;
if(rayResult != null) {
RayMeshGeometry3DHitTestResult rayMeshResult =
rayResult as RayMeshGeometry3DHitTestResult;
if(rayMeshResult != null) {
GeometryModel3D model =
rayMeshResult.ModelHit as GeometryModel3D;
foreach(KeyValuePair<int,> item in _cubeLookup) {
if(item.Value.Content == model &&
_searchResultLookup.TryGetValue(item.Key,
out correspondingSearchResult)) {
foundCube = item.Value;
break;
}
}
}
}
然后,选择/取消选中的多维数据集的笔刷和对应的搜索结果也可以进行相应更新。
我们可以通过按住鼠标左键或右键并拖动它以定位 3D 场景。这个轨迹球代码的有趣的之处,就是鼠标事件是被触发在一个叠加在 3D 场景上的透明的边界上的。这是因为WPF的Viewport3D
类只有在光标位于 3D 模型上时才会触发鼠标事件。轨迹球代码基本上是一个“黑盒”,它可以附加到任何 3D 场景来实现场景的视觉操纵。我们将其附加如下(注意,我们正在附加到强加的边界):
ModelViewer.Trackball trackball = new ModelViewer.Trackball();
myPerspectiveCamera.Transform = trackball.Transform;
directionalLight.Transform = trackball.Transform;
...
trackball.EventSource = borderCapture;
LSA 是广泛用于减少数据集维度的一项技术。在本文所述项目中,我们通过将其投影到三维而不是更为典型的二维来可视化,使我们得以保留更多实用的信息。
使用本文的可视化技术我们可以看到,这些论文通常遵循着相当一致的模式,论文的三个主要峰值主题是 博弈论,人工智能与人类 以及 计划与执行,我们还能发现在所收集到的论文中,有大量的论文主要描述了一些具体的机器学习技术。
这样的可视化方法也可以让我们更容易地发现数据集中的异常值。
这种技术的主要缺点是 SVD 计算开销较大。如果你使用 GPU 运行 Bright Wire,可能会有所改进,但通常 LSA 对于非常大的矩阵来说并不实用。