语言模型十年演进:从n-gram到Transformer再到GPT

其实语言模型的目标一直很简单:给定一段话,猜下一个词是什么。从这个朴素的想法出发,过去十年里我们经历了从统计计数到注意力机制的全面升级,现在能写文章、编代码的模型本质上还是那个猜词游戏,只是猜得越来越准。
猜词游戏如何变得靠谱
最早的做法是n-gram:统计相邻几个词一起出现的频率,然后用条件概率预测。比如'我 喜欢 吃',后面接'苹果'的概率比'桌子'高。这方法直观,但碰到长句子或者没见过的新组合就抓瞎——数据稀疏,而且跨几个词的依赖根本抓不住。
2003年Bengio团队用前馈神经网络学习词向量,把每个词映射到稠密空间,再通过神经网络建模序列,算是打开了用深度学习做语言模型的大门。再往后RNN、LSTM陆续登场,模型开始能处理变长序列,记忆更长距离的上下文。不过RNN串行计算慢,靠得稍远的信息还是容易丢。
Transformer:只用注意力,扔掉循环
2017年的'Attention is All You Need'是一个分水岭。它提出的Transformer完全抛弃了循环结构,全靠自注意力来捕捉全局关系,同时支持并行计算,训练速度大幅提升。现在几乎所有主流语言模型都基于Transformer架构。

自注意力的核心是让句子里的每个词都和所有其他词计算关联度。输入经过线性变换得到查询(Q)、键(K)、值(V)三个矩阵,计算缩放点积注意力:
Attention(Q,K,V) = softmax(QK^T / √d_k) V
多头注意力相当于并行做多次注意力计算,每个头关注不同的表示子空间,最后拼接起来。因为Transformer没有顺序概念,还得用正弦余弦位置编码把位置信息加进去。
Transformer的结构分编码器和解码器。编码器把输入编码为上下文表示,解码器基于编码结果自回归生成输出。后来的模型常常只用其中一部分:BERT只用编码器做双向理解;GPT只用解码器做单向生成;T5则两者都用。
预训练时代:GPT、BERT和他们的朋友们
有了Transformer,下一个突破是'预训练-微调'范式。先在大规模无标注语料上训练通用模型,再针对具体任务微调。这种思路让单个模型可以适应各种下游任务。
GPT系列是典型的自回归模型,从左到右预测下一个词,天然擅长文本生成。GPT-1初试身手,GPT-2生成能力惊艳,GPT-3靠着1750亿参数展现出强大的少样本学习能力,到ChatGPT加入了指令微调和RLHF,对话能力强得让人以为是真人。
BERT则走双向路线,通过掩码语言模型(随机遮住一些词然后预测)同时利用左右两侧的上下文,在理解类任务上表现极佳。发布后刷新了11项NLP纪录。后续的RoBERTa、ALBERT等都在其基础上做了进一步优化。
T5把各种NLP任务统一成'文本到文本'的格式,无论翻译、问答还是摘要,输入和输出全是文本序列,用一个模型搞定。
另外XLNet结合了自回归和自编码的优点,通过排列语言模型捕捉更全面的依赖关系。总之这几年的模型虽然名字五花八门,但底子都是Transformer加预训练加注意力。
动手跑一个:用BERT做情感分类
光是理论不够,我们来实际微调一个BERT做二分类情感分析。用Hugging Face的Transformers库,几行代码就能跑。
from transformers import BertTokenizer, BertForSequenceClassification
from torch.utils.data DataLoader, Dataset
torch
torch.nn.functional F
():
():
.texts = texts
.labels = labels
.tokenizer = tokenizer
.max_len = max_len
():
(.texts)
():
encoding = .tokenizer(
.texts[idx],
truncation=,
padding=,
max_length=.max_len,
return_tensors=
)
{
: encoding[].squeeze(),
: encoding[].squeeze(),
: torch.tensor(.labels[idx], dtype=torch.long)
}
tokenizer = BertTokenizer.from_pretrained()
model = BertForSequenceClassification.from_pretrained()
texts = [, ]
labels = [, ]
dataset = SentimentDataset(texts, labels, tokenizer, max_len=)
loader = DataLoader(dataset, batch_size=)
optimizer = torch.optim.AdamW(model.parameters(), lr=)
model.train()
batch loader:
optimizer.zero_grad()
outputs = model(
input_ids=batch[],
attention_mask=batch[],
labels=batch[]
)
loss = outputs.loss
loss.backward()
optimizer.step()
()



