做过直播电商技术的人,大概都遇到过这种场面:一场四小时的带货直播结束,运营拿着回放截图来找你,问“主播手里这件米白色的,挂的到底是哪个链接”。你去翻后台,发现那段时间窗里挂了三个商品,主播换了两次衣服,画面里还并排摆着两件同款不同色的样品。最后靠人工一帧一帧对,对完已经过去两个小时。这类问题的本质不是“识别不准”,而是商品在直播画面里没有被赋予一个稳定、可被机器核对的视觉身份。我把它叫做 JVID,全称是商品视觉身份系统,说白了就是给每一个在镜头前出现过的商品发一张带照片的身份证,让系统能回答“这一帧里的这个东西,对应商品库里的哪一条记录”。这篇文章拆的就是这套东西怎么搭、哪些地方会翻车、哪些参数是需要算而不是需要调的。适合正在做直播电商中台、内容理解、商品挂链归因的同行看,也适合刚接手这类需求、还没想清楚从哪儿下手的朋友。
1. 直播间里的“这件是哪个链接”,为什么值得单独做一套视觉身份
1.1 挂错链、挂重链、挂不上链:三种每天都在发生的损耗
先把这个需求的业务价值说清楚,不然很容易被当成一个“炫技的 CV 项目”。直播场景里商品和画面之间的绑定关系,目前主流做法还是人工操作——运营在后台点击挂链,系统记录一个时间窗口,然后把这段时间窗内的商品展示归因给这个链接。这套机制的脆弱之处在于,它默认“运营点的时刻”和“主播举起的时刻”是一致的,而现实中这两个时刻经常差十几秒甚至几十秒。
我梳理过实际产生的损耗,大概分三类。第一类是挂错链,主播手里拿的是 A 款,后台挂的是 B 款,用户点进去发现颜色不对,退货率和客诉都上来了。第二类是挂重链,同一个商品在短时间内被反复挂载,或者两个链接指向同一个 SKU,导致流量分散、数据口径混乱。第三类是挂不上链,主播临时拿起一个样品讲了两分钟,运营没反应过来,这两分钟的成交就归因不到任何商品上,商品侧的数据是缺失的。
这三类损耗加起来,对一场中大型直播来说,影响的是整场 GMV 归因的可信度。而归因不准,后面所有的选品分析、主播话术评估、投放复盘都建立在沙子上。所以 JVID 要解决的核心问题非常具体:在不需要运营手动干预的前提下,把画面里的商品和商品库里的记录对上,并且给出一个可解释的置信度。
1.2 JVID 到底定义了哪些字段:把商品变成可被机器核对的对象
很多人一上来就想做“以图搜图”,这是把问题想简单了。以图搜图解决的是“这张图和那张图像不像”,而 JVID 要解决的是“这件商品在这个场次里的身份状态是什么”。我在设计这套系统的时候,把一条 JVID 记录拆成了三层字段,这三层缺一层,后面就会出问题。
| 层级 | 字段举例 | 作用 | 缺失后果 |
|---|---|---|---|
| 视觉层 | 主图特征向量、多角度特征向量组、特征版本号 | 支撑向量检索召回 | 无法做自动匹配,只能人工 |
| 属性层 | 类目、颜色、材质、款式关键词、规格 | 支撑细粒度区分和结果解释 | 同款不同色无法区分 |
| 关系层 | 所属场次、首次出现时间、持续时长、绑定主播、来源标记 | 支撑归因和时序证据 | 无法回溯,无法做投票 |
视觉层是大家都会做的,属性层是最容易被偷懒的,关系层是最容易被忽略但价值最高的。我见过一个团队,特征提取做得相当不错,召回率能到 90% 以上,但因为没有关系层的“持续时长”字段,系统没办法判断“这个匹配结果是主播举着讲了 40 秒的主推品,还是背景货架上扫过去的一个摆件”。结果就是高置信度的误匹配大量出现,因为背景里的商品也被识别出来了,而且特征和主推品几乎一样。
关系层的字段设计有个小技巧:不要只记录“出现了”,要记录“出现的形态”。比如同一个 JVID 可能在一场直播里出现五次,第一次是主播手持近景,第二次是放在桌面中景,第三次是背景远景,这三者的匹配置信度天然不同。把出现形态作为一个枚举字段记下来,后面做加权投票的时候非常有用。这个字段是我在被误匹配坑了两个月之后才补上的,补上之后,同样的模型,误挂率直接降了将近四成。
1.3 为什么不能只靠商品标题和口播文本
有人会问,现在语音识别这么成熟,主播不是会念商品名吗,直接做 ASR 加文本匹配不就行了。这个思路在小类目里确实能跑通,但放大到全类目就有明显天花板。
第一个问题是主播不一定念全称。卖服装的主播会说“这个雾霾蓝的”,卖食品的会说“这个上次断货那个”,这些表述和商品库里的标准标题重合度很低。第二个问题是同款商品的标题高度同质,一个类目下可能有几百条标题都是“XX 同款宽松显瘦针织衫”,光靠文本根本区分不出颜色和版型。第三个问题是口播和画面有时间差,主播经常是先举起来展示,讲完一段才说名字,或者反过来先预告再拿出来。
所以我的判断是:文本信号一定要用,但只能作为辅助证据,不能作为主链路。主链路必须是视觉,辅助链路是文本和时序,最后做加权融合。这个主次关系一旦搞反,系统就会变成一个“昂贵的语音搜索”。
2. 从一帧画面到一个可检索的身份:JVID 的链路拆解
2.1 采集与抽帧:间隔怎么定,为什么要留“关键帧冗余”
整条链路的第一步是抽帧,这一步看起来最没有技术含量,实际上决定了后面所有环节的成本上限。直播流一般是 25 到 30 帧每秒,全量处理是不现实的,必须抽帧。抽帧间隔怎么定?我给一个可以套用的算法。
先估算商品的平均展示时长。拿历史数据统计一下,主播拿起一件商品从入镜到出镜,中位数大概是多少秒。假设是 30 秒。再确定你希望在这段时间里采样多少次,这个数字取决于商品的运动剧烈程度,手持展示我一般取 8 到 12 次,因为要覆盖不同的角度和姿态。那么抽帧间隔就是 30 除以 10,等于 3 秒。
但这只是基线,实际不能这么均匀。我的做法是双通道抽帧:一路是按固定间隔的低频抽帧,比如每 2 秒一帧,用于全局扫描;另一路是触发式抽帧,当检测模块发现画面中出现新的大面积物体、或者画面发生剧烈变化(比如主播突然举起东西、镜头推近)时,临时提高抽帧频率到每 0.3 秒一帧,持续 3 到 5 秒。这样做的理由是,商品展示的信息密度集中在入镜的头几秒,那几秒里角度最多、最清晰,把算力堆在那里性价比最高。
算一笔账:一场四小时的直播,均匀抽帧按每 2 秒一帧是 7200 帧。检测环节如果单帧耗时 20 毫秒(用轻量模型加批处理可以做到),单卡推理总耗时约 144 秒,四小时的直播内容几分钟就能处理完,这是可以接受的。但如果抽帧间隔改成 0.5 秒,帧数变成 28800 帧,耗时就翻四倍,而且后面特征提取和向量检索的开销同步翻倍。所以抽帧策略是成本控制的第一道闸门,先把它定下来,再谈模型优化。
还有一个细节:保留关键帧的原始分辨率。检测和跟踪可以用降采样的画面跑,又快又稳,但特征提取一定要用高分辨率裁剪图。我试过用 640 分辨率的裁剪图做特征,同款不同色的区分能力直接下降一个档次,因为颜色细节被压缩掉了。折中方案是:检测用 640,跟踪用 640,最后按跟踪框从原图裁剪出高分辨率区域做特征提取。内存开销会大一些,但值得。
2.2 检测、跟踪、裁剪:三个环节的职责边界别搞混
新手最容易犯的错是把检测和跟踪当成一件事,或者把跟踪框直接拿去做特征。这三个环节各有各的职责,混在一起就会互相拖累。
检测负责“这一帧里有什么”,输出的是框和类别。直播场景的检测有两个特殊要求:一是要能检出被手遮挡的商品,主播握着商品的时候,手指会盖住 10% 到 30% 的面积,训练数据里必须有足够的遮挡样本;二是要能检出小目标,货架上的商品在远景里可能只有几十个像素,这部分我通常不追求检出,因为检出来也认不准,反而增加误匹配,直接设一个面积阈值过滤掉更划算。
跟踪负责“这个东西和上一帧那个是不是同一个”,输出的是稳定的轨迹 ID。跟踪模块的价值在于把零散的帧级检测结果串成一条轨迹,后面才能做时序投票。这里我用的是检测加运动模型的经典组合,实测在直播场景下比纯检测稳定得多,因为主播的手部运动有连贯性,相邻帧之间的位移不会突变。跟踪的 ID 切换(ID switch)是主要痛点,两个相似商品挨在一起的时候容易串号,处理办法是在跟踪阶段就引入轻量的外观特征做二次校验,成本不高但效果明显。
裁剪负责“把这块区域变成可比较的东西”,这一步的关键是扩边。检测框通常贴得很紧,只框住商品主体,但特征提取网络在训练时见到的样本往往带一点背景上下文。我的经验是把检测框向外扩 10% 到 15%,让裁剪图里保留一点边缘信息,特征的一致性会更好。反过来,如果商品是细长形状(比如口红、数据线),直接按框裁剪会导致严重的形变,这时候要用带 padding 的等比缩放,而不是强行拉伸。
2.3 特征向量的版本管理:这是最容易被忽略的一层
我敢说这一节是整篇文章里最容易被跳过、但事后最后悔没做的一节。特征向量不是提取完就完事了,它是有版本的。特征提取网络一换,或者预处理参数一改,同一张图提取出来的向量就和旧库里的向量不在一个空间里了,余弦相似度直接失去意义。
这件事的破坏力在于它是静默失效的。系统不会报错,接口照常返回结果,只是准确率悄悄掉下去。我第一次遇到的时候,排查了整整两天,从抽帧查到检测,从检测查到检索,最后才发现是有人更新了特征模型的权重文件,而商品库里的存量向量还是旧版本。
解决办法是把版本号做成强制字段。具体做法是:特征模型的权重文件带一个哈希值,每次加载时读取,作为当前版本号;商品库的向量记录里必须写这个版本号;检索时只允许在同版本内做相似度计算,跨版本的记录直接跳过并计入“待重建”队列。同时维护一个双写过渡期:新版本上线后,新入库的商品用新版本,存量商品后台异步重建,重建完成前,检索走“新版优先、旧版兜底”的策略,但兜底结果要打一个折扣系数。
这套机制听起来啰嗦,但它把一次可能持续几周的静默劣化,变成了一个可控的、可观测的迁移过程。我现在做任何涉及向量空间的改动,第一件事就是问:版本号加了吗,重建任务排了吗。
3. 匹配环节的取舍:阈值、Top-K 与时序证据
3.1 阈值是算出来的:从误挂成本和漏挂成本倒推
向量检索出来的是 Top-K 个候选和对应的相似度分数,接下来的问题就是:多少分以上才算匹配成功。绝大多数人的做法是拿一批测试集跑一下,看哪个阈值 F1 最高,就定哪个。这个做法在学术上没问题,在业务上是有问题的,因为误挂和漏挂的成本不对称。
误挂的代价是用户点进去看到不合适的商品,产生客诉和退货,同时污染归因数据;漏挂的代价是这两分钟的成交没被记录下来,数据缺失但不产生直接损失。显然误挂的代价更高,所以阈值应该偏向保守。
我的算法是这样的:先准备一个标注集,至少覆盖 200 个场次、5000 条样本,标注每条样本的正确匹配结果。然后画出不同阈值下的误挂率和漏挂率曲线,找到误挂率降到业务可接受水平(比如 2%)的那个点,再看这个点上的漏挂率是多少。如果漏挂率太高(比如超过 25%),说明单纯调阈值已经不够了,得回去补召回手段,而不是继续压阈值。
还有一个更细的做法:分层阈值。不同类目的视觉区分度差异很大,数码配件这种外形高度相似的类目,阈值要设得很高;家居大件这种外形独特的类目,阈值可以低一些。我按类目做了个分组,每组独立标定阈值,整体误挂率降了三成多,而漏挂率几乎没变。分层阈值的维护成本不高,但收益很直接,值得做。
Top-K 的取值也要算。K 太小,正确结果可能进不了候选;K 太大,后面的重排序压力大,而且会引入更多干扰项。我的经验值是 K 取 10 到 20,配合后面的重排序逻辑。因为向量检索的精度在 Top-10 之内通常还不错,再往后就是长尾噪声了。
3.2 时序投票:把“讲了 40 秒”变成一条证据
单帧匹配的结果是不可靠的,这是常识。同一件商品在不同角度、不同光照下的相似度分数可能波动 20 个点。但如果一件商品在连续 40 秒内、跨越 15 帧、被匹配到同一个 SKU,那这个结论的可信度就完全不同了。
时序投票的实现方式不复杂:以跟踪轨迹为单位,把轨迹内所有帧的匹配结果聚合起来,每个候选 SKU 得到一组分数,然后用加权投票决定最终结果。权重怎么给?我给三个维度加权:相似度分数本身、该帧的清晰度(可以用拉普拉斯方差或者检测置信度近似)、以及该帧在轨迹中的位置(靠近轨迹中段的帧权重更高,因为头尾往往是运动模糊最严重的)。
投票的判定规则我做了个调整:不取平均分最高的,而是取票数占比最高的。原因是平均分容易被个别高分帧带偏,而票数占比更能反映“稳定命中”这个事实。举个例子,某条轨迹 15 帧,候选 A 有 8 帧命中但分数中等,候选 B 有 3 帧命中但分数极高,取平均分会选 B,取票数占比会选 A。实测下来,A 才是正确答案的概率明显更高,因为 B 的那 3 帧很可能是背景里的一件相似商品。
还有一个必须加的兜底规则:轨迹长度不足的,不参与投票。比如只出现了 3 帧的轨迹,直接丢弃。这个规则会损失一些真实的短时展示,但换来的是误挂率的大幅下降。短时展示的场景,我另外走一条人工提示的通道,不混在自动链路里。
3.3 细粒度难题:同款不同色、同色不同款的区分手段
这是整个系统里最难啃的部分,也是实际效果和 POC 效果差距最大的地方。POC 阶段用的测试集往往是不同商品之间的匹配,准确率很好看;一上真实数据,发现直播间里最常见的就是同款不同色摆在一起,主播左手一个雾霾蓝右手一个燕麦色,讲完这个讲那个。
纯全局特征在这种场景下基本失效,因为两个商品的形状、版型、纹理完全一致,只有颜色分布不同。我的解决办法是把特征拆成两路:全局特征负责粗召回,局部属性特征负责精区分。
全局特征还是走常规的 embedding,用于从几十万商品里召回 Top-20。然后在这 20 个候选上做属性比对,重点比对三样东西:主色调分布、材质反光特征、以及结构细节。主色调用颜色直方图加空间分块统计,材质反光可以用简单的纹理统计量近似,结构细节则依赖关键点或者分割结果。
举个具体的判断逻辑:如果 Top-20 里有三个候选,全局分数分别是 0.92、0.90、0.89,非常接近。这时候看颜色,如果画面中商品的主色调与候选 A 的主色调距离最小,且差异超过一个经验阈值,就直接判定为 A,同时把置信度提升。如果三个候选的颜色都差不多,那就说明这是同一款的不同规格,这时候不能硬判,应该降级为“待复核”,推到人工队列。
这里有个我踩过的坑:不要用“颜色名称”做匹配。商品库里的颜色字段是运营填的,“雾霾蓝”“灰蓝”“藏青”这些词在不同运营手里含义完全不同,机器没法对齐。要用的是从图片里直接提取的颜色分布向量,和商品主图上提取的做比对,双方都在同一个数值空间里,才有可比性。
4. 线上真实翻车:几个让效果一夜回到原点的细节
4.1 时钟漂移:抽帧时间戳和挂链时间对不上
这个问题我第一次遇到的时候完全没往这个方向想。现象是:离线评测准确率 92%,上线之后运营反馈“老是挂到上一个商品上”。差了大概十几秒,而且是稳定地往后偏。
排查过程是这样的:先怀疑模型,把线上截图拉回来重新跑一遍离线流程,结果是对的,说明模型没问题。再怀疑检索,把当时的 Top-10 打出来看,正确商品排在第一位,分数也很高,说明检索也没问题。那就只剩下一个可能:这条匹配结果被归因到了错误的时间窗。
继续往下查,发现是抽帧环节的时间戳用的是服务端接收流的时间,而挂链记录用的是运营客户端的操作时间,两边有十几秒的偏差。直播流经过转码、分发,到达处理服务的时候已经延迟了;运营客户端的时间则取决于本地时钟准不准。两个时间源不一致,归因自然错位。
修复方案是统一时间基准:所有时间戳都换算到直播流的媒体时间轴上,也就是从流里解析出来的 PTS(显示时间戳),而不是服务端接收时间。挂链操作的时间则通过一个校准接口,用当时的流位置做锚点换算。这套改动不大,但把归因准确率拉回来了将近 20 个点。
经验是:凡是涉及时间对齐的系统,第一件事是把时间基准统一,而且要在设计文档里写明白。我现在做任何流处理相关的项目,第一步都是画一张时间轴对照图,把采集时间、处理时间、媒体时间、业务时间四个概念标清楚,谁跟谁换算,误差范围多少。
4.2 幽灵商品:特征版本不一致引发的连锁反应
上一节提到过特征版本的问题,这里讲一个更隐蔽的变种。现象是:商品库里出现了一批“幽灵商品”,它们能被检索到,但点进去商品详情是空的,或者指向一个已经不存在的 SKU。
根因是商品下架和向量删除之间的时序问题。商品 A 下架了,商品库里的记录被标记为删除,但向量库里的向量是异步删除的,中间有一个时间窗。在这个窗口里,检索还能召回 A,返回给业务层之后,业务层去查商品详情查不到,就成了一条脏数据。这个问题在正常流量下不明显,一到大促期间商品批量上下架,脏数据比例就上来了。
处理办法是在检索结果返回前加一道存在性校验,而且这道校验要带缓存。具体做法是维护一个轻量的商品状态缓存(内存里放,几万个 SKU 的位图完全放得下),检索召回后先过一遍缓存,状态为删除的直接过滤。同时把向量删除从异步改成“标记删除加定期物理清理”,标记删除是同步的,物理清理可以慢慢来。这样既保证了实时性,又不至于让向量库频繁做重操作。
顺带说一句,下架商品的向量不要急着物理删除。因为直播回放的处理可能是延迟的,今天处理的回放里可能还有上周下架的商品。我的做法是保留一段时间(比如 30 天)的“已下架但仍可检索”状态,只是检索结果的置信度打折,并且标记为历史商品,不参与实时挂链。这个设计后来在处理历史回放归因的时候救了我一次。
4.3 长尾 SKU 的冷启动:新品上架当天就得能认出来
直播电商的上新节奏极快,很多商品是当天上架当天播。这就带来一个冷启动问题:商品库里没有足够的图片素材,特征向量只有一个主图能提,多角度特征完全是空的。用单主图特征去匹配直播画面,准确率会明显下降,因为直播画面里的角度、光照、背景和精修主图差异太大了。
我的应对方案分三层。第一层是主图增强:拿到主图之后,做一批自动的增广变换,包括不同背景的合成、不同光照的模拟、轻微的透视变换和遮挡模拟,生成 20 到 30 个变体,每个变体提一个特征,形成一个小型的特征组。检索时取组内最高分。这个做法成本很低,但对单主图商品的召回率提升很明显。
第二层是类目先验兜底。当某个商品的匹配置信度低于阈值时,不退化成“无结果”,而是退化成“类目级结果”,也就是告诉业务层“画面里大概率是一件女装针织衫,但具体是哪一件不确定”。这个信息对运营是有用的,可以提示他们手动确认,比完全空白强。
第三层是运营侧的轻量补录。给运营提供一个极简的入口,在开播前花十几秒给新品拍三张图(正面、侧面、手持),系统自动提取特征入库。这个功能上线后,新品的首场匹配准确率提升非常明显。代价是增加了一点运营负担,但因为只针对新品,实际工作量可以接受。
5. 让系统越跑越准:复核队列、回流节奏与成本账
5.1 人工复核队列的分层设计
自动链路一定会有置信度不足的情况,这些样本去哪了,决定了系统能不能持续变好。最常见的错误做法是把所有低置信度样本一股脑丢进一个队列,让运营从头到尾看。结果就是队列永远清不完,运营看两天就放弃了。
我的做法是按价值分层。第一层是“高价值高不确定”:商品是当场的重点主推(可以从运营的挂链记录里识别出来),但匹配置信度处于中间地带。这类样本优先看,因为一旦错了影响最大。第二层是“低价值高不确定”:背景里的、边缘的、短暂出现的商品,这类样本直接丢弃,不看。第三层是“高置信度但被人工推翻过”的:这类样本最有价值,它是模型自信地犯了错,说明模型存在系统性偏差,必须重点分析。
队列的容量也要控制。我一般把每日复核量控制在 200 到 500 条之间,超出的自动截断,宁可漏掉一些也不让队列积压。同时给复核界面加一个“一键确认”和“一键纠正”的快捷操作,把单条处理时间压缩到 3 秒以内。这个体验优化听起来是小事,实际上直接决定了运营愿不愿意长期配合。
5.2 bad case 回流与模型更新的节奏控制
复核产生的标注数据,回流到训练集,这是一个标准闭环。但节奏控制很讲究。我的经验是不要频繁全量更新模型,因为每次更新都会带来特征空间的变化,前面说的版本管理成本会跟着上来。我一般把更新节奏定在两周一次,而且是小步迭代,主要是补充训练数据、微调分类头,不动主干网络的架构。
真正需要频繁更新的是商品库本身,也就是新增商品的向量入库。这个是每天甚至每小时都在发生的,它和模型更新是两条独立的流水线,不能混在一起。我见过一个团队把商品入库和模型训练耦合在一个任务里,结果每次上新商品都要触发模型重训,整个系统变得极其笨重。
另外,回流数据要做去重和平衡。直播数据天然不均衡,某些爆款商品会出现几千次,长尾商品可能只有几次。直接拿去训练会导致模型对爆款过拟合。我的处理是按商品维度做采样上限,单个商品在每个训练批次里最多贡献固定条数,保证长尾商品的曝光。
5.3 算一笔账:GPU 推理与向量检索的成本结构
最后说说成本,因为这套系统的成本结构如果不提前算清楚,很容易做成一个“效果不错但养不起”的项目。
推理由三部分组成:检测、跟踪、特征提取。检测和跟踪是每帧都要跑的,特征提取只对跟踪轨迹的关键帧跑。假设一场四小时直播,双通道抽帧下来大约 9000 帧,检测按每帧 20 毫秒算是 180 秒,跟踪的耗时通常低于检测,算 90 秒,特征提取只跑轨迹里的关键帧,大概 1500 次,每次 15 毫秒,算 23 秒。总计约 300 秒单卡时间。如果一天处理 500 场直播,就是 42 小时单卡时间,需要至少 3 张卡才能在一个工作日内跑完。这个量级对中型团队是可以承受的。
向量检索的成本相对低很多,主要开销在内存。假设商品库有 50 万个 SKU,每个 SKU 存 5 个特征向量(主图加增广),每个向量 512 维 float32,大概是 2KB,总共 500 万条向量,约 10GB 内存。这个规模用单机内存索引完全扛得住,不需要上分布式集群。检索 QPS 方面,一场直播产生的检索请求大概几千次,峰值也就几十 QPS,压力很小。
真正的成本大头其实是存储和带宽,因为要保留裁剪图用于复核和回溯。一张裁剪图按 100KB 算,一场直播 1500 张就是 150MB,一天 500 场就是 75GB。这个数字会快速增长,所以裁剪图必须设保留期,我一般设 15 天,超过就转冷存储或者只保留缩略图。
一个省钱的小技巧:复核队列里的图片用低分辨率缩略图就够了,运营确认的时候看的是“是不是这件”,不需要看清纹理。真正需要高分辨率的是 bad case 分析环节,那部分样本量小,可以单独存。
这套系统我陆陆续续做了挺长时间,最大的体会是:模型能力只是其中一环,工程细节和业务理解占的比重更大。同样的特征网络,时间戳对齐做对了、版本管理做扎实了、复核队列设计合理了,效果能差出一倍。反过来,模型再强,只要有一个环节的时序或者状态管理出问题,整条链路的表现就会全面崩掉。如果让我重新做一遍,我会先花两周把时间基准和版本管理这两件事设计清楚,再动手写第一行推理代码——这两周的时间,后面能省回来两个月。