Published on

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

Authors
  • avatar
    Name
    Allen Wang
    Twitter

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

🎯 本文目标:如果你听过 Transformer 但总觉得"似懂非懂",或者想知道为什么 ChatGPT 开头和结尾记得最牢、为什么 Chain-of-Thought 有效——这篇文章就是为你写的。我们将从最朴素的直觉出发,一步步拆解 Transformer 的每一个组件,直到你不仅能用 PyTorch 从零写出一个完整的 Transformer,还能真正理解你每天使用的 AI 背后的原理。


📑 目录


一、为什么需要 Transformer?

在 Transformer 出现之前(2017 年以前),自然语言处理的"标配"模型是 RNN(循环神经网络)(白话版:一种像流水线一样,一个词一个词顺序处理文本的神经网络)和它的升级版 LSTM(长短期记忆网络)(白话版:RNN 加了一个"记忆本"来缓解遗忘问题)。

但是它们有三个致命的缺陷:

1.1 RNN 的三大痛点

痛点 ①:串行处理,慢如蜗牛 🐌

想象你在翻译一本 1000 页的书。RNN 的做法是:读完第 1 页→翻译→读第 2 页→翻译→……→第 1000 页。它必须一页一页来,前面没读完就不能读后面的。

Python
# RNN 的伪代码 — 注意这里是一个 for 循环,必须串行执行
hidden = zeros(hidden_size)
for word in sentence:      # 必须顺序处理每个词!
    hidden = rnn_cell(word, hidden)  # 当前词 + 上一步的隐藏状态
output = hidden  # 最终状态包含"全部信息"

这意味着一个 1000 词的句子,需要做 1000 步计算,完全无法利用 GPU 的并行计算能力。

痛点 ②:长距离遗忘 🧠💨

JavaScript
"那只在公园里追蝴蝶的、毛色金黄的、带着红色项圈的小猫,最终还是没有抓到它。"

当 RNN 读到"它"这个字时,它需要知道"它"指的是"小猫"。但问题是:从"小猫"到"它"中间隔了十几个词,RNN 的记忆会随着每一步计算逐渐衰减——就像传话游戏,传了十几个人以后,原始信息早已面目全非。

痛点 ③:信息瓶颈 🍾

在 Encoder-Decoder 结构(比如机器翻译)中,RNN 把整个输入句子压缩成一个固定大小的向量,然后让 Decoder 从这个向量生成翻译。一个几百维的向量要承载整个句子的所有信息——这就像把一本书的内容压缩到一张便利贴上。

1.2 Transformer 的革命性思想

2017 年,Google 的论文 "Attention Is All You Need" 提出了 Transformer,核心思想简单到令人惊叹:

不需要按顺序读,直接"全局扫描",让每个词同时看到所有其他词。

对比维度RNN/LSTMTransformer
处理方式串行(一个接一个)并行(一次看全部)
长距离依赖随距离衰减直接连接,无衰减
训练速度慢(无法并行)快(充分利用 GPU)
信息传递通过隐藏状态传递通过注意力直接访问

用一个生活中的类比:

  • RNN 像是在一个嘈杂的派对上,你只能和旁边的人说话,然后让他帮你传话给远处的人。传到第十个人时,信息早就变了。
  • Transformer 像是你拥有一副千里眼,可以同时看到派对上的每一个人,直接和你想交流的人对话。

这就是 注意力机制(Attention Mechanism) 的核心直觉。接下来,让我们深入它的细节。

📝 本节小结

  • RNN 的三大问题:串行处理、长距离遗忘、信息瓶颈
  • Transformer 用"注意力机制"替代了循环结构,实现并行处理和全局信息访问
  • 论文名 "Attention Is All You Need" 不是夸张——它真的只靠注意力就完成了一切

二、注意力机制:从"查字典"说起

注意力机制(Attention Mechanism)(白话版:一种让模型在处理某个词时,能够"回头看"所有其他词,并决定每个词有多重要的技术)是 Transformer 的灵魂。我们用一个人人都懂的例子来理解它。

2.1 直觉类比:查字典

想象你在查一本英汉字典

  1. 你有一个想查的词(比如 "apple")——这是你的 Query(查询)
  2. 字典里每个词条有一个索引词(比如 "apple", "apply", "banana")——这些是 Key(键)
  3. 每个词条对应一个翻译("苹果", "应用", "香蕉")——这些是 Value(值)

查字典的过程就是:

JavaScript
1. 拿着 Query("apple")和每个 Key 比较相似度
2. 发现 Key="apple" 完全匹配 → 得分最高
3. Key="apply" 有点像 → 得分较高
4. Key="banana" 完全不像 → 得分很低
5. 根据得分高低,加权获取对应的 Value
6. 最终结果:主要获取"苹果"的翻译,略带一点"应用"的信息

💡 这就是注意力机制的本质:根据相似度,对信息进行加权组合。

2.2 从字典到注意力的数学

让我们把"查字典"翻译成数学语言:

第一步:计算相似度

最直观的相似度度量就是 点积(Dot Product)(白话版:两个向量对应元素相乘再求和,数值越大说明两个向量方向越一致,即越"像"):

score(Q,Ki)=QKi=j=1dQj×Ki,j\text{score}(Q, K_i) = Q \cdot K_i = \sum_{j=1}^{d} Q_j \times K_{i,j}
Python
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 中找到最相似的那个的。不要跳步,我们每一步都写出来:

JavaScript
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"):

JavaScript
  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"):

JavaScript
  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"):

JavaScript
  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

汇总原始得分:

JavaScript
Score("apple") = 1.29  ← 最高!果然 "apple""apple" 最像
Score("apply") = 1.01  ← 有点像,但不如完全匹配
Score("banana") = 0.26 ← 得分很低,"banana""apple" 差别大

💡 直觉理解:点积越大 = 两个向量越"像" = 注意力越强。就像你在字典里翻到了一个和你要查的词长得很像的词条,你自然会多看几眼。

第二步:Softmax — 把分数变成"聚光灯"

Softmax(白话版:一种数学函数,把任意数值转换成 0 到 1 之间的概率分布,所有值加起来等于 1)就像一个聚光灯——把模糊的分数变成清晰的概率分布:

attention_weighti=escoreijescorej\text{attention\_weight}_i = \frac{e^{\text{score}_i}}{\sum_{j} e^{\text{score}_j}}
Python
# 应用 Softmax
weights = torch.softmax(scores, dim=0)
print(f"注意力权重: {weights}")
# 输出类似: tensor([0.50, 0.34, 0.16])
# "apple" 获得最高权重,"banana" 最低

第三步:加权组合 Value

Python
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)(白话版:一个句子中的每个词,都把自己句子中的所有词当作字典来查)。

来看一个经典例子:

JavaScript
"动物没有穿过马路,因为它太累了。"

当模型处理"它"这个词时,自注意力机制会让"它"去查看句子中所有词

  • "动物" → 相似度很高("它"指代"动物")✅
  • "马路" → 相似度低("它"不是"马路")
  • "累" → 相似度中等(和"它"的状态有关)
  • "穿过" → 相似度低

这样,模型就自动学会了指代消解(Coreference Resolution)——"它"指的是"动物",而不是"马路"。

🎨 交互式演示:下面的可视化展示了自注意力的工作过程——点击任意词查看它对其他词的注意力分布。

2.4 注意力的不对称性

一个常被忽略的重要性质:注意力不是对称的

"苹果"对"牛顿"的注意力 ≠ "牛顿"对"苹果"的注意力

💡 白话版:"苹果砸到了牛顿"——从苹果的角度看,牛顿只是一个被砸的路人;但从牛顿的角度看,苹果是改变他命运的关键!

这是因为 Q 和 K 是通过不同的投影矩阵生成的,所以 QAKBQBKAQ_A \cdot K_B \neq Q_B \cdot K_A

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(简历内容):我具体能提供什么内容?

同一个词,通过不同的线性变换,被投影到三个不同的"角色空间":

Q=XWQ,K=XWK,V=XWVQ = X \cdot W_Q, \quad K = X \cdot W_K, \quad V = X \cdot W_V

其中 XX 是输入嵌入矩阵(白话版:把每个词转换成一串数字后,把整个句子的数字排成一个矩阵),WQ,WK,WVW_Q, W_K, W_V 是三个可学习的 权重矩阵(Weight Matrix)(白话版:一组可以在训练中不断调整的参数,用来做线性变换)。

3.2 现实例子:一步步算注意力

假设我们有一个包含 4 个词的句子 "I love deep learning",嵌入维度 d=3d = 3(实际中是 512 或 768,这里缩小便于理解)。

Python
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}")
Python
# 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 个词的原始注意力分数
Python
# Step 3: 缩放(下一节会详细解释为什么要缩放)
import math
d_k = d_model  # Key 的维度
scaled_scores = scores / math.sqrt(d_k)
print(f"缩放后的分数:\n{scaled_scores}")
Python
# 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)}")
Python
# 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 就一头雾水——这个矩阵乘法到底在做什么?让我们用上面的例子一个格子一个格子地算清楚:

JavaScript
输入 X4个词 × 3维嵌入):        权重矩阵 W_Q3×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 维):

JavaScript
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 维):

JavaScript
Q[0][1] = 1.0 × 0  +  0.0 × 1  +  1.0 × 0  =  0.0

计算 Q[0][2]("I" 的 Query 第 3 维):

JavaScript
Q[0][2] = 1.0 × 1  +  0.0 × 0  +  1.0 × 0  =  1.0

所以 "I" 的 Query 向量 = [2.0, 0.0, 1.0]

同理可以算出所有词的 Query:

JavaScript
完整的 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 的优雅写法

Python
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 的维度可以不同

虽然在自注意力中 dq=dk=dv=dmodeld_q = d_k = d_v = d_{model} 是最常见的设定,但实际上 Q 和 K 的维度必须相同(因为要做点积),而 V 的维度可以不同。在多头注意力中,每个头的 dk=dmodel/hd_k = d_{model} / h

⚠️ 坑 2:注意力分数矩阵是方阵

在自注意力中,Q 和 K 来自同一个序列,所以分数矩阵是 [seq_len, seq_len] 的方阵。但在交叉注意力(Cross-Attention) 中,Q 来自 Decoder,K/V 来自 Encoder,分数矩阵是 [decoder_len, encoder_len] 的矩形。

📝 本节小结

  • Q、K、V 通过三个不同的线性变换从同一个输入 X 产生
  • 完整公式:Attention(Q,K,V)=softmax(QKTdk)V\text{Attention}(Q,K,V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right) V
  • 维度变化:[seq, d] → [seq, seq] → [seq, d]
  • Q、K 维度必须匹配,V 维度可以不同

四、缩放点积注意力:为什么要除以 √d_k

你可能已经注意到了公式里那个 dk\sqrt{d_k}——为什么不直接 softmax(QK^T) 呢?这看起来是个小细节,但理解它对于训练稳定性至关重要。

4.1 直觉类比:音量调节旋钮 🔊

想象你在一个考场里,学生们在小声讨论答案(点积分数)。Softmax 就像一个"音量调节器"——把讨论声变成清晰的"谁说得对"的排名。

如果讨论声很小(分数值适中),你还能听清每个人都在说什么(分布均匀)。

但如果讨论声越来越大(维度 dkd_k 增大导致点积数值暴涨),声音最大的那个人会完全盖过其他所有人(softmax 饱和,变成 one-hot)——你只听到一个人的声音,其他所有信息都丢失了!

dk\sqrt{d_k} 就是那个音量旋钮:把音量调回合理范围,让你听清所有人的意见。

4.2 数学原理:方差爆炸

假设 QQKK 中的每个元素都是独立的、均值为 0、方差为 1 的随机变量,那么它们的点积:

QK=i=1dkQiKiQ \cdot K = \sum_{i=1}^{d_k} Q_i \cdot K_i

根据概率论,这个和的方差dkd_k(每一项的方差是 1,共 dkd_k 项,独立相加方差线性增长)。

  • dk=64d_k = 64 时,点积的标准差约为 64=8\sqrt{64} = 8
  • dk=512d_k = 512 时,点积的标准差约为 51222.6\sqrt{512} \approx 22.6

这意味着,随着维度增大,点积的数值会越来越大——大到 softmax 几乎变成 one-hot 分布

Python
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}")

运行结果(大致):

JavaScript
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 的梯度:

softmaxizj=softmaxi(δijsoftmaxj)\frac{\partial \text{softmax}_i}{\partial z_j} = \text{softmax}_i (\delta_{ij} - \text{softmax}_j)

当某个 softmaxi1\text{softmax}_i \approx 1 时,softmaxj0\text{softmax}_j \approx 0(对所有 jij \neq i),于是梯度接近 1×(11)=01 \times (1 - 1) = 0

结果就是:模型在训练初期就"坚信"某个词最重要,再也不更新——彻底丧失学习能力。

🎨 交互式演示:拖动滑块调整维度 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 就会发现模型完全不收敛。

📝 本节小结

  • 除以 dk\sqrt{d_k} 是为了控制点积的方差,防止 softmax 饱和
  • 没有缩放:d_k 越大,softmax 越接近 one-hot,梯度越接近零
  • 缩放后:无论 d_k 多大,softmax 都保持健康的概率分布
  • 这不是可选的优化,而是必需的操作

五、多头注意力:一个脑袋怎么够?

一个自注意力头只能学到一种"关注模式"。但语言中的关系是多种多样的——语法关系、语义关系、指代关系、位置关系……一个头怎么能同时捕获所有这些?

5.1 直觉类比:阅读理解小组 📖

想象一个阅读理解小组,每个人负责从不同角度分析一段文字:

  • 小明(头 1)负责找语法关系:主语-谓语-宾语
  • 小红(头 2)负责找指代关系:代词指代什么
  • 小华(头 3)负责找相邻关系:前后词的搭配
  • 小李(头 4)负责找语义关系:近义词、反义词

最后,他们把各自的发现汇总,得到一份全面的分析报告。

这就是多头注意力(Multi-Head Attention)(白话版:把注意力机制复制多份,每份关注不同类型的关系,最后把结果拼接起来)的核心思想。

5.2 技术实现:切分、并行、拼接

具体来说,多头注意力做了三件事:

Step 1:切分

把输入的嵌入维度 dmodeld_{model} 均匀切分成 hh 份(hh = 头的数量):

dk=dv=dmodel/hd_k = d_v = d_{model} / h

比如 dmodel=512d_{model} = 512, h=8h = 8,则每个头处理 dk=64d_k = 64 维的子空间。

Step 2:并行计算

每个头独立做一次完整的注意力计算:

headi=Attention(QWiQ,KWiK,VWiV)\text{head}_i = \text{Attention}(Q W_i^Q, K W_i^K, V W_i^V)

Step 3:拼接 + 投影

MultiHead(Q,K,V)=Concat(head1,...,headh)WO\text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_1, ..., \text{head}_h) W^O

WOW^O 是一个 输出投影矩阵(白话版:把拼接后的长向量压缩回原始维度 dmodeld_{model})。

5.3 代码实现

Python
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×512512×512512×512786K
8 头 (d_k=64)512×512512×512512×512786K

区别在于多头会多一个 WOW^O(512×512),但这只增加了 25% 的参数。核心原因是:多头并不是把参数量翻 h 倍,而是把同一维度的空间切分成 h 个子空间。

5.4 GPT-3 有 96 个头!

实际的大模型使用的头数远比 8 多:

模型d_modeln_headsd_k
BERT-base7681264
BERT-large10241664
GPT-27681264
GPT-31228896128
LLaMA-7B409632128

研究表明不同的头确实会学到不同的"技能"——有的擅长语法分析,有的擅长指代消解,有的只关注相邻的词。

🎨 交互式演示:下面的可视化展示了多头注意力的工作过程——每个头用不同颜色表示不同的关注模式,你可以切换查看各个头分别学到了什么。

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 直觉:打乱顺序的灾难 🔀

JavaScript
"猫 追 狗"  →  猫在追狗
"狗 追 猫"  →  狗在追猫

这两句话意思完全相反!但对于自注意力来说,它只关心"每个词和其他词的关系",完全不知道谁在前面、谁在后面

因为注意力的计算公式 softmax(QKT)V\text{softmax}(QK^T)V 中,QQKKVV 是通过逐元素的线性变换得到的,不包含任何位置信息。你打乱输入词的顺序,每个词得到的 Q/K/V 向量完全不变,只是注意力矩阵的行列顺序变了——但 softmax 是对称的,所以结果中的信息也不变。

💡 白话版:自注意力就像一群人围坐在圆桌旁开会——它能听懂每个人在说什么,但不知道谁坐在谁旁边。

6.2 解决方案:给每个位置一个"身份证"

位置编码(Positional Encoding, PE)(白话版:给序列中每个位置分配一个独特的数字向量,加到词嵌入上,让模型知道每个词的位置)的思想很简单:在输入嵌入上加上一个和位置相关的向量。

Input=TokenEmbedding(x)+PositionalEncoding(pos)\text{Input} = \text{TokenEmbedding}(x) + \text{PositionalEncoding}(pos)

但怎么设计这个位置向量呢?

6.3 失败的尝试:线性编码

最简单的想法:位置 0 编码为 0,位置 1 编码为 1,位置 2 编码为 2……

Python
# 方案 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) 函数:

PE(pos,2i)=sin(pos100002i/dmodel)PE_{(pos, 2i)} = \sin\left(\frac{pos}{10000^{2i/d_{model}}}\right) PE(pos,2i+1)=cos(pos100002i/dmodel)PE_{(pos, 2i+1)} = \cos\left(\frac{pos}{10000^{2i/d_{model}}}\right)

其中 pospos 是位置索引,ii 是维度索引。

为什么这么设计?三大优点:

① 有界性:sin 和 cos 的值永远在 [1,1][-1, 1] 之间,不会像线性编码那样数值爆炸。

② 唯一性:不同位置的编码是唯一的。因为不同维度使用不同频率——低维度用高频(变化快),高维度用低频(变化慢),就像钟表的秒针、分针、时针一样,组合起来可以唯一表示任何时刻。

③ 相对距离可学习:对于任意固定偏移 kkPEpos+kPE_{pos+k} 可以表示为 PEposPE_{pos} 的线性变换。这意味着模型可以通过学习简单的线性关系来捕获相对位置信息。

6.5 代码实现

Python
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 就像一个阅读理解专家,它的工作流程是:

  1. 阅读全文(Self-Attention):看完所有词,理解词与词之间的关系
  2. 做笔记(Feed-Forward Network):对每个词的理解做深入加工
  3. 反复精读(堆叠多层):每一层都在上一层理解的基础上进一步深化

7.2 Encoder Layer 的四个组件

一个 Encoder Layer 包含以下组件,按顺序执行:

JavaScript
输入 x
[Multi-Head Self-Attention]  ← 全局信息交互
[Add & Norm]                 ← 残差连接 + 层归一化
[Feed-Forward Network]       ← 逐位置深度加工
[Add & Norm]                 ← 残差连接 + 层归一化
输出

组件 ① Multi-Head Self-Attention

已经在第五节详细讲过了——让每个词看到所有其他词。

组件 ② Add & Norm(残差连接 + 层归一化)

残差连接(Residual Connection)(白话版:把输入直接"跳过"某个子层,和子层的输出相加。这样即使子层学不好,信号也能通过"跳线"直接传过去,防止深层网络退化):

output=LayerNorm(x+SubLayer(x))\text{output} = \text{LayerNorm}(x + \text{SubLayer}(x))

层归一化(Layer Normalization, LayerNorm)(白话版:对每个样本的所有特征做归一化——减去均值、除以标准差——让数值保持在合理范围内,加速训练收敛):

LayerNorm(x)=γxμσ+ϵ+β\text{LayerNorm}(x) = \gamma \cdot \frac{x - \mu}{\sigma + \epsilon} + \beta

💡 为什么需要残差连接?

Transformer 通常堆叠 6-96 层。如果没有残差连接,梯度在反向传播时需要经过每一层的变换,很容易消失或爆炸。残差连接提供了一条"高速公路",让梯度可以直接跳过任意多层,保持信号强度。

组件 ③ Feed-Forward Network (FFN)

前馈网络(Feed-Forward Network, FFN)(白话版:两个线性层中间夹一个 ReLU 激活函数,对每个位置独立做变换——不涉及词与词的交互,而是对单个词的表示做深度加工):

FFN(x)=max(0,xW1+b1)W2+b2\text{FFN}(x) = \max(0, x W_1 + b_1) W_2 + b_2

一个有趣的设计:FFN 的隐藏层维度 dffd_{ff} 通常是 dmodeld_{model}4 倍。比如 dmodel=512d_{model} = 512dff=2048d_{ff} = 2048。这个"先扩展再压缩"的瓶颈结构是非常有效的信息加工模式。

7.3 代码实现

Python
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,方差 1/dmodel1/d_{model}),而位置编码的值在 [1,1][-1, 1] 范围。为了防止位置编码的信号压过嵌入的信号,我们把嵌入乘以 dmodel\sqrt{d_{model}} 来放大它。

7.4 Encoder 小结

JavaScript
完整 Encoder 数据流:

Token IDsEmbedding  →  × √d_model  →  + PositionalEncoding
                                              EncoderLayer × N
                                              ┌─────────────────┐
Self-AttentionAdd & NormFeed-ForwardAdd & 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 直觉类比:考试中的作文题 ✍️

想象你在写一篇英语作文,考试规则是:

  1. 不能偷看答案(Masked Self-Attention):你只能基于已经写下的内容来决定下一个词
  2. 可以参考阅读材料(Cross-Attention):你可以回头翻看阅读理解部分(Encoder 的输出)来获取信息
  3. 从左到右写(自回归生成):一个词一个词地写,前面的词决定后面的词

8.2 Decoder Layer 的六个组件

Decoder 比 Encoder 多了一个子层:

JavaScript
输入 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,从而"遮住"未来的信息):

Python
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

在注意力计算中,被遮住的位置设为 -\infty

scoresmasked=scores+mask×()\text{scores}_{masked} = \text{scores} + \text{mask} \times (-\infty)

经过 softmax 后,e=0e^{-\infty} = 0,这些位置的注意力权重就变成了 0。

新组件 ② Cross-Attention(交叉注意力)

交叉注意力(Cross-Attention)(白话版:Decoder 的 Query 去"询问" Encoder 的输出,从中获取源序列的信息。就像翻译时回头看原文):

CrossAttn(Qdec,Kenc,Venc)=softmax(QdecKencTdk)Venc\text{CrossAttn}(Q_{dec}, K_{enc}, V_{enc}) = \text{softmax}\left(\frac{Q_{dec} K_{enc}^T}{\sqrt{d_k}}\right) V_{enc}

关键点:Q 来自 Decoder,K 和 V 来自 Encoder

8.3 代码实现

Python
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)

JavaScript
输入: [<BOS>,,, 自然, 语言]
目标: [,, 自然, 语言, <EOS>]

所有位置并行计算(因为目标序列是已知的),但通过因果掩码确保每个位置只看到前面的词。

推理时(自回归)

JavaScript
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 直觉:翻译流水线 🏭

想象一个专业的翻译流水线:

  1. 阅读部门(Encoder):把德语原文从头到尾读完,形成深度理解
  2. 翻译部门(Decoder):参考阅读部门的理解,一个词一个词地写出英文翻译
  3. 审核部门(Softmax + argmax):从所有候选词中选出概率最高的

9.2 完整架构图

JavaScript
源语言 Token IDs                                目标语言 Token IDs(右移一位)
      ↓                                                ↓
  [Embedding]                                     [Embedding]
      ↓                                                ↓
  [+ Pos Encoding]                              [+ Pos Encoding]
      ↓                                                ↓
┌─────────────────┐                         ┌──────────────────────┐
Encoder × N    │                         │  Decoder × N│                 │    K, V                  │                      │
Self-Attn      │────────────────────────→│  Masked Self-AttnAdd & Norm     │                         │  Add & NormFFN            │                         │  Cross-Attn (Q←Dec)Add & Norm     │                         │  Add & Norm└─────────────────┘                         │  FFNAdd & Norm                                            └──────────────────────┘
                                               [Linear Layer]
                                                 [Softmax]
                                             Output Probabilities

9.3 完整代码实现

Python
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 训练循环

Python
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:] 的错位:

JavaScript
tgt:        [<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% 的词,让模型猜
Python
# 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,因果语言模型)——预测下一个词
Python
# 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——随机遮住连续的片段,让模型还原
Python
# 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, RoBERTaGPT-4, LLaMA, ClaudeT5, BART

10.5 当今主流 LLM 都用什么架构?

让我们看看 2024-2025 年你每天在用的 LLM 到底是什么架构:

模型公司架构参数量关键技术
GPT-4 / GPT-4oOpenAIDecoder-Only(传闻 MoE)~1.8T(传闻)RLHF, 多模态
Claude 3.5/4AnthropicDecoder-Only未公开Constitutional AI (RLAIF)
Gemini 2.0/2.5GoogleDecoder-Only + MoE~1T+多模态统一 token 流, Multi-Query Attention
DeepSeek V3/R1DeepSeekDecoder-Only + MoE671B(激活 37B)MLA(多头潜在注意力), 256 专家取 8
LLaMA 3MetaDecoder-Only(密集)8B / 70B / 405BGQA, RoPE, SwiGLU
Mistral Large 3MistralDecoder-Only + MoE675B(激活 41B)GQA, 滑动窗口注意力
Qwen 2.5阿里Decoder-Only(密集/MoE)72B / MoE 版GQA, RoPE, YaRN
T5 / mT5GoogleEncoder-Decoder11BSpan Corruption
BERT / DeBERTaGoogle/微软Encoder-Only340MMLM, 理解类任务

💡 一个震撼的事实:你每天用的 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。

JavaScript
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 是基座,但各家都在上面做了大量创新:

① 注意力机制优化

JavaScript
原始 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 只激活其中几个。

JavaScript
DeepSeek V3:  256 个路由专家 + 1 个共享专家,每个 token 激活 Top-8
              → 671B 总参数,但每个 token 只用 37B → 10x 计算效率提升

这就像一个大医院:有骨科、眼科、心内科……的专家,每个病人只看最相关的几个科室,不需要所有医生都出动。

③ 位置编码进化

JavaScript
原始 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 完整模型代码(整合版)

Python
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 快速验证

Python
# 创建模型
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_maskcreate_padding_mask
  • Xavier 初始化对训练稳定性很重要

十二、常见坑点与最佳实践

经过前面十一节的学习,你已经理解了 Transformer 的方方面面。但在实际开发中,还有很多"踩坑经验"值得分享。

12.1 学习率 Warmup:不能一开始就大步跑

原论文使用了一个特殊的学习率调度:先 warmup 再 decay。

lr=dmodel0.5min(step0.5,stepwarmup_steps1.5)lr = d_{model}^{-0.5} \cdot \min(step^{-0.5}, step \cdot warmup\_steps^{-1.5})
Python
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% 均匀分配给其他选项,防止模型过度自信):

Python
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 梯度裁剪:防止梯度爆炸

Python
# 在 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 计算注意力。由于:

  1. 开头的 token 参与了后续所有 token 的注意力计算,被反复"强化",形成了稳定的表示
  2. 结尾的 token 在位置上最接近当前生成位置,注意力权重天然更高(近距离偏好)
  3. 中间的 token 既没有开头的"累计强化"优势,也没有结尾的"距离近"优势

这形成了一个 U 型注意力分布——开头和结尾获得最多关注,中间最容易被"遗忘"。

JavaScript
注意力强度
  │  ██                                          ██
  │  ██ ██                                    ██ ██
  │  ██ ██ ██                              ██ ██ ██
  │  ██ ██ ██ ██                        ██ ██ ██ ██
  │  ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
  └──────────────────────────────────────────────→ Token 位置
     开头        ← 中间(注意力最弱)→        结尾

2024 年的论文 "Lost in the Middle" 通过实验证实了这一现象。

✅ 实践技巧:

JavaScript
❌ 差的 Prompt 结构:
"这里有一堆背景信息……(10000 字)……
请基于以上信息回答:xxx 是什么?"

✅ 好的 Prompt 结构:
"请回答以下问题:xxx 是什么?

【参考资料开始】
这里有一堆背景信息……(10000 字)……
【参考资料结束】

再次提醒:请严格基于以上参考资料回答 xxx 是什么。"

💡 白话版:把"任务指令"放开头和结尾,把"参考资料"夹在中间。这样模型最先和最后看到的都是你的指令,不容易跑偏。

13.2 温度参数:Softmax 的"音量旋钮"

你在使用 ChatGPT 或 Claude API 时,经常会设置一个 temperature 参数。还记得第四节的 Softmax 吗?温度就是在 Softmax 之前多了一步:

P(wi)=ezi/Tjezj/TP(w_i) = \frac{e^{z_i / T}}{\sum_j e^{z_j / T}}

其中 TT 就是温度(Temperature)。

温度如何影响输出:

温度 TSoftmax 行为输出特点适用场景
T → 0接近 argmax(one-hot)几乎确定性地选最高分词代码生成、数学推理、事实问答
T = 0.3高峰,低多样性大概率选高分词,偶尔变化翻译、摘要、结构化输出
T = 1.0原始分布模型的"本色"输出通用对话、默认设置
T = 1.5+分布变平坦低分词也有机会被选中创意写作、头脑风暴
Python
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 个词中选择,其他词直接排除):

JavaScript
原始分布: [苹果=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,只从这些词中选择):

JavaScript
原始分布: [苹果=0.6, 水果=0.2, 食物=0.1, 电脑=0.05, ...]
Top-P=0.9: 0.6 + 0.2 + 0.1 = 0.9P → 保留前 3Top-P=0.8: 0.6 + 0.2 = 0.8P → 只保留前 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 中获取信息。

JavaScript
不用 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 就像做数学题时的"草稿纸"——你不是直接写答案,而是把中间步骤写下来。每一步中间结果都会被"存储"在上下文中,成为下一步的"近距离参考"。

✅ 实践技巧:

JavaScript
"请回答:如果 A 比 B 大,B 比 C 大,C 比 D 大,那 A 和 D 谁大?"

✅ "请一步一步思考:
   如果 AB 大,BC 大,CD 大,那 AD 谁大?
   请先列出已知条件,然后逐步推理。"

13.5 Few-Shot 示例:In-Context Learning 的注意力本质

给 LLM 一些示例就能让它做新任务——这种 In-Context Learning 能力的本质是什么?

Transformer 视角:示例设定了注意力模式

当你给模型几个 输入→输出 的示例时:

JavaScript
请将以下英文翻译为中文:
英文: Hello → 中文: 你好
英文: Thank you → 中文: 谢谢
英文: Good morning → 中文:

模型在生成最后一行的输出时,注意力会:

  1. 发现前面有重复的模式("英文: X → 中文: Y")
  2. 对这些模式形成注意力锚点
  3. 将 "Good morning" 的 Query 与前面示例的 Key 做匹配
  4. 利用匹配到的模式来生成对应的翻译

这就是为什么 few-shot 示例越多、越一致,效果就越好——它们为模型建立了更稳定的注意力模式模板

✅ 实践技巧:

  • 示例格式要完全一致(让注意力更容易发现模式)
  • 示例数量 3-5 个通常最佳(太少模式不稳定,太多浪费上下文)
  • 示例的顺序很重要:和你的问题最相似的示例放在最后(最近的注意力更强)

13.6 结构化 Prompt:注意力的"分隔符"

为什么使用 ---###、XML 标签等分隔符能提升效果?

Transformer 视角:

分隔符在 token 序列中创造了注意力断点。当模型看到 ---### 时,这些特殊 token 形成了注意力的"墙壁"——前后内容的注意力更倾向于在同一个区域内部流动,而不是跨区域混杂。

JavaScript
❌ 无结构:
"你是一个翻译专家请将以下内容翻译为中文Hello World这是一个测试"
→ 所有 token 的注意力混在一起,模型不确定哪些是指令、哪些是待翻译内容

✅ 有结构:
"你是一个翻译专家。
---
请将以下内容翻译为中文:
---
Hello World
这是一个测试"
→ 分隔符帮助注意力"分区管理"

✅ 最佳实践——System Prompt 的黄金结构:

JavaScript
【角色定义】你是一个 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 只能获得 1/N1/N 的注意力。

JavaScript
上下文 100 token:   每个 token 平均 1% 的注意力 → 关键信息容易被"看到"
上下文 10000 token:  每个 token 平均 0.01% 的注意力 → 关键信息容易被"淹没"
上下文 100000 token: 每个 token 平均 0.001% 的注意力 → 大海捞针!

✅ 实践技巧:

  • 别一股脑把所有信息塞进上下文——精选最相关的内容
  • 关键信息放在开头和结尾(U 型注意力曲线)
  • 使用 RAG(检索增强生成) 只检索最相关的片段,而不是塞入整个文档
  • 如果必须使用长上下文,用分隔符和标题帮助模型"索引"

13.8 为什么"重复指令"有效?

你可能注意到,在长 prompt 中重复关键指令效果更好。原理很简单:

  1. 多个位置的注意力叠加:关键指令出现在多个位置,模型在生成时有更多"锚点"可以注意到
  2. 对抗稀释:在长上下文中,单个位置的注意力很弱,但多次重复让总注意力增强
  3. 首尾效应强化:如果指令在开头和结尾都出现,就利用了 U 型注意力曲线的两个峰值
JavaScript
✅ "请用中文回答以下问题。
   [大量参考资料...]
   再次提醒:请用中文回答。
   问题是: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-12022~50%IO-aware 分块计算,减少内存读写
FlashAttention-22023~72%更好的并行化,支持多种注意力变体
FlashAttention-32024~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²)基线
FlashAttentionO(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

核心思想对比:

JavaScript
Transformer(注意力):
  每个 token 都可以"回头看"所有之前的 token
  → 信息通过 全局注意力矩阵 传递
  → 代价:O(N²) 计算,O(N) 内存

Mamba(选择性 SSM:
"记忆"压缩到一个固定大小的隐状态中
  → 信息通过 状态递推 传递(类似升级版 RNN  → 代价:O(N) 计算,O(1) 每步内存

Mamba 的"选择性"创新:

传统 SSM 对所有输入一视同仁(状态转移矩阵是固定的)。Mamba 的突破在于让状态转移依赖于输入内容——模型可以根据当前 token 的内容决定要"记住"什么、"遗忘"什么。这使得 Mamba 在语言建模上首次匹配甚至超越了同规模的 Transformer。

但 Transformer 并没有被取代:

维度TransformerMamba/SSM
长序列效率O(N²),长序列昂贵O(N),线性扩展
信息检索强(全局注意力)弱(有限的状态容量)
并行训练✅ 天然并行✅(结构化递推可并行)
上下文学习✅ 强 ICL 能力相对较弱
实际部署极度成熟快速增长中

14.3 混合架构:取各家之长

2025-2026 最明确的趋势是 混合架构——结合 Transformer 和 SSM 的优点:

JavaScript
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 一张图看未来

JavaScript
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-3H100 深度优化)
  ├── 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 核心概念回顾

JavaScript
Transformer(20172026 进化中)
├── 注意力机制
│   ├── 点积相似度 → 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 关键论文与参考资料

📚 参考资料


本文参考了 IBM Skills Network 的 Transformer 系列实验、吴恩达深度学习专项课程的 Transformer 作业,以及多篇原始论文。感谢这些优秀的教育资源。

最后更新:2026-02-09