ChatGPT技术解析之:GPT写代码的能力从何而来

5,614 阅读7分钟

【本文是ChatGPT技术解析系列的3篇,后面会马不停蹄更新“算法”和“大模型训练”的内容,感兴趣的朋友可以持续关注~】

第一篇:ChatGPT技术解析系列之:训练框架InstructGPT
第二篇:ChatGPT技术解析系列之:GPT1,GPT2与GPT3
第三篇:ChatGPT技术解析系列之:GPT写代码的能力从何而来

在之前的介绍中,我们已经知道ChatGPT基本沿用了InstructGPT训练框架(见上图),不一样的地方在于:

  • ChatGPT使用GPT3.5替代InstructGPT中的GPT3
  • ChatGPT在训练数据集上做了更新,以便做指令微调(Instruction Tuning),也就是让模型更好理解人类意图。

这些模型间的关系见下图:

需要说明的是,根据openAI官网介绍,GPT3.5是一个系列模型,也就是保持基本训练框架不变,用不同的数据做指令微调,会得到不同的模型,这些模型都叫做GPT3.5。我们为了简化,就不罗列不同版本间的迭代关系了。只需要记住核心一点:上述中的所有模型,都是以GPT3的模型架构为准,通过变换训练数据做指令微调,或引入RLHF(Reinformcement Learning from Human Feedback)得到的

本文将重点关注图中的Codex,来介绍ChatGPT是如何拥有编写代码的能力的。本文中介绍的是初代Codex,只具有编写Python的能力。后续的迭代版本中对训练数据做了改进,直至引入ChatGPT时,能编写多种语言代码,也拥有更高的准确率。在这过程中,算法层面的设计思想,是基本保持不变的。

本文涵盖的主要内容如下:

一、Codex整体设计

二、Codex评估方式

三、Codex的训练数据与训练方法

四、模型效果

五、总结和参考

一、Codex整体设计

(1)训练数据:从github上爬下小于1MB的python文件,去除掉那些可能是自动生成的、平均每行长度大于100的、最大行长度大于1000的、几乎不含字母数字的。经过清洗处理后,最终得到159GB的训练集

(2)预训练:将清洗过后的数据集送入GPT3架构的模型中,重新训练一个模型。注意这里不再是基于GPT3做微调,也不再使用GPT3训好的权重。而是整个重新训练。最终得到一个12B参数量的模型Codex。

(3)有监督微调:为了解决训练数据和评估数据间的的gap(下文会细说),需要新的训练数据做微调。

格式上,这批数据需要满足:

  • prompt + completion形式。prompt中包含函数签名(def部分)和函数注释,completion即代码的主体,是根据注视要求写出的代码,也即监督训练的文本标签。
  • 单元测(unit tests) 。用于测试代码是否准确。这也是代码文本和文字文本在评估方面的主要差异,下文会展开说。

来源上,这批数据来自:

  • 算法竞赛或者面试网站。 Leetcode类型,相信大家都不陌生,其中也涵盖了单元测。共收集1w条数据。
  • Github repo中,使用travis和tox的那些脚本,因为这些脚本一般都用来做持续集成(Continuous Integration, CI) 。持续集成这个概念常见于敏捷开发中,也就是我先写好单元测试,布好虚拟环境。等你的代码一提交上来,我就对代码进行测试,确定它是否能无误合并到主分支上。这样的脚本里通常蕴含待测试的函数和单元测,非常符合微调数据需求。共收集4w条数据。

经过微调后,最终得到Codex-S

二、评估方式

2.1 评估数据集

传统的文本评估中,我们采用BLEU分数,来比较模型产出的结果,和标准结果间的相似度。

但BLEU不适合做代码结果评估,因为代码正确与否,看得不是和标准答案间的相似程度,而是要看这段代码能否通过单元测,正确运行。

因此,Codex在评估时,构造了全新数据集HumanEval。它的组成为:

  • 函数签名(function signature,即def部分)
  • 函数注释(docstring)
  • 函数主体
  • 单元测

为了防止模型在训练时看到过类似题目,HumanEval中的全部数据都是人类亲自构造的。虽然不能保证算法题目具有创新性,但是整体表达,特别是注释表达上,是全新的。这套数据集一共有164个题目。

示例数据如下,白色部分是输入给模型的数据(prompt),黄色部分是期待模型给出的答案(completion)。

2.2 评估标准pass@k

在人类写代码的过程中,会经历一遍遍debug到最终写对的过程。因此,我们也模拟这个方式,来评估模型产出的代码是否正确:

对同一个问题(prompt),我让模型产生k个答案,只要有1个通过单元测,我就认为模型做对了这道题。

基于这一标准,我们给出一个量化的评估指标pass@k:

pass@k:=Eproblems[1CnckCnk]pass@k := E_{problems}[1-\frac{C_{n-c}^{k}}{{C_{n}^{k}}}]

其中:

  • nn :对同一个问题,让模型产生n个答案。在论文中,取n = 200

  • kk :从这n个答案中,随机抽出k个。在论文中,取 k<=n

  • cc :模型产生的n个答案里,通过单元测的答案有c个。

  • CnckCnk\frac{C_{n-c}^{k}}{{C_{n}^{k}}} :从模型产生的k个答案里抽取k个,这k个答案全部错误的概率。

  • pass@kpass@k 整合起来,这个指标表示,从模型的答案中随机抽取k个后,能从这k个里得到正确答案的概率

可能有人会问,你这怎么又n又k的,这么麻烦呢。不如对同一个问题,都让模型直接生成k个答案,去里面找有没有正确的就行了。何必又从n里抽k呢?

这样做的原因是,当k越大时,模型更有可能产出正确的答案。因此研究时,也要考虑k对评估指标的影响。为了方便,干脆让模型对同一个问题,一次性生成n个答案,我们再从中评估不同k值大小的影响就行。

pass@kpass@k 在工程计算时,又会产生新的问题:CnckCnk\frac{C_{n-c}^{k}}{{C_{n}^{k}}} 这一项在展开计算时,涉及到多次阶层计算,数字会非常大,引起精度上的不稳定。因此在实际写代码时,我们需要对这项化简,尽量减少阶层运算次数,化简过程如下:

1CnckCnk=1(nc)!k!(nck)!k!(nk)!n!=1(nc)!n!(nk)!(nck)!=1i=nc+1n1ii=nck+1nki=1i=nc+1n1ii=nc+1n(ik)=1i=nc+1niki=1i=nc+1n(1ki)\begin{aligned} 1-\frac{C_{n-c}^{k}}{{C_{n}^{k}}} &= 1-\frac{(n-c)!}{k!(n-c-k)!} * \frac{k!(n-k)!}{n!} \\ & = 1- \frac{(n-c)!}{n!} * \frac{(n-k)!}{(n-c-k)!}\\ & = 1-\prod_{i=n-c+1}^{n}\frac{1}{i}\prod_{i=n-c-k+1}^{n-k}i \\ & = 1-\prod_{i=n-c+1}^{n}\frac{1}{i}\prod_{i=n-c+1}^{n}(i-k) \\ & = 1-\prod_{i=n-c+1}^{n}\frac{i-k}{i}\\ & = 1 - \prod_{i=n-c+1}^{n}(1-\frac{k}{i}) \end{aligned}

写成numpy代码就是:

2.3 评估阶段的模型输出

(1)停止条件

评估阶段,我们不能让模型一直无条件地生成代码,必须给定一个停止标志。当模型遇到'\nclass','\ndef','\n#','\nif','\nprint'时,停止输出。这里碰到'\nif'停止,主要原因可能是HumanEval数据集里,所有的if...elif...else的条件都放在一行进行表述。'\nclass'和'\ndef'则表示开启了一个新类/方法。遇到'\nprint'停止则是防止开始产生废话。

(2)输出采样

我们会发现一个现象:给GPT类的模型输入同一个问题,得到的答案可能是不相同的。因为GPT往往通过采样的方式,决定token的产出结果,而不是固定取softmax算出的最大概率token。在文字文本里,可以通过Beam Search等方式做到概率采样,而Codex中使用的是一种叫Nucleus Sampling的方法,具体过程是:

  • 在每一个timestep,把词的概率从大到小排列
  • 从概率最大的词开始,依次取词,直到取出词的概率总和>=0.95为止
  • 在取出的词中,按概率进行采样,得到最终的该timestep上的词。

Nucleus Sampling的优点在于:

  • 尽量让生成的结果具有多样性。即每次取概率最大的词,在代码层面上并不能保证最终结果是最优的。
  • 排除掉那些特别不靠谱的词。(定>=0.95这一准则的原因。)

三、训练数据与训练方法

3.1 训练数据

在2.1中,我们介绍过训练数据,这里我们额外提一下本文是code时的token表示办法。

在代码中,会出现换行、缩进、空格、冒号等含有特殊代码意义的表达方式,这些与它们在文字文本中的含义不相同。因此在训练Codex时,用了特殊的token embedding来表示这些符号。下图显示了一段代码在GPT3与Codex的token embedding下的表达方式。每行中用的不同颜色分别表示不同token,感兴趣的朋友可以在platform.openai.com/tokenizer中自…

3.2 预训练模型

前文说过,Codex模型是基于GPT3的。

最开始,Codex试过直接在GPT3上fine-tune,但是发现效果并好不好。因此才确定了用GPT3的框架重train的方案,训练参数如下:

  • Learning rate:和GPT3一样,采用warmup的方式,175 step linear warmup + cosine learning rate
  • tokens:100B
  • Adam optimizer,,β1=0.9, β2=0.95, ϵ=108, weight decay coefficient = 0.1\beta_{1}=0.9, \beta_{2}=0.95, \epsilon=10^{-8}, weight decay coefficient = 0.1

3.3 有监督的微调

到这一步,我们知道对于Codex:

  • 训练数据,是从github上爬下来的python代码。
  • 测试数据,按照“函数签名 + 函数注释 + 代码本体”构造的。

训练数据和测试数据的形式上有显著差异,并且我们最终的目标是,希望模型能够根据我们的指令来写代码(类似于根据注释写代码)。因此,我们需要引入和测试数据相似结构的数据,做有监督的微调。在第一部分里已介绍过用于微调的数据,这里我们就不赘述了。

四、模型效果

4.1 GPT3 VS Codex VS Codex-S

横轴表示模型的参数量,纵轴表示模型产生的代码的准确率。

  • GPT3 pass@1:原始GPT3模型,可以发现它的准确率为0
  • Codex pass@1:只经过预训练,在12B参数的情况下,模型准确率为28.8%
  • Codex-S pass@1:预训练+fine-tune,在12B参数的情况下,模型准确率为37.7%
  • Codex-S mean logp reranking:允许模型生成100个答案,并通过计算最大mean logP的方式,选出1个答案,模型准确率为44.5%。
  • Codex-S oracle reranking:允许模型生成100个答案,并通过单元测的方式,选出1个最佳答案,模型准确率为77.5%。

虽然我们不可能要求每次都以单元测的方式来给出最佳答案,但从logP的方法上,已能说明当前的模型可以解决44.5%的代码问题。并且随着模型参数量的增加,准确率依然有上升的趋势。即如果切换到一个更大的模型上时,还能表现得更好。这就是再后面的工作了。

五、总结

1、ChatGPT具备写代码的能力这件事,可以追溯到Codex。Codex的训练目标是,给定文字描述,模型生成符合描述的代码。

2、Codex是在GPT3的框架上,采用预训练+有监督微调的方式,重新train出来的新模型。

预训练阶段的训练数据为github上爬下的python文件。微调阶段的数据则是算法题+github上持续集成脚本中的函数,以及这两者的单元测。

3、代码不同于文本,不能只用相似度衡量。因此传统的BLEU评分对Codex评估不再适用,转而使用pass@k。

4、随着模型增大,训练数据增加,Codex依然有提升空间。

附注:本文正在参加 人工智能创作者扶持计划

参考

1、arxiv.org/abs/2107.03…

2、platform.openai.com/tokenizer

3、openai.com/blog/chatgp…

4、platform.openai.com/docs/model-…