ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于ViT的图像检索系统搭建:特征提取、相似度检索与FAISS索引实战

基于ViT的图像检索系统搭建:特征提取、相似度检索与FAISS索引实战 简介面向人工智能图像检索初学者与深度学习开发者这是一份基于视觉TransformerViT预训练模型实现的相似图像检索项目。它解决从本地图像库中快速查找与查询图最相似图片的问题通过预处理、特征提取、余弦相似度比较和内存数据库构建完成端到端检索流程。资源共2个文件包含1个Python脚本和1个Markdown说明文档压缩包仅2KB脚本基于PyTorch实现使用预训练的vit_b_16权重提取图像特征并完成数据库构建与相似度排序文档则对核心组件、模型调用方式和运行逻辑作简要说明。目前已有662人学习下载。读者可借此掌握视觉Transformer特征向量的实际落地方式了解如何用余弦相似度衡量图像语义接近程度并可直接修改脚本适配自定义数据集作为图像检索入门或毕业设计模块的轻量参考。 如果你手里有几万张商品图想通过一张查询图快速找出款式、颜色、构图都相似的图片传统的关键词标签办法很难覆盖所有视觉维度这时候就需要一套“以图搜图”的方案。而基于ViTVision Transformer视觉Transformer模型来实现相似图像检索是目前比较稳、上限也比较高的做法之一——不需要额外标注不需要训练分类器只要用预训练模型把图片变成向量再算向量之间的距离就行。这篇文章就把我在这类项目里踩过的坑、验证过的细节和完整可落地的流程整理出来给打算自己动手做图像检索的同学一个参考。这个方案适合这几类人刚接触多模态或向量检索的工程师、想做商品图或素材库搜索的产品同学、以及想深入了解ViT模型到底怎么用在真实任务上的研究者。我会从整体设计讲起再拆解每个环节的取舍依据最后给出一套可以直接跑的实操流程和常见问题速查。1. 项目目标与整体技术思路1.1 为什么用ViT做图像检索传统图像检索多用CNN卷积神经网络提取特征比如ResNet系列。CNN的感受野是逐步扩大的对局部纹理比较敏感但遇到“整体构图相似但局部细节不同”的图片时CNN的特征往往纠缠在局部纹理上检索出来的结果经常让人摸不着头脑。ViT把图像切成一堆patch然后通过自注意力机制在全局范围内建模patch之间的关系这相当于模型一开始就在同时看整张图的上下文。体现在检索任务上就是它对“语义相似”和“结构相似”的判别力明显更强。我用同一批商品图对比过ResNet50和ViT-Base的检索效果查询图是一张白色运动鞋的侧视图ResNet的结果里混进了好几张相近色但完全不同款式的休闲鞋而ViT返回的前10张里鞋型轮廓和摆放角度都比较贴近。这背后的原因在于ViT的全局注意力能把“鞋跟高度、鞋头弧度、鞋带走向”这类需要跨区域对比才能判断的特征整合起来而不是只盯着某个patch的纹理。1.2 完整检索流程拆解一个图像检索系统听起来复杂拆开其实就四步图像预处理、特征提取、向量索引、相似度检索。预处理把输入图片统一缩放到模型要求的尺寸做归一化保证每张图进入模型时的数据分布一致。特征提取用ViT模型跑一次前向把分类token或全局池化后的向量拿出来这就是“图像的特征表示”。向量索引把所有图片的特征向量收集起来建索引方便后续快速查找。相似度检索给定一张查询图提取它的特征向量与索引库里的向量计算相似度返回TopK结果。这个流程里最关键的不是模型本身而是“特征怎么取”和“相似度怎么算”。很多新手一上来就选个最大的模型跑去提特征结果检索效果还是差多半是卡在这两个细节上。后面我会分别展开说。2. 关键模型设计与技术选型解析2.1 骨干网络怎么选Base还是LargeViT家族里最常用的是ViT-Base和ViT-Large。Base模型参数量约8600万输出特征维度通常是768维Large模型参数量约3亿特征维度1024维。选型时主要看你的数据规模和算力情况。数据量在几万到几十万级别用ViT-Base就够检索质量已经明显优于CNN而且显存占用和推理速度都可控。数据量到百万级以上或者图片类别极其丰富、细粒度差异很关键时Large模型的高维特征更有区分度但需要相应提升显存和索引存储的开销。另外要注意同样叫ViT不同的预训练权重效果差距非常大。我的经验是优先用在ImageNet-21k上预训练过的权重或者CLIP的ViT权重。CLIP训练的ViT特征是图文对齐的对语义相似性的捕捉特别强非常适合做检索纯ImageNet权重的优势则在于对物体类别更敏感。如果业务场景偏“视觉相似”我建议两个都试一下在评测集上跑一下mAP再决定。2.2 位置编码的选择一个容易被忽略的坑ViT默认使用的是可学习的绝对位置编码也就是把每个patch的位置信息直接加进token embedding里。这种方式实现简单绝大多数预训练权重也是这么训练的所以迁移使用时最稳妥。但有些同学会想能否换成相对位置编码或者去掉位置编码。这里要泼一盆冷水对于图像检索这种任务换位置编码大概率会导致精度下降。原因很简单预训练模型已经习惯了绝对位置编码提供的位置先验你换了编码方式等于让模型在一个陌生的输入分布上做推理。除非你打算做大规模微调否则不建议折腾。在实际项目中如果你用的是timm库加载权重默认就带了位置编码直接微调或推理即可如果用的是transformers库的ViTModel需要留意output_attentions之类的参数但位置编码部分不需要额外干预。真正要小心的反而是预处理时的尺寸变化——比如模型原始训练尺寸是224x224你为了保留细节改成384x384patch数量变了位置编码的维度就对不上。解决办法是加载权重时设置img_size384模型会自动插值位置编码这一点timm做得很方便。2.3 相似度度量与向量索引方案特征向量提取出来后常用相似度度量有两种余弦相似度和欧氏距离。在图像检索里我的建议是统一先对特征向量做L2归一化再用余弦相似度做排序。因为归一化之后余弦相似度与内积等价向量模长的影响被去掉检索结果更关注方向上的相似也就是“语义和结构上的相似”而不是某张图片整体亮度、饱和度带来的数值差异。索引方面如果你的检索库在十万级以内直接用暴力计算矩阵乘法就行毫秒级响应完全扛得住。到了百万级甚至千万级就要引入ANN近似最近邻索引。实际项目中我用得比较多的是FAISS的IndexFlatIP和IndexIVFFlat场景建议索引说明库小于10万要求高精度IndexFlatIP暴力精确检索余弦相似度结果最准库大于10万允许轻微召回损失IndexIVFFlat先聚类再检索速度快很多精度损失可控内存紧张库特别大IndexIVFPQ对向量做乘积量化内存显著降低但要调参从实践来看第一次搭建不要急着上IVF或PQ这类加速索引先用精确的暴力检索把流程跑通拿到baseline效果再根据瓶颈决定是否优化。3. 完整实操流程3.1 环境准备与模型加载我用的是PyTorch timm FAISS的组合这几个库生态成熟文档齐全踩坑成本低。环境安装命令如下pip install torch torchvision timm faiss-cpu pillow numpy scikit-learn模型加载我推荐用timm一行代码就能拿到预训练ViT还能方便地控制输出特征。这里以ViT-Basepatch16输入224x224为例import timm import torch from PIL import Image from torchvision import transforms device cuda if torch.cuda.is_available() else cpu model timm.create_model(vit_base_patch16_224, pretrainedTrue, num_classes0) model model.to(device).eval()关键就在num_classes0这会让模型去掉最后的分类头直接输出特征向量。默认输出维度是768对应ViT-Base。如果你更习惯用HuggingFace transformers加载本地模型也可以这样做from transformers import ViTModel, ViTImageProcessor processor ViTImageProcessor.from_pretrained(/your/local/vit-base-patch16-224) model ViTModel.from_pretrained(/your/local/vit-base-patch16-224)这里说的“本地模型”指你提前下载好的预训练权重目录可以避免每次运行都去外网下载。建议把权重文件放到固定的模型目录统一管理也方便以后做对比实验。3.2 图像预处理细节ViT的预处理逻辑很多人在切换模型时容易出错。timm里的预训练模型通常要求输入做mean[0.485, 0.456, 0.406]、std[0.229, 0.224, 0.225]的标准化尺寸统一为224x224。如果你用transformers的processor它会内置这些参数直接用就行。自定义pipeline时一定要保持一致transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])这里有一个实操细节容易被忽略Resize直接压缩图片会改变宽高比导致物体形变影响检索效果。如果你的图片本身长宽比差异很大建议用Resize(256, interpolation3)后再做CenterCrop(224)先统一短边再中心裁剪能在保留更多原始信息的同时满足模型输入要求。我在处理用户上传的商品图时发现这一步对检索质量的影响经常超过换模型的区别。3.3 特征提取函数与向量归一化提取特征的核心逻辑很简单但要注意关闭梯度计算和切到eval模式避免意外更新BatchNorm和注意力层的状态torch.no_grad() def extract_feature(image_path): img Image.open(image_path).convert(RGB) img_tensor transform(img).unsqueeze(0).to(device) feat model(img_tensor).cpu().numpy().flatten() # L2归一化 feat feat / np.linalg.norm(feat) return feat这段代码返回的就是一个归一化后的768维向量。所有库里的图片都过一遍这个函数把向量存起来。保存格式我建议用numpy的.npy或者直接拼成一个大矩阵存成.npy同时把图片路径列表存成.txt或.json保持一一对应关系。这里有一个容易踩的坑如果图库里有大量相似甚至重复的图片特征会很接近但归一化后的小数值差异依然有意义。所以不要为了节省存储把向量强转成float16前期建议用float32等检索质量稳定后再考虑压缩。3.4 构建索引与执行检索所有特征都提取好后就可以建索引了。这里以FAISS的精确检索为例import numpy as np import faiss # feature_matrix: shape (N, 768), float32, 已归一化 index faiss.IndexFlatIP(768) index.add(feature_matrix.astype(np.float32)) # 查询 query_feat extract_feature(query.jpg).astype(np.float32) D, I index.search(query_feat.reshape(1, -1), k10)I返回的是索引库中相似度最高的前10个图片下标D是对应的相似度分数。注意因为我们已经做过归一化IndexFlatIP的内积就是余弦相似度分数越接近1说明越相似。我自己习惯加一个阈值过滤比如相似度低于0.75的直接不展示减少低质量噪声的干扰。阈值需要根据你的图库特点调整商品图库通常0.75-0.85比较合理通用素材库可以放宽到0.7。如果你的图库到了几十万级别可以考虑IVF索引但第一次做的话我建议先精确检索后面再按需优化。3.5 效果评测怎么判断检索好不好评测是很多项目虎头蛇尾的部分。我做这类项目时最简单的评测方式是自建小规模query集人工标注出每个query对应的相关图片然后计算RecallK或PrecisionK。举个例子你挑选50张查询图每张图人工确认5张相关图检索返回Top10里有多少张落在相关集合内就是Recall10。批量评测时可以用一个简单脚本统计def recall_at_k(query_feat, relevant_ids, k10): D, I index.search(query_feat.reshape(1, -1), k) hit len(set(I[0].tolist()) set(relevant_ids)) return hit / len(relevant_ids)如果时间紧不想手工标注也可以用“查询图自身”做召回测试把每张图从索引库里拿出并作为query如果检索结果的Top1不是它自己说明特征提取或索引过程大概率有问题。这个自检方法能快速暴露bug但不能反映真实检索质量。3.6 引入模型融合提升检索效果如果你发现单一模型的特征在某些场景下不够稳定可以试试特征融合。比较省力的做法是把ViT-Base和ViT-Large的特征拼接起来或者把CLIP-ViT和ImageNet-ViT的特征拼接起来再一起做检索。拼接后维度变高索引和存储开销增大但检索精度往往有稳定提升。我实测过的一个方案是把CLIP-ViT-B/32和timm的ViT-Base特征拼接成1536维在服装场景下mAP提升了3到5个百分点。如果你做过实验发现两个模型特征尺度差异太大记得分别归一化后再拼接而不是先拼接再归一化。原因很简单不同模型的原始向量分布不在同一尺度上直接拼接会让高范数的特征主导相似度计算。4. 常见问题与排查技巧实录4.1 检索效果差先从预处理和特征下手遇到检索结果莫名其妙时不要第一时间怀疑模型不够大。我排查问题时有一个固定顺序先是预处理再是特征提取方式最后才是模型选型。预处理最容易被忽视的是图片方向。很多手机拍摄或用户上传的图片自带EXIF旋转信息读取时没有自动转正进入模型的就是旋转后的图像特征自然不对。解决办法是用ImageOps.exif_transpose把图片转正再做后续处理。特征提取方式上很多人直接把ViT所有patch token平均池化作为特征。这在某些场景下效果尚可但ViT最后一层分类token经过整个Transformer层的沉淀信息更集中默认推荐用它。如果分类token效果不理想再尝试concat分类token和平均池化特征往往能兼顾全局和局部信息。4.2 显存不够或推理太慢显存不够时优先降低batch size而不是更换模型。提取特征是离线操作用batch方式虽然快但单卡显存有限。我实际处理10万张图时batch size设为32ViT-Base的显存占用在7GB左右刚好适应常见显卡。如果显存只有4GB可以把batch size调到8甚至4或者用torch.cuda.amp.autocast()半精度推理。推理速度方面ViT-Base在CPU上单张图大约200到400毫秒GPU上能到10到20毫秒。如果离线提取特征这个速度完全能接受。如果是实时查询建议把模型常驻显存并用半精度推理。进一步加速可以考虑用ONNX导出模型但在项目中收益没那么大前期不推荐折腾。4.3 索引规模变大后检索变慢如果你从精确检索切换到IVF索引有个重要参数是nlist也就是聚类中心个数。经验法则是nlist sqrt(N)N为图库总量。IVF检索时会先找到最近的聚类中心所在桶再在桶内精确搜索nprobe表示搜索几个桶值越大越准但越慢。举个例子100万张图的库nlist设为1000nprobe设为10检索速度能从暴力检索的每秒几百次提升到几千次而Recall10损失通常控制在2%以内。但请记住切换索引前一定要用精确索引跑出baseline否则你根本不知道压缩或近似带来的损失有多大。4.4 一个容易被忽略的bug特征矩阵和路径列表顺序不一致提取特征时如果用了多线程或多进程很容易出现特征矩阵与图片路径列表顺序不匹配的问题。一旦顺序乱了检索返回的是“张冠李戴”的结果而且这种问题表面很难察觉。排查方法很简单随机挑几张图把它作为query查一下Top1是不是它自己如果不是通常就是顺序错位或者特征与路径对应关系坏了。我推荐在保存特征矩阵时同时保存一份图片路径列表并且每次加载后都检查一下长度是否一致。如果用了shuffle或者数据增强提取特征时必须保证原始文件名与特征向量是一一对应写入的不要用隐式顺序。5. 这个方案还可以怎么扩展做到这里一套基于ViT的相似图像检索系统已经能跑通了。但如果你的业务需求更复杂还可以在现有基础上扩展几个方向。语义检索混合把ViT图像特征和CLIP文本特征放到同一个向量空间实现“输入文字描述也能检索图片”甚至“以图搜图文字精排”。增量更新图库每天新增图片时不必全量重建索引。FAISS支持index.add()增量添加向量但IVF索引在建好后添加向量会落在不合适的聚类桶里建议定期重建或使用能增量更新的索引类型。这个问题在近线更新场景下特别重要。排序优化如果对检索精度要求很高可以先用向量检索召回Top100再用一个rerank模型或人工规则对Top100精排既能控制速度又能提升精度。我在实际使用中发现ViT模型的稳定性比想象中好真正影响系统体验的往往是工程细节比如预处理、特征归一化、索引更新策略。做这类项目先把数据流程梳理清楚再投入精力调模型是性价比最高的路径。最后再分享一个小技巧第一次搭建时不要一上来就追求高级索引和分布式部署。用一台带GPU的开发机几百行代码你就能完成一个能用的图像检索demo然后在这个基础上逐步加数据、调参、换模型。这类项目的复杂度往往是被数据规模逼出来的而不是一开始就需要高深的设计。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进