ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

微信生态多模态Embedding实战:从CLIP微调到向量检索部署

微信生态多模态Embedding实战:从CLIP微调到向量检索部署 微信做多模态 Embedding这个标题一看就知道是冲着业务检索和内容理解去的。这两年多模态大模型火是火但真正落到微信小程序、公众号、视频号这种生产环境里最常用的其实不是让模型生成图文而是让它把图片、文本、甚至视频统一映射到一个向量空间里然后做语义检索、相似匹配、推荐召回。这套东西做得好不好直接决定搜索准不准、推荐贴不贴、去重灵不灵。这篇内容适合谁一类是微信小程序开发者想在站内做“以图搜商品”、“拍照识物”另一类是做内容中台的算法工程师想把公众号文章和视频封面统一建索引还有一类就是刚入门多模态检索的学生想搞清楚从数据到部署到底要踩多少坑。我会按真实的项目推进顺序来讲从需求拆解到数据清洗从模型选型到训练细节最后聊到在小程序端部署的体感所有参数和代码都是我在类似场景里验证过的可以直接拿去改。先泼一盆冷水别指望自己从零预训练一个多模态模型除非你有几百张卡和几千万的预算。真实做法是拿开源的中文多模态模型做底座在微信生态的数据上做对比学习微调这一步投入产出比最高。1. 项目启动前先把需求拆成可落地的技术指标很多团队一上来就说“我要一个多模态模型”但“多模态”三个字背后是完全不同的技术路线。如果需求是“用户拍一张商品图返回同款商品”这是图像到文本的跨模态检索如果需求是“视频号里找和当前视频风格相似的视频”这是视频到视频的同模态检索难度天差地别。所以第一步不是选模型而是把产品需求翻译成技术指标。1.1 检索场景决定Embedding的架构设计先问自己三个问题第一query端是什么模态候选库是什么模态第二是单条匹配还是批量召回第三容忍多高的延迟这几个问题的答案基本就定死了技术路线。如果两端模态不同比如文本搜图片、图片搜文本那就必须做跨模态对齐输出一个共享的向量空间。如果两端模态相同比如图片搜图片那其实可以偷懒用单模态的图片Embedding模型就够了没必要上多模态。还有一个关键问题微信生态的query通常很短可能就是“红色连衣裙”五个字而候选文本是公众号文章标题、商品标题这种长文本这种长短文本的匹配问题在结构设计上就要考虑进去不能直接拿BERT那种token级输出硬怼。我把需求拆成了四个维度模态类型、匹配粒度、实时性要求、数据规模。模态类型决定用双塔还是单塔匹配粒度决定是拿整句Embedding还是token级交叉注意力实时性要求决定是否要用ANN索引Approximate Nearest Neighbor近似最近邻搜索数据规模决定要不要分布式训练。微信生态的典型场景是千万级商品/文章库、毫秒级响应所以几乎只能走“双塔Embedding Faiss/向量数据库召回 粗排精排”这条路线。1.2 离线指标和线上指标要提前对齐我在项目里碰到过一个经典问题模型在离线测试集上Recall10从85%涨到了90%但线上用户点击率反而掉了。为什么因为离线指标用的是“人工标注的相关性”线上用户的行为信号是“点击与否”这两个不完全等价。多模态检索尤其明显用户搜“粉色毛衣”却点了图片上带猫的商品这种标签噪声会直接扭曲模型的学习方向。所以项目启动时就要定义清楚离线看RecallK和MRR线上看点击率提升和搜索无结果率下降。另外要特别注意“头部效应”微信生态的数据分布极度不均衡头部商品占了80%的流量模型很容易在头部过拟合、在尾部崩掉。评估的时候要把测试集按商品热度分层分别报告指标要不然模型上线后长尾检索效果会惨不忍睹。2. 多模态 Embedding 的核心原理用一句话讲透多模态Embedding的核心目标是把文本、图片、视频等不同模态的实体映射到同一个向量空间里让“语义相关的实体在空间里离得近语义不相关的离得远”。听起来很简单但落到实现上有三条路线选错了方向后面会非常痛苦。2.1 三条技术路线双塔、单塔、跨模态注意力第一条是双塔结构文本塔和图像塔各自独立编码然后在顶层做对齐。代表作就是CLIPContrastive Language-Image Pre-training它的优势是图像和文本可以分别预计算向量线上检索时提前把图片向量建好索引query过来只算一次文本向量再查ANN就行性能极其友好。劣势是文本和图像的交互只发生在最后的对比学习层细粒度匹配能力偏弱。第二条是单塔结构把文本和图像拼接在一起输入Transformer靠交叉注意力去做深度融合。代表作是UNITER、VLMO这类效果比双塔好但线上推理慢得离谱因为每来一个候选商品都要把文本和图片重新拼一次跑一遍forward根本扛不住千万级检索。第三条是跨模态注意力图像编码器出token序列文本编码器加一个cross-attention层去attend图像token。这种设计在图文匹配上效果好但工程复杂度高不太适合检索场景。微信生态的检索场景我建议直接用双塔结构而且要时刻提醒自己我们是来做检索的不是来做竞赛刷分的。能力边界很清楚双塔做粗召回后面可以接一个小的精排模型去补细粒度匹配。2.2 对比学习多模态对齐的基石双塔结构怎么对齐靠的是对比学习。思路很直接一个batch里有N对(文本, 图片)正样本对这N对做数据增强/负采样后模型要能分辨出哪对是匹配的。最常用的loss就是InfoNCE网上有大量实现。形式上是把文本向量和图片向量做点积得到相似度矩阵矩阵的对角线是正样本其余位置是负样本然后对每一行算交叉熵。import torch import torch.nn.functional as F def info_nce_loss(image_embeds, text_embeds, temperature0.07): # image_embeds: [batch_size, dim] # text_embeds: [batch_size, dim] # 归一化 image_embeds F.normalize(image_embeds, dim-1) text_embeds F.normalize(text_embeds, dim-1) # 相似度矩阵 [batch, batch] logits torch.matmul(image_embeds, text_embeds.T) / temperature batch_size image_embeds.size(0) labels torch.arange(batch_size, devicelogits.device) # 双向损失图片到文本的方向 loss_i2t F.cross_entropy(logits, labels) # 文本到图片的方向 loss_t2i F.cross_entropy(logits.T, labels) return (loss_i2t loss_t2i) / 2temperature这个超参数非常关键它控制着logits的平滑程度temperature越小模型对困难样本的惩罚越强。我用0.07做了个基线后来调到0.05在微信商品数据上好了不少但太低会训练不稳定。正负样本的构造对对比学习的效果影响比模型结构还要大后面专门展开聊。2.3 为什么说“模态对齐”不等于“模态融合”很多人把多模态Embedding和VQA视觉问答混为一谈。Embedding追求的是空间映射的一致性它不要求模型“理解”图像里的因果关系只要在向量空间里把“一个穿红色裙子的女模特照片”和“红色连衣裙”放得足够近就行。这大大降低了对模型参数量和训练数据量的要求也是为什么双塔结构在这种任务上能打。这也决定了训练的侧重点不需要花里胡哨的跨模态注意力最重要的反而是“难负样本挖掘”和“模态内数据增强”。这两个做好了效果直线上升否则再大的模型也白搭。3. 微信生态数据的特殊性三条数据来源与清洗经验模型结构定了接下来就是最痛的数据环节。微信生态的数据不像公开数据集那么规整公众号文章是长文本配图视频号是短视频配标题和封面小程序商品是短标题配商品图。这些数据散落在不同的业务端首先要解决的是“怎么拿到成对的图文数据”。3.1 三条数据来源各有坑公开数据集、业务日志、爬虫第一个来源是公开中文多模态数据集最有名的是WuDaoCorpora、MUGE、Zero等。优点是干净、量大缺点是领域偏差大。MUGE里面是电商数据还好一点零样本数据集里的图文匹配比较泛用来做预训练或者冷启动还行但想直接上线不经过微调效果很勉强。第二个来源是微信生态的业务日志。比如用户点了哪条商品、收藏了哪篇公众号文章点击行为天然构成了弱相关的图文对。这条路最贴近业务但噪声也最大需要花大力气清洗。我有一个心得不要用所有点击数据只用“点击后停留超过3秒”或“点击后购买了商品”这类强信号宁可数据量少一半也别把噪声喂给模型。第三个来源是爬虫获取的电商平台公开数据。这个上线前一定要做好合规评估我一般只用来做辅助训练不上线作为业务数据。微信生态还有一个特殊性大量用户会从聊天记录里转发小程序商品卡片这个卡片包含商品标题、主图、价格、甚至评价摘要这其实是天然的图文对。如果业务方能拿到用户授权后的脱敏数据这个数据质量比日志点击高一个数量级。3.2 数据配准关键不是“成对”而是“语义对齐”多模态威idata最怕的不是数据少而是“图文不对齐”。举个例子商品标题是“2024新款夏季纯棉T恤女宽松显瘦”但商品主图里除了T恤还有模特背的包、戴的帽子这就是典型的部分对齐问题。如果模型把“T恤”和“包”的视觉特征强行绑定检索的时候就会翻车。我的清洗流程是三道工序第一道是规则清洗扣掉敏感词、乱码、过短标题、纯表情包等第二道是人工抽检抽200条让标注同学看图文是否匹配如果匹配率低于85%就说明数据采集环节有问题得回炉第三道是模型辅助清洗用已经训练到一定程度的模型给图文对打分把相似度低于阈值的对丢回去让人工确认。这三道工序走下来数据质量会肉眼可见地提升模型训练效率也高很多。3.3 难负样本多模态检索效果的分水岭对比学习里负样本的选取质量直接决定最终效果。如果负样本太简单比如拿“狗的照片”去匹配文本“汽车”模型随便学学就分开了根本学不到细粒度语义。真实的微信电商场景难负样本是“红色连衣裙”对“粉色连衣裙”图片标题相似、但款式不同这才是考验模型能力的地方。我用的策略是“batch内随机负例 跨batch困难负例 向量索引挖掘”三管齐下。batch内随机负例是常规做法跨batch困难负例是每N个step用之前batch的向量做一次近似检索揪出最难区分的负样本向量索引挖掘则是定期用当前模型跑一遍全量候选库找出“相似度高但实际不相关”的样本加进训练集。这套组合拳下来模型的细粒度区分能力提升非常明显代价是训练时间增加了大概30%但相比效果的提升完全值得。4. 模型选型16G显存能跑什么OpenAI的CLIP还是中文开源模型先给结论不要直接拿OpenAI的CLIP跑中文场景效果会很惨。CLIP在英文互联网数据上预训练中文和中文商品图片的分布它根本没吃过。微信生态是重度中文场景你拿CLIP提取特征分词器和词汇表都不匹配再审慎地讲这是“用英文模型的脑子理解中文世界”能干活才怪。4.1 我实测过几个中文多模态底座各有取舍Chinese-CLIP目前中文生态里最扎实的开源多模态模型基于CLIP架构在中文图文对上做了大规模预训练有ViT-B/16、ViT-L/14等不同规模。直接拿来微调微信场景数据效果稳定部署难度低这是我最推荐的起步方案。Taiyi-CLIP昆仑万维的中文多模态模型也是CLIP路线在中文数据上有自己的优势某些场景下效果不错但社区生态和开源工具链不如前者成熟。mPLUG阿里达摩院的多模态理解模型走的是跨模态注意力路线效果更强但结构更重不太适合高效检索部署。通义/文心等大厂API不是模型选型而是方案选型。如果你业务量不大、对数据安全要求不高调API是最快的冷启动方式但长跑成本和延迟都是问题。4.2 16G显存的甜蜜点ViT-B/16 BERT-Base很多同学纠结自己的显卡能跑多大的模型。我自己的体验是一块16G显存的卡比如V100 16G或4090 Laptop用Chinese-CLIP的ViT-B/16配BERT-Basebatch size开到64、序列长度64用fp16混合精度训练稳稳的。再往上加模型规模就要考虑梯度累积和冻结部分层了。ViT-B/16的图像编码器大概是86M参数BERT-Base文本编码器是110M参数双塔加起来也就200M左右。这个体量在16G显存上完全跑得动而且推理的时候还能拆开图片塔全部预计算向量文本塔单独部署轻量级得很。如果非要上ViT-L/14这种大模型也不是不行但要付出代价。我的做法是冻结图像塔只微调文本塔和最后的映射层这样显存占用直接砍掉一半以上训练速度还翻倍。缺点是最终效果会比全量微调差一到两个点看你的预算和取舍。4.3 微调还是蒸馏从零训练是伪需求微信场景的多模态Embedding最优解永远是“大底座 领域微调”这里有一个公式化的判断标准你的领域数据量如果低于10万图文对别折腾训练了直接拿Chinese-CLIP提取特征建索引就能跑如果到了数十万级别做微调才有明显收益如果到了千万级别可以考虑蒸馏一个更小的模型出来部署。蒸馏这条路径在微信小程序场景特别实用。因为小程序包大小有限几百MB的大模型根本塞不进去所以可以训练的时候用大模型当teacher产出一批高置信度的伪标签再用一个小模型比如MobileViT TinyBERT去学teacher的输出向量。这样在线端模型小到可以塞进小程序里效果还能保住大模型七八成的水平。5. 训练全流程实操从配置到评估一步步走通这个章节我直接给出一个可复现的训练流程。以Chinese-CLIP的ViT-B/16为底座在微信商品图文对上做微调。所有代码都在PyTorch 2.x环境下验证过A100和4090都能跑。5.1 训练环境配置别在环境上浪费时间# 基于conda创建环境 conda create -n clip-finetune python3.10 -y conda activate clip-finetune # 安装PyTorch根据自己的CUDA版本选择 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 # 安装transformers、datasets、faiss pip install transformers datasets faiss-gpu这里有一个很重要的点transformers版本不能太新也不要太老我用的是4.30左右。太新了Chinese-CLIP的权重映射可能会有兼容问题太老了又缺一些新特性。如果碰到权重加载报错多半是版本不匹配先把transformers固定到4.30再试试。5.2 模型结构搭建双塔加载与冻结策略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) # 冻结图像塔的前几层文本塔全量微调 for name, param in model.vision_model.named_parameters(): if encoder.layers.0 in name or encoder.layers.1 in name: param.requires_grad False冻结策略是有讲究的。底层提取的是边缘、颜色、纹理这种基础视觉特征这些特征在所有视觉任务里都是通用的不需要重新学。高层提取的是语义特征比如“这是裙子还是裤子”这种才需要针对电商场景微调。同理文本塔里词向量层和底层的语法特征也可以考虑冻结只微调上层语义部分。另外注意CLIPModel里除了视觉和文本塔还有一个visual_projection和text_projection这两个映射层一定要开微调它们是模态对齐的核心通路。5.3 数据加载器与Batch的组织艺术数据加载器的核心不是把图片读进来而是怎么在batch内组织负样本。一个batch里除了成对的正样本其他都是负样本所以batch size越大负样本越丰富模型学得越好。但显存有限这时候梯度累积就派上用场了。from torch.utils.data import Dataset, DataLoader from PIL import Image class WxProductDataset(Dataset): def __init__(self, df, processor, max_length64): self.df df self.processor processor self.max_length max_length def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.iloc[idx] image Image.open(row[image_path]).convert(RGB) text row[title] # 分别处理图像和文本 pixel_values self.processor( imagesimage, return_tensorspt )[pixel_values][0] input_ids self.processor( text[text], paddingmax_length, max_lengthself.max_length, truncationTrue, return_tensorspt )[input_ids][0] return {pixel_values: pixel_values, input_ids: input_ids}dataset这里有个容易被忽略的坑图像路径不要训练时实时去磁盘读而是提前把图片全部decode成内存数组或转成jpg字节串否则IO会成为训练瓶颈。另外一定要对图片做随机裁剪和水平翻转增强这相当于在视觉侧做扰动增加模型鲁棒性。5.4 训练循环与Loss实现别把InfoNCE写错方向from torch.cuda.amp import autocast, GradScaler scaler GradScaler() optimizer torch.optim.AdamW(model.parameters(), lr2e-5, weight_decay0.02) accumulation_steps 4 global_step 0 for epoch in range(3): for batch_idx, batch in enumerate(train_loader): pixel_values batch[pixel_values].cuda() input_ids batch[input_ids].cuda() with autocast(): outputs model( input_idsinput_ids, pixel_valuespixel_values, return_dictTrue ) image_embeds outputs.image_embeds text_embeds outputs.text_embeds loss info_nce_loss(image_embeds, text_embeds, temperature0.05) scaler.scale(loss).backward() if (batch_idx 1) % accumulation_steps 0: scaler.step(optimizer) scaler.update() optimizer.zero_grad() global_step 1 if global_step % 500 0: print(fstep {global_step}, loss {loss.item():.4f})learning rate的选择我用2e-5做全量微调如果冻结了图像塔只调文本塔可以放宽到5e-5。warmup比例设在10%总epoch数不用多3个epoch在商品数据上基本就收敛了再多容易过拟合。温度参数t在训练中可以做动态调整比如从0.07慢慢降到0.05不过我偷懒直接固定0.05效果也够好。5.5 评估RecallK的坑import faiss import numpy as np def evaluate_recall(image_embeds, text_embeds, k10): # 建索引 dim image_embeds.shape[1] index faiss.IndexFlatIP(dim) faiss.normalize_L2(image_embeds) index.add(image_embeds) # 查询 faiss.normalize_L2(text_embeds) _, indices index.search(text_embeds, k) return indices # 每个文本查到的图片索引评估时有个细节如果测试集和训练集高度重合性能会被严重高估。所以我做评估时会单独维护一个不参与训练的候选库每次跑完再做一次基于距离的“去重”确保评估结果的真实性。微信场景还有一个指标很关键首次检索的成功率也就是用户搜一次就能找到目标的比例。这个指标线上盯得最紧比平均Recall更有业务感知度。6. 微信小程序端部署模型量化、体积控制与在线检索训练只是一个阶段能在微信小程序里真正跑起来才算落地。小程序环境极其受限代码包有2MB主包限制总包可以到20MB不能安装重依赖计算资源有限所以部署策略和传统服务端完全不一样。6.1 模型量化INT8是底线微调好的模型如果以fp32推出来ViT-B/16大约330MBBERT-Base大约420MB加起来接近800MB。这个小程序根本塞不下。我的做法是做两步压缩第一步转ONNX第二步做INT8量化。ONNX的好处是推理框架成熟配合onnxruntime在小程序端附近的设备或云函数上运行比原生PyTorch快好几倍。INT8量化一般能把模型压到原来的四分之一左右精度损失控制在两个点以内。如果还想更激进可以尝试把模型蒸馏成更小的结构比如把ViT-B/16蒸馏到MobileViT-S这个我在4.3提到过最终体积可以压到30MB以内。6.2 端侧模型 vs 云端API延迟和隐私的权衡微信小程序部署有两条路。一是纯端侧模型打成插件包放小程序里用户设备本地计算隐私最好、延迟最低但模型体积受限一般只能跑蒸馏后的小模型效果会打折扣。二是云端API把模型部署到云函数或自建服务器小程序端只负责上传图片、请求接口灵活性和效果上限更高但每次检索多一跳网络延迟数据也要经过服务端。我的建议是混合方案冷启动和低端机型走云端API高端机型且二次检索时走端侧小模型。微信有缓存能力同一个商品图向量可以缓存减少重复计算。另外云端API要注意加签名和频率限制防止被白嫖。6.3 隐私合规与审核注意事项微信小程序审核对“上传图片到服务器”这件事卡得很严。如果你的功能是“拍照识商品”必须明确告知用户图片会被上传并且提供“仅在本地处理”的选项如果支持端侧的话。合规做法是在《用户隐私保护指引》里列清楚收集的信息类型和使用目的并且把上传的图片做脱敏处理不保存原始图片、只保存特征向量和反馈结果。审核时还有一个坑小程序名称和简介里如果有“AI”、“识别”这类词有些类目需要提供额外的资质证明。最稳妥的方式是先以“搜索工具”或“工具-信息查询”类目提交等过审后再迭代AI功能。7. 常见问题与排查技巧训练和部署中踩过的坑这部分是我最想写的因为很多坑不踩一遍根本想不到。我把高频问题按出现频率排个序附上排查思路和解决办法能让你少走很多弯路。7.1 显存溢出不是batch size的锅是序列长度训练时如果OOM第一反应是降低batch size没错但更隐蔽的原因是文本序列长度。BERT的attention复杂度是序列长度的平方你设max_length64不代表每句话都用64个token实际Padding之后短句和长句占的显存一样多。我在商品标题场景把max_length从64降到32显存直接省了三分之一效果几乎没有损失因为商品标题平均长度也就20个字。7.2 Loss不下降或模态坍塌用了InfoNCE之后最让人头疼的是loss不降或者骤降到接近0但模型效果很差。loss不降先检查是不是相似度矩阵全接近均匀这通常是temperature太大或者模型初始化太差把temperature调小重新训。loss骤降到接近0但效果差大概率是模型找到了捷径比如图文对里所有文本都带着同一个品牌词“XX旗舰店”模型直接把品牌词当成了分类线索。模态坍塌也是常见问题训练刚开始还好训到一半文本向量和图像向量的分布严重偏离相似度矩阵变成一堆接近0的死值。解决办法是在loss里加一个均匀性正则项让向量在单位球上尽量均匀分布。我直接用现成的vICReg或者简单的方差正则就能压住。7.3 线上效果和离线指标对不上我做过的项目里离线Recall10从81%掉到79%但线上转化率反而涨了10%这种事情是有的。原因在于离线测试集的“相关性标签”是人工标的人工判断的是“图文语义相关”但线上用户的行为是“是否感兴趣”。多模态Embedding不管做得多好都只能保证语义相关不能保证用户喜欢。所以线上评估一定要分层新用户、沉默用户、活跃用户分开看转化率的涨跌可能只来自某一层人群。7.4 快速排错自查表下面这个表是我所有多模态检索项目通用的排错表遇到问题先对着过一遍能解决80%的坑。症状可能原因解决方案loss不降temperature过大 / lr过大减temperaturelr降到2e-5loss骤降但效果差模型走了捷径学习到噪声特征检查数据配准加强难负样本向量空间坍塌缺少正则加均匀性正则或增大batch size显存溢出文本序列过长 / 不必要的参数梯度剪短max_length冻结部分层效果不如基线测试集和训练集重叠 / 数据泄露重做评估集去重端侧模型误检量化精度损失过大改用动态量化或蒸馏小模型线上延迟高ANN索引参数没调好nprobe调到合适值或用HNSW关于具体调配Faiss的nprobe不是越大越好nprobe越大召回越准但越慢。我在100万规模的候选库上nprobe取10和取100的Recall10只差1.5个百分点但延迟差了五倍。工程上要找到那个甜蜜点别一味追求最高准确率。微信生态的多模态Embedding项目技术难点其实不在模型结构而在数据配准、难负样本和工程部署。只要你把这三块做实了哪怕是拿现成的Chinese-CLIP跑微调效果也能吊打那些模型结构复杂但数据粗糙的方案。做多模态检索这几年我最大的体感是把模型做复杂是容易的把数据做干净是难的把系统做稳定是更难的。如果你也在微信生态里折腾AI搜索这套思路可以直接套用祝顺利。
返回列表