多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Chinese-CLIP图文检索系统实战:从双塔原理到代码落地

Chinese-CLIP图文检索系统实战:从双塔原理到代码落地 简介面向计算机视觉课程的期末大作业与课程设计场景这套基于Chinese-CLIP的图文检索系统提供完整的Python实现源码与配套文档说明。项目涵盖图像与文本特征提取、跨模态匹配、检索排序及可视化界面等核心模块代码含详细注释适合需要快速搭建可演示系统的高校学生参考。压缩包共59个文件以40个Python脚本为主力辅以9个JSON配置、说明文档与界面预览图整体仅543KB轻量便于本地部署与二次修改。已有178人浏览学习属于评分较高的大作业资源。下载后可按文档完成环境配置直接运行入口程序即可体验图文双向检索流程同时通过目录结构理解CLIP模型调用、预处理与评估环节为答辩和功能扩展提供扎实基础。1. 计算机视觉课程设计选这个方向前先想清楚这两件事大学里的计算机视觉课程设计每年都有大量同学倒在选题这一步。选目标检测数据标注标到凌晨选图像分类跑出来的精度和随机猜测差不多。如果你手里的题目是“基于Chinese-CLIP的图文检索系统”我先说一句这个题选得不亏。它要搭的是一个双塔检索系统把图库里的每张图编码成向量用户输入一句中文或另一张图系统返回语义最匹配的图片。它不依赖框级别的标注又能完整展示你对深度学习和工程落地的理解是计算机视觉大作业里性价比很高的方向。这篇笔记按“原理→环境→代码→避坑→加分”的顺序把能复现的细节全部摊开讲新手照着走能交差熟手也可以直接拿走做检索基线。2. 为什么是Chinese-CLIP而不是CLIP双塔结构、中文分词与检索流程拆解2.1 图文检索的本质把图和文本装进同一个向量空间图文检索系统要解决的核心问题是“语义对齐”。一张猫在沙发上的照片和一句“一只猫趴在沙发上”在字面上完全不像但在语义上是同一个东西。传统做法靠标签匹配图上有“猫”标签、文本里有“猫”字才能召回换个说法比如“主子”“喵星人”就彻底断掉。CLIP 这类模型的做法是让图像编码器和文本编码器各自把输入映射到一个共享向量空间在这个空间里语义相近的图和文本向量距离也近。这里的关键是双塔结构。图像塔通常用 ViT 或 ResNet文本塔用 BERT 系的 Transformer两个塔各自独立 forward最后把向量做 L2 归一化再算余弦相似度。训练时用对比学习目标一个 batch 里有 N 对图文对角线上的 是正样本对要拉近非对角线的是负样本对要推远。这比“把图和文字拼在一起过一个大模型”的分类方案灵活得多分类方案换一个类别就要重新训练双塔方案里图库是动态的换库不需要动模型。推理时双塔的优势更明显。图库里的图片可以离线预先全部过一遍图像塔把特征矩阵存成文件。在线查询时只用算一句文本的特征然后和这个矩阵做一次乘法就能拿到全库相似度排序。这也是为什么检索系统普遍选双塔而不是单塔——单塔没法预计算图库特征每来一个 query 都得把所有图文对重新过一遍模型实时性完全跟不上。2.2 Chinese-CLIP 对中文的“本土化改造”原版 CLIP 是 OpenAI 用英文图文对训练的文本塔基于英文 BPE 分词。直接拿它处理中文文本塔看到的是被拆成碎片的 token甚至一个汉字被切成两半语义信息丢失严重。再加上训练数据里几乎没有中文英文 CLIP 在中文图文检索上基本属于“能用但很勉强”做课程设计答辩时很容易被老师问倒。Chinese-CLIP 是阿里达摩院基于 open_clip 框架实现的中文版本训练数据来自 Wukong 数据集和 LAION-5B 的中文子集文本塔换成了针对中文预训练的 RoBERTa-wwm-ext。RoBERTa-wwm 在全词掩码策略下训练对中文分词和语义理解比英文 BPE 靠谱得多。Chinese-CLIP 提供了多个规格的权重从 ResNet-50 到 ViT-B/16、ViT-L/14不同规模对应不同显存需求。做课程设计建议选 ViT-B/16效果和资源占用比较平衡单张消费级显卡跑得动。下表把两个模型的关键差异列一下答辩时可以直接用对比项原版 CLIPChinese-CLIP文本编码器基础英文 BERT英文 BPE 分词RoBERTa-wwm-ext中文全词掩码主要训练数据LAION-400M 等英文图文对Wukong、LAION-5B 中文子集中文输入支持差中文被切成碎片原生支持中文语义理解明显更好典型应用场景英文图文检索、英文 zero-shot 分类中文电商检索、中文图文匹配、课设系统2.3 图文检索系统的完整链路离线建库与在线查询分离一个完整的图文检索系统工程上分为两条链路。离线建库把图库全部图片经过图像塔编码得到特征矩阵连同图片路径一起落盘。在线查询用户输入中文文本或一张查询图经过对应编码塔得到查询向量与特征矩阵做内积排序取 Top-K。离线部分可以慢慢跑在线部分要求毫秒级响应两者解耦之后图库更新只需要重跑离线建库模型本身不用动。实现上有一个小的性能要点不要在一个 for 循环里逐张计算查询向量和每张图片的余弦相似度。正确做法是先把全库特征拼成一个 (N, D) 的矩阵查询向量是 (1, D)一次矩阵乘法就能得到 (1, N) 的相似度分数。依赖矩阵乘法而不是循环一方面 GPU 对矩阵乘的并行优化非常成熟另一方面也省掉了 Python 层循环的开销。后面第 4 章的代码就是按这个思路写的。3. 环境搭建与数据集准备从 Python 版本到图文对目录3.1 依赖安装与模型权重下载最容易被卡住的一步这个项目对 Python 版本没有很苛刻的要求3.8 到 3.10 都可以我建议直接用 3.9 或 3.10。核心依赖是 PyTorch、transformers、Pillow、numpy。transformers 需要 4.24 以上版本才内置了 ChineseCLIP 的模型类和处理器类低于这个版本会报找不到类的错误。安装命令如下pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install transformers4.24 pillow numpy tqdm说明一下参数第一行指定了 CUDA 11.8 版本的 PyTorch如果你本机 CUDA 版本不同去 PyTorch 官网选对应命令即可没有 NVIDIA 显卡就把--index-url去掉装 CPU 版后面代码里设备会自动选择。第二行的4.24是关键transformers 的 ChineseCLIP 支持从 4.24 开始低版本没有ChineseCLIPModel这个类。权重下载走的是 HuggingFace 仓库模型编号是OFA-Sys/chinese-clip-vit-base-patch16。第一次运行from_pretrained时会自动下载如果网络不稳定可以先手动设置镜像环境变量再运行export HF_ENDPOINThttps://hf-mirror.com这是国内访问 HuggingFace 最常用的替代方案下载完成后权重缓存在本地后续运行不再走网络。权重文件包含模型结构、文本词表和图像预处理配置所有文件加起来约 500MB 量级下载前确认磁盘空间充足。3.2 数据集这四种选法按你的课设要求从中挑一个数据是图文检索课设里最容易被低估的环节。很多人一上来就想着自己爬数据结果爬下来图片质量参差、文本描述乱写最后系统的检索结果怎么调都奇怪。实际上有现成的公开中文图文数据集按自己的时间预算选一个就行数据集规模特点适合场景Flickr8k-CN8000 张图每张 5 条中文描述规模小加载快课设经典选择入门、快速跑通COCO-CN约 2 万张图中英双语描述内容覆盖日常场景规模适中想做得更完整一点MUGE电商图文数据商品图与中文描述领域性强想做电商检索方向的展示自建数据几十到几百张自己收集图片并写中文描述有特定主题需求我一般建议课设选 Flickr8k-CN数据量小、预处理简单跑一次离线建库在 CPU 上也就几分钟。如果你想突出自己的工作可以自己拍 50 张校园照片每张写一句中文描述做成一个“校园场景图文检索”的 demo这个比直接跑公开数据集更能打动答辩老师。无论选哪个数据集都建议整理成统一的目录结构一个文件夹放图片一个 UTF-8 编码的文本文件记录“图片文件名 Tab 中文描述”。Tab 分隔比逗号分隔稳因为描述文本里经常出现中文逗号用逗号分列容易拆错。3.3 Dataset 类与文本清洗先让程序跑起来再谈精度数据读取建议写成 Dataset 类后续换数据集只需要改路径和解析逻辑。这里给出一个最小实现import os from PIL import Image from torch.utils.data import Dataset class ImageTextDataset(Dataset): def __init__(self, img_dir, ann_file, transformNone): self.img_dir img_dir self.transform transform self.samples [] # ann_file: 每行 图片文件名TAB中文描述 with open(ann_file, r, encodingutf-8) as f: for line in f: parts line.rstrip(\n).split(\t) if len(parts) 2: img_name, caption parts self.samples.append((os.path.join(img_dir, img_name), caption)) def __len__(self): return len(self.samples) def __getitem__(self, idx): img_path, caption self.samples[idx] img Image.open(img_path).convert(RGB) if self.transform: img self.transform(img) return img, caption这个类的作用是把“图片文件路径 对应中文描述”组织成可迭代的样本对__getitem__返回的是 PIL 图像和原始文本。注意img.convert(RGB)这步有些图片是 RGBA 或灰度模式不转 RGB 的话后面预处理会报通道数不匹配。文本清洗这里不用做很重把全角空格替换成半角、去掉首尾空白就行。很多人纠结要不要去停用词实际上 CLIP 系的文本编码器对停用词不敏感清洗过多反而丢失语义。4. 核心代码实现特征提取、向量检索与 Top-K 可视化4.1 加载模型与预处理分辨率、归一化与设备放置模型加载用 transformers 的封装是最稳的路径from_pretrained一句自动下载权重并初始化结构。处理器Processor负责把图像和文本都转成模型需要的输入格式图像部分内部包含缩放、裁剪、归一化文本部分包含分词、编码、padding这些细节如果自己手写很容易写错用官方处理器可以少踩很多坑。import torch from transformers import ChineseCLIPModel, ChineseCLIPProcessor device cuda if torch.cuda.is_available() else cpu model_id OFA-Sys/chinese-clip-vit-base-patch16 model ChineseCLIPModel.from_pretrained(model_id).to(device) processor ChineseCLIPProcessor.from_pretrained(model_id) model.eval() print(fmodel loaded on {device})逻辑说明model是双塔模型对象内部包含get_image_features和get_text_features两个方法分别对应图像塔和文本塔。processor是所有输入的预处理入口调用时传images...或text...会自动分发。model.eval()这行很容易被忘掉不切到 eval 模式的话模型里 Dropout 层仍处于激活状态同样输入每次得到的特征都不一样检索结果会抖动。4.2 离线建库把图库编码成特征矩阵并落盘离线建库的目的是把图库里每一张图片变成向量最后拼成一个 (N, D) 的特征矩阵存到磁盘。D 是特征维度ViT-B/16 的 Chinese-CLIP 输出 512 维。N 是图片数量。向量在做检索前必须 L2 归一化这样后面矩阵乘法算出来的内积就是余弦相似度范围在 [-1, 1]不归一化的话高分特征天然占便宜检索结果会偏向“亮、纹理多”的图片。import os import numpy as np from PIL import Image from tqdm import tqdm gallery_dir data/gallery image_paths [os.path.join(gallery_dir, f) for f in os.listdir(gallery_dir) if f.lower().endswith((.jpg, .jpeg, .png))] feats [] with torch.no_grad(): for p in tqdm(image_paths): img Image.open(p).convert(RGB) inputs processor(imagesimg, return_tensorspt).to(device) feat model.get_image_features(**inputs) feat feat / feat.norm(dim-1, keepdimTrue) feats.append(feat.cpu().numpy()) feat_matrix np.concatenate(feats, axis0) np.save(gallery_features.npy, feat_matrix) with open(gallery_paths.txt, w, encodingutf-8) as f: f.write(\n.join(image_paths)) print(gallery feature matrix:, feat_matrix.shape)逻辑说明循环里每个processor调用会得到模型输入字典get_image_features返回的是 (1, D) 的张量归一化之后转成 numpy 放入列表最后np.concatenate沿第 0 维拼起来得到 (N, D)。图片路径列表单独存成文本文件检索出索引后要靠它找到对应图片。参数上有一点可以调processor默认把图像缩放到模型要求的输入尺寸一般是 224x224如果图库里都是超大尺寸的图片可以在 processor 调用时显式传入size224避免默认逻辑在某些版本下行为不一致。这里的循环是串行 forward图库几百张图没问题。如果图库达到几千张或更多建议改成 batch 处理一次喂 32 张图显存占用和速度都能优化。代码里torch.no_grad()是必须的推理模式下关掉梯度计算能省大量显存和计算时间。4.3 在线检索文搜图与图搜图的完整实现图库特征落盘后在线检索逻辑就非常简单了。把特征矩阵加载回显存查询文本过文本塔得到 (1, D) 向量一次矩阵乘法拿到全库相似度topk排序取前 K 个。查询图走图像塔同理只改一个方法名。gallery_feat np.load(gallery_features.npy) gallery_paths open(gallery_paths.txt, encodingutf-8).read().splitlines() gallery_feat torch.from_numpy(gallery_feat).to(device) def search_by_text(query, k5): with torch.no_grad(): inputs processor(textquery, return_tensorspt).to(device) t_feat model.get_text_features(**inputs) t_feat t_feat / t_feat.norm(dim-1, keepdimTrue) scores t_feat gallery_feat.T topk scores.topk(k) for score, idx in zip(topk.values[0], topk.indices[0]): print(f{score.item():.3f}\t{gallery_paths[idx]}) def search_by_image(query_img, k5): with torch.no_grad(): inputs processor(imagesquery_img, return_tensorspt).to(device) q_feat model.get_image_features(**inputs) q_feat q_feat / q_feat.norm(dim-1, keepdimTrue) scores q_feat gallery_feat.T topk scores.topk(k) for score, idx in zip(topk.values[0], topk.indices[0]): print(f{score.item():.3f}\t{gallery_paths[idx]})逻辑说明t_feat gallery_feat.T这行本质上是 (1, D) 和 (D, N) 做矩阵乘法结果是一个 (1, N) 的相似度向量。由于两个向量都做了 L2 归一化这个内积就是余弦相似度。torch.topk返回两个对象values 是对应的相似度分数indices 是图库里的位置索引。索引去gallery_paths列表里取值就能拿到图片路径。这里值得强调的是查询文本不需要额外加任何前缀模板。很多人跑过英文 CLIP 的 zero-shot 分类习惯性给文本加 “a photo of...” 这样的 prompt在图文检索场景这是多余的直接输入用户原始查询语句即可。另外如果查询文本是英文Chinese-CLIP 的效果会退化这不算是 bug模型的文本塔是为中文设计的课设 demo 里尽量全中文输入。4.4 评估指标与可视化答辩时最有用的一张图课程设计答辩时老师最关心的是“你怎么证明你的系统是有效的”。图文检索的标准评估指标是 RecallK给定一组查询每个查询有一个人工标注的正确答案在库里的索引系统返回 Top-K 结果如果正确答案出现在其中就算命中。命中数除以查询总数就是 RecallK。K 通常取 1、5、10。课程设计阶段不需要做完整的评测集手动构造 20 到 50 个查询-答案对就够展示效果了实现也很直接def recall_at_k(query_texts, ground_truth_indices, k5): hits 0 for query, gt_idx in zip(query_texts, ground_truth_indices): with torch.no_grad(): inputs processor(textquery, return_tensorspt).to(device) t_feat model.get_text_features(**inputs) t_feat t_feat / t_feat.norm(dim-1, keepdimTrue) scores t_feat gallery_feat.T topk scores.topk(k) if gt_idx in topk.indices[0].tolist(): hits 1 return hits / len(query_texts) print(R5:, recall_at_k(test_queries, test_gt_indices, k5))可视化方面建议做一个“查询词 图片九宫格 相似度分数”的展示页面。用 matplotlib 就能实现每张检索图下面标上分数一眼能看到排序是否合理。这张图放在课设报告里比贴十页代码有说服力得多。5. 避坑五个能把人卡一晚上的问题5.1 权重下载卡在 0%网络与镜像现象第一次运行from_pretrained进度条长时间不动或者下到一半报连接超时。原因HuggingFace 默认域名在国内网络环境下的连通性不稳定加上权重文件体量大很容易中断。解决先设置HF_ENDPOINThttps://hf-mirror.com环境变量再跑代码也可以在命令行手动下载整个仓库目录放到本地缓存路径。下载完成后from_pretrained会自动识别本地缓存后续不再联网。5.2 检索结果答非所问预处理不一致现象换了一批图片建库之后检索结果明显变差甚至相似度全都接近 0。原因最常见的是图片预处理不一致。建库时用的processor默认参数和查询时如果手动改了size或crop_size会导致特征空间错位。另一个隐蔽原因是图库里有损坏图片Image.open不报错但返回空图特征变成噪声。解决建库和查询必须用完全相同的 processor 配置统一走processor(images...)不要手工做 resize建库前过滤掉无法解码的图片文件。5.3 中文输入乱码或被截断编码与长度现象查询文本里有中文引号、特殊符号或者句子太长返回结果明显偏离意图。原因编码问题多半出在 Python 字符串源文件不是 UTF-8或者终端环境编码不对截断问题则是因为处理器对文本有最大长度限制超长文本从尾部截断关键信息如果正好在尾部就丢了。解决所有代码文件统一在头部声明# coding: utf-8读取查询文本用open(..., encodingutf-8)超长查询手动做截断把核心语义放在句子前部一般建议长度控制在 50 个字以内。5.4 GPU 显存溢出与 CPU 推理过慢现象显存报CUDA out of memory或者 CPU 上跑一张图要好几秒。原因batch 太大、输入图分辨率太高、或者模型误加载了 ViT-L/14 这种大规格。CPU 推理慢则是硬件决定的ViT 在 CPU 上的矩阵运算效率远低于 GPU。解决显存溢出时把离线建库的 batch 降到 16 或 8查询时本来就是单张输入基本不会爆没有 GPU 的话尽量用 ViT-B/16 而不是 ViT-L/14并把建库任务放到夜间跑。另外全程确保torch.no_grad()生效能省 30% 到 50% 的显存。5.5 top-1 总是不对先从数据本身找原因现象模型参数、代码都正确但某些查询返回的第一名明显不合理。原因很多时候问题出在数据而不是模型。图库里存在重复图片、相似图片或者标注的图文对本身语义就不匹配模型学到的匹配关系就是错的。另一个隐蔽问题是查询词太抽象比如搜“美食”而图库里的描述都是“红烧肉”“烤鱼”这种具体菜名语义粒度不匹配。解决先用人工检查图库的图文对质量删掉明显错的查询词尽量具体化拿“一盘红烧肉”代替“美食”去搜效果立竿见影。这块属于模型之外的经验调参有时候比换模型权重更管用。6. 进阶给课设加分的小技巧以及怎么验证你的系统真的可用6.1 Web 界面与演示Gradio 三行搞定课程设计的演示环节黑框命令行跑代码的冲击力远不如一个带输入框的网页界面。Gradio 是封装好的演示工具几行代码就能搭一个图文检索 Demo支持文本输入框和图片上传两种查询方式。把第 4 章的检索函数包一层再给函数加一个gr.Image类型的输入和一个gr.Gallery类型的输出启动后自动生成本地网页。这个界面放进答辩现场老师自己动手输一句中文比你看十页 PPT 都有说服力。注意参数上要把k固定为 5 或 10不用暴露给使用者避免误操作。6.2 消融实验与文档结构答辩的得分点答辩老师大概率会问“你为什么用 Chinese-CLIP 而不用别的模型”。提前做一个简单的消融实验就能接住这个问题分别用 BERT 图像特征做传统检索基线、用原版 CLIP、用 Chinese-CLIP在同一个测试集上算 Recall5三行数据一对比中文场景下 Chinese-CLIP 的优势一目了然。这个实验本身不复杂传统基线可以只提取文本 BERT 特征和图像 ViT 特征做余弦匹配跑出来的结果大概率明显低于 Chinese-CLIP正好作为对比支撑。文档结构上课设报告建议按下表组织重点突出“工程链路”和“实验对比”两部分文档章节内容要点系统设计双塔结构图、离线建库与在线查询流程图数据集说明数据来源、规模、清洗方式核心实现四个模块预处理、建库、检索、评估实验结果Recall1/5/10 表格、检索案例截图不足与改进失败案例分析、未来可做的扩展6.3 验证系统是否真的可用盲测与失败归因最后一个建议找两个没写过代码的同学来试你的 Demo让他们随便输入中文句子不要给提示。这个盲测能暴露三类问题一是部分查询语义太偏图库里根本没有相关内容系统返回的结果看起来自然很离谱二是图库本身太小比如只有 200 张图很多语义无法覆盖三是查询表述和图库描述差异过大“夜景”搜不出“夜晚的城市灯光”。遇到这种情况优先扩充图库而不是调模型参数。模型权重是训练好的你调不动也调不好能做的是让图库覆盖度匹配你的查询场景。我自己做过几个检索方向的项目最大的教训是不要一上来就折腾模型选型和训练策略先把“图库建好、查询路径跑通、评估能出数”这一条链路打通剩下的都是锦上添花。这个思路放在课程设计里同样适用希望帮到你。本文还有配套的精品资源点击获取
返回列表