4. 注意力机制入门

注意力机制,说白了就是让模型学会「看哪里」。

我刚开始做多模态时,总觉得这玩意儿玄乎。后来踩过几次坑才明白——没有注意力机制,图像和文本就像两条平行线,永远碰不到一起。今天咱们就把这块硬骨头啃下来。

4.1 从Seq2Seq到注意力:为什么需要「看重点」?

早期的Seq2Seq模型有个致命问题:编码器把整个输入序列压成一个固定长度的向量。你想想看,如果句子有50个词,这个向量得承载多少信息?

我在项目中遇到过这种情况:用Seq2Seq做图像描述生成,输入一张复杂图片,模型输出的描述总是丢三落四。后来才发现,问题就出在这个「固定向量」上——它根本记不住所有细节。

核心痛点:固定上下文向量成为信息瓶颈,长序列或高分辨率图像下性能急剧下降。

注意力机制的解决方案很直观:不再压缩成一个向量,而是让解码器每一步都能「回头看」输入序列的所有位置,并给每个位置分配一个权重。

对比维度 传统Seq2Seq 带注意力Seq2Seq
上下文表示 单一固定向量 动态加权求和
长序列处理 信息丢失严重 可保留全部信息
可解释性 黑盒 可观察关注区域
计算复杂度 O(n) O(n²)

4.2 Self-Attention:让序列内部「对话」

Self-Attention,也叫自注意力。它解决的是序列内部元素之间的关系建模问题。

举个例子:句子「那只猫追到了老鼠,它很开心。」——这个「它」指的是猫还是老鼠?人类一眼就能看出来,但模型需要知道「它」和「猫」之间的关联。

Self-Attention的计算分三步走:

  1. 生成Q、K、V:每个输入向量通过三个不同的线性变换,得到Query、Key、Value。
  2. 计算注意力分数:Q与所有K做点积,除以√d_k做缩放,再经过Softmax归一化。
  3. 加权聚合:用归一化后的权重对V做加权求和。
import torch
import torch.nn.functional as F

def scaled_dot_product_attention(Q, K, V):
    """
    Q: [batch, seq_len, d_k]
    K: [batch, seq_len, d_k]
    V: [batch, seq_len, d_v]
    """
    d_k = Q.size(-1)
    scores = torch.matmul(Q, K.transpose(-2, -1)) / torch.sqrt(torch.tensor(d_k, dtype=torch.float32))
    attn_weights = F.softmax(scores, dim=-1)
    output = torch.matmul(attn_weights, V)
    return output, attn_weights

我的经验:缩放因子√d_k非常关键。如果不做缩放,当d_k较大时,点积结果会进入Softmax的饱和区,梯度会变得极小。我曾经因为这个细节让模型训了三天都不收敛。

4.3 Multi-Head Attention:从不同角度看问题

单一注意力头只能捕捉一种关系模式。Multi-Head Attention的思路是:用多组Q、K、V并行计算,每组关注不同的子空间。

我个人习惯把多头注意力理解为「多个专家同时看同一份报告」——有的专家关注语法结构,有的关注语义关系,有的关注位置信息。最后把他们的意见拼接起来,再投影回原始维度。

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, num_heads):
        super().__init__()
        self.num_heads = num_heads
        self.d_k = d_model // num_heads
        
        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)
    
    def forward(self, Q, K, V, mask=None):
        batch_size = Q.size(0)
        
        # 线性变换 + 分头
        Q = self.W_q(Q).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
        K = self.W_k(K).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
        V = self.W_v(V).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
        
        # 注意力计算
        attn_output, _ = scaled_dot_product_attention(Q, K, V)
        
        # 合并头 + 输出投影
        attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, -1, self.num_heads * self.d_k)
        return self.W_o(attn_output)

4.4 Cross-Attention:多模态融合的桥梁

Cross-Attention(交叉注意力)是多模态融合的核心机制。它和Self-Attention的区别在于:Q来自一个模态,K和V来自另一个模态。

举个例子:给一张图片配文字描述。Q来自文本解码器当前步的隐状态,K和V来自图像编码器的输出。这样模型在生成每个词时,都能「看」到图片中对应的区域。

关键区别:Self-Attention是模态内部建模,Cross-Attention是模态间信息交互。

注意力类型 Q来源 K/V来源 典型应用
Self-Attention 模态A 模态A 文本编码、图像特征增强
Cross-Attention 模态A 模态B 图像描述、视觉问答
Co-Attention 交替 交替 图文匹配、多模态推理

避坑指南:我曾经在做一个图文检索项目时,直接把图像特征和文本特征拼起来送进Self-Attention。结果模型学到的全是模态内部的关联,跨模态交互几乎为零。后来改成Cross-Attention结构,效果直接提升了12个点。

4.5 Transformer架构:注意力机制的集大成者

Transformer把注意力机制用到了极致。它的编码器由多层Self-Attention + Feed-Forward组成,解码器则多了一层Cross-Attention来接收编码器输出。

我刚开始看Transformer时,觉得结构图密密麻麻。后来自己动手画了一遍,才真正理解它的设计哲学:

  • 编码器:每层先做Self-Attention(让词与词交互),再做FFN(非线性变换)。
  • 解码器:先做Masked Self-Attention(防止看到未来词),再做Cross-Attention(看编码器输出),最后做FFN。
Transformer 核心架构 编码器 (Encoder) Self-Attention Q、K、V 均来自同一序列 Add & LayerNorm 残差连接 + 层归一化 Feed-Forward 两层线性变换 + ReLU Add & LayerNorm 残差连接 + 层归一化 解码器 (Decoder) Masked Self-Attention 防止看到未来位置 Add & LayerNorm K、V Cross-Attention Q来自解码器,K/V来自编码器 Add & LayerNorm Feed-Forward + Add & Norm

4.6 多模态中的注意力实践

在实际的多模态项目中,注意力机制的使用有几种常见模式:

  • 单流模型:图像和文本特征拼在一起,送入共享的Transformer。适合图文匹配、视觉推理等任务。
  • 双流模型:图像和文本各自走独立的编码器,只在特定层做Cross-Attention交互。适合检索、生成等任务。
  • 分层注意力:先做模态内Self-Attention,再做模态间Cross-Attention,层层递进。

我的建议:刚开始做多模态项目时,优先尝试双流+Cross-Attention的方案。它比单流更灵活,而且容易调试——你可以单独观察每个模态的编码质量,再检查Cross-Attention的权重分布是否合理。

嗯,注意力机制这块内容确实不少。但说白了,核心就三句话:Self-Attention让模态内部「对齐」,Cross-Attention让模态之间「对话」,Multi-Head让模型从多个角度「看问题」。掌握了这些,多模态融合的大门就算真正推开了。