這個是暫時的效果,一個點是一個類或者全局函數。高度場暗示依賴關系,高度高的會依賴高度低的。
下面是代碼可視化的算法流程:
- 收集代碼元素的詞頻向量
詞頻向量的每一個元素是一個詞的出現次數,而一個代碼元素(類或函數)對應一個詞頻向量。詞語從類名、函數名、函數代碼之中提取。這一步聽起來容易,做起來難,因為要對代碼做語法分析,而C++的語法是出名多變復雜的。當前主要是根據代碼樹來分析,代碼樹就是指一個工程有什么類,每個類有什么成員,每個函數又用了什么局部變量這樣的層次關系。
2.根據詞頻向量,做潛在語義分析(Latent Sematic Indexing, LSI)。
把每個代碼元素的詞頻向量排成一個矩陣。矩陣的每一行對應一個代碼元素的詞頻向量,每一列對應一個詞語在不同代碼元素的出現次數。對此矩陣A做SVD分解,A = UΣVT。
矩陣U*Σ1/2的每一行表示一個文檔與各個主題的相關程度。
矩陣Σ1/2*VT 的每一列表示一個詞語與各個主題的相關程度。
於是,可以把文檔、詞語都用“主題空間”的點表示,如下圖:
圖中紅色點是一個詞,藍色點是一個文檔,縱橫軸分別表示兩個主題。數據點的位置就表示這個詞或文檔蘊含的兩個主題的強度。
在實現中發現,文檔稍微更改時(例如在類中添加一個變量),各個主題的奇異值相對大小會發生突變,但是U 和V值基本不變。
LSI的第二步是,取前k個大的奇異值,其余的奇異值設為0,有些文章提到把最大的奇異值也忽略,因為它與文檔的長度有關。之后重新構造出來的矩陣A’就去除了同義詞和多義詞的影響,這一點具體解釋還不清楚。不過現在並不需要重新去構造A’,只是利用了U*Σ1/2 矩陣作為文檔的特征向量(一行一個文檔,一列一個主題,也就是特征)。利用特征向量的余弦度量文檔的相似程度。
3.降維
做LSI之后,文檔的特征向量大概有幾十維(現在取20),相比之前的詞頻向量(幾百上千維)短多了,這樣可以降低計算量。現在的問題是,想辦法把這些向量降至二維,而且在文檔發生改變時,盡可能保持降維結果的穩定。現在第一個目標是做到了,但是結果不太穩定。
降維用到的辦法稱為MDS(Multidimensional Scaling),實際上這是一大類算法的統稱。MDS的輸入是高維數據兩兩之間的相似度構成的方陣。其目的是,盡可能使得對於降維后的低維數據,其兩兩的相似度度量也跟原來的吻合。“相似度”可以是數據之間的歐氏距離、夾角余弦等等度量。
最直接想到的是使得高維和低維數據彼此之間的歐氏距離盡可能吻合。根據這個目標,產生一類算法,稱為Stress MDS,這類算法構造一個評分函數(稱為 stress function),函數求每對數據的高低維歐氏距離差的平方和,算法的目標就是使得此函數最小,即:
最小化這個函數有很多算法,但很多都難免收斂於局部最優。其中一個最簡單的算法就是在每次迭代中,考慮所有點對,把過於遠(低維相似度比高維小)的點對拉近(兩個點向着對方靠近),把過於近的點對推遠。Stress MDS還有一個共同缺點,就是對初始位置太敏感,不同的初始低維位置會導致不同的收斂結果。在實現中,似乎點數少的時候更敏感一些。
由於對初始位置敏感,所以需要有一個確定的算法來計算初始位置。PCA是一種(就是取高維數據在前幾個特征向量上的投影作為初始位置),另一種稱為經典MDS,這種算法在相似度矩陣為歐氏距離時,結果和PCA相同,但是它的思路是不一樣的。經典MDS的目標是使得數據的低維內積盡可能接近數據的高維內積。但是此時高維內積不知道(輸入的只有“相似度”),於是需要由相似度反推內積。其過程如下:
假設輸入n個數據,排成一個n行h列矩陣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,就有一條A到B的邊。最后由只有入邊的節點開始遍歷,就可以依次定出各個節點的等級。(當然要用些小技巧解決循環依賴的問題)
當前畫出來的圖有點粗糙(因為效率比較低,所以像素不敢設太多),下一步着手改進一下。另外就是加入更多的信息。例如在圖中表示函數的調用關系,用不同的圖標表示每個類每個函數的具體情況,畫出每個工程每個類的“領土”范圍等等。
5.總結
我覺得當前這個東西值得研究的點主要是尋找一個穩定的,但是又能暗示代碼某些關系的布局方案,以及盡可能把豐富的信息有條理地呈現在圖中,讓用戶可以根據地圖來提取需要的信息。
