做图文检索的第一版,我们把能抓到的图文对都灌了进去。离线指标一般,线上更差。后来狠心砍掉一批样本,检索结果反而好了一截。
那次改动让我调整了对整条链路的排序:模型结构基本定型之后,剩下的差别几乎都在数据和索引上。
图文对齐是体力活
真实数据里的噪声比想象中朴素。图片太小、长宽比极端、带大幅水印,文本侧是“点击查看大图”“IMG_2034”这类占位符。这些用规则就能筛掉,成本很低,收益很直接。
麻烦的是“看起来对但实际不对”的样本。电商图配的是营销文案,图里是模特,文字讲的是满减;新闻图配的图注讲的是另一件事。这类样本会把两个不相关的向量硬拉近,训练时表现为 loss 降不下去,或者降下去但检索结果飘。
我一般叠三层校验。OCR 提取图内文字,和文本描述比对,重合度太低就丢;用一个小的图文匹配模型算相似度,低于阈值就丢;最后用感知哈希或图像向量去重,避免同一张图重复主导梯度。三层都是可调的,阈值不能抄,要拿一批人工看过的样本校准。
垂直领域还要更严。通用模型打分分不出“红色连衣裙”和“蓝色连衣裙”的差别,它觉得都挺像。这种时候要靠属性检测或者目标检测,强制要求文本里的关键属性在图中得到验证。代价是数据量会掉得很厉害,有时掉一半以上。这件事在汇报里不好看,但比训练一个语义空间混乱的模型划算。
清洗的目标不是留下更多,是留下更一致。
数据配比也值得单独看。通用数据给模型世界知识,垂类数据给它业务判断,两者的比例会直接影响线上表现。全是通用数据,模型认不出自家商品;全是垂类数据,它对没见过的表达又很脆。我通常保留一个较大的通用底座,再用垂类数据做后段训练,比例靠评测集试,不靠直觉。
还有一个容易漏的环节是文本侧的质量。图注是机器翻译过来的、带大量标签堆砌、或者把商品参数直接拼成一句话,这些文本本身就不像人在描述图片。用它们训练,模型学到的是关键词共现,不是语义对应。
数据规模到这个阶段反而不是最难的。几十万条精选样本,足够把检索做到可用;几千万条未清洗的数据,只会让训练更慢、评估更难解释。我现在的顺序是先做小规模高质量闭环,确认整条链路能出效果,再考虑扩量。扩量之前先扩评测集,否则涨了也不知道为什么涨。
评测集要覆盖两类问题:一类是常见查询,保证主流体验;一类是容易混淆的样本,比如同款不同色、同系列不同代。后一类样本很少,但它决定了系统在关键场景下是否可信。标注这些样本的成本不低,不过比事后从用户投诉里反推要便宜。
负采样最怕假负样本
对比学习靠负样本来定义边界。批次内负采样是最省事的做法,同一批里其他样本的文本都当成当前图片的负样本,不需要额外检索,算力复用。
问题是批次不够大的时候,负样本太好区分。模型学到的边界很粗,遇到“哈士奇”和“阿拉斯加”这种细粒度差异就失效。
困难负样本能解决这个问题,但它有一个很隐蔽的坑:假负样本。海量互联网数据里,两张不同角度的同一座塔可能被互相当成负样本惩罚。模型的语义空间会被这种错误的惩罚拉歪,而且从 loss 曲线上不一定看得出来。表现出来是检索结果“有点怪”,相似的东西不一定靠近,不相似的东西偶尔很近。
我的处理是取“比较像但不是最像”的那一档。在向量空间里选距离较近但排除了最近邻的样本,避开那些可能是同一物体的重复项。再保守一点,用教师模型给软标签,告诉模型这个负样本不该被惩罚得太重。
训练节奏上,前期用简单的批内负样本让它先收敛,后期再逐渐掺入困难样本。一上来就上最难的,很多模型直接训崩。这个顺序看起来像技巧,实际是在控制梯度的难度,和人学习的节奏差不多。
批次大小和显存是一对老矛盾。对比学习需要大批次,因为负样本数量和梯度方差直接相关;但显存装不下。梯度累积解决不了这个问题,因为负样本必须在同一次前向里可见。所以要动量队列,把历史特征存下来,解耦批次和负样本数量;要么接受小批次,用更强的数据增强和难负样本弥补。
这两种选择各有代价。动量队列引入额外的超参和特征滞后,小批次则限制了上限。我倾向于先小批次跑通,确认数据没问题,再考虑加队列。数据脏的时候,加多少工程手段都是在放大噪声。
负采样的策略还跟任务有关。以图搜图时,负样本应该是“不同的图”,而不是“不同的商品描述”;反过来,文本检索图片时,重点在语义层面的区分。用同一套采样配置应付两种任务,通常只有一种能跑好。
训练目标也要和线上指标对齐。离线看的是批内分类准确率,线上看的是前几条里有没有正确答案,这两个东西不完全一致。模型学会了在几千个候选里挑出唯一正确的那一个,不代表它在近似搜索返回的几百条里排序合理。所以我会在训练后单独做一次检索评测,而不是只看 loss。
温度系数是个很敏感的旋钮。它控制 softmax 的陡峭程度,决定模型有多关注最难区分的那个负样本。调得太小,分布接近 one-hot,梯度几乎只来自一个样本,训练震荡;调得太大,分布被抹平,正负样本的差异消失,学不到判别性特征。
多数实现把它设成可学习参数,让模型自己找平衡点。这个做法有效,但也意味着它对初始化和数据分布敏感。我在这上面吃过亏:换数据集之后沿用旧配置,训练前期看起来正常,后期指标就是上不去。后来养成习惯,任何迁移都先把温度和学习率一起重扫。
它还会影响检索时的相似度分布。训练用的温度和线上算相似度用的温度不一致,排序会整体偏移,阈值也失去意义。所以我把温度写进模型配置一起发布,不单独改。
双塔召回,单塔精排
双塔把图像和文本各自编码,最后算一次余弦相似度。好处是文档侧可以离线算好,建索引,在线只算查询向量,一次近似最近邻搜索就能从上亿条里捞出候选。
单塔把图文拼在一起过交叉注意力,能捕捉“哪个词对应图中哪个区域”这种细粒度关系,精度高,但每一对都要实时推理,不可能扫全库。
所以这两者不是竞争关系,是流水线的两段。双塔负责召回,单塔负责在几百条候选里精排。真正的问题是候选质量:召回阶段漏掉的,精排再强也捞不回来。
查询侧也有工程活。用户输入“红色连衣裙 长款”,文本塔要能理解这是一个带属性的需求,而不是三个独立的词。有些场景还需要把查询扩写成更完整的描述,或者抽取属性做结构化过滤,这些都要和检索链路配合。查询扩写能提召回,但扩写错了会把结果带偏,所以只在短查询上开。
单塔重排的成本要控。候选数量从几百降到几十,延迟就降一个量级;批大小和序列长度也直接影响显存占用。我一般固定重排的候选数,把它当成一个约束,而不是让召回自由膨胀再指望重排兜底。候选太多,重排本身就会超时,最后退化成截断。
从模型演进看,后来的工作基本都在补双塔的交互不足。早期是在编码后加融合层,再往后是把对比学习和生成目标放在一起训练,让模型既会判别也会描述。这些改进在离线指标上都有效,但工程上要不要跟,取决于业务对细粒度差异的敏感程度。商品检索对颜色、款式很敏感,值得;通用图库检索,双塔加轻量重排往往已经够。
双塔有一个绕不开的现象:图像和文本的向量分布往往落在空间里两个不同的锥形区域,即使语义匹配,余弦相似度也偏低。缓解手段不外乎投影层、可学习的温度、更难的负样本,都是工程上的补丁,没法彻底消除。知道它存在,比假装它不存在更有用:调阈值的时候要按模态分别看分布,不要用一个全局数字。
双塔买的是可索引,单塔买的是判断力。
选型时我会先问一句:这个场景的候选规模有多大。几千条的库,单塔直接扫也许更简单,少一套索引,少一份维护;上亿条的库,没有双塔就没有可行性。中间的规模最尴尬,两种方案都能跑,要靠延迟预算和团队能力定。
索引和模型一样需要灰度
向量索引这块,图索引和倒排聚类的取舍很直接。图索引查询快、召回高,代价是内存,结构要常驻,构建也慢;倒排配合量化省内存,适合海量数据,召回低一些,而且对聚类参数敏感。
调参的直觉是:想提召回,就扩大搜索范围,代价是延迟线性上升。线上真正难的是在延迟预算内找到那个点,而不是把召回刷到最高。刷到最高往往意味着每次都扫遍全库,成本不可持续。
我见过最常见的事故不是选错索引,是过滤和搜索的顺序搞反。先搜索再按价格过滤,可能前一千条全被过滤掉,返回空结果;先过滤再搜索,过滤后的子集太小,图结构的连通性退化,性能反而更差。多数数据库内部做了优化,但极端分布下还是要有兜底策略,比如过滤率极高时直接退化成暴力搜索。
真正麻烦的是模型升级。换模型意味着向量空间变了,新旧向量不兼容。用新模型编码的查询,在旧索引里召回不出正确结果。全量重刷在亿级数据上要跑很久,期间服务不能停。
我的做法是双索引灰度:后台用新模型回刷存量数据,构建第二套索引;构建期间新增数据双写;建好之后切一小部分流量过去,看召回和业务指标;确认没问题再全量切,最后下线旧索引。这套流程不新鲜,就是蓝绿发布。麻烦的是它要求索引层从一开始就支持多版本共存,很多项目是事后才想起来的。
还有一个更省事的思路是在训练阶段加兼容性约束,让新模型的向量空间尽量贴近旧的,上线初期可以直接复用旧索引。这属于把运维问题前移到训练,值不值得看团队构成。对小团队来说,一次性把索引做对,比维护两套系统便宜。
索引构建本身的工程细节也不少。大规模建库通常要并行,分片生成再合并,中间会占大量临时空间。构建期间查询性能也会受影响,所以我一般安排在低峰期,或者用独立的构建集群,建完再把索引文件挂上去。这些事不写进设计文档,上线时就会撞上。
评测指标要分层。召回阶段看的是正确答案有没有进前若干条,重排阶段看的是它排在第几位。两个数字一起变好才算真的变好;只提升召回,可能是候选变多带来的假象;只提升排序,可能是把长尾样本牺牲掉了。
线上还要看空结果率。检索系统最容易被忽略的失败是“什么都没返回”,用户不会投诉,只会离开。这个指标比平均准确率更能反映真实体验。
维护成本最后会落在一件很朴素的事上:这套东西换一个人能不能接手。数据管线的每一步有没有文档,索引怎么重建,模型怎么回滚,评测集放在哪。我见过效果不错但没人敢动的系统,那种系统在人员变动之后基本等于重做。所以我现在评估方案时,会把“交接成本”单独列一项,和延迟、准确率放在一起看。
量化之后的验证我也比较谨慎。只看整体准确率不够,量化最先伤害的是长尾样本。所以除了对比输出的分布偏移,还要单独准备一个难例集跑回归,确认边缘 case 没有突然崩掉。业务指标的账也要算清楚:延迟降了,但点击率掉了,省下的算力钱够不够补这个损失。
算力账还要算上索引常驻的那部分。向量索引占的内存不产生直接收益,但它决定了单机能承载多少数据。为了省机器把索引压得太狠,召回掉了,最后还是要靠更大的模型或者更多的人工兜底,那笔钱更贵。
排查 bad case 的时候,我按固定顺序走:先看文本编码有没有理解错实体,再看图像编码有没有认错物体,然后看两个空间的相似度分布是否正常,最后才怀疑索引参数。这个顺序能挡掉大部分误判,因为索引问题往往表现为整体召回下降,而不是某一个查询特别离谱。
到现在我仍然觉得多模态比纯文本难维护。原因不在模型,在于每一步都有多种输入要对齐,出问题时很难用一句话说清是谁的错。
评论