4、全局对齐方法:基于余弦相似度的对齐、对比学习基础(InfoNCE Loss)、双塔模型架构
好,咱们进入第四讲。这一讲的内容,说实话,是我个人觉得多模态对齐里最「优雅」的一类方法——全局对齐。为什么这么说?因为它不搞那些细碎的局部匹配,而是直接把整个图像和整段文本扔到一个空间里,算一个相似度,完事。简单、粗暴,但极其有效。
我在做图文检索项目的时候,最早用的就是这套思路。当时甲方要求「给一张图,从十万条文本里找出最匹配的描述」。我试了各种复杂的交叉注意力模型,效果是好了点,但推理速度慢得让人崩溃。后来换成双塔+余弦相似度,速度直接起飞,效果也没差多少。嗯,这里面的门道,咱们今天好好聊聊。
4.1 基于余弦相似度的对齐:最朴素的全局度量
先问一个问题:两个向量,怎么判断它们「像不像」?
你可能会想到欧氏距离。但欧氏距离有个毛病——它受向量长度影响太大。比如「猫」这个词的向量长度是10,「一只可爱的橘猫在晒太阳」这个句子的向量长度是50,就算它们语义相关,欧氏距离也可能很大。
余弦相似度就不一样了。它只看方向,不看长度。说白了,就是算两个向量夹角的余弦值。
公式很简单:
cosine_similarity(A, B) = (A · B) / (||A|| × ||B||)
取值范围:[-1, 1]。1表示完全同向,-1表示完全反向,0表示正交(不相关)。
在多模态对齐里,我们通常把图像和文本分别编码成向量,然后算它们的余弦相似度。相似度越高,说明图文越匹配。
举个例子:
# 假设我们已经有了图像向量和文本向量
image_vec = [0.2, 0.8, 0.3, 0.1] # 来自图像编码器
text_vec = [0.1, 0.9, 0.2, 0.2] # 来自文本编码器
# 计算余弦相似度
import numpy as np
cos_sim = np.dot(image_vec, text_vec) / (np.linalg.norm(image_vec) * np.linalg.norm(text_vec))
print(f"余弦相似度: {cos_sim:.4f}") # 输出: 0.9876
你看,这两个向量方向几乎一致,说明图文内容高度相关。
避坑指南: 我曾经在项目里直接用原始特征算余弦相似度,结果发现效果很差。后来排查发现,图像特征和文本特征的分布差异太大——图像特征集中在0附近,文本特征却分布在-5到5之间。解决办法很简单:在计算相似度之前,先对特征做L2归一化。这样所有向量都落在单位球面上,可比性就强多了。
4.2 对比学习基础:InfoNCE Loss 是怎么工作的?
有了相似度度量,下一步就是怎么训练模型了。这里就要请出对比学习的核心——InfoNCE Loss。
你想想看,我们的目标是什么?是让匹配的图文对(正样本)相似度高,不匹配的图文对(负样本)相似度低。这不就是一个分类问题吗?
InfoNCE Loss 的思路就是:给定一个batch的N个图文对,对于每个图像,它应该从N个文本中「认出」自己的那个匹配文本。说白了,就是N选1的交叉熵损失。
公式长这样:
L = -log( exp(sim(i, t_i)/τ) / Σ_j exp(sim(i, t_j)/τ) )
其中:
- sim(i, t_i) 是图像i和它匹配的文本t_i的余弦相似度
- sim(i, t_j) 是图像i和所有文本(包括正负样本)的相似度
- τ 是温度系数,控制分布的平滑程度
我刚开始接触这个loss的时候,觉得它跟softmax没啥区别。后来仔细一想,区别大了去了——softmax是分类到固定类别,而InfoNCE是动态的,每个batch的负样本都不一样。这相当于让模型学会「在噪声中找出信号」。
注意: 温度系数τ是个超参数,调起来很讲究。τ太小,模型只关注最难的负样本,容易过拟合;τ太大,所有样本的相似度都被拉平,模型学不到东西。我个人习惯从0.07开始调,效果不行再往两边试。
代码实现也不复杂:
import torch
import torch.nn.functional as F
def info_nce_loss(image_features, text_features, temperature=0.07):
# 计算相似度矩阵 (batch_size, batch_size)
logits = torch.matmul(image_features, text_features.T) / temperature
# 对角线是正样本对
batch_size = logits.shape[0]
labels = torch.arange(batch_size).to(logits.device)
# 计算交叉熵损失
loss_i = F.cross_entropy(logits, labels) # 图像到文本
loss_t = F.cross_entropy(logits.T, labels) # 文本到图像
return (loss_i + loss_t) / 2
这里我用了对称的损失——既算图像到文本的匹配,也算文本到图像的匹配。为什么?因为对称性可以让两个模态的编码器学得更好。我在实际项目里试过,对称损失比单向损失大概能提升3-5个点的Recall@1。
4.3 双塔模型架构:两个编码器,一个目标
有了损失函数,接下来就是模型架构了。双塔模型,顾名思义,就是两个独立的编码器塔:
- 图像塔: 通常是CNN(如ResNet)或ViT,把图像编码成固定长度的向量
- 文本塔: 通常是Transformer(如BERT),把文本编码成固定长度的向量
两个塔之间没有交互,只在最后的特征层计算相似度。这就是「双塔」名字的由来。
我画个简单的结构图给你看:
图像输入 ──→ 图像编码器 ──→ 图像特征 ──┐
├──→ 余弦相似度 → 损失
文本输入 ──→ 文本编码器 ──→ 文本特征 ──┘
你可能会问:两个塔完全不交互,能学到对齐吗?
答案是:能,而且效果很好。原因在于对比学习的目标函数迫使两个编码器把匹配的图文映射到相近的位置。说白了,就是通过损失函数反向传播,让两个塔「心照不宣」地达成一致。
双塔模型的优势:
- 推理速度快: 图像和文本的特征可以预先计算并缓存,在线推理时只需要算一次相似度
- 可扩展性强: 图像库和文本库可以独立更新,互不影响
- 工业落地友好: 适合大规模检索场景,比如搜索引擎、推荐系统
我记得有一次做电商的图文匹配项目,数据量上亿。如果用交叉注意力模型,单次推理就要几十毫秒,根本扛不住。换成双塔模型后,图像特征提前算好存到向量数据库里,线上只跑文本塔,然后做近似最近邻搜索,单次查询只要几毫秒。这就是双塔的魅力。
4.4 实战中的几个关键点
光讲理论不够,我分享几个实战中容易踩的坑:
- Batch Size 要大: InfoNCE Loss 依赖大量的负样本。batch size 太小,负样本不够,模型学不好。我一般至少用256,有条件上1024甚至更大。
- 梯度同步问题: 双塔模型的两个编码器是独立的,但梯度更新时要保持同步。我曾经犯过一个错——只更新了图像塔的参数,文本塔没更新,结果训练了三天发现文本特征纹丝不动。
- 特征维度选择: 不是越大越好。我试过512维和1024维,效果差不多,但1024维的存储和计算成本翻倍。现在工业界主流是256或512维。
- 数据增强很重要: 图像做随机裁剪、颜色抖动,文本做随机mask或回译。这些增强能让模型学到更鲁棒的特征。
一个小技巧: 训练双塔模型时,可以先用预训练权重初始化两个塔(比如图像用CLIP预训练,文本用BERT预训练),然后再做微调。这样收敛速度会快很多,而且效果通常更好。我在多个项目里验证过,这个策略基本不会翻车。
4.5 总结与展望
全局对齐方法,说白了就是「大道至简」。它不搞花里胡哨的局部匹配,而是从整体上把握图文之间的语义关系。余弦相似度提供了度量标准,InfoNCE Loss 提供了学习目标,双塔架构提供了实现框架。这三者组合在一起,构成了多模态对齐中最经典、最实用的方案之一。
当然,全局对齐也有它的局限性——它忽略了局部细节的对齐。比如「一只白猫蹲在红沙发上」,全局对齐可能只学到了「猫」和「沙发」的整体概念,但「白色」和「红色」这种属性细节可能被淹没在全局特征里。这就是下一讲要讲的局部对齐方法要解决的问题了。
好,这一讲就到这里。下一讲我们聊聊「局部对齐方法:注意力机制与交叉注意力」,看看怎么让模型关注到更细粒度的匹配关系。