首页 / 资讯中心 / 文章详情

DIN深度兴趣网络:候选商品引导的注意力机制与推荐系统CTR模型实战解析

DIN深度兴趣网络:候选商品引导的注意力机制与推荐系统CTR模型实战解析 ★ FEATURED ARTICLE
做推荐系统的同行应该都有这种感觉用户的历史行为越长数据越丰富模型反而越难做。明明一个用户点了不少裙子、看了不少口红给他推下一件商品的时候这些历史行为到底哪些有用、哪些是噪声就成了一个很难回答的问题。早年大家习惯把用户的历史行为向量全部池化成一个固定向量然后拼上候选商品特征一起过MLP。这个方法很稳定但它有一个先天缺陷——不管候选商品是什么池化出的用户兴趣向量都是同一份。仔细想想这是不合理的用户在看一个篮球的时候他历史里的口红行为真的应该和看一支口红时贡献相同的权重吗DIN模型全称Deep Interest Network就是冲着这个问题来的。它的核心思路特别直白用户的历史行为不应该被一成不变地压缩成固定向量而应该根据当前候选商品去“唤醒”历史行为中和这个候选商品最相关的部分。这个唤醒过程就是注意力机制只不过它在推荐系统里换了名字叫“局部激活单元”。这篇文章我会把这个模型的动机、网络结构、训练细节、复现要点和工程落地中的坑全部讲透尤其会解释很多论文里一笔带过、但实践中非常关键的地方。1. 为什么需要DIN从固定池化到局部激活1.1 固定向量池化的根本问题在DIN出来之前业内主流的深度学习CTR模型基本长这样把用户的历史点击商品ID序列做Embedding得到一个向量序列然后做sum pooling或者mean pooling把序列压成一个向量再把这个向量和候选商品Embedding、用户画像特征拼在一起送进MLP最后过一个sigmoid输出点击概率。这个结构本身没有问题问题出在pooling这个环节。sum pooling的本质是把所有行为向量的信息全部“揉”进一个等维度的向量里。假设用户的兴趣维度是128他点了100个商品这100个商品的Embedding加起来除以100得到的平均向量已经把每个商品自身的细节抹掉了一大半。更关键的是这个池化后的向量无论如何都不会变——候选商品是篮球还是口红用户历史行为的权重分配都是那同一套。你可以把它理解成一位导购员不管顾客进来是想买运动鞋还是想买护肤品她都用同一套说辞推荐。这套说辞可能在某种场景下有效但面对兴趣跨度极大的真实用户它必然顾此失彼。用户的兴趣天生就是多样的一个年轻用户可能同时喜欢数码产品和潮牌服饰这两个兴趣在池化时会互相干扰。结果就是模型为了平衡学出来的用户表示变得“平平无奇”哪个兴趣方向都表达得不够充分。1.2 为什么要用候选商品来“指挥”注意力DIN的论文给了我一个非常关键的思想转变用户兴趣表示不应该是独立计算出来的它应该和候选商品有关。所谓局部激活就是当候选商品是篮球鞋的时候模型应该去用户的历史行为序列里重点提取和篮球鞋相关的行为当候选商品是美白精华的时候模型应该把注意力放到美妆类的历史行为上。这个“重点提取”的过程就是让每个历史行为Embedding和候选商品Embedding做一个交互计算得出一个权重然后对所有历史行为向量做加权和。这样做的好处立竿见影。第一模型的有效表达能力变强了因为用户兴趣表示不是固定的而是针对每个候选商品“定制”的。第二噪声干扰减少了历史行为中与当前候选商品无关的部分会被自动压权不会拖累预测。第三这个机制是端到端学出来的不需要人工去定义“篮球鞋和哪些历史行为有关”这样的规则完全靠数据自己拟合。我当时读完论文的第一反应是这个思路太自然了为什么之前没人这么做其实是有人这么想的但把注意力机制稳定地用在亿级稀疏特征的CTR场景里并且把训练技巧、激活函数、正则方法都补齐到工程可用DIN是做得最完整的一个。2. DIN模型结构逐层拆解2.1 候选商品、用户行为与上下文特征的输入设计DIN的输入分为三大块。第一块是候选商品相关的特征这一块直接决定预测目标通常包括商品ID、类目ID、店铺ID等全部经过Embedding后拼接成一个候选向量。第二块是用户历史行为序列重点就是用户在过去一段时间内点击过的商品ID序列每个商品ID同样要做Embedding形成一个向量序列。第三块是用户画像特征和上下文特征比如用户的年龄、性别、城市等级、当前时间等。在特征工程层面有一个值得注意的设计候选商品和用户历史行为里的商品在Embedding层是共享参数的。候选商品ID是ID行为序列里的商品ID也是ID如果分开建两套Embedding模型就学不到“候选商品和历史点击商品的关系”注意力权重也就失去了基础。这一点在复现时特别容易漏掉一旦漏掉AUC会明显下降。输入长度方面历史行为序列不能无限长。常见做法是固定截断长度比如取最近点击的50个或者100个商品。如果不足长度就做padding如果超过就按时间倒序截断。实际场景中很多用户的历史行为可能只有十几个固定长度会带来大量padding需要在Embedding层把padding对应的向量置为零防止它参与有效性计算。2.2 局部激活单元的计算逻辑局部激活单元是整个DIN的核心它的输入是历史行为序列里某一个商品对应的Embedding向量以及候选商品对应的Embedding向量。DIN的局部激活单元结构与传统的直接拼接做点乘不同它设计了一个小型的MLP来学习这两个向量之间的交互关系。具体的计算方式是把两个向量本身拼接在一起再让它们做逐元素的外积得到一组交叉项然后把这三部分拼接起来送进一个两层的全连接网络最后输出一个标量权重。这个权重再经过指数映射就是那个历史行为对候选商品的重要程度。很多初学者会问一个问题为什么不直接用softmax归一化DIN有意没有对全部行为序列做softmax因为用户兴趣的绝对强度本身是有意义的。如果一个用户对某个历史行为极其感兴趣他对候选商品的外显权重就应该整体偏高如果做全序列的softmax就相当于把所有行为权重强制拉成相对比例这会丢失兴趣强度的绝对值信息反而影响预测精度。计算出每个行为向量的权重之后对所有行为向量做加权求和得到最终的向量再与候选商品向量、用户画像向量拼接送入MLP。MLP部分就是标准的全连接网络最后接sigmoid输出点击率。2.3 为什么不是先建模行为顺序很多人会觉得历史行为是序列数据应该先用GRU、LSTM这些建模时序然后再接注意力。这个想法在逻辑上没错但在DIN提出的那个阶段用序列模型把所有用户历史行为建模完整计算量非常大效果增益却未必匹配。DIN的定位是尽量简洁地解决“兴趣表达与候选商品无关”的问题它没有刻意去建模行为之间的先后依赖也没有做长短期兴趣的拆分就是一个注意力加权求和。论文后续确实有人在这条路上继续延伸比如把行为序列换成Transformer结构做建模但作为基石DIN把最有价值的一步先走通了——让每个候选商品都能看到用户历史行为的“不同切面”。这一步走通之后后面的一切优化都有了更牢固的地基。3. 训练中的两个关键细节Dice激活与Mini-batch Aware正则3.1 Dice激活函数做了什么DIN论文里第一次看的时候大部分人都会被Dice激活函数这个小节卡住。它看起来只比PReLU多了一个自适应开关但理解它的动机很重要。PReLU是带可学习参数的ReLU变体公式很好理解当输入大于零时保持原值当输入小于等于零时乘以一个可学习的系数。这个系数是全局固定的一组标量在所有输入样本上共享。问题来了推荐系统里的特征输入分布极其不稳定有的特征均值可能在零附近有的特征整体是负数有的特征方差爆炸。用一个固定负斜率去处理所有分布特征很容易压不住数据分布的变化。Dice的做法是对输入做标准化用当前batch数据的均值和方差把输入调整到零均值单位方差附近再用sigmoid函数动态控制这个激活门。当输入分布不稳定时这个sigmoid门会自适应地调节激活点的位置相当于一个可以随着数据分布流动的“软开关”。复现的时候可以直接用PyTorch的batch normalization思路类比但要清楚Dice的标准化是不做仿射变换的重建的它只做标准化和门控。3.2 Mini-batch Aware正则的动机与简化这个部分论文里给了比较复杂的形式很多复现教程会直接略过但工程上它非常重要。推荐系统的Embedding层参数动辄上亿模型的权重数量远超样本数过拟合风险极大。常用的L2正则理论上能压制复杂度但它要求对全量参数更新时计算梯度这在工业级的Embedding规模下是不可行的——每个batch只碰到了极少一部分稀疏ID对全部Embedding做惩罚是巨大的浪费。Mini-batch Aware正则的思路是在计算L2正则时只对当前batch中出现过的稀疏特征对应的Embedding做惩罚并且惩罚力度要依据该特征在当前batch中的出现频率进行调整。更直白的解释是如果一个参数在当前batch中出现了两次它获得的惩罚是出现一次样本的两倍如果一个参数在当前batch中一次都没出现那就不惩罚它。论文里还做了一个频次的p次方近似处理避免高频特征被惩罚过重在实际复现时如果数据量不大不做p次方近似也不会差太多。我自己的复现经验是Mini-batch Aware正则这个细节最容易被省略如果不加结果往往是离线AUC有小幅上升但波动大线上反馈差距更明显。做工程落地时这个正则一定要保留。4. 动手复现DINPyTorch代码核心要点4.1 稀疏特征与行为序列的预处理在开始写模型之前先把数据流想清楚。输入样本通常长这样user_id、一个候选商品id、一个候选商品的类目id、一个历史点击商品id序列、一个历史点击商品类目序列、还有若干上下文特征。这里我建议把用户历史行为里的商品id和候选商品id放进同一个词表序列长度固定为N。预处理阶段把历史行为序列倒序排列按时间最近优先取前N个不足补零。class DINBase(nn.Module): def __init__(self, item_count, embedding_dim, sequence_length, hidden_units): super().__init__() self.item_embedding nn.Embedding(item_count, embedding_dim, padding_idx0) self.sequence_length sequence_length self.activation_unit ActivationUnit(embedding_dim, hidden_units) self.mlp nn.Sequential( nn.Linear(embedding_dim * 3 48, 200), Dice(200), nn.Linear(200, 80), Dice(80), nn.Linear(80, 1) )注意这里我拼接了用户向量、候选商品向量、用户画像特征总计维度要算清楚。用户向量就是注意力加权求和后的维度候选商品向量就是单个Embedding的维度。画像特征通常是稠密数值特征加离散特征的Embedding拼接。4.2 局部激活单元的代码实现局域激活单元的重点是计算交互特征。我给的实现是把候选商品向量复制成多个副本与每个历史行为向量做外积再把三部分拼接送进MLP。class ActivationUnit(nn.Module): def __init__(self, embedding_dim, hidden_units): super().__init__() self.linear nn.Linear(embedding_dim * 3, hidden_units) self.dice Dice(hidden_units) self.linear_out nn.Linear(hidden_units, 1) def forward(self, candidate_item, behavior_sequence): # candidate_item: [batch, dim] # behavior_sequence: [batch, seq_len, dim] batch_size, seq_len, dim behavior_sequence.shape c candidate_item.unsqueeze(1).expand(batch_size, seq_len, dim) interaction c * behavior_sequence concat torch.cat([behavior_sequence, c, interaction], dim-1) hidden self.dice(self.linear(concat)) weight self.linear_out(hidden).squeeze(-1) weight torch.exp(weight) mask (behavior_sequence.sum(dim-1) ! 0).float() weight weight * mask weight weight / (weight.sum(dim-1, keepdimTrue) 1e-8) return weight大多数人第一次实现都会纠结要不要对权重做归一化。刚才分析过论文思想是不强制做softmax但实际工程里如果序列长度差异很大不做归一化长序列用户注意力分数天然会大跨样本比较就混乱了。我习惯在加权求和前对有效权重做一次求和归一化保留兴趣强度的相对差异但避免样本间的序列长度偏差问题。当然如果有更贴近论文原始含义的复现需求也可以不做这里的归一化而是直接加权求和。4.3 训练主流程与超参数配置DIN在训练时的超参数选择直接影响最终效果。论文给我的经验是使用Adam优化器初始学习率从0.001开始配合指数衰减大概每两轮迭代衰减10%。Batch size在稠密ID规模不夸张时设为256到512比较合适。负样本采样也是一个重要环节。曝光未点击样本与点击样本的比例通常在5:1到20:1之间调整。比例高了模型会倾向预测低点击率需要配合阈值校准比例低了正样本噪声会污染注意力权重的学习。训练过程中的Loss就是标准的二值交叉熵。我建议在训练日志里同时记录训练集和验证集AUC并且按用户分组计算GAUC这一点和DIN论文的实验设置保持一致因为全局AUC会被少数高频用户主导掩盖模型在长尾用户上的真实表现。5. 实验评估、工程落地与常见问题排查5.1 评估指标为什么全局AUC不够用DIN论文在实验部分强调了评估指标的选择问题。直接算全量样本的AUC结果会被行为数量极大的头部用户“带偏”这些用户的兴趣通常比较集中历史行为很长模型对它们的预测本来就比较容易真正难预测的是那些历史行为稀疏的长尾用户他们的点击行为跨领域跨度大候选商品的注意力计算会遇到更多挑战。所以推荐的做法是先把测试样本按用户分组在每个用户组内计算AUC再按曝光数加权平均得到GAUC。我自己的复现对比中DIN的相对AUC提升大约在0.5到1个百分点但GAUC提升会更明显说明模型的增益确实集中在了原有模型搞不定的样本上。评估时千万要两个指标一起看。5.2 线上推理时的性能瓶颈与优化工业落地中局部激活单元带来的计算开销不容忽视。在线推理时候选商品数量可能是几千上万个用户历史行为序列长度在50到100如果每个候选都和全部历史行为做一次外积和MLP计算压力不小。一种有效优化是预先缓存用户行为序列的Embedding结果因为用户的动作频率远低于候选商品的变化频率。行为向量的个数不变只是权重计算时需要和每个候选商品重新交互这部分无法完全预计算但可以把行为Embedding向量的查询开销省掉。如果延迟敏感还可以对行为序列做截断或者只保留最高注意力权重的前K个行为实测下来精度损失很小。模型参数存储方面Embedding矩阵是最大的内存消耗点。如果商品量级是千万级每个商品32维Embedding那么单是商品Embedding就有数亿个参数必须利用参数服务器或者按分片方式存储不要让单机内存成为瓶颈。5.3 复现与落地常见问题速查问题现象可能原因解决思路局部激活单元训练不收敛学习率过大、行为序列padding没有遮罩调小初始学习率对padding位置强制置零权重AUC提升不明显候选商品与历史行为商品用了两套Embedding检查是否共享词表与Embedding矩阵训练过程loss震荡正负样本比例失衡或batch size过小提高负样本比例或增大batch size线上延迟超标注意力计算量过大截断行为序列、只保留TopK高权重行为长尾用户预测偏平行为序列太短、候选信息不足引入跨会话行为或补充用户长期画像特征还有一个隐蔽的坑值得单独提注意力权重的饱和现象。当候选商品非常好猜时比如用户历史行为里有大量同款商品注意力网络可能会把几乎全部权重集中到一个行为上导致加权向量退化成单纯的某个行为Embedding。这种情况虽然不报错但会让模型丧失泛化能力。我的经验是可以在激活单元输出层加少量dropout或者对注意力权重logits做轻微的正则防止过度集中。迭代到后面你会发现DIN的真正价值不在模型结构有多复杂而在于它把“候选商品引导历史行为表示”这个思路做成了推荐系统深度模型的标配。后续很多模型比如用多头注意力做多兴趣抽取的MIND用胶囊网络区分兴趣的ComiRec本质上都能看到DIN的影子。如果你想在推荐领域深入吃透DIN的每一处细节是最值得投入的一件事。最后分享一个我在实际复现中收获最大的习惯不要只对着论文公式写代码把注意力网络输出的权重可视化出来。随机挑几个样本看模型到底把注意力给了哪些历史行为你会发现一个训练充分的DIN它的注意力分布和人的直觉高度吻合——这个观察过程能帮你快速定位特征问题和模型问题比单纯盯AUC数字有效得多。
阅读完成 · 觉得有帮助?
咨询建站