4. 多头注意力

好,咱们进入第四章。前面我们聊了单头注意力,它就像一个专注的观察者,能抓住序列里最重要的关系。但问题来了——一个观察者,真的够用吗?

我个人习惯把单头注意力比作「用一把尺子量世界」。你想想看,在视觉语言模型里,一个词可能同时跟好几个词有关系。比如句子「那只站在树枝上的猫,正盯着池塘里的鱼」。这里的「它」到底指猫还是鱼?单头注意力只能给出一个加权平均结果,它很难同时捕捉「猫-树枝」的空间关系和「猫-鱼」的捕食关系。

这就是多头注意力要解决的问题。

4.1 为什么需要多个头?

说白了,多头注意力就是让模型从不同角度、不同子空间去理解输入。每个头可以关注不同的特征模式:

  • 头A:关注语法关系,比如主谓宾
  • 头B:关注位置关系,比如距离远近
  • 头C:关注语义相似度,比如同义词替换
  • 头D:关注视觉-文本对齐,比如物体和描述词的对应

我在项目中遇到过这样一个情况:用单头注意力训练一个图文检索模型,召回率死活上不去。后来换成8头注意力,同样的数据,召回率直接涨了5个点。为什么?因为不同头学会了关注不同的视觉区域和文本片段,信息互补了。

核心观点: 多头注意力本质上是将模型的能力「复制多份」,每份在不同的投影子空间里学习,最后把学到的不同知识拼起来。这比单头在相同参数量下表达能力更强。

4.2 多头注意力的数学原理

公式其实不复杂,就是在单头注意力的基础上加了一个「分头-并行-合并」的流程。我直接给出标准定义:

MultiHead(Q, K, V) = Concat(head_1, ..., head_h) · W_O

其中 head_i = Attention(Q · W_Q_i, K · W_K_i, V · W_V_i)

这里有几个关键点:

  • h:头的数量,常见取值8、12、16
  • W_Q_i, W_K_i, W_V_i:每个头独立的投影矩阵,把输入映射到不同的子空间
  • W_O:输出投影矩阵,把拼接后的结果再映射回原始维度

嗯,这里要注意:每个头的维度 d_k = d_model / h。比如 d_model=512,h=8,那每个头的维度就是64。这样总计算量跟单头512维差不多,但表达能力却强了很多。

4.3 多头如何并行计算?

并行计算是多头注意力的核心优势。你想想看,如果8个头串行跑,那速度就慢了8倍。实际工程中,我们通过矩阵分块来实现真正的并行。

具体做法是这样的:

  1. 线性变换合并:把h个头的Q、K、V投影矩阵拼成一个大矩阵,一次矩阵乘法搞定所有头的投影
  2. 维度重塑:把结果从 [batch, seq_len, d_model] 重塑成 [batch, h, seq_len, d_k]
  3. 批量计算:利用GPU的并行能力,一次性计算h个头的注意力分数
  4. 结果合并:把 [batch, h, seq_len, d_k] 再重塑回 [batch, seq_len, d_model]

我曾经在优化一个视觉语言模型时,发现多头注意力的并行效率只有理论值的60%。排查后发现是维度重塑的布局不对,导致GPU内存访问不连续。调整了内存布局后,效率直接拉到90%以上。

工程小技巧: 在PyTorch中,使用 torch.nn.MultiheadAttention 时,记得设置 batch_first=True。这能让batch维度在最前面,内存布局更友好,推理速度能快10%-15%。

4.4 多头信息的融合机制

每个头算完自己的注意力结果后,怎么把这些信息融合起来?这里有个关键设计:拼接 + 线性投影。

流程是这样的:

  • 每个头输出维度为 d_k 的特征向量
  • 把h个头的输出沿特征维度拼接,得到 h·d_k = d_model 的向量
  • 通过一个可学习的输出投影矩阵 W_O,把拼接结果映射到最终输出

这个 W_O 矩阵很关键。它不只是简单的拼接,而是可学习的加权融合。模型在训练过程中,会自动学会哪些头的信息更重要,给它们分配更高的权重。

我观察过训练好的模型,发现不同头确实学会了不同的关注模式。有的头几乎只关注相邻词,有的头则能捕捉长距离依赖。这种自动分工,就是多头注意力的精髓。

4.5 视觉语言模型中的多头设计

在视觉语言模型里,多头注意力还有个特殊设计:跨模态头。比如在ViLT、CLIP这些模型里,有些头专门负责文本到图像的注意力,有些头负责图像到文本的注意力,还有些头负责模态内的自注意力。

头类型 Q来源 K/V来源 学习目标
文本自注意力头 文本 文本 文本上下文关系
图像自注意力头 图像patch 图像patch 图像空间关系
文本到图像头 文本 图像patch 文本描述对应哪个图像区域
图像到文本头 图像patch 文本 图像区域对应哪个文本描述

这种设计让模型能同时处理模态内和模态间的信息交互。我在做图文检索项目时,发现增加2个跨模态头,模型对「图片中有猫但文字没提猫」这种负样本的判别能力明显提升。

避坑指南: 头数不是越多越好。我曾经试过把头数从8增加到32,结果模型反而过拟合了,验证集性能下降。一般来说,d_model=512时用8头,d_model=768时用12头,d_model=1024时用16头,这个比例比较稳妥。

4.6 多头注意力的可视化分析

下面这张图展示了多头注意力在视觉语言模型中的完整工作流程。你可以看到输入经过不同的投影矩阵后,分头并行计算,最后融合输出。

多头注意力并行计算与融合流程图 多头注意力:并行计算与信息融合 输入序列 (Q, K, V) 线性投影:W_Q_i, W_K_i, W_V_i (i=1..h) 头 1 d_k = d_model/h 关注语法关系 头 2 d_k = d_model/h 关注位置关系 头 h d_k = d_model/h 关注语义相似度 ... ... 拼接 Concat(head_1, ..., head_h) 输出投影 W_O(可学习融合) 融合后的输出 每个头在独立的子空间学习,最后通过可学习的 W_O 自动融合不同头的信息

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

理论说完了,咱们看看代码。下面是一个简洁的多头注意力实现,我注释了关键步骤:

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

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, n_heads, dropout=0.1):
        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
        
        # 合并所有头的Q、K、V投影
        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)
        
        # 1. 线性投影 + 分头
        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)
        
        # 2. 并行计算注意力分数
        scores = torch.matmul(Q, K.transpose(-2, -1)) / (self.d_k ** 0.5)
        if mask is not None:
            scores = scores.masked_fill(mask == 0, float('-inf'))
        attn_weights = F.softmax(scores, dim=-1)
        attn_weights = self.dropout(attn_weights)
        
        # 3. 加权求和
        context = torch.matmul(attn_weights, V)
        
        # 4. 合并头 + 输出投影
        context = context.transpose(1, 2).contiguous().view(
            batch_size, -1, self.d_model
        )
        output = self.w_o(context)
        
        return output, attn_weights

这段代码里,我特别想强调view和transpose的用法。很多新手在这里容易搞错维度顺序。记住:view之后维度是 [batch, seq_len, n_heads, d_k],transpose(1,2)之后变成 [batch, n_heads, seq_len, d_k],这样每个头就能独立计算了。

调试经验: 如果你发现多头注意力的输出跟单头差不多,很可能是所有头学到了相同的模式。可以打印一下 attn_weights 的分布,看看不同头的注意力分布是否不同。如果都一样,说明投影矩阵初始化有问题,试试用不同的随机种子初始化每个头的投影。

好了,多头注意力的核心内容就这些。总结一下:多头注意力通过分头并行计算,让模型从不同子空间学习互补的特征,再通过可学习的融合机制整合信息。这是Transformer系列模型能取得巨大成功的关键设计之一。