APP下载

融合语法和语义的代码注释生成方法

2023-11-10王瀚森陈铁明季白杨

小型微型计算机系统 2023年11期
关键词:语法结构源代码代码

王瀚森,王 婷,陈铁明,季白杨

(浙江工业大学 计算机科学与技术学院,杭州 310023)

1 引 言

伴随互联网的发展,软件数量、规模都急剧增大,代码开发和维护工作中的程序理解问题已成为一大难题.据统计,在软件开发周期中,有59%的工作用于理解和维护相关软件源代码[1],相当耗时耗力,影响工作效率.代码的维护和复用往往需要对代码有透彻的理解,程序理解是任何代码复用和修改的先决条件,代码注释对于辅助程序员理解代码含义是十分重要的[2].现如今,开源代码仓库的实际项目中存在大量代码没有含义清晰明确的注释,这导致软件开发人员在浏览代码时,不得不耗费大量时间去理解代码的功能,极大影响了开源资源的复用.因此,代码注释生成技术需要更加深入的研究.通过深度学习构建编程语言到自然语言注释的映射模型,在大规模数据库上学习代码的抽象特征表示,自动生成含义清晰准确的代码注释,不仅可以省去开发人员写注释的时间,同时可以用来帮助理解代码,具有相当大的应用价值.

早期方法是通过预定义人工模板生成注释.基于预定义的启发式规则从给定源代码中提取关键字,添加到人工模板中,合成基于自然语言描述的注释[3-6].此外,基于信息检索的方法也广泛应用于代码注释生成技术[7-13].这类方法基于信息检索模型,通过识别出目标模块内的关键单词,尝试从软件仓库中寻找与目标模块相似的代码段,并利用这些相似代码片段的摘要、注释或讨论生成代码注释.深度学习的最新进展催生了新一代数据驱动的代码注释生成技术.这种技术很大程度上是受到神经机器翻译(Neural Machine Translation,NMT)领域的启发,使用端到端的神经网络生成注释.

相较于结构化较弱的自然语言,编程语言是形式化语言,用它们编写的源代码具有明确的语法结构[14].源代码具有语法结构信息和顺序语义信息.将结构化的代码应用到自然语言的神经机器翻译方法中,难免会丢失编程语言结构特性包含的信息.要理解代码内容并生成含义准确且可读性强的代码注释,这两部分信息是必不可少的.过往研究大多只利用其中一种信息,如何将编程语言的语义信息与语法结构信息进行结合,还需要更深入的研究.

针对上述问题,本文提出一种融合语法结构信息和顺序语义信息的代码注释生成方法,通过将代码片段转化为AST,并对AST节点类型进行控制,简化AST结构.应用多路树-LSTM[15]以获得树形结构的节点表示.再结合源代码序列表示的语义信息,输入Transformer[16],以两种类型的代码信息为指导训练注释生成模型.为了评估本文方法的有效性,本文在大型Java数据集上进行了实验[17].实验结果表明,本文方法生成的代码注释在BLEU[18]、METEOR[19]和ROUGE[20]评价指标上的得分高于对照模型.

2 相关工作

随着深度学习技术的发展,基于端到端神经网络模型的代码注释生成研究不断深入.这一类方法将代码注释自动生成任务建模为神经机器翻译问题.Iyer等[21]首次将深度学习应用到代码摘要研究中,并提出CODE-NN方法,使用LSTM和注意力机制,生成C#代码片段和SQL查询语句的描述.Allamanis等[22]使用CNN和注意力机制作为编码器,解码器使用GRU,通过卷积操作将源代码总结为简短的、描述性的函数名称.Wei等[23]提出一种对偶学习的方法,通过代码摘要和代码生成两个任务之间的对偶性训练模型.针对RNN模型对于长代码序列进行建模时难以捕获长距离依赖问题,Ahmad等[24]利用Transformer模型来生成代码注释,并利用复制注意力机制和相对位置编码,有效捕获长距离依赖(long-range dependencies).

除以上方法外,近期研究人员越发关注代码的语法结构信息,开始将研究重心转移到利用AST表示源代码.其中最具代表性的方法是Hu等[25]提出的DeepCom.通过AST表示源代码,以保留源代码的语法结构信息,并提出一种AST遍历方法SBT(Structure-base Traversal),将AST遍历成序列结构.随后,他们基于DeepCom,进一步提出Hybrid-DeepCom[17]方法,结合了源代码语义信息和AST提供的语法结构信息,并通过使用分隔标识符的方法缓解OOV(out of vocabulary,即需要生成的注释可能含有未在代码中出现的单词)问题.然而这种将AST转化为序列进行编码的方式不可避免地丢失了源代码的语法结构信息.因此,一些工作尝试使用树形结构编码输入.包括Mou等[26]设计的基于树的卷积神经网络(TBCNNs)、Shido等[15]提出的Tree-LSTM,以及Harer等[27]提出的Tree-Transformer.

由于AST节点数量多,导致训练开销大,加重模型训练负担.为了平衡语法结构信息的保留和降低训练难度,Chen等[28]提出了一种将长代码段分割为短AST路径的方法,从而缩短输入长度.Wan等[29]将AST转换为二叉树,以丢失部分信息为代价减少训练开销.Alon等[30]将代码片段表示为AST中的组合路径集.Zhang等[31]将抽象语法树拆分成多个规模更小的语句树,每次只关注一条语句,降低训练难度.

总体来看,AST在代码注释自动生成技术中的应用为这一方向的研究提供了新的技术途径.其优势在于可以保留源代码的语法结构信息,将源代码的语法结构信息和语义信息结合,生成高质量的代码注释.

3 源代码的结构化表示

本节将着重介绍源代码的抽象语法结构化表示.编程语言相较于自然语言,不仅包含语义信息,还有其独特的语法结构信息.通常情况下,可以采用抽象语法树(AST)捕捉源代码的语法结构信息.AST是一种用于表示源代码语法的抽象树形结构.AST的叶节点表示代码中的标识符或变量值,非叶节点表示语言的语法和层次结构.图1显示了一个Java代码片段及其AST的示例.

图1 Java代码片段和对应的AST示例Fig.1 Example of Java code snippet and the abstract syntax tree

3.1 简化AST

现有研究在利用抽象语法树提取源代码语法结构信息时,通常将AST经过特殊的遍历方法转化为顺序序列,旨在提取语法过程中尽可能地保留源代码的结构信息.由于完整的AST深度大且结构复杂,通过此类基于结构的遍历方式生成的序列通常包含冗余语义信息,导致训练困难.为了更有效地捕获代码的结构信息,本文提出一种依据节点类型的AST简化方法,目的是简化AST结构,减小其编码难度,提供简洁的语法结构关系来指导训练.然后通过Tree-LSTM模型提取语法结构信息.

控制流图(Control Flow Gragh,CFG)[32]使用图的表示方式,记录代码的结构和执行路径.控制流图中的每个顶点对应一个代码区块.本文希望从AST中删除不影响控制流图结构的节点.由源代码转化成的AST,节点类型大致包括4类:基本语句节点、调用式节点、表达式节点、和不包含在上述类型中的其他类型节点(主要包括声明式节点等).其中,基本语句节点定义了代码段的层次结构,例如分支结构,循环结构等.调用式节点通常决定程序流程走向,属于影响代码结构的部分.而表达式节点,声明式节点和其他类型节点通常不会单独构成控制流图中的顶点,本文认为其不对代码结构起决定性影响,属于冗余的语义信息,此类型往往位于AST的叶子节点处.

图2给出了两个源代码及其注释的示例.其中阴影部分是生成注释时重点关注的部分.可以看出,在样例1中,其关注点在分支结构和调用式节点paths.add(s)上.样例2中,生成注释的关注点主要在方法名和参数上,方法体中大量声明式节点和表达式节点是被忽略的.

图2 源代码及其注释示例Fig.2 Example of the source code and comment

通过移除声明式,表达式和其他类型的节点,使树编码组件专注对源代码层次结构的学习而不去关注语义细节.这些节点所包含的语义信息则通过源代码序列的形式在Transformer中学习,在此删除这些节点并不会带来语义信息的缺失.形式化地,将某个ASTt的节点集合定义为V(t),G(t)⊆V(t)代表t中决定语法结构的节点,主要包括基本语句节点和调用式节点;S(t)⊆V(t)代表t中涵盖语义信息的节点,主要包括声明式节点和部分表达式节点.这部分节点在代码库中大量重复,直接影响了AST的规模并且干扰语法结构的训练.通过算法1将这部分节点从AST中删除获得简化的AST.

算法1.简化AST

输入:ASTtwith node setV(t)

输出:G(t)

1.forv←V(t) do

2.if Children(v)inS(t) then

3. Children(v)←Children(Children(v));

4.returnt;

3.2 树编码组件

Tree-LSTM[33]是一种处理树形结构数据的神经网络框架,是对标准LSTM[34]在树形结构上的泛化.相较于标准LSTM的每个时间步接受一个隐藏状态向量,Tree-LSTM的每个父节点可以接受多个子节点隐藏状态向量,并将信息从叶子传播到根.考虑到多路树-LSTM可以处理多分支且节点有序的树形结构,符合AST的结构特点,保证在处理任意数量的子节点时,同时考虑子节点间的顺序交互.本文通过多路树-LSTM对AST进行编码,将每个AST转化为向量表示,用以表示源代码的语法结构信息.

对于给定输入AST的任意节点向量xj,C(j)表示其子节点集合,nj表示C(j)集合数量;其第k个子节点的隐藏状态为hjk,cell状态为cjk,多路树-LSTM的隐藏状态hj计算方式如公式(1)~公式(10)所示:

(1)

(2)

(3)

(4)

(5)

(6)

(7)

(8)

cj=∑k∈C(j)ck⊙fjk+ij⊙uj

(9)

hj=oj⊙tanh(cj)

(10)

给定一个ASTt,t包含一组节点G(t).将G(t)中的每个节点转化成type_value格式的唯一标记,作为输入嵌入到固定维度的向量中.例如,节点类型为BasicType,节点值为int的节点表示为BasicType_int.对于所有非叶节点j,该节点输出的隐藏向量hj由输入xj和该节点的所有子节点的隐藏向量hC(j)通过多路树-LSTM生成,如公式(11)所示:

hj=F(xj,hC(j))

(11)

对于不包含任何子节点的叶子节点,本文选择创建虚拟的子节点并随机初始化,方便计算其隐藏向量.最终,获得根节点的输出作为表示ASTt的语法结构向量.

4 基于Transformer的融合方法

本文通过Transformer神经网络对源代码的语法结构信息和顺序语义信息进行融合.由于AST的深层特性,传统的Seq2Seq模型在捕获长距离依赖关系的表现上不如Transformer神经网络[16].Transformer的自注意力机制是纯数据驱动的,并且Transformer的位置编码可以学习输入令牌之间的依赖关系,因此可以将代码的语法结构信息和顺序语义信息融合到Transformer编码器的输入中.

图3 模型框架Fig.3 Framework of model

(12)

此外,源代码中标记的位置和排列顺序是非常重要的信息,他们不仅是代码语法结构的组成部分,同时表达重要的语义信息.由于Transformer完全采用注意力机制,丢失了词序信息,因此需要为每个嵌入向量加入位置编码(Positional Encoding).位置编码遵循模型学习到的特定模式,将位置编码加入到嵌入向量中,有助于捕获长期依赖关系,更好地对标记间相对关系建模[35].

4.1 编码器

Transformer遵循编码器解码器结构.编码组件由一组堆叠的编码器组成,所有编码器结构相同,但不会共享参数.编码器由两个子层组成,即多头自注意力层(Multi-Head Self Attention)和前馈神经网络层.编码器接收到的句子首先通过自注意力子层,随着模型处理输入序列中的每个标记,自注意力会关注整个输入序列中的标记,帮助模型更好地理解当前标记和其他标记的对应关系.自注意力的计算方法如公式(13)所示:

(13)

其中Q,K,V分别由权重矩阵WQ,WK,WV和输入向量X相乘获得.此外,dk作为比例因子防止过大值通过softmax函数导致的梯度消失问题.

Transformer的自注意力是多头的,多头自注意力机制允许模型在不同的表示子空间里学习相关的信息.每个自注意力头定义为headi=Attention(Qi,Ki,Vi),然后将h个自注意力头拼接,再经过一次线性变换获得多头自注意力的输出,如公式(14)所示:

MultiHead(Q,K,V)=Concat(head1,…,headh)Wo

(14)

每个自注意力子层的输出会传递到前馈神经网络层.此外,在编码器的每个子层后都应用了残差连接并伴随层归一化步骤[36],用于消除随着层数加深带来的信息损失问题.

4.2 解码器

Transformer解码器负责根据先前编码器的输出和当前编码状态生成代码注释.解码器结构与编码器相似,包含两个多头自注意力层.第1层的自注意力机制采用了Masked操作,使自注意力层只被允许处理输出序列中更靠前的那些位置.第2层为编码-解码自注意力层,根据先前编码器输出的一个包含键向量K,值向量V的注意力向量集C,从上层解码器得到的查询向量Q,帮助解码器关注输入序列.随后,解码器组输出的向量通过一个全连接层,投射到单词表维度的向量中.最终通过Softmax层生成对应位置注释单词的概率.

5 实 验

本节将从实验设置、评价方法以及实验结果分析3方面展开论述.

5.1 实验设置

本文使用大规模Java数据集进行实验[17].该数据集搜集自GitHub,是目前代码注释生成方向使用最为频繁的数据集[37].该数据集由一对Java语言编写的方法源代码和JavaDoc文档注释组成.以90∶5∶5的比例将数据集划分为训练集,验证集和测试集.本文从验证集和测试集中删除了出现在训练集中的数据.将单词表大小限制在30000个,超过限制的低频标记采用代替.

将数据集源代码中的数字和字符串替换为特殊标记.为了减少OOV问题的影响,将源代码中大量出现的标识符通过驼峰命名法或下划线命名法分割为多个标记.通过工具Javalang解析源代码数据生成AST.最终数据集的统计数据如表1所示.

表1 数据集统计Table 1 Statistics of datasets

本文将数据集中源代码长度上限设置为100,摘要的最大长度设置为30.使用特殊标记填充短序列,将较长序列剪切.使用Adam优化器训练模型参数,并将学习率设为10-4,批处理大小设置为128.词嵌入维度和隐藏状态维度设为512;对于涉及到Transformer的部分,将多头数设置为8.使用Adam优化器训练模型参数,对模型进行100个epoch的训练.在测试阶段,使用beam_size大小为5的集束搜索.

本文实验基于pyhton3.6和TensorFlow框架实现,在Linux服务器(Ubuntu16.04,Intel®CoreTM i9-10900X CPU @ 3.70GHz,GeForce RTX 2080 Ti×4 GPU)上运行.

5.2 实验评价指标

为了验证代码注释生成方法的有效性,文本使用在神经机器翻译领域内应用最广泛的评价指标BLEU[18]、METEOR[19]和ROUGE[20]指导实验.这些指标可以评估预测文本(即代码注释生成方法生成的文本)和参考文本(即源数据集中手工方式生成的文本)的相似程度.

BLEU指标用于比较预测文本和参考文本中n元词组的重合程度,随着n增大,BLEU指标可以进一步衡量文本流畅性,本文使用BLEU-4作为实验指标.BLEU计算公式如式(15)所示:

(15)

其中BP代表预测文本较短的惩罚因子,wn是正权重,Pn是修改后的n-gram精度的几何平均值.虽然有短文本惩罚因子,但从整体上来看,BLEU指标更偏向于较短的候选文本[19].因此本文还使用了METEOR和ROUGE作为评价指标,以弥补BLEU指标的不足.

METEOR指标引入了同义词匹配,通过句子级别的相似度来评估翻译效果.METEOR可通过公式(16)计算:

METEOR=(1-Pen)Fmean

(16)

其中Pen是惩罚系数,Fmean是精确率和召回率的加权调和平均.分别由公式(17)、公式(18)计算:

(17)

(18)

其中γ、θ和α均为用于评价的默认参数.ch是块(候选文本和参考文本能对齐,并且在排列上连续的单词称作一个块)的数量,m是匹配项的数量.Pm和Rm分别代表精确率和召回率.

ROUGE是一种基于召回率的相似性度量指标,可以考察候选文本的充分性.本文选用ROUGE-L作为评价指标,通过公式(19)~公式(22)计算.

(19)

(20)

(21)

(22)

其中,LCS(X,Y)代表预测文本和参考文本的最大长度公共子序列,m和n分别是候选文本和参考文本的长度.

5.3 实验结果分析

本小节将全面评估本文所提出模型的实验结果,分别从两个方面评价模型性能:1)通过与以往先进的代码注释生成模型在相同数据集上进行的对比实验;2)探究了本文提出的AST简化方法和树编码组件对模型的影响.最后,本文从数据集中选取了3个样例说明本文模型的有效性.

5.3.1 对比实验

本文选取了其他3种以往先进的基于神经机器翻译的基线方法与本文模型进行对比实验,实验结果如表2所示.表2中所涉及的对比模型如下.

表2 对比实验结果Table 2 Comparison of experimental results

CodeNN[21]:在编码和解码时均基于LSTM和注意力机制.

Hybrid-DeepCom[17]:通过将AST序列化,使用GRU构建了一个抽象语法树编码器,另一个GRU构建了源代码标记编码器,使用注意力机制融合两部分信息.考虑了源代码的部分结构信息.

Multi-way Tree-LSTM[15]:提出扩展的Tree-LSTM直接对AST编码,通过Tree2Seq结构的模型生成注释.

根据表2,可以发现本文方法优于3种对照方法.本文提出的融合源代码语法结构信息和顺序语义信息的方法相较于Hybrid-DeepCom,在BLEU-4指标上提高了6.86,METEOR指标提高了6.76,ROUGLE-L指标提高了9.92.Hybrid-DeepCom使用两个编码器,一个对源代码序列编码,另一个对抽象语法树经由SBT方法转化的序列编码.他们的方法优于单纯使用源代码序列的CodeNN,证明了源代码中这两部分信息对于模型理解源代码的重要性.但由于代码结构的复杂性,这种融合源代码语法结构和顺序语义的方式可能会丢失部分结构信息.此外,本文方法在3项指标上的得分均高于Multi-way Tree-LSTM.上述实验结果表明,本文所提出的融合源代码结构信息和语义信息的方法是有效的,优于其他对照方法,相较于只使用语义信息的方法提升巨大.本文模型在代码注释生成方向上有着更好的性能,在生成注释的质量上有明显提高.

本文还分析了代码注释生成模型的性能受源代码长度的影响.实验结果如图4所示.可以看到3种模型随着源代码序列变长,模型性能会逐渐下降,尤其是对于Seq2Seq结构的Hybrid-DeepCom,在源代码序列长度超过100后,BLEU-4评分显著降低.这是由于Seq2Seq模型将源代码信息编码为固定长度的向量,从而导致越长的序列会丢失越多的信息.Tree2Seq结构的Multi-way Tree-LSTM模型随着代码序列长度变长,波动同样剧烈,因为Tree2Seq对抽象语法树建模,而源代码序列的长度通常决定抽象语法树规模.由于本文所提出的AST简化方法可以缩小AST规模,且Transformer模型可以捕获长距离依赖信息,所以文本方法相对于Hybrid-DeepCom模型和Multi-way Tree-LSTM模型受源代码序列长度影响最小,并且即便在长序列下,BLEU-4的得分仍要高于另外两种模型.

图4 3种模型在不同代码长度下的性能表现Fig.4 Performance of the three models at different code lengths

5.3.2 消融实验

为了更细致地分析本文模型各组件的有效性,本文进行了相应的消融实验,以了解不同子模块对完整模型效果的影响.消融实验结果展示在表3.可以发现,树编码组件和简化AST都有助于模型效果的提升,但程度各不相同.具体地,删除树编码组件后,仅通过Transformer对源代码序列进行语义建模,在Transformer有效捕获长距离依赖的强大加持下,性能优于表2的CodeNN和Hybrid-DeepCom.这表明Transformer比LSTM或GRU更适合代码注释生成任务.加入树编码组件后,BLEU-4提升了3.66,METEOR和ROUGE-L分别提高了3.93和3.52.这表明融入源代码语法结构信息对于学习代码特征的重要性,缺乏这部分信息会对代码注释生成任务带来巨大影响.

表3 消融实验结果Table 3 Ablation study results

进一步地,本文发现将源代码序列转化为AST后,树深度相对较大,会导致编码困难,削弱了神经网络捕获复杂语义的能力.简化AST结构后,模型性能优于对完整AST进行编码,分别在BLEU-4、METEOR和ROUGE-L指标上提升了1.45,1.22,1.83.这表明由于AST结构过于复杂,某些冗余节点的存在对模型学习起阻碍效果.简化AST可以减少语法结构编码中和语义信息冗余的部分,简化训练.

为了探究为何简化AST对模型性能起到提升作用,以更直观地分析不同粒度下AST对模型性能的影响,本文探究了不同程度简化AST下的实验结果并进行了详细对比.将AST节点类型按照对代码的层次结构影响从高到低划分为基本语句节点,调用式节点,表达式节点和声明式节点.最简约版本只包含基本语句节点,其余3个版本根据节点类型优先级依次叠加.4个版本AST平均节点数分别为4.8,13.6,19.1,21.6.

图5给出了不同程度下简化AST的4个版本在BLUE-4、METEOR和ROUGE-L指标上度量方面的性能变化.横坐标中的a,b,c和d分别代表平均节点数为4.8,13.6,19.1和21.6的4个简化程度不同的版本.结果发现,模型性能随着AST平均长度的增加先升高后降低;过于简略的AST和过于复杂的AST都不会生成最佳的结果.在删除声明式节点和表达式节点后,平均长度为13.6的AST获得最高的BLEU-4和METEOR分数.实验结果表明,本文所提出的简化AST方法有效缩小了AST规模,简化AST可以提炼更加准确的语法结构信息,增大内容结构的层次性,从而提升注释质量.

图5 简化AST对模型性能的影响Fig.5 Impact of simplified AST on model performace

5.3.3 模型样例分析

图6给出了3个数据集中的实际例子.其中Reference表示相应源代码由人工标注的真实注释.阴影部分是生成注释重点关注的部分.可以看出,样例1中本文方法生成的注释可以清晰表示源代码的含义,其关注点在基本语句节点return和调用式节点_stream.read()上.样例2中可以看到注释的关注点在方法名以及分支结构中,并且本文方法生成的注释和参考注释含义一样.样例3中,生成注释的关注点主要在方法名和参数上,方法体中大量声明式节点和表达式节点在语法结构编码阶段是被忽略的,但仍可以在语义编码阶段被捕捉,生成几乎和参考注释相同的文本.此外,本文分析生成的注释在部分数据中得分较低的情况(如样例1),这是因为实际项目中编写注释时开发人员会参考方法上下文或加入自己的主观意识,但这在方法级别的注释生成任务中是难以实现的.不难看出,如果只关注独立存在的方法,本文方法生成的注释可以清晰地表达源代码含义.

图6 源代码,参考注释和生成注释示例Fig.6 Example of the source code,reference and the generated comments

6 结束语

本文提出一种同时学习源代码语法结构特征和顺序语义特征的代码注释生成方.本文设计了一种AST简化方法,缩小AST规模,通过基于AST的树编码组件学习代码结构信息,并在Transformer中将语法结构信息和语义信息融合.本文在大规模Java数据集上进行了全面实验.实验结果表明,本文方法在BLEU-4、METEOR和ROUGE-L指标上的得分均高于3种对照方法.

未来研究可以集成更大的语料库,并结合其他基于深度学习的方法,探索对语法结构信息和顺序语义信息更高效的融合方法,进而提高自动生成代码注释的质量.

猜你喜欢

语法结构源代码代码
人工智能下复杂软件源代码缺陷精准校正
基于TXL的源代码插桩技术研究
创世代码
创世代码
创世代码
创世代码
软件源代码非公知性司法鉴定方法探析
长沙方言中的特色词尾
浅析古代汉语的名词动用
培养阅读技巧,提高阅读能力