Transformer 是一种以注意力机制(Attention)为核心的序列建模架构,它通过“序列中任意位置之间的可学习加权聚合”替代了 RNN 的递归结构,从而实现了更强的并行处理能力和长程依赖建模能力。这一架构已成为自然语言处理、计算机视觉等多个领域的基础模型。
注意力机制
为了解决什么问题?
理解特定情境下某个词的语义信息,以及整个句子的语义信息。这听起来似乎很简单,但实际实现起来并不容易。
在进入注意力模块之前,我们通过 Embedding 层把字符转换为向量。但此时的向量是“静态的”“孤立的”。例如,“打”这个字,不论出现在“打篮球”、“打酱油”还是“打车”中,它的初始 Embedding 都完全相同。经过 Attention 处理后,输出的特征向量不再只是单纯的“打”字,而是“包含了打篮球这一真实语境的复合语义向量”。这就是上下文感知(Contextualized)的核心概念。理解这一转变,是掌握 Transformer 的第一步。
核心结构:
特定语义空间 – V 矩阵与注意力得分
我们从词向量的起点出发,已经能够捕捉到词语本身的基础含义。但语言的魅力在于多变性——同一个词在不同语境下可能呈现截然不同的意思。比如“bank”既可以指金融机构,也可以指河岸。仅依赖静态的词向量,我们无法区分这种细微却至关重要的语义差异。这就引出了一个核心问题:如何让模型理解并区分词语在不同语境下的含义,从而构建一个更丰富、更精确的语义空间?
因此,我们需要考虑一个单词是否需要加入上下文。传统的 RNN 是顺序地考虑上下文,这导致了两个问题:容易遗忘,且计算效率低。通过研究人类阅读时的注意力表现,我们发现这并不是一个顺序过程——我们在阅读时能自动忽略不重要的事物,抓住关键词即可理解整句话的含义。(这里有一个特别恰当的比喻:RNN 类似顺序传话,而 Transformer 类似圆桌会议,每个人同时发表意见。)
好了,现在我们有了明确的目标:我们要对不同场景下的词语进行区分,拓展语义空间,找到每个词在不同语境下的向量——也就是找到一个新向量空间,这个空间不仅存储词的意思,还能区分同一个词语在不同语境下的含义。用公式表达就是:词向量 + 上下文 → 新的带语境信息的词向量。我们需要求解中间的过程(f 函数)。
然后再拆分目标:总不能把整个上下文都利用吧?开销太大,而且不合理。我们需要找到重要的词,忽略不重要的词。这种“抓住重点、忽略无关”的能力,正是我们所追求的“注意力”的体现。那么如何判断哪个词重要、哪个不重要呢?为此,我们进行量化,给每个词对 f 的贡献起了一个名字,也就是后来的 注意力得分。还有一个说法叫“关联程度计算”,我认为意思相近。
提取出重要的关键词后,还需要转化为语义向量,这就是 V 矩阵。
注意力得分:Q、K 矩阵
设计两个变换:一个是对当前词的变换 Q,一个是对其他词的变换 K,然后通过点积来计算匹配程度。原理很简单:每个词可能有它固定关注的词语类型(或者需要提取一种特征,即当前词对哪种词敏感、对哪类信息敏感),这相当于发出一次查询 Q——也就是“我需要注意的词的特征”。同时我们还要提取每个词的特征,也就是这个词是什么样的,即 Key,用来与 Q 匹配。
这个命名和操作的灵感,直接来源于计算机科学中一个非常经典的领域:信息检索与数据库系统。
总结:每个单词准备三张名片
- Q (Query – 查询):代表当前词语“我想了解什么?”它的本质是当前词语发出的一个“查询请求”,携带了当前词语对上下文的“兴趣点”或“关注方向”。
- K (Key – 键):代表每个上下文词语“我有什么特点?”它是上下文词语自身的一种“特征表示”,用于与 Q 进行匹配。
- V (Value – 值):代表每个上下文词语“我的具体信息是什么?”它是上下文词语本身携带的“信息内容”,在计算出权重后,将被加权求和。
注意:QKV 的含义仅仅是人类对其含义的形象化理解。
假设句子:“猫喜欢吃鱼,它很聪明。”以“它”为例:
- “它”提出 Query:“我指代谁?”从“ta”到“我指代谁”是学习出来的 Q。
- 所有词出示 Key:“我是名词/动词/代词...”中间的过程也是学习出来的。
- 计算匹配度:Qi·Kj(点积相似度)。
- 得到注意力权重:
["猫", "喜欢", "吃", "鱼", "它", "很", "聪明"]
[0.7, 0.05, 0.05, 0.1, 0.02, 0.03, 0.05] - 加权求和:0.7×"猫"的信息 + 0.05×"喜欢"的信息 + ...(注意这里,这是后来位置编码的基础)。
整个网络的核心要义在于注意力——如何计算每个数的贡献,也就是注意力得分。这三张卡片就是 WQ、WK、WV 矩阵。这里也蕴含着我认为的核心设计原则:我们设定好功能以及结构,通过梯度下降和反向传播,就能让这个部件具备我们想要的功能。最终呈现的结构就是一个矩阵变换,记录了转换规则(从几何意义上说,矩阵就是空间变换的规则)。例如 Embedding,并不是记录一个个转换后的向量,而是 Win 记录了这个转换过程。
Scaling
为什么一定要加上这个缩放操作?
- 统计学危机:维度越大,点积越“爆炸” 矩阵相乘本质是加权求和,方差会放大。
- Softmax 的坏脾气:极度敏感与“赢者通吃” 降低所有数据的大小,在这个温和的区间里,Softmax 的梯度非常健康。
多头注意力机制
这个可以用 CNN 中的多通道来类比。CNN 设置不同的通道,每个通道独立一个卷积核,然后每个卷积核单独扫描全部数据,提取不同的特征(所以,注意力头 → 卷积核)。
但是跟 CNN 还是有区别的:
- CNN 的逻辑:全盘接收,多角度扫描 ——在 CNN 中,假设输入图像的特征图是
[Batch, 512, Height, Width](有 512 个通道)。如果你设定了 8 个卷积核,每一个卷积核在扫过画面时,它的深度必须也是 512。 - Transformer 的逻辑:物理隔离,强行分工 ——将一个大的向量空间拆成多个小的向量空间。
在 Transformer 中,输入的词特征是 [Batch, SeqLen, 512]。按照 CNN 的逻辑,8 个头应该每个头都去拿完整的 512 维来算,对吧?但 Transformer 偏不!它是把 512 强行切成了 8 份,每份只有 64 维。物理意义:头 1 只允许看特征的第 0~63 维;头 2 只允许看第 64~127 维。大家井水不犯河水。
为什么 Transformer 要这么“抠门”地拆分 Q 和 K?有两个精妙之处:精妙的算力恒定魔法与强迫子空间正交化。把一个 512 维的大向量空间,强行切分成 8 个 64 维的小向量空间(子空间)。这就好比把一个笼统的“综合评分”体系,拆分成了“外貌”、“性格”、“学历”、“财力”等多个独立的评价维度。每个头只在自己的小维度空间里去计算距离(相似度),最后再把大家的结果拼回来。
假设我们只有单头注意力。模型在计算一个词与其他词的关联时,只能聚焦于一种特定的关系。比如在“The animal didn't cross the street because it was too tired”这句话中,单头注意力可能把所有的权重都用来寻找“it”指代的是什么(animal)。但这还不够——我们还需要理解“tired”是谁的状态,时态是什么,等等。多头注意力就像是给模型分配了一个“专家委员会”:Head 1 专门负责分析语法结构(主谓宾关系),Head 2 专门负责分析代词指代,Head 3 专门负责分析情感色彩。每个头都有自己独立的权重矩阵,把输入映射到不同的子空间中进行学习。最后,模型把所有专家的意见汇总(Concat),从而获得对这句话极其丰富的多维度理解。
在真正的代码实现上,有一个小技巧:不需要使用 for 循环让每个注意力头跑一遍,而是可以将每个注意力头的 WQ 和 WK 拼成一个大矩阵,生成 QKV,然后进行拆分,再做 Attention 计算。
总结一下代码里的完整数据流(以 dmodel=512,SeqLen 为一个句子的 Token 数为例):
- 输入
[Batch, SeqLen, 512] - 生成 QKV:用大矩阵映射,依然是
[Batch, SeqLen, 512] - 拆分维度(view):变成
[Batch, SeqLen, 8, 64]
(PyTorch 矩阵乘法的死规定:永远只看张量的最后两个维度来做矩阵乘法。) - 交换维度(transpose):变成
[Batch, 8, SeqLen, 64](在这里做各头独立的 Attention 计算:QKT dot → Scaling → 加入掩码 → softmax) - 计算完 Attention:形状还是
[Batch, 8, SeqLen, 64] - 换回维度(transpose):变成
[Batch, SeqLen, 8, 64] - 拼接维度(view):把最后的
8和64揉在一起,瞬间“变回”了[Batch, SeqLen, 512] - 输出投影(Wo):经过
nn.Linear(512, 512),最终安全输出[Batch, SeqLen, 512]
前馈神经网络(Feed-Forward Network, FFN)
Attention 的职责是“收集信息”:它决定了当前字应该看向哪里。FFN 的职责是“理解和记忆”:它通常是一个把维度放大 4 倍再缩回来的两层线性网络结构(加上激活函数),拿着 Attention 收集来的信息进行深度的非线性思考,把模型在海量数据中背下来的“知识”注入进去。整个流程可以简化为:输入 → Attention(看上下文) → FFN(思考与回忆) → 最后的线性层(语言建模层,输出下一个字的概率)。
Embedding 和位置编码
Embedding:它就是一个“查表”的超级大矩阵
在 PyTorch 中,我们会使用 nn.Embedding(vocab_size, d_model) 来实现这一层。假设你的字典里有 3000 个汉字(vocab_size=3000),模型维度是 d_model=512,那么 nn.Embedding 在底层其实就是随机初始化了一个形状为 [3000, 512] 的可学习权重矩阵。当输入是一个包含了字 ID 的张量(比如“床”的 ID 是 12)时,Embedding 层根本不做任何复杂的矩阵运算——它只是去那个 [3000, 512] 的矩阵里,把第 12 行的那 512 个浮点数直接“抽”出来,作为“床”的初始词向量。所以这 3000 行向量就是模型在训练过程中要不断通过梯度下降来微调(Fine-Tune)的参数。模型要自己学会在 512 维的坐标系里,把“猫”和“狗”的坐标拉近,把“猫”和“宇宙”的坐标推远。
- Word2Vec:通常是“预训练”好的。先用一个巨大的语料库跑一遍 Word2Vec 算法,得到一个固定的词典到向量的映射表,然后把它死死地固定住,拿去给下游任务(比如情感分析)用。
- Transformer 中的 Embedding(Mini-GPT):通常是从零开始随机初始化这个矩阵,然后让它跟着后面的 Attention 层、预测层一起,为了“预测下一个字”这个终极目标进行端到端的联合训练。它完全是为当前任务量身定制的。
位置编码(Positional Encoding)
单纯的注意力计算来说,“狗咬人”和“人咬狗”最后对于“咬”这个字计算出来的语义信息是一致的。原因就在于“加法交换律”——最后的矩阵相乘分解到每个向量就是一个加权求和的过程。所以我们需要一个位置编码。
1. 最朴素也最实用的方案:可学习的绝对位置编码
这是 GPT 系列(包括 GPT-1 到 GPT-3)采用的方案。如果你打算手搓一个 Mini-GPT,强烈建议首选这个方案。核心逻辑很简单:既然词意可以通过一个巨大的矩阵(查表)学出来,那位置信息为什么不能也让模型自己学呢?我们预设一个模型支持的最大序列长度,比如 max_seq_len=1024,直接初始化一个新的 Embedding 矩阵:nn.Embedding(1024, d_model)。第 0 个位置就去查第 0 行的 512 维向量,第 1 个位置就去查第 1 行的向量。然后把查到的词向量和位置向量直接相加:x = token_emb + pos_emb。优点:代码极其简单,符合深度学习“万物皆可学”的暴力美学。缺点:缺乏外推性——如果训练时句子最长只有 1024,推理时突然来了一个长度为 1025 的句子,模型就傻眼了,因为第 1025 个位置的编码它从来没学过。
2. 原版论文的浪漫:正余弦位置编码
这是《Attention Is All You Need》原论文使用的经典方案。作者不想让模型死记硬背位置,而是想用一个确定性的数学公式,直接“算”出独一无二的位置向量。对于位置 pos 的 512 维向量,它的每一个维度 i 的值,都是通过正弦或余弦函数算出来的。直觉理解:想象一个由 512 个齿轮组成的机械钟表。向量的第 0 维(低维)像钟表的秒针,频率极快,位置 0 和位置 1 的值变化非常剧烈;向量的第 511 维(高维)像钟表的时针,频率极慢,可能走过几百个字,它的值才发生微小的改变。这 512 个不同频率的波形组合在一起,就像是指纹一样,保证了绝对不可能有两个位置算出相同的向量。
为什么用三角函数?因为三角函数有一个神奇的数学性质:sin(A+B) 和 cos(A+B) 可以展开成 sin(A)、cos(A)、sin(B)、cos(B) 的线性组合。这意味着位置 pos+k 的编码,可以通过位置 pos 的编码进行线性变换得到——这就让模型非常容易学到词与词之间的相对距离。
3. 现代大模型的霸主:旋转位置编码(RoPE)
在目前最前沿的开源大模型(如 Llama、Qwen、GLM)以及你的联邦微调研究中,经常会遇到各种参数高效的方法,而在底层架构上,它们几乎全部抛弃了前两种,采用了 RoPE。前两种方案都是在网络的最开头,把位置编码加到词向量里。而 RoPE 的思维极其跳跃:它不在 Embedding 层加,而是在每次计算 Attention 之前,直接作用于 Q 和 K 矩阵!核心逻辑:它把 Q 和 K 的向量两两分组,看作复数平面上的向量。如果一个词在第 m 个位置,RoPE 就会把它的 Q 向量在复平面上旋转 m·θ 的角度。当 Q 和 K 在计算内积(点积相似度)时,由于它们都发生了旋转,内积的结果只与它们的相对角度差(也就是相对位置差距 m-n)有关,而与它们的绝对位置无关。这种方案完美融合了绝对位置的计算效率和相对位置的泛化能力,是目前大模型能够支持超长上下文的核心技术之一。
Mini-GPT 训练过程
在 Decoder-only 的大语言模型(Mini-GPT)中,训练过程的核心被称为自回归的“下一个词预测”。
0. X 和 Y 的形式:错位时空
假设我们正在用唐诗训练你的模型,词表里有很多中文字符,每个字符都有一个唯一的整数 ID。假设上下文窗口大小(block_size 或 seq_len)设置为 4。当前要训练的一句诗是:“床前明月光”。在构建训练数据时,X 和 Y 是从同一段文本中截取出来的,仅仅相差一个字符的位移。
- 输入 X(Input):
[床, 前, 明, 月]对应 ID[12, 45, 9, 33] - 标签 Y(Target):
[前, 明, 月, 光]对应 ID[45, 9, 33, 88]
它们的形状都是 [Batch, SeqLen](这里是 [Batch, 4])。在 RNN 时代,计算“明”的概率,必须等“前”的概率算完,因为信息是串行传递的。但在 Transformer 中,这四个字的预测是在 GPU 中同一瞬间、完全并发算出来的。它是怎么做到的?核心秘诀就藏在“矩阵乘法”和“下三角掩码(Causal Mask)”的精妙配合中。
1. 算力全开:不管三七二十一,先全算出来
假设我们已经得到了 4 个字的 Q 和 K 矩阵。此时,GPU 不会去管什么“先后顺序”,它直接执行一次极其暴力的矩阵乘法:Q×KT。这一步算出来的是一个 [4, 4] 的注意力原始分数矩阵。在这个矩阵里,每一个字都看到了所有的字(包括未来):第 1 行(Q=“床”)算出了它对 [床, 前, 明, 月] 的注意力分数;第 2 行(Q=“前”)算出了它对 [床, 前, 明, 月] 的注意力分数;第 3 行(Q=“明”)算出了它对 [床, 前, 明, 月] 的注意力分数;第 4 行(Q=“月”)算出了它对 [床, 前, 明, 月] 的注意力分数。注意:此时物理层面的计算已经全部并行完成了!这是 GPU 最擅长的事情。只是现在的数据是不合法的,因为“床”看到了未来的“前明月”。
2. 物理阉割:下三角掩码
计算虽然完成了,但为了防止作弊,我们需要在把这些分数送进 Softmax 变成概率之前,进行一次“物理阉割”。我们会凭空生成一个同样是 [4, 4] 的掩码矩阵。这个矩阵的左下角(包含对角线)全是 0,右上角全是负无穷(或 -1e9)。我们将这个 Mask 直接加到刚才算出的 Scores 矩阵上。相加之后,神奇的事情发生了:右上角的所有分数都变成了负无穷,左下角保持原样。
3. 化腐朽为神奇:Softmax 激活
现在,我们对这个带负无穷的矩阵的每一行做 Softmax 归一化(把分数变成总和为 1 的概率)。数学上,e-∞ 等于 0。所以经过 Softmax 后,注意力权重矩阵变成了这样:
- 第 1 行(“床”)的权重:[1.0, 0, 0, 0](它被迫把 100% 的注意力放在“床”上,后面的未来全被抹零了)。
- 第 2 行(“前”)的权重:[0.01, 0.99, 0, 0](它只能在“床”和“前”之间分配注意力)。
- 第 3 行(“明”)的权重:[0.1, 0.2, 0.7, 0](它看到了前三个字)。
- 第 4 行(“月”)的权重:[0.1, 0.2, 0.2, 0.5](它看到了全部四个字)。
4. 并行提取特征
最后,拿这个 [4, 4] 的权重矩阵去乘以 V(值)矩阵。因为每一行的权重已经被 Mask 强行修改过了,所以:输出的第一行只融合了“床”的 V;输出的第二行只融合了“床、前”的 V;……以此类推。这 4 个融合了正确历史上下文的特征向量,会同时继续向上层传递,最终同时通过最后的线性层,变成 4 个预测下一个字的概率分布,然后与真实的 Y 中的 4 个 token 进行交叉熵损失计算。
