推荐系统召回模型:多任务学习与负反馈建模实战 1. 召回模型为什么需要多任务和负反馈推荐系统里召回环节是整个链路的“第一道闸门”。闸门开得好不好直接决定了后面排序、重排能拿到什么样的候选集。早期大家用协同过滤、ItemCF那套简单直接但天花板很明显——用户兴趣稍微复杂一点或者物品侧特征丰富一点就兜不住了。后来双塔模型成为主流把用户和物品分别映射到向量空间用内积或余弦相似度衡量匹配程度工程上容易落地效果也比传统方法稳。但双塔有个天然短板它本质上是一个“单目标”模型。训练时通常只优化一个主目标比如点击率或者转化率。可实际业务里用户的行为是多元的——有人点进去看了三秒就退有人点了之后收藏加购有人直接下单。这些行为背后的兴趣强度完全不同用一个目标去拟合所有行为模型学到的表示就会“糊掉”。更麻烦的是负反馈信号长期被忽视。用户划走、点“不感兴趣”、快速返回这些动作在训练数据里往往被当成简单的负样本甚至直接被丢掉。结果就是模型反复推同类内容用户越用越烦生态越来越窄。多任务学习加负反馈建模就是冲着这两个痛点去的。多任务让模型同时学多个行为目标共享底层表示各自又有独立的输出头负反馈则把“不喜欢”的信号显式建模让模型知道什么该少推、什么该不推。这两件事合在一起召回阶段就能做到既“懂你要什么”也“懂你不要什么”。注意多任务不是简单地把几个loss加起来。任务之间的相关性、梯度冲突、样本空间差异都会影响最终效果。负反馈也不是把负样本比例调高就行关键在于如何定义和利用这些信号。2. 多任务学习的核心设计思路2.1 为什么选多任务而不是单任务单任务模型在召回阶段通常只优化一个目标比如“是否点击”。但点击这个动作太粗了。用户点了一个标题党内容不代表他真的感兴趣用户没点也不代表他不感兴趣可能只是没刷到。多任务学习的逻辑是把用户行为拆成多个层次比如“曝光→点击→停留→互动→转化”每个层次对应一个任务。底层共享embedding和特征交叉层上层每个任务有自己的MLP和输出。这样做的好处有三个。第一底层表示被多个任务共同约束泛化能力更强不容易过拟合到某一个行为的噪声上。第二不同任务的样本空间可以互补。比如点击样本多但转化样本少转化任务可以通过共享底层从点击任务里“借”到信息。第三线上可以灵活调整不同任务的权重适应不同阶段的业务目标。我试过在一个中等规模的推荐场景里对比单任务和多任务。单任务双塔的召回点击率大概在8%左右换成多任务之后点击率没掉但人均停留时长涨了12%负反馈率降了将近20%。这个收益在召回阶段已经相当可观了。2.2 任务之间的相关性怎么处理多任务学习最怕的是任务之间“打架”。比如“点击”和“不感兴趣”这两个任务理论上应该是负相关的——一个东西越容易被点击应该越不容易被标记为不感兴趣。但如果数据里存在大量“误点”或者“诱导点击”这两个任务的梯度就会互相拉扯底层表示学得四不像。处理这个问题常见做法有三种。第一种是硬共享加门控机制让模型自己学每个任务该从共享层拿多少信息。第二种是软共享每个任务有自己的底层但通过正则项约束彼此靠近。第三种是MMoE或者PLE这类结构用多个专家网络加门控让不同任务走不同的专家组合。实际落地时我倾向于从硬共享开始先跑通基线再逐步引入门控。因为门控结构虽然表达能力强但训练不稳定尤其是任务数量超过三个之后门控权重的可解释性会变差。如果团队工程能力一般硬共享加任务权重调参性价比更高。2.3 损失函数的组合方式多任务学习的损失函数通常是加权和L Σ w_i * L_i。这里的w_i怎么定是个经验活。最粗暴的是等权重但效果往往不好。因为不同任务的loss量级可能差很多比如点击的交叉熵可能在0.3左右而停留时长的回归loss可能上百。直接相加回归任务会主导梯度。我的做法是先用等权重跑一版观察每个任务的loss曲线和梯度范数。然后对量级大的任务做归一化或者用不确定性加权Uncertainty Weighting让模型自己学每个任务的权重。后者在工程上更优雅但需要额外注意数值稳定性尤其是任务数量多的时候log方差项容易跑飞。还有一种做法是动态调权比如根据验证集上各任务的表现用帕累托最优的思路去调。这个在学术上很漂亮但线上环境里验证集分布和线上分布经常不一致调出来的权重未必稳。我一般只在离线阶段用动态调权找大致范围线上还是用固定权重加人工微调。3. 负反馈信号的挖掘与建模3.1 负反馈到底包括哪些行为很多人一提负反馈就想到“不感兴趣”按钮。实际上负反馈的范畴大得多。显式负反馈包括点“不感兴趣”、举报、拉黑、取消关注。隐式负反馈包括快速划走停留时长低于阈值、曝光未点击、点击后立即返回、频繁切换品类。这些信号强度不同建模方式也应该不同。显式负反馈信号强但量少而且有偏——只有极少数用户会主动点“不感兴趣”。隐式负反馈量大但噪声也大。比如曝光未点击可能是用户没看到也可能是看到了不感兴趣还可能是位置太靠后。直接当负样本会把很多“中性”甚至“潜在正样本”打成负类。我的经验是分层处理。显式负反馈给高权重直接进负样本池并且可以在损失函数里加一个额外的惩罚项。隐式负反馈则根据置信度分级比如“点击后3秒内返回”置信度高“曝光未点击”置信度低。低置信度的信号不直接当负样本而是作为弱监督信号用对比学习或者辅助任务的方式引入。3.2 负反馈样本的构造策略负样本构造是召回模型的核心环节之一。传统做法是全局随机采样但这样采出来的负样本太“容易”模型学不到有区分度的边界。后来大家用in-batch负采样把同一个batch里其他用户的正面样本当负样本难度上来了但存在“假负例”问题——同一个物品可能对多个用户都是正样本。引入负反馈之后负样本池可以分三层。第一层是全局随机负样本保证覆盖度。第二层是曝光未点击负样本难度中等数量大。第三层是显式负反馈样本难度最高数量最少。训练时按比例混合比如1:3:1。这样模型既能学到粗粒度的区分也能在细粒度上把“讨厌”的东西推开。注意负反馈样本不能无限加权。如果负反馈样本占比过高模型会变得过于保守召回结果多样性下降甚至出现“什么都不推”的情况。我一般把负反馈样本的总权重控制在20%到30%之间具体看业务对多样性和准确性的权衡。3.3 负反馈在模型结构里的接入方式负反馈信号接入模型有两种主流方式。一种是作为额外的输入特征比如把“用户对该品类的历史负反馈次数”作为一个特征拼接到用户侧塔的输入里。这种方式简单但负反馈信号只在输入层起作用深层表示未必能充分捕捉。另一种是作为独立的任务比如加一个“是否负反馈”的二分类头和点击、转化等任务并列。这样负反馈信号会参与底层表示的学习影响整个模型的梯度方向。我倾向于第二种因为召回阶段的核心是表示学习让负反馈直接影响表示比只当特征更有效。还有一种更激进的做法是在损失函数里加一个“推开”项。对于显式负反馈的(user, item)对不仅不鼓励它们的内积大反而鼓励内积小。这相当于在向量空间里主动把负反馈物品推离用户。实测下来这种方式对降低负反馈率效果明显但需要小心控制推力强度否则会把用户表示推到一个“孤岛”上导致召回结果过于单一。4. 从离线到线上的完整实操流程4.1 数据准备与特征工程数据这块第一步是定义清楚什么是“正样本”、什么是“负样本”、什么是“负反馈样本”。以内容推荐为例正样本可以是“点击且停留超过10秒”负样本是“曝光未点击”负反馈样本是“点击后3秒内返回”或“显式不感兴趣”。定义不同模型学到的东西完全不同。特征方面用户侧包括基础属性、历史行为序列、品类偏好分布、负反馈统计。物品侧包括内容标签、作者特征、时长、发布时间、历史CTR。交叉特征包括用户对某品类的历史点击率、用户对某作者的负反馈率。序列特征建议用Transformer或者GRU编码比简单的平均池化效果好很多。这里有个坑负反馈统计特征一定要做时间衰减。用户三个月前拉黑了一个作者不代表现在还讨厌。我一般用指数衰减半衰期设7到14天具体看业务节奏。另外负反馈特征要做平滑避免“一次负反馈就永久打压”的情况。4.2 模型训练与调参训练流程上我习惯先跑单任务双塔基线确认数据管道和特征没问题。然后逐步加任务每加一个任务观察验证集上各指标的变化。如果某个任务加进去之后主任务指标掉超过1%就要检查任务权重或者样本比例。学习率方面多任务模型比单任务更敏感。我一般用1e-3起步配合warmup和余弦退火。如果用了门控结构门控网络的学习率可以设小一点比如主网络的十分之一避免门控权重震荡。Batch size对负采样影响很大。Batch越大in-batch负样本越多但显存压力也越大。我试过从1024到8192发现4096左右是个比较平衡的点。再大收益递减而且训练时间线性增长。还有一个细节多任务模型的评估不能只看单个任务的AUC。我一般会看三个东西主任务的AUC、负反馈任务的AUC、以及召回结果在验证集上的多样性指标比如品类覆盖数、基尼系数。三个都达标才认为模型是可用的。4.3 线上部署与A/B测试离线指标好不代表线上效果好。召回模型上线第一步是影子模式让新模型和旧模型并行跑只记录不生效。对比两者的召回结果重叠度、品类分布、以及后续排序阶段的点击率。如果重叠度太低说明新模型召回的东西和旧模型差异太大直接切流量风险高。A/B测试阶段我一般先切5%流量观察三天。核心看四个指标人均点击次数、人均停留时长、负反馈率、次日留存。前两个涨后两个不跌才算正向。如果负反馈率涨了哪怕点击率涨了也要谨慎——这说明模型在“讨好”用户但可能在透支长期体验。注意召回模型的A/B测试周期要比排序模型长。因为召回影响的是候选集用户需要多刷几次才能感受到变化。我一般至少观察一周避开周末和工作日的分布差异。5. 常见问题与排查技巧实录5.1 多任务模型训练不收敛怎么办这是最常见的问题。表现是loss震荡或者某个任务的loss一直不降。排查顺序如下先看数据确认各任务的样本量和特征分布没有异常再看梯度用TensorBoard或者WandB监控每个任务的梯度范数如果某个任务的梯度比其他任务大一个量级以上说明权重需要调最后看结构如果用了门控检查门控输出是否饱和。我遇到过一次负反馈任务的loss一直不降后来发现是负反馈样本里混入了大量“误点”数据。用户手滑点进去又退出来被当成了负反馈。清洗数据之后问题解决。所以数据质量永远比模型结构重要。5.2 负反馈率降了但点击率也降了这说明模型过于保守。可能的原因有三个负反馈样本权重过高、负反馈任务的学习率过大、或者负反馈特征的时间衰减太慢。我一般先调负反馈样本权重从30%降到20%试试。如果还不行检查负反馈任务的梯度是否主导了底层表示。可以用梯度裁剪或者任务-specific的BN层来缓解。另一个思路是引入“探索”机制。在召回阶段保留一定比例的随机或半随机结果让模型有机会接触到新的正样本。这个比例一般控制在5%到10%太高会影响短期指标太低起不到探索作用。5.3 线上效果和离线不一致这是推荐系统的经典问题。召回阶段尤其明显因为离线评估用的是历史日志而线上模型会影响用户行为产生新的日志分布。缓解办法有三个一是用离线模拟器做反事实评估虽然不完美但比纯离线指标靠谱二是做小流量在线学习用实时反馈更新模型三是监控特征分布漂移一旦发现线上特征和训练特征差异大及时重新训练。我自己的习惯是每次模型上线前都会用最近三天的日志做一次“时间外”验证。如果时间外验证的指标比随机划分验证低超过5%说明模型对时间敏感需要加时间特征或者缩短训练窗口。5.4 负反馈信号稀疏怎么处理显式负反馈量少是常态。我的做法是第一用隐式负反馈做数据增强比如把“快速划走”也纳入负反馈任务但给低权重第二用元学习或者迁移学习从其他 domain 的负反馈数据里预训练第三在损失函数里用focal loss让模型更关注难分的负反馈样本。还有一个偏方把负反馈任务和点击任务做联合训练但负反馈任务的输出头初始化时偏置设负值让模型一开始倾向于“不认为有负反馈”然后慢慢学。这样能缓解稀疏信号导致的早期训练不稳定。5.5 多任务权重怎么调最省事如果团队没有专门的调参资源我建议用“主任务优先”策略。先固定主任务权重为1其他任务从0.1开始每次增加0.1观察主任务指标。找到主任务指标不降的最大辅助任务权重就停。这个方法虽然粗糙但胜在稳定不容易翻车。如果任务数量超过五个建议上不确定性加权但要做好数值裁剪。我一般把log方差限制在[-3, 3]之间防止跑飞。另外不确定性加权的初始值可以设成每个任务loss的倒数这样一开始的权重比较合理。6. 一些踩过的坑和实操心得第一个坑负反馈样本的时间戳一定要对齐。我遇到过负反馈样本的时间戳比正样本晚了一天导致模型在训练时“偷看”了未来信息。线上效果虚高切流量之后直接崩。后来加了严格的时间窗口校验才解决。第二个坑多任务模型的embedding维度不要设太大。单任务的时候64维或者128维够用。多任务之后底层共享embedding如果太大容易过拟合而且训练慢。我一般控制在64维个别任务如果不够在上层MLP里加宽。第三个坑负反馈任务不要用和点击任务一样的负采样策略。点击任务的负样本可以随机采负反馈任务的负样本必须从“曝光未点击”里采而且要做难度分层。否则模型学到的负反馈边界太粗线上区分度不够。第四个坑A/B测试的时候一定要看长期指标。我见过一个模型短期点击率涨了8%但一周后次日留存掉了3%。原因是模型过度推短平快的内容用户刷的时候爽刷完就腻。召回模型对生态的影响是慢性的短期指标好看不代表真的好。第五个坑模型上线之后负反馈信号的分布会变。因为用户看到新模型推的东西负反馈行为也会变。所以负反馈任务需要定期重新训练我一般两周更新一次负反馈任务的输出头底层表示可以一个月更新一次。最后分享一个小技巧如果负反馈信号实在太稀疏可以先用规则做一版“硬打压”比如用户拉黑了某个作者召回阶段直接过滤掉这个作者的所有内容。等负反馈模型训练得差不多了再逐步用模型替代规则。这样既能快速见效又能给模型争取训练时间。这个方向后续还可以往几个方向扩展。一是引入因果推断区分“因为看到所以不喜欢”和“因为不喜欢所以不点”这两种完全不同的负反馈机制。二是做跨域负反馈迁移把用户在短视频里的负反馈信号迁移到图文推荐里。三是把负反馈建模和强化学习结合让模型在召回阶段就考虑长期收益而不是只看单次交互。这些方向我都在关注有些已经在小流量实验里跑起来了等有稳定结论再单独写一篇。