简介:一套基于Chinese-CLIP的图文检索系统课程设计资料包,面向人工智能、通信工程、自动化、电子信息、物联网等计算机相关专业的在校学生与教师,可支撑课程设计、毕业设计、项目立项演示及进阶学习。资源围绕“文本—图像”跨模态检索完整流程,覆盖预处理、模型训练、评估与部署等环节,代码经测试运行成功,功能可靠。包内共60个文件,以40个Python源文件为代码主体,辅以9个JSON数据配置、7个pyc编译文件、2个txt文本、1个png图片与1个md说明文档,压缩包大小仅544KB,轻量且目录规范,便于按模块阅读。入口与推理脚本可直接运行,结合预处理、训练、部署、评估等目录模块,能快速复现实验效果,也方便对照流程做调试或二次开发。内含详细文档与全部资料,源自高分课程设计并通过答辩评审,具备较强参考价值。目前已有216人学习下载,适合想深入NLP与视觉融合应用的读者,既可作为系统的入门指南,也能为后续算法改进与界面优化提供基础。
1. 课程设计抽到“基于Chinese-CLIP的图文检索”:这个题目到底在考什么
答辩现场老师问的第一句话通常是:“你这系统和你自己训的模型有什么区别?”如果只回答“我调了个包”,基本就告别高分了。这门课设真正想让你做的,是理解一个完整图文检索系统的数据流:图片和文字如何被编码、映射到同一个向量空间、再通过相似度排序返回结果——Chinese-CLIP恰好是把这条路走通的最省力起点,因为它在中文语料上预训练好了,你不需要造轮子,但必须清楚轮子为什么能转。
这篇文章只聊一件事:拿到“基于Chinese-CLIP的图文检索系统”这个课设题目后,从数据准备、离线特征提取、后端接口到课程文档,怎么一步步把它做成一个能演示、能讲清、能被老师追问不慌的系统。新手能照着跑,熟手能直接抄参数和踩坑清单。目录结构那个zip包里有什么,其实不重要,重要的是你自己能把这一套从头到尾复现一遍。
2. 从 CLIP 到 Chinese-CLIP:为什么图文检索课设绕不开对比学习
2.1 图文检索的两条老路为什么撑不住课设演示
图文检索不是新东西。传统方案里,图像侧用 CNN 提特征,文本侧用 TF-IDF 或 Word2Vec 提特征,然后想办法做“对齐”。问题在于:CNN 的 ImageNet 分类特征和 Word2Vec 的语义向量根本不在同一个空间里,硬算余弦相似度,搜出来的结果毫无可解释性。另一条路是训练一个多标签分类器,把图片分类成若干语义标签,再拿标签去匹配文本——这等于把开放域检索退化成了有限类别匹配,课设演示时换个没见过的词就翻车。
这两条路的共同问题是:模型只学了“图像是什么”或“文本说什么”,没学“图和文之间怎么对应”。而图文检索要的是跨模态匹配,这恰恰是需要专门训练的。
2.2 对比学习:把图和文拉进同一个向量空间
CLIP 的思路一句话就能讲完:训练一个双塔网络,图像塔负责把图片编码成向量,文本塔负责把句子编码成向量,然后用对比损失让“匹配的图文对”在向量空间里靠近,“不匹配的”互相远离。
关键在损失函数。给定一个 batch 里的 N 个图文对,对每张图片来说,它对应的文本是正样本,batch 里其余 N-1 条文本都是负样本;文本侧同理。模型要做的,是让正样本对的余弦相似度尽量大、负样本对的尽量小。训练完成后,整个模型变成了一个“映射函数”:任意图片和任意文本都被压进同一个向量空间,检索时直接算相似度排序就行。
这个设计的好处是:图搜文和文搜图不需要两套系统。把图片特征矩阵存下来,拿一条文本向量去点积,就是文搜图;把文本特征矩阵存下来,拿一张图片向量去点积,就是图搜文。方向只是矩阵乘法的转置问题。这也是为什么课设里你只需要写一个相似度计算函数就能覆盖两个检索方向。
顺着这个思路往后延伸,视觉大语言模型里常见的图文对齐模块,底层用的也是同一套对比学习逻辑。理解 CLIP,后续看任何多模态模型都轻松得多。
2.3 Chinese-CLIP 的模型组成与选型:base 还是 large
Chinese-CLIP 是 CLIP 的中文版本,解决的是英文 CLIP 在中文场景下表现差的问题。它用 Wukong、LAION 等中文图文对数据重新预训练,效果比直接拿英文 CLIP + 翻译要靠谱得多。模型结构上没有惊天动地的改动,依然是双塔:
- 图像塔:ViT-B/16,把图片切成 16×16 的 patch 序列,过 Transformer 编码
- 文本塔:RoBERTa-wwm-ext,针对中文优化过的 BERT 变体
- 投影层:两个塔各自输出后都会过一层投影,把特征压到同一个维度空间做对比
课程设计选 base 就够。large 模型检索质量的提升幅度,对于一两千张图的课设 demo 来说几乎看不出来,但显存占用和推理时间翻倍。我一般直接用OFA-Sys/chinese-clip-vit-base-patch16这个权重,推理时用 transformers 加载:
from transformers import ChineseCLIPProcessor, ChineseCLIPModel model = ChineseCLIPModel.from_pretrained("OFA-Sys/chinese-clip-vit-base-patch16") processor = ChineseCLIPProcessor.from_pretrained("OFA-Sys/chinese-clip-vit-base-patch16") model.eval()逻辑说明:from_pretrained会自动下载模型权重和 processor 配置。eval()一定要加,否则模型里的 dropout 和 layernorm 在推理时行为不一致,检索结果会随机抖动。
参数说明:OFA-Sys是作者组织名,chinese-clip-vit-base-patch16表示图像塔用的是 ViT-B/16。如果显存紧张可以换成-patch14或移动端小模型,但检索精度会下降,课设阶段不建议折腾。
3. 搭一个能跑的图文检索:数据组织、特征提取与相似度计算
3.1 数据准备:用 Flickr30K-CN 子集还是自建数据
课设的数据集选择有个原则:越小越可控。公开中文图文数据集里,Flickr30K-CN 和 COCO-CN 都可以直接下载,但完整数据集动辄几万张图,跑一遍特征提取上小时起步。我习惯只抽一个子集,比如 Flickr30K-CN 里抽 2000 个图文对,每张图带 5 条参考描述——这个配置刚好够算 Recall 指标。
数据目录组织建议长这样:
dataset/ ├── images/ # 图像文件夹,统一 jpg 格式 └── captions.json # 标注文件captions.json的结构我一般设计成:
{ "img_0001.jpg": { "file": "img_0001.jpg", "captions": ["一只猫坐在窗台上", "白猫望着窗外", "..."] } }关键点:每张图保留多条 caption。这不仅是为了检索时能返回多条结果,更重要的是,后面评估 Recall@K 时,一个 query 的 ground truth 是一个集合,命中任意一条就算对。没有多条 caption,Recall 指标根本没法算。
JSON 格式还有个好处:跟 Flask 接口的交互几乎零成本。前端传回来的结果列表可以直接按 key 取图片路径,不需要额外的数据库。
3.2 离线提取图库特征:归一化比想象中重要
特征提取是整个系统的核心耗时环节,一定放到检索前面做。我一般写一个脚本,一次性把图库里所有图片的特征提取完,存成.npy文件,之后 Flask 启动时直接np.load加载,几秒钟搞定,不用每次请求都过模型。
import json import numpy as np import torch from pathlib import Path from PIL import Image from transformers import ChineseCLIPProcessor, ChineseCLIPModel model = ChineseCLIPModel.from_pretrained("OFA-Sys/chinese-clip-vit-base-patch16") processor = ChineseCLIPProcessor.from_pretrained("OFA-Sys/chinese-clip-vit-base-patch16") model.eval() images_dir = Path("dataset/images") anns = json.load(open("dataset/captions.json", encoding="utf-8")) img_ids = list(anns.keys()) feats = [] batch = [] batch_ids = [] batch_size = 32 # 显存小就改成 16 for idx, img_id in enumerate(img_ids): img = Image.open(images_dir / anns[img_id]["file"]).convert("RGB") batch.append(img) batch_ids.append(img_id) if len(batch) == batch_size or idx == len(img_ids) - 1: inputs = processor(images=batch, return_tensors="pt") with torch.no_grad(): embeds = model.get_image_features(**inputs) # L2 归一化:检索相似度等价于余弦相似度 embeds = embeds / embeds.norm(dim=-1, keepdim=True) feats.append(embeds.cpu().numpy()) batch.clear() batch_ids.clear() feats = np.concatenate(feats, axis=0) np.save("dataset/image_embeds.npy", feats) np.save("dataset/image_ids.npy", np.array(img_ids))逻辑说明:get_image_features提取图像特征后必须手动归一化。这一步不做,后面算相似度时数据分布会受特征模长干扰,看起来结果也能出,但排序不稳定,换一张 query 图排名就乱跳。
参数说明:batch_size=32在普通 8GB 显存的 GPU 上没问题;CPU 环境建议降到 4~8,且不要开 fp16。.convert("RGB")是必须的,否则遇到 RGBA 或灰度模式的图片会直接让 processor 报错或形状不匹配。
3.3 文搜图与图搜文:一次矩阵乘法的两个方向
图库特征存好了,检索就变成一个纯矩阵运算逻辑。文搜图的实现如下:
def text_search(query, top_k=10): inputs = processor(text=[query], return_tensors="pt") with torch.no_grad(): text_emb = model.get_text_features(**inputs) text_emb = text_emb / text_emb.norm(dim=-1, keepdim=True) # feats 是 (N, dim) 的图库特征矩阵,已经被 L2 归一化 sims = text_emb.cpu().numpy() @ feats.T # (1, N) idx = np.argsort(-sims[0])[:top_k] return [{"img_id": img_ids[i], "score": float(sims[0][i])} for i in idx]逻辑说明:一句话查询变成向量后,和整个图库特征矩阵做矩阵乘法,得到的(1, N)向量就是这条 query 与每张图的相似度。argsort(-sims[0])取相似度最高的前 K 个索引。
图搜文更简单,只需事先把每一张图的 5 条 caption 也过一遍文本塔,存成dataset/text_embeds.npy和对应的文本 id 列表。查询时把输入图片过get_image_features,归一化后直接和文本特征矩阵点积。两个方向的代码几乎一样,区别只在“query 走图像塔还是文本塔”。
这里有个容易被忽略的点:文本侧每条 caption 单独编码,而不是把 5 条拼成一段。因为模型训练时一条文本 token 序列对应一张图,拼接会让语义浑浊,检索精度明显下降。
4. 把检索结果变成能交差的系统:Flask 接口、页面与课程文档怎么组织
4.1 系统功能与接口设计:先定交互再写代码
课设系统不需要花哨,但一定要“完整”。说得直白点,老师打开你的系统,应该能完成三个动作:输入文字搜图片、上传图片搜文本、看一眼结果页觉得像那么回事。我建议先画两个接口再动手:
POST /api/search_text— 请求参数{"query": "一只猫在窗台上"},返回图片 id + 相似度列表POST /api/search_image— 请求参数为 multipart 上传的图片文件,返回文本描述列表
Claude 出的方案经常喜欢给一堆接口,但课设场景做多就是给自己找事。两个接口,一个网页,前后端打通,就是完整闭环。
4.2 Flask 后端与检索页面最小实现
from flask import Flask, request, jsonify, render_template import numpy as np import torch from PIL import Image from transformers import ChineseCLIPProcessor, ChineseCLIPModel app = Flask(__name__) model = ChineseCLIPModel.from_pretrained("OFA-Sys/chinese-clip-vit-base-patch16") processor = ChineseCLIPProcessor.from_pretrained("OFA-Sys/chinese-clip-vit-base-patch16") model.eval() image_embeds = np.load("dataset/image_embeds.npy") image_ids = np.load("dataset/image_ids.npy") @app.route("/api/search_text", methods=["POST"]) def search_text(): query = request.get_json()["query"] inputs = processor(text=[query], return_tensors="pt") with torch.no_grad(): text_emb = model.get_text_features(**inputs) text_emb = text_emb / text_emb.norm(dim=-1, keepdim=True) sims = text_emb.cpu().numpy() @ image_embeds.T topk = np.argsort(-sims[0])[:12] results = [{"img_id": image_ids[i], "score": float(sims[0][i])} for i in topk] return jsonify({"results": results}) @app.route("/api/search_image", methods=["POST"]) def search_image(): file = request.files["image"] img = Image.open(file.stream).convert("RGB") inputs = processor(images=[img], return_tensors="pt") with torch.no_grad(): img_emb = model.get_image_features(**inputs) img_emb = img_emb / img_emb.norm(dim=-1, keepdim=True) text_embeds = np.load("dataset/text_embeds.npy") # 启动时加载更好,这里只是演示 sims = img_emb.cpu().numpy() @ text_embeds.T topk = np.argsort(-sims[0])[:5] results = [{"caption": text_ids[i], "score": float(sims[0][i])} for i in topk] return jsonify({"results": results}) @app.route("/") def index(): return render_template("index.html") if __name__ == "__main__": app.run(debug=True, port=5000)逻辑说明:模型在模块加载时就初始化,避免每个请求重新载入。图库特征也提前加载成全局变量。text_embeds.npy在图搜文接口里每次请求都 load 一次,正式代码应该和 image 特征一样在启动时加载,这里写成演示逻辑是为了强调它的存在。
前端页面建议直接用原生 HTML + JavaScript fetch,不要引 Vue 或 React,课设不需要前端工程化。页面里一个文本框、一个上传按钮、一个结果网格区,约 80 行代码就够。
4.3 课程设计文档怎么写:让老师相信你“做完了”而不是“跑通了”
“详细文档 + 全部资料 + 优秀项目”这个后缀说明,这门课设的评分重点很大程度在文档上。我评审过几届课程设计,最常见的通病是:代码能跑,但文档只有环境安装步骤和代码贴图,没有任何设计过程。
一份合格的课程设计文档至少要有这几部分:
- 需求分析:说明系统解决什么问题,用户是谁,核心功能是什么
- 方案选型:为什么选 Chinese-CLIP 而不是 ResNet + BERT,要有对比
- 系统设计:数据流图、模块划分、接口定义
- 核心算法:对比学习原理、特征归一化、相似度计算,要写出公式
- 测试与评估:Recall@K 指标、检索示例、失败案例分析
- 总结:做了什么、没做什么、如果重做会怎么改
如果你的文档里能写清楚“为什么每张图保留 5 条 caption 才能算 Recall@K”,这个深度已经超过九成同学了。这个 zip 包里哪怕自带优秀模板,也建议自己按上述结构重写一份,答辩时被追问“这文档是你写的吗”,照稿念和临场答完全是两个印象分。
5. Chinese-CLIP 图文检索踩坑实录:维度、显存和玄学检索质量
5.1 显存总是在特征提取时爆炸
现象:一张图一张图过模型没问题,但为了提高速度把 batch_size 调到 64,直接 OOM。
原因:get_image_features虽然只提取图像特征,但模型的前向传播会同时构建图像和文本两个塔的计算图,实际显存占用比单塔翻倍。而且很多人忘了在eval()模式下仍然开着梯度,torch.no_grad()没包住 forward,额外多占一份显存。
解决:推理时务必用with torch.no_grad():包住前向计算;batch_size 从 32 降到 16,显存占用直接砍半。如果还是不够,再把模型的torch_dtype切到torch.float16,但注意 fp16 下如果出现 NaN,优先怀疑 LayerNorm 精度问题,这时候改回 fp32 比调任何参数都管用。
5.2 相似度全挤在 0.7 到 0.8,排序没区分度
现象:检索结果里所有分数都接近,第一名和第十名只差 0.02,看起来像随机排序。
原因:特征没有做 L2 归一化。CLIP 类模型的输出特征模长分布很宽,不归一化直接点积,相似度会被大模长的维度主导,导致分数挤在一起。
解决:特征提取和 query 编码时都执行embeds = embeds / embeds.norm(dim=-1, keepdim=True)。归一化之后,相似度范围在 [-1, 1],排序间隔拉开,阈值过滤才有意义。这是最容易被忽略但影响最大的一步。
5.3 长中文描述检索效果明显变差
现象:输入“一只橘色的猫趴在窗台上晒太阳”能搜对,输入一小段场景描述反而返回一堆无关图。
原因:Chinese-CLIP 预训练时文本侧有最大长度限制,默认配置下超过 52 个 token 的内容会被截断。课设里大家写的 query 往往不是短语而是完整句子,后半句的关键信息被截掉,特征自然不对。
解决:明确传processor(text=[query], max_length=64, truncation=True),同时检查input_ids的真实长度。如果序列被截断,要么精简 query 表达,要么调大max_length,但注意模型位置编码有上限,不是越大越好。
5.4 同一张图,换一种预处理方式检索结果就变
现象:你用 PIL 打开图片直接送进模型,和用 OpenCV 读图再送进去,前几名结果不一样。
原因:PIL 和 OpenCV 的颜色通道顺序不同,模型花在识别颜色属性上的特征被扰动。顺手一点的是 processor 内部已经做了 resize、center crop 和归一化,只要输入是 RGB 模式且图像尺寸大于等于 224×224,问题不大;但如果你在传图前自己 resize 或裁剪过,模型看到的分布就和预训练不一致。
解决:所有图片统一走 processor 处理,不要在外部手写 resize。遇到尺寸很小的图,先补边到 224×224 再送处理器,而不是直接拉伸。拉伸会让宽高比畸变,ViT 切 patch 时位置信息被破坏,检索质量断崖式下降。
5.5 检索结果“感觉不对”:CLIP 的语义粒度到底有多细
现象:搜“高楼大厦”返回了山峰的图,搜“厨房”返回了餐桌的图,你觉得它理解错了,但它评分确实最高。
原因:CLIP 学到的是“概念级”对齐,不是“属性级”对齐。“高楼大厦”和“山峰”在视觉特征上有共同点——竖直方向大面积连续纹理,这在向量空间里确实近。这是模型本身的语义粒度边界,参数上无解。
解决:接受这个边界,然后在产品层面处理。一是给检索结果加阈值过滤,低于阈值的直接不展示;二是做后处理重排,比如对返回候选集做一个基于颜色直方图的微调,把明显不符合的排下去。后者能让课设答辩的演示效果好一大截,原理也讲得通,很加分。
6. 把检索系统从“能跑”拉到“能答辩”的三个进阶动作
6.1 用 faiss 把检索从 numpy 点积换成索引查询
课设图库只有几千张图时,numpy 矩阵乘法完全够用,延迟在毫秒级。但如果你的数据集抽到 10 万张以上,或者答辩时老师问“你这检索怎么支撑更大规模数据”,答案不能是“换一台更大的机器”。
用 faiss 做向量索引是标准解法。特征已经 L2 归一化的情况下,用内积索引就能精确复现 numpy 的排序结果:
import faiss dim = image_embeds.shape[1] index = faiss.IndexFlatIP(dim) # 内积,等价于归一化后的余弦 index.add(image_embeds.astype("float32")) D, I = index.search(query_emb.astype("float32"), top_k)参数说明:IndexFlatIP是暴力精确检索,不牺牲精度,适合百万级以下的数据。再往上可以换IndexIVFFlat,但那要调 nlist 和 nprobe,课设阶段写了反而容易被追问细节。一句话带过“后续可以换 IVF 近似索引”就够了。
6.2 算 Recall@K:图文检索系统好不好的硬指标
演示效果可以“看起来不错”,但文档里的评估必须有数值。图文检索的标准指标是 Recall@K:对每个 query,取检索结果前 K 个,看 ground truth 是否在其中。K 通常取 1、5、10。两个方向分开算:文搜图的 Recall@K 和图搜文的 Recall@K。
def recall_at_k(sim_matrix, ground_truth, k): hits = 0 for i, gt in enumerate(ground_truth): topk = np.argsort(-sim_matrix[i])[:k] if any(g in topk for g in gt): hits += 1 return hits / len(ground_truth)注意 ground_truth 是一个列表集合,不是单个 id。因为每张图有多条相关描述,命中任意一条就算一次命中。这个细节写进文档,老师一眼就知道你真的动手算过指标,而不是抄了个数字。
6.3 答辩演示前必做的三张对比图
我自己的习惯是:答辩前一晚,必然跑三组固定的检索样例,截图存下来。第一组是常见场景的成功检索,证明系统基本能力;第二组是相似语义的不同表达,比如“小猫”和“幼猫”搜出的结果对比,证明模型学到了语义对齐;第三组是故意失败的案例,并准备一段解释“为什么失败”——这比全是成功截图更有说服力,因为老师知道 CLIP 有语义粒度边界,你能主动说清边界,就说明你是真的理解模型,而不是调参碰运气。
做这个项目最大的教训是:检索系统的好坏不是“看起来像”,而是“算得出”。下次再碰多模态项目,我会在第一天就把特征归一化、Recall@K、faiss 索引这三件事放进技术方案里,而不是等到答辩前一周才补。希望这篇笔记能帮你少走一次我当时走过的弯路。
本文还有配套的精品资源,点击获取