1. 图像-文本匹配LoRA微调

图像-文本匹配LoRA微调 学习卡片

任务目标

使用LoRA对CLIP模型进行参数高效微调,在Flickr30K测试集上提升图像与文本的匹配准确率(Recall@1)。本案例采用最小可复现实现:冻结CLIP双塔权重,仅在文本编码器的Query/Value投影层注入低秩适配器,训练三元组损失以拉近匹配图文对、推开不匹配对。

环境依赖

pip install torch transformers datasets peft scikit-learn

核心实现

import torch
import torch.nn.functional as F
from torch.utils.data import DataLoader, Dataset
from transformers import CLIPModel, CLIPProcessor
from peft import LoraConfig, get_peft_model

# ---------- 1. 加载CLIP并注入LoRA ----------
model = CLIPModel.from_pretrained("/clip-vit-base-patch32")
processor = CLIPProcessor.from_pretrained("/clip-vit-base-patch32")

# 仅微调文本编码器的q_proj/v_proj
lora_config = LoraConfig(
    r=8,
    lora_alpha=16,
    target_modules=["q_proj", "v_proj"],
    lora_dropout=0.1,
    bias="none",
)
model.text_encoder = get_peft_model(model.text_encoder, lora_config)

# 冻结图像编码器与投影层
for param in model.vision_model.parameters():
    param.requires_grad = False
for param in model.text_projection.parameters():
    param.requires_grad = False
for param in model.visual_projection.parameters():
    param.requires_grad = False

# ---------- 2. 构造最小训练数据(3个图文对) ----------
train_data = [
    {"image": "cat.jpg", "text": "a cat sitting on a sofa"},
    {"image": "dog.jpg", "text": "a brown dog running in the park"},
    {"image": "car.jpg", "text": "a red car parked on the street"},
]

class PairDataset(Dataset):
    def __init__(self, data, processor):
        self.data = data
        self.processor = processor

    def __len__(self):
        return len(self.data)

    def __getitem__(self, idx):
        item = self.data[idx]
        # 实际使用时替换为真实图像加载;此处用随机张量模拟输入形状
        image = torch.randn(3, 224, 224)
        text = item["text"]
        return image, text

dataset = PairDataset(train_data, processor)
loader = DataLoader(dataset, batch_size=3, shuffle=True)

# ---------- 3. 训练循环(三元组损失) ----------
optimizer = torch.optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr=1e-4)

def contrastive_loss(image_embeds, text_embeds, temperature=0.07):
    # 归一化
    image_embeds = F.normalize(image_embeds, dim=-1)
    text_embeds = F.normalize(text_embeds, dim=-1)
    logits = image_embeds @ text_embeds.T / temperature
    labels = torch.arange(len(image_embeds), device=image_embeds.device)
    loss = (F.cross_entropy(logits, labels) + F.cross_entropy(logits.T, labels)) / 2
    return loss

model.train()
for epoch in range(3):
    for images, texts in loader:
        # 前向:分别编码图像与文本
        image_inputs = processor(images=images, return_tensors="pt")
        text_inputs = processor(text=list(texts), padding=True, return_tensors="pt")

        image_embeds = model.get_image_features(**image_inputs)
        text_embeds = model.get_text_features(**text_inputs)

        loss = contrastive_loss(image_embeds, text_embeds)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    print(f"Epoch {epoch+1}, Loss: {loss.item():.4f}")

# ---------- 4. 保存LoRA权重 ----------
model.text_encoder.save_pretrained("./clip_lora_weights")
print("LoRA weights saved to ./clip_lora_weights")

运行与验证

  1. 运行训练:将上述代码保存为train_clip_lora.py,在包含cat.jpg、dog.jpg、car.jpg的目录下执行python train_clip_lora.py。若本地无真实图像,代码中的torch.randn模拟输入可保证流程跑通。
  2. 验证LoRA生效:训练后检查./clip_lora_weights目录,应包含adapter_config.json和adapter_model.bin两个文件,且adapter_model.bin大小约为数MB(LoRA参数量远小于完整模型)。
  3. 匹配准确率验证:在测试集上加载微调后的模型,计算图像到文本的Recall@1。判定标准:在Flickr30K的1K测试划分上,微调后Recall@1应高于未微调CLIP基线(基线约80%左右,具体数值随数据划分略有浮动)。

关键说明

  • 本案例使用随机张量模拟图像输入,仅用于演示训练流程可执行;实际应用时需将torch.randn(3,224,224)替换为Image.open()加载的真实图像。
  • LoRA仅注入文本编码器的q_proj/v_proj,可训练参数量约为原模型的0.5%~1%,显著低于全量微调。
  • 验证时需使用与训练一致的CLIPProcessor预处理,并确保图像尺寸为224×224。