郭 峰 徐 建
(南京理工大学计算机科学与工程学院 南京 210094)
在软件开发中,调试是在程序中找到并修复故障的过程。通常,这个过程需要调试人员大量的主观参与(如插入打印语句和设置断点等),而人工调试寻找故障的效率取决于调试人员对程序的理解、调试人员的逻辑判断能力、调试经验以及调试方式[1]。因此,调试工作是软件开发过程中成本昂贵、耗时最长的步骤之一。为了降低开发成本和缩短开发周期,研究人员提出了许多故障定位技术,通过自动提供程序语句的可疑性信息来提高调试效率[2~4]。
近年来,深度学习得到了快速发展,在鲁棒性和准确性方面都有了很大的提高,已经成功应用于图像分类、目标检测、分割[5]等领域。这意味着深度学习或许可以为故障定位提供一个新的解决方案,构建故障定位模型利用其对数据的学习能力,对程序语句的可疑性进行评估。实际上,研究人员已经使用具有多个隐藏层的神经网络来探索和评估深度学习在故障定位中的潜力[6]。
然而,当前的一些故障定位方法还不足以证明深度学习在该领域的潜力,需要进行进一步的研究。例如,仅使用含有错误的小型程序进行实验论证[7]。此外,神经网络中隐藏层的参数数量也会对故障定位的效率有所限制。随着应用程序的规模越来越大,待排查语句的数目也越来越多,这导致隐藏层的数目非常大,伴随而来的是待训练参数数量的飞速增长,这将大大限制深度学习在故障定位领域的潜力。
因此,本文将更多地探讨如何通过深度学习来提高故障定位效率,我们的目标是提出一种新的故障定位方法,并通过实验对故障定位模型进行评估,从而证明其有效性。
传统的基于程序谱的故障定位技术由于其形式简单、易于操作实现、存储方便,而且可以很清晰地表示程序执行信息,为评估语句的可疑性提供了便利的条件。基于程序谱的故障定位技术[8]通常将语句作为程序实体,根据每条语句的运行特征,计算可疑度。但是由于程序谱仅考虑了每条语句的执行情况,并没有对上下文语句之间包含的依赖关系进行分析,因此有时会出现定位到的结果并不是真正的错误语句,而是由于错误传递而产生非预期运行结果的相关语句。
Hinton等[9]提出了深度神经网络模型(DNNs),多层感知机(Multi-Layer Perceptron,MLP)是一种含有多个隐含层的深度神经网络,MLP具有结构灵活的特点,能够拟合出输入和输出之间高度复杂的非线性函数关系。但随着输入数据集规模的增长,待训练的隐藏层参数也会骤增,同时也可能带来局部极小值等问题。Wong和Qi[10]提出了一种基于反向传播(BP)神经网络的故障定位技术,BP 神经网络不仅具有结构简单,容易实现的特性,还具有近似复杂非线性函数的能力。Ascari 等[11]将基于BP神经网络的定位技术拓展到面向对象程序。然而,BP 神经网络仍存在局部极小化以及收敛速度慢等问题,随着卷积神经网络CNN 的问世,这些问题被很好地解决。卷积神经网络是一种具有极强的数据处理能力的神经网络,可以通过学习到的高度抽象的数据特征来有效地表示复杂对象[12]。
因此,本文提出了一种基于深度卷积神经网络的软件故障定位方法(DCFL),通过权值共享可以明显减少神经网络模型中的参数数量。与标准的多层感知器相比,卷积神经网络具有更少的连接和参数,有助于减轻局部极小值问题。首先,DCFL构建一个深度卷积神经网络模型,然后使用测试用例数据集对神经网络模型进行训练,最后将虚拟测试集输入到训练好的神经网络模型中,对程序语句的可疑性进行评估。在实验部分,我们对7 个现实中的大型程序进行了验证分析,结果表明DCFL 可以有效地降低代码的检查成本。
DCFL 的模型框架如图1 所示,基本思想是以卷积神经网络架构为基础,构建一个可以用于对程序语句可疑性进行评估的模型。数据准备工作包括:需要将输入程序在测试用例集上运行,并在程序的执行过程中采集语句覆盖信息以及执行结果信息,然后将采集到的信息转换成矩阵形式,作为神经网络模型的训练集数据。再根据输入程序的规模生成虚拟测试用例集,作为神经网络模型的测试集数据。
图1 DCFL模型框架
1)构造深度卷积神经网络模型,根据程序的规模估算每个神经层中的节点数。
2)使用程序覆盖信息和对应的执行结果作为训练集数据来训练网络模型。DCFL将每个覆盖信息矩阵输入到神经网络模型中,以获得覆盖数据和执行结果之间的复杂非线性映射关系。
3)将生成的虚拟测试用例集输入到经过训练的神经网络模型中,并获得输出。输出值ti反映了xi包含错误的可能性,输出值ti越大,则程序语句i为错误语句的可能性就越大。
4)按照语句的可疑性进行降序排名,以文本形式输出到本地文件。文本提供了程序语句的可疑性信息,可帮助调试人员或自动程序修复工具快速查找故障。
1)训练集中程序表示
对于训练集,我们这里采用了在软件故障定位领域中有效且被广泛使用的数据模型[13],该模型由程序执行过程中的覆盖信息和程序执行结果组成。如图2 所示,给定一个含有n语句的程序P,以及包含m个测试用例的测试集合T(集合T中至少包含一个失败测试用例),T在程序P上执行。矩阵元素xij=1表示语句j在测试用例i上被执行,xij=0表示不被执行。大小为m*n的矩阵中记录了每条程序语句在不同测试用例中的执行情况,误差向量e记录了测试用例的执行结果,ei的值为1 表示测试用例i执行失败,值为0 则表示执行成功。通过覆盖信息矩阵与误差向量e可以将可执行语句与执行结果相关联。
图2 测试用例覆盖信息模型
在此基础上,我们使用小批量随机梯度下降算法来更新网络参数,批量大小固定为h,每次将一个大小为h*n的矩阵作为网络的输入,并将与其对应的误差向量作为标签,迭代训练卷积神经网络。
2)测试集中程序表示
对于测试集,我们同样采用了矩阵形式的数据模型。与训练集有所不同,测试集是根据待检测程序的规模由系统自动生成的虚拟覆盖信息集合,在这里我们称之为虚拟用例集。如图3 所示,给定一个含有n条语句的程序P,则生成大小为n*n的虚拟用例集,其中包含n个虚拟测试用例,假设每个虚拟用例在程序上执行时只覆盖到一条程序语句。
图3 虚拟测试用例模型
经过训练得到的神经网络模型,可以反映程序语句覆盖信息与执行结果之间的复杂非线性关系。最后,将虚拟测试用例输入得到模型中,以评估每条可执行语句与执行结果之间的关联性。
卷积神经网络能够在输入数据集上学习到大量高度抽象的数据特征,我们认为它强大的学习能力有助于评估程序语句与程序运行失败之间的关联性。DCFL 网络模型由一个输入层、多组卷积池化层、多个全连接层以及一个输出层构成。
1)数据输入层:在输入层,训练集数据是大小为M×N的覆盖信息矩阵和误差向量,每次在其中选择h行作为输入数据,即当前选定的h个测试用例对应的覆盖信息和执行结果。
2)卷积计算层:卷积运算就是使用卷积核在输入数据上进行滑动并计算的过程。卷积层通过使用多个卷积核来提取输入数据中的特征,从而识别出输入数据中包含的重要属性。进行卷积运算时,可以通过strides 参数调整滑动步长,合适的滑动步长可以在确保数据完整的前提下,有效地降低输出数据的维度。在我们的网络模型中有3 个卷积层,每个卷积层也包含多个卷积核。
3)激励层:将卷积层的输出结果进行非线性映射。常用的激活函数有三种:Sigmoid 函数(式(1))、Tanh 函数(式(2))和ReLU 函数(式(3))。Sigmoid 函数的输出值为[0,1]之间的实数,但存在饱和使梯度消失、输出值不是“零为中心”以及计算消耗资源等问题。Tanh 函数,也称双曲正切函数,与Sigmoid 函数非常相似,解决了输出非“零为中心”的问题,但仍无法解决另外两个问题。ReLU(The Rectified Linear Unit,修正线性单元),它的特点是收敛速度快,求梯度简单。对于所有正数,输出均为输入本身,对于所有负数输出值均为零。ReLU 函数可以解决梯度消失问题,并且由于其线性、非饱和的形式,在SGD 优化器中能够快速收敛。在我们的网络模型中,将使用ReLU 函数作为激活函数。
4)池化层:该层主要用于压缩数据和降低参数数量,减小过拟合同时可以提高模型的容错性。池化层使用的子采样有两种形式,分别是均值子采样(avg-pooling)和最大值子采样(max-pooling)。最大值子采样在当前窗口范围内选取最大值,目标是在张量中保持最大值。与最大值子采样不同,均值子采样是在当前窗口范围内计算平均值。实验表明向量中的最大值对故障定位是有益的,因此在我们的模型中采用最大值子采样。
5)全连接层:相邻层网络之间的所有神经元均有权重连接。卷积神经网络将全连接层排列成链状结构,通过对特征进行重新拟合,减少特征信息的丢失。在训练过程中,迭代更新权重和偏置参数。
6)输出层:用于输出结果。根据实际输出与预期输出之间的差异构建损失函数,通过反向传播算法训练网络模型。最终,拟合出输入与输出之间高度复杂的非线性关系。这里,我们使用Sigmoid函数作为模型的输出函数,Sigmoid 函数可以确保输出值的范围为0~1。Sigmoid 函数结果向量中的每个元素与目标向量对应的元素间存在差异,这个差异是输出层和目标之间的损失。
我们使用反向传播算法对模型的参数进行更新调整,旨在得到最优的全局参数矩阵。前向传递输入信号直至输出产生误差,反向传播误差信息更新权重矩阵。由于学习率会影响收敛速度,我们的模型采用了动态调整学习率。在式(4)中,一个Ep⁃och 表示一次完成所有训练数据,LR 表示学习率,DropRate是每次修改学习率的量,而EpochDrop是更改学习率的频率。我们将初始学习率设置为0.01,将DropRate 设置为0.95。根据测试用例的数量设置EpochDrop。
图4 展示了一个带有15 条语句的错误程序P,其中包含一条错误语句S2。表1 中每个语句下面的单元格表示该语句是否被测试用例覆盖,最右边的单元格表示测试用例的执行结果。
表1 测试用例信息
图4 示例程序
我们可以观察到有6 个测试用例,其中有两个失败。 第一步构造一个卷积网络模型,其中输入层的节点数为15,全连接层中节点数设置为8,输出层的节点数为1,使用Sigmoid函数作为输出。第二步使用测试数据集训练网络,在这个例子中我们将h设置为3,首先输入的矩阵是t1、t2、t3,目标输出向量为(1,0,0),然后我们再输入矩阵t4、t5、t6,及其执行结果向量(0,0,1)。重复使用这些数据训练网络,直到损失足够小并达到收敛条件为止。
经过训练后,第三步是构建一个包含15 个测试用例的虚拟测试集,将每个虚拟测试用例输入到经过训练的网络中。模型输出值就是对应语句的错误可疑性,结果如表2 所示,我们可以检查列表中的语句并首先找到错误的语句S2。
表2 程序语句可疑性评估结果
Zheng 等[6]提出使用深度神经网络进行软件故障定位方法,获得了比PPDG[14]和Tarantula[15]等方法更好的实验结果,因此我们在研究中选择了将其(简称MLPs)作为对比方法。与此同时,我们还选择了另外两种技术(Dstar[13]和Barinel[16])进行对比分析。最终,我们将DCFL 与这三种方法进行了比较,以证明其在软件故障定位过程中的有效性。
为了确保实验结果的可靠性,我们选择了7 个基准程序集,程序规模为5K 行到491K 行。表3 中记录了程序的概括信息,包括程序功能的简单描述、程序的错误版本数目、程序规模、测试用例数目。
表3 程序集相关信息
这里我们采用两个被广泛使用的指标来评估DCFL 模型的有效性,分别是Exam 和RImp。Exam值表示在找到错误语句之前要检查的程序语句占比,Exam 值越低表示软件故障定位模型的效果越好。RImp 表示分别使用两种不同的软件故障定位方法时,在找到错误程序语句之前需要检查的程序语句数量的比值,RImp的值越小表示DCFL相对其它定位技术具有更高的有效性。
表4展示了DCFL 和其它三种软件故障定位方法在各个程序集上的Exam 指标结果,对于每种方法,我们给出了它在每个程序集上的最优Exam 结果和平均Exam 结果。最优Exam 结果是指实验中在程序的不同版本中取得的最小Exam 值,表示当前方法在该程序集上可以达到的最佳效率。平均Exam 结果是指实验中在程序的所有版本上取得的Exam 的平均值,表示当前方法在该程序集上的平均效率。从表4 中我们可以看到,在七个程序集上无论是最优Exam 还是平均Exam,DCFL 都取得了较好的实验结果。在图5展示4种软件故障定位方法在不同程序集上的Exam 方差情况,通过Exam 方差我们可以了解每个定位方法的稳定性。从图中不难看出DCFL 在大部分程序集上的Exam 方差均取得了最小值,说明DCFL 在这4 种方法中性能更稳定,同时也可以看出在不同程序集上,DCFL的性能波动较小,不会产生随着程序集的变化导致方法性能出现较大波动的问题。
表4 不同方法的Exam实验结果
图5 Exam方差波动情况
为了更详细比较DCFL 与其他故障定位方法的效率,我们使用RImp 指标来评估不同方法之间的性能差异。图6、7、8中展示了DCFL分别与Bari⁃nel、Dstar 和MLPs 在RImp 指标上的实验结果。以MLPs 为例说明,DCFL 在RImp 指标上的得分从nanoxml_v3程序集上的30.87%到space程序集上的82.83%,这表示相对MLPs 方法需要检查的语句数目,DCFL需要检查其30.87%到82.83%即可定位出软件故障位置。也就是说相对于MLPs 方法,我们最多可以减少69.13%的代码检查量,最少可以减少17.17%的代码检查量。实验结果表明,DCFL 与MLPs 相比,在软件故障定位的过程中平均可以减少43.15%的代码检测量。如图7 所示,与Dstar 方法的代码检查量相比,DCFL在libtiff程序集上的代码检查量最多减少59.66%,在nanoxml_v2 程序集上最少减少5.69%,平均减少32.68%。与Barinel方法的代码检测量相比,DCFL 在gzip 程序集上最多可以减少60.7%,在nanoxml_v1 程序集上最少可以减少22.46%,平均减少41.58%。因此,DCFL 方法在软件故障定位中更为有效。
图6 基于Barinel的RImp
图7 基于Dstar的RImp
软件故障定位是软件调试过程中最重要的任务之一,本文提出了一种利用深度卷积神经网络进行故障定位的有效方法(DCFL)。首先构建一个深度卷积网络模型,通过程序集测试用例对应的语句覆盖信息及执行结果来训练神经网络,然后将一组虚拟测试用例作为测试数据集输入到训练完成的网络模型中,最后通过模型输出的结果来评估程序语句的可疑性,进而根据可疑性进行软件故障定位。进一步,通过将DCFL 方法与当前先进的三种软件故障定位方法进行对比发现,在进行软件故障定位时,DCFL 方法的总体效率优于其他三种方法。但该方法仍存在改进之处,比如将程序集的控制流信息应用到软件故障定位过程中,进一步提高DCFL 的准确性。此外,未来我们还计划将目前的工作扩展到多故障软件定位的情景中去。
图8 基于MLPs的RImp