
简介一份面向计算机视觉与Python课程设计场景的图文检索系统完整源码包基于Chinese-CLIP模型实现“文本搜图”“图像检索文本”等核心功能包含前后端逻辑与可视化界面适合用作期末大作业或课程设计项目。代码内附有详细注释文档说明覆盖部署与使用流程即使是新手也能快速上手。资源压缩包共59个文件以Python源码为主40个py文件同时包含JSON配置9个、说明文档1个md、项目截图1个png等整体大小仅542KB轻量易部署。截至目前已有172人学习下载属于高复用型作业参考。通过该项目可掌握中文多模态模型Chinese-CLIP的调用与微调方法理解图文特征对齐、检索排序、前端交互等完整实现链路并可直接改装成其他多媒体检索应用。1. 计算机视觉课程设计为什么要做图文检索一个不需要卷目标检测的选题又到了计算机视觉大作业的季节。目标检测、人脸识别这些题目代码满天飞答辩老师一眼就能看出哪些是复制粘贴哪些是自己动手搭的。如果你不想在“机器学习还是计算机视觉”之间反复纠结选哪个方向又想让作业在答辩时有工程完整度图文检索系统是个性价比很高的选题用 Python 加载 Chinese-CLIP 模型实现“文搜图”和“图搜文”两条检索链路再配上源码和文档说明。它既不是单纯的分类任务也不是检测任务而是把视觉特征和中文语义特征对齐到同一个向量空间里做检索。对刚入门计算机视觉、想走多模态方向的学生来说两三周时间足够跑通一版还能把数据准备、模型加载、特征建库、评测指标这些环节都亲手过一遍。2. 先看懂 Chinese-CLIP 的检索链路双塔编码、对比学习与特征向量对齐2.1 双塔结构图像和文本各走各的编码器最后在同一个向量空间里碰面Chinese-CLIP 沿用了 OpenAI CLIP 的经典双塔设计但把文本侧替换成了中文预训练语言模型。所谓双塔就是图像和文本分别进入两个独立的编码器互不干扰训练时让匹配的图文对在两个编码器输出的向量空间里互相靠近。这样设计有一个直接好处特征抽取完全解耦。做课程设计时图像库的特征可以离线批量算完存下来一个 1 万张的图库也就占几十 MB 到一百多 MB在线检索时只需要算一条文本 query 的向量然后做向量点积排序速度自然快。图像侧用的是 ViT 结构。以 base 规模为例一张 224×224 的图会被切成 16×16 的 patch每个 patch 经过线性映射和位置编码后进入 Transformer最后把 [CLS] token 对应的输出当作整幅图的向量。这个向量再经过一层视觉投影映射到模型的共享语义空间。文本侧则用类似 BERT 的架构但词表换成了中文词表所以同样一句话“一只在草地上奔跑的狗”中文分词得到的 token 序列和英文完全不同这也是为什么直接用 OpenAI CLIP 做中文图文检索效果会明显偏差。文本输出的 [CLS] 向量经过文本投影层后和图像向量落在同一个维度空间里比如 512 维或 768 维。投影层是容易忽略但很重要的部分。两个编码器各自输出的原始特征维度不一样语义空间也不一样必须通过可学习的投影矩阵统一到同一维度。而且这之后通常要接一层 L2 归一化把向量长度归一成 1因为检索时用的是余弦相似度而不是欧氏距离。如果不做归一化向量的模长会干扰相似度比较你会看到检索分数普遍偏高但没有区分度。有关注过 CLIP 训练细节的人应该都知道模型内部的相似度计算是“归一化后的点积”等价于 cosine similarity。2.2 对比学习图与文如何在训练中学会互相靠近如果只给模型一堆图片和文本模型怎么知道哪张图和哪句话是匹配的答案是对比学习。训练时一个 batch 里有 N 对图文数据正样本是配对的那一对负样本则是 batch 内其他 N-1 个不配对的图文组合。对每个图像模型要把正确的文本从所有文本中区分出来对每个文本也要把正确的图像从所有图像中区分出来。这可以理解成做了一个双向的 softmax 分类损失函数通常用 InfoNCE。这里有个关键参数叫温度系数。在计算相似度矩阵时点积结果会乘以一个温度缩放因子。温度小softmax 分布就越尖锐模型会更激进一步拉大正负样本差距温度大分布平滑训练早期不容易梯度爆炸。原版 CLIP 的温度系数是可学习的初始值大概在 0.07 左右Chinese-CLIP 保留了这个机制。在课程设计阶段不建议手动改温度因为你没有在训练模型改推理时的温度意义不大检索分数区分度低的原因往往出在特征没有归一化或者是预处理不一致。另一个核心点是为什么不能从头训练一个图文双塔。因为对比学习极度依赖 batch size 和负样本质量CLIP 原文在大规模训练时用了上万级别的 batch个人机器完全跑不动。就算强行用小 batch 训练模型也很容易退化成把“图库里的高频词”当成万能答案因为你提供的负样本太少模型找不到有意义的边界。课程设计阶段的正确路径是加载官方预训练权重直接做 zero-shot 检索这套权重已经在大规模中文图文对上对齐过语义直接用效果就够。如果后续想提升效果优先做轻量微调比如只微调投影层而不是全参微调。3. 把最小演示跑通环境准备、权重下载与第一个图文匹配3.1 环境安装Python 版本、PyTorch 与模型的搭配刚入门的同学如果还没装 Python先去官网装一个 3.9 或 3.10 版本再用 VSCode 配置 Python 解释器或者直接用 PyCharm 新建项目。这里不推荐最新版 Python因为部分依赖如 faiss 对新版本的支持偶尔会滞后。推荐用 conda 建独立环境避免和系统 Python 环境互相污染。conda create -n clip-demo python3.9 conda activate clip-demo pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install transformers pillow faiss-cpu第一行创建虚拟环境第二行激活第三行安装带 CUDA 11.8 的 PyTorch第四行安装模型加载库和图像处理库。如果你的电脑没有 NVIDIA 显卡把第三行换成pip install torch torchvision即可CPU 版跑推理完全够用。faiss 这里先装 cpu 版后面建索引时不需要 GPU 加速一万级别的图库 CPU 也只需要秒级返回。参数说明CUDA 版本要跟显卡驱动兼容驱动太老装新版 CUDA 会报CUDA initialization error。装完以后可以用一行命令验证 PyTorch 是否识别到了 GPUpython -c import torch; print(torch.cuda.is_available())输出 True 说明 GPU 可用False 则说明 torch 和驱动不匹配。这一步是后面所有流程的地基建议先花十分钟确认好。3.2 加载模型和处理器一次完整的文搜图推理最小可运行的图文匹配脚本只需要十几行代码。这里用的是 Hugging Face 的 transformers 接口加载 Chinese-CLIP权重会自动下载到本地缓存目录。from transformers import CLIPProcessor, CLIPModel from PIL import Image import torch # 加载中文CLIP模型和配套处理器 model CLIPModel.from_pretrained(OFA-Sys/chinese-clip-vit-base-patch32) processor CLIPProcessor.from_pretrained(OFA-Sys/chinese-clip-vit-base-patch32) model.eval() # 打开一张测试图准备两条候选文本 image Image.open(cat.jpg).convert(RGB) texts [一只猫趴在沙发上, 一只狗在草地上奔跑] # 文本和图像统一走processor预处理 inputs processor(texttexts, imagesimage, return_tensorspt, paddingTrue) with torch.no_grad(): img_feat model.get_image_features(inputs[pixel_values]) txt_feats model.get_text_features(inputs[input_ids]) # 归一化后计算余弦相似度 img_feat torch.nn.functional.normalize(img_feat, p2, dim-1) txt_feats torch.nn.functional.normalize(txt_feats, p2, dim-1) scores (img_feat txt_feats.T).squeeze(0) probs scores.softmax(dim-1) for t, s, p in zip(texts, scores, probs): print(f{t}: 相似度 {s.item():.4f}概率 {p.item():.4f})这段代码的逻辑是先把图像编码成一个特征向量再把两条文本分别编码成特征向量最后用点积算相似度。get_image_features和get_text_features返回的是投影后的特征但为了保险还是手动做一次 L2 归一化让点积真正等价于余弦相似度。softmax是为了把分数转成概率这样能直观看到模型相对更倾向于哪一句话。参数说明texts必须传列表不能传裸字符串paddingTrue让两条长度不等的文本在 token 层面补齐return_tensorspt指定返回 PyTorch 张量。首次运行模型会下载几个 G 的权重如果网络条件不好会卡在进度条上解决办法是用国内镜像或者手动下载权重这个坑后面专门讲。4. 搭建完整检索系统的课程设计实现离线建库、faiss 检索和演示界面4.1 批量抽取图像特征并落盘为图库建向量库最小脚本只能做单张图的图文匹配课程设计要的是一个系统。第一步是把整个图库的图像特征批量抽出来存成本地文件后面检索直接读文件就行不用每次启动重新抽一遍。这也是双塔模型相比单塔模型的工程优势——图像特征本质上是可离线缓存的。import os import numpy as np import torch from PIL import Image from transformers import CLIPProcessor, CLIPModel model CLIPModel.from_pretrained(OFA-Sys/chinese-clip-vit-base-patch32) processor CLIPProcessor.from_pretrained(OFA-Sys/chinese-clip-vit-base-patch32) model.eval() IMAGE_DIR images # 图库目录里面放jpg/png paths [os.path.join(IMAGE_DIR, f) for f in os.listdir(IMAGE_DIR) if f.lower().endswith((.jpg, .png))] paths.sort() batch_size 32 all_feats [] with torch.no_grad(): for i in range(0, len(paths), batch_size): batch_paths paths[i:i batch_size] images [Image.open(p).convert(RGB) for p in batch_paths] inputs processor(imagesimages, return_tensorspt) feats model.get_image_features(inputs[pixel_values]) feats torch.nn.functional.normalize(feats, p2, dim-1) all_feats.append(feats.numpy()) feats_array np.concatenate(all_feats, axis0).astype(float32) np.save(image_feats.npy, feats_array) with open(image_paths.txt, w, encodingutf-8) as f: f.write(\n.join(paths)) print(f特征矩阵形状: {feats_array.shape})批量抽特征时有两个细节。第一个是.convert(RGB)很多图片是 RGBA 四通道或灰度单通道不转换会直接报错。第二个是归一化放在模型输出之后、存盘之前保证落盘的所有特征向量都是单位向量方便后面用 faiss 的IndexFlatIP直接算内积。batch_size 32在 8G 显存下跑 base 模型比较安全显存小就降到 8 或 16。参数说明feats_array的形状是(图片数量, 特征维度)base 模型的特征维度是 512。image_paths.txt记录了路径和向量的对应关系顺序不能乱因为特征矩阵的行索引就是路径列表的行索引。这一步跑完图库就算建好了后面所有检索操作都不再需要扫描原始图片。4.2 用 faiss 建索引文搜图和图搜文的对称实现特征已经归一化检索就可以用 faiss 的IndexFlatIP也就是内积索引。因为向量都是单位向量内积结果就是余弦相似度。这个索引是暴力精确检索一万级别的图库检索耗时在毫秒级完全够用。import faiss import numpy as np import torch # 读取之前落盘的特征和路径 img_feats np.load(image_feats.npy) with open(image_paths.txt, r, encodingutf-8) as f: image_paths [line.strip() for line in f if line.strip()] # 建索引并添加全部特征 index faiss.IndexFlatIP(img_feats.shape[1]) index.add(img_feats) # 文搜图文本query - 文本特征 - 检索图像库 def search_by_text(query_text, k5): inputs processor(text[query_text], return_tensorspt, paddingTrue) with torch.no_grad(): q_feat model.get_text_features(inputs[input_ids]) q_feat torch.nn.functional.normalize(q_feat, p2, dim-1).numpy().astype(float32) scores, idxs index.search(q_feat, k) return [(float(scores[0][j]), image_paths[idxs[0][j]]) for j in range(k)]这个函数接收一条文本返回最相似的 k 张图片路径。k是 top-k 检索数量课程设计里一般取 5 或 10。astype(float32)是 faiss 的硬性要求它不接受 float64传了会报类型错误。图搜文的实现是对称的区别只在于 query 是图像。因为双塔模型结构对称图像 query 编码得到的向量可以直接在“文本特征库”里做同样操作。如果你希望图搜文返回的是相关文本描述就需要先把所有图片对应的 caption 文本抽成特征建一个文本索引如果希望图搜文返回的还是图片那其实是“以图搜图”反过来在图像库里检索即可。很多课程设计把图搜文理解成“以图搜图”虽然流程一样但答辩时要说清楚你建的是文本库还是图像库。参数说明index.ntotal可以查看索引里的向量总数用来核对是否全部写入。检索结果里的scores已经是归一化后的余弦相似度范围在 -1 到 1 之间实际效果好的匹配通常能到 0.25 以上。4.3 用 Flask 包一个演示界面文本输入框和图片上传课程设计答辩最好有一个交互界面不用花哨能用就行。用 Flask 写两个接口就够了一个接收文本返回图片一个接收图片返回候选结果。from flask import Flask, request, jsonify from PIL import Image import torch import io app Flask(__name__) app.route(/search_by_text, methods[GET]) def search_by_text(): query request.args.get(q, ) if not query: return jsonify({error: empty query}) results search_by_text(query, k5) return jsonify({results: [{path: p, score: s} for s, p in results]}) app.route(/search_by_image, methods[POST]) def search_by_image(): file request.files.get(image) if file is None: return jsonify({error: no image}) image Image.open(file.stream).convert(RGB) inputs processor(imagesimage, return_tensorspt) with torch.no_grad(): q_feat model.get_image_features(inputs[pixel_values]) q_feat torch.nn.functional.normalize(q_feat, p2, dim-1).numpy().astype(float32) scores, idxs index.search(q_feat, 5) results [{path: image_paths[idxs[0][j]], score: float(scores[0][j])} for j in range(len(idxs[0]))] return jsonify({results: results}) if __name__ __main__: app.run(host0.0.0.0, port5000)这个界面的核心价值不是好看而是把前面两节的功能串成了可演示的系统。文本检索用 GET 参数方便在浏览器直接拼 URL 测试图像检索用 POST前端表单上传图片文件后端用 PIL 打开、走同一个 preprocessing 流程。注意search_by_image里必须先.convert(RGB)再送进 processor否则四通道图片会在模型推理时报错。到这里一个“源码文档说明”完整度足够的课程设计项目已经成型图库特征离线建库文本检索和图像检索两个接口在线服务前后端分离。文档部分除了写环境和运行步骤还建议补一张系统架构图把离线建库和在线检索两条链路画清楚这对答辩很有帮助。5. 避坑与常见问题Chinese-CLIP 图文检索的 5 个典型翻车现场5.1 模型加载卡在进度条上权重下载反复失败现象第一次执行from_pretrained时进度条走到一半就停了或者报网络超时多重启都一样。原因Hugging Face 权重文件默认从国外服务器下载单个文件几百 MB 到几个 G网络不稳定时很容易中断。很多同学以为自己在等模型加载其实是在等一个永远到不了的下载任务。解决两种方案任选。第一种设置国内镜像环境变量再执行代码export HF_ENDPOINThttps://hf-mirror.com第二种手动把权重文件下载到本地后直接加载本地路径model CLIPModel.from_pretrained(./chinese-clip-vit-base-patch32) processor CLIPProcessor.from_pretrained(./chinese-clip-vit-base-patch32)注意本地路径下要包含config.json、pytorch_model.bin、vocab.txt等完整文件缺一个都会报错。判断是否下载完整可以用权重文件大小和官方仓库核对。5.2 检索结果跟随机选差不多分数普遍很低现象代码跑通了但搜“猫”返回的图片里有一半是风景相似度最高也只有 0.1 几。原因最典型的坑是图像预处理和模型训练时的预处理不一致。CLIP 在训练时对图像做了固定尺寸缩放、中心裁剪、归一化如果你直接用 PIL 打开原图塞进模型没有经过 processor输入的像素分布和模型期望的完全不一样特征自然学不到东西。解决所有进入模型的图像必须统一走processor(images...)不要手动resize后直接喂给模型。如果还想自定义预处理参数要以模型训练时的配置为准比如 base 模型输入分辨率是 224×224不是 512×512也不是原图尺寸。5.3 faiss 报维度错误或者检索结果全部是同一个索引现象index.search报wrong dimension或者返回的 k 个结果全是编号一样的重复项。原因faiss 索引一旦建立维度就固定了。如果你建索引用的特征矩阵是 512 维查询时传进一个 768 维的向量直接抛异常。返回重复索引则是因为查询向量没有归一化某些维度数值特别大导致模型对某一条特征“过度偏爱”落在同一个向量上。解决先打印特征矩阵形状确认维度再确认查询向量的维度一致。标准动作是重跑一遍特征抽取保证训练侧特征和查询侧特征来自同一个模型文件自己改过模型中间层会导致维度漂移。另外查询向量记得做astype(float32)faiss 对类型很敏感。5.4 中文文本效果偏差一条 query 被截断或分错现象检索中文长句时结果明显变差比如“一只戴红项圈的白猫趴在灰色沙发上”返回的全是戴红项圈的狗。原因中文文本的 token 序列长度比英文短原版 CLIP 默认的 max_length 是 77对中文来说这个长度偏长导致大量 padding token 混进特征计算稀释了真正的语义信息。另一个常见错误是误用了英文 BERT 的分词器中文被按字符或按拼音乱切。解决在 processor 调用时显式指定 max_length常用做法是设 52 左右并开启截断inputs processor( text[query_text], return_tensorspt, paddingTrue, truncationTrue, max_length52, )同时确认你加载的是OFA-Sys/chinese-clip-vit-base-patch32而不是openai/clip-vit-base-patch32后者的 tokenizer 对中文基本无效。5.5 单个循环抽特征太慢显存不够但不知道怎么办现象图库有几千张图自己写了个 for 循环一张张抽特征跑了一个小时还没完加大 batch 又爆显存。原因问题出在两点。一是逐张推理没有利用 batch 并行GPU 利用率极低二是特征抽取时机不对应该在推理阶段全程开torch.no_grad()否则模型会保存梯度图显存翻倍消耗。解决用本文章 4.1 节的批量抽取方式一次处理 16 到 32 张图。显存不足时先降低 batch 到 8还不够就把模型转成半精度推理model model.half().eval()半精度推理显存占用大约是原来的 60%。注意用了half()之后输入的张量也要转成半精度否则会报 dtype 不匹配inputs processor(imagesimages, return_tensorspt) pixel_values inputs[pixel_values].half()CPU 上跑同样适用这套逻辑只是速度慢能跑通功能就行不要在 CPU 上做完整图库评测时间成本太高。6. 用 RecallK 验收课程设计再往 zero-shot 分类与 rerank 两个方向进阶图文检索系统做完了答辩时不能只说“模型效果还行”。需要一组量化指标最常用的是 RecallK。它的含义是对每个 query 取检索结果前 K 条看正确结果是否在里面命中率多少。课程设计里人工标注几十条 query 就够展示趋势。def evaluate_recall_at_k(query_texts, ground_truth, k5): hit 0 for query, gt_path in zip(query_texts, ground_truth): results search_by_text(query, kk) result_paths [r[0] for r in results] if gt_path in result_paths: hit 1 return hit / len(query_texts)ground_truth是预先标注好的“这条 query 对应的正确图片”。亲手用 50 条测试样例把 Recall5 跑到 0.7 以上比任何口头描述都有说服力。如果还想让这个课程设计更有含金量有两个进阶方向可以选。第一个是 zero-shot 图像分类。既然 CLIP 能算图文相似度把类别名称变成“这是一张猫的照片”这类文本然后和图像算相似度取 argmax就能做分类。这和计算机视觉与目标检测之间最大的差别是不需要训练分类头类别可以随时换新增类别零成本。第二个方向是 rerank。首轮用 faiss 粗召回 top 50再用一个更精细的模型对这 50 条重新打分排序。Rerank 能明显提升 top 5 的准确率而且一轮粗排加一轮精排的时间开销完全可控。我在做这版课程设计时最后悔的事情是没有提前把预处理参数统一成一张表贴进文档里。答辩时老师问“你的模型输入是什么尺寸、文本最长多少”我翻了半天代码才找到。建议你从一开始就把这些参数记在文档开头。希望帮到你。本文还有配套的精品资源点击获取