做推荐系统的人应该都有这种体会召回阶段决定了整个推荐链路的上限。排序模型再强如果match阶段没把用户真正感兴趣的东西捞回来后面全是白搭。早期我们常用的双塔结构简单高效但对用户行为的建模非常浅基本就是把行为序列里的item embedding拉平做平均或加权用户当天想买什么、过去一个月长期偏好什么完全混在一起。SDM模型Sequential Deep Matching就是冲着这个问题去的。它把用户兴趣拆成两部分来建模短期会话兴趣和长期行为偏好再用一个门控机制融合最终输出能代表用户当前多元化兴趣的向量。我在实际项目里复现并改过这个模型今天把这套东西从头到尾拆一遍包括架构设计、关键参数、训练细节以及那些论文里没明说但实践里很要命的坑。1. SDM模型要解决的问题——为什么不能在召回阶段只用一个兴趣向量1.1 用户兴趣是多个的不是一个绝大多数召回模型在做的事情是拿用户侧的向量去和物品侧的向量做内积然后取TopK。双塔结构就是典型的两个Embedding层分别编码用户和物品问题在于它最终只输出一个用户向量。你想想一个用户昨天在刷手机数码今天想买猫粮历史上一周还在看户外装备你用一个向量怎么表达这三种完全不同的意图有人可能说那用向量内积本身就能表达多样兴趣只要训练得当。这个说法理论上没错但实践里你会发现多兴趣会让向量被“平均化”每个兴趣都沾一点每个兴趣都不够突出。最终召回回来的东西像一锅乱炖排序阶段再强也救不回来。SDM的思路很直接既然一个向量不行那我从不同维度提取多个兴趣向量同时把用户“当下的短暂意图”和“历史积累的稳定偏好”分开建模再做融合。这样既能知道用户此刻手头在忙什么也能知道他长期是什么样的人。1.2 短期兴趣和长期兴趣分别对应什么这里我先用大白话把两个概念说清楚。短期兴趣指的是用户在当前这次会话里体现出来的意图。比如打开App后连续点了3件连衣裙、再看了2双高跟鞋那她当下的意图大概率是穿搭相关。这种兴趣变化快、时效性强需要用最近一小段行为序列来捕捉。长期兴趣则反映用户在过去几天甚至几周内沉淀下来的稳定偏好。比如她过去两周经常看某品牌的护肤品那即便今天这次会话刚开始看运动服她对美妆品牌的高偏好也应该作为一个背景信号参与召回。SDM原文里短期兴趣用用户最近一次会话内的行为序列来建模长期兴趣则从过去较长时间窗口内的行为中提取。这个拆分在业务上非常合理因为电商、内容社区、短视频本质上都有“当下需求”和“长期口味”两个维度。你在抖音上今天刷了十几个修驴蹄视频短期兴趣就是解压类内容但长期来看你更常看的是做饭教程。召回阶段只抓短期容易让人陷入信息茧房的瞬间只抓长期又对即时意图反应太慢。1.3 为什么简单的序列拼接不够用一说到长短期结合很多人第一反应是把短期序列和长期序列拼接在一起送进一个模型不就行了我一开始也是这么干的结果效果不升反降。原因有两个一是短期和长期的序列长度差太多短期可能只有几十个item长期可能有几百上千个直接塞进同一个注意力层短期信号会被长期信号淹没二是短期兴趣和长期兴趣对当前行为的影响方式不一样强行混在一个空间里提取模型很难学到“当下意图为主、长期偏好为辅”这种灵活的决策方式。SDM之所以效果好就是因为它把这两个分支分开建了。短期走一个基于多头自注意力的结构快速捕捉序列内部的转移规律长期走一个基于注意力池化的结构从长期行为中筛选出与当前会话相关的部分。两个分支各干各的活最后再用门控融合让模型自己学当前该信谁多一点。这个设计逻辑比单纯拼接要清晰得多。2. 模型整体架构拆解——从Embedding到兴趣融合的完整路径2.1 输入侧不只是Item ID原论文里对输入的构造讲得比较简洁但实际落地时输入侧的设计对最终效果影响极大。SDM的基础输入是用户的行为序列每个行为是一个物品物品用Embedding向量表示。如果只送item id进去模型学到的只是物品ID之间的共现关系泛化能力非常有限。我在实践里一定会拼上side information包括物品的类目、品牌、价格带、内容标签等。物品种类特别多的平台比如电商或短视频纯ID Embedding的表会非常大训练速度慢而且长尾物品根本学不饱。加了side info之后哪怕某个新物品ID没出现过它的类目Embedding、品牌Embedding都有对应的向量表示模型依然能对它做出合理的召回判断。具体做法是把item id的embedding和各类side info的embedding拼接起来过一层全连接压到统一的向量维度d。这相当于物品侧的一个小融合层。别小看这步它让模型对冷启动物品的容忍度提升了一个档次也间接缓解了序列数据稀疏的问题。2.2 短期兴趣分支多头自注意力编码会话序列短期兴趣分支的结构说白了就是用Transformer Encoder里的Self-Attention去建模最后一次会话内的行为序列。用户最近点击的item序列比如长度为n每个item转换成一个d维向量后叠加上位置编码然后送给多头注意力层。为什么用Self-Attention而不用LSTM或GRU我在项目里两种都试过LSTM、GRU这类循环网络对序列是顺序建模的能捕捉到前后依赖但用户点击行为里的“前一跳和后一跳”未必有强因果顺序更多是兴趣的跳跃和扩展。Self-Attention的好处是任意两个位置之间直接建立关联能捕捉到“第1个item和第5个item其实同属一个意图”这种跨距离关系而且天然适合并行训练长序列效率也高。多头注意力的含义就是把注意力计算拆成多组每组关注不同维度的关系。举个具体例子用户点了一个手机、一个手机壳、一副耳机、一根数据线某一头可能学到了“手机和手机壳配套”另一头学到了“数码相关产品的共性”。多头机制让模型有多个视角去看这段序列提取出的兴趣自然更立体。模块里还有一个人人都知道但经常被忽略的细节Mask。Self-Attention在编码用户行为序列时只能让当前位置attend到它之前的行为不能让它看到未来的行为否则就形成了标签泄漏。训练时这属于基本操作但很多入门同学写代码时会漏掉这一点导致离线指标虚高上线立刻崩。此外为了让序列里每个item有位置感我还会加入可学习的位置Embedding和item Embedding相加。这个不加不行Self-Attention本身不具备序列顺序信息没有位置编码的话序列顺序怎么换都会得到同样的结果这对行为建模来说是致命的。2.3 长期兴趣分支用注意力从历史行为中“挑重点”长期兴趣分支的输入是用户在过去一段较长时间窗口内的行为序列通常取最近7天或14天。但这里有个问题长期行为可能有几百上千个item全量做注意力计算计算量太大而且大量历史行为跟当前意图无关。比如一个用户过去一个月买过猫粮、看过手机、下过几单日用品今天他进来是为了看电脑那猫粮那部分历史行为其实是噪音。SDM的思路是对长期行为做基于会话的分割。把长期行为按会话时间间隔切成多个session每个session内部的item合并成一个“行为块”整个session整体去和当前短期兴趣做注意力计算。这样既压缩了长期序列的长度又保留了一定的结构化信息。听上去有点绕我拿一个例子说明。假设用户最近14天产生的行为按时间间隔自动分成3个sessionsession1是上周末晚上连续看的5件家电session2是三天前陆续点开的4本小说session3是今天刚刚打开没多久的3双球鞋。长期兴趣分支不是对每个item做注意力而是对session1、session2、session3三个粒度做注意力。哪个session和当前短期兴趣最相关它的权重就会更高。长期分支计算时每个session的向量表示可以用内部item向量的平均或者用一个轻量级Self-Attention再做一次池化。我试过直接平均效果也还行但用一次轻量级Self-Attention后session内部的重要item能被凸显出来长短期融合的质量更高。代价是多了一些参数和计算量大家根据自己的资源情况取舍。2.4 兴趣融合门控让模型自己决定“当下”和“历史”谁说了算短期兴趣分支输出一个兴趣向量长期兴趣分支输出一个兴趣向量接下来怎么结合最粗暴的办法是拼接或者相加问题在于权重是固定的。但现实业务里不同用户、不同时刻“短期”和“长期”的重要性是完全不一样的。一个典型的新用户长期行为几乎为空那召回几乎完全依赖短期兴趣一个老用户短期行为偶然性很大比如不小心点了个钓鱼视频那长期偏好就应该占更高的权重。如果拿固定权重去融合对这两类用户都很难适配。SDM的做法是引入一个门控机制核心思想是学习两个权重系数分别控制短期、长期兴趣向量对最终输出的贡献。门控的设计并不是简单拼接后接一个全连接层输出两个标量那么简单工程上会考虑把短期、长期兴趣向量与当前行为上下文拼接起来通过sigmoid生成0到1之间的软开关。具体计算可以这样理解模型先拼接短期兴趣向量和长期兴趣向量过一层全连接再用sigmoid输出一个权重α这个α表示“当前该信短期多少”。最终的兴趣向量等于α乘以短期兴趣向量加上(1-α)乘以长期兴趣向量。学过LSTM的人应该对这玩意非常眼熟它本质上就是一个简化版的门控循环结构让模型以软开关的方式决定两种信号的混合比例。我在实际调参中发现门控这一层不需要做得太复杂过深的层反而容易过拟合。一个单层、输出维度很小的全连接就够用了。重点在于输入门控的那些特征应该既包含短期兴趣向量、长期兴趣向量也包含一些上下文特征比如用户当前会话内行为数、用户历史活跃度等这些特征作为先验线索能帮门控学得更稳。最终融合出的向量作为用户的最终表示和物品侧向量做内积通过softmax去优化。到这里SDM的主体结构就完整了输入层做side info融合短期分支做多头自注意力长期分支做session级别注意力最后门控融合。3. 核心模块的实操要点与关键参数3.1 Embedding维度和序列长度的选择逻辑很多初学同学上来就把Embedding维度设成64或者128理由是“大家都这么设”。但维度选择跟你的数据量和物品量直接相关。物品量上亿的大厂embedding维度就算设到256每个id也只有256个浮点数表示信息容量有限但对于千万级物品量的场景64维可能就够用了太高反而浪费训练资源。我个人的经验是先看你的物品量级。物品量在百万级embedding可以从32维开始试千万级用64或128亿级才考虑上到256。另外side info的embedding维度可以比item id低一些比如类目用16维品牌用32维这样不会因为拼接后维度太高导致主向量膨胀。序列长度方面论文里的短期会话通常取最近的几十个item即可。取太短信息不够取太长噪音太多而且会拖慢训练速度。长期行为窗口我一般先取最近7天只想建模到更长的历史偏好再适当放宽到14天或30天但要控制长期session的数量。session数量太多长期分支的注意力矩阵会非常大训练效率会明显下降。我常用的配置是短期序列长度64长期session数最多32个每个session内部item数不超过20。这样长期分支计算量可控同时信息覆盖足够广。具体数字大家还是要根据自己业务调核心原则是短期序列要覆盖住“最近一次意图的完整上下文”长期session数要覆盖住“用户最近的稳定兴趣画像”。3.2 多头注意力的头数和Dropout设置多头注意力的头数论文里用的比较多的是8或16。实践下来8头在绝大多数场景都是比较稳的选择。头数太多不一定好每个头的维度会被压缩比如64维的向量分成16个头每个头只有4维学到的关系太细碎反而容易过拟合。头数太少又表达力不够无法同时捕捉多种维度上的关系。在训练SDM时我特别强调Dropout的作用。行为序列数据天生存在大量随机噪声用户可能误点了一个不感兴趣的item可能因为页面排版点了个位置好看但不喜欢的商品。这些噪声如果不过滤自注意力很容易学到错误的关系。Dropout的添加位置有两个关键点一个是Embedding输出后加一层Dropout让item向量在进入注意力前就不那么“固执”抑制过拟合另一个是注意力矩阵计算时也加Dropout注意力权重的随机置零能让模型不依赖单个强关联item增强泛化能力。我的经验是Embedding层的Dropout设0.1到0.2之间注意力层内部的Dropout设0.1左右。强度不是越大越好加太猛会导致模型欠拟合尤其用户行为已经足够稀疏时再Dropout信息就太少了。3.3 负采样召回模型绕不开的关键操作SDM是一个召回模型召回模型的训练本质上是度量学习让正样本用户真实交互过的item和用户向量的相似度尽可能高让负样本的相似度尽可能低。这就落到负采样上。因为隐式反馈数据里没有显式的“负样本”我们需要从用户的全部交互中随机抽取那些用户没有交互过的item当作负样本。但这里有个问题Batch内负采样是常用的办法。什么意思呢假设一个batch里有256个用户的样本每个用户有一个正样本item那么对于第一个用户来说其余255个用户的item就充当负样本。这样几乎零成本地拿到了负样本训练效率高。Batch内负采样有个显著风险样本选择偏差。在一个batch内热门item出现的概率本来就高容易被当成别人的负样本这会让模型误以为热门item不好从而产生对热门item的抑制效应。缓解办法是加一个Popularity Bias的修正项常见做法是在logits计算时减去一个与item热度成正比的偏置项。负样本数量也不是越多越好。我试过把负样本数从1加到5开始时效果提升明显加到10以后收益就边际递减了反而增加了负采样计算开销。实际项目里batch内负采样配合1到2个随机负样本是性价比很高的组合。4. 训练样本构造与工程落地细节4.1 样本构造预测未来而不是拟合过去SDM训练样本的构造方式和普通推荐模型有点区别。核心原则是用用户过去的行为序列作为输入预测他未来行为的item。具体构造方法是将一个用户在某一天的全部行为按时间排序取前70%作为行为序列后30%作为监督目标。假设用户在晚上8点之前点击过50个item那这50个item作为输入序列去预测他8点之后点击的item。这样做的好处是序列和标签在时间上是严格区分的确保模型不会用以后的信息去预测以前的东西。不少人一开始会在时间切分上出错比如直接把同一时间段的点击既当输入又当标签离线指标虚高得离谱上线直接崩。短期会话和长期行为的切分规则也要在样本构造阶段就定好。短期会话一般定义为用户连续操作、且两个行为之间的时间间隔不超过某一阈值比如10分钟或30分钟。超过阈值的下一次行为视为开启了一个新的session。长期行为则是从当前时间往前回溯最近的N天并按照断session的逻辑切分成若干session块。阈值的设定和业务类型强相关。电商场景用户浏览速度慢间隔阈值可以放宽到30分钟短视频场景用户切换极快间隔5分钟以上就已经算新会话了。没有一个万能参数最好用线上数据的session长度分布来确定。4.2 多任务学习兴趣召回不能只看点击SDM原文里还设计了一个多任务学习的变体同时预测用户对item的点击和购买。这个思路非常聪明因为纯点击信号里包含了大量误触和无意浏览纯购买信号又过于稀疏。把两个目标放到同一个模型里通过采样和权重调整模型既能在点击数据上学到足量的兴趣信号又能在稀疏的购买信号上学到更强烈的购买意图。做多任务时损失函数是各项损失的加权和。权重怎么定我的经验是如果当前模型阶段主要目标是提升点击率点击权重可以设大一些如果想要召回带来更多转化效果就把购买权重加大。开始可以设为1:1观察两个损失的变化和召回指标再做调整。有些场景下购买行为太少权重太高会导致模型被少数购买样本带偏反而伤害点击召回效果。多任务在SDM里还有一个好处它相当于给长期兴趣分支多提供了一类监督信号。长期行为里其实只有很少一部分和购买强相关有了购买任务约束后长期分支会更倾向于提取“有商业价值”的偏好而不仅仅是“用户喜欢看什么”。4.3 上线Serving向量召回链路怎么搭SDM训练完成后的上线并不复杂这是它相比一些排序模型容易落地的原因。因为SDM属于双塔类召回模型用户侧塔和物品侧塔是解耦的。物品侧向量可以离线算好全量物品过一遍模型输出每个item的向量存入向量索引库线上用Faiss这类向量检索工具做ANN召回。用户侧向量的计算则要实时一些。用户每产生一个新的item行为短期会话序列就变了短期兴趣向量也变了最终融合出的用户向量也随之变化。所以线上serving时需要实时拼接用户最近的session行为交给短期分支计算长期分支则可以从缓存里取上一次算好的结果定期更新即可。这里有个工程优化点长期分支的计算涉及大量session和历史行为实时计算成本高。可以做成异步更新每5分钟或10分钟重算一次长期兴趣向量缓存在Redis里短期分支则随请求实时计算。这样既保证了短期兴趣的实时性又不至于让服务器每时每刻都被长序列计算压垮。我当时落地时还踩过一个坑训练时是“一个模型同时输出短期、长期、门控”但serving时如果实现得不对比如门控的输入特征和线上对不上算出来的α权重就跟训练时分布不一致召回结果会明显变差。所以上线前务必做一场针对线上特征和模型输入的优劣对齐测试确保训练和serving的输入口径完全一致。5. 离线评估与常见问题排查实录5.1 离线评估指标的选择召回模型的离线评估一直是老大难问题。大家最常用的RecallK含义是对于测试集里那些“用户真实交互过的item”模型能不能在召回的前K个结果里把它们找出来。Recall50代表在召回100个结果时真实交互的item有多少比例被捞回来了。但我要提醒一句不要在纯离线Recall上过度乐观。因为用户不只是会和模型召回的item交互也会和没被召回但被推荐到其他位置的item交互。离线数据天然有position bias真实交互过的item本身就更倾向于出现在头部推荐位。因此离线Recall偏高并不代表线上效果一定好。我一般会在离线阶段重点看两个维度一是RecallK二是和现有baseline比如双塔在同一样本上的提升幅度。重点看相对提升而不是绝对值。另外提一嘴SDM这种多兴趣融合模型在评估时最好按兴趣维度切分测试数据比如按当前session主题分或者按用户历史活跃度分层。这样可以观察短期兴趣模型在“新会话主题切换频繁”的用户群上是否更有优势长期兴趣模型在“回访老用户”上是否提升明显。这种分层的评估方式比只看一个整体指标更有说服力。5.2 训练不收敛或收敛过慢我复现SDM时最常遇到的第一个问题就是训练不收敛。loss一直在高位抖动降不下去。此时排查顺序很有讲究。第一优先检查数据管道。看看正样本的item id是否真的存在于物品Embedding表里是否存在太多冷启动物品在交互样本里但没有任何历史embedding可以初始化。第二检查标签泄漏。看序列截断和标签切分的时间逻辑是否有误如果输入的“历史行为”里面包含标签item自身模型会走得非常快但上线就废。第三检查Embedding初始化。Item Embedding表一般用均匀分布随机初始化范围要合适太大会让初始logits极大梯度爆炸太小会让模型学得太慢。我一般初始化范围设为(-0.05, 0.05)或者按照1/sqrt(embedding_dim)来设置。收敛慢还有一个很隐蔽的原因学习率。自注意力结构对学习率非常敏感尤其多头注意力和门控部分的参数我用Adam优化器时学习率从1e-3开始经常发散调成3e-4或1e-4后模型稳定多了。建议大家训练SDM时一上来先把学习率设小比如1e-4或5e-5观察loss稳定下降后再考虑用warmup策略慢慢调大。5.3 短期和长期分支失衡很多同学复现SDM时会发现短期兴趣分支的效果很好长期兴趣分支的提升几乎看不出来或者反过来长期分支学得很好短期分支几乎被忽略。这类问题的本质是两个分支的学习速度不均衡。短期分支有多头自注意力参数量远大于长期分支天然拟合能力更强训练时loss下降更快长期分支如果session注意力没设计好很容易退化成“对session向量简单求平均”那长期兴趣就被弱化成一个整体平滑信号了。我的处理办法是分别记录两个分支输出向量的L2范数和各自对最终损失的梯度贡献。如果发现某个分支的梯度范数持续远低于另一个就给它加一个梯度缩放系数或者调整门控初始偏向。比如短期分支在冷启动数据上贡献很大但长期分支在老用户数据上更重要可以按用户活跃度动态调整loss权重。另外也可以让门控的初始偏置设置得偏向长期分支一点让训练早期两个分支都能被充分利用而不是开局就被短期分支带偏。5.4 线上效果与离线不一致离线测试RecallK涨了几个点上线一看线上点击率没变化这种情况我见了太多次。原因通常出在几个地方一是训练和serving的特征口径不一致比如离线样本里session切分是用全量数据回溯的线上serving却只用了窗口内的行为二是向量索引的召回量与离线评测不一致离线用暴力全量计算线上因为性能限制只召回了部分候选效果自然打折三是用户向量更新频率影响了推荐新鲜度离线评测时默认用户向量是最新的线上如果定期更新用户短期内已经变化的需求就无法被捕捉。针对最后一点我的建议是上线初期可以提升用户向量的更新频率至少做到用户有新的关键行为后短期兴趣向量能秒级更新。长期兴趣向量则可以放宽到分钟级或小时级这个递进关系能比较好地平衡计算成本和推荐时效性。每一层更新频率到底设在多少合适没有标准答案最好用线上A/B实验来测。6. 一些想多说两句的经验之谈现在回头去看SDM这个模型的核心价值不是它用了多么新奇的结构——多头自注意力现在已经是标配了门控机制在LSTM里也存在了几十年——而是它把“用户兴趣是动态变化的、且同时存在多个层面”这个业务洞察落成了一个清晰可实现的网络结构。这一点远比追求某个SOTA模型结构重要。如果你打算在业务里引入SDM我的建议是从最简单的配置开始先不管长期兴趣分支只用短期兴趣分支加门控和双塔做对比看清提升幅度再把长期兴趣分支加进去继续对比。每一步都有清晰的收益分析后面做汇报、做决策都更有底气。先把能解决的问题解决掉再考虑如何做得更复杂。根据我个人踩坑的经验还有一点非常想说SDM这类多兴趣召回模型的效果很大程度依赖样本构造和特征工程的质量而不是模型结构本身。很多人在模型上花了大把时间调参却忽略了样本里session切分不干净、长期行为窗口选得不合适这些基础问题。先把这些基本功练好再上模型路会顺很多。希望这篇拆解能让你在学习和落地的过程中少走一些弯路。