上一篇文章From Text to Large Language Models讲了如何用RNN、LSTM、GRU等模型来处理文本数据,但它们都存在一些问题。
- 无法并行训练
- 长距离依赖问题
探索 attention 和 self-attention
为了讲清楚上面的问题,用机器翻译这个例子来说明。因为机器翻译非常适合暴露旧模型的问题:
- 输入和输出都是序列
- 输入句子和输出句子长度可能不一样
- 一个词的翻译,往往依赖很远处的词
- 语法、代词、上下文一致性都很重要
比如英文翻法语时,一个词该怎么翻,可能要看句子前面很久以前出现的主语、时态、否定词。
seq2seq
当时研究人员尝试将现有的RNN等模型应用于机器翻译。其中有个叫seq2seq的系统,它把输入和输出都作为序列,然后通过一个编码器(encoder)和译码器(decoder)来处理。
它的思路是:
- encoder 读入原句
- 把整句压缩成一个表示
- decoder 再根据这个表示,一步一步生成翻译后的句子
你可以把它理解成:
- 编码器先"读懂原文"
- 解码器再"复述成另一种语言"
这个想法本身很好,但很快遇到问题。下面是4个 seq2seq 的核心问题:
1. Alignment(对齐) 问题
输入词和输出词不是一一对应的。
比如一句英文,翻成法语后:
- 词序可能变了
- 一个词可能翻成多个词
- 有些词在另一种语言里要补出来
- 有些词要合并表达
所以不能简单假设:第 3 个输入词对应第 3 个输出词
这个叫 alignment problem,也就是"对齐问题"。
机器翻译真正难的地方之一,就是模型要知道:当前我在生成这个目标词时,源句里到底该看哪里?
2. Long-distance dependency(长距离依赖) 问题
RNN 是按顺序一个词一个词处理的。
理论上它能记住很长上下文,但实际上不行。
原因你前面已经学过了:
- 信息要沿着时间步一步一步传
- 早期信息会逐渐衰减
- 会出现 vanishing gradient / exploding gradient
- 即使 LSTM、GRU 有改进,也还是不够理想
所以一句很长的话里,句首的信息很难稳定地影响句尾。
这在翻译里就很致命,因为很多翻译决定依赖远距离信息。
3. 编码瓶颈问题
早期 seq2seq 常常让 encoder 把整句压缩成一个固定长度向量,再交给 decoder。
问题是:一整句话的信息,被硬塞进一个向量里,很容易丢信息。句子越长,这个问题越严重。
4. 不能并行
RNN 的天然结构决定了:
- 第
t步必须等第t-1步 - 训练和推理都很难并行
- GPU 的优势吃不满
这在大规模训练时非常伤。
所以RNN 不只是效果还不够好 ,而且也不够高效。
attention 的出现
attention 最初并不是为了"彻底替代 RNN"。它一开始是为了修复 seq2seq 的对齐问题。
关键思想是:
decoder 在生成当前词时,不要只看 encoder 最后压缩出来的那个总向量,而是动态去看源句中最相关的部分。
简单举例来说:
假设你在翻一句话:
She doesn't like potatoes.
当模型生成不同目标词时,关注点应该不同:
- 生成
she时,重点看She - 生成否定结构时,重点看
doesn't - 生成
potatoes时,重点看potatoes
也就是说:
不是整句里所有词都同等重要。不同生成时刻,重要的源词不同。
attention 做的就是这件事:
- 给源句每个位置打分
- 算出"当前时刻该关注谁"
- 把这些关注结果加权汇总成当前上下文
所以 attention 的本质不是"记忆更多内容",而是:按需读取相关内容。
这比让 RNN 把一切都死记在隐藏状态里聪明得多。
学术一点理解:
attention 先出现,是为了解决 seq2seq 翻译模型里的一个具体问题:解码器在生成当前词时,不能只靠一个压缩后的"整句摘要向量",而应该动态去看源句里哪些词最相关。
attention 解决的是"跨两段表示的对齐"。
在最早的机器翻译里:
- 编码器产生源句的一串 hidden states
- 解码器在生成第 t 个词时,用当前状态去和源句各位置做匹配
- 然后决定现在应该重点看源句哪个部分
所以普通 attention 本质上是:
- "我现在要输出这个词"
- "那我去源句里找和它最相关的位置"
这是跨模块、跨序列的关注。也就是"decoder attend to encoder"(解码器看编码器)。
研究者很快发现,既然"动态挑重点"这么有效,就会自然追问一个更强的问题:
不是只有"解码器看编码器"才需要挑重点,输入句子内部自己是不是也需要这样做?
这就是 self-attention 出现的根本原因。
self-attention
最重要的发现:句子内部本身也存在大量依赖关系。
研究者后来意识到,很多问题根本不只是"输出端该看输入端哪里",而是:
- 输入句子里的每个词,本身就需要理解和它相关的其他词
- 一个词的含义,常常依赖句内别的位置的信息
比如这句:
The animal didn't cross the street because it was too tired.
这里的 it 指的是谁? 模型必须看前面更远的位置,判断它和 animal、street 的关系。
再比如:
The book on the table near the window is mine.
要理解 is 对应的主语是谁,也要跨较远位置建关系。
所以研究者会发现:
RNN 是按时间一步一步传信息,但很多依赖其实不是"相邻传递"能高效表达的,而是"当前位置直接看全句其他位置"更自然。
于是就有了 self-attention:
不是"我拿 decoder state 去看 encoder outputs",而是句子里的每个 token 都可以直接看同一句子里的其他 token。
更深的理解:
把 self-attention 理解成 attention 的一次抽象升级。
普通 attention 里,通常是两套不同来源:
- query 来自 decoder
- key/value 来自 encoder
而 self-attention 里,三者都来自同一个输入序列:
- Q 来自当前序列
- K 来自当前序列
- V 也来自当前序列
所以它叫 self-attention。"self" 的意思不是"自己只看自己",而是同一个序列内部,彼此做 attention。
也就是:
- 每个 token 发出一个 query
- 去和所有 token 的 key 做匹配
- 再把匹配到的重要 value 加权汇总
- 得到这个 token 的新表示
所以一个 token 的表示,不再只是它自己原本的 embedding,而是"它结合全句上下文之后"的表示。
self-attention轻松解决了RNN 的几个硬伤:
- 长距离依赖难学:一个词可以直接看全句,不必靠隐藏状态一层层传过去。
- 并行性差:RNN 必须按时间步一个个算;self-attention 可以整句同时算。
- 信息瓶颈:RNN 往往把信息压进一个不断传递的状态里;self-attention 允许每个位置都动态取用全局信息。 但是,self-attention 也有自己的问题:
- 计算量对序列长度是平方级增长
- 本身不自带顺序感
所以,这为后续的Transformer模型埋下了伏笔。
Transformer模型的介绍
Transformer 的出发点是谷歌的研究人员的一篇论文:《Attention is all you need》。既然 attention 这么有效,那能不能把 RNN 整个拿掉,只靠 attention 搭一个模型?
所以,Transformer 的核心是 self-attention。Transformer = 完全围绕 self-attention 构建的序列建模架构。
Transformer 的总思路
transformer 的第一步:先把 token 变成向量。和前一章一致,模型不能直接吃文字,只能吃向量。 所以输入流程还是:
- tokenization
- embedding
- 把 token 变成向量
但是在这之前,前面有一个self-attention的问题还没解决:self-attention不懂顺序。所以,经典 transformer 的做法是positional encoding位置编码。
详细来说是: 给每个 token embedding 再加上一份"位置向量"。也就是说,模型真正看到的不是单纯的 word embedding,而是:
token embedding + positional encoding
这样每个 token 的表示里,同时包含:
- 它是什么词
- 它在第几个位置
那么,现在就变成attention 只负责"看谁重要",position 负责"告诉它顺序"。
核心思路:transformer block
Transformer 不是一层 attention,而是很多层 block 叠起来。
每个 transformer block 主要有四个部件:
- multi-head self-attention
- feed-forward network
- residual connection
- layer normalization
这四个东西你要牢牢记住,因为后面所有 LLM,几乎都是这个骨架的变体。
- multi-head self-attention:从多个角度同时看上下文
同一句话,模型不只用一种方式去看 token 之间的关系,而是用多个"头"并行去看。
你可以把它想成:
- 一个头擅长看语法关系
- 一个头擅长看指代关系
- 一个头擅长看长距离依赖
- 一个头擅长看局部搭配
当然这些不是人工规定的,而是训练自己学出来的。
最后再把多个头的结果拼起来,形成更丰富的表示。
所以 multi-head 的价值是:
不是只问一个问题,而是同时从多个视角理解序列。
- Feed-forward network:对每个位置再做一次非线性变换
attention 做完以后,还不够。
因为 attention 的核心是"信息混合"和"加权聚合",但它本身还不够强,不能单独完成复杂表征变换。
所以每个 block 后面还接一个 feed-forward network(FFN)。
它通常就是两层线性层加一个非线性激活。
你可以把它理解成:
attention 负责让 token 彼此交流,FFN 负责对每个 token 自己做更深的特征加工。
这两个配合起来,模型就既能看上下文,也能加工自身表示。
3.Residual connection:防止模型太深以后难训练
这是很重要的工程设计。
它的意思很简单:不要只保留"这一层算出来的新东西",而是把"输入"也直接加回去。
形式上就是:输出 = 子层结果 + 原输入
为什么这样做?
因为深层网络容易出现:
- 梯度传不下去
- 信息在层层变换中被破坏
- 优化越来越困难
残差连接的本质作用就是:
给信息和梯度开一条捷径。
所以它让深层 transformer 能稳定训练。没有它,transformer 很难堆很多层。
你可以把 residual 理解成一句话:
模型每一层不是从零重写表示,而是在原表示基础上做增量修正。
- Layer normalization:让训练更稳定
它做的事是把每层里的数值分布拉回一个相对稳定的范围。
为什么需要它?因为深层网络训练时,内部表示会不断漂移,数值可能忽大忽小,导致训练不稳定。
Layer norm 的作用就是:减少无意义的数值波动,让优化更平稳。
所以如果你把 transformer block 简化记忆:
- attention:让 token 彼此看见
- FFN:做局部非线性加工
- residual:保留原信息、便于训练
- layer norm:稳定训练过程
这就是一整个 block。
所以,一个 transformer block 到底在做什么?
现在你可以把一个 block 整体理解成四步:
csharp
第一步:self-attention
每个 token 看整句,收集和自己相关的信息。
第二步:add + norm
把新信息和原输入合并,再做归一化。
第三步:FFN
对每个位置单独做进一步非线性加工。
第四步:add + norm
再次保留原信息并稳定训练。
所以一个 block 的本质不是"做一次复杂运算",而是:先全局交互,再局部变换。多个 block 叠起来后,表示会越来越抽象、越来越高级。
所以,在现代应用中,输入经过 embedding 和 position 编码之后,不是只过一层,而是会连续通过很多 transformer blocks。
每过一层,表示都会更抽象一点、更上下文化一点。
所以你可以这样理解层次变化:
- 底层:更多局部词法、邻近关系
- 中层:更多语法、依存、短程结构
- 高层:更多语义、任务相关抽象
这也是为什么现代模型可以堆到几十层甚至更多。不是某一层神奇,而是层层堆叠让表示越来越强。
原始的Transformer
原始 transformer 不是今天常见的 GPT 那种纯 decoder,而是用于机器翻译的encoder-decoder 架构。
也就是两部分:
- encoder:读输入句子
- decoder:生成输出句子
比如英译法:
- encoder 读英文句子
- decoder
encoder 这一边主要是:
- embedding
- positional encoding
- 多层 transformer block
它的目标是把输入序列编码成一组富含上下文的信息表示。
简单说:encoder 负责理解输入。
decoder 做什么:
decoder 这边也有 embedding、position、多个 block,但它多了两个特殊点。
第一,decoder 不是直接看到完整未来答案。因为训练和生成时都不能作弊。
第二,decoder 不只是看自己前面已经生成的内容,还要看 encoder 的输出。
所以 decoder 里有两类 attention:
- 对自己已生成部分的 attention
- 对 encoder 输出的 attention(cross-attention)
- cross-attention
cross-attention就是让 decoder 去"看输入"
cross-attention 的本质是:decoder 在生成当前词时,不只参考自己前面生成了什么,还会去 encoder 那边找和当前生成最相关的信息。
所以:
- Query 来自 decoder 当前状态
- Key / Value 来自 encoder 输出
它的意义可以一句话说完:decoder 一边生成,一边回头查看输入内容。
这对翻译特别关键。因为法文当前该生成哪个词,要参考英文输入中哪个部分。
所以:
- self-attention:在自己序列内部找关系
- cross-attention:在输出序列和输入序列之间找关系
2.masked attention:为什么 decoder 要遮住即将生成的词
如果训练时让 decoder 看到未来词,那任务就变成作弊了。
例如要预测:
I love ...
如果模型已经看到了后面的 you,那它根本不需要学会语言建模。
所以 decoder 的 self-attention 必须加遮罩,只允许看:
- 当前 token 之前的内容
- 不允许看之后的 token
这叫 causal mask 或 masked attention。
本质上:生成模型必须只依赖过去,不能偷看未来。
这是 GPT 一类模型后面最关键的训练约束之一。
训练Transformer模型
Transformer 结构这么复杂,它到底是怎么训练出来的? 答案反而很朴素:
不给它人工标很多复杂标签,而是直接给它大量文本,让它学"下一个词该是什么"。
这就是现代大模型训练最核心的起点。
训练目标:语言建模
Transformer 之所以强,不是因为人类手工教了它很多语法规则,而是因为:
- 它有很强的表示能力
- 互联网提供了海量文本
- 我们可以设计一个"自监督"任务让它自己学
这个任务就是:
给前文,预测下一个 token。
比如输入:
to be or not to
模型要预测下一个词更可能是 be。
所以训练时,模型本质上在学:一个词在上下文里出现的概率。。
因为它看起来只是"猜下一个词",但本质上逼着模型学习很多更深的东西:
- 语法
- 搭配
- 语义
- 常识模式
- 长距离依赖
- 不同文本风格
所以训练目标虽然简单,但学出来的能力可以很复杂。
如果一个模型真的能持续准确预测"下一个词",那它就不可能只靠死记硬背局部模式。
它必须逐渐学会:
- 这个句子现在在讲什么
- 前面哪些词最关键
- 当前 token 和更远位置的 token 有什么关系
- 什么样的表达在真实语言里更自然
所以"预测下一个词"其实是一个自监督任务。
重点在"自监督":
- 不需要人工给标签
- 文本本身就天然带标签
- 前文是输入
- 后一个词就是目标
这也是为什么互联网海量文本能直接拿来训练模型。
这就是语言模型。
transformer 的输出
前面我们讲过,Transformer block 的输入输出维度一直保持一致。
但训练时,我们真正想要的是:从当前上下文,预测整个词表里每个 token 的概率。
所以在 Transformer 最后,还要接两步:
- 一个线性投影层:把最后的隐藏表示映射到词表大小。
- 一个 softmax:把这些分数变成概率分布。
这一步书里把它叫做 unembedder。
为什么叫这个名字?
因为前面 embedding 是把 token 映射到向量空间;这里反过来,是把隐藏向量重新映射回"词表空间"。
也就是:
- 输入:一个隐藏表示
- 输出:对整个 vocabulary 的打分
这个打分向量通常叫 logits。再经过 softmax,就变成概率分布。
比如词表里有 50,000 个 token,那模型在某一步输出的就是:
- cat 的概率
- dog 的概率
- run 的概率
- ...
- 所有 token 的概率
所以流程是:上下文 -> Transformer -> logits -> softmax -> 下一个 token 的概率分布
那拿到这个概率分布后,是为了做什么?为了解决训练时怎么知道模型预测得好不好。
拿模型预测的概率分布,和真实下一个 token 比。
如果真实答案是 be,模型却把 the 的概率给得最高,那就错了。
于是我们计算 loss,让模型更新参数。
这一过程会对句子里每个位置都做一遍。
比如一句话:
I love deep learning
训练时会变成很多个预测任务:
- 看到 I,预测 love
- 看到 I love,预测 deep
- 看到 I love deep,预测 learning
最后把这些位置上的损失取平均。 这就是书里说的:final loss is the average of the loss of all the time steps。
为什么transformer比RNN快
RNN 的问题是:
- 必须一个词一个词往后算
- 第 2 步要等第 1 步
- 第 3 步要等第 2 步
所以天然串行。但 Transformer 不一样。
虽然任务仍然是"预测下一个词",但训练时我们可以把整句一起喂进去,并行算所有位置的预测。
你可以把它理解成:
- RNN:一边读一边想
- Transformer:整段都看到了,同时对每个位置做训练
所以它:
- 更适合 GPU
- 更容易扩展到大模型
- 训练速度更快
这也是 Transformer 能统治现代大模型的现实原因之一。
一些经典训练方式:tearcher forcing
teacher forcing的意思是:训练时,模型在预测第 t 个词时,它前面的上下文不是它自己乱生成的,而是真实文本中的正确前文。
比如真实句子是:The cat sits on the mat
训练时预测 sits,给模型的前文是:The cat
而不是让模型先从头自己生成一个词,再接着往后。
所以:
- 训练时:喂真实前文
- 推理时:只能喂自己刚生成出来的词
因为训练初期模型很弱,如果一开始就喂它自己生成的错误结果,错误会一路累积,训练会非常不稳定。这也是训练和生成的一个重要区别。
所以 teacher forcing 的本质是:训练时走"标准答案轨道",让模型学会条件概率。
训练完成后,生成时怎么选词
训练时,模型会预测每个 token 的概率。生成时,它需要从这些概率中选一个。
通常有以下几种方式:
- 最大概率(greedy decoding):选择概率最高的 token
- 完全随机(Random sampling):从概率分布中随机选一个 token
- top-k(Top-k sampling):只保留概率最高的前 k 个 token,再在这 k 个里面随机采样。
- top-p(Top-p sampling):不是固定保留前 k 个,而是保留累计概率达到 p 的那一批 token。这个比 top-k 更自适应。
- temperature(Temperature sampling):调整概率分布的形状,让模型更随机。
这里,你是不是看到了一些平时使用模型时需要填写的参数。
token的来源
为什么还要讲 token,而不直接讲"词"?
因为如果词表固定,那模型总会遇到没见过的新词。这就是 问题。
比如训练时见过:
- big
- bigger
- small
但没见过 smaller。
那如果模型把最小单位定义成"整词",它就只能把 smaller 当未知词处理。
所以后来大家不再强依赖"整词词表",而是用 subword,也就是"子词",也就是token。