3.3. 序列建模¶
在上一节中,我们探讨了如何通过各类特征交叉模型,让机器自动学习特征之间复杂的组合关系。无论是二阶交叉的FM、AFM,还是高阶交叉的DCN、xDeepFM,它们的核心目标都是从一个静态的特征集合中挖掘出有价值的信息。然而,这些模型普遍存在一个共同的局限:它们大多将用户的历史行为看作一个无序的“物品袋”(a bag of items),如同用户的兴趣是一个静态的表示。
但用户的兴趣不是静止的,而是具有明显的时序性和动态演化特点。一个用户先浏览“鼠标”再浏览“显示器”,与先浏览“小说”再浏览“显示器”,这两个行为序列背后指向的购买意图截然不同。前者可能是一位正在组装电脑的数码爱好者,而后者可能只是在工作之余的随性浏览。传统的特征交叉模型难以捕捉这种蕴含在行为顺序中的、随时间变化的意图。
因此,本节我们将转换视角,不再将用户历史看作一堆静态特征的集合,而是将其视为一个动态的序列。我们将聚焦于如何对用户的行为序列进行建模,从这个序列中挖掘出用户动态、演化的兴趣。接下来,我们将介绍工业界在序列建模方向上的三个代表性模型:DIN、DIEN和DSIN,看看它们是如何解决这个核心挑战的。
3.3.1. 局部激活的注意力机制¶
在大型电商平台中,用户的兴趣是多样的。一个用户可能在一段时间内,既关注数码产品,又浏览运动装备,还会购买生活用品。在传统的深度学习模型(即Embedding&MLP范式)中,通常的做法是将用户所有的历史行为(如点击过的商品ID)对应的Embedding向量通过池化(Pooling)操作,压缩成一个固定长度的向量来代表该用户。
这个固定长度的用户向量,很快就成为了表达用户多样兴趣的瓶颈。想象一下,无论系统准备向这个用户推荐“跑鞋”还是“手机”,代表他的都是同一个向量。这个向量试图“一视同仁”地蕴含该用户所有的兴趣点,这不仅非常困难,而且在面对具体推荐任务时显得不够聚焦。为了增强表达能力而粗暴地增加向量维度,又会带来参数量爆炸和过拟合的风险。
DIN的核心思想:局部激活 (Local Activation)
深度兴趣网络(Deep Interest Network, DIN) (Zhou et al., 2018) 的提出者们发现,用户的某一次具体点击行为,通常只由其历史兴趣中的一部分所“激活”。当向一位数码爱好者推荐“机械键盘”时,真正起决定性作用的,很可能是他最近浏览“游戏鼠标”和“显卡”的行为,而不是他上个月购买的“跑鞋”。
基于此,DIN提出了一个观点:用户的兴趣表示不应该是固定的,而应是根据当前的候选广告(Target Ad)不同而动态变化的。

图3.3.1 DIN模型架构图(右)及其与基准模型(左)的对比¶
技术实现:注意力机制
为了实现“局部激活”这一思想,DIN在模型中引入了一个关键模块——局部激活单元(Local Activation Unit),其本质就是注意力机制。如上图右侧所示,DIN不再像基准模型(图3.3.1 左)那样对所有历史行为的Embedding进行简单的池化,而是进行了一次“加权求和”。
这个权重(即注意力分数)的计算,体现了DIN的核心思想。具体来说,对于一个给定的用户U和候选广告A,用户的兴趣表示向量\(\boldsymbol{v}_{U}(A)\)是这样计算的:
其中:
\(\boldsymbol{e}_{1}, \boldsymbol{e}_{2}, \ldots, \boldsymbol{e}_{H}\) 是用户U的历史行为Embedding向量列表。
\(\boldsymbol{v}_{A}\) 是候选广告A的Embedding向量。
\(a(\boldsymbol{e}_{j}, \boldsymbol{v}_{A})\) 是一个激活单元(通常是一个小型前馈神经网络),它接收历史行为\(\boldsymbol{e}_{j}\)和候选广告\(\boldsymbol{v}_{A}\)作为输入,输出一个权重\(\boldsymbol{w}_{j}\)。这个权重就代表了历史行为\(\boldsymbol{e}_{j}\)在面对广告\(\boldsymbol{v}_{A}\)时的“相关性”或“注意力得分”。
通过这个公式,用户的最终兴趣表示\(\boldsymbol{v}_{U}(A)\)不再是一个固定的向量,而是与候选广告A紧密相关。与广告A越相关的历史行为,会获得越高的权重,从而在最终的兴趣向量中占据主导地位。
一个值得注意的细节是,DIN计算出的注意力权重\(\boldsymbol{w}_{j}\)没有经过Softmax归一化。这意味着\(\sum \boldsymbol{w}_{j}\)不一定等于1。这样设计的目的是为了保留用户兴趣的绝对强度。例如,如果一个用户的历史行为大部分都与某个广告高度相关,那么加权和之后的向量模长就会比较大,反之则较小。这种设计使得模型不仅能捕捉兴趣的“方向”,还能感知兴趣的“强度”。
3.3.2. 兴趣的演化建模¶
DIN成功地捕捉了用户兴趣的“多样性”和“局部激活”特性,但它仍然存在一个局限:它将用户的历史行为看作是一个无序的集合,忽略了行为之间的时序依赖关系。用户的兴趣不仅是多样的,更是在持续演化的。
为了解决这个问题,深度兴趣演化网络(Deep Interest Evolution Network, DIEN) (Zhou et al., 2019) 被提出。DIEN认为,我们不仅要关注哪些历史兴趣是相关的,更要理解这些兴趣是如何一步步演化至今的。

图3.3.2 DIEN模型架构图¶
DIEN的核心思想是,直接对原始、显性的行为序列建模是不够的。行为只是表象,我们更应该关注行为背后那个潜在的、抽象的 “兴趣”状态,并对这个兴趣状态的演化过程进行建模。为此,DIEN设计了一个两阶段结构,如上图所示。
第一阶段:兴趣提取层 (Interest Extractor Layer)
这一层的目标是从原始的行为序列中,抽取出更能代表“潜在兴趣”的兴趣状态序列。
DIEN使用门控循环单元(GRU)来按时间顺序处理用户的行为Embedding序列\({\boldsymbol{e}_1, \boldsymbol{e}_2, \dots, \boldsymbol{e}_T}\)。理论上,GRU在t时刻的隐状态\(\boldsymbol{h}_t\)就捕捉了到该时刻为止的序列信息。但DIEN的作者认为,这样的隐状态还不足以精准地代表“兴趣”。
因此,他们引入了一项关键创新:辅助损失 (Auxiliary Loss)。其核心思想是:用户在:math:`t`时刻的兴趣,直接导致了他在:math:`t+1`时刻的行为。基于此,DIEN增加了一个辅助的监督任务:用\(t\)时刻的兴趣状态\(\boldsymbol{h}_t\)去预测用户在\(t+1\)时刻的真实行为\(\boldsymbol{e}_{t+1}\)。
具体地,辅助损失\(L_{aux}\)定义如下:
其中:
\(\boldsymbol{h}^i_t\) 是用户i在t时刻的兴趣状态(即GRU的隐状态)。
\(\boldsymbol{e}^i_{b[t+1]}\) 是用户i在t+1时刻真实点击的物品Embedding(正样本)。
\(\boldsymbol{\hat{e}}^i_{b[t+1]}\) 是从物品池中负采样得到的物品Embedding(负样本)。
\(\sigma(\cdot)\) 是Sigmoid函数,这里用于计算两个向量的点积并映射到(0,1)区间。
这个辅助损失会与模型最终的CTR预测损失\(L_{target}\)加在一起共同优化:\(L = L_{target} + \alpha L_{aux}\)。这个额外的监督信号,在每个时间步都对GRU的学习进行指导,使其产出的隐状态\(\boldsymbol{h}_t\)能够更精准地表达用户的潜在兴趣。
第二阶段:兴趣演化层 (Interest Evolving Layer)
经过第一阶段,我们得到了一个更能代表用户内在兴趣的兴趣状态序列 \(\boldsymbol{h}_1, \boldsymbol{h}_2, \dots, \boldsymbol{h}_T\)。第二阶段的目标,就是对这个兴趣序列的演化过程进行建模。
然而,兴趣的演化并不总是平滑的,常常会伴随着兴趣漂移(Interest Drifting)现象,即用户可能在不同的兴趣点之间快速切换。如果用一个标准的GRU来建模这个兴趣序列,不相关的历史兴趣(漂移)可能会干扰对当前主要兴趣演化的判断。
为了解决这个问题,DIEN再次借鉴了DIN的思想,并将其与序列模型融合,设计了带注意力更新门的GRU(AUGRU)。AUGRU的核心是在GRU的更新门(Update Gate)上融入了注意力机制。注意力得分\(a_t\)由\(t\)时刻的兴趣状态\(\boldsymbol{h}_t\)和候选广告\(\boldsymbol{e}_a\)共同决定:
然后,这个注意力得分\(a_t\)会去调整(scale)GRU的原始更新门\(\boldsymbol{u}'_t\):
最后,使用这个被注意力调整过的更新门\(\boldsymbol{\tilde{u}}'_t\)来更新隐状态:
其中\(\circ\)表示元素级乘积(element-wise product)。
通过这种方式,AUGRU在兴趣演化的每一步,都会参考当前的候选广告,来判断历史兴趣的相关性。与候选广告越相关的兴趣,其对应的\(a_t\)越大,其信息在更新门中的权重也越大,从而能更顺畅地在序列中传递;反之,不相关的兴趣(漂移)其影响力就会被削弱。这使得模型能够聚焦于与当前推荐任务最相关的兴趣演化路径。
3.3.3. 从行为序列到会话序列¶
从DIN到DIEN,我们看到了模型对用户兴趣的理解从“静态相关”走向了“动态演化”。然而,它们都将用户的行为看作一条连续的序列。但现实中,用户的行为模式更多是间断性的。用户通常在一个会话(Session) 内拥有一个明确且集中的意图,而在不同会话之间,兴趣点可能发生巨大转变。

图3.3.3 用户行为的会话结构示例¶
如上图所示,一个用户可能在一个会话里集中浏览各种裤子,而在下一个会话则专注于戒指。这种会话内同质、会话间异质的现象非常普遍。如果直接用一个RNN模型处理这种“断层”明显的长序列,模型需要花费很大力气去学习这种兴趣的突变,效果并不理想。
深度会话兴趣网络(Deep Session Interest Network, DSIN) (Feng et al., 2019) 基于这一观察,提出我们应该将“会话”作为分析用户行为的基本单元,并采用一种分层的思想来建模。

图3.3.4 DSIN模型架构图¶
DSIN的技术实现:分层建模
DSIN的架构如上图所示,其建模过程可以清晰地分为几个层次:
会话划分层 (Session Division Layer):这是模型的第一步,也是DSIN的基础。它根据行为发生的时间间隔(例如,如果两个行为间隔超过30分钟),将原始的、连续的用户行为长序列\(\mathbf{S}\),切分成多个独立的会话短序列\(\mathbf{Q} = [\mathbf{Q}_1, \mathbf{Q}_2, ..., \mathbf{Q}_K]\)。
会话兴趣提取层 (Session Interest Extractor Layer):这一层的目标是为每一个会话\(\mathbf{Q}_k\)提取出一个核心的兴趣向量。DSIN认为,一个会话内的行为虽然意图集中,但彼此之间的重要性也不同。因此,它没有使用简单的池化,而是采用了自注意力机制(Self−Attention)(与Transformer的核心思想一致)。自注意力网络能够捕捉该会话内部所有行为之间的内在关联,并聚合最重要的信息,最终为每个会话\(\mathbf{Q}_k\)生成一个浓缩的兴趣向量\(\mathbf{I}_k\)。
会话兴趣交互层 (Session Interest Interacting Layer):经过上一步,我们得到了一个更高层次的序列——会话兴趣向量的序列\(\mathbf{I}_1, \mathbf{I}_2, ..., \mathbf{I}_K\)。这个序列反映了用户兴趣在更长的时间尺度上的演变。DSIN使用一个 双向长短期记忆网络(Bi-LSTM) 来对这个会话序列进行建模,从而捕捉不同会话之间的演进和依赖关系。Bi-LSTM的输出是一个包含了上下文信息的会话兴趣序列\([\mathbf{H}_1, \mathbf{H}_2, ..., \mathbf{H}_K]\)。
会话兴趣激活层 (Session Interest Activating Layer):最后一步与DIN的思想一脉相承。模型会根据当前的候选广告\(\mathbf{X}_I\),使用注意力机制来计算每个会话兴趣的重要性,并进行加权求和,得到最终的用户兴趣表示。DSIN分别对会话兴趣提取层和交互层的输出都进行了激活:
(3.3.6)¶\[\mathbf{U}^{I} = \sum_{k=1}^{K} a_{k}^{I} \mathbf{I}_{k} \quad \text{和} \quad \mathbf{U}^{H} = \sum_{k=1}^{K} a_{k}^{H} \mathbf{H}_{k}\]其中,\(a_{k}^{I}\)和\(a_{k}^{H}\)是根据候选广告计算出的注意力权重。最终,将这两个激活后的向量\(\mathbf{U}^{I}\)和\(\mathbf{U}^{H}\)拼接,得到用户的最终兴趣表示。
DSIN通过引入“会话”这一更符合用户实际行为模式的中间单元,将复杂的长序列建模问题分解为“会话内信息聚合”(通过自注意力)和“会话间信息传递”(通过Bi-LSTM)两个更清晰的子问题。这种分层建模思想,使得模型能够对用户兴趣进行更精细的刻画。
本节介绍了序列建模的三个关键模型:DIN通过注意力机制解决用户兴趣多样性问题,DIEN进一步建模兴趣的时序演化过程,DSIN则引入会话概念进行分层建模。这些模型体现了序列建模的核心思想:动态性(根据任务调整兴趣表示)、序列性(利用时间顺序信息)和聚焦性(针对任务筛选相关信息)。随着技术发展,未来的序列建模方法将结合更多先进技术来更好地理解用户动态需求。
3.3.4. 代码实践¶
import sys
import funrec
from funrec.evaluation import compare_models
models = ['din', 'dien', 'dsin']
results, table = compare_models(models)
print(table)
+--------+--------+--------+---------------+
| 模型 | auc | gauc | valid_users |
+========+========+========+===============+
| din | 0.5783 | 0.5557 | 928 |
+--------+--------+--------+---------------+
| dien | 0.5719 | 0.5482 | 928 |
+--------+--------+--------+---------------+
| dsin | 0.4343 | 0.5342 | 99 |
+--------+--------+--------+---------------+