news 2026/10/27 12:09:18

DSIN深度会话兴趣网络:会话划分、兴趣提取与演化建模实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DSIN深度会话兴趣网络:会话划分、兴趣提取与演化建模实战

1. 从“用户兴趣会漂移”说起:DSIN到底在解决什么问题

做推荐系统和广告排序的同学,大概率都经历过这样的场景:离线AUC涨了两个千分位,上线后CTR却纹丝不动,甚至偶尔还跌一点。排查到最后,往往不是特征工程没做好,也不是模型容量不够,而是用户兴趣的时序结构被粗暴地压扁了。我们习惯把用户历史行为序列当成一个“袋子”,用pooling或者attention一把揉成一个向量,然后喂给后面的MLP。这种做法在行为序列短、兴趣稳定的场景下没问题,但一旦用户行为跨越多个会话,兴趣在会话之间发生了明显跳变,这种“一锅炖”的建模方式就会丢失最关键的信息——兴趣的边界在哪里,兴趣是怎么演化的。

DSIN(Deep Session Interest Network,深度会话兴趣网络)就是冲着这个问题来的。它要做的核心事情,是把用户的历史行为先按时间切分成一个个会话(Session),然后在会话内部建模兴趣的同质性,在会话之间建模兴趣的演化性,最后再把这两层信息融合起来做CTR预估。说白了,它认为用户的兴趣不是一条平滑的曲线,而是一段一段的,每一段内部相对稳定,段与段之间会发生迁移。

这个思路听起来很直觉,但真正落地的时候,有几个关键问题必须回答清楚:会话怎么切?会话内部的兴趣用什么建模?会话之间的演化又用什么建模?这三个问题分别对应DSIN的三个核心模块——会话划分层、会话兴趣提取层、会话兴趣演化层。我下面会把这几个模块拆开讲,重点讲清楚每个设计选择背后的“为什么”,以及在实际工程中怎么落地、怎么避坑。

适合读这篇内容的人,我大致分三类:一是正在做推荐/广告CTR模型,想从序列建模角度找突破口的算法工程师;二是对Transformer、Bi-LSTM、Attention这些结构在推荐场景下怎么组合感兴趣的同学;三是准备复现DSIN或者拿它做baseline的研究者。不管你是哪一类,我都尽量把原理讲到能自己推导、把代码讲到能直接抄作业的程度。

2. 会话划分:DSIN的第一刀切在哪里

2.1 为什么一定要切会话,而不是直接用完整序列

先回答一个最容易被跳过的问题:为什么不直接把用户全部行为序列丢给Transformer或者RNN,非要先切会话?原因有两个,而且都很硬。

第一个原因是兴趣的局部稳定性。用户在一次连续浏览中,兴趣通常是聚焦的。比如晚上十点打开购物App,连续看了五六个机械键盘,这半小时内的行为高度同质,都是在“键盘”这个兴趣点上。但如果把时间拉长到一个月,中间可能还买过猫粮、看过跑鞋、搜过显示器。这些行为混在一起,如果用全局attention,模型很难区分“当前这次会话到底在关注什么”,因为远处那些不相关的行为会形成噪声。

第二个原因是计算和噪声的权衡。完整序列动辄几百上千,直接上Transformer,attention矩阵是O(n²),显存和延迟都吃不消。切分会话之后,每个会话长度通常控制在几十以内,会话数量也有限,整体计算量可控。更重要的是,切分本身就是一种去噪:把时间上不连续、兴趣上不相关的行为隔离开,让模型在每个会话内部专注建模同质兴趣。

注意:会话划分不是DSIN首创,但DSIN把它和后续的双层建模(会话内+会话间)绑在一起,形成了一个完整的兴趣建模链路。这一点是它区别于普通Session-based推荐的地方。

2.2 会话划分的两种主流策略:固定窗口 vs 时间间隔

实际落地时,会话划分主要有两种做法,我分别说一下适用场景和坑。

第一种是固定时间窗口,比如按自然天切分,一天一个会话。这种做法实现简单,离线特征也好回溯,但问题是它假设“一天之内兴趣稳定”,这个假设在很多场景下不成立。用户上午看母婴、下午看数码,硬塞进一个会话,反而引入了噪声。所以固定窗口更适合行为密度低、兴趣切换慢的场景,比如长视频、低频电商。

第二种是基于时间间隔的动态切分,这也是DSIN论文里采用的做法:如果相邻两次行为的时间差超过某个阈值(比如30分钟),就认为进入了新会话。这个阈值的设定很关键,我自己的经验是,不要拍脑袋定,要看业务的行为间隔分布。具体做法是:统计用户相邻行为时间差的分布,找到那个“长尾起点”,比如80分位或者90分位对应的时间差,作为切分阈值。这样切出来的会话,内部行为间隔紧凑,会话之间有明显断裂。

# 基于时间间隔的会话切分示例 def split_sessions(behaviors, gap_threshold=1800): """ behaviors: list of (item_id, timestamp) 按时间升序 gap_threshold: 秒,默认30分钟 """ sessions = [] current = [] for i, (item, ts) in enumerate(behaviors): if i == 0: current.append((item, ts)) else: if ts - behaviors[i-1][1] > gap_threshold: sessions.append(current) current = [(item, ts)] else: current.append((item, ts)) if current: sessions.append(current) return sessions

这段代码看起来简单,但有两个细节要注意。一是时间戳必须严格升序,如果原始日志有乱序,要先排序,否则切分会错乱。二是阈值要分场景调,比如短视频场景用户切换快,阈值可以设短一点(10-15分钟);电商场景用户可能反复比价,阈值可以设长一点(30-60分钟)。我一般会做A/B,看不同阈值下离线AUC和线上CTR的差异,选一个平衡点。

2.3 会话数量截断与长度截断的工程取舍

真实场景里,用户历史可能非常长,切出来的会话数量可能几十上百。直接全用,计算量爆炸;截断太多,又丢信息。DSIN论文里对会话数量和每个会话的长度都做了截断,通常是保留最近N个会话,每个会话保留最近M个行为。这个N和M怎么定?

我的经验是,N取5-10,M取10-20,具体看业务。N太大,远处会话对当前预测的贡献本来就低,还增加噪声;N太小,可能丢掉周期性兴趣。M太大,会话内部attention的复杂度上升,而且一个会话内行为太多,本身也可能包含多个子兴趣。实际调参时,我会先固定一个保守值(比如N=5,M=10),然后逐步增加,观察离线指标是否还有提升,直到收益递减。

提示:截断时一定要保留最近的会话和行为,因为CTR预估本质是预测“下一次点击”,越近的行为相关性越高。这一点和NLP里的长文本建模不同,不能简单用“取前N个”或者“随机采样”。

3. 会话兴趣提取:Bi-LSTM + Attention到底在学什么

3.1 为什么用Bi-LSTM而不是普通RNN或Transformer

会话内部的兴趣提取,DSIN用的是Bi-LSTM + Attention。这个选择在当时(2019年)是很合理的,放到今天看,虽然Transformer大行其道,但Bi-LSTM在这个位置依然有它的道理。

先说为什么不用单向RNN。会话内部的行为是有顺序的,比如用户先看了“机械键盘”,再看了“键帽”,再看了“轴体”,这个顺序反映了兴趣的递进。单向RNN只能看到历史,看不到未来,而Bi-LSTM同时看前后文,能更好地捕捉“这个行为在会话中处于什么位置、和前后行为是什么关系”。比如“键帽”这个行为,前有“机械键盘”,后有“轴体”,Bi-LSTM能把它编码成一个更准确的兴趣表示。

再说为什么不用Transformer。Transformer的self-attention确实强,但它对位置信息的编码是隐式的,而且在小规模会话(长度10-20)上,Bi-LSTM的归纳偏置(顺序敏感、局部依赖)反而更匹配。另外,Bi-LSTM的计算复杂度是O(n),Transformer是O(n²),在会话长度不大时差距不明显,但Bi-LSTM的实现更简单,线上推理也更稳。当然,如果你现在要复现,完全可以试试用Transformer encoder替换Bi-LSTM,我实测过在一些场景下Transformer能涨点,但需要调好位置编码和层数。

3.2 Attention在会话内部的作用:给行为加权

Bi-LSTM输出的是每个行为的隐状态,但一个会话的兴趣表示不能简单用最后一个隐状态或者平均池化。因为会话内部的行为重要性不同,比如用户点了一个商品,又滑过了几个,显然点击行为更重要。Attention的作用就是自动学习每个行为的权重,然后加权求和得到会话的兴趣向量。

具体来说,DSIN用一个可学习的query向量去和每个行为的隐状态做attention,公式大致是:

a_i = softmax(v^T * tanh(W * h_i + b)) session_interest = sum(a_i * h_i)

这里的query向量是全局共享的,相当于在问“什么样的行为对表达这个会话的兴趣最重要”。这个设计的好处是不依赖人工规则,模型自己学权重。我踩过的一个坑是:如果会话内行为类型差异很大(比如点击、收藏、加购混在一起),最好给不同行为类型加embedding区分,否则attention可能会被高频行为主导。

import torch import torch.nn as nn class SessionInterestExtractor(nn.Module): def __init__(self, item_dim, hidden_dim): super().__init__() self.bilstm = nn.LSTM(item_dim, hidden_dim, batch_first=True, bidirectional=True) self.attention_query = nn.Parameter(torch.randn(hidden_dim * 2)) self.attention_fc = nn.Linear(hidden_dim * 2, hidden_dim * 2) def forward(self, session_items): # session_items: [batch, session_len, item_dim] lstm_out, _ = self.bilstm(session_items) # [batch, session_len, hidden*2] score = torch.tanh(self.attention_fc(lstm_out)) score = torch.matmul(score, self.attention_query) # [batch, session_len] weight = torch.softmax(score, dim=-1) interest = torch.sum(weight.unsqueeze(-1) * lstm_out, dim=1) # [batch, hidden*2] return interest

这段代码是简化版,实际落地时还要考虑mask,因为padding的位置不能参与attention。mask的处理很简单,在softmax之前把padding位置的score设成负无穷即可。这个细节很多开源实现会漏掉,导致padding影响兴趣表示,离线指标会掉。

3.3 会话兴趣提取的实操心得

我在实际训练这个模块时,有几个经验值得分享。

第一,Bi-LSTM的hidden_dim不要设太大。会话长度通常只有10-20,hidden_dim设64或128就够了,设256以上容易过拟合,而且推理延迟明显上升。我一般从64开始试,如果离线AUC还有提升空间,再往上加。

第二,attention的query初始化很重要。如果随机初始化,训练初期attention权重接近均匀,相当于平均池化,收敛慢。我一般用Xavier初始化,并且把学习率设得比主网络稍大一点,让attention更快学到有区分度的权重。

第三,会话内部的行为embedding最好和后面演化层的embedding共享。DSIN里item embedding是全局共享的,这样参数效率高,而且会话内和会话间的兴趣表示在同一空间里,融合时更自然。如果分开,不仅参数多,还可能出现空间不一致的问题。

4. 会话兴趣演化:从“各自为政”到“前后关联”

4.1 为什么会话之间需要演化建模

切完会话、提完每个会话的兴趣之后,我们得到的是一个会话兴趣序列:[S1, S2, S3, ..., Sn]。如果直接把这些兴趣向量拼接或者平均,就丢掉了会话之间的顺序和演化关系。但用户的兴趣是会迁移的,比如从“键盘”迁移到“显示器”,再到“主机”,这是一个逐步升级的装机过程。DSIN用Bi-LSTM + Attention来建模这个演化过程,思路和会话内部类似,但作用对象从“行为”变成了“会话兴趣”。

这里有个关键点:会话之间的演化不是简单的时序依赖,而是有选择性的。用户可能中间插入了一个不相关的会话(比如帮朋友看了一个母婴商品),这个会话对当前预测的贡献应该被抑制。所以DSIN在演化层也用了attention,让模型自己决定哪些历史会话对当前兴趣影响大。

4.2 Bi-LSTM在演化层的双向建模

演化层用Bi-LSTM,意味着它不仅看“过去如何影响现在”,也看“未来如何反推现在”。在CTR预估里,我们预测的是下一次点击,所以严格来说只有历史信息可用。但DSIN在训练时用了双向,因为训练数据里“未来”是已知的,双向能学到更丰富的表示。线上推理时,只取前向的最后一个隐状态,或者把双向输出拼接后过一层变换。

这个做法在工业界有争议。支持的人认为,训练时双向能提供更强的监督信号,相当于一种正则;反对的人认为,训练和推理不一致会带来偏差。我的经验是,如果线上延迟允许,训练用双向、推理用单向是可行的,但要做一致性校验。具体做法是:离线评估时,分别用双向和单向推理,看AUC差距。如果差距在千分位以内,可以接受;如果差距大,说明模型过度依赖未来信息,要改成单向训练。

4.3 演化层的attention设计细节

演化层的attention和会话内attention结构类似,但query的语义不同。会话内attention问的是“哪些行为重要”,演化层attention问的是“哪些历史会话对当前兴趣重要”。DSIN里,演化层的attention query是当前会话的兴趣向量,也就是说,用当前会话去和历史会话做匹配,匹配度高的历史会话权重大。

这个设计很符合直觉:如果当前会话在看“显示器”,那么历史会话里看“键盘”和“主机”的权重大,看“猫粮”的权重小。实际实现时,可以用加性attention或者点积attention,我一般用加性,因为它在小数据上更稳。

class SessionEvolutionLayer(nn.Module): def __init__(self, interest_dim, hidden_dim): super().__init__() self.bilstm = nn.LSTM(interest_dim, hidden_dim, batch_first=True, bidirectional=True) self.attention_fc = nn.Linear(hidden_dim * 2, hidden_dim * 2) def forward(self, session_interests): # session_interests: [batch, num_sessions, interest_dim] lstm_out, _ = self.bilstm(session_interests) # [batch, num_sessions, hidden*2] # 用最后一个会话的兴趣作为query query = lstm_out[:, -1, :] # [batch, hidden*2] score = torch.tanh(self.attention_fc(lstm_out)) score = torch.sum(score * query.unsqueeze(1), dim=-1) # [batch, num_sessions] weight = torch.softmax(score, dim=-1) evolution_interest = torch.sum(weight.unsqueeze(-1) * lstm_out, dim=1) return evolution_interest

这段代码里,query用的是最后一个会话的LSTM输出,而不是原始兴趣向量,这样能融合上下文信息。实际调参时,也可以试试用所有会话的加权平均作为query,或者用一个可学习的query,效果差异不大,看具体数据。

4.4 演化层实操中的三个坑

第一个坑是会话数量截断后的mask。如果用户只有3个会话,但模型固定输入5个,padding的会话不能参与attention和LSTM。LSTM的mask要用pack_padded_sequence,attention的mask要在softmax前设负无穷。这个细节不做,padding会污染演化表示,离线指标可能掉一个百分点以上。

第二个坑是会话兴趣的归一化。Bi-LSTM输出的兴趣向量尺度可能不一致,直接做attention时,尺度大的会话会主导权重。我一般会在演化层之前加一层LayerNorm,或者对兴趣向量做L2归一化,让尺度一致。

第三个坑是学习率设置。演化层的Bi-LSTM比会话内的Bi-LSTM更深,训练时容易梯度消失。我一般给演化层设更小的学习率,或者用梯度裁剪(clip=5.0),保证训练稳定。

5. 特征融合与CTR预估:把兴趣表示用起来

5.1 局部激活单元:让兴趣表示和候选物品交互

DSIN的最后一层是局部激活单元(Local Activation Unit),这个设计借鉴了DIN(Deep Interest Network)。它的作用是:会话兴趣表示和演化兴趣表示不是直接拼接送MLP,而是先和候选物品做attention,得到“针对当前候选物品的兴趣表示”,再拼接。

为什么这么做?因为用户的兴趣是多峰的,候选物品不同,相关的兴趣也不同。比如候选是“机械键盘”,那么会话里看键盘的兴趣应该被激活;候选是“猫粮”,那么看键盘的兴趣就不相关。局部激活单元就是做这个兴趣筛选的。

具体实现上,把候选物品embedding作为query,和会话兴趣序列、演化兴趣序列分别做attention,得到两个加权表示,然后和候选物品embedding、用户画像特征等拼接,送MLP。

5.2 特征拼接与MLP结构

拼接的特征通常包括:局部激活后的会话兴趣、局部激活后的演化兴趣、候选物品embedding、用户画像特征(年龄、性别等)、上下文特征(时间、位置等)。MLP结构一般是3层,每层维度递减,比如256-128-64,最后接一个sigmoid输出CTR概率。

这里有个经验:MLP的输入最好做BN或者LayerNorm,因为不同特征的尺度差异大,不做归一化训练会不稳定。另外,dropout在CTR模型里要慎用,我一般只在最后两层加dropout(rate=0.2-0.3),太靠前会丢信息。

5.3 损失函数与训练技巧

CTR预估是二分类,损失函数用交叉熵。但实际数据里负样本远多于正样本,所以要做负采样或者用focal loss。我一般用负采样,正负比1:3到1:5,具体看数据。采样后要做校准,否则预测概率有偏。

训练时,Adam优化器是标配,学习率从1e-3开始,每轮衰减0.9。batch size设256或512,太大收敛慢,太小不稳定。epoch一般3-5轮,看验证集AUC,早停。

注意:DSIN的参数比普通DNN多不少,训练时容易过拟合。除了dropout,还可以加L2正则(weight_decay=1e-5),或者用embedding dropout。我实测下来,embedding dropout对CTR模型很有效,尤其是item embedding维度大的时候。

6. 常见问题与排查技巧实录

6.1 离线AUC涨了,线上CTR不涨反跌

这是最经典的问题。原因通常有三个:一是训练和推理不一致,比如训练用了双向LSTM,推理用了单向,但没做一致性校验;二是特征穿越,比如会话划分时用了未来信息,或者时间戳处理有误;三是线上特征分布和离线不一致,比如会话切分阈值在线上变了,但模型没重训。

排查思路:先做离线模拟线上,用单向推理跑一遍离线评估,看AUC是否还涨。如果掉很多,说明模型依赖了未来信息。再检查特征回溯逻辑,确保每个特征在预测时刻都是可用的。最后对比线上线下特征分布,看是否有明显偏移。

6.2 会话切分后,短会话太多怎么办

有些用户行为稀疏,切出来的会话很多只有1-2个行为。这种短会话用Bi-LSTM建模意义不大,反而增加噪声。我的做法是:对长度小于阈值的会话,直接用行为embedding的平均或者最后一个行为表示,不走Bi-LSTM。或者把相邻的短会话合并,直到长度达到阈值。这个策略在稀疏场景下能明显提升效果。

6.3 训练loss震荡,不收敛

DSIN结构深,训练不稳定很常见。排查顺序:先看学习率,是不是太大,试试降到1e-4;再看梯度裁剪,有没有加;然后看初始化,Bi-LSTM和attention的初始化是否合理;最后看batch size,太小会导致梯度噪声大,试试加大到512或1024。

还有一个容易被忽略的点:会话数量截断后的padding。如果padding的会话参与了LSTM和attention,梯度会乱。一定要用mask,并且检查mask逻辑是否正确。

6.4 线上推理延迟高

DSIN的推理延迟主要来自Bi-LSTM和attention。优化手段:一是减少会话数量和长度,N从10降到5,M从20降到10,延迟能降一半;二是用单向LSTM推理,比双向快;三是把item embedding做量化或者哈希,减少内存和计算;四是模型蒸馏,用DSIN训一个小模型上线。

我一般会先做延迟profile,看瓶颈在哪一层。如果是attention,可以试试用线性attention或者把attention头数减少;如果是LSTM,可以试试用GRU或者QRNN替代。

常见问题可能原因排查方法解决技巧
离线涨线上跌训练推理不一致/特征穿越单向推理离线评估/特征回溯检查统一训练推理/严格时间切分
短会话太多行为稀疏/切分阈值太小统计会话长度分布短会话走平均池化/合并相邻会话
Loss震荡学习率大/无梯度裁剪/无mask检查学习率和mask逻辑降学习率/加clip/加mask
推理延迟高会话多/双向LSTM/embedding大profile各层耗时截断会话/单向推理/量化蒸馏

6.5 独家避坑技巧

最后分享几个我在实际项目里踩过的坑,都是文档里不会写的。

第一个是时间戳的时区问题。如果日志里的时间戳是UTC,但业务按本地时间切会话,切出来的会话边界会错。我遇到过因为时区没对齐,导致会话切分完全乱掉,离线AUC掉了一个点。解决办法是统一时区,或者在切分前做时区转换。

第二个是item embedding的冷启动。DSIN依赖item embedding,新item没有历史行为,embedding是随机初始化的,会导致会话兴趣表示不准。我的做法是:对新item,用内容特征(类目、品牌、标题embedding)初始化,或者用同类目item的embedding平均。这个策略能明显提升新item的CTR。

第三个是会话兴趣的维度。DSIN里会话兴趣和演化兴趣的维度通常和item embedding一致,比如64或128。如果维度太大,attention计算慢;太小,表达能力不足。我一般从64开始,如果离线AUC还有提升空间,再往上加。实测128是一个比较平衡的值。

第四个是负采样后的校准。负采样会改变正负样本比例,导致模型输出的概率偏高。上线前一定要做概率校准,比如用Platt scaling或者isotonic regression,否则出价会偏高,影响ROI。

这个内容后续还可以这样扩展:一是把Bi-LSTM换成Transformer encoder,对比效果;二是把会话划分从固定阈值改成可学习的软划分;三是把DSIN和多任务学习结合,同时预估CTR和CVR。我自己试过第一个方向,在行为序列长的场景下,Transformer能涨点,但需要调好位置编码和层数,训练成本也更高。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 12:09:16

de4dot-netcore:专为.NET 6/7/8设计的跨平台反混淆工具

简介:本资源为适配.NET Core平台的开源脱壳工具de4dot-netcore正式构建版本,面向安全研究人员、逆向工程师及.NET开发者,专用于剥离ConfuserEx、DNEmu、.NET Reactor等主流保护壳,还原被混淆或加密的.NET Core程序原始逻辑&#x…

作者头像 李华
网站建设 2026/10/27 12:08:37

Transformer注意力机制演进:从缩放点积到高效变体

1. 从一张注意力热力图说起:Transformer到底在算什么 很多人第一次接触Transformer,是从一张花花绿绿的注意力热力图开始的。图上每个词对其它词的关注程度用深浅不一的色块表示,看起来挺直观,但真要问一句“这张图到底是怎么算出…

作者头像 李华
网站建设 2026/10/27 12:08:23

动力电池与自动驾驶融合:技术协同与商业创新

1. 动力电池与自动驾驶的资本联姻2023年开年最引人注目的融资事件,莫过于自动驾驶公司Momenta在半年内连续完成C轮和D轮融资,累计金额高达17亿美元。更值得玩味的是,动力电池巨头宁德时代赫然出现在投资方名单中。这场看似跨界的资本联姻&…

作者头像 李华
网站建设 2026/10/27 12:08:03

Sybase 15.7 安装避坑指南:密钥、服务账户与验证全流程

简介:这份资源是面向数据库初学者与运维人员的 Sybase 15.7 安装图解说明,针对初次接触 Sybase 的用户在安装环节容易卡壳、选项拿不准的问题,以逐步截图配合文字的方式给出可对照执行的指导。压缩包内共 1 个 doc 文档,约 1.52MB…

作者头像 李华
网站建设 2026/10/27 12:07:58

Windows 11右键菜单变革与效率优化指南

1. Windows 11右键菜单的颠覆性改变 Windows 11的右键菜单设计是系统界面革新中最具争议的改动之一。微软将沿用多年的经典右键菜单彻底重构,采用了全新的Fluent Design设计语言。这个看似简单的交互元素变化,实际上反映了微软对现代操作系统交互逻辑的重…

作者头像 李华