屈 薇,周 栋,赵文玉,曹步清
(湖南科技大学 计算机科学与工程学院,湖南 湘潭 411201)
代码摘要生成任务旨在实现全自动化地产生自然语言描述源代码的功能,有利于软件维护和程序理解[1]。随着软件更新速度不断加快,再加上开发人员重视程度不够等原因,导致现有的代码摘要常常存在重复或者与代码实际内容不匹配等问题。因此,代码摘要成为当前软件工程领域的一个研究热点[2]。
早期的代码摘要生成研究大多采用信息检索的方法[3-4],通过检索相似代码片段生成摘要,但该类方法缺乏通用性,效果并不理想。近年来,随着神经网络技术的快速发展,大多数研究者将该任务视为序列生成任务[5-11],通常依赖于循环神经网络(Recurrent Neural Network,RNN)结构。
众所周知,普通的源代码片段通常包含数百个标记,而基于RNN结构的方法无法很好地建模序列的长距离依赖关系,成为限制该类模型性能的瓶颈。最近,Ahamd等人[12]尝试采用增强的Transformer结构捕获源代码标记的长距离依赖关系和语义特征,在很大程度上超过了之前的基于RNN的方法。与RNN结构相比,Transformer具有与全连接层相同的最大路径长度,更适合建模长距离依赖关系。
然而,现有的基于Transformer的方法[12-14]大多都只考虑源代码的文本和结构化语义特征,忽略了其他与源代码密切相关的外部知识语义特征。例如在软件开发过程中,外部应用程序编程接口(Application Programming Interface,API)文档是一种非常重要的资源,包含丰富的方法和接口的功能描述,可能有利于代码摘要生成。如参考在线API文档(1)https://docs.oracle.com/javase/7/docs/api/java/io/OutputStreamWriter.html中实现字符串小写转换接口的描述“converts all of the characters in this String to lower case”有利于生成可读性更高的摘要。大量的研究[11,15-18]也表明,学习源代码的外部知识的语义信息,可以使模型更好地理解代码片段,生成可读性更好的摘要。例如,有研究[11]提出捕获 API名称与摘要之间的关联关系,能够有效提升生成摘要的质量。相比之下,API文档基于自然语言,且包含了源代码更多的功能细节描述,应该能够为代码摘要生成任务提供更有效的信息。
另一方面,代码摘要任务的输入序列可能很长, Transformer结构的自注意力模块需要计算所有相似度分数,存在计算成本和内存占用量大的问题,亟待改进。
基于以上原因,本文提出一种改进的Transformer多编码器代码摘要生成方法(如图1所示)。通过构建三个独立的编码器充分学习源代码的多种语义特征(文本、结构和外部API文档),解决该类方法中忽略源代码外部信息的问题。同时,针对Transformer结构的性能问题,受到谷歌研究人员的启发[19],使用非参数化傅里叶变换代替复杂的自注意力层,通过线性变换降低编码器的计算时间和内存占用量。最后,将三个编码器输出的初始语义特征通过交叉注意力块[20]融合到一个特征空间中,作为解码器的输入,生成对应的摘要。
本文的贡献总结如下:
(1) 基于Transformer结构,引入外部API文档语义特征,构建三个独立编码器[即文本、抽象语法树(Abstract Syntax Tree,AST)结构和外部API文档],给代码摘要任务提供足够的语义信息,提高生成摘要的准确性和可读性。
(2) 为了缓解Transformer的效率问题,采用非参数化的傅里叶变换代替编码器中核心的自注意力层,通过线性变换降低编码器的计算时间和内存占用量。
(3) 将以上两点结合并提出融合多种语义特征的代码摘要生成方法,在公开可用的数据集上进行相关对比实验,证明了本文提出方法的有效性。
早期的代码摘要方法[3-4]主要基于信息检索技术,随着神经网络技术的快速发展,最近的研究工作将其建模成序列生成任务[5-11]。同时,为了更好地捕获远距离特征,注意力机制得到广泛的应用。Iyer等人[5]首次提出联合注意力机制的RNN网络,为C#代码片段和SQL查询语句生成摘要描述。此类方法的主要区别是模型的输入。例如,Loyola等人[21]将源代码的文本序列输入到RNN模型生成摘要。2018年Hu等人[11]分别将源代码的文本和API名称序列作为输入,对应两个摘要任务,利用捕获的API名称和摘要的映射关系增强代码摘要任务。对于结构性强的编程语言而言,结构化信息显得尤为重要。因此LeClair等人[8]和Wan等人[9]都相继提出使用双编码器分别对源代码的文本和AST结构序列进行编码,然后使用混合注意力层将两个编码器的输出结合并输入到解码器。
众所周知,基于RNN结构的方法在编码长代码序列时可能会遇到瓶颈,影响方法性能。而Transformer在许多人工智能领域取得了巨大的成功,自然而言也吸引了软件领域研究人员的兴趣。2020年,Ahmad等人[12]探索了基于Transformer结构的代码摘要生成方法,提出具有复制注意力和相对位置编码的增强版Transformer方法。之后,多项研究[13-14]都利用Transformer结构建模代码摘要任务,其性能在很大程度上超过了先前基于RNN的方法。
本文首先针对现有的基于Transformer结构的代码摘要生成方法只考虑源代码的文本和结构化语义特征,而忽略了与之密切相关的API文档等外部语义特征的这一问题,通过在Transformer中使用三个编码器,充分学习源代码的多种语义特征(即文本、AST结构和外部API文档)。同时为了缓解Transformer编码器的效率问题,我们采用傅里叶变换替代编码器中核心的自注意力层,降低编码器的计算复杂度和内存占用量,并联合交叉注意力网络[20]将编码器初步提取的三种语义特征融合并输出到解码器。
如图1所示,本文模型主要由解析器(Parser)、编码器、融合组件和解码器组成。
假设X={x1,…,x|X|}和Y={y1,…,y|Y|}分别表示处理后的源代码片段和自然语言组成的摘要序列,xi和yi分别表示X和Y序列中的单词,|·|表示序列的长度。那么代码摘要任务可以形式化为: 给定源语言X生成自然语言表示的摘要句子Y。实际上,代码摘要的任务是学习一个摘要生成器f,给定源代码片段X,生成其对应的摘要序列Y,如式(1)所示。
f:X→Y
(1)
本节主要描述源代码片段的处理过程,分别得到AST 编码器、文本编码器和API 文档编码器的输入序列。
首先使用Eclipse的JDT工具生成源代码片段的AST,先序遍历后采用文献[14]提出的层次分割方式,得到一组包含全局和局部结构的AST子树集,将其作为AST 编码器的输入序列。
由于源代码中大量的标识符均为稀有字符, 且大部分标识符都是根据驼峰命名法进行命名的,名字通常由多个单词拼接而成,包含源代码功能的关键信息。因此,基于驼峰命名法对其进行分割,将全部转换成小写(如“toLowerCase”分割成“to”“lower”“case”)得到文本编码器的输入序列。
最后,为了提取源代码片段对应的API文档信息,受Hu 等人[6,11]启发,利用从AST序列中提取的API名称从在线参考文档中检索出对应的功能描述信息。对于重载的方法根据方法中参数的个数,确定最合适的API文档描述。其次,对于参考文档中出现的多个相同的API名称,且描述内容不同的情况下,本文选取频率使用最高的API名称对应的描述。实验中发现,随着源代码中使用的API数量增加,API文档的贡献会相应减少。我们分析主要原因可能是: 输入较多的API文档时,会向模型提供一个长文本。然而,摘要和API文档间通用词的数量非常少,导致引入大量的噪声。因此,为了在利用API文档信息的同时减少其引入的噪声,我们设计了一个基于BM25算法的脚本,检索出与源代码片段(当API名称超过3个)最相关的3个API组成API 文档编码器的输入序列。
本节将详细介绍我们构建的文本编码器、AST Encoder和API Encoder三个独立编码器。
2.3.1 文本编码器和API 文档编码器
源代码片段的文本信息是给代码摘要任务提供词法语义特征的原始数据源,包含大量描述代码功能的关键信息,得到众多研究人员的关注。与文献[12]中基于Transformer结构的源代码文本编码器不同的是,如图2所示,与标准的Transformer编码器相比,我们使用傅里叶变换替代了自注意力层,通过简单的线性变换降低编码器的计算成本和内存占用量。那么,输入序列x的傅里叶变换可以用式(2)计算:
图2 文本编码器和API 文档编码器结构
(2)
由于傅里叶变换具有卷积特性(即频域上的乘法相当于时域上的卷积),首先把输入的特征向量变换到频域,再输入到前馈层进行乘法计算,实质上是对输入进行了一个大卷积核的卷积。考虑到傅里叶变换的对偶性(建立了时域序列与频域序列之间完美的映射关系),所以多个编码器叠加之后,使输入序列在频域与时域间来回变换,反复进行卷积与乘法操作,因此能够充分融合序列中每个标记的信息。同时,傅里叶变换没有学习参数,所以能够降低编码器的计算成本和内存占用量。
实验中我们发现,如果将编码器中的自注意力层全部替换成傅里叶变换层,虽然编码器的参数减少到原来的2/5,但是精度会有损失。因此,我们保留了编码器的最后两个自注意力层。如图2所示,将通过堆叠的傅里叶变换层得到的特征向量输入到两个自注意力层中,得到最终的源代码的文本语义特征hCode。
由于API文档由自然语言组成,因此也采用和文本编码器结构相同的编码器进行编码,得到初步提取的API文档语义特征hAPI。
2.3.2 AST 编码器
对于结构性强的源代码来说,其AST包含的语法和结构信息,与文本语义信息同样重要,这在多个研究[8-11,14]中得到广泛应用,均取得了不错的效果。然而, Transformer对于由结构展开的非线性关系(即AST)表示能力差,编码AST并不能得到期望结果。因此,本文依照源代码结构分层和嵌套的特点,采用递归神经网络(Recursive Neural Network,RvNN)[14]自底向上递归访问AST,提取源代码的全局和局部结构语义特征,如图3所示。
图3 RvNN网络聚合过程
具体来说,第一阶段采用RvNN和最大池化层对分割后的每个子树进行编码。假设一个AST子树Tt被定义为(Nt,Et),其中Nt是节点集,Et是节点之间的边集。如图3所示,利用RvNN对Tt进行正向传播如式(3)所示。
(3)
最后,通过最大池化操作将所有节点聚合,作为子树Tt最终的特征向量st如式(4)所示。
(4)
第一阶段得到AST所有子树的聚合向量后,采用另一组不同参数的RvNN进行第二阶段学习,提取所有子树之间的层次依赖关系,学习过程与第一阶段类似。最终,得到基于源代码片段X的AST的向量表示hAST。
在实验初期,我们尝试将三部分信息拼接后输入到同一个Transformer编码器中,但模型的预测性能并没有得到改善。我们分析这一结果可能是由以下两点原因导致的: ①拼接后的输入序列长度远超于摘要序列的长度,更长的输入序列相应的也引入更多的噪声,影响模型的预测性能。②标准的Transformer结构对于AST序列并不友好,可能是因为顺序特征AST不适合用非顺序结构的Transformer编码器提取隐式结构信息,这一猜想也得到了部分研究人员[12,14]的证实。因此,在本文中我们采用三个独立的编码器分别提取不同的语义特征,再使用交叉注意力层[20]三个语义特征进行融合。未来的工作中,我们将探索使用其他类型的编码器进行类似研究。
为了进一步学习源代码片段的多种语义特征,本文采用交叉注意力块(Cross attention blocks)[20]融合编码器初步提取的三种语义特征。交叉注意力允许一种语义特征接收与另一种语义特征之间关联的细粒度关系。参照细粒度关系,两种不同语义特征应该关注其互相对应的部分(例如,实现大写转换的源代码片段关注的应该是API 文档中“toUpperCase”的描述信息),这意味着在交叉注意力模块中,两种不同的语义特征相互补充和增强,反之亦然。融合组件如图4所示。
图4 融合组件
定义Querys为Qe=hCodeWQe,Keys为Ka=hAPIWKa和Values为Va=hAPIWVa,同时WQc∈m×dmodel,WKa∈n×dmodel,WVa∈n×dmodel表示权重,m、n和dmodel分别表示输入序列的长度和维度。那么,利用编码器初步提取API文档语义特征增强文本语义特征用式(5)表示(为了方便表示,用e、p和a分别表示文本、AST结构和API文档)。
(5)
其中,CAa→e表示利用特征a增强特征e的单头交叉注意力计算。同样的,我们也能够得到利用特征p增强特征e的单头交叉注意力,只需要把Values和Keys的来源替换成特征p。
如式(5)所示,Querys来源于目标语义特征e,而Keys和Values来自其他语义特征(a或p)。当Cross attention堆叠为D层时,第1层到第D层的前馈计算如式(6)(利用a增强e)所示。
(6)
为了捕获更深层次的三种语义聚合后的内部特征,利用线性层将交叉注意力聚合得到的两个特征向量拼接后进行降维,得到最终包含丰富代码语义的特征向量。
(7)
(8)
本文提出的方法在公开可用的数据集上进行了充分实验,在第3.1节介绍实验需要的数据集,在第3.2节介绍实验中参数设置,在第3.3节中介绍基线和评价指标,在第3.4节进行实验,并对结果进行分析。
本文使用Github上共享的真实数据集(2)https://github.com/wasiahmad/NeuralCodeSum/tree/ma-ster/data/java进行验证,该数据集从2015年至2016年期间Github平台上评分较高的项目中提取。每个数据样本组成〈源代码片段,摘要〉数据对。该数据集的详细统计数据如表1所示。
表1 实验数据集的统计数据
本文提出方法中超参数的设置如下,基于源代码的三种语义表征的初始维度都设置为512,训练和测试的批大小分别设置为32和64。我们训练的最高次数设置为150次,如果连续20次迭代的验证性能没有得到改善,则提前结束训练。优化器使用Adam优化器[22]训练模型,丢失率和初始学习率分别设置为0.2和10-4。
本文使用代码摘要任务中常用的BLEU[23]、METEOR[24]和ROUGE-L[25]评价指标作为本次实验的评估方法,并使用五种基线方法验证本文提出方法的有效性。各基线方法详细信息如下:
CODE-NNIyer等人[5]首次提出联合注意力机制的长短时记忆网络(Long Short Term Memory,LSTM)网络为C#代码片段和SQL查询语句生成摘要描述。
Dual modelWei等人[7]将代码生成和摘要生成视为对偶任务,构建两个独立的联合注意力机制的Seq2Seq网络,利用对偶约束在损失函数中加入正则化项进行联合训练,同时提高两个任务的性能。
Co-ConvGNNs该方法[8]在学习源代码文本信息的同时引入额外的两层卷积图神经网络 (Convolutional Graph Neural Networks ,ConvGNNs)对源代码片段的AST结构进行编码,并使用密集层生成摘要序列。
TransAhmad等人[12]基于Transformer模型实现对源代码输入序列标记的并行编码,并通过引入复制机制和相对位置优化模型,与之前基于RNN方法相比,性能得到较大提升。
Code-Trans该方法[13]提出一种基于Transformer结构的多语言(Java、Python等)模型,通过将源代码标记链接到 AST 节点并使用节点之间的成对距离(例如,最短路径,PPR)表示AST。
我们将本文提出方法的性能与五种基线方法进行比较,实验结果如表2所示(部分基线方法的结果报告来自文献[8])。我们可以观察到,本文所提方法在数据集上优于所有基线方法。基线中,CODE-NN性能最低,与之相比本文方法的BLEU等三个评价指标得分分别高17.99%、15.01%和14.61%。基线方法中,Trans在数据集上表现最佳,然而本文所提方法的BLEU评价指标得分从Trans的44.34%、25.95%和54.25%提升到45.59%、27.62%和55.71%,表明在Trans中引入AST和API语义特征是有效的。而Dual model和Co-ConvGNN基线方法的结果表明,ConvGNNs层可能在建模源代码结构信息方面并没有明显的优势。
表2 与基线方法进行比较 (单位:%)
其次,为了分析摘要任务中Transformer结构的优势,我们将基线方法分为两类(参照模型结构划分为基于RNN和基于Transformer),并分别选择两类中最好的且仅利用源代码文本信息的基线Dual model和Trans。结果表明,与基于RNN的Dual model相比,Trans表现更好。这表明Transformer结构能够更好地建模代码片段标记的长距离依赖关系。但同样基于Transformer结构的Code-Trans表现较差,原因可能是该方法的设计目的是基于函数体预测方法名称任务,作用于摘要生成任务存在限制。
综上所述,本文提出的基于Transformer结构的多编码器(文本、AST和外部API)方法,能够更全面地学习代码的语义特征,提高生成摘要的可读性和准确性。
3.5.1 AST对性能的影响
为了研究AST结构特征对方法性能的影响,本文设计了一组消融实验。结果如表3所示。其中,.*表示采用基线方法[12]进行实验,.&.连接方法输入序列(排在首位的为融合的目标序列),在基线方法[12]中引入 AST,BLEU指标得分反而下降0.77。分析产生这个结果的原因可能是 Transformer结构并不适合捕获非顺序结构的AST。而本文使用RvNN建模AST,与基线相比,BLEU指标得分提升了1.95。为了进一步研究学习全局结构对方法性能的影响,设计了Code&AST-&API方法,利用拼接替换第二阶段聚合所有子树向量的递归过程。结果表明,与Code&AST&API相比,性能有所下降,表明学习AST的全局结构信息是有效的。另外,表3中还发现三种语义特征融合过程中,将源代码的文本序列作为目标序列,使用 AST 和 API文档增强文本序列,比选择其他两种序列作为目标序列在实验中性能更好。
表3 不同编码器的消融实验结果 (单位:%)
3.5.2 API文档对性能的影响
为了验证外部资源API文档语义特征对摘要任务性能的影响,在设计的消融实验中,首先将提取的所有API文档输入到基线方法[12]中,表3和表4结果显示,性能并没有提升,与预期结果明显不符。如表4所示,我们通过改变API文档数量对性能的影响来分析原因,可以发现随着数量增加,性能得到提升。当API 文档数量为3时,利用BM25算法(处理后的方法名作为查询词)对提取的API文档进行相关性排序,截取出最相关的文档描述信息增强摘要任务,得到最优的性能,而未排序的方法unsorted-3仅仅只是优于基线,分析原因可能是因为随着输入的API文档数量增加,API文档序列的长度远超过摘要的长度,引入的噪声也就越大。因此,引入外部知识API文档是否有效的关键是检索出合适的与源代码片段相关的有限个API文档。
表4 API文档数量的消融实验结果 (单位:%)
3.5.3 傅里叶变换层(Fourier)对性能的影响
我们通过改变编码器中傅里叶变换层的数量来验证傅里叶变换对方法性能以及参数的影响。为了使结果更明显,该组实验方法采用基线模型Trans[12](仅包含文本编码器,自注意力层设置为6层)。为了验证傅里叶变换的有效性,我们通过实验获得了表5中方法的训练时间,可以观察到,由于傅里叶变换层不包含可学习参数,在部分精度损失的基础上,与基线方法Trans相比,Fourier方法中编码器的参数数量减少了25.2兆,每个epoch的平均训练时间(所有的训练数据样本送入模型中,完成了一次前向计算+反向传播的过程所需的平均时间)减少了115.53s,由此可见,编码器中利用傅里叶变换层替换自注意力层,能够降低模型的计算成本和内存占用量。为了找到性能和训练效率之间的平衡点,实验中通过改变编码器中傅里叶变换层的数量发现,保留最后两个自注意力层(表5中方法Attn+Fourier4),模型在计算时间、内存占用和准确性之间提供了良好的折衷。理论上而言,自注意力是一种自相关操作,计算序列标记的自相关性,傅里叶变换通过计算卷积后再做乘积,也能进行自相关操作,充分融合输入序列中各个标记的信息。
表5 傅里叶变换层数量的消融实验结果
注: Trans方法表示编码器核心组件为自注意力层的基线方法,编码器中自注意力层设置为6层。Fourier方法表示将Trans中的自注意力层全部替换为傅里叶变换Attn+Fourier5和Attn+Fourier4方法分别表示将Trans中的前5和前4个自注意力层替换成傅里叶变换层我们首次使用傅里叶变换替换自注意力层,并应用在代码摘要生成任务中,取得了较好的效果。同时我们也注意到,还有一些工作使用改进自注意力机制来提升效率,解决长文本依赖问题[27-28]。因为这些方法并未特别应用于代码摘要中,因此本文并未对其进行比较。在未来工作中,我们将进一步探索其他有效的自注意力机制改进方法在代码摘要中的应用,希望能够进一步提升摘要任务的性能和降低模型训练的成本。
除此之外,我们还进行了一项消融研究,评估模型大小和和层数对性能的影响。从实验中发现,随着输入序列的维数和模型层数的增加,各项指标得到了不同程度的提高。例如,本文中三种输入的维度从初始的512增加到 768,BLEU、METEOR 和 ROUGE-L 指标分别增加了 1.2、1.3和 0.8。
源代码和使用自然语言描述的摘要序列之间的语义差距是生成代码摘要的一个主要挑战,捕获代码中丰富的语义信息成为该任务的关键步骤。本文提出的基于Transformer的方法,将该任务建模为生成任务,搭建源代码和摘要之间的桥梁,使模型的鲁棒性更强。在表6中,本文与基线方法分别生成不同长度的摘要序列进行对比, 以此验证本文提出方法的有效性。此外,我们观察到,当使用 API 文档语义特征时,在API文档中出现的单词能够提升生成摘要的可读性。
表6 本文提出的方法与部分基线对比生成代码摘要示例
现有的基于Transformer结构的代码摘要自动生成方法,大都只编码源代码的文本和结构化语义特征,忽略了与其紧密相关的外部API文档语义特征。针对这一问题,本文基于Transformer结构,构建多个编码器,分别学习源代码的文本和结构化语义特征,同时融入外部API文档语义特征,给代码摘要任务提供足够的语义信息,并采用傅里叶变换代替编码器中核心的自注意力层,通过线性变换降低编码器中计算的成本和内存占用量。实验结果表明,本文提出方法的性能优于基线。在未来的工作中,我们拟开展以下三方面的研究: ①进一步处理Python数据集中对应API文档信息以及引入源代码其他语义特征,验证本文提出方法的有效性;②探索其他语义特征提取和交互的方法,充分学习源代码中丰富的语义特征;③尝试其他有效的自注意力机制改进方法在代码摘要任务中的应用,希望能够在进一步提升该任务的性能的同时降低模型训练成本。