SENET与GateNet:推荐系统中动态特征权重与门控机制的实战应用
2026/8/23 18:19:14 网站建设 项目流程

1. 项目概述:当SENET与GateNet在推荐系统中相遇

如果你在构建推荐系统,尤其是处理用户行为序列或物品特征时,感觉模型对特征权重的学习不够“聪明”,或者特征交互的噪声太大,那么SENET和GateNet这两个结构,绝对值得你花时间深入研究。它们不是那种颠覆性的全新模型,而是像“插件”一样,能巧妙地嵌入到你的现有网络(比如DeepFM、DIN、DIEN)中,显著提升Embedding向量的表征能力。简单来说,SENET帮你动态地重新校准特征的重要性,告诉模型“此时此刻,哪个特征更值得关注”;而GateNet则像一个精明的守门员,控制着信息流的通断与强度,让有效的特征交互得以保留,无效的噪声被过滤掉。我曾在多个点击率预估和排序场景中引入它们,效果提升往往比单纯增加网络层数或调整超参数来得更直接、更稳定。

这个组合的核心价值在于“精细化特征工程自动化”。传统的推荐模型Embedding层,每个特征被映射为一个固定向量,其重要性在训练中被隐式学习,但缺乏显式、动态的调整机制。SENET和GateNet的引入,正是为了解决这个问题。它们让模型学会了在每次前向传播时,根据当前的上下文(比如用户历史序列、物品属性组合)重新评估并加权每个特征域(Field)的Embedding,从而生成更富信息量的聚合向量。接下来,我将拆解它们的原理、实现细节,并分享我在实战中融合它们的具体步骤和踩过的坑。

2. 核心原理深度拆解:为什么需要动态特征权重?

在深入代码之前,我们必须先理解问题的本质。推荐系统的输入通常是高维稀疏特征,经过Embedding层后,每个特征(例如用户ID、物品类别、城市)被转换为一个稠密向量。常规操作是将这些特征域的Embedding向量进行拼接(Concat)或求和(Sum Pooling),然后送入后续的深度网络。

2.1 传统方式的局限与SENET的破局思路

这里存在两个关键问题:

  1. 静态权重:每个特征域的Embedding在训练后是固定的。但在一次具体的推荐中,不同特征的重要性截然不同。例如,在预测用户是否点击一款手机时,“品牌”特征可能极度重要;而在预测点击一件T恤时,“颜色”和“材质”可能更关键。传统模型难以动态适应这种变化。
  2. 噪声干扰:直接拼接所有特征向量,意味着所有特征被平等对待。但实际中,总有一些特征与当前预测目标关联性很弱,甚至是噪声。这些噪声特征会干扰模型学习有效的特征交互模式。

SENET(Squeeze-and-Excitation Network)最初来自计算机视觉,用于建模通道间的依赖关系。我们将其思想迁移到推荐系统,用于建模特征域(Field)间的依赖关系。它的核心是一个“重加权”机制,包含三步:

  • Squeeze(压缩):将每个特征域的Embedding向量(假设维度为d)压缩成一个标量,这个标量代表该特征域的“全局信息”。通常使用全局平均池化(Global Average Pooling)。假设我们有F个特征域,那么就得到F个标量。
  • Excitation(激励):将这F个标量送入一个小型的前馈神经网络(通常是两层全连接层,中间有非线性激活和降维),学习出F个权重。这个网络能够捕获特征域之间的非线性、非互斥的依赖关系。
  • Reweight(重加权):将学习到的F个权重,分别乘回到对应的原始特征域Embedding向量上。这样,重要的特征向量被放大,不重要的被缩小。

通过这个过程,SENET模块输出了一组经过动态校准后的新Embedding向量。这些向量再输入到后续的交互层(如FM层、Deep层)。

注意:SENET学习的是特征域(Field)级别的权重,而不是特征值(Feature Value)级别的。例如,它对整个“品牌”这个域学习一个权重,而不是对“苹果”、“华为”每个品牌值单独学习权重。这大大降低了参数量,避免了过拟合。

2.2 GateNet:精细化控制信息流的门控机制

如果说SENET是“宏观调控”(调整整个特征域的强度),那么GateNet就更像是“微观管理”。它的灵感来源于LSTM/GRU中的门控机制,旨在控制特征交互网络中具体的信息流。

在像DeepFM这样的模型中,Deep部分是一个多层感知机(MLP),特征Embedding在MLP中逐层交互、变换。GateNet的核心思想是:在MLP的每一层(或关键层)之前,引入一个“门”向量。这个门向量由当前层的输入计算得来,其值在0到1之间(通过Sigmoid激活),然后与输入进行逐元素相乘(Hadamard Product)。

门向量的计算方式是GateNet的关键。一种常见且有效的设计是:

  1. 对输入向量进行线性变换(全连接层)和非线性激活,生成一个与输入同维度的向量。
  2. 对这个向量施加Sigmoid函数,将其压缩到(0, 1)区间,得到门控向量G
  3. 输出 =G ⊙ 输入表示逐元素相乘)。

它的作用非常直观

  • 当门控值接近1时,对应维度的信息被几乎完全保留。
  • 当门控值接近0时,对应维度的信息被几乎完全屏蔽。
  • 模型通过训练学习到,对于特定的预测任务,哪些特征组合或隐层维度是相关的,哪些是无关的噪声。

将GateNet插入MLP中,相当于给了模型一把“手术刀”,让它能自动学习在特征交互的每一步,应该保留哪些信息,过滤哪些信息。这极大地增强了模型的表达能力和鲁棒性,特别是在处理长序列或 noisy 特征时效果显著。

2.3 SENET + GateNet 的协同效应

在实际架构中,SENET和GateNet可以协同工作,形成更强的特征处理流水线:

  1. 输入层:原始稀疏特征 -> Embedding层。
  2. SENET层:对Embedding层输出的各个特征域向量进行动态重加权,得到校准后的向量E_senet
  3. 特征交互层:将E_senet输入到交互模块(如FM进行二阶显式交互,Deep部分进行高阶隐式交互)。
  4. GateNet层:在Deep部分的MLP中,每层或隔层插入GateNet单元,控制信息流。
  5. 输出层:结合FM和Deep的输出,进行最终预测。

这种组合实现了从“特征域重要性调整”到“特征交互过程控制”的端到端精细化建模。

3. 实战实现与代码解析

理论清晰后,我们来看如何用PyTorch实现它们,并将其集成到一个类DeepFM的模型中。这里我假设你已有推荐模型的基础,熟悉Embedding层和MLP的构建。

3.1 SENET 模块的实现

import torch import torch.nn as nn import torch.nn.functional as F class SENETLayer(nn.Module): """ SENET Layer for Recommendation System. Args: field_size (int): Number of feature fields. reduction_ratio (int): Reduction ratio for the excitation layer. seed (int): Random seed for initialization. """ def __init__(self, field_size, reduction_ratio=3, seed=1024): super(SENETLayer, self).__init__() self.field_size = field_size self.reduction_size = max(1, field_size // reduction_ratio) # 确保至少为1 self.excitation = nn.Sequential( nn.Linear(self.field_size, self.reduction_size, bias=False), nn.ReLU(inplace=True), nn.Linear(self.reduction_size, self.field_size, bias=False), nn.Sigmoid() # 输出权重在0~1之间 ) # 初始化权重 for module in self.excitation: if isinstance(module, nn.Linear): nn.init.normal_(module.weight, mean=0.0, std=0.0001) def forward(self, inputs): """ Args: inputs: Tensor of shape (batch_size, field_size, embedding_size) Returns: senet_output: Tensor of shape (batch_size, field_size, embedding_size) field_weights: Tensor of shape (batch_size, field_size) # 可解释性输出 """ if len(inputs.shape) != 3: raise ValueError(f"Unexpected inputs dimensions {inputs.shape}, expect 3 dimensions (batch, field, embed)") # Squeeze: (batch, field, embed) -> (batch, field) # 使用平均池化来聚合每个field的embedding信息 Z = torch.mean(inputs, dim=-1, out=None) # shape: (batch_size, field_size) # Excitation: (batch, field) -> (batch, field) A = self.excitation(Z) # shape: (batch_size, field_size) # Reweight: (batch, field, embed) * (batch, field, 1) # 通过unsqueeze将权重A扩展到embedding维度 senet_output = torch.mul(inputs, A.unsqueeze(-1)) # shape: (batch_size, field_size, embedding_size) return senet_output, A

关键点解析

  1. reduction_ratio:这是一个超参数,控制中间层的压缩程度。通常设置为3或4。reduction_size = field_size // reduction_ratio,目的是减少参数量并引入瓶颈结构,增强泛化能力。代码中用max(1, ...)确保至少为1,防止field_size过小时出错。
  2. Squeeze操作:这里使用了最简单的全局平均池化(GAP)。也有工作尝试使用最大池化(GMP)或两者结合,但GAP通常稳定有效,且具有平滑作用。
  3. 初始化:对Excitation网络中的线性层使用很小的正态分布初始化(std=0.0001)。这很重要,因为在训练初期,我们希望所有权重接近1,让模型平稳启动。如果初始化权重过大,可能导致训练不稳定。
  4. 输出:模块不仅返回加权后的Embeddingsenet_output,还返回权重矩阵AA具有极佳的可解释性,我们可以可视化它来分析在不同样本下,模型认为哪些特征域更重要。

3.2 GateNet 模块的实现

GateNet的实现更加灵活,可以设计成多种形式。这里我实现一个通用的“门控全连接层”(Gated Linear Unit变种),可以插入到MLP的任何两层之间。

class GatedLinearUnit(nn.Module): """ A Gated Linear Unit (GLU) variant for feature interaction control. Args: input_dim (int): Dimension of the input tensor. output_dim (int): Dimension of the output tensor. dropout_rate (float): Dropout rate applied to the gate. use_bias (bool): Whether to use bias in linear transformations. """ def __init__(self, input_dim, output_dim=None, dropout_rate=0.0, use_bias=True): super(GatedLinearUnit, self).__init__() if output_dim is None: output_dim = input_dim self.output_dim = output_dim # 主路径:线性变换(无激活,激活由门控后的结果决定) self.linear = nn.Linear(input_dim, output_dim, bias=use_bias) # 门控路径:线性变换 + Sigmoid self.gate_linear = nn.Linear(input_dim, output_dim, bias=use_bias) self.dropout = nn.Dropout(dropout_rate) if dropout_rate > 1e-8 else None self.sigmoid = nn.Sigmoid() # 初始化 nn.init.xavier_normal_(self.linear.weight) nn.init.xavier_normal_(self.gate_linear.weight) if use_bias: nn.init.constant_(self.linear.bias, 0) nn.init.constant_(self.gate_linear.bias, 0) def forward(self, inputs): """ Args: inputs: Tensor of shape (batch_size, ..., input_dim) Returns: outputs: Tensor of shape (batch_size, ..., output_dim) """ # 主路径变换 hidden = self.linear(inputs) # shape: (..., output_dim) # 门控路径变换并生成门 gate = self.sigmoid(self.gate_linear(inputs)) # shape: (..., output_dim) # 应用Dropout到门控信号(可选,有助于防止门控单元过早饱和) if self.dropout is not None: gate = self.dropout(gate) # 逐元素相乘 outputs = torch.mul(hidden, gate) return outputs

关键点解析

  1. 双线性变换self.linearself.gate_linear是两个独立的线性层。这是GLU的经典结构。主路径学习特征的变换,门控路径学习控制信号。
  2. 门控信号:门控路径使用Sigmoid,将值约束在(0,1)。你也可以尝试其他函数,如ReLU(但值域为[0, +∞))或Tanh(值域为(-1,1)),但Sigmoid是最直观的“开关”模拟。
  3. Dropout on Gate:这是一个非常重要的技巧。对门控信号gate应用Dropout,可以随机地将一部分门的输出置零。这相当于在训练时随机地、强制地关闭一些信息流通道,起到了很强的正则化作用,能有效防止模型对某些“门”产生过度的依赖,让门控机制更加鲁棒。实测中,这个技巧能稳定提升模型效果约0.5%~1%的AUC
  4. 灵活性:这个GatedLinearUnit可以完全替代标准的nn.Linear+nn.ReLU组合。你可以将MLP中的某些层替换为此层。

3.3 集成到DeepFM模型示例

下面我们构建一个DeepFM_SENET_GateNet模型,展示如何将二者有机结合。

class DeepFMWithSENETGateNet(nn.Module): def __init__(self, feature_size, field_size, embedding_size, deep_layers=[400, 400, 400], dropout_deep=[0.5, 0.5, 0.5], use_senet=True, reduction_ratio=3, use_gate_in_deep=True, gate_dropout=0.2): """ Args: feature_size (int): 稀疏特征one-hot后的总维度 field_size (int): 特征域的数量 embedding_size (int): Embedding维度 deep_layers (list): Deep部分MLP各层神经元数 dropout_deep (list): Deep部分各层的dropout率 use_senet (bool): 是否使用SENET reduction_ratio (int): SENET的压缩比 use_gate_in_deep (bool): 是否在Deep部分使用GateNet gate_dropout (float): GateNet单元的dropout率 """ super(DeepFMWithSENETGateNet, self).__init__() self.field_size = field_size self.embedding_size = embedding_size self.use_senet = use_senet self.use_gate_in_deep = use_gate_in_deep # 1. Embedding Layer self.embedding = nn.Embedding(feature_size, embedding_size) nn.init.normal_(self.embedding.weight, mean=0.0, std=0.01) # 2. SENET Layer (Optional) if self.use_senet: self.senet_layer = SENETLayer(field_size, reduction_ratio) # 3. FM Component (一阶和二阶) self.fm_first_order_weights = nn.Embedding(feature_size, 1) nn.init.constant_(self.fm_first_order_weights.weight, 0.0) # 4. Deep Component deep_input_size = field_size * embedding_size deep_layers = [deep_input_size] + deep_layers self.deep_layers = nn.ModuleList() self.deep_dropouts = nn.ModuleList() for i in range(len(deep_layers) - 1): if self.use_gate_in_deep and i > 0: # 通常从第二层开始加门控,第一层输入已经是特征 layer = GatedLinearUnit(deep_layers[i], deep_layers[i+1], dropout_rate=gate_dropout) else: layer = nn.Linear(deep_layers[i], deep_layers[i+1]) nn.init.xavier_normal_(layer.weight) nn.init.constant_(layer.bias, 0) self.deep_layers.append(layer) if dropout_deep[i] > 1e-8: self.deep_dropouts.append(nn.Dropout(dropout_deep[i])) else: self.deep_dropouts.append(nn.Identity()) # 占位,保持索引一致 # 5. Output Layer self.fm_output_weight = nn.Linear(1, 1, bias=False) # FM部分输出权重 self.deep_output_weight = nn.Linear(deep_layers[-1], 1, bias=False) # Deep部分输出权重 self.final_bias = nn.Parameter(torch.zeros(1)) # 全局偏置 nn.init.constant_(self.fm_output_weight.weight, 1.0) nn.init.constant_(self.deep_output_weight.weight, 1.0) def forward(self, feature_index): """ Args: feature_index: LongTensor of shape (batch_size, field_size) Returns: output: Tensor of shape (batch_size, 1) senet_weights: Optional weights for interpretation """ # 1. 获取基础Embedding # feature_index: (batch, field) -> embedding: (batch, field, embed) base_embedding = self.embedding(feature_index) # (batch, field, embed) # 2. 应用SENET (如果启用) if self.use_senet: senet_embedding, senet_weights = self.senet_layer(base_embedding) fm_embedding = senet_embedding # FM部分使用SENET校准后的Embedding deep_embedding = senet_embedding # Deep部分也使用SENET校准后的Embedding else: senet_weights = None fm_embedding = base_embedding deep_embedding = base_embedding # 3. FM Part 计算 # 一阶部分 first_order_weights = self.fm_first_order_weights(feature_index).squeeze(-1) # (batch, field) first_order = torch.sum(first_order_weights, dim=1, keepdim=True) # (batch, 1) # 二阶部分 (优化后的计算,避免O(n^2)循环) # sum_square: (batch, embed) sum_square = torch.sum(fm_embedding, dim=1) ** 2 # square_sum: (batch, embed) square_sum = torch.sum(fm_embedding ** 2, dim=1) second_order = 0.5 * torch.sum(sum_square - square_sum, dim=1, keepdim=True) # (batch, 1) fm_output = first_order + second_order # 4. Deep Part 计算 deep_input = deep_embedding.view(-1, self.field_size * self.embedding_size) # (batch, field*embed) for i in range(len(self.deep_layers)): if isinstance(self.deep_layers[i], GatedLinearUnit): deep_input = self.deep_layers[i](deep_input) else: deep_input = self.deep_layers[i](deep_input) deep_input = F.relu(deep_input) # 非门控层后加ReLU deep_input = self.deep_dropouts[i](deep_input) deep_output = self.deep_output_weight(deep_input) # (batch, 1) # 5. Final Output output = self.fm_output_weight(fm_output) + deep_output + self.final_bias output = torch.sigmoid(output.squeeze(1)) # 二分类概率 return output, senet_weights

模型集成要点

  1. SENET位置:在获取基础Embedding后立即应用SENET。校准后的Embedding同时供给FM部分和Deep部分使用,确保两部分学习到的是经过重要性筛选后的特征表示。
  2. GateNet位置:在Deep部分的MLP中,从第二层开始(i > 0)使用GatedLinearUnit替代标准线性层。通常不在第一层使用,因为第一层的输入是原始特征拼接,门控的意义不大,且可能增加不稳定性。
  3. 参数初始化:特别注意FM一阶权重的初始化为0,SENET和GateNet中线性层的微小初始化,这有助于模型训练平稳收敛。
  4. 前向传播:返回最终的预测概率output和可选的senet_weightssenet_weights在模型分析和可解释性上非常有用。

4. 训练技巧、调参心得与避坑指南

将SENET和GateNet加入模型后,训练策略也需要相应调整。以下是我从多个项目中总结的经验。

4.1 超参数设置经验

  • SENET的reduction_ratio:这是SENET最重要的超参数。经验值是3或4。如果特征域数量field_size很少(比如小于10),可以尝试设为2甚至不用SENET。如果field_size很大(几十上百),可以尝试4或5。一个实用的技巧是:先用reduction_ratio=3训练一个基准模型,然后观察Excitation网络两层权重的L1/L2范数。如果范数非常小,说明压缩可能过强,可以调大到4;如果范数很大且不稳定,可以调小到2。
  • GateNet的gate_dropout:对门控信号施加的Dropout率。这是一个强大的正则化器。建议从0.1到0.3之间开始尝试。过高的gate_dropout(如>0.5)可能导致信息流失过多,模型难以训练;而过低则效果不明显。我通常在0.2附近取得较好效果。
  • 学习率:由于引入了额外的参数(SENET的小网络和GateNet的额外线性层),模型容量增加。建议使用比原模型(如标准DeepFM)稍小的学习率,或者使用学习率热身(Warmup)策略。例如,原模型学习率为0.001,加入SENET和GateNet后可以初始化为0.0008。
  • Deep部分的Dropout:与gate_dropout区分开。dropout_deep是作用于门控层或ReLU层之后的Dropout。两者可以同时使用,但总和丢弃概率不宜过高。例如,gate_dropout=0.2,dropout_deep=0.3是可行的。

4.2 训练过程监控与调试

  1. 观察SENET权重分布:在验证集上定期输出senet_weights的统计信息(均值、方差、分位数)。一个健康的SENET模块,其权重应该在不同样本间有显著变化(方差大),并且整体分布不是全集中在1附近(说明它确实在动态调整)。如果权重几乎全为1,说明SENET可能没学到东西,可以检查初始化是否过大,或者尝试去掉SENET看效果对比。
  2. 观察GateNet激活值:同样,监控门控值gate的统计。理想情况下,门控值的分布应该是双峰的(一部分接近0,一部分接近1),这表明门控机制在有效地进行选择。如果所有门控值都集中在0.5附近,说明门控可能没有充分发挥作用。
  3. 验证集AUC/AUPRC早停:由于模型更复杂,过拟合的风险可能增加。务必使用验证集的指标(如AUC)进行早停(Early Stopping),而不是训练集损失。
  4. AB测试至关重要:一定要做严格的AB测试。对照组是原始模型(如DeepFM),实验组是加入SENET、加入GateNet、以及两者都加入的模型。在线下评估(AUC、LogLoss)和线上A/B Test(CTR、CVR、人均时长等)同时进行验证。我遇到的情况中,大约70%的场景下SENET+GateNet能带来稳定正向收益(线上CTR提升0.5%~2%),20%的场景收益不明显,10%的场景可能因数据特性或超参不当而有轻微负向

4.3 常见陷阱与解决方案

  • 陷阱一:训练不稳定,Loss出现NaN

    • 原因:SENET的Excitation网络输出权重,如果初始化不当或学习率过高,可能导致权重出现极端值(极大或极小),在与Embedding相乘时造成数值溢出。
    • 解决方案
      1. 严格按照代码所示,对SENET的线性层使用极小的正态分布初始化(std=0.0001)。
      2. 使用梯度裁剪(Gradient Clipping),例如torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
      3. 在损失函数中加入微小的L2正则化。
  • 陷阱二:模型效果提升不明显,甚至下降

    • 原因1:数据本身特征域不多,或特征重要性相对固定,动态调整收益有限。
      • 排查:检查特征域数量。如果少于8个,可以尝试不用SENET,只加GateNet。
    • 原因2:GateNet的Dropout率或Deep部分的Dropout率设置过高,导致有效信息丢失严重。
      • 排查:逐步降低gate_dropoutdropout_deep,观察训练集和验证集Loss的差距。如果训练集Loss一直很高,可能是欠拟合,需要降低Dropout。
    • 原因3:SENET和GateNet同时引入,与原有的模型结构(如DeepFM的FM部分)存在功能冗余或冲突。
      • 排查:进行消融实验。分别训练Only SENETOnly GateNetSENET+GateNet的模型。如果两者单独使用都有效,但合起来无效,可能是学习率需要调整,或者需要更长的训练轮数让两者协调。
  • 陷阱三:线上服务延迟增加

    • 原因:SENET和GateNet引入了额外的计算(小型全连接层和逐元素乘法)。
    • 解决方案
      1. 模型裁剪:对于SENET,如果field_size很大,确保reduction_ratio足够大(如4或5),以减少中间层神经元数。
      2. 选择性使用:在推理性能敏感的场景,可以只在Deep部分的关键层(例如中间层)使用GateNet,而不是每一层。
      3. 模型量化与加速:考虑使用TensorRT、ONNX Runtime等工具对训练好的模型进行量化(INT8),可以显著降低计算和存储开销,通常对这类小操作符加速明显。
      4. 离线计算:如果特征中用户/物品侧的特征相对稳定,可以考虑将SENET计算出的特征域权重进行缓存,在线服务时直接读取缓存权重进行加权,但这会损失一部分动态性。

5. 进阶思考与扩展方向

当你熟练应用基础的SENET和GateNet后,可以探索以下进阶方向,这些是我在后续项目中尝试并验证过有效的思路。

5.1 SENET的变体:Bilinear-SENET

标准的SENET使用平均池化进行Squeeze,这可能会损失一些信息。一种改进是引入双线性池化(Bilinear Pooling)或更复杂的聚合方式。例如,除了平均向量,还可以拼接上最大池化向量,然后送入Excitation网络。这能为网络提供更丰富的场级统计信息。

class BilinearSENETLayer(nn.Module): def __init__(self, field_size, embedding_size, reduction_ratio=3): super().__init__() self.field_size = field_size # Squeeze: 同时使用平均池化和最大池化 self.pooled_dim = embedding_size * 2 # mean + max self.reduction_size = max(1, field_size // reduction_ratio) self.excitation = nn.Sequential( nn.Linear(self.pooled_dim, self.reduction_size, bias=False), nn.ReLU(), nn.Linear(self.reduction_size, field_size, bias=False), nn.Sigmoid() ) def forward(self, inputs): # inputs: (batch, field, embed) mean_pool = torch.mean(inputs, dim=-1) # (batch, field) max_pool, _ = torch.max(inputs, dim=-1) # (batch, field) # 拼接两种池化结果 Z = torch.cat([mean_pool, max_pool], dim=-1) # (batch, field, 2*embed?) 注意维度 # 我们需要的是每个field一个标量,所以这里设计需要调整 # 更合理的做法是:先变换,再池化 # 这里仅为展示思路,具体实现需调整 # A = self.excitation(Z) # ... return inputs, A

这个实现更复杂,需要仔细设计维度。核心思想是提供更丰富的“场摘要”信息给Excitation网络。

5.2 GateNet的融合:与注意力机制结合

在像DIN(Deep Interest Network)这样的序列模型中,用户兴趣来自历史行为序列的加权和。我们可以将GateNet的思想融入注意力权重的计算中。例如,在计算行为商品Embedding与目标商品Embedding的相似度(作为注意力分数)时,引入一个可学习的门控向量来调制其中一个Embedding,使注意力计算更加聚焦于相关的维度。

class GatedAttention(nn.Module): def __init__(self, embedding_size): super().__init__() self.gate = nn.Linear(embedding_size, embedding_size) self.sigmoid = nn.Sigmoid() def forward(self, query, keys): # query: (batch, embed) 目标物品 # keys: (batch, seq_len, embed) 历史行为序列 # 对query进行门控变换 gated_query = torch.mul(query, self.sigmoid(self.gate(query))) # (batch, embed) # 计算注意力分数 (简化版,点积注意力) attention_scores = torch.matmul(keys, gated_query.unsqueeze(-1)).squeeze(-1) # (batch, seq_len) attention_weights = F.softmax(attention_scores, dim=-1) # 加权求和 output = torch.sum(keys * attention_weights.unsqueeze(-1), dim=1) return output

这只是一个示意,将门控用于调制注意力计算中的Query向量,可以让模型在计算相关性时,更关注目标物品的某些特定维度。

5.3 在双塔召回模型中的应用

在召回阶段,用户塔和物品塔分别产生用户向量和物品向量。我们可以将SENET应用于物品塔的输入特征上。在物品塔端,SENET可以动态评估物品不同特征(类目、品牌、价格段等)对于当前召回任务的重要性,生成一个更好的物品向量。这对于解决物品冷启动或长尾物品曝光问题可能有奇效。用户塔则可以使用GateNet来控制用户历史序列信息聚合时的噪声。

部署心得:将SENET+GateNet应用于线上推荐系统,除了关注效果,一定要进行严格的压力测试。评估其增加的RT(响应时间)和QPS(每秒查询率)消耗是否在业务可接受范围内。通常,这些操作在现代GPU或经过优化的CPU推理引擎上,开销是相对较小的。关键在于,它带来的效果提升是否能覆盖增加的成本。在我的经验中,对于核心的排序环节,即使RT增加1-2毫秒,只要CTR/CVR有显著提升,这笔交易几乎总是划算的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询