60行NumPy手搓GPT( 三 )


训练
我们与训练其它神经网络一样 , 针对特定的损失函数使用梯度下降[25]训练GPT 。对于GPT , 我们使用语言建模任务的交叉熵损失[26]:

def lm_loss(inputs: list[int], params) -> float:# the labels y are just the input shifted 1 to the left## inputs = [not,all,heros,wear,capes]#x = [not,all,heroes,wear]#y = [all,heroes,wear,capes]## of course, we don't have a label for inputs[-1], so we exclude it from x## as such, for N inputs, we have N - 1 langauge modeling example pairsx, y = inputs[:-1], inputs[1:]# forward pass# all the predicted next token probability distributions at each positionoutput = gpt(x, params)# cross entropy loss# we take the average over all N-1 examplesloss = np.mean(-np.log(output[y]))return lossdef train(texts: list[list[str]], params) -> float:for text in texts:inputs = tokenizer.encode(text)loss = lm_loss(inputs, params)gradients = compute_gradients_via_backpropagation(loss, params)params = gradient_descent_update_step(gradients, params)return params
以上是一个极度简化的训练设置 , 但是它基本覆盖了重点 。这里注意一下 , 我们的gpt函数签名中加入了(为了简化 , 我们在上一节是把它去掉的) 。在训练循环的每次迭代中:
我们为给定的输入文本示例计算语言建模损失 。
损失决定了我们的梯度 , 我们可以通过反向传播计算梯度 。
我们使用梯度来更新我们的模型参数 , 使得我们的损失能够最小化(梯度下降) 。
请注意 , 我们在这里并未使用明确的标注数据 。取而代之的是 , 我们可以通过原始文本自身 , 产生大量的输入/标签对(input/label pairs) 。这就是所谓的自监督学习 。
自监督学习的范式 , 让我们能够海量扩充训练数据 。我们只需要尽可能多的搞到大量的文本数据 , 然后将其丢入模型即可 。比如 , GPT-3就是基于来自互联网和书籍的3000亿token进行训练的:
当然 , 这里你就需要一个足够大的模型有能力去从这么大量的数据中学到内容 , 这就是为什么GPT-3模型拥有1750亿的参数 , 并且大概消耗了100万--1000万美元的计算费用进行训练[27] 。
这个自监督训练的步骤称之为预训练 , 而我们可以重复使用预训练模型权重来训练下游任务上的特定模型 , 比如对文本进行分类(分类某条推文是有害的还是无害的) 。预训练模型有时也被称为基础模型( ) 。
在下游任务上训练模型被称之为微调 , 由于模型权重已经预训练好了 , 已经能够理解语言了 , 那么我们需要做的就是针对特定的任务去微调这些权重 。
译者注:听上去很简单是不是?那就快来入坑啊(doge)
这个所谓“在通用任务上预训练 + 特定任务上微调”的策略就称之为迁移学习[28] 。
提示()
本质上看 , 原始的GPT论文[29]只是提供了用来迁移学习的模型的预训练 。文章显示 , 一个117M的GPT预训练模型 , 在针对下游任务的标注数据上微调之后 , 它能够在各种NLP()任务上达到最优性能 。
直到GPT-2[30]和GPT-3[31]的论文出来 , 我们才意识到 , 一个GPT模型只要在足够多的数据上训练 , 只要模型拥有足够多的参数 , 那么不需要微调 , 模型本身就有能力执行各种任务 。只要你对模型进行提示 , 运行自回归语言模型 , 然后你猜咋地?模型就神奇的返回给我们合适的响应了 。这 , 就是所谓的in-  ,  也就是说模型仅仅根据提示的内容 , 就能够执行各种任务了 。In- 可以是zero shot, one shot, 或者是few shot的: