董玉坤,李浩杰,位欣欣,唐道龙
中国石油大学(华东)计算机科学与技术学院,山东 青岛 266580
在软件开发过程中,不可避免地会引入各种缺陷,有些缺陷仅仅会造成软件的部分中止,对整个系统的运行影响不大,但有的缺陷则会造成严重的功能性、安全性等错误,影响系统安全。因此,在软件开发过程中进行软件缺陷预测显得至关重要。软件缺陷预测需要全面分析程序的语法、语义等特征,如果进行人工检测会耗费大量的人力和时间,因此构建软件缺陷预测模型自动进行软件缺陷预测成为了一个热门的研究方向。
传统的机器学习方法将软件缺陷预测问题转化为一个构建分类器的问题[1-6],通过手工定义特征训练分类器,用训练好的分类器预测可能包含缺陷的代码区域,这个区域可以是文件、类或者方法。许多机器学习算法都可以完成这项分类工作,例如决策树(decision tree)[7]、随机森林(random forest)[8]、朴素贝叶斯(naive Bayes)[9],但均不能获得较高的准确率,并且手工定义特征也是一个费时费力的过程。
深度学习作为机器学习的一个分支,近年来被应用于软件缺陷预测领域,并且取得了良好的效果。与传统的机器学习方法相比,深度学习通过搭建神经网络自动提取代码特征,可以获得更高的准确率。但是,现有的研究往往只在同一粒度上提取特征,比如对每行代码做映射表示整个程序[10]。该方法可以将代码转换为输入样本,但是会丢失很多代码的细粒度信息,不能预测出所有类型的缺陷。
程序的抽象语法树(abstract syntax tree,AST)可以清晰地展示出程序各个节点之间的层次关系,利用这种层次关系对程序结构的表示,可以精确地描述代码的结构信息,获取结构特征与简单的语义特征。同时,程序的Token序列可以在更细的粒度上表示程序,其中蕴含着丰富的语义信息,通过将程序转换为Token 序列,对Token序列进行特征提取,可以准确地理解程序语义,获取语义特征与部分结构特征。如图1 所示的Java 代码片段,通过将其解析为AST,分析AST 上的节点可以清楚地发现,在第3、4行的remove和add方法的先后顺序会引起NoSuchElementException异常,但是并不能发现第5行for循环条件语句中“i--”引起的死循环缺陷,因为受AST上的节点粒度所限,并不能提取到循环条件中的语义信息。而通过将其解析为Token序列,可以有效地发现这个死循环缺陷,因为Token 序列是在Token 级别表示程序,蕴含了程序的语义特征。
图1 一个有代表性的例子Fig.1 A representative example
软件缺陷预测按照预测粒度划分为方法级、类级、文件级等,本文在文件级进行软件缺陷预测。本文提出了一个软件缺陷预测框架2T-CNN(TBCNN and TextCNN),使用该框架,可以在软件版本迭代时快速预测新版本中可能包含代码缺陷的文件,向代码审阅者发出警告,缩短代码审查的时间。2T-CNN 基于卷积方式的不同,通过树卷积神经网络(tree-based convolutional neural network,TBCNN)[11]从程序的AST节点中提取结构特征,通过文本卷积神经网络(text convolutional neural networks,TextCNN)[12]从程序的Token 序列中提取语义特征,将两个模型提取到的特征融合后,输入全连接神经网络(fully connected neural network,FCNN),经过逻辑回归分类器,输出程序是否含有缺陷。本文对所提出的方法进行实现,并针对开源Java项目进行实验评估,结果表明,该方法能够有效提升软件缺陷预测的准确率。
利用深度学习技术预测软件缺陷是近几年兴起的新颖的方法,在预测方法的整体流程上和传统机器学习方法相似。不同之处在于深度学习技术可以利用神经网络从程序代码中自动提取出隐藏特征,这些隐藏特征往往是设计判别特征的人想象不到且无法设计出来的。
使用深度学习技术预测程序缺陷的核心思想在于程序代码已被证实具有自然语言的相似属性[13],因此现有的大多数基于深度学习的软件缺陷预测技术都将程序代码视为自然语言文本,将软件缺陷预测转化为文本分类任务,从自然语言处理的角度对文本进行缺陷分类。例如,Li等人[14]提出了一个基于深度学习的软件漏洞检测系统VulDeePecker,该方法使用双向长短时记忆网络(bi-directional long short-term memory,Bi-LSTM)在Token级别提取到程序的语义特征,但无法提取到代码中其他丰富的特征,例如结构特征。该文的另一个重大贡献是提供了一个可供深度学习训练使用的软件安全漏洞数据集[15]。Zou等人[16]在VulDeePecker的基础上提出了μVulDeePecker,它基于代码注意力和控制依赖,结合新的神经网络building-block Bi-LSTM融合不同的特征,实现了对具体漏洞类型的检测。Duan等人[17]为了更好地捕捉程序源代码的关键特征信息,提出了一种基于代码属性图及注意力Bi-LSTM 的漏洞挖掘方法,与基于规则的静态挖掘工具Flawfinder和RATS相比有显著的提高。
为了更好地提取代码的结构特征,有很多研究将深度学习技术应用在AST上。Wang等人[18]利用深度置信网络(deep belief network,DBN)[19]从程序的AST 中提取符号向量来学习语义特征,在效果上优于传统的基于特征的方法,但并没有考虑程序的结构特征。Li等人[20]为了充分考虑程序代码的结构特征,对AST中具有代表性的节点进行映射,结合传统的缺陷特征训练判别器,取得了比DBN 更好的效果。但正如该文中的例子所示,将for循环的整个条件部分映射为一个整数,显然无法发现在条件语句中的缺陷。另外,这种方法仍然需要手工定义的特征。Dam 等人[21]的研究是对AST 中的每个节点进行映射,将一个个AST分支输入基于树的LSTM单元中训练模型。这种方法可以充分考虑到AST 中的结构特征,对于发现特定的序列模式也有很大的帮助。值得一提的是,该方法使用无监督的方式训练LSTM单元,因此不需要大量的数据集标记成本。Pan 等人[22]对DP-CNN[20]进行改进,提出了Improved-CNN,获得了更好的效果,并且探究了缺陷预测模型中超参数的不稳定性。
下面详细介绍了本文提出的2T-CNN,它可以自动从源代码中提取代码的结构和语义特征,对源代码是否具有缺陷进行预测。图2 展示了2T-CNN 的整体框架,通过以下5 个步骤对程序代码进行缺陷预测:(1)源代码解析;(2)词嵌入;(3)特征提取;(4)特征融合;(5)缺陷预测。
图2 2T-CNN整体工作流程图Fig.2 Overall workflow of 2T-CNN
程序源代码中蕴含着丰富的特征,软件缺陷预测就是从程序源代码中挖掘这些特征用于预测程序缺陷。但是,从原始的源代码结构中提取特征是困难的,同时源代码中所蕴含的特征并不都是对缺陷预测有价值的,例如注释文本。因此,需要将源代码解析为合适的结构和粒度,并且去除其中的冗余特征以提取有利于缺陷预测的特征。
2T-CNN 的源代码解析分为两部分,一部分是将源代码解析为AST,另一部分是将源代码的文本转换为Token 序列,两种程序表示形式分别对应了两个特征提取网络的输入,具体步骤将在下面的内容中详细介绍。
2.1.1 解析AST
程序代码具有严格的结构信息,这种结构信息在AST上可以得到具体体现。AST是树状结构,可以准确描述每个节点与其相关节点的结构关系,因此本文将程序源代码解析为AST来提取源代码的结构特征。
本文使用javalang[23]将源代码解析为AST,该工具基于Java 语言规范对源代码进行解析。使用该工具可以方便地将源代码解析为AST,并提取出AST上的各个节点用以解析程序。例如,ClassDeclaration节点代表类定义,ForStatement 节点代表for 循环,它的两个子节点ForControl和BlockStatement构成了for循环的条件语句和循环体。
因为需对完整的AST进行卷积操作,为了尽可能保留程序中所有的结构信息,舍弃了Import节点、Package节点,还有注释节点等对缺陷预测没有作用的节点,对其他所有的节点类型进行了保留。此外,由于文献[11]使用的方法仅仅关注了AST节点类型信息,没有对不同的变量和方法作区分,因此本文对生成的AST进行了更改。具体来说,将方法调用、变量引用替换为它们的值。例如add函数和remove函数都属于MethodInvocation节点,但它们具有不同的作用。
在获得了程序的AST之后,构建一个二维列表来表示整个AST。其中,第一维的索引代表相对应的节点,第二维中的元素代表该节点的所有子节点的映射,整个二维列表就可以用来表示一段代码的AST。二维列表的表示形式如下:
其中,nodem表示AST 中的节点,childn表示nodem的子节点。以图1 中的代码片段为例,图3(a)展示了for循环的整个AST结构,图3(b)是它所对应的二维列表。
图3 图1中for循环的AST及其二维列表Fig.3 AST of for loop and its two-dimension list in Fig.1
2.1.2 解析Token序列
程序语言已经被证实具有自然语言的特点,因此可以将程序语言视为自然语言文本来提取代码的语义特征。通过词法分析将程序代码解析为Token 序列可以最大程度地保留代码中的语义信息,同时Token序列也是最符合自然语言文本的表示形式,使模型可以更有效地学习到代码中的语义特征。
本文将程序代码解析为Token 序列的伪代码表示如下:
具体来说,删除的冗余代码包括导包、打包、单行注释、多行注释、行尾注释等对于缺陷预测没有任何帮助的代码,并且它们本身的语义也会影响预测结果。借助javalang 工具中tokenizer 模块对代码进行分割,得到程序的Token列表。本文舍弃了类型为Separator的Token,因为对于TextCNN 来说,输入样本的长度要一致,长度过大会消耗更多资源,也不利于模型收敛,并且Separator类型的Token虽然蕴含一定的程序结构特征,但它没有AST中蕴含的结构特征明显,同时也会干扰TextCNN对代码语义特征的提取。对于类型为Identifier 的自定义标识符建立映射集M并进行值替换,通过对其进行标准化可以在不影响程序语义和结构的情况下,减少词汇量,加快词嵌入的训练速度,同时有效降低不同自定义标识符带来的影响。映射机制描述如下:
对于图1例子中的for循环,舍弃其类型为Separator的Token,同时对自定义标识符i、add、remove进行替换,提取得到的Token序列为:<for,int,identifier1,=,Integer,identifier1,‘<’,Integer,identifier1,++,identifier2,identifier3,Integer,identifier2,identifier4,Integer>。
类比计算机视觉领域将图片转换为[0,255]的像素值矩阵作为神经网络的输入,2T-CNN 也需要将程序代码文本转换为数值向量输入到神经网络中,本文选用word2vec[24]来进行词嵌入处理。
word2vec是一种利用BP神经网络来自动学习词语表示的工具,区别于独热编码,它可以将词语映射到一个固定长度的向量中,同时还可以保留词语的语义特征,因为经过训练后,语义相近的词语在向量空间的分布上很接近。本文使用Gensim工具包提供的word2vec模块进行词嵌入处理。
对于程序的两种表示方式,分别在两个语料库上进行训练。在AST 表示方式中,将2.1.1 小节得到的AST中的每个节点替换为它的词向量,构建词嵌入AST。在Token 序列表示方式中,训练获取每个出现次数超过5次的Token 的词向量,出现次数过少的Token 不会对预测结果造成较大影响。对于TextCNN来说,输入矩阵的形状必须相同,通过观察数据集中所有文件的Token序列长度,统计它们的平均数和中位数,最终将Token 序列长度设置为250,超过此长度部分舍弃,不足此长度的补0。设置词向量长度为50,最终构建了大小为(250,50)的输入矩阵。
经过两种训练方法训练后,获得了具有词嵌入编码的AST和Token序列矩阵,根据源代码文件编号将相同程序的词嵌入AST 和Token 序列矩阵一一对应,构建(tb_nodes,tb_ast,tx_input|label)输入样本。其中tb_nodes是AST节点的词向量矩阵,tb_ast是代表整个AST的二维列表,tx_input是Token序列的向量矩阵,label是标签矩阵,表示程序是否具有缺陷。
2.3.1 基于TBCNN的结构特征提取
本文使用的TBCNN 如图4 所示,它包含一个基于树的卷积层(tree-based convolution)、一个池化层(max pooling)以及一个全连接层(fully connected)。图4(a)是经过词嵌入的AST,虚线三角形框表示卷积核,卷积核在AST 的每一个节点上滑动后得到图4(b)中的特征图。
图4 2T-CNN中提取结构特征所用TBCNNFig.4 TBCNN for extracting structural features from 2T-CNN
在卷积层,卷积核的深度设置为2,权重矩阵Wconv由三个矩阵的线性组合构成,它们的系数分别是使用LeakyReLU作为激活函数,它可以有效解决Tanh 造成的梯度消失问题,并且加快模型收敛速度。在一次卷积中,假设卷积窗口中有n个节点x1,x2,…,xn,卷积过程可以由式(3)来表示:
其中,权重系数wconv,i可由式(4)确定:
其中,d表示卷积核的深度,di表示节点xi在卷积核窗口中的深度,pi表示节点xi的位置,n表示节点xi的所有子节点数量。由式(4)可以看出权重系数是由节点在卷积核窗口中的相对位置计算得到,可以合理地反映出各节点间的位置关系。整个卷积层的参数量为15 100个,复杂度为O( )nodes×d×c,其中nodes表示AST 节点数量,d表示词向量长度,c表示卷积核个数。
在池化层,使用MaxPooling 进行池化,保留主要特征并减少参数。为了简化特征融合的过程,添加了一个全连接层使TBCNN 最终输出的特征向量长度和TextCNN相同,在本文中,TBCNN输出的特征向量长度被设置为100。
2.3.2 基于TextCNN的语义特征提取
程序代码的Token序列是由源代码解析而来,相比于其他的程序表示方式,它在Token 级别表示程序,具有最细的粒度,因此其中蕴含着丰富的语义信息。本文没有选择文献[16]中使用的Bi-LSTM来提取Token序列中的语义特征,主要原因有两点:一是因为虽然TextCNN在联系文本上下文的表现上不如Bi-LSTM,但它可以通过设置多个不同大小的卷积核,抓住文本多个不同的ngram 特征,并且对于同一个n-gram 特征也会从不同角度去提取关键的信息;二是因为在2.2节,将Token序列长度设置为250,Bi-LSTM在处理长文本的训练速度上比TextCNN慢得多。因此在2T-CNN中选择使用TextCNN来提取代码的语义特征。
本文设计实现的TextCNN 如图5 所示,它将Token序列的词嵌入矩阵(embedding matrix)作为输入,包含两个卷积层(convolutional layer)和池化层(pooling layer),最后连接一个扁平层(flatten layer)。词嵌入矩阵是一个s×d的矩阵,其中s代表样本中Token 序列的长度,d代表Token 的词向量长度,整个矩阵就表示了一条样本。在卷积层中,通过设置多个宽度不同,长度等于词向量长度d的卷积核,保证卷积核每次滑动过的位置都是完整的单词,从而更全面地提取Token序列中的语义信息,每个卷积核的卷积过程可以用式(5)表示:
图5 2T-CNN中提取语义特征所用TextCNNFig.5 TextCNN for extracting semantic features from 2T-CNN
其中,W∈ℝh×d,A∈ℝs×d,A[i:i+h-1] 表示词嵌入矩阵A的第i行到第i+h-1 行,卷积核在每一个嵌入矩阵上从上到下滑动提取程序的语义特征。两个卷积层的参数量为24 550 个,复杂度为,其中l表示卷积层数,hl表示卷积核宽度,sl-1表示序列长度,dl表示卷积核个数。
在池化层,采用MaxPooling进行池化操作。在扁平层,使用Flatten方法将所有特征图展开堆叠成一个长度固定的特征向量。在本文中,TextCNN输出的特征向量长度和TBCNN保持一致,设置为100。
使用TBCNN 和TextCNN 获取到的不同层面的代码特征均是高维且抽象的,虽然它们的侧重点不同,但它们并不是相互独立的,因此采用特征融合的方式将这些高维特征融合可以提高预测的准确率。
常见的特征融合方式很多,例如取Average、Maximum,本文使用的特征融合方式是Concatenate。一种先验知识认为Concatenate的融合方式可以将从相同程序的两种表示中提取到的不同特征进行强化互补,从而提高预测的准确率。在3.4.4小节通过实验验证了Concatenate融合方式相较于Average和Maximum,可以获得更好的效果。
具体做法是,从TBCNN 和TextCNN 的最后一层输出中得到特征向量,分别记作tbout、txout,对tbout和txout进行Concatenate操作,将它们拼接为特征向量fuse_vec。
此外,正如图1 中的例子所示,不同缺陷在不同特征表现上具有不同的强弱度。根据缺陷类型的不同,为提取到的结构特征和语义特征分配不同的比重,可以提高预测的准确率。人工定义这个比重缺乏合理性,因此在模型中实现一个自定义层,让网络自动学习特征中每个元素的系数。添加的自定义层如图6所示,分为系数分支和短路分支。
图6 用于学习结构和语义特征比重系数的自定义层Fig.6 Defined-self layer for learning structure and semantic features proportion coefficient
在系数分支,将权重矩阵Wp初始化为大小和fuse_vec相同的矩阵,将fuse_vec和Wp逐元素相乘,并通过Sigmoid 函数进行归一化并对其加1,防止梯度消失,通过反向传播不断更新Wp的参数。在短路分支,直接将fuse_vec作为输出,从而将系数分支输出的权重矩阵以逐元素相乘的方式作用在fuse_vec上。自定义层的输出O可以用式(6)表示:
其中,⊗代表逐元素相乘,1代表元素全为1的矩阵。
模型使用小批量随机梯度下降算法[25]和Adam优化器[26]进行训练,以加快训练速度,同时采用二进制交叉熵作为损失函数。
考虑到逻辑回归分类器在此类研究中有着广泛的应用[27],因此采用逻辑回归作为最终的分类器。将融合后的特征向量输入全连接层,并将全连接层的输出向量输入到逻辑回归分类器中,得到最终的预测结果。
按照上述步骤对训练集的数据进行处理,通过训练集训练模型之后,模型中的所有权重都是固定的。在缺陷预测阶段,将没有训练过的样本经过同样的数据处理过程,输入到模型中,通过分类器得到最终的分类结果,预测程序是否含有缺陷。
下面主要介绍了实验相关工作以及对2T-CNN 的评估。通过比较2T-CNN 与其他最新方法在软件缺陷预测方面的准确性来评估其有效性。
本实验的实验环境为:Linux 操作系统,i7-10700 CPU,RTX3090 GPU。考虑到神经网络训练的随机性,所有的实验数据均取10次实验结果的平均值。
本文使用的数据集是PSC(PROMISE source code)数据集和一个工程项目DTSC[28-30]。PSC 来自软件工程领域的公共存储库PROMISE[31],是进行跨版本软件缺陷预测的常用公开数据集[32-34]。为了与基线方法比较,使用PSC的子数据集SPSC(simplified PROMISE source code)进行对比实验。SPSC中包含了PSC中7个项目的两个相邻版本,例如Camel-1.4、Camel-1.6。特别的是,实验中删除了PSC中不能通过编译的文件,并舍弃了项目名为Pbeans 的项目,因为它的文件数过少,无法使模型收敛。DTSC是一个基于Java语言编写的C语言程序缺陷检测工具,可以用于检测软件缺陷,在实验时从中挑选3个最新版本作为DTSC数据集。表1列出了两个数据集的详细信息。
将所有文件中的buggy 代码(即具有缺陷的代码)标记为1记作正样本,clean代码(即没有缺陷的代码)标记为0 记作负样本。如表1 中缺陷文件占比一列所示,可以观察到一些项目中的缺陷文件占比过小,例如项目Camel-1.4中缺陷文件占比仅为17.3%,这会使数据集中正负样本差距过大,造成样本不均衡的问题,样本不均衡会导致训练出来的模型泛化能力差并且容易过拟合。样本不均衡问题在数据层面的处理方法通常有三种:上采样、下采样和数据合成。本实验中,考虑到样本数量较少且不易合成,故对样本不均衡项目中的少量样本进行上采样,随机复制少量样本,使正负样本比例达到0.4至0.6。
表1 数据集详细信息Table 1 Dataset details
为了更有效地评估2T-CNN,仅仅使用准确率没有太大的意义,因为针对软件缺陷预测的问题,通常更关注的是预测出缺陷的那一部分结果。因此,使用F1 分数(F1-score)来评估2T-CNN,它是由精确率(Precision)和召回率(Recall)共同决定的。
在明确Precision、Recall 和F1-score 的定义之前,首先需要定义四个基本概念:
TP(true positive):预测值为正,标签值为正。
FP(false positive):预测值为正,标签值为负。
FN(false negative):预测值为负,标签值为正。
TN(true negative):预测值为负,标签值为负。
在本实验中,将存在缺陷的代码文件标记为正样本,不存在缺陷的代码文件标记为负样本。借助于以上4个指标,可以计算以下3个指标:
Precision:正确预测的正样本个数与所有预测为正样本个数之比。
Recall:正确预测的正样本个数与所有标签为正样本个数之比。
F1-score:精确率和召回率的调和平均值。
从式(7)可以看出Precision越高,误报率越低,从式(8)可以看出Recall 越高,漏报率越低。虽然Precision和Recall 的值越高证明模型的预测效果越好,但由于Precision 和Recall 之间的相互影响,不能认为其中单一指标可以评估模型的效果,因此引入了F1-score,它是Precision 和Recall 的一个综合度量,取值范围是[0,1]。式(9)显示了F1-score与Precision和Recall的关系,只有F1-score的值越高,才能综合判断模型的预测表现更好。
本文将提出的2T-CNN 软件缺陷预测方法与以下基线方法进行比较:
传统机器学习模型:使用四种机器学习模型作为传统模型进行对比,分别是逻辑回归(LR)、朴素贝叶斯(NB)、决策树(DT)和随机森林(RF),这是四种最常用的机器学习模型。与深度学习模型不同的是,传统机器学习模型使用手工定义特征度量的方式作为输入。
TBCNN:它取自2T-CNN 的一部分,即将源代码的AST输入TBCNN进行特征提取并预测缺陷。
TextCNN:它同样取自2T-CNN的一部分,即将源代码解析为Token序列,通过TextCNN提取代码特征输入到分类器中,预测程序缺陷。
DP-CNN[20]:它是一种结合了手工定义传统特征的CNN方法,效果优于仅仅使用CNN的方法。
Improved-CNN[22]:它是一种改进的CNN方法,摒弃了预测模型对手工定义特征的依赖,比原有的CNN 方法在结果上有一定提高。
Bi-LSTM[17]:它是一个结合了代码属性图和注意力机制的双向LSTM预测模型,可以有效地避免程序语义信息的丢失以及捕获关键特征。
在探究2T-CNN 同基线方法的优劣时,使用SPSC数据集,这是Li等人[20]和Pan等人[22]使用的数据集,也是跨版本软件缺陷预测常用的数据集。但仅仅只在SPSC上实验并不能获得足够多的样本来验证2T-CNN 的泛化性,因此在探究2T-CNN 的泛化性时,使用PSC 数据集和DTSC数据集。
本文设计了多组对比实验,并依据实验数据和实验结果给出了以下四点分析和结论。
3.4.1 2T-CNN与基线方法的比较
表2展示了2T-CNN 与本文除TBCNN 和TextCNN之外的基线方法在SPSC 数据集上的表现对比。观察表2可以得到以下两点结论:
表2 2T-CNN与基线方法在SPSC数据集上的表现比较Table 2 Performance comparison between 2T-CNN and baseline models on SPSC dataset
首先,四种最常见的传统机器学习方法的平均F1-score均低于四种深度学习方法,因此可以得出结论,在跨版本软件缺陷预测中,深度学习方法普遍优于传统机器学习方法,并且可以节省手工定义特征花费的时间。
其次,虽然基于深度学习方法的四种预测模型在不同项目上的表现互有优劣,但2T-CNN取得了最高的F1-score 平均值,因此可以证明基于特征融合的方法在整体上优于已有的深度学习方法。此外,区别于DP-CNN,2T-CNN 的特征提取完全由神经网络获取,不需要手工定义特征,因此可以节省大量成本。同时,在初始化2TCNN的超参数时借鉴了Improved-CNN的超参数设定,例如全连接层的节点数、卷积核的步长和大小等,基于初始化的超参数再进行超参数调优,最终获得了使模型表现最好的超参数设定,这也证明了Pan等人[22]的研究,即超参数使模型具有不稳定性,因为在调参过程中,模型在不同超参数下的表现显著不同。最后,结合代码属性图的Bi-LSTM的表现比两种CNN方法好,比2T-CNN稍差,这反映了在代码解析中,综合了多种数据结构的代码属性图相较于单一的代码解析方式,可以蕴含更丰富的特征,但Bi-LSTM要付出更多的训练时间,这是由于CNN在训练速度上普遍快于Bi-LSTM[35]。
3.4.2 2T-CNN的泛化能力
为了验证2T-CNN在软件缺陷预测中的泛化能力,使用较SPSC更为完整的PSC数据集以及DTSC数据集进行实验,具体结果如表3所示。
对于PSC数据集,观察2T-CNN一列,被*标记的数据是显著低于其他版本对的F1-score。除了带*的版本对,2T-CNN在其他所有版本对中都能获得较好的表现,平均F1-score为0.658。
对于表3中被*标记的F1-score,考虑它们较低的原因主要受到两方面的影响:一是因为在它们的版本对中,存在严重的样本不均衡问题,例如在<Camel-1.0,Camel-1.2>中,Camel-1.0 的缺陷文件数为13,缺陷文件占比仅有3.8%,神经网络不能充分学习到缺陷特征,F1-score 仅为0.396;二是因为当同一个项目的两个版本之间进行较大的更新时,它们之间的参数分布会有较大差异,例如在<JEdit-4.2,JEdit-4.3>中,文件数量增加了约1倍,缺陷文件数量却下降了75%,训练后的神经网络不能处理这种巨大差异,对于JEdit-4.3 中的11 个缺陷,在10次实验中2T-CNN只有2次找到了1个缺陷。在由这两方面原因造成的样本结构差异较大时,2T-CNN 的表现不佳。
表3 2T-CNN、TBCNN、TextCNN在两个数据集上的表现Table 3 Performance of 2T-CNN,TBCNN and TextCNN on two datasets
对于DTSC 数据集,将DTSC 相邻版本的前一个版本作为训练集,后一个版本作为测试集进行实验,最终得到了0.644的平均F1-score,结果略好于PSC数据集上的表现。这证明2T-CNN有着不错的泛化能力,可以在不同的数据集上获得相似的表现。
3.4.3 特征融合的有效性
观察表3中TBCNN、TextCNN和2T-CNN在各个项目上的表现可以看到,基于特征融合的2T-CNN在各个项目上的表现都比它的两个子模型TBCNN和TextCNN好,这证明了特征融合的思想是正确的,即TBCNN 和TextCNN提取到的特征经过融合后,可以获得更好的分类效果。
同时应注意到,在缺陷文件占比较小的项目中,F1-score在TBCNN和TextCNN模型上的最大值普遍较低,这是因为训练集中正样本比例较低。虽然在训练集上进行过样本不均衡处理,但模型依然不能充分学习到缺陷特征。而经过特征融合后,2T-CNN 在这些项目中的表现要优于TBCNN 和TextCNN,这也说明特征融合方法是有效的。
3.4.4 特征融合方式对模型表现的影响
在深度学习中,特征融合的方式有很多种,在本文中探讨了三种基本的特征融合方式,分别是Average、Maximum和Concatenate。在保证模型超参数不变的情况下,分别对三种融合方式在SPSC 数据集上进行实验验证,结果如表4所示。除了在Lucene项目中Maximum融合表现最好之外,其他项目中都是Concatenate 融合的表现最好,并且所有项目的F1-score 平均值也是Concatenate 融合最好。这也证明了通过TBCNN 和TextCNN 分别提取程序代码的结构和语义特征的合理性,因为这两种特征是程序的不同特征,它们具有一定的互质性和互补性,所以Average和Maximum这两种融合方法会淡化这种差异,而Concatenate 融合方法可以充分考虑它们之间的这种差异,获得最好的融合效果。
本文针对现有的软件缺陷预测方法存在的特征提取不完全问题,研究并实现了一种基于结构与语义特征融合的软件缺陷预测框架2T-CNN。该框架将程序源代码分别解析为AST和Token序列,从两种程序表示方式中提取代码的结构和语义特征,将两种特征融合后经过分类器输出预测结果。该方法可以快速预测软件新版本中可能存在的缺陷,实验结果表明相较于原有的软件缺陷预测模型有一定的提高。
在未来的研究工作中,通过研究更多的程序表示方式,例如在程序依赖图、程序调用图上设计合适的神经网络提取特征,用于软件缺陷预测,以期望获得更好的预测效果。此外,构建高质量的软件缺陷预测数据集也是一个值得推进的工作。最后,不同类型的缺陷模式往往具有不同的特征,针对各种缺陷模式的特点,设计不同的神经网络模型可能会提高预测的准确率,这也将是未来的研究方向。