简介:CLIP-ReID 是一套基于 CLIP 视觉语言模型的行人重识别与车辆重识别完整代码,面向计算机视觉领域的研究者、算法工程师和需要快速搭建 ReID 项目的开发者。该实现覆盖从数据准备、模型设计到训练评估的全流程,可用于行人检索、车辆重识别以及跨镜追踪等实际任务。资源包共包含 77 个文件,整体大小约 4.2MB;其中以 51 个 Python 脚本为核心,配合 12 个 YAML 配置文件和少量 XML、TXT 文档,负责模型构建、数据加载、训练推理、超参数配置与运行说明,目录按 loss、processor、data、configs、utils 等功能模块划分,易于定位和修改。该资源已吸引 642 人学习下载,适合具有一定深度学习基础并希望参考官方实现细节的读者。代码提供了完整的训练与测试入口,并内置两阶段训练处理器,支持多种损失函数如中心损失、三元组损失、ArcFace 和监督对比损失;同时兼容 Market1501、MSMT17、DukeMTMC、VeRi、VehicleID 等主流行人与车辆数据集,包含数据预处理、DDP 采样、评估指标、重排序及日志记录等配套工具。配合随包的模型示意图和标注文件,可以快速完成环境配置、结果复现和二次开发。
1. 为什么是CLIP-ReID:从ReID痛点说起
1.1 ReID任务到底是什么,难点在哪里
行人重识别(Person ReID)和车辆重识别(Vehicle ReID),说白了就是解决“跨摄像头找人/找车”的问题。一个目标在摄像头A出现,几分钟后在摄像头B又出现,算法需要在海量图库中把属于同一目标的那几张捞出来。这跟人脸识别不一样——ReID面对的往往是低分辨率、遮挡、视角剧烈变化的条件,人脸根本看不清,只能依赖整体外观特征。
传统做法大家应该不陌生:先拿ResNet、ResNet-50 or Swin Transformer这类主干网络提取全局特征,再用交叉熵损失配合三元组损失(Triplet Loss)去拉近同类、推远异类。这个套路在Market1501这类经典数据集上早就刷得很高了,但有一个致命问题:模型的“视觉词典”太小,只能在训练集见过的ID里去分辨。一旦跨数据集、跨场景,比如用A城市的数据训练,直接拿去B城市的摄像头下检索,性能会断崖式下跌。这背后的原因也好理解——CNN学到的特征更多是颜色、纹理等低层信息的组合,对“这件黑夹克配红背包”这种语义描述没有真正理解。
1.2 CLIP带来的变量:语义迁移与zero-shot潜力
CLIP(Contrastive Language-Image Pre-training)出现之后,ReID这个方向一下子就打开了新思路。CLIP是在4亿张图文对上训练出来的,它把图像和文本映射到了同一个语义空间,模型对“红色”“条纹”“SUV”这类概念有真实的语义理解,而不是单纯的像素统计。
CLIP-ReID这个项目,核心就是在ReID训练中引入CLIP的语义对齐能力。它不只是把CLIP当成一个更强的特征提取器,而是把CLIP的文本分支也拉进来参与训练——用文本描述去引导图像特征的学习。这样做最直接的好处就是:模型学到的特征更有泛化性,在训练集之外的新场景上,迁移能力明显强于传统ResNet方案。项目作者开源了整套训练和评估代码,行人、车辆两个任务都能直接跑,代码结构清晰,非常适合想在这个方向做实验或者落地算法的人参考。我自己的体会是,这套代码对刚接触ReID的新手也够友好,数据集格式理清楚之后,基本能复现论文里的大部分结论。
2. 整体设计与代码架构拆解
2.1 双流结构:图像分支与文本分支如何协作
CLIP-ReID不是简单地拿CLIP的image encoder出来跑一遍就完事,它的训练框架是双流学习的思路。
图像分支用一个视觉Transformer(通常是ViT-B/16)提取特征,这个分支会对输入图片做分块、位置编码、多层自注意力,最后拿到一个全局特征向量。文本分支这边比较讲究:每个训练ID会分配一个可学习的soft prompt(比如一组长度为16的文本token),这个prompt经过文本Transformer编码,得到当前ID对应的“语义描述向量”。训练时,模型让同一ID的图像特征和文本特征尽量靠近,不同ID之间尽量远离。这个设计很巧妙——文本分支相当于是给每个ID上了一堂“语义课”,图像分支学到的不再只是“这个人和那个人长得像”,而是“这个人具有哪些属性组合”。
代码里还有个细节值得留意:训练时文本分支生成的特征会和图像特征做相似度对齐,会同时算ID损失(交叉熵)和pairwise的对比损失。ID损失负责把类别分对,对比损失负责拉近模态间的距离。两个损失一起优化,比单纯用CLIP做backbone要稳得多。
2.2 与纯CNN/Transformer方案的对比:为什么选CLIP
直接放一张对比表可能更清楚,我在本地复现的时候把这些方案的差异总结过一遍:
| 方案 | 特征语义性 | 跨域迁移能力 | 训练成本 | 对标注数据依赖 |
|---|---|---|---|---|
| ResNet-50 + Triplet | 较弱,偏底层纹理 | 差,数据集间掉点明显 | 低 | 高 |
| Swin Transformer + ID Loss | 中等,能学全局 | 一般,稍好于CNN | 中 | 高 |
| CLIP零样本直接测试 | 强,语义明确 | 强,但ReID精度不足 | 无需训练 | 可零样本 |
| CLIP-ReID(本项目) | 强,语义+ID联合 | 强,跨域明显优于传统方案 | 中等 | 仍需要标注,但泛化好 |
从这张表能看出选CLIP做基座的逻辑:直接拿CLIP零样本去跑ReID是能跑的,但精度也就勉强能用,毕竟ReID要求的粒度太细——“穿黑色羽绒服、背双肩包的东亚男性”这种描述,CLIP能大致理解,但同一个描述对应的人可能有好几个,需要ID粒度上的精细区分。CLIP-ReID的做法就是把CLIP的语义底子拿过来,再用ReID数据做ID粒度的微调,语义和判别性两头都占。
2.3 车辆重识别场景下的适配细节
车辆ReID比行人ReID更折腾,原因也很朴素:车长的都差不多,同一品牌同一型号的黑色SUV,在外观上差异极小,最可靠的区分点是车身上的贴纸、年检标、临时物或者细微刮痕。CLIP-ReID在车辆任务上依然走双流结构,但训练时有个细节——车辆数据的类间相似度极高,ID损失的收敛速度比行人慢不少。
我在VeRi-776上跑的时候发现一个规律:车辆任务对文本prompt的初始化更敏感。如果prompt初始值跟车辆域差异太大,前几个epoch文本分支的loss会异常大,甚至把图像分支带偏。处理方法是把prompt的初始化范围调小,让它在训练初期保持低调,然后再逐步放开。另外,车辆ReID中“颜色”和“车型”这两个属性对CLIP来说本来就容易对齐,所以它的zero-shot迁移优势在车辆任务上体现得更明显。用Market1501训练的权重直接去VeRi-776上测,虽然mAP不会太高,但比传统ResNet要高出好几个点,这个在论文里有数据,我自己复现也验证过。
3. 实操:环境、数据与训练流程
3.1 环境搭建与依赖版本对照
整个项目基于PyTorch,依赖不算复杂。环境上我建议直接用Python 3.8或3.9,PyTorch版本选1.10以上的稳定版。有一个坑是:CLIP的tokenizer依赖ftfy和regex这两个库,clip包安装时会自动带上,但如果你只clone了项目没装clip,就会在数据加载阶段报ImportError。装CLIP官方库是最稳妥的,pip install ftfy regex tqdm然后把clip包放到项目能import到的地方就行。
依赖清单大致如下:
- Python 3.8+
- PyTorch 1.10+
- torchvision
- ftfy
- regex
- tqdm
- timm(部分分支用到)
- tensorboard
建议先创建独立conda环境再装,避免跟其他项目的环境冲突。配置文件一般就是yaml,集中在项目的configs目录下,backbone类型、prompt长度、温度系数、学习率这些都在里面改,不用动源码。
3.2 数据集准备与目录格式
ReID数据集的标准组织方式是Market1501那种文件夹风格。以Market1501为例,训练集路径结构是:
Market-1501-v15.09.15/ ├── bounding_box_train/ │ ├── 0001_c1s1_001051_00.jpg │ ├── 0001_c1s1_000976_00.jpg │ └── ... └── bounding_box_test/ ├── 0001_c1s1_001418_00.jpg └── ...文件名本身的含义是:0001是行人ID,c1s1是摄像头ID和序列号,后面是帧号。训练集和测试集中的ID不重叠,这是ReID评测的基本前提。车辆数据集VeRi-776的格式也类似,但文件名编码方式不同,写数据加载脚本时记得按照项目仓库里prepare脚本做一遍重命名,否则ID解析会错乱。
CLIP-ReID项目的数据加载部分通常会读一个meta.json或直接扫目录,把每个文件名解析成(img_path, pid, camid)的元组,再按pid划分train/query/gallery。这里最容易踩的坑是:query和gallery的划分逻辑搞反。行人检索的query是一张带标签的“目标图”,gallery是待检索图库,划分规则一般是每个摄像头下随机选一张作为query,其余进gallery。项目里已经写好了默认划分脚本,直接按README跑就行,不建议自己去重新划分,除非你有意做难样本评测。
3.3 训练启动与核心参数调优
环境配好、数据就位之后,启动训练就比较机械了。以单卡训练为例,命令大概是这样的风格:
python train.py \ --config configs/clipreid_market1501.yaml \ --dataset market1501 \ --data-path /your/data/path \ --output-dir ./logs/market1501打开配置文件,几个关键参数需要重点理解:
model.backbone: 默认ViT-B/16。如果显存紧张可以换成ViT-B/32,速度能提不少,精度会掉1-2个点。model.prompt_length: soft prompt的token长度,默认16。太短语义表达能力不足,太长训练开销变大,16是论文调出来的平衡点。loss.temperature: CLIP对比损失的温度系数,默认0.05左右。这个参数控制相似度分布的sharpness,温度越小,模型对难分对的样本惩罚越重。实际观察下来,0.02到0.1之间问题都不大,但别直接抄大模型训练里常用的0.07,ReID场景下容易训出“过于自信”的特征。optimizer.lr: 一般设1e-5到3e-5区间。注意CLIP的预训练权重已经被大语料对齐得很好,lr过大会在几个epoch内把语义空间洗掉,再想找回就难了。train.batch_size: 最好设成64以上,ReID的对比损失对batch内样本数量敏感,batch越小,hard negative的采样越稀疏。
论文里的做法通常是先在ImageNet或者LUPerson上做预训练(对行人来说LUPerson效果好),再到目标数据集上finetune几十个epoch。自己复现时如果资源有限,直接用CLIP预训练权重起步也能收敛,就是Rank-1会比论文低一些,这属于正常现象,别纠结。
3.4 评估流程与指标解读
训练结束后,评估脚本一般会进行一次全量特征提取:query图像过一遍模型,gallery图像过一遍模型,然后计算两者的余弦相似度矩阵,最后按相似度排序给出指标。核心指标是Rank-1和mAP。
| 数据集 | Rank-1(参考) | mAP(参考) |
|---|---|---|
| Market1501 | 90+ | 88+ |
| DukeMTMC | 88+ | 82+ |
| MSMT17 | 75+ | 55+(此数据集难度高) |
| VeRi-776(车辆) | 88+ | 65+ |
上面的数字是论文和社区复现的常见区间,实际值会因为随机种子、图像尺寸、是否开re-ranking有波动。有一件事我特别提醒:如果不做re-ranking,mAP会低不少,但Rank-1变化不大。Re-ranking本质是对初始排序再做一次k近邻重排,能把一些漏掉的正确样本捞回来,但开销很大,生产环境慎用。评估时如果发现Rank-1和mAP同时掉,多半是训练出了问题;如果只是mAP低而Rank-1正常,先别急着调参,试试开re-ranking再说。
4. 常见问题排查与避坑实录
4.1 显存不足与训练速度问题
CLIP的ViT-B/16在ReID任务里batch_size开64,输入分辨率256x128,单卡3090也就勉强够用,如果你用的是24G以下显存的卡,非常容易OOM。我的建议顺序是:先开混合精度(AMP)训练,通常能省30%左右显存;还不够就把batch_size降到32,同时把梯度累积步数设成2,等效batch不变但峰值显存降了;再不够才考虑换ViT-B/32。
另外有个提速小技巧:CLIP图像编码器在数据集上做finetune时,前面几层的特征比较通用,可以冻结前两层的参数,只更新后面层和prompt,速度能快不少,精度损失在可接受范围。我试过在Market1501上冻结前两层,Rank-1只掉了0.3个点左右,训练时间缩短了15%。
4.2 训练不收敛与loss异常
最常见的是文本分支的loss一开始就炸。原因我之前提过,prompt初始化不当或者学习率太猛。排查时先看训练日志前20步的loss曲线:
- 如果
image_loss正常下降但text_loss忽高忽低,大概率是prompt长度太短或温度系数太小。尝试把prompt_length从16提到32,温度系数从0.05改到0.1。 - 如果两个loss都直接飞掉,先检查学习率,CLIP内部的大预训练模型对lr极其敏感,
1e-4以上必飞。 - 还有种情况是训练集图片路径有损坏。某些开源数据集在搬运过程中会丢文件,图像解码失败会返回空张量,这种样本在DataLoader里不会报错,但会导致loss出现NaN。建议训练前先跑一下数据校验脚本,把能解码的图片数量跟目录文件数对一下。数据问题在ReID这类小数据集上比模型问题更常见。
4.3 zero-shot迁移效果不佳的排查
CLIP-ReID的亮点是zero-shot迁移,也就是在A数据集训练完,不经过微调直接在B数据集上做检索。如果你复现时发现zero-shot效果很弱,先检查两件事。
第一,B数据集上的query/gallery图片预处理是否跟训练集一致。ReID通常有裁剪、归一化、resize的固定流程,如果训练用256x128,测试却用224x224,特征分布肯定对不上。第二,确认你测试用的不是gallery里已有的ID。Zero-shot迁移的评测前提是训练集和测试集ID完全不相交,如果数据划分做错,某些ID训练时见过,测试分数会虚高;反之,如果划分交叉过多,模型会没见过足够多的“searching概念”,表现自然差。造成这两种问题的原因都是数据集划分脚本没仔细看,项目里自带的脚本是经过验证的,直接用它,别自己发明新划分。
4.4 数据集与后处理陷阱
再补几个我实际踩过的坑。第一,有些数据集带“junk”图片,比如Market1501里那些包含多个人或遮挡严重的图。官方评测协议会把junk放在gallery,但它们不计入mAP计算。如果你的评估脚本没按官方协议过滤junk,mAP会偏低。第二,做车辆ReID时要特别注意同型号不同ID的样本——它们长得实在太像,模型很容易学出“只看车型不看个体”的偷懒特征。如果在VeRi-776上训练时发现Rank-1一直上不去,可以在训练数据上做更强的随机擦除增强,强迫模型关注非共性的细节。第三,测试时关闭BatchNorm的training mode。很多ReID代码在提取特征时忘了调用model.eval(),BatchNorm统计量没固定,特征每个batch之间分布不一致,Rank-1会掉2个点左右,这个坑非常隐蔽,建议先排查再怀疑模型结构。
最后再分享一个使用技巧。CLIP-ReID训练出来的模型,单看Rank-1可能没有那些刷了点、用复杂re-ranking的大模型高,但论特征的可解释性,它能做很多传统ReID做不到的事情——比如直接输入一句“白色的轿车”做文本检索。我会在推理脚本里保留文本encoder的输出,这样同一套特征既能做以图搜图,也能做以文搜图,给下游系统扩展了不少玩法。项目的推理代码里已经保留了这部分接口,不用自己再从零写。
本文还有配套的精品资源,点击获取