機器之心原創
參與:思源
大家都說深度神經網路能力很強,那麼從函式註釋生成函式程式碼,以及從函式程式碼總結函式註釋這種最基礎的程式碼任務到底能不能行?像 Python、Java 這樣的通用高階語言,到底在程式碼生成上能達到什麼水平?本文介紹的就是這樣一篇北大前沿研究。
開發者寫程式碼,和數學家寫公式一樣是非常自然的一件事。開發者將完成某個任務的步驟和邏輯,一行行寫成程式碼,並期待達到預定的效果。數學家從某個事實出發,將思考過程一行行寫成表示式,並期待找到複雜邏輯背後的簡單關係。
這兩者經常會有交叉,也會有融合。數學推導結果可以大量簡化程式碼,並提供新的解決路徑;而程式碼可以快速驗證推導過程,並用於實際的生活中。程式碼和表示式都是一種形式化語言,而另一種必不可少的是用來描述它的自然語言,也就是註釋或文件。
透過註釋,我們能知道這段程式碼幹了什麼,甚至很自然地想到「 如果是我,這段程式碼該怎麼寫」。透過閱讀程式碼,我們能沿著開發者的思路走一遍, 總結出它到底幹了什麼。這兩者似乎是一種對偶關係,從程式碼到註釋、從註釋到程式碼,這就是程式碼生成與程式碼總結兩大任務。
在這篇文章中,我們將介紹程式碼生成與總結的最新進展,北大 Bolin Wei、李戈等研究者提出的對偶學習在 Python 和 Java 程式碼生成上獲得了新的 SOTA,並且被接收為 NeurIPS 2019 論文。
如下是北大 新研究根據註釋生成的兩段程式碼,其中 dcsp 表示 tab 鍵、dcnl 表示換行符,它們控制 Python 程式碼的縮排結構。
值得注意的是,在 Python 語言上,根據註釋這種自然語言,生成有效的程式碼已經達到了 51.9% 的準確率。也就是說,
生成的一半程式碼能透過詞法分析、語法分析,並生成正確的抽象語法樹。
程式碼生成與總結,是一對兄弟
之前這兩項研究大多都是獨立的,程式碼總結會利用 Encoder-Decoder、抽象語法樹和 Tree RNN 等技術生成意圖,程式碼生成會利用 Seq2Seq、語法規則和基於語法的結構化 CNN 來生成程式碼,這些研究並沒有深入挖掘它們之間的關係。
而北大的這一項研究從對偶學習出發,探索如何利用它們之間的關係促進提升學習效果。
具體而言,研究者考慮了 機率與注意力權重中的對偶性,從而設計了一種正則項來約束對偶性。更直觀而言,這種「對偶性」表示程式碼生成任務的輸入"意圖"同樣是程式碼總結的輸出,反之亦然。其中意圖指開發者寫這一段程式碼的目的,一般而言會透過註釋的方式用自然語言表達。
利用對偶學習,研究者獲得了當前最優的效果。其實這種提升也非常合理,例如當前效果最好的神經機器翻譯模型 Transformer Big + BT,它就大量採用回譯機制,希望根據原語與目標語之間的相互翻譯,從而得到更好的最終模型。
統一的聯合訓練框架
如下所示為程式碼生成、總結的對偶學習框架,總體上生成與總結兩條路徑都非常容易理解,它們都採用了常規基於注意力機制的 Seq2Seq 模型。現在重要的是理解中間的對偶約束,該約束用於給損失函式加正則項,從而令它們之間相互促進。
對偶訓練的整體過程,程式碼生成模組與總結模組會聯合訓練。
上面 Seq2Seq 的過程就不再贅述了,它們採用的損失函式也是常規將所有時間步上的損失相加。不過需要注意的是,原始碼的詞彙量要比註釋更大一些,因此程式碼生成模組輸出層的引數量要大於程式碼總結的輸出層引數量。
聯合機率來約束
如前所述,對偶訓練框架包含了非常重要的對偶約束,它由兩個對偶正則項組成,分別用於約束兩個模型的對偶性。這兩種正則項受到了注意力權重具有對稱性的啟發,也受到了兩種模型之間機率相關性的啟發。
若現在給定輸入樣本<x, y>,其中假設 x 為程式碼,y 為對應的程式碼註釋。那麼程式碼生成可以描述為 p(x|y)、程式碼總結可以描述為 p(y|x)。現在如果要找到它們之間的機率相關性,那麼 根據聯合機率與條件機率之間的關係式就可以快速得出:
也就是說, logP(x) + logP(y|x) 需要等於 logP(y) + logP(x|y),這是程式碼生成與總結的內在聯絡。如果兩項差別很大,那麼至少可以判定程式碼生成與總結都沒有達到最優。所以,常規的做法就是把這個約束構建為損失函式:
其中 P(x) 和 P(y) 分別是針對程式碼和註釋的語言模型,它們都是邊緣分佈。這個損失有點類似於迴歸模型常用的均方誤差,如上所示,只要兩個子模型不滿足理論上的機率條件,那麼肯定會產生損失,在訓練中就會建立起程式碼生成與總結的關係。
注意力權重也來約束
上面是其中一個正則項,另一個正則項主要是考慮兩個子模型之間的對稱性。在北大的這一項研究中,他們考慮了注意力權重的對稱性。研究者表明,因為注意力權重能度量原始碼 Token 與註釋 Token 之間的匹配關係,而這種匹配關係又是對稱的,所以注意力權重也需要是對稱的。
研究者舉了一個例子,例如程式碼註釋為「find the position of a character inside a string」,那麼對應原始碼可能為「string . find ( character )」。現在,不論是從程式碼到註釋還是從註釋到程式碼, 原始碼中的「find」一定需要匹配到註釋中的「find」,它們之間的關係是不變的。
所以,現在最直觀的思想是, 我們希望兩個注意力權重矩陣 A_xy 和 A_yx,它們之間對應的元素儘可能相等。
因為 A_xy 表示程式碼部分注意到註釋部分的程度,所以,A_xy 矩陣的每一行表示程式碼的某個 Token,與註釋的所有 Tokens 之間的關係。同理 A_yx 表示註釋部分注意到程式碼部分的程度,A_yx 的每一列表示程式碼的某個 Token,和註釋的所有 Tokens 之間的關係。
具體而言,如果
,其中 i 表示 A_xy 的第 i 行;
,其中 i 表示 A_yx 的第 i 列。那麼很明顯,我們需要令 b_i 儘可能等於 b_i'。 如果它們非常相近,那麼可以表明注意力權重矩陣是對稱的,原始碼和程式碼註釋之間的匹配是成功的。因為經過 softmax 的 b_i 和 b_i'都是一種機率分佈,所以北大研究者透過 JS 散度度量這兩類分佈之間的距離。
最常見的 KL 散度是不對稱的,也就是說 KL(b_i || b_i') 不等於 KL(b_i' || b_i),而 JS 散度是 KL 散度的「對稱版」,所以採用 JS 散度非常合理。此外,因為 JS 散度是對稱的,所以程式碼生成模型與程式碼總結模型都能採用這樣的距離度量作為約束條件。
最後,以注意力權重的對稱性作為正則項,JS 散度可以表述為:
虛擬碼帶你走近聯合訓練
現在兩種正則項都已經完成了,只需要聯合訓練兩個子模型就行了。如下演算法 1 所示,輸入兩種資料來源的語言模型預計對應的資料,模型就能開始學。
如上所示,對於每一個批次資料,模型會計算兩個子模型各自的預測損失,並同時計算兩個公共的對偶正則項。這樣的損失能算出對應的梯度,並分別更新兩個子模組的權重。
目前該研究的開源實現已經放到了 GitHub,研究者使用 PyTorch 實現了整個模型的訓練過程。如上虛擬碼所示,模型架構方面,Seq2Seq 大家已經比較熟了,我們需要重點理解的是目標函式。
如上程式碼片段所示,損失函式主要由三部分組成:即常規的交叉熵損失函式,它度量生成序列與標註序列間的距離;對偶損失函式,它度量的是程式碼與註釋的機率相關性;最後是注意力損失,它度量的是兩組注意力權重之間的分佈距離。
透過這些訓練目標的約束,程式碼生成與總結才會真正地相輔相成。
真實的 GitHub 程式碼生成
這種最正統的程式碼生成與總結無疑是非常困難的,它遠遠不能像 UI 介面那樣生成簡易的程式碼。也許藉助卷積神經網路,UI 介面的程式碼生成已經能用於實際的介面設計,但是對於「更正統」的純程式碼生成,目前的準確度還遠遠不能滿足我們的要求。
在這篇論文中,北大研究者在 Java 與 Python 兩個資料集,測試了程式碼生成與總結的效果。其中 Java 資料集是從 GitHub Java 專案中抽取的 Java 方法,以及對應的自然語言註釋,該自然語言了這個方法的用途。與 Java 類似,Python 資料集也是從 GitHub 中抽取的。兩種資料集的統計資訊如下所示:
論文表 1,我們可以看到,訓練集有 5 萬到 7 萬段程式碼,且確實一段 Python 程式碼平均長度要遠遠少於 Java 程式碼。
最後,我們可以看看北大研究者得出的最終效果。他們主要透過 BLEU 值、METEOR 和 ROUGE-L 三種度量方法評估模型生成的程式碼註釋,這對於自然語言生成來說是比較常規的度量標準;此外,研究者透過 BLEU 值與有效程式碼率(PoV)來評估程式碼生成的效果,其中 PoV 指生成程式碼能解析為抽象語法樹的比例。
如上所示為程式碼生成與總結的總體效果,我們可以發現對偶訓練效果要超過其它方法,且相比獨立訓練的 Basic Model,效果也要更好一些。
值得注意的是,在程式碼生成中,Java 和 Python 的 PoV 分別只有 27.4 與 51.9%。也就是說,生成的程式碼首先不管是不是完成了自然語言描述的功能,它能透過詞法分析、語法分析,最終成功地構建成抽象語法樹,佔比並不高。
這樣的效果,也許代表著正統程式碼生成,最前沿的水平。它離生成合理的程式碼,輔助開發者完成實戰開發還太遠了。正如該論文作者李戈教授所說,程式的資料空間非常稀疏,而自然語言資料空間也比較稀疏,這兩個稀疏空間的變換肯定會比較困難。它並不能像影像生成這種連續空間的變換,程式的生成還有很長的路要走。