Vision-Language多模态:CLIP模型与图文对齐详解
引言
在人工智能领域,视觉感知和语言理解曾经是两个相对独立的赛道。
Vision-Language多模态技术,正是搭建在这两者之间的一座桥梁——它让机器既能“看懂”图像,也能“读懂”文字,更关键的是,能把这两种不同的信息拉通对齐。
2021年,OpenAI提出的 CLIP(Contrastive Language-Image Pre-training) 就是这座桥梁上的一个里程碑。
它仅靠一种朴素的对比学习方法,在海量的图文对上训练,就把图像和文字映射到了同一个语义空间,从而具备了开箱即用的零样本分类、图文检索等能力。后续火爆的 DALL·E、Midjourney 等文生图模型,也离不开这类对齐思路的铺垫。
📂 所属阶段:第二阶段 — 深度学习视觉基础(CNN 篇)
🔗 相关章节:MAE (Masked Autoencoders) · 模型轻量化
1. 多模态基础与CLIP的任务定位
1.1 视觉-语言的核心应用场景
多模态学习不是纸上谈兵,它已经渗透到大量实际产品中。下面这张表帮你快速建立印象:
1.2 一句话理解CLIP
CLIP就是一个通用的图文语义对齐器。
它的训练目标非常朴素:让原本“匹配”的图像和文字在特征空间中靠得更近,而“不匹配”的图文对离得更远。
这种思路看似简单,但因为使用了大规模弱监督数据(互联网上天然存在的图文配对),最终学到的对齐效果出奇地好。
2. CLIP核心技术拆解
2.1 双编码器架构:图像一路,文本一路
CLIP 的结构可以用一个词来形容:双塔。
图像和文本各走各的编码器,中间没有任何跨模态的注意力机制,只在最后通过简单计算来比较相似度。
- 图像编码器(Image Encoder):可以用 ResNet,也可以用 Vision Transformer (ViT)。
经典强模型使用的是 ViT‑L/14,即 Large 级别的 ViT,输入图像被切成 14×14 大小的块。
- 文本编码器(Text Encoder):就是 Transformer 的编码器部分(类似于 BERT,但没有解码器)。
- 投影层(Projection):分别把图像特征和文本特征映射到同一个维度(例如 512 或 768),再进行 L2 归一化,使得所有向量都落在单位球面上。
这样的设计有两个明显好处:
- 速度快:推理时,图像编码和文本编码可以完全独立进行,甚至可以提前算好文本特征;
- 部署友好:图像服务用 GPU,文本服务用 CPU,互不干扰。
下面是精简但完整的 PyTorch 实现,为了聚焦核心逻辑,略去了一些初始化参数细节。
图像编码器(ViT 简化版)
import torch
import torch.nn as nn
import torch.nn.functional as F
class ImageEncoder(nn.Module):
def __init__(self, embed_dim=512, img_size=224, patch_size=16,
vision_width=768, vision_layers=12):
super().__init__()
num_patches = (img_size // patch_size) ** 2
# 把图像切成小块,并用卷积将每个小块投影到 vision_width 维度
self.patch_embed = nn.Conv2d(3, vision_width, patch_size, patch_size, bias=False)
# 学习一个类别 token(类似 BERT 的 [CLS])
self.cls_token = nn.Parameter(torch.randn(1, 1, vision_width))
# 位置编码
self.pos_embed = nn.Parameter(torch.randn(1, num_patches + 1, vision_width))
self.ln_pre = nn.LayerNorm(vision_width)
# Transformer 编码器
self.transformer = nn.TransformerEncoder(
nn.TransformerEncoderLayer(vision_width, nhead=12, batch_first=True),
num_layers=vision_layers
)
self.ln_post = nn.LayerNorm(vision_width)
# 投影到统一的嵌入维度
self.proj = nn.Parameter(torch.randn(vision_width, embed_dim))
def forward(self, x):
# x: (B, 3, H, W)
x = self.patch_embed(x).flatten(2).transpose(1, 2) # (B, num_patches, vision_width)
# 在前面拼接 class token
x = torch.cat([self.cls_token.expand(x.shape[0], -1, -1), x], dim=1)
x = x + self.pos_embed
x = self.ln_pre(x)
x = self.transformer(x)
# 取出 class token 对应的输出,再通过投影矩阵映射
x = self.ln_post(x[:, 0, :]) @ self.proj
return x
文本编码器(简化版)
class TextEncoder(nn.Module):
def __init__(self, embed_dim=512, context_len=77, vocab_size=49408,
text_width=512, text_layers=12):
super().__init__()
self.token_embed = nn.Embedding(vocab_size, text_width)
self.pos_embed = nn.Parameter(torch.randn(1, context_len, text_width))
self.transformer = nn.TransformerEncoder(
nn.TransformerEncoderLayer(text_width, nhead=8, batch_first=True),
num_layers=text_layers
)
self.ln_final = nn.LayerNorm(text_width)
self.proj = nn.Parameter(torch.randn(text_width, embed_dim))
def forward(self, text):
# text: (B, 77),每个序列的结束符是最后出现的特殊 token(eot_token)
x = self.token_embed(text) + self.pos_embed
x = self.transformer(x)
x = self.ln_final(x)
# 取 eot_token 位置的输出进行投影
x = x[torch.arange(x.shape[0]), text.argmax(dim=-1)] @ self.proj
return x
2.2 对比学习与 InfoNCE 损失
CLIP 的训练灵魂,是一种叫做 InfoNCE 的对比损失。理解它,你才能真正理解 CLIP。
假设一个批次里有 N 对图文,那么:
- 正样本只有 N 对:第 i 张图和第 i 条文本是匹配的。
- 负样本则非常多:图 i 与所有文本 j(j≠i);文本 i 与所有图像 j(j≠i)。总共 N×(N‑1) 个负样本。
CLIP 同时做两件事:
- 让图去找正确的文本:计算每张图与所有文本的相似度,希望第 i 张图与第 i 条文本最相似。
- 让文本去找正确的图:同理,希望每条文本也能准确找回自己的“原配”图像。
这两部分合在一起,就是 双向对比损失。
损失函数实现
代码会比你想象的还要简单:
class CLIPLoss(nn.Module):
def __init__(self, temperature=0.07):
super().__init__()
# 温度参数:控制相似度矩阵的“锐度”
# 设为一个可学习的 log 值,训练过程中也会调整
self.temperature = nn.Parameter(torch.tensor(temperature).log())
def forward(self, img_feat, text_feat):
# 1. L2 归一化,确保向量长度都为 1
img_feat = F.normalize(img_feat, dim=-1)
text_feat = F.normalize(text_feat, dim=-1)
# 2. 计算相似度矩阵,并乘以温度系数的倒数(即放缩)
logits_per_img = img_feat @ text_feat.t() * self.temperature.exp()
logits_per_text = logits_per_img.t()
# 3. 构造标签:第 i 张图的正确答案就是文本 i
batch_size = img_feat.shape[0]
labels = torch.arange(batch_size, device=img_feat.device)
# 4. 两个方向的交叉熵损失,取平均
loss_img = F.cross_entropy(logits_per_img, labels)
loss_text = F.cross_entropy(logits_per_text, labels)
return (loss_img + loss_text) / 2
- 温度越低,相似度分布越“尖锐”,模型会更关注那些非常确定的负样本,容易产生过拟合;
- 温度越高,分布越平滑,负样本的区分度会下降。
预训练中默认使用 **0.07** 是一个经验上的平衡点。如果训练初期不稳定,可以先设置为 0.1 再逐渐降低。
3. CLIP 的杀手锏:零样本分类
3.1 它为什么能“无师自通”?
传统图像分类模型,需要事先定义好类别,并且每个类别都要有大量标注数据。
CLIP 完全跳过了这一步——它不需要任何下游标注,甚至不需要知道具体有哪些类别。
原理其实很直接:
- 你希望分类的每个类别,都可以用自然语言描述出来,比如 “cat”、“a photo of a dog”。
- CLIP 把这些类别文本编码成向量,作为这个类别的“模板”。
- 把待分类的图片也编码成向量。
- 比较图像向量和所有类别文本向量的相似度,最相似的那个就是分类结果。
为了提高鲁棒性,实际使用时不会只用单一描述,而是构造一组语义相近的提示模板(prompt templates),例如:
- “a photo of a {}”
- “a blurry photo of a {}”
- “a close-up of a {}”
最后把这些模板的相似度取平均,就能得到更稳定的预测。
3.2 动手体验:用 Hugging Face 实现零样本分类
原始 CLIP 对 PyTorch 版本有特定要求,现在更方便的方式是直接使用 Hugging Face 的 transformers 库。
下面这个例子,你只要装好 transformers 和 pillow 就能直接运行:
from transformers import CLIPProcessor, CLIPModel
from PIL import Image
import torch
# 1. 加载模型和处理器(ViT‑B/32 速度快,ViT‑L/14 精度更高)
device = "cuda" if torch.cuda.is_available() else "cpu"
model_name = "openai/clip-vit-base-patch32"
processor = CLIPProcessor.from_pretrained(model_name)
model = CLIPModel.from_pretrained(model_name).to(device)
# 2. 准备图像和类别描述(配合提示模板)
image = Image.open("cat_dog.jpg").convert("RGB")
class_names = ["cat", "dog", "bird", "car"]
templates = [
"a photo of a {}",
"a photo of the {}",
"a blurry photo of a {}",
"a close-up of a {}"
]
# 将所有模板组合成完整的句子列表
texts = [template.format(cls) for cls in class_names for template in templates]
# 3. 预处理并推理
inputs = processor(text=texts, images=image, return_tensors="pt", padding=True).to(device)
with torch.no_grad():
outputs = model(**inputs)
# 4. 计算每个类别的平均相似度,并转换为概率
logits_per_img = outputs.logits_per_image # shape: [1, num_texts]
# 按类别重新分组,取平均
class_logits = logits_per_img.view(len(class_names), len(templates)).mean(dim=-1)
probs = class_logits.softmax(dim=-1).cpu().numpy()[0]
for cls, prob in zip(class_names, probs):
print(f"{cls}: {prob:.2%}")
即使模型从没见过你提供的图片类型,也能给出一个相当靠谱的分类结果,这正是零样本泛化的魅力。
4. CLIP 的优缺点与改进方向
4.1 一张表看懂它的长与短
4.2 从 CLIP 出发,已经衍生出哪些改进工作?
CLIP 像一把瑞士军刀,但不同场景下需要更精密的工具。目前主流的改进方向包括:
- 数据效率:
- ALBEF:引入动量蒸馏和额外的图文匹配任务,减少对数据量的依赖。
- BLIP‑2:分别冻结一个现成的大语言模型和一个视觉模型,只训练一个轻量的 Q‑Former 来桥接两者,极大降低了计算开销。
- 细粒度理解:
- FLAVA:同时做全局对齐和局部区域-单词对齐。
- CLIP‑Dissect:尝试解耦 CLIP 的语义表示,明白它到底“学到了什么”。
- 垂直领域适配:
- MedCLIP:专门针对医学图像和临床文本进行预训练。
- AgriCLIP:农业场景下的图文对齐。
5. 总结
CLIP 的核心贡献,并不在于结构有多复杂,而在于它用了一种极简的范式,证明了大规模弱监督数据 + 对比学习足以打通视觉和语言之间的壁垒。
正因为思路足够干净,CLIP 已经从一个模型变成了一种“基础设施”:几乎所有涉及图文对齐的任务,都可以用它作为起点。
虽然我们很少从零预训练一个 CLIP,但它的开源预训练权重,以及 Hugging Face 等社区提供的开箱即用方案,让我们能够方便地将这种能力集成到自己的项目里。
1. 先吃透对比学习和 InfoNCE 损失是怎么工作的;
2. 用 Hugging Face 跑通零样本分类和图文检索,亲手体验效果;
3. 再回过头来研究 ViT 和 Text Encoder 的内部细节,理解每一层的计算。
🔗 核心参考论文