【大模型】从零手写 CLIP:对比学习如何让模型“看懂“图文匹配?|多模态大模型专栏②

发布时间:2026/7/9 18:14:37
【大模型】从零手写 CLIP:对比学习如何让模型“看懂“图文匹配?|多模态大模型专栏② 从零手写 CLIP对比学习如何让模型看懂图文匹配多模态大模型专栏②一句话讲透 CLIP别去预测图像里有什么词而是让匹配的图文在向量空间里靠得最近。这就是对比学习contrastive learning也是所有现代 VLM「那双眼睛」的训练方法。写在前面上一篇 建立了 VLM 的三组件心智模型图像 →视觉编码器→ Projector → LLM。其中视觉编码器被形容为租来的眼睛——其权重在 VLM 训练开始前便已固定。那这双眼睛是怎么训练出来的答案就是CLIPRadford et al., 2021。它开创的对比学习范式至今仍是绝大多数 VLMLLaVA、Qwen-VL、InternVL视觉编码器的训练基础SigLIP 更是它的直接改进版、当前 VLM 的默认编码器。本篇从零手写一个 CLIP阅读后可获得✅ 画出 CLIP 的双塔架构说清图文如何被拉到同一个向量空间✅ 逐行理解 InfoNCE 损失知道对角线为什么是核心✅ 搞懂 CLIPsoftmax和 SigLIPsigmoid的本质区别以及为什么 SigLIP 不需要大 batch✅ 跑通一份不到 100 行的 PyTorch demo亲眼看见 loss 下降本文代码对应code/01_vision/02_clip_from_scratch.py可直接python运行。一、CLIP 的核心思想不预测只对比传统视觉模型是预测式的给定图像要求输出这是一只猫之类的判断。其局限在于类别必须预先定义猫、狗、车……难以泛化到新类别。CLIP 换了个思路——对比式给模型一批(图像, 文本)配对不要求它说出图像里有什么只要求它判断哪段文本和哪张图是配对的。具体说同一批里有 N 个配对模型要在一个 N×N 的匹配矩阵里把对角线正确配对的分数拉高把非对角线错误配对的分数压低。️ 图像 batch(N, 3, H, W)Image Encoder图像编码器img_emb(N, d)L2 归一化 文本 batchtoken ids (N, L)Text Encoder文本编码器text_emb(N, d)L2 归一化相似度矩阵S img_emb · text_embᵀ(N, N)对称 InfoNCE 损失对角线 正样本关键设计两个编码器把图像和文本分别压成d维向量并且共享同一个向量空间。训练后“一只猫的图片和文本a photo of a cat的向量会非常接近——这就是图文对齐”。 这个共享向量空间是 CLIP 留给后世最重要的遗产。VLM 的 Projector 干的活本质就是把 LLM 的向量空间对接到这个视觉向量空间上。二、逐行实现Step 1图像编码器真实 CLIP 的图像编码器是 ViT不熟的话可把它理解成把图切成 patch、当 token 喂给 Transformer详见01_vit_from_scratch.py。这里为了 demo 跑得快用几层卷积替代原理一样把图像压成d维向量。classImageEncoder(nn.Module):def__init__(self,d:int256):super().__init__()self.convnn.Sequential(nn.Conv2d(3,32,3,2,1),nn.GELU(),# 32x32 - 16x16nn.Conv2d(32,64,3,2,1),nn.GELU(),# 16x16 - 8x8nn.Conv2d(64,128,3,2,1),nn.GELU(),# 8x8 - 4x4nn.AdaptiveAvgPool2d(1),# 全局池化 - 1x1nn.Flatten(),)self.projnn.Linear(128,d)# 压到 d 维defforward(self,x):returnself.proj(self.conv(x))# (N, d)Step 2文本编码器文本侧更简单词嵌入 masked 平均池化 一层线性。真实 CLIP 用 Transformer这里同样简化。classTextEncoder(nn.Module):def__init__(self,vocab_size1000,d256):super().__init__()self.embednn.Embedding(vocab_size,d)self.projnn.Linear(d,d)defforward(self,token_ids,attention_mask):xself.embed(token_ids)*attention_mask.unsqueeze(-1).float()# masked mean pool只在非 padding 位置上取平均xx.sum(dim1)/attention_mask.sum(dim1,keepdimTrue).clamp(min1)returnself.proj(x)# (N, d)Step 3归一化 可学习温度CLIP 的两个灵魂细节把两路输出都L2 归一化这样点积就是余弦相似度取值落在[-1, 1]训练稳定。再乘一个可学习的温度logit_scaleclassCLIP(nn.Module):def__init__(self,d256,vocab_size1000,init_temperature0.07):super().__init__()self.image_encoderImageEncoder(d)self.text_encoderTextEncoder(vocab_size,d)# 可学习温度初始 0.07论文设置self.logit_scalenn.Parameter(torch.tensor(1.0/init_temperature).log())defencode_image(self,x):returnF.normalize(self.image_encoder(x),dim-1)# ← L2 归一化defencode_text(self,ids,mask):returnF.normalize(self.text_encoder(ids,mask),dim-1)defforward(self,image,text_ids,text_mask):img_embself.encode_image(image)# (N, d)txt_embself.encode_text(text_ids,text_mask)logit_scaleself.logit_scale.exp().clamp(max100.0)logits_per_imagelogit_scale*img_emb txt_emb.t()# (N, N)returnlogits_per_image,logits_per_image.t()温度为什么重要温度越小softmax 越尖锐模型对正确配对和错误配对的区分越狠。CLIP 训练后学到的温度 ≈ 0.01非常尖锐这说明模型变得极度自信。Step 4对称 InfoNCE 损失这是 CLIP 的心脏。给定 N×N 相似度矩阵每一行的正确答案就是对角线那个位置defclip_loss(logits_per_image,logits_per_text):Nlogits_per_image.shape[0]labelstorch.arange(N)# [0, 1, 2, ..., N-1] ← 对角线loss_iF.cross_entropy(logits_per_image,labels)# 图→文loss_tF.cross_entropy(logits_per_text,labels)# 文→图return(loss_iloss_t)/2# 对称labels [0,1,...,N-1]这行是精髓它告诉交叉熵第 0 行的正确列是第 0 列第 1 行是第 1 列……——正好是对角线。两个方向的 loss 取平均保证图能找文、文也能找图。三、一个 N×N 矩阵看懂全部光看代码不直观。假设 batch 里有 3 个图文对训练后理想状态下的相似度矩阵长这样文本A「一只猫」文本B「沙滩海浪」文本C「城市夜景」图A✅0.850.100.12图B️0.08✅0.900.15图C0.110.13✅0.88✅对角线 正样本配对图A 配 文本ACLIP 要把它们拉到最大其余格子 负样本配对图A 配 文本B/C要压到最小InfoNCE 在每一行做 softmax目标是让对角线那一列的概率最大。用公式写就是L img − 1 N ∑ i 1 N log ⁡ exp ⁡ ( s i i / τ ) ∑ j 1 N exp ⁡ ( s i j / τ ) \mathcal{L}_{\text{img}} -\frac{1}{N}\sum_{i1}^{N} \log \frac{\exp(s_{ii}/\tau)}{\sum_{j1}^{N}\exp(s_{ij}/\tau)}Limg​−N1​i1∑N​log∑j1N​exp(sij​/τ)exp(sii​/τ)​其中s i j s_{ij}sij​是第i ii张图和第j jj段文本的相似度τ \tauτ是温度。文本侧对称地做一遍两者求平均。配图说明上表用 emoji ✅ 高亮对角线发布即可。想更直观可改成 mermaid 热力图但表格在移动端可读性更好建议保留表格。直觉记忆CLIP 的全部工作就是把这个矩阵的对角线擦亮、非对角线擦暗。四、InfoNCE vs SigLIP为什么后者成了 VLM 默认编码器CLIP 虽经典但有个硬伤softmax 把一行的 N 个格子耦合在一起意味着必须有足够多的负样本大 batch才能学到好的对比。OpenAI 训 CLIP 用了 batch size 32768普通实验室根本玩不起。SigLIPZhai et al., 2023的改进极简却致命别 softmax 了每个格子独立做二分类。defsiglip_loss(img_emb,txt_emb,logit_scale,bias):Nimg_emb.shape[0]logitslogit_scale*(img_emb txt_emb.t())bias# (N, N)labelstorch.eye(N)# 单位阵对角线1正其余0负returnF.binary_cross_entropy_with_logits(logits,labels)torch.eye(N)生成单位阵——对角线是 1正样本其余是 0负样本每个格子独立算 BCE互不耦合。完整对比CLIPInfoNCE / softmaxSigLIPsigmoid每行操作softmax over N 个候选每个 cell 独立 BCE正样本对角线是 argmax 目标对角线 label 1负样本同行其余 N-1 个非对角线 label 0对 batch 的要求大数千级小 batch 也行靠大累计随机初始化 loss≈ ln(N)N8 时 ≈2.08≈ ln(2) ≈ 0.69当前地位经典开山VLM 默认编码器 这就是为什么第一篇里说 SigLIP-SO400M 是当前 VLM 的租来的眼睛——它把 CLIP 的对齐思想保留下来又摆脱了对超大 batch 的依赖让中等规模数据/算力也能训出强编码器。Qwen-VL、InternVL 的视觉侧都用它。五、跑起来完整 demo把上面拼起来跑一个 N8 的假数据 demodefdemo():torch.manual_seed(0)N,d,vocab8,256,100imagestorch.randn(N,3,32,32)text_idstorch.randint(0,vocab,(N,10))text_masktorch.ones(N,10,dtypetorch.long)modelCLIP(dd,vocab_sizevocab)logits_i,logits_tmodel(images,text_ids,text_mask)print( CLIP (softmax / InfoNCE) )print(fsimilarity matrix shape:{tuple(logits_i.shape)})print(fCLIP loss:{clip_loss(logits_i,logits_t).item():.4f})# 同一组 embedding 上对比 SigLIPimg_embmodel.encode_image(images)txt_embmodel.encode_text(text_ids,text_mask)print(\n SigLIP (sigmoid) )print(fSigLIP loss:{siglip_loss(img_emb,txt_emb,model.logit_scale.exp(),torch.zeros(1)).item():.4f})预期输出随机初始化时 CLIP (softmax / InfoNCE) similarity matrix shape: (8, 8) CLIP loss: ~2.20 # ≈ ln(8)因为 8 个候选随机猜 SigLIP (sigmoid) SigLIP loss: ~0.69 # ≈ ln(2)每个 cell 像抛硬币这两个初始 loss 值非常值得记住CLIP 初始 ≈ ln(N)因为每行要在 N 个候选里 softmax全随机时正确概率 1/Nloss -ln(1/N) ln(N)SigLIP 初始 ≈ ln(2)每个 cell 是独立的二分类全随机时就是抛硬币loss -ln(0.5) ln(2)随着训练对角线被擦亮两个 loss 都会趋近 0。动手建议将N依次改为 16、32可观察到 CLIP 的初始 loss 随 ln(N) 增长而 SigLIP 始终保持在 0.69 附近。这一对比能直观揭示为何 CLIP 依赖大 batch。六、CLIP 何以成为所有 VLM 的眼睛回到专栏主线。CLIP/SigLIP 训练完后图像编码器的权重就冻结了它会把任何图像编码成一个高质量的视觉向量。后续所有 VLM 工作LLaVA直接拿 CLIP-ViT-L 当眼睛后面接 MLP Projector → LLMQwen-VL / InternVL拿 SigLIP-SO400M 当眼睛配 M-RoPE 动态分辨率所以搞懂 CLIP 的对比学习就搞懂了 VLM 视觉侧的根基。Projector 要做的维度翻译翻译的源语言正是 CLIP 学到的这个视觉向量空间。 本篇涉及代码文件说明code/01_vision/02_clip_from_scratch.py本文核心手写 CLIP SigLIP 损失含 InfoNCE vs sigmoid 对比code/01_vision/01_vit_from_scratch.py最小 ViT 实现CLIP 图像编码器的真实形态小结概念一句话CLIP 目标把匹配图文拉近、不匹配拉远不预测具体类别双塔结构图像编码器 文本编码器输出到同一d 维空间InfoNCEN×N 矩阵每行 softmax对角线是正样本温度 τ可学习越小对比越尖锐SigLIP 改进每格独立 BCE摆脱大 batch 依赖 → VLM 默认编码器写在最后下一篇是专栏的张量级 walkthrough我们会用一个零依赖纯 Python demo跟着一个 batch 的张量从头走到尾亲眼看「图像 → patch → ViT → Projector → 拼进文本流 → LLM」每一步的 shape 变化。关注专栏更新第一时间通知。专栏《从 NLP 到 VLM多模态大模型研发实战》上一篇① 多模态大模型全景图 本篇② 手写 CLIP 下一篇③ 张量级端到端 walkthrough