2026/8/29 2:27:21

AutoDis:连续特征Embedding的自动离散化方法,提升CTR模型效果

AutoDis:连续特征Embedding的自动离散化方法,提升CTR模型效果 1. 项目概述为什么我们需要AutoDis在推荐、广告、搜索这些我们每天都会接触的互联网核心业务里点击率预估模型是背后的“大脑”。这个大脑要做的就是判断用户会不会点击一个商品、一篇新闻或者一条广告。传统的模型比如逻辑回归处理用户年龄、商品价格这类数值型特征时通常就是简单地把数字扔进去。但深度学习模型特别是基于Embedding的模型在处理这类连续特征时遇到了一个不大不小的麻烦。我们都知道像用户ID、商品ID这类离散特征Embedding层处理起来得心应手每个ID对应一个向量模型自己去学这个向量长什么样。但“年龄28”、“价格199.9”这种连续值怎么办最直接的办法是离散化也就是分桶。比如把年龄分成“0-18”“19-30”“31-45”“46”几个桶然后每个桶当成一个离散的类别去学Embedding。这个方法用了很多年但它有个致命伤信息损失和边界敏感。一个29岁和一个30岁的用户可能被分到两个不同的桶里他们的Embedding向量完全不同这显然不合理。而且分多少个桶、桶的边界划在哪全靠人工经验调参成本高效果还不稳定。另一种思路是直接把数值本身作为输入但这就失去了Embedding的“表征学习”能力模型难以捕捉连续特征中复杂的非线性模式。所以在2021年的KDD大会上阿里妈妈团队提出的AutoDis瞄准的就是这个核心痛点如何让连续特征也能像离散特征一样拥有高质量、可学习的Embedding同时避免人工分桶的弊端它不是一个全新的模型架构而是一个精巧的“插件”可以无缝嵌入到DeepFM、DIN、DIEN等主流CTR模型中替换掉其中对连续特征的处理模块。简单说AutoDis让模型自己学会“如何更好地理解一个数字”。2. 核心设计思路从“硬分桶”到“软分配”要理解AutoDis得先抛开“分桶”这个固有思维。AutoDis的核心思想是我不预先定义桶而是定义一组“元Embedding”然后让每个具体的连续特征值自动地、以不同的权重去聚合这组元Embedding从而生成自己独一无二的Embedding。这有点像什么呢假设我们要给“电影”这个连续特征比如豆瓣评分找Embedding。传统分桶是准备几个固定的标签“烂片”、“一般”、“好片”、“神作”然后把评分对号入座。AutoDis的做法则是我先准备10个数量可调基础的“电影概念元Embedding”比如E1可能代表“剧情深度”E2代表“视觉特效”E3代表“演员演技”……对于一个具体的评分8.5分的电影模型会计算这个分数与每个“概念元”的相关性权重然后加权求和这些元Embedding最终得到代表“8.5分电影”的Embedding。这样一来8.4分和8.6分的电影其Embedding会非常相似但又不完全相同完美解决了边界问题。2.1 三大核心模块解析AutoDis将这个思想拆解为三个可学习的、端到端训练的模块这也是其名称的由来Automatic Discretization。2.1.1 元Embedding层这是整个框架的基石。对于每一个连续特征字段比如“用户年龄”我们预先定义一组大小为K的元Embedding集合{e_1, e_2, ..., e_K}。这里的K是一个超参数比如10或20。每个元Embeddinge_k的维度与模型其他离散特征的Embedding维度d一致。关键点在于这些元Embedding不是对应某个具体的数值区间而是代表了这个连续特征可能蕴含的多种“潜在模式”或“概念基底”。它们通过模型训练随机初始化并在梯度下降过程中被共同优化。2.1.2 自动相关性度量模块这个模块的任务是对于一个输入的具体连续特征值x比如年龄28计算它与第k个元Embeddinge_k的“相关性”或“亲和度”w_k。AutoDis论文中提出了几种可选的度量函数最经典的是基于距离的核函数w_k exp(-distance(x, mu_k)^2 / sigma_k)这里mu_k和sigma_k是可学习的参数分别代表第k个元Embedding所“关注”的数值中心点和宽度。distance通常使用绝对值距离。这个公式很好理解如果x离mu_k越近且sigma_k越宽容忍度越高那么权重w_k就越大。通过训练mu_k会自适应地移动到特征值分布的关键区域比如年龄的20-30岁高峰区sigma_k则学会了该模式的覆盖范围。2.1.3 聚合输出层有了权重w_1, w_2, ..., w_K之后最后一步就是加权求和。最终这个连续特征值x的EmbeddingE(x)就是E(x) sum_{k1 to K} (w_k * e_k)并且通常会对权重w_k进行归一化如softmax使得所有权重之和为1保证聚合后的Embedding数值稳定。这个E(x)就是送入后续深度神经网络如MLP的向量表示。2.2 与传统方法的对比优势为了更直观我们用一个表格来对比AutoDis和传统方法特性传统离散化分桶直接输入数值AutoDis信息损失严重桶内无差别无极小相近值有相似表征边界问题敏感边界突变无平滑边界过渡自然表征能力依赖人工设计有限线性或简单非线性强自动学习非线性表征参数量与桶数成正比极少几乎无固定与K值成正比可控可解释性一般桶含义明确差中等可通过mu_k观察关注点与模型集成作为离散特征输入作为稠密特征输入输出Embedding无缝集成从表格可以看出AutoDis在平衡表达能力、泛化能力和工程便利性上找到了一个很好的折中点。它把“如何离散化”这个特征工程问题转化成了一个可以通过梯度下降来优化的学习问题。3. 实操要点与代码实现解析理解了原理我们来看看怎么用。这里以PyTorch为例展示一个最简化的AutoDis层实现并穿插讲解实操中的关键点。3.1 环境准备与依赖首先确保你的环境包含PyTorch。AutoDis本身不依赖特定框架但PyTorch的动态图特性便于理解和实验。import torch import torch.nn as nn import torch.nn.functional as F import numpy as np3.2 AutoDis层核心代码实现下面是一个完整的、可嵌入到任何模型中的AutoDis模块类。class AutoDis(nn.Module): 针对单个连续特征字段的AutoDis层。 Args: embedding_dim (int): 输出Embedding的维度。 num_meta_embeddings (int): 元Embedding的数量 K。 temperature (float, optional): Softmax的温度系数控制权重分布尖锐程度。默认1.0。 def __init__(self, embedding_dim, num_meta_embeddings, temperature1.0): super(AutoDis, self).__init__() self.embedding_dim embedding_dim self.K num_meta_embeddings self.temperature temperature # 模块1: 元Embedding层 self.meta_embeddings nn.Parameter(torch.Tensor(num_meta_embeddings, embedding_dim)) # 模块2: 自动相关性度量模块的参数 # mu_k: 每个元Embedding对应的中心值 self.mu nn.Parameter(torch.Tensor(num_meta_embeddings)) # sigma_k: 每个元Embedding对应的宽度正值用softplus确保为正 self.log_sigma nn.Parameter(torch.Tensor(num_meta_embeddings)) # 初始化参数 self.reset_parameters() def reset_parameters(self): # 元Embedding用Xavier初始化 nn.init.xavier_uniform_(self.meta_embeddings) # mu初始化为[0,1]均匀分布假设输入特征已归一化 nn.init.uniform_(self.mu, 0, 1) # log_sigma初始化为0即sigma初始约为1.0 nn.init.constant_(self.log_sigma, 0) def forward(self, x): Args: x (Tensor): 形状为 (batch_size, 1) 的连续特征值张量。建议输入值已在[0,1]范围。 Returns: Tensor: 形状为 (batch_size, embedding_dim) 的Embedding张量。 batch_size x.size(0) # 确保输入是二维的 x x.view(-1, 1) # (batch_size, 1) # 计算sigma确保为正数 sigma F.softplus(self.log_sigma) 1e-6 # 形状 (K,) # 计算距离|x - mu_k|利用广播机制 # x: (batch_size, 1), mu: (K,) - distance: (batch_size, K) distance torch.abs(x - self.mu) # 计算非归一化权重核函数 exp(-(distance^2) / sigma) # 这里除以 temperature 可以调节权重分布的平滑度 weights torch.exp(- (distance.pow(2)) / sigma / self.temperature) # (batch_size, K) # 对权重进行softmax归一化得到最终的聚合权重 weights F.softmax(weights, dim1) # (batch_size, K) # 聚合加权求和元Embedding # meta_embeddings: (K, emb_dim) # weights: (batch_size, K) - unsqueeze: (batch_size, K, 1) # 利用torch.bmm进行批量矩阵乘法 embeddings torch.bmm(weights.unsqueeze(1), # (batch_size, 1, K) self.meta_embeddings.unsqueeze(0).expand(batch_size, -1, -1) # (batch_size, K, emb_dim) ).squeeze(1) # (batch_size, emb_dim) return embeddings3.3 关键实现细节与调参心得输入归一化是必须的AutoDis的mu参数学习的是特征值域内的中心点。如果输入特征x的尺度差异巨大比如年龄0-100价格0-10000会导致mu的学习不稳定且sigma的尺度难以统一。务必在将数据输入AutoDis层之前进行归一化处理如Min-Max归一化到[0,1]区间。这是实践中最容易忽略却最关键的一步。sigma的正则化与初始化sigma代表了每个元Embedding的“影响范围”。如果sigma学得太大所有权重会趋同失去区分度如果学得太小则只有极接近mu的值才有响应导致梯度稀疏。代码中使用F.softplus确保其为正并加了一个小常数防止除零。初始化时让log_sigma为0即sigma初始约等于1.0是一个不错的起点。如果发现效果不佳可以尝试对sigma施加L2正则防止其变得过大或过小。温度系数temperature的作用这个参数控制着权重分布的“尖锐”程度。temperature越小如0.1softmax后的权重分布越接近one-hot即每个特征值只强烈关联一个元Embedding这有点像“软分桶”。temperature越大如10.0权重分布越平滑每个特征值会均匀地关联所有元Embedding表征能力会下降。通常从1.0开始调试根据验证集效果微调。元Embedding数量K的选择K是核心超参数。太小如3-5表征能力可能不足太大如50-100不仅增加参数量也可能导致过拟合和训练困难。经验上对于CTR模型中的大多数连续特征K在10到20之间是一个较好的起点。可以通过观察训练后mu的分布来辅助判断如果多个mu挤在一起可能意味着K太大了如果mu的分布覆盖了值域的主要部分且间隔均匀则说明K大小合适。梯度流动分析AutoDis的巧妙之处在于它是完全可微的。梯度会通过聚合后的Embedding反向传播到权重w_k再传到元Embeddinge_k以及参数mu_k和sigma_k。这意味着mu_k会朝着损失函数下降的方向即特征值密集且重要的区域移动。你可以把训练后的mu值打印出来直观地看到模型为这个连续特征自动找到了哪些“关键点”。4. 在CTR模型中的集成与应用AutoDis不是一个孤立的模型它的价值在于赋能。下面以经典的DeepFM模型为例展示如何用AutoDis替换其原有的连续特征处理方式。4.1 DeepFM原有结构回顾标准的DeepFM模型输入包含两部分稀疏特征用户ID、物品ID等通过Embedding层处理。稠密特征用户年龄、物品价格等通常直接拼接成一个向量送入Deep部分的MLP。问题在于稠密特征直接进入MLP只经过了简单的全连接变换缺乏像稀疏特征那样丰富的表征学习过程。4.2 集成AutoDis的DeepFM-AutoDis集成思路非常简单为每一个连续特征字段配备一个独立的AutoDis层将每个连续值转换成一个embedding_dim维的向量。然后将这些向量与稀疏特征的Embedding向量一起既送入FM部分进行二阶交叉也送入Deep部分的MLP。class DeepFMWithAutoDis(nn.Module): def __init__(self, sparse_field_dims, dense_field_num, embedding_dim, num_meta_embeddings, deep_layers, drop_rate): super(DeepFMWithAutoDis, self).__init__() self.embedding_dim embedding_dim self.dense_field_num dense_field_num # 稀疏特征Embedding层 self.embedding nn.Embedding(sum(sparse_field_dims), embedding_dim) # 为每个连续特征创建一个AutoDis层 self.autodis_layers nn.ModuleList([ AutoDis(embedding_dim, num_meta_embeddings) for _ in range(dense_field_num) ]) # FM部分的一阶线性项包含稀疏和稠密特征 self.fm_linear nn.Linear(len(sparse_field_dims) dense_field_num, 1) # Deep部分 # 输入维度所有特征的Embedding拼接后的总维度 deep_input_dim (len(sparse_field_dims) dense_field_num) * embedding_dim deep_layers [deep_input_dim] deep_layers self.deep nn.ModuleList() for i in range(len(deep_layers)-1): self.deep.append(nn.Linear(deep_layers[i], deep_layers[i1])) self.deep.append(nn.BatchNorm1d(deep_layers[i1])) self.deep.append(nn.ReLU()) self.deep.append(nn.Dropout(drop_rate)) self.deep_out nn.Linear(deep_layers[-1], 1) # 输出层 self.output_activation nn.Sigmoid() def forward(self, sparse_x, dense_x): sparse_x: LongTensor of shape (batch_size, num_sparse_fields) dense_x: FloatTensor of shape (batch_size, num_dense_fields), 假设已归一化到[0,1] batch_size sparse_x.size(0) # 1. 处理稀疏特征Embedding sparse_emb self.embedding(sparse_x) # (batch_size, num_sparse, emb_dim) # 2. 处理连续特征通过各自的AutoDis层 dense_emb_list [] for i in range(self.dense_field_num): # 取出第i个连续特征列形状 (batch_size, 1) dense_feat_col dense_x[:, i:i1] # 通过对应的AutoDis层 dense_emb self.autodis_layers[i](dense_feat_col) # (batch_size, emb_dim) dense_emb_list.append(dense_emb.unsqueeze(1)) # 变为 (batch_size, 1, emb_dim) # 拼接所有连续特征的Embedding dense_emb torch.cat(dense_emb_list, dim1) # (batch_size, num_dense, emb_dim) # 3. 拼接所有特征的Embedding用于Deep部分 all_emb torch.cat([sparse_emb, dense_emb], dim1) # (batch_size, num_total_fields, emb_dim) deep_input all_emb.view(batch_size, -1) # (batch_size, num_total_fields * emb_dim) # 4. FM部分计算 # 一阶部分线性回归 linear_sparse_part self.fm_linear(sparse_x.float()) linear_dense_part self.fm_linear(dense_x) # 实际中稀疏和稠密特征的线性项可能需要分开处理这里为简化示意 fm_order1 linear_sparse_part linear_dense_part # 二阶部分所有特征包括AutoDis输出的进行Embedding交互 # sum_of_emb torch.sum(all_emb, dim1) # (batch_size, emb_dim) # sum_of_square torch.sum(all_emb.pow(2), dim1) # fm_order2 0.5 * (sum_of_emb.pow(2) - sum_of_square).sum(dim1, keepdimTrue) # 更标准的FM二阶项计算 fm_order2_list [] for i in range(all_emb.size(1)): for j in range(i1, all_emb.size(1)): fm_order2_list.append(torch.sum(all_emb[:, i] * all_emb[:, j], dim1, keepdimTrue)) fm_order2 torch.sum(torch.cat(fm_order2_list, dim1), dim1, keepdimTrue) if fm_order2_list else 0 # 5. Deep部分计算 deep_out deep_input for layer in self.deep: deep_out layer(deep_out) deep_out self.deep_out(deep_out) # 6. 输出 y_pred fm_order1 fm_order2 deep_out y_pred self.output_activation(y_pred) return y_pred.squeeze(1)注意以上FM二阶项的计算采用了直观但非最优的双重循环在实际工业级实现中会使用优化后的公式0.5 * (sum_of_emb^2 - sum_of_emb^2)来计算复杂度从O(n^2)降到O(n)。这里为了代码清晰展示了原理。4.3 训练技巧与经验分享将AutoDis集成到现有CTR模型进行训练时有几个地方需要特别注意学习率设置AutoDis引入的mu和sigma参数与Embedding参数的学习动态可能不同。可以考虑为AutoDis层设置稍大的学习率例如是全局学习率的1.5-2倍帮助它们更快地找到合适的数值中心点和宽度。在PyTorch中可以通过配置不同的param_group来实现。特征重要性感知不是所有连续特征都同样重要。对于重要性极高的特征如“历史点击价格”可以适当增加其K值元Embedding数量赋予其更强的表征能力。对于重要性低的特征可以减小K值以节省参数。这需要结合特征重要性分析如Permutation Importance来进行。冷启动问题缓解对于训练数据中很少出现的极端值比如一个异常高的价格由于缺乏样本对应的mu可能难以学习到合适的位置。AutoDis的平滑聚合机制本身对此有一定鲁棒性因为极端值也会从邻近的、学习良好的mu对应的元Embedding中获取信息。为进一步稳定可以在初始化时根据特征的分布如分位数来初始化mu参数而不是均匀分布。与其它特征变换方法结合AutoDis并不排斥其他特征工程。例如对于偏态分布的特征如收入可以先进行对数变换再进行归一化和AutoDis处理。也可以将连续特征的多项式特征如x^2, sqrt(x)也作为独立的“连续特征”输入不同的AutoDis层让模型自动学习更复杂的组合关系。5. 实战常见问题与排查指南在实际部署和训练AutoDis时你可能会遇到以下典型问题。这里我结合自己的踩坑经验给出排查思路。5.1 问题一模型效果没有提升甚至下降可能原因A输入特征未归一化。这是最常见的原因。检查输入到AutoDis层的dense_x是否在[0,1]或[-1,1]区间。如果特征尺度不一mu和sigma的学习会陷入混乱。排查在模型forward函数最开始打印dense_x的min()和max()。解决在数据预处理管道中增加严格的归一化步骤。可能原因B元Embedding数量K设置不当。K太小表征能力不足K太大容易过拟合尤其是在该连续特征数据量不大的情况下。排查观察训练集和验证集Loss的差距。如果训练Loss远低于验证Loss可能是过拟合尝试减小K。也可以可视化训练后mu的分布看是否有很多mu挤在一起。解决进行K值的网格搜索例如尝试 [5, 10, 15, 20]。对于重要特征从10开始次要特征从5开始。可能原因C温度系数temperature不合适。过小的temperature导致权重过于尖锐近似于硬分桶失去了平滑性的优势过大的temperature导致权重过于平均所有元Embedding贡献趋同表征能力弱。排查在验证集上尝试不同的temperature值如0.1, 0.5, 1.0, 2.0, 5.0。解决将其作为一个常规超参数进行调优。5.2 问题二训练不稳定Loss出现NaN可能原因Asigma值过小导致数值溢出。在计算权重exp(-d^2/sigma)时如果sigma学习到非常接近0的正数而d不为0则指数部分会趋向负无穷大导致权重计算为0。在后续的加权求和或梯度计算中可能引发问题。排查在AutoDis层的forward函数中打印sigma的最小值。解决确保sigma的计算有下限保护如代码中的F.softplus(...) 1e-6。也可以对log_sigma参数施加权重衰减L2正则防止其走向极端。可能原因B梯度爆炸。如果特征值范围极大且未归一化计算距离d |x - mu|时会产生巨大值导致梯度异常。排查检查输入数据的分布。解决务必进行特征归一化。5.3 问题三训练速度明显变慢可能原因AutoDis层引入了额外的计算。主要是权重计算中的指数运算exp和批矩阵乘法bmm。当连续特征字段很多几十上百个且K值较大时计算开销不可忽视。排查使用Profiling工具如PyTorch Profiler分析模型各层耗时。解决减少K在效果可接受的范围内使用更小的K。优化实现检查权重计算部分是否有向量化优化的空间。上述示例代码已基本向量化。选择性使用并非所有连续特征都需要AutoDis。对模型贡献度低的特征可以退回到简单的归一化后直接输入MLP。5.4 问题四如何解释AutoDis学习到了什么AutoDis具有一定的可解释性这比纯黑箱的MLP处理要好。观察mu的分布训练结束后将每个AutoDis层的mu参数取值打印出来并按大小排序。这些点代表了模型自动为该连续特征发现的“关键数值锚点”。例如在“商品价格”特征上mu可能会聚集在低价区、中价区和高价区的几个关键点附近这反映了模型认为价格影响的几个关键阈值。观察权重分布对于一个给定的输入值x打印出它对各个元Embedding的权重w_k。你可以看到x主要“激活”了哪几个元Embedding。例如一个中等偏高的价格可能同时以较高权重关联了“性价比锚点”和“品质感锚点”对应的元Embedding。分析元Embedding相似性计算不同元Embeddinge_k之间的余弦相似度。如果某两个e_k非常相似且它们的mu也接近可能意味着这个K值设置过大了存在冗余。5.5 性能对比实验设计心得当你需要写报告或论文证明AutoDis的有效性时一个严谨的实验设计至关重要基线模型选择至少对比三种基线方法直接输入将归一化后的连续值直接拼接输入MLP。等宽分桶人工将特征值域分为N个等宽区间然后做Embedding。等频分桶人工将特征按样本频率分为N个桶然后做Embedding。其他SOTA方法如Google的Piecewise Linear Model (PLM)或DCN-V2中的Cross Network。评估指标在CTR任务中AUC和LogLoss是黄金标准。一定要在验证集和测试集上分别报告。对于在线业务还可以关注校准度Calibration即预测CTR与实际CTR的吻合程度。消融实验为了证明AutoDis每个部分的作用可以设计消融实验固定mu将mu参数固定为均匀分布的值不参与训练看自动学习中心点的作用。固定sigma将sigma设为一个较大的常数取消自适应宽度看其作用。移除温度系数即temperature1固定。超参数敏感性分析展示模型效果如何随K和temperature的变化而变化。用折线图清晰地呈现能让读者信服你选择的超参数是合理的。从我个人的几次实践来看AutoDis在大多数业务场景下都能带来稳定的AUC提升0.001-0.005尤其是在连续特征与离散特征交叉作用强烈的场景下其优势更为明显。它的引入几乎不增加模型服务的复杂度因为所有计算都在训练阶段完成线上推理只是多几次向量加权求和开销极小。对于长期受困于连续特征处理的算法工程师来说AutoDis确实是一个优雅而有效的解决方案。