五一假期,我基于 CLIP 模型搭建一个支持中文的图片搜索应用——你输入文字,它返回匹配的图片;但原生 CLIP 对中文有些“水土不服”,搜出来的结果经常不相关;我用了 3 万张中文图文对微调 CLIP 模型,大幅提升了结果召回的准确率,这篇文章分享一下搭建和优化过程。
微调效果评估如下图:使用 500 条中文&图片数据作为验证集,观察到TopK的准确率有非常大的提升。(蓝线-OpenCLIP原始版本,橘色-OpenCLIP全参微调版,绿色-OpenCLIP LoRA微调版)
主观一些query评测结果如下图,可以看到LoRA版本的图片召回相关性提升很大。
| query | 几个大学生在打篮球 | 几个可爱的小孩 | 美女和花朵 |
|---|---|---|---|
| 搜索结果 |
一、基于 CLIP 的图搜应用搭建
在开始微调之前,我先用原始 CLIP 模型搭建了一个基础的图文检索系统。搜索过程分为三步:
1、用 OpenCLIP 把图片库里的每一张图编码成向量,存入 Milvus。
2、当用户输入文字搜索时,将文字也编码成向量,然后在 Milvus 里搜索最相似的图片向量。
3、用 Streamlit 做一个简单的 Web 界面,让整个检索过程可视化。
1.1 环境准备
我使用的硬件是 Mac M3(36GB 内存),Python 3.12,milvus v2.6.13,可以使用docker安装milvus。
1.2 计算图片向量并入库
1、准备图片数据,可以在ModelScope等平台下载数据。
2、加载 OpenCLIP 模型
3、遍历图片目录,用 model.encode_image() 获得 512 维向量并归一化。
4、将图片路径和向量插入 Milvus 的 Collection 中。
5、插入完成后,给向量字段建立 IVF_FLAT 索引,以便后续快速检索。
1.3 实时检索与展示
检索时,用户输入文字后,使用OpenCLIP进行文本编码得到归一化向量,而后在 Milvus 中执行向量搜索,返回相似度最高的几张图片并展示。
下面是其中一个query 石头(rock)的结果截图,可以明显看到,原生clip在中文搜索的情况下对比英文效果有较大差距,这为后面的微调埋下了伏笔。
至此,一个基础版的中文图搜系统已经跑通,但是对于中文不太友好,接下来我的目标是用中文图文对微调模型,提升中文的检索质量。
二、模型微调,让CLIP对中文更友好
上一节我们看到,原生 CLIP 对英文查询表现优异,但在中文场景下却差强人意。猜测可能是 CLIP 的预训练数据以英文图文对为主,它的文本编码器基本没有见过中文句子,自然无法把“大海”和“石头”这种中文概念与图片对应起来。
要解决这个问题,最直接的办法就是用中文图文数据重新“教”一遍 CLIP,也就是微调。下面我会完整记录从数据准备、训练脚本编写、过程监控到最终效果的全过程。
2.1 数据集准备:3万张图片+15万中文描述
我使用的数据集来自 AI Challenger 图像中文描述数据集(数据来自ModelScope)。它包含约 3 万张图片,每张图片配有 5 句不同的中文描述,总计约 15 万图文对。图片内容主要涵盖新闻、活动、日常生活等场景,描述质量较高且已经过人工清洗。
经过一系列的数据清洗和划分,最终得到
训练集:147,500 条图文对
验证集:2,500 条图文对
2.2 微调策略:全参数微调 vs LoRA 微调
微调策略可以选择全参数微调或 LoRA 微调,为了对比两种微调的效果,我选择了全参微调和LoRA微调同时进行。
全参微调方面,CLIP 模型只有 1.5 亿参数(0.15B),远小于传统大模型,全参数微调在 Mac M3 上完全可以承受。另外 OpenCLIP 官方提供了开箱即用的训练脚本,执行一条命令并指定参数就能开始全参数微调,省去了手写 LoRA 适配的麻烦。
LoRA 微调方案就比较复杂了,需要理解一下 CLIP 的模型的原理(后面会讲这个),基于 peft 框架对于 CLIP 模型注入 LoRA 低秩矩阵,基于训练数据进行正常传播,计算交叉熵损失,然后反向传播更新模型参数,完成一轮训练。
具体训练的参数如下:
| 参数 | 全参微调 | LoRA微调 |
|---|---|---|
| 模型 | ViT-B-16 | ViT-B-16 |
| 预训练权重 | openai | openai |
| 可训练参数 | 1.5亿左右(100%) | 150万左右(0.97%) |
| batch_size | 128 | 128 |
| 学习率 | 1e-5 | 5e-4(大50倍) |
| epochs | 5 | 5 |
| 精度 | bf16 | bf16 |
| 优化器 | AdamW | AdamW |
| 训练框架 | open_clip_train.main | 手写训练循环 |
经过几天的调试运行,终于完成了全部微调训练。
2.3 微调过程
全参微调持续大约9个小时,在训练过程中loss持续下降:初始 loss 约 5.6,到第一个 epoch 结束时平均 loss 降至 0.93,第二个 epoch 继续下降至 0.56,最终第五个 epoch 结束时 loss 稳定在 0.41 左右。
LoRA微调需要手写图片加载和模型训练过程,在经历几轮OOM和怪异的现象处理后也跑了起来, 经过8个小时左右的训练成功完成,loss从初始的5.4下降到0.26
三、效果对比:微调前后的差距有多大?
模型微调完成后,我通过预留的测试数据和主观评测对微调效果进行了评估,结果可以说立竿见影:微调后的模型在准确率上全面优于原始模型。
3.1 定量评估:Recall@K曲线
我使用验证集中的2500条中文描述作为查询,分别用原始模型、全参微调模型、LoRA 微调模型计算 top-K 检索结果的命中率(Recall@K)。评估逻辑很简单:如果某条查询的正确图片出现在返回的前K个结果中,就算一次命中,K从 1 取到10。
评估结果如下图所示:
| 模型 | Recall@1 | Recall@3 | Recall@5 | Recall@10 |
|---|---|---|---|---|
| 原始模型 | 1.0% | 1.8% | 2.2% | 4.0% |
| 全参微调 | 53.8% | 79.4% | 87.2% | 92.8% |
| LoRA 微调 | 66.6% | 86.0% | 90.0% | 95.2% |
结论非常清晰:原始 OpenCLIP 模型的 Recall@1 仅有 1%,几乎完全不具备中文图文检索能力——这是意料之中的,该模型在英文数据上预训练,对中文语义的理解几乎为零。经过微调后,Recall@1 跃升至53%~66%,Recall@10 达到 92%~95%,检索能力得到了质的飞跃。
更值得关注的是,LoRA 微调在所有指标上均优于全参微调,而它的可训练参数量仅有 147 万,只占全参微调 1.5 亿参数的 0.97%,这一点和之前的认知有一些不同。LoRA作为一种外挂的低秩矩阵,我之前的认知中会认为LoRA是一种成本和效果的权衡,通过小的参数量、小的参数改动达到相对还不错的效果,但在这次训练中,LoRA的效果比全参微调还好。这个问题的原因大概有几种猜测:
第一,预训练知识保留程度不同。 LoRA 的核心机制是冻结原始权重,只在旁边学习一个增量修正:W' = W_冻结 + ΔW。这意味着 CLIP 在大规模英文图文数据上积累的通用视觉语义能力被完整保留,微调只是在此基础上叠加了中文语义的"方向修正"。全参微调则直接修改所有权重,原始的对齐能力难免受到干扰。
第二,数据量与参数量的匹配问题。 本次训练数据约 15 万条,对于 1.5 亿参数的全参微调而言相对有限,优化空间过大反而容易让模型在训练集上走偏。LoRA 只优化 147 万参数,搜索空间大幅压缩,15 万条数据已经足够让这些参数充分收敛。
3.2 定性对比:中文搜索能力大幅提升
除了定量指标之外,我还挑选了几个具体的查询,直接展示搜索结果的截图,搜索结果质量有明显提升。
query: 几个大学生在打篮球
四、CLIP基础原理介绍:图片&文本双塔模型
CLIP模型来源于2021年1月openAI发布的论文《Learning Transferable Visual Models From Natural Language Supervision》,这篇论文的作者足足有十几位,其中就有前两代 GPT 模型作者 Alec Radford,以及 OpenAI 的前首席科学家 Ilya Sutskever。
CLIP 的核心思想是大规模利用互联网上的图文配对,训练一个双塔模型:图像塔和文本塔。通过对比学习,让配对图文在向量空间靠近、错误配对远离。预训练完成后,模型仅靠自然语言提示就能零样本迁移到新任务,无需额外标注数据,实现了视觉概念与语言描述的灵活对齐。
详细解释下这个模型的组成部分:
1、TextEncoder: 基于Transformer架构,通过多头注意(multihead-attention)机制生成的文本embedding,可以简单理解为:每个词都能看到句子中其他词,并根据相关性汇总信息,最终得到一个能反映整句话语义的embedding。
2、ImageEncoder: ViT模型,将图片切成若干patch,如16*16的小块,每个patch被拉平成一个向量,类似一个单词,把这些单词序列输入 Transformer 编码器,最后输出一个代表整张图的embedding。
3、CLIP模型: 将N个匹配的图文描述对分别计算文本embedding和图片embedding,然后将这些向量两两相乘(点积运算)。其中对角线 I₁T₁, I₂T₂, …, IₙTₙ 这些元素是匹配的正例,通过训练拉近这些向量的距离。其余N^2-N个乘积均为负例,要通过训练拉远这些向量的距离。
4、损失函数: 损失函数看着比较吓人,其实拆解一下很简单:
(1) 先看懂第一个式子。
a、左边的求和再1/N,是指单轮训练内多个图片训练loss的平均值。
b、去掉上述部分,下一层是-ln(xxx),这个函数其实是对数函数的负数,当xxx里面的值接近1时,整体函数趋近于 0;当xxx里面的值接近0时,整体函数值会比较大。这其实机器学习中非常常见的损失函数:交叉熵损失。
c、去掉上述-ln(xxx)这个对数函数后,里面的分数部分也可以简称是一种softmax函数。分子代表第i个图片和第i个文本匹配的相似度,分母代表第i个图片和所有文本(从1到N)匹配的相似度之和。前面我们说过,第i个图片和第i个文本是正例,因此模型需要尽量提高这个值;而第i个图片和其他文本则是反例,这些反例和正例的相似度加一起放到分母,因此模型需要尽量降低这个值。
d、上述两步配合起来,当模型训练很好时,正例匹配,负例不匹配,分子的大小占到了分母的绝大部分,结合-ln(xxx)函数,损失很小。当模型训练效果很差时,正例不匹配或负例匹配度高,那么后面的分数就会很小,对应到-ln(xxx)函数,损失会很大。
(2) 有了第一个式子后,第二个式子是把矩阵转置过来,用同一个文本再去对应所有的图像,同样是增大正例得分,减少负例得分
(3) 最终的一次训练batch的loss等于1和2两部分的平均值。
五、总结
1、效果局限性: 这个训练本身只使用了15万条数据,从数据集看大部分与人类活动有关,因此与人类活动相关的图片识别准确率确实非常大,但是我用于检索自然风光、风景图片时,同样存在提升但效果相对有限。这也说明在clip模型应用中,训练数据最好来源于生产环境,最大程度让模型学会某个领域内的知识。如果想使用已经调好的中文clip模型,可以参考github.com/OFA-Sys/Chi…
2、关于模型微调: 在有限的数据情况下,全参微调的效果不一定比LoRA微调更好,全参微调需要学习的参数量大,在微调数据远远小于预训练数据的情况下,全参微调可能因为更新的参数过多导致丧失了一些预训练的效果。
3、训练代码: 本文的训练代码已经放到了 github:github.com/liuchenbuaa…,欢迎感兴趣的同学一起学习和探讨。
wx 刘琛AI实践,分享11年的大厂认知与实践。