在深度学习(尤其是 Transformer、Stable Diffusion 等多模态大模型)中,交叉注意力机制(Cross-Attention) 是连接不同模态或不同信息的纽带。
在你有条件扩散模型的语境下,交叉注意力机制的作用是:让生成图像的 U-Net 网络(代表图像特征),在去噪的每一步都能"看一眼"你输入的文本或标签(代表条件特征),从而引导图像朝着正确的方向生成。
它的核心实现原理可以从自注意力(Self-Attention)的对比 以及具体的矩阵计算步骤来理解:
1. 核心区别:它与自注意力(Self-Attention)有什么不同?
两者的数学公式完全一样,唯一的区别在于 Query (Q)、Key (K)、Value (V) 的来源不同:
- 自注意力(Self-Attention) :Q、K、V 全部来自同一个输入。比如在处理图像特征时,图像自己和自己比对,计算图像内部像素或区域之间的关系。
- 交叉注意力(Cross-Attention) :Q 和 K、V 来自两个不同的输入。
- Query (Q) :来自主干网络(如 U-Net 的图像特征)。它代表:"我现在手里有这些图像信息,我想知道应该往里面填什么。"
- Key (K) 和 Value (V) :来自条件输入(如文本特征、标签 Embedding)。它代表:"我这里有用户输入的指令和引导信息。"
2. 交叉注意力的具体实现步骤(以文本引导图像为例)
假设你的 U-Net 提取出的图像特征为 XimgX_{\text{img}}Ximg,文本编码器(如 CLIP)提取出的文本特征为 YtextY_{\text{text}}Ytext。
步骤一:投影得到 Q、K、V 矩阵
使用三个不同的线性变换层(Linear Layers,即全连接层),将图像和文本分别投影到统一的通道维度(Head Dimension)中:
-
Q (Query) :由图像特征 通过线性层 WQW_QWQ 得到:
Q=Ximg⋅WQQ = X_{\text{img}} \cdot W_QQ=Ximg⋅WQ
-
K (Key) :由文本特征 通过线性层 WKW_KWK 得到:
K=Ytext⋅WKK = Y_{\text{text}} \cdot W_KK=Ytext⋅WK
-
V (Value) :由文本特征 通过线性层 WVW_VWV 得到:
V=Ytext⋅WVV = Y_{\text{text}} \cdot W_VV=Ytext⋅WV
步骤二:计算注意力权重(相似度匹配)
将图像的 QQQ 与文本的 KKK 进行矩阵乘法(点积)。这一步是在计算图像中的每一个像素/区域,与文本中的每一个单词之间的相关性(匹配度)。
Scores=Q⋅KT\text{Scores} = Q \cdot K^TScores=Q⋅KT
为了防止点积结果过大导致梯度消失,会除以缩放因子 dk\sqrt{d_k}dk (dkd_kdk 为特征维度),然后通过 Softmax 函数进行归一化,得到一个权重矩阵:
Attention_Map=Softmax(Q⋅KTdk)\text{Attention\_Map} = \text{Softmax}\left(\frac{Q \cdot K^T}{\sqrt{d_k}}\right)Attention_Map=Softmax(dk Q⋅KT)
这个权重矩阵的每一行都代表:为了生成当前这个位置的像素,模型应该对文本中的各个单词分配多少注意力。
步骤三:加权融合条件信息
将计算好的注意力权重乘以文本的 VVV(Value)。这意味着把文本中被高度关注的特征,"注入"到对应的图像位置中去。
Output=Attention_Map⋅V\text{Output} = \text{Attention\_Map} \cdot VOutput=Attention_Map⋅V
步骤四:输出融合
最后,将这个融合了文本信息的特征通过一个输出线性层 WOW_OWO,并与原图像特征进行残差连接(Residual Connection),送入 U-Net 的下一层。
3. PyTorch 代码的极简直观实现
在写代码时,通常会直接使用 PyTorch 内置的高性能函数 F.scaled_dot_product_attention 或标准的矩阵操作。以下是写在 U-Net 模块内部的大致逻辑:
python
import torch
import torch.nn as nn
import torch.nn.functional as F
class CrossAttention(nn.Module):
def __init__(self, query_dim, context_dim, heads=8, dim_head=64):
super().__init__()
inner_dim = dim_head * heads
self.heads = heads
self.scale = dim_head ** -0.5
# Q 来自图像 (query_dim)
self.to_q = nn.Linear(query_dim, inner_dim, bias=False)
# K, V 来自条件/文本 (context_dim)
self.to_k = nn.Linear(context_dim, inner_dim, bias=False)
self.to_v = nn.Linear(context_dim, inner_dim, bias=False)
self.to_out = nn.Linear(inner_dim, query_dim)
def forward(self, x, context=None):
# x: 图像特征,形状为 [Batch, Tokens_img, query_dim] (例如 [B, H*W, C])
# context: 文本条件特征,形状为 [Batch, Tokens_text, context_dim]
if context is None:
context = x # 如果没有条件,退化为自注意力
b, n, _ = x.shape
# 1. 线性投影并拆分为多头 (Multi-head)
q = self.to_q(x)
k = self.to_k(context)
v = self.to_v(context)
# 调整形状以适应多头计算 [B, heads, Tokens, dim_head]
# (此处省略具体的多头 reshape 细节,原理相同)
# 2. 计算缩放点积注意力 (PyTorch 2.0+ 推荐写法)
# 内置函数会自动帮你算 Softmax(Q K^T / sqrt(d)) * V
out = F.scaled_dot_product_attention(q, k, v)
# 3. 恢复形状并通过输出层
return self.to_out(out)
总结
在扩散模型中,交叉注意力就像一个"翻译官":图像(Q)问:"我这里需要添加什么?" 文本(K, V)回答:"这里对应的描述是'猫的耳朵'。" 于是模型就能精准地在图像的相应位置画出猫的耳朵。