代码可视化算法流程

这个是暂时的效果,一个点是一个类或者全局函数。高度场暗示依赖关系,高度高的会依赖高度低的。

 

 

下面是代码可视化的算法流程:

  1. 收集代码元素的词频向量

词频向量的每一个元素是一个词的出现次数,而一个代码元素(类或函数)对应一个词频向量。词语从类名、函数名、函数代码之中提取。这一步听起来容易,做起来难,因为要对代码做语法分析,而C++的语法是出名多变复杂的。当前主要是根据代码树来分析,代码树就是指一个工程有什么类,每个类有什么成员,每个函数又用了什么局部变量这样的层次关系。

 

  2.根据词频向量,做潜在语义分析(Latent Sematic Indexing, LSI)。

把每个代码元素的词频向量排成一个矩阵。矩阵的每一行对应一个代码元素的词频向量,每一列对应一个词语在不同代码元素的出现次数。对此矩阵ASVD分解,A = UΣVT。

矩阵U*Σ1/2的每一行表示一个文档与各个主题的相关程度。

矩阵Σ1/2*VT 的每一列表示一个词语与各个主题的相关程度。

于是,可以把文档、词语都用“主题空间”的点表示,如下图:

 

图中红色点是一个词,蓝色点是一个文档,纵横轴分别表示两个主题。数据点的位置就表示这个词或文档蕴含的两个主题的强度。

在实现中发现,文档稍微更改时(例如在类中添加一个变量),各个主题的奇异值相对大小会发生突变,但是U V值基本不变。

 

LSI的第二步是,取前k个大的奇异值,其余的奇异值设为0,有些文章提到把最大的奇异值也忽略,因为它与文档的长度有关。之后重新构造出来的矩阵A’就去除了同义词和多义词的影响,这一点具体解释还不清楚。不过现在并不需要重新去构造A’,只是利用了U*Σ1/2 矩阵作为文档的特征向量(一行一个文档,一列一个主题,也就是特征)。利用特征向量的余弦度量文档的相似程度。

 

  3.降维

LSI之后,文档的特征向量大概有几十维(现在取20),相比之前的词频向量(几百上千维)短多了,这样可以降低计算量。现在的问题是,想办法把这些向量降至二维,而且在文档发生改变时,尽可能保持降维结果的稳定。现在第一个目标是做到了,但是结果不太稳定。

 

降维用到的办法称为MDSMultidimensional Scaling),实际上这是一大类算法的统称。MDS的输入是高维数据两两之间的相似度构成的方阵。其目的是,尽可能使得对于降维后的低维数据,其两两的相似度度量也跟原来的吻合。“相似度”可以是数据之间的欧氏距离、夹角余弦等等度量。

 

最直接想到的是使得高维和低维数据彼此之间的欧氏距离尽可能吻合。根据这个目标,产生一类算法,称为Stress MDS,这类算法构造一个评分函数(称为 stress function),函数求每对数据的高低维欧氏距离差的平方和,算法的目标就是使得此函数最小,即:

 

最小化这个函数有很多算法,但很多都难免收敛于局部最优。其中一个最简单的算法就是在每次迭代中,考虑所有点对,把过于远(低维相似度比高维小)的点对拉近(两个点向着对方靠近),把过于近的点对推远。Stress MDS还有一个共同缺点,就是对初始位置太敏感,不同的初始低维位置会导致不同的收敛结果。在实现中,似乎点数少的时候更敏感一些。

 

由于对初始位置敏感,所以需要有一个确定的算法来计算初始位置。PCA是一种(就是取高维数据在前几个特征向量上的投影作为初始位置),另一种称为经典MDS,这种算法在相似度矩阵为欧氏距离时,结果和PCA相同,但是它的思路是不一样的。经典MDS的目标是使得数据的低维内积尽可能接近数据的高维内积。但是此时高维内积不知道(输入的只有“相似度”),于是需要由相似度反推内积。其过程如下:

假设输入n个数据,排成一个nh列矩阵X,彼此之间相似度为D。假定这种相似度为数据的欧氏距离,而且数据关于每一维做了中心化(每个数据每维减去所有数据在该维的均值)。这样的假定使得经典MDS的几何意义(保内积)明确。下面对此说明:

 

定义矩阵 J = I - 1/n * E  ,I是单位阵,E是全1阵。

设D2 为相似度矩阵各元素的平方组成的矩阵,可以证明如下等式,

  -0.5 * J * D2 * J =(J X)(J X)T =B

(J X)(J X)T 即为对X每一维做中心化之后的高维数据X’的内积。

之后设B的特征值为λ1,λ2,λ3…… ,特征列向量为v1,v2,v3…… ,则 [λ1v1,λ2v2,λ3v3 ……]为降维后的位置。解释如下:

 

经典MDS实际上是最小化以下函数:

 

X此处为低维坐标,一行一个数据,一列一维。B与前文的B一样。B为高维内积,XXT为低维内积,所以说此种算法的目标是使得低维内积尽可能逼近高维内积。此处是两个矩阵的逼近问题,又知道B的特征分解后取前k个特征值与特征向量构成的X能最小化上述函数(具体过程现在我还没有完全搞懂)。 于是X就是经典MDS的结果了。

 

经典MDS的算法是不需要初始化的,于是用其结果作为Stress MDS的初始位置。整个降维算法也就不含随机性了。

 

MDS算法是一种广泛使用的降维算法。但是它有一个缺点,就是每个高维数据点的移动都会对其他点产生影响。例如,如果加入一个文档,其特征向量与已有的其他文档都不相似,它对应的低维点就会把其他点推开,以保持这种不相似;反过来,一个拥有其他文档公共特征的文档又会把其他文档向自己拉近。这些都是当前布局不稳定的可能原因。在大量文档的场合中,很可能有一部分文档由于太短,其特征与其他文档都不太相似,导致第一种不稳定情况的发生。

 

针对此种情况,现在准备的解决方法是使用一种称为ISOMap的算法。这个算法把高维数据的彼此空间距离变成图的距离。算法首先是建立一个图,图的每个节点对应每个高维数据。对于每个高维数据,再找与其最接近(欧氏距离或其他距离最小)的几个数据,在图上与这几个数据建立边连接。最后,两个节点的距离就定义为图上最短路径的长度,而不是原来高维空间的距离。根据这个新定义的距离,运行MDS算法,即得到降维后的结果。这个算法的好处是,如果个别数据出现变动,可能只会影响一部分边,而不会产生全局的影响(当然如果它恰好把原先的几个连通分量连起来了,那影响就大了)。在流形学习中,它的好处更加突出,就是能捕捉到嵌入到高维的低维数据(例如把一个三维螺旋卷展开, 卷本身是二维的,只是嵌入到三维空间(被卷起来了))。不过现在只是想找一个稳定的降维办法,而且代码特征也不一定包含所谓流形信息,所以这点好处不能套过来说。

 

IsoMap还没有实现,距之前做代码可视化的那个人说,效果好一些,理由就是上面所说的。下一步准备试试。

 

 

  4.绘图

 

这一步其实没有什么算法可言,基本上就是每个低维数据点给一个高斯核,各个核加起来就是高度场。但有一点值得说说,由于希望高度场能表达体系结构上的“底层”和“高层”,也就是说希望高的类依赖低的类,所以预先要建一个依赖图。如果一个类A用了类B的变量,那就是说类A依赖了类B,根据这些依赖关系,可以建一个有向图,A依赖B,就有一条AB的边。最后由只有入边的节点开始遍历,就可以依次定出各个节点的等级。(当然要用些小技巧解决循环依赖的问题)

 

当前画出来的图有点粗糙(因为效率比较低,所以像素不敢设太多),下一步着手改进一下。另外就是加入更多的信息。例如在图中表示函数的调用关系,用不同的图标表示每个类每个函数的具体情况,画出每个工程每个类的“领土”范围等等。

 

  5.总结

 

我觉得当前这个东西值得研究的点主要是寻找一个稳定的,但是又能暗示代码某些关系的布局方案,以及尽可能把丰富的信息有条理地呈现在图中,让用户可以根据地图来提取需要的信息。

posted @ 2014-02-17 12:01  dydx  阅读(5743)  评论(0编辑  收藏  举报