陳 凱,邵培南
(中國電子科技集團第三十二研究所,上海 201808)
隨著現代軟件的不斷發展,軟件可靠性已經成為評價軟件的關鍵因素.軟件規模的不斷擴展和功能的日益增強,軟件復雜度不斷上升,軟件缺陷出現的概率也不斷上升,從而導致軟件的失敗.為了幫助開發人員和測試人員及時找到軟件缺陷,軟件缺陷預測已經成為軟件工程領域、數據挖掘領域的研究方向之一.軟件缺陷預測技術可以在一定程度上預測軟件中是否存在著缺陷,以此幫助相關團隊快速了解軟件的整體情況和質量,制定相關策略測試和改善軟件,提高軟件的可靠性和穩定性.
基于此,許多研究人員前赴后繼潛心研究軟件缺陷預測技術并嘗試通過機器學習的方法來檢測軟件中是否存在著缺陷.傳統軟件缺陷預測[1]是以手工獲取軟件度量特征的基礎進行分類學習,而特征選擇的方法直接影響軟件缺陷預測的準確性和穩定性.而在以往的軟件缺陷預測研究中,通常使用靜態軟件度量作為代碼的特征,靜態軟件度量[1]主要包括以下幾種方法:
(1)代碼度量
代碼度量是最直接、應用最普遍的度量方式.通過對源代碼相關指標的簡單計數得到度量值.度量包括總行數、代碼行數目、注釋行數目、空白行數目和代碼及注釋行總數目等,通過對總行數、代碼行數、注釋行數等不同的處理方式,度量結果就會不同.
(2)McCabe 度量
McCabe 度量是一種基于程序流程圖的復雜性度量方法,度量的是程序的復雜性,主要包括圈復雜度、基本復雜度、設計復雜度等.
(3)Halstead 度量
Halstead 度量考慮了程序中出現的操作數和運算符,具體有程序長度、操作符出現的總數量、操作數出現的總數量、程序容量、程序難度、程序級別、程序工作量等.
(4)CK 度量
CK 度量是面向對象程序的度量,具體包括類方法復雜度帶權和(WMC)、類在繼承樹中的最大深度(DIT)、繼承樹中類的直接子類個數(NOC)等.
根據代碼的實際情況,選擇合適的度量方法,或在各種度量方法中選擇合適的指標組成新的特征集合,然后根據從歷史軟件源碼中提取出來的特征構建如邏輯回歸、隨機森林、支持向量機等分類器,對新版本的軟件源碼進行軟件缺陷預測,以此來幫助編程人員找到可能包含缺陷的部分.
然而,傳統軟件缺陷預測方法使用靜態代碼度量作為特征,未能充分考慮潛藏在代碼中的語義特征,這無疑會對缺陷預測造成影響.而抽象語法樹能夠表達出源代碼的語義,已經有相關的論文[2]證實了其可以用于源碼的完整性和缺陷的檢測.抽象語法樹是基于源代碼采用樹狀結構來描述代碼上下文之間的關系,其中包含了程序模塊的語法結構和語義信息.從抽象語法樹中提取表征用于軟件缺陷預測,可以充分考慮到代碼的語法語義特征.
近年來,深度學習作為數據挖掘的技術之一得到了充足的研究.在軟件缺陷預測領域,深度學習同樣可以用于挖掘代碼中隱含的特征.Iqbal 等[3]介紹了用靜態度量的方法獲得特征,然后用4 種方法對特征進行投票,選取出最合適的特征,然后構建一個多層感知機(MLP)網絡來對樣本進行學習分類.Wang 等[2]介紹了用多層受限玻爾茲曼機疊加而成的深度置信網絡(DBN),自動提取源代碼中的語法語義特征,并用提取出來的特征構建軟件缺陷預測模型.Li 等[4]利用卷積神經網絡(CNN)提取源碼特征后,將其與傳統靜態特征進行連結,構建邏輯回歸分類器來對軟件缺陷進行預測.然而,這些方法依然存在著數據挖掘不足的問題,所使用的網絡大多是簡單模型,CNN 也僅使用單層卷積層.基于這種情況,本文以抽象語法樹為特征來源,提出了一種卷積神經網路模型來進行軟件缺陷預測,并利用Promise 官網上的歷史工程數據來對模型進行實驗,取得了較好的結果.
能否從源代碼中提取到合適的特征,是影響軟件缺陷預測性能的一個關鍵因素.在過去的研究中,常常用靜態軟件度量的方法來處理源代碼,忽視了潛藏在代碼中的語義特征.而本文使用了一種基于抽象語法樹的方法來獲取代碼表征.
抽象語法樹(Abstract Syntax Tree,AST)是源代碼關于抽象語法結構的樹狀表示,源代碼中的每一種結構都表征為樹上的結點.之所以說是抽象的,是因為AST 并不會將源代碼的細節表示出來,例如,一串For語句,在結點中就記錄為“ForStatement”以及一些關鍵要素,而具體循環的內容并不會被記錄下來.另外,抽象語法樹并不依賴源代碼語言的語法,也就是說,語法分析時所采用的是上下文無關文法,因為在寫文法時,通常會對文法進行等價的轉換(消除左遞歸,二義性,回溯等),這樣會在文法分析時引入一些冗余成分,對后續階段造成不好的影響.
抽象語法樹能有效保存代碼的語法結構和語義信息.如圖1所示,代碼a 和代碼b 十分相似,且有著幾乎一樣的靜態代碼度量,也就是說,他們有著幾乎一樣的特征,在特征空間中,它們的距離會非常小,用分類器分類的話,很有可能將兩份代碼歸為一類,但顯然代碼a 是沒有缺陷的,而代碼b 是有缺陷的,這就造成了錯誤的檢測結果.圖2展示兩份代碼的抽象語法樹,代碼a 的抽象語法樹比代碼b 的抽象算法樹多了兩個結點,在提取表征向量時,兩份代碼在序列上就會有一定的區別,而這種區別就可以方便模型區分兩種代碼,得到更好的軟件缺陷預測性能.

圖1 抽象語法樹示例代碼
本文使用了一款開源的Python 依賴包Javalang來對源代碼進行解析,它提供了一個基于Java 語言規范的詞法分析器和解析器,可以構造Java 源代碼的抽象語法樹.在得到源代碼的抽象語法樹后,按照深度優先的順序來遍歷AST 的所有節點,然后主要選擇以下3 類結點作為抽象語法樹的表征向量元素:
(1)表示為方法調用的結點
(2)表示為聲明的結點,包括方法聲明、類聲明、接口聲明等
(3)控制流結點,譬如說條件分支、循環控制等
根據結點的類型,我們選取不同的要素來作為結點的特征.對于第1 類結點,我們使用結點對應的方法名來作為結點在特征向量中的標識;第2 類結點選擇結點的名稱來作為標識;第3 類控制流結點選擇節點的類型來作為標識,比如表征條件分支的結點,記錄結點的類型名“IfStatement”作為該結點的特征.表1列出了本文所使用的所有的結點類型.
由此,我們可以得到一棵樹即一份代碼的表征向量.
從抽象語法樹中獲得的表征向量不能直接用于神經網絡,訓練神經網絡需要輸入整數向量,所以我們需要在獲得的特征和整數之間建立一個映射,將表征向量轉換為整數向量.
為了在獲得的特征和整數之間建立映射,我們建立一個字典[2]將表征和正整數一一對應起來.將訓練樣本和測試樣本中的代碼全部提取為表征向量后,統計各個特征的頻率,將其轉換為對應的整數.假設不同的特征的數量是m,每個特征都有著對應的整數,那么正整數的范圍也是1~m.具體的,在從訓練樣本和測試樣本中提取表征向量后,首先,計算各個特征在所有樣本中出現的頻數,并且根據頻數將它們排列;然后,為排列好的特征建立一個序列字典,頻數高的特征排在前面,這意味著出現頻率越高的特征對應的正整數越小;構建完字典后,就可以將之前的表征向量轉化為整數向量.但因為神經網絡的輸入要求有固定的長度,為了避免向量過于稀疏,選擇適當的向量長度對向量進行處理.如果一個向量的長度小于設定的長度,那么我們就在向量末尾添0,而0 在神經網絡的計算過程中沒有意義;如果一個向量的長度大于設定的長度,那就在向量中尋找最大的正整數將它刪去,因為最大的整數對應的是頻數最小的特征,循環往復,直到向量的長度符合設定的長度.由此,我們得到了每份源代碼對應的整數向量.圖3給出了從源代碼到整數向量的全部流程.

圖2 示例代碼的抽象語法樹

表1 使用到的所有結點類型

圖3 從源代碼到得到整數向量的流程圖(①從源代碼中解析出抽象語法樹;②從抽象語法樹中提取表征向量;③根據提取的特征構建字典;④將表征向量映射為整數向量)
卷積神經網絡是一種含卷積計算且具有深度結構的前饋式神經網絡,具有表征學習、深度挖掘數據的能力.卷積神經網絡已經被證實在圖像處理、語音識別、自然語言處理等領域有著不錯的性能表現.而且,卷積神經網絡的隱含層內的卷積核參數共享、層與層之間的稀疏連接使得卷積神經網絡能夠以較小的計算量對格點化特征.
在軟件缺陷預測領域,數據分類不均衡問題是普遍存在的,如何處理這種不均衡問題也是具有挑戰性的.在獲得的數據集中,往往有缺陷的樣本數是要少于沒有缺陷的樣本數,如果直接用這樣的樣本對模型進行訓練,訓練出來的模型往往會對樣本數量較少的類別的識別能力較弱,在軟件缺陷預測時,模型便會偏向沒有缺陷的結果.因此,我們需要對數據進行預處理以彌補數據集分類不均衡帶來的偏差.
一般而言,針對數據集中存在的樣本分類不均衡問題,通常采用過采樣或欠采樣的方法來使得數據集中各類的樣本數量達到均衡.欠采樣是從多數樣本中隨機選擇和少數樣本一樣數量的樣本,以此構建分類平衡的數據樣本,但這樣會大大減少數據的訓練樣本,造成信息的損失,會造成模型的欠擬合,因此我們選擇過采樣的方法來處理分類不均衡問題.由于AST 數值向量并非是特征向量,所以類似像SMOTE[5]等基于樣本間的歐式距離來生成新樣本的分類不均衡處理方法并不一定適用.因此,在這里,我們采取簡單的隨機過采樣的方法來對數據進行預處理,在少數樣本即有缺陷的樣本中隨機選擇進行復制,使得有缺陷的樣本數量和沒有缺陷的樣本數量保持一致,以此保證數據類別間的均衡.具體的算法如算法1 所示.

算法1.隨機過采樣算法輸入:分類不均衡的數據集D輸出:分類均衡的數據集D'(1)初始化已復制樣本集合C 為空集.(2)將數據集D 復制,組成數據集D'.(3)在數據集D 中篩選出有缺陷的樣本集d.(4)在數據集d 中隨機選擇樣本a,如果a 不在已復制樣本集合C 中,將樣本a 加入數據集D'和已復制樣本集合C;如果已復制樣本集C 的大小和d 一樣,則清空C.(5)重復步驟(4)直至數據集D'中無缺陷樣本和有缺陷樣本數量保持一致.
首先介紹所要構建的網絡中用到的基礎卷積塊Inception 塊[6],這個卷積模塊的結構如圖4所示.

圖4 Inception 塊
如圖4所示,Inception 塊含有4 條并行的網絡線路.前三條線路所使用的卷積核大小分別是1×1、3×3、5×5,以用來抽取不同空間尺寸下的信息,第2,3 層會先對輸入做1×1 卷積操作以減少輸入通道數,以降低模型復雜度.第4 條線路會先使用3×3 最大池化層,然后接一個1×1 卷積層來改變通道數.并且,根據4 條線路的具體情況,設置合適的填充來使得輸入和輸出的高和寬一致.最后將每條線路的輸出在通道維上連結,以此完成卷積的功能,將結果輸入到下一層.相較于普通的卷積層,Inception 塊因為使用了3 個卷積層和1 個池化層,能夠更深層次的挖掘數據中的特征,以此幫助模型進行更好的學習分類.
為了能夠更深層次地挖掘出潛藏在向量中的語法語義特征,本文基于GoogLeNet[7]設計了一個卷積神經網絡,GoogLeNet 最初設計出來是用來進行圖像處理的,在ImageNet 圖像識別挑戰賽中大放異彩,GoogLeNet 串聯了多個Inception 塊來對圖像進行深度挖掘,以此進行更好的分類.本文基于GoogLeNet 設計一個卷積神經網絡,具體的網絡結構如圖5所示,除了最后的輸出層使用Sigmoid 函數作為激活函數外,其他層的激活函數均使用ReLU 函數,并且根據實際情況調整網絡中各個層的參數,網絡整體分為主要分為以下3 個部分:
(1)輸入層:輸入層主要是一個嵌入層[8],嵌入層的主要作用是將輸入的整數向量中的整數元素轉換成整數向量,使得向量可以進行卷積操作.嵌入層有兩個重要參數:嵌入層字典的大小(num_embeddings)和每個產出向量的大小(embedding_dim).這里,本文將num_embeddings 設置為2.3 節構建的字典中所含有的特征的數量,將embedding_dim 設置為2.3 節中通過映射得到的整數向量的長度.將長度為n的整數向量輸入到嵌入層,嵌入層將給出一個n×n的矩陣向量.并且,為了提高內存利用率和訓練模型的速度,本文選擇分批量進行訓練,設置每次訓練樣本個數(批尺寸,Batch_Size)為16,即一次輸入16 個樣本進行訓練.
(2)卷積部分:卷積部分是網絡的主體部分,共由5 個模塊組成.模塊與模塊之間使用步幅為2 的3×3 最大池化層來減小輸出高度.第1 個模塊包含3 層的3×3 卷積層;第2 個模塊使用2 個卷積層,首先接一個64 通道的1×1 卷積層,然后接了一個將通道數擴大3 倍的3×3 卷積層;第3 個模塊串聯了2 個完整的Inception 塊;第4 模塊串聯了5 個Inception 塊;第5 模塊串聯了2 個Inception 塊.通過多層的不同空間尺寸的卷積操作,來深度挖掘數據中的特征,從而進行性能更好穩定性更高的學習分類.
(3)輸出層:輸出層主要是根據之前卷積層輸出的結果來輸出分類結果.首先使用一個全局平均池化層來將每個通道的高和寬都變成1,然后接上一個全連接層,輸出通道數為標簽類別數,最后,連結一個Sigmoid函數構建邏輯回歸分類器來計算程序代碼的缺陷概率,從而得到分類結果.

圖5 網絡結構圖
之前,我們在數據預處理時采用隨機過采樣的方法來解決數據分類不均衡問題,提升了模型的泛化能力,但是這樣也有一定的過擬合的風險,因此我們選擇使用丟棄法(Dropout)[9],通過隨機丟棄一部分神經元來避免過擬合.在訓練過程中,隨機丟棄一部分隱藏層神經單元,即所有神經單元都有可能被清零,這樣就減少了神經元之間的依賴性,輸出層的計算也無法過度依賴任何一個隱含層神經元,從而在訓練模型時起到正則化的作用,用來應對過擬合.在測試模型時,為了拿到更加準確的結果,我們不使用丟棄法.
另外,在訓練模型的過程中,為了得到最優的模型參數,我們需要根據損失函數的梯度不斷地對參數進行迭代,這里我們選擇使用Adam[10]優化器來更新參數.Adam 算法結合了AdaGrad 和RMSProp 兩種優化算法的優點[10],能夠自動調整學習率,并且參數的更新不受梯度的伸縮變換影響.Adam 算法能夠從梯度均值及梯度平方兩個角度進行自適應地調節,綜合考慮梯度的一階矩估計和二階矩估計計算出更新步長,而不是直接由當前梯度決定.
為了評估訓練出來的模型的性能,本文從Promise[11]上下載了5 個工程,總共11 個項目,組成6 組軟件缺陷預測任務,用于模型的測試.在同一軟件工程中,將舊版本的工程項目作為訓練集,將新版本的工程項目作為測試集,根據測試結果來評估模型的預測能力.例如,對于Camel 工程,我們將Camel 1.4 版本的工程代碼用來訓練模型,然后用Camel 1.6 版本的代碼用來測試模型.表2列出了測試時所使用的軟件項目的基本信息.

表2 測試使用的工程信息
另外,在數據集中,每個項目不僅含有工程的源代碼,還統計了源代碼的靜態代碼度量和缺陷注釋,度量方法主要是針對面向對象編程的靜態代碼度量,具體的指標內容如表3所示.這些指標可以用于其他的軟件缺陷預測方法,來和本文模型進行比較.

表3 數據集中所使用的20 個靜態代碼度量
本文采用AUC[12]和F1-measure[13]這兩個指標來評估模型性能,AUC 主要用來評估模型的識別缺陷的能力,而F1-measure 主要用來評估模型的穩定性.
在二分類的學習預測過程中,根據分類結果可以將其分為4 類:(1)若一個實例為正類且被預測為正類,即為真正類(True Positive,TP);(2)若一個實例為正類但被預測負類,即為假負類(False Negative,FN);(3)若一個實例為負類但被預測為正類,即為假正類(False Positive,FP);(4)若一個實例為負類且被預測為負類,即為真負類(True Negative,TN).基于這4 個數據,可以得到擊中概率(TPR)和虛報概率(FPR),其計算公式如式(1)和式(2)所示:

然后以FPR為橫軸,TPR為縱軸,就可以繪制出ROC 曲線,而ROC 曲線下的面積就是AUC.根據AUC 的定義,識別能力更好的模型對應著更高的TPR和更低的FPR,所以有AUC 值越大的預測方法越好.
F1-measure 是精確率(P)和召回率(R)的調和平均,其中精確率和召回率的計算公式如式(3)和式(4)所示:

通常情況下,我們希望精確率越高越好,召回率也越高越好,但事實上這兩個指標在某些情況下是矛盾的,而F1-measure 則綜合考慮了這兩個指標.F1-measure的計算公式如式(5)所示.

另外,用于評估軟件缺陷預測模型的指標還有很多,例如MCC[14]和G-mean[15],MCC 考慮所有的有缺陷數據和無缺陷數據來綜合評估預測結果和真實結果之間的關系,G-mean 是在數據不平衡時十分有參考價值的一個指標,但因為AUC 和F1-measure 綜合評估了模型的準確性和穩定性,具有廣泛的代表意義.
為了能夠正確估計模型對于軟件缺陷預測的性能,將本文提出的模型與以下3 種方法進行比較:
(1)靜態代碼度量+邏輯回歸(LR):以數據集中提供的20 個靜態代碼度量作為代碼特征,并用邏輯回歸的方法進行分類
(2)深度置信網絡+邏輯回歸(DBN)[2]:使用深度置信網絡從源代碼中提取特征,然后使用邏輯回歸的方法進行分類
(3)卷積神經網絡+邏輯回歸(CNN)[4]:利用單層卷積神經網絡對源代碼進行特征提取,然后使用邏輯回歸分類器得到分類結果
對于傳統軟件缺陷預測算法,因為使用的是20 個靜態代碼度量所構成的特征向量,所以在數據預處理時,可以使用SMOTE 方法進行過采樣來處理數據集分類不均衡問題;而對于DBN、CNN 和本文模型,只能簡單地采用隨機過采樣的方法來對數據進行預處理.
本文使用Python 環境以及深度學習框架PyTorch來實現本文提出的模型,所有的實驗均在一臺帶有NVIDIA GTX 1080 的Linux 服務器上運行.此外,因為隨機過采樣和丟棄法都具有一定的隨機性,因此實驗中每個方法都執行10 次,取平均值來進行模型性能的比較.
本文采用AUC 和F1-measure 來比較4 種方法在6 組預測任務上的性能.表4和表5分別記錄了這4 種方法關于AUC 和F1-measure 的實驗結果,每次測試任務的表現最好的已在表格中加粗.

表4 4 種方法關于6 項測試任務的AUC

表5 4 種方法關于6 項測試任務的F1-measure
表3和表4分別列出了4 種方法關于每個測試任務的AUC 值和F1-measure.AUC 評估了模型分類的準確性,而F1-measure 評估了模型的穩定性.從表3和表4中我們可以看到,總體而言,本文提出的模型在軟件缺陷預測性能方面和模型穩定性明顯優于LR、DBN和CNN 的.而本文模型的AUC 和F1-measure 的均值也都高于其他方法,這也證實了本文提出模型的合理性和可行性.此外,從兩張表中我們可以看出,相較于傳統的軟件缺陷預測方法,應用深度學習方法在軟件缺陷預測性能和模型穩定性上都得到一定的提高.這也證實了,在軟件缺陷預測性能方面,深度學習方法優于傳統的機器學習方法.
綜上所述,針對傳統軟件缺陷預測方法中對源代碼語義特征挖掘不足的問題,本文測試實驗結果表明,在軟件缺陷預測領域,相比于傳統的預測方法,應用深度學習方法得到了一定的提高.而本文也根據前人的工作,提出了用多層卷積神經網絡對基于抽象語法樹得到的表征向量進行分類學習,有效提高了缺陷預測的準確性.
本文針對傳統軟件缺陷預測方法應用靜態代碼度量而忽視代碼語義的缺點,從代碼的抽象語法樹中提取出向量,再利用卷積神經網絡深度挖掘數據的能力挖掘代碼中的語法語義特征,從而對軟件缺陷進行學習分類.并且,通過與LR、DBN、MLP 方法的實驗比較,由AUC 和F1_measure 兩個指標我們可以看出本文提出的模型在軟件缺陷預測性能上得到了一定的提高.然而,關于數據集分類不均衡、模型優化等問題,本文的處理方法相對粗糙,這也是未來需要繼續研究的方向.