作者:郝逸洋
整理:王子彧
近一年,大型语言模型(LLM)对序列信息建模的能力有目共睹,创建了像 ChatGPT、GPT-4 这样惊人的产品。如果 AI 作为操作系统可以直接控制硬件,程序员就能解放双手去编写驱动、操作系统和软件或是研发新的硬件,这就是软件 3.0 的图景。
年 3 月 25 日,在CSDN 与《新程序员》联合主办的“新程序员大会(NPCon)——AIGC 与大模型技术论坛”上,aiXcoder 联合创始人郝逸洋围绕如何利用 LLM 技术辅助代码开发为我们带来精彩分享,并对交互式多模态LLM(如 ChatGPT、GPT-4 )如何应用于智能化软件开发展开畅想。
直播回放地址:/room/csdnlive5/CCEAhsEs
aiXcoder 联合创始人 郝逸洋
年以前:专注开发代码生成模型
在 年以前,我们专注于开发代码生成模型。当我们设计这个模型时,我们研究了产业界和学术界已有的产品和模型,发现它们的本质做法都很相似,可能并不像之前所讲的那么复杂。如 Xcoder、清华的 CodeGEEX、华为的 CodeArts Snap 以及阿里云的 Cosy 等都是属于国产品牌。此外,还有一些来自美国、以色列和加拿大的品牌。其中,最具影响力的可能是OpenAI 的 Codex,用其为底座做出的 GitHub Copilot,这个模型是我们所有人都需要追赶的目标。
在 年以前的代码生成模型中,我们主要采用语言模型的方式对代码模型进行训练。我们将代码视为自然语言,以程序续写模型的方式进行训练。每个产品之间可能存在小细节上的技术差异,例如激活函数的选择或模型的最终结构。此外,在训练数据的选择上也可能存在差异。
GPT-4 时代:直接向程序提要求
“在 年之前,代码生成模型主要采用语言模型来训练代码模型。而在 GPT-4 时代,一切都改变了。我们可以直接向程序提出需求,就像产品经理向程序员描述需求时一样。”
如图所示,GPT-4 是基于 GPT-3.5 开发的,而 GPT-3.5 又是基于 GPT-3 开发的。GPT-3 是一个 1750 亿参数的模型,它从互联网上抓取了大量网页作为文本数据,其中包括大量的自然语言和代码语言的数据,例如代码文件和自然语言与代码混合的数据。它将自然语言和代码衔接在一起,这很关键。
实际上,代码本身也包含很多自然语言,例如注释。在网络上爬取数据时,CSDN 就是一个很好的数据源,它包含了很多自然语言对代码的描述,包括项目配置等信息。虽然作为博客,它可能篇幅有限,不能包含很大的项目,但它确实能够像罗塞塔石碑一样,将不同的自然语言、代码和配置信息连接起来,使模型能够学习这种关系。这些数据最终使得 GPT-3 本身具备一定的代码生成能力。
再经过接下来的指令微调和 RLHF 人工标注的强化学习,我们可以训练出一个质量较高的问答模型,它可以理解你的问题并生成一个有质量的回答。这是 OpenAI 未来的工作之一。
场景一:GPT-4 在代码生成方面有很强的能力
“当 ChatGPT 升级至 GPT-4 时,它可支持更长的序列,并在更多领域上进行了微调。“ 例如在竞赛题或面试题问答场景中,问一个程序关于经典算法的问题,它会给你一个完整的解答。虽然在网上可以爬取一些数据,但是数据量并不多。若想在此问题上获得好的效果,需要专门去寻找一些竞赛数据来训练模型。像 Leet Code 就是一个很好的数据源,它包含了许多经典算法的解答和良好定义的问题。此外,许多算法竞赛也会提出一些难点问题。通过将这些数据输入模型并进行微调,可以得出相比于 GPT-3,GPT-4 的求解能力有了巨大提升。此外,GPT-4 还有一个未开放使用的能力,即理解图片,但目前还未对外开放使用。”
在典型的代码生成场景中,给它一个简短的任务,让它去检测一个文件是否存在,并且说明是日志文件。如果存在,就输出一个信息,退出程序。给它一个空的模板,让它在模板中填充实现内容。最后,GPT-4 以一种完美格式给出了一个完整的代码实现,并且对这个代码实现做出了一个解释。
有趣的是,在代码实现中,有一个 logFilePath 变量。并未告知它需要检查什么文件路径,但是它生成的样例中,写的是 path to your log file,指向日志文件的路径,是一个占位符的形式生成的。后面还有一个注释,设置日志文件路径,这是 GPT-3 无法实现的效果。因为 GPT-3 是用已有的文件的代码进行训练的,训练后已有的文件里面都是写好的代码。这种情况只有在明确告诉提问者并明确说明的情况下才会出现。
这是 GPT-4 非常独特的能力,是通过 RLHF 的方式训练出来的。开发者给它一个好的样例让它学习、模仿。在多次采样得到的多个结果中再去选择,并进行分类器的训练,再利用分类器进行强化学习。因此,它的训练模式发生了改变。
场景二:GPT-4 代码错误检测与修复
”为了进一步对 GPT-4 的能力边界展开探索,我对代码进行了错误检测和修复,这个功能非常强大,也是编程神器 Copilot 还不具备的功能——它只是一个代码补全模型。“
如图所示,通过给出有问题的单例模式的代码,询问这个代码的问题是什么?它不仅给出了解答,而且给出了一段修复好的代码。
场景三:GPT-4 优化代码
如图所示,在循环了100万次往一个 release 里面放东西。原本期望的结果是,它先对 released 进行一个初始化,再往里面放东西,这样可以加快速度。
但 GPT-4 的反馈结果出乎意料,它发现了其他问题。第一个问题是不应该做字符串的拼接,因而它使用了一个string builder 来代替字符串拼接。第二个问题是不应该在循环里面频繁地往命令行上输出东西,因为频繁地调用命令行会使程序变慢。因此,它使用了一个 string builder 来存储所有要打印出来的东西,最后一口气打印。这种方法确实可以提高效率,但需要消耗更多的内存将这些东西保存下来,并且需要实时查看结果以确保进度正常,如果中间发生错误,就会失去意义。
场景四:GPT4 理解代码中的对应功能
这段代码比较复杂,主要是用来检测文件的编码格式。用注释包裹了一段代码,再询问 GPT-4 这段代码的意思,它给出了一个详细的解答,不仅解释了函数的作用,还解释了语法结构的含义。这表明 GPT-4 在代码领域非常全能,能够应对各种不同的场景。
值得一提的是,GPT-4 是基于 GPT-3 逐步训练出来的。因此可以猜测它具备这种能力的根源可以追溯到 GPT-3,而不是完全依赖微调和预训练语言模型去学习新的代码信息。
如图所示,用 GPT3 进行实验。同样的一个有问题的代码,询问 GPT3 这段代码的问题是什么?它并没有给出回答。这是因为 GPT3 是续写模型。如果加了一个 answer:,那么它就会输出答案。
而在 GPT-4 出现后,这类续写模型也就随之消失了。
如何处理代码中的问题?
第一步:把问题转化成一个自然语言描述。
第二步:将问题抛给GPT-4。
第三步:提取结果中有用的部分。
上述流程已有 GitHub 把它进行产品化,生成了 GitHub Copilot X。除了上述的四种场景,它还对多次代码提交进行汇总,根据提交记录生成一个描述,整理成一个故事。并且进行文档搜索。
代码纠错
相比于大型的模型,aiXcoder 的参数量较小,但它的功能却非常实用。它可以在用户敲代码时,实时检测并纠正一些常见的拼写错误或者语法错误,提高编码的准确性和效率。这一点,GPT-4 却难以做到。
GPT-4 的现有短板
GPT-4 反映出的第一个问题就是它速度较慢,因此无法响应很多实时场景;
第二,上下文序列有限(最长 32,768 ),尽管 GPT-4 的序列长度已经非常厉害,但是想要将整个项目的信息全部放入其中仍然非常困难;对于一些复杂的业务逻辑和场景,GPT-4 等自然语言处理模型可能会存在一定的局限性。这些模型虽然可以理解自然语言的意思,但是无法理解具体的业务场景和逻辑,也无法获取特定的业务数据和代码实现细节。
第三,代码项目的上下文和网页爬取的文本差距巨大。对于一个完整的项目来说,它涉及的信息和知识点非常多,从数据库结构到依赖库的版本,再到各种配置文件和代码实现细节,需要综合考虑和理解才能完成项目的开发和维护。
程序生成模型与语言模型的区别
同时,程序生成模型与 GPT-4 此类语言模型的模式仍有区别。
在实际场景中,写代码所依赖的信息量非常多。除了当前文件的上下文信息,它还依赖整个项目中其他文件的信息甚至其他项目的信息。因此,这是个需要独立设计的问题,普通语言模型很难处理如此复杂的需求。
第二大区别则体现交互模式。在创建程序生成模型时,一般有几种模式可供选择,包括填空、补全和排序,而对话语言模型其模式则是“续写”与“问答”。“填空、补全和排序”三种场景都对实时性有要求,如果速度过慢,开发人员是难以接受的。
第三种就是填空。在填空的场景中,需要根据前序操作和后续操作的信息来选择合适的操作填入其中,同时需要考虑填入操作后整个流式操作的逻辑是否正确,以及是否会影响后续操作的执行。
然而,这种三种场景下都对实时性有要求。
如何设计插入任务?
填空任务设计的目标就是关注前文、后文,再进行生成。
方案一:区间中随机
随机在文件中找一个 1024 长度的区间。在其中随机找一个起点和一个终点,中间的部分作为 SPAN。前面的算上文,后面的算作下文。但效果并不好。因为 SPAN 长度被限制住,不会超过1024。
方案二:允许SPAN超过当前区间
如果允许 SPAN 超过当前序列长度,模型无法获取足够的上下文信息,可能会导致预测结果不准确。但如果限制 SPAN 长度,又可能导致模型无法处理复杂的填空场景,无法充分发挥模型的能力。
方案三:强制保留一部分下文信息
这类方案比一个普通的语言模型,甚至一个填空的语言模型设计来说要复杂得多,但仍然存在一个问题。比如我们要预测左边的代码,左边代码的例子就是下一行是给当前的一个局部的成员去赋一个值,但是这个值是不存在的。我们希望模型能够生成并初始化该成员的值。
实际上,模型很可能会输出类似如右图这种情况,因为它会认为左边这种情况非常奇怪。这个结果会使当前函数直接结束,再新起一个函数,将下面看起来不相关的内容包含进去。这种方法的一个结果是,你很难事后去补救这个问题。因为右边生成的代码概率非常高。相比于左边用户想要的东西来说,无论如何采样,最终都会得到类似右边的结果。
因此,在数据生成的任务设计时,需要考虑上下文逻辑的连贯性。
aiXcoder 路线图
aiXcoder 是以插入模型为主的工作方式。
要实现一个1750亿参数的模型,我们需要加入大量的自然语音的理解能力。此外,我们还需在不同的代码场景下进行大量的指令微调和场景设计。因此,需要加入大量无法通过简单爬取获取的数据,例如整个项目的数据和依赖信息。加入数据后,我们再进行预训练,进行微调。最后,解决超长序列的依赖性问题。
目前,我们团队正在努力提高模型的序列长度,内部测试的一个模型已经达到了 8000+ 的序列长度,并且在一块 40 GB 的 A100 显卡上可以运行。我们计划通过并行计算的方式,让模型能够更快地找到超长序列的依赖关系,从而进一步提高序列长度。
如何测评一个代码生成模型?
对于不同代码生成模型,需要有一个评测的标准。
首先,代码生成模型本质上是一个语言模型。当给定一个前序时,模型会计算下一个词出现的概率。这个概率是在一个字典表上计算的,比如一个大小为 5 万的字典表,会生成 5 万个概率。这些概率求和后,会产生一个概率分布。如图所示,输入 "the" 后,模型会计算下一个词的概率,最高的概率是 "nice",其次是 "dog" 和 "car"。如果告诉模型前两个词是 "the car",那么下一个词的概率会是什么。会发现概率最高的是 "drives",这就是概率表的作用。
除此之外,一个最直接的代码模型的衡量标准就是准确率。下一个词如果正确算 1,下一个词如果错误算 2。方法很简单,但并不是一个好的衡量方式。
因此,出现了 Perplexity 困惑度这一指标。它根据模型生成的概率值来计算一个困惑度值,如果预测完全正确,困惑度值为 1。如果预测完全错误,困惑度值会变为无穷大。
Perplexity 并不是一个好的指标,因为代码的语序是不唯一的,同一段代码可以有多种实现方式。因此,我们从自然语言翻译领域借鉴了一个评价指标叫做 BLEU(Bilingual Evaluation Understudy),它将每个单词的正确性和连续出现的词语视为一个翻译的正确性,最终将这些分数加起来,分数越高,评价指标就越高。
在代码中,上述指标都不太适用。代码与自然语言有着很大的区别,因为代码中的算法和结构是多样的,变量名也可以随意定义。但这些因素并不影响最终程序的实际效果。因此,我们最终提出了一个唯一的评估指标,那就是代码的正确性。如果我们编写的代码能够通过相应的单元测试,那么就是合格的;如果不能通过,那么就是有问题的。
HumanEval
我们可以通过构建一个测试用例集合,包含问题描述和相应的输入输出,然后让模型生成对应的代码。如果代码能够通过测试用例,就算一分,否则就算零分。最终根据通过的测试用例数量来评估模型的性能,通过的测试用例越多,模型的性能就越好。这种评估方法更加贴近实际应用场景,也更加准确。
这种评估方法也存在一些不足之处。首先,其语言单一,只能根据一个前文进行生成,缺乏实际开发场景。其次,它的功能只能实现一些非常独立的功能。此外,该方法包含大量低频需求算法题。
同时,代码中包含大量指令性描述,指导我们如何实现相应的功能。这种方式其实并不友好,因为在实际生成的场景中,我们发现其可用率可能低于30%,即70%的情况下,生成的代码是无法使用的。与此同时,还需要重新改成注释。
第二个问题就是示例。在实际场景中对程序员要求过高。
基于这一点,我们新建了简单的评测标准 aix-bench 和 AixBench-L。但后来使用较少,因为跟实际的开发场景差别太大。
当一个程序生成模型创建完毕,就需要对补全率和生成率两个指标进行测试,评估它的表现能力。自动化测试用户实际开发场景非常难,运用一些手动测试来配合验证可能会更精准。
作者简介:北京大学本科,早稻田大学硕士,毕业后任职微软北京创新工程部语音技术组。 年离开微软联合创办北京硅心科技有限公司,作为 CTO 负责智能编程机器人 aiXcoder 的研发工作。