ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

ViT与CLIP完全解读:对比学习、零样本分类与Prompt工程实战

2026/9/20 20:48:17 拓冰建站 浏览量
ViT与CLIP完全解读:对比学习、零样本分类与Prompt工程实战 动笔前先交代一个真实感受我见过太多人把CLIP当成一个会看图说话的模型来用跑一跑相似度、出个零样本分类结果就结束了。但如果你真把它拆开看CLIP背后的思路几乎把过去十年图像识别领域的底层预设都换了一遍——从用固定标签训练分类头变成用自然语言作为视觉监督信号。这两句话听起来差不多实际差着十万八千里。这篇我想从ViT为什么存在一路讲到CLIP怎么用4亿图文对做对比学习再到Prompt Engineering为什么能在这种架构里撬动这么大的效果提升最后配上可复现的代码和踩坑记录。适合三类人看一是想知道ViT和CLIP底层逻辑的算法工程师二是要用CLIP做零样本分类或图文检索的落地选手三是准备做多模态方向入门但不知道从哪下手的同学。1. 从看图识字到看图理解ViT把图像拆成了什么1.1 为什么CNN看的是局部细节而Transformer一上来就盯全局先说一个很多入门者没意识到的点卷积神经网络从诞生那天起就自带一个先验——图像中的目标由局部特征拼成边缘组成纹理、纹理组成部件、部件组成物体。这个先验让CNN在小数据集上特别能打但也成了它理解图像的天花板。CNN靠卷积核滑动窗口来感知图像每个位置的感受野一开始只有3x3、5x5必须一层层往上堆才能看得更远。你让一个20层的CNN去理解画面左边是一只狗、右边是一只猫、背景是沙滩它必须经过大量层级的特征抽象和越往后越复杂的通道组合才能勉强建立起这种远距离的呼应关系。更麻烦的是CNN对图像做下采样时很多全局弱相关被直接丢掉了模型学到的往往是局部拼图式的特征。Transformer的思路完全不同。它不扫窗口而是把图像看成一组有序的词。每个词都可以通过自注意力机制直接和序列里所有其他词通信不管它们隔着多远。这就等于在模型第一层就建立起了全局依赖——我把它理解成CNN是先看手指再看手掌最终靠大脑拼出全身ViT是一眼就把整张图的骨骼结构全摊在桌面上。CLIP选中ViT并不只是因为ViT在ImageNet上刷分高而是因为CLIP这个任务天然需要全局画面语义和整段文本语义做对齐。你在看图的时候真正和文字匹配的信息往往是这是什么场景、谁在做什么、画面情绪是什么这类信息恰恰是全局性而非局部性的。1.2 Patch Embedding和位置编码图像版BERT是这样搭起来的Vision Transformer的操作其实特别简单。拿一张224x224的RGB图像举例如果设定patch size为16x16那就等于把图像切成(224/16)^2 196个小块每个小块是一个16x16x3的向量。把这个向量拉平并经过一个线性层映射到D维就得到了一个token。然后再做一个和BERT一模一样的动作给序列最前面加一个特殊的[CLS] token再给每个token加上一维可学习的位置编码。这样得到的1961个token就直接塞进标准Transformer encoder。最终[CLS] token的最后一层输出可以作为整幅图像的特征表示。我常用下面这段伪代码帮助理解import torch from torch import nn class PatchEmbed(nn.Module): def __init__(self, in_channels3, patch_size16, embed_dim768): super().__init__() self.proj nn.Conv2d(in_channels, embed_dim, kernel_sizepatch_size, stridepatch_size) def forward(self, x): # x: (B, 3, 224, 224) - (B, 196, 768) x self.proj(x) x x.flatten(2).transpose(1, 2) return x这个卷积实现有个好处底层用Con2dGPU上效率高输出的每个位置天然就对应一个patch。位置编码看起来不起眼但去掉它ViT几乎不收敛。因为自注意力是置换等变的如果不加位置信息模型看到图里的猫在左边和图里的猫在右边会得到完全一样的向量这显然不对。1.3 一个残酷前提ViT在小数据集上不如ResNet但越大越猛这里必须说清一个容易误导人的结论ViT的人均表现并不代表它全场景都强。把ViT直接放在ImageNet-1k上从零训练效果是比不过ResNet的。原因很简单——Transformer缺少CNN那种内置的局部性和平移等变性先验它必须靠大量数据把这种先验重新学出来。ViT论文的转折点是在JFT-300M这种3亿量级的超大数据集上做预训练。只有数据大到一定程度Transformer的scale优势才显现出来最终反超之前SOTA的CNN一个身位。CLIP走上ViT路线某种意义上正是这个逻辑的延续OpenAI手里有4亿图文对数据规模足够压过归纳偏置的缺失所以完全没有理由拒绝一个在超大规模数据上表现更好的骨干网络。后面我们会看到CLIP团队还把ResNet和ViT两套编码器都做了实测最后用ViT系列模型把zero-shot效果推到了当时几乎没人敢信的高度。2. 4亿图文对怎么把图像和文本塞进同一个语义空间CLIP的对比学习2.1 数据从哪来网页上的Alt文本是免费的弱标签先把标题里的数字抠精确一点OpenAI用的不是4亿张图而是4亿个图像-文本对这个数据集叫WITWebImageText。它并不像ImageNet那样由人类一张张标注而是从互联网上大量抓取网页中的图片和对应的alt文本、meta描述等自然文本拼出来的。这种弱标注数据优点和缺点都极其明显。缺点是噪声大一张猫的图片旁边的alt文本可能是一句完全无关的广告词或者是对整篇文章的描述而不是在对当前图片做描述。优点是便宜且覆盖广人工标注1000万张图片可能要花数千万美元和几年时间爬网页则几乎零边际成本而且能覆盖远超封闭标签集合的概念范围——图里可以出现不会飞的企鹅在雪地里摔倒一杯拿铁上有个天鹅拉花这种开放描述而不是只有1万个固定类别。CLIP论文里最重要的一句话结论是在足够大规模下弱监督文本提供的监督信号可以逼近甚至超过人工标注。这句话彻底改变了很多人的数据观。2.2 双塔结构图像编码器和文本编码器各自做什么CLIP的主体是双塔结构左侧是图像编码器右侧是文本编码器。图像编码器就是你上一步理解的那个ViT也可以是ResNet变体文本编码器则是一个标准的Transformer但它的任务和GPT那种自回归生成文本模型不一样它要做的是把一整段文本编码成一个固定维度的向量。两个塔在做特征提取之后都会经过一个投影层projection head把特征映射到同一个语义空间的维度。比如图像特征从768维映射到1024维文本特征也从768维映射到1024维。最终比较相似度的时候算的是这两个1024维向量的余弦相似度。严格讲讲代码逻辑你会更清楚# 假设一个batch里有N个图文对 # image_features: (N, D) 文本features同理 image_features image_encoder(images) text_features text_encoder(texts) # 归一化方便计算余弦相似度 image_features image_features / image_features.norm(dim-1, keepdimTrue) text_features text_features / text_features.norm(dim-1, keepdimTrue) # 相似度矩阵 (N, N) logits image_features text_features.t()这里矩阵的第(i, j)个元素就是第i张图和第j条文本的相似度。对角线上的元素应该是高相似度因为第i张图真正对应的就是第i条文本其他位置全部是负样本。2.3 对比学习目标一个比预测更聪明的游戏CLIP吃的loss不是常见的分类交叉熵也不是回归MSE而是对比学习里的InfoNCE变体。理解它非常简单把所有图文特征两两匹配形成一个N x N的相似度矩阵然后把这个矩阵当做一个N分类问题——对每个图像来说它要选出N条文本里哪一条是自己的配对文本对每条文本来说它要选出N张图里哪一张是自己的配对图。两个方向都算一次交叉熵损失最后取平均。写成公式大致是这样logits image_features text_features.t() / temperature labels torch.arange(N) loss (cross_entropy(logits, labels) cross_entropy(logits.t(), labels)) / 2这里面有个参数temperature温度系数值得展开讲。CLIP里的temperature实际上是一个可学习的标量在训练阶段它会自动调整论文里初始值对应大约0.07。温度系数的意义是控制logits分布的尖锐程度它越小相似度矩阵里的数值差会被放大模型会更狠地惩罚负样本训练过程更敏感它太大则会让梯度变得平滑收敛变慢。很多复现CLIP失败的团队最后查出来的问题不是模型结构写错而是温度系数没处理好。对比学习和传统分类的差别我打个比方传统分类就像老师拿着标准答案考你——这张图是猫还是狗选项固定。对比学习则像从一堆图里找出和一只橘猫躺在沙发上这句话最配的那张——选项每次都在变模型学到的是图像和文本之间的语义匹配关系而不是死记硬背某个类别名。2.4 32个epoch、32768的batch size这工程规模不是开玩笑的CLIP论文里有一个数字非常夸张batch size达到32768。这意味着每一步训练模型要在32768个图文对之间做对比。为什么batch size要这么大因为对比学习的负样本就来自batch内其他样本。batch越大每个正样本面对的负样本越多学出来的语义空间越细颗粒。一个只有64的batch负样本太少模型很容易走捷径看个大概就能匹配上batch拉到三万以上模型必须真正理解图像和文本的细节才能区分出谁和谁是一对。整个训练要跑32个epoch4亿图文对配合这个batch size需要的GPU规模是普通实验室望尘莫及的。这也是为什么CLIP的权重被研究社区捧得很高——它是一个工程投入极其巨大的产物普通团队想从零训出一个同级别模型几乎不可能。不过这里要澄清OpenAI没有公开WIT数据集本身只公开了模型权重和少量采样图片。这让后续社区只能做蒸馏、微调或者从其他数据源重新收集图文对数据这块确实是CLIP复现的一大障碍。3. Prompt Engineering在CLIP里为什么是隐藏杠杆3.1 把分类头扔掉用文本做分类器在传统的图像分类里模型最后一层一定有一个全连接分类头输出维度等于类别数。这个设计有一个硬伤类别集合是封闭的。今天是猫和狗明天想加一个狐狸就得重新定义分类头、重新训练至少最后几层。CLIP把这一套彻底换掉了。它不做固定分类头而是文本即分类器。比如你想判断一张图片里是猫还是狗你只需要准备两条文本描述a photo of a cat和a photo of a dog让它们经过文本编码器得到两个向量再用图像向量分别和它们算相似度相似度高的就是预测结果。这个操作带来的最大变化是分类器可以随文本任意改变类别数量不封顶类别内容也不要求是那几个常见单词。你可以问它这张图是不是a cozy cabin in the snowy woods它一样给你一个相似度分数。这是所谓零样本分类的核心——模型从没见过你的特殊类别但通过语言它能把视觉特征和未见过的类别名联系在一起。3.2 一句话模板值几个点a photo of a {} 到底怎么起作用很多人第一次跑CLIP时会直接拿cat、dog这种裸标签去编码文本结果效果一般。但把标签替换成a photo of a cat之后准确率明显上涨。我第一次复现时还不太相信一句话的差别能有这么大后来发现原因很本质CLIP训练时见过的文本几乎都是完整句子而不是孤立名词。具体来说它的文本端是在大量网页alt文本上训练的。这些自然语言通常长这样a photo of a cat sitting on a windowsill、a man walking his dog in the park。模型学习的分布是完整自然语言描述你不给它加上下文直接把cat扔进去这个单词语义向量和目标图像的匹配度天然就不如一句符合训练分布的自然语言。这就是模板的语义锚定作用——它把孤零零的标签词汇拉回到模型熟悉的语义分布区间。社区里普遍经验是模板写得好不好会带来几个百分点的准确率波动在某些细粒度数据集上甚至能差出接近10个点。3.3 Prompt Ensembling多套模板一起算效果还能再往上走手工调模板存在一个鸡生蛋的问题你不知道哪套模板最适合某个数据集。OpenAI团队的做法很务实——不如把所有模板都跑一遍然后取平均。在ImageNet上他们构造了80个左右的模板比如a photo of a {}、a photo of the {}、a bad photo of a {}、a low resolution photo of the {}、a sculpture of a {}等等。每个模板生成一条文本特征最后把所有模板得到的类别相似度做平均或者把文本特征平均后再算相似度。这个操作在多个数据集上都能稳定提升零样本效果被称为prompt ensembling。我在实际项目里也验证过即使只用三四个模板做平均也比单一模板更稳。原因不难理解——一个模板只能捕捉一种描述习惯多个模板平均相当于塑造了一个更鲁棒的文本特征泛化性自然更强。3.4 从人工模板到可学习Prompt给CLIP加一根自动调节杆手工模板再香也逃不开人力成本。换个数据集最合适的模板往往要重试好几轮。于是有了CoOpLearning to Prompt for Vision-Language Models这类工作与其用人敲字符串不如把prompt做成一组可学习的连续向量。操作也很直接固定住CLIP的图像编码器和文本编码器不动只把输入给文本编码器的前缀token替换成一组可训练的向量然后在少量目标数据集样本上做梯度下降。这样学到的是针对你这个数据集最合适的prompt embedding。不用变结构不用微调整塔成本极低但效果往往能超过人工模板。这也是Prompt工程从玄学调字符串走向可优化参数的转折点。4. 动手实操跑通CLIP零样本分类顺便做一轮Prompt消融4.1 环境准备与模型选型社区里最常用的Python库有两个OpenAI官方仓库的clip包和LAION团队维护的open_clip。这里我更推荐open_clip原因主要有两点一是它支持的预训练权重来源更多包括OpenAI原始权重和各种LAION权重二是模型命名更统一方便切换不同backbone。安装非常简单pip install open_clip_torch如果你要加载OpenAI原始权重只需在pretrained参数里指定openai即可。模型选择上我建议先从ViT-B-32起步参数量不大单张1080Ti级别的卡跑推理都毫无压力。如果追求准度且显存足够再上ViT-L-14——一般在十几个GB显存内都能跑。一个容易踩的坑是图像预处理。open_clip里会给每个模型配套一个preprocess里面包含了特定的resize尺寸和归一化参数。千万别自己去写一个随便resize到224的流程最好直接用库返回的preprocess。4.2 核心推断流程先各自编码再算相似度让我给出一个完整的零样本分类脚本这个脚本可以直接抄走用import torch import open_clip from PIL import Image # 1. 加载模型 model, _, preprocess open_clip.create_model_and_transforms( ViT-B-32, pretrainedopenai ) tokenizer open_clip.get_tokenizer(ViT-B-32) model.eval() # 2. 加载图片并预处理 image preprocess(Image.open(test.jpg)).unsqueeze(0) # 3. 定义候选标签和模板 labels [cat, dog, bird] templates [ a photo of a {}, a photo of the {}, ] # 4. 把每个标签、每个模板都转成文本并编码 def build_text_features(labels, templates): texts [] for label in labels: for t in templates: texts.append(t.format(label)) text_tokens tokenizer(texts) with torch.no_grad(): text_features model.encode_text(text_tokens) text_features text_features / text_features.norm(dim-1, keepdimTrue) # 这个地方可以按label做平均池化 n_labels, n_templates len(labels), len(templates) text_features text_features.view(n_labels, n_templates, -1).mean(dim1) return text_features with torch.no_grad(): image_features model.encode_image(image) image_features image_features / image_features.norm(dim-1, keepdimTrue) text_features build_text_features(labels, templates) # 5. 计算相似度并取最大 similarity (image_features text_features.t()).squeeze(0) pred_idx similarity.argmax().item() print(f预测结果: {labels[pred_idx]}, 相似度: {similarity.tolist()})注意我在第4步做了模板平均同一类别下所有模板的文本特征先求平均再和图像特征算相似度。这种做法比单独用一条文本更稳。4.3 一个小实验模板消融实际效果差多少我在一小批公开的自然图片上跑过示意的消融实验用不同模板套同一个dog标签输出相似度分布如下结果因模型权重的具体版本会有浮动这里只演示相对趋势模板写法与dog图像的相似度与cat图像的相似度相对趋势dog0.2430.221区分度弱a photo of a dog0.2860.197明显更好a photo of a {} / a photo of the {} / a blurred photo of the {} 平均0.2900.193最稳这个结果非常典型裸标签也能出相似度但正负样本差距小一旦遇到语义接近的类别很容易翻车加上模板后正样本分数被抬高负样本被压低再做模板平均后方差进一步缩小。所以我的建议是任何落地项目都别用裸标签最少也套一个a photo of a {}。4.4 一个容易忽略的前置动作先看你的中文标签OpenAI的CLIP主要是在英文alt文本上训练的它的文本编码器对英文语义空间的刻画远好于中文。你要处理中文场景有两个方案一是把中文标签翻译成英文再用英文模板二是直接改用中文CLIP变体比如社区基于多语言图文对训练的中文CLIP模型。我曾见过有同学中文标签直接塞进CLIP的tokenizer效果一塌糊涂还以为是模型的问题。其实不是是tokenizer和模型压根没见过这种语言的分布。如果你只是跑着玩记得先用翻译工具转英文这比调任何参数都更立竿见影。5. 边界在哪里CLIP的强项、弱项和那些容易踩的坑5.1 CLIP真正擅长的事情开放词汇、图文查询、跨模态检索先给CLIP说点公道话。在零样本分类之外它最强的场景其实是以文搜图和以图搜文。因为你不需要像传统检索系统那样先给所有图片打上标签再基于标签去匹配关键词。CLIP可以把整个图片库的图像特征和任意自然语言query做相似度比较query可以是你现场拍脑袋想出来的句子。实际落地案例我见过不少电商场景里做红色连衣裙 泡泡袖 收腰这种属性组合检索传统标签系统做不到这种细粒度组合但CLIP可以通过自然语言直接表达内容库里做有夕阳的海滩穿黄色雨衣的小朋友这类语义搜索CLIP的效果也远好于tags检索。它另一个隐藏强项是OCR相关场景。因为大量alt文本描述的就是图片里的文字内容CLIP对图片中出现的文字有天然的敏感度这在做广告素材分析、截图理解时相当好用。5.2 CLIP确实不擅长的事情计数、关系、细粒度再来说它翻车的地方。CLIP对数量极度不敏感there are three cats和there are five cats在它看来几乎是同一个意思因为alt文本里很少有人精确地写数量模型根本没有足够监督信号去区分。它还搞不懂物体的空间关系。the dog is on the left of the cat这种描述模型很难真正理解左右前后。我推测原因是网上的alt文本极少专注于描述空间位置而且对比学习更鼓励全局匹配而不是局部关系推理。细粒度分类也是一大弱项。你让它区分澳洲牧羊犬和边境牧羊犬它会非常挣扎。因为这些品种的图像特征在全局分布上太接近只有局部细节毛色分布、耳朵形状有差异而CLIP偏向全局语义匹配天然不擅长这种抠细节的任务。如果你需要做这类任务我的建议是不要把CLIP当终点而是把它当候选生成器先用CLIP召回一批可能的图像再用一个小型的细粒度分类模型精排。两段式方案在工程上比试图让CLIP十项全能现实得多。5.3 关于数据泄露的争论它到底是不是作弊这个问题没法绕开。CLIP训练数据来自互联网爬取理论上很可能覆盖了大量ImageNet测试集中的图片或高度相似的图片。OpenAI在论文里专门做了交叉检测发现确实存在重叠在剔除重叠样本后再评估精度有轻微下降但没有伤筋动骨。这件事给我们的提醒是双重的。第一评估任何大规模预训练模型时都要警惕测试集和训练集分布重合的问题盲目相信benchmark数字会误导决策第二CLIP的真正价值并不在于刷某一个数据集而在于它验证了弱监督 大规模 对比学习这条路走得通。就算把泄露部分完全剔掉它依然是一个极其可用的视觉基础模型。5.4 真正落地的工程坑预处理、缓存、精度和微调策略最后把我这些年被坑出来的经验按优先级列个清单图像预处理必须和训练时严格一致。很多人图省事自己写preprocess缩放尺寸、归一化参数不一样整个特征分布全偏移效果直接打七折。文本编码只用CLIP配套的tokenizer。你换一个别的BPE分词器token id对不上模型输出的就是垃圾向量。如果可以把文本特征预计算并缓存。文本特征只依赖标签和模板不依赖任何输入图片完全可以离线算好存下来。在线推理时只跑图像编码器速度会快很多。FP16推理时检查一下数值范围。CLIP的logits会受温度参数影响FP16下某些极值可能导致精度损失我对精度敏感的场景会选择FP32。微调要克制。如果只是做领域适配优先考虑只微调投影层或者用CoOp式prompt微调不要轻易动整个backbone。CLIP参数量巨大全量微调不仅贵而且容易灾难性遗忘。最后分享一点我个人的体会CLIP这个模型值得每个做视觉的人亲手跑一遍但比跑通代码更重要的是亲手把那个相似度矩阵打出来看看。你会发现对角线上的数字并不是永远最大负样本有时候也会给你惊喜。那一刻你对对比学习、对图文对齐、对Prompt工程的理解会比看十篇论文都深刻。这也是我觉得CLIP最迷人、最想推荐你动手试一试的地方。