ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

推荐系统双塔召回落地全解析:结构、训练与工程坑

推荐系统双塔召回落地全解析:结构、训练与工程坑

行业里有句话说得挺准:精排决定推荐的下限,召回决定推荐的上限。不管你的精排模型是DeepFM还是更复杂的结构,它只能对你交给它的几百个候选排序;用户最终能不能看到一件商品,依赖的是召回环节有没有把好的东西捞进候选池。双塔召回模型(Two-Tower Retrieval Model)正是为了解决"全量精排算不过来、规则召回又不够聪明"这个矛盾出现的标准方案,目前在各类电商、短视频、内容平台上基本都是召回链路的主流底座。这篇文章不是理论的复述,而是我从一个真实推荐团队落地双塔的全过程里梳理出来的记录——从为什么必须引入双塔,到网络结构怎么设计,再到训练样本、损失函数、线上索引和各类调优坑,一条线说清楚。不管你是刚开始做推荐的新人,还是已经在召回环节摸爬滚打了一阵子的工程师,应该都能从中找到对你有用的东西。

1. 召回环节面临的两难:全量精排算不过来,规则召回又不够聪明

1.1 推荐漏斗里召回的位置:为什么它是上限

推荐系统通常是一条流水线:召回、粗排、精排、重排,最后才把结果返回给客户端。召回阶段要从几万到几千万的物料池里,先圈出几百到几千个候选;粗排和精排再在这个候选集上做越来越复杂的打分排序。这套漏斗设计完全是出于算力考虑,不是谁拍脑袋定的架构。

我们做个简单算术。假设平台有100万可推荐商品,精排模型对单个样本打分的耗时是1毫秒,对于一次请求,如果想让精排过一遍全量商品池,耗时就是1000秒。这还没算特征读取、模型加载、结果排序这些附加开销。任何线上服务都不会接受这种延迟,所以召回必须存在。

但关键问题在于:如果召回质量差,好东西根本没进候选池,精排模型再强也无能为力。我见过不止一个团队,把精力全花在精排模型的AUC提升上,结果线上指标纹丝不动,一查链路,问题出在召回把大量相关商品漏掉了。这就是"召回决定上限"的来由。

1.2 全量精排的算力困境与规则召回的局限

在双塔模型普及之前,主流召回方案大致分两类。

第一类是规则召回,比如热门召回、新品召回、同品类下新、同品牌召回。规则召回的优点是解释性强、上线快、几乎没有训练成本,缺点是覆盖维度太单一。"热门"天然偏向头部商品,长尾商品几乎没有出头之日;"同品类"则完全忽略了用户当前的行为上下文,推荐结果千篇一律。规则召回适合做兜底,单靠它撑不起个性化推荐。

第二类是协同过滤,包括ItemCF和UserCF。协同过滤的核心逻辑是"与你相似的用户还买了什么"或者"看了这个商品的用户还看了什么"。在小规模数据上,协同过滤效果不错,但随着用户量和商品量增长,稀疏性问题会越来越严重——一个新商品如果没有任何交互记录,就永远不会被协同过滤召回;一个新用户如果没有历史行为,同样无法计算相似用户。而且协同过滤依赖的是item-item共现矩阵,矩阵计算和存储的成本会随规模指数膨胀。

1.3 双塔的核心洞察:把"打分"拆成"两个向量求内积"

双塔召回模型的出发点其实很朴素:既然精排对全量打分不现实,那就找一个能在亚毫秒级完成全量候选排序的打分函数。要让几百毫秒内检索完百万级物品,唯一的办法就是预计算。我们提前把每个物品表示成一个固定维度的向量,存进向量检索引擎;线上请求到来时,只计算用户向量,然后用用户向量去检索最相似的K个物品向量。

这个流程能成立的关键在于:打分函数必须是可分解的。双塔模型把用户特征和物品特征分别映射成向量u(x)和v(y),用内积u(x)·v(y)或者余弦相似度来衡量匹配程度。注意,这种形式下物品向量v(y)是独立于用户存在的,所以我们完全可以离线对全量物品做一次前向计算,把v(y)全部预计算好并建成索引。等到线上,真正需要实时计算的就只有用户塔那一半。

换个方式理解:精排模型像一位要求所有候选者都到场的评委,认真打分但代价很高;双塔召回则是把"评委"拆成两半,一半是掌握用户口味的考官,一半是每一位候选者提前交上来的简历,考官不需要见所有人,翻简历就能圈出最可能合适的候选人。

2. 拆开双塔:User塔和Item塔各自的网络设计

2.1 User塔与Item塔的输入特征设计

双塔的两个塔在结构上可以长得一样,常见的都是"Embedding层 + MLP层 + 输出向量",但输入特征差异很大。

User塔的输入侧,主要吃三类特征。第一类是用户画像,比如性别、年龄、城市层级、消费力档位;第二类是行为序列特征,比如最近点击/购买过的商品ID序列、最近交互过的类目序列、最近搜索过的关键词序列;第三类是统计特征,比如近7天活跃天数、历史点击次数、平均客单价、会员等级。这里面最核心的是行为序列,它携带了用户当前兴趣的最直接信号。

Item塔的输入侧,主要吃的事实特征包括:商品ID、叶子类目、一级类目、品牌、价格、上架时间、所在店铺的评分等级,以及近7天曝光量、点击率、转化率等统计特征。与User塔不同,Item塔更像是在描述"一件商品本身是什么样",这些特征在模型刷量上承担着重要作用——特别是新品,在没有交互行为时,只能靠这层内容特征被模型表达。

需要提醒的是,连续特征尽量不要直接塞进网络。价格、点击率这类数值特征分布通常很偏(长尾),直接输入会让模型对这些特征过于敏感。我的习惯做法是做分桶Embedding或者做z-score标准化后再进MLP,这比让网络自己学习归一化稳定得多。

2.2 网络主体:Embedding、MLP与输出向量

输入特征进入双塔后,第一步是Embedding。稀疏ID类特征(用户ID、商品ID、类目ID)通过lookup table查出一个稠密向量,embedding维度我一般取32到64。对于行为序列里的商品ID序列,最简单的做法是取平均或者取加权和(按时间衰减),得到序列向量后再和商品侧拼接。第一版双塔完全没必要上复杂的attention或者Transformer结构,mean pooling就够用,我后面会解释为什么。

Embedding之后接入MLP。以输出向量维度64为例,一个稳妥的配置是:[512, 256, 64],激活函数用ReLU,每个全连接层后面接BN(BatchNorm)。加BN对双塔训练特别重要,因为user塔和item塔的输入分布差异很大,BN能有效缓解内部协变量偏移,让两个塔的训练更稳定。

最终输出的向量要不要做L2归一化?我的建议是必须做。归一化之后,内积就等于余弦相似度,这带来了两个直接好处:其一,向量检索时可以用内积操作替代专门的余弦相似度计算,Faiss里直接设置MetricType.INNER_PRODUCT,省掉一次额外的向量模长求解;其二,归一化把向量约束在单位超球面上,数值稳定,梯度不会因为向量模长过大而爆炸。

2.3 两个关键设计选择:为什么不共享参数、输出维度怎么选

很多初学者会问:user塔和item塔不都是神经网络吗,能不能共享参数?答案是尽量不要。原因有三层。

第一,两边的输入空间完全不同。user侧主要是ID、行为序列和统计特征,item侧主要是商品属性特征,共享Embedding层的意义不大,还容易造成稀疏侧的Embedding学不充分。第二,user塔和item塔各自的"语义空间"并不对齐,强制共享参数会限制网络表达。第三,从工程角度看,双塔模型上线时两个塔往往是分开服务、独立更新的(user塔在线跑,item塔离线产向量),共享参数会让这个架构变得极难维护。

输出向量的维度选择同样有讲究。64维是我见过的最常见配置,32维常用于超大规模检索场景,128维则适合标注数据充足、精度优先的场景。维度越高,理论上信息容量越大,但代价是索引内存变大、检索变慢,而且在训练数据不足时高维向量反而容易过拟合。我建议从小维度开始,用64维起步跑基线,等确认数据量和业务收益后,再评估是否需要升维。

3. 决定召回效果上限的三件事:正样本、负样本、损失函数

3.1 正样本定义里的门道

训练双塔模型,第一步要回答"什么样的user-item对被当作匹配的正样本"。最简单直接的做法是"曝光且点击",这也是多数团队的第一版选择。

但只做这一步会埋下偏好偏差:模型会倾向给点击率高但在转化上表现差的商品更高分。比如一个9.9元包邮的小商品,人人都愿意点,但真正下单率极低,如果召回只优化点击,这类商品就会大量进入候选池。所以后续迭代时,我建议按业务目标给不同行为赋予权重:点击权重1,加购权重2,下单权重5,支付权重10。甚至可以直接把"支付"当成正样本,"点击但未下单"按比例降权或干脆排除,视业务增长目标而定。

还有一个很容易犯的错误:把同一用户的重复曝光当成多次正样本。用户反复看到某商品却不点,这本身就是强烈的负信号,不能简单重复采样。正样本要去重、限制单个用户对同一商品样本数,否则模型会被频次带偏。

3.2 负样本采样:随机、batch内、困难负样本与staircase

召回模型的训练本质上是"给定正样本,学习如何区分随机负样本"。负样本的质量直接决定模型能学到多细的判别边界。

最基础的是随机负采样,从全量商品池里均匀随机抽取若干商品作为负样本。这个方法的缺点是负样本太简单——随便抽一个商品,用户大概率确实不感兴趣,模型很快就能把它们区分开,学到的特征区分度不足。

工程上最常用的其实是in-batch负采样:一个batch内除了当前用户的正样本商品,batch里其他用户的正样本商品都可以当作当前用户的负样本。这个做法几乎不增加额外计算成本,还天然有一种"热门商品被更多批次选中,因此被当负样本的概率更高"的纠偏作用。我在实践里batch大小一般取512到2048,batch太小负样本不足,太大容易引入太多噪声。

但in-batch负采样也有隐患:热门商品被反复当负样本,会被过度打压。缓解方案是抽样时给热门商品降权,或者控制单个热门商品在batch内被重复选中的次数上限。

如果想要更高精度,就需要引入hard negative(困难负样本)。典型做法是用上一版模型对全量商品计算相似度,把与用户相似度高但不是正样本的商品挑出来当负样本。加入hard negative之后,召回模型才能学会区分"看起来很像但用户没买"和"完全无关"这两类情况。直接全部用hard negative会训练得太难、收敛很慢,业界常用的做法是staircase策略:训练初期用全随机负样本让模型学到基本语义,训练中后期逐步把hard negative比例从0提升到20%-30%。

下面是三种方式的对比,我做了个表格,方便你按自己的场景选型:

负样本方式实现成本难度适用阶段主要风险
随机负采样低简单任何阶段样本太简单,判别力不足
in-batch负采样极低简单所有双塔训练热门商品被过度打压
hard negative挖掘中高较难基线上线后迭代训练不稳定,需要调节比例

3.3 损失函数与温度系数:一段PyTorch实现

双塔训练最主流的损失函数是in-batch softmax交叉熵。给定一个batch的user向量u_emb和item向量i_emb,我们把对角线上的位置当作正样本,其余位置当作负样本。下面这段代码就是我实际用过的核心逻辑,结构非常简洁:

import torch import torch.nn.functional as F def compute_loss(u_emb, i_emb, tau=0.1): # u_emb, i_emb: [batch, dim],已经做L2归一化 # 相似度矩阵 [batch, batch] logits = torch.matmul(u_emb, i_emb.t()) / tau # 对角线是正样本 batch_size = u_emb.size(0) labels = torch.arange(batch_size, device=u_emb.device) loss = F.cross_entropy(logits, labels) return loss

这段代码里最值得玩味的参数是tau,也就是温度系数。它做的事情是:把相似度分数除以一个很小的数字,放大正负样本之间的差异。tau越大,softmax越平滑,所有样本的loss都差不多,模型学不到重点;tau越小,softmax越尖锐,模型几乎只看那个最大的logit,容易过拟合到一个难样本上。

我在调双塔初期最常用的tau范围是0.05到0.2。这个参数敏感度极高,我在一个具体项目里的经验是:同样的样本配置,tau=0.1时Recall@100稳定在0.32,tau=0.5时直接掉到0.21,而tau=0.02时训练loss直接NaN。如果你的loss在训练早期就出现异常波动,优先检查温度和负样本比例,而不是动网络结构。

4. 双塔上线后的工程真相:向量索引与在线Serving

4.1 离线产向量与在线产向量的分工

模型训练完成之后,双塔模型的下半场考验是工程能力。这里有一个所有团队都要面对的问题:item向量和user向量,分别在什么时候算?

Item向量天然适合离线批量计算。每天定时用最新模型对全量物品跑一遍前向,得到每个物品的向量,写入向量数据库。这个流程没有必要放在在线请求链路里,因为物品数量虽然大,但变化节奏是小时级别,离线完全可以覆盖。

User向量则必须实时计算。原因很简单:用户的行为序列每一秒都在变,如果复用一个小时前的user向量,那用户刚刚点击的那件商品就有可能无法被召回。所以线上一般单独部署一个user塔服务,请求到来时,实时读取用户最新特征,过一次前向计算,得到当前时刻的user向量,然后带着这个向量去检索item向量库。

这两个塔的部署方式差异,正是双塔模型工程上和普通精排最不一样的地方。维护两套推理服务会带来额外的运维成本,但这是一个值得付的代价。

4.2 向量索引选型:Flat、IVF还是HNSW

user向量算好之后,怎么从百万级item向量里快速找到top K,这是向量索引的战场。我常用的方案集中在Faiss里。

最朴素的是暴力搜索(Flat),把item向量全部读进内存,逐个计算内积,结果绝对精确。但代价是查询耗时随数据量线性增长,百万级向量时每次请求可能要几十毫秒到上百毫秒,顶不住高并发。

稍微进阶的是IVF(倒排索引),思路是先对整个向量空间做聚类(比如分成N个聚类中心),查询时只计算与目标向量最近的几个聚类中的向量。IVF能显著提速,但召回率有一定损失,通俗地说就是容易漏掉聚类边界附近的相似向量。它适合数据量大、对召回率要求不那么苛刻的场景。

HNSW(层级可导航小世界图)是目前兼顾速度和精度的最优解之一。它把向量组织成一张多层图,查询时从顶层开始逐层下探,复杂度大约是对数级别。CPU版本实测百万级向量时,单次查询能做到个位数毫秒,召回率只比暴力搜索低一点点。但HNSW的代价是内存占用偏高,单机撑大规模向量需要评估内存预算。

索引查询速度召回精度内存开销适用规模
Flat慢100%低百万级以内、离线评测
IVF中快中等低百万到千万级、精度容忍度高
HNSW快高高百万到千万级、推荐业务首选

我的建议是:第一版线上先用HNSW,图复杂度参数M取16到32,efSearch取64到128;离线评测时再上Flat做对照,算出HNSW的召回率损失在多少,后续好评估索引优化空间。

4.3 全量更新与增量更新的节奏问题

建好索引之后,物品向量不是一成不变的。新品要上线,老商品的状态会变化,模型本身也要迭代,这就涉及更新节奏。

最简单的是每日全量重建:每天凌晨用当日最新数据重新训练或增量训练模型,然后对全量物品重算向量、重建HNSW索引。这种方法逻辑清晰、数据一致性最好,但对算力要求高,而且实时性差——新上架的商品要等到第二天凌晨才进索引,当天完全丧失曝光机会。

想做出新商品当天就有召回机会的效果,就得加增量通道。常见的折中方案是"每日全量 + 每小时增量":全量索引保证整体一致性,增量通道负责当天新上架的少数商品,实时产向量并同步进索引。实施增量更新时,新向量之间、新向量与旧向量之间最好能达到一致的分布,所以模型版本一变,增量通道的向量也必须重启。

4.4 召回结果交给精排前,还需要做什么

召回的top K结果不能直接当最终推荐列表。原因很简单:聚类效应。双塔在大类目上很容易出现"召回的K个全是同一品牌/同一类目商品"的情况,用户的兴趣宽度被大幅压缩,推荐结果的多样性会很差。

所以召回结果交给下游之前,一般会做一件事:类目/品牌维度打散。最简单的打散策略,是限制每个叶子类目最多出现N个商品,或者用按类目分桶 + 组间轮转的方式保证多样性。如果对效果要求更高,可以上MMR(最大边际相关)之类的标准算法,在相关性与多样性之间做权衡。这一步的收益在线上往往非常明显,我见过一个项目只做了类目打散,推荐页的次均点击就提升了8%,完全是白捡的增量。

5. 实践中最容易翻车的几个细节与调参经验

5.1 冷启动:新用户和新商品没有向量怎么办

双塔模型对ID类特征依赖较重,所以冷启动是绕不开的坎。新商品没有任何交互记录,它的ID Embedding在训练阶段从未被更新过,如果直接查表,得到的基本是随机初始化向量,召回效果约等于零。

我的解决方案是"特征兜底 + 向量融合"。在新商品ID的Embedding之外,引入内容侧特征(类目、品牌、价格带、文本类目表示)作为主力信号,让模型在商品没有交互行为时,主要依靠内容特征产出向量;一旦商品产生了点击数据,ID Embedding逐步被更新,内容特征的权重自然退居其次。这个思路的本质是让双塔模型学会"利用静态属性作为冷启动期的临时锚点"。

新用户冷启动的原理类似。对于没有任何行为序列的用户,user塔要依靠画像特征和场景上下文(比如当前是否在首页、当前时间段的城市属性)输出向量。如果画像也是空的,就退化成热门召回——用全局Top商品做兜底。这在业务上完全合理:不了解用户的时候,先用热门内容试探兴趣,等积累了行为再个性化。

5.2 特征穿越与其他样本层面的翻车现场

我见过最隐蔽的问题就是特征穿越。所谓特征穿越,指的是训练样本里混入了"按理说模型在预测时不可能提前知道"的信息。

举一个真实例子。某个团队想提升召回效果,给item塔加上"近30分钟曝光量"这个特征。离线评测时指标涨了5个百分点,团队以为找到了宝,结果线上效果纹丝不动甚至变差。复盘才发现,训练样本里这条特征是在商品被曝光之后才记录下来的——也就是说,训练时模型用到了"未来信息",而线上预测那一刻根本拿不到这个未来值。这种特征穿越会让离线指标虚高,但真实场景复现不出来。

另一个翻车点是行为序列的时间顺序。如果用用户当天的完整行为序列去预测当天是否点击某商品,等于让模型"偷看答案"。正确做法是严格按行为发生时间切分:训练时只用用户截止到上一个时间戳(比如昨天的行为)去预测下一个时间戳的行为,评估时同样遵循这个时序切割逻辑。双塔模型里的行为序列特征,一定要做到"只包含预测时刻之前的信息"。

5.3 评估与迭代:不要被Recall@K骗了

双塔召回常用的离线指标是Recall@K,比如"用户真实交互的商品里,有多少能被召回的top K覆盖到"。这个指标简单直观,但有一个大坑:它天然偏向高频热门商品。如果一个商品被大量用户交互过,它出现在各种用户top K召回列表里的概率本来就高,最终Recall@K数值很好看,但其中有多少是模型真的捕捉到了用户个性化兴趣,有多少只是"热门商品本身容易命中"?后台一拆解,常常发现高召回完全由热门堆出来。

所以我评估双塔效果时,除了看整体Recall@K,还会拆两层:核心类目覆盖率和长尾商品召回占比。长尾商品(比如交互次数排名后80%的商品)召回占比越高,说明模型个性化能力越强。同时还要关注和上线后的搜索链路、精排点击率联动——毕竟召回只是中间环节,最终业务指标是成交或时长,不是模型自己的指标。

如果发现模型召回的K个结果同质化严重,但Recall@K却不低,那很有可能又是热门商品刷出来的。这时候与其调loss,不如先看样本层有没有对热门商品过采样。

5.4 从基线上线到持续迭代的节奏感

最后聊一点团队协作层面的经验。很多团队在双塔模型第一版就急着加各种花活,比如用户多兴趣向量、注意力机制、图神经网络邻居聚合。我的建议是千万别这样。

第一版双塔,用最简单的Embedding + MLP结构,配上in-batch softmax和随机负样本,先把召回链路完整跑通:训练、产向量、建索引、上线、评估。这一步的价值是把整个工程管线打通,把"模型效果不好"和"链路有Bug"这两类问题分开。第二版再逐步加hard negative、调温度系数、上特征优化。第三版才考虑多兴趣、蒸馏精排信号之类的高阶玩法。

原因在于,双塔模型的技术门槛主要在工程链路上,而不在模型结构本身。链路稳定之前,任何高阶结构带来的收益都会被工程抖动吞掉。我统计过,一个中小型推荐团队从零到双塔稳定上线,大约需要四到六周,其中至少一半时间花在数据管道和索引部署上。想清楚这个时间分配,你就能理解为什么我总是劝人"先跑链路,再雕模型"。

最后再分享一点个人体会。双塔召回模型这套方案,表面上是一个"两个塔的网络结构",实际上是一套完整的数据与工程闭环:样本怎么标、负样本怎么采、温度系数怎么调、向量索引怎么建、增量更新怎么做、冷启动怎么兜底,每个环节都决定了最终效果的天花板。如果你正准备在团队里落地双塔,把本文涉及的链路先走一遍,再回头看看哪些环节还有提升空间,这种"由链路反推算法"的做事顺序,通常比一开始就扎进模型细节里走得更稳。

返回列表