news 2026/10/22 11:50:33

中文图文检索实战:基于Chinese-CLIP的双塔模型与向量检索

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
中文图文检索实战:基于Chinese-CLIP的双塔模型与向量检索

简介:这是一份面向计算机视觉课程设计的Python实现资源,基于Chinese-CLIP模型搭建中文图文检索系统,适合正在学习多模态检索、预训练模型应用或需要完成课程设计、毕业设计的学生使用。资源涵盖完整项目代码,共59个文件,包含40个Python脚本、9个JSON配置、7个pyc编译文件以及说明文档、示例图片等,压缩包整体仅577KB,便于快速下载与部署。代码按功能拆分为图像预处理、CLIP模型封装、模型训练与评估、应用部署等模块,并提供utils工具脚本与基于交互界面的入口文件,读者可借此理解图文特征提取、相似度匹配以及前端展示的完整链路。项目中还附带README和使用说明,方便对照调试;整体结构清晰、注释简明,已有440人学习,适合作后续扩展开发的基线项目。

1. 课程设计做图文检索,为什么我劝你直接上 Chinese-CLIP

计算机视觉课程设计里,“图文检索”是个既好讲又容易出效果的方向:给定一句中文描述,从图库里把最匹配的图找出来。但很多人一开始就奔着“自己训练一个模型”去,结果数据标注、显卡、调参三座大山压下来,交作业前一周还在跑 baseline。我的建议很直接:用 Python 基于 Chinese-CLIP 来做。它是 OpenAI CLIP 的中文版本,已经在大规模中文图文对上做过预训练,你真正要做的不是训练模型,而是把预训练权重加载进来,做特征提取和向量检索。这个路线对计算机视觉大作业来说,CPU 都能跑、效果立得住、答辩有讲头。下面从原理到代码,把一条能直接复现的路给你铺好。

2. Chinese-CLIP 到底改了什么:双塔模型与图文对齐原理

动手写代码之前,得先把模型的结构讲清楚。否则代码跑通了,答辩时被问一句“双塔的两条塔各是什么”,一样会卡壳。

2.1 从 CLIP 到 Chinese-CLIP:英文模型在中文场景的三处翻车

先说说为什么不能直接用原版 CLIP。如果你拿 OpenAI 的 CLIP 做中文图文检索,会遇到三个很现实的问题。

第一,词表问题。CLIP 的文本编码器是英文 BERT,词表里没有中文字符,中文句子喂进去会被 tokenizer 切得稀碎,甚至大量变成[UNK]。第二,翻译损耗。一个常见补救是先把中文翻译成英文再检索,但翻译本身就会丢信息,“红烧肉”翻成 braised pork 勉强能懂,“烟火气”这种词基本就翻废了。第三,训练分布。CLIP 的预训练数据是英文互联网图文,它对中文语境里的概念理解很弱。

Chinese-CLIP 做的事情就是针对这三个问题:把文本塔换成中文预训练模型,用大规模中文图文对重新做一次对比预训练。它保留了 CLIP 的双塔结构,但文本侧从英文 BERT 换成中文 RoBERTa 系模型,视觉侧仍然是 ViT。中文文本直接进模型,不再需要翻译这一步。

2.2 双塔结构的核心:图和文怎样落在同一个向量空间

双塔结构听起来玄学,原理其实很直白。图像塔接收一张图,输出一个向量;文本塔接收一段文本,输出一个向量。两个塔的输出维度必须一致,比如 ViT-B-16 这一档,图和文最后都落到 512 维。

模型训练时用的是对比学习:一个 batch 里有 N 张图和 N 条文本,第 i 张图和第 i 条文本是配对的正样本,其余 N-1 对是负样本。模型被要求把正样本对的相似度拉高、负样本对压低。训练完成后,一个“语义对齐”的向量空间就形成了。

推理时你做的事更简单:把查询文本送进文本塔得到查询向量,把图库里每张图提前送进图像塔得到图像向量矩阵,然后做一次余弦相似度排序,取 top-k。整个检索系统的主体不是一个实时推理模型,而是一个“预先提取特征 + 向量比较”的索引系统。

2.3 加载 Chinese-CLIP:用一段代码验证你的模型真的装对了

先别急着跑完整检索,第一步是确认模型加载正确,并且图像特征和文本特征的维度确实一致。我用 transformers 接口写一个最小验证,这是课程设计里最快的起步方式。

import torch from transformers import CLIPModel, CLIPProcessor model = CLIPModel.from_pretrained("OFA-Sys/chinese-clip-vit-base-patch16") processor = CLIPProcessor.from_pretrained("OFA-Sys/chinese-clip-vit-base-patch16") model.eval() # 打印模型两塔的输出维度,验证双塔是否对齐 print("text_proj:", model.text_projection.out_features) print("visual_proj:", model.visual_projection.out_features)

这段代码的重点是最后两行。text_projection 和 visual_projection 是两个线性映射层,分别把文本塔和图像塔的原始输出映射到公共向量空间。如果这两个值不一致,说明你的模型没配对,下游检索全部白搭。在 ViT-B-16 这个规模上,两边都应该是 512。

用 open_clip 的方式也常见,写法稍有不同,但同样能拿到一致的两塔输出。我一般用 transformers 接口做课程设计,因为它把 tokenizer、preprocess 这些周边组件一起打包好了,少写很多胶水代码。

注意:from_pretrained里的模型名要和 processor 完全一致。模型和 tokenizer 不匹配,是新手最常见的加载错误之一,后面避坑章节会专门说。

3. 把数据集和模型先跑起来:最小可复现的环境与加载代码

这一章进入可执行环节。目标只有一个:在你的机器上把模型跑起来,让一张图、一句中文文本真正过一遍模型,拿到相似度。

3.1 环境安装:Python、PyTorch、transformers 的版本搭配

先说环境。Python 版本选 3.8 起步比较稳,3.8 到 3.11 都跑过,没遇到兼容性问题。PyTorch 用 2.x 即可,transformers 用 4.x。这几个包之间没有强绑定,按常规安装顺序来就行。

conda create -n clip_search python=3.8 conda activate clip_search pip install torch torchvision pip install transformers open_clip_torch pillow numpy pip install opencv-python

参数说明:conda 创建虚拟环境时指定 python=3.8,可以避免污染系统 Python;torch 默认装的是 CPU 版或当前 CUDA 对应的版本,如果机器上有 N 卡,建议先到 PyTorch 官网按 CUDA 版本选对应命令,再装 transformers。opencv-python 不是模型必需,但课设里经常要画框、缩放、预览结果图,顺手装上能省很多事。

装完在命令行跑一句验证:

python -c "import torch, transformers, open_clip; print(torch.__version__, transformers.__version__)"

如果你用的编辑器是 VS Code,记得在右下角把解释器切到 clip_search 这个虚拟环境,否则会出现“明明装了包却 import 不到”的假性报错,这个问题在 Windows 机器上出现频率很高。

3.2 权重下载慢?从 ModelScope 拉同一个模型

from_pretrained会从 Hugging Face 拉权重,ViT-B-16 的权重文件几百 MB,网络状况不好的时候会卡很久。常见的替代做法是去 ModelScope 魔搭社区下载同一份权重。Chinese-CLIP 的作者在两个平台都放了权重,文件结构和格式完全一致。

把下载好的目录放进项目文件夹,from_pretrained直接传本地路径就行:

model = CLIPModel.from_pretrained("./chinese-clip-vit-base-patch16") processor = CLIPProcessor.from_pretrained("./chinese-clip-vit-base-patch16")

这是我的真实习惯:先把权重下到本地,再改成本地路径加载。原因有两个,一是离线跑课设演示更稳,二是把权重纳入项目目录后,答辩换机器不用重新下载。

3.3 跑通第一张图和第一句中文:最小推理代码

现在做第一次前向推理。找一张测试图,比如自己拍的、或网上随便找的风景照,再准备一句中文描述,比如“一只猫趴在窗台上”。

from PIL import Image import torch # 1. 预处理图片和文本 image = Image.open("test.jpg").convert("RGB") inputs = processor( text=["一只猫趴在窗台上", "一辆红色汽车在马路上"], images=image, padding=True, return_tensors="pt", ) # 2. 前向计算,拿相似度矩阵 with torch.no_grad(): outputs = model(**inputs) # 3. logits_per_image 是 [1, 2] 的相似度矩阵 probs = outputs.logits_per_image.softmax(dim=1) print(probs)

逻辑说明:processor 在一个调用里同时做了两件事——把文本经 tokenizer 转成 input_ids,把图片经预处理转成 pixel_values。padding=True 保证两条长度不等的文本在 batch 内对齐。模型前向之后返回的 logits_per_image 是一个形状为 [图片数, 文本数] 的相似度矩阵,再套一层 softmax 就变成“这张图更匹配哪条文本”的概率分布。

注意,这里返回的是相似度打分,数值本身没有绝对意义,只有相对大小有意义。这是 CLIP 系列模型的共同特点,后边做检索排序时我们直接拿这个分数排序,不会去纠结它是否落在 0 到 1 之间。如果你的显卡够用,device 可以改成 cuda,推理速度会快一个数量级;纯 CPU 跑单张图也在秒级,课设完全够用。

4. 特征提取与索引构建:把三千张图变成可检索库

单张推理跑通之后,真正的检索系统才刚开始。课程设计通常要面对一个几百到几千张的图库,如果每次查询都对每张图现场跑一次模型,速度会慢到无法演示。正确的做法是:离线把图库里所有图像特征提取出来存成文件,查询时只跑文本塔一次,然后拿文本向量和预存的图像向量矩阵做矩阵运算。

4.1 批量提取图像特征:用 DataLoader 替代 for 循环

批量提取特征最忌讳的是一个 for 循环逐张图推理。Python 的单张推理虽然能跑,几千张图下来性能很难看。常见做法是用 DataLoader 组织数据,以 batch 方式喂给模型。model 和 processor 沿用上一章加载好的全局变量。

import os import numpy as np import torch from PIL import Image from torch.utils.data import Dataset, DataLoader class ImageDataset(Dataset): def __init__(self, img_dir): self.paths = [] for name in os.listdir(img_dir): if name.lower().endswith((".jpg", ".jpeg", ".png")): self.paths.append(os.path.join(img_dir, name)) def __len__(self): return len(self.paths) def __getitem__(self, idx): image = Image.open(self.paths[idx]).convert("RGB") # pipeline 里包含 resize、normalize,务必走这里 return processor(images=image, return_tensors="pt")["pixel_values"].squeeze(0) dataset = ImageDataset("images") loader = DataLoader(dataset, batch_size=32, shuffle=False) features = [] with torch.no_grad(): for batch in loader: batch = batch.to(device) image_features = model.get_image_features(batch) # 归一化,后面检索直接做内积 image_features = image_features / image_features.norm(dim=-1, keepdim=True) features.append(image_features.cpu().numpy()) features = np.concatenate(features, axis=0) np.save("image_features.npy", features)

逻辑说明:ImageDataset 负责把图片读进来并完成预处理,关键点是__getitem__里必须走 processor 的 images 分支,而不是自己随意 resize。图片路径列表和特征数组按相同顺序排列,这样检索出第几个特征,就能反查回第几张图。model.get_image_features 只取图像塔的输出,跳过文本塔的计算,比完整 forward 更快。

参数说明:batch_size 直接决定显存占用,如果显卡只有 6G 显存,建议从 32 降到 8 或 16。训练模式下的 dropout 层在推理时必须关掉,这就是为什么要包在torch.no_grad()里并让 model 保持 eval 状态。归一化这行不是可选项,它保证了后面向量内积等于余弦相似度,否则不同图的特征向量模长差异会干扰排序。

4.2 文本特征与检索打分:余弦相似度还是点积

图像特征存好了,查询流程就变得很轻:让文本塔输出一个查询向量,然后和图像特征矩阵做一个矩阵乘法,取 top-k。

def search(text, image_features, top_k=5): # 文本特征提取,同样要归一化 inputs = processor(text=[text], padding=True, return_tensors="pt") with torch.no_grad(): text_features = model.get_text_features(**inputs) text_features = text_features / text_features.norm(dim=-1, keepdim=True) text_features = text_features.numpy() # 图像特征已经归一化,内积即余弦相似度 scores = image_features @ text_features.T # [N, 1] scores = scores.flatten() top_indices = np.argsort(-scores)[:top_k] return top_indices, scores[top_indices] indices, scores = search("一只猫趴在窗台上", features) for idx, score in zip(indices, scores): print(dataset.paths[idx], round(float(score), 4))

逻辑说明:image_features @ text_features.T把 [N, 512] 和 [512, 1] 相乘,得到 N 个相似度分数。因为两个向量都做了 L2 归一化,这个内积就是余弦相似度。np.argsort(-scores)是对分数做降序排列的惯用写法,比np.argsort(scores)[::-1]更清晰,也少一个翻转步骤。

参数说明:top_k 在课程设计里一般取 5 到 10,演示效果最好。如果检索结果不理想,先别急着换模型,多半是文本描述和图像内容在语义上本来就不匹配,或者图中主体占比太小,后边避坑章节会细说。

4.3 索引落地:用 npz 把特征和路径打包存盘

特征提取是一次性的工作,第二次运行程序直接从磁盘加载就行。如果图库不变,没必要每次启动都重新跑一遍图像塔。

np.savez("search_index.npz", paths=np.array(dataset.paths, dtype=object), features=features)

加载时:

data = np.load("search_index.npz", allow_pickle=True) paths = list(data["paths"]) features = data["features"]

逻辑说明:npz 是 numpy 的多数组打包格式,一个文件同时存路径清单和特征矩阵。dtype=object是因为路径字符串长度不一致,numpy 默认的 unicode 数组会强制截断到最长字符串的长度,容易踩坑。allow_pickle=True用于加载带有 Python 对象的数组,只对信任的数据文件开启。

规模上有一个边界:课程设计的两三千张图,每张 512 维 float32 特征大约 2KB,三千张也就 6MB 左右,线性扫描完全够快。真到了十万张级别再考虑 FAISS 这类向量索引,课设阶段用 numpy 反而是最好的选择——代码少、逻辑透明、答辩时好解释。把这一步想清楚,你就不会在课设里过早引入黑匣子。

5. 课程设计避坑:五个让图文检索系统“测出来没用”的常见问题

这一章全是血泪经验。下面的每个坑都来自实际做图文检索的人反复踩过的,按“现象 → 原因 → 解决”写清楚。我按遇到频率从高到低排,你可以直接对照自己的现象查。

5.1 相似度分数全部一样,检索结果总是同一张图

现象:不管搜什么,返回的 top-k 几乎都是固定的几张图,分数还都差不多。

原因:图片预处理没有走 processor,而是直接喂了原始尺寸的原图。CLIP 系列模型对输入分布非常敏感,图像必须被 resize 到 224x224 并做与训练时一致的归一化。乱传尺寸会让所有图片的视觉特征塌缩到相近的位置,特征矩阵退化成几条相似的向量,检索自然失效。这个坑之所以隐蔽,是因为它不报错,程序运行完全正常,只是结果毫无区分度。

解决:把预处理统一收敛到 processor 的 images 分支里,不要自己在外面做 resize 再传进去。自查方法也简单:打印提取出来的 image_features 的标准差,如果非常小,说明特征塌缩了,十有八九是预处理不对。

5.2 中文被 tokenizer 切碎,搜“红烧肉”返回一堆厨房杂图

现象:语义匹配非常差,检索结果看起来是按字面在匹配,而不是按语义。

原因:用错了 tokenizer。典型情况是你之前跑过英文 CLIP 的 demo,把英文 CLIPProcessor 加载进来处理中文文本。英文词表里没有中文字符,整句中文被切成一堆未知 token,模型只能拿着碎片去找视觉特征,结果自然全乱。这种错在代码层面同样完全不报错,因为 tokenizer 很“宽容”,切碎之后照样给你输出向量。

解决:确认CLIPProcessor.from_pretrained加载的就是 Chinese-CLIP 那个目录,模型和 processor 必须来自同一个 checkpoint。一个简单的自查方法:打印processor.tokenizer.vocab_size,中文版本的词表明显大于英文版本。你如果是从本地路径加载的,这个错基本不会犯;容易犯的是模型从 A 目录加载、processor 从 B 目录加载。

5.3 批量提取特征时显存溢出,跑一半 OOM

现象:单张推理正常,一上 DataLoader 就报 CUDA out of memory,程序在提取了几百张图之后突然崩掉。

原因:batch_size 设置过高,同时没有用 no_grad 和 eval,模型依然在维护反向传播的中间变量。很多人以为model.eval()就够了,实际上一旦没有torch.no_grad(),PyTorch 会照常构建计算图,显存占用是按训练模式来算的,翻一倍不止。这两种因素叠加,显存就爆了。

解决:把 batch_size 降到 16 或 8;确认特征提取代码里写了with torch.no_grad():,并且在循环前执行过model.eval()。如果你的数据集中有超大分辨率图片,预处理阶段也会临时占显存,可以在 processor 里确认图像已经被缩到 224,而不是带着 4K 原图进模型。

5.4 查询时把整张图也送进文本塔,报 shape mismatch

现象:跑检索函数时报维度对不上,或者输出的特征形状和你预期的不一致。

原因:用了model(**inputs)做特征提取,而 inputs 里同时包含了图像和文本。这种完整 forward 返回的是图文匹配分数和两塔的最终输出,但它不是按“单塔特征提取”的方式组织的。有人会在拿到 outputs 之后随手取outputs.image_embeds当查询向量,地址是取到了,形状却和预存的图像特征矩阵不匹配。

解决:特征提取阶段,分别用model.get_image_features(pixel_values)和model.get_text_features(input_ids)来取对应塔的输出。这和前面推理章节并不矛盾——完整 forward 适合算相似度,特征提取阶段只关心单塔输出。

5.5 检索结果“相关性很弱”,图里明明有猫却没搜到“猫”

现象:直观上应该命中的图没有出现在结果里,检索出来的图看着“沾边”但又不完全对,说不上来哪里不对。

原因:CLIP 类模型做的是语义级别的对齐,对细粒度属性不敏感,尤其是“画面左边有一只小花猫”这种包含位置、颜色、数量的复合描述,模型经常失误。这是模型本身的特性,不是你的代码 bug,属于“知道这个边界就不会焦虑”的问题。

解决:查询词尽量写语义化的场景描述,比如“窗台上的一只猫”比“橙色的猫”更容易命中。做课设演示前,先用自己的图库实测几轮,挑那种语义明显、主体突出的图像和查询词组合来演示。如果你想硬啃细粒度检索,那就不是加层归一化能解决的了,需要走微调或加入检测模型辅助的路线,课设阶段不建议碰。

6. 从能跑到能答辩:检索排序的小技巧与评估指标

系统能跑通只是及格线,课程设计要拿高分,还得在两件事上下点功夫:排序的演示质量和评估的量化指标。

先说立刻能提升演示效果的技巧:给相似度加一个阈值过滤。CLIP 的相似度分数即使在完全不匹配时也不会是负数,反而可能是一个不算低的正值。演示时如果不管什么都强行返回 top-5,结果里就会混入毫无关联的图。我一般会在检索函数里加一个最小分数阈值,低于阈值的就不展示,宁可少返回,也别返回明显不相关的“硬凑”结果。

def search_with_threshold(text, image_features, top_k=5, threshold=0.20): indices, scores = search(text, image_features, top_k=len(image_features)) valid = [(i, s) for i, s in zip(indices, scores) if s >= threshold] return valid[:top_k]

阈值取多少是个玄学,跟你的图库内容有关,我习惯先用 0.2 起步,跑几个查询看分数分布再调。这个技巧在演示时非常提气,评委看到“没有结果就不硬出”的行为,会觉得你的系统有工程判断力。

然后是评估指标。课程设计要量化效果时,最常用的指标是 Recall@K,意思是:对每个查询,正确答案是否出现在前 K 个结果中。实现很简单,准备一批“查询文本 → 正确答案图片”的测试对,跑检索后检查答案图片的排名即可:

def recall_at_k(query_text, ground_truth_path, image_features, k=5): indices, _ = search(query_text, image_features, top_k=k) return 1 if ground_truth_path in [paths[i] for i in indices] else 0

把二三十个查询的 Recall@5 平均一下,就是能写进报告的一个数字。答辩时用“本系统在自建测试集上 Recall@5 达到多少”这样的表述,比“效果看起来不错”有力得多。

最后一个个人建议:课程设计阶段的图库别贪大。我见过有人一上来爬了一万张图,结果特征提取跑了一个通宵,检索质量却没比三千张好多少。图像内容越杂,检索稳定性越差。选一个主题集中的图库,比如“餐饮菜品”“户外风景”“宠物图片”,把注意力放在检索链路和评估闭环上,这才是课设评分真正看的东西。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 1:11:13

开源+Web3.0:从COSCon‘25看去中心化生态创新路径

COSCon‘25 的 Web3.0 开源论坛议程刚放出来,我所在的几个开源社群里就有人转发了。在开源圈子里,COSCon 这几年的议题一直偏工程、偏社区落地,这次专门把 Web3.0 单独拎出来建了一个论坛,并且主题定成“去中心化生态创新路径”&a…

作者头像 李华
网站建设 2026/10/20 15:37:27

排班考勤数智化转型:从需求预测到算法优化的实践指南

1. 用工波动常态化,为什么排班考勤成了第一块硬骨头这几年走访过不少制造工厂、连锁零售企业和物流仓储基地,几乎每家都在抱怨同一个问题:人力需求的波峰波谷越来越陡,淡旺季界限模糊,促销节点、天气变化、平台流量波动…

作者头像 李华
网站建设 2026/10/21 1:49:38

并行归约+区间贪心+树状数组:大规模区间统计的三层优化实践

我最近在处理一个大规模区间统计任务时,把并行归约、区间贪心和树状数组这三种东西放到了一起用,效果出乎意料地好。说实在的,以前我一直觉得这类题目靠单线程加个懒标记线段树就够用了,直到数据量涨到百万级别、查询请求密集成串…

作者头像 李华
网站建设 2026/10/19 12:06:16

Agent-Reach:智能体从能聊到能干的四层链路与工具调用实践

做 AI Agent 项目的人应该都有同感:模型智商早就不是瓶颈了,真正卡住我们的是 Agent-Reach——智能体到底能不能“够得着”真实的业务系统、数据库和外部工具。上个季度我接手了一个内部助手项目,模型选得再强,一到调考勤接口、写…

作者头像 李华
网站建设 2026/10/19 3:10:40

RocketMQ核心原理与生产实践:事务消息、顺序消息与消费堆积避坑

1. 为什么是 RocketMQ:消息中间件选型背后的取舍我第一次认真研究 RocketMQ,是因为一个订单系统的线上事故。那会儿团队用的还是自研的基于 Redis 的队列方案,业务一上来,延迟和丢消息的问题接踵而至。订单创建后要同步通知积分、…

作者头像 李华