謝文凱 彭 鑫 趙文耘
(復旦大學軟件學院 上海 201203)(上海市數據科學重點實驗室 上海 201203)
近些年來,互聯網技術的發展日新月異。新技術層出不窮,同時一些舊的技術也在加快迭代的速度,煥發出新的活力,這導致了軟件開發所涉及的技術體系也越來越繁瑣、越來越復雜。但由于人的精力有限,軟件開發人員也不會掌握全部的知識,他們在開發過程中也經常會遇到一些自己難以解決的問題。于是,一些能夠專門為開發人員提供交流平臺的編程類問答網站也逐漸變得火熱起來。用戶能夠針對自己遇到的問題尋求幫助,也能夠針對別人的問題提供自己的見解和解決方案。Stack Overflow是目前所有的編程類問答網站中最活躍、使用最廣泛的其中之一。
與以往的用戶能夠自由交流討論、暢所欲言的論壇不同,Stack Overflow的目標是希望成為編程類問答的一個超級數據庫。在網站建立之初,它就要求用戶在提問時要堅持“practical, answerable questions based on actual problems that you face”的原則,所以網站上的每個問題都不止是為了僅僅幫助提問者本人,更重要的是希望將來當別人遇到同樣的問題時,同樣能夠提供幫助。
而在Stack Overflow的所有問答帖中,存在著大量的代碼片段,它們可能是存在問題的代碼,也可能是針對遇到問題所提出的解決問題的代碼,當然也可能僅僅只是一個可能會用到的API等。如果能將這些代碼片段進行合理、準確的分類,一方面,能夠使得展示在網站上的代碼片段更加直觀、易于理解,能夠幫助開發人員更加容易地分辨清楚哪些是自己需要的作為解決方案的代碼片段,哪些是出現問題、需要進行修改或者優化的代碼片段;另一方面,如果能夠自動化地將問答帖中的代碼片段進行分類,那么分類后的代碼片段將更能夠幫助構建起一個更加完整、更加結構化的針對answerable questions的知識庫。
Stack Overflow是隸屬于Stack Exchange旗下的編程類問答網站,自2009年創建以來,每隔2~3個月Stack Exchange都會將進行處理后的網站數據公開發布,需要的用戶可以直接去官網下載相應的dmp文件。得益于此,現在每年都會有大量基于Stack Overflow所做的研究。然而,大部分關于Stack Overflow的文獻主要的關注點還是集中于Stack Overflow本身或是網站所提供的數據進行的[1-4],例如對網站數據文本的挖掘、問題和內容的分析等。
Arora等[2]基于Stack Overflow提供的數據,采用了一種將問題向量轉換為特征空間中的不同向量的方法,不同于以往單獨地從問題中學習分類邊界,對離散項空間、從文獻載體嵌入了實數的連續向量空間進行探索,實現了一種能夠將新發布的問題進行分類的方法,將分類的準確率提高了8%。Joorabchi等[3]采用了一種啟發式研究方法,結合文本挖掘的方法來研究Stack Overflow網站上問答帖的主題和類別,對近18 600個問答帖進行了分析,使用維基百科對人群進行分類,建立它們之間的語義關系,并通過Gephi等可視化軟件將數據轉化為圖像呈現出來,確定發布回答是否確切主題,并縮小其類別,以幫助確定學習者的學習難題。Ye等[4]利用Stack Overflow所提供的數據,在現有的命名實體識別技術的基礎上,提出了一種應用于軟件工程領域的基于機器學習的命名實體識別技術,可以識別廣泛的流行編程語言、平臺、API等軟件實體。
而對于問答帖中的大量存在的代碼片段等,幾乎沒有專門針對此內容的研究,從這一方面著手開展研究存在著很大的潛力。
機器學習主要致力于研究如何通過計算的手段,利用經驗來改善系統自身的性能。在計算機系統中,“經驗”通常以“數據”的形式存在,因此,機器學習所研究的主要內容是關于在計算機上從數據中產生“模型”的算法,即“學習算法”。學習算法能夠基于提供的經驗數據產生模型;在面對新情況時,模型會提供相應的判斷。如果說計算機科學是研究關于“算法”的學問,那么類似地,可以說機器學習是研究關于“學習算法”的學問[5]。
現如今,機器學習已經發展成為一個相當大的學科領域。從學習方式的角度來分,機器學習可以分為無監督學習、有監督學習和半監督學習。有監督學習的常見場景如分類問題和回歸問題,無監督學習的常見場景包括關聯規則的學習和聚類等,半監督學習的常見算法包括圖論推理算法等[6]。而從算法的角度來分,則可以分為基于樹的算法、基于神經網絡的算法、貝葉斯方法等。其中,貝葉斯方法的主要原理就是貝葉斯公式,這一類方法主要應用于分類問題。而樸素貝葉斯方法是一種基于條件獨立假設的貝葉斯方法,與其他的方法相比,其收斂速度更快,模型也更加簡單,也有很多的學者運用樸素貝葉斯方法做了大量的研究。
Catal等[7]為Java程序開發了一款能夠基于Eclipse的軟件故障檢測工具,并運用了樸素貝葉斯方法,將軟件度量與軟件數據故障相結合,來簡化故預測過程。Zhang等[8]針對云計算環境中調度算法的重復性和分配的不均衡,提出了一種基于樸素貝葉斯算法的負載均衡技術,該技術利用心跳機制全面收集每個節點的負載信息,從而基于樸素貝葉斯算法對所有節點的負載狀態進行分類,根據分類對每個節點的任務和資源進行合理調度。
若想能夠對Stack Overflow上的代碼片段進行比較全面、完整的分類,首先就需要先了解開發者經常會提問的問題的類型。在研究開始之前,首先對Stack Overflow上包含Java標簽的問答帖進行了大量的分析和研究。經過大量的分析后,本文將Stack Overflow上包含代碼片段的問題主要歸納為以下幾個類型,并總結了這些問題類型的特點。
1) 第一類問題是debug類型的問題,這一類問題主要是開發者在實際開發的過程中實際得到結果與預期不相符并且自己又難以解決時提出的。在提問時,開發者一般首先會對自己所做的工作及遇到的問題進行簡單的描述,大部分情況下也會貼出存在問題的代碼片段或者出現問題時的報錯棧等,這一類問題通常會包括“error”“exception”等單詞,在網站上比較常見。例如,在問題“Getting Invalid Address with javax.mail when the addresses are fine”中,開發者在利用javax.mail開發郵件系統時使用合法的郵件地址卻拋出了“Invalid Address”的異常,于是貼出自己的問題代碼求助,得到了其他開發者合理的解答。
2) 第二類問題屬于改進類型的問題,這一類問題主要是因為開發者對當前程序性能不太滿意,例如耗時較長等,希望在原有的版本基礎上尋求更加優化的方案而提出的。在提問時通常會包括“need a better method”“not efficient”等語句。例如,在問題“Get Last Friday of Month in Java”中,開發者在沒有利用現有API的情況下實現了一個能夠得到每月的最后一個星期五的程序,但是效率不夠高而且代碼比較繁瑣,可重用性較差,希望能夠得到一個效率更高的代碼,其屬于改進類型的問題。
3) 第三類問題是具體實現的問題,這一類問題主要是因為開發者相對缺乏當前開發程序所需的知識,在開發過程中遇到了困難而提出的。通常,在這類問題中會包括“how can I…”等詞語。例如,在問題“How do I restrict JFileChooser to a directory?”中,開發者希望對用戶的瀏覽目錄進行限制,而“Parent Directory”可以瀏覽任意目錄,開發者希望能夠尋求比較完整的具體實現的代碼片段。
4) 第四類問題與第三類問題相對,屬于抽象類型的問題。在這類問題中,開發者不再是針對具體的實現問題尋求解決方案,而是上升到了比較抽象的層面,一般在回答中的代碼片段只是用來舉例,使抽象的問題更加具體化,通常,會包括“what…”等詞語。例如在問題“What is the difference between an int and an Integer in Java and C#?”中,回答中包含的代碼片段如“Integer i=new Integer(6);”“int i=6;”只是為了說明Integer和int的區別,而并不是針對問題提出的解決方案。
在Stack Overflow網站上的所有問答帖中,代碼片段一般由兩種存在形式:一種是一段完整的代碼片段,與其他的文本描述分隔開來,被包裹在
標簽中;另一種主要存在于問題和答案的文本之間,一般只是用來表示變量、數據類型等,被包裹在標簽中。按照標簽的不同,可以將代碼片段分為“大代碼片段”和“小代碼片段”兩種類型。根據2.1節中對Stack Overflow問答帖的研究,本文將“大代碼片段”劃分為了如下六個類型:
1) 開發者在開發過程中出現問題或bug的完整代碼,這類代碼主要存在于問答帖的問題中。
2) 開發者在開發過程中的完整代碼,沒有明顯的bug和error,但是可以進行優化的代碼,這類代碼主要存在于問答帖的問題中。
3) 沒有bug或error,用來作為舉例說明的代碼,這類代碼即為2.1節中第四類問題使用的代碼,在問答帖的問題和回答中都會存在。
4) Stack Overflow用戶針對開發者所遇到的問題、bug或針對可優化的代碼提出的解決方案,這類代碼主要存在于問答帖的回答中,在網站中最為常見。
5) 開發者在問題或回答中對內容進行補充說明時所用到的輸入輸出,這類雖然不是真正的代碼,但是也由“大代碼片段”標簽包裹,在問答帖的問題和回答中都會存在。
6) 開發者對表述內容進行補充說明時所用到的程序出現問題時拋出的異常信息、報錯棧等,主要存在于問答帖的問題中。
“小代碼片段”主要存在于問答帖的文本描述中,用于表示開發者在問題或答案中可能會涉及到的API、變量等。與“大代碼片段”相比,這些“小代碼片段”價值相對較低。“小代碼片段”分為如下兩個類型:
1) 開發者在編寫問題或答案時可能會用到的代碼中的類名、API等信息,在問題和回答中都會存在。
2) 對于“小代碼片段”,除去上述第一類,其余全部都歸于此類中,常見的如變量名、數據及一些軟件工程相關的通用知識等,同樣在問題和回答中都會存在。
代碼片段的上下文非常重要,那么選取合適的文本作為上下文就極為關鍵。文本太短可能會導致相同類型的不同代碼片段上下文之間沒有太多的相似性;過長又可能會帶來更多的噪聲,造成相同類型的不同代碼片段上下文之間的無關聯系增加,干擾判斷。
為最大限度地選取合適的上下文內容,保證上下文內容的質量,本文針對“大代碼片段”和“小代碼片段”采取了兩種不同的策略。
“小代碼片段”策略比較簡單,因為其存在于成段的文本中,不同段落之間影響較小,因此其上下文內容均為其所在段落的文本。
而對于“大代碼片段”而言,經過分析發現可以以“小代碼片段”所在的段落作為邊界,因為在“大代碼片段”附近出現的“小代碼片段”,很大程度上是為了解釋“大代碼片段”中的一些信息。因此可以采取如下策略:(1) 從“大代碼片段”的位置向上(下)查找直到某段文本中出現“小代碼片段”為止(包含此段文本);(2) 如果沒有“小代碼片段”,則與相鄰“大代碼片段”之間的文本作為相應的上文或下文;(3) 如果以上情況(1)、(2)均不存在,則該代碼片段上(下)面的所有文本作為相應的上(下)文。
Stack Exchange在發布數據文件時,只是將從HTML中提取出的數據放入到數據庫中,并沒有對其做相應的去標簽化處理。同時,因為開發者在Stack Overflow網站的問答帖中編輯問題或答案時,在描述時有時會用到一些符號,簡單的分詞和分句會造成分詞不正確,例如“exception)”“question,”,會導致程序在處理數據時出現誤差;此外,單詞在不同場景下,會有不同表示形式,例如“question”和“questions”是同一個單詞的單復數形式,“better”是“good”的比較級;不同的代碼片段之間的上下文之間可能會出現重復現象,如果同一段文本的重復次數過多,在進行特征詞提取時,會導致重復文本出現次數異常增多,在最后的特征詞集中也會出現過多的噪聲,對最后的預測準確性造成很大的影響。
基于以上四種情況,本文對數據進行了以下四個方面的預處理。
1) 去標簽化。對于數據中存在的HTML標簽,因為其比較固定,均是包裹在“<…>”中,而數據中原本存在的“<”和“>”,則是由轉義字符“<”和“>”表示。因此,將數據中原本由“<…>”包裹的所有標簽替換掉,而對原本在數據中表示“<”“>”等的轉義字符,按照對照表修改為對應的字符即可。
2) 去字符化。與中文不同,在英文中可能為了縮寫某些單詞會在單詞中間添加標點符號等作為標志,同時在某些字段中間的一些字符也需要進行保留,例如“C:Users”等,這就導致不能以偏概全的將單詞中所有出現的字符、標點等全部去除掉。在這樣的前提下,本文通過數據分析,歸納出了單詞中的無用字符出現的一些特點:
(1) 在單詞開頭或結尾地方出現的字符一般為無用字符,例如“(exception”、“example,”等;(2) 一些特殊的字符,如“/”“<”“>”和不在單詞結尾的“.”等,有特殊的含義,不可去掉。
結合單詞的這些特點,本文采用正則表達式對單詞進行匹配,對于不符合條件的單詞去除相應的字符來實現單詞的去符號化。
3) 詞干化。本文利用Java庫TT4J(TreeTagger for Java)進行詞干化,這個庫的主要功能是識別語句中每個單詞在語句中的詞性,并能夠根據單詞的詞性得到單詞的詞干。不僅如此,在不同的環境下相同的單詞可能因為語境的不同可以得到不同的結果,例如,“better”在不同的語境下可能會被轉化為“good”或“well”。
4) 去重。兩段代碼片段的上下文之間出現重復現象,如果這兩段代碼片段的類型一樣,那么就直接把重復的上下文去除掉,不考慮重復文本;如果這兩段代碼片段的類型不同,那么就按照正常的邏輯將重復文本劃為不同類型,對于每段代碼片段都采用這樣的方式檢查。
樸素貝葉斯法是指基于條件獨立假設和貝葉斯定理的分類方法[9-10]。設x={a1,a2,…,am}為一個待分類項,而x中的每個元素ai為x的特征屬性,設C={y1,y2,…,yn}為類別集合,C中每個元素yi為每個可能的類別。分別計算P(y1|x)、P(y2|x)、…、P(yn|x),如果P(yk|x)=max(P(y1|x),P(y1|x),…,P(y1|x)),則x∈yk。
若想得到x屬于C中的哪個類別,關鍵就是得到各個條件概率,根據貝葉斯定理:
(1) 確定已知分類的數據集合作為訓練樣本N;
(2) 對于每個特征屬性在各個類別下的條件概率:
P(a1|y1),P(a2|y1),…,P(am|y1);…;P(a1|yn),P(a2|yn),…,P(am|yn)。
可以通過極大似然估計來求得,公式如下:
(1)
(3) 根據條件獨立假設及貝葉斯定理,可得:
(2)

在實際的編碼中,大量的乘法計算會導致計算的時間開銷比較大。因此,本文采取通過比較x在不同類型yi情況下互信息量的大小來對應x在不同類型yi情況下的概率的大小。
互信息主要用于度量兩個事件集合之間的相關性,可以看成是一個隨機變量由于已知另一個隨機變量而減少的不肯定性[11]。
設兩個隨機變量(X,Y)的聯合分布為P(X,Y),邊際分布分別為P(X)、P(Y),互信息I(X;Y)是聯合分布P(X,Y)與乘積分布P(X)P(Y)的相對熵,即:
(3)
對于本文來講,設變量X表示數據集中的不同文本,變量Y表示可選擇類型,互信息I(X;Y)就可以表示不同的文本與可選擇類型的聯系程度,互信息越大,那么該文本與該類型之間的聯系越緊密,該文本屬于該類型的概率也就越大。
本文主要運用了機器學習中的樸素貝葉斯算法,根據通過分析、總結出的Stack Overflow中的八種常見的代碼片段的類型,實現了對提取出的代碼片段的種類進行預測的算法。算法流程如算法1所示。
算法1樸素貝葉斯分類器
1. function classification(){
2. InitialCode=readData()
3. CodeSnippets=preprocess(InitialCode)
4. TrainSet=part of CodeSnippets
5. TestSet=part of CodeSnippets
6. FeatureWords=extractFeatureWords(TrainSet)
7. BayesModel=trainModel(FeatureWords)
8. Result=predict(FeatureWords,BayesModel,TestSet)
9. return Result
10. }
首先,從數據庫中讀取出未經處理過的原始數據集,經過數據預處理后,得到可以用于樸素貝葉斯算法的數據集,并將該數據集隨機劃分為訓練集和測試集,其中訓練集與測試集的比例為9 ∶1。
對訓練集中的數據,進行提取特征詞操作,計算訓練集中的每條數據中所有單詞對于不同類型的互信息,對于每種類型提取出50個互信息較大的單詞,合并后作為特征詞集。
接著,對訓練集的數據進行訓練,計算特征詞集中的每個單詞在不同類型下出現的概率,作為訓練好的貝葉斯模型。
最后,對測試集中的數據進行種類預測,計算測試集中的每條數據在不同類型中的概率,得到該數據最可能的類型。
本實驗為探究不同類型數據對準確率的影響程度,即探究算法能夠比較高效的預測哪些類型的數據、對哪些類型的數據預測能力較差,主要測試了算法對測試集中不同類型的數據進行預測所得到的準確率和召回率,并通過對所得結果進行分析,總結出產生這些差異的原因。
本文所使用的數據主要來自2019年3月發布的公共數據集,主要選取了包含Java標簽的31 153條數據作為原始數據集,其中,4 998條數據屬于問答帖中的問題,而剩余26 155條數據屬于問答帖中的回答部分。隨機對8 000條數據進行了人工標注,得到每種代碼片段類型的數據量如表1所示。

表1 8種代碼片段標注數據數量
實驗一共選取了800個不同的代碼片段作為總數據集,其中每個類別各包含100個代碼片段。一共進行兩次實驗,每次分別取各種類型數據的一半一共400個代碼片段進行實驗,最后取兩次實驗的平均值作為最后的結果。
實驗環境是在macOS操作系統的PyCharm編譯器,運行服務器配置為2.3 GHz四核Intel Core i5和16 GB內存,實驗的評價標準是準確率P和召回率R。
利用本文的自動分類方法進行實驗,得到八種代碼片段對應的準確率和召回率分別如圖1和圖2所示。

圖1 八種類型代碼片段的準確率

圖2 八種類型代碼片段的召回率
可以看出,大代碼片段中第一種和第六種代碼片段及兩種小代碼片段類型對應的準確率較高,均達到了70%以上;第二和第三種類型的代碼片段準確率較低,在50%~60%之間,第四種和第五種類型代碼片段準確率在60%~70%之間。大代碼片段中前五種類型的召回率比較低,均在50%~75%之間,第六種類型數據的召回率較高;小代碼片段中第七種類型數據的召回率超過了90%,而第八種類型的召回率在70%~80%之間。
對于預測準確率較高的大代碼片段中的第一種和第六種類型,這兩種類型的代碼片段上下文更容易提取特征詞,例如,在第一類代碼的上下文描述中,一般都會用“I have a/an question/exception…”等語句來引出存在問題的代碼片段,question、exception等單詞出現的評率較高。兩種小代碼片段類型因為上下文僅選取了所在的段落,遇到的噪聲較小,特征更加明顯,準確率也更高。
而準確率較差的這幾種代碼片段類型,通過對它們的代碼片段上下文及所提取的特征詞集的分析,發現它們之間的差異并不明顯,一方面是因為訓練集中這幾種類型的數據較少,特征詞的頻率較低,另外一方面是因為利用特征詞進行預測還不能獲取到所有的信息,還需要進一步結合上下文描述中語義層次的信息,采用一些諸如word embedding的技術來提升實驗效果。
本文基于機器學習算法,利用Stack Overflow網站中大量存在的代碼片段,對代碼片段的用途分類進行研究。通過對Stack Overflow網站上的問答帖的大量分析,歸納出了八種比較常見的代碼片段的類別,并利用機器學習中的樸素貝葉斯方法實現了這種分類方法。實驗結果表明,預測的準確率能夠達到70%以上,在一定程度上解決了最初提出的問題,未來可以考慮利用一些更加成熟的技術來對效果進行改進。