资讯动态

DIN深度兴趣网络:用注意力机制动态建模CTR预估中的用户行为序列

发布时间:2026/10/17 19:08:46 来源:尧图企业网站定制
1. 为什么CTR预估需要DIN行为序列建模的痛点1.1 平均池化的“平滑陷阱”先从一个很常见的场景说起。某商品平台做CTR预估用户历史上点击过手机、键盘、羽毛球拍还买过一袋坚果零食。现在备选池里有一部新手机模型要不要推给他直觉上应该推因为他最近的浏览行为明显偏向数码。但问题来了——传统的行为序列建模方式是把用户过去N个行为对应的embedding取平均或者按行为类型分组后逐位相加再平均然后拼到主特征里。这个操作在今天看来相当朴素甚至会引发一个让人哭笑不得的结果如果用户在一天内既看了手机又看了零食平均池化会把“数码兴趣”和“零食兴趣”叠加成某种不伦不类的中间向量既不像喜欢手机也不像喜欢零食。等模型面对另一部手机时这个模糊的向量很难给出高权重推荐效果自然受影响。我当年第一次看到这个现象时第一反应是“是不是序列长度不够、embedding没学好”。后来做了大量实验才发现问题出在池化策略本身用户兴趣本身是多样且分散的单一固定向量无法表征这种多样性。这就引出了DIN模型的核心出发点——用户的历史行为对当前候选商品的重要性是不一样的模型必须能根据候选商品动态“激活”对应的历史兴趣而不是把所有历史行为一视同仁地压成一个静态向量。1.2 候选商品才是“钥匙”兴趣与候选的关联性DIN模型的英文全称是Deep Interest Network中文一般叫深度兴趣网络。它是针对电商场景CTR预估提出的一种模型结构核心解决的就是上面说的这个“静态池化”问题。它引入了一个类似Attention的机制拿当前的候选商品去和用户历史行为序列中的每一个行为做相关性计算相关度高的历史行为权重放大相关度低的历史行为权重缩小最后做加权求和。这个加权后的兴趣向量才是模型真正用来预估点击率的“用户兴趣表示”。这里的关键点是同一个用户面对不同候选商品兴趣表示是不同的。比如他刚看完一台笔记本电脑系统给他推荐鼠标时历史行为中“办公用品”相关的记录被激活权重高“零食”相关的记录权重低。而当他刷到一包薯片时权重分布又反过来。这种“兴趣动态变化”的能力是DIN区别于传统BaseModel简单池化模型的核心价值。论文的实验里有一个很形象的结论如果用户的兴趣非常集中比如只看某一种品类DIN和BaseModel差距不大但如果用户兴趣分散、行为序列多样性高DIN的AUC提升会非常明显。换句话说DIN特别适合那些“行为历史丰富、兴趣跨度大”的真实用户。这也是我在实际业务中最终决定采用它而不是更复杂的序列模型比如直接上LSTM或GRU的原因——先搞清楚模型要解决的核心矛盾再选结构而不是结构越复杂越好。2. DIN的核心机制局部激活单元的Attention式设计2.1 从直觉到公式兴趣权重是怎么算出来的DIN最核心的结构叫“局部激活单元”Local Activation Unit本质是一个小型神经网络打分器。它的输入有三路历史行为商品embeddingu_i、候选商品embeddingv_a、以及两者的逐元素乘积u_i ⊙ v_a。把这三路向量拼接后送入一个带隐藏层的MLP最后一层输出一个标量权重再经过softmax归一化论文中实际是直接加权不强求softmax但很多复现版本会做softmax。公式可以写成这样a_i h^T ReLU(W [u_i, v_a, u_i ⊙ v_a] b)权重a_i的物理含义就是“用户历史第i个行为和当前候选商品的相关程度”。我们用生活化的方式理解用户在逛超市手里拿着购物清单候选商品他会想起自己以前买过的同类商品忽略那些无关的清单变了想起的东西也就变了。局部激活单元就是模拟这个“想不想得起来”的过程。这个设计有几个非常值得细品的地方。第一为什么不直接用内积算相似度内积只能刻画向量之间的线性相关性而用户历史行为与候选商品之间往往是非线性的关联比如买了A类商品的人可能同时关注B类商品MLP能拟合更复杂的关联模式。第二为什么要引入逐元素乘积这个操作相当于在“历史特征”和“候选特征”之间显式加入了二阶交叉信息能捕捉“候选商品的某个维度是否与历史行为的某个维度强相关”比单纯拼接更有表达力。我做过对比实验去掉u_i ⊙ v_a这一路之后线上点击率大概掉了0.3个百分点看起来不大但在CTR场景里已经是很可观的损失了。这里需要补充一个容易被忽略的细节论文里的局部激活单元其实并没有做严格的softmax归一化而是直接输出加权系数。原因在于用户对某个历史行为的权重应该是绝对的“激活程度”而不是在所有历史行为中做竞争分配。如果做softmax只要序列足够长每条行为的权重都会趋于很小加权后的兴趣向量会丢失幅度信息。我试过在复现时加了softmax效果并不好后来才理解论文为什么这么设计。2.2 数据流拆解一个样本从输入到输出的完整路径下面我用实际的维度设定来拆解数据流方便大家对照代码实现。假设用户历史行为序列长度固定为T50超过截断不足补零每个行为对应一个商品ID商品embedding维度d64。候选商品也是64维embedding。那么对于一条样本把用户历史中50个商品的ID映射成[50, 64]的张量把候选商品ID映射成[64]的向量广播成[50, 64]在局部激活单元里对每个时间步i拼接[u_i, v_a, u_i ⊙ v_a]得到[50, 64*3192]的输入经过隐藏层比如128维和输出层得到50个标量权重对50个历史行为embedding做加权求和得到[64]的兴趣向量把兴趣向量和候选商品embedding、用户侧特征年龄、性别、消费层级等拼接输入一个全连接网络最后经过sigmoid输出点击概率。这一段流程可以用一张简单的伪代码描述def din_interest_net(hist_emb, target_emb): # hist_emb: [batch, T, d] # target_emb: [batch, d] target_emb_tile tf.tile(target_emb[:, None, :], [1, T, 1]) # [batch, T, d] concat_input tf.concat([ hist_emb, target_emb_tile, hist_emb * target_emb_tile ], axis-1) # [batch, T, 3*d] hidden tf.nn.relu(tf.layers.dense(concat_input, units128)) weight tf.squeeze(tf.layers.dense(hidden, units1), axis-1) # [batch, T] weight tf.nn.softmax(weight, axis-1) # 可选取决于实现 return tf.reduce_sum(weight[:, :, None] * hist_emb, axis1) # [batch, d]实际生产环境里embedding维度d通常取32到96之间。维度不是越大越好CTR场景特征非常稀疏维度太大容易过拟合而且会增加激活单元里的参数量。我个人的经验是先取32起步看AUC提升情况再往64、96调。2.3 池化方式的进阶细节不只是加权求和有一个细节很多人第一次看DIN时会忽略——用户的兴趣并不是一维的而是多峰的。加权求和的兴趣向量v_u本质上是把所有历史行为的加权平均即便权重分配合理它也只是一个“主峰”方向上的向量。如果用户既喜欢篮球又喜欢钓鱼兴趣向量会偏向某一侧或者落在两者中间并不能同时表达两个方向。为了解决这个问题论文后续的一些工作引入了多头兴趣、胶囊网络等思路。但在DIN本身的框架里它做了一个很实用的妥协把用户历史行为按时间窗口或按行为类型分成多个子序列对每个子序列分别做自适应加权平均再拼接起来。这样做的好处是简单地保留了“多峰”信息坏处是增加了特征工程的复杂度如果行为类型划分得太细每个子序列里的行为数量太少统计意义就不够。我在实践中的一个折中方案是只按“强意图行为”和“弱意图行为”两组做划分。把点击、停留归为弱意图把加购、收藏、下单归为强意图然后分别过DIN得到两个兴趣向量再与候选商品向量拼接。相比单一的DIN池化这个做法在GAUC上能再提升0.5~1个百分点。为什么因为点击行为噪音大而加购/下单行为信号强强行混淆在一起反而会稀释了强信号。3. 支撑模型落地的工程细节Dice激活与自适应正则3.1 处理输入分布漂移的Dice激活函数DIN原始论文里有两个配套设计目的都很工程化。第一个是Dice激活函数Data-dependent Activation Function它的全称是“数据依赖的激活函数”。这个函数并不是简单把ReLU换成PReLU而是针对CTR场景中输入分布会随着训练动态变化的问题专门设计的。PReLU在负半轴引入了一个可学习的斜率参数但阈值固定为0。Dice的改进点在于负半轴的“转折点”不再固定为0而是由当前输入数据的均值决定并且激活函数会动态判断该维度输入“应该处于激活区还是抑制区”通过一个类似sigmoid的概率门控做平滑切换。公式大致是p(s) sigmoid((s - E[s]) / sqrt(Var[s] eps)) f(s) p(s) * s (1 - p(s)) * alpha * s训练时E[s]和Var[s]用每个mini-batch的统计量计算预测时使用训练阶段累计的全局统计量。这样做的本质是把激活函数和数据分布联动起来当输入分布整体右移时转折点也右移避免神经元大面积“死掉”。我在一个很长周期的训练任务中对比过ReLU和DiceReLU在训练前期表现正常跑了几轮之后部分神经元进入“死区”个人感觉模型整体表达能力有些停滞换Dice之后激活单元的输出分布更稳定收敛速度也快了一些。但Dice不是银弹它需要额外维护一组均值和方差的统计量在小batch比如batch size 256左右场景下统计量波动会比较明显可能影响训练稳定性。我的建议是如果batch size不大可以先用PReLU或者使用Dice的累积统计版本等模型结构调通之后再切换。3.2 大规模稀疏Embedding的Mini-batch Aware正则化第二个配套设计是Mini-batch Aware正则化MBA正则化。很多人会忽略DIN训练中的一个现实问题稀疏特征ID数量极大几千万甚至几亿对应embedding表的行数也极大但每个batch里只有极小一部分ID被激活。如果对整个embedding矩阵做L2正则每次更新都要计算所有ID的梯度范数存储和算力开销都不可接受。MBA正则化的思路非常务实只对当前batch里出现过的特征ID施加L2正则并且根据该ID在batch中出现的频次动态调整正则强度。频次越高的特征暴露机会多正则力度可以稍强一些频次低的特征数据本身就少正则太强会让embedding被压到几乎为零。实现上有一个近似做法假设embedding矩阵为W当前batch里稀疏特征ID集合为S则正则项只累加S中每个ID对应embedding向量的L2范数。为了不打断原有梯度流实际操作时可以先查表得到batch内的embedding计算正则损失后通过类似tf.gather的反向传播把梯度只更新到对应行。伪代码示意ids get_batch_sparse_ids() # [batch * field] unique_ids, idx tf.unique(ids) emb tf.gather(embedding_table, unique_ids) # 只取出出现过的行 reg_loss tf.reduce_sum(tf.norm(emb, axis1)) * l2_lambda # 训练的时候把这个 loss 加入总 loss为什么频次要参与正则强度计算因为CTR模型天然是长尾的头部ID出现几百万次尾部ID只出现几次。如果不区分频次对头部和尾部施加同等正则尾部ID的embedding会被过度惩罚学不到有效表示。我自己踩过的坑就是在一版复现中直接把全局L2套在embedding上线上GAUC掉了将近两个点后来换成MBA正则才恢复正常。如果你用的框架已经内置了类似embedding_regularizer的机制务必看清楚它是不是只在batch内生效不要被“看起来加了正则”这种假安全感骗过了。4. 训练稳定与调参的实践心得从代码到经验4.1 训练过程中的超参数与收敛观察DIN模型的整体结构并不复杂真正花时间的往往是训练过程中的“玄学问题”。我把自己的实践经验整理成一张表做一个参考超参数推荐范围说明Embedding维度32~64视特征数量和数据量而定激活单元隐藏层宽度输入宽度的1~2倍太宽容易过拟合主网络隐藏层200~400一般两到三层足够Dropout0.2~0.5主要加在Concat之后的全连接部分优化器Adam初始学习率1e-3看loss下降情况衰减Batch Size1024~8192大一点统计量更稳序列长度50~200过长收益有限推理耗时会上升关于学习率的设置我有一个具体的心得用Adam训练DIN时初始学习率在1e-3左右往往能较快下降但到了第3~5个epochloss可能会出现一个周期性震荡这时不要急着调学习率先观察一两个epoch很多时候是因为batch内出现了某些极端样本。如果震荡持续就把学习率降到3e-4重新续训通常比从头训练收敛更快。另一个比较隐蔽的问题是embedding的初始化方式。常用做法是正态分布或均匀分布标准差一般取0.01到0.1之间。如果初始化标准差太大前几个batch的激活单元输出权重会非常极端导致兴趣向量某些维度数值很大主网络早期训练不稳定。我在一次实验里把标准差从0.1改成0.01同等条件下GAUC能好千分之二左右虽然不多但胜在纯零成本。4.2 评估为什么用GAUC而不是全局AUC评估DIN模型时最容易犯的一个错误是直接看全局AUC。全局AUC把所有样本混在一起计算用户多、曝光多的样本会主导结果模型哪怕对少数几个重度用户的兴趣捕捉得特别好整体AUC也会虚高。DIN模型关注的是“同一用户面对不同商品时兴趣激活是否有区分度”所以评估指标应该按用户分组先算每个用户自己的AUC再按用户曝光数加权平均这就是GAUC。GAUC的计算公式可以简化为GAUC sum(user_weight_i * AUC_i) / sum(user_weight_i)其中user_weight_i可以是用户曝光数、点击数或自定义权重。如果你的评估样本中大部分用户只有2~3次曝光直接算GAUC会因为样本过少产生很大抖动这种情况下我通常会对曝光次数少于5的用户做降权或过滤保证评估结果稳定。还有一种更上游的评估方式是在线A/B测试但成本高、周期长。我的建议是离线先用GAUC粗筛再用小流量A/B验证。GAUC的提升幅度与线上点击率往往呈正相关但幅度会打折——线下提升1%线上可能只有0.3%到0.5%这是CTR预估的常态不用太惊讶。5. 从DIN到后续演进怎么在业务里用好这个思路5.1 DIN的局限与改进方向DIN虽然解决了“静态兴趣表达”的问题但它在结构上有一个明显的局限性它只对行为本身做加权但没有建模行为之间的时序依赖。比如用户先看了手机又看了手机壳再看了蓝牙耳机这三个行为是有先后因果关系的——他对手机的兴趣可能正在升温。DIN的局部激活单元把这些行为平等地看待只是权重不同并没有显式捕捉“从手机到手机壳”的递进关系。为了解决这个问题后续的DIEN模型在DIN的基础上引入了GRU结构在用户行为序列上建模兴趣的演化过程同时用辅助损失函数刻画像“下一个行为”的监督信号。但在实际应用里DIEN的计算量比DIN大不少训练时间更长调参也更敏感。我的建议是如果用户行为序列长且信号密集DIEN值得试如果序列本身比较稀疏或者业务对推理延迟要求很高DIN仍然是一个非常稳妥的选择。除了DIEN还有一些工程上的低成本改进可以在不换模型的前提下提升效果。比如把候选商品与行为商品的交叉特征显式编码进$u_i \odot v_a$或者引入行为时间间隔衰减权重——用户一个月前的点击和一周前的点击对当前兴趣的贡献显然不同。这类小改动配合DIN在不少场景里能带来额外的AUC提升。5.2 落地建议特征工程与行为序列的组织回到实践层面如果用DIN重构一个CTR预估系统我会建议先从下面几件事入手。第一行为序列的定义要清晰。是点击序列、购买序列还是混合序列混合序列处理方式最复杂不同行为的价值不一样不能直接混在一个序列里做加权。我上文提到按强/弱意图拆分序列是一种可行方案。更精细的做法是把“点击”“收藏”“加购”“下单”分别映射到不同的embedding空间但共享商品ID信息这样模型既能区分行为类型又能共享商品表示。第二控制序列长度。真实用户的行为序列往往极长不可能全部喂给模型。截断策略一般有两种取最近N条或者按时间窗口取“最近30天”。我测试下来的经验是取最近50条比取最近200条的效果几乎不差但训练速度和推理速度提升两倍以上。原因很简单——和当前候选商品相关的行为通常就集中在最近一段时间的浏览里。更早的历史行为既稀疏又与当下弱关联强行保留反而引入噪音。第三候选商品侧不要只用一个ID。在原始论文里候选行为用的是一组特征商品ID、店铺ID、类目ID等拼出来的embedding不是孤零零的单一向量。复现DIN时很多人只用了商品ID效果打折扣就是因为候选侧信息量太少。类目和店铺特征的存在能让权重计算更准确。尤其是类目ID它天然起到了“历史兴趣与候选兴趣是否同域”的粗粒度对齐作用这种对齐对局部激活单元的学习非常有帮助。第四做好冷启动。DIN对行为丰富的用户非常有效但对新用户几乎无能为力——行为序列为空时兴趣向量退化为零向量模型只能依赖用户属性特征。这种情况下我通常会在用户属性侧单独拼一个统计特征比如新用户热门行为的sketch embedding或者用同人群的序列作为冷启动补充。这不是DIN本身的问题而是所有行为序列模型的通病需要有意识地设计兜底策略。回到技术层面再看DIN它的价值并不仅仅在于Attention机制本身而是提出了一个非常重要的建模观——CTR预估里的用户兴趣应当是候选商品相关的函数而不是一个静态变量。这个思路在后来的一大批深度推荐模型里都能看到影子。你在实际项目里应用DIN的时候不必每一步都照搬论文结构先理解它要解决的核心问题再结合手头的数据和算力做取舍别把模型当成攒积木一样拼复杂效果好才是真的好。我个人把DIN复现并上线到某个跨品类推荐场景之后最大的感慨是这类模型真正考验人的地方不在网络结构而在行为序列的组织和特征的对齐。同样的模型结构A同学离线GAUC只能做到0.72B同学做到0.78差别往往就在序列清洗和特征构造上。所以别急着换更花哨的模型先把DIN的基础工作做扎实再考虑往DIEN或更复杂的结构上走。希望这篇拆解能让你少走一些我走过的弯路。

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

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

免费获取报价 →
↑