1. 为什么微信业务场景必须自研多模态 Embedding
先说一个很多人容易忽略的点:微信生态里的内容形态,根本不是单一模态能搞定的。你在视频号看到一个美食教程,用户点进去之后看到的画面、听到的背景音乐、理解的旁白文案、甚至左下角挂的小程序链接,这四类信息分别属于图像、音频、文本、结构化行为数据。如果只用文本 Embedding 去召回这条视频,标题写得模糊一点,比如“三分钟搞定晚饭”,检索效果会立刻崩塌,因为“晚饭”对应的视觉特征向量压根没参与计算。
我早期也在通用 Embedding 模型上踩过坑。拿开源的 CLIP 或者 Chinese-CLIP 直接套到微信内容场景里,形式上行得通,但一到真实业务就露馅:通用模型对微信生态里高度口语化的表达、小程序封面图、带二维码的营销图、长图拼接的公众号头图,语义理解基本等于零。更致命的是,通用模型训练时用的是互联网图片-文本对,和微信里“用户实际点击的内容”分布差异巨大,导致召回的候选集里混入大量低质内容,后续排序模型再怎么调也救不回来。
所以正确的思路是自己训。不是从零训练一个视觉模型或者语言模型,而是基于预训练好的单模态基座,在一个大规模、微信生态内的多模态数据集上做对齐和微调。这样既保留了 base model 已有的语言能力和视觉能力,又能把两种模态映射到同一语义空间,让视频、图文、小程序这些异构内容可以用同一个向量体系去比较和检索。
这套方案落地之后能解决的问题非常直接:
- 视频号搜索从“文本找视频”升级成“视觉+文本联合找视频”;
- 朋友圈广告候选集可以用图片向量直接召回,不依赖广告主填的关键词;
- 小程序推荐可以融合截图视觉信息和页面文本信息做统一向量化;
- 公众号文章可以被图像向量命中,哪怕标题和正文跟搜索词没有一个字重叠。
下面我把从数据处理、模型结构、训练策略到工程部署的完整路径拆开讲,全程基于微信业务场景的真实约束来讨论,不是实验室里的玩具方案。
2. 微信生态内多模态训练数据的采集与清洗
2.1 数据源识别:哪些内容能构成训练对
训练多模态 Embedding 模型,第一件事不是选模型,而是搞数据。微信生态里天然存在大量隐式的“图文对齐”信号,关键看你有没有意识到这些信号的价值。
- 视频号:视频封面帧 + 视频标题/描述。这是最干净也最大量的数据源,一个经过用户点击和完播验证的标题,通常和视频画面语义高度相关。
- 公众号文章:文章头图 + 文章标题。头图经常是编辑精心挑选的,和标题相关性很强,但存在一部分头图是纯装饰、纯二维码的情况,清洗时要过滤。
- 朋友圈广告:广告外层素材图 + 广告文案。这是商业数据,点击率本身就包含质量信号,高点击的图文对相当于有了人工标注。
- 小程序:页面截图 + 页面标题。小程序页面截图质量参差不齐,需要额外的 OCR 过滤,但胜在量大,且覆盖很多长尾场景。
- 聊天场景中的表情包:表情图 + 表情名称/描述。这部分数据偏杂,但可以用来扩充模型对“夸张、搞笑、情绪化表达”的理解。
这里要强调一个原则:数据对的质量 > 数据对的绝对数量。和很多团队一开始追求“几十亿对”不同,我在实践中发现,把数据量从 10 亿降到 2 亿,把清洗规则做严格,训练出来的模型在业务评测集上反而提升显著。因为 Embedding 模型本质上是度量学习,脏数据里的错误对齐会让模型学到错误的距离关系,这种错误是非常难通过增加训练步数来修正的。
2.2 清洗链路:不能直接拿“标题+封面”就去训练
如果直接把视频封面和标题扔进模型训练,你会得到一堆让模型困惑的样本。微信生态内容有个显著特点:夸张封面、标题党、营销图泛滥。封面上一行大字写着“震惊!”,标题其实是讲养生小知识,视觉图和语义信息严重矛盾。模型反复看到这种对齐关系,学出来的向量空间必然是扭曲的。
我们的清洗链路分五步:
第一步,低质图过滤。用 CLIP 的 image encoder 给图片计算一个“清晰度得分”,配合传统的方差/边缘检测,把模糊图、纯色图、带大面积水印的图直接筛掉。微信场景里还有大量截图型封面,这类图信息密度高但视觉上很像垃圾图,需要保留,因为公众号头图大量是这个类型。
第二步,OCR 文字覆盖率检测。对图片做 OCR,识别出的文字区域面积超过图片面积 30% 的,大概率是文字海报图。这种图不是不能用,而是它的语义和文字标题天然重复,导致模型学到的视觉分支偏向把“文字识别”当成视觉特征,而不是去理解构图、物体、场景。我们对这类图单独建桶处理,不让它们污染主训练集。
第三步,文本侧去重。公众号标题、视频标题里大量存在“标题党模板”,比如“不看后悔系列”“30岁以后一定要知道的事”“再忙也要看”。这些文本没有具体语义指向,直接作为正样本会让模型把这些泛化短语和任意图片关联起来。对这类样本按模板聚类后降采样,保留一部分增强泛化即可。
第四步,图文相关性粗筛。这里用一个小技巧:先用一个现成的 CLIP 模型给图文对打分,把得分极低的配对删掉。注意只是筛掉得分极低的那部分,比如相似度低于阈值的样本,而不是只保留高分样本,否则会引入选择偏差,训出来的模型只能处理“本来就高度相关”的内容,对真实场景中低相关性内容的判别力会变差。
第五步,用户行为信号加权。清洗完的数据并非同等重要。视频号里完播率超过 60% 的图文对,权重可以设为 2;被用户主动收藏、转发的对,权重设为 3;只被曝光但滑走的对,权重降到 0.5。这个按行为信号加权的样本采样策略,在实践中比单纯堆数据量有效得多。
2.3 组批次的艺术:不要让训练数据“太简单”
训练对比学习模型时,batch 内部的负样本分布决定了下限。微信场景里的难点在于:很多视频标题和封面看起来很像,但其实是完全不同的内容。比如“北京美食探店”这个标题,可能对应几百条不同餐厅探店的封面图,如果它们恰好出现在同一个 batch 里,模型会把它们当成互相的负样本,这其实是合理的——因为图文对虽然标题相似,但视觉特征应该区分开。
但如果整个 batch 都是“标题相同、封面不同”的样本,梯度就会在“拉近标题与封面”和“推开标题与不同类型封面”之间剧烈振荡,导致 loss 波动大、收敛慢。我建议在组 batch 时做一层哈希约束:保证同一个 batch 内,相同标题模板的样本数量占比不超过 20%,同时尽量让同一视频的不同截帧不要进入同一个 batch,这些细节对训练稳定性帮助非常明显。
3. 模型结构选型:双塔与单塔融合方案的取舍
3.1 微信场景的检索需求决定了结构
多模态 Embedding 模型的结构选择,首先不取决于你对多模态融合算法的偏好,而取决于线上系统的工程约束。微信的召回链路,无论是搜索、推荐还是广告,都对延迟极其敏感。一次用户请求,通常是毫秒级就要从千万甚至亿级候选池里捞出几百条候选。
这就要求我们必须走双塔或者多塔结构:文本塔负责把 query/标题编码成向量,视觉塔负责把图片/视频帧编码成另一个向量,两者都落到同一向量空间,线上用向量检索完成召回。如果采用单塔交叉注意力结构,比如把文本和图片 concat 后过一层跨模态 Transformer,效果确实更好,但推理时要对每个候选图文对单独过一遍模型,这个计算量在微信的量级上完全不现实。
所以我们的最终结构是:
- 文本塔:基于一个 6 层左右的 BERT 变体(微调自预训练中文 RoBERTa),输出 768 维文本向量;
- 视觉塔:基于 ViT-B/16 或者 Swin-T 结构,输出也是 768 维向量;
- 投影层:各自接一个 MLP,把向量从 768 维压到 256 维,做 L2 归一化后计算余弦相似度。
压缩到 256 维是工程上的一个关键取舍。768 维向量做内积检索,单机 QPS 大概只有 256 维的六成不到,而 256 维在召回任务的精度损失基本可以控制在 1% 以内。对微信这种体量的场景来说,用 5% 的召回精度换 40% 的性能提升,完全划算。
3.2 双塔的“不对称”设计细节
很多人做双塔时犯的错是让两个塔完全对称,这可能带来几个问题。我们踩坑之后做了一个不对称设计:文本塔更宽(hidden size 768),视觉塔更窄(hidden size 384)。原因很简单:微信生态里的文本,尤其是搜索 query,往往非常短且碎片化,“烤肠机价格”“附近打印店”这种词,语义信息的密度极低,需要更宽的文本塔才能捕捉上下文关系和意图。而视觉塔对应的图片,语义密度天然较高,一个物体、一个场景、一种构图,信息都在像素里,不需要过宽的隐层就能提取到足够的判别性特征。
另外,文本塔和视觉塔的初始化方式也做了差异化:文本塔完全用中文 RoBERTa 的权重初始化,视觉塔则先用 CLIP 的 image encoder 权重做 5 个 epoch 的热启动,再进入联合训练。这样做的理由是:视觉特征的跨域迁移性比文本更强,CLIP 在互联网图片上学到的通用视觉先验可以直接用起来;而文本侧中文生态词表差异太大,直接用开源权重继续预训练反而更好。
3.3 为什么不直接上“三塔”融合
微信生态里音频也很重要,比如视频号的背景音乐、语音消息的转写文本。于是我们最初设想过三塔结构(文本塔、图像塔、音频塔),把三者的向量 concat 到一起作为最终表示。但实测下来发现收益极低,音频塔的存在反而拉低了整体效果。
问题出在模态对齐的复杂度上。图文对的语义关系是直接的,而音频和文本、音频和图像之间的相关性,在微信场景里非常弱。一条视频的背景音乐可能和内容毫无关联(比如剪辑统一配的流行音乐),这时候强行拉近音频向量和图像向量,等于往模型里注入噪声。最后我们干脆放弃了音频塔,只在视频场景用关键帧处理音频相关的特征,单独走一个轻量级音频分类模型,不参与多模态 Embedding 主链路。
所以我的经验是:多模态不是模态越多越好,而是每一种模态都要有真实、稳定、可学习的对齐信号再去加塔。否则加一个模态的塔,等于多一个噪声源。
4. 训练策略:对比学习的几个关键细节
4.1 Loss 函数和困难样本挖掘
我们在训练中采用的是 InfoNCE 对比损失,这也是 CLIP 系列模型的标配。核心思路是:一个 batch 里有 N 个图文对,对每个文本,把正确配对的图片当正样本,把其他 N-1 张图片当负样本,计算交叉熵损失;反过来对每张图片也做同样的操作。整体 loss 是图文两个方向的平均。
但纯 InfoNCE 有个问题:batch 内的负样本通常是“简单负样本”,模型学一阵子之后,对这些样本的区分能力已经很强,loss 下降开始变慢。微信业务场景里真正难判别的是那些“看起来很像但实际不同的内容”——比如两个不同品牌的手机评测视频,封面都是手机局部特写,标题都带“评测”字样,这种 pair 模型最容易搞混。
因此我们做了两件事:
第一,在 batch 内增加人工构造的难负样本。对每条视频,从同类别(同一视频号博主的其他视频、相同话题标签的视频)里随机抽取 3-5 条作为额外负样本,加进 loss 计算中。这样模型被迫在一个更小的语义距离内做判别,学出来的向量空间会更加紧凑。
第二,使用“困难负样本挖掘”的在线版本。每训练几步,用当前模型对一小批候选样本做向量检索,把相似度高但确实不是一对的样本挑出来,作为难负样本加入下一轮的训练数据。这个策略一开始我们不敢用,因为容易导致模型坍缩——在训练初期模型还比较弱,选出来的“难负样本”可能本身就是对的配对。后来加了保护机制:只在训练进行到 60% 之后开启难负样本挖掘,并且选出的负样本加入后还需要人工抽检,确认确实是不相关的内容,才允许进入训练集。
4.2 温度系数和 FLOPS 约束
温度系数是对比学习里最敏感的超参数之一。温度设得太大,模型对所有负样本一视同仁,学不到细微差别;温度设得太小,模型只关注最难的那一个负样本,训练不稳定,loss 震荡剧烈。我们最终的设定是 0.05,这个值在很多对比学习工作中都被验证过效果不错。训练初期可以先用 0.1 跑 2 个 epoch 进行预热,让模型先收敛到一个合理的区域,再调低温度做精细化对齐,这个改动让最终评测指标提升了大约 2 个点。
还有一个小技巧是 FLOPS 约束正则项。这个不是微信场景特有的,但对我们的内容生态特别有用:在计算 loss 时同时计算一个“负样本相似度均值”的正则项,惩罚那些把全局所有向量都拉近的行为。因为公众号和视频号内容存在明显的头部效应,爆款内容和长尾内容的特征分布差异巨大,如果不加约束,模型倾向于把所有向量都映射到一个很小的区域内(反正 loss 也能降),导致召回的多样性极差。加了这个正则之后,至少保证头部内容和长尾内容在向量空间里能被分开。
4.3 数据并行和梯度同步的几个坑
双塔模型的训练和单模型训练不一样。两个塔各有独立的参数,但 loss 是两者联合计算的,这导致反向传播的时候,两个塔的梯度其实是相互依赖的。如果用两个 GPU 分别放两个塔,做梯度同步时必须保证同步的是“联合 loss 对各自参数的梯度”,而不是各自 loss 的梯度。这个细节如果搞错,训练出来的模型表现为“单塔效果正常,但图文检索完全不可用”。
我们最终采用的方案是单卡放双塔,通过数据并行做多卡扩展。具体来说,每张卡上都有一个完整的双塔副本,各自处理一批数据,前向传播结束后把所有卡上的特征向量 gather 到一起组成一个大 batch,再统一计算 loss 和梯度。这样虽然显存开销更大(每张卡都要存两份模型),但逻辑简单,训练稳定。实际用 8 张 A100 训练,单卡 batch size 设为 512,gather 之后等效 batch size 达到 4096,效果已经足够好。
这个环节我还想多说一句:训练框架层面的问题往往比模型结构的问题更难排查。我们的模型在一次升级版本后,文本塔和视觉塔的 BN 层行为异常,排查了很久才发现是框架在数据并行时对 BN 的 sync 设置不对,导致视觉塔的每卡 BN 统计量没有同步。如果你的训练过程中出现“loss 降了但评测不涨”的诡异情况,优先检查归一化层的跨卡同步逻辑。
5. 基于微信业务的评测体系设计
5.1 通用的图文检索指标为什么“不够用”
很多团队训完多模态 Embedding 模型,随手用 Recall@10、Recall@100 这些通用指标评估一轮就上线了,这个做法在微信场景里是不够严谨的。通用图文检索指标考察的是“给定一个 query 文本,是否能从候选图集里召回正确图”,但微信场景里的实际业务需求往往是反向的“给定一个候选内容(图/视频/小程序),是否能在用户搜索词中命中”,以及更复杂的“给定一个用户的浏览行为序列,能否召回相似内容用于推荐”。
我们建了三个维度的评测集,每个维度对应一条业务线:
第一,搜索维度。从真实用户搜索日志中抽样 query,用模型召回视频号和公众号的内容,由标注员判断前 10 条结果是否相关。这块的评估指标用的是 Recall@10 加人工相关性打分,核心观察点是长尾 query 的召回质量。
第二,推荐维度。从视频号的推荐系统中抽取曝光但未点击、和曝光且点击的样本对,用模型计算向量相似度,看“点击正样本对”的相似度是否显著高于未点击负样本对。这里用 AUC 作为核心指标,更贴近推荐的排序目标。
第三,去重维度。微信生态里大量存在“重复内容”:同一个视频被不同博主剪辑重复发布、同一篇文章被多个公众号转载但改了头图。我们的模型一开始在这块表现很差,因为它会把内容完全不同的向量映射得很接近。后来在训练数据里专门加了“重复内容对”作为额外负样本,才逐步改善。这个维度的评测方式是抽一批已知重复内容对,看模型给的相似度分数是否够高,从而能否触发去重规则。
5.2 人工评测集的构建与迭代闭环
如果完全依赖自动指标,很容易被“数据泄漏”欺骗。我们遇到过的情况是:训练数据里有大量公众号文章标题和头图,而评测数据也来自公众号,导致模型在评测集上 Recall@10 高达 90%,但上线后真实搜索效果一塌糊涂。排查后发现,模型根本没有学到图文语义的对齐,而是死记硬背了“哪些标题对应哪些文章”这种 ID 级别的映射,训练集里出现过的标题一旦换个图,模型就认不出来了。
解决方法是建一个“时间隔离评测集”。用每天新增的数据做评测,训练数据只用过去 7 天前的内容,保证评测集里的图文对从来没在训练时出现过。这样虽然评测难度更大、指标数字更难看好,但更接近真实的线上分布。
这个评测集不是一成不变的。我们每两周人工审一批模型犯错的 case,把典型错误归纳成规则,反馈到数据清洗和训练策略里。比如有一轮我们发现模型对“带超大文字的封面图”特别容易误判,把几个完全不相关的标题都召回出来,就是因为 OCR 过滤的阈值设得太狠,导致模型没见过这种图,理解不了图和文字的关系。后来把 OCR 过滤策略改成“文字面积大于 30% 但小于 50% 的图保留进训练集”,模型的表现立刻得到提升。
5.3 向量相似度的业务校准
模型训完之后,不能直接把余弦相似度得分当质量分用。微信场景里不同业务对“相似度得分”的校准需求完全不同:搜索场景要求严格(只有真正的强相关才能进前 10),推荐场景要求宽松(相关即可,不要求强相关)。同一个阈值不可能同时满足两条业务线。
我们的做法是训练完之后,额外用一个小型的温度缩放层,针对不同业务线单独校准。具体实现是收集该业务线上的人工标注数据,学习一个标量温度参数,把向量相似度得分映射到 0-1 区间的“业务相关概率”。这一步成本极低(只需要拟合一个参数),但对业务效果提升非常明显。
6. 向量检索上线与持续迭代的实战经验
6.1 亿级向量索引选型:从 FAISS 到 Milvus
模型训练出来只是第一步,真正难的是把它上线到召回链路里。微信生态的候选池是亿级甚至十亿级的,要让模型产出的 256 维向量在这个规模下做毫秒级近邻检索,工程上需要仔细打磨。
早期我们直接用 FAISS 建 IVF-PQ 索引。IVF 负责把向量空间划分为若干个 cell,PQ 负责把向量压缩成低比特表示。这套方案的优势是纯 CPU 内存就能跑,部署简单,但在亿级规模下的召回质量损耗比较明显。特别是在微信内容生态里,长尾内容多,PQ 压缩会显著降低长尾向量的区分度,导致长尾内容召回率偏低。
后来迁移到 Milvus,使用 IVF_SQ8 和 HNSW 混合索引。策略是这样:头部高频内容的向量(大概是总量的 5%)用 HNSW 索引,保证召回精度;长尾内容默认用 IVF_SQ8,牺牲一点精度换取内存效率。这样既保证了推荐头部内容的效果,也不至于让内存爆掉。这套混合索引跑下来的体感是:单机 8 核 64G 内存可以承载 5000 万级别的向量,单次查询延迟在 3-5ms,召回质量相比纯 IVF-PQ 提升了大约 3 个百分点。
6.2 向量版本管理与回滚机制
多模态 Embedding 模型的迭代和常规模型的迭代有个很大区别:向量一旦算完并导入索引,如果模型要换版本,是没法原地更新的。必须用新模型重新算一遍所有候选内容的向量,再重新建索引。在微信的场景下,全量重算一次亿级内容的向量大概需要 2000 卡时的算力和十几个小时的在线任务。
我们为此建立了一套“双通道发布”机制。新模型训练完毕,先在离线把全量向量算好、建好索引,同时保持旧模型产出的索引在线服务。新索引在灰度环境先跑一周,实时对比新旧两个索引的线上召回指标和业务指标。只有确认新索引在各项指标上都不低于旧版,才切换线上流量,实现平滑过渡。
这个环节最常见的坑是“向量空间漂移”。新模型输出的向量分布和旧模型不一定一致,就算同一条内容,新旧模型的向量也存在偏差。直接对比新旧索引的召回结果意义不大,因为分数体系都变了。我们的方法是保留一份固定的“锚定测试集”,每次模型升级,都用这份测试集重新计算新模型的向量,和旧模型向量做对比,确保新模型没有在特定子集上出现明显的定向偏移。
6.3 持续迭代的数据回收机制
最后说一下模型上线之后怎么持续迭代。很多团队训完模型就完事了,后续只是在固定数据集上调参,这其实是把训练好的模型白白浪费掉。更好的方式是建立“模型-业务-数据”的闭环:从线上每一条用户反馈(点击、完播、搜索无结果、举报)中回收信号,重新加工成训练样本,进入下一轮模型的训练数据。
微信生态里最有价值的数据回收信号是“搜索无结果/低点击”。用户搜索一个词,召回出来的视频被大量滑走、不点击,说明这一条内容虽然是检索命中了,但语义上并不被用户认可。这类负反馈数据收集到一定量级,加入训练集后,模型对于这类模糊语义的辨识力会大幅提升。实测下来,每引入 500 万级别的真实负反馈样本,下一版模型的 R@10 大约能提升 1.5-2 个百分点,比单纯在原有数据集上加大训练步数划算得多。
还有一个小技巧:给训练数据打上“数据来源时间戳”。因为微信生态的内容偏好变化很快(比如某个梗突然火了),如果不加时间戳,旧数据会持续干扰新模型的训练,导致模型对“当前内容热点”的反应滞后。我们在训练时按时间衰减采样,近 7 天的数据权重是 1.0,30 天前的数据权重衰减到 0.3,90 天前的数据直接不再进入训练集。这个改动让模型对热点内容的召回表现明显提升。
7. 几个我踩过的坑和最终建议
这套系统从最早的原型验证到稳定线上运行,花了大半年时间,中间踩过的坑比上面写到的还要多。挑几个对别人最有参考价值的再说一下。
第一个坑是要不要自己训多模态模型。如果你在微信生态里做内容理解,至少不要让通用模型直接对接业务,用一个百万级的高质量业务图文对做微调,成本不高但效果提升显著。如果连微调的算力都没有,那至少要保证训练数据的质量,比如用行为信号筛选出的高置信度样本,而不是随便拿互联网开源的图文对凑数。
第二个坑是千万不要把“模型效果提升”直接等同于“业务指标提升”。向量召回只是整个链路的开头,后面的粗排、精排、重排同样重要。我们有一版模型在离线评测上 R@10 提升了 5 个点,看起来非常亮眼,结果线上推荐业务指标基本没动,因为召回的候选里真正能贡献 PV 的头部内容早就被旧模型稳定召回,新增的 5% 长尾召回对线上大盘影响微乎其微。后来我们把评测目标改成“新增有效召回量”(被下游排序接受且曝光点击的候选),模型的迭代方向才终于对齐业务目标。
第三个坑是团队协作问题。多模态 Embedding 模型不是纯算法团队能独立搞定的,它需要数据团队(提供用户反馈日志)、搜索/推荐工程团队(提供检索链路和 embedding 上线能力)、评测团队(提供标注平台和质量评估)紧密配合。如果组织架构上这三拨人离得太远,项目很容易在数据口径和线上接入上反复扯皮,这个问题比任何技术难点都更拖慢节奏。
最后说点方法论层面的建议。多模态 Embedding 模型本质上是在做“语义对齐”,但微信生态的语义空间不是静态的,它会随着内容供给、用户偏好和产品功能的变化持续移动。所以这个系统真正要建的不是一个模型,而是一套能够持续吸收业务反馈、不断调整对齐方向的机制。模型结构、训练策略、工程架构都只是这套机制的执行器,真正驱动整个系统迭代的,是你对微信内容生态里“什么内容相似,什么内容互斥”这个问题一版比一版更精准的理解。
我自己的体会是,多模态 Embedding 的建模难度不在某个单点技术上的突破,而在于把所有环节——数据感知、清洗策略、模型训练、评测校准、检索部署——捏成一个不断螺旋上升的闭环。这个闭环一旦转起来,模型的能力会随着每次数据回收和业务反馈变得越来越强,这才是训练一个真正可用的多模态 Embedding 模型最核心的秘密。