推荐系统重训新思路:序列元学习如何解决灾难性遗忘
1. 这篇论文到底在解决什么问题推荐系统这玩意儿做过的人都知道离线训练和线上服务之间有一条很难跨过去的鸿沟。你拿历史日志训一个模型AUC、NDCG看着都挺漂亮一上线就发现效果打折扣。原因不复杂——用户兴趣在漂移物品池在变分布也在变。传统做法是定期全量重训或者用增量学习去微调。但这两条路都有各自的坑全量重训成本高、周期长中间那段时间模型就是“过期”的增量学习看起来轻量但它对旧知识的保持能力很弱学新忘旧灾难性遗忘几乎是必然。这篇论文的标题是“How to Retrain Recommender System: A Sequential Meta-Learning Method”翻译过来就是“如何重训推荐系统一种序列元学习方法”。它要回答的核心问题非常具体在推荐系统需要更新的时候到底该怎么更新才能既吸收新数据里的新趋势又不把旧数据里学到的稳定偏好丢掉。我先把结论摆出来这篇论文提出的方法叫序列元学习英文是Sequential Meta-Learning简称SML。它的核心思路是把“重训”这件事本身建模成一个元学习任务让模型学会“如何更新参数”这件事而不是直接去学“参数应该是什么”。这个区别很关键后面我会详细拆。适合读这篇论文的人我觉得有三类。第一类是做推荐系统工程落地的尤其是那些被“模型更新周期”折磨过的同学。第二类是对元学习感兴趣、想看看元学习怎么在推荐场景里落地的人。第三类是做增量学习或者持续学习研究的因为这篇论文本质上是在解决一个持续学习里的稳定性-可塑性平衡问题只不过它把场景限定在了推荐系统。2. 为什么传统重训方案不够用2.1 全量重训的代价与滞后全量重训的逻辑很简单攒一批新数据和旧数据混在一起从头训一个模型。这个做法最稳因为它不存在遗忘问题——旧数据还在模型自然能保持旧知识。但问题也很明显。首先是计算成本。推荐系统的训练数据量通常是亿级甚至十亿级每次全量重训动辄几十上百张卡跑几个小时甚至几天。如果业务变化快比如电商大促期间用户行为模式一天一个样你不可能每天都来一次全量重训。其次是时效性滞后。假设你每周重训一次那周一到周六的模型都是上周的产物。这期间新出现的爆款商品、突发的兴趣迁移模型完全感知不到。等下周重训完热点可能已经过去了。还有一个容易被忽略的点全量重训会引入历史偏差的累积。旧数据里的曝光偏差、位置偏差会随着一次次重训被反复强化模型越来越难跳出旧的分布。2.2 增量学习的遗忘困境增量学习的思路是不从头训而是在旧模型的基础上用新数据做微调。听起来很美好但实际操作过的人都知道这里有个经典的稳定性-可塑性困境。你如果学习率设得小、更新步数少模型确实不会忘旧知识但它也学不进新知识等于白更新。你如果学习率设得大、更新步数多模型能快速拟合新数据但旧知识会被覆盖掉出现灾难性遗忘。这个平衡点非常难找而且不同业务、不同时间段的最优平衡点还不一样。更麻烦的是推荐系统的数据分布是非独立同分布的。用户在不同时间段的兴趣分布不同物品的流行度分布也不同。增量学习假设新数据和旧数据来自同一个分布这个假设在推荐场景里基本不成立。2.3 序列元学习的切入角度这篇论文的切入角度很有意思。它不去直接解决“怎么更新参数”这个问题而是去解决“怎么学会更新参数”这个问题。打个比方。传统增量学习就像是一个学生每次考试前临时抱佛脚背多少算多少考完就忘。全量重训就像是一个学生每次考试前把整本书从头学一遍学得扎实但效率极低。而序列元学习就像是一个学生它不直接学知识而是学一套学习方法——它知道哪些知识是核心的、不能忘的哪些知识是临时的、可以快速替换的。具体到技术实现上SML把推荐系统的重训过程拆成了一系列任务。每个任务对应一个时间段的数据。模型在任务序列上训练学习的是一个参数更新策略而不是参数本身。这个更新策略的目标是在学完当前任务后模型在旧任务上的表现不能掉太多同时在新任务上的表现要足够好。3. 序列元学习的核心机制拆解3.1 任务序列的构建方式SML的第一步是把连续的时间轴切分成多个任务。假设你有从T1到Tn共n个时间段的数据每个时间段的数据构成一个任务。任务之间是有顺序的T2在T1之后T3在T2之后以此类推。这个切分方式看起来简单但里面有个关键设计任务之间的时间间隔可以是不均匀的。比如大促期间数据变化快你可以把时间窗口切得细一点平稳期数据变化慢窗口可以粗一点。论文里没有强制要求等间隔切分这给了工程落地很大的灵活性。每个任务内部数据被进一步分成支持集和查询集。支持集用来计算梯度、更新参数查询集用来评估更新后的参数好不好。这个划分是元学习的标准做法目的是模拟“用少量数据快速适应新任务”的场景。注意支持集和查询集的划分必须是随机的不能按时间顺序切。否则查询集里的数据分布和支持集不一致元学习的目标就偏了。3.2 元学习器的参数化设计SML的核心是一个元学习器它负责输出参数更新的方向和步长。具体来说对于每个任务元学习器接收当前模型参数和当前任务的梯度信息输出一个更新后的参数。这个元学习器本身是一个小型网络参数量远小于主模型。它的输入包括当前参数、当前梯度、以及一个表示任务特征的向量。输出是参数的更新量。用公式表示就是θ θ g(θ, ∇L, z)其中θ是当前参数∇L是当前任务的梯度z是任务特征g是元学习器。这个设计的好处是元学习器可以学习到不同任务应该用多大的步长、往哪个方向更新。比如当新任务和旧任务差异很大时元学习器可以输出一个较大的更新量让模型快速适应当新任务和旧任务相似时元学习器可以输出一个较小的更新量避免破坏旧知识。3.3 双重优化目标的平衡SML的损失函数由两部分组成。第一部分是新任务上的表现也就是模型更新后在当前任务查询集上的损失。第二部分是旧任务上的保持也就是模型更新后在之前任务查询集上的损失。这两部分是矛盾的。你越偏向新任务旧任务掉得越厉害你越偏向旧任务新任务学得越差。SML的做法是让元学习器自己去学这个平衡而不是人工设一个固定的权重。具体实现上论文采用了一个在线更新的策略。每学完一个任务元学习器就根据新旧任务上的综合表现更新一次自己的参数。这样元学习器会逐渐学会在什么情况下应该多学新知识在什么情况下应该多保旧知识。这个机制我觉得是整篇论文最巧妙的地方。它把“平衡”这件事从人工调参变成了模型自己学习。在实际业务里这个平衡点往往是随着时间变化的人工调参根本跟不上。4. 实操层面的关键细节4.1 数据准备与任务切分如果你要复现SML第一步是准备数据。推荐系统的数据通常是用户-物品交互日志格式是(user_id, item_id, timestamp, label)。你需要按timestamp排序然后切分成多个时间段。切分粒度怎么定论文里没有给固定答案我的经验是每个任务至少要有足够的数据让模型能算出有意义的梯度。如果某个时间段数据太少梯度噪声会很大元学习器学不到东西。一般来说每个任务的数据量至少要是模型参数量的几十倍。任务数量也很关键。任务太少元学习器没有足够多的“学习经验”学不到通用的更新策略。任务太多训练时间会很长。论文里的实验用了10到20个任务我觉得这个范围是比较合理的起点。4.2 模型架构的选择SML本身不限制主模型的架构。你可以用矩阵分解、可以用神经协同过滤、可以用深度兴趣网络都可以。元学习器是一个独立的小网络和主模型解耦。但有一个细节需要注意元学习器的输入维度要和主模型参数维度匹配。如果主模型参数很多元学习器的输入维度会很大参数量也会跟着涨。论文里用了一个技巧不对全部参数做元学习只对嵌入层做元学习。因为推荐系统里嵌入层参数量最大、也最容易受分布变化影响而全连接层的参数相对稳定。这个设计很实用。嵌入层是推荐系统里最“重”的部分动辄几百万甚至上千万参数。如果对全部参数做元学习元学习器本身就会变得很大训练成本受不了。只对嵌入层做元学习器的参数量可以控制在主模型的百分之一甚至千分之一。4.3 训练流程与超参数训练流程大致是这样的初始化主模型参数θ和元学习器参数φ。对于每个任务t从任务t的支持集中采样一批数据计算梯度∇L。用元学习器计算更新量θ θ g(θ, ∇L, z; φ)。在任务t的查询集上计算损失L_new。在之前任务的查询集上计算损失L_old。用L_new λL_old更新元学习器参数φ。用θ更新主模型参数θ。重复直到所有任务学完。超参数方面有几个需要重点关注元学习器的学习率通常比主模型的学习率大一个数量级因为元学习器需要快速适应。λ控制旧任务保持的权重。论文里用的是自适应调整初始值可以设0.5。任务采样比例每次更新元学习器时从旧任务里采样多少个任务来计算L_old。论文里用的是全部旧任务但如果任务太多可以随机采样一部分。实操心得元学习器的训练非常不稳定梯度容易爆炸。建议加梯度裁剪阈值设在1.0到5.0之间。另外元学习器的初始化很重要不要用全零初始化用小的随机值。5. 常见问题与排查思路5.1 元学习器不收敛怎么办这是最常见的问题。元学习器的损失曲线震荡剧烈或者直接发散。排查思路如下问题现象可能原因解决方法损失震荡剧烈学习率太大降低元学习器学习率从1e-3降到1e-4损失持续上升梯度爆炸加梯度裁剪检查输入特征是否归一化损失下降后反弹过拟合增加任务数量或对元学习器加Dropout损失几乎不变学习率太小提高学习率检查元学习器是否真的在更新我踩过的一个坑是元学习器的输入没有做归一化。主模型的梯度在不同任务之间量级差异很大有的任务梯度范数是10有的是0.01。元学习器看到这种输入根本学不到稳定的更新策略。后来加了一层LayerNorm问题就解决了。5.2 旧任务保持效果差如果发现模型在新任务上表现好但旧任务掉得厉害说明λ设小了或者元学习器没有学到“保持”的策略。可以尝试的做法增大λ或者在元学习器的输入里显式加入“当前任务和旧任务的相似度”特征。相似度高的时候元学习器应该输出较小的更新量相似度低的时候可以输出较大的更新量。还有一个技巧在旧任务上做回放。每次更新元学习器时不只从旧任务的查询集里采样还从旧任务的支持集里采样一批数据混到当前任务的支持集里一起算梯度。这样主模型在更新时就能“看到”旧数据遗忘会少很多。5.3 训练时间过长SML的训练时间比普通增量学习长因为每个任务都要算两次前向传播一次算梯度一次算更新后的损失。如果任务多、模型大训练时间会很难接受。优化方向有几个。第一只对嵌入层做元学习这个前面说过了。第二减少每个任务的支持集和查询集采样量用更少的样本估计梯度。第三用分布式训练把不同任务分配到不同worker上并行计算。注意并行计算时元学习器的更新必须是串行的因为后一个任务的元学习器参数依赖前一个任务的更新结果。只有主模型的前向传播可以并行。6. 这个方法在实际业务里的影响范围6.1 适用场景与边界SML最适合的场景是数据分布变化快、重训成本高的推荐系统。比如新闻推荐、短视频推荐、电商推荐这些场景里用户兴趣迁移快物品池更新频繁传统重训方案很难跟上节奏。但它也不是万能的。如果业务本身很稳定用户兴趣半年都不变那全量重训就够了没必要上元学习。另外如果数据量特别小元学习器没有足够的任务来学习效果也不会好。还有一个边界SML假设任务之间是有顺序的而且顺序信息是有意义的。如果你的数据没有明显的时间顺序或者任务之间的顺序可以随意打乱那SML的序列建模就没有意义了。6.2 对推荐系统迭代流程的改变传统推荐系统的迭代流程是离线训练→离线评估→线上A/B→全量上线。这个流程的周期通常是周级别甚至月级别。SML把这个流程压缩了。因为模型可以持续更新不需要等全量重训迭代周期可以缩短到天级别甚至小时级别。这意味着业务侧的变化可以更快地反映到模型里模型的效果衰减也会更慢。但这也带来了新的工程挑战。你需要一套在线学习的基础设施实时数据流、模型版本管理、快速回滚机制。这些不是算法层面能解决的需要工程团队配合。6.3 后续可以扩展的方向这篇论文发表之后我看到几个有意思的扩展方向。一个是把SML和强化学习结合用强化学习来学更新策略而不是用元学习的梯度下降。另一个是把SML用到多模态推荐里因为多模态数据的分布变化更快元学习的优势更明显。还有一个方向是跨域推荐。不同域的推荐系统面临的数据分布变化不同但更新策略可能有共通之处。用元学习学一个跨域的更新策略然后在新域上快速适应这个思路我觉得很有潜力。我个人在实际操作中的体会是SML的核心价值不在于它提出了一个多复杂的模型而在于它把“重训”这件事从一个工程问题变成了一个学习问题。这个视角的转换比具体的算法细节更重要。你在做推荐系统的时候如果遇到模型更新跟不上业务变化的问题不妨想想能不能让模型自己学会怎么更新而不是人工设计更新规则。这个思路一旦打开很多问题都会有新的解法。