推荐系统召回双塔模型:从原理到线上部署的完整指南
发布时间:2026/10/28 8:26:02 锦皓数字建站

1. 为什么召回阶段需要双塔模型很多刚接触推荐系统的朋友会有一个疑问精排模型已经那么强了为什么还要单独搞一个召回模型道理其实很简单。拿一个日活百万级的APP举例物品池子里可能有几百万甚至上千万条内容。精排模型再牛也只能对几百上千个候选做精细打分它不可能对全库所有物品都跑一遍神经网络。这时候就需要召回阶段先从千万级物品里快速捞出几百个“可能感兴趣”的候选交给精排去细排。召回拼的不是精度而是覆盖率——能不能把用户真正感兴趣的东西尽量留在候选池里。双塔召回模型就是解决“全库检索”问题的主流方案之一它的核心思路是把用户和物品分别映射成两个向量然后在向量空间里用相似度做近邻检索。这个思路本身不复杂但真正落地的时候处处都是细节。我最早接触双塔是在电商场景做商品召回后来又在内容推荐场景优化过视频召回踩了不少坑这篇就把整个模型的原理、训练、上线流程和常见问题一次性说清楚。对比一下常见的召回方案为什么双塔能成为主流看个表就明白了召回方案核心思路优点痛点ItemCF / UserCF基于共现行为计算相似度实现简单可解释性强无法利用用户和物品的丰富特征冷启动差FM / FFM特征二阶交叉后打分排序特征利用充分精度较高线上推理需要遍历全库打分性能瓶颈明显双塔向量召回用户/物品分别编码成向量ANN检索离线训练和在线检索解耦性能极佳双塔交互太弱容易欠拟合需要精细调样本和损失理解了双塔在推荐链路里的定位再看它的结构和训练方法就清楚多了。2. 双塔模型结构拆解与设计原理2.1 用户塔和物品塔到底长什么样双塔模型的结构用一句话概括就是两侧各自独立的DNN最后输出一个固定维度的向量用向量内积或余弦相似度作为匹配分数。用户塔的输入是用户侧特征包括用户ID、年龄、性别、城市、历史点击序列、历史行为统计等等。这些特征经过Embedding层变成稠密向量然后拼接或按特征域做聚合比如对历史点击物品的Embedding做加权和再经过几层全连接网络最终输出一个d维的用户向量。物品塔的结构几乎一样输入是物品侧特征包括物品ID、类目、品牌、标题词Embedding、价格分桶等同样经过Embedding层和全连接层输出一个d维的物品向量。两个塔之间在结构上是完全独立的没有任何参数交互。这是双塔的“双”字的含义也是它能高效上线的根本原因。这里有一个非常关键的设计点两个塔最终输出向量的维度d。我见过不少团队把d设得过大比如256维甚至512维结果训练时loss降得很漂亮但线上召回效果反而一般。因为向量维度越高ANN检索的精度退化越明显而且高维稠密向量的存储和计算开销也大。说实话64维到128维是大多数场景的甜点区间短视频推荐用128维比较常见电商场景64维往往就够用了。用户向量和物品向量的相似度最常用的有两种计算方式内积Dot Product直接用两个向量的点积作为分数得分范围不受限制适合需要区分明显偏好强弱的场景余弦相似度Cosine对向量做L2归一化后再算内积等价于余弦相似度分数范围被限制在[-1,1]更稳定实际工程中推荐用归一化后的余弦相似度。原因有两个一是归一化后向量的模长信息被抹掉模型被迫学“方向”而不是“长度”这对召回来说更合理——用户喜不喜欢这个物品应该看方向而非绝对长度二是后面对向量做近邻检索时余弦相似度和内积在归一化后是等价的但数值范围更可控量化压缩时误差更小。2.2 为什么两侧不能用同一个网络有人会想既然都是DNN能不能用同一个网络结构共享参数这样还能减少一半参数量。答案是不能共享参数。用户和物品的特征空间差别太大了用户侧有历史行为序列这种长度不固定的特征物品侧则是类目、标题这种静态属性。共享参数意味着这两类特征被强行映射到同一个语义空间里模型基本学不出有用的匹配关系。我在一个项目里试过共享底层的做法结果训练loss比不共享高了将近30%AUC也掉了不少。还有一种常见变体是把用户侧的历史行为序列用Attention机制加权再和用户静态特征拼接。这相当于给用户塔加了一个小型的序列编码器效果通常比简单平均要好。但要注意加复杂结构的同时要控制参数规模双塔模型的一大优势就是简洁结构搞复杂了违背初衷。2.3 Embedding层的特征工程心法双塔模型对特征的处理方式和精排模型有很大区别。精排模型可以放几百个特征反正只对几百个候选打分。但召回模型面对的是全库检索特征的覆盖面和泛化能力才是第一位的。我的经验是召回侧的特征尽量控制在20个以内优先选以下几类强标识类特征用户ID、物品ID这两个必须放是模型的“锚点”窄分布类目特征一级类目、二级类目、品牌这类特征能快速缩小检索范围统计类特征用户近7天点击类目分布、物品近3天曝光点击率能显著提升冷启动和热门泛化能力行为序列特征用户最近点击/购买的N个物品ID序列这是召回效果的最大贡献者之一有一个坑一定要避开不要把精排里的复杂交叉特征放到双塔里。比如“用户近期购买品牌∩物品品牌”这种强交叉特征在双塔结构里根本没法用——用户侧特征和物品侧特征在最后的交互层之前是完全隔离的交叉特征只能塞进其中一侧效果反而会变差。这类特征留在精排阶段用就好。3. 训练样本构造双塔模型的生死线3.1 正样本怎么选双塔模型的正样本本质上就是“用户在这个场景下真实感兴趣的物品”。最简单直接的选择是用户点击过的物品。但点击行为有噪声比如用户可能是误点或者因为封面标题点进去但内容很差。这就引出了不同业务场景的差异电商场景偏向用“购买/加购”作为正样本实在没有购买数据时再降级用点击内容场景点击可以作为弱正样本如果有点赞、收藏、完播率这些强反馈可以按权重加权做多级正样本广告场景转化是最终目标点击样本需要降权这里有个细节正样本要不要做时间衰减。我见过很多团队直接用全部历史行为当正样本效果往往不如只取近30天或近90天的行为。因为用户的兴趣会漂移半年前买的婴儿纸尿裤现在再作为正样本去训练反而会把模型往错误的方向带。我在实际项目中通常取近30~90天的行为做正样本用更近的样本做负采样效果一般比全量好3~5个点的召回率。3.2 Batch内负采样双塔训练的标配负样本的选择直接影响双塔模型的上限。常见的负样本来源有全局随机采样从全库随机抽物品简单但有偏差容易被热门物品主导曝光未点击用户看到了但没点这种是“真负样本”质量高但数量有限而且存在选择偏差混合负采样随机负样本曝光未点击负样本按比例混合但在双塔模型的训练中最主流的做法其实是Batch内负采样In-batch Negative Sampling。原理特别巧妙在一个batch内有N个用户和N个物品配对对于第i个用户正样本是第i个物品剩下的N-1个物品就自然成了这个用户的负样本。这个做法有几个好处不用额外构造负样本训练效率高负样本来源于真实曝光流分布接近自然场景配合温度系数能在训练时做“难负样本”的挖掘但是Batch内负采样有个著名的毛病——热门物品容易被当成负样本。因为热门物品出现在batch里的概率高它们被“冤枉”成负样本的次数也越多导致模型倾向降低热门物品的分数这就是Popularity Bias流行度偏差的来源之一。解决办法有两种我在项目里都试过对负样本做频率加权在计算softmax概率时对batch内负样本的logits减去log(物品采样概率)的修正项这在学术界被称为“sampling probability correction”加一个物品频率特征把物品的对数热度作为一个输入特征放进物品塔让模型自己学会根据热度做校准。后一种方法工程上更简单效果也很稳3.3 温度系数双塔模型的灵魂参数很多初学者不理解温度系数是干嘛的我这里用一个类比解释softmax里的温度系数相当于“注意力的聚焦程度”。温度τ很大时softmax的输出分布变得平滑所有物品的分数差异被拉平模型训练时“一视同仁”温度τ很小时softmax的输出分布变得尖锐正样本和负样本的分数差被放大模型会盯住最有区分度的样本从梯度角度说温度系数控制了负样本的梯度贡献力度。温度越小那些“分数较高的负样本”也就是难负样本会获得更大的梯度模型会更努力地把它们压下去。实际上温度系数就是用来控制“对难负样本的关注程度”。太小容易让模型过拟合到个别难样本上太大则会让模型学得过于平滑、区分度不够。我在实际项目中通常从τ0.05开始调看验证集召回率的反馈再增减。有一点要提醒温度系数和Embedding归一化是配套使用的。如果做了L2归一化向量模长恒为1logits的最大值受限于维度d此时需要调大温度比如0.05~0.2区间如果不做归一化logits的数值范围不受限温度可以设得更大一些。4. 模型训练与损失函数详解4.1 从Softmax到In-batch Softmax双塔模型的经典损失函数是Sampled Softmax它把召回问题建模成一个多分类问题给定一个用户从全库M个物品中预测他最可能点击的那个。全库M太大没法算完整softmax于是用batch内采样来近似。具体来说对于用户u在包含正样本i和batch内其余N-1个负样本的集合里计算softmax概率P(i|u) exp(score(u,i) / τ) / (Σj∈batch exp(score(u,j) / τ))然后对P(i|u)取负对数作为loss。这个loss值越小说明模型越能把正样本的分数排到负样本前面。这里有一个容易忽略的细节在in-batch softmax里理论上每个用户都有一个正样本这个batch就是按“用户-物品对”采样的所以loss的batch内平均是有意义的。实际训练时还需要对同一batch内的数据格式做处理。我一般用如下流程把用户特征和物品特征分别组织成两个输入张量两者按行一一对应用户塔前向计算得到用户向量矩阵Ushape为batch_size × dim物品塔前向计算得到物品向量矩阵Vshape为batch_size × dim计算相似度矩阵S U × V.T对角线上的元素就是正样本分数对S按列或按行做temperature-scaled softmax对角线位置作为监督标签4.2 一个标准的PyTorch训练核心代码把上面这套逻辑写成PyTorch代码非常简洁核心部分长这样import torch import torch.nn as nn import torch.nn.functional as F class TwoTowerModel(nn.Module): def __init__(self, user_feature_dim, item_feature_dim, embed_dim64, temperature0.05): super().__init__() self.user_tower nn.Sequential( nn.Linear(user_feature_dim, 128), nn.ReLU(), nn.Linear(128, 64), nn.ReLU(), nn.Linear(64, embed_dim) ) self.item_tower nn.Sequential( nn.Linear(item_feature_dim, 128), nn.ReLU(), nn.Linear(128, 64), nn.ReLU(), nn.Linear(64, embed_dim) ) self.temperature temperature def forward(self, user_features, item_features): # 用户塔和物品塔分别输出向量并做L2归一化 user_emb F.normalize(self.user_tower(user_features), p2, dim1) item_emb F.normalize(self.item_tower(item_features), p2, dim1) # 相似度矩阵 logits torch.matmul(user_emb, item_emb.t()) / self.temperature return logits def sampled_softmax_loss(logits): # 对角线是正样本对每个用户对应一个正样本 batch_size logits.size(0) labels torch.arange(batch_size, devicelogits.device) # 对每一行计算交叉熵 loss F.cross_entropy(logits, labels) return loss逻辑上就是前向算出相似度矩阵然后用对角线索引做标签一行就是多分类交叉熵一算完事。这段代码里我特意做了L2归一化温度和归一化的配套关系前面说过。另外要注意F.cross_entropy自带softmax操作所以logits直接进loss就行不要再单独做softmax否则就变成二重softmax了一个低级但容易犯的错。4.3 离线评估的指标坑双塔模型离线评估常用的指标是RecallK和HitRateK。但在样本构造上有一个天然的不公平如果负样本是随机采样的RecallK天然会偏高如果用曝光未点击做负样本RecallK会明显偏低。所以在看离线指标时一定要搞清楚负样本是怎么来的不能跨实验直接比绝对值。我在实际项目里还会加一个指标叫“正样本平均分位数”。做法是对于测试集里的每个正样本计算它在100个随机负样本中的分数排行分位数然后对所有正样本求平均。这个指标能更精细地反映双塔模型“把正样本排多高”对调参更敏感比RecallK更好用。5. 线上部署与向量检索5.1 用户塔和物品塔的异步计算双塔模型能支撑千万级物品检索的秘诀就是两侧可以完全异步计算。物品塔物品特征变化比较慢类目、品牌、标题基本都是静态的所以可以离线全量跑一遍把几千万个物品的Embedding算好存入向量搜索引擎。每隔几小时或每天更新一次全量加上一个增量更新的管道处理新物品。用户塔用户特征变化很快刚点了新视频、新加了购物车需要在请求时实时计算用户向量。线上服务加载用户塔的模型权重输入请求里的用户特征几毫秒算出一个用户向量。用户向量算出来之后去向量检索系统里查TopK个物品。这整个过程只涉及一次DNN前向和一次ANN检索延迟能做到10~20毫秒以内比传统的遍历全库打分方案快了不止一个量级。5.2 向量检索工具怎么选向量检索这个环节没有太多自研空间主流方案如下方案适用规模特点FAISS千万级以下检索能力极强可以直接内嵌到推荐服务里运维简单HNSW类如hnswlib千万级以下基于图的算法召回率高索引构建内存占用大Milvus亿级分布式向量数据库需要单独部署集群适合大数据团队Elasticsearch向量插件千万到亿级适合已深度使用ES的团队复用能力强我的建议是起步阶段直接用FAISS就够了。FAISS支持IDMap、IVF、PQ等索引类型千万级物品建索引的开销也不大。等规模涨到亿级以上、需要分布式扩容时再迁移到Milvus这类专用向量库。这里有个容易忽略的细节向量检索之前一定先把物品向量做L2归一化再存进去。因为训练时用的是余弦相似度如果检索时直接用内积顺序可能一致但距离尺度不一样阈值不好控制。归一化后余弦和内积统一阈值设置就只有一个标准。5.3 特征一致性最容易翻车的环节双塔模型上线后效果和离线评测对不上十次里有八次是特征一致性出了问题。典型的场景是这样离线训练时用户塔的输入特征用了“用户最近点击的物品序列”这些是实时特征从日志里读取。但线上的用户向量服务是个独立模块它的特征管道可能来自不同的数据源——比如缓存系统里存的用户画像没有实时行为序列或者序列的更新延迟有20分钟。结果就是线上用户向量和离线训练时完全不是一回事召回效果直接崩。解决这个问题的核心是“特征管道对齐”。具体做法训练样本里记录的每个特征都要能回溯到“在当时那个时间点”线上系统里实际存在的值用户向量的计算服务必须用与训练时完全一致的特征管道来喂数据每一步特征加工逻辑归一化、分桶、序列截断长度保持一致我踩过一次血淋淋的坑线上用户向量计算时少做了一步特征归一化导致所有向量的数值范围比离线大了一个量级ANN检索出来的结果近乎随机。排查了整整一天最后用“线上向量和离线复算向量做余弦相似度对比”才发现问题。从那以后我给自己定了一条铁律上线前必须做向量级一致性校验即随机抽100个用户线上服务算出的向量和离线用同特征重算的向量做相似度对比平均相似度要高于0.99才算通过。6. 常见问题与排查技巧实录6.1 冷启动问题怎么救双塔模型对新用户和新物品天然不友好因为它们的ID Embedding没有训练充分的梯度更新。新用户没有历史行为序列用户塔输入的序列特征为空模型只能依赖年龄、性别、设备等粗粒度特征向量区分度很低新物品Embedding随机初始化和任何用户向量的相似度都很接近针对新物品常用的兜底方案有几种先用物品的内容特征文本、图片Embedding做泛化或者回退到规则召回按类目热门兜底顶上又或者做“双塔物品相似度”的混合召回——新物品先基于内容相似度找一批“相似老物品”用老物品的曝光数据间接预估点击率。针对新用户常规做法是加一个“热门兜底队列”等用户积累了足够行为比如超过5个点击再启用个性化向量召回。6.2 流行度偏差让模型“只会推热门”前面提到batch内负采样会带来流行度偏差严重时会出现头部热门物品的召回占比极高长尾物品曝光极低最后变成变相的“热门榜单”。排查方法很简单统计召回结果里物品的曝光次数分布如果头部物品占比明显超过自然流量分布说明流行度偏差偏大。修正手段我在前面提过两种频率修正、热度特征这里再补充一个亲测有效的思路对正样本的热度做降权。具体做法是当正样本是热门物品时这个样本的loss权重适当降低一些相当于告诉模型“推热门不算本事把长尾推准才算本事”。权重系数可以控制在0.5~0.9之间需要线上AB实验看整体点击率或转化率变化。6.3 向量召回结果“看着相关但没转化”这种现象在电商场景特别常见。用户向量和物品向量的相似度很高召回的商品从类目、品牌角度看都很合理但CTR或CVR偏低。原因在于双塔模型只学到了“相关性”没学到“效率”。用户喜欢看运动鞋但大部分运动鞋用户只会浏览不下单真正转化的是特定价位、特定品牌的几款。这是召回模型和排序模型的本质分工——召回管“相关”精排管“效率”。但如果你发现召回层面就已经把效率信号全丢了可以做一个轻量优化在训练正样本里把“购买/加购”样本的权重调高到“点击”样本的2~3倍让模型在“相关”的基础上尽量偏一点“高效”。这个改动实施成本极低效果往往立竿见影。我把常见问题的排查方向整理成了下面这个速查表基于我自己踩坑总结的现象优先排查方向我的经验提示离线指标高、线上效果差特征一致性、样本分布差异先跑向量级一致性校验召回结果太集中流行度偏差、温度系数过小试试热度特征校准再调温度新物品召回率为0物品Embedding未增量更新检查增量管道和兜底逻辑用户向量算得很慢用户塔结构过深、特征过多精简用户塔DNN层数控制在3层以内训练loss下降但AUC不变Embedding维度太大、过拟合降维到64看验证集Recall变化深夜流量召回率异常特征管道上游任务延迟给用户向量服务加特征快照缓存6.4 双塔的“双”也会带来结构天花板最后说一个方向性问题。双塔结构最大的局限是两侧在最终交互之前完全隔离所以它学不到用户和物品之间细粒度的交叉特征。打个比方精排模型能轻松学到“25岁男性运动鞋类目高客单价品牌”这种三重交叉但双塔模型很难精确建模这种高阶交互。因此业界在双塔之外出现了不少改进方案比如把双塔最后一层换成多向量的“多向量召回”或者用交叉注意力做更深的交互但牺牲一部分性能。但我个人的建议是双塔模型先做到位再谈升级。大部分团队的召回效果问题根源不在模型结构而在样本质量、特征一致性、温度调参这些基本功上。在我做过的一个视频推荐项目里仅仅把温度系数从0.1调到0.03再配合热度特征校准召回Recall100就从0.51提升到了0.58。相比换一个更复杂的模型结构这些改动性价比高得多。最后分享一个调参小技巧双塔模型上线之后我一般会长期监控一个指标——用户向量的平均模长。如果模长突然变小说明特征管道出问题了如果模长持续偏大说明L2归一化可能没做好。这个指标计算成本极低加个监控能少踩很多坑。另外如果你准备从零开始搭双塔召回我的建议是第一次迭代不要把结构设计得太复杂。先用最简单的“用户ID物品ID类目特征”跑通全流程拿到线上基线再逐步加序列特征、统计特征、温度调优。这样每一步的效果变化都能归因清楚不至于出了问题和面糊一样找不到源头。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。