成思强,刘建勋,彭珍连,曹 奔
1.湖南科技大学 计算机科学与工程学院,湖南 湘潭 411201
2.湖南科技大学 服务计算与软件服务新技术湖南重点实验室,湖南 湘潭 411201
近些年来,随着软件工程的不断发展,大部分软件开发人员为了代码的进一步完善或二次开发,都愿意将源代码分享于开源社区。因此,开源社区上源代码数据逐步增长,进一步地促进了软件工程的持续发展。据相关文献研究,开源社区上已经存有大量的源代码片段,如Google 的代码库在2015 年时就已储存超过20 亿行代码,而2017年时Windows代码库大约有350万个代码文件。然而,源代码片段数量的激增也使得代码库的管理愈加困难[1],进而无法形成高效的大型代码库。如果不能对源代码进行有效的管理,不仅加重开源社区的管理任务,同时导致代码库体积臃肿、运行效率低、代码质量低等一系列问题。因此,如何快速地标注出代码所对应的功能,减少程序员开发和维护所需的时间,对软件工程的发展具有十分重要的意义。
为了提高源代码分类的效率,越来越多的研究人员将机器学习技术应用于代码任务的处理中,尤其是预训练模型的研究及应用[2-10]。在历史研究中,大型预训练模型,如BERT[2]、RoBERTa[3]、GPT[4]等,在多项NLP任务上的效果都取得了很大的提高。其中,BERT[2]的应用最为广泛,如QA问答系统[5]、信息检索[6]、文本分类[7]、数据增强[8]、序列标注[9]等研究。这些预训练模型通过自监督学习的方式,从大量无标签文本中学习源代码上下文中的特征信息。随着预训练模型在NLP 领域中的应用研究,研究者逐步将预训练模型引入到软件工程研究中。Feng 等人[10]提出CodeBERT 预训练模型,首次将BERT预训练模型引入到源代码处理中。同时使用Husain 等人[11]在2019 年提供的数据集进行预训练实验。实验结果表明,预训练模型在源代码预处理研究中的有效性。然而,据相关文献的研究和分析,目前没有研究者将预训练模型引入到代码分类任务中。
Hindle 等人[12]已经论证了程序语言和自然语言类似,例如,两者都是由单词构成且都存在上下文语义逻辑关系。然而,源代码具有其自身独有的特性。一方面,源代码相比自然语言更具有更强的逻辑结构。另一方面,源代码具有自定义的标志符,例如程序语言中包含的标点符号,这是自然语言所没有的。在传统的研究中,将源代码当作自然语言进行处理必然会造成特征信息的丢失,导致源代码表征准确率低。为了提高源代码特征信息的表征准确度,抽象语法树(abstract syntax tree,AST)被应用于源代码的预处理研究。例如,Mou等人[13]将源代码段转换为AST,通过提取AST内部节点之间的依赖关系,进而获得源代码的结构信息,而不是简单地根据代码的标志符序列进行语义信息提取,进而对源代码进行分类研究。谢文凯等人[14]将机器学习中的朴素贝叶斯应用于代码分类研究,利用代码片段的用途特征信息对Stack Overflow 网站中大量存在的代码片段进行分类研究。然而,这些使用文本卷积神经网络、基于抽象语法树的深度学习模型或传统的机器学习模型仍然存在三点局限性。
(1)使用文本卷积神经网络不能充分地提取源代码中的语义信息。
(2)将代码转换为抽象语法树的形式破坏了代码的整体性,导致特征提取时的准确度不高。
(3)使用朴素贝叶斯模型的代码分类效果差,且规则的制定往往是面向特定任务的,可迁移性差。
为了提高源代码特征信息的表征准确度,进而提高代码分类的有效性,本文第一次将CodeBERT预训练模型应用到代码分类研究中。同时,为了验证模型的分类效果,实验分析对比了另外两种BERT预训练模型。实验表明,预训练模型的使用能有效地提升代码分类任务的准确性。但仅采用预训练模型来进行代码分类任务时,实验结果较差。因此,对源代码需要结合预处理和微调,在预训练出来了后仍需要通过微调来适应代码分类任务。本文基于CodeBERT预训练模型构建了一个新的代码分类模型。为了进一步研究各个预训练模型能给代码分类带来什么影响,本文使用了BERT[2]、RoBERTa[3]、CodeBERT[10]三种预训练模型在代码分类任务上进行测试。
本文的主要以下两点贡献:
(1)首次将BERT预训练模型引入到代码分类任务中,在代码预处理上保留了代码的整体性,模型对源代码的特征提取相比于卷积神经网络要更充分,同时相比传统模型可迁移性更好。
(2)本文测试了对代码去常用词和标点符号对代码分类任务的影响,这是第一个进行此类实验的研究。
在本章中将介绍本文中使用的技术的基本知识,包括代码分类、代码表征和本文使用的预训练模型。
文本分类的目的是将文本分配到预定义的类别之中,这些分类工具逐步被研究者引入到软件工程的研究中。由于不同模型的代码处理的方式不同,因此可以按照代码的功能(或者其他特征属性)来对代码库进行分类管理。代码的分类管理能使代码库中的代码片段语义信息更加清晰、直观,更能够有效地提高软件开发人员对于源代码的利用效率,进而更好地对代码库进行维护和更新。例如,按功能类型对程序进行分类有利于管理大型项目,因为新产生的组件会自动标记到合适的数据库中。根据程序功能和编程语言对GitHub等网站上的源代码进行分类管理,促进软件代码复用。文章讨论了一些用C++语言编写的源代码进行分类的方法。其中,具有相同标签的程序代码表明其具有相类似的功能。然后,在对源代码进行代码分类、代码搜索[6,15]、代码克隆[16-17]等任务前,都需要先对源代码进行特征表征,再将特征信息表征结果送入相应的模型中进行训练学习。代码表征的准确性直接影响后续任务的效果,本文在1.2节对代码表征的研究进行讨论。
由于模型不能直接处理源代码,因此在模型学习之前需要对其进行特征信息的表征,表征成中间特征形式,例如向量。代码表征是通过相应的模型对源代码的语义语法信息进行表征,得到源代码特征向量,通过表征后的源代码向量将送入相应的模型进行特征学习。Hindle等人[12]已经论证了程序语言和自然语言类似,两者之间具备众多可供分析的统计属性,同时程序语言具有自然语言特定的特征和关键字[18],因此可以使用文本分类技术作为解决方案。然而,程序语言与自然语言又存在很大的语义鸿沟。例如,源代码的可执行性、形式和结构上与自然语言截然不同,因此对程序语言进行分析处理的挑战比自然语言更大。
现有的代码分析工作通常是从以下两个角度对代码进行分析[19]:(1)代码的静态分析;(2)代码的动态分析。其中代码的静态分析指基于代码的静态属性对代码进行特征提取,如代码的符号序列信息、API 调用序列信息、代码对应的抽象语法树以及控制流图中存在的结构信息。而代码的动态分析关注于代码运行过程中产生的中间结果,基于中间结果对代码任务进行分析。本文所研究的内容是静态情况下代码信息的抽取与表征,因此本节内容仅介绍代码静态分析。
传统的代码表征方法简单地使用自然语言处理领域中成熟的算法将代码转换为表征向量,并在其上应用机器学习算法完成代码分析。如使用TF-IDF算法将代码转换为与词频相关的代码向量表征,此类方法仅仅使用简单的词频统计信息,没有将代码中丰富的语义信息以及结构信息融入到代码向量中,因此所生成的代码向量质量较低。现如今,由于计算机计算能力的提升,代码分析领域已从传统的人工制定特征提取方法转向对代码使用深度学习技术进行特征提取[13,15,20]。与传统的使用人工制定的特征提取方法不同,后者对代码分析的效率要远高于人工分析。首先,基于深度学习的代码分析方法可以减少人工制定特征的时间,降低了对代码分析的成本。其次,基于深度学习的代码分析方法能够发现更多人工难以发现的高维特征,也能够从不同的视角获取代码之间联系。而预训练的提出更是打破了使用RNN 等神经网络的局限性,预训练模型通过一个或者多个任务来预先学习相对泛化的知识,再通过微调阶段利用学到具体的知识表示,来进行下游任务,这样在下游任务中,可以缓解数据饥饿的问题,提高了对小规模样本的特征提取的效果。
预训练首先在计算机视觉领域ImageNet[21]上取得了突破性的进展[22],人们发现底层的网络可以捕捉到边角弧线等基础特征,而高层网络捕捉到的特征更为复杂,与下游任务更相关。所以底层网络其实是可以在不同的任务间通用的,而高层网络则可以根据任务进行调整,这种方法后来也被用到了NLP领域。本文选择基于CodeBERT[10]预训练模型来进行代码的分类工作。而CodeBERT 又由BERT[2]和RoBERTa[3]模型演变而来,本文将依次介绍其中的原理。
1.3.1 BERT框架
如今,随着深度学习在自然语言中的应用不断扩大,各种神经网络模型被提出。其中,BERT[2](bidirectional encoder representations from transformers)成为其中最受欢迎的一种。在BERT 发布之前,RNN 或CNN 是大多NLP 任务的首选,代码分类任务同样也是使用基于RNN 或CNN 的框架,但RNN 或CNN 在训练中是按文本序列处理,这种方式对于模型并行化训练是一个障碍,这在BERT 框架中的多头自注意力机制中得到解决,得益于多头自注意机制可以提取序列中每个元素与其他元素之间的关系特征,BERT仅使用了自注意力来构建神经网络,虽然其他大规模的预训练模型例如ELMo[23]、GPT[4]等已经能够在各种NLP 任务中提升SOTA,但BERT[2]对预训练方法的创新仍然提高了在各领域上的成绩。BERT 整体是一个自编码语言模型(autoencoder LM),并且包含MLM(masked language modeling)和NSP(next sentence prediction)两种任务预训练方式。
BERT 模型的输入结构如图1 所示,模型的输入由令牌嵌入、句嵌入和位置嵌入三者结合构成,下面对三者进行详细的介绍。
图1 BERT输入Fig.1 BERT input
(1)令牌嵌入:在输入文本传递到token 嵌入层之前,首先对其进行token 化。另外,在序列的首端[CLS]和末端[SEP]处增加额外的tokens,作为分类任务的输入表示,并分割一对输入文本。
(2)段嵌入:BERT 能够解决包含文本分类的NLP任务。例如对两个文本在语义上是否相似进行分类。这对输入文本被简单地连接并输入到模型中,BERT 是通过段嵌入来对输入进行区分。
(3)位置嵌入:BERT将位置嵌入添加到编码层最底部的输入嵌入中。位置嵌入维度与段嵌入相同,因此可以对两者进行拼接。
BERT[2]是第一个使用无监督学习的方式来预训练NLP 模型的工作,但它只能使用纯文本语料库进行训练。BERT 模型和ELMo[23]有大不同,在之前的预训练模型(如word2vec[24]等)都会生成每个词的向量,这种类别的预训练模型属于基于特征的迁移学习。而近一两年提出的GPT[4]、BERT等都属于模型迁移。
BERT模型是将预训练模型和下游任务结合在一起的,也就是说在做下游任务时仍然是用BERT 模型,而且天然支持文本分类任务,在做文本分类任务时不需要对模型做修改。但BERT 也存在如预训练方式的提取效率不高、预训练的资源要求较高等问题。
随着BERT 预训练模型的提出,涌现出许多基于BERT框架的预训练模型来对不同的语言环境或任务进行特征提取。同时这些基于BERT 的模型通过改进预训练方式来对BERT的缺陷进行弥补。而CodeBERT的提出,则是将基于BERT预训练的方式引入到对代码分析的领域当中。
1.3.2 CodeBERT
由于BERT 框架具有相当的可迁移性,BERT 预训练模型在NLP领域取得了显著的成功,目前被广泛应用于诸多领域。因此,研究人员开始对BERT预训练模型在程序语言上的适用性进行研究。其中Feng 等人[10]提出CodeBERT预训练模型并取得非常好的效果,这也是首次将BERT预训练方式引入到代码表征的领域当中,CodeBERT 使用Husain 等人[11]在2019 年提供的数据集进行预训练,与NLP 中的预训练模型不同,CodeBERT使用多模态的代码数据进行训练,即代码和代码相对应的由自然语言组成的注释两者构成的双模态数据和仅代码的单模态数据,使用MLM 和RTD 两种方式对CodeBERT预训练模型进行训练,这两项预训练任务赋予了CodeBERT很强的泛化能力。由于卓越的性能,研究人员还将CodeBERT应用于跨语言代码搜索,他们用Python、Java、PHP、Javascript和Go等多种语言进行了预训练,然后在Ruby 等看不见的语言上对代码搜索模型进行了微调。结果表明,跨语言代码搜索比从头开始用单一语言进行训练取得了更好的效果。这进一步验证了代码搜索中迁移学习的有效性[25]。
CodeBERT 模型的整体训练流程如图2 所示,在模型的整体架构上,CodeBERT并未脱离BERT和RoBERTa的思想。和大多数基于BERT框架的工作类似,CodeBERT也使用了多层Ecoder 来构建模型。CodeBERT 使用的模型架构与BERT 和RoBERTa 基本一致,即都有12 层Ecoder编码层,每个编码层有12个自注意头,每个头的大小是64,隐藏尺寸为768,前馈层的内部隐藏尺寸为3 072,模型参数的总数为1.25 亿。而使用大型语料库来对预训练模型进行训练以提高预训练模型的表征效果也是参考RoBERTa 的方法。这也使得CodeBERT 能在代码表征领域取得突破性成果的原因。
图2 CodeBERT模型Fig.2 CodeBERT model
其中训练使用的MLM(maskd language modeling)任务具体为:给定一个代码片段的数据点x作为输入,为x选一组随机位置进行掩码Mx,然后用一个特殊的[mask]令牌替换所选位置。
RTD关于判别器θ的损失函数如下所示,其中δ是一个指示函数,是预测第i个单词为原始词概率的判别器。
最终训练的损失函数由MLM 和RTD 的损失函数构成。
本章将详细介绍CBBCC(CodeBERT-based code classification)模型。
整体模型如图3 所示。CBBCC 使用基于BERT 框架的CodeBERT 预训练模型来进行代码分类任务。第一个部分为代码预处理操作,其中包括将源代码去掉部分标点符号和常用词,预处理的目的是将源代码转换为Transformer输入的格式,代码段在清洗后变为一行精简的代码文本。模型的第二部分主要作用是对文本加入位置信息进行重构和词嵌入,从而得到一个初始向量。第三个部分由CodeBERT预训练模型构成,其中12层编码层在初始状态下为CodeBERT的训练参数,将经过词嵌入后形成的向量输入预训练模型当中进行微调,通过微调训练调整预训练模型中的参数,从而更充分地对代码的语义信息进行提取。代码经过特征提取后形成的向量送入Softmax分类模块来获得代码段对每个类别的概率,从而完成对代码进行分类的任务。
图3 CBBCC模型图Fig.3 CBBCC model
模型流程如图4 所示,CBBCC 模型在设置模型的训练类别k、微调轮数epoch 和每批输入的大小batch size 后开始运行,模型需要使用训练集进行微调实验,在进行k轮微调后,模型参数对代码分类任务的适应性得到进一步提高。微调完成后使用验证集对模型的性能进行检验,得出模型在代码分类任务上的准确率等参数后结束。
图4 模型流程图Fig.4 Model flow chart
CBBCC模型的算法如算法1所示,由于CBBCC是基于BERT 框架上,伪代码也与BERT 类似,其中4~11行为编码层中的多头自注意力机制和归一化方程,通过12行的softmax分类函数得到每个代码段在所有类别上的概率,再选取概率最高的类别为预测类别从而完成整个代码分类任务。
算法1Code Classification Learning
数据预处理是特征学习中的重要过程,将代码处理成更适合词嵌入和特征提取的形式,有效的数据预处理方式可以去除噪声,降低模型运行的复杂度等。由于神经网络模型训练需要数据规范化,需要把这两个特征的数据设定到一定维度内。
本文从代码数据集开始,将代码转换为Transformer能接受的输入方式。预处理主要分为两步,第一步如图5所示,使用WordPiece对代码分词,第二步对模型训练的清洗阶段如图6所示。
图5 WordPieceFig.5 WordPiece
图6 数据清洗Fig.6 Data washing
将代码拆分成各个单词后去掉部分标点符号等,用于每一个代码段,并执行以下操作:
(1)删除空行并将换行符替换为空格。
(2)将部分常用词删除。
(3)用占位符替换向量、数组和浮点常量,保留类型并删除所赋的值。
(4)替换图7出现的标点符号以外的标点符号并替换为空格,并在剩余标点符号前加入空格。
图7 保留的标点符号Fig.7 Reserved punctuation
本文使用以多层双向Transformer 为基础的BERT作为CBBCC 的模型架构。首先将代码进行预处理操作,将代码按照BERT 的输入方式进行重构,同时尽量减小重构导致的语义信息丢失。第二步进行词嵌入,将处理好的代码转换为包含位置信息的向量矩阵。第三步将由代码转换成的向量矩阵输入到由多层Transformer的编码层构成的预训练模型中,由预训练模型对代码特征进行提取。由于不同的预训练模型对代码特征提取的准确率不同,本文尝试了多种基于BERT的预训练模型来对代码进行特征提取。经过多次实验证明CodeBERT预训练模型在代码分类的任务中准确率最佳,进而本文使用CodeBERT 来作为模型的预训练模型。随后本文对CodeBERT 预训练模型进行针对代码分类任务的微调训练,以适应代码分类任务。
2.3.1 模型输入
CBBCC模型的输入结构由图8所示,模型与Code-BERT[10]和RoBERTa[2]模型的输入类似,CBBCC 的输入由词嵌入、位置嵌入两者结合构成,下面对输入方式进行详细的介绍。
图8 CBBCC模型输入Fig.8 CBBCC input
模型的输入分为两步,第一步对已经处理好的代码进行词嵌入,将词按照预训练模型提供的字典进行转换,将代码序列转换为标识符序列,如图9 所示。第二步将嵌入后的代码序列与对应的位置信息叠加,位置信息的叠加能使得模型更好地学习文本中的语义信息,从而提高表征的准确率。与传统的BERT模型相比,本文借鉴了ALBERT[5]的思路,同时考虑到代码段中并不含代码的注释信息,而代码文本中并没有特定的上下段关系,因此去掉了句嵌入的格式。在将输入文本传递到token嵌入层之前,首先对其进行token化。如图8所示,在tokens 的首端([CLS])和末端([SEP])处添加额外的tokens。例如,本文将void main char a int len 这段经过预处理的代码转化为[CLS],void,main,char,a,int,len,[SEP]来作为模型的输入。[CLS]是作为输入首端的特殊token,其最终隐藏表示可以用做分类或排序的聚合序列表示。在转换为tokens后,输入还需将tokens的位置信息一同嵌入。输入分两部分:
图9 词嵌入Fig.9 Word embedding
(1)令牌嵌入:在将输入文本传递到token嵌入层之前,首先对其进行token化。与BERT不同的是,由于取消了段嵌入,在令牌嵌入时也不使用[SEP]插入语句中,而仅作为结束符使用。这些tokens 的目的是作为分类任务的输入表示,并分别分隔一对输入文本。
(2)位置嵌入:与BERT将位置嵌入相同,本文将位置信息添加到编码层最底部的嵌入过程中。位置嵌入与令牌嵌入具有相同的维度,因此可以对两者进行叠加。具体公式如下:
2.3.2 特征提取
CBBCC 是由12 个相同的Transformer 编码层堆叠而成的,由这12 个编码层对输入的文本进行特征提取。本文将介绍Transformer编码层里的结构。每一层主要由多头自注意力块模块和全连接前馈神经网络组成,两个模块的输出都被送到残差网络,最后进行归一化处理。自注意力机制旨在捕捉序列中任何两个元素之间的逻辑语义和语法信息,通过计算注意力得分,可以测量两个任意元素之间的重要性。自注意力机制在计算的过程中衍生三个新的向量Q、K、V这三个向量都是由输入的x演化而来。并且,Q、K、V是用embedding向量与一个矩阵相乘获得的结果,并在训练中不断更新参数,如下方程所示:
多头注意力的输出由前馈网络进一步处理,表示为:
其中,x表示由多头注意力层处理的输入矩阵,W1和W2是两个可学习的参数,b1、b2是相应的偏差。使用自注意力、前馈神经网络和残差的网络具有更大的能力来捕获全局和局部信息,可以最大限度地避免梯度消失问题。基于BERT 的预训练模型采用已训练好的参数进行特征提取,并根据微调任务再对整个模型进行微调,从而更好地适应下游任务。
2.3.3 模型输出
CBBCC 模型输出有两种方式,第一种为自然语言的每个词的向量表示,第二种为输出[CLS]作为整个句子的表示,这种方法主要用与分类任务当中。本文使用第二种方式,用Softmax 函数对输出句子的向量计算每个类别的概率,选取概率最大的作为代码段预测的类别。
CBBCC 模型输出为代码段的整体特征向量,并将表征出的向量经Softmax 函数归一化后进行类别的划分,选择预测值最高的类别作为分类的结果,从而完成代码分类的任务。同时,在微调训练中通过代码分类任务也可以实现对编码层中的参数进行调整,使得模型能更适合代码分类的任务。
在代码类别预测的任务上,由于有多个类别,预测的类别可以通过以下方式获得:
如图10 所示,CBBCC 的损失函数主要是对代码段的分类任务进行微调,通过分类的效果来评估误差来优化预训练模型中已有的参数,使特征提取更准确。CBBCC 通过预训练模型和针对下游任务微调的方法,使得编码层学习到的表征既有token 级别信息,同时也可学习到整条语句的语义信息,使得模型能更充分、更准确地学习到有利于对代码进行分类的语义信息。
图10 模型微调Fig.10 Model fine-tuning
本文使用多分类交叉熵函数来当本文代码分类任务的损失函数。在实际使用中,常用逻辑回归模型去解决分类问题,当逻辑回归撞上平方损失,损失函数关于参数非凸。
所以,不是分类问题中不使用平方损失,而是逻辑回归不使用平方损失。而代码中的log_probs使用了对数,故而不使用平方损失,而使用多分类交叉熵损失函数。公式如下所示:
其中,n表示模型一次输入的batch中有n个样本,k表示任务分类数,以本文所做的代码分类任务中有104类来举例,本文实际使用的k值为104。式中,yij是真实标签,表示神经网络预测后归一化的结果,是一个属于0 到1 的k维数1 组,数组内k个数值相加得1,本文希望这个数组在正确类别位置的值能更接近1。
本文多次实验调整代码分类的设置参数,最终决定微调训练采用batch size 为128、epoch 为8、learn rate为2E-5来作为本文的模型微调参数。
为了验证模型在代码分类上的有效性,本文基于有监督的学习任务对BERT预训练模型进行微调,并以此对模型分析和评估。实验数据来自TBCNN[13]的POJ104代码分类数据集。如表1所示,POJ104代码分类数据集是根据教学编程开放判断(online judge,OJ)系统中的问题和答案组成的。OJ 系统上有大量编程问题,学生提交他们的源代码作为某个问题的解决方案,同时OJ系统将自动判断提交的源代码的正确性和有效性。数据集的标签是104个编程问题中的一个,具有相同标签的源代码意味着具有相同的功能。POJ104数据集共有52 000 个样本,为了使模型进行有效地学习和验证,按40 000 训练集、2 000 的验证集和10 000 的测试集的比例对数据集进行随机分割。
表1 POJ104代码分类数据集Table 1 POJ104Code classification dataset
为了对实验结果进行评价和分析,本文将用ACC、Recall、F1-measure 和MCC 四个指标来评估实验结果。其中使用了混淆矩阵中的TP、FN、FP、TN(如表2)。
表2 混淆矩阵Table 2 Confusion matrix
ACC:准确率是针对代码分类的预测结果而言,是分类任务最常见的评价指标。通常来说,准确率越高,分类器越好;
Recall:召回率表示的是样本中的正例中被预测正确的概率。主要由两部分组成,一部分是是把正类预测成正类(TP),另一部分就是把正类预测为负类(FN)。
F1:F1 值是统计学中用来衡量二分类模型准确率的一种指标,是准确率和召回率两者的综合,同时兼顾了分类模型的准确率和召回率。F1分数可以看作是模型准确率和召回率的一种加权平均,它的最大值是1,最小值是0。
MCC(Matthews correlation coefficient):MCC是测量分类性能的指标,该指标考虑了真阳性、真阴性、假阳性和假阴性,通常认为该指标是一个比较均衡的指标。MCC 是一个描述实际分类与预测分类之间的相关系数,它的取值范围为[-1,1],取值为1 时表示对受试对象的正确预测,取值为0时表示预测的结果比随机预测的结果差,-1 是指预测分类和实际分类完全不一致。
本文使用BERT、RoBERTa 和CodeBERT 这三种预训练模型作为训练代码片段的嵌入模型。微调任务的训练batch size大小设置为128,学习率为2E-2。所有的实验都是在一台拥有12 个2.4 GHz CPU 核心和GeForce RTX 2080 Ti GPU的服务器上进行的。
本文在OJ 数据集上进行了多次的实验,为了对比分析所提模型的有效性,文章采用TBCNN、ASTNN、CVRNN、TBCC四种模型进行对比分析。
TBCNN[13]:TBCNN 是第一个用于处理104 代码分类任务的深度学习模型。TBCNN设计了一组子树特征探测器,称为基于树的卷积核。通过抽象语法树(AST)来提取代码的结构信息,并采用动态池来收集树的不同部分的信息。在模型最后添加一个隐藏层和一个输出层,用于代码分类任务。
ASTNN[26]:对源代码片段构建AST 树,并将整个AST分割为小的语句树后输入进神经网络中。ASTNN设计了一个递归编码器的多路语句树来捕获语句级的词法和语法信息,并采用语句向量进行表征。ASTNN使用基于语句的序列使用双向门通循环单元(GRU[20]),利用语句的顺序自然性,最终获得整个代码片段的向量表示。
CVRNN[27]:CVRNN是一种基于卷积和循环神经网络的自动代码特征提取模型,借助AST提取源代码特征信息。为了缓解长序列代码段转为AST 后带来的梯度消失问题,CVRNN 对AST 进行切割,将一个AST 转换为多个子AST序列再输入进模型当中。该模型采用CNN提取代码中的结构信息,采用LSTM提取代码中的序列信息。
TBCC[28]:一种基于Transformer 的神经网络的新型模型。TBCC 借鉴了ASTNN 中的AST 切割的方法,将AST切成更小的子树。通过Transformer网络对AST子树进行特征提取,并将其应用于源代码分类研究。目前,TBCC 是在POJ104 数据集[13]上进行代码分类任务中表现最好的模型。
研究问题1预处理对代码分类的效果影响?
本文在RoBERTa 和BERT 两种预训练模型的基础上,对删减常用词和标点符号对代码分类的影响来进行测试,从而选取更适合代码分类的预处理方式。微调采用Batch Size=32、Epoch=5 和Learning Rate=2E-5,采用准确率作为评估指标。
研究问题2CBBCC在代码分类任务上是否具有最优效果?
基于POJ104 数据集[13]进行代码分类实验,并采用10次随机划分数据集进行交叉验证。在与其他模型比较的同时,也将BERT、RoBERTa、CodeBERT 预训练模型等基准模型与本文提出的CBBCC模型进行对比分析。为了评价实验结果,采用准确率、召回率、F1 值和MCC这四种评价指标进行分析。
研究问题3CBBCC中各模块对于分类任务的影响?
为了对CBBCC 模型中各模块对于分类任务的影响,本文将进行消融实验来评估代码分类任务中微调和预处理对模型的影响。
其中am为去掉所有标点符号,acw为去掉常用词,pm 为去掉部分标点符号,pcw 为去掉部分常用词,n 为不做任何改动。
实验结果如表3 所示,使用准确率来作为实验指标。本文发现,去除所有的标点符号会对分类结果产生负面影响,这也验证了上文的推断,标点符号在程序语言中也含有部分语义。而去除所有的重用词也会对代码分类产生负面影响,而代码中也含有部分产生噪声的无意义词影响模型对代码表征的准确率,进而本文选择去除部分常用词和部分标点符号来作为本文的模型输入。
表3 预处理实验结果Table 3 Results of pre-processing experiments 单位:%
在实验过程中,TBCNN、CVRNN、ASTNN和TBCC四种基准模型采用参考文献中的最优参数来进行实验,BERT、RoBERTa、CodeBERT 和CBBCC 四种预训练模型使用相同的参数进行训练。
从表4 的实验结果可以看出,最初始的BERT 预训练模型并没有达到一个最优的分类效果,但也达到90%以上。这可能是因为在BERT 预训练过程中的语料库较小、缺少与源代码之间的联系导致的。使用RoBERTa预训练模型在进行代码分类任务上获得了95%以上的准确率和召回率,在分类的准确率上比TBCNN 提高了1.3个百分点,同时也超过BERT预训练模型2.7个百分点。结果低于ASTNN 和TBCC,主要原因可能是RoBERTa预训练使用的数据集由自然语言构成,在预训练时并未消除自然语言与代码之间的语义鸿沟。
表4 代码分类任务实验结果Table 4 Results of code classification tasks 单位:%
CBBCC采用CodeBERT预训练模型进行微调的方式取得了最好的实验结果,在微调后代码分类在准确率上相比TBCC 超出1.1 个百分点,与ASTNN 相比在准确率上超出1.4 个百分点。证明CBBCC 模型在代码的特征提取上要比传统的卷积神经网络更充分。得益于CodeBERT 预训练模型使用了大型的代码语料库来进行预训练,语料库中包含有代码和代码对应的注释,从而达到在预训练过程中学习到两者之间的相关性,弥补了自然语言和代码之间的语义鸿沟,同时使用了针对代码分类的预处理方式进一步提高了代码分类的准确率。实验证明本文提出的CBBCC 模型在POJ104 数据集[13]上进行的代码分类任务各项指标均达到SOTA值。
CBBCC使用CodeBERT作为预训练模型并进行微调参数的调整实验,调整的参数为Epoch、Batch Size和Learning Rate,实验结果如图11所示。本文尝试了多种训练批次和多种Batch Size 来进行针对代码分类任务的微调训练。实验结果发现,当训练批次数在8次时分类效果最好,超过8次时由于模型过拟合导致分类效果下降。Batch Size更大时对分类结果产生正面的影响,但Batch Size过大会导致训练时占用显卡资源过多,综合考虑下本文采用Batch Size 为128。最终本文采用Batch Size=128和Epoch=8作为模型的参数。
图11 模型参数调整Fig.11 Model parameter adjustment
灾难性遗忘是迁移学习中的常见问题,由于本文使用了基于BERT框架的预训练模型来完成任务,因此减小灾难性遗忘也是值得进行的研究。CBBCC 对使用CodeBERT 预训练模型在代码分类任务中使用不同的学习率进行测试,同时使用Batch Size=128 和Epoch=8为实验参数。实验结果表明在较大的学习率下模型无法收敛,而过小的学习率导致微调效果不明显,最终选取2E-5来作为CBBCC的学习率。
在表5 中,nf 表示不进行微调,np 表示不进行预处理操作,f 和p 则表示进行微调和预处理操作。通过实验发现预处理对代码分类作用没有微调的作用明显,但也对结果产生正面影响。实验结果证明微调对实验结果影响相比预处理更大,且同时使用微调和预处理的方法效果最好。
表5 消融实验结果Table 5 Results of CBBCC ablation experiments
近年来代码表征领域不断发展[29],许多研究者在代码表征的领域中应用了神经网络模型。同时代码分类的领域当中开始使用神经网络模型来对代码进行特征提取后分类,例如根据源代码的功能对源代码进行分类[13],根据代码片段的用途进行分类[14],恶意代码分类[30],代码缺陷检测[31],恶意代码家族分类[32],代码语言分类[1],以及代码片段总结[33]。尽管所有这些研究都使用神经网络模型作为各种代码分类任务的方法,但他们并没有很好地对代码进行充分的特征提取。
代码表征在代码分类任务上的研究主要通过抽象语法树(abstract syntax tree,AST)进行表征。Mou等人[13]提出了一种基于树的卷积神经网络模型(TBCNN),该模型考虑到不同代码对应的AST之间并不相似,采用了“连续二叉树”的概念,直接在代码所对应的抽象语法树上进行卷积操作模型将代码段转换为AST,通过使用CNN 对AST 内部节点之间的依赖关系进行提取,从而获得代码的结构信息后分类,而不是简单地根据代码的标志符序列提取语义信息后对代码进行分类操作,且POJ104数据集(由C语言编写的104个任务的代码数据集)也是由Mou等人[13]创建,并且TBCNN在此数据集上进行的分类任务准确率能够达到94%。Lu 等人[34]从代码中提取数据流与函数调用信息,将其融合到抽象语法树中,从而将代码构建为一个包含丰富信息的图结构表示,在传统的GGNN 模型上引入了注意力机制,用于获得图中每个节点的重要程度,进而获得更具有区分度的代码表征向量,所生成的代码表征向量用于代码功能分类任务中。Phan 等人[35]提出了两种基于树的卷积神经网络TBCNN+SVM 和TBCNN+kNN,SVM 可以利用结构和语义AST 的信息,而kNN 算法可以提高分类的效果,Phan 等人还提出了修剪树的技术来细化AST 的数据。实验结果表明,修剪多余的AST 分支,不仅大大减少了执行时间,而且分类器的性能在准确性和执行时间方面都有明显的改善。Zhang 等人[26]通过借鉴TBCNN的工作,提出了ASTNN模型,ASTNN在TBCNN的基础上对AST 进行切割操作,得到代码的语句树。ASTNN 通过GRU 模型[20]在切割后的AST 上进行语义提取,获得了代码表征向量,这也表明ASTNN任然是一个以RNN为基础的神经网络模型。该表征向量被用于代码克隆检测和代码分类,代码克隆检测和代码分类的结果远超于TBCNN,在POJ104数据集[13]上的代码分类准确率达到了97.3%。但基于树的方法通常需要按照特定顺序将树全部遍历,其计算开销相较于基于代码序列的代码方法会有所上升。史志成等人[27]提出的CVRNN对代码分类任务上效果的提升并不大,分类准确率为94%,仅在同等参数的条件下高于ASTNN。Hua等人[28]提出了TBCC模型,一种用于代码表征的新型模型,TBCC模型基于Transformer的神经网络,借鉴了ASTNN中的AST切割的方法将AST切成更小的子树,通过Transformer网络将子树进行特征提取,以此来进行代码表征的任务,并对其进行代码分类。TBCC在POJ104数据集[13]上进行的代码分类任务上效果超过ASTNN。
Ugurel等人[1]应用机器学习方法将开源代码自动分类为11 个应用主题和10 种编程语言,利用特征提取器为每个程序的源代码文件进行向量表示。然后在这些特征向量上训练SVM 分类器,然后使用训练好的分类器来对代码进行分类。Alvares等人[36]使用词法分析、评分策略和遗传算法(GA)对代码进行分类,Alvares 等人将关键字提取并作为标记,通过计算给定代码文件的概率来确定属于哪种编程语言。Alreshedy 等人[37]提出了一种识别用21 种不同语言编写的代码片段的分类模型,首先他们将代码片段转换为数字特征向量,然后应用多项式朴素贝叶斯方法进行代码分类。谢文凯等人[14]使用了朴素贝叶斯的方法对Stack Overflow 等软件开发网站的问题及代码片段进行分类,将问题和代码总共分为4 种问题类型和8 种代码片段,计算每个词在不同类型下的出现概率,作为训练好的贝叶斯模型,以此实现对代码进行分类。卢喜东等人[30]恶意代码映射为无压缩的灰度图像,然后根据图像变换方法将图像变换为恒定大小的图像,使用方向梯度直方图提取图像的特征,最后提出一种基于深度森林的恶意代码分类方法,实验中选择不同家族的多个恶意代码样本进行分类。王晓萌等人[31]使用基于卷积神经网络在图像领域的多通道学习策略,融合word2vec[24]、fasttext[38]等词嵌套技术提出基于文本卷积神经网络(textCNN)的多类源代码缺陷检测方法,创建源代码的综合向量表征,旨在利用textCNN学习源代码流中蕴含的深层次语义特征,训练形成源代码缺陷检测卷积网络,以实现对跨函数的多类源代码缺陷检测。
本文在代码分类领域提出CBBCC模型,使用Code-BERT预训练模型来进行代码分类任务,通过对POJ104数据集[13]的分类实验来验证模型的有效性。CBBCC是第一个将BERT预训练模型用于代码分类当中的模型。通过使用CodeBERT 预训练模型来对代码分类任务微调和特征提取,这种方式完善了使用AST+卷积神经网络对代码特征提取后分类存在精度不高的问题,在保留了代码完整性的同时模型对代码特征提取更充分,使得在POJ104 数据集上进行的代码分类任务达到SOTA值。在开源社区或代码库的代码管理中,CBBCC 能对新增的代码进行准确分类。代码分类可以为代码搜索、代码克隆等任务的研究提供基础,从而促进代码大数据的发展,为软件工程的发展提供有利保障。
然而,CBBCC仍有较大的改进空间,目前仍然缺少较大的代码功能分类的数据集,同时在预训练模型上仍然存在可以改进的空间,本文计划下一步将创建更大的代码分类数据集并将CBBCC 进一步改进和应用,同时探索训练一个更适用于代码分类的预训练模型。