这一篇是我们这个系列的最后一篇,也是AI for code的第二个应用。前面一篇:液态黑洞:让AI帮你写代码 –(二)编译优化 主要是解决代码效率的问题,我们这篇主要针对代码的正确性或可读性的一些应用等等。我们主要针对四种智能编程的问题依次进行介绍利用AI技术的方法,主要包括:
- 错误自动查找
- 错误自动修复
- 代码搜索
- 代码克隆检测
当我们打算应用深度学习来解决智能编程中的问题时,我们首先把它拆分成三个小问题:1. 能否在自然语言处理任务中找到一个类似的问题;2. 如何为这个问题创建训练数据;3. 如何表示代码。对每个任务我们都会从这三个维度来解答,从而能够更清晰地看到问题的解决思路。
- 错误自动查找
由于程序员的很多时间都会花费在代码维护或debug上,因此自动发现错误的深度学习技术对软件开发来说非常重要,可以很大程度上提高生产力。这里的代码错误不仅是普通的语法错误,也有可能是编译器检查不到的错误。在词法层面上通常是和代码中的标识符或名称相关联的错误,也有与程序语法相关的错误,和与程序逻辑或语义相关的更难查找的错误。
对于这个任务,我们首先回答一下我们上面提到的三个拆分的问题。对于第一个问题,能否在NLP中找到一个类似的任务。如果我们想要识别出错误代码和正确代码,在标准的自然语言处理领域种有一些技术可以已经被用来识别文本错误,无论是拼写、语法还是语义错误,这就是我们所说的文本错误检测。它的应用非常普遍,你会看到word编辑器会提供拼写检查和语法错误检测。这种任务就是给定某种人类语言,识别出其中的错误。类似地,代码的错误检测可以被建模为一个文本分类问题,我们可以考虑应用文本分类任务中的模型或研究经验来查找错误。
对于剩余两个问题,创建训练数据和表示代码,我们引入一篇论文来解释,叫DeepBugs,主要针对命名相关的错误debug,这种错误形式是比较表面的,是一种相对容易检测的错误类型,我们通过几个示例来展示什么是命名类型的错误。
我们看一下下面这段代码,其中有一个函数setPoint,它以x, y两个参数为输入。我们可以先忽视它的内部实现而看成一个黑盒。当我们调用这个函数时,我们是用 y_dim 和 x_dim 作为参数调用的,这里很明显传递参数的顺序在我们的理解中被错误地交换了,因此这是一个命名错误,通常是因为开发人员不了解参数的顺序或者疏忽而出现,这种错误是不会被编译器识别的。
function setPoint(x, y) {...} var x_dim = 23; var y_dim = 5; setPoint(y_dim, x_dim)
我们再看一个命名相关的错误。如下,for 循环中正在遍历一个名为 params 的数组,for 循环中的条件检查实际上应该是将循环变量 j 与 params 的长度(params.length)进行比较,但这里由于疏忽写错了,所以这是另一种命名错误。
for (j=0; j<params; j++){ if (params[j] == paramVal) { ... } }
有很多工作试图在源代码中检测此类命名错误,并且这些工作大多是基于确定性规则的,因此通常不能很好地移植到类似的代码库中,并且不够鲁棒。利用深度学习或机器学习来学习一个检测器自动检测代码中的错误,可以减少手工制作的错误检测器遇到的问题。所以DeepBugs是从这个角度提出的,首先给定一个代码语料库,它的基本流程如下图,先生成训练数据,然后将这些输入代码表示为向量,再提供给一个两类的文本分类器,一旦你训练好了这个分类器,就可以用来预测新代码中的错误。
那我们如何生成训练数据呢,在第一篇我们说过在网上有大量公开可用的代码库,例如 github等等,但是通常代码库的代码应该是正确没有bug的,对于我们的二分类问题,我们需要假设正样本是正确代码,负样本是错误代码。一种生成负样本的方式是让一些人工在代码库中手工标注分类,将代码注释为正确或错误,但这将花费巨大的精力和费用,并且可能找不到足够的错误代码的样本。
所以这篇论文实际上提出了合成生成训练数据的想法,他们将在代码库中获取的正确的代码使用简单的代码转换来人工生成错误样本,其中包括三种类型的错误,一种是参数交换,如下,将原本正确的参数顺序交换为错误的顺序;
SetPoint (X, Y) → SetPoint (Y, X)
第二种是错误的二元运算符,使用随机选择的操作符替换现有的操作符。比如下面这个 for 循环,本意是遍历长度,这里把小于等于运算符改成了%运算符;
for (; i <= length;...) → for (; i % length;...)
第三种是错误的操作数,用随机选择的操作数替换现有操作数。就像下面的左移操作,而将2替换为其他一些不正确的操作数。
value << 2 → value << next
这种生成训练数据的方式是可扩展的,这些合成的训练数据在训练模型时表现地很有用。
一旦我有了训练数据,我们还剩余一个问题是如何表示代码,在之前我们介绍了很多代码建模的方式,例如控制流图或 ir ,DeepBugs中是通过连接代码token的embedding生成向量,使用这种较浅的表示方式的原因是DeepBugs只专注与名称相关的错误,而不必考虑程序逻辑方面的语法错误和语义错误。另外像参数映射这样的问题还需要了解类型和形参名称,因此对于每个参数都有类型和参数名称的附加信息,并且还为操作数添加了抽象语法树的父节点和祖父节点信息。我们可以看到,对于特定的任务会使用不同类型的特定表示。在标准 nlp 模型中的表示方法要么是单词,要么是句子,要么是一些词或句子嵌入的组合表示,但是在 Al for code中有时还需要额外的辅助信息,这是任务决定的,这是和一般NLP领域的一个显著区别。
在效果评估上,他们使用了 6800 万行 javascript 代码并且表明可以达到 68%的准确率。但这篇特定论文的限制是,他们只考虑了命名错误,而且只有三种命名类型的错误,个人觉得主要还是受限于数据集的获取,如果想针对更多语义层面的debug,那么训练数据的获取方式就会更加困难。
2. 错误自动修复
现在我们知道可以使用使用深度学习技术来进行代码错误的自动查找,有没有办法自动修复错误呢。作为软件维护周期中的下一步,识别出错误后需要弄清楚什么是正确的修复,如果我们有一个 DL 模型,它可以向程序员提出修复建议,就可以大大减少代码的维护时间。
如果我们想构建一个可以自动修复错误的系统,如下图,那么首先给定一个错误的程序,然后需要一个可以debug的 DL 模型来确定错误语句是什么,得到一个潜在错误语句的列表,之后需要另一个模型来建议这些错误语句的修复是什么,然后应用这些修复并验证修复是否正确。而代码中的错误可能属于不同的类别,并不是所有的错误都很容易自动修复,例如在语义层面或程序逻辑上存的错误,它们的自动修复将更加困难。
我们这里要讨论一篇论文叫DeepFix,他们使用深度学习构建了一个程序修复系统来修复常见的编程错误,这些错误不是程序逻辑问题,而是可能由于不熟悉特定编程语言导致的,比如缺少代码块分隔符、不兼容的运算符,和丢失变量声明等等。这里你可能会质疑为什么我们需要一个单独的系统,毕竟编译器会报告代码中有错误,然后开发人员可以去修复对应的部分。主要是两个原因,一是编译器错误通常不直观,二是,如果一个工具可以自动修复它,可以节省开发时间。
DeepFix中将这个问题建模为编码-解码器模型,输入是错误问题代码,并试图在解码器中生成一个修复后的代码。他们使用的 DL 模型非常简单,是一个基于attention的sequence to sequence模型。但这个任务中的关键不仅仅是模型,还需要保证修改后代码的正确性。在基于 nlp 的人类语言机器翻译或总结任务中,输出结果中的一些小错误,比如没有得到确切的单词或短语,并不十分重要,因为在许多情况下翻译结果的接受者可以从上下文中推断出含义。但程序修复后的接收者是编译器,因此修复后的代码必须完全正确才可以顺利编译,这意味着如果我们要预测修复程序,必须确保所有生成的输出token完全正确,这是一个非常高的要求。
我们看一下上面的DeepFix框图,他们对输入程序进行建模的方式是一个元组,包括一个行号和一个语句,然后是seq2seq代码模型,它会输出一个建议的修复,包括一个行号和修改后的语句。后面的Oracle我们可以看成一个预测单元,它会决定是否接受修正,然后反馈给输入。这是因为,你的程序可能有多个错误,所以当模型第一次提供一个修复建议,如果Oracle接受,那么你需要迭代地完成这个过程,直到代码中的所有错误都得到修复。DeepFix论文中实际上使用编译器本身作为 Oracle ,对于一条修复建议,它可以检查使用该修复和未使用该修复的代码获得的编译器错误消息的数量,如果修复后的编译器错误较少那么就接受修复,如果它有更多的错误则拒绝修复。另外他们还有一些额外的启发式或约束,比如修复语句中的标识符和关键字的名称应该与原始语句中的内容相匹配,这些都是为了保证修复后的代码的正确性。
DeepFix的数据集是学生编写的 c 程序,其中有一些代码是错误的。但由于这些样本没有多少,所以他们也使用了数据合成的方式,在正确代码中合成错误,这是我们在DeepBugs里也提到过的。
对于原始数据集,它们可以修复接近 32% 的错误,对于合成数据集,它们可以修复到63% 的错误。因为这篇论文讨论的也是非常常见的或表面级的编程错误,还有很多剩下的工作就是思考如何推断程序逻辑并找到可以自动修复逻辑或语义错误的系统,这仍然是一个开放的问题。
3. 代码搜索
当我们很多时候想要编写特定的代码或功能时,很多情况下会查找 github 或stack overflow,看有没有满足我们需求的代码片段,这是一种代码搜索。例如我想读取存在于 xml 对象中的数据,理想的情况我会在搜索引擎输入查询“如何正确读取 xml 对象”,然后返回一个给我满足要求的代码片段。对于使用AI技术来完成这件事时,我们首先需要思考的问题时,类似的 nlp 任务是什么?这其实是一个标准的信息检索问题,给定一个query我们希望得到一组搜索结果,但不同之处在于,当考虑标准 nlp 中的信息检索问题时,query和结果都在相同的语言空间中,它可以是英语也可以是中文,但都是人类语言。而在代码搜索中,query实际上是自然语言,输出应该是编程语言,这两个的领域是不匹配的。我们输入的query和代码片段之间的表面级别的文本相似性非常少,所以如果使用任何基于文本的相似性方法,在我们的任务上不会表现得很好。
我们要介绍的解决方法来自一篇论文Deep Code Search, 他们试图为代码和自然语言query构建统一的表示,在给定一段代码片段和该片段的相关描述的情况下将它们都投影到单个向量空间表示,这是有效解决此问题的关键。
我们可以从上图了解它的流程。首先从公共代码存储库中获取一段代码和它的注释,注释是对该方法相关的描述。训练样本将包含三个元素,一个是代码片段,一个是该代码的描述,还有一个随机的错误描述。这样可以获得一对相似度,一个是代码片段和正确描述之间的相似度,一个是代码和错误描述之间的相似度,通过ranking loss最大化正确相似性并最小化错误相似性。通过这种方式训练深度学习模型,即图中的CODEnn,详细结构如下图,它包括一个代码嵌入网络 (CoNN)和一个描述嵌入网络 (DeNN),以及相似性度量模块。源代码和query的表示方法可以根据特定的任务而改变。
在推理时,我们为代码库中的所有代码创建这些代码嵌入,当一个新的query进来时,使用描述嵌入模型来构建一个嵌入表示,然后为该query找到 K 个最近邻代码表示,就是向量空间中的代码片段,返回一个代码段的排名列表。强迫代码和query进入同一个的向量空间能更好地捕获它们的语义相似性,这是本文成功的一个关键因素。
4. 代码克隆检测
我们介绍的最后一种应用是代码克隆检测。代码克隆的出现是因为我们经常从公共代码库中拉取代码,然后根据自己的特定需求对代码进行微小的更改,也导致了在代码未授权情况下存在代码抄袭的情况,在软件工程中是一个重要问题。我们按照程度分成四种情况的代码克隆:
1)除了空白、布局和注释之外,代码片段相似
2)语法相同,但标识符名称和文字不同
3)语法相似,但在语句级别可能有所不同,添加、修改或删除了一些语句
4)功能相同但语法不同
其中检测前两种非常容易,可以使用标准的基于文本的相似性检测,第三种稍微难一点,可能需要将输入表示为树结构。最后一种是最难的,因为它需要了解什么是语义,然后正确地表示它,这正是基于深度学习的代码技术表现良好的地方,因此可以使用深度学习模型来识别代码克隆。
我们这里引入一篇论文:Deep Learning Code Fragments for Code Clone Detection,它使用端到端的方法来进行代码克隆检测。通过它,我们再次讨论一下上面的三个问题。对于类似的标准 nlp 中的问题,我们有释义检测或重复文本检测,所以这些领域的所有技术和方法都可以用于代码克隆检测;第二个问题对于训练数据,他们是从公开代码库中收集代码,通过遍历 AST 收集相同方法的代码,并进行标注。关于代码表示,他们使用循环神经网络(RtNN)获得词法级别的表示。在句法级别,使用递归神经网络(RvNN)在不同级别对完整二叉树进行建模,并将二者结合起来构建一个表示,传递给深度学习模型,将其视为标准释义检测任务来做。网络的细节我们就不展开介绍了,有兴趣可以看论文。
所以这篇论文是结合表面水平的词汇相似性和句法相似性,在检测克隆方面取得了进展,而不必生成确切的代码片段。到目前为止,我们介绍了四种AI for code的应用方向,其实还有很多其他应用,比如自动API搜索、代码总结等等,我们都可以按照我们一直提到的三个问题来思考具体的任务,然后找到合适的解决方法。从上面我们可以感觉到这个领域的重要问题是训练数据的获取,很多任务都是因为训练数据的缺少而没法进行,比如更深层次的自动debug等等,因此能够推动这个领域迈一大步的条件除了需要更新的技术、更新的模型,还有构建一个丰富的数据集和benchmark,就像图像中的ImageNet、视频中的Kinetics或者NLP中的GLUE一样。但和这些任务不同的是,对代码进行标注是需要专业级别的知识的,并且不同的任务需要不同的标签,因此效率和成本都很受限。同时我们也需要更合适的评估指标,最好能体现代码语义的提取能力。正因如此,半监督、无监督的方向也是可以深入探索的。
另外一个重要的问题是如何保证正确性,这对于代码生成类的任务十分重要。人类认为更简单的任务(比如命名约定)对机器来说却是困难的,我们上面提到的DeepFix中将编译器作为最后的决定工具是一种方向,但还不够精确,这仍然是一个开放的问题。总而言之,在AI for code这个领域里还有很多可以研究的空间,这也是我们继续前进的动力。
到此为止,我们整个系列就告一段落了,我们分别对代码建模、编译优化和智能编程进行了一些方法的介绍,这是一个正在发展的领域,希望大家能够了解如何将深度学习用于编程语言并解决许多有趣的问题。可能以后AI真的能帮助我们接管编程的工作,可能失控玩家电影里的情景在不远的将来就会实现~。
原文链接:https://zhuanlan.zhihu.com/p/410626458