2D多头自注意力计算流程图解:从零实现Transformer核心模块

1次阅读
没有评论

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

image.webp

为什么需要自注意力机制

在传统的 RNN 和 LSTM 中,序列处理是逐步进行的,这导致两个主要问题:

2D 多头自注意力计算流程图解:从零实现 Transformer 核心模块

  1. 难以捕捉长距离依赖关系
  2. 无法并行计算,训练速度慢

Transformer 通过自注意力机制完美解决了这些问题。它的核心思想是:让序列中的每个元素都能直接 ” 看到 ” 其他所有元素,并通过学习到的权重来决定关注哪些部分。

2D 多头自注意力计算全流程

1. 输入表示

假设我们有一个序列输入 $X \in \mathbb{R}^{n \times d_{model}}$,其中 n 是序列长度,$d_{model}$ 是嵌入维度。

2. 计算 Q,K,V

通过三个不同的线性变换得到查询 (Query)、键(Key) 和值(Value):

$$
Q = XW^Q, \quad K = XW^K, \quad V = XW^V
$$

其中 $W^Q, W^K, W^V \in \mathbb{R}^{d_{model} \times d_k}$

3. 多头拆分

使用 einops 库将 Q,K,V 拆分为 h 个头:

from einops import rearrange

# 假设 h 是头数,d_k 是每个头的维度
q = rearrange(q, 'b n (h d) -> b h n d', h=h)
k = rearrange(k, 'b n (h d) -> b h n d', h=h)
v = rearrange(v, 'b n (h d) -> b h n d', h=h)

4. 缩放点积注意力

计算注意力权重并应用 softmax:

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

Python 实现:

attn = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(k.size(-1)))
attn = attn.softmax(dim=-1)
out = attn @ v

5. 多头拼接

将多个头的输出拼接起来:

out = rearrange(out, 'b h n d -> b n (h d)')

完整 PyTorch 实现

import torch
import torch.nn as nn
import torch.nn.functional as F
from einops import rearrange
import math

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, h, dropout=0.1):
        super().__init__()
        assert d_model % h == 0
        self.d_k = d_model // h
        self.h = h

        self.q_linear = nn.Linear(d_model, d_model)
        self.k_linear = nn.Linear(d_model, d_model)
        self.v_linear = nn.Linear(d_model, d_model)
        self.dropout = nn.Dropout(dropout)
        self.out = nn.Linear(d_model, d_model)

    def forward(self, q, k, v, mask=None):
        bs = q.size(0)

        # 线性变换
        q = self.q_linear(q)
        k = self.k_linear(k)
        v = self.v_linear(v)

        # 拆分为多头
        q = rearrange(q, 'b n (h d) -> b h n d', h=self.h)
        k = rearrange(k, 'b n (h d) -> b h n d', h=self.h)
        v = rearrange(v, 'b n (h d) -> b h n d', h=self.h)

        # 计算注意力
        scores = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(self.d_k))

        if mask is not None:
            scores = scores.masked_fill(mask == 0, -1e9)

        attn = F.softmax(scores, dim=-1)
        attn = self.dropout(attn)

        # 应用注意力权重
        out = attn @ v

        # 拼接多头
        out = rearrange(out, 'b h n d -> b n (h d)')

        # 最终线性变换
        return self.out(out)

性能优化与避坑指南

计算复杂度分析

  1. 自注意力的计算复杂度是 $O(n^2d)$,其中 n 是序列长度,d 是嵌入维度
  2. 当序列长度增加时,显存占用会平方级增长

显存优化技巧

  1. 使用梯度检查点(gradient checkpointing)
  2. 混合精度训练
  3. 适当减少头数(h),例如从 8 头改为 4 头

常见问题解决

  1. 梯度爆炸:确保除以 $\sqrt{d_k}$ 进行缩放
  2. 变长序列处理:
    # 创建 mask 矩阵
    mask = torch.ones(bs, seq_len)
    for i in range(bs):
        mask[i, lengths[i]:] = 0
    mask = mask.unsqueeze(1).unsqueeze(1)  # 形状变为(b,1,1,seq_len)
  3. 数值稳定性:在 softmax 前对 masked 位置填充一个很大的负数(-1e9)

思考与扩展

本文的实现虽然完整,但在处理超长序列时仍然面临计算效率问题。Flash Attention 通过以下方式进行了优化:

  1. 分块计算注意力,减少显存访问
  2. 融合多个操作,减少中间结果存储
  3. 利用 GPU 内存层次结构优化

读者可以思考:如何在本代码基础上集成 Flash Attention?如何处理超过 GPU 显存容量的超长序列?

希望这篇图解 + 代码的实现方式能帮助初学者更好地理解 Transformer 的核心机制。在实际应用中,还需要考虑批处理效率、混合精度训练等工程细节,这些我们可以在后续文章中继续探讨。

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