BERT Transformer 多头注意力机制:从原理到新手实践指南

1次阅读
没有评论

共计 2497 个字符,预计需要花费 7 分钟才能阅读完成。

image.webp

从 RNN 到 Transformer 的进化之路

在自然语言处理领域,处理长序列数据一直是个难题。传统 RNN(循环神经网络)虽然能够处理序列数据,但它有个致命缺陷:

BERT Transformer 多头注意力机制:从原理到新手实践指南

  • 梯度消失问题:随着序列长度增加,RNN 难以捕捉远距离依赖关系
  • 顺序计算限制:必须按时间步逐步计算,无法并行处理
  • 信息瓶颈:最后时刻的隐藏状态需要记住整个序列信息

这些问题在 2017 年被 Transformer 架构彻底改变。Transformer 引入了自注意力机制,允许模型直接计算序列中任意两个位置的关系,无论它们相距多远。

自注意力机制的核心原理

自注意力机制的核心是三个矩阵:Query(Q)、Key(K)和 Value(V)。给定输入序列 $X \in \mathbb{R}^{n \times d_{model}}$,计算过程如下:

  1. 线性变换得到 Q /K/ V 矩阵:
    $$
    Q = XW^Q, \quad K = XW^K, \quad V = XW^V
    $$

  2. 计算注意力分数:
    $$
    \text{Attention}(Q,K,V) = \text{softmax}(\frac{QK^T}{\sqrt{d_k}})V
    $$

其中 $d_k$ 是 key 的维度,缩放因子 $\frac{1}{\sqrt{d_k}}$ 防止点积过大导致梯度消失。

  1. 多头注意力就是将这个过程并行执行多次:
    $$
    \text{MultiHead}(Q,K,V) = \text{Concat}(head_1,…,head_h)W^O
    $$
    $$
    head_i = \text{Attention}(QW_i^Q,KW_i^K,VW_i^V)
    $$

PyTorch 实战:实现多头注意力层

下面我们实现一个完整的多头注意力模块,包含 mask 机制和梯度检查点优化:

import torch
import torch.nn as nn
import torch.nn.functional as F

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model=512, n_heads=8):
        super().__init__()
        assert d_model % n_heads == 0, "d_model 必须能被 n_heads 整除"

        # 初始化参数
        self.d_model = d_model          # 模型维度
        self.n_heads = n_heads          # 注意力头数量
        self.d_head = d_model // n_heads # 每个头的维度

        # 定义线性变换层
        self.w_q = nn.Linear(d_model, d_model)  # Query 变换
        self.w_k = nn.Linear(d_model, d_model)  # Key 变换
        self.w_v = nn.Linear(d_model, d_model)  # Value 变换
        self.w_o = nn.Linear(d_model, d_model)  # 输出变换

    def forward(self, x, mask=None):
        """
        输入:
            x: 输入张量 [batch_size, seq_len, d_model]
            mask: 掩码张量 [batch_size, seq_len, seq_len]
        输出:
            output: 多头注意力输出 [batch_size, seq_len, d_model]
            attn_weights: 注意力权重 [batch_size, n_heads, seq_len, seq_len]
        """
        batch_size, seq_len, _ = x.size()

        # 1. 线性变换得到 Q /K/V
        q = self.w_q(x)  # [batch_size, seq_len, d_model]
        k = self.w_k(x)  
        v = self.w_v(x)

        # 2. 分割多头
        q = q.view(batch_size, seq_len, self.n_heads, self.d_head).transpose(1, 2)
        k = k.view(batch_size, seq_len, self.n_heads, self.d_head).transpose(1, 2)
        v = v.view(batch_size, seq_len, self.n_heads, self.d_head).transpose(1, 2)

        # 3. 计算缩放点积注意力
        attn_scores = torch.matmul(q, k.transpose(-2, -1)) / (self.d_head ** 0.5)

        # 4. 应用 mask(如需要)if mask is not None:
            attn_scores = attn_scores.masked_fill(mask == 0, float('-inf'))

        # 5. 计算注意力权重
        attn_weights = F.softmax(attn_scores, dim=-1)

        # 6. 计算输出
        output = torch.matmul(attn_weights, v)

        # 7. 合并多头
        output = output.transpose(1, 2).contiguous()
        output = output.view(batch_size, seq_len, self.d_model)

        # 8. 最终线性变换
        output = self.w_o(output)

        return output, attn_weights

避坑指南:常见错误与解决方法

在实际实现中,新手常会遇到以下问题:

  1. 忘记缩放点积结果:
  2. 错误表现:注意力权重分布过于尖锐或平坦
  3. 解决方法:始终记得除以 $\sqrt{d_k}$

  4. 位置编码初始化不当:

  5. 错误表现:模型无法捕捉位置信息
  6. 解决方法:使用正余弦位置编码或学习式位置编码

  7. 梯度检查点未正确使用:

  8. 错误表现:显存溢出或训练不稳定
  9. 解决方法:对注意力计算使用 torch.utils.checkpoint

延伸思考与开放问题

在实践中,我们还需要思考以下问题:

  1. 注意力头数量选择:
  2. 实验表明,不同头可能学习到不同的关注模式
  3. 但过多头数会导致计算开销增加

  4. 注意力模式解释性:

  5. 如何可视化并理解注意力权重?
  6. 不同层的注意力模式有何差异?

完整的 Colab 实践代码可以在 这里 找到。通过本文的学习,你应该已经掌握了多头注意力的核心原理和实现方法。接下来可以尝试在更大数据集上应用这些知识,或者探索注意力机制在其他领域的应用。

正文完
 0
评论(没有评论)