多头机制详解:多头分割、拼接、线性变换与特征增强

说到多头注意力,我理解它就像是一组专家同时审视同一个问题。每个专家关注的视角不同,最后把他们的发现汇总起来,就能得到更全面的认识。这个机制,说白了就是把单头注意力拆成多个子空间,让模型自己学会从不同维度提取特征。

1. 多头分割:拆解特征子空间

输入序列的每个token经过线性变换得到Q、K、V后,我们要做的第一件事就是“分割”。

假设我们的模型维度是d_model = 512,头数设为h = 8。那么每个头的维度就是d_k = d_model / h = 64。为什么要这样分割?说白了,就是让每个头只负责一小块特征空间,降低单头压力。

分割的核心公式:把 [batch, seq_len, d_model] 的 Q/K/V reshape 为 [batch, seq_len, h, d_k],然后转置成 [batch, h, seq_len, d_k]。

我在项目中曾经直接用.view()做reshape,结果在deploy时发现显存占得特别大。嗯,后来改成.reshape()并用.transpose()调整维度顺序,问题就解决了。

典型的分割实现如下:

def split_heads(x, h, d_k):
    # x: [batch, seq_len, d_model]
    batch, seq_len, _ = x.shape
    x = x.reshape(batch, seq_len, h, d_k)
    # 转置为 [batch, h, seq_len, d_k]
    return x.transpose(1, 2)

我习惯把分割和后续计算放在一起,这样代码更干净。

2. 并行注意力计算

分割完每个head,接下来就是各算各的注意力。注意,这里是并行计算的——每个头独立计算softmax(Q*K^T / sqrt(d_k)) * V。因为矩阵运算可以批处理,所以实际计算时所有头同时进行。

每个头的缩放因子 sqrt(d_k) 是固定的,d_k 越小,缩放越大。我的经验是,如果 d_k 太小(比如小于32),注意力分布容易变“尖”,导致梯度不稳定。一般把 d_k 设在64或128比较稳。

计算完每个头的注意力输出,shape是 [batch, h, seq_len, d_k]。

3. 拼接与线性变换

所有头算完后,需要把他们的结果拼回一个完整的特征空间。拼接很简单:把 [batch, h, seq_len, d_k] 转置回 [batch, seq_len, h, d_k],然后reshape成 [batch, seq_len, d_model]。

def combine_heads(x, h, d_k):
    # x: [batch, h, seq_len, d_k]
    batch, _, seq_len, _ = x.shape
    x = x.transpose(1, 2).reshape(batch, seq_len, -1)
    return x

但拼接完还不算完——每个头的输出只是各自视角的“报告”,我们需要一个最终的线性变换层来融合这些信息。这个线性变换的权重矩阵 W_O 维度是 d_model × d_model。它的作用就是让模型学习如何把8份64维的局部信息,组合成一份512维的全局信息。

我曾经遇到过一个问题:拼接头之后直接使用全连接层,结果训练初期loss降得很慢。后来我发现,在线性变换前加一个LayerNorm,效果好了很多。你想想看,多头输出本身就是不同分布的,不加归一化,线性层学起来会很吃力。

4. 特征增强的奥秘

为什么多头机制能增强特征?我自己的理解是:每个头相当于一个独立的“特征过滤器”,它们可以关注序列中不同的模式。比如在文本中,某个头可能专门捕捉句法结构,另一个头关注语义相似性,还有一个头可能对位置信息敏感。这种分工让模型能同时表达多种关系,表达能力自然比单头强。

我参与过一个语音识别项目,使用8头注意力。实验发现,有2个头几乎完全关注了音素边界信息,另外3个头集中处理了音调特征。这就是多头的实际效果——你甚至可以不看模型细节,光看头部的注意力图就能猜出它们在做什么。

注意头数不是越多越好。我记得有一次我把头数从8加到32,效果反而下降了。因为每个头的维度变小(d_k = d_model / h),信息太碎片化,难以学到有效特征。我个人建议:头数设置在8~16之间,并且确保 d_k 不小于32。

下面我画了一张图,帮大家快速理解整个流程:

多头机制完整流程 输入 (Q/K/V) 分割为h个头 Head 1 注意力 Head 2 注意力 ... Head h 注意力 拼接 (Concat) 线性变换 W_O 输出 小贴士:每个头独立计算 Softmax(Q*K^T/√d_k)*V 实际使用批处理并行

为了更直观对比单头和多头的差异,我整理了一个表格:

对比项单头注意力多头注意力
特征空间单个全局空间多个子空间并行
表达能力一次只能学习一种关系可同时学习多种关系
参数量较少,线性变换少每个头独立W_Q/W_K/W_V,但总参数量与单头相当(因为d_model不变)
训练稳定性容易陷入局部模式多个头互补,更稳定
典型应用场景小模型、快速实验中大型模型,如BERT、T5

我在调参时总结过一条经验:如果头数增加后效果没有明显提升,先检查一下每个头的注意力分布是不是太相似。如果所有头的关注区域都差不多,那说明多头没有学到差异化,这时候增加头数只会浪费计算资源。可以尝试加大dropout或者初始化时给不同头分配不同的W_Q初始值,来强制多样性。

我曾经把每个头的Q/K/V投影矩阵共享,想着省参数,结果模型直接崩了。多头之所以有效,就是因为每个头有独立的参数,才能学到不同的模式。千万不要为了省显存而共享头间的投影矩阵!

最后说一句,多头机制的魅力在于,你不需要手动告诉模型该关注什么——它自己会通过训练找到最优的分工方式。你只需要给足够的头数和合适的维度,剩下的交给梯度下降就好。