4. 注意力机制入门
注意力机制,说白了就是让模型学会「看哪里」。
我刚开始做多模态时,总觉得这玩意儿玄乎。后来踩过几次坑才明白——没有注意力机制,图像和文本就像两条平行线,永远碰不到一起。今天咱们就把这块硬骨头啃下来。
4.1 从Seq2Seq到注意力:为什么需要「看重点」?
早期的Seq2Seq模型有个致命问题:编码器把整个输入序列压成一个固定长度的向量。你想想看,如果句子有50个词,这个向量得承载多少信息?
我在项目中遇到过这种情况:用Seq2Seq做图像描述生成,输入一张复杂图片,模型输出的描述总是丢三落四。后来才发现,问题就出在这个「固定向量」上——它根本记不住所有细节。
核心痛点:固定上下文向量成为信息瓶颈,长序列或高分辨率图像下性能急剧下降。
注意力机制的解决方案很直观:不再压缩成一个向量,而是让解码器每一步都能「回头看」输入序列的所有位置,并给每个位置分配一个权重。
| 对比维度 | 传统Seq2Seq | 带注意力Seq2Seq |
|---|---|---|
| 上下文表示 | 单一固定向量 | 动态加权求和 |
| 长序列处理 | 信息丢失严重 | 可保留全部信息 |
| 可解释性 | 黑盒 | 可观察关注区域 |
| 计算复杂度 | O(n) | O(n²) |
4.2 Self-Attention:让序列内部「对话」
Self-Attention,也叫自注意力。它解决的是序列内部元素之间的关系建模问题。
举个例子:句子「那只猫追到了老鼠,它很开心。」——这个「它」指的是猫还是老鼠?人类一眼就能看出来,但模型需要知道「它」和「猫」之间的关联。
Self-Attention的计算分三步走:
- 生成Q、K、V:每个输入向量通过三个不同的线性变换,得到Query、Key、Value。
- 计算注意力分数:Q与所有K做点积,除以√d_k做缩放,再经过Softmax归一化。
- 加权聚合:用归一化后的权重对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。
4.6 多模态中的注意力实践
在实际的多模态项目中,注意力机制的使用有几种常见模式:
- 单流模型:图像和文本特征拼在一起,送入共享的Transformer。适合图文匹配、视觉推理等任务。
- 双流模型:图像和文本各自走独立的编码器,只在特定层做Cross-Attention交互。适合检索、生成等任务。
- 分层注意力:先做模态内Self-Attention,再做模态间Cross-Attention,层层递进。
我的建议:刚开始做多模态项目时,优先尝试双流+Cross-Attention的方案。它比单流更灵活,而且容易调试——你可以单独观察每个模态的编码质量,再检查Cross-Attention的权重分布是否合理。
嗯,注意力机制这块内容确实不少。但说白了,核心就三句话:Self-Attention让模态内部「对齐」,Cross-Attention让模态之间「对话」,Multi-Head让模型从多个角度「看问题」。掌握了这些,多模态融合的大门就算真正推开了。