【大模型学习 | CLIP 原理代码实现】
## Learning Transferable Visual Models From Natural Language Supervision
作者在摘要中指出,传统的监督式学习方法限制了视觉模型的泛化能力,特别是在迁移到新任务或新类别时的能力有限。以往的图像识别任务通常依赖于人为定义的分类标签进行训练,这种方式不仅数据成本高,而且模型更容易过拟合于训练类别。为了解决这一问题,CLIP 提出了一个新的预训练框架:利用网络上现成的大规模图文对(如标题+图像)作为监督信号,将图像与自然语言描述进行匹配,从而在无需特定分类标签的情况下,学习具有通用性的视觉表征。
一、数据缺陷
🔴 目前的图像数据存在着质量低、数据量不足的情况,例如 ① MS-COCO 虽然是高质量的标注数据,但也只有十万张图像,在目前的视觉系统来说属于小数据量; ② YFCC100M 有 一亿张图像,但是质量低,过滤后只有1500万张图像,与 ImageNet数据大小相同;现有视觉系统尚未能充分利用这些语言资源中的监督信息,限制了自然语言在视觉模型中的潜力发挥。
🟢 作者构建了一个4亿对的数据集WIT
(图像,文本对),根据50万个关键词搜集相关的图文对,最后数据的文本数与GPT-2训练所需数据集大小相似;
二、预训练方法
🔴 作者首先尝试了直接采用联合学习的方式,采用CNN和Transformer来预测图像标题;(Transformer采用了6300万个参数,识别图像的类别会比训练一个词袋模型慢三倍);这两种方法来学习识别图像都有一个相同点:预测图像的准确文字;
🟢 作者提出一种图文匹配的更简单的代理任务,而不是精确预测每个单词;给定 N × N N \times N N×N 的图文对,在 CLIP 中,图像和文本分别通过两个独立的编码器(视觉 Transformer 和文本 Transformer)进行编码,基于余弦相似度学习多模态的嵌入空间,最大化配对图文之间的相似度,最小化不匹配对的相似度,并通过symeertric entropy loss
优化相似得分:
🟢 训练模型并没有采用预训练权重模型,而是从头训练;并通过线性映射将不同模态编码器的表示映射到嵌入空间中;
🧠 为什么不采用非线性映射?
- 作者并没有发现线性映射和非线性映射的差别,并推测非线性映射在一些纯自监督任务中(如 SimCLR、MoCo)可能会适应图像中更细节的特征,形成一种“共适应(co-adaptation)”,但在 CLIP 这样具有明确语言监督的图文对齐任务中,线性映射已经足够表达语义关系,且更稳定、更易训练;
# 线性映射文本、图像# 线性映射文本、图像# 线性映射文本、图像# 线性映射文本、图像te'z
I_e = l2_normalize(np.dot(I_f, W_i), axis=1)
T_e = l2_normalize(np.dot(T_f, W_t), axis=1)# 图文相似矩阵向量
logits = np.dot(I_e, T_e.T) * np.exp(t)师弟# symeertric entropy loss
loss_i = cross_entropy_loss(logits, labels, axis=0) # 图像 → 文本,即图像作为query
loss_t = cross_entropy_loss(logits, labels, axis=1) # 文本 → 图像,即文本作为query
loss = (loss_i + loss_t) / 2
🟢 在图像特征提取模型上,作者采用了两种架构: ① ResNet modifications (We also replace the global average pooling layer with an attention pooling mechanism.) ② Vit (with only the minor modification of adding an additional layer normalization)
🧠 为什么在将 patch + 位置编码输入 Transformer 之前加入layer normalization?
- 稳定训练过程:合并后的向量(patch + 位置编码)可能存在数值不一致(例如 patch embedding 和 position embedding 的分布不同),LayerNorm 有助于规范化 patch + 位置编码的数值分布,防止某些 token 占主导。
🟢 文本编码器:① CBOW ② Transformer 12层 512维 8个注意力头
维度 | 视觉编码器(如 ResNet) | 文本编码器(Transformer) |
---|---|---|
宽度 | 增加通道数 | 等比例增加 d_model |
深度 | 增加层数 | 固定不变 |
分辨率 | 增加输入图像尺寸 | 不适用(文本长度固定) |
三、代码实现
from PIL import Image
import requestsfrom transformers import CLIPProcessor, CLIPModelmodel = CLIPModel.from_pretrained("openai/clip-vit-large-patch14")
processor = CLIPProcessor.from_pretrained("openai/clip-vit-large-patch14")url = "http://images.cocodataset.org/val2017/000000039769.jpg"
image = Image.open(requests.get(url, stream=True).raw)inputs = processor(text=["a photo of a cat", "a photo of a dog"], images=image, return_tensors="pt", padding=True)outputs = model(**inputs)
logits_per_image = outputs.logits_per_image # this is the image-text similarity score
probs = logits_per_image.softmax(dim=1) # we can take the softmax to get the label probabilities
- QAT动态量化
from transformers import CLIPModel
import torch
import torch.quantizationmodel = CLIPModel.from_pretrained("openai/clip-vit-large-patch14")# 对 Linear 层做动态量化
quantized_model = torch.quantization.quantize_dynamic(model,{torch.nn.Linear}, # 可以加上 torch.nn.LayerNorm 等dtype=torch.qint8
)# 保存模型
torch.save(quantized_model.state_dict(), "clip_quantized.pth")
- 微调
import torch
from torch import nn
from torch.utils.data import DataLoader, Dataset
from transformers import CLIPProcessor, CLIPModel
from PIL import Image# 1. 加载模型和 processor
model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")# 2. 冻结图像和文本编码器(只训练投影头)
for param in model.vision_model.parameters():param.requires_grad = False
for param in model.text_model.parameters():param.requires_grad = False# 3. 自定义小数据集(图像路径 + 对应文本)
class MyDataset(Dataset):def __init__(self, image_paths, texts):self.image_paths = image_pathsself.texts = textsdef __len__(self):return len(self.image_paths)def __getitem__(self, idx):image = Image.open(self.image_paths[idx]).convert("RGB")text = self.texts[idx]return image, text# 替换为你自己的图像路径和描述文本
image_paths = ["cat.jpg", "dog.jpg", "apple.jpg"]
texts = ["a photo of a cat", "a photo of a dog", "a photo of an apple"]
dataset = MyDataset(image_paths, texts)
dataloader = DataLoader(dataset, batch_size=2, shuffle=True)# 4. 定义优化器(只优化 projection 层)
optimizer = torch.optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr=5e-5)
device = "cuda" if torch.cuda.is_available() else "cpu"
model.to(device)# 5. 训练 loop
for epoch in range(10):model.train()total_loss = 0for images, texts in dataloader:inputs = processor(text=texts, images=images, return_tensors="pt", padding=True, truncation=True).to(device)outputs = model(**inputs)logits_per_image = outputs.logits_per_image # [batch, batch]logits_per_text = outputs.logits_per_text # [batch, batch]ground_truth = torch.arange(len(images)).to(device)# CLIP 的双向对比损失loss_i = nn.CrossEntropyLoss()(logits_per_image, ground_truth)loss_t = nn.CrossEntropyLoss()(logits_per_text, ground_truth)loss = (loss_i + loss_t) / 2loss.backward()optimizer.step()optimizer.zero_grad()total_loss += loss.item()print(f"Epoch {epoch+1}, Loss: {total_loss:.4f}")