AlphaCode 到底是怎么练成的?
春节期间,DeepMind 的编程版 AlphaGo——AlphaCode 一度火到刷屏。它可以编写与普通程序员水平相媲美的计算机程序,在 Codeforces 网站的 10 项挑战中总体排名前 54.3%,击败了 46% 的参赛者。
这一成绩给程序员群体带来了不小的压力,仿佛纺织工被纺织机淘汰的历史正在重演。
那么,AlphaCode 是如何做到如此强大的?在最近的一个 YouTube 视频中,清华大学朱军门下博士后 Tim Pearce 详细解析了 AlphaCode 的系统架构、「解题」原理、训练流程等内容。
原视频地址:https://www.youtube.com/watch?v=YjsoN5aJChA
AlphaCode 到底做了什么?
前面提到,DeepMind 的研究者将 AlphaCode 放在 Codeforces 挑战中进行了测试。Codeforces 是一个在线编程的平台,里面有各种各样的编程题目,各种各样的比赛。它类似于国际象棋中使用的 Elo 评级系统,每周分享编程挑战和问题排名。不同于编程人员在打造商业应用程序时可能面临的任务,Codeforces 的挑战更加独立,需要对计算机科学中的算法和理论概念有更广泛的了解,一般是结合逻辑、数学和编码专业知识的非常专业的难题。
下图展示了其中一个赛题的例子,包含赛题描述、输入输出示例等内容,挑战者的任务就是根据这些内容写出一段代码,使得其输出符合要求。
以下是 AlphaCode 写出的代码:
对于 AlphaCode 来说,这还只是中等难度的挑战。
在类似的十项挑战中,研究者将赛题输入 AlphaCode。然后,AlphaCode 生成大量可能的答案,并通过运行代码和检查输出来筛选这些答案,就像人类竞争对手一样。AlphaCode 论文的联合负责人 Yujia Li 和 David Choi 表示:「整个过程是自动的,无需人工选择最佳样本。」
为什么 AlphaCode 那么厉害?
下图是 AlphaCode 的概念图。这是一个精心设计的系统,主要构建块是基于 Transformer 的语言模型。但从本质上来说,没有一个单独的组件是全新的。
「考场」上的 AlphaCode
我们先来看一下上述系统在测试时是如何工作的。
在解决编码问题时,AlphaCode 使用了一个非常具体的协议(protocol),这个协议决定了整个系统的 pipeline。协议规定,他们可以无限制地使用示例测试用例,因为这些是作为问题的一部分给出的。但在提交给隐藏测试用例时,他们将提交版本数限制在了 10 次以内(至多 10 次)。
在测试时,AlphaCode 经历了三个阶段。
在第一个阶段,他们使用一个大型 Transformer 模型,该模型将问题描述示例测试和一些关于问题的元数据都放在一个字符串中作为输入。然后,他们从这个模型中采样,生成大量的潜在解决方案。所以第一个阶段得到的是 100 万套可能的代码脚本。
在第二个阶段,他们用示例测试用例测试了得到的 100 万套代码,其中 99% 的代码都没有通过测试,可以直接排除。这就将可行的代码套数降到了 1000 个左右(具体数量取决于题目的难度)。
在第三个阶段,他们使用了第二个 Transformer 模型。该模型将问题描述作为输入,但它并没有试图生成代码来解决问题,而是生成测试用例输入(每个问题对应 50 个输入)。也就是说,他们并没有选择生成输入和输出对,而是生成了一些与问题相关的实际输入。所以模型可能要生成字符串、二进制数或数列(具体生成形式取决于问题类型)。
这种做法好在哪儿呢?他们认为,如果两个脚本为所有 50 个测试返回相同的答案,那么它们可能使用的是相同的算法。这就可以避免浪费两次提交机会把这两个脚本都测试一下。所以在第二步得到 1000 套脚本后,他们就根据这 50 个生成的测试输入的输出对脚本进行聚类,然后从每个聚类中选出一个示例脚本,总共选出 10 个。如果这 10 个脚本中有一个通过了所有的隐藏测试,那么他们就成功地解决了这个编程问题,否则就宣告失败。
以上就是 AlphaCode 在测试时的工作原理,其中用到了两个 Transformer 模型。那么这两个模型是怎么训练的呢
AlphaCode 的训练
AlphaCode 的训练分为两个阶段:预训练和微调。这一过程涉及两个数据集:第一个是由各种编程语言组成的公共 GitHub 库,用于预训练,数据量高达 715GB;第二个是从各个编程挑战网站(包括 codeforces)搜集的赛题,用于微调,包括问题描述、测试用例和人类程序员编写的答案。
数据集有了,接下来就是训练了。
在预训练阶段,他们会抓取 GitHub 上的一些代码,然后随机选择他们所谓的「pivot point」。
pivot point 之前的所有东西都将被输入到编码器中,解码器的目标则是重建 pivot point 以下的代码。编码器输出代码的向量表示,后续可用于整个解码过程。
解码器以自回归的方式工作。它从预测代码的第一个 token 开始。损失函数就是预测的 softmax 输出和真实 token 之间的交叉熵。然后,第一个真实的 token 成为解码器的输入,第二个 token 随之被预测出来。在解码器被要求预测出一个特殊的代码结束标记前,这种情况会一直重复下去。现在,这些损失通过解码器和编码器反向传播,但对于编码器来说,增加第二个损失是非常重要的。这被称为掩蔽语言建模损失:你将输入到编码器中的一些 token 留空,作为一种辅助任务,编码器会试图预测哪个 token 被掩蔽了。
预训练结束之后就到了微调环节。在这个环节,他们将问题描述元数据和示例的输入输入到编码器中,试着用解码器生成人类编写的代码。此时可以看到,这与编码器 - 解码器架构所规定的结构非常自然地吻合在一起。这一阶段的损失与预训练时完全相同。而且,这里也有第二个 Transformer 用来生成测试输入。这也是由相同的 GitHub 预训练任务初始化的,但它被微调以生成测试输入而不是代码。
除了上面提到的常规训练步骤和架构,AlphaCode 还从最近的其他论文中借鉴了一些经验。Tim Pearce 指出了其中比较不错的一个:
AlphaCode 的元数据调节
除了问题描述,研究者还总是将元数据作为 Transformer 的输入,包括编程语言、问题的难度等级、关于问题的一些标签,以及解决方案是否正确等。在训练时,模型显然知道这些字段的值是什么,但在测试时,它们并不知道。非常有趣的是,他们可以在测试时向这些字段中输入不同的东西来影响生成的代码。例如,你可以控制系统将要生成的编程语言,甚至影响它试图生成的解决方案类型,比如是尝试动态编程方法还是进行穷举搜索。
他们在测试时发现,当他们对最初的 100 万个代码脚本进行采样时,将很多字段随机化是非常有帮助的。因为,通过增加原始采样池的多样性,正确答案出现的可能性就会增加。
以上就是 Tim Pearce 对 AlphaCode 的全部解析。他认为,DeepMind 的团队在这项工作中的确取得了一些进展。但他比较不解的是:为什么他们在这些编程问题中取得的成就远远不及他们在围棋、《星际争霸》等游戏中取得的超越人类的成果?Tim Pearce 初步猜测是因为编程问题比较难,而且数据比较难以获取,因为在游戏中你可以无限制地产生很多模拟数据,但在编程问题上却不能这么做。