共计 2533 个字符,预计需要花费 7 分钟才能阅读完成。
背景痛点
跨模态检索在电商搜索、内容审核等场景中有着广泛的需求。比如在电商平台,用户可能用文字描述搜索商品图片,或者反过来用图片搜索相关商品描述。传统方法通常面临以下问题:

- 特征空间不一致:图像和文本特征通常由不同模型提取,导致特征空间不匹配
- 人工标注成本高:需要大量人工标注的图文对数据
- 计算效率低下:传统的跨模态检索方法通常需要复杂的特征融合和匹配过程
架构解析
CLIP 模型采用双编码器设计,分别处理图像和文本输入,并通过对比学习实现模态对齐。架构如下:
[Image Input] [Text Input]
| |
[ViT Encoder] [Text Encoder]
| |
[Projection] [Projection]
\ /
\ /
[对比损失 (InfoNCE)]
关键组件说明:
- ViT- 图像编码器:基于 Vision Transformer 架构,将图像分割为 patch 序列进行处理
- 文本编码器:通常使用 Transformer 架构处理文本序列
- Projection Head:将不同模态的特征映射到同一语义空间
- InfoNCE 损失:通过负样本对比学习实现模态对齐
代码实现
图像 / 文本预处理流水线
from torchvision import transforms
from transformers import AutoTokenizer
# 图像预处理
train_transform = transforms.Compose([transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize(mean=[0.48145466, 0.4578275, 0.40821073],
std=[0.26862954, 0.26130258, 0.27577711]
)
])
# 文本预处理
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
texts = ["a photo of a cat", "a picture of a dog"]
inputs = tokenizer(texts, padding=True, return_tensors="pt")
可扩展的 Projection Head 实现
import torch.nn as nn
class ProjectionHead(nn.Module):
def __init__(self, input_dim, output_dim=256):
super().__init__()
self.projection = nn.Linear(input_dim, output_dim)
self.gelu = nn.GELU()
self.fc = nn.Linear(output_dim, output_dim)
def forward(self, x):
x = self.projection(x)
x = self.gelu(x)
x = self.fc(x)
return x
带温度系数的对比损失函数
def contrastive_loss(logits, temperature=0.07):
# logits: (batch_size, batch_size)
labels = torch.arange(logits.size(0), device=logits.device)
loss_i = F.cross_entropy(logits/temperature, labels)
loss_t = F.cross_entropy(logits.t()/temperature, labels)
return (loss_i + loss_t) / 2
生产优化
混合精度训练
from torch.cuda.amp import autocast, GradScaler
scaler = GradScaler()
with autocast():
image_features = image_encoder(images)
text_features = text_encoder(input_ids, attention_mask)
loss = contrastive_loss(image_features, text_features)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
大 batch size 下的梯度累积
accumulation_steps = 4
for i, (images, texts) in enumerate(train_loader):
# Forward pass
loss = model(images, texts)
# Normalize loss
loss = loss / accumulation_steps
# Backward pass
loss.backward()
if (i+1) % accumulation_steps == 0:
optimizer.step()
optimizer.zero_grad()
ONNX 转换优化
torch.onnx.export(
model,
(dummy_image, dummy_text),
"clip_model.onnx",
input_names=["image", "text"],
output_names=["features"],
dynamic_axes={"image": {0: "batch_size"},
"text": {0: "batch_size"},
"features": {0: "batch_size"}
},
opset_version=13
)
避坑指南
- 文本 tokenizer 超长截断问题
-
解决方案:预处理时统计文本长度分布,合理设置 max_length
-
图像增强导致的模态泄露风险
-
注意点:避免使用过度改变语义的图像增强(如颜色反转)
-
余弦相似度计算的数值稳定性
- 技巧:添加小 epsilon 防止除以零
延伸思考
- 小样本场景优化:如何利用预训练 CLIP 模型进行 few-shot 学习?
- 多模态融合:除了对比学习,还有哪些有效的跨模态融合方法?
- 模型压缩:如何在保持性能的同时减小模型体积?
参考实现:
HuggingFace Spaces 演示
正文完
发表至: 未分类
近两天内
