
简介对比语言-图像预训练CLIP是一套基于PyTorch实现的多模态预训练模型代码包面向深度学习研究者与计算机视觉开发者旨在解决零样本图像分类、图文匹配等任务中需要大量标注数据与特定微调的问题。资源包含完整的模型定义、推理逻辑、词表文件及配套说明并附带Jupyter Notebook交互式示例可直观体验Prompt Engineering与CLIP在ImageNet上的零样本预测过程非常适合学习多模态表征与对照复现。整个压缩包共16个文件以Python源码和Notebook为主体辅以Markdown文档、词表压缩包、配置与测试脚本打包后仅3.87MB结构清晰、轻量易用。目前已有1413人学习浏览适合具备Python和深度学习基础、希望快速上手CLIP零样本能力的读者。1. CLIP对比语言-图像预训练一个不训练就能分类的模型做图像分类多年我一直默认一个前提换一个数据集就要重新标数据、训模型。直到用 CLIP 跑了一次零样本分类——不训练、不微调只给模型一组文本标签它就能在自定义类别上出结果某些场景下甚至超过此前微调过的小模型。CLIPContrastive Language-Image Pre-training是一种把图像和文本对齐到同一个向量空间的预训练模型。它用海量图文对做对比学习训练完成后图像编码器和文本编码器输出的向量可以直接比较所以分类头不再是你设计的而是文本标签本身。对做内容理解、检索排序、冷启动分类的从业者来说这个模型值得作为基线先跑一遍。本文从预训练原理讲起配合可复现的推理代码最后把常见的坑一条条拆开。2. CLIP 的原理与训练配方从双塔对比到温度参数2.1 双塔结构图像与文本如何共用一个向量空间CLIP 的核心不是单个模型而是两个编码器并行工作。图像侧可以是 ResNet 或 ViT文本侧是 Transformer。两个编码器各自把输入映射成一个特征向量再投影到同一个维度空间。训练的目标就是让匹配的图文对在这个空间里靠得近不匹配的离得远。我在实际使用时一般直接用 ViT 版本因为图像侧的特征提取质量更高而且 open_clip 里预训练权重更全。双塔结构带来的直接好处是图像和文本可以分别独立编码、独立缓存。也就是说你可以把一万张图的特征提前算好存起来来了新文本只算文本侧不用重新过一遍图像编码器。这个特性在检索和召回场景里非常关键。文本编码器的输出需要做 LayerNorm 和投影图像编码器的最后一层也会经过投影层两边的输出维度必须一致。ViT-B/32 是 512 维ViT-L/14 是 768 维ViT-H/14 是 1024 维。特征维度越高空间容量越大但对显存和计算的要求也水涨船高。我通常在项目起步阶段先用 ViT-B/32 跑通流程确认方向没问题再换大模型。import torch import open_clip # 常见做法先用 ViT-B/32 验证流程确认效果后再切 ViT-L/14 model, _, preprocess open_clip.create_model_and_transforms( ViT-B-32, pretrainedlaion2b_s34b_b79k, ) model.eval() device cuda if torch.cuda.is_available() else cpu model model.to(device)上面的代码做了三件事创建模型结构、加载预训练权重、把模型切到推理模式。preprocess是配套的图像预处理流程包含缩放、裁剪和归一化后面所有图像输入都必须走它不能自己另写一套。pretrained参数指定权重来源不同权重的数据分布和效果有差异固定一个版本便于复现。2.2 InfoNCE 与对称交叉熵batch 内部如何构造正负样本预训练阶段的目标函数是 InfoNCE 的变体。假设一个 batch 里有 N 对图文对图像特征和文本特征两两计算相似度得到 N×N 的相似度矩阵。对角线上的 N 个元素是匹配的图文对其余 N²-N 个是不匹配的负样本。对每一行做 softmax拉大对角线的概率这就是 image-to-text 方向的损失。反过来对每一列做同样的操作就是 text-to-image 方向的损失。两个方向相加再取平均CLIP 原文里叫对称交叉熵。温度参数在这里扮演放大镜的角色。相似度除以温度再进 softmax温度越低分布越尖锐模型对正负样本的区分越苛刻。这个温度不是一个手工设定的超参而是一个可学习的参数初始化为 log(1/0.07)约等于 2.66训练结束后通常落在 4.6 左右对应的温度大约是 0.011。我见过不少人在复现零样本分类时直接把相似度乘一个 100 当作缩放系数这在多数情况下和最终训练好的 logit scale 接近但不是所有模型都一样。正确做法是从模型里把这个参数读出来用不要硬编码。logit_scale model.logit_scale.exp() print(logit_scale)这段代码读取模型内部的温度缩放参数并转成实际倍数。不同预训练权重的 logit_scale 不一样硬编码 100 会让某些模型的输出过锐或过钝直接影响分类结果的置信度分布。2.3 数据与训练配方4 亿图文对、三万级 batch 与 prompt 的作用CLIP 的预训练数据大约有 4 亿图文对来源是互联网抓取。这带来一个特点数据的语义覆盖面极广但噪音也大文本可能和图像弱相关甚至不相关。大规模数据配合对比学习让模型学会了更鲁棒的语义对齐而不是死记硬背某一个数据集。batch size 在预训练中直接决定负样本数量。CLIP 原文把 batch size 拉到了三万以上原因很简单负样本对越多对比学习越充分。这个概念一直影响到你后续的微调或适配——即使下游任务只有几百张图如果 batch size 太小负样本不足模型很难学会区分细粒度类别这一点在后面避坑章节还会展开。prompt 模板的作用也不容小视。CLIP 在预训练时看到的文本是一些完整的句子比如“一只猫在窗台上晒太阳”而不是孤零零的“猫”两个字。所以推理时如果只输入类别名特征分布和预训练时就有偏移。常见做法是为每个类别套一个模板比如a photo of a {label}或者用一组模板取平均。模板的选择会影响最终精度有时差出几个点。模型规模图像编码器特征维度适用场景ViT-B/32ViT-Base/32512快速验证、CPU 推理、大批量特征提取ViT-L/14ViT-Large/14768精度优先、显存充足ViT-H/14ViT-Huge/141024高精度检索、小批量精排3. 零样本图像分类加载 open_clip、构造 prompt 与温度参数实测3.1 环境准备与模型加载ViT-B/32 搭配 open_clip零样本分类是 CLIP 最直观的落地场景。假设你手头有一个自定义分类任务比如区分不同风格的建筑照片但没有任何标注数据。传统做法是手工提特征或者找人标数据CLIP 的做法是把候选类别名变成句子算句子和图像的相似度取最高分。open_clip 是目前使用最方便的开源实现支持多种预训练权重和模型结构。它和官方权重兼容接口也更规范。我一般会建议先确认 PyTorch 版本在 2.0 以上并且有可用的 CUDA 环境因为 ViT 模型即使推理也比较吃算力。pip install open_clip_torch3.2 类别标签到文本特征prompt 模板选择与 tokenizer假设我要在一个包含“猫、狗、鸟”三类图片的测试集上做分类。首先把类别名扩展成文本描述再用 tokenizer 编码。如果直接输入“cat”“dog”“bird”效果通常不如“a photo of a cat”这种完整句子。模板越贴近预训练数据的文本分布对齐效果越好。tokenizer open_clip.get_tokenizer(ViT-B-32) labels [cat, dog, bird] texts [fa photo of a {label} for label in labels] with torch.no_grad(): text_features model.encode_text(tokenizer(texts)) text_features text_features / text_features.norm(dim-1, keepdimTrue)这里把文本特征做了 L2 归一化。归一化之后点积就是余弦相似度值域限制在 [-1,1]方便后续和图像特征做统一度量。如果不归一化特征向量的模长差异会干扰相似度排序尤其是文本侧特征往往模长偏大。这个细节会直接影响排序稳定性。3.3 相似度计算与结果排序温度参数去哪里找图像侧的处理要严格走 preprocess。测试时常见错误是只做了 resize 没做 CenterCrop或者把归一化的 mean/std 记错了。CLIP 用的是 ImageNet 的均值标准差和你在其他任务里用的可能不一样。from PIL import Image image preprocess(Image.open(demo.jpg)).unsqueeze(0).to(device) with torch.no_grad(): image_features model.encode_image(image) image_features image_features / image_features.norm(dim-1, keepdimTrue) logit_scale model.logit_scale.exp() similarity logit_scale * image_features text_features.T probs similarity.softmax(dim-1) print(probs)logit_scale从模型内部读取乘在相似度矩阵上等于把温度参数还原到计算流程里。softmax之后得到的就是每个类别的概率分数。如果不用 logit_scale直接对原生相似度做 softmax结果会偏向均匀分布类别之间的区分度明显下降。想象一下温差很大的场景原本相似度差 0.1 在温度 0.01 下会被放大成 0.1/0.0110 的差距softmax 后几乎是一边倒。Prompt 模板特点建议a photo of a {label}最通用适合常见物体起步就用它a photo of {label}不区分单复数适合专有词效果好于 a photo of aa photo of a {label} in a scene强调场景语义当标签包含场景描述时用多模板集成多种描述取平均精度最高计算量翻倍模板不是越复杂越好关键看你的类别是什么类型的实体。动物、日用品适合加冠词艺术品、建筑名通常不加冠词直接上。拿不准的时候把你数据集里的典型图像过一遍模型对比不同模板的 Top-1 命中数很快能判断出哪个模板靠谱。4. 特征复用把 CLIP 当视觉骨干接入检索、排序与分类4.1 图像-文本检索把数据库变成向量索引CLIP 最适合的场景其实是检索。电商商品匹配、视频标签推荐、素材库管理本质上都是“给一段文本找出最相关的图像”或者“给定一张图找出语义相似的其他图”。这类任务用 CLIP 特征加向量检索库就能搭一个不错的基线。部署时先把全量图像过一遍图像编码器得到特征矩阵存下来然后建索引。查询时同样把文本编码成向量做最近邻搜索。因为两边的特征在同一空间不需要任何额外映射。import faiss import numpy as np # 假设 image_features 是 N 张图的特征矩阵已做 L2 归一化 index faiss.IndexFlatIP(image_features.shape[1]) index.add(image_features.astype(float32)) query_text a photo of a red car with torch.no_grad(): query_feat model.encode_text(tokenizer([query_text])) query_feat query_feat / query_feat.norm(dim-1, keepdimTrue) scores, indices index.search(query_feat.cpu().numpy(), k10)这里用的是 IndexFlatIP内积检索。因为特征已经做了 L2 归一化内积等价于余弦相似度。Top-10 返回的结果就是语义最接近文本的图像索引。k参数控制召回数量实际调优时根据业务需求决定检索精度不高时先加大 k再靠精排模型拉回精度。4.2 图像相似度排序特征归一化与距离度量图像到图像的检索逻辑一样只不过查询端也从图像编码器出。这一步最需要注意的是特征归一化的一致性。如果你建立索引时做了归一化查询时也必须要做否则内积分数失去了余弦相似度的意义。我曾经在一个模拟项目X里遇到过一个问题图像特征矩阵的 norm 在索引构建前被原地修改查询时又用了未归一化的原始特征导致排序结果完全错乱。这类错误很难排查因为分数整体没变但相对顺序完全变了。建议把特征归一化写成一个函数在索引构建和查询时强制调用同一个函数不要复制粘贴代码片段。def normalize_features(feats): return feats / feats.norm(dim-1, keepdimTrue)4.3 线性探针CLIP 特征做闭集分类的下限与上限如果你有一个带标签的小数据集CLIP 特征可以当作现成的特征提取器。把图像过一遍 CLIP 得到特征然后接一个逻辑回归或一层线性分类器。这个方案在几百个样本的小数据上通常能碾压从头训的 ViT因为你蹭了大规模预训练学到的语义先验。线性探针的实现非常简单sklearn 的逻辑回归就能跑。关键是在这个步骤里验证特征质量如果线性探针的分数很低说明你的数据分布和 CLIP 预训练分布偏差太大或者图像预处理出了问题此时不要急着上更复杂的模型先查数据。from sklearn.linear_model import LogisticRegression train_feats extract_features(train_images) # [N, 512] clf LogisticRegression(max_iter1000) clf.fit(train_feats, train_labels) test_feats extract_features(test_images) acc clf.score(test_feats, test_labels)我在实际项目里通常先用这个方式快速估算任务难度。线性探针分数低不代表 CLIP 没用但至少说明需要更多标注数据或者领域适配。5. 避坑与常见问题排查五个翻车场景的记录5.1 加载失败版本与 checkpoint 的序列化兼容现象调用open_clip.create_model_and_transforms时抛出 KeyError 或权重缺失错误比如Missing key(s) in state_dict。原因常见的两类。一是 PyTorch 版本太老部分权重文件是用高版本 torch 序列化的二是预训练权重名和模型结构不匹配比如用ViT-B-32的结构去加载ViT-L-14的参数。解决先把 PyTorch 升到 2.0 以上然后确认pretrained参数和模型名对应同一套权重open_clip.list_pretrained()可以查到当前可用的所有权重组合直接选对应的一对不要自己猜。5.2 显存溢出batch size 改小了还是炸现象推理时 CUDA out of memory尤其用 ViT-L/14 或 ViT-H/14 时特别明显。原因ViT 在推理时同样会保存中间激活值用于梯度计算虽然推理不需要梯度但如果不加torch.no_grad()PyTorch 默认还是会构建计算图显存占用翻倍。解决推理代码统一套torch.no_grad()进一步可以用torch.autocast做半精度推理显存直接砍半再不行就把图像分 batch 过编码器而不是一次性全喂进去。我一般用 batch size 256 配合半精度ViT-B/32 在 24G 显存的卡上可以稳定跑。5.3 分类结果全是一个类logit scale 与 prompt 的连锁反应现象不管输入什么图模型都预测同一个类别而且概率接近 1。原因要么是文本侧特征没归一化导致某些类别的特征模长特别大相似度被模长带偏要么是没乘 logit scalesoftmax 输入值太小输出接近均匀分布分数稍微高一点的类别每次都赢还有一种情况是 prompt 里包含特殊字符tokenizer 编码结果异常。解决先检查文本特征是否做了 L2 归一化再看是否用了model.logit_scale.exp()最后打印 tokenizer 出来的 input_ids看看特殊 token 是否正常。这三步排查顺序要固定每次翻车都从第一步开始。5.4 特征漂移预处理不一致导致检索失效现象同一张图分别走两条代码路径提取出来的特征相似度只有 0.8 左右检索结果不稳定。原因CLIP 的预训练里对图像做了特定尺寸的缩放和中心裁剪比如 ViT-B/32 训练时用 224×224。如果你推理时用了 256×256 或者其他 resize 方式图像编码器看到的分布就变了特征自然偏移。解决强制所有图像只走preprocess这一个入口不要自己写 resize。preprocess内部已经把 resize 和 CenterCrop 组合好了。你只需要保证输入是 PIL Image 格式而不是 numpy 数组否则通道顺序和数值范围都可能出问题。5.5 中文标签效果差tokenizer 词表与语言偏置现象用中文类别名做零样本分类效果明显比英文差有些类别甚至完全无法区分。原因CLIP 的预训练数据以英文为主tokenizer 的词表里中文覆盖很少中文文本编码出来的特征分布和预训练时的文本分布差异很大。这个问题不是 bug是数据分布决定的。解决最直接的办法是把中文标签翻译成英文再跑如果你必须保留中文链路可以用一个翻译模型先转英文或者自己收集中文图文对做领域适配训练。在绝大多数项目里翻译成英文的成本最低、效果最稳。6. 再进一步缓存特征矩阵与阈值调优6.1 缓存特征一次编码多次复用CLIP 特征提取的耗时不低尤其是全量数据库图片。常见做法是把所有图像的特征一次性算完存成.npy文件后续检索、排序、分类都直接读缓存不再过模型。这个方案在数据库新增图片时只需要增量算新增部分极大节省迭代时间。feats [] for img_path in all_image_paths: img preprocess(Image.open(img_path)).unsqueeze(0).to(device) with torch.no_grad(): f model.encode_image(img).cpu().numpy() feats.append(f) np.save(cache_feats.npy, np.concatenate(feats))每次重新算一遍全量特征等于把一次检索任务的时间从分钟级拉到秒级而精度完全不变。我把这个习惯带进了所有涉及 CLIP 的项目包括某跨平台系统的图像理解模块缓存命中后再调阈值整个迭代周期缩短了一半以上。6.2 阈值调优与召回评估检索系统里阈值决定精度和召回的平衡点。CLIP 相似度分数的绝对值和业务语义没有直接映射关系必须基于验证集统计分布。方法是取一批正负样本对画出相似度的分桶直方图用分数分布交叉点作为初始阈值。scores [] labels [] for query, target, label in validation_triplets: s compute_similarity(query, target) scores.append(s) labels.append(label) # 根据 labels 选 best threshold用验证集做 grid search阈值不能凭感觉拍脑袋也不能只看正样本的分数分布负样本的长尾很关键。6.3 一次完整的基线验证流程现在我会用一套固定流程验收每个新任务先跑 CLIP 零样本看上限再提取特征做线性探针看特征质量最后缓存特征矩阵做检索阈值调优。如果这三步都走完效果还不达标才会考虑训练专门模型。这套流程帮我省掉了无数无效训练。从那以后我每次拿到新的图像理解任务都强制先走一遍 CLIP 特征基线宁可多花半天验证特征质量也不急着上大模型训练。这套思路让我少走了很多弯路希望帮到你。本文还有配套的精品资源点击获取