让模型理解和推断代码背后的意图是预训练模型的核心挑战 | NPCon演讲实录
分享嘉宾 | 卢帅
整理 | 王子彧
2023 年 3 月 25 日下午,在 CSDN 与《新程序员》联合主办的“新程序员(NPCon)——AIGC 与大模型技术应用峰会”上,微软亚洲研究院高级研究工程师卢帅受邀出席,并发表了题为《基于预训练的代码理解与生成》主题演讲,与现场开发者围绕智能代码生成展开激烈的观点碰撞。
微软亚洲研究院高级研究工程师 卢帅
以下为卢帅的现场演讲实录。
大家好,我今天分享的主题是《基于预训练的代码理解与生成》。本次分享的内容主要包括微软亚洲研究院团队在过去几年中在预训练模型方面的实践经验,以及如何将预训练模型应用于具体的代码智能任务和场景中。
什么是代码智能?
近年来,代码智能逐渐成为关注焦点,它巧妙地融合了人工智能与软件工程领域。其主要目标在于使模型掌握自动化代码理解与生成的技能,而其核心挑战则是如何让模型理解并推断代码背后的意图。如今,最好的 AI 解决方案便是采用预训练模型。
如何进行代码预训练?
代码预训练的灵感来源于自然语言预训练。将代码语言视为自然语言,其预训练范式非常简单。
首先,我们可以从软件工程领域的开源社区中获取高质量的代码库,例如 GitHub 开源社区。接着,我们可以将这些代码库构建成一个预训练的语料库或数据集。最后,我们将所得到的代码视为一串文本,直接输入到模型中进行预训练,就可以得到一个完整的预训练框架。
之前的 GPT 系列训练、GitHub Copilot 和 OpenAI 提出的 Codex 等常用的代码补全工具都使用了预训练框架,但是这些模型并没有经过较多的优化。
源代码和自然语言有何不同?
如果站在一个软件工程的角度来看,源代码和自然语言有一些区别,通过这套框架所进行预训练,可能只探索到了源代码的冰山一角。
源代码和自然语言的不同之处在于它有非常明确的结构性,源代码可以解析成一个非常明确的抽象语法树,其中包含控制流信息和数据流信息。这些信息可以在预训练技术中让模型学习,从而促进模型对源代码的理解。
源代码还有不同的展现形式。比如带 bug 代码是否会影响预训练模型的输出内容?但是,如果合理运用,预训练模型可能会帮助人们判断代码是否有错误,甚至修复错误。另外,Code Diff 是代码审查场景中广泛使用的形式。如果让预训练模型学习 Code Diff,它也可以帮助一些特定场景,比如代码审查。此外,底层代码如汇编代码可能会让预训练模型学习到代码的最底层逻辑等等。
团队尝试
微软亚洲研究院团队在代码与训练模型上起步较早。
2020 年初,提出了首个将文本和代码结合作为训练的模型——CodeBERT。随后,我们对其进行了不断的优化,例如加入一些代码特有的信息。这些尝试都是基于 foundation models 的探索之一。其次,对一些特定的具体的代码应用场景做了较深入尝试,比如 Code Generation 和 Code Review。最后,为学术界提供了一个 Code benchmark 数据集,方便大家评估和测试自己的模型。
首个代码+文本预训练模型 CodeBERT
代码智能的核心挑战在于如何让模型理解和推断代码背后的意图。注释是我们人类通常使用的自然语言,因此在阅读代码时,如果代码前面有注释,理解它就会更容易。在预训练模型发展到编程语言之前,预训练模型已经在自然语言上获得了很多知识,如 BERT。因此,让模型快速扩展到编程语言上的关键是将编程语言和自然语言连接起来。
在处理源代码时,我们可以提取与之相关的注释,将它们作为一对输入直接输入到模型中。这种做法可以帮助 CodeBERT 模型更好地理解和推断代码背后的意图。这是团队最初的尝试,未涉及代码的特殊性信息分析。在之后的工作中,我们需要将代码的特有结构信息融入预训练模型中,以进一步提高模型的表现能力。
GraphCodeBERT:建模代码变量关系
我们考虑到代码的结构性以及可能蕴含数据流的信息,因为数据流包含了代码当中丰富的语义信息。为了将代码的结构信息融入预训练模型中,我们采用以下具体做法:
首先,将一段源代码解析成其抽象语法树。其次,从抽象语法树中提取出变量序列,并分析变量在语法树上的路径关系,以抽取出变量之间的关系。如上图最右侧图表示在代码执行时,数据流会从哪个变量流向其他变量,这就包含了代码执行的一些语义信息。如果将其融入预训练模型中,可以增强模型对代码的理解能力。
将这些代码结构信息融入预训练模型的具体方法是,在 CodeBERT 的基础上,将变量序列拼接到文本和代码中,并将原来的注意力机制改为只能看到与自己相关的变量或有数据流向的变量的注意力。从而使模型获得数据流信息。由于我们成功地建模了语义信息,GraphCodeBERT 在代码理解任务上的表现要比 CodeBERT 更好。
UniXCoder:统一代码预训练
2022 年,我们提出了统一代码预训练模型框架 UniXCoder。在此之前,自然语言和代码的生成和理解往往需要采用两种不同的范式。对于理解性模型,通常采用纯编码器结构,例如 CodeBERT 和 GraphCodeBERT。如果需要进行生成任务,则需要采用解码器结构。基于这一背景,我们在 UniXCoder 中将这两种范式进行了结合。
具体而言,我们使用不同的预训练任务和注意力机制,使模型能够同时进行代码理解和生成任务。同时,我们也将包含结构性信息的抽象语法树融入到 UniXCoder 的预训练中,以进一步提高模型的表现能力。
在 GraphCodeBERT 时,团队就设想将抽象语法树融入预训练模型中。但当时我们受到了两个问题的限制,其中一个问题至今仍未完全解决。第一个问题是模型所能接受的输入长度是有限的。对于理解性模型来说,输入长度可能为 512 个词,现在可能已经提高到了 1024 个词。因此,我们当时将输入长度提高到了 1024,才能够将语法树结构的信息融入到模型中。因为语法树结构比正常的代码要长 1-2 倍,如果将其全部输入模型,则输入长度就会爆炸。
第二个问题是,transformer 结构的预训练模型无法直接处理树形结构的输入。因此,如果要将树形结构输入模型,就需要将其拍扁。一般来说,拍扁的过程有两种方法:深度优先和广度优先。但无论采用哪种遍历方法,都会丢失一定的信息。为了解决这个问题,我们在遍历时采用了深度优先遍历,并添加了左边界和右边界的特殊标记,使得它在回溯时可以一一对应。这样,可以将抽象语法树与相应的序列一一对应。
在训练过程中,我们将代码和抽象语法树的信息随机输入到模型中,使得模型在训练时既可以学习序列化的代码信息,又可以学习结构化的代码信息。经过多轮训练后,模型就可以同时具备这两种能力。
UniXCoder 在当时的 10 项常见代码智能任务中,包括代码克隆、代码补全、代码生成、代码摘要生成等等任务上,取得了非常不错的结果。
在代码克隆检测任务中,UniXCoder 展现了出色的成果。该任务的定义是从候选代码库中搜索与当前代码具有相同功能的代码。因此,对于代码预训练模型的理解能力要求很高。
UniXCoder 的方法是将每个代码片段通过预训练模型输出为一个向量,并比较向量之间的距离,选择与当前搜索向量最相似的代码片段。
如图所示,与 OpenAI-embedding 相比,UniXCoder 的效果更好,因为 AST 提供了更丰富的代码信息,从而能更好地表示代码。
如何将预训练的模型应用于具体的代码智能任务和场景?
首先,是代码生成场景,我们主要关注的就是代码补全。
GPT-C:多语言代码补全模型
它提供了代码补全的插件,可以在 VS Code 上下载使用。无论是哪种模型,甚至是 ChatGPT,基础的训练框架都是给定上文预测下一个 token 的训练范式。同时,GPT-C 在 10 种编程语言上都可以做代码补全。
微软研究更注重在预训练模型的基础上加入一些优化。这些优化既适用于规模稍小的模型,也适用于规模更大的模型。我们的优化从以下几个方面入手。
优化一:扩展上下文模型技术
首先,一个问题是模型输入长度的限制。GPT-C 当时限制输入长度为 1024,而 GPT-3 和 GPT-3.5 最大支持的输入长度可能是 4096 个 token。需要注意的是,token 并不是词,因为在训练模型时,每个词都需要经过一层词表编码到模型中。词表并不是按每个词来计算的,而是按某种特殊的子词计算的。
GPT-4 发布了两个版本,一个版本支持 8192 个 token。另一个版本暂未发布,它可以支持 32000 个 token,大大提高了输入长度,缓解了这个问题。但是,如果使用相同的训练框架来训练一个完全相同的模型,时间和内存开销将非常大。
因此,我们提出了扩展上下文模型技术。通过限制模型的输入长度,并根据一些手动规则定义优先级,可以将更重要的信息放入有限的输入长度中。这种方法可以在不增加模型复杂度的情况下,提高模型的性能和效率。
首先,需要获取代码的具体语法树。其次,根据一些定义规则,对各语法单元进行优先级的定义。比如,当前待补全函数的签名和注释、所在类的签名、文件 import 和所定义的全局变量等,都可能对当前所写函数有重要的作用。接下来,我们认为当前函数所在类的成员函数或全局属性的优先级可能排第三。
最后,根据优先级获取上下文,将其填满模型输入窗口。这种方法比将上文直接输入更有效,因为它可以确保更重要的信息被纳入考虑范围。
优化二:Grammformer 生成代码骨架
另一个优化方法是 Grammformer,它用于生成代码骨架。这种方法抛弃了从左到右生成的方式,让代码具有非常强的结构性。每一步都从其生成式入手,从而能够保证其语法正确性。这种方法可以解决从左到右生成代码的缺陷,从而保证代码的语法正确性。
具体的结构如上图所示。最下面的两个 expression 最初为空,我们需要选择接下来应该扩展哪个 expression。通过从语法生成式入手,让模型去生成正确的代码结构。不断地循环迭代,直到完成。由于语法树的叶子节点通常是数字、自定义标识符或变量名等,因此让模型进行推荐或留空有很多好处。
留空的好处在于,人在编写代码时很难预测需要定义的变量名是什么。如果模型提供了一个不太好的建议,反而会影响用户的体验。而留空则能够增强用户的体验。因此,在选择扩展哪个 expression 时,我们需要权衡推荐和留空的优劣,并根据具体情况进行选择。
优化三:ReACC 基于检索相似代码的补全模型
“在编写代码时,并不是所有的代码都是程序员自己原创的。”
ReACC 是一种基于检索相似代码的补全模型。它的基本思想是将未完成的代码作为输入,从一个大的代码库中检索出与当前代码在语义上相似的代码,并将其作为生成模型的输入。ReACC 的好处在于,它可以提供更准确的代码补全建议,因为它是基于实际的代码库进行搜索和推荐的。同时,它还可以帮助程序员学习如何编写更好的代码,因为程序员可以看到他人是如何处理类似问题。
在 Python 中使用补全模型可以显著提高编程效率,特别是在处理大量重复代码时。在 Python 上效果最强的 Codex 模型可以从我们的框架中受益,进一步提高其准确性和效率。
CodeReviewer:预训练模型带来代码审查自动化
CodeReviewer 是一个针对代码审查场景的自动化工具,它利用人工智能和预训练模型来优化和提高开发效率。在代码审查场景下,可以通过以下三种任务来使用人工智能或预训练模型来优化和提高开发效率:
1. 评估代码更改的质量:在代码审查过程中,我们需要评估代码更改的质量,以确保代码的正确性、可读性和性能。
2. 辅助生成代码审查意见:在代码审查过程中,审查人员需要提供有关代码更改的意见和建议。
3. 代码优化:在代码审查过程中,审查人员需要对代码进行优化,以提高代码的性能和可读性。
综上所述,代码审查的三个重要的步骤,都可以用 AI 来去提升效率。
如图所示,开发者添加了一个新的函数,需要进行代码审查。如果审查人员反馈的信息量很少,那么开发人员可能无法理解审查人员的意见,也无法及时修复问题。但是,如果使用 CodeReviewer,它可以基于预训练模型和机器学习算法来分析代码更改,并自动生成相关性强、有信息量的审查意见。
大模型时代仍需优化模型结构,覆盖更广泛智能代码场景
GPT 系列的前两代在代码场景中的表现非常有限,只能够完成一些简单的代码补全工作。但是,GPT-3 的推出及其表现引起了广泛关注,GPT-4 更是在参数量上取得了大幅提升,能够覆盖绝大部分的代码场景。
但是,目前 GPT-4 仍然存在一些问题,例如缺少代码语法树等信息、无法对项目级代码进行分析等。此外,个性化模型问题也是未来需要思考的方向之一。
在大型模型时代,GPT-4 的优化方向已经从增加参数量转变为从数据优化和训练方式的角度。但是,当前的研究方向仍然需要继续探索如何让神经网络模型更好地理解程序,以覆盖更广泛的智能代码场景。
卢帅及其团队的下一步研究方向是如何让神经网络模型更好地理解程序,而不是简单地增加模型的参数量。他们认为,未来的发展趋势应该是在探讨如何利用大模型来完成任务的同时,进一步优化模型的结构,以覆盖更广泛的智能代码场景。
点击阅读原文或在CSDN视频号观看『新程序员大会(NPCon):AIGC 与大模型技术应用峰会』直播回放视频。