推荐系统召回双塔模型:从原理到样本构造的工程实践
1. 项目概述从“区分兴趣”到“召回双塔”做推荐系统这些年我越来越觉得理解一个模型不能只看它的结构有多精巧更要看它到底在解决什么问题以及它解决问题的“姿势”对不对。今天聊的“双塔模型”就是一个典型的例子。乍一听这名字挺唬人好像是什么高深莫测的架构。但如果你理解了推荐系统里“召回”和“精排”这两个阶段最根本的任务差异你就会发现双塔模型的设计简直是“召回”阶段的“天选之子”。它的核心思想用一句话概括就是召回的目的是把用户可能“感兴趣”的东西从海量物品中捞出来而精排则是从这些捞出来的东西里再挑出用户“非常感兴趣”的。双塔模型就是为了高效、精准地完成前一个任务——大规模“捞取”而生的。为什么这么说想象一下你是一个图书管理员面对一个拥有上千万本书的巨型图书馆。用户走进来说“我想找点有意思的历史书看看。”你的工作分两步第一步你不可能把整个图书馆的书都搬到他面前让他一本本挑你得先快速地从“科幻”、“烹饪”、“计算机”等几十个大类里把“历史”这个大类的书以及可能和“历史”沾边的“传记”、“考古”等相邻类目的书统统先找出来堆成一个小山。这个过程就是“召回”。第二步用户面对这座由几百上千本书堆成的小山开始仔细翻阅比较哪本书的作者更权威、内容更生动、装帧更精美最终选出三五本借走。这个过程就是“精排”。双塔模型就是你完成第一步“快速找书”的那个高效工具。它的设计哲学完全服务于“快速筛选”而非“精细比较”。理解了“召回是区分感兴趣和不感兴趣的”这个核心目标你就能明白双塔模型里每一个设计选择背后的逻辑尤其是今天要重点拆解的正负样本构造这直接决定了你这个“图书管理员”的判断标准是否准确。样本没选对后面模型学得再好也是南辕北辙。2. 召回阶段的核心目标与双塔模型的适配性2.1 召回 vs. 精排任务本质的差异要理解双塔模型必须先把召回和精排这两个阶段掰扯清楚。很多新手容易混淆觉得不都是预测用户喜不喜欢吗其实它们的任务目标、技术约束和评价标准有着本质区别。召回阶段的核心目标是“全”和“快”。全面对百万、千万甚至亿级的物品库我们称之为“候选集”召回模型的目标是尽可能不遗漏高召回率任何用户可能感兴趣的物品。用刚才图书馆的例子就是宁可多拿几本可能相关的书也绝不能漏掉那本用户最想看的。因为一旦在召回阶段被漏掉无论后面的精排模型多强大这个物品也永远没有机会展现给用户了。所以召回模型是在做一个“大海捞针”的粗筛。快由于候选集巨大必须在极短的时间内通常要求毫秒级完成对所有物品的筛选。这就决定了召回模型不能太复杂计算开销必须小。你不可能对每一本书都做长达几分钟的深度阅读来判断它是否属于历史类。精排阶段的核心目标是“准”和“细”。准它的输入是召回阶段筛选出的几百到几千个物品我们称之为“精排候选集”。这个集合已经相对较小且质量较高都是用户可能感兴趣的。精排的任务是在这个“小池塘”里精准地预测用户对每个物品的偏好程度如点击率、转化率、观看时长等并进行严格排序。细为了达到“准”精排模型可以做得非常复杂使用海量的特征用户画像、物品属性、丰富的上下文特征、复杂的交叉特征等进行深度的非线性建模。因为它只需要处理几千个样本有足够的计算资源和时间进行“精细比较”。2.2 双塔模型为何是召回阶段的“最优解”双塔模型的结构完美契合了召回阶段“全”和“快”的要求。它的基本结构如下图所示此处为概念描述模型分为两个独立的“塔”通常是结构相同或相似的两个神经网络。一个塔负责处理用户相关的特征用户ID、历史行为、画像等输出一个固定长度的向量称为“用户向量”User Embedding。另一个塔负责处理物品相关的特征物品ID、属性、标签等输出一个同样长度的“物品向量”Item Embedding。它的核心优势在于“离线计算”和“在线快速检索”离线计算物品向量我们可以提前比如每天一次将全量物品库中的每一个物品都通过物品塔计算好其对应的物品向量并存储到向量数据库中。这一步计算量虽大但因为是离线进行时间不是问题。在线快速检索当用户发起请求时我们只需要实时地将用户特征输入用户塔计算出该用户的用户向量一次前向传播速度极快。然后将这个用户向量作为“查询条件”去向量数据库中通过近似最近邻搜索ANN如 Faiss, HNSW 等技术快速找出与之最相似的Top-K个物品向量。这些物品对应的物品就是召回的结果。这个过程就像提前给图书馆里的每一本书都贴上一个包含其核心内容的“特征条形码”物品向量。当用户来时我们快速生成一个代表他需求的“需求条形码”用户向量然后用一个高速扫码枪ANN检索去匹配最相似的书籍条形码。这个方案将在线计算的复杂度从 O(N)遍历所有物品降低到了 O(1)计算一次用户向量加上 O(log N)高效检索从而实现了在海量候选集中的毫秒级响应。而这一切高效检索的基础在于用户向量和物品向量在一个共享的语义空间里它们的相似度通常用内积或余弦相似度能够准确反映用户对该物品的感兴趣程度。如何让模型学会这个“准确反映”关键就在于训练时喂给它的“教材”——也就是正负样本。3. 双塔模型训练的核心正负样本的选择艺术如果说双塔模型的结构是它的“骨架”那么训练样本就是它的“血液”和“养料”。样本尤其是负样本的选择直接决定了模型学习到的“兴趣区分”标准是什么。在召回场景下我们的目标是将用户感兴趣的物品正样本与用户不感兴趣的物品负样本分开。这听起来简单但实操中陷阱重重。3.1 正样本的定义什么是“感兴趣”正样本相对明确通常是用户有过明确正向交互的物品。具体定义取决于业务目标点击率CTR导向用户点击过的物品即为正样本。转化率CVR导向用户购买、付费、完播等深度转化行为对应的物品为正样本。时长/互动导向用户观看视频时长超过一定阈值、或进行了点赞、评论、分享的物品为正样本。实操心得一正样本的“纯度”与“代表性”在实际操作中正样本的构造也需要精细处理。并非所有点击都是平等的。例如在信息流场景中用户可能因为封面党误点停留不到1秒就关闭这种样本作为正样本的“信号”就很弱。我通常的做法是引入“有效点击”的概念结合点击后的停留时长、是否滑动等后续行为定义一个综合阈值来筛选高质量正样本。同时要警惕“曝光偏差”即模型只学会了预测“容易被系统展示”的物品而不是用户“真正感兴趣”的物品。适当引入一些随机探索曝光的数据作为补充有助于缓解这个问题。3.2 负样本的选择难点与主流策略负样本的选择才是真正的技术活和艺术活。我们不能简单地把用户没点过的物品都当作负样本因为未曝光物品用户根本没看到过这个物品谈不上“不感兴趣”这属于“未知”样本。如果将其作为负样本模型会错误地学习到“只要不展示给用户用户就不感兴趣”的荒谬结论。曝光未点击物品这是最常用的负样本来源因为它包含了明确的用户反馈——系统展示了但用户没点。这通常被认为是用户“不感兴趣”的信号。基于此业界演化出几种主流的负样本采样策略策略一全局随机采样从全量物品库中随机抽取一部分物品作为负样本。这是最简单的方法但问题很大。它会让模型面临大量“简单负样本”用户根本不可能感兴趣的物品比如给足球迷推荐口红模型很快就能学会区分这些“简单负例”导致在“困难负例”用户可能有点兴趣但最终没点的物品上的区分能力不足模型效果容易陷入瓶颈。策略二曝光未点击采样即使用户在一次请求中曝光了但未点击的物品作为负样本。这是目前最主流、最基础的做法。它在一定程度上模拟了召回场景模型需要从曝光过的物品中找出用户感兴趣的那一个。但这里也有坑位置偏差排在前面的物品仅仅因为位置好而获得了点击未点击不一定是不感兴趣。需要结合位置信息进行纠偏。新鲜度偏差老物品曝光多累积的未点击也多容易被过度打压。策略三基于批内负采样这是双塔模型训练中一个非常经典且高效的技巧。在一个训练批次Batch内对于某个用户的正样本物品同一批次内其他用户的正样本物品自然成为该用户的负样本。假设一个Batch有B个(用户, 正物品)对那么对于其中一个用户他就自动获得了B-1个负样本。优点高效无需额外采样逻辑且这些负样本通常是热门物品属于“困难负样本”能有效提升模型区分度。缺点容易导致“流行度偏差”模型会倾向于给热门物品打低分因为老被当作负样本需要配合流行度纠偏技术。策略四困难负样本挖掘这是提升模型效果的关键进阶手段。核心思想是主动去寻找那些模型当前容易“搞错”的、与正样本相似的物品作为负样本。离线挖掘定期用上一版模型跑全量数据对每个用户找出模型预测分数很高排名靠前但用户实际未点击的物品作为困难负样本加入下一轮训练。在线挖掘在线上服务时将召回结果中排名靠前但用户未点击的物品回流到训练数据中作为困难负样本。注意困难负样本的引入需要循序渐进初期模型能力弱时引入过多困难负例会加大训练难度可能导致不收敛。通常的做法是先用“曝光未点击批内负采样”训练一个基础模型再逐步加入一定比例的困难负样本进行“强化训练”。3.3 样本权重与纠偏让信号更清晰除了选择我们还可以通过给样本赋以不同的权重来优化学习过程。负样本降权对于“曝光未点击”样本可以根据曝光位置、用户停留上下文等信息判断用户是真的不感兴趣还是没注意到给予不同的权重。疑似误判的可以降低权重。流行度纠偏热门物品作为负样本出现的频率天然高容易被打压过度。可以在损失函数中引入物品频率的逆项进行纠偏或者对热门物品的负样本进行降采样。时间衰减用户兴趣会变化。越久远的行为其作为正样本的权重应该越低或者对久远行为产生的负样本进行过滤。我个人的经验是构建一个“混合负样本池”效果最好。例如70%的曝光未点击样本 20%的批内负样本 10%的困难负样本。这个比例需要在你的业务数据上通过A/B测试来精细调整。4. 双塔模型的具体实现与训练细节理解了样本构造的“为什么”我们来看看具体“怎么做”。这里以一个经典的点击率预测召回双塔模型为例拆解实现步骤。4.1 模型架构设计假设我们有以下特征用户侧特征用户ID离散年龄离散化性别离散近期点击的物品ID序列离散。物品侧特征物品ID离散类别离散标签多值离散。模型架构如下输入层分别接收用户特征和物品特征。嵌入层将所有离散特征包括ID类映射为稠密向量Embedding。对于用户历史序列通常会对序列中的物品ID Embedding进行池化操作如平均池化、注意力池化得到一个代表用户短期兴趣的向量。塔身结构通常由几层全连接层Dense Layer组成中间使用ReLU等激活函数。用户塔和物品塔的结构可以对称也可以根据特征复杂度略有不同。最后一层输出维度为d的向量即user_embedding和item_embeddingd通常是64, 128, 256等。相似度计算训练时计算user_embedding和item_embedding的内积或余弦相似度作为用户对物品感兴趣程度的预测分。# 简化伪代码示意核心结构 import tensorflow as tf class DualTowerModel(tf.keras.Model): def __init__(self, user_feature_columns, item_feature_columns, embedding_dim128, tower_layers[256, 128]): super().__init__() # 用户侧特征处理层 self.user_features_layer tf.keras.layers.DenseFeatures(user_feature_columns) self.user_tower tf.keras.Sequential([ tf.keras.layers.Dense(units, activationrelu) for units in tower_layers ] [tf.keras.layers.Dense(embedding_dim, activationNone)]) # 最后一层无激活 # 物品侧特征处理层 self.item_features_layer tf.keras.layers.DenseFeatures(item_feature_columns) self.item_tower tf.keras.Sequential([ tf.keras.layers.Dense(units, activationrelu) for units in tower_layers ] [tf.keras.layers.Dense(embedding_dim, activationNone)]) def call(self, inputs): user_inputs, item_inputs inputs # 分别通过两个塔 user_emb self.user_tower(self.user_features_layer(user_inputs)) item_emb self.item_tower(self.item_features_layer(item_inputs)) # 计算内积作为预测分 output tf.reduce_sum(user_emb * item_emb, axis1, keepdimsTrue) return output4.2 损失函数的选择Pairwise vs. Pointwise对于召回任务常用的损失函数是基于Pairwise样本对的损失因为它直接优化排序目标。Pointwise Loss如交叉熵将问题视为二分类点击/未点击。它独立看待每个样本适合精排。但对于召回它没有显式地让正样本的分数高于负样本在区分“感兴趣”与“不感兴趣”的排序任务上相对间接。Pairwise Loss如BPR Loss, Margin Loss直接比较一个正样本和一个负样本的分数差。BPR Loss最大化正样本分数与负样本分数之差。公式为-log(sigmoid(正样本分 - 负样本分))。这是非常经典的选择。Margin LossTriplet Loss让正样本分数至少比负样本分数高出一个边界值margin。公式为max(0, 负样本分 - 正样本分 margin)。在双塔模型中结合批内负采样使用Softmax交叉熵损失是一种非常高效且效果出色的做法。它将一个用户的正样本与批次内所有其他物品包括该用户的正样本和所有其他用户的物品后者自然成为负样本一起视为一个多分类问题。这等价于一种高效的Pairwise学习。# 使用批内负采样时的Softmax损失示例 # 假设 user_emb: [batch_size, emb_dim], item_emb: [batch_size, emb_dim] 其中每个item是对应用户的正样本 def batch_softmax_loss(user_emb, item_emb, temperature1.0): # 计算批次内所有用户与所有物品的相似度矩阵 # [batch_size, emb_dim] * [emb_dim, batch_size] - [batch_size, batch_size] similarity_matrix tf.matmul(user_emb, item_emb, transpose_bTrue) / temperature # 标签是每个用户对应自己正样本的位置对角线 labels tf.range(tf.shape(user_emb)[0]) # 计算交叉熵损失 loss tf.nn.sparse_softmax_cross_entropy_with_logits(labelslabels, logitssimilarity_matrix) return tf.reduce_mean(loss)实操心得二温度系数Temperature的魔力上面代码中的temperature参数至关重要。它控制着Softmax分布的平滑程度。temperature越小1分布越尖锐模型会更关注最难区分的负样本困难负例有助于提升区分度。temperature越大1分布越平缓学习更温和。通常需要将其作为一个超参数进行调优我一般从0.1开始尝试。4.3 训练流程与技巧数据准备按照前述策略构建训练样本(user_features, item_features, label)。正样本label1负样本label0如果使用Pairwise Loss则样本对格式为(user, positive_item, negative_item)。模型训练使用Adam等优化器进行训练。由于使用了批内负采样Batch Size不宜过小否则负样本数量不足一般设置256、512甚至更大。归一化技巧对输出的用户向量和物品向量进行L2归一化将内积计算转化为余弦相似度。这能提高训练稳定性并使ANN检索中的距离度量更加一致。梯度裁剪对于较深的塔或Embedding层梯度裁剪可以防止训练不稳定。5. 线上服务与效果评估5.1 离线索引构建与ANN检索模型训练好后进入部署阶段全量物品向量化用训练好的物品塔离线处理全量物品库生成所有物品的向量存入向量数据库如Faiss, Milvus, Elasticsearch with vector plugin。ANN索引构建在向量数据库上建立近似最近邻索引如HNSW、IVFPQ。这一步是为了将在线检索的复杂度从线性降为对数级。在线服务线上服务时实时计算用户向量然后向ANN服务发起查询获取最相似的Top-K个物品ID作为召回结果。5.2 效果评估指标召回模型的评估通常分离线和在线两部分离线评估RecallK / HitRateK在测试集上对于每个用户模型召回Top-K个物品中包含用户真实点击正样本的比例。这是最核心的指标直接衡量“捞得全不全”。MAPK, NDCGK这些排序指标在召回阶段也常用但重要性次于Recall。它们衡量捞出来的物品排序好不好。多样性、新颖性也会关注召回结果是否过于集中多样性差或总是老物品新颖性差。在线A/B测试离线指标好不代表线上效果好。最终一定要通过A/B测试观察核心业务指标的变化如召回率线上真实场景下用户点击的物品有多少比例出自你的召回通道。下游精排/重排模块的输入质量你的召回结果是否提升了精排模型的整体表现如精排模型的AUC。最终业务指标是否提升了整体的点击率、人均时长、转化率等。5.3 常见问题与排查技巧实录在实际应用中双塔模型召回会遇到各种问题以下是一些典型场景和我的排查思路问题1离线RecallK指标很高但线上效果不升反降。排查思路样本穿越检查训练数据中是否混入了“未来信息”。例如用今天的数据训练但样本特征里包含了用户在今天之后才发生的行为。这会导致离线评估虚高。线上-线下特征不一致这是最常见的原因。检查线上服务时用户特征如历史序列的生成逻辑、物品特征的获取来源是否与训练时完全一致。一个字符的差异都可能导致向量空间偏移。ANN检索失真检查ANN索引构建的参数如HNSW的efConstruction,M参数。efSearch参数设置过低会导致检索精度严重下降拿回的并不是真正的最近邻。可以逐步调高efSearch观察召回结果的变化如果线上指标随之提升说明问题在此。服务延迟双塔召回增加了向量检索的环节检查是否因延迟增加导致精排等下游模块等待超时影响了最终结果。问题2模型倾向于召回热门物品长尾物品几乎没有曝光机会。排查思路样本偏差检查训练数据中热门物品作为正样本和负样本的频率。如果热门物品作为负样本尤其是批内负样本的频率过高模型会过度打压它们。需要实施流行度纠偏如对热门物品的负样本进行降采样或在损失函数中加入流行度逆权重。Embedding归一化确保使用了L2归一化。内积操作下向量模长会影响分数。热门物品的Embedding模长可能在训练中变得很大导致相似度计算失真。归一化后所有向量位于超球面上相似度完全由角度决定更公平。引入多样性负样本在负样本中除了曝光未点击和批内负样本可以主动加入一些全局随机采样的长尾物品作为“易负例”让模型不要只关注困难样本也给长尾物品一些“露面”的机会。问题3训练损失震荡或不收敛。排查思路学习率过大这是首要怀疑对象。尝试降低学习率或使用学习率预热Warmup策略。梯度爆炸检查Embedding层或深层网络的梯度。实施梯度裁剪Gradient Clipping。样本噪声过大检查正样本中是否混入了大量误点击如刷量数据或负样本中混入了大量未曝光样本应使用曝光未点击。清洗训练数据。困难负样本比例过高初期模型能力弱时过早引入大量困难负样本如上一版模型的高分未点击物品会导致学习目标过于困难。应逐步增加其比例。问题4用户冷启动或物品冷启动效果差。排查思路对于新用户双塔模型严重依赖用户历史行为。对于新用户其行为序列为空或很短用户向量质量差。解决方案是加强用户侧的非ID类特征如人口属性、设备信息、实时上下文的建模能力或者引入一个独立的“冷启动塔”来处理稀疏特征。对于新物品新物品没有历史交互数据物品向量仅由属性特征生成。确保物品属性特征类别、标签、创作者等足够丰富和有区分度。可以考虑利用知识图谱或内容理解模型为新物品生成高质量的属性Embedding作为补充。双塔模型是推荐系统召回阶段的基石模型它的高效性和有效性经过了大规模工业实践的检验。但其效果的上限很大程度上取决于你对业务的理解尤其是对正负样本这片“数据土壤”的耕耘。样本是模型认知世界的源头源头偏了后面的一切努力都可能事倍功半。每一次模型迭代不妨都先从审视和优化你的样本构造策略开始。在我自己的实践中花在样本工程上的时间往往比调参和改结构要多得多而收益也通常是最显著的。记住召回的目标不是追求极致的排序准确而是在浩瀚的物品海洋中高效、稳健地布下一张疏而不漏的网把那些可能的光都先拢到用户面前。双塔就是织这张网最称手的梭子之一。