周培君,吳軍華
(南京工業(yè)大學(xué) 計(jì)算機(jī)科學(xué)與技術(shù)學(xué)院,南京 211816) E-mail:413402140@qq.com
代碼注釋又稱程序注釋,是對(duì)源代碼的一種簡(jiǎn)單易讀的自然語言描述.在實(shí)際應(yīng)用中,大多數(shù)程序員只關(guān)注代碼而忽略了注釋,這使得程序的可讀性和可維護(hù)性大大降低[1].開發(fā)人員平均花費(fèi)超過50%的時(shí)間閱讀和理解他人編寫的代碼[2].好的代碼注釋可以幫助開發(fā)和維護(hù)人員更準(zhǔn)確、更快地理解程序,從而節(jié)省他們額外的閱讀時(shí)間.
隨著軟件代碼規(guī)模的不斷擴(kuò)大,如何幫助開發(fā)人員在軟件開發(fā)過程中理解、編寫或維護(hù)代碼已成為軟件工程領(lǐng)域的重要課題.Java作為最流行的主要編程語言被提出了許多方法來生成注釋,以減輕對(duì)代碼進(jìn)行注釋的人工工作.信息檢索是自動(dòng)代碼注釋生成研究中最早使用的技術(shù).基于信息檢索技術(shù)的評(píng)論算法一般采用基于向量空間模型(VSM)、潛在語義索引(LSI)、潛在狄利克雷分配(LDA)或代碼克隆檢測(cè)等相關(guān)技術(shù).近年來,隨著深度神經(jīng)網(wǎng)絡(luò)在自然語言處理、機(jī)器翻譯、圖像識(shí)別和語音處理等方面的發(fā)展,軟件工程領(lǐng)域中也引入了深度神經(jīng)網(wǎng)絡(luò)來解決代碼注釋問題.絕大多數(shù)基于深度神經(jīng)網(wǎng)絡(luò)的代碼注釋工作的靈感來源于自然語言處理(NLP)的最先進(jìn)的神經(jīng)機(jī)器翻譯(NMT),即將源代碼到注釋的轉(zhuǎn)換問題表示為編程語言與自然語言之間的翻譯問題.
本文提出了一種組合源碼結(jié)構(gòu)和語義的神經(jīng)模型Code2Com(Code to Comment),一種基于深度學(xué)習(xí)的為給定代碼生成注釋的方法.工作主要集中在模型輸入和注釋生成.模型組合了兩種關(guān)于源代碼的信息:1)代碼的順序表示;2)抽象語法樹表示.與之前的方法不同,Code2Com以不同的方式對(duì)待每種輸入,增加了方法的靈活性.為了克服曝光偏差,模型訓(xùn)練與推理應(yīng)在相同的條件下進(jìn)行預(yù)測(cè),Code2Com在訓(xùn)練過程中提供預(yù)測(cè)詞序列作為下一步的輸入,以便逐步迫使模型處理自己的錯(cuò)誤,讓模型在訓(xùn)練過程中進(jìn)行更多探索,以此減小訓(xùn)練和推理之間的差異.
作為自然語言處理(NLP)系統(tǒng)的核心組成部分,語言模型可以提供詞表征和單詞序列的極大似然.語言模型描述了單詞出現(xiàn)在序列中的概率.對(duì)于一個(gè)由n個(gè)按順序構(gòu)成的自然語言序列x=(x1,…,xn),語言模型根據(jù)已知序列(x1,…,xk-1)預(yù)測(cè)下一個(gè)詞語xk的概率,即:
(1)
對(duì)語言模型建模時(shí),為了減少建模時(shí)的維數(shù)災(zāi)難,Brown等人提出一種N元(N-gram)語言模型的近似方法,即預(yù)測(cè)的第k個(gè)詞的出現(xiàn)僅依賴于前面的k-1個(gè)詞:
p(xk|x1,…,xk-1)≈p(xk|xk-n+1,…,xk-1)
(2)
然而,這種N-gram方法有明顯的局限性,例如未考慮自然語言的離散型、組合性和稀疏性.為了解決這個(gè)問題,神經(jīng)網(wǎng)絡(luò)(NN)被引入到語言模型的訓(xùn)練中,例如循環(huán)神經(jīng)網(wǎng)絡(luò)(RNN,Recurrent Neural Networks)、長(zhǎng)短期記憶網(wǎng)絡(luò)(LSTM,Long Short-Term Memory)、門控循環(huán)單元網(wǎng)絡(luò)(GRU,Gated Recurrent Unit)等.RNN包括3層,將每個(gè)輸入映射到向量中的輸入層,在讀取每個(gè)輸入向量后循計(jì)算并更新隱藏狀態(tài)的循環(huán)隱藏層,以及利用隱藏狀態(tài)計(jì)算預(yù)測(cè)token概率的輸出層.RNN在訓(xùn)練過程中,每層的梯度大小會(huì)在長(zhǎng)序列上呈指數(shù)增長(zhǎng)或衰減[3].梯度爆炸或消失的問題使RNN模型難以學(xué)習(xí)序列中的長(zhǎng)距離相關(guān)性.LSTM通過引入一個(gè)能夠長(zhǎng)時(shí)間保存狀態(tài)的記憶單元,有效解決學(xué)習(xí)長(zhǎng)期依賴關(guān)系的問題.在本文中,我們使用Chung等人提出的GRU,因?yàn)樗谛蛄薪7矫娴男阅芘cLSTM相當(dāng)[4],但參數(shù)較少,易于訓(xùn)練.
對(duì)源代碼和注釋之間的關(guān)系進(jìn)行建??捎糜谧詣?dòng)代碼注釋.一種直接的方法是將問題轉(zhuǎn)換為機(jī)器翻譯問題,其中源句子是代碼中的token序列,目標(biāo)句子是相應(yīng)的注釋序列.Iyer等人[5]提出了一種基于LSTM的注釋生成模型CodeNN,它使用具有注意機(jī)制的LSTM為C#和SQL生成注釋.CodeNN通過加入注意力機(jī)制,直接將注釋中的單詞與相關(guān)代碼token對(duì)齊.Allamanis等人[6]使用卷積神經(jīng)網(wǎng)絡(luò)(CNN)和注意力機(jī)制生成源代碼的摘要.該方法使用卷積注意模塊對(duì)輸入的源代碼提取特征,并確定序列中應(yīng)注意的重要token.此外,一些論文也將源代碼建模為一系列token[7]和字符[8].這些工作在各種生成代碼注釋和文檔任務(wù)中都取得了最先進(jìn)的性能.還有一些方法考慮了源代碼的結(jié)構(gòu)信息.Liang等人[9]提出了一種基于RNN的AST編碼器Code-RNN.Chen等人[10]提出了用于代碼注釋生成的樹到序列模型Tree2Seq,Tree2Seq使用了一個(gè)基于AST的編碼器代替RNN編碼器.
這些方法沒有充分利用代碼的結(jié)構(gòu)和語義信息在特征表示上,如程序語法、函數(shù)和方法調(diào)用[11],大大降低了代碼表達(dá)的質(zhì)量,這就需要模型隱式地重新學(xué)習(xí)編程語言的語法,浪費(fèi)資源并且降低了準(zhǔn)確性.其次在編碼器解碼器框架下,訓(xùn)練時(shí)模型的輸入完全采用真實(shí)序列標(biāo)記,而在推理時(shí),解碼器的當(dāng)前輸入是模型生成的上一個(gè)詞.這使得在訓(xùn)練和推理時(shí)預(yù)測(cè)的詞來自不同的分布,隨著目標(biāo)序列的增長(zhǎng),訓(xùn)練和推理之間的差異會(huì)導(dǎo)致錯(cuò)誤的累積.
如圖1所示,Code2Com主要包括數(shù)據(jù)處理,模型訓(xùn)練和代碼注釋生成.在該結(jié)構(gòu)中,使用兩個(gè)編碼器獲得源代碼的表示:AST編碼器負(fù)責(zé)讀取AST序列,代碼編碼器負(fù)責(zé)讀取代碼token序列.解碼器在生成目標(biāo)序列時(shí)一次預(yù)測(cè)一個(gè)單詞.
圖1 Code2Com的整體流程Fig.1 Overview of Code2Com
為了利用Java的語法結(jié)構(gòu)來更好地編碼源代碼,將代碼片段表示為抽象語法樹(AST,Abstract Syntax Tree).通過深度優(yōu)先搜索對(duì)AST的節(jié)點(diǎn)進(jìn)行遍歷,可將Java源代碼轉(zhuǎn)化為序列化數(shù)據(jù)供神經(jīng)網(wǎng)絡(luò)學(xué)習(xí).然而,AST的節(jié)點(diǎn)并不對(duì)應(yīng)語法的所有細(xì)節(jié),比如對(duì)于嵌套括號(hào)等沒有對(duì)應(yīng)的節(jié)點(diǎn),這種模糊性可能會(huì)導(dǎo)致將不同的Java方法映射到相同的序列表示.為了保留結(jié)構(gòu)信息,本文在遍歷時(shí)使用“(”和“)”將語法樹的中間結(jié)點(diǎn)包圍起來,并在括號(hào)前面注明該結(jié)點(diǎn)本身.例如,考慮圖2中的Java方法.
圖2 Java示例Fig.2 Java example
從根節(jié)點(diǎn)MethodDeclaration開始,使用MethodDecla-ration()表示第一層樹結(jié)構(gòu).接下來遍歷MethodDeclaration節(jié)點(diǎn)的子樹,將子樹的葉子節(jié)點(diǎn)int放入括號(hào)中,即Metho-dDeclaration(int),依次遞歸遍歷所有子樹得到最終序列.
上述序列與AST的對(duì)應(yīng)關(guān)系如圖3所示.
圖3 Java代碼片段的AST和處理后的AST序列Fig.3 AST of the Java method and processed AST sequence
Code2Com遵循NMT的標(biāo)準(zhǔn)編碼器-解碼器結(jié)構(gòu),該結(jié)構(gòu)通常由兩個(gè)RNN組成,一個(gè)稱為編碼器,編碼器將不定長(zhǎng)的輸入數(shù)據(jù)X=(x1,…,xt)轉(zhuǎn)換為定長(zhǎng)的隱表征序列H=(h1,…,ht),其中t表示輸入序列的長(zhǎng)度.另一個(gè)稱為解碼器,根據(jù)從編碼過程中獲取的隱向量H,解碼器預(yù)測(cè)生成任意長(zhǎng)度的輸出序列Y=(y1,…,yn),其中n表示解碼器預(yù)測(cè)結(jié)果token的數(shù)量.
本文使用了一種新的神經(jīng)注意網(wǎng)絡(luò)結(jié)構(gòu).Bahdanau等人[12]提出的注意力機(jī)制被廣泛應(yīng)用于NMT,閱讀理解,語音識(shí)別和計(jì)算機(jī)視覺.在基于注意的模型中,注意力機(jī)制負(fù)責(zé)將較高的權(quán)重值動(dòng)態(tài)分配給解碼器輸入單詞中更相關(guān)的token,這樣就提高了解碼器的效率.
如圖4所示,Code2Com利用GRU的優(yōu)點(diǎn),設(shè)計(jì)了一種新的組合代碼的表示方法.在給定源代碼作為輸入的情況下,將代碼序列投影到連續(xù)向量空間,利用AST和代碼編碼器獲得源代碼的表示.一旦源代碼被編碼,注意解碼器便開始逐詞生成目標(biāo)注釋.為了減少訓(xùn)練和推理之間的差距,在模型訓(xùn)練階段,通過在預(yù)測(cè)得分中添加干擾項(xiàng)來獲得新的預(yù)測(cè)詞,然后從數(shù)據(jù)分布和模型分布中選擇解碼器下一步的輸入,讓模型能夠更穩(wěn)健地糾正自己在推理時(shí)的錯(cuò)誤.
圖4 模型結(jié)構(gòu)Fig.4 Model structure
3.2.1 AST編碼器和代碼編碼器
Code2Com使用GRU作為編碼器中的基本構(gòu)建塊.AST編碼器負(fù)責(zé)學(xué)習(xí)代碼的結(jié)構(gòu)表示,具體地從一個(gè)相當(dāng)常見的編碼結(jié)構(gòu)開始,包括為每個(gè)輸入編碼的嵌入層.AST編碼器使用GRU逐個(gè)讀取AST序列as=(as1,…,asn),通過embedding層將輸出shape(batch_size,vocab_size,embd_dim),其中batch_size為批處理大小,vocab_size為詞表大小,embd_dim為嵌入維度,也就是每個(gè)詞對(duì)應(yīng)的詞嵌入維度的向量.通過對(duì)每個(gè)詞的各個(gè)特征進(jìn)行embedding,可以得到每個(gè)特征的向量化表示.接下來是用作AST編碼的GRU單元,GRU將該層的輸入(AS1,…,ASn)映射到隱藏狀態(tài)(h1,…,hn).在時(shí)間步t,根據(jù)當(dāng)前輸入ASt和上一步隱藏層的狀態(tài)ht-1按照式(3)遞歸地更新隱藏狀態(tài)ht:
ht=fGRU(ht-1,ASt)
(3)
其中ASi為單詞asi的embedding向量.編碼器從源代碼中學(xué)習(xí)標(biāo)識(shí)符、控制結(jié)構(gòu)等潛在的特征,并將這些特征編碼到上下文向量中.代碼編碼器工作方式與AST編碼器幾乎相同.
3.2.2 組合注意
(4)
(5)
3.2.3 注釋生成
在獲得代碼片段的表示之后,需要將其解碼為注釋.首先模型根據(jù)上一個(gè)詞、上下文向量和當(dāng)前步的隱藏狀態(tài)通過線性變換生成目標(biāo)詞,接下來將目標(biāo)詞映射到分類得分中,產(chǎn)生對(duì)應(yīng)的維度:
Ot=g(Yt-1,Ct,st)
(6)
Mt=WOt
(7)
其中,Mt為softmax操作前的預(yù)測(cè)得分.本文通過引入Gumbel[13]噪聲實(shí)現(xiàn)多項(xiàng)分布采樣的再參數(shù)化.具體地,對(duì)于解碼的第t步,在式(7)的Mt中引入正則化項(xiàng)Gt:
Gt=-log(-log(ut))
(8)
(9)
(10)
其中pt表示目標(biāo)單詞yt的概率分布.最后模型根據(jù)pt選擇預(yù)測(cè)詞:
(11)
(12)
在這部分,進(jìn)行了大量的實(shí)驗(yàn)來證明本文提出的模型在代碼注釋自動(dòng)生成任務(wù)上的優(yōu)勢(shì).實(shí)驗(yàn)比較了Code2Com與3種典型方法Seq2Seq+Attn、CodeNN和TreeLSTM的性能,同時(shí)還通過性能對(duì)比實(shí)驗(yàn)進(jìn)行了組件分析,以評(píng)估每個(gè)組件在Code2Com中的貢獻(xiàn).
實(shí)驗(yàn)使用Allamanis等人[6]提供的數(shù)據(jù)集,該數(shù)據(jù)集中包含11個(gè)相對(duì)較大的Java項(xiàng)目,最初用于同一項(xiàng)目范圍內(nèi)的11個(gè)不同的模型.以往的研究使用的數(shù)據(jù)集沒有按項(xiàng)目劃分,同一項(xiàng)目中幾乎相同的代碼同時(shí)出現(xiàn)在訓(xùn)練集和測(cè)試集中,這讓模型嚴(yán)重過擬合了訓(xùn)練數(shù)據(jù),導(dǎo)致BLEU得分虛高.通過按項(xiàng)目劃分?jǐn)?shù)據(jù)集,進(jìn)行跨項(xiàng)目訓(xùn)練,對(duì)不同的項(xiàng)目進(jìn)行預(yù)測(cè)任務(wù),以獲得更真實(shí)的預(yù)測(cè)結(jié)果:將數(shù)據(jù)集分成3組,9個(gè)項(xiàng)目用于訓(xùn)練,1個(gè)項(xiàng)目用于驗(yàn)證,1個(gè)項(xiàng)目用于測(cè)試.這個(gè)數(shù)據(jù)集包含大約70萬個(gè)示例.
Code2Com使用TensorFlow框架在GPU上進(jìn)行訓(xùn)練.對(duì)于編碼器和解碼器,都使用了256個(gè)隱藏單元的GRU.單詞嵌入的維數(shù)為256,我們發(fā)現(xiàn)兩個(gè)獨(dú)立的嵌入比單一的嵌入空間有更好的性能.在訓(xùn)練過程中,用Adam優(yōu)化了模型,使用了默認(rèn)的超參數(shù):學(xué)習(xí)率α=0.01,β1=0.9,β2=0.999,β2=0.999,=1e-8,其他參數(shù)在[-0.1,0.1]范圍內(nèi)隨機(jī)初始化.對(duì)于解碼,使用貪婪搜索算法進(jìn)行推理,以最小化實(shí)驗(yàn)變量和計(jì)算成本.在式(9)中設(shè)置temp=0.5,在式(12)中設(shè)置μ=12.代碼序列和AST序列大小為100,注釋的序列大小為13,每個(gè)序列至少覆蓋80%的訓(xùn)練集.低于最小長(zhǎng)度的短序列用零填充,超過最大長(zhǎng)度的長(zhǎng)序列將被截?cái)?
在代碼到注釋任務(wù)上評(píng)估本文的模型:根據(jù)一個(gè)Java方法來預(yù)測(cè)它的注釋.實(shí)驗(yàn)結(jié)果證明本文提出方法可以產(chǎn)生注釋,并且在性能上有一定的提高.
4.2.1 性能分析
模型使用BLEU得分[15]進(jìn)行評(píng)估.BLEU是評(píng)估模型預(yù)測(cè)注釋和參考注釋之間文本相似度的指標(biāo).本節(jié)將我們的方法與Iyer等人[5]提出的注釋生成模型CodeNN進(jìn)行比較,后者是一種最先進(jìn)的代碼摘要方法,該方法使用帶有注意力機(jī)制的LSTM直接從源代碼的token嵌入生成注釋.此外,我們還比較了兩個(gè)將輸入源代碼作為token流讀取的NMT基線:基于注意力的Seq2Seq模型(Seq2Seq+Attn),編碼器和解碼器都采用GRU,Seq2Seq+Attn代表了NLP研究領(lǐng)域強(qiáng)大的現(xiàn)有方法的應(yīng)用;Tai等人提出的帶有注意輸入子樹,并采用LSTM解碼器的TreeLSTM[16];代碼注釋自動(dòng)生成任務(wù)的性能如表1所示.
表1 Code2Com及相關(guān)方法的BLEU得分(%)Table 1 BLEU scores of Code2Com and related methods(%)
表1總結(jié)了代碼注釋生成的每種方法的BLEU得分.由于BLEU的關(guān)鍵評(píng)價(jià)內(nèi)容是N-gram精度,實(shí)驗(yàn)分析了n取不同值時(shí)的BLEU得分.Seq2Seq模型在Java方法注釋方面表現(xiàn)優(yōu)于CodeNN,CodeNN直接從源代碼的token生成注釋時(shí)無法學(xué)習(xí)Java方法的語義,而Seq2Seq模型可以利用GRU為源代碼建立語言模型,有效地學(xué)習(xí)Java方法的語義.盡管TreeLSTM也利用了語法,但本文的模型同時(shí)考慮了代碼的結(jié)構(gòu)和序列信息,并且通過解決曝光偏差問題增強(qiáng)了模型的容錯(cuò)能力.本文的模型達(dá)到了21.37的BLEU分?jǐn)?shù),與TreeLSTM模型相比,BLEU得分提高了2.97分(相對(duì)16.1%),并且高于所有對(duì)比模型.這表明我們的方法適用于代碼注釋.我們?cè)诒?中展示了測(cè)試集中的Java示例,Code2Com生成的注釋最接近正確結(jié)果,雖然TreeLSTM和Seq2Seq+Attn也生成了部分真實(shí)序列中的token,但是它們不能預(yù)測(cè)那些在訓(xùn)練集中不常出現(xiàn)的token.在大多數(shù)情況下,模型非常依賴明確的方法名,比如setter和getter方法.如圖5所示,方法名沒有明確說明該方法需要做什么,其他模型便很難正確地注釋.相反,本文的模型可以生成更接近真實(shí)結(jié)果的token.
表2 代碼注釋預(yù)測(cè)結(jié)果Table 2 Code comment prediction results
圖5 Java示例Fig.5 Java example
4.2.2 組件分析
為了評(píng)估理解各組件的貢獻(xiàn),對(duì)模型進(jìn)行了消融實(shí)驗(yàn).首先,只考慮代碼的順序表示,在不使用AST的情況下實(shí)現(xiàn)了該模型;其次,實(shí)現(xiàn)了一個(gè)只采用AST節(jié)點(diǎn)作為輸入的模型;最后,通過訓(xùn)練了一個(gè)使用計(jì)劃采樣的模型,以驗(yàn)證模型性能的提升不僅僅是一種簡(jiǎn)單的正則化形式.模型評(píng)估結(jié)果見表3.
表3 Code2Com中每個(gè)組件的有效性Table 3 Effectiveness of each component in Code2Com
如表3所示,不編碼AST會(huì)導(dǎo)致BLEU得分下降,通過對(duì)AST分配權(quán)重,可以增加對(duì)命名token的注意力,并有效地忽略括號(hào)、分號(hào)等功能性token.其次,簡(jiǎn)單地在模型中使用AST而不使用token會(huì)大大降低分?jǐn)?shù),因?yàn)椴皇褂胻oken相當(dāng)于對(duì)沒有標(biāo)識(shí)符名稱、類型、APIs,和常量值的代碼進(jìn)行推理,這非常困難.源碼結(jié)構(gòu)和語義相結(jié)合能夠?yàn)樽⑨屔筛玫姆謹(jǐn)?shù).另外,我們觀察到使用Gumbel采樣的模型可以提高源代碼注釋生成的性能,BLEU得分提高了1.17.輸入Code2Com的預(yù)測(cè)詞可以有效地減輕曝光偏差,這優(yōu)勢(shì)背后的原因可能是Gumbel噪聲的采樣效果更接近真實(shí)分布的樣本,可以幫助選擇更有效而健壯的預(yù)測(cè)詞.此外,參數(shù)值的設(shè)置直接影響到模型生成質(zhì)量.本文對(duì)temp進(jìn)行了參數(shù)分析,當(dāng)temp在0.5時(shí)得到了最佳模型,如圖6所示.
圖6 Code2Com在不同temp設(shè)置下的性能Fig.6 Code2Com performance under different temp settings
4.2.3 句長(zhǎng)分析
本節(jié)研究了每個(gè)模型的性能如何隨著代碼長(zhǎng)度而變化.如圖7所示,隨著輸入代碼長(zhǎng)度的增加,所有模型BLEU得分都顯示出自然下降,與其他基線相比,在不同代碼長(zhǎng)度的度量上,Code2Com性能最好.此外,我們的模型在處理長(zhǎng)句方面顯示出穩(wěn)定的性能.
圖7 BLEU分?jǐn)?shù)與代碼長(zhǎng)度的比較Fig.7 Comparison of BLEU score and code length
提出了一種新的自動(dòng)生成Java代碼注釋模型,考慮了源代碼獨(dú)特的語法結(jié)構(gòu)和順序信息.核心思想是通過對(duì)代碼的結(jié)構(gòu)和順序內(nèi)容進(jìn)行編碼,并在訓(xùn)練時(shí)使用采樣方案將真實(shí)詞或優(yōu)化生成的前一個(gè)預(yù)測(cè)詞作為上下文.實(shí)驗(yàn)結(jié)果表明,Code2Com優(yōu)于現(xiàn)有的方法.在未來的工作中,計(jì)劃設(shè)計(jì)一個(gè)復(fù)制機(jī)制來處理詞匯表外特殊的未知token,同時(shí)研究如何來捕獲輸入結(jié)構(gòu),并將提出的方法擴(kuò)展到其他機(jī)器翻譯問題的軟件工程任務(wù),例如代碼遷移.