ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

零标签图像分类怎么办?OpenCLIP知识蒸馏落地全解

零标签图像分类怎么办?OpenCLIP知识蒸馏落地全解 简介知识蒸馏与跨模态预训练模型的结合是降低图像分类对标注数据依赖的有效路径。这份项目源码围绕OpenCLIP设计了一整套零标签图像分类方案先借助OpenCLIP提取未标记图像特征再通过蒸馏策略将关键知识迁移至轻量级分类模型并完成微调适配覆盖了从特征提取、蒸馏训练到预测评估的完整流程。适合算法工程师与研究生用于复现实验和二次开发参考。压缩包共16个文件以Python源码为主9个py脚本覆盖OpenCLIP嵌入计算、ONNX导出、图像检索、模型蒸馏与PyTorch推理等环节另含2个shell脚本、2张示例图片、1个Dockerfile、1张架构示意图和1份README说明整体仅1.42MB结构紧凑、易于部署。目前已有352人学习下载侧面印证其实用价值。源码附带可直接运行的demo脚本与推理示例并给出Docker部署配置便于快速验证。对希望掌握CLIP系列模型蒸馏落地细节的读者而言这套实现省去了大量从零搭建的工作值得作为实战参考。1. 零标签图像分类怎么落地一张标注都没有知识蒸馏先把流程跑通场景是这样产品拿过来一批图片说三天后要一个能自动分类的小模型你去要标注被告知没有预算、没有人力、也没有时间。这是很多做视觉落地的工程师真实遇到过的事。标题里“知识蒸馏 OpenCLIP 零标签图像分类”这个组合给的路径是不标数据用 OpenCLIP 这个图文对齐模型当 teacher对每张图直接输出类别概率然后让一个小模型通过知识蒸馏把这些概率学下来。整个流程不产生一张人工标注适合快速验证、冷启动以及图片不能送出去标注的私有场景。下面按这条路径把方案拆开讲从原理、选型到参数和坑照着就能复现。2. 零标签蒸馏的原理与选型OpenCLIP 当 teacher 的最省事用法动手之前先想清楚一个问题零标签图像分类不是“无监督”而是把标注这一步外包给了图文对齐。CLIP 在数亿张图文对上训练学的是“一张图像和一句自然语言描述是否匹配”的对齐关系。把候选类别名写成“a photo of a cat”这样的句子编码成文本特征再和图像特征做余弦相似度就能得到一张图属于每个类别的概率。这就是零标签分类里 teacher 输出的来源。2.1 零标签分类不是无监督把标注外包给图文对齐很多人一听“零标签”第一反应是聚类或者无监督表征学习。这个理解在蒸馏方案里是错的。零标签蒸馏的方案里唯一的“人工标注”其实只有一个你写的类别名。类别名写得好不好直接决定 teacher 输出质量也就决定蒸馏上限。为什么 teacher 必须选 OpenCLIP 这类图文对齐模型而不是自己训练一个 CNN因为普通 CNN 的分类器输出维度是固定的换一个类别集合就要重新训练没有 zero-shot 能力。而 OpenCLIP 是双塔结构图像塔和文本塔分别编码类别集合变了只需要重算文本特征图像特征完全不用动。这也是零标签场景能成立的结构前提。对应到标题里的组合知识蒸馏在这里做的事情是OpenCLIP 是大而全的通用模型直接部署它做分类显存占用高、单张推理慢、还依赖一堆外部库而业务往往只需要在一个固定类别集合上分类并且要部署在普通 CPU 机器或者端侧。蒸馏的本质是把 OpenCLIP 对固定类别集合的判别能力压缩进一个小模型里。对下游来说它拿到的是一个普通分类网络推理是标准 CNN 前向部署链路完全可控。2.2 OpenCLIP 怎么当 teacher加载、编码、出 logitsOpenCLIP 是 CLIP 的开源复现提供了多个预训练权重来源。做零标签蒸馏用它当 teacher 比手动搭别的模型省事很多权重下载、预处理、模型封装都是现成的import torch import open_clip model, _, preprocess open_clip.create_model_and_transforms( ViT-B-32, pretrainedlaion2b_s34b_b79k, ) model model.cuda() tokenizer open_clip.get_tokenizer(ViT-B-32) class_names [cat, dog, car, bird] text_tokens tokenizer([fa photo of a {c} for c in class_names])这段代码做了三件事创建 ViT-B/32 结构的 CLIP 模型并加载预训练权重拿到配套的预处理函数和 tokenizer把类别名变成文本 token。pretrained是权重来源常见的有laion2b_s34b_b79k和openai不同来源在不同数据域上差异很大后面避坑章会专门讲。preprocess会在数据加载时用到作用是把任意尺寸的图片规范到模型要求的 224x224并做归一化。text_tokens只需要在训练前编码一次后面反复用它和图像特征做点乘得到的就是 logits。显存方面ViT-B/32 的 fp16 推理大约占 2G 显存跑得动就能做 teacher不需要多卡。2.3 方案选型伪标签两阶段、logits 蒸馏还是混合拿到 teacher 之后落地时有三种做法方案保存内容训练 loss对噪音的容忍度适合场景A伪标签两阶段hard label交叉熵低伪标签错一个带偏一片快速验证、类别少、teacher 置信度高Blogits 蒸馏每个样本的 logitsKL 散度中保留不确定性但损失波动大类别多、teacher 输出有重叠C混合hard soft 同时用KL CE较高最终上线方案我一般建议第一次跑先按方案 A 把硬标签训练链路跑通确认数据加载、模型结构、迭代流程都没问题然后再切方案 C把 soft logits 加进来。因为 hard 伪标签可解释、好排查直接上 KL 散度模型不收敛时你很难分清是数据问题还是损失函数问题。第 4 章给的损失函数就是方案 C 的完整写法。方案 B 单独用的情况比较少它适合 teacher 输出极度不确定的场景比如类别之间视觉差异很小、很多样本其实同时属于两个类。但它的训练曲线很难看没有 hard 分支兜底的话发散风险高。3. 从图片目录到可用模型零标签蒸馏最小可复现流程的四个步骤拿到源码包先别急着换模型。我一般先做三件事确认数据组织方式是不是“目录即类别”确认 teacher 权重能不能加载成功确认伪标签以什么格式落盘。这三件事对不上后面代码再好看也跑不起来。3.1 数据准备目录结构当标签省掉标注文件零标签蒸馏的数据组织是最简单的因为不需要任何标注文件data/ ├── cat/ │ ├── 001.jpg │ └── 002.jpg ├── dog/ │ ├── 001.jpg │ └── 002.jpg └── bird/ ├── 001.jpg └── 002.jpg图片按类别目录存放目录名就是类别名。这里有个细节如果业务里的类别是中文建议在脚本里维护一个class_name - prompt_name的映射把中文名转成英文描述短语再传给 CLIP。比如“玻璃划痕”转成a close-up photo of a scratched glass surface效果差异很大。同时抽一层留出集每个类别随机抽 10% 的图片挪到val/下面不参与 teacher 前向和学生训练。这 10% 图片不需要任何人工标注留作蒸馏效果的验收后面的 3.4 会用到。3.2 teacher 前向OpenCLIP 推理并保存 logits数据准备好了下一步就是跑一遍 teacher对所有训练图片生成 logits。这里有一个关键经验保存 logits不要保存 softmax 之后的概率。因为蒸馏温度 T 在训练时可能调整保存概率的话换一次温度就得重跑一遍 teacher。import os import numpy as np import torch import torch.nn.functional as F import open_clip from PIL import Image from torch.utils.data import DataLoader, Dataset class ImageFolderForTeacher(Dataset): def __init__(self, root, transform): self.samples [] for c in sorted(os.listdir(root)): c_dir os.path.join(root, c) if not os.path.isdir(c_dir): continue for f in sorted(os.listdir(c_dir)): if f.lower().endswith((.jpg, .jpeg, .png)): self.samples.append((os.path.join(c_dir, f), c)) self.transform transform def __len__(self): return len(self.samples) def __getitem__(self, idx): path, _ self.samples[idx] return self.transform(Image.open(path).convert(RGB)), path model, _, preprocess open_clip.create_model_and_transforms( ViT-B-32, pretrainedlaion2b_s34b_b79k) model model.cuda() tokenizer open_clip.get_tokenizer(ViT-B-32) class_names [cat, dog, bird] text_tokens tokenizer([fa photo of a {c} for c in class_names]) text_features F.normalize(model.encode_text(text_tokens), dim-1) loader DataLoader(ImageFolderForTeacher(data, preprocess), batch_size64, num_workers4, shuffleFalse) all_logits, all_paths [], [] for images, paths in loader: with torch.no_grad(): image_features F.normalize(model.encode_image(images.cuda()), dim-1) logits (image_features text_features.T) / 0.07 all_logits.append(logits.cpu().numpy()) all_paths.extend(paths) all_logits np.concatenate(all_logits) np.savez(teacher_logits.npz, logitsall_logits, pathsnp.array(all_paths)) print(all_logits.shape)逻辑说明text_features在循环外算一次因为文本特征不随图片变化省显存也省时间除以 0.07 是 CLIP 常见的 logits 缩放因子相当于把相似度分布拉开。如果后续发现所有类别概率都接近均匀可以尝试把 0.07 改小到 0.05 或 0.03。参数说明batch_size64在 24G 显存上比较稳妥显存小就调成 32 或 16只影响速度不影响结果保存格式用np.savez把 logits 和图片路径存到同一个文件里。这一步落盘之后后面训练 student 时不再需要加载 OpenCLIP 模型小显存机器也能跑。保存后建议顺手打印一份 label 分布np.argmax(all_logits, axis1)的直方图。分布严重偏斜的话先排查类别名再决定要不要继续训练。3.3 训练 student一个分类网络的标准训练循环学生模型选型不需要复杂ResNet-18 或 MobileNetV3 在这个方案里性价比最高。数据集大、类别多时用 ResNet-50 也不亏但零标签蒸馏的上限由 teacher 决定student 容量再大也带不来额外信息。import torchvision.models as models from torch.utils.data import DataLoader, TensorDataset, random_split import numpy as np import torch data np.load(teacher_logits.npz, allow_pickleTrue) pseudo_labels data[logits].argmax(axis1) paths data[paths] # 复用 3.2 的 ImageFolderForTeacher 配合 preprocess 读图 # images: (N, 3, 224, 224) 的 tensorlabels: pseudo_labels dataset TensorDataset(images, torch.tensor(pseudo_labels)) train_set, val_set random_split( dataset, [int(len(dataset) * 0.9), len(dataset) - int(len(dataset) * 0.9)], ) train_loader DataLoader(train_set, batch_size128, shuffleTrue, num_workers4) student models.resnet18(num_classeslen(class_names)) optimizer torch.optim.AdamW(student.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) criterion torch.nn.CrossEntropyLoss() for epoch in range(30): student.train() for images, labels in train_loader: out student(images.cuda()) loss criterion(out, labels.cuda()) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step()这段代码先按伪标签的 argmax 当硬标签训练目的是把链路跑通。参数说明学习率1e-3搭配 AdamW 在大多数分类任务上起步都不用改发现 loss 震荡就降到3e-4T_max30要和总 epoch 数保持一致否则余弦退火还没退完就断了。一个容易被忽略的点这里直接读图成 tensor 是为了先跑通正式做的话我建议在 DataLoader 里加随机翻转和裁剪。student 学的是不变性对蒸馏有益特别是 OpenCLIP 自己用的预处理里没有数据增强student 见过更多扰动版本泛化会更好。3.4 本地验证没有测试集时用什么指标验收零标签场景下没有人工标注模型训完不能只看 loss。我一般用一致率在留出来的val目录上用 OpenCLIP 重新跑一遍 teacher取 top1再看 student 的预测和 teacher 一致的比例。clip_pred np.argmax(val_logits, axis1) # 留出集上的 teacher 输出 student.eval() with torch.no_grad(): student_pred torch.argmax(student(val_images.cuda()), dim1).cpu().numpy() agree_rate (student_pred clip_pred).mean() print(fstudent vs teacher agree rate: {agree_rate:.3f})一致率不是真实准确率但它是零标签蒸馏最直接的进度条如果学生模型只有 70% 和 teacher 一致说明知识还没学到位到了 90% 以上说明已经逼近 teacher 的预测边界。真实准确率最终靠人工抽验兜底这个在第 6 章会讲。4. 蒸馏损失与参数把 OpenCLIP 的知识搬进小模型网上搜知识蒸馏代码十有八九是在有标签的 CIFAR 上做实验到了零标签场景第一个要改的就是 loss 的输入来源teacher logits 从哪来、学生怎么对齐。第 3 章跑通了硬伪标签链路这一章把 soft logits 加进来变成真正意义上的蒸馏。4.1 蒸馏损失函数怎么写KL 散度加伪标签交叉熵完整写一个零标签蒸馏的损失函数import torch import torch.nn.functional as F def distillation_loss(student_logits, teacher_logits, pseudo_labels, temperature3.0, alpha0.7, beta0.3): # soft loss学生、老师的 logits 都除以温度再做 KL 散度 soft_targets F.softmax(teacher_logits / temperature, dim-1) student_soft F.log_softmax(student_logits / temperature, dim-1) soft_loss F.kl_div( student_soft, soft_targets, reductionbatchmean ) * (temperature * temperature) # hard loss直接用伪标签交叉熵 hard_loss F.cross_entropy(student_logits, pseudo_labels) return alpha * soft_loss beta * hard_loss这是零标签蒸馏的核心。两个关键点乘temperature * temperature是为了抵消 softmax 除以温度带来的梯度缩小。温度越大soft target 越均匀loss 数值越小不乘回去的话梯度会很小训练半天不往前走。pseudo_labels就是第 3 章teacher_logits.npz里 argmax 出来的硬标签。alpha和beta加起来是 1alpha 大说明更相信 teacher 的概率分布beta 大说明更相信硬标签。一般从 0.7 / 0.3 起步。这里的teacher_logits来源和常规蒸馏不一样它不是某个网络在真标签上训出来的 logits而是 OpenCLIP 的 zero-shot 输出。所以 soft target 里包含的是图文对齐模型对样本不确定性的判断这是零标签蒸馏最大的信息量来源也是它跟“拿伪标签训练”的本质差别。4.2 必调参数表温度、阈值、学习率一次说清零标签蒸馏里值得调的参数其实就几个整理成一张表参数推荐值调节方向蒸馏温度 T2 ~ 6T 偏大分布趋均匀T 偏小趋近 one-hot伪标签阈值 τ0.7 ~ 0.9置信度低于阈值的样本soft loss 权重降为 0alpha / beta0.7 / 0.3teacher 不准时降 alpha加 betabatch size64 ~ 128小 batch 下 KL 梯度方差大调大更平滑学习率3e-4 ~ 1e-3用 AdamWloss 震荡就降epoch20 ~ 40看一致率曲线平了再停温度这个参数最玄学。OpenCLIP 输出的 logits 本身数值范围窄往往在正负零点几之间直接除以 0.07 之后依然集中在很小的区间所以温度通常往低调效果更明显。我一般先跑 T3如果 soft loss 长时间不降把 T 调到 1.5 或 2 再看分布。伪标签阈值 τ 的作用是对置信度低的样本做平滑处理teacher 自己都拿不准的样本它对蒸馏的贡献应该被削弱。常见做法是把低于 τ 的样本的 soft loss 乘一个 0 到 1 之间的衰减系数或者干脆只用 hard loss 训练这部分样本。4.3 伪标签会过期吗教师输出重算与两阶段训练策略OpenCLIP 作为 teacher 是冻结的它的 logits 不会因为 student 变化而过期。真正的问题在 hard 伪标签初始化时 teacher 对某些样本判断错误这些错误会一直传导到训练结束。我常用的策略是两阶段。第一阶段只用 hard 伪标签跑 5 个 epoch让 student 先具备粗糙的分类能力第二阶段切到 4.1 的混合损失把 soft logits 加进来。这样做的好处是第一阶段把模型拉到正确区域附近第二阶段才不会在满是错误伪标签的早期被带偏。如果中途想重算伪标签不需要重新加载 OpenCLIP。直接用 student 当前预测的高置信度样本去“修正”hard 标签这种做法也叫伪标签自训练。实际跑下来第二阶段后期加一遍自训练一致率能再涨 1 到 3 个点。5. 避坑零标签蒸馏常见的 5 个翻车现场与排查思路这一章的每一条都是血泪经验。零标签方案最大的问题是没有真标签兜底出问题的时候很容易先怀疑代码写错了最后发现大多是数据或参数问题。5.1 伪标签严重偏向某一类loss 下降但一致率极低现象训练时交叉熵 loss 正常下降epoch 跑完一看student 的预测全部落在同一个类别上和 teacher 的一致率只有 50% 出头。原因CLIP 对某些类别名有先验偏好。比如类别集合里有“背景”“其他”这种抽象词或者某个类别名写得太宽泛teacher 在大量图片上的输出都偏向了它伪标签 argmax 之后模型被多数类淹没。解决先统计 teacher 伪标签的分布如果发现某个类占比超过 60%不要急着训练。检查类别名描述把抽象词改成具体短语然后用类别均衡采样重做 DataLoader最后再看温度温度低会让 softmax 更尖锐放大 teacher 的偏好先调高到 T5 观察几轮。5.2 soft loss 怎么调都不降hard loss 却正常收敛现象切到混合损失后KL 散度项停在某个值附近不动硬标签交叉熵在下降。原因最常见的两种情况。一是 OpenCLIP 的 logits 分布太平softmax 之后接近均匀分布任何学生输出和它算 KL 散度差异都很小二是学生模型容量不够拟合不了 teacher 的细粒度输出。解决把温度下调到 1 或 2让 soft target 更尖锐确认保存的是 logits 而不是概率否则你根本没机会换温度重算如果还有问题把 alpha 降到 0.5、beta 提到 0.5让硬标签主导学习soft loss 只做辅助平滑。5.3 换一个 OpenCLIP 预训练权重结果天差地别现象同一个数据、同一套代码pretrained从laion2b_s34b_b79k换成openai蒸馏后一致率差了 5 个点以上。原因不同来源的 CLIP 权重训练数据分布差异很大。OpenAI 原始权重对自然图像、常见物体类别名更可靠LAION 权重在更广泛更杂的图文对上训练对细粒度、非自然图像、抽象短语的鲁棒性更高。解决按数据域选权重。常规相机照片、常见物体用openai工业缺陷、遥感、医疗影像这类特殊域先在少量留出集上分别跑 teacher比较伪标签分布和后续一致率再做决定。最稳妥的做法是跑两个 teacher蒸馏时把两套 logits 平均等于免费做了一层集成。5.4 类别名是中文或太抽象这一类的一致率直接对半砍现象其他类别一致率都到了 90%就某一类只有 50%看伪标签这一类的样本大量被 teacher 分错到相近类上。原因CLIP 是英文训练中文类别名先过翻译就不准太抽象的类别名比如“故障”“异常”“风景”没有视觉实体图文对齐的分数低。解决把类别名写成描述性短语。给出一个映射示例name_mapping { 划痕: a close-up photo of a scratched metal surface, 凹陷: a close-up photo of a dented car body panel, 正常: a clean and undamaged car body panel, 背景: an out-of-focus background area of a workshop, }同时对每个类别生成多条模板变体取文本特征均值能缓解单条 prompt 不准的问题。模板变化比如加 different、a large、a small 这类词平均之后特征更稳a photo of a {c}单模板跑不动的类别多模板平均往往能拉回来。5.5 蒸馏完精度反而不如直接训伪标签现象加 soft loss 后一致率从 92% 掉到 89%损失函数看起来没毛病结果反而亏了。原因alpha 权重过高student 被 teacher 的模糊输出带偏了。OpenCLIP 在部分样本上本身就置信度低、分布跨类soft loss 比例太大会把这种不确定性放大导致 student 变得“不敢给确定答案”。解决第一阶段先纯 hard 训练 5 轮再切混合 lossalpha 从 0.3 起步看一致率涨再往上加。这也是 4.3 里两阶段策略的意义。记住一个原则hard 标签是下限soft logits 是上限alpha 是两者之间的游标。6. 验证与进阶从跑通到能上线还要做的两件事模型跑通容易上线前总得有人说清楚它到底行不行。我的习惯是用三板斧验收一致率、类别分布、人工抽验三关都过才敢部署。6.1 上线前验收三板斧一致率、类别分布、人工抽验第一板斧是一致率。student 在留出集上和 teacher 的 top1 一致率到 95% 以上说明学生已经是“低配版 teacher”如果只有 80%知识还没学到位。第二板斧是看 student 预测的类别分布是否和 teacher 接近——如果 student 在“cat”上占比 20%teacher 只有 10%说明模型学到的是偏置而不是知识。第三板斧是从每个类别随机抽 30 张图给人看确认没有系统性错误。最后这一步最笨但零标签方案没有它兜底上线就是赌运气。6.2 进阶技巧伪标签清洗与二次蒸馏再谈部署进阶做法里最值钱的是伪标签清洗。蒸馏完成后用 student 对全量数据再预测一次筛出置信度高于 0.95 的样本和 teacher 原本的伪标签合并再做一轮自蒸馏。这个方法本质上是把 student 学到的信息反馈回去让伪标签质量随训练轮次提高是零标签蒸馏里少数能持续涨分的技巧。部署方面student 选 ResNet-18 或 MobileNetV3 就是为了这一步导出 ONNX、做 int8 量化参数量小损失可控。我自己的习惯是每一步都留一个能加载的 checkpoint 和对应的伪标签版本方便随时回滚。零标签蒸馏最怕的是一路训完才发现某个参数错了所以中间产物的落盘比精度更重要。希望这一套从选型到避坑的流程能帮到你少踩几个我踩过的坑。本文还有配套的精品资源点击获取
返回列表