1. 从“用户兴趣会漂移”说起DSIN到底在解决什么问题做推荐系统和计算广告的朋友对CTR预估这个任务肯定不陌生。给定一个用户和一件候选物品模型要输出一个0到1之间的概率表示这个用户点击这件物品的可能性。这个概率值直接决定了广告的排序和出价差之毫厘收入上可能就是天壤之别。过去几年CTR预估模型经历了一条比较清晰的演进路线。最早大家用LR加人工特征工程后来FM、FFM自动做二阶交叉再后来DeepFM、DIN、DIEN这些深度模型把特征交叉和用户行为序列建模推到了新高度。DIN引入了注意力机制让模型根据候选物品动态地给历史行为分配权重DIEN更进一步用GRU建模了用户兴趣随时间的演化过程。但DIEN有一个容易被忽略的问题它把用户的行为序列当成一条连续的时间线来处理假设兴趣是平滑演化的。可真实场景里用户的行为往往是分段的、跳跃的。比如一个用户上午在浏览母婴用品中午突然点开了几篇关于机械键盘的帖子晚上又开始看旅游攻略。这三段行为之间可能毫无关联背后对应的是完全不同的兴趣动机。如果强行用一条RNN把整条序列串起来建模不同会话之间的“兴趣断层”就会被错误地平滑掉模型学到的兴趣表示反而变得模糊。DSINDeep Session Interest Network这篇工作正是冲着这个痛点来的。它的核心洞察是用户的历史行为应该先按会话切分在会话内部建模兴趣再在会话之间建模兴趣的演化。这个“先分后合”的思路让模型既能捕捉会话内的局部兴趣一致性又能刻画跨会话的兴趣迁移。我第一次读到这个思路的时候感觉它特别符合直觉。你想想自己刷手机的习惯是不是经常在一段时间内集中看某一类内容然后过一会儿又切换到完全不同的主题会话这个粒度恰好对应了人注意力的自然分段。DSIN把这种分段结构显式地建模进网络里比让模型自己去“悟”要靠谱得多。这篇文章我会从工程落地的角度把DSIN的每个模块拆开讲清楚会话怎么切、Bi-LSTM怎么用、会话内和会话间的注意力分别怎么设计、特征怎么组织、训练时有哪些坑。如果你正在做推荐或广告的CTR模型想在自己的数据上试试DSIN或者只是想搞明白它和DIN、DIEN的区别下面的内容应该能帮到你。2. DSIN的整体架构与设计思路拆解2.1 为什么是“会话”这个粒度在讲网络结构之前得先把“会话”这个概念说清楚。DSIN里的会话指的是把用户的历史行为序列按时间间隔切分后得到的子序列。具体来说如果两个相邻行为之间的时间差超过某个阈值论文里用的是30分钟就在这两个行为之间切一刀形成一个新的会话。为什么选30分钟这个数字不是拍脑袋来的。在电商和内容场景里30分钟大致对应一次连续的浏览行为。用户打开App刷个二三十分钟然后去干别的事过几个小时再回来。这中间的间隔就是天然的会话边界。当然这个阈值不是固定的不同业务场景下需要调整。比如短视频场景可能15分钟更合适而长视频场景可能一两个小时才算一个会话。提示会话切分的阈值是一个超参数建议在自己的数据上做敏感性分析。我的经验是先统计用户行为时间间隔的分布找到那个明显的“长尾起点”把它作为初始阈值再在验证集上微调。切分会话的好处在于它把一条可能包含几百个行为的超长序列拆成了若干个长度可控的短序列。每个会话内部的兴趣相对一致建模起来噪声更小。同时会话的数量和顺序又保留了兴趣演化的宏观信息。这种“局部精细、全局粗粒”的结构比直接对整条序列建模要高效得多。2.2 四层结构从行为到兴趣的逐级抽象DSIN的网络结构可以分成四个层次每一层都在做一次信息的抽象和压缩。第一层是会话切分层。输入是用户的行为序列输出是若干个会话。这一层没有可学习的参数纯粹是数据预处理。但它的质量直接影响后续所有模块的效果所以不能马虎。第二层是会话内建模层。每个会话内部的行为序列用一个Bi-LSTM来处理。为什么用Bi-LSTM而不是单向LSTM因为会话内部的行为顺序虽然重要但兴趣的提取不应该只依赖单向的因果关系。比如一个会话里用户先看了A再看了BBi-LSTM能同时利用“看了A之后看B”和“看了B之前看过A”两个方向的信息对会话兴趣的刻画更完整。第三层是会话间建模层。把每个会话经过Bi-LSTM得到的兴趣表示按时间顺序排成一条新的序列再用另一个Bi-LSTM来处理。这一层捕捉的是兴趣在不同会话之间的迁移和演化。比如用户从“母婴”会话过渡到“旅游”会话这个转变本身就是一个有价值的信号。第四层是注意力聚合层。这里有两个注意力模块一个是会话内的注意力用来把会话里每个行为的隐状态聚合成一个会话兴趣向量另一个是会话间的注意力用来把每个会话的兴趣向量聚合成一个全局兴趣向量。两个注意力都跟候选物品相关也就是说同一个用户面对不同的候选物品提取出的兴趣表示是不同的。这个四层结构的设计逻辑很清晰行为→会话→兴趣→全局兴趣每一步都在做信息的筛选和浓缩。最终得到的全局兴趣向量和候选物品特征、用户画像特征拼在一起送进MLP做最终的CTR预测。2.3 和DIN、DIEN的关键差异很多人会把DSIN和DIN、DIEN放在一起比较因为它们都涉及用户行为序列建模。但三者的侧重点完全不同。DIN的核心是注意力机制。它不关心行为序列的顺序只关心哪些历史行为和当前候选物品相关。所以DIN的输入是一堆行为的embedding通过注意力加权求和得到一个兴趣向量。它没有显式建模时间结构。DIEN在DIN的基础上加了兴趣演化。它用GRU把行为序列编码成一系列隐状态每个隐状态代表一个时刻的兴趣然后用注意力从这些隐状态里提取和候选物品相关的部分。但DIEN的序列是整条行为序列没有做会话切分。DSIN则是在DIEN的基础上加了会话结构。它认为兴趣演化不是一条平滑的曲线而是分段连续的。会话内部用Bi-LSTM建模局部兴趣会话之间用Bi-LSTM建模兴趣迁移再用两层注意力做聚合。这个结构比DIEN更复杂但更贴近真实的行为模式。从工程角度看DSIN的参数量和计算量都比DIN大不少但比DIEN的GRU序列建模要可控。因为会话切分把长序列拆成了短序列Bi-LSTM的处理长度大大缩短训练效率反而可能更高。3. 核心模块的细节解析与实操要点3.1 会话切分的工程实现会话切分听起来简单但在实际工程里有一些细节需要注意。首先是时间戳的处理。用户行为日志里的时间戳通常是秒级或毫秒级的Unix时间。切分时先按用户ID分组再按时间戳排序然后计算相邻行为的时间差。如果时间差超过阈值就切一刀。def split_sessions(behaviors, threshold_seconds1800): behaviors: list of (item_id, timestamp) sorted by timestamp threshold_seconds: 30 minutes default sessions [] current_session [] for i, (item_id, ts) in enumerate(behaviors): if i 0: current_session.append((item_id, ts)) else: prev_ts behaviors[i-1][1] if ts - prev_ts threshold_seconds: sessions.append(current_session) current_session [(item_id, ts)] else: current_session.append((item_id, ts)) if current_session: sessions.append(current_session) return sessions这段代码是基础版本实际生产环境里还要考虑几个问题。一是最大会话数的限制。一个用户可能有几十个会话但模型只能处理固定数量的会话。通常的做法是保留最近的N个会话N一般取5到10。二是最大会话长度的限制。单个会话可能很长比如用户连续刷了一个小时。这时候需要截断保留最近的M个行为M一般取10到20。三是冷启动用户的处理。新用户可能只有一两个行为连一个完整会话都凑不齐。这时候要么用默认值填充要么在模型里加一个标志位让模型自己决定怎么处理。注意会话切分的阈值、最大会话数、最大会话长度这三个参数需要根据业务场景的数据分布来定。建议先做一轮数据分析看看用户行为时间间隔的分布、会话长度的分布再确定初始值。3.2 Bi-LSTM在会话内建模中的角色会话内的Bi-LSTM输入是会话里每个行为的embedding序列输出是每个位置的隐状态。假设一个会话有T个行为每个行为的embedding维度是d那么Bi-LSTM的输出是一个T×2h的矩阵h是LSTM的隐层维度。为什么用Bi-LSTM而不是Transformer这是一个值得讨论的选择。Transformer在长序列建模上确实更强但会话内的序列通常不长10到20个行为Bi-LSTM的计算效率和参数量都更友好。而且Bi-LSTM对顺序的归纳偏置更强在小数据上不容易过拟合。当然如果你的会话长度经常超过50或者你有足够的算力用Transformer也是可以的。Bi-LSTM的输出需要经过一个注意力层聚合成一个会话兴趣向量。这个注意力的query是候选物品的embeddingkey和value是Bi-LSTM的隐状态。具体来说# 伪代码示意 # session_hidden: [T, 2h] # candidate_emb: [d] # W: [2h, d] 可学习参数 attention_scores softmax(candidate_emb W session_hidden.T) # [T] session_interest attention_scores session_hidden # [2h]这个注意力的含义是在会话内部哪些行为跟当前候选物品更相关就给它们更大的权重。比如候选物品是奶粉那么会话里浏览奶瓶、尿不湿的行为权重会更高而浏览手机壳的行为权重会降低。3.3 会话间Bi-LSTM与兴趣演化会话间的Bi-LSTM输入是每个会话的兴趣向量序列输出是每个会话位置的隐状态。假设有S个会话每个会话的兴趣向量维度是2h那么输出是S×2h的矩阵。这一层的作用是建模兴趣在不同会话之间的迁移。比如用户第一个会话在看母婴用品第二个会话在看旅游攻略第三个会话又在看母婴用品。Bi-LSTM能捕捉到这种“回归”模式知道用户的兴趣可能在母婴和旅游之间摇摆。同样会话间的输出也要经过注意力聚合。这里的query还是候选物品的embeddingkey和value是会话间Bi-LSTM的隐状态。最终得到一个全局兴趣向量。3.4 两层注意力的设计差异DSIN里的两个注意力模块虽然结构相似但作用完全不同。会话内注意力是在行为级别做筛选。它回答的问题是在这个会话里哪些具体行为跟候选物品最相关这个注意力的粒度细但范围局限在单个会话内。会话间注意力是在会话级别做筛选。它回答的问题是在用户的多个会话里哪些会话跟候选物品最相关这个注意力的粒度粗但覆盖了用户的全部历史。两层注意力是串联的先会话内聚合得到每个会话的兴趣向量再会话间聚合得到全局兴趣向量。这种设计的好处是模型可以先在局部找到相关行为再在全局找到相关会话层层递进最终的兴趣表示既精细又全面。实操心得在训练时两层注意力的温度参数softmax前的缩放系数可以分别设置。会话内注意力的温度可以低一些让模型更聚焦于少数关键行为会话间注意力的温度可以高一些让模型更平滑地融合多个会话的信息。这个技巧在我自己的实验里能带来0.3%到0.5%的AUC提升。4. 完整实操流程从数据准备到模型训练4.1 特征工程与数据组织DSIN的输入特征可以分成四类用户画像特征、候选物品特征、上下文特征、用户行为序列特征。用户画像特征包括年龄、性别、地域、会员等级等。这些是静态特征直接做embedding或者分桶处理。候选物品特征包括物品ID、类目、品牌、价格等。上下文特征包括时间、设备、网络环境等。这三类特征的处理方式和常规CTR模型没有区别。关键是行为序列特征的组织。每个用户需要准备三个东西行为序列本身item_id列表、对应的时间戳列表、以及每个行为的其他属性比如行为类型点击、收藏、加购。这些数据通常从用户行为日志里抽取按用户ID聚合按时间排序。数据组织成TFRecord或者Parquet格式每个样本包含用户特征、候选物品特征、上下文特征、行为序列特征、标签是否点击。行为序列特征需要做padding保证每个样本的会话数和会话长度一致。# 样本结构示意 { user_features: [0.2, 0.5, ...], # 用户画像 item_features: [0.1, 0.8, ...], # 候选物品 context_features: [0.3, 0.1, ...], # 上下文 session_items: [[1,2,3], [4,5], [6,7,8,9]], # 会话列表 session_times: [[100,200,300], [400,500], [600,700,800,900]], session_mask: [[1,1,1], [1,1], [1,1,1,1]], # padding mask label: 1 }4.2 模型搭建的关键参数DSIN的模型结构不算特别复杂但参数不少。下面是我在实际项目中用过的一套配置可以作为起点。参数建议值说明行为embedding维度32或64根据物品数量调整物品多则维度大Bi-LSTM隐层维度64或128会话内和会话间可以不同最大会话数5到10保留最近N个会话最大会话长度10到20每个会话保留最近M个行为注意力温度会话内0.5会话间1.0可调超参数Dropout0.2到0.5在embedding层和LSTM输出后加学习率1e-3到1e-4Adam优化器配合warmupBatch size256到1024根据显存调整Bi-LSTM的隐层维度选择有一个经验法则如果行为embedding是64维Bi-LSTM隐层取128维比较合适这样输出维度是256信息容量足够。如果隐层太小会话兴趣向量会欠拟合如果太大容易过拟合而且计算量增加。4.3 训练过程中的监控与调优DSIN的训练有几个关键监控指标。除了常规的AUC和LogLoss还要关注两个东西一是会话内注意力的熵二是会话间注意力的熵。注意力熵衡量的是注意力分布的集中程度。熵越低说明模型越聚焦于少数行为或会话熵越高说明模型在平均地看待所有行为。训练初期熵通常比较高随着训练进行熵会逐渐降低。如果熵一直不降说明注意力模块没有学到有效的信息可能是学习率太小或者注意力温度设置不当。def attention_entropy(attention_weights): attention_weights: [batch, seq_len] eps 1e-8 entropy -torch.sum(attention_weights * torch.log(attention_weights eps), dim-1) return entropy.mean()另一个要监控的是会话兴趣向量的方差。如果所有会话的兴趣向量都差不多说明会话间建模没有起到作用模型可能退化成DIN。这时候需要检查会话切分是否合理或者会话间Bi-LSTM是否被正确训练。踩过的坑有一次我在训练时发现AUC一直上不去排查后发现是会话切分的阈值设得太小导致每个会话只有一两个行为Bi-LSTM根本学不到东西。后来把阈值从10分钟调到30分钟AUC直接涨了1.2个点。所以会话切分的质量真的会决定模型的成败。4.4 线上部署的考量DSIN的线上部署比DIN要复杂一些因为多了会话切分和两层LSTM。延迟主要来自三个方面行为序列的读取和切分、Bi-LSTM的前向计算、两层注意力的计算。优化手段有几个。一是预计算会话兴趣向量。用户的会话兴趣向量在短时间内不会变化可以离线预计算好线上只做会话间注意力的聚合。这样能把大部分计算放在离线。二是限制会话数和会话长度。线上服务时只取最近的5个会话每个会话最多10个行为这样Bi-LSTM的序列长度可控。三是模型蒸馏。用DSIN作为teacher模型蒸馏一个更小的student模型上线牺牲一点精度换延迟。我自己的经验是在QPS几千的场景下DSIN的线上延迟可以控制在20毫秒以内前提是做好了预计算和序列截断。如果QPS更高建议走蒸馏路线。5. 常见问题与排查技巧实录5.1 会话切分相关的典型问题问题一用户行为太少切不出会话。新用户或者低频用户可能只有三五个行为时间跨度也很短切分会话后每个会话只有一两个行为。这时候Bi-LSTM的输入太短学不到有效表示。解决办法是设置一个最小会话长度比如3个行为。如果切分后某个会话不足3个行为就把它合并到相邻会话里。或者对这类用户单独走一个简化模型不用DSIN。问题二会话数量太多超出模型限制。重度用户可能有几十个会话但模型只能处理固定数量。这时候要按时间倒序保留最近的N个会话。但要注意最近的会话不一定是最相关的。有些用户的兴趣是周期性的比如每周五都会看某类内容。如果只保留最近的会话可能会丢掉这个周期性信号。一个折中方案是保留最近的N个会话同时额外保留一个“历史热门会话”即历史上行为最多的那个会话。问题三时间戳异常导致切分错误。日志里的时间戳偶尔会出现异常值比如突然跳到一个很远未来的时间或者重复的时间戳。这会导致切分出的会话结构完全错乱。处理方法是先做时间戳清洗把超出合理范围的时间戳过滤掉把重复时间戳的行为合并。5.2 模型训练中的常见故障故障一Loss不下降AUC卡在0.5。这种情况通常是数据问题。检查一下行为序列的padding是否正确mask是否传入了模型。如果padding的位置没有被mask掉Bi-LSTM会把padding的embedding也当成真实行为来处理导致学到的表示完全错误。故障二过拟合严重训练集AUC很高但验证集很低。DSIN的参数量不小尤其是两层Bi-LSTM。过拟合的常见原因是行为embedding维度太大或者LSTM隐层太大。解决办法是加Dropout减小embedding维度或者加L2正则。另外行为序列的dropout也很有效随机把一些行为mask掉强迫模型不依赖个别行为。故障三会话间注意力权重几乎均匀。这说明模型没有学到会话之间的区分度。可能的原因是会话兴趣向量的区分度不够所有会话看起来都差不多。检查一下会话内注意力的输出如果每个会话的兴趣向量都很接近那会话间注意力自然分不出轻重。这时候要回头看会话切分是否合理或者会话内Bi-LSTM是否训练充分。5.3 效果调优的实用技巧技巧一多尺度会话切分。不要只用一种时间阈值切分会话。可以同时用30分钟和2小时两个阈值切出两套会话结构分别过Bi-LSTM然后把两套兴趣向量拼在一起。这样模型能同时捕捉短时会话和长时会话的信息。这个技巧在我自己的实验里能带来0.5%左右的AUC提升代价是计算量增加。技巧二会话位置编码。在会话间Bi-LSTM的输入里给每个会话加上位置编码让模型知道哪些会话是近期的哪些是远期的。位置编码可以用可学习的方式也可以用正弦函数。这个技巧对兴趣演化建模很有帮助尤其是当用户兴趣变化较快时。技巧三注意力温度的自适应调整。不要固定注意力温度而是让模型自己学一个温度参数。具体来说把softmax前的缩放系数设为一个可学习的标量初始值为1.0让模型在训练中自己调整。这样模型可以根据数据自动决定注意力的集中程度。问题类型现象排查方向解决方案数据问题Loss不降AUC0.5检查padding和mask确保mask正确传入过拟合训练AUC高验证AUC低检查模型复杂度加Dropout减维度注意力失效注意力权重均匀检查会话兴趣向量调整会话切分阈值会话太短Bi-LSTM学不到东西统计会话长度分布提高切分阈值或合并会话线上延迟高QPS上不去检查计算瓶颈预计算蒸馏截断6. 一些工程落地的个人体会DSIN这个模型我从论文复现到线上部署前后折腾了大概两个月。最大的感受是会话切分的质量比模型结构本身更重要。论文里只说了用30分钟做阈值但实际数据里不同业务的时间间隔分布差异很大。电商场景可能30分钟合适但内容社区可能15分钟更合适而本地生活服务可能一两个小时才算一个会话。这个阈值一定要在自己的数据上做实验不能照搬。另一个体会是DSIN的收益在行为序列较长的用户上更明显。对于行为很少的新用户DSIN和DIN的差距不大因为会话结构根本体现不出来。所以如果你的业务里新用户占比很高DSIN的收益可能没有论文里说的那么大。这时候可以考虑对新用户走简化模型对老用户走DSIN做模型的分层。还有一点DSIN的训练对数据量有一定要求。如果行为序列的样本太少两层Bi-LSTM很容易过拟合。我的经验是至少要有百万级别的训练样本DSIN才能稳定地超过DIN。如果数据量不够建议先用DIN等数据积累够了再上DSIN。最后分享一个小技巧在会话间Bi-LSTM的输出后面可以加一个残差连接把原始会话兴趣向量直接加到Bi-LSTM的输出上。这样即使Bi-LSTM没学好模型至少能退化成DIN的效果不会比DIN差。这个残差连接在我自己的实验里让模型在训练初期的稳定性好了很多收敛也更快。