第九章、GPT1:Improving Language Understanding by Generative Pre-Training(理论部分)
0 前言
前面我们已经对transfoemer模型以及transformers库做了详细的介绍,接下来我们来看看GPT系列的开篇之作,GPT1它提出了生成式预训练模型,也是至今为止大语言模型的常用训练方式。
论文:https://cdn.openai.com/research-covers/language-unsupervised/language_understanding_paper.pdf
代码:https://github.com/openai/finetune-transformer-lm
1 整体叙述
自然语言理解实际上包含许多人物
- 文本蕴含:例 "John is a married man "可以推断出"John is a man "这就是蕴含的关系
- 文本问答:我们现在用的deepseek和GPT都属于问答类的
- 语义相似性判断
- 文本分类
但自然语言处理实际上一直面临着比较大的挑战,以文本分类而言,传统的方法,我们需要先对大量的文本做标签,再利用做好的大量数据集来进行训练。但遗憾的是我们人类发展至今虽然已经有了大量语料库,但大多数数据都是没有标签的,尤其是对一些特定任务而言,这就使得从头开始用符合任务的标签数据做判别训练变得非常困难。
GPT1提出了一种利用未标记的数据做生成式预训练,再在标记数据上做微调的训练范式,并证明这样的方式对于特定任务而言是有效的。
那么,新的疑问就产生了,这个预训练模型结构是怎样的?如何做微调?
在解释这个问题之前,我们来回忆一下CV是怎么做微调的,在许多图片任务中,我们预训练的模型实际上是为了提取模型特征,在根据后续的分类任务,在模型的最后添加一个全连接来做分类。如下:
# 传统方法1:修改模型架构
class TraditionalApproach:def __init__(self, pretrained_model):self.backbone = pretrained_model# 需要添加任务特定的层self.task_specific_head = nn.Linear(768, num_classes)self.dropout = nn.Dropout(0.1)# 可能还需要修改中间层# 传统方法2:大幅修改参数
def traditional_finetuning():# 需要训练大量新参数# 可能破坏预训练的知识# 每个任务需要不同的模型架构
那这种方法的问题是什么呢?首先,显而易见我们修改了模型结构,也许不只最后一层,这会导致模型需要训练大量的新的参数,且可能破坏预训练已经学到的特征。
GPT1是怎么做的呢?
它用到了Task-aware input transformations,看起来很复杂也不太明白到底在做什么。
实际上就是现在我们熟悉的prompt,它不修改模型而是通过修改输入格式来进行微调,如下:
# 不修改模型,而是修改输入格式
def task_aware_input_transform(text, task_type):if task_type == "sentiment_analysis":# 情感分析任务的输入格式return f"Analyze sentiment: {text} Result:"elif task_type == "summarization":# 摘要任务的输入格式return f"Summarize: {text} Summary:"elif task_type == "translation":# 翻译任务的输入格式return f"Translate to Chinese: {text} Translation:"# 通过不同的prompt模板告诉模型要做什么任务
这种方法和传统方法相比,在12个任务中有9个任务在效果上都有显著的提升。
2 训练框架
整体框架分为两步:
- 预训练:根据超大的语料库训练一个大的语言模型
- 微调:根据特定任务的标记数据对模型进行微调
2.1 无监督预训练
1、基础知识(可跳过)
无监督只有文本没有标签,区别如下:
# 无监督数据:只有文本,没有标签
unsupervised_data = ["The quick brown fox jumps over the lazy dog","Machine learning is a subset of artificial intelligence","Python is a popular programming language",# 数十亿条这样的纯文本,无需人工标注
]# 对比:监督数据需要标签
supervised_data = [("This movie is great!", "positive"), # 文本 + 情感标签("I hate this product", "negative"), # 文本 + 情感标签
]
词元语料库,或者你可以理解为token语料库(不了解token的小伙伴可以回顾一下之前的章节有说哦~)
# 原始文本
raw_text = "Hello world! How are you?"# 分词后的token序列
tokens = ["Hello", "world", "!", "How", "are", "you", "?"]# 转换为ID序列
token_ids = [15496, 995, 0, 1374, 389, 345, 30]# 数学表示
U = {u1, u2, u3, u4, u5, u6, u7}
# 其中 u1="Hello", u2="world", u3="!", ...
2、GPT1无监督预训练
清楚上面这些内容之后,我们再来看GPT1的无监督预训练在做什么
目标任务:
假设我们给定一个token语料库:为U={u1,u2,⋯ ,un}U=\{u_1,u_2,\cdots ,u_n\}U={u1,u2,⋯,un}【举个例子就是U={“Hello”, “world”, “!”, “How”, “are”, “you”, “?”}当然史记索这个可能很长很长】
那我们要做的事情其实就是最大化似然估计(统计学中很常见的一个东西,推理比较麻烦理论部分不做叙述,感兴趣的小伙伴可以直接查相关的知识。)
L1(U)=∑ilogP(ui∣ui−k,⋯ ,ui−1;Θ)L_1(U)=\sum_{i}logP(u_{i}|u_{i-k},\cdots,u_{i-1};\Theta)L1(U)=∑ilogP(ui∣ui−k,⋯,ui−1;Θ)
其中k是文本窗的大小,实际上就是神经网络模型输入的大小,因为我们会用一个神经网络拟合上面的估计,而Θ\ThetaΘ就是神经网络待估计参数,这个参数通过随机梯度下降的方法来优化迭代。
模型:
神经网络模型实际上是由多层的Transformer解码器组成,回忆一下解码器的结构它是由带掩码的多头注意力机制+前馈层(当然可能还有残差连接,这里的具体内容会在下一章的代码详解中重新叙述)。简单来看我们可以把用到的神经网络抽象成下面的数学公式:
h0=UWe+Wph_0=UW_e+W_ph0=UWe+Wp其中U是token向量(通过字典已经对应成数字了),WeW_eWe是编码矩阵,WpW_pWp是位置编码矩阵,都是可以学习的。
hl=transformer_block(hl−1)∀i∈[1,n]h_l=transformer\_block(h_{l-1})\quad \forall_i\in [1,n]hl=transformer_block(hl−1)∀i∈[1,n] 这部分实际上是由很多解码器堆叠而成
P(u)=softmax(hnWet)P(u)=softmax(h_nW_e^t)P(u)=softmax(hnWet) 通过一个全连接加一个softmax将其转化成概率
再用一个很简单的例子来说明这件事。
假设我们的语料库:I am Lily and I like reading book.【因为标点符号也是放在字典里为了简单起见我们忽略它】
token 向量:[I,am,Lily,and,like,reading,book][I,am,Lily,and,like,reading,book][I,am,Lily,and,like,reading,book]
对应的token字典:dictu={1:′I′,2:′am′,3:′Lily′,4:′and′,5:′like′,6:′reading′,7:′book′}dict_u=\{1:'I',2:'am',3:'Lily',4:'and',5:'like',6:'reading',7:'book'\}dictu={1:′I′,2:′am′,3:′Lily′,4:′and′,5:′like′,6:′reading′,7:′book′}
文本数字向量化的 token :U=[1,2,3,4,1,5,6,7]U=[1,2,3,4,1,5,6,7]U=[1,2,3,4,1,5,6,7]
如果要预测下一个单词,那么模型输入就是[1,2,3,4,1,5,6][1,2,3,4,1,5,6][1,2,3,4,1,5,6],通过一系列编码,我们输出一个向量概率,实际上对应的标签应该是[0,0,0,0,0,0,1][0,0,0,0,0,0,1][0,0,0,0,0,0,1]
为什么对应标签是[0,0,0,0,0,0,1][0,0,0,0,0,0,1][0,0,0,0,0,0,1]?实际上就是最后这个位置的概率是1,也就是下一个单词应该是book。
2.2 有监督微调
数据集假设
有一个有标签的数据集 CCC,每个样本包括:
输入 token 序列 x1,...,xmx_1, ..., x_mx1,...,xm,
标签 yyy(比如分类任务的类别)。
模型结构
输入序列 x1,...,xmx_1, ..., x_mx1,...,xm 经过预训练的 Transformer 模型,得到最后一层的激活 hlmh^m_lhlm(即最后一个 token 在最后一层的输出)。
在此基础上,新增一个线性输出层(参数为 WyW_yWy),用于预测标签 yyy。
预测公式
预测公式:
P(y∣x1,...,xm)=softmax(hlmWy)
P(y|x_1, ..., x_m) = \mathrm{softmax}(h^m_l W_y)
P(y∣x1,...,xm)=softmax(hlmWy)
其中,hlmh^m_lhlm 表示最后一个 token 的 Transformer 激活,WyW_yWy 是新加的线性层参数。softmax 用于将输出转为概率分布,预测属于每个类别的概率。
监督目标函数
监督目标函数:
L2(C)=∑(x,y)logP(y∣x1,...,xm)
L_2(C) = \sum_{(x, y)} \log P(y|x_1, ..., x_m)
L2(C)=(x,y)∑logP(y∣x1,...,xm)
对每个样本,最大化预测标签的概率(即最大化对数似然)。
辅助语言建模目标(Auxiliary Objective)
在微调时,除了主要的监督目标 L2(C)L_2(C)L2(C),还加入语言建模目标 L1(C)L_1(C)L1(C) 作为辅助。
这样做有两个好处:
(a) 提升模型泛化能力(减少过拟合,提升在新数据上的表现)
(b) 加快训练收敛速度(更快达到较好性能)
% 联合优化目标
联合优化目标:
L3(C)=L2(C)+λ⋅L1(C)
L_3(C) = L_2(C) + \lambda \cdot L_1(C)
L3(C)=L2(C)+λ⋅L1(C)
其中,L2(C)L_2(C)L2(C) 是监督任务的目标,L1(C)L_1(C)L1(C) 是语言建模目标(比如预测下一个 token),λ\lambdaλ 是权重系数,用于平衡两者的影响。
2.3 不同任务的输入变化
通过把结构化输入转化为有序文本序列,并加上特殊标记,可以让预训练模型适用于更多任务,而无需针对每个任务设计新的模型结构。
举例来说:
- 文本蕴含(Textual entailment)
- 输入是前提(premise, p)和假设(hypothesis, h)。
- 做法:将前提和假设的token序列拼接在一起,中间用一个分隔符($)隔开。
- 这样模型可以一次性处理整个输入序列,判断假设是否能从前提推断出来。
- 相似性任务(Similarity)
- 输入是两个句子,没有固定顺序。
- 做法:分别生成两种顺序的输入(句子A在前,句子B在后;句子B在前,句子A在后),中间用分隔符($)。
- 每种顺序都独立处理,得到两个序列表示(hm和hl),然后将它们按元素相加,最后送入线性输出层进行预测。
- 问答与常识推理(Question Answering and Commonsense Reasoning
- 输入是文档(zzz)、问题(qqq)和一组可能的答案({ak}\{ak\}{ak})。
- 做法:将文档和问题与每个可能答案分别拼接,中间用分隔符形成[z;q;;ak][z;q;; ak][z;q;;ak]。
- 每个拼接后的序列都独立送入模型处理,最后用softmax归一化,得到对每个答案的概率分布。
