【本文是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[1−Cn−ckCnk]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个。
-
Cn−ckCnkfrac{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 在工程计算时,又会产生新的问题:Cn−ckCnkfrac{C_{n-c}^{k}}{{C_{n}^{k}}} 这一项在展开计算时,涉及到多次阶层计算,数字会非常大,引起精度上的不稳定。因此在实际写代码时,我们需要对这项化简,尽量减少阶层运算次数,化简过程如下:
1−Cn−ckCnk=1−(n−c)!k!(n−c−k)!∗k!(n−k)!n!=1−(n−c)!n!∗(n−k)!(n−c−k)!=1−∏i=n−c+1n1i∏i=n−c−k+1n−ki=1−∏i=n−c+1n1i∏i=n−c+1n(i−k)=1−∏i=n−c+1ni−ki=1−∏i=n−c+1n(1−ki)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, ϵ=10−8, weight decay coefficient = 0.1beta_{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依然有提升空间。
附注:本文正在参加 人工智能创作者扶持计划
参考
2、platform.openai.com/tokenizer
4、platform.openai.com/docs/model-…