- Published on
Transformer 完全指南:从注意力机制到 GPT/DeepSeek 架构,再到 LLM 使用技巧
- Authors

- Name
- Allen Wang
Transformer 完全指南:从注意力机制到 GPT/DeepSeek 架构,再到 LLM 使用技巧
🎯 本文目标:如果你听过 Transformer 但总觉得"似懂非懂",或者想知道为什么 ChatGPT 开头和结尾记得最牢、为什么 Chain-of-Thought 有效——这篇文章就是为你写的。我们将从最朴素的直觉出发,一步步拆解 Transformer 的每一个组件,直到你不仅能用 PyTorch 从零写出一个完整的 Transformer,还能真正理解你每天使用的 AI 背后的原理。
📑 目录
- 一、为什么需要 Transformer?
- 二、注意力机制:从"查字典"说起
- 三、Q、K、V 矩阵运算:注意力的数学实现
- 四、缩放点积注意力:为什么要除以 √d_k
- 五、多头注意力:一个脑袋怎么够?
- 六、位置编码:让模型知道"谁在前谁在后"
- 七、Encoder 架构:阅读理解专家
- 八、Decoder 架构:严禁偷看的写作专家
- 九、完整 Transformer:组装编码器与解码器
- 十、三大架构家族与主流 LLM 架构详解
- 十一、PyTorch 代码实战:从零搭建 Transformer
- 十二、常见坑点与最佳实践
- 十三、从 Transformer 原理到 LLM 使用技巧
- 十四、2025-2026 Transformer 前沿趋势
- 十五、总结与学习路线
一、为什么需要 Transformer?
在 Transformer 出现之前(2017 年以前),自然语言处理的"标配"模型是 RNN(循环神经网络)(白话版:一种像流水线一样,一个词一个词顺序处理文本的神经网络)和它的升级版 LSTM(长短期记忆网络)(白话版:RNN 加了一个"记忆本"来缓解遗忘问题)。
但是它们有三个致命的缺陷:
1.1 RNN 的三大痛点
痛点 ①:串行处理,慢如蜗牛 🐌
想象你在翻译一本 1000 页的书。RNN 的做法是:读完第 1 页→翻译→读第 2 页→翻译→……→第 1000 页。它必须一页一页来,前面没读完就不能读后面的。
# RNN 的伪代码 — 注意这里是一个 for 循环,必须串行执行
hidden = zeros(hidden_size)
for word in sentence: # 必须顺序处理每个词!
hidden = rnn_cell(word, hidden) # 当前词 + 上一步的隐藏状态
output = hidden # 最终状态包含"全部信息"
这意味着一个 1000 词的句子,需要做 1000 步计算,完全无法利用 GPU 的并行计算能力。
痛点 ②:长距离遗忘 🧠💨
"那只在公园里追蝴蝶的、毛色金黄的、带着红色项圈的小猫,最终还是没有抓到它。"
当 RNN 读到"它"这个字时,它需要知道"它"指的是"小猫"。但问题是:从"小猫"到"它"中间隔了十几个词,RNN 的记忆会随着每一步计算逐渐衰减——就像传话游戏,传了十几个人以后,原始信息早已面目全非。
痛点 ③:信息瓶颈 🍾
在 Encoder-Decoder 结构(比如机器翻译)中,RNN 把整个输入句子压缩成一个固定大小的向量,然后让 Decoder 从这个向量生成翻译。一个几百维的向量要承载整个句子的所有信息——这就像把一本书的内容压缩到一张便利贴上。
1.2 Transformer 的革命性思想
2017 年,Google 的论文 "Attention Is All You Need" 提出了 Transformer,核心思想简单到令人惊叹:
不需要按顺序读,直接"全局扫描",让每个词同时看到所有其他词。
| 对比维度 | RNN/LSTM | Transformer |
|---|---|---|
| 处理方式 | 串行(一个接一个) | 并行(一次看全部) |
| 长距离依赖 | 随距离衰减 | 直接连接,无衰减 |
| 训练速度 | 慢(无法并行) | 快(充分利用 GPU) |
| 信息传递 | 通过隐藏状态传递 | 通过注意力直接访问 |
用一个生活中的类比:
- RNN 像是在一个嘈杂的派对上,你只能和旁边的人说话,然后让他帮你传话给远处的人。传到第十个人时,信息早就变了。
- Transformer 像是你拥有一副千里眼,可以同时看到派对上的每一个人,直接和你想交流的人对话。
这就是 注意力机制(Attention Mechanism) 的核心直觉。接下来,让我们深入它的细节。
📝 本节小结
- RNN 的三大问题:串行处理、长距离遗忘、信息瓶颈
- Transformer 用"注意力机制"替代了循环结构,实现并行处理和全局信息访问
- 论文名 "Attention Is All You Need" 不是夸张——它真的只靠注意力就完成了一切
二、注意力机制:从"查字典"说起
注意力机制(Attention Mechanism)(白话版:一种让模型在处理某个词时,能够"回头看"所有其他词,并决定每个词有多重要的技术)是 Transformer 的灵魂。我们用一个人人都懂的例子来理解它。
2.1 直觉类比:查字典
想象你在查一本英汉字典:
- 你有一个想查的词(比如 "apple")——这是你的 Query(查询)
- 字典里每个词条有一个索引词(比如 "apple", "apply", "banana")——这些是 Key(键)
- 每个词条对应一个翻译("苹果", "应用", "香蕉")——这些是 Value(值)
查字典的过程就是:
1. 拿着 Query("apple")和每个 Key 比较相似度
2. 发现 Key="apple" 完全匹配 → 得分最高
3. Key="apply" 有点像 → 得分较高
4. Key="banana" 完全不像 → 得分很低
5. 根据得分高低,加权获取对应的 Value
6. 最终结果:主要获取"苹果"的翻译,略带一点"应用"的信息
💡 这就是注意力机制的本质:根据相似度,对信息进行加权组合。
2.2 从字典到注意力的数学
让我们把"查字典"翻译成数学语言:
第一步:计算相似度
最直观的相似度度量就是 点积(Dot Product)(白话版:两个向量对应元素相乘再求和,数值越大说明两个向量方向越一致,即越"像"):
import torch
# 一个简单的例子:3个词,每个词用4维向量表示
query = torch.tensor([1.0, 0.5, 0.0, 0.3]) # "apple" 的向量
keys = torch.tensor([
[1.0, 0.4, 0.1, 0.3], # "apple" — 非常相似
[0.8, 0.3, 0.2, 0.2], # "apply" — 有点像
[0.0, 0.1, 0.9, 0.7], # "banana" — 完全不像
])
# 点积计算相似度
scores = torch.matmul(keys, query)
print(f"原始得分: {scores}")
# 输出类似: tensor([1.39, 1.01, 0.26])
🔍 手算完整过程:Query 如何找到最相似的 Key
让我们用上面的例子,一步一步手算出 Query "apple" 是怎么在 3 个 Key 中找到最相似的那个的。不要跳步,我们每一步都写出来:
Query(我要查的词 "apple"): [1.0, 0.5, 0.0, 0.3]
Key₁("apple" 的索引): [1.0, 0.4, 0.1, 0.3]
Key₂("apply" 的索引): [0.8, 0.3, 0.2, 0.2]
Key₃("banana" 的索引): [0.0, 0.1, 0.9, 0.7]
计算 Query 和 Key₁ 的点积("apple" vs "apple"):
Q · K₁ = 1.0×1.0 + 0.5×0.4 + 0.0×0.1 + 0.3×0.3
= 1.0 + 0.2 + 0.0 + 0.09
= 1.29
🧐 每一对对应位置的数字相乘,然后全部加起来。两个向量越"朝同一个方向",乘出来的和就越大。
计算 Query 和 Key₂ 的点积("apple" vs "apply"):
Q · K₂ = 1.0×0.8 + 0.5×0.3 + 0.0×0.2 + 0.3×0.2
= 0.8 + 0.15 + 0.0 + 0.06
= 1.01
计算 Query 和 Key₃ 的点积("apple" vs "banana"):
Q · K₃ = 1.0×0.0 + 0.5×0.1 + 0.0×0.9 + 0.3×0.7
= 0.0 + 0.05 + 0.0 + 0.21
= 0.26
汇总原始得分:
Score("apple") = 1.29 ← 最高!果然 "apple" 和 "apple" 最像
Score("apply") = 1.01 ← 有点像,但不如完全匹配
Score("banana") = 0.26 ← 得分很低,"banana" 和 "apple" 差别大
💡 直觉理解:点积越大 = 两个向量越"像" = 注意力越强。就像你在字典里翻到了一个和你要查的词长得很像的词条,你自然会多看几眼。
第二步:Softmax — 把分数变成"聚光灯"
Softmax(白话版:一种数学函数,把任意数值转换成 0 到 1 之间的概率分布,所有值加起来等于 1)就像一个聚光灯——把模糊的分数变成清晰的概率分布:
# 应用 Softmax
weights = torch.softmax(scores, dim=0)
print(f"注意力权重: {weights}")
# 输出类似: tensor([0.50, 0.34, 0.16])
# "apple" 获得最高权重,"banana" 最低
第三步:加权组合 Value
values = torch.tensor([
[1.0, 0.0, 0.0], # "苹果" 的表示
[0.0, 1.0, 0.0], # "应用" 的表示
[0.0, 0.0, 1.0], # "香蕉" 的表示
])
# 加权组合
output = torch.matmul(weights, values)
print(f"最终输出: {output}")
# 输出类似: tensor([0.50, 0.34, 0.16])
# 主要是"苹果"的信息,少量"应用"的信息
2.3 自注意力:自己查自己的字典
上面的例子是在两种不同的东西之间做注意力(英文→中文)。但 Transformer 最常用的是 自注意力(Self-Attention)(白话版:一个句子中的每个词,都把自己句子中的所有词当作字典来查)。
来看一个经典例子:
"动物没有穿过马路,因为它太累了。"
当模型处理"它"这个词时,自注意力机制会让"它"去查看句子中所有词:
- "动物" → 相似度很高("它"指代"动物")✅
- "马路" → 相似度低("它"不是"马路")
- "累" → 相似度中等(和"它"的状态有关)
- "穿过" → 相似度低
这样,模型就自动学会了指代消解(Coreference Resolution)——"它"指的是"动物",而不是"马路"。
🎨 交互式演示:下面的可视化展示了自注意力的工作过程——点击任意词查看它对其他词的注意力分布。
2.4 注意力的不对称性
一个常被忽略的重要性质:注意力不是对称的!
"苹果"对"牛顿"的注意力 ≠ "牛顿"对"苹果"的注意力
💡 白话版:"苹果砸到了牛顿"——从苹果的角度看,牛顿只是一个被砸的路人;但从牛顿的角度看,苹果是改变他命运的关键!
这是因为 Q 和 K 是通过不同的投影矩阵生成的,所以 。
2.5 常见坑点
⚠️ 坑 1:混淆注意力权重和注意力输出
- 注意力权重(weights)是 softmax 后的概率分布,形状为
[seq_len, seq_len]- 注意力输出(output)是权重与 Value 的加权和,形状为
[seq_len, d_v]- 很多初学者把这两个搞混了
⚠️ 坑 2:以为注意力只看"最相关"的词
Softmax 产生的是概率分布,所以模型实际上会看所有词,只是权重不同。即使一个词的权重只有 0.01,它的信息也会被包含在输出中——只是贡献很小。
📝 本节小结
- 注意力 = 用 Query 在 Key 中查找相似项,然后用相似度加权获取 Value
- 自注意力 = 句子中的每个词都和自己句子中的所有词做注意力
- 注意力是不对称的:A 关注 B ≠ B 关注 A
- 核心公式:
output = softmax(Q · K^T) · V
三、Q、K、V 矩阵运算:注意力的数学实现
上一节我们理解了注意力的直觉,但还有一个关键问题:Q、K、V 从哪来?
3.1 直觉:同一个输入的"三重人格"
还记得查字典的比喻吗?在自注意力中,Q、K、V 全部来自同一个输入!这就像一个人在面试中同时扮演三个角色:
- 作为 Query(提问者):我想了解什么信息?
- 作为 Key(简历标题):我能提供什么类型的信息?
- 作为 Value(简历内容):我具体能提供什么内容?
同一个词,通过不同的线性变换,被投影到三个不同的"角色空间":
其中 是输入嵌入矩阵(白话版:把每个词转换成一串数字后,把整个句子的数字排成一个矩阵), 是三个可学习的 权重矩阵(Weight Matrix)(白话版:一组可以在训练中不断调整的参数,用来做线性变换)。
3.2 现实例子:一步步算注意力
假设我们有一个包含 4 个词的句子 "I love deep learning",嵌入维度 (实际中是 512 或 768,这里缩小便于理解)。
import torch
import torch.nn as nn
# 模拟输入:4个词,每个词3维嵌入
X = torch.tensor([
[1.0, 0.0, 1.0], # "I"
[0.0, 1.0, 1.0], # "love"
[1.0, 1.0, 0.0], # "deep"
[0.0, 1.0, 0.0], # "learning"
], dtype=torch.float32)
d_model = 3 # 嵌入维度
# 三个投影矩阵(实际中这些是可学习的参数)
W_Q = torch.tensor([[1, 0, 1], [0, 1, 0], [1, 0, 0]], dtype=torch.float32)
W_K = torch.tensor([[0, 1, 0], [1, 0, 1], [0, 1, 1]], dtype=torch.float32)
W_V = torch.tensor([[1, 0, 0], [0, 0, 1], [0, 1, 0]], dtype=torch.float32)
# Step 1: 投影得到 Q, K, V
Q = X @ W_Q # [4, 3] @ [3, 3] = [4, 3]
K = X @ W_K # [4, 3] @ [3, 3] = [4, 3]
V = X @ W_V # [4, 3] @ [3, 3] = [4, 3]
print(f"Q:\n{Q}")
print(f"K:\n{K}")
print(f"V:\n{V}")
# Step 2: 计算注意力分数 Q · K^T
scores = Q @ K.T # [4, 3] @ [3, 4] = [4, 4]
print(f"注意力分数矩阵:\n{scores}")
# 这是一个 4×4 的矩阵:scores[i][j] 表示第 i 个词对第 j 个词的原始注意力分数
# Step 3: 缩放(下一节会详细解释为什么要缩放)
import math
d_k = d_model # Key 的维度
scaled_scores = scores / math.sqrt(d_k)
print(f"缩放后的分数:\n{scaled_scores}")
# Step 4: Softmax — 每一行变成概率分布
attention_weights = torch.softmax(scaled_scores, dim=-1)
print(f"注意力权重:\n{attention_weights}")
# 每一行的和都等于 1.0
print(f"每行求和验证: {attention_weights.sum(dim=-1)}")
# Step 5: 加权组合 Value
output = attention_weights @ V # [4, 4] @ [4, 3] = [4, 3]
print(f"注意力输出:\n{output}")
# 每一行是对所有 Value 向量的加权组合
💡 维度变化路径
步骤 操作 形状 输入 X [4, 3](seq_len × d_model)投影 X × W_Q [4, 3](seq_len × d_k)分数 Q × K^T [4, 4](seq_len × seq_len)权重 softmax(scores) [4, 4]输出 weights × V [4, 3](seq_len × d_v)
🔍 手算矩阵乘法:X × W_Q 到底在算什么?
很多初学者看到 Q = X @ W_Q 就一头雾水——这个矩阵乘法到底在做什么?让我们用上面的例子一个格子一个格子地算清楚:
输入 X(4个词 × 3维嵌入): 权重矩阵 W_Q(3×3):
d₁ d₂ d₃ d₁' d₂' d₃'
"I" [1.0, 0.0, 1.0] d₁ [ 1, 0, 1 ]
"love"[0.0, 1.0, 1.0] × d₂ [ 0, 1, 0 ]
"deep"[1.0, 1.0, 0.0] d₃ [ 1, 0, 0 ]
"learning"[0.0, 1.0, 0.0]
计算 Q[0][0]("I" 的 Query 第 1 维):
Q[0][0] = X[0][0]×W_Q[0][0] + X[0][1]×W_Q[1][0] + X[0][2]×W_Q[2][0]
= 1.0 × 1 + 0.0 × 0 + 1.0 × 1
= 1.0 + 0.0 + 1.0
= 2.0
🧐 就是取 X 的第 0 行("I" 的嵌入)和 W_Q 的第 0 列,对应相乘再求和。
计算 Q[0][1]("I" 的 Query 第 2 维):
Q[0][1] = 1.0 × 0 + 0.0 × 1 + 1.0 × 0 = 0.0
计算 Q[0][2]("I" 的 Query 第 3 维):
Q[0][2] = 1.0 × 1 + 0.0 × 0 + 1.0 × 0 = 1.0
所以 "I" 的 Query 向量 = [2.0, 0.0, 1.0]
同理可以算出所有词的 Query:
完整的 Q 矩阵:
d₁' d₂' d₃'
"I" [2.0, 0.0, 1.0] ← 1×1+0×0+1×1, 1×0+0×1+1×0, 1×1+0×0+1×0
"love"[1.0, 1.0, 0.0] ← 0×1+1×0+1×1, 0×0+1×1+1×0, 0×1+1×0+1×0
"deep"[1.0, 1.0, 1.0] ← 1×1+1×0+0×1, 1×0+1×1+0×0, 1×1+1×0+0×0
"learning"[0.0, 1.0, 0.0] ← 0×1+1×0+0×1, 0×0+1×1+0×0, 0×1+1×0+0×0
💡 直觉理解:矩阵乘法 X × W_Q 的本质是——对输入 X 的每个词向量做一次线性变换,把它从"原始嵌入空间"投影到"Query 空间"。W_Q 就像一副"Query 眼镜",让同一个词从不同角度(Q/K/V)被"看到"。
同样的 X 乘以不同的权重矩阵 W_K、W_V,就得到 Key 和 Value——同一个输入,三种不同的"视角"。
🎨 交互式演示:下面的可视化让你点击结果矩阵的任意一个格子,就能高亮看到它是 X 的哪一行和 W_Q 的哪一列相乘得到的,还有逐步计算过程的动画。
3.3 用 PyTorch nn.Linear 的优雅写法
import torch
import torch.nn as nn
class SelfAttention(nn.Module):
def __init__(self, d_model):
super().__init__()
self.d_model = d_model
# 三个线性层分别生成 Q, K, V
self.W_Q = nn.Linear(d_model, d_model, bias=False)
self.W_K = nn.Linear(d_model, d_model, bias=False)
self.W_V = nn.Linear(d_model, d_model, bias=False)
def forward(self, x):
Q = self.W_Q(x) # [batch, seq_len, d_model]
K = self.W_K(x)
V = self.W_V(x)
# 计算缩放点积注意力
scores = torch.matmul(Q, K.transpose(-2, -1)) / (self.d_model ** 0.5)
weights = torch.softmax(scores, dim=-1)
output = torch.matmul(weights, V)
return output, weights
# 测试
attn = SelfAttention(d_model=64)
x = torch.randn(2, 10, 64) # batch=2, seq_len=10, d_model=64
output, weights = attn(x)
print(f"输出形状: {output.shape}") # [2, 10, 64]
print(f"权重形状: {weights.shape}") # [2, 10, 10]
🎨 交互式演示:下面的可视化将 Q×K^T→缩放→Softmax→×V 的每一步以动画形式展示,你可以逐步查看矩阵运算的过程。
3.4 常见坑点
⚠️ 坑 1:Q、K、V 的维度可以不同
虽然在自注意力中 是最常见的设定,但实际上 Q 和 K 的维度必须相同(因为要做点积),而 V 的维度可以不同。在多头注意力中,每个头的 。
⚠️ 坑 2:注意力分数矩阵是方阵
在自注意力中,Q 和 K 来自同一个序列,所以分数矩阵是
[seq_len, seq_len]的方阵。但在交叉注意力(Cross-Attention) 中,Q 来自 Decoder,K/V 来自 Encoder,分数矩阵是[decoder_len, encoder_len]的矩形。
📝 本节小结
- Q、K、V 通过三个不同的线性变换从同一个输入 X 产生
- 完整公式:
- 维度变化:
[seq, d] → [seq, seq] → [seq, d]- Q、K 维度必须匹配,V 维度可以不同
四、缩放点积注意力:为什么要除以 √d_k
你可能已经注意到了公式里那个 ——为什么不直接 softmax(QK^T) 呢?这看起来是个小细节,但理解它对于训练稳定性至关重要。
4.1 直觉类比:音量调节旋钮 🔊
想象你在一个考场里,学生们在小声讨论答案(点积分数)。Softmax 就像一个"音量调节器"——把讨论声变成清晰的"谁说得对"的排名。
如果讨论声很小(分数值适中),你还能听清每个人都在说什么(分布均匀)。
但如果讨论声越来越大(维度 增大导致点积数值暴涨),声音最大的那个人会完全盖过其他所有人(softmax 饱和,变成 one-hot)——你只听到一个人的声音,其他所有信息都丢失了!
就是那个音量旋钮:把音量调回合理范围,让你听清所有人的意见。
4.2 数学原理:方差爆炸
假设 和 中的每个元素都是独立的、均值为 0、方差为 1 的随机变量,那么它们的点积:
根据概率论,这个和的方差为 (每一项的方差是 1,共 项,独立相加方差线性增长)。
- 当 时,点积的标准差约为
- 当 时,点积的标准差约为
这意味着,随着维度增大,点积的数值会越来越大——大到 softmax 几乎变成 one-hot 分布。
import torch
d_k_values = [1, 4, 16, 64, 256, 512]
for d_k in d_k_values:
q = torch.randn(1, d_k)
k = torch.randn(5, d_k)
# 未缩放
raw_scores = q @ k.T
raw_probs = torch.softmax(raw_scores, dim=-1)
# 缩放后
scaled_scores = raw_scores / (d_k ** 0.5)
scaled_probs = torch.softmax(scaled_scores, dim=-1)
print(f"d_k={d_k:>3d} | 未缩放最大权重: {raw_probs.max():.4f} | "
f"缩放后最大权重: {scaled_probs.max():.4f}")
运行结果(大致):
d_k= 1 | 未缩放最大权重: 0.3500 | 缩放后最大权重: 0.3500
d_k= 4 | 未缩放最大权重: 0.5200 | 缩放后最大权重: 0.3800
d_k= 16 | 未缩放最大权重: 0.7800 | 缩放后最大权重: 0.3500
d_k= 64 | 未缩放最大权重: 0.9500 | 缩放后最大权重: 0.3200
d_k=256 | 未缩放最大权重: 0.9990 | 缩放后最大权重: 0.3600
d_k=512 | 未缩放最大权重: 0.9999 | 缩放后最大权重: 0.3400
4.3 梯度消失:为什么 softmax 饱和是致命的
当 softmax 输出接近 one-hot 分布时,梯度几乎为零。回忆 softmax 的梯度:
当某个 时,(对所有 ),于是梯度接近 。
结果就是:模型在训练初期就"坚信"某个词最重要,再也不更新——彻底丧失学习能力。
🎨 交互式演示:拖动滑块调整维度 d_k,直观感受缩放对 softmax 分布的影响——看看不缩放时注意力是如何变成"one-hot"的。
4.4 最佳实践
💡 最佳实践 1:始终使用缩放
除非你有非常好的理由(比如 d_k 很小),否则永远除以 √d_k。这是论文的默认设定,也是 PyTorch
nn.MultiheadAttention的默认行为。
💡 最佳实践 2:初始化权重时考虑维度
使用 Xavier/Glorot 初始化或 Kaiming 初始化,它们会自动根据维度调整权重的方差,配合缩放点积效果更好。
4.5 常见坑点
⚠️ 坑:手动实现时忘了缩放
很多初学者在手写 Attention 时直接写
softmax(Q @ K.T),忘了除以sqrt(d_k)。小维度时问题不大,一旦 d_k > 64 就会发现模型完全不收敛。
📝 本节小结
- 除以 是为了控制点积的方差,防止 softmax 饱和
- 没有缩放:d_k 越大,softmax 越接近 one-hot,梯度越接近零
- 缩放后:无论 d_k 多大,softmax 都保持健康的概率分布
- 这不是可选的优化,而是必需的操作
五、多头注意力:一个脑袋怎么够?
一个自注意力头只能学到一种"关注模式"。但语言中的关系是多种多样的——语法关系、语义关系、指代关系、位置关系……一个头怎么能同时捕获所有这些?
5.1 直觉类比:阅读理解小组 📖
想象一个阅读理解小组,每个人负责从不同角度分析一段文字:
- 小明(头 1)负责找语法关系:主语-谓语-宾语
- 小红(头 2)负责找指代关系:代词指代什么
- 小华(头 3)负责找相邻关系:前后词的搭配
- 小李(头 4)负责找语义关系:近义词、反义词
最后,他们把各自的发现汇总,得到一份全面的分析报告。
这就是多头注意力(Multi-Head Attention)(白话版:把注意力机制复制多份,每份关注不同类型的关系,最后把结果拼接起来)的核心思想。
5.2 技术实现:切分、并行、拼接
具体来说,多头注意力做了三件事:
Step 1:切分
把输入的嵌入维度 均匀切分成 份( = 头的数量):
比如 , ,则每个头处理 维的子空间。
Step 2:并行计算
每个头独立做一次完整的注意力计算:
Step 3:拼接 + 投影
是一个 输出投影矩阵(白话版:把拼接后的长向量压缩回原始维度 )。
5.3 代码实现
import torch
import torch.nn as nn
import math
class MultiHeadAttention(nn.Module):
def __init__(self, d_model, n_heads):
super().__init__()
assert d_model % n_heads == 0, "d_model 必须能被 n_heads 整除"
self.d_model = d_model
self.n_heads = n_heads
self.d_k = d_model // n_heads # 每个头的维度
# 四个投影矩阵
self.W_Q = nn.Linear(d_model, d_model, bias=False)
self.W_K = nn.Linear(d_model, d_model, bias=False)
self.W_V = nn.Linear(d_model, d_model, bias=False)
self.W_O = nn.Linear(d_model, d_model, bias=False)
def forward(self, Q, K, V, mask=None):
batch_size = Q.size(0)
# 1. 线性投影
Q = self.W_Q(Q) # [batch, seq_len, d_model]
K = self.W_K(K)
V = self.W_V(V)
# 2. 切分成多个头: [batch, seq, d_model] → [batch, n_heads, seq, d_k]
Q = Q.view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
K = K.view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
V = V.view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
# 3. 缩放点积注意力
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))
attention_weights = torch.softmax(scores, dim=-1)
context = torch.matmul(attention_weights, V)
# 4. 拼接所有头: [batch, n_heads, seq, d_k] → [batch, seq, d_model]
context = context.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model)
# 5. 输出投影
output = self.W_O(context)
return output, attention_weights
# 测试
mha = MultiHeadAttention(d_model=512, n_heads=8)
x = torch.randn(2, 10, 512) # batch=2, seq_len=10
output, weights = mha(x, x, x) # 自注意力:Q=K=V=x
print(f"输出形状: {output.shape}") # [2, 10, 512]
print(f"权重形状: {weights.shape}") # [2, 8, 10, 10] — 8 个头的注意力矩阵
💡 关键洞察:参数量完全相同!
一个容易混淆的点:多头注意力和单头注意力的参数量是一样的!
方式 Q 投影 K 投影 V 投影 总计 单头 (d=512) 512×512 512×512 512×512 786K 8 头 (d_k=64) 512×512 512×512 512×512 786K 区别在于多头会多一个 (512×512),但这只增加了 25% 的参数。核心原因是:多头并不是把参数量翻 h 倍,而是把同一维度的空间切分成 h 个子空间。
5.4 GPT-3 有 96 个头!
实际的大模型使用的头数远比 8 多:
| 模型 | d_model | n_heads | d_k |
|---|---|---|---|
| BERT-base | 768 | 12 | 64 |
| BERT-large | 1024 | 16 | 64 |
| GPT-2 | 768 | 12 | 64 |
| GPT-3 | 12288 | 96 | 128 |
| LLaMA-7B | 4096 | 32 | 128 |
研究表明不同的头确实会学到不同的"技能"——有的擅长语法分析,有的擅长指代消解,有的只关注相邻的词。
🎨 交互式演示:下面的可视化展示了多头注意力的工作过程——每个头用不同颜色表示不同的关注模式,你可以切换查看各个头分别学到了什么。
5.5 常见坑点
⚠️ 坑 1:d_model 不能被 n_heads 整除
如果 d_model=100,n_heads=8,那 100/8=12.5——无法均匀切分!所以 d_model 必须是 n_heads 的整数倍。常见组合:512/8、768/12、1024/16。
⚠️ 坑 2:忘了 transpose 和 contiguous
多头的 view/reshape 操作需要先 transpose 再 contiguous。如果不加
.contiguous(),后面的.view()会报错 "view size is not compatible with input tensor's size and stride"。
⚠️ 坑 3:以为每个头有独立的 W_Q/W_K/W_V
实际实现中,我们只有一个
nn.Linear(d_model, d_model)作为 W_Q,然后通过 view/reshape 把输出切分成多个头。不是每个头一个nn.Linear(d_model, d_k)——那样参数量就真的翻倍了。
📝 本节小结
- 多头注意力 = 把嵌入空间切分成多个子空间,每个头独立做注意力
- 步骤:投影 → 切分 → 并行注意力 → 拼接 → 输出投影
- 参数量与单头相当,但信息提取能力更强
- 不同的头自动学会关注不同类型的关系
六、位置编码:让模型知道"谁在前谁在后"
到目前为止,你可能已经发现了一个问题:注意力机制是对称的——它不关心词的顺序!
6.1 直觉:打乱顺序的灾难 🔀
"猫 追 狗" → 猫在追狗
"狗 追 猫" → 狗在追猫
这两句话意思完全相反!但对于自注意力来说,它只关心"每个词和其他词的关系",完全不知道谁在前面、谁在后面。
因为注意力的计算公式 中,、、 是通过逐元素的线性变换得到的,不包含任何位置信息。你打乱输入词的顺序,每个词得到的 Q/K/V 向量完全不变,只是注意力矩阵的行列顺序变了——但 softmax 是对称的,所以结果中的信息也不变。
💡 白话版:自注意力就像一群人围坐在圆桌旁开会——它能听懂每个人在说什么,但不知道谁坐在谁旁边。
6.2 解决方案:给每个位置一个"身份证"
位置编码(Positional Encoding, PE)(白话版:给序列中每个位置分配一个独特的数字向量,加到词嵌入上,让模型知道每个词的位置)的思想很简单:在输入嵌入上加上一个和位置相关的向量。
但怎么设计这个位置向量呢?
6.3 失败的尝试:线性编码
最简单的想法:位置 0 编码为 0,位置 1 编码为 1,位置 2 编码为 2……
# 方案 1:线性编码(有问题!)
pe = torch.arange(0, seq_len).float()
# 位置 0: [0, 0, 0, ...]
# 位置 1: [1, 1, 1, ...]
# 位置 100: [100, 100, 100, ...]
问题:位置 100 的编码值是位置 1 的 100 倍!数值范围差异巨大,会严重干扰词嵌入的信息。而且模型无法推广到训练时没见过的更长序列。
6.4 正弦余弦编码:Transformer 的优雅方案
论文中使用的方案非常巧妙——用不同频率的 正弦(sin) 和 余弦(cos) 函数:
其中 是位置索引, 是维度索引。
为什么这么设计?三大优点:
① 有界性:sin 和 cos 的值永远在 之间,不会像线性编码那样数值爆炸。
② 唯一性:不同位置的编码是唯一的。因为不同维度使用不同频率——低维度用高频(变化快),高维度用低频(变化慢),就像钟表的秒针、分针、时针一样,组合起来可以唯一表示任何时刻。
③ 相对距离可学习:对于任意固定偏移 , 可以表示为 的线性变换。这意味着模型可以通过学习简单的线性关系来捕获相对位置信息。
6.5 代码实现
import torch
import math
class PositionalEncoding(torch.nn.Module):
def __init__(self, d_model, max_len=5000):
super().__init__()
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len).unsqueeze(1).float()
# 频率递减:低维度高频,高维度低频
div_term = torch.exp(
torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)
)
pe[:, 0::2] = torch.sin(position * div_term) # 偶数维度用 sin
pe[:, 1::2] = torch.cos(position * div_term) # 奇数维度用 cos
pe = pe.unsqueeze(0) # 增加 batch 维度: [1, max_len, d_model]
self.register_buffer('pe', pe)
def forward(self, x):
# x: [batch, seq_len, d_model]
seq_len = x.size(1)
return x + self.pe[:, :seq_len, :]
# 使用示例
d_model = 128
pe = PositionalEncoding(d_model)
x = torch.randn(2, 20, d_model) # 2 个句子,每句 20 个词
output = pe(x)
print(f"输入形状: {x.shape}") # [2, 20, 128]
print(f"输出形状: {output.shape}") # [2, 20, 128] — 加了位置编码但形状不变
💡 理解频率递减
JavaScript维度 0-1 (i=0): 频率 = 1/10000^0 = 1 → 变化最快(秒针) 维度 2-3 (i=1): 频率 = 1/10000^(2/d) ≈ 0.98 → 稍慢 ... 维度 d-2, d-1: 频率 = 1/10000^1 = 0.0001 → 变化最慢(时针)这种"多尺度"的设计让模型既能捕获近距离的位置关系(通过高频维度),也能捕获远距离的位置关系(通过低频维度)。
🎨 交互式演示:下面的可视化展示了位置编码的热力图——拖动滑块调整维度和序列长度,观察 sin/cos 函数如何为每个位置生成唯一编码。
6.6 RoPE:更现代的位置编码
值得一提的是,现代大模型(如 LLaMA、GPT-Neo)普遍使用 RoPE(Rotary Position Embedding)(白话版:通过旋转向量的方式编码相对位置,让注意力分数自然包含距离信息)而非原始的 sin/cos 编码。RoPE 的核心优势是能更好地外推到训练时没见过的长序列。
6.7 常见坑点
⚠️ 坑 1:位置编码是加法,不是拼接
正确做法是
embedding + PE,不是concat(embedding, PE)。加法不改变维度,拼接会把维度翻倍。
⚠️ 坑 2:忘了 register_buffer
位置编码不需要梯度(它是固定的),所以应该用
self.register_buffer('pe', pe)而不是self.pe = nn.Parameter(pe)。register_buffer 让它跟随模型一起移动到 GPU,但不参与梯度更新。
📝 本节小结
- 自注意力不感知位置,需要额外的位置编码
- sin/cos 编码的三大优点:有界、唯一、可学习相对距离
- 低维度高频(捕获近距离),高维度低频(捕获远距离)
- 位置编码通过加法融入词嵌入,不改变维度
七、Encoder 架构:阅读理解专家
有了前面的组件——多头注意力、位置编码——现在是时候把它们组装成完整的 Encoder(编码器) 了。
7.1 直觉类比:一个越来越懂你的阅读理解专家 📚
Encoder 就像一个阅读理解专家,它的工作流程是:
- 阅读全文(Self-Attention):看完所有词,理解词与词之间的关系
- 做笔记(Feed-Forward Network):对每个词的理解做深入加工
- 反复精读(堆叠多层):每一层都在上一层理解的基础上进一步深化
7.2 Encoder Layer 的四个组件
一个 Encoder Layer 包含以下组件,按顺序执行:
输入 x
↓
[Multi-Head Self-Attention] ← 全局信息交互
↓
[Add & Norm] ← 残差连接 + 层归一化
↓
[Feed-Forward Network] ← 逐位置深度加工
↓
[Add & Norm] ← 残差连接 + 层归一化
↓
输出
组件 ① Multi-Head Self-Attention
已经在第五节详细讲过了——让每个词看到所有其他词。
组件 ② Add & Norm(残差连接 + 层归一化)
残差连接(Residual Connection)(白话版:把输入直接"跳过"某个子层,和子层的输出相加。这样即使子层学不好,信号也能通过"跳线"直接传过去,防止深层网络退化):
层归一化(Layer Normalization, LayerNorm)(白话版:对每个样本的所有特征做归一化——减去均值、除以标准差——让数值保持在合理范围内,加速训练收敛):
💡 为什么需要残差连接?
Transformer 通常堆叠 6-96 层。如果没有残差连接,梯度在反向传播时需要经过每一层的变换,很容易消失或爆炸。残差连接提供了一条"高速公路",让梯度可以直接跳过任意多层,保持信号强度。
组件 ③ Feed-Forward Network (FFN)
前馈网络(Feed-Forward Network, FFN)(白话版:两个线性层中间夹一个 ReLU 激活函数,对每个位置独立做变换——不涉及词与词的交互,而是对单个词的表示做深度加工):
一个有趣的设计:FFN 的隐藏层维度 通常是 的 4 倍。比如 时 。这个"先扩展再压缩"的瓶颈结构是非常有效的信息加工模式。
7.3 代码实现
import torch
import torch.nn as nn
import math
class FeedForward(nn.Module):
"""前馈网络:两层线性变换 + ReLU"""
def __init__(self, d_model, d_ff, dropout=0.1):
super().__init__()
self.linear1 = nn.Linear(d_model, d_ff)
self.linear2 = nn.Linear(d_ff, d_model)
self.relu = nn.ReLU()
self.dropout = nn.Dropout(dropout)
def forward(self, x):
return self.linear2(self.dropout(self.relu(self.linear1(x))))
class EncoderLayer(nn.Module):
"""单个 Encoder 层"""
def __init__(self, d_model, n_heads, d_ff, dropout=0.1):
super().__init__()
self.self_attention = MultiHeadAttention(d_model, n_heads)
self.feed_forward = FeedForward(d_model, d_ff, dropout)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.dropout1 = nn.Dropout(dropout)
self.dropout2 = nn.Dropout(dropout)
def forward(self, x, mask=None):
# 子层 1: Multi-Head Self-Attention + Add & Norm
attn_output, _ = self.self_attention(x, x, x, mask)
x = self.norm1(x + self.dropout1(attn_output)) # 残差 + 归一化
# 子层 2: Feed-Forward + Add & Norm
ff_output = self.feed_forward(x)
x = self.norm2(x + self.dropout2(ff_output)) # 残差 + 归一化
return x
class Encoder(nn.Module):
"""完整 Encoder:词嵌入 + 位置编码 + N 个 Encoder 层"""
def __init__(self, vocab_size, d_model, n_heads, d_ff, n_layers, max_len=5000, dropout=0.1):
super().__init__()
self.d_model = d_model
self.embedding = nn.Embedding(vocab_size, d_model)
self.pos_encoding = PositionalEncoding(d_model, max_len)
self.layers = nn.ModuleList([
EncoderLayer(d_model, n_heads, d_ff, dropout)
for _ in range(n_layers)
])
self.dropout = nn.Dropout(dropout)
def forward(self, x, mask=None):
# 词嵌入 + 缩放 + 位置编码
x = self.embedding(x) * math.sqrt(self.d_model)
x = self.pos_encoding(x)
x = self.dropout(x)
# 通过 N 个 Encoder 层
for layer in self.layers:
x = layer(x, mask)
return x
# 测试
encoder = Encoder(
vocab_size=10000, d_model=512, n_heads=8,
d_ff=2048, n_layers=6
)
src = torch.randint(0, 10000, (2, 20)) # batch=2, seq_len=20
output = encoder(src)
print(f"Encoder 输出: {output.shape}") # [2, 20, 512]
💡 注意
* math.sqrt(d_model)这一步嵌入向量通常初始化为很小的值(均值 0,方差 ),而位置编码的值在 范围。为了防止位置编码的信号压过嵌入的信号,我们把嵌入乘以 来放大它。
7.4 Encoder 小结
完整 Encoder 数据流:
Token IDs → Embedding → × √d_model → + PositionalEncoding
↓
EncoderLayer × N
┌─────────────────┐
│ Self-Attention │
│ Add & Norm │
│ Feed-Forward │
│ Add & Norm │
└─────────────────┘
↓
Encoder Output
[batch, seq_len, d_model]
📝 本节小结
- Encoder = 词嵌入 + 位置编码 + N 个相同结构的层
- 每层:Self-Attention → Add&Norm → FFN → Add&Norm
- 残差连接保证梯度流动,LayerNorm 稳定训练
- FFN 的隐藏维度通常是 d_model 的 4 倍
- Encoder 可以看到全部输入(双向注意力)
八、Decoder 架构:严禁偷看的写作专家
如果 Encoder 是阅读理解专家,那 Decoder 就是写作专家——它的任务是逐词生成输出序列。但它有一个严格的规矩:写到第 n 个词时,绝对不能偷看第 n+1 个词以后的内容。
8.1 直觉类比:考试中的作文题 ✍️
想象你在写一篇英语作文,考试规则是:
- 不能偷看答案(Masked Self-Attention):你只能基于已经写下的内容来决定下一个词
- 可以参考阅读材料(Cross-Attention):你可以回头翻看阅读理解部分(Encoder 的输出)来获取信息
- 从左到右写(自回归生成):一个词一个词地写,前面的词决定后面的词
8.2 Decoder Layer 的六个组件
Decoder 比 Encoder 多了一个子层:
输入 x(已生成的词)
↓
[Masked Multi-Head Self-Attention] ← 只看已生成的词,不能偷看未来
↓
[Add & Norm]
↓
[Multi-Head Cross-Attention] ← Q 来自 Decoder,K/V 来自 Encoder
↓
[Add & Norm]
↓
[Feed-Forward Network]
↓
[Add & Norm]
↓
输出
新组件 ① Masked Self-Attention
因果掩码(Causal Mask)(白话版:一个上三角矩阵,把未来位置的注意力分数设为负无穷,经过 softmax 后变成 0,从而"遮住"未来的信息):
def create_causal_mask(seq_len):
"""创建因果掩码:上三角为 True(将被遮住)"""
mask = torch.triu(torch.ones(seq_len, seq_len), diagonal=1).bool()
return mask
# 对于 seq_len=5:
# [[0, 1, 1, 1, 1], ← 位置 0 只能看位置 0
# [0, 0, 1, 1, 1], ← 位置 1 能看位置 0, 1
# [0, 0, 0, 1, 1], ← 位置 2 能看位置 0, 1, 2
# [0, 0, 0, 0, 1], ← 位置 3 能看位置 0, 1, 2, 3
# [0, 0, 0, 0, 0]] ← 位置 4 能看位置 0, 1, 2, 3, 4
在注意力计算中,被遮住的位置设为 :
经过 softmax 后,,这些位置的注意力权重就变成了 0。
新组件 ② Cross-Attention(交叉注意力)
交叉注意力(Cross-Attention)(白话版:Decoder 的 Query 去"询问" Encoder 的输出,从中获取源序列的信息。就像翻译时回头看原文):
关键点:Q 来自 Decoder,K 和 V 来自 Encoder。
8.3 代码实现
class DecoderLayer(nn.Module):
"""单个 Decoder 层"""
def __init__(self, d_model, n_heads, d_ff, dropout=0.1):
super().__init__()
# 三个子层
self.masked_self_attention = MultiHeadAttention(d_model, n_heads)
self.cross_attention = MultiHeadAttention(d_model, n_heads)
self.feed_forward = FeedForward(d_model, d_ff, dropout)
# 三个 LayerNorm
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.norm3 = nn.LayerNorm(d_model)
# 三个 Dropout
self.dropout1 = nn.Dropout(dropout)
self.dropout2 = nn.Dropout(dropout)
self.dropout3 = nn.Dropout(dropout)
def forward(self, x, encoder_output, src_mask=None, tgt_mask=None):
# 子层 1: Masked Self-Attention
attn1, _ = self.masked_self_attention(x, x, x, tgt_mask)
x = self.norm1(x + self.dropout1(attn1))
# 子层 2: Cross-Attention(Q=decoder, K=V=encoder)
attn2, _ = self.cross_attention(x, encoder_output, encoder_output, src_mask)
x = self.norm2(x + self.dropout2(attn2))
# 子层 3: Feed-Forward
ff_output = self.feed_forward(x)
x = self.norm3(x + self.dropout3(ff_output))
return x
class Decoder(nn.Module):
"""完整 Decoder"""
def __init__(self, vocab_size, d_model, n_heads, d_ff, n_layers, max_len=5000, dropout=0.1):
super().__init__()
self.d_model = d_model
self.embedding = nn.Embedding(vocab_size, d_model)
self.pos_encoding = PositionalEncoding(d_model, max_len)
self.layers = nn.ModuleList([
DecoderLayer(d_model, n_heads, d_ff, dropout)
for _ in range(n_layers)
])
self.dropout = nn.Dropout(dropout)
def forward(self, x, encoder_output, src_mask=None, tgt_mask=None):
x = self.embedding(x) * math.sqrt(self.d_model)
x = self.pos_encoding(x)
x = self.dropout(x)
for layer in self.layers:
x = layer(x, encoder_output, src_mask, tgt_mask)
return x
8.4 自回归生成:今天的输出是明天的输入
训练和推理时,Decoder 的行为是不同的:
训练时(Teacher Forcing):
输入: [<BOS>, 我, 爱, 自然, 语言]
目标: [我, 爱, 自然, 语言, <EOS>]
所有位置并行计算(因为目标序列是已知的),但通过因果掩码确保每个位置只看到前面的词。
推理时(自回归):
Step 1: 输入 [<BOS>] → 预测 "我"
Step 2: 输入 [<BOS>, 我] → 预测 "爱"
Step 3: 输入 [<BOS>, 我, 爱] → 预测 "自然"
...
Step N: 输入 [<BOS>, 我, 爱, ... ] → 预测 <EOS> → 停止
每一步只生成一个词,然后把这个词追加到输入中,再预测下一个词。
🎨 交互式演示:下面的可视化对比了三种注意力掩码——双向(BERT)、因果(GPT)和交叉注意力——点击切换查看不同掩码的效果。
8.5 常见坑点
⚠️ 坑 1:训练时忘了用因果掩码
训练时虽然目标序列是完整的,但必须加因果掩码!否则模型在预测第 3 个词时已经"看过"了第 4、5 个词——这就是"信息泄漏(data leakage)",模型会得到虚假的高分但泛化性极差。
⚠️ 坑 2:混淆 Padding Mask 和 Causal Mask
- Padding Mask:遮住
<PAD>位置,形状为[batch, 1, 1, seq_len]- Causal Mask:遮住未来位置,形状为
[1, 1, seq_len, seq_len]- Decoder 需要两者都用:
combined_mask = causal_mask | padding_mask
⚠️ 坑 3:Cross-Attention 的 Q/K/V 搞反
Cross-Attention 中:Q 来自 Decoder,K 和 V 来自 Encoder。写成代码就是
cross_attn(dec_output, enc_output, enc_output)。如果写成cross_attn(enc_output, dec_output, dec_output)就完全反了!
📝 本节小结
- Decoder 比 Encoder 多一个 Cross-Attention 子层
- Masked Self-Attention 用因果掩码防止偷看未来
- Cross-Attention 让 Decoder 访问 Encoder 的输出信息
- 训练用 Teacher Forcing(并行),推理用自回归(串行)
九、完整 Transformer:组装编码器与解码器
终于到了最激动人心的时刻——把 Encoder 和 Decoder 组装成完整的 Transformer!
9.1 直觉:翻译流水线 🏭
想象一个专业的翻译流水线:
- 阅读部门(Encoder):把德语原文从头到尾读完,形成深度理解
- 翻译部门(Decoder):参考阅读部门的理解,一个词一个词地写出英文翻译
- 审核部门(Softmax + argmax):从所有候选词中选出概率最高的
9.2 完整架构图
源语言 Token IDs 目标语言 Token IDs(右移一位)
↓ ↓
[Embedding] [Embedding]
↓ ↓
[+ Pos Encoding] [+ Pos Encoding]
↓ ↓
┌─────────────────┐ ┌──────────────────────┐
│ Encoder × N │ │ Decoder × N │
│ │ K, V │ │
│ Self-Attn │────────────────────────→│ Masked Self-Attn │
│ Add & Norm │ │ Add & Norm │
│ FFN │ │ Cross-Attn (Q←Dec) │
│ Add & Norm │ │ Add & Norm │
└─────────────────┘ │ FFN │
│ Add & Norm │
└──────────────────────┘
↓
[Linear Layer]
↓
[Softmax]
↓
Output Probabilities
9.3 完整代码实现
class Transformer(nn.Module):
"""完整的 Transformer 模型"""
def __init__(
self,
src_vocab_size, # 源语言词表大小
tgt_vocab_size, # 目标语言词表大小
d_model=512, # 嵌入维度
n_heads=8, # 注意力头数
d_ff=2048, # FFN 隐藏层维度
n_encoder_layers=6,
n_decoder_layers=6,
max_len=5000,
dropout=0.1,
):
super().__init__()
self.encoder = Encoder(src_vocab_size, d_model, n_heads, d_ff,
n_encoder_layers, max_len, dropout)
self.decoder = Decoder(tgt_vocab_size, d_model, n_heads, d_ff,
n_decoder_layers, max_len, dropout)
# 最终输出层:d_model → vocab_size
self.output_projection = nn.Linear(d_model, tgt_vocab_size)
def forward(self, src, tgt, src_mask=None, tgt_mask=None):
# 1. Encoder 处理源序列
encoder_output = self.encoder(src, src_mask)
# 2. Decoder 处理目标序列(参考 Encoder 输出)
decoder_output = self.decoder(tgt, encoder_output, src_mask, tgt_mask)
# 3. 映射到词表大小的 logits
logits = self.output_projection(decoder_output)
return logits
def generate(self, src, max_len=50, start_token=1, end_token=2):
"""贪心解码生成"""
self.eval()
with torch.no_grad():
encoder_output = self.encoder(src)
# 从 <BOS> 开始
tgt = torch.full((src.size(0), 1), start_token, dtype=torch.long, device=src.device)
for _ in range(max_len):
# 创建因果掩码
tgt_mask = create_causal_mask(tgt.size(1)).to(src.device)
decoder_output = self.decoder(tgt, encoder_output, tgt_mask=tgt_mask)
logits = self.output_projection(decoder_output[:, -1, :]) # 只取最后一个位置
next_token = logits.argmax(dim=-1, keepdim=True)
tgt = torch.cat([tgt, next_token], dim=1)
# 如果所有样本都生成了 <EOS>,停止
if (next_token == end_token).all():
break
return tgt
# 创建模型
model = Transformer(
src_vocab_size=10000,
tgt_vocab_size=8000,
d_model=512,
n_heads=8,
d_ff=2048,
n_encoder_layers=6,
n_decoder_layers=6,
)
# 统计参数量
total_params = sum(p.numel() for p in model.parameters())
print(f"总参数量: {total_params:,}") # 约 65M 参数(类似原始论文的 base 模型)
9.4 训练循环
import torch.optim as optim
# 训练配置
optimizer = optim.Adam(model.parameters(), lr=1e-4, betas=(0.9, 0.98), eps=1e-9)
criterion = nn.CrossEntropyLoss(ignore_index=0) # 忽略 <PAD> 的损失
def train_step(model, src, tgt, optimizer, criterion):
model.train()
optimizer.zero_grad()
# tgt_input: 去掉最后一个 token(作为输入)
# tgt_output: 去掉第一个 token(作为目标)
tgt_input = tgt[:, :-1]
tgt_output = tgt[:, 1:]
# 创建掩码
tgt_mask = create_causal_mask(tgt_input.size(1)).to(src.device)
# 前向传播
logits = model(src, tgt_input, tgt_mask=tgt_mask)
# 计算损失
loss = criterion(
logits.reshape(-1, logits.size(-1)), # [batch*seq, vocab]
tgt_output.reshape(-1), # [batch*seq]
)
# 反向传播
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
return loss.item()
💡 Teacher Forcing 的细节
注意
tgt_input = tgt[:, :-1]和tgt_output = tgt[:, 1:]的错位:JavaScripttgt: [<BOS>, 我, 爱, NLP, <EOS>] tgt_input: [<BOS>, 我, 爱, NLP] ← 喂给 Decoder tgt_output: [我, 爱, NLP, <EOS>] ← 期望 Decoder 输出这就是 "Teacher Forcing"——训练时把正确答案的前缀喂给 Decoder,让它学会预测下一个词。
🎨 交互式演示:下面的可视化展示了完整 Transformer 的架构和数据流动——点击组件查看详细说明,播放动画观察 token 如何从输入流经整个模型产生输出。
9.5 常见坑点
⚠️ 坑 1:目标序列的右移(Shifted Right)
Decoder 的输入必须是目标序列"右移一位":
[<BOS>, w1, w2, ..., wN],而不是[w1, w2, ..., wN, <EOS>]。如果不右移,模型在位置 0 就能看到答案 w1——这又是信息泄漏!
⚠️ 坑 2:Encoder 和 Decoder 不共享词嵌入
在机器翻译中,源语言和目标语言可能有不同的词表,所以 Encoder 和 Decoder 各自有独立的 Embedding 层。但如果源语言和目标语言相同(如文本摘要),可以共享嵌入。
⚠️ 坑 3:推理时忘了 model.eval() 和 torch.no_grad()
推理时必须关闭 Dropout(
model.eval())和梯度计算(torch.no_grad()),否则结果不确定且浪费内存。
📝 本节小结
- 完整 Transformer = Encoder + Decoder + Output Projection
- Encoder 输出的 K/V 传给 Decoder 的 Cross-Attention
- 训练用 Teacher Forcing,推理用自回归贪心/束搜索
- 注意目标序列的右移和掩码的正确使用
十、三大架构家族与主流 LLM 架构详解
原始 Transformer 使用了完整的 Encoder-Decoder 结构,但后来的研究发现,只用其中一部分也能取得惊人的效果。这催生了三大架构家族。
10.1 Encoder-Only:BERT 家族
BERT(Bidirectional Encoder Representations from Transformers)(白话版:只用 Encoder 部分,让每个词同时看到左边和右边的所有词,通过"完形填空"训练出强大的语言理解能力)。
核心特点:
- 只有 Encoder,没有 Decoder
- 双向注意力:每个位置可以看到所有其他位置
- 训练任务:MLM(Masked Language Model,掩码语言模型)——随机遮住 15% 的词,让模型猜
# BERT 风格的预训练
# 输入: "我 [MASK] 自然语言 [MASK]"
# 目标: 预测 [MASK] 位置的词 → "爱", "处理"
# MLM 的掩码策略(80/10/10 规则):
# 被选中的 15% 的 token 中:
# - 80% 替换为 [MASK]
# - 10% 替换为随机 token
# - 10% 保持不变
适用任务: 文本分类、命名实体识别、问答、语义相似度——所有需要理解文本的任务。
代表模型: BERT, RoBERTa, ALBERT, DeBERTa, ELECTRA
10.2 Decoder-Only:GPT 家族
GPT(Generative Pre-trained Transformer)(白话版:只用 Decoder 部分,使用因果掩码从左到右逐词生成,通过"下一个词预测"训练出强大的文本生成能力)。
核心特点:
- 只有 Decoder(去掉了 Cross-Attention 子层)
- 因果注意力:每个位置只能看到左边(包括自己)
- 训练任务:CLM(Causal Language Model,因果语言模型)——预测下一个词
# GPT 风格的预训练
# 输入: "我 爱 自然 语言"
# 目标: "爱 自然 语言 处理"
# 位置 0: 看到 [我] → 预测 "爱"
# 位置 1: 看到 [我, 爱] → 预测 "自然"
# 位置 2: 看到 [我, 爱, 自然] → 预测 "语言"
适用任务: 文本生成、代码生成、对话、翻译——所有需要生成文本的任务。ChatGPT、GPT-4、Claude 都属于这个家族。
代表模型: GPT-2, GPT-3, GPT-4, LLaMA, Mistral, Claude
10.3 Encoder-Decoder:T5 家族
T5(Text-to-Text Transfer Transformer)(白话版:保留完整的 Encoder-Decoder 结构,把所有 NLP 任务都统一为"输入文本→输出文本"的格式)。
核心特点:
- 完整的 Encoder + Decoder
- Encoder 双向注意力,Decoder 因果注意力 + Cross-Attention
- 训练任务:Span Corruption——随机遮住连续的片段,让模型还原
# T5 风格的训练
# 输入: "我 <X> 语言处理"
# 目标: "<X> 爱 自然"
# 其中 <X> 是被遮住的连续片段的标记
适用任务: 翻译、摘要、问答——特别适合输入和输出都是序列的任务。
代表模型: T5, BART, mBART, UL2
10.4 全局对比
| 维度 | BERT (Encoder) | GPT (Decoder) | T5 (Enc-Dec) |
|---|---|---|---|
| 注意力方向 | 双向 ↔ | 单向 → | 双向(Enc) + 单向(Dec) |
| 训练目标 | MLM (完形填空) | CLM (预测下一个词) | Span Corruption |
| 擅长任务 | 理解类 | 生成类 | 序列到序列 |
| 参数效率 | 高(理解任务) | 随规模涨(涌现能力) | 中等 |
| 推理方式 | 一次性输出 | 自回归逐词生成 | 先编码再逐词解码 |
| 典型应用 | 分类、NER、QA | 聊天、写作、编程 | 翻译、摘要 |
| 代表 | BERT, RoBERTa | GPT-4, LLaMA, Claude | T5, BART |
10.5 当今主流 LLM 都用什么架构?
让我们看看 2024-2025 年你每天在用的 LLM 到底是什么架构:
| 模型 | 公司 | 架构 | 参数量 | 关键技术 |
|---|---|---|---|---|
| GPT-4 / GPT-4o | OpenAI | Decoder-Only(传闻 MoE) | ~1.8T(传闻) | RLHF, 多模态 |
| Claude 3.5/4 | Anthropic | Decoder-Only | 未公开 | Constitutional AI (RLAIF) |
| Gemini 2.0/2.5 | Decoder-Only + MoE | ~1T+ | 多模态统一 token 流, Multi-Query Attention | |
| DeepSeek V3/R1 | DeepSeek | Decoder-Only + MoE | 671B(激活 37B) | MLA(多头潜在注意力), 256 专家取 8 |
| LLaMA 3 | Meta | Decoder-Only(密集) | 8B / 70B / 405B | GQA, RoPE, SwiGLU |
| Mistral Large 3 | Mistral | Decoder-Only + MoE | 675B(激活 41B) | GQA, 滑动窗口注意力 |
| Qwen 2.5 | 阿里 | Decoder-Only(密集/MoE) | 72B / MoE 版 | GQA, RoPE, YaRN |
| T5 / mT5 | Encoder-Decoder | 11B | Span Corruption | |
| BERT / DeBERTa | Google/微软 | Encoder-Only | 340M | MLM, 理解类任务 |
💡 一个震撼的事实:你每天用的 ChatGPT、Claude、Gemini、DeepSeek——全部都是 Decoder-Only 架构!Encoder-Decoder 和 Encoder-Only 在生成类任务中已经近乎消失。
10.6 为什么 Decoder-Only 统治了一切?
这不是偶然的,而是有深层原因的:
原因 ① 规模效应(Scaling Law)
研究表明,当计算预算足够大时,Decoder-Only 架构几乎总是占据最优前沿。2025 年的论文 "Revisiting Encoder-Decoder LLM" 证实:虽然在小规模(不到 1B 参数)时 Encoder-Decoder 有优势,但一旦规模扩大,Decoder-Only 的性能增长更快。
原因 ② 训练效率
Decoder-Only 的因果语言模型(CLM)目标天然利用了每一个 token作为训练信号——序列中的每个位置都在预测下一个词。而 Encoder-Decoder 的训练只能利用被遮住的那 15% 的 token。
Decoder-Only(GPT)训练效率:
输入: "我 爱 自然 语言 处理"
目标: "爱 自然 语言 处理 <EOS>"
→ 每个位置都是一个训练样本!5个token = 5个训练信号
Encoder-Only(BERT)训练效率:
输入: "我 [MASK] 自然 [MASK] 处理"
目标: 预测 "爱" 和 "语言"
→ 只有被MASK的位置有训练信号!5个token = 2个训练信号
原因 ③ 架构简洁性
Decoder-Only 只有一种模块(带因果掩码的 Self-Attention),没有 Cross-Attention。这意味着:
- 更少的超参数需要调优
- 更简单的并行化策略
- 更容易做 KV Cache 优化
原因 ④ 涌现能力
大规模 Decoder-Only 模型展现出了惊人的 In-Context Learning 能力——不需要微调,只靠 few-shot 示例就能完成新任务。这种能力在 Encoder-Only 模型中几乎不存在。
原因 ⑤ 通用性
一个 Decoder-Only 模型可以做翻译、摘要、问答、编程、数学推理……而 Encoder-Only 只擅长理解类任务。用一种架构解决所有问题,在工程上远比维护多种架构简单。
10.7 架构创新趋势:不止于 Decoder-Only
虽然 Decoder-Only 是基座,但各家都在上面做了大量创新:
① 注意力机制优化
原始 Transformer: Multi-Head Attention (MHA) → 每个头独立的 K/V
↓
LLaMA / Mistral: Grouped-Query Attention (GQA) → 多个 Q 头共享 K/V
↓
DeepSeek V3: Multi-Head Latent Attention (MLA) → 压缩 K/V 到低维再还原
GQA 和 MLA 的核心目的是一样的:减少 KV Cache 的内存占用。当你用 ChatGPT 聊了 100K 个 token 时,模型需要缓存所有历史 token 的 Key 和 Value 向量——这个缓存(KV Cache)是推理时最大的内存瓶颈。
② 混合专家模型(MoE)
DeepSeek V3 和 Gemini 2.5 都使用 MoE:模型有几百个"专家"网络,但每个 token 只激活其中几个。
DeepSeek V3: 256 个路由专家 + 1 个共享专家,每个 token 激活 Top-8
→ 671B 总参数,但每个 token 只用 37B → 10x 计算效率提升
这就像一个大医院:有骨科、眼科、心内科……的专家,每个病人只看最相关的几个科室,不需要所有医生都出动。
③ 位置编码进化
原始 Transformer: sin/cos 绝对位置编码 → 外推能力差
↓
LLaMA / 大部分现代 LLM: RoPE(旋转位置编码) → 更好的相对距离建模
↓
+ NTK-aware / YaRN 扩展 → 支持 100K+ 上下文
⚠️ 但 Encoder-Decoder 并没有死
在推理效率上,Encoder-Decoder 有一个杀手级优势:2025 年的研究表明,同等规模的 Encoder-Decoder 模型首 token 延迟降低 47%,吞吐量提升 4.7 倍。原因很简单——Encoder 可以并行处理整个输入,而 Decoder-Only 必须串行处理长 prompt。在边缘设备和低延迟场景下,Encoder-Decoder 仍然是更好的选择。
🎨 交互式演示:下面的可视化对比了 BERT、GPT 和 T5 三种架构的结构差异、注意力模式和训练目标。
📝 本节小结
- BERT(Encoder-Only):双向注意力,擅长理解,适合分类/NER/QA
- GPT(Decoder-Only):因果注意力,擅长生成,是当前绝对主流
- T5(Encoder-Decoder):两者兼备,适合翻译/摘要
- 当今所有主流 LLM(GPT-4、Claude、Gemini、DeepSeek、LLaMA)全部是 Decoder-Only
- Decoder-Only 统治的核心原因:更好的规模效应、更高的训练效率、涌现能力
- 架构创新趋势:GQA/MLA 优化注意力、MoE 提升效率、RoPE 支持长上下文
十一、PyTorch 代码实战:从零搭建 Transformer
让我们把前面所有的组件整合起来,写一个完整的、可运行的 Transformer 模型。
11.1 完整模型代码(整合版)
import torch
import torch.nn as nn
import math
class PositionalEncoding(nn.Module):
"""位置编码:sin/cos 固定编码"""
def __init__(self, d_model, max_len=5000, dropout=0.1):
super().__init__()
self.dropout = nn.Dropout(dropout)
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len).unsqueeze(1).float()
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
self.register_buffer('pe', pe.unsqueeze(0))
def forward(self, x):
x = x + self.pe[:, :x.size(1)]
return self.dropout(x)
class MultiHeadAttention(nn.Module):
"""多头注意力"""
def __init__(self, d_model, n_heads, dropout=0.1):
super().__init__()
assert d_model % n_heads == 0
self.d_model = d_model
self.n_heads = n_heads
self.d_k = d_model // n_heads
self.W_Q = nn.Linear(d_model, d_model)
self.W_K = nn.Linear(d_model, d_model)
self.W_V = nn.Linear(d_model, d_model)
self.W_O = nn.Linear(d_model, d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, Q, K, V, mask=None):
batch_size = Q.size(0)
Q = self.W_Q(Q).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
K = self.W_K(K).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
V = self.W_V(V).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))
attn_weights = self.dropout(torch.softmax(scores, dim=-1))
context = torch.matmul(attn_weights, V)
context = context.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model)
return self.W_O(context)
class FeedForward(nn.Module):
"""前馈网络"""
def __init__(self, d_model, d_ff=2048, dropout=0.1):
super().__init__()
self.net = nn.Sequential(
nn.Linear(d_model, d_ff),
nn.ReLU(),
nn.Dropout(dropout),
nn.Linear(d_ff, d_model),
)
def forward(self, x):
return self.net(x)
class EncoderLayer(nn.Module):
def __init__(self, d_model, n_heads, d_ff, dropout=0.1):
super().__init__()
self.self_attn = MultiHeadAttention(d_model, n_heads, dropout)
self.ffn = FeedForward(d_model, d_ff, dropout)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.dropout1 = nn.Dropout(dropout)
self.dropout2 = nn.Dropout(dropout)
def forward(self, x, mask=None):
x = self.norm1(x + self.dropout1(self.self_attn(x, x, x, mask)))
x = self.norm2(x + self.dropout2(self.ffn(x)))
return x
class DecoderLayer(nn.Module):
def __init__(self, d_model, n_heads, d_ff, dropout=0.1):
super().__init__()
self.self_attn = MultiHeadAttention(d_model, n_heads, dropout)
self.cross_attn = MultiHeadAttention(d_model, n_heads, dropout)
self.ffn = FeedForward(d_model, d_ff, dropout)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.norm3 = nn.LayerNorm(d_model)
self.dropout1 = nn.Dropout(dropout)
self.dropout2 = nn.Dropout(dropout)
self.dropout3 = nn.Dropout(dropout)
def forward(self, x, enc_output, src_mask=None, tgt_mask=None):
x = self.norm1(x + self.dropout1(self.self_attn(x, x, x, tgt_mask)))
x = self.norm2(x + self.dropout2(self.cross_attn(x, enc_output, enc_output, src_mask)))
x = self.norm3(x + self.dropout3(self.ffn(x)))
return x
class Transformer(nn.Module):
def __init__(self, src_vocab, tgt_vocab, d_model=512, n_heads=8,
d_ff=2048, n_enc_layers=6, n_dec_layers=6,
max_len=5000, dropout=0.1):
super().__init__()
self.d_model = d_model
# Embeddings + Positional Encoding
self.src_embed = nn.Embedding(src_vocab, d_model)
self.tgt_embed = nn.Embedding(tgt_vocab, d_model)
self.pos_enc = PositionalEncoding(d_model, max_len, dropout)
# Encoder & Decoder stacks
self.encoder_layers = nn.ModuleList(
[EncoderLayer(d_model, n_heads, d_ff, dropout) for _ in range(n_enc_layers)]
)
self.decoder_layers = nn.ModuleList(
[DecoderLayer(d_model, n_heads, d_ff, dropout) for _ in range(n_dec_layers)]
)
# Output projection
self.output_proj = nn.Linear(d_model, tgt_vocab)
# Initialize weights
self._init_weights()
def _init_weights(self):
for p in self.parameters():
if p.dim() > 1:
nn.init.xavier_uniform_(p)
def encode(self, src, src_mask=None):
x = self.pos_enc(self.src_embed(src) * math.sqrt(self.d_model))
for layer in self.encoder_layers:
x = layer(x, src_mask)
return x
def decode(self, tgt, enc_output, src_mask=None, tgt_mask=None):
x = self.pos_enc(self.tgt_embed(tgt) * math.sqrt(self.d_model))
for layer in self.decoder_layers:
x = layer(x, enc_output, src_mask, tgt_mask)
return x
def forward(self, src, tgt, src_mask=None, tgt_mask=None):
enc_output = self.encode(src, src_mask)
dec_output = self.decode(tgt, enc_output, src_mask, tgt_mask)
return self.output_proj(dec_output)
# ===== 工具函数 =====
def create_causal_mask(size):
"""创建因果掩码(下三角为 1,上三角为 0)"""
return torch.tril(torch.ones(size, size)).unsqueeze(0).unsqueeze(0)
def create_padding_mask(seq, pad_idx=0):
"""创建 padding 掩码"""
return (seq != pad_idx).unsqueeze(1).unsqueeze(2)
11.2 快速验证
# 创建模型
model = Transformer(src_vocab=5000, tgt_vocab=5000, d_model=256, n_heads=8, d_ff=1024, n_enc_layers=3, n_dec_layers=3)
# 模拟输入
src = torch.randint(1, 5000, (4, 20)) # batch=4, src_len=20
tgt = torch.randint(1, 5000, (4, 15)) # batch=4, tgt_len=15
# 创建掩码
src_mask = create_padding_mask(src)
tgt_mask = create_causal_mask(tgt.size(1)) & create_padding_mask(tgt)
# 前向传播
logits = model(src, tgt, src_mask, tgt_mask)
print(f"输出 logits 形状: {logits.shape}") # [4, 15, 5000]
# 参数统计
total = sum(p.numel() for p in model.parameters())
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"总参数量: {total:,}")
print(f"可训练参数量: {trainable:,}")
📝 本节小结
- 完整 Transformer 约 200 行 PyTorch 代码即可实现
- 关键组件:PositionalEncoding → MultiHeadAttention → FeedForward → EncoderLayer → DecoderLayer
- 工具函数:
create_causal_mask和create_padding_mask- Xavier 初始化对训练稳定性很重要
十二、常见坑点与最佳实践
经过前面十一节的学习,你已经理解了 Transformer 的方方面面。但在实际开发中,还有很多"踩坑经验"值得分享。
12.1 学习率 Warmup:不能一开始就大步跑
原论文使用了一个特殊的学习率调度:先 warmup 再 decay。
class TransformerScheduler:
def __init__(self, optimizer, d_model, warmup_steps=4000):
self.optimizer = optimizer
self.d_model = d_model
self.warmup_steps = warmup_steps
self.step_num = 0
def step(self):
self.step_num += 1
lr = self.d_model ** (-0.5) * min(
self.step_num ** (-0.5),
self.step_num * self.warmup_steps ** (-1.5)
)
for param_group in self.optimizer.param_groups:
param_group['lr'] = lr
💡 为什么需要 Warmup?
训练初期模型参数是随机的,梯度方向很不稳定。如果一开始学习率就很大,模型可能直接"跑飞"。Warmup 让模型先用小学习率稳住,然后再逐渐加大。
12.2 Label Smoothing:不要太自信
标签平滑(Label Smoothing)(白话版:训练时不让模型 100% 确信正确答案,而是给正确答案 90% 的概率,剩下 10% 均匀分配给其他选项,防止模型过度自信):
class LabelSmoothingLoss(nn.Module):
def __init__(self, vocab_size, smoothing=0.1, padding_idx=0):
super().__init__()
self.smoothing = smoothing
self.vocab_size = vocab_size
self.padding_idx = padding_idx
def forward(self, logits, target):
log_probs = torch.log_softmax(logits, dim=-1)
# 均匀分布
smooth_loss = -log_probs.mean(dim=-1)
# NLL loss
nll_loss = -log_probs.gather(dim=-1, index=target.unsqueeze(-1)).squeeze(-1)
# 混合
loss = (1 - self.smoothing) * nll_loss + self.smoothing * smooth_loss
# 忽略 padding
mask = (target != self.padding_idx).float()
return (loss * mask).sum() / mask.sum()
12.3 梯度裁剪:防止梯度爆炸
# 在 loss.backward() 之后,optimizer.step() 之前
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
12.4 常见错误汇总
| 错误 | 症状 | 解决方案 |
|---|---|---|
| 忘了除以 √d_k | 训练不收敛,loss 不降 | 检查 attention 的 scale |
| 因果掩码方向反了 | 模型"作弊",训练 loss 极低但推理乱码 | 确保上三角被遮住 |
| 位置编码维度不匹配 | RuntimeError: size mismatch | 检查 d_model 一致性 |
| 没用 Teacher Forcing | 训练极慢 | 训练时用 ground truth 作为 Decoder 输入 |
| Embedding 没乘 √d_model | 位置编码信号盖过词嵌入 | 加上 * math.sqrt(d_model) |
| 推理时没关 Dropout | 每次生成结果不同 | model.eval() |
| Batch 中序列长度不同 | 报错或结果错误 | 正确使用 padding + padding mask |
12.5 Attention 长上下文的稀释问题
当序列很长时(比如 GPT-4 支持 128K token),注意力会被稀释:每个位置的注意力权重分散到太多位置上,导致关键信息被淹没。
已知的解决方案包括:
- 稀疏注意力(Sparse Attention):只关注局部窗口 + 少量全局位置
- FlashAttention:算法优化,减少内存访问次数
- RoPE + NTK-aware Scaling:让位置编码更好地外推到长序列
- Ring Attention:分布式场景下的长上下文方案
12.6 Prompt 工程的底层原理(预告)
理解了 Transformer 的工作原理后,很多 Prompt Engineering 的技巧就有了理论解释。下一节我们会深入展开这个话题。
📝 本节小结
- 学习率 Warmup 是 Transformer 训练的标配
- Label Smoothing 防止过度自信,提升泛化
- 梯度裁剪防止梯度爆炸
- 长上下文稀释是当前研究热点
- 理解 Transformer 原理能让你写出更好的 Prompt
十三、从 Transformer 原理到 LLM 使用技巧
🎯 本节目标:理解 Transformer 原理不只是"学术趣味"——它直接决定了你如何更好地使用 ChatGPT、Claude、DeepSeek 等 LLM。本节将揭示每一个 Prompt 技巧背后的 Transformer 原理。
13.1 为什么开头和结尾最重要?——首尾记忆效应
你可能听过这样的建议:"把最重要的指令放在 prompt 的开头或结尾"。这不是玄学,而是由 Transformer 的注意力机制直接决定的。
原理解析:因果注意力的 U 型曲线
在 Decoder-Only 模型中,最后一个 token(即将生成回复的位置)对之前所有 token 计算注意力。由于:
- 开头的 token 参与了后续所有 token 的注意力计算,被反复"强化",形成了稳定的表示
- 结尾的 token 在位置上最接近当前生成位置,注意力权重天然更高(近距离偏好)
- 中间的 token 既没有开头的"累计强化"优势,也没有结尾的"距离近"优势
这形成了一个 U 型注意力分布——开头和结尾获得最多关注,中间最容易被"遗忘"。
注意力强度
▲
│ ██ ██
│ ██ ██ ██ ██
│ ██ ██ ██ ██ ██ ██
│ ██ ██ ██ ██ ██ ██ ██ ██
│ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
└──────────────────────────────────────────────→ Token 位置
开头 ← 中间(注意力最弱)→ 结尾
2024 年的论文 "Lost in the Middle" 通过实验证实了这一现象。
✅ 实践技巧:
❌ 差的 Prompt 结构:
"这里有一堆背景信息……(10000 字)……
请基于以上信息回答:xxx 是什么?"
✅ 好的 Prompt 结构:
"请回答以下问题:xxx 是什么?
【参考资料开始】
这里有一堆背景信息……(10000 字)……
【参考资料结束】
再次提醒:请严格基于以上参考资料回答 xxx 是什么。"
💡 白话版:把"任务指令"放开头和结尾,把"参考资料"夹在中间。这样模型最先和最后看到的都是你的指令,不容易跑偏。
13.2 温度参数:Softmax 的"音量旋钮"
你在使用 ChatGPT 或 Claude API 时,经常会设置一个 temperature 参数。还记得第四节的 Softmax 吗?温度就是在 Softmax 之前多了一步:
其中 就是温度(Temperature)。
温度如何影响输出:
| 温度 T | Softmax 行为 | 输出特点 | 适用场景 |
|---|---|---|---|
| T → 0 | 接近 argmax(one-hot) | 几乎确定性地选最高分词 | 代码生成、数学推理、事实问答 |
| T = 0.3 | 高峰,低多样性 | 大概率选高分词,偶尔变化 | 翻译、摘要、结构化输出 |
| T = 1.0 | 原始分布 | 模型的"本色"输出 | 通用对话、默认设置 |
| T = 1.5+ | 分布变平坦 | 低分词也有机会被选中 | 创意写作、头脑风暴 |
import torch
logits = torch.tensor([5.0, 3.0, 1.0, 0.5, 0.1])
words = ["苹果", "水果", "食物", "电脑", "天气"]
for T in [0.1, 0.5, 1.0, 2.0]:
probs = torch.softmax(logits / T, dim=0)
print(f"T={T:.1f}: ", end="")
for w, p in zip(words, probs):
print(f"{w}={p:.3f} ", end="")
print()
# T=0.1: 苹果=1.000 水果=0.000 食物=0.000 电脑=0.000 天气=0.000
# T=0.5: 苹果=0.880 水果=0.108 食物=0.010 电脑=0.003 天气=0.001
# T=1.0: 苹果=0.618 水果=0.226 食物=0.031 电脑=0.017 天气=0.012
# T=2.0: 苹果=0.392 水果=0.261 食物=0.113 电脑=0.082 天气=0.063
💡 直觉理解:温度就是 Softmax 的"音量旋钮"(第四节讲过!)。低温度 = 只听声音最大的,高温度 = 所有声音都差不多大。
✅ 实践技巧:
- 写代码 / 做数学 →
temperature = 0~0.3(你希望模型给出最"确定"的答案) - 日常对话 →
temperature = 0.7~1.0(平衡准确性和自然度) - 创意写作 / 取名字 →
temperature = 1.0~1.5(鼓励"意想不到"的组合)
13.3 Top-K 和 Top-P:更精细的采样控制
除了温度,LLM 还有两个重要的采样参数:
Top-K 采样(白话版:只从概率最高的 K 个词中选择,其他词直接排除):
原始分布: [苹果=0.6, 水果=0.2, 食物=0.1, 电脑=0.05, 天气=0.03, ...]
Top-K=3: [苹果=0.67, 水果=0.22, 食物=0.11] ← 只保留前 3 个,重新归一化
Top-P 采样(Nucleus Sampling)(白话版:从概率最高的词开始累加,直到累计概率超过 P,只从这些词中选择):
原始分布: [苹果=0.6, 水果=0.2, 食物=0.1, 电脑=0.05, ...]
Top-P=0.9: 0.6 + 0.2 + 0.1 = 0.9 ≥ P → 保留前 3 个
Top-P=0.8: 0.6 + 0.2 = 0.8 ≥ P → 只保留前 2 个
💡 为什么 Top-P 比 Top-K 更智能? 因为 Top-K 固定选 K 个词,但有时模型非常确信(一个词占 99%),有时很犹豫(前 20 个词都差不多)。Top-P 能自适应——确信时只取 1-2 个词,犹豫时取更多词。
13.4 Chain-of-Thought:为什么"先想后答"效果好?
Chain-of-Thought (CoT)(白话版:让 LLM 先写出推理步骤,再给最终答案)是目前最重要的 Prompt 技巧之一。它的效果惊人,但原理是什么?
Transformer 视角的解释:
在 Decoder-Only 模型中,生成第 N 个 token 时,它只能通过注意力机制从前面的 token 中获取信息。
不用 CoT:
"9876 × 4321 = " → 模型需要一步从问题直接跳到答案
注意力必须跨越很长的"推理链"
用 CoT:
"9876 × 4321
先算 9876 × 1 = 9876
再算 9876 × 20 = 197520
再算 9876 × 300 = 2962800
再算 9876 × 4000 = 39504000
加起来: 9876 + 197520 + 2962800 + 39504000 = 42674196"
→ 每一步都为下一步提供了"近距离的上下文"
核心原理:CoT 创造了注意力的"垫脚石"
没有 CoT 时,最终答案需要"远距离"从问题中提取信息。有了 CoT,中间步骤就像一连串垫脚石——每一步只需要从最近的几个 token 中获取信息,注意力不需要跨越太远。
💡 白话版:CoT 就像做数学题时的"草稿纸"——你不是直接写答案,而是把中间步骤写下来。每一步中间结果都会被"存储"在上下文中,成为下一步的"近距离参考"。
✅ 实践技巧:
❌ "请回答:如果 A 比 B 大,B 比 C 大,C 比 D 大,那 A 和 D 谁大?"
✅ "请一步一步思考:
如果 A 比 B 大,B 比 C 大,C 比 D 大,那 A 和 D 谁大?
请先列出已知条件,然后逐步推理。"
13.5 Few-Shot 示例:In-Context Learning 的注意力本质
给 LLM 一些示例就能让它做新任务——这种 In-Context Learning 能力的本质是什么?
Transformer 视角:示例设定了注意力模式
当你给模型几个 输入→输出 的示例时:
请将以下英文翻译为中文:
英文: Hello → 中文: 你好
英文: Thank you → 中文: 谢谢
英文: Good morning → 中文:
模型在生成最后一行的输出时,注意力会:
- 发现前面有重复的模式("英文: X → 中文: Y")
- 对这些模式形成注意力锚点
- 将 "Good morning" 的 Query 与前面示例的 Key 做匹配
- 利用匹配到的模式来生成对应的翻译
这就是为什么 few-shot 示例越多、越一致,效果就越好——它们为模型建立了更稳定的注意力模式模板。
✅ 实践技巧:
- 示例格式要完全一致(让注意力更容易发现模式)
- 示例数量 3-5 个通常最佳(太少模式不稳定,太多浪费上下文)
- 示例的顺序很重要:和你的问题最相似的示例放在最后(最近的注意力更强)
13.6 结构化 Prompt:注意力的"分隔符"
为什么使用 ---、###、XML 标签等分隔符能提升效果?
Transformer 视角:
分隔符在 token 序列中创造了注意力断点。当模型看到 --- 或 ### 时,这些特殊 token 形成了注意力的"墙壁"——前后内容的注意力更倾向于在同一个区域内部流动,而不是跨区域混杂。
❌ 无结构:
"你是一个翻译专家请将以下内容翻译为中文Hello World这是一个测试"
→ 所有 token 的注意力混在一起,模型不确定哪些是指令、哪些是待翻译内容
✅ 有结构:
"你是一个翻译专家。
---
请将以下内容翻译为中文:
---
Hello World
这是一个测试"
→ 分隔符帮助注意力"分区管理"
✅ 最佳实践——System Prompt 的黄金结构:
【角色定义】你是一个 xxx 专家。
【任务描述】请执行以下任务:
1. xxx
2. xxx
【约束条件】
- 输出格式:JSON
- 语言:中文
- 长度:不超过 200 字
【输入内容】
{用户的实际输入}
【输出要求】
请严格按照上述格式输出。
13.7 上下文窗口与 "Lost in the Middle"
现代 LLM 支持越来越长的上下文——GPT-4 Turbo 支持 128K,Gemini 2.0 Pro 支持 2M token。但更长不一定更好。
问题:注意力稀释
回忆 Softmax 的性质:所有注意力权重加起来等于 1。当上下文有 N 个 token 时,平均每个 token 只能获得 的注意力。
上下文 100 token: 每个 token 平均 1% 的注意力 → 关键信息容易被"看到"
上下文 10000 token: 每个 token 平均 0.01% 的注意力 → 关键信息容易被"淹没"
上下文 100000 token: 每个 token 平均 0.001% 的注意力 → 大海捞针!
✅ 实践技巧:
- 别一股脑把所有信息塞进上下文——精选最相关的内容
- 关键信息放在开头和结尾(U 型注意力曲线)
- 使用 RAG(检索增强生成) 只检索最相关的片段,而不是塞入整个文档
- 如果必须使用长上下文,用分隔符和标题帮助模型"索引"
13.8 为什么"重复指令"有效?
你可能注意到,在长 prompt 中重复关键指令效果更好。原理很简单:
- 多个位置的注意力叠加:关键指令出现在多个位置,模型在生成时有更多"锚点"可以注意到
- 对抗稀释:在长上下文中,单个位置的注意力很弱,但多次重复让总注意力增强
- 首尾效应强化:如果指令在开头和结尾都出现,就利用了 U 型注意力曲线的两个峰值
✅ "请用中文回答以下问题。
[大量参考资料...]
再次提醒:请用中文回答。
问题是:xxx?
注意:回答必须使用中文。"
13.9 技巧速查表
| 技巧 | Transformer 原理 | 推荐做法 |
|---|---|---|
| 指令放首尾 | U 型注意力曲线(首尾效应) | 任务描述放开头 + 结尾重复关键约束 |
| 低温度 | Softmax 变尖锐 → 确定性输出 | 代码/数学用 T=0~0.3 |
| 高温度 | Softmax 变平坦 → 多样性输出 | 创意写作用 T=0.7~1.5 |
| Chain-of-Thought | 中间 token 作为注意力垫脚石 | "请一步一步思考" |
| Few-shot 示例 | 建立注意力模式模板 | 3-5 个格式一致的示例 |
| 结构化分隔符 | 创造注意力边界,分区管理 | 用 ---、###、XML 标签分隔 |
| 精简上下文 | 减少注意力稀释 | 用 RAG 检索而非全文塞入 |
| 重复关键指令 | 多位置注意力叠加 | 重要约束在开头、中间、结尾各出现一次 |
| Top-P 采样 | 自适应截断低概率词 | API 调用设 top_p=0.9 |
| 示例放最后 | 近距离注意力更强 | 最相似的示例放在 prompt 末尾 |
🎨 交互式演示:下面的可视化通过 4 个互动 Demo 展示这些原理——体验首尾记忆效应、温度参数调节、上下文稀释和 CoT 注意力流动。
📝 本节小结
- 首尾记忆效应 → 重要指令放开头和结尾
- 温度 = Softmax 的音量旋钮 → 不同任务用不同温度
- Top-K / Top-P = 候选词的筛选策略
- CoT = 为注意力创造"垫脚石"
- Few-shot = 建立注意力模式模板
- 结构化 Prompt = 创造注意力分区
- 长上下文 ≠ 好上下文,精选最相关的信息
- 理解 Transformer 原理,让你从"调参玄学"变成"有理有据的工程"
十四、2025-2026 Transformer 前沿趋势
🎯 本节目标:了解 Transformer 架构在 2025-2026 年的最新进展和未来方向。即使你不做模型研发,了解这些趋势也能帮你理解"为什么 AI 越来越强、越来越便宜"。
14.1 注意力机制的效率革命
原始 Transformer 的注意力复杂度是 O(N²)——N 是序列长度。这意味着上下文从 4K 翻到 128K,计算量翻了 1024 倍!为了解决这个问题,2024-2025 年涌现了大量注意力优化技术:
FlashAttention 系列:让硬件跑满
| 版本 | 年份 | GPU 利用率 | 核心创新 |
|---|---|---|---|
| FlashAttention-1 | 2022 | ~50% | IO-aware 分块计算,减少内存读写 |
| FlashAttention-2 | 2023 | ~72% | 更好的并行化,支持多种注意力变体 |
| FlashAttention-3 | 2024 | ~85% (BF16) | 异步流水线、FP8 量化、H100 深度优化 |
FlashAttention 没有改变注意力的数学——它的输出和标准注意力完全一致。它只是通过巧妙的内存管理,让 GPU 的利用率从 35% 提升到 85%。这就像同一条高速公路,通过更好的交通管理让通车量翻倍。
💡 FlashAttention 是 LLM 上下文长度从 2K→4K→128K→1M 的核心推动力之一。没有它,128K 上下文的 GPT-4 在当前硬件上几乎不可能实现。
Ring Attention:分布式长上下文
当一块 GPU 放不下整个注意力矩阵时,Ring Attention 将注意力计算分散到多块 GPU 上,每块 GPU 只计算一部分,然后像"传环"一样传递中间结果。这使得百万级 token 的上下文成为可能。
线性注意力:从 O(N²) 到 O(N)
| 方法 | 复杂度 | 精确? | 状态 |
|---|---|---|---|
| 标准注意力 | O(N²) | ✅ | 基线 |
| FlashAttention | O(N²)(IO 优化) | ✅ | 生产就绪 |
| 线性注意力 | O(N) | ❌ 近似 | 快速成熟中 |
| Flash-线性注意力 | O(N)(IO 优化) | ❌ 近似 | 2025 已进入生产(Qwen3-Next) |
线性注意力通过核函数技巧和矩阵乘法结合律,避免计算完整的 N×N 注意力矩阵。虽然是近似的,但在 2025 年已经成熟到进入生产模型。
14.2 Mamba 与 SSM:Transformer 的挑战者
状态空间模型(State Space Model, SSM) 是近年来最有力的 Transformer 挑战者。其中最知名的是 Mamba。
核心思想对比:
Transformer(注意力):
每个 token 都可以"回头看"所有之前的 token
→ 信息通过 全局注意力矩阵 传递
→ 代价:O(N²) 计算,O(N) 内存
Mamba(选择性 SSM):
把"记忆"压缩到一个固定大小的隐状态中
→ 信息通过 状态递推 传递(类似升级版 RNN)
→ 代价:O(N) 计算,O(1) 每步内存
Mamba 的"选择性"创新:
传统 SSM 对所有输入一视同仁(状态转移矩阵是固定的)。Mamba 的突破在于让状态转移依赖于输入内容——模型可以根据当前 token 的内容决定要"记住"什么、"遗忘"什么。这使得 Mamba 在语言建模上首次匹配甚至超越了同规模的 Transformer。
但 Transformer 并没有被取代:
| 维度 | Transformer | Mamba/SSM |
|---|---|---|
| 长序列效率 | O(N²),长序列昂贵 | O(N),线性扩展 |
| 信息检索 | 强(全局注意力) | 弱(有限的状态容量) |
| 并行训练 | ✅ 天然并行 | ✅(结构化递推可并行) |
| 上下文学习 | ✅ 强 ICL 能力 | 相对较弱 |
| 实际部署 | 极度成熟 | 快速增长中 |
14.3 混合架构:取各家之长
2025-2026 最明确的趋势是 混合架构——结合 Transformer 和 SSM 的优点:
Hybrid Architecture(混合架构):
┌────────────────────────────────────────┐
│ 底层:Mamba/SSM 层(处理长距离依赖) │
│ ↓ │
│ 中间:穿插少量注意力层(全局信息检索) │
│ ↓ │
│ 顶层:注意力层(关键决策和生成) │
└────────────────────────────────────────┘
→ 既有 SSM 的线性效率,又有注意力的全局检索能力
代表模型:Jamba(AI21 Labs,Mamba + Transformer 混合)、Mamba-2(可以同时看作 SSM 和线性注意力)。
14.4 其他值得关注的方向
① Titans:可在推理时学习的架构
Google Research 的 Titans 架构引入了神经长期记忆(Neural Long-Term Memory) 模块,让模型在推理时能"学习"新信息——而不仅仅依赖训练时学到的知识。这解决了 Transformer 的一个根本限制:一旦训练完成,模型的知识就是固定的。
② 扩散语言模型(Diffusion LLM)
传统 LLM 逐词生成(自回归),而扩散模型可以并行生成整段文本,然后逐步"去噪"精炼。这能大幅降低推理延迟,但目前在文本生成质量上还不如自回归模型。
③ MoE 成为主流
2025 年,几乎所有新发布的大模型都采用了 MoE(混合专家) 架构。DeepSeek V3 的成功证明了 MoE 可以用 1/10 的计算量达到密集模型的性能。这不是 Transformer 的替代品,而是 Transformer 的升级配件。
14.5 一张图看未来
2017 ──── 原始 Transformer(Attention Is All You Need)
│
├── 2018 BERT(Encoder-Only, 双向注意力)
├── 2018 GPT-1(Decoder-Only, 因果注意力)
│
├── 2020 GPT-3(规模 + In-Context Learning)
├── 2022 FlashAttention(注意力加速)
├── 2023 Mamba(SSM 挑战者)
├── 2024 DeepSeek V3(MoE + MLA)
├── 2024 FlashAttention-3(H100 深度优化)
├── 2025 混合架构(Transformer + SSM)
│ 线性注意力进入生产(Qwen3-Next)
│ Flash-Linear-Attention 生态繁荣
│
└── 2026 ────→ ???
可能的方向:
• 混合 SSM + 注意力成为默认架构
• 线性注意力全面替代标准注意力
• 推理时学习(Titans 类模型)
• 扩散式并行生成
📝 本节小结
- FlashAttention 系列:不改变数学,只优化硬件利用率 → 让长上下文成为可能
- Ring Attention:分布式注意力计算 → 百万级 token 上下文
- 线性注意力:O(N²) → O(N),2025 年已进入生产
- Mamba/SSM:Transformer 最有力的挑战者,线性复杂度 + 选择性记忆
- 混合架构是当前最明确的趋势:结合注意力和 SSM 的优点
- MoE 成为大模型标配:用少量激活参数达到密集模型的性能
- Transformer 没有被取代,而是在进化
十五、总结与学习路线
恭喜你读完了这篇超长的教程!🎉 让我们回顾一下整个 Transformer 的知识体系。
15.1 为什么每个人都应该了解 Transformer?
你可能会想:"我又不做大模型算法,学这些有什么用?"
答案是:理解 Transformer 让你更好地使用 AI。
| 场景 | 不懂原理 | 懂原理 |
|---|---|---|
| 写 Prompt | 凭感觉,试错,"玄学调参" | 知道把重点放首尾(U 型注意力),知道用分隔符(注意力分区) |
| 选温度参数 | "0.7 好像不错?" | 知道温度是 Softmax 的缩放因子,代码用 0,创意用 1.0+ |
| 处理长文本 | "塞进去就行了吧?" | 知道存在注意力稀释和 Lost in the Middle,用 RAG 精选内容 |
| 评估 AI 工具 | "这个模型好神奇!" | 知道它只是在做下一个 token 预测,理解它的局限性 |
| 读技术新闻 | "MoE?SSM?看不懂" | 能理解为什么 DeepSeek 用 1/10 计算量达到 GPT-4 水平 |
| 写复杂指令 | "为什么 AI 总是忘了我的要求?" | 知道要重复关键指令,利用注意力叠加对抗稀释 |
💡 一句话总结:学 Transformer 不是为了自己造大模型,而是为了成为 AI 时代的明白人——知其然,也知其所以然。
15.2 核心概念回顾
Transformer(2017 → 2026 进化中)
├── 注意力机制
│ ├── 点积相似度 → Softmax → 加权组合
│ ├── Q/K/V 线性投影(同一输入的三重角色)
│ ├── 缩放因子 √d_k(防止 Softmax 饱和)
│ └── 多头注意力(多角度同时关注)
├── 位置编码
│ ├── sin/cos 固定编码(原始方案)
│ └── RoPE(旋转位置编码,现代主流)
├── Encoder(双向注意力,理解专家)
│ ├── Self-Attention + Add&Norm
│ └── FFN + Add&Norm
├── Decoder(因果注意力,生成专家)
│ ├── Masked Self-Attention + Add&Norm
│ ├── Cross-Attention + Add&Norm
│ └── FFN + Add&Norm
├── 三大架构家族
│ ├── BERT(Encoder-Only → 理解类任务)
│ ├── GPT(Decoder-Only → 生成类任务,当前绝对主流)
│ └── T5(Encoder-Decoder → 序列到序列任务)
├── 现代优化
│ ├── GQA / MLA(减少 KV Cache 内存)
│ ├── MoE(用少量计算达到大模型性能)
│ ├── FlashAttention(硬件级优化,不改变数学)
│ └── 线性注意力(O(N²) → O(N))
└── LLM 使用技巧
├── 首尾效应 → 重要信息放开头和结尾
├── 温度参数 → Softmax 的音量旋钮
├── CoT → 注意力的垫脚石
└── 结构化 Prompt → 注意力分区管理
15.3 推荐学习路线
| 阶段 | 内容 | 资源 |
|---|---|---|
| 入门 | 理解注意力机制直觉 | 本文第一到四节 |
| 进阶 | 手写 Transformer | 本文第五到十一节 + PyTorch 官方教程 |
| 应用 | 学会高效使用 LLM | 本文第十三节(LLM 使用技巧) |
| 实战 | 使用 HuggingFace 微调 | HuggingFace Transformers 文档 |
| 深入 | 阅读原始论文 | Attention Is All You Need (2017) |
| 前沿 | Flash Attention、MoE、SSM | 本文第十四节 + 各模型技术报告 |
15.4 关键论文与参考资料
📚 参考资料
- Attention Is All You Need — Transformer 原始论文 (Vaswani et al., 2017)
- BERT: Pre-training of Deep Bidirectional Transformers — BERT 论文 (Devlin et al., 2018)
- Language Models are Unsupervised Multitask Learners — GPT-2 论文 (Radford et al., 2019)
- Exploring the Limits of Transfer Learning with a Unified Text-to-Text Transformer — T5 论文 (Raffel et al., 2019)
- FlashAttention: Fast and Memory-Efficient Exact Attention — FlashAttention (Dao et al., 2022)
- RoFormer: Enhanced Transformer with Rotary Position Embedding — RoPE (Su et al., 2021)
- DeepSeek-V3 Technical Report — DeepSeek V3 (DeepSeek-AI, 2024)
- Mamba: Linear-Time Sequence Modeling with Selective State Spaces — Mamba (Gu & Dao, 2023)
- The Big LLM Architecture Comparison — Sebastian Raschka 的 LLM 架构对比
本文参考了 IBM Skills Network 的 Transformer 系列实验、吴恩达深度学习专项课程的 Transformer 作业,以及多篇原始论文。感谢这些优秀的教育资源。
最后更新:2026-02-09