资讯动态

DSIN深度解析:会话兴趣网络如何提升CTR预估效果

发布时间:2026/10/28 14:01:53 来源:尧图企业网站定制
推荐系统里做CTR预估的同学大概率都经历过这样的场景离线AUC涨了千分之几信心满满上线结果线上CTR纹丝不动甚至偶尔还跌。排查一圈发现问题出在用户行为序列的建模方式上——大多数模型把用户历史行为当成一个扁平的序列来处理完全忽略了行为之间天然存在的会话边界。DSINDeep Session Interest Network这篇工作就是冲着这个痛点去的它把用户行为按时间间隔切分成一个个会话再在会话内部和会话之间分别建模最后用一个自适应的方式把会话兴趣聚合起来。我第一次读到这个结构的时候感觉它把用户兴趣是有节奏的这件事讲清楚了而不是简单堆一个更深的网络。下面我就按自己复现和调参的过程把这个模型的来龙去脉、结构细节、实操坑点完整拆一遍适合已经做过基础CTR模型、想进一步优化序列建模的算法同学参考。1. 为什么扁平序列建模会丢掉用户兴趣的节奏感1.1 用户行为天然是分段的不是一条直线先想一个很日常的例子。你上午刷电商App看了几款跑鞋比了比价格中午吃饭的时候顺手点开了几个外卖店铺晚上下班又去看了几件衬衫。这三个时间段的行为动机完全不同跑鞋、外卖、衬衫之间几乎没有关联。但如果你把这些行为按时间顺序排成一条序列喂给模型模型看到的是一条跑鞋→跑鞋→外卖→外卖→衬衫的混合序列它很难判断用户当前到底对什么感兴趣。这就是扁平序列建模的核心问题它假设用户的所有历史行为都在同一个兴趣空间里连续演化而现实中用户的兴趣是分段的、跳跃的。传统做法里DIN用注意力机制去捕捉当前候选商品和历史行为的相关性思路是对的但它依然是在一整条序列上做注意力没有区分哪些行为属于同一段兴趣。DIEN引入了GRU来建模兴趣的演化过程比DIN进了一步但同样没有显式地处理会话边界。DSIN的关键洞察就是用户行为序列应该先按时间间隔切分成会话session会话内部的兴趣是相对一致的会话之间的兴趣则会发生明显漂移。这个假设非常符合直觉也是它区别于前作的根本点。1.2 会话切分的判定标准与工程实现那怎么切会话DSIN用的判据很朴素相邻两次行为的时间间隔超过某个阈值论文里用的是30分钟就认为进入了新的会话。这个30分钟不是拍脑袋来的它和业界常用的会话定义一致——很多日志分析系统默认30分钟无操作就算会话结束。工程上实现切分的时候有几个细节要注意。第一切分是在样本构造阶段做的不是在模型里做的也就是说每个样本要预先算好它的会话列表。第二会话长度是不固定的有的会话只有一两个行为有的可能有几十个这就涉及到后续的padding和mask处理。第三要保留会话内部的行为顺序因为会话内的兴趣演化是有方向性的。我自己的实现里切分逻辑大概是这样先按用户ID和时间戳排序然后遍历一旦发现当前行为时间减去上一条行为时间超过阈值就新开一个会话。这里有个容易踩的坑——时间戳的精度。如果你的日志时间戳只精确到秒而用户行为又很密集可能会出现大量间隔刚好卡在阈值附近的情况建议统一转成分钟级再做比较避免边界抖动。def split_sessions(behaviors, threshold_minutes30): sessions [] current [] for i, b in enumerate(behaviors): if i 0: current.append(b) else: gap (b[ts] - behaviors[i-1][ts]) / 60.0 if gap threshold_minutes: sessions.append(current) current [b] else: current.append(b) if current: sessions.append(current) return sessions这段代码看着简单但它是整个模型的地基。切分质量直接决定了后面会话兴趣抽取的效果如果切得太碎每个会话信息量不足切得太粗又会把不同兴趣混在一起。后面我会讲怎么调这个阈值。2. 会话兴趣抽取Bi-LSTM加注意力到底在做什么2.1 为什么用Bi-LSTM而不是单向RNNDSIN在会话内部用的是Bi-LSTM来抽取会话兴趣。这里有个问题值得说清楚为什么是双向的单向RNN只能看到当前行为之前的信息而一个会话内部的兴趣演化往往需要结合前后文才能判断准确。比如会话里是跑鞋→袜子→跑鞋如果只看前向第二个跑鞋的表示里已经混入了袜子的信息但如果结合后向信息模型能更好地判断这个会话的核心兴趣其实是跑鞋。Bi-LSTM把前向和后向的隐状态拼接起来得到每个行为的表示然后对这个会话内的所有行为表示做一次注意力聚合得到一个会话级别的兴趣向量。注意力的query是可学习的相当于问这个会话里哪些行为最能代表这段兴趣。这一步的输出维度是固定的不管会话多长最后都压成一个向量方便后续处理。实操中我建议把Bi-LSTM的隐藏层维度设得不要太大64到128之间就够了。原因有两个一是会话本身不长维度太大容易过拟合二是后面还有会话间的建模参数量会叠加。我试过256的隐藏层离线指标没涨训练时间倒是翻倍了。2.2 会话内注意力的偏置问题这里有个论文里没细说、但实操中很关键的点会话内注意力容易偏向长会话。因为注意力权重是在会话内做softmax的会话越长单个行为分到的权重越容易被稀释聚合出来的兴趣向量就越平均反而丢失了焦点。我的处理办法是在注意力打分上加一个长度归一化项或者干脆对超长会话做截断。截断的策略是保留最近的N个行为因为越近的行为越能反映当前兴趣。N取多少我一般取20超过20的会话只保留最后20个。这个数字不是绝对的你可以根据自己业务的平均会话长度来定原则是覆盖大部分会话的完整长度。另外padding的处理也要小心。不同会话长度不同batch里要对齐padding位置必须在注意力计算时mask掉否则softmax会把权重分给这些无意义的占位符。这个坑我在第一次复现的时候就踩了离线AUC莫名其妙低了一大截查了半天才发现是mask没做对。3. 会话间建模兴趣漂移怎么被捕捉3.1 Bi-LSTM在会话序列上的作用抽取出每个会话的兴趣向量之后这些向量按时间顺序排成一个会话序列。DSIN在这个序列上又跑了一层Bi-LSTM目的是捕捉会话之间的兴趣演化。这一步和会话内的Bi-LSTM是两回事会话内建模的是一段兴趣内部的行为关联会话间建模的是兴趣本身如何随时间迁移。举个例子用户第一个会话在看跑鞋第二个会话在看运动服第三个会话在看健身器材。这三个会话的兴趣向量之间是有演化逻辑的——从单一品类扩展到运动装备生态。会话间的Bi-LSTM就是要把这种演化趋势编码进去。它的输出是每个会话位置上的隐状态这些隐状态既包含了当前会话的兴趣也包含了它和前后会话的关系。这里要注意的是会话序列的长度通常比行为序列短很多一般也就几个到十几个会话。所以这一层的计算量不大但它的信息密度很高。我在调参时发现会话间Bi-LSTM的隐藏层维度和会话兴趣向量的维度保持一致比较好这样拼接和后续处理都方便不用额外做投影。3.2 局部激活单元让当前候选决定看哪些会话会话间Bi-LSTM的输出是一串隐状态但最终要得到一个用户表示就需要把这些隐状态聚合起来。DSIN在这里用了一个局部激活单元Local Activation Unit本质上是注意力机制但它的query是当前候选商品。这个设计很巧妙。传统的做法是把所有会话兴趣简单求和或平均但用户的兴趣是多样的当前要预测的这个候选商品可能只和其中某几个会话相关。比如候选是跑鞋那用户看外卖和衬衫的会话就不应该贡献太多。局部激活单元就是让候选商品去挑选相关的会话给相关会话更高的权重。实现上它计算候选商品embedding和每个会话隐状态的相关性分数softmax归一化后加权求和。这里有个细节DSIN用的是候选商品和会话隐状态的外积或者拼接后过MLP来算分数而不是简单的点积。拼接后过MLP的表达能力更强但参数更多。我实测下来如果候选商品和会话兴趣的维度都不大比如64拼接过MLP的效果确实比点积好一些但差距没有想象中那么大追求轻量化的场景用点积也够。4. 完整前向流程与关键张量形状梳理4.1 从原始行为到最终预测的链路把前面的模块串起来DSIN的完整前向流程是这样的输入层用户画像特征、候选商品特征、上下文特征以及用户行为序列已切分成会话。会话兴趣抽取层对每个会话用Bi-LSTM加注意力得到一个会话兴趣向量。会话间建模层把所有会话兴趣向量按时间排成序列过Bi-LSTM得到会话隐状态序列。局部激活层用候选商品作为query对会话隐状态序列做注意力聚合得到用户会话兴趣表示。特征拼接层把用户会话兴趣表示、用户画像、候选商品、上下文特征拼接。全连接层过几层MLP最后sigmoid输出CTR预估值。这个链路里张量形状的变化是最容易出错的地方。我整理了一个对照表方便你在实现时核对阶段张量含义典型形状行为embedding每个行为的向量[B, S, L, D]会话兴趣向量每个会话压成一个向量[B, S, D]会话隐状态会话间Bi-LSTM输出[B, S, D]用户表示局部激活聚合后[B, D]拼接特征所有特征合并[B, D_total]其中B是batch sizeS是最大会话数L是最大会话长度D是embedding维度。注意S和L都是需要padding对齐的实际实现里要维护两个mask矩阵一个标记有效会话一个标记会话内的有效行为。4.2 两个mask矩阵的维护mask的处理是DSIN实现里最琐碎但也最不能出错的部分。第一个mask是会话级别的标记哪些会话是真实存在的因为不同用户的会话数不同batch里要对齐到最大会话数。第二个mask是行为级别的标记每个会话内哪些行为是真实的因为会话长度不同。这两个mask会分别用在两个注意力模块里。会话内注意力要用行为mask把padding行为的注意力分数置为负无穷softmax后权重为0。局部激活单元要用会话mask把padding会话的分数置为负无穷。如果漏了任何一个模型都会学到padding位置的虚假模式离线指标会明显下降。我踩过的一个坑是在会话间Bi-LSTM里padding会话也会参与计算虽然它们的输出最终会被mask掉但Bi-LSTM是序列模型padding会话的隐状态会影响后续真实会话的隐状态。解决办法是用sequence_length参数让框架自动处理或者在计算时把padding位置的输入置零并配合mask。TensorFlow的dynamic_rnn和PyTorch的pack_padded_sequence都能处理这个问题但要注意会话序列本身也要按真实长度排序后再pack。5. 复现过程中最容易翻车的几个地方5.1 会话切分阈值对效果的影响前面提到30分钟这个阈值但它是论文在特定数据集上的选择不一定适合你的业务。我在自己的数据上做过一组对比实验阈值从10分钟到60分钟离线AUC的变化大概在千分之三到千分之五之间。太小的阈值会把一个完整兴趣切碎太大的阈值又会把不同兴趣混在一起。怎么定这个阈值我的经验是看业务的行为密度。如果用户平均每天只有几次行为阈值可以设大一点比如60分钟如果是高频场景比如短视频、资讯用户一天几十次行为阈值就要小一点15到30分钟比较合适。更严谨的做法是画一下行为时间间隔的分布找一个明显的谷底作为切分点。还有一个细节不同用户的活跃度差异很大用统一的绝对阈值可能对低频用户不友好。可以考虑用相对阈值比如取该用户行为间隔的中位数作为参考。不过这样会增加实现复杂度是否值得要看你的业务场景。5.2 序列长度截断与padding策略行为序列和会话序列都需要截断到固定长度。截断策略直接影响模型能看到多少历史信息。我的建议是会话数S取最近S个会话S一般取5到10。太早的会话对当前预测的贡献很小反而引入噪声。会话长度L每个会话取最近的L个行为L一般取10到20。总行为数如果S乘以L太大显存扛不住可以进一步限制总行为数比如最多保留100个行为。截断的时候一定要保留最近的因为CTR预测本质上是预测当前时刻的点击概率越近的行为越相关。我见过有人为了省事直接取前N个效果差很多。padding的时候embedding层要用mask不能让padding的embedding参与梯度更新。有些框架里padding的index是0如果0对应的embedding被更新了会污染整个embedding表。稳妥的做法是给padding单独留一个index并且在优化时把它排除或者直接用mask把它的梯度置零。5.3 学习率与正则化的调参经验DSIN的参数量比DIN大不少主要是多了两层Bi-LSTM。训练的时候过拟合的风险更高。我自己的调参经验是学习率从1e-3开始配合Adam如果loss震荡就降到5e-4。Bi-LSTM对学习率比较敏感太大容易梯度爆炸。Dropout在Bi-LSTM的输出和全连接层都加dropout比例0.2到0.5之间。会话兴趣向量那一层dropout可以设大一点因为它是中间表示过拟合风险高。L2正则对embedding层加L2系数1e-5到1e-4。embedding是最容易过拟合的部分。梯度裁剪Bi-LSTM一定要做梯度裁剪阈值设5左右防止梯度爆炸。还有一个容易被忽略的点batch size。DSIN的序列结构比较深batch size太小会导致BN统计不稳定如果用了BN太大又会让每个epoch的更新次数变少。我一般用256到1024之间根据显存调整。6. 和其他序列建模方案的横向对比6.1 DSIN与DIN、DIEN的核心差异把DSIN和它的前作放在一起看能更清楚地理解它的定位模型序列建模方式是否处理会话兴趣演化建模主要局限DIN注意力否无忽略行为顺序和分段DIENGRU注意力否有未显式切分会话DSIN双层Bi-LSTM注意力是有结构复杂参数量大DIN的贡献是引入了注意力来捕捉候选商品和历史行为的相关性但它把序列当成无序集合。DIEN用GRU建模了兴趣随时间的演化比DIN进了一步但它依然是在整条序列上做演化没有区分会话。DSIN则明确地把会话作为建模单元先会话内、再会话间层次更清晰。从效果上看论文里的实验显示DSIN在多个数据集上都优于DIN和DIEN提升幅度在千分之几到百分之一之间。这个提升幅度在CTR领域已经算可观了但代价是模型复杂度和训练成本上升。6.2 什么场景适合上DSIN不是所有场景都值得上DSIN。我的判断标准是用户行为序列比较长且存在明显的时间分段特征。如果用户行为很稀疏一天就一两次切会话没意义。候选商品和用户历史行为的相关性对预测很重要。如果CTR主要靠用户画像和商品特征就能预测准序列建模的边际收益有限。有足够的算力和工程能力支撑更复杂的模型。DSIN的训练和推理成本都比DIN高线上serving的延迟要提前评估。如果以上条件不满足我建议先用DIN打好基础确认序列建模确实有收益再考虑升级到DSIN。盲目上复杂模型很可能投入产出比不划算。7. 线上部署时的一些实际考量7.1 推理延迟与模型压缩DSIN的推理延迟主要来自两层Bi-LSTM。Bi-LSTM是序列模型没法像全连接层那样完全并行会话数多的时候延迟会比较明显。线上部署时我做过几个优化第一限制会话数和会话长度。线上serving时不需要保留太长的历史S取5、L取10延迟能降不少效果损失很小。第二把Bi-LSTM换成单向LSTM或者用卷积替代。如果延迟实在压不下来可以牺牲一点效果换速度。我试过用一维卷积加注意力替代会话内的Bi-LSTM离线AUC掉了千分之一左右但延迟降了将近一半。第三模型蒸馏。用DSIN做teacher蒸馏到一个更简单的student模型上。这个做法在工业界比较常见能把复杂模型的能力迁移到轻量模型上。7.2 特征更新与在线学习CTR模型的特征分布变化很快DSIN这种依赖行为序列的模型对特征新鲜度更敏感。线上部署时要注意用户行为序列要实时更新不能用过期的缓存。一般用近线的方式行为发生后几秒内更新到特征库。会话切分要在线上做或者近线预计算。如果每次请求都重新切分整个历史序列延迟会很高。我的做法是近线维护每个用户的会话列表线上只做读取和拼接。模型要定期更新最好是每天全量加增量。DSIN参数量大全量更新的成本不低可以用增量学习的方式只更新变化大的部分。还有一个实际问题是冷启动。新用户没有历史行为会话序列是空的DSIN的序列部分就失效了。这时候要退化到只用画像和上下文特征或者用一些默认的会话填充。这个降级逻辑要提前设计好不能等线上出问题了才补。8. 我复现时踩过的几个具体坑8.1 会话内注意力的mask写反了第一次复现的时候我把会话内注意力的mask逻辑写反了——本该mask掉padding位置结果mask掉了真实位置。训练loss一直降不下去我还以为是学习率的问题调了半天。后来打印了注意力权重才发现权重全集中在padding上。这个错误的隐蔽性在于模型依然能训练只是学到的全是噪声。教训是mask相关的代码一定要写单元测试用一个小例子验证mask后的softmax权重是否符合预期。8.2 会话间Bi-LSTM没有处理变长序列第二个坑是会话间Bi-LSTM。我一开始直接把padding后的会话序列喂进去没有传真实长度。结果padding会话的隐状态影响了后续真实会话离线指标比预期低了不少。后来改用pack_padded_sequence把真实长度传进去指标才恢复正常。这个坑的根源是RNN类模型对padding很敏感不像CNN那样可以通过mask完全屏蔽。用RNN就一定要处理变长序列。8.3 embedding维度设得太大导致过拟合第三个坑是embedding维度。我一开始把行为embedding设成了256想着维度大表达能力强。结果训练集loss降得很快验证集AUC早早见顶然后下滑典型的过拟合。后来降到64加了dropout和L2验证集指标反而更好。CTR场景里embedding维度不是越大越好64到128通常就够了关键看特征基数和数据量。特征基数大、数据量足可以适当加大否则宁可小一点。8.4 忽略了负样本的构造方式最后一个坑和模型无关但影响很大负样本构造。CTR训练样本里负样本的选取方式直接影响模型学到的偏好。我一开始用的是随机负采样后来发现曝光未点击的样本和随机样本分布差异很大改用曝光未点击作为负样本后离线AUC看着降了一点但线上CTR反而涨了。原因是随机负样本太容易区分模型学到的判别边界和线上真实分布不匹配。这个经验说明离线指标和线上效果之间隔着样本分布这道坎不能只看离线。9. 一些可以继续深挖的方向DSIN本身是个比较完整的框架但还有不少可以改进的地方。我自己想过几个方向一是会话切分的自适应化不用固定阈值而是根据用户行为模式动态调整二是把会话兴趣和长期兴趣结合起来DSIN主要建模的是会话级别的中短期兴趣长期稳定兴趣可以用单独的模块建模三是引入多任务学习把CTR和CVR一起优化共享底层的会话兴趣表示。另外DSIN的局部激活单元用的是候选商品作为query这个设计可以扩展到多query的场景比如同时考虑候选商品和上下文让聚合更精准。这些方向有的已经有后续工作在做了有的还比较开放感兴趣的同学可以自己试试。最后分享一个我在调参时的小技巧DSIN的模块比较多出问题的时候很难定位是哪个模块的锅。我的做法是逐模块验证——先只保留会话内建模看指标是否合理再加上会话间建模最后加局部激活。每加一个模块都对比指标这样一旦某一步指标异常就能快速定位。这个增量式调试的思路在复现任何复杂模型时都适用比一上来就端到端调参高效得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑