Part9.第15章:Transformer(下)

整体架构

在这里插入图片描述
左半部分是编码器Encoder部分,右边部分是解码器Decoder部分。

编码器

在这里插入图片描述

全连接模块实现

class FeedForwardBlock(nn.Module):

    def __init__(self, d_model: int, d_ff: int, dropout: float) -> None:
        super().__init__()
        self.linear_1 = nn.Linear(d_model, d_ff)
        self.dropout = nn.Dropout(dropout)
        self.linear_2 = nn.Linear(d_ff, d_model)

    def forward(self, x):
        # (batch, seq_len, d_model) --> (batch, seq_len, d_ff) --> (batch, seq_len, d_model)
        return self.linear_2(self.dropout(torch.relu(self.linear_1(x))))

全连接模块里定义了两个层,第一个层将每个token embedding的维度从512扩展到2048,然后应用了ReLU激活,接着进行dropout。第二层将每个token embedding的维度从2048重新降维到512。
整个前馈网络可以表示为:
FFN(x)=W2⋅Dropout(ReLU(W1⋅x+b1))+b2FFN(x)=W_2⋅Dropout(ReLU(W_1⋅x+b_1))+b_2FFN(x)=W2Dropout(ReLU(W1x+b1))+b2

  • 维度扩展-压缩模式
    d_model → d_ff → d_model
    先升维(增加表达能力)
    再降维(保持维度一致)

  • 位置独立性
    对序列中的每个位置独立应用相同的前馈网络
    每个token的处理不依赖其他位置

Add & Norm 模块实现

class ResidualConnection(nn.Module):

    def __init__(self, features: int, dropout: float) -> None:
        super().__init__()
        self.dropout = nn.Dropout(dropout)
        self.norm = LayerNormalization(features)

    def forward(self, x, sublayer):
        return x + self.dropout(sublayer(self.norm(x)))
  • 参数
    features: 特征维度,通常是 d_model
    dropout: dropout概率
  • self.dropout = nn.Dropout(dropout)
    用于对子层输出进行正则化
  • self.norm = LayerNormalization(features)
    归一化在残差连接中起到稳定训练的作用
  • def forward(self, x, sublayer)
    x: 输入张量
    sublayer: 一个子层函数(如自注意力或前馈网络)

整个残差连接可以表示为:
ResidualConnection(x)=x+Dropout(Sublayer(LayerNorm(x)))ResidualConnection(x)=x+Dropout(Sublayer(LayerNorm(x)))ResidualConnection(x)=x+Dropout(Sublayer(LayerNorm(x)))

设计特点

  1. 计算顺序:Pre-LN 架构
# Pre-LN: LayerNorm在子层之前
x → LayerNorm → Sublayer → Dropout → + x
# 对比 Post-LN: LayerNorm在残差连接之后
x → Sublayer → Dropout → + x → LayerNorm

Pre-LN(这里实现的)通常训练更稳定
Post-LN 是原始Transformer论文中的设计

  1. 残差连接的重要性
残差连接的核心思想
output = input + transformation(input)
  • 解决梯度消失: 梯度可以直接通过加法路径反向传播
  • 恒等映射: 当子层学习效果不好时,网络可以退化为恒等映射
  • 信息保留: 原始信息得到保留,新信息在残差上学习
  1. Dropout的位置
    在残差连接之前应用dropout
    防止子层输出过拟合

编码器实现代码

class EncoderBlock(nn.Module):

    def __init__(self, features: int, self_attention_block: MultiHeadAttentionBlock,
                 feed_forward_block: FeedForwardBlock, dropout: float) -> None:
        super().__init__()
        # 定义多头自注意力模块
        self.self_attention_block = self_attention_block
        # 定义全连接模块
        self.feed_forward_block = feed_forward_block
        # 定义两个Add & Norm模块
        self.residual_connections = nn.ModuleList([ResidualConnection(features, dropout) for _ in range(2)])

    def forward(self, x, src_mask):
        # 第一个残差连接,跳过多头注意力模块
        x = self.residual_connections[0](x, lambda x: self.self_attention_block(x, x, x, src_mask))
        # 第二个残差连接,跳过全连接模块
        x = self.residual_connections[1](x, self.feed_forward_block)
        return x


class Encoder(nn.Module):

    def __init__(self, features: int, layers: nn.ModuleList) -> None:
        super().__init__()
        # 传入的6个EncoderBlock
        self.layers = layers
        self.norm = LayerNormalization(features)

    def forward(self, x, mask):
        # 依次调用6个EncoderBlock
        for layer in self.layers:
            x = layer(x, mask)
        # 输出前进行Layer Norm
        return self.norm(x)

【代码解释】

  • 【1】关键行: 创建包含2个残差连接的模块列表
    第一个用于自注意力层
    第二个用于前馈网络层
self.residual_connections = nn.ModuleList([ResidualConnection(features, dropout) for _ in range(2)])
  • 【2】def forward(self, x, src_mask):
    x: 输入张量,形状为 [batch_size, seq_len, d_model]
    src_mask: 源序列掩码,用于防止注意力机制关注填充位置
  • 【3】x = self.residual_connections[0](x, lambda x: self.self_attention_block(x, x, x, src_mask))
    第一个残差连接:多头自注意力
    详细分解:
    self.residual_connections[0]: 第一个残差连接模块
    lambda x: self.self_attention_block(x, x, x, src_mask):
    这是一个匿名函数,将作为子层传递给残差连接
    self.self_attention_block(x, x, x, src_mask):
    三个 x 分别对应:query, key, value(自注意力)
    src_mask: 防止关注到填充位置
    计算流程:
原始输入x → LayerNorm → 自注意力 → Dropout → + 原始输入x
  • 【4】x = self.residual_connections[1](x, self.feed_forward_block)
    第二个残差连接:前馈网络
    计算流程:
上一步输出 → LayerNorm → 前馈网络 → Dropout → + 上一步输出
  • 【5】def forward(self, x, mask):
    参数:
    x: 输入张量(已加上位置编码)
    mask: 源序列掩码

一个编码器块的计算可以表示为:
在这里插入图片描述
其中 lll 表示第 lll 个编码器块。

编码器总结

编码器的作用是根据上下文,不断更新输入序列中每个token的Embedding让最终输出序列token的embedding有最佳的语义。因为自注意力机制,可以一次性输入整个序列,序列内多个token可以并行处理,大大加快了处理速度。其中使用了位置编码增加了token的位置信息。通过多头注意力让token可以根据上下文信息修改自身embedding。通过全连接模块让每个token更新自身embedding。通过残差连接和LayerNorm让深度网络更容易训练。

解码器

以英文翻译中文任务为例,解码器的作用就是参考编码器的输出,以及自身已经翻译的内容,生成下一个中文token。
在这里插入图片描述

解码器的输入

编码器输出 编码器输出的是英文序列里每个token的embedding。它的维度为512。经过多层编码器的自注意力机制,每个token的embedding都已经根据上下文,计算出恰当的语义信息。它们将作为解码器输出的重要参考。

已经翻译出来的token序列 Transformer的编码器可以一次性输入完整的英文token序列,但在模型进行实际翻译时,需要解码器逐个生成对应的中文token序列。<bos><bos><bos>为解码器的初始输入,代表序列开始。在上图例子里,我们希望模型输入<bos><bos><bos>,能输出“我”这个token。然后将“<bos>,我”“<bos>, 我”<bos>,这两个token 序列作为输入,希望模型能输出“爱”这个token。依次类推,直到模型输出<eos><eos><eos>这个token。当然在这个过程中的每一步解码器也同时参考了编码器给出的英文token 序列的embedding。

对于Transformer模型在推理时,解码器确实如上述过程所述,是逐个生成中文token的。但是在训练时,因为我们已经知道英文对应的中文token序列,所以我们可以通过一种叫做带掩码的多头注意力机制(Masked Multi-Head Attention,MMHA)来实现并行化训练。
对于上图中中文token序列:<bos><bos><bos> | 我 | 爱 | 吃 | 苹果,一共有5个token,我们可以构造一个下三角矩阵:
在这里插入图片描述
Mask矩阵每行表示当前token可以看到的token,1代表可以看到,0代表看不到。
比如第一行只有第一个位置为1,代表第一个token <bos><bos><bos> 只能看到第一个token,也就是自己本身。

第二行可以看到前两个token。

Transformer里只有注意力计算时是跨token,根据上下文更新自身embedding的。在进行注意计算时,不光传入Q,K,V矩阵,还需要传入这个Mask矩阵。让每个token在注意里计算时,根据mask矩阵,只关注自己以及前边的token。这样在一次训练里就可以对整个中文序列一次性进行训练了。

在实际代码实现时,先利用Q、K矩阵进行注意力计算,在softmax()前,把Mask矩阵为0的位置的注意力结果填充成一个非常大的负值,这样经过softmax()后,这些位置的值就都为0。在接下来计算当前token的特征值时,序列中Mask为0的token的权重就为0,代码如下:

def attention(query, key, value, mask, dropout: nn.Dropout):
    d_k = query.shape[-1]
    # (batch, h, seq_len, d_k) --> (batch, h, seq_len, seq_len)
    attention_scores = (query @ key.transpose(-2, -1)) / math.sqrt(d_k)
    if mask is not None:
        # 给mask为0的位置填入一个很大的负值
        attention_scores.masked_fill_(mask == 0, -1e9)
    # (batch, h, seq_len, seq_len)
    attention_scores = attention_scores.softmax(dim=-1)
    if dropout is not None:
        attention_scores = dropout(attention_scores)
    # (batch, h, seq_len, seq_len) --> (batch, h, seq_len, d_k)
    return (attention_scores @ value), attention_score

代码解释*

  • 【1】def attention(query, key, value, mask, dropout: nn.Dropout):
    query: 查询向量,形状为 (batch, num_heads, seq_len_q, d_k)
    key: 键向量,形状为 (batch, num_heads, seq_len_kv, d_k)
    value: 值向量,形状为 (batch, num_heads, seq_len_kv, d_v)
    mask: 注意力掩码,用于屏蔽某些位置
    dropout: Dropout层实例
  • 【2】注意力计算核心公式
# (batch, h, seq_len, d_k) --> (batch, h, seq_len, seq_len)
attention_scores = (query @ key.transpose(-2, -1)) / math.sqrt(d_k)

【2.1】key.transpose(-2, -1): 转置键矩阵的最后两个维度
形状: (batch, num_heads, seq_len_kv, d_k) → (batch, num_heads, d_k, seq_len_kv)
【2.2】矩阵乘法 query @ key.transpose(…):

查询: [batch, num_heads, seq_len_q, d_k]
键转置: [batch, num_heads, d_k, seq_len_kv]
结果: [batch, num_heads, seq_len_q, seq_len_kv]

计算查询和键的点积
表示每个查询位置与每个键位置的相关性
【2.3】缩放因子 / math.sqrt(d_k):
数学公式: Attention(Q,K,V)=softmax(QKTdk)V\text{Attention}(Q,K,V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)VAttention(Q,K,V)=softmax(dkQKT)V
为什么需要缩放?防止点积过大导致softmax梯度消失

  • 【3】掩码处理
if mask is not None:
    # 给mask为0的位置填入一个很大的负值
    attention_scores.masked_fill_(mask == 0, -1e9)

将掩码为0的位置填充为负无穷(这里用-1e9近似)
在softmax后,这些位置的概率会趋近于0。

整个过程可以用公式表示:
在这里插入图片描述
Q,K,VQ, K, VQ,K,V: 查询、键、值矩阵
dkd_kdk: 键向量的维度
MMM: 掩码矩阵(负无穷表示需要屏蔽的位置)

完整流程图示如下:

查询向量Q        键向量K
    ↓             ↓
    ┌─────────────┐
    │ Q·K^T / √d_k │ ← 计算点积并缩放
    └─────────────┘
          ↓
    ┌─────────────┐
    │  掩码处理   │ ← 应用掩码(如需要)
    └─────────────┘
          ↓
    ┌─────────────┐
    │   softmax   │ ← 转换为概率分布
    └─────────────┘
          ↓
    ┌─────────────┐
    │   dropout   │ ← 随机丢弃(训练时)
    └─────────────┘
          ↓
    ┌─────────────┐
    │   · V       │ ← 加权求和值向量
    └─────────────┘
          ↓
       输出

Mask机制只能在训练时一次性对解码器所有位置的token进行训练。但是在模型训练好进行生成时,因为事先并不知道答案,只能一步生成一个新的token,然后拼接到解码器的输入,再生成下一个。

交叉注意力 Cross-Attention

观察Transformer模型架构图的Decoder Block部分,它也是6个,每个Decoder Block有2个注意力模块+1个全连接模块
Corss-Attention是非常重要的一步,它是解码器从编码器获取信息的唯一途径。从模型架构图中可以看到,交叉注意力模块的K和V矩阵来自编码器的输出Q矩阵来自解码器部分。所以解码器根据当前翻译的需要提出查询向量q,和所有编码器输出的k进行匹配,计算注意力。最终得到编码器输出的v的注意力加权值。
代码实现:

def forward(self, x, encoder_output, src_mask, tgt_mask):
    # 第一个自注意力模块,Q、K、V都来自自身。
    x = self.residual_connections[0](x, lambda x: self.self_attention_block(x, x, x, tgt_mask))
    # 第二个交叉注意力模块的Q矩阵来自Decoder,K,V矩阵来自Encoder的输出。
    x = self.residual_connections[1](x, lambda x: self.cross_attention_block(x, encoder_output,encoder_output, src_mask))
    # 全连接模块。
    x = self.residual_connections[2](x, self.feed_forward_block)
    return x

代码解释:

  • 【1】def forward(self, x, encoder_output, src_mask, tgt_mask):
    x: 解码器输入(目标序列的嵌入+位置编码)
    encoder_output: 编码器的输出
    src_mask: 源序列掩码(用于编码器-解码器注意力)
    tgt_mask: 目标序列掩码(用于解码器自注意力)
  • 【2】解码器自注意力(掩码自注意力)
x = self.residual_connections[0](x, lambda x: self.self_attention_block(x, x, x, tgt_mask))
# 残差链接结构
# x → LayerNorm → 自注意力 → Dropout → + x
    • self_attention_block: 解码器的自注意力模块
      参数说明:
      第一个 x: query(来自解码器自身)
      第二个 x: key(来自解码器自身)
      第三个 x: value(来自解码器自身)
      tgt_mask: 目标序列掩码
    • tgt_mask 的作用:
      前瞻掩码(Look-Ahead Mask): 防止当前位置关注未来的位置
      填充掩码: 防止关注到填充位置
  • 【3】编码器-解码器注意力(交叉注意力)
x = self.residual_connections[1](x, lambda x: self.cross_attention_block(x, encoder_output, encoder_output, src_mask))
# 残差链接结构
上一步输出 → LayerNorm → 交叉注意力 → Dropout → + 上一步输出
    • lambda x: self.cross_attention_block(x, encoder_output, encoder_output, src_mask)
      cross_attention_block: 编码器-解码器注意力模块
      参数说明:
      第一个 x: query(来自解码器)
      第二个 encoder_output: key(来自编码器)
      第三个 encoder_output: value(来自编码器)
      src_mask: 源序列掩码
    • 为什么Q、K、V不同?
      Query: 来自解码器(目标语言)
      Key & Value: 来自编码器输出(源语言)
      这使得解码器可以"查询"编码器的信息
    • src_mask 的作用:
      防止解码器关注到编码器输出的填充位置
      确保只关注有效的源语言信息
  • 【4】前馈神经网络
x = self.residual_connections[2](x, self.feed_forward_block)
残差连接结构:
上一步输出 → LayerNorm → 前馈网络 → Dropout → + 上一步输出
    • 前馈网络特点:
      与编码器中的前馈网络结构相同
      独立处理每个位置
      提供非线性变换能力

完整流程图示

解码器输入 (x)
       ↓
┌─────────────────────────────────┐
│ 第1个残差连接: 解码器自注意力         │
│ - Query: x                        │
│ - Key: x                          │
│ - Value: x                        │
│ - 掩码: tgt_mask (前瞻+填充掩码)    │
└─────────────────────────────────┘
       ↓
┌─────────────────────────────────┐
│ 第2个残差连接: 编码器-解码器注意力  │
│ - Query: 上一步输出               │
│ - Key: encoder_output           │
│ - Value: encoder_output         │
│ - 掩码: src_mask (填充掩码)       │
└─────────────────────────────────┘
       ↓
┌─────────────────────────────────┐
│ 第3个残差连接: 前馈神经网络        │
│ - 独立处理每个位置                │
│ - 增加非线性表达能力              │
└─────────────────────────────────┘
       ↓
解码器块输出

解码器最终会接一个线性分类头,输入是512,输出维度是字典的大小。线性层的输出再经过softmax()之后,就是字典里每个token作为输出下一个token的概率值。

解码器数学表示

自注意力: 捕捉目标序列的内部依赖关系;
交叉注意力: 建立源语言和目标语言之间的对应关系;
前馈网络: 增加非线性表达能力。
在这里插入图片描述
不同类型注意力的对比:

# 解码器自注意力(掩码自注意力)
self_attention_output = self_attention(
    query=x,     # 来自解码器
    key=x,       # 来自解码器
    value=x,     # 来自解码器
    mask=tgt_mask  # 前瞻掩码+填充掩码
)

# 编码器-解码器注意力(交叉注意力)
cross_attention_output = cross_attention(
    query=x,             # 来自解码器
    key=encoder_output,  # 来自编码器
    value=encoder_output,  # 来自编码器
    mask=src_mask        # 填充掩码
)

自注意力模块:为什么需要前瞻掩码

因果性约束

  • 解码器生成目标序列时,当前位置只能看到当前和之前的token
  • 防止模型在训练时"作弊"——看到未来的信息
  • 这是序列生成任务的核心约束

假设生成句子:“我 喜欢 机器 学习”

步骤1: 输入: [<s>]          -> 输出: "我"
步骤2: 输入: [<s>, "我"]     -> 输出: "喜欢"
步骤3: 输入: [<s>, "我", "喜欢"] -> 输出: "机器"
步骤4: 输入: [<s>, "我", "喜欢", "机器"] -> 输出: "学习"

每个步骤只能看到已生成的部分
如果不使用前瞻掩码会怎样?

# 无掩码的训练(错误!)
输入: [<s>, "我", "喜欢", "机器", "学习"]
输出: "我 喜欢 机器 学习"

模型会在训练时看到整个目标句子!!!
推理时:模型只能看到部分句子 -> 性能严重下降!!!

交叉注意力:为什么只需要填充掩码

信息访问权限

  • 解码器访问编码器输出时,可以访问整个源序列
  • 因为源序列是已知的完整输入(如源语言句子)
  • 没有"因果性"约束,只有"有效性"约束

为什么不需要前瞻掩码?

  • 1.源序列是静态的:训练和推理时,编码器已经处理了完整的源序列
  • 2.全序列信息可用:翻译时,目标词的生成可以依赖于源序列的任何位置
  • 3.对齐需求:目标词可能需要与源序列的任意位置对齐
举例:英译中
源英文: "I love machine learning" (4个词)
目标中文: "我 喜欢 机器 学习" (4个词)

交叉注意力允许:
- "我" 可以关注 "I"
- "喜欢" 可以关注 "love"  
- "机器" 可以关注 "machine"
- "学习" 可以关注 "learning"
- 每个目标词都可以关注整个源序列

两者对比:
在这里插入图片描述
在这里插入图片描述

解码器实现代码

class DecoderBlock(nn.Module):
    def __init__(self, features: int, self_attention_block: MultiHeadAttentionBlock,
                 cross_attention_block: MultiHeadAttentionBlock, feed_forward_block: FeedForwardBlock,
                 dropout: float) -> None:
        super().__init__()
        self.self_attention_block = self_attention_block
        self.cross_attention_block = cross_attention_block
        self.feed_forward_block = feed_forward_block
        self.residual_connections = nn.ModuleList([ResidualConnection(features, dropout) for _ in range(3)])

    def forward(self, x, encoder_output, src_mask, tgt_mask):
        x = self.residual_connections[0](x, lambda x: self.self_attention_block(x, x, x, tgt_mask))
        # 交叉注意力模块的Q矩阵来自Decoder,K,V矩阵来自Encoder的输出
        x = self.residual_connections[1](x, lambda x: self.cross_attention_block(x, encoder_output,encoder_output, src_mask))
        x = self.residual_connections[2](x, self.feed_forward_block)
        return x

class Decoder(nn.Module):
    def __init__(self, features: int, layers: nn.ModuleList) -> None:
        super().__init__()
        self.layers = layers
        self.norm = LayerNormalization(features)

    def forward(self, x, encoder_output, src_mask, tgt_mask):
        for layer in self.layers:
            x = layer(x, encoder_output, src_mask, tgt_mask)
        return self.norm(x)

完整架构图示

解码器输入 (目标序列嵌入 + 位置编码)
       ↓
┌─────────────────────────────────┐
│  Decoder Block 1                │
│  ┌─────────────────────────┐    │
│  │ 1. 掩码自注意力           │    │
│  │    - Q,K,V: 解码器自身    │    │
│  │    - 掩码: tgt_mask      │    │
│  └─────────────────────────┘    │
│  ┌─────────────────────────┐    │
│  │ 2. 交叉注意力            │    │
│  │    - Q: 解码器           │    │
│  │    - K,V: 编码器输出     │    │
│  │    - 掩码: src_mask      │    │
│  └─────────────────────────┘    │
│  ┌─────────────────────────┐    │
│  │ 3. 前馈神经网络          │    │
│  └─────────────────────────┘    │
└─────────────────────────────────┘
       ↓
┌─────────────────────────────────┐
│  Decoder Block 2                │
│  ...                            │
└─────────────────────────────────┘
       ↓
       ...
       ↓
┌─────────────────────────────────┐
│  Decoder Block N                │
│  ...                            │
└─────────────────────────────────┘
       ↓
     层归一化
       ↓
   解码器输出

训练与推理的区别
训练模式:

# 训练时:使用完整目标序列进行教师强制
def train_step(self, src, tgt, src_mask, tgt_mask):
    # 前向传播
    logits = self.forward(src, tgt, src_mask, tgt_mask)
    
    # 计算损失(只计算非填充位置)
    loss = self.compute_loss(logits, tgt)
    return loss

推理模式:

def generate(self, src, src_mask, max_len=50, start_token=1):
    """自回归序列生成"""
    batch_size = src.size(0)
    device = src.device
    
    # 编码源序列
    encoder_output = self.encode(src, src_mask)
    
    # 初始化目标序列(只有开始标记)
    tgt = torch.ones(batch_size, 1, dtype=torch.long, device=device) * start_token
    
    for step in range(max_len):
        # 创建当前步的目标序列掩码
        tgt_mask = create_look_ahead_mask(tgt.size(1)).to(device)
        
        # 解码
        decoder_output = self.decode(tgt, encoder_output, src_mask, tgt_mask)
        
        # 预测下一个词
        logits = self.project(decoder_output[:, -1:, :])  # 只取最后一个位置
        next_token = torch.argmax(logits, dim=-1)
        
        # 添加到序列中
        tgt = torch.cat([tgt, next_token], dim=1)
        
        # 检查是否所有序列都生成了结束标记
        if (next_token == END_TOKEN).all():
            break
    
    return tgt

在这里插入图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值