欢迎光临
我们一直在努力

Transformer架构和实现

Transformer架构和实现

目录

  • 核心思想
  • 为什么需要Transformer?——从RNN的困境说起
  • Transformer整体架构
  • Input Embeddings
  • Positional Encoding位置编码
  • Scaled Dot-Product Attention——注意力的心脏
  • Multi-Head Attention——多个视角看问题
  • Mask机制——防止作弊和信息泄漏
  • Position-wise Feed Forward——逐位置的前馈网络
  • Layer Normalization与残差连接
  • Encoder与Decoder的完整实现
  • Generator与完整模型组装
  • 补充内容
  • 自测问题

  • 一、核心思想

    1.1 一句话理解Transformer

    Transformer由Google在2017年论文《Attention is All You Need》中提出。它的核心思想可以用一句话概括:用**纯注意力机制(Self-Attention)**取代循环神经网络(RNN/LSTM),实现完全并行的序列处理。

    1.2 核心创新(4个关键突破)

    创新点解决的问题通俗理解
    完全并行计算 RNN必须逐词串行处理,速度慢 所有词同时处理,像GPU渲染一样快
    Self-Attention RNN难以捕捉远距离依赖 每个词直接看到所有其他词,无视距离
    Multi-Head Attention 单一视角不够全面 8个"专家"从不同角度理解句子
    Positional Encoding Attention不感知词序 给每个词加上"位置标签"

    1.3 生活中的类比:会议室里的头脑风暴

    想象一个会议室里有6个人,每人代表句子中的一个词。他们要理解彼此之间的关系:

    • RNN的方式:第1个人把想法传给第2个人,第2个人结合自己的想法再传给第3个人…。问题是传到第6个人时,第1个人的原始想法可能已经被稀释模糊了。而且每个人都必须等前一个人说完才能开始工作(串行)。

    • Transformer的方式:所有人同时把自己的想法写在黑板上的6个位置。然后每个人可以同时查看所有6个位置的信息,并根据自己的需求有选择地关注某些位置的信息。所有人都同时工作(并行),而且每个人都能直接看到所有人的原始信息(无视距离)。

    这就是Transformer革命性的本质:让所有信息对所有人同时可见,每个人自主决定关注什么。

    1.4 论文信息

    • 论文: Attention is All You Need (2017)
    • 链接: https://arxiv.org/pdf/1706.03762.pdf
    • 作者: Google Research (Ashish Vaswani et al.)
    • 引用量: 10万+(截至2024年,是AI领域引用最多的论文之一)
    • 影响力: 催生了BERT、GPT、T5、 ViT等几乎所有现代NLP/CV大模型

    二、为什么需要Transformer?——从RNN的困境说起

    2.1 回顾:RNN做了什么,以及它的"硬伤"

    在上一章的Seq2Seq中,我们使用GRU(一种RNN)逐词处理句子。让我们深入理解RNN的三个致命问题:

    问题1: 串行计算——“高速公路变单车道”

    RNN处理句子 "I love deep learning":

    Step 1: 处理"I" → hidden_1 (耗时t)
    Step 2: 处理"love" → hidden_2 (耗时t, 必须先等Step 1完成)
    Step 3: 处理"deep" → hidden_3 (耗时t, 必须先等Step 2完成)
    Step 4: 处理"learning" → hidden_4 (耗时t, 必须先等Step 3完成)

    总耗时: 4t (无法并行)

    而Transformer可以同时处理这4个词,总耗时 ≈ t。
    句子越长,优势越明显。

    问题2: 长距离依赖——“传话游戏的最后一环”

    考虑句子: "The cat that ate the fish which was caught by the fisherman is sleeping."

    问题: "is sleeping"的主语是谁?
    答案: "The cat" (距离 "is" 有10个词)

    在RNN中,"The cat"的信息经过10步传递后变得模糊。
    这就像"传话游戏"——传了10个人后,原始信息面目全非。

    Transformer中,"is"可以直接看到"The cat"(距离为0),
    信息不需要经过中间词传递。

    问题3: 梯度消失/爆炸——“信号衰减”

    RNN的反向传播需要沿时间步展开(BPTT),梯度需要经过多次乘法。如果每次乘的值都小于1,梯度会指数级衰减(梯度消失);如果都大于1,会指数级增长(梯度爆炸)。这导致RNN难以学习长距离的模式。

    Transformer的Self-Attention通过直接连接避免了这种多步传播。

    2.2 Transformer如何解决这些问题

    RNN Transformer

    串行? 是 (逐词处理) 否 (所有词同时处理)
    长距离? 信息衰减,需要逐词传递 每个词直接看到所有其他词
    梯度? 沿时间展开,梯度消失/爆炸 直接连接,梯度畅通
    训练速度? 慢 (GPU利用率低) 快 (GPU并行能力充分发挥)

    实际影响: 训练一个大型翻译模型,RNN可能需要一周,Transformer可能只要一天(同样的硬件)。

    2.3 "Attention is All You Need"这个名字的含义

    论文标题故意说"Attention is All You Need"(注意力就是你所需要的一切),是在强调:你不需要RNN,不需要卷积,你需要的只是Attention。 这是一种宣言——我们找到了更简单、更优雅、更高效的方案来取代原来复杂的架构。


    三、Transformer整体架构

    3.1 “巨型夹心饼干”——Encoder-Decoder对称结构

    Transformer整体上仍然是Encoder-Decoder架构(和Seq2Seq一样),但内部的"发动机"从RNN换成了Self-Attention。

    输入(Source) 输出(Target)
    ↓ ↓
    ┌───────────────┐ ┌───────────────┐
    │Input Embedding│ │Output Embedding│
    │+ Positional │ │+ Positional │
    │ Encoding │ │ Encoding │
    └───────┬───────┘ └───────┬───────┘
    │ │
    ┌───────────────┴───────────────┐ ┌───────────────┴───────────────┐
    │ Encoder × N (N=6) │ │ Decoder × N (N=6) │
    │ ┌─────────────────────────┐ │ │ ┌─────────────────────────┐ │
    │ │ 1. Multi-Head Self-Attn │ │ │ │ 1. Masked Multi-Head │ │
    │ │ 2. Feed Forward │ │ │ │ Self-Attention │ │
    │ └─────────────────────────┘ │ │ │ 2. Multi-Head Cross- │ │
    │ 重复6次 │ │ │ Attention │ │
    └───────────────┬───────────────┘ │ │ 3. Feed Forward │ │
    │ │ └─────────────────────────┘ │
    │ │ 重复6次 │
    │ └───────────────┬───────────────┘
    │ │
    └──────────→ Cross-Attention ←────────┘
    (Decoder的Q查询Encoder的K,V)


    ┌───────────────┐
    │ Linear │
    │ + │
    │ Softmax │
    └───────────────┘


    Output Probabilities
    (每个目标词的概率分布)

    3.2 数据流的完整路径

    让我们跟踪一个翻译任务的数据流:

    任务: 英语 "I love you" → 德语 "Ich liebe dich"

    Encoder侧:
    "I" "love" "you"
    ↓ ↓ ↓
    词嵌入 + 位置编码
    ↓ ↓ ↓
    Encoder Layer 1 → Encoder Layer 2 → … → Encoder Layer 6
    ↓ ↓ ↓
    memory (encoder输出,供Decoder的Cross-Attention使用)

    Decoder侧:
    "SOS" "Ich" "liebe" "dich"
    ↓ ↓ ↓ ↓
    词嵌入 + 位置编码
    ↓ ↓ ↓ ↓
    Decoder Layer 1 → … → Decoder Layer 6
    ↑ ↑
    └── 每层的Cross-Attention都查看Encoder的memory ──┘

    Linear + Softmax → 预测下一个词

    3.3 标准配置(N=6的Transformer Base)

    超参数值含义为什么选这个值
    Encoder层数 N 6 Encoder堆叠6层 平衡深度和训练效率
    Decoder层数 N 6 Decoder堆叠6层 与Encoder对称
    d_model 512 模型的主维度(embedding/hidden维度) 足够表达丰富语义
    d_ff 2048 Feed Forward内部维度 d_model × 4 是经验值
    head数 8 多头注意力的头数 8个视角,每个64维
    d_k = d_v 64 每个头的维度(=512/8) 8 × 64 = 512 (拼回去)
    dropout 0.1 dropout概率 标准正则化值
    总参数量 ~65M 模型参数总量 适合单GPU训练

    3.4 层之间的数据流

    假设输入: batch_size=2, seq_len=4, d_model=512

    经过Embedding: [2, 4, 512]
    + Positional Enc: [2, 4, 512] (形状不变)
    → Encoder Layer 1: [2, 4, 512] (输出形状始终不变!)
    → Encoder Layer 2: [2, 4, 512]
    → …
    → Encoder Layer 6: [2, 4, 512] ← 这个就是"memory"
    传给Decoder的Cross-Attention

    关键发现: 在Transformer内部,数据形状 [batch, seq, d_model]
    始终不变。所有sub-layer保持输入输出的形状一致,
    这使得残差连接(相加)变得非常简单。

    3.5 整体架构自测问题

  • Transformer和Seq2Seq(GRU)在高层架构上有什么相同点和不同点?
  • Encoder的输出"memory"在Decoder中如何使用?
  • 为什么Encoder和Decoder都是6层?如果只有1层会怎样?
  • d_model=512, head=8, 那么d_k是多少?这个数字怎么来的?

  • 四、Input Embeddings

    4.1 Embedding的基本原理

    词嵌入(Embedding)是将离散的词索引映射为连续的稠密向量。这个向量不仅代表词本身,还编码了词的语义。

    词索引 "42" → Embedding表查询 → [0.23, -0.15, 0.78, …, 0.04] (512维)

    直观理解:
    – 相似含义的词有相似的向量 (king和queen的向量比较接近)
    – 向量中的每个维度编码某种"语义属性"
    – 这些向量不是人工设定的,而是训练过程中学到的

    4.2 代码实现——为什么乘以√d_model?

    class Embeddings(nn.Module):
    def __init__(self, d_model, vocab):
    """
    d_model: 嵌入维度 = 512
    vocab: 词汇表大小 (例如30000个词)

    nn.Embedding(num_embeddings, embedding_dim):
    – 本质上是一个大的查找表 [vocab_size, d_model]
    – 输入词索引 → 返回对应的向量行
    – 类似于一个字典: {0: [0.1,0.2,…], 1: [-0.1,0.3,…], …}
    – 这些值初始是随机的,然后通过训练优化
    """
    super(Embeddings, self).__init__()
    self.d_model = d_model
    self.lut = nn.Embedding(num_embeddings=vocab, embedding_dim=d_model)

    def forward(self, x):
    """
    x: [batch_size, seq_len] 整数索引
    例如: [[2, 5, 7, 1], [3, 8, 1, 0]]

    返回: [batch_size, seq_len, d_model]
    """
    # ===== Step 1: 查表获取词向量 =====
    # lut(x): [batch, seq] → [batch, seq, d_model]
    x = self.lut(x)

    # ===== Step 2: 乘以 √d_model =====
    # ★ 为什么要乘 √512 ≈ 22.6? ★
    #
    # 原因: Embedding初始值的方差较小(nn.Embedding使用
    # N(0,1)初始化),而后面要加的Positional Encoding
    # 取值范围在[-1, 1]。
    #
    # 如果不乘以√d_model:
    # embedding值 ≈ [-0.03, 0.03] (方差小)
    # positional值 ≈ [-1.0, 1.0] (范围大)
    # 两者相加后,位置编码信息会"淹没"词嵌入信息
    #
    # 乘以√d_model后:
    # embedding值 ≈ [-0.7, 0.7] (放大约22.6倍)
    # 现在两者在相近的数量级上
    # 位置信息和语义信息得以"平等融合"
    #
    # math.sqrt(self.d_model) = math.sqrt(512) ≈ 22.627
    x = x * math.sqrt(self.d_model)
    return x

    4.3 Embedding的输出形状

    输入: [2, 4] (2个句子, 每个4个词, 值为词索引)
    [[5, 12, 3, 1],
    [8, 2, 15, 3]]

    查表:
    索引5 → [0.01, -0.02, …, 0.03] (512维)
    索引12 → [-0.01, 0.05, …, -0.02]

    乘√512:
    每个值 × 22.627

    输出: [2, 4, 512] (2个句子, 4个词, 每个词512维向量)

    4.4 Embedding自测问题

  • nn.Embedding(vocab, d_model) 内部存储了什么?大概有多少个参数?
  • 为什么要乘以 math.sqrt(d_model)?不乘会怎样?
  • 同一个词在不同句子中出现,它的Embedding向量会一样吗?(训练时 vs 推理时)

  • 五、Positional Encoding(位置编码)

    5.1 核心问题:Transformer怎么知道词序?

    Attention机制有一个"盲点":它只看内容,不看位置。

    句子1: "Dog bites man." (狗咬人)
    句子2: "Man bites dog." (人咬狗)

    两个句子的词集合完全一样 {"Dog", "bites", "man", "."}
    如果没有位置信息,Transformer会认为它们是一样的!

    而RNN天然知道词序——因为它逐词处理,顺序本身就是信息。Transformer失去了这个天然的时序信息,所以需要人为地给每个位置注入一个"位置信号"——这就是Positional Encoding。

    5.2 位置编码的设计要求

    一个理想的位置编码应该满足:

  • 每个位置有唯一的编码:位置0和位置3的编码不同
  • 相对位置关系可计算:模型应该能从编码中推断"词A在词B前面2个位置"
  • 值域有界:不随位置增大而无限增大
  • 可外推:即使训练时只见过100个位置的编码,推理时第150个位置也能合理编码
  • 确定性:不需要额外的训练参数
  • 5.3 正弦/余弦位置编码公式

    Transformer使用正弦和余弦函数的组合来编码位置:

    P

    E

    (

    p

    o

    s

    ,

    2

    i

    )

    =

    sin

    (

    p

    o

    s

    10000

    2

    i

    /

    d

    m

    o

    d

    e

    l

    )

    PE_{(pos, 2i)} = \\sin\\left(\\frac{pos}{10000^{2i/d_{model}}}\\right)

    PE(pos,2i)=sin(100002i/dmodelpos)

    P

    E

    (

    p

    o

    s

    ,

    2

    i

    +

    1

    )

    =

    cos

    (

    p

    o

    s

    10000

    2

    i

    /

    d

    m

    o

    d

    e

    l

    )

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

    PE(pos,2i+1)=cos(100002i/dmodelpos)

    其中:

    • p

      o

      s

      pos

      pos:词在句子中的位置(0, 1, 2, 3, …)

    • i

      i

      i:维度的索引(0, 1, 2, …, d_model/2 – 1)

    • d

      m

      o

      d

      e

      l

      d_{model}

      dmodel:嵌入维度(512)

    5.4 为什么用sin和cos?——逐步理解

    Step 1: 理解"10000^(2i/d_model)"

    这个指数项控制不同维度的"频率"。

    当 i=0: 10000^(0/512) = 10000^0 = 1 → 最低频率 (波长最长)
    当 i=128: 10000^(256/512) = 10000^0.5 = 100 → 中等频率
    当 i=255: 10000^(510/512) ≈ 10000^1 = 10000 → 最高频率 (波长最短)

    所以:

    • 低维度(i小):变化慢,波长长 → 捕捉"宏观位置"(大概在第几段)
    • 高维度(i大):变化快,波长短 → 捕捉"精确位置"(具体第几个词)

    Step 2: 理解为什么用sin和cos交替

    偶数维度(2i): 使用 sin
    奇数维度(2i+1): 使用 cos

    这样的好处: 对于任意固定的偏移量k,PE(pos+k) 可以表示为 PE(pos) 的线性函数。
    这使得模型可以轻松学习"相对位置关系"。

    数学原理:
    sin(a+b) = sin(a)cos(b) + cos(a)sin(b)
    cos(a+b) = cos(a)cos(b) – sin(a)sin(b)

    所以 PE(pos+k) = PE(pos) × 某个线性变换
    这意味着注意力机制可以自然地学习到位置之间的相对关系

    5.5 代码实现——逐行详解

    class PositionalEncoding(nn.Module):
    def __init__(self, d_model, dropout=0.1, max_len=5000):
    """
    d_model: 嵌入维度 = 512
    dropout: 正则化 = 0.1
    max_len: 最大句子长度 = 5000 (预计算这么多位置的编码)
    """

    super(PositionalEncoding, self).__init__()
    self.dropout = nn.Dropout(p=dropout)

    # ===== Step 1: 创建位置编码矩阵 [max_len, d_model] =====
    pe = torch.zeros(max_len, d_model) # [5000, 512] 全零初始化

    # ===== Step 2: 创建位置索引 =====
    # torch.arange(0, max_len) → [0, 1, 2, …, 4999]
    # .unsqueeze(1) → [[0], [1], [2], …, [4999]] 形状 [5000, 1]
    position = torch.arange(0, max_len).unsqueeze(1)

    # ===== Step 3: 计算 div_term (频率项) =====
    # 公式: 1 / (10000^(2i/d_model))
    #
    # 为什么用 exp 而不是 pow?
    # 因为 exp(log(x)) = x,且 exp 在GPU上更快
    #
    # torch.arange(0, d_model, 2): [0, 2, 4, …, 510] (256个值)
    # -(math.log(10000.0) / d_model): -(9.21 / 512) ≈ -0.01799
    # 相乘: [0, -0.036, -0.072, …, -9.17]
    # exp(…): [1.0, 0.965, 0.931, …, 0.0001]
    div_term = torch.exp(
    torch.arange(0, d_model, 2) *
    (math.log(10000.0) / d_model)
    )
    # div_term 形状: [256] (d_model/2 = 256)

    # ===== Step 4: 计算位置 × 频率 =====
    # position: [5000, 1]
    # div_term: [256]
    # 广播相乘: [5000, 256]
    #
    # my_matmulres[pos][i] = pos / (10000^(2i/d_model))
    my_matmulres = position * div_term

    # ===== Step 5: 填充 pe 矩阵 =====
    # 偶数维度 (0, 2, 4, …, 510): 填入 sin
    # pe[:, 0::2] 表示: 所有行, 从第0列开始每隔2列取
    pe[:, 0::2] = torch.sin(my_matmulres)

    # 奇数维度 (1, 3, 5, …, 511): 填入 cos
    # pe[:, 1::2] 表示: 所有行, 从第1列开始每隔2列取
    pe[:, 1::2] = torch.cos(my_matmulres)

    # ===== Step 6: 增加batch维度 =====
    # pe: [5000, 512] → [1, 5000, 512]
    # 增加batch维度方便与输入相加(广播)
    pe = pe.unsqueeze(0)

    # ===== Step 7: 注册为buffer(非训练参数) =====
    # register_buffer: 保存到state_dict中,但不参与梯度计算
    # 位置编码是固定的,不需要训练
    self.register_buffer('pe', pe)

    def forward(self, x):
    """
    x: [batch_size, seq_len, d_model] 例如 [2, 4, 512]

    返回: x + pe (位置编码直接加到词嵌入上)
    """
    # Variable(self.pe[:, :x.size(1)], requires_grad=False)
    # self.pe[:, :x.size(1)]: 取前 seq_len 个位置的编码
    # 例如 x.size(1)=4 → 取 pe[0, 0:4, :] → [1, 4, 512]
    # requires_grad=False: 位置编码不参与梯度计算
    #
    # x + pe: 广播相加
    # x: [2, 4, 512]
    # pe: [1, 4, 512] ← 广播到 [2, 4, 512]
    # 结果: [2, 4, 512]
    x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False)

    # Dropout: 训练时随机丢弃10%的神经元
    return self.dropout(x)

    5.6 位置编码的可视化理解

    如果你把PE矩阵画出来,你会看到:

    位置编码矩阵 pe[位置, 维度] 的图案:

    维度0 (低频sin波):
    位置: 0 1 2 3 4 5 … 99 100
    值: 0.0 0.8 0.9 0.1 -0.8 -0.9 … ->
    像一个非常平缓的正弦波,周期很长

    维度255 (高频sin波):
    位置: 0 1 2 3 4 5 … 99 100
    值: 0.0 0.1 -0.2 0.3 -0.1 0.0 … ->
    像一个非常快速振荡的波,周期很短

    每个位置(pos)的512维编码就像该位置的一个"指纹"
    位置0的指纹 ≠ 位置1的指纹 ≠ 位置2的指纹…
    但位置0和位置2之间的"差异模式"是可学习的

    5.7 Positional Encoding自测问题

  • 为什么Transformer需要位置编码而RNN不需要?
  • 为什么用sin和cos而不用一个可学习的embedding表来编码位置?
  • 公式中的10000是什么?改成100会怎样?
  • register_buffer 和 nn.Parameter 有什么不同?为什么要用buffer?

  • 六、Scaled Dot-Product Attention——注意力的心脏

    6.1 从"查字典"理解Dot-Product Attention

    让我们用一个日常生活中的类比来理解注意力机制:

    场景:你在图书馆查资料

    你有一个问题Q:“Transformer的注意力机制是怎么工作的?”

    图书馆有很多本书,每本书前面有目录/关键词K,书里面有具体内容V。

    过程:

  • 你把你的问题Q和每本书的目录K比较 → 判断哪本书相关(计算相似度)
  • 根据相关程度给每本书分配"关注权重"(Softmax归一化)
  • 从最相关的几本书中提取内容V → 按权重混合 → 得到综合答案
  • Scaled Dot-Product Attention就是做这件事的数学公式。

    6.2 核心公式——每个符号的含义

    Attention

    (

    Q

    ,

    K

    ,

    V

    )

    =

    softmax

    (

    Q

    K

    T

    d

    k

    )

    V

    \\text{Attention}(Q, K, V) = \\text{softmax}\\left(\\frac{QK^T}{\\sqrt{d_k}}\\right)V

    Attention(Q,K,V)=softmax(dk

    QKT)V

    逐个符号解释:

    符号含义形状

    Q

    Q

    Q

    Query(查询):“我要找什么?” [batch, heads, seq_q, d_k]

    K

    K

    K

    Key(键):“每个位置有什么标签?” [batch, heads, seq_k, d_k]

    V

    V

    V

    Value(值):“每个位置的实际内容?” [batch, heads, seq_k, d_v]

    Q

    K

    T

    QK^T

    QKT

    Q和K的内积(点积),衡量相似度 [batch, heads, seq_q, seq_k]

    d

    k

    \\sqrt{d_k}

    dk

    缩放因子,防止点积太大 标量 (通常d_k=64)
    softmax(…) 归一化为概率分布(每行和为1) [batch, heads, seq_q, seq_k]
    右乘V 按注意力权重对V加权求和 [batch, heads, seq_q, d_v]

    6.3 为什么要除以√d_k?——"缩放"的关键

    问题:当d_k很大时(比如512),Q和K的点积值也会很大。大的点积值经过Softmax后会变得非常"尖锐"——只有一个位置的权重接近1,其他都接近0。

    不缩放的情况 (d_k=64, 不除以√64=8):

    Q·K^T的值:
    K[0] K[1] K[2] K[3]
    Q[0]: [0.3, 0.5, 0.1, 0.2] ← 差异不大,Softmax后分布合理
    Q[1]: [12.3, 10.5, 8.1, 9.2] ← 差异大但其实还正常

    当 d_k=512, Q和K的每个元素期望≈0,方差≈1:
    Q·K^T 是512个独立随机变量的和,方差≈512
    所以 Q·K^T 的值通常在 [-√512, √512] ≈ [-22, 22] 范围内

    Softmax(22, -15, 3, -8) ≈ [~1.0, ~0.0, ~0.0, ~0.0]

    梯度几乎为零!模型学不动了!

    缩放后 (除以√64=8):
    Q·K^T/8 的值范围缩小到 [-2.75, 2.75]
    Softmax(2.75, -1.9, 0.4, -1.0) ≈ [0.8, 0.05, 0.5, 0.01]
    分布合理,梯度正常流动 ✓

    核心结论:除以√d_k控制点积的方差为1,使得Softmax的输出不会过于极端,保持梯度健康流动。

    6.4 代码实现——逐步跟踪张量形状

    def attention(query, key, value, mask=None, dropout=None):
    """
    参数:
    – query: [batch, heads, seq_len, d_k] 例如 [2, 8, 4, 64]
    – key: [batch, heads, seq_len, d_k] 例如 [2, 8, 4, 64]
    – value: [batch, heads, seq_len, d_k] 例如 [2, 8, 4, 64]
    – mask: [batch, 1, seq_len, seq_len] 或 None

    返回:
    – output: [batch, heads, seq_len, d_k]
    – p_attn: [batch, heads, seq_len, seq_len] 注意力权重
    """

    # ===== Step 1: 获取d_k =====
    # query.size()[-1]: 取query最后一个维度的大小
    # 如果query是 [2, 8, 4, 64],则d_k = 64
    d_k = query.size()[1]

    # ===== Step 2: 计算注意力分数 =====
    # Q @ K^T: 矩阵乘法
    # query: [2, 8, 4, 64]
    # key.transpose(-2, -1): [2, 8, 64, 4] (最后两维交换)
    # torch.matmul: [2, 8, 4, 4] ← 每个头一个4×4的注意力分数矩阵
    #
    # 除以√d_k: 缩放,防止Softmax饱和
    # math.sqrt(64) = 8
    scores = torch.matmul(query, key.transpose(2, 1)) / math.sqrt(d_k)

    # ===== Step 3: Mask(如果有) =====
    # 将mask==0的位置的分数设为 -1e9 (极大的负数)
    # Softmax(-1e9) ≈ 0 — 让被mask的位置不参与注意力计算
    if mask is not None:
    scores = scores.masked_fill(mask == 0, 1e9)

    # ===== Step 4: Softmax归一化 =====
    # dim=-1: 在最后一个维度(seq_len维度)做Softmax
    # 结果: 每行的和为1,每个值在[0,1]之间
    # [2, 8, 4, 4] → [2, 8, 4, 4]
    p_attn = F.softmax(scores, dim=1)

    # ===== Step 5: Dropout正则化(训练时) =====
    if dropout is not None:
    p_attn = dropout(p_attn)

    # ===== Step 6: 加权求和 =====
    # p_attn: [2, 8, 4, 4] 注意力权重
    # value: [2, 8, 4, 64] 原始值
    # 结果: [2, 8, 4, 64] 加权后的输出
    #
    # 每行的含义: output[q_idx] = Σ (p_attn[q_idx][k_idx] × value[k_idx])
    # 即: 每个查询位置的输出 = 所有键位置值的加权平均
    return torch.matmul(p_attn, value), p_attn

    6.5 计算流程的可视化——以"我爱中国"为例

    输入句子: "I love China" (加上标点 ".")
    词索引: [I, love, China, .]
    位置: 0 1 2 3

    假设 d_k = 4 (为了简化演示,实际是64)

    Step 1: 计算 Q @ K^T
    Q和K都是 [1, 8, 4, 4] (1个batch, 8个头, 4个词, 4维)

    以某一个头为例:
    Q(一个头) = [4×4]矩阵 K(一个头) = [4×4]矩阵

    I love China . I love China .
    Q[0]: [0.2, 0.5, -0.1, 0.3] K[0]: [0.1, -0.2, 0.4, 0.1]
    Q[1]: [0.1, -0.3, 0.4, -0.2] K[1]: [-0.1, 0.3, -0.2, 0.5]
    Q[2]: [-0.2, 0.1, 0.5, 0.2] K[2]: [0.3, 0.1, -0.1, 0.2]
    Q[3]: [0.4, -0.1, -0.3, 0.1] K[3]: [-0.2, 0.4, 0.2, -0.1]

    scores = Q @ K^T / √4:
    K[0] K[1] K[2] K[3]
    Q[0]: [0.3, 0.1, -0.2, 0.0]
    Q[1]: [0.0, 0.4, 0.2, -0.1]
    Q[2]: [-0.1, 0.2, 0.5, 0.1]
    Q[3]: [0.2, -0.1, 0.0, 0.4]

    scores[0][0] = 0.3 表示: Q[0]("I") 与 K[0]("I") 的相似度为0.3

    Step 2: Softmax(每行)

    K[0] K[1] K[2] K[3] (和为1)
    Q[0]: [0.35, 0.25, 0.15, 0.25] ← "I"最关注自己(0.35)
    Q[1]: [0.20, 0.35, 0.30, 0.15] ← "love"关注自己和"China"
    Q[2]: [0.15, 0.25, 0.40, 0.20] ← "China"最关注自己
    Q[3]: [0.25, 0.15, 0.15, 0.45] ← "."最关注自己

    解读: 每个词对自己的关注度最高(这是自注意力的典型模式),
    同时也关注其他相关的词。

    Step 3: 加权求和

    output[i] = Σ_j p_attn[i][j] × V[j]

    output[0] = 0.35×V[0] + 0.25×V[1] + 0.15×V[2] + 0.25×V[3]
    (这行表示: "I"的新表示包含了"I"(35%)、"love"(25%)等的信息)

    output[2] = 0.15×V[0] + 0.25×V[1] + 0.40×V[2] + 0.20×V[3]
    ("China"的新表示主要来自自己(40%),但也吸收其他词的信息)

    6.6 三种Attention的对比

    Self-AttentionMasked Self-AttnCross-Attention
    Q来源 自身的上一层输出 自身的上一层输出 Decoder的自身输出
    K来源 自身的上一层输出 自身的上一层输出 Encoder的输出(memory)
    V来源 自身的上一层输出 自身的上一层输出 Encoder的输出(memory)
    Mask 可选的Padding Mask Sequence + Padding Mask Padding Mask
    用途 理解句子内部关系 Decoder防止看到未来 Decoder关注输入信息

    6.7 Scaled Dot-Product Attention自测问题

  • 为什么要除以√d_k?如果不除会怎样?
  • 如果一个词的注意力权重是[0.1, 0.1, 0.7, 0.1],意味着什么?
  • Self-Attention中Q、K、V都来自同一个输入,这样做有什么好处?
  • masked_fill(mask == 0, -1e9) 为什么填-1e9而不是0?

  • 七、Multi-Head Attention——多个视角看问题

    7.1 为什么需要"多头"?

    单头Attention只有一个视角。想象一个翻译任务:

    句子: "The bank is by the river bank."

    单头注意力可能只关注到一种关系:
    – "bank"和"river"的关系 (河岸)
    – 但"bank"也有"银行"的含义
    – 另外"is"和"bank"之间有语法关系

    单一视角无法同时捕捉多种关系!

    多头注意力让模型可以从多个子空间(不同语义角度)同时关注信息:

    • Head 1: 关注"语法关系"(主语-谓语-宾语)→ “bank” ← “is”(系动词)
    • Head 2: 关注"语义关系"(词的含义关联)→ “bank” ← “river”(河岸)
    • Head 3: 关注"位置关系"(相邻词)→ “the” → “bank”(冠词+名词)
    • Head 4: 关注"另一个含义" → “bank” ← “money”(银行,如果上下文中有的话)

    8个"专家"从8个不同的角度理解句子,然后综合意见。

    7.2 公式

    MultiHead

    (

    Q

    ,

    K

    ,

    V

    )

    =

    Concat

    (

    head

    1

    ,

    head

    2

    ,

    .

    .

    .

    ,

    head

    h

    )

    W

    O

    \\text{MultiHead}(Q, K, V) = \\text{Concat}(\\text{head}_1, \\text{head}_2, …, \\text{head}_h)W^O

    MultiHead(Q,K,V)=Concat(head1,head2,,headh)WO

    head

    i

    =

    Attention

    (

    Q

    W

    i

    Q

    ,

    K

    W

    i

    K

    ,

    V

    W

    i

    V

    )

    \\text{head}_i = \\text{Attention}(QW_i^Q, KW_i^K, VW_i^V)

    headi=Attention(QWiQ,KWiK,VWiV)

    其中

    W

    i

    Q

    ,

    W

    i

    K

    ,

    W

    i

    V

    W_i^Q, W_i^K, W_i^V

    WiQ,WiK,WiV 是每个头的投影矩阵,

    W

    O

    W^O

    WO 是输出投影矩阵。

    7.3 代码实现——分量与合体

    def clones(module, N):
    """
    克隆N个相同的模块(深拷贝)

    为什么需要clones?
    – Multi-Headed Attention中有4个Linear层(Q,K,V,O)
    – 代码中用clones一次性创建4个,保持代码简洁
    – copy.deepcopy确保每个副本是独立的(参数不共享)
    """
    return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])

    class MultiHeadedAttention(nn.Module):
    def __init__(self, head, embedding_dim, dropout=0.1):
    """
    head: 注意力头数 = 8
    embedding_dim: 嵌入维度 = 512
    dropout: 0.1

    关键计算: d_k = embedding_dim / head = 512 / 8 = 64
    为什么d_k=64?
    – 8个头 × 64维 = 512维,拼回去刚好等于原始维度
    – 这样残差连接时可以直接相加,不需要额外变换
    """
    super(MultiHeadedAttention, self).__init__()

    # 断言: embedding_dim必须能被head整除
    # 如果512不能被8整除,就会报错
    assert embedding_dim % head == 0

    self.d_k = embedding_dim // head # 512 // 8 = 64
    self.head = head # 8

    # ===== 4个线性变换层 =====
    # 为什么是4个?
    # linears[0]: W^Q (Query投影)
    # linears[1]: W^K (Key投影)
    # linears[2]: W^V (Value投影)
    # linears[3]: W^O (Output投影,用于合并多头)
    #
    # 每个Linear(512, 512)的作用:
    # 将输入投影到一个新的空间,让模型学习"从什么角度看问题"
    # 虽然输入512维、输出512维,但这不是恒等变换
    # — 它学到的是一种"视角转换"
    self.linears = clones(nn.Linear(embedding_dim, embedding_dim), 4)
    self.attn = None # 保存最后的注意力权重(用于可视化)
    self.dropout = nn.Dropout(p=dropout)

    def forward(self, query, key, value, mask=None):
    """
    前向传播: 多头注意力的完整计算

    参数:
    – query: [batch, seq_len, d_model] 例如 [2, 4, 512]
    – key: [batch, seq_len, d_model] 例如 [2, 4, 512]
    – value: [batch, seq_len, d_model] 例如 [2, 4, 512]
    – mask: [batch, 1, seq_len, seq_len] 或 None

    返回: [batch, seq_len, d_model]

    === 整体流程 ===

    输入: [2, 4, 512]
    |
    v Linear变换 (每个头有独立的Q,K,V投影)
    [2, 4, 512] → [2, 4, 8, 64] → [2, 8, 4, 64]
    | (view分头) (transpose换轴)
    v
    Scaled Dot-Product Attention (每个头独立计算)
    |
    v [2, 8, 4, 64]
    |
    v transpose + view: 合并多头
    [2, 4, 8, 64] → [2, 4, 512]
    |
    v Linear(W^O) 最终投影
    [2, 4, 512]
    """
    # ===== Mask处理: 增加头维度 =====
    # mask原始形状: [batch, seq_len, seq_len] 例如 [2, 4, 4]
    # 增加维度后: [batch, 1, seq_len, seq_len] 例如 [2, 1, 4, 4]
    # 这样mask可以广播到所有8个头
    if mask is not None:
    mask = mask.unsqueeze(0) # 此处的实现略有特殊

    batch_size = query.size(0)

    # ===== Step 1: 线性变换 + 分头 =====
    # 这个列表推导同时处理Q、K、V:
    #
    # 对于query:
    # linears[0](query): [2, 4, 512] → [2, 4, 512] (Linear变换)
    # .view(batch, -1, head, d_k): [2, 4, 512] → [2, 4, 8, 64]
    # -1 表示"自动推断",这里=4(seq_len)
    # 将512维拆成 [8个头 × 64维/头]
    # .transpose(1,2): [2, 4, 8, 64] → [2, 8, 4, 64]
    # 交换维度1和维度2,使heads维度在seq维度之前
    # 这样每个头独立处理自己的[seq, d_k]数据块
    #
    # 对于key和value也是同样的操作
    query, key, value = [
    model(x).view(batch_size, 1, self.head, self.d_k).transpose(1, 2)
    for model, x in zip(self.linears, (query, key, value))
    ]
    # 现在: query=[2, 8, 4, 64], key=[2, 8, 4, 64], value=[2, 8, 4, 64]

    # ===== Step 2: Scaled Dot-Product Attention =====
    # 调用之前定义的attention函数
    # 输入: [2, 8, 4, 64] × 3
    # 输出: x=[2, 8, 4, 64], self.attn=[2, 8, 4, 4]
    x, self.attn = attention(query, key, value, mask, dropout=self.dropout)

    # ===== Step 3: 合并多头 (Concat) =====
    # Step 3a: transpose – 将heads维度移回原位置
    # [2, 8, 4, 64] → [2, 4, 8, 64]
    #
    # Step 3b: contiguous – 使内存连续
    # transpose只是改变了"视角"(stride),不改变实际内存布局
    # .contiguous() 重新排列内存,使其连续
    # 这是PyTorch的要求: view()只能用于连续tensor
    #
    # Step 3c: view – 合并最后两个维度
    # [2, 4, 8, 64] → [2, 4, 512]
    # batch=2, seq_len=4, 8*64=512=d_model
    x = x.transpose(1, 2).contiguous().view(
    batch_size, 1, self.head * self.d_k
    )

    # ===== Step 4: 最终线性变换 (W^O) =====
    # linears[-1] 是第4个Linear: W^O投影
    # [2, 4, 512] → [2, 4, 512]
    # 虽然维度没变,但这个变换混合了8个头的信息
    return self.linears[1](x)

    7.4 view + transpose 的区别——一个重要的PyTorch陷阱

    # 这是很多初学者会踩的坑!

    # 创建一个测试张量
    a = torch.randn(1, 2, 3, 4) # [1, 2, 3, 4]

    # view: 只改变"形状解释",不改变内存中的数据顺序
    b = a.view(1, 3, 2, 4)
    # a[0,0,0,:] 和 b[0,0,0,:] 的数据一样吗?
    # 是的!因为view只是重新解释同一块内存

    # transpose: 交换两个维度的步长(stride)
    c = a.transpose(1, 2) # [1, 3, 2, 4]
    # 现在 a[0,0,1,:] 变成了 c[0,1,0,:]
    # 数据在内存中的排列不同了

    # view在非连续tensor上会报错!
    # d = c.view(1, 3, 2, 4) ← 这会报错!
    # 必须先用.contiguous()使内存连续
    d = c.contiguous().view(1, 3, 2, 4) # ← 正确做法

    # 简单记忆:
    # view = 重塑(要求内存连续)
    # transpose = 交换(打乱内存,需要contiguous后才能view)

    7.5 每个头学到什么?

    虽然我们无法精确控制每个头关注什么,但研究表明(通过分析注意力模式),不同的头确实自然地学到了不同的模式:

    • 有的头关注"相邻位置"(局部语法关系)
    • 有的头关注"远距离关系"(例如主语-谓语一致)
    • 有的头关注"特定词性"(将所有名词关联起来)
    • 有的头关注"自身"(保留原词信息)

    这种多样性正是Multi-Head的设计目标。

    7.6 Multi-Head Attention自测问题

  • 为什么需要多个头?如果只有一个头会怎样?
  • d_k=d_model/head,为什么?如果d_k=128而d_model=512,head应该等于几?
  • transpose(1,2) 之后为什么需要 .contiguous() 才能 .view()?
  • 最后的linears[-1](x)的作用是什么?它的输入输出维度一样,为什么不能省略?

  • 八、Mask机制——防止作弊和信息泄漏

    8.1 为什么需要Mask?

    Transformer中有两种完全不同的Mask,解决两种不同的问题:

  • Padding Mask:序列长度不一致时,短句子的填充部分不应参与注意力计算
  • Sequence Mask (Look-ahead Mask):Decoder在训练时不应"看到未来"
  • 8.2 Padding Mask —— 让填充词"隐身"

    场景: batch中有两个不同长度的句子:

    句子1: "I love NLP" → [I, love, NLP, EOS] (4个词)
    句子2: "Hello world" → [Hello, world, EOS] (3个词)

    为了组成batch,需要将句子2填充到同样长度:
    句子2(padded): [Hello, world, EOS, PAD] (第4个位置是填充)

    填充的PAD不应该参与Attention计算!

    Padding Mask:
    句子1: [1, 1, 1, 1] ← 所有位置都参与
    句子2: [1, 1, 1, 0] ← 最后一个位置(填充)的mask=0

    在attention函数中:
    if mask is not None:
    scores = scores.masked_fill(mask == 0, -1e9)
    # PAD位置的score变成 -1e9
    # Softmax(-1e9) ≈ 0 → PAD的注意力权重≈0

    代码示例:

    # 被mask的位置设为-1e9,softmax后趋近于0
    scores = scores.masked_fill(mask == 0, 1e9)

    # 为什么是-1e9而不是负无穷?
    # -float('inf') 在数值计算中可能产生NaN
    # -1e9足够小,在Softmax下 ≈ 0,且数值稳定

    8.3 Sequence Mask —— 阻止"时间旅行"

    问题场景:在Decoder训练时,我们的输入是整个目标序列:

    输入: [SOS, Ich, liebe, dich] (目标语言的完整句子)
    目标: [Ich, liebe, dich, EOS] (向右偏移一位的预测目标)

    如果不用mask,Decoder的自注意力就能看到:

    • 位置0(“SOS”)在计算注意力时可以"偷看"位置1(“Ich”)、位置2(“liebe”)…
    • 这等于"先看到答案再答题"——作弊!

    解决方法:Sequence Mask(上三角掩码)

    def subsequent_mask(size):
    """
    生成一个下三角矩阵 (只能往过去看, 不能往未来看)

    参数: size = 5
    返回: [1, 5, 5] 的mask矩阵
    """
    # Step 1: 创建上三角矩阵
    # np.triu(np.ones((1,5,5)), k=1)
    # k=1 表示从对角线之上(k=1)开始保留
    #
    # 结果(上三角=1, 下三角=0):
    # [[0, 1, 1, 1, 1],
    # [0, 0, 1, 1, 1],
    # [0, 0, 0, 1, 1],
    # [0, 0, 0, 0, 1],
    # [0, 0, 0, 0, 0]]

    subsequent_mask = np.triu(
    m=np.ones((1, size, size)), k=1
    ).astype('uint8')

    # Step 2: 取反 (下三角=1, 上三角=0)
    # 1 – subsequent_mask:
    # [[1, 0, 0, 0, 0], ← 位置0只能看到位置0
    # [1, 1, 0, 0, 0], ← 位置1可以看到位置0和1
    # [1, 1, 1, 0, 0], ← 位置2可以看到位置0,1,2
    # [1, 1, 1, 1, 0], ← 位置3可以看到位置0,1,2,3
    # [1, 1, 1, 1, 1]] ← 位置4可以看到所有位置
    return torch.from_numpy(1 subsequent_mask)

    8.4 Sequence Mask的直观理解

    翻译 "I love you" → "Je t'aime"

    Decoder训练时输入: [SOS, Je, t', aime]

    Sequence Mask使得:
    – "SOS"的注意力: 只能看 [SOS, X, X, X] (什么也看不到)
    – "Je"的注意力: 只能看 [SOS, Je, X, X] (可以看到SOS)
    – "t'"的注意力: 只能看 [SOS, Je, t', X] (可以看到前面两个)
    – "aime"的注意力: 只能看 [SOS, Je, t', aime] (全部看到)

    这模拟了"逐词生成"的过程:
    在真实推理时,生成"aime"时,前三个词已经生成了,
    但"aime"后面的词还不存在,所以不应该看到。

    8.5 Mask的数值实现

    Mask矩阵 (1=可见, 0=不可见):
    [[1, 0, 0, 0],
    [1, 1, 0, 0],
    [1, 1, 1, 0],
    [1, 1, 1, 1]]

    在attention函数中:
    scores = Q @ K^T / √d_k

    假设scores:
    [[0.3, 0.1, 0.2, 0.4],
    [0.1, 0.5, 0.1, 0.3],
    [0.2, 0.1, 0.6, 0.1],
    [0.0, 0.3, 0.2, 0.5]]

    应用mask: scores[mask==0] = -1e9
    [[0.3, -1e9, -1e9, -1e9],
    [0.1, 0.5, -1e9, -1e9],
    [0.2, 0.1, 0.6, -1e9],
    [0.0, 0.3, 0.2, 0.5]]

    Softmax后(每行归一化):
    [[1.00, 0.00, 0.00, 0.00], ← SOS只能关注自己
    [0.40, 0.60, 0.00, 0.00], ← Je关注SOS(0.4)和自己(0.6)
    [0.24, 0.22, 0.54, 0.00], ← t'关注前三个位置
    [0.12, 0.26, 0.21, 0.41]] ← aime可以看所有

    注意: 被mask的位置权重为0 (不漏未来信息) ✓

    8.6 Mask自测问题

  • Padding Mask和Sequence Mask分别解决什么问题?
  • 为什么Sequence Mask只在Decoder的Self-Attention中使用?
  • masked_fill(mask == 0, -1e9) 中为什么用-1e9而不是-float(‘inf’)?
  • np.triu(x, k=1) 中k=1的含义是什么?如果k=0呢?

  • 九、Position-wise Feed Forward——逐位置的前馈网络

    9.1 为什么需要Feed Forward?

    Self-Attention负责"信息交流"(词与词之间交换信息),但它本质上是一个线性加权操作(虽然有多头和多层的非线性的叠加)。Feed Forward给每个位置独立地添加非线性变换能力:

    Self-Attention: "每个词和其他词交流"
    Feed Forward: "每个词独立地思考、消化刚才交流获得的信息"

    9.2 公式

    F

    F

    N

    (

    x

    )

    =

    max

    (

    0

    ,

    x

    W

    1

    +

    b

    1

    )

    W

    2

    +

    b

    2

    FFN(x) = \\max(0, xW_1 + b_1)W_2 + b_2

    FFN(x)=max(0,xW1+b1)W2+b2

    用ReLU更准确地写:

    F

    F

    N

    (

    x

    )

    =

    ReLU

    (

    x

    W

    1

    +

    b

    1

    )

    W

    2

    +

    b

    2

    FFN(x) = \\text{ReLU}(xW_1 + b_1)W_2 + b_2

    FFN(x)=ReLU(xW1+b1)W2+b2

    关键结构特点:

    • W

      1

      W_1

      W1: 512 → 2048 (扩张4倍)

    • W

      2

      W_2

      W2: 2048 → 512 (缩回原维度)

    • 扩张-收缩 结构,类似一个"沙漏"或"瓶颈"

    9.3 "Position-wise"是什么意思?

    "Position-wise"意味着:每个位置独立地应用相同的FFN变换,位置之间不交互。

    对于每个位置i (i=0,1,…,seq_len-1):
    x[i] 是 [512] 维向量
    FFN(x[i]) = ReLU(x[i] @ W1 + b1) @ W2 + b2

    所有位置共享W1和W2参数(权重绑定)

    这就像:
    – 每个词独立地走进自己的"处理室"
    – 都使用同样的处理规则(W1, W2)
    – 处理后走出来
    – 词之间在FFN阶段不交流

    9.4 代码实现

    class PositionwiseFeedForward(nn.Module):
    def __init__(self, d_model, d_ff, dropout=0.1):
    """
    d_model: 主维度 = 512
    d_ff: FFN内部维度 = 2048
    dropout: 0.1

    为什么 d_ff = 2048 = 4 × d_model?
    – 经验表明,4倍扩张是最好的平衡
    – 太小:非线性表达能力不够
    – 太大:参数太多,训练慢
    – 512→2048→512 是标准配置
    """
    super(PositionwiseFeedForward, self).__init__()

    # ===== 第1层: 扩张 (512 → 2048) =====
    self.w1 = nn.Linear(d_model, d_ff)
    # 参数量: 512 × 2048 + 2048(偏置) ≈ 1,050,624

    # ===== 第2层: 收缩 (2048 → 512) =====
    self.w2 = nn.Linear(d_ff, d_model)
    # 参数量: 2048 × 512 + 512(偏置) ≈ 1,049,088

    self.dropout = nn.Dropout(p=dropout)

    def forward(self, x):
    """
    x: [batch, seq_len, d_model] 例如 [2, 4, 512]

    返回: [batch, seq_len, d_model]

    处理流程:
    [2, 4, 512]
    → w1: Linear(512→2048) → [2, 4, 2048]
    → F.relu: 非线性激活 → [2, 4, 2048]
    → dropout: 正则化 → [2, 4, 2048]
    → w2: Linear(2048→512) → [2, 4, 512]
    """
    # self.w1(x): [2, 4, 512] → [2, 4, 2048]
    # F.relu(…): [2, 4, 2048] → [2, 4, 2048]
    # self.dropout(…):[2, 4, 2048] → [2, 4, 2048]
    # self.w2(…): [2, 4, 2048] → [2, 4, 512]
    x = self.w2(self.dropout(F.relu(self.w1(x))))
    return x

    9.5 为什么需要扩张-收缩结构?

    512 → 2048 → 512

    扩张阶段(512→2048):
    – 在更高维空间中"分解"信息
    – 更多的神经元 = 更强的表达能力
    – 类似: 用小本子记录 vs 用大黑板演算

    激活(ReLU):
    – 引入非线性
    – 没有激活函数的话,两层Linear可以合并为一层
    (Linear(512,2048) + Linear(2048,512) = Linear(512,512))
    – 所以ReLU是必须的,否则二层的表达能力=一层

    收缩阶段(2048→512):
    – 信息压缩,提取精华
    – 恢复到原始维度,保证残差连接通畅

    9.6 Feed Forward自测问题

  • "Position-wise"的含义是什么?
  • 为什么FFN使用"扩张→收缩"结构?
  • 如果没有ReLU激活函数,两层Linear会怎样?
  • FFN的参数量大约是多少?(512→2048→512)

  • 十、Layer Normalization与残差连接

    10.1 为什么需要Normalization?

    深层神经网络的训练面临一个挑战:每一层的输出分布会不断变化(Internal Covariate Shift)。BatchNorm和LayerNorm都是为了稳定每一层的输入分布。

    10.2 BatchNorm vs LayerNorm——为什么NLP用LayerNorm?

    对比维度BatchNormLayerNorm
    归一化维度 在batch维度上归一化 在feature维度上归一化
    计算方式 同一个特征在不同样本间求均值 同一个样本在不同特征间求均值
    依赖batch大小 是(batch小→统计不准)
    NLP适用性 差(变长序列+小batch)
    训练/推理行为 不同(训练用batch统计,推理用全局统计) 一致

    BatchNorm (对NLP不友好):
    样本1: "I love cats" → [0.1, 0.2, …, 0.5] (512维)
    样本2: "The quick brown fox jumps" → [0.3, -0.1, …, 0.2] (512维)

    归一化: 取样本1和样本2的**同一维度**求均值

    问题:
    – 序列长度不同需要padding,padding会影响统计
    – NLP任务batch_size通常较小(2-8),统计不准确

    LayerNorm (对NLP友好):
    样本1: "I love cats" → [0.1, 0.2, …, 0.5] (512维)
    归一化: 在样本1的**512个维度**上求均值和方差

    优点:
    – 每个样本独立归一化,不受batch大小影响
    – 不依赖padding
    – 训练和推理行为一致

    10.3 LayerNorm公式

    LayerNorm

    (

    x

    )

    =

    γ

    x

    μ

    σ

    +

    ϵ

    +

    β

    \\text{LayerNorm}(x) = \\gamma \\cdot \\frac{x – \\mu}{\\sigma + \\epsilon} + \\beta

    LayerNorm(x)=γσ+ϵxμ+β

    • μ

      \\mu

      μ: x在最后一个维度上的均值

    • σ

      \\sigma

      σ: x在最后一个维度上的标准差

    • γ

      \\gamma

      γ: 可学习的缩放参数(初始化为1)

    • β

      \\beta

      β: 可学习的平移参数(初始化为0)

    • ϵ

      \\epsilon

      ϵ: 防止除零的小数(1e-6)

    10.4 代码实现

    class LayerNorm(nn.Module):
    def __init__(self, features, eps=1e-6):
    """
    features: 归一化的特征维度 = d_model = 512
    eps: 防止除零 = 1e-6

    γ (a2)和β (b2) 是可学习的参数:
    – 初始时γ=1, β=0 → LayerNorm输出 = 归一化后的x
    – 训练过程中模型可以学习调整γ和β
    – 这保持了模型的灵活性(如果归一化不合适,可以"撤销")
    """
    super(LayerNorm, self).__init__()
    # γ (gamma): 缩放参数
    # torch.ones(features): 初始化为全1 → 不改变缩放
    self.a2 = nn.Parameter(torch.ones(features))

    # β (beta): 平移参数
    # torch.zeros(features): 初始化为全0 → 不改变位置
    self.b2 = nn.Parameter(torch.zeros(features))

    self.eps = eps

    def forward(self, x):
    """
    x: [batch, seq_len, d_model] 例如 [2, 4, 512]

    归一化步骤:
    1. 计算均值: 在最后一个维度(512维)上求平均
    2. 计算标准差: 在最后一个维度(512维)上求标准差
    3. 归一化: (x – 均值) / (标准差 + eps)
    4. 缩放和平移: γ * 归一化结果 + β

    返回: [batch, seq_len, d_model]
    """
    # Step 1: 计算均值 [2, 4, 1]
    # keepdim=True: 保持维度以便广播
    mean = x.mean(1, keepdim=True)

    # Step 2: 计算标准差 [2, 4, 1]
    std = x.std(1, keepdim=True)

    # Step 3 & 4: 归一化 + 缩放平移
    # (x – mean) / (std + eps): [2, 4, 512]
    # * self.a2: 按元素乘以γ
    # + self.b2: 按元素加上β
    y = self.a2 * (x mean) / (std + self.eps) + self.b2
    return y

    10.5 残差连接——梯度的高速公路

    残差连接(Residual Connection)的思想很简单:

    o

    u

    t

    p

    u

    t

    =

    x

    +

    F

    (

    x

    )

    output = x + F(x)

    output=x+F(x)

    其中x是输入,F(x)是某层(如Self-Attention或FFN)的输出。

    没有残差连接的网络:
    输入 → Layer1 → Layer2 → … → LayerN → 输出
    梯度需要穿过N层才能到达浅层,每经过一层衰减一次

    问题: 深层网络的浅层很难训练

    有残差连接的网络:
    输入 → [Layer1 → +] → [Layer2 → +] → … → 输出
    ↑_____┘ ↑_____┘
    残差连接 残差连接

    梯度有两条路径:
    路径1: 经过LayerN → LayerN-1 → … → Layer1 (长路径)
    路径2: 直接跳过Layer → 前一层 (短路径,梯度不衰减)

    即使F(x)学得很差(F(x)≈0),x也能直接通过
    这保证了"最差情况下也能维持原有的信息"

    残差连接需要输入输出形状一致——这也是为什么Transformer中所有sub-layer都保持输出形状为[batch, seq, d_model]。

    10.6 Post-Norm vs Pre-Norm

    Transformer原始论文使用的是Post-Norm,但现代实现通常用Pre-Norm:

    class SublayerConnection(nn.Module):
    """
    子层连接 = 残差 + LayerNorm

    两种范式:

    Post-Norm (原始论文):
    x → Sublayer → Dropout → + → LayerNorm → output
    | ↑
    └──── x (残差) ───────┘
    即: output = LayerNorm(x + Dropout(Sublayer(x)))

    特点:
    – LayerNorm在最后
    – 训练早期可能需要warmup
    – 梯度更稳定(归一化在加法之后)

    Pre-Norm (现代变体):
    x → LayerNorm → Sublayer → Dropout → + → output
    | ↑
    └────────── x (残差) ────────────┘
    即: output = x + Dropout(Sublayer(LayerNorm(x)))

    特点:
    – LayerNorm在最前
    – 训练更稳定,通常不需要warmup
    – 是现代实现(如GPT)的默认选择
    """
    def __init__(self, size, dropout=0.1):
    super(SublayerConnection, self).__init__()
    self.norm = LayerNorm(size)
    self.dropout = nn.Dropout(p=dropout)

    def forward(self, x, sublayer):
    """
    x: 输入张量
    sublayer: 一个可调用对象 (函数/lambda)

    使用Pre-Norm:
    1. 先对x做LayerNorm
    2. 通过sublayer处理
    3. Dropout正则化
    4. 与原始x相加 (残差连接)
    """
    # sublayer(self.norm(x)):
    # LayerNorm(x) → Sublayer → 结果
    # self.dropout(…):
    # 在结果上做dropout
    # x + …:
    # 残差连接: 原始输入 + 处理后的结果
    return x + self.dropout(sublayer(self.norm(x)))

    10.7 残差连接和LayerNorm的协同工作

    在Transformer的一个Encoder Layer中:

    x [2,4,512] ─────────────────────────────┐
    │ │
    ├─→ LayerNorm → Self-Attention → Dropout┤
    │ ├─→ x + attn_output
    └───────────────────────────────────────┘

    x' [2,4,512] ──────────────────────────┐ │
    │ │ │
    ├─→ LayerNorm → FeedForward → Dropout ─┤─┘
    │ │ 残差连接2
    └───────────────────────────────────────┘

    output [2,4,512]

    10.8 Normalization自测问题

  • BatchNorm和LayerNorm的最大区别是什么?为什么NLP用LayerNorm?
  • γ和β参数的作用是什么?为什么不能省略?
  • 残差连接解决什么问题?可以没有残差连接吗?
  • Pre-Norm和Post-Norm有什么区别?

  • 十一、Encoder与Decoder的完整实现

    11.1 Encoder Layer —— 编码器的基本构建块

    class EncoderLayer(nn.Module):
    """
    一个Encoder层包含两个子层:
    1. Multi-Head Self-Attention
    2. Position-wise Feed Forward

    每个子层都有残差连接和LayerNorm
    """
    def __init__(self, size, self_attn, feed_forward, dropout):
    """
    size: d_model = 512
    self_attn: MultiHeadedAttention 实例
    feed_forward: PositionwiseFeedForward 实例
    dropout: 0.1
    """

    super(EncoderLayer, self).__init__()
    self.self_attn = self_attn
    self.feed_forward = feed_forward

    # 2个SublayerConnection (每个子层一个)
    # clones: 创建2个独立的SublayerConnection副本
    self.sublayer = clones(SublayerConnection(size, dropout), 2)
    self.size = size

    def forward(self, x, mask):
    """
    x: [batch, seq_len, d_model] Encoder输入
    mask: Padding mask

    处理流程:
    1. Self-Attention子层:
    x → SublayerConnection → Self-Attention(x, x, x, mask) → 输出
    注意: Q=K=V=x (所以叫Self-Attention)

    2. Feed Forward子层:
    上一步输出 → SublayerConnection → Feed Forward → 输出
    """
    # ===== SubLayer 1: Self-Attention + 残差 + LayerNorm =====
    # lambda x: self.self_attn(x, x, x, mask)
    # 表示: 对x做Self-Attention,Q=K=V=x
    # self.sublayer[0](x, lambda x: …)
    # 内部会执行: LayerNorm(x) → Self-Attention → Dropout → + x(残差)
    x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))

    # ===== SubLayer 2: Feed Forward + 残差 + LayerNorm =====
    x = self.sublayer[1](x, self.feed_forward)

    return x

    11.2 Decoder Layer —— 多了一个Cross-Attention

    class DecoderLayer(nn.Module):
    """
    一个Decoder层包含三个子层:
    1. Masked Multi-Head Self-Attention (看自身,不能看未来)
    2. Multi-Head Cross-Attention (Q来自Decoder, K,V来自Encoder)
    3. Position-wise Feed Forward

    总共有3个SublayerConnection
    """
    def __init__(self, size, self_attn, src_attn, feed_forward, dropout):
    """
    size: d_model = 512
    self_attn: Self-Attention (会用Sequence Mask)
    src_attn: Cross-Attention (Q=Decoder, K=V=Encoder memory)
    feed_forward: PositionwiseFeedForward
    """

    super(DecoderLayer, self).__init__()
    self.size = size
    self.self_attn = self_attn # Masked Self-Attention
    self.src_attn = src_attn # Cross-Attention
    self.feed_forward = feed_forward

    # 3个SublayerConnection
    self.sublayer = clones(SublayerConnection(size, dropout), 3)

    def forward(self, x, memory, source_mask, target_mask):
    """
    x: [batch, tgt_len, d_model] Decoder输入
    memory: [batch, src_len, d_model] Encoder的输出
    source_mask: 源语言的padding mask
    target_mask: 目标语言的sequence mask + padding mask

    处理流程:
    1. Masked Self-Attention:
    x → SublayerConnection → Self-Attn(x,x,x, target_mask) → 输出
    target_mask 包含了sequence mask (防止看到未来)

    2. Cross-Attention:
    上一步输出 → SublayerConnection →
    Cross-Attn(Q=上一步输出, K=memory, V=memory, source_mask) → 输出
    这是Decoder和Encoder"对话"的地方

    3. Feed Forward:
    上一步输出 → SublayerConnection → FFN → 输出
    """
    # ===== SubLayer 1: Masked Self-Attention =====
    # 使用 target_mask: 包含sequence mask
    # 确保位置i不能看到位置i+1,i+2,…的信息
    m = memory
    x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, target_mask))

    # ===== SubLayer 2: Cross-Attention =====
    # Q = x (Decoder的当前表示: "我要生成什么?")
    # K = m (Encoder的输出: "源句子有什么信息?")
    # V = m (Encoder的输出: "具体内容是什么?")
    # 使用 source_mask: 只mask源句子的padding部分
    x = self.sublayer[1](x, lambda x: self.src_attn(x, m, m, source_mask))

    # ===== SubLayer 3: Feed Forward =====
    x = self.sublayer[2](x, self.feed_forward)

    return x

    11.3 完整Encoder——堆叠6层

    class Encoder(nn.Module):
    """
    完整的Encoder = N个EncoderLayer堆叠 + 最后的LayerNorm
    """

    def __init__(self, layer, N):
    """
    layer: EncoderLayer实例
    N: 层数 = 6
    """

    super(Encoder, self).__init__()
    # clones: 创建N个独立的EncoderLayer副本(深层拷贝)
    # 每一层有自己的参数,互相独立
    self.layers = clones(layer, N)

    # 最后的LayerNorm (对所有层的输出做一次归一化)
    self.norm = LayerNorm(layer.size)

    def forward(self, x, mask):
    """
    x: [batch, src_len, d_model] 经过Embedding+Positional的输入
    mask: Padding mask

    数据依次经过6个EncoderLayer:
    输入 → Layer1 → Layer2 → Layer3 → Layer4 → Layer5 → Layer6 → LayerNorm → 输出
    """
    # 逐层传递
    for layer in self.layers:
    x = layer(x, mask)

    # 最后的归一化
    return self.norm(x)

    11.4 完整Decoder——堆叠6层

    class Decoder(nn.Module):
    """
    完整的Decoder = N个DecoderLayer堆叠 + 最后的LayerNorm
    """

    def __init__(self, layer, N):
    super(Decoder, self).__init__()
    self.layers = clones(layer, N)
    self.norm = LayerNorm(layer.size)

    def forward(self, x, memory, source_mask, target_mask):
    """
    x: [batch, tgt_len, d_model] Decoder输入
    memory: [batch, src_len, d_model] Encoder输出
    source_mask: 源语言padding mask
    target_mask: 目标语言mask (sequence + padding)
    """

    for layer in self.layers:
    x = layer(x, memory, source_mask, target_mask)
    return self.norm(x)

    11.5 Encoder/Decoder自测问题

  • EncoderLayer和DecoderLayer各自有几个子层?子层的类型是什么?
  • Decoder的Cross-Attention中,Q、K、V分别来自哪里?
  • 为什么Encoder的Self-Attention只需要padding mask,而Decoder的还需要sequence mask?
  • 6层Encoder之间是什么关系?每一层的输入形状是什么?

  • 十二、Generator与完整模型组装

    12.1 Generator —— 从向量到词

    class Generator(nn.Module):
    """
    将Decoder的输出(512维向量)映射为词汇概率分布
    """

    def __init__(self, d_model, vocab_size):
    """
    d_model: 512
    vocab_size: 目标语言词汇表大小 (例如 30000)
    """

    super(Generator, self).__init__()
    # Linear(512 → vocab_size): 将每个位置的向量映射到词汇空间
    # 参数量: 512 × vocab_size + vocab_size
    # 例如 vocab=30000: 约 15,360,000 参数
    self.project = nn.Linear(d_model, vocab_size)

    def forward(self, x):
    """
    x: [batch, seq_len, 512]

    返回: [batch, seq_len, vocab_size]
    每个位置的vocab_size个值是该位置每个词的对数概率
    """
    # Step 1: 线性投影 […, 512] → […, vocab_size]
    x = self.project(x)

    # Step 2: LogSoftmax
    # dim=-1: 在词汇维度上做Softmax
    # 用log_softmax而非softmax的原因是配合NLLLoss
    x = F.log_softmax(x, dim=1)
    return x

    12.2 EncoderDecoder —— 完整模型

    class EncoderDecoder(nn.Module):
    """
    将Encoder、Decoder、Embedding、Generator组装成完整模型
    """

    def __init__(self, encoder, decoder, source_embed, target_embed, generator):
    """
    encoder: Encoder实例
    decoder: Decoder实例
    source_embed: 源语言Embedding + PositionalEncoding
    target_embed: 目标语言Embedding + PositionalEncoding
    generator: Generator实例
    """

    super(EncoderDecoder, self).__init__()
    self.encoder = encoder
    self.decoder = decoder
    self.source_embed = source_embed
    self.target_embed = target_embed
    self.generator = generator

    def forward(self, source, target, source_mask, target_mask):
    """
    完整的前向传播

    参数:
    – source: [batch, src_len] 源语言词索引
    – target: [batch, tgt_len] 目标语言词索引
    – source_mask: Padding mask
    – target_mask: Sequence + Padding mask

    返回: [batch, tgt_len, vocab_size] 目标词的概率分布

    流程:
    1. Encoder: source → Embedding+Positional → Encoder → memory
    2. Decoder: target → Embedding+Positional → Decoder(memory) → output
    3. Generator: output → Linear+LogSoftmax → 概率分布
    """
    # ===== 1. Encode =====
    # source: 例如 [2, 5] (5是源句子长度)
    # self.source_embed(source): [2, 5, 512]
    # self.encoder(…): [2, 5, 512]
    # memory 就是"上下文表示",传给Decoder的Cross-Attention
    memory = self.encoder(self.source_embed(source), source_mask)

    # ===== 2. Decode =====
    # target: 例如 [2, 4] (4是目标句子长度)
    # self.target_embed(target): [2, 4, 512]
    # self.decoder(…): [2, 4, 512]
    # 每层的Cross-Attention都会用Q(Decoder)去查K,V(memory)
    x = self.decoder(self.target_embed(target), memory, source_mask, target_mask)

    # ===== 3. Generate =====
    # [2, 4, 512] → [2, 4, vocab_size]
    # 每个位置得到词汇表大小的概率分布
    x = self.generator(x)
    return x

    12.3 make_model —— 工厂函数

    def make_model(source_vocab, target_vocab, N=6, d_model=512,
    d_ff=2048, head=8, dropout=0.1):
    """
    工厂函数: 创建完整的Transformer模型

    参数:
    – source_vocab: 源语言词汇表大小
    – target_vocab: 目标语言词汇表大小
    – N: Encoder和Decoder的层数 = 6
    – d_model: 模型维度 = 512
    – d_ff: FFN内部维度 = 2048
    – head: 注意力头数 = 8
    – dropout: 0.1

    返回: 完整的EncoderDecoder模型
    """
    # c = copy.deepcopy: 用于创建独立副本
    c = copy.deepcopy

    # ===== 创建组件 =====
    # Attention: 用于Encoder的Self-Attention和Decoder的Cross-Attention
    attn = MultiHeadedAttention(head, d_model, dropout)

    # Feed Forward
    ff = PositionwiseFeedForward(d_model, d_ff, dropout)

    # Positional Encoding
    position = PositionalEncoding(d_model, dropout)

    # ===== 组装模型 =====
    # 注意: Decoder需要两个独立的Attention实例
    # – 一个用于Masked Self-Attention (c(attn))
    # – 一个用于Cross-Attention (c(attn))
    # 两者参数不共享!

    model = EncoderDecoder(
    # Encoder: 6层, 每层有Self-Attention + FFN
    Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N),

    # Decoder: 6层, 每层有Masked Self-Attn + Cross-Attn + FFN
    # 注意: 这里传入了两个c(attn),分别是:
    # – 第1个c(attn): Decoder的Masked Self-Attention
    # – 第2个c(attn): Decoder的Cross-Attention
    Decoder(DecoderLayer(d_model, c(attn), c(attn), c(ff), dropout), N),

    # Source Embedding: Embeddings + PositionalEncoding
    nn.Sequential(Embeddings(d_model, source_vocab), c(position)),

    # Target Embedding: Embeddings + PositionalEncoding
    nn.Sequential(Embeddings(d_model, target_vocab), c(position)),

    # Generator: Linear + LogSoftmax
    Generator(d_model, target_vocab)
    )

    # ===== 参数初始化 =====
    # Xavier/Glorot初始化: 帮助梯度更好地流动
    for p in model.parameters():
    if p.dim() > 1:
    nn.init.xavier_uniform_(p)

    return model

    12.4 完整的推理流程

    训练时的推理(已在EncoderDecoder.forward中覆盖),但实际使用时的自回归生成(逐词生成)还需要额外的代码:

    预测 "I love you" → 法语

    Step 1: Encoder处理源句子
    "I" "love" "you" → Encoder → memory

    Step 2: 自回归生成
    Step 2.1: [SOS] → Decoder(memory) → [256维] → Softmax → "Je"
    Step 2.2: [SOS, Je] → Decoder(memory) → [256维] → Softmax → "t'aime"
    Step 2.3: [SOS, Je, t'aime] → Decoder(memory) → … → EOS

    t'aime 注意: 生成"Je"时看不到"t'aime" (Sequence Mask的作用)

    12.5 模型组装自测问题

  • make_model 中为什么对Attention使用 c(attn)(深拷贝)而不是直接传递同一个实例?
  • Generator为什么使用LogSoftmax而不是Softmax?
  • EncoderDecoder的forward方法中,target参数是什么?(提示:训练时 vs 推理时)
  • 如果source_vocab=5000, target_vocab=6000, d_model=512,模型的embedding层总参数量是多少?

  • 十三、补充内容

    13.1 Transformer的核心优势总结

    优势说明实际影响
    并行计算 不依赖时序,所有位置同时处理 训练时间从周减少到天
    长距离依赖 任意位置O(1)路径连接 长句翻译质量大幅提升
    可解释性 注意力权重可视化 可以观察模型"在看什么"
    通用性 不限于NLP 延伸到CV(ViT)、语音、多模态

    13.2 Transformer vs RNN/LSTM 深度对比

    对比维度RNN/LSTMTransformer
    并行计算 否(串行,时间步依赖) 是(所有位置同时计算)
    长距依赖 难(信息需逐词传递,有遗忘) 易(直接连接,路径长度O(1))
    计算复杂度 O(n·d^2) O(n^2·d)
    空间复杂度 O(n·d) O(n^2)(注意力矩阵)
    位置信息 天然编码(时序结构) 需Positional Encoding
    训练速度 慢(GPU利用率低) 快(GPU友好,矩阵运算密集)
    序列长度限制 无硬限制(但梯度问题) O(n^2)内存,长序列受限
    小数据表现 尚可 需更多数据(缺少归纳偏置)

    复杂度解读:

    • RNN: O(n·d^2),n=序列长度,d=隐藏维度。线性依赖于n,但串行执行。
    • Transformer: O(n^2·d),平方依赖于n。n长时内存和计算量大,但可以在一轮矩阵乘法中完成。
    • 对于n=100的词序列,n2=10000,d=512,n2·d ≈ 5M,GPU轻松处理。
    • 对于n=10000的序列(如整本书),n2=100M,d=512,n2·d ≈ 51B,普通GPU困难。这也是为什么长文本处理需要特殊技术(如Longformer, BigBird等稀疏注意力)。

    13.3 自注意力的注意力矩阵解读

    输入: "I love China ." (4个词)

    4×4的注意力矩阵(某个头的可视化):

    I love China .
    I [0.5, 0.3, 0.1, 0.1] ← "I"最关注自己(0.5)
    love [0.2, 0.5, 0.2, 0.1] ← "love"关注自己和"I"
    China[0.1, 0.3, 0.5, 0.1] ← "China"关注自己和"love"
    . [0.2, 0.2, 0.2, 0.4] ← "."关注所有词,偏向自己

    每行表示该词(Query)对所有词(Key)的关注程度
    行和为1 (Softmax归一化)

    典型模式:
    – 对角线值较高: 每个词关注自己 (保留自身信息)
    – 相邻位置值较高: 关注邻近词 (局部语法关系)
    – 长距离也有非零值: 捕捉远距离依赖

    13.4 Transformer家族发展史

    Transformer (2017)
    "Attention is All You Need"
    |
    ┌────────────────┼────────────────┐
    | | |
    仅用Encoder 仅用Decoder Encoder-Decoder
    | | |
    BERT (2018) GPT (2018) T5 (2019)
    "Bidirectional" "Auto-regressive" "Text-to-Text"
    预训练: MLM 预训练: LM 统一框架
    | |
    ┌─────────┼──────┐ ┌───┼──────────┐
    | | | | | |
    RoBERTa ALBERT … GPT-2 GPT-3 GPT-4
    (2019) (2019) (2019)(2020)(2023)
    |
    ┌─────────┼──────────┐
    | | |
    LLaMA PaLM Claude
    (Meta,2023)(Google) (Anthropic)

    跨领域发展:
    ViT (2020): Vision Transformer → 图像分类
    DETR (2020): Detection Transformer → 目标检测
    Whisper (2022): 语音识别
    CLIP (2021): 图文多模态

    13.5 训练技巧与注意事项

    学习率调度(Learning Rate Schedule):

    Transformer原始论文使用了一个特殊的"warmup + decay"学习率策略:

    lr = d_model^(-0.5) * min(step_num^(-0.5), step_num * warmup_steps^(-1.5))

    warmup阶段(前4000步): 学习率线性增加
    decay阶段(4000步之后): 学习率逐步减小

    为什么需要warmup?
    – 训练初期,模型参数是随机的,梯度的方向和大小都不稳定
    – 使用大的学习率可能导致训练崩溃
    – Warmup提供了一个"热身阶段",让优化器先找到正确的方向

    Label Smoothing(标签平滑):

    训练时不用one-hot标签,而是给正确词0.9的概率,其余0.1均匀分配给其他词。这样可以防止模型过度自信,提高泛化能力。

    Dropout的位置:

    • 每个子层的输出处
    • Embedding + Positional Encoding 的输出处
    • Attention权重上

    13.6 常见误解澄清

  • “Transformer完全不需要循环结构” — 对,但它仍然使用了Positional Encoding来编码位置信息。Attention本身位置无关,所以需要额外注入位置信号。

  • “Multi-Head Attention只是重复计算8次” — 不对。每个头有独立的Q、K、V投影矩阵,它们把输入投影到8个不同的子空间。8个头关注的是不同的语义关系,不是简单重复。

  • “Transformer的参数量很大” — Transformer Base只有约65M参数(对比:GPT-3有175B)。核心模块都很精简,大部分参数在Embedding和Generator中。

  • “Self-Attention就是矩阵乘法” — 核心计算确实是矩阵乘法,但经过Softmax非线性处理,以及Multi-Head的多视角机制,整体效果远不止是一个线性变换。


  • 十四、自测问题

    基础理解

  • 用自己的话解释:为什么Transformer不需要RNN就能处理序列?
  • Multi-Head Attention中"多头"的含义是什么?每个头在做什么?
  • Positional Encoding为什么用sin/cos而不用可学习的embedding?
  • 什么是Self-Attention?它和Cross-Attention有什么不同?
  • 架构理解

  • Transformer中Encoder和Decoder各有多少层?每层包含哪些子层?
  • Decoder的Cross-Attention中,Q、K、V分别来自哪里?
  • Sequence Mask的作用是什么?为什么只在Decoder中使用?
  • 残差连接和LayerNorm在Transformer中各起什么作用?
  • 计算细节

  • 为什么Scaled Dot-Product Attention要除以√d_k?
  • d_model=512, head=8时,每个头的d_k和d_v分别是多少?
  • Feed Forward为什么使用512→2048→512的扩张收缩结构?
  • view 和 transpose 的区别是什么?transpose后为什么需要.contiguous()?
  • 实践理解

  • 如果训练时batch_size=2, seq_len=10, d_model=512, head=8,Multi-Head Attention中各个张量的形状分别是多少?
  • BERT和GPT各使用了Transformer的哪一部分?为什么?
  • Transformer的O(n^2)复杂度在什么情况下会成为瓶颈?如何解决?

  • 学习建议: Transformer是现代NLP的基石。建议你:

  • 先理解整体数据流(输入→Embedding→Positional→Encoder→Decoder→输出)
  • 再深入理解Self-Attention的计算过程(Q·K^T/√dk → Softmax → ·V)
  • 然后理解Multi-Head如何将多个Self-Attention组合
  • 最后理解完整的Layer组装
  • 画图是非常有效的学习方式——试着画出每一步的张量形状变化!

    赞(0)
    未经允许不得转载:171主机测评 » Transformer架构和实现
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址