推荐系统召回双塔模型:从原理到样本构造的工程实践

1. 项目概述:从“区分兴趣”到“召回双塔”

做推荐系统这些年,我越来越觉得,理解一个模型,不能只看它的结构有多精巧,更要看它到底在解决什么问题,以及它解决问题的“姿势”对不对。今天聊的“双塔模型”,就是一个典型的例子。乍一听,这名字挺唬人,好像是什么高深莫测的架构。但如果你理解了推荐系统里“召回”和“精排”这两个阶段最根本的任务差异,你就会发现,双塔模型的设计简直是“召回”阶段的“天选之子”。它的核心思想,用一句话概括就是:召回的目的是把用户可能“感兴趣”的东西从海量物品中捞出来,而精排则是从这些捞出来的东西里,再挑出用户“非常感兴趣”的。双塔模型,就是为了高效、精准地完成前一个任务——大规模“捞取”而生的。

为什么这么说?想象一下,你是一个图书管理员,面对一个拥有上千万本书的巨型图书馆。用户走进来,说:“我想找点有意思的历史书看看。”你的工作分两步:第一步,你不可能把整个图书馆的书都搬到他面前让他一本本挑,你得先快速地从“科幻”、“烹饪”、“计算机”等几十个大类里,把“历史”这个大类的书,以及可能和“历史”沾边的“传记”、“考古”等相邻类目的书,统统先找出来,堆成一个小山。这个过程,就是“召回”。第二步,用户面对这座由几百上千本书堆成的小山,开始仔细翻阅,比较哪本书的作者更权威、内容更生动、装帧更精美,最终选出三五本借走。这个过程,就是“精排”。

双塔模型,就是你完成第一步“快速找书”的那个高效工具。它的设计哲学完全服务于“快速筛选”而非“精细比较”。理解了“召回是区分感兴趣和不感兴趣的”这个核心目标,你就能明白双塔模型里每一个设计选择背后的逻辑,尤其是今天要重点拆解的正负样本构造,这直接决定了你这个“图书管理员”的判断标准是否准确。样本没选对,后面模型学得再好,也是南辕北辙。

2. 召回阶段的核心目标与双塔模型的适配性

2.1 召回 vs. 精排:任务本质的差异

要理解双塔模型,必须先把召回和精排这两个阶段掰扯清楚。很多新手容易混淆,觉得不都是预测用户喜不喜欢吗?其实,它们的任务目标、技术约束和评价标准有着本质区别。

召回阶段的核心目标是“全”和“快”。

  • :面对百万、千万甚至亿级的物品库(我们称之为“候选集”),召回模型的目标是尽可能不遗漏(高召回率)任何用户可能感兴趣的物品。用刚才图书馆的例子,就是宁可多拿几本可能相关的书,也绝不能漏掉那本用户最想看的。因为一旦在召回阶段被漏掉,无论后面的精排模型多强大,这个物品也永远没有机会展现给用户了。所以,召回模型是在做一个“大海捞针”的粗筛。
  • :由于候选集巨大,必须在极短的时间内(通常要求毫秒级)完成对所有物品的筛选。这就决定了召回模型不能太复杂,计算开销必须小。你不可能对每一本书都做长达几分钟的深度阅读来判断它是否属于历史类。

精排阶段的核心目标是“准”和“细”。

  • :它的输入是召回阶段筛选出的几百到几千个物品(我们称之为“精排候选集”)。这个集合已经相对较小,且质量较高(都是用户可能感兴趣的)。精排的任务是在这个“小池塘”里,精准地预测用户对每个物品的偏好程度(如点击率、转化率、观看时长等),并进行严格排序。
  • :为了达到“准”,精排模型可以做得非常复杂,使用海量的特征(用户画像、物品属性、丰富的上下文特征、复杂的交叉特征等),进行深度的非线性建模。因为它只需要处理几千个样本,有足够的计算资源和时间进行“精细比较”。

2.2 双塔模型为何是召回阶段的“最优解”?

双塔模型的结构完美契合了召回阶段“全”和“快”的要求。它的基本结构如下图所示(此处为概念描述):模型分为两个独立的“塔”,通常是结构相同或相似的两个神经网络。一个塔负责处理用户相关的特征(用户ID、历史行为、画像等),输出一个固定长度的向量,称为“用户向量”(User Embedding)。另一个塔负责处理物品相关的特征(物品ID、属性、标签等),输出一个同样长度的“物品向量”(Item Embedding)。

它的核心优势在于“离线计算”和“在线快速检索”:

  1. 离线计算物品向量:我们可以提前(比如每天一次)将全量物品库中的每一个物品,都通过物品塔计算好其对应的物品向量,并存储到向量数据库中。这一步计算量虽大,但因为是离线进行,时间不是问题。
  2. 在线快速检索:当用户发起请求时,我们只需要实时地将用户特征输入用户塔,计算出该用户的用户向量(一次前向传播,速度极快)。然后,将这个用户向量作为“查询条件”,去向量数据库中,通过近似最近邻搜索(ANN,如 Faiss, HNSW 等)技术,快速找出与之最相似的Top-K个物品向量。这些物品对应的物品,就是召回的结果。

这个过程就像提前给图书馆里的每一本书都贴上一个包含其核心内容的“特征条形码”(物品向量)。当用户来时,我们快速生成一个代表他需求的“需求条形码”(用户向量),然后用一个高速扫码枪(ANN检索)去匹配最相似的书籍条形码。这个方案将在线计算的复杂度从 O(N)(遍历所有物品)降低到了 O(1)(计算一次用户向量)加上 O(log N)(高效检索),从而实现了在海量候选集中的毫秒级响应。

而这一切高效检索的基础,在于用户向量和物品向量在一个共享的语义空间里,它们的相似度(通常用内积或余弦相似度)能够准确反映用户对该物品的感兴趣程度。如何让模型学会这个“准确反映”?关键就在于训练时喂给它的“教材”——也就是正负样本

3. 双塔模型训练的核心:正负样本的选择艺术

如果说双塔模型的结构是它的“骨架”,那么训练样本就是它的“血液”和“养料”。样本,尤其是负样本的选择,直接决定了模型学习到的“兴趣区分”标准是什么。在召回场景下,我们的目标是将用户感兴趣的物品(正样本)与用户不感兴趣的物品(负样本)分开。这听起来简单,但实操中陷阱重重。

3.1 正样本的定义:什么是“感兴趣”?

正样本相对明确,通常是用户有过明确正向交互的物品。具体定义取决于业务目标:

  • 点击率(CTR)导向:用户点击过的物品即为正样本。
  • 转化率(CVR)导向:用户购买、付费、完播等深度转化行为对应的物品为正样本。
  • 时长/互动导向:用户观看视频时长超过一定阈值、或进行了点赞、评论、分享的物品为正样本。

实操心得一:正样本的“纯度”与“代表性”在实际操作中,正样本的构造也需要精细处理。并非所有点击都是平等的。例如,在信息流场景中,用户可能因为封面党误点,停留不到1秒就关闭,这种样本作为正样本的“信号”就很弱。我通常的做法是引入“有效点击”的概念,结合点击后的停留时长、是否滑动等后续行为,定义一个综合阈值来筛选高质量正样本。同时,要警惕“曝光偏差”,即模型只学会了预测“容易被系统展示”的物品,而不是用户“真正感兴趣”的物品。适当引入一些随机探索曝光的数据作为补充,有助于缓解这个问题。

3.2 负样本的选择:难点与主流策略

负样本的选择才是真正的技术活和艺术活。我们不能简单地把用户没点过的物品都当作负样本,因为:

  1. 未曝光物品:用户根本没看到过这个物品,谈不上“不感兴趣”,这属于“未知”样本。如果将其作为负样本,模型会错误地学习到“只要不展示给用户,用户就不感兴趣”的荒谬结论。
  2. 曝光未点击物品:这是最常用的负样本来源,因为它包含了明确的用户反馈——系统展示了,但用户没点。这通常被认为是用户“不感兴趣”的信号。

基于此,业界演化出几种主流的负样本采样策略:

策略一:全局随机采样从全量物品库中随机抽取一部分物品作为负样本。这是最简单的方法,但问题很大。它会让模型面临大量“简单负样本”(用户根本不可能感兴趣的物品,比如给足球迷推荐口红),模型很快就能学会区分这些“简单负例”,导致在“困难负例”(用户可能有点兴趣但最终没点的物品)上的区分能力不足,模型效果容易陷入瓶颈。

策略二:曝光未点击采样即使用户在一次请求中曝光了但未点击的物品作为负样本。这是目前最主流、最基础的做法。它在一定程度上模拟了召回场景:模型需要从曝光过的物品中,找出用户感兴趣的那一个。但这里也有坑:

  • 位置偏差:排在前面的物品仅仅因为位置好而获得了点击,未点击不一定是不感兴趣。需要结合位置信息进行纠偏。
  • 新鲜度偏差:老物品曝光多,累积的未点击也多,容易被过度打压。

策略三:基于批内负采样这是双塔模型训练中一个非常经典且高效的技巧。在一个训练批次(Batch)内,对于某个用户的正样本物品,同一批次内其他用户的正样本物品,自然成为该用户的负样本。假设一个Batch有B个(用户, 正物品)对,那么对于其中一个用户,他就自动获得了B-1个负样本。

  • 优点:高效,无需额外采样逻辑,且这些负样本通常是热门物品,属于“困难负样本”,能有效提升模型区分度。
  • 缺点:容易导致“流行度偏差”,模型会倾向于给热门物品打低分(因为老被当作负样本),需要配合流行度纠偏技术。

策略四:困难负样本挖掘这是提升模型效果的关键进阶手段。核心思想是主动去寻找那些模型当前容易“搞错”的、与正样本相似的物品作为负样本。

  1. 离线挖掘:定期用上一版模型跑全量数据,对每个用户,找出模型预测分数很高(排名靠前)但用户实际未点击的物品,作为困难负样本加入下一轮训练。
  2. 在线挖掘:在线上服务时,将召回结果中排名靠前但用户未点击的物品,回流到训练数据中作为困难负样本。

注意:困难负样本的引入需要循序渐进,初期模型能力弱时,引入过多困难负例会加大训练难度,可能导致不收敛。通常的做法是,先用“曝光未点击+批内负采样”训练一个基础模型,再逐步加入一定比例的困难负样本进行“强化训练”。

3.3 样本权重与纠偏:让信号更清晰

除了选择,我们还可以通过给样本赋以不同的权重来优化学习过程。

  • 负样本降权:对于“曝光未点击”样本,可以根据曝光位置、用户停留上下文等信息,判断用户是真的不感兴趣还是没注意到,给予不同的权重。疑似误判的可以降低权重。
  • 流行度纠偏:热门物品作为负样本出现的频率天然高,容易被打压过度。可以在损失函数中引入物品频率的逆项进行纠偏,或者对热门物品的负样本进行降采样。
  • 时间衰减:用户兴趣会变化。越久远的行为,其作为正样本的权重应该越低,或者对久远行为产生的负样本进行过滤。

我个人的经验是,构建一个“混合负样本池”效果最好。例如,70%的曝光未点击样本 + 20%的批内负样本 + 10%的困难负样本。这个比例需要在你的业务数据上通过A/B测试来精细调整。

4. 双塔模型的具体实现与训练细节

理解了样本构造的“为什么”,我们来看看具体“怎么做”。这里以一个经典的点击率预测召回双塔模型为例,拆解实现步骤。

4.1 模型架构设计

假设我们有以下特征:

  • 用户侧特征:用户ID(离散),年龄(离散化),性别(离散),近期点击的物品ID序列(离散)。
  • 物品侧特征:物品ID(离散),类别(离散),标签(多值离散)。

模型架构如下:

  1. 输入层:分别接收用户特征和物品特征。
  2. 嵌入层:将所有离散特征(包括ID类)映射为稠密向量(Embedding)。对于用户历史序列,通常会对序列中的物品ID Embedding进行池化操作(如平均池化、注意力池化)得到一个代表用户短期兴趣的向量。
  3. 塔身结构:通常由几层全连接层(Dense Layer)组成,中间使用ReLU等激活函数。用户塔和物品塔的结构可以对称,也可以根据特征复杂度略有不同。最后一层输出维度为d的向量(即user_embeddingitem_embedding),d通常是64, 128, 256等。
  4. 相似度计算:训练时,计算user_embeddingitem_embedding的内积(或余弦相似度),作为用户对物品感兴趣程度的预测分。
# 简化伪代码示意核心结构 import tensorflow as tf class DualTowerModel(tf.keras.Model): def __init__(self, user_feature_columns, item_feature_columns, embedding_dim=128, 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, activation='relu') for units in tower_layers ] + [tf.keras.layers.Dense(embedding_dim, activation=None)]) # 最后一层无激活 # 物品侧特征处理层 self.item_features_layer = tf.keras.layers.DenseFeatures(item_feature_columns) self.item_tower = tf.keras.Sequential([ tf.keras.layers.Dense(units, activation='relu') for units in tower_layers ] + [tf.keras.layers.Dense(embedding_dim, activation=None)]) 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, axis=1, keepdims=True) return output

4.2 损失函数的选择:Pairwise vs. Pointwise

对于召回任务,常用的损失函数是基于Pairwise(样本对)的损失,因为它直接优化排序目标。

  • Pointwise Loss(如交叉熵):将问题视为二分类(点击/未点击)。它独立看待每个样本,适合精排。但对于召回,它没有显式地让正样本的分数高于负样本,在区分“感兴趣”与“不感兴趣”的排序任务上相对间接。
  • Pairwise Loss(如BPR Loss, Margin Loss):直接比较一个正样本和一个负样本的分数差。
    • BPR Loss:最大化正样本分数与负样本分数之差。公式为:-log(sigmoid(正样本分 - 负样本分))。这是非常经典的选择。
    • Margin Loss(Triplet 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, temperature=1.0): # 计算批次内所有用户与所有物品的相似度矩阵 # [batch_size, emb_dim] * [emb_dim, batch_size] -> [batch_size, batch_size] similarity_matrix = tf.matmul(user_emb, item_emb, transpose_b=True) / temperature # 标签是每个用户对应自己正样本的位置(对角线) labels = tf.range(tf.shape(user_emb)[0]) # 计算交叉熵损失 loss = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=labels, logits=similarity_matrix) return tf.reduce_mean(loss)

实操心得二:温度系数(Temperature)的魔力上面代码中的temperature参数至关重要。它控制着Softmax分布的平滑程度。temperature越小(<1),分布越尖锐,模型会更关注最难区分的负样本(困难负例),有助于提升区分度。temperature越大(>1),分布越平缓,学习更温和。通常需要将其作为一个超参数进行调优,我一般从0.1开始尝试。

4.3 训练流程与技巧

  1. 数据准备:按照前述策略构建训练样本(user_features, item_features, label)。正样本label=1,负样本label=0(如果使用Pairwise Loss,则样本对格式为(user, positive_item, negative_item))。
  2. 模型训练:使用Adam等优化器进行训练。由于使用了批内负采样,Batch Size不宜过小,否则负样本数量不足,一般设置256、512甚至更大。
  3. 归一化技巧:对输出的用户向量和物品向量进行L2归一化,将内积计算转化为余弦相似度。这能提高训练稳定性,并使ANN检索中的距离度量更加一致。
  4. 梯度裁剪:对于较深的塔或Embedding层,梯度裁剪可以防止训练不稳定。

5. 线上服务与效果评估

5.1 离线索引构建与ANN检索

模型训练好后,进入部署阶段:

  1. 全量物品向量化:用训练好的物品塔,离线处理全量物品库,生成所有物品的向量,存入向量数据库(如Faiss, Milvus, Elasticsearch with vector plugin)。
  2. ANN索引构建:在向量数据库上建立近似最近邻索引(如HNSW、IVFPQ)。这一步是为了将在线检索的复杂度从线性降为对数级。
  3. 在线服务:线上服务时,实时计算用户向量,然后向ANN服务发起查询,获取最相似的Top-K个物品ID,作为召回结果。

5.2 效果评估指标

召回模型的评估通常分离线和在线两部分:

离线评估:

  • Recall@K / HitRate@K:在测试集上,对于每个用户,模型召回Top-K个物品中,包含用户真实点击(正样本)的比例。这是最核心的指标,直接衡量“捞得全不全”。
  • MAP@K, NDCG@K:这些排序指标在召回阶段也常用,但重要性次于Recall。它们衡量捞出来的物品排序好不好。
  • 多样性、新颖性:也会关注召回结果是否过于集中(多样性差)或总是老物品(新颖性差)。

在线A/B测试:离线指标好,不代表线上效果好。最终一定要通过A/B测试,观察核心业务指标的变化,如:

  • 召回率:线上真实场景下,用户点击的物品有多少比例出自你的召回通道。
  • 下游精排/重排模块的输入质量:你的召回结果,是否提升了精排模型的整体表现(如精排模型的AUC)。
  • 最终业务指标:是否提升了整体的点击率、人均时长、转化率等。

5.3 常见问题与排查技巧实录

在实际应用中,双塔模型召回会遇到各种问题,以下是一些典型场景和我的排查思路:

问题1:离线Recall@K指标很高,但线上效果不升反降。

  • 排查思路
    • 样本穿越:检查训练数据中是否混入了“未来信息”。例如,用今天的数据训练,但样本特征里包含了用户在今天之后才发生的行为。这会导致离线评估虚高。
    • 线上-线下特征不一致:这是最常见的原因。检查线上服务时,用户特征(如历史序列)的生成逻辑、物品特征的获取来源,是否与训练时完全一致。一个字符的差异都可能导致向量空间偏移。
    • ANN检索失真:检查ANN索引构建的参数(如HNSW的efConstruction,M参数)。efSearch参数设置过低会导致检索精度严重下降,拿回的并不是真正的最近邻。可以逐步调高efSearch,观察召回结果的变化,如果线上指标随之提升,说明问题在此。
    • 服务延迟:双塔召回增加了向量检索的环节,检查是否因延迟增加导致精排等下游模块等待超时,影响了最终结果。

问题2:模型倾向于召回热门物品,长尾物品几乎没有曝光机会。

  • 排查思路
    • 样本偏差:检查训练数据中,热门物品作为正样本和负样本的频率。如果热门物品作为负样本(尤其是批内负样本)的频率过高,模型会过度打压它们。需要实施流行度纠偏,如对热门物品的负样本进行降采样,或在损失函数中加入流行度逆权重。
    • Embedding归一化:确保使用了L2归一化。内积操作下,向量模长会影响分数。热门物品的Embedding模长可能在训练中变得很大,导致相似度计算失真。归一化后,所有向量位于超球面上,相似度完全由角度决定,更公平。
    • 引入多样性负样本:在负样本中,除了曝光未点击和批内负样本,可以主动加入一些全局随机采样的长尾物品作为“易负例”,让模型不要只关注困难样本,也给长尾物品一些“露面”的机会。

问题3:训练损失震荡或不收敛。

  • 排查思路
    • 学习率过大:这是首要怀疑对象。尝试降低学习率,或使用学习率预热(Warmup)策略。
    • 梯度爆炸:检查Embedding层或深层网络的梯度。实施梯度裁剪(Gradient Clipping)。
    • 样本噪声过大:检查正样本中是否混入了大量误点击(如刷量数据),或负样本中混入了大量未曝光样本(应使用曝光未点击)。清洗训练数据。
    • 困难负样本比例过高:初期模型能力弱时,过早引入大量困难负样本(如上一版模型的高分未点击物品),会导致学习目标过于困难。应逐步增加其比例。

问题4:用户冷启动或物品冷启动效果差。

  • 排查思路
    • 对于新用户:双塔模型严重依赖用户历史行为。对于新用户,其行为序列为空或很短,用户向量质量差。解决方案是加强用户侧的非ID类特征(如人口属性、设备信息、实时上下文)的建模能力,或者引入一个独立的“冷启动塔”来处理稀疏特征。
    • 对于新物品:新物品没有历史交互数据,物品向量仅由属性特征生成。确保物品属性特征(类别、标签、创作者等)足够丰富和有区分度。可以考虑利用知识图谱或内容理解模型,为新物品生成高质量的属性Embedding作为补充。

双塔模型是推荐系统召回阶段的基石模型,它的高效性和有效性经过了大规模工业实践的检验。但其效果的上限,很大程度上取决于你对业务的理解,尤其是对正负样本这片“数据土壤”的耕耘。样本是模型认知世界的源头,源头偏了,后面的一切努力都可能事倍功半。每一次模型迭代,不妨都先从审视和优化你的样本构造策略开始。在我自己的实践中,花在样本工程上的时间,往往比调参和改结构要多得多,而收益也通常是最显著的。记住,召回的目标不是追求极致的排序准确,而是在浩瀚的物品海洋中,高效、稳健地布下一张疏而不漏的网,把那些可能的光,都先拢到用户面前。双塔,就是织这张网最称手的梭子之一。