基于Chinese-CLIP的图文检索系统:从原理到Flask部署实战 简介这是一份面向计算机视觉课程设计、毕业设计及工程实训的图文检索系统项目基于 Python 与 Chinese-CLIP 实现适合希望掌握跨模态检索技术的小白或进阶学习者。项目涵盖数据预处理、文本与图像特征提取、相似度匹配、界面展示等完整流程可直接作为课程大作业或初期项目立项参考。压缩包共 59 个文件包含 40 个 Python 脚本、9 个 JSON 配置、7 个编译后的 pyc 文件以及 txt、png、md 等辅助材料Python 脚本覆盖模型部署、评估与演示JSON 用于参数和标签管理整体大小约 577KB轻量易部署。目前已吸引 439 人浏览学习说明这套方案在同类资源中具备一定的参考价值。通过阅读源码和目录结构学习者能快速理解 Chinese-CLIP 在中文图文检索场景下的应用思路并利用现有框架扩展自己的数据集与功能模块节省从零搭建的时间。1. 为什么是 Chinese-CLIP一个能跑通的图文检索起点做计算机视觉课程设计时图文检索是很容易出效果又不容易跑通的题目数据集好找但模型加载、中文分词、特征对齐这些环节任何一个出错界面就尴尬地卡在那里。Chinese-CLIP 的出现把这条链路缩短了——它把中文文本和图片放进同一个语义空间用余弦相似度排序就能实现“以文搜图”和“以图搜文”。我看过一份对应的课程设计源码结构很干净cn_clip 包提供预训练模型text2image.py 负责特征提取app.py 用 Flask 包了一层检索接口。这篇文章就按“原理—拆代码—部署—微调—排错”的顺序把这个项目讲透适合正在做图文检索相关大作业或毕设的人直接参考。2. 图文检索的核心原理与中文优化2.1 CLIP 的双塔结构与对比学习CLIP 的出发点很简单让模型学会判断“哪张图和哪句话更匹配”。它用两个编码器组成双塔结构——左边是图像编码器通常是 ViT 或 ResNet右边是文本编码器通常是 Transformer两个塔分别把图像和文本映射到一个共享的向量空间。训练时模型从 batch 里随机取配对的正样本和不配对的负样本用对比学习最大化正样本对之间的余弦相似度同时最小化负样本对之间的相似度。最终学到的文本向量和图像向量具有可比的语义方向这也正是“图文检索”能跑通的基础。比如给定一张猫的图片和“一只猫趴在地上”的文本模型应该让这两个向量靠得很近而和“一辆红色汽车”的文本向量距离较远。检索的实质就是在向量空间里做最近邻搜索文本查询向量与库中所有图片向量算余弦相似度按得分从高到低排序。这个思路在英文场景下已经被 OpenAI 的 CLIP 验证过但直接搬到中文环境时会发现问题原生 CLIP 的文本编码器是在英文语料上预训练的对中文分词、一词多义和中文专有名词的表示都不够友好检索精度会明显下降。从课程设计的数据流来看整个系统其实只有三块离线建立图像特征库、在线接收文本查询、计算相似度并返回候选图。离线部分可以提前把图像库所有图片过一遍图像编码器把特征存成 numpy 或 faiss 索引在线部分只需要编码一条文本然后做一次矩阵乘法。这种设计让 demo 的实时性非常好即使没有 GPU用 CPU 跑 ViT-B/16 也能在几百张图的库中做到秒级响应。2.2 Chinese-CLIP 在中文语义上的关键改动Chinese-CLIP 没有重新发明轮子而是在 CLIP 的架构基础上做了两件事。第一重新预训练文本侧使用约 2 亿中文图文对让文本编码器重新学习中文语义同时保留图像侧的预训练权重这样迁移成本低图像特征的质量也不下降。第二针对中文语料做了数据清洗和文本增强像中文标点、繁体字、口语化表达都做了处理。最终的模型支持 ViT-B/16、ViT-B/32、ViT-L/14 等常见主干用户可以通过available_models()查看具体列表。从课程设计的角度看这些改动带来的直接收益是你不需要自己标注大量数据也不用从头训练模型直接加载预训练权重就能完成大部分场景的中文图文检索。衡量效果时主要看两个指标top-k 准确率和召回率。top-k 表示前 k 个结果中命中正确目标的概率课程设计里通常用 top-1 和 top-5 展示效果。选择主干时要注意显存和推理速度的平衡。ViT-B/16 的文本特征维度是 512图片编码耗时在 CPU 上约 100ms/张ViT-L/14 的特征维度是 768精度更高但显存占用接近前者的 3 倍。如果只是做一个几千张图的检索 demoViT-B/16 足够如果是毕设要冲指标再考虑 ViT-L/14。我见过不少同学一上来就用最大的模型结果 CPU 推理一张图要好几秒演示时体验很差。下面是一个简单的对比表可以帮你决定选哪个基础模型。对比项原生 CLIPChinese-CLIP文本编码器预训练语料英文中文为主对中文图文对的语义对齐较弱强是否支持中文 tokenize需额外处理内置适合中文课程设计一般推荐2.3 特征归一化与相似度计算不管是图像还是文本特征向量在计算相似度之前通常要做 L2 归一化。原因很直白归一化之后余弦相似度就退化成了向量点积一方面计算更快另一方面数值范围固定在 [-1, 1]方便设定阈值。Chinese-CLIP 在encode_image和encode_text返回的特征已经是归一化后的结果所以记得在计算相似度时不要再手动做归一化否则会二次缩放导致排序结果失真。使用预训练模型时模型内部还有一个logit_scale参数它控制 logits 的缩放温度。默认值通常让相似度分布在比较陡峭的区间排序时区分度更大。计算检索分数时常见做法是# 计算图文相似度并排序 import torch model.eval() with torch.no_grad(): # image_features: (N, D), text_features: (M, D) # 矩阵乘法得到 (M, N) 的相似度矩阵 logits text_features image_features.T * model.logit_scale.exp() scores logits.softmax(dim-1) # 按图像维度归一化这里image_features是图像库的特征矩阵每一行代表一张图text_features是查询文本的特征向量。矩阵乘法完成后scores的第 i 行就是第 i 条文本对库中所有图片的匹配概率。softmax会让你更容易观察相对关系但要注意它不会改变排序顺序。logit_scale是一个可学习的参数代码里用exp()取正数。如果你想调节检索的敏感度可以放大或缩小这个缩放因子但要注意它不是阈值而是影响整体分布的平滑程度。实际项目里我会保留默认值只有在同分现象严重时才手动调整。另外如果图像库很大建议把特征矩阵用faiss或hnswlib建索引用knn代替暴力矩阵乘法否则 O(N) 的计算量在万级图片库上会明显拖慢响应速度。3. 项目源码拆解从模型加载到结果排序3.1 文件结构与职责划分这份课程设计代码的文件组织很典型适合直接作为大作业模板。解压Text2Image-Retrieval-code.zip后你会看到以下核心文件文件/目录职责关键接口cn_clip/Chinese-CLIP 模型包包含 clip、preprocess、eval、training 等子模块模型加载、特征提取、损失函数text2image.py图文检索主流程加载模型、构建索引、执行查询build_index(),search()utils.py图像预处理、文件扫描、结果格式化load_images(),format_results()app.pyFlask Web 服务暴露 HTTP 接口/searchPOST 接口test.py冒烟测试验证单条查询是否正常query(一只猫)README.md运行说明与依赖列表安装步骤、示例命令这个分层很实用text2image.py把模型和检索逻辑封装成函数app.py不直接接触模型细节只负责接收请求和返回 JSON。这样做的好处是调试时可以单独跑text2image.py不用每次启动 Web 服务也方便在 Jupyter Notebook 里逐步看中间结果。从数据流角度看utils.py先扫描图像目录得到所有图片路径text2image.py对每张图调用model.encode_image生成特征矩阵查询时同样调用model.encode_text编码文本然后做相似度排序。整个过程不涉及训练只是在预训练模型上做推理所以代码量不大核心逻辑就集中在两个文件里。3.2 text2image.py图文特征提取主流程text2image.py是系统的核心。它先加载 Chinese-CLIP 模型和预处理器然后定义一个函数批量处理图片。一个典型的实现如下# text2image.py 核心流程 from PIL import Image import torch from cn_clip.clip import load_from_name, tokenize import utils device cuda if torch.cuda.is_available() else cpu model, preprocess load_from_name(ViT-B-16, devicedevice, download_root./checkpoints) model.eval() def build_index(image_dir): 扫描图片目录返回图像特征矩阵和路径列表 paths utils.list_images(image_dir) features [] for p in paths: img preprocess(Image.open(p)).unsqueeze(0).to(device) with torch.no_grad(): feat model.encode_image(img) feat feat / feat.norm(dim-1, keepdimTrue) # 确保归一化 features.append(feat.cpu()) return torch.cat(features, dim0), paths def search_text(query, features, paths, top_k5): 输入中文文本返回最相似的 top_k 张图片路径 text tokenize([query]).to(device) with torch.no_grad(): text_feat model.encode_text(text) text_feat text_feat / text_feat.norm(dim-1, keepdimTrue) scores (text_feat features.T).squeeze(0) top_idx scores.topk(top_k).indices.tolist() return [paths[i] for i in top_idx], scores[top_idx].tolist()注意这里preprocess是cn_clip自带的数据增强流程它会把图片缩放到 224x224、转成 tensor、做归一化。tokenize会把中文句子转成模型需要的 token id 序列内部已经处理了中文分词不需要再自己装 jieba。encode_image和encode_text都是在torch.no_grad()下执行的否则会构建计算图浪费显存。build_index中我额外做了一次feat / feat.norm()虽然模型输出本身是归一化的但显式归一化可以防止某些版本的模型输出未归一化属于保险操作。search_text返回的scores是原始相似度没有经过 softmax你可以自己决定展示百分比还是保留原值。一般课程设计展示原值加 top-5 排名就够了。3.3 utils.py 与 test.py预处理与验证utils.py通常包含两个小函数一个是递归扫描图片另一个是把结果包装成 dict 方便 JSON 序列化。代码可能长这样# utils.py import os from PIL import Image IMAGE_EXT (.jpg, .jpeg, .png, .bmp) def list_images(folder): 递归收集所有图片的绝对路径 result [] for root, _, files in os.walk(folder): for f in sorted(files): if f.lower().endswith(IMAGE_EXT): result.append(os.path.join(root, f)) return result def format_results(paths, scores): 把路径和分数转成列表字典便于前端展示 return [{path: p, score: round(s, 4)} for p, s in zip(paths, scores)]这里sorted保证了多次运行结果稳定避免因为文件系统遍历顺序不同导致返回顺序抖动。IMAGE_EXT是一个元组放在函数外是为了让list_images只能读不能误改。format_results的round(s, 4)很有必要因为相似度原始值可能是 0.8324321 这种长小数前端展示时需要保留 4 位即可。test.py通常就几行用于验证环境是否配好。运行python test.py如果输出类似[(images/cat.jpg, 0.8921)]的结果说明模型和依赖都正常。这个文件虽然简单但很有价值——很多同学在装完环境后直接跑 Flask结果报错都不知道是模型没下完还是依赖缺失先跑通test.py能快速缩小问题范围。4. 用 Flask 搭建检索服务4.1 app.py 的接口设计Flask 是 Python 里最轻量的 Web 框架适合给课程设计包一层接口。这个项目里的app.py没有把检索逻辑写在路由里而是从text2image.py导入函数这样保持职责清晰。核心代码如下# app.py from flask import Flask, request, jsonify from text2image import build_index, search_text import utils app Flask(__name__) IMAGE_DIR ./images FEATURES, PATHS build_index(IMAGE_DIR) # 启动时预建索引 app.route(/search, methods[POST]) def search(): data request.get_json() query data.get(text, ) if not query: return jsonify({error: 缺少 text 字段}), 400 top_k min(int(data.get(top_k, 5)), 20) paths, scores search_text(query, FEATURES, PATHS, top_k) return jsonify({query: query, results: utils.format_results(paths, scores)}) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)注意build_index在模块加载时执行意味着服务启动会花几秒钟把图片特征全部算完。如果图片库很大建议把特征矩阵持久化到磁盘比如存成.pt或.npy文件下次启动时直接加载省去重复计算。top_k做了 20 的上限防止恶意请求一次性拉走全库。接口设计上我习惯用 POST 加 JSON 请求体而不是 GET 带 query string因为中文文本在 URL 里容易产生编码问题。前端调用时用fetch或 axios 发 JSON后端返回的也是 JSON两边都好处理。4.2 启动服务与调用示例依赖安装完成后在项目根目录执行pip install -r requirements.txt python app.py看到Running on http://0.0.0.0:5000后可以用 curl 测试接口curl -X POST http://localhost:5000/search \ -H Content-Type: application/json \ -d {text: 一只白色的猫, top_k: 3}返回的 JSON 大致是{ query: 一只白色的猫, results: [ {path: images/cat_01.jpg, score: 0.8931}, {path: images/cat_02.jpg, score: 0.8712}, {path: images/dog_01.jpg, score: 0.6523} ] }host0.0.0.0意味着同一局域网的其他设备也能访问方便在演示时让同学用手机或另一台电脑打开你写的简单前端页面。如果只想本地调试可以改成127.0.0.1。debugFalse是一个关键点开着 debug 会导致普通请求触发 reloader同时暴露调试器课程设计答辩时显得不专业所以生产或演示时必须关掉。4.3 并发与性能优化方向Flask 自带的开发服务器是单线程的同时来了十个请求就会排队。如果演示现场有很多终端同时查询建议用 gunicorn 或 waitress 启动比如gunicorn -w 4 -b 0.0.0.0:5000 app:app-w 4表示开 4 个 worker 进程每个进程会共享模型的参数但显存占用会翻 4 倍。对于课程设计单 worker 其实也够这个参数写到答辩 PPT 里可以作为“我给你测过并发”的亮点。图像特征矩阵如果已经存在内存里查询过程只有一个矩阵乘法和一个 topk速度瓶颈基本在网络传输。另一个优化点是图片预处理preprocess中的 resize 和归一化会占用一定 CPU如果查询请求频繁可以在search_text外面加一层缓存把最近查询过的文本和结果存进字典相同查询直接命中缓存。优化方向做法适用场景多进程gunicorn -w 4并发查询 10 QPS特征持久化保存到 .pt 文件图片库 5000 张最近邻索引faiss.IndexFlatIP图片库 10000 张结果缓存dict 或 redis高频相似查询5. 进阶微调模型与排错实战5.1 在自定义数据集上微调 Chinese-CLIP如果你不满足于直接使用预训练权重想针对自己的图片库比如校园场景、动漫风格提升准确率可以用cn_clip/training里的脚本做微调。常见做法是准备一个 CSV 文件每行包含image_path和text两列然后用cn_clip/training/main.py启动训练。一个最小可用的启动命令是python -m cn_clip.training.main \ --data-path /path/to/train.csv \ --model ViT-B-16 \ --lr 2e-6 \ --epochs 1 \ --batch-size 32 \ --device cuda微调时学习率要设置得比从头训练小很多因为预训练权重已经很好学习率太大会导致灾难性遗忘。2e-6是一个比较保守的起点如果训练损失长期不降可以提高到1e-5。batch-size受显存限制ViT-B-16 在 16G 显存下可以跑到 64但一般 32 就够了太大反而让对比学习的负样本分布过均匀收益有限。需要注意的是微调后的模型应当保存为新文件比如checkpoints/my_model.pt然后修改text2image.py里的load_from_name的download_root指向该文件。不要覆盖原始预训练权重否则以后回溯效果时没有干净基线。5.2 常见问题内存、分词与模型加载失败我实际跑这个项目时遇到过几个坑列出来可以作为排错清单。错误现象可能原因解决方式RuntimeError: CUDA out of memory图片 batch 太大把encode_image改成单张循环或用torch.cuda.empty_cache()KeyError: UNKtokenize 版本不匹配重新安装 cn_clip 配套的 transformers 版本ConnectionError下载权重失败网络问题手动下载 checkpoint 放入download_rootpreprocess找不到依赖缺失pip install cn_clip -i 清华源中文文本返回乱码结果Flask 接收编码问题确保请求头Content-Type: application/json; charsetutf-8有几个细节值得多说一句。第一CN-CLIP 的 tokenizer 是自带的不要使用 huggingface 的BertTokenizer替代否则分出来的 token 编号不对检索结果会很离谱。第二当图片库包含大量背景相似的图时top-k 结果会非常集中这时候可以在search_text中加入一个简单的去重逻辑根据结果图片的路径前缀过滤掉同一个目录下的重复图。第三如果服务器只有 CPU建议安装onnxruntime并把模型导出成 ONNX推理速度能提升 2-3 倍不过这个属于锦上添花答辩时提一句就能加分。本文还有配套的精品资源点击获取