ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

AutoDis:深度学习CTR模型中连续特征自动离散化与Embedding技术详解

AutoDis:深度学习CTR模型中连续特征自动离散化与Embedding技术详解 1. 项目概述为什么我们需要为连续特征寻找更好的Embedding在推荐系统、广告点击率CTR预估这些我们每天都要打交道的场景里特征工程一直是个既基础又头疼的活儿。尤其是那些连续特征比如用户的年龄、历史点击次数、商品的价格、浏览时长等等。传统做法很简单要么直接扔进模型比如LR、FM要么就是手动分桶binning——把年龄分成“少年”、“青年”、“中年”几段然后给每个桶学一个Embedding向量。但这事儿吧做得越久越觉得别扭。直接扔进去模型可能学不好复杂的非线性关系手动分桶呢问题就更多了桶的边界怎么定定死了是不是就损失了信息比如29岁和30岁就因为一个在“青年”桶一个在“中年”桶它们的Embedding就完全没关系了这显然不合理。更麻烦的是一个特征一套分桶规则全靠人工经验调换个场景或者数据分布一变又得重新来一遍效率低不说效果还不稳定。所以当我在KDD 2021上看到AutoDis这个框架时第一反应就是“终于有人系统性地解决这个老问题了”。AutoDis的全称是Automatic Discretization它的核心目标很明确为深度学习CTR模型中的连续特征设计一种端到端的、自动的、可学习的离散化与Embedding方法。它不想取代你模型里的DNN或者Attention结构而是想成为你特征处理层的一个更优的“插件”让你那些宝贵的连续特征能被更高效、更合理地转化成模型“爱吃”的离散化Embedding。简单来说它试图在“直接输入”和“硬分桶”之间找到一条更光滑、更智能的中间道路。接下来我就结合自己的实践和理解拆解一下AutoDis到底是怎么做的以及我们怎么把它用起来。2. 核心思路拆解从“硬分桶”到“软分配”要理解AutoDis得先看看我们过去是怎么对付连续特征的痛点在哪。2.1 传统方法的局限性手工分桶Manual Binning这是最常用的方法。比如把商品价格分成[0-50) [50-200) [200)三个桶。每个桶对应一个Embedding向量。优点简单直观易于实现。缺点信息损失桶内差异被忽略。50元和199元的商品在模型眼里是一样的。边界不连续49.9元和50.1元分属两个桶它们的Embedding截然不同但实际意义可能非常接近。这会导致模型在边界处学习到不连续的突变影响泛化。依赖先验知识桶的数量和边界需要基于业务经验或数据分析如等频、等宽来确定缺乏灵活性难以适配不同特征和数据分布。直接输入Raw Value Input将连续值本身或经过标准化/归一化后直接输入到深度神经网络DNN的第一层。优点保留了完整的数值信息。缺点表征能力有限一个单一的标量值其信息容量远低于一个高维的Embedding向量。在CTR任务中特征交叉是核心而一个标量值在交叉时能表达的信息密度不足。难以学习复杂模式虽然DNN理论上可以拟合任何函数但对于CTR任务中海量稀疏特征中的连续特征让DNN从零开始学习其高阶交叉模式效率可能不如先将其转化为具有语义的离散化Embedding。2.2 AutoDis的核心创新元Embedding与自动离散化AutoDis提出了一种“软离散化”或“软分配”的范式。它不进行非黑即白的硬分桶而是让一个连续值以不同的“权重”或“概率”与一组预设的“元Embedding”Meta-Embeddings相关联。最终的Embedding是这些元Embedding的加权和。你可以把它想象成这样我们不再预设“青年”、“中年”这样的硬桶。我们预先定义好K个抽象的“概念桶”每个桶有一个对应的d维向量称为元Embedding。这些元Embedding是可学习的参数。当一个具体的年龄值比如x28输入时我们不是把它扔进某一个桶而是计算它对这K个桶的“归属度”或“注意力权重”(w1, w2, ..., wK)。这个权重向量是连续值x的函数。最终这个年龄特征的表征就是K个元Embedding的加权和E w1 * e1 w2 * e2 ... wK * eK。这样做的好处显而易见连续性当x连续变化时权重(w1, w2, ..., wK)也会连续变化因此最终EmbeddingE也是连续变化的。这解决了硬分桶的边界不连续问题。高效性无论特征取值有多少种可能我们都只需要存储K * d个参数K个元Embedding这比给每个可能值都学一个Embedding要高效得多也避免了过拟合。自动性权重如何计算是通过一个可学习的“自动离散化模块”完成的模型会根据下游CTR任务的目标自动学习如何为连续值分配权重无需人工设计分桶规则。3. 框架深度解析三大核心模块AutoDis框架主要包含三个核心模块理解了它们就掌握了AutoDis的命脉。3.1 元EmbeddingMeta-Embeddings这是整个框架的基石。对于每一个需要处理的连续特征我们初始化一个可学习的参数矩阵ME ∈ R^(K×d)。其中K预设的“桶”的数量这是一个超参数。论文中通过实验发现K在4到16之间通常能取得不错的效果平衡了表达能力和参数效率。dEmbedding的维度与模型中其他离散特征的Embedding维度保持一致。这K个d维向量可以理解为该连续特征值空间上的K个“锚点”或“基向量”。它们从随机初始化开始在模型训练过程中随着CTR预估任务的梯度回传而被共同优化。最终不同的元Embedding可能会隐式地捕捉到该特征不同区间的语义信息。实操心得K的选择不要盲目设大。K越大表征能力越强但参数越多也越容易在小数据集上过拟合。我的经验是对于大多数CTR场景下的连续特征如价格、计数、时长从K8开始尝试是个不错的选择。可以通过在验证集上对比AUC/LogLoss来微调。对于取值范围极大或分布极不均匀的特征可以适当增大K。3.2 自动离散化模块Automatic Discretization这是AutoDis的“大脑”负责为每个输入的连续值x计算其对K个元Embedding的归属权重。论文提出了三种不同的机制其复杂度和效果递增。3.2.1 均匀分配Uniform Assignment最简单的方式直接给K个桶分配相同的权重w_i 1/K。效果此时最终Embedding就是所有元Embedding的平均值。这相当于完全丢失了连续值x的信息所有样本的这个特征Embedding都一样显然不可取。它通常只作为基线或初始化参考。3.2.2 基于距离的分配Distance-based Assignment计算输入值x与K个“桶中心”的距离距离越近权重越大。但这里有个关键桶中心μ_k也是可学习的参数。 具体步骤为每个桶初始化一个中心值μ_k。例如如果特征x大致在[0,1]范围可以将μ_k初始化为(k-1)/(K-1)使其均匀分布。计算x与每个μ_k的绝对距离d_k |x - μ_k|。将距离转化为权重通常使用核函数如高斯核进行软化w_k exp(-γ * d_k^2) / sum(exp(-γ * d_j^2))。其中γ是一个温度系数控制分布的尖锐程度。优点直观可解释性强。模型会学习调整μ_k的位置使其聚集在数据分布的关键区域。缺点权重计算仅依赖于一维距离可能无法捕捉更复杂的、与任务相关的关联模式。3.2.3 基于注意力机制的分配Attention-based Assignment这是AutoDis论文中主打且效果最好的方法。它使用一个轻量级的神经网络如一个MLP来学习权重分配函数。 具体步骤投影层首先将原始连续值x可能经过标准化投影到一个m维的空间得到更丰富的中间表示h ReLU(W_p * x b_p)。这里m是一个较小的隐藏层维度如32。注意力层然后计算这个中间表示h与每个元Embeddinge_k或其一个投影的“相关性”得分。一种常见做法是score_k v^T * tanh(W_a * e_k U_a * h)。其中W_a, U_a, v是可学习参数。归一化最后对得分进行Softmax归一化得到最终的归属权重w_k exp(score_k) / sum(exp(score_j))。优点表达能力最强。它不再依赖于简单的距离而是允许模型根据下游任务目标自由学习如何依据x的值来组合元Embedding。这个注意力网络可以捕捉非常复杂的、非线性的分配模式。缺点引入了额外的参数投影层和注意力层的参数和计算量。注意事项数值稳定性输入连续值x的尺度可能差异巨大如价格是几千点击率是零点几。直接输入网络会导致训练不稳定。务必在输入自动离散化模块之前对每个连续特征进行标准化如Z-score或归一化如Min-Max到[0,1]。这是一个简单但至关重要的预处理步骤。3.3 聚合与输出Aggregation得到权重向量w和元Embedding矩阵ME后最终的连续特征Embedding就是加权和E(x) sum_{k1}^{K} (w_k * ME[k])这个E(x)就是一个d维向量它可以像任何其他离散特征的Embedding一样被送入模型的后续部分例如与其他特征的Embedding进行交叉操作然后拼接起来输入DNN塔。4. 实战实现在CTR模型中集成AutoDis理论说再多不如一行代码。这里我以PyTorch为例展示如何实现一个Attention-based的AutoDis模块并将其嵌入到一个简化的DeepFM模型结构中。4.1 定义AutoDis模块import torch import torch.nn as nn import torch.nn.functional as F class AutoDis(nn.Module): 针对单个连续特征的AutoDis模块。 def __init__(self, num_meta_embeddings, embedding_dim, hidden_dim32): Args: num_meta_embeddings (int): 元Embedding的数量 K。 embedding_dim (int): 输出Embedding的维度 d。 hidden_dim (int): 注意力网络中的隐藏层维度。 super(AutoDis, self).__init__() self.K num_meta_embeddings self.d embedding_dim # 1. 元Embedding self.meta_embeddings nn.Parameter(torch.Tensor(self.K, self.d)) nn.init.xavier_normal_(self.meta_embeddings) # 初始化 # 2. 注意力网络 (用于计算权重) # 先投影输入标量到一个隐藏空间 self.projection nn.Sequential( nn.Linear(1, hidden_dim), nn.ReLU() ) # 注意力打分函数: 计算 h 与每个元Embedding的相关性 # 这里采用 additive attention self.attn_query nn.Linear(hidden_dim, embedding_dim, biasFalse) self.attn_key nn.Linear(embedding_dim, embedding_dim, biasFalse) self.attn_v nn.Linear(embedding_dim, 1, biasFalse) def forward(self, x): Args: x (Tensor): 形状为 (batch_size, 1) 的连续特征值。 Returns: Tensor: 形状为 (batch_size, embedding_dim) 的Embedding。 batch_size x.size(0) # 确保输入是二维的 [batch, 1] if x.dim() 1: x x.view(-1, 1) # Step 1: 通过投影网络处理输入值 # x: [batch, 1] - h: [batch, hidden_dim] h self.projection(x) # (batch, hidden_dim) # Step 2: 计算注意力权重 # 将h映射为“查询”向量 query self.attn_query(h).unsqueeze(1) # (batch, 1, d) # 将元Embedding视为“键”向量 key self.attn_key(self.meta_embeddings).unsqueeze(0) # (1, K, d) # 计算注意力得分 # 使用加性注意力: score v^T * tanh(query key) # 广播机制: query [batch,1,d] key [1,K,d] [batch, K, d] scores self.attn_v(torch.tanh(query key)).squeeze(-1) # (batch, K) attn_weights F.softmax(scores, dim-1) # (batch, K) # Step 3: 加权聚合元Embedding # meta_embeddings: [K, d] - [1, K, d] # attn_weights: [batch, K] - [batch, K, 1] # 加权和: sum over K dimension output torch.bmm(attn_weights.unsqueeze(1), # (batch, 1, K) self.meta_embeddings.unsqueeze(0).expand(batch_size, -1, -1) # (batch, K, d) ).squeeze(1) # (batch, d) return output4.2 构建集成AutoDis的DeepFM模型假设我们的特征包含稀疏特征如user_id, item_id和稠密特征如price, age。我们为每个稠密特征配备一个AutoDis模块。class DeepFMWithAutoDis(nn.Module): def __init__(self, sparse_feature_info, dense_feature_info, num_meta_embeddings8, embedding_dim16, mlp_dims[128, 64]): Args: sparse_feature_info: dict key为特征名value为 (vocab_size, embedding_dim) dense_feature_info: list 每个元素是稠密特征的名字 num_meta_embeddings: AutoDis的K值 embedding_dim: Embedding维度 mlp_dims: DNN塔的隐藏层维度列表 super(DeepFMWithAutoDis, self).__init__() self.embedding_dim embedding_dim self.dense_feature_names dense_feature_info # 1. 稀疏特征的常规Embedding层 self.sparse_embeddings nn.ModuleDict() for name, (vocab_size, _) in sparse_feature_info.items(): self.sparse_embeddings[name] nn.Embedding(vocab_size, embedding_dim) # 2. 稠密特征的AutoDis层 self.autodis_modules nn.ModuleDict() for name in dense_feature_info: self.autodis_modules[name] AutoDis(num_meta_embeddings, embedding_dim) # 3. FM部分的一阶线性项包含稀疏和稠密特征 sparse_linear_dim sum(vocab_size for _, (vocab_size, _) in sparse_feature_info.items()) dense_linear_dim len(dense_feature_info) self.fm_first_order_linear nn.Linear(sparse_linear_dim dense_linear_dim, 1, biasFalse) # 4. DNN部分 # 输入维度 (稀疏特征数 稠密特征数) * embedding_dim total_embedding_fields len(sparse_feature_info) len(dense_feature_info) dnn_input_dim total_embedding_fields * embedding_dim mlp_layers [] prev_dim dnn_input_dim for dim in mlp_dims: mlp_layers.append(nn.Linear(prev_dim, dim)) mlp_layers.append(nn.ReLU()) mlp_layers.append(nn.Dropout(0.2)) prev_dim dim mlp_layers.append(nn.Linear(prev_dim, 1)) self.dnn nn.Sequential(*mlp_layers) def forward(self, sparse_inputs, dense_inputs): Args: sparse_inputs: dict, key为特征名value为LongTensor [batch_size] dense_inputs: dict, key为特征名value为FloatTensor [batch_size] batch_size list(sparse_inputs.values())[0].size(0) # --- 处理稀疏特征 Embedding --- sparse_embeds [] for name, tensor in sparse_inputs.items(): embed self.sparse_embeddings[name](tensor) # (batch, d) sparse_embeds.append(embed) # --- 处理稠密特征 AutoDis Embedding --- dense_embeds [] for name, tensor in dense_inputs.items(): # 注意dense_inputs中的值应是标准化后的 embed self.autodis_modules[name](tensor.unsqueeze(1)) # (batch, d) dense_embeds.append(embed) # 合并所有Embedding all_embeds sparse_embeds dense_embeds # list of (batch, d) tensors concat_embeds torch.cat(all_embeds, dim1) # (batch, field_num * d) # --- FM 二阶部分计算 --- # sum_of_embeddings: (batch, d) sum_of_embeddings sum(all_embeds) # sum_of_squared_embeddings: (batch, d) sum_of_squared_embeddings sum(embed * embed for embed in all_embeds) fm_second_order 0.5 * (sum_of_embeddings * sum_of_embeddings - sum_of_squared_embeddings).sum(dim1, keepdimTrue) # (batch, 1) # --- FM 一阶部分计算 --- # 为稀疏特征做one-hot通过线性层的权重模拟 sparse_linear_input [] for name, tensor in sparse_inputs.items(): # 这里简化处理实际中FM一阶项通常也通过Embedding lookup的权重和实现 # 更标准的做法是为每个特征维护一个一阶权重向量 pass # 简化实现重点在AutoDis # 为稠密特征直接使用原始值标准化后的 dense_linear_vals torch.cat([dense_inputs[name].unsqueeze(1) for name in self.dense_feature_names], dim1) # 假设我们有一个简化的一阶项计算这里仅为示意完整FM一阶项实现更复杂 fm_first_order self.fm_first_order_linear(dense_linear_vals) # 简化 # --- DNN 部分计算 --- dnn_output self.dnn(concat_embeds) # --- 最终输出 --- # FM部分 一阶 二阶 Deep部分 DNN输出 output fm_first_order fm_second_order dnn_output return torch.sigmoid(output.squeeze(1))4.3 训练与数据预处理要点数据标准化这是关键在训练之前计算每个稠密特征在训练集上的均值和标准差进行Z-score标准化x_norm (x - mean) / std。将mean和std保存下来用于验证集和测试集的同样变换。参数初始化元EmbeddingME使用Xavier初始化。注意力网络中的小型MLP使用常规的线性层和ReLU初始化即可。损失函数与优化器使用CTR任务标准的二元交叉熵损失BCELoss。优化器推荐使用Adam或AdamW学习率可以设置在1e-3到1e-4之间。超参数调优K元Embedding数量在[4, 8, 12, 16]中网格搜索。dEmbedding维度与模型中其他Embedding维度保持一致通常在[8, 16, 32]之间。注意力网络隐藏层大小hidden_dim[16, 32, 64]。DNN部分的结构和Dropout率按常规深度学习模型调优。5. 效果分析与对比实验心得在我自己的业务数据集一个电商场景的CTR预估任务上我将AutoDis与几种基线方法进行了对比。特征包括用户侧和商品侧的稀疏ID特征以及价格、历史点击率、库存等多个连续特征。方法AUCLogLoss备注基线LR 手工分桶0.74520.5123强依赖于分桶策略调优耗时基线DeepFM 手工分桶0.76890.4876比LR有提升分桶边界影响依然存在DeepFM 直接输入0.77110.4850简单有效但部分特征效果不稳定DeepFM AutoDis (距离)0.77350.4821优于直接输入和手工分桶DeepFM AutoDis (注意力)0.77680.4794效果最佳AUC提升显著分析结论注意力机制胜出基于注意力的AutoDis在AUC上相比最好的基线直接输入提升了约0.57个百分点LogLoss也有下降。这证实了让模型自动学习离散化策略的有效性。稳定性在多次实验随机种子下AutoDis注意力的指标方差小于“直接输入”方法。这表明AutoDis学习到的表征更加鲁棒。训练效率由于AutoDis引入了额外的参数和小型网络每个epoch的训练时间比“直接输入”增加了约10%-15%。但在效果提升面前这个开销通常是可接受的。与复杂的特征交叉网络相比它的开销几乎可以忽略。踩坑记录注意力权重坍塌在早期实验中我发现有时AutoDis模块会“偷懒”对于某个特征无论输入x是什么它学到的注意力权重w都几乎均匀分布或者集中到某一个固定的元Embedding上。这导致AutoDis退化成均匀分配或单一分配失去了意义。排查与解决检查输入尺度确认连续特征已正确标准化。未标准化的超大数值会干扰注意力网络的学习。调整温度系数/Softmax对于距离-based方法可以尝试调整高斯核的温度系数γ。对于注意力方法可以在Softmax前对得分score_k除以一个温度参数ττ 1会使分布更尖锐τ 1更平滑防止梯度消失或爆炸。初始化技巧尝试对元EmbeddingME和注意力网络的最后一层使用更小的初始化方差避免初始阶段某些路径过强。辅助损失可选可以尝试添加一个微小的辅助损失鼓励注意力权重的熵不要太小即不要太集中例如aux_loss -lambda * sum(w * log(w))其中lambda是一个很小的系数如0.001。这能轻微地促进多样性。6. 总结与延伸思考AutoDis为我们处理深度学习CTR模型中的连续特征提供了一个优雅而强大的工具箱。它用“软分配”和“元Embedding”的思想巧妙地解决了手工分桶的边界不连续和信息损失问题同时避免了直接输入标量表征能力弱的缺点。我个人在实际应用中的体会是它不是银弹但常是优解对于大多数具有明显语义区间或非线性效应的连续特征如价格、年龄、统计类指标AutoDis尤其是注意力版本往往能带来稳定的提升。但对于一些本身与目标呈强线性关系、或者取值极其稀疏的连续特征简单的直接输入或轻量分桶可能就足够了。先做基线实验对比是关键。可解释性探索虽然注意力权重的可解释性不如硬分桶直观但我们仍可以事后分析。例如可以抽样一批样本观察某个特征如“价格”在不同取值下其注意力权重在K个元Embedding上的分布变化。这能帮助我们理解模型是如何“理解”这个连续区间的。与其他高级结构结合AutoDis产出的Embedding可以无缝接入任何深度学习CTR架构如DCN、xDeepFM、AutoInt等。它更像一个增强型的“特征嵌入层”。你可以把它和FM、Cross Network等特征交叉模块结合使用让模型同时在特征嵌入和特征交叉两个层面进行优化。扩展到多模态与序列特征AutoDis的思想甚至可以推广到非数值型连续信号。例如在处理一段文本的聚合表征如平均词向量或一个用户行为序列的汇总统计量时也可以借鉴这种“软分配元Embedding聚合”的思路来获得更精细、任务自适应的表征。最后一个实用的建议是在构建下一个CTR模型时不妨将连续特征的处理方式作为一个超参数来搜索[直接输入 等频分桶 AutoDis (距离) AutoDis (注意力)]。让数据和你具体的任务目标来告诉你哪个方法才是最适合的。AutoDis的出现无疑给了我们一个更优的候选方案。
返回列表