1. 从“硬分桶”到“软学习”为什么我们需要AutoDis在推荐、广告、搜索这些以深度学习模型为核心的场景里特征工程是决定模型效果上限的基石。其中连续特征Continuous Features的处理一直是个既基础又棘手的问题。比如用户的年龄、消费金额、浏览时长、物品的价格、历史点击率CTR等等。这些特征不像用户ID、物品ID这类离散特征天然就有一个现成的Embedding表可以查。传统做法也是很多工程师的第一反应就是“分桶”Bucketization或者叫“离散化”Discretization。简单来说就是把一个连续的数值比如年龄18岁根据预设的阈值比如[0, 20), [20, 30), [30, 40)...硬生生地划归到某一个桶里然后把这个桶当作一个离散的类别再去学习它的Embedding。这个方法直接、有效在很长一段时间里都是工业界的标准操作。但干过这活儿的同学都知道这里面的坑太多了。首先阈值怎么定凭经验等频等宽对于年龄等宽似乎合理但对于消费金额长尾分布严重等宽分桶会让头部极少数样本独占多个桶尾部大量样本挤在一个桶里信息损失巨大。等频分桶能缓解分布问题但桶的边界值可能没有业务意义比如把100.3元和100.5元分到两个不同的桶而且线上服务时一个新来的数值比如一个前所未有的高消费额可能落不到任何一个桶里需要兜底策略。其次信息损失不可避免。一旦分桶18岁和19岁属于同一个“[0,20)”桶它们在模型眼里就是完全相同的而19岁和20岁虽然只差1岁却因为跨过了阈值被分到了两个不同的桶拥有了完全独立的Embedding。这种“桶内无差别桶间硬切割”的特性破坏了连续特征本身固有的序关系和数值相近性模型学习起来非常别扭。最后超参数敏感。桶的数量是一个需要精心调校的超参数。桶太少表达能力不足模型学不到精细的差异桶太多不仅增加模型参数和计算量还容易在小桶上过拟合。我们往往需要花费大量的精力在特征分桶的调参上。那么有没有一种方法能让模型自动地、更优雅地从连续特征中学习到表征既保留其连续性又能像离散特征一样生成高质量的Embedding向量呢这就是KDD 2021上提出的AutoDis框架要解决的核心问题。它不再进行“硬分桶”而是转向一种“软学习”的范式让Embedding的生成过程本身是可微的、可学习的。接下来我们就深入这个框架的内部看看它是如何巧妙设计的。2. AutoDis核心三件套元嵌入、自动相关性、聚合门控AutoDis的整个设计非常模块化且直观它主要包含三个核心组件我习惯称之为“三件套”。理解了这三部分你就掌握了AutoDis的精髓。2.1 元嵌入先准备好“候选素材库”AutoDis首先承认为连续特征预设一些“原型”或“基础元素”是必要的这类似于分桶中的“桶”。但它不把数值硬分到某一个桶而是准备一组可以灵活组合的“素材”。具体来说对于每一个连续特征字段比如“年龄”我们定义H个元嵌入。你可以把H想象成我们预设的“桶”的数量比如H10。每个元嵌入ME_h都是一个d维的向量和最终我们想要的Embedding维度一致。这H个元嵌入就构成了这个特征的一个“可学习的嵌入字典”或“素材库”。注意这里H的选择相对宽松。因为它不再是“分桶数”而是“基向量的数量”。即使H设置得比较大比如20、30由于后续的聚合机制也不会导致参数爆炸或过拟合因为它并不直接对应样本的分配。这在一定程度上降低了对这个超参数的敏感性。这一步对应论文中的Meta-Embeddings。所有样本共享这同一组H个元嵌入。对于一个具体的年龄值比如25岁模型的任务不是选择其中一个元嵌入而是思考“如何用这H个素材组合出一个最能代表25岁的Embedding”2.2 自动相关性计算数值与每个“素材”的亲和度接下来对于输入的具体连续值x经过标准化等预处理AutoDis需要计算这个x与每一个元嵌入ME_h的“相关性”或“亲和度”。这个计算是通过一个自动相关性模块实现的。这个模块通常是一个简单的神经网络比如一个多层感知机。输入是标量值x输出是一个H维的向量我们称之为相关性权重w。w的每个元素w_h代表了数值x与第h个元嵌入ME_h的相关程度。w softmax(MLP(x))这里使用softmax是为了让所有权重归一化形成一个概率分布表示x的“注意力”在这H个元嵌入上的分配。这个过程是完全可微的并且是数据驱动的。模型通过训练自己学会什么样的数值比如较小的年龄应该与哪几个元嵌入有更高的相关性。举个例子假设H5。对于x25年龄模型可能学到的权重是[0.1, 0.6, 0.2, 0.1, 0.0]。这意味着25岁这个特征与第2个元嵌入关系最密切与第1、3、4个也有一定关联与第5个无关。这就实现了“软分配”25岁不是硬属于“20-30岁”桶而是与多个“概念”以不同强度关联。2.3 聚合门控加权求和生成最终Embedding有了元嵌入MEH个每个d维和相关性权重wH维最后一步就水到渠成了生成最终的Embedding。最终Embeddinge就是这H个元嵌入的加权和e sum_{h1}^{H} (w_h * ME_h)由于w是通过可微函数MLPsoftmax从x计算出来的因此整个从x到e的映射过程是端到端可训练的。梯度可以一路从损失函数反向传播经过聚合层通过权重w更新自动相关性模块的MLP参数同时也会更新元嵌入ME本身。为什么这个设计是巧妙的保持连续性输入x的微小变化会导致MLP输出的权重w发生连续、平滑的变化从而使得生成的Embeddinge也连续变化。这保留了连续特征的内在性质。可解释性虽然不像线性变换那样直接但我们可以通过分析权重w的分布来理解模型是如何“理解”这个连续特征的。比如我们可能发现某个元嵌入总是对“高消费”数值有高权重那它就可以被解释为“高消费模式”的基底。表达能力强最终的Embedding空间是H个元嵌入张成的子空间中的一点。只要H足够且元嵌入线性无关这个空间可以表达非常丰富的模式远超简单的线性变换或分段常数函数硬分桶。下表对比了AutoDis与传统硬分桶的核心差异特性传统硬分桶AutoDis处理方式分段常数函数硬分配可微的软分配加权聚合连续性破坏桶内相同桶间跳跃保持输出随输入连续变化参数每个桶一个独立EmbeddingH个元嵌入 一个小型MLP参数超参数敏感度高依赖桶数和边界中低H影响较小MLP结构固定外推能力差需兜底策略较好MLP可泛化到未见过的x可解释性强桶有明确边界中等通过权重分布解释3. 手把手实现将AutoDis集成到你的DNN模型中理论很优美但更重要的是落地。这里我将以一个简化的点击率预测模型为例展示如何用PyTorch实现AutoDis层并把它嵌入到一个典型的深度推荐模型如DeepFM、DCN等中。我们假设我们的特征包含稀疏特征用户ID、物品ID和稠密特征年龄、消费额。这里聚焦于如何用AutoDis处理“年龄”和“消费额”这两个连续特征。3.1 定义AutoDis模块首先我们实现一个通用的AutoDis模块。import torch import torch.nn as nn import torch.nn.functional as F class AutoDis(nn.Module): AutoDis: Automatic Discretization for Deep Learning with Continuous Features. 针对单个连续特征字段。 Args: meta_embedding_dim (int): 元嵌入的维度也是最终输出Embedding的维度。 num_meta_embeddings (int): 元嵌入的数量 H。 temperature (float, optional): softmax的温度参数用于控制权重分布的平滑程度。默认为1.0。 def __init__(self, meta_embedding_dim, num_meta_embeddings, temperature1.0): super(AutoDis, self).__init__() self.meta_embedding_dim meta_embedding_dim self.num_meta_embeddings num_meta_embeddings self.temperature temperature # 1. 定义H个元嵌入 self.meta_embeddings nn.Parameter( torch.randn(num_meta_embeddings, meta_embedding_dim) * 0.02 ) # 形状: [H, d] # 2. 定义自动相关性网络 (一个简单的MLP) # 输入是标量x输出是H维的logits self.correlation_net nn.Sequential( nn.Linear(1, 64), nn.ReLU(), nn.Dropout(0.1), # 可选的Dropout防止过拟合 nn.Linear(64, num_meta_embeddings) # 输出H个logits ) def forward(self, x): Args: x (Tensor): 输入的具体特征值形状为 [batch_size, 1] 或 [batch_size]。 Returns: e (Tensor): 生成的Embedding形状为 [batch_size, meta_embedding_dim]。 weights (Tensor): 相关性权重形状为 [batch_size, num_meta_embeddings]。用于可解释性分析。 # 确保输入是二维的 [batch_size, 1] if x.dim() 1: x x.unsqueeze(-1) # [batch_size] - [batch_size, 1] batch_size x.size(0) # 通过相关性网络得到logits logits self.correlation_net(x) # [batch_size, H] # 应用温度系数的softmax得到归一化权重 weights F.softmax(logits / self.temperature, dim-1) # [batch_size, H] # 加权聚合元嵌入 # self.meta_embeddings: [H, d] # weights.unsqueeze(-1): [batch_size, H, 1] # 广播相乘后求和 weighted_meta_embeds self.meta_embeddings.unsqueeze(0) * weights.unsqueeze(-1) # [batch_size, H, d] e weighted_meta_embeds.sum(dim1) # [batch_size, d] return e, weights3.2 构建包含AutoDis的CTR模型现在我们构建一个简单的模型它包含对稀疏特征user_id, item_id使用常规的Embedding层。对连续特征age, expense使用我们刚实现的AutoDis层。一个简单的多层感知机作为预测层。class CTRModelWithAutoDis(nn.Module): def __init__(self, num_users, num_items, embedding_dim, auto_dis_dim, auto_dis_h): super(CTRModelWithAutoDis, self).__init__() self.embedding_dim embedding_dim # 稀疏特征嵌入层 self.user_embedding nn.Embedding(num_users, embedding_dim) self.item_embedding nn.Embedding(num_items, embedding_dim) # AutoDis层处理连续特征 self.autodis_age AutoDis(meta_embedding_dimauto_dis_dim, num_meta_embeddingsauto_dis_h) self.autodis_expense AutoDis(meta_embedding_dimauto_dis_dim, num_meta_embeddingsauto_dis_h) # 假设我们还有两个普通的稠密特征非AutoDis处理比如历史点击数和曝光数 self.dense_fc nn.Linear(2, embedding_dim) # 将2维稠密特征映射到embedding_dim维 # 预测层 # 输入user_emb, item_emb, age_emb, expense_emb, 其他稠密特征emb # 总共 5 * embedding_dim 维 total_emb_dim embedding_dim * 5 self.predictor nn.Sequential( nn.Linear(total_emb_dim, 128), nn.ReLU(), nn.Dropout(0.3), nn.Linear(128, 64), nn.ReLU(), nn.Dropout(0.2), nn.Linear(64, 1), nn.Sigmoid() ) def forward(self, user_id, item_id, age, expense, other_dense_features): # 1. 稀疏特征嵌入 user_emb self.user_embedding(user_id) # [batch_size, embedding_dim] item_emb self.item_embedding(item_id) # [batch_size, embedding_dim] # 2. AutoDis处理连续特征 age_emb, _ self.autodis_age(age) # [batch_size, auto_dis_dim] expense_emb, _ self.autodis_expense(expense) # [batch_size, auto_dis_dim] # 3. 处理其他普通稠密特征 other_dense_emb self.dense_fc(other_dense_features) # [batch_size, embedding_dim] # 4. 拼接所有特征向量 # 注意需要确保auto_dis_dim和embedding_dim一致或者通过一个线性层统一维度。 # 这里假设我们设置 auto_dis_dim embedding_dim concat_emb torch.cat([user_emb, item_emb, age_emb, expense_emb, other_dense_emb], dim1) # 5. 通过预测层得到点击概率 click_prob self.predictor(concat_emb).squeeze(-1) # [batch_size] return click_prob3.3 训练与数据准备在训练时数据的预处理需要特别注意连续特征。import numpy as np from torch.utils.data import Dataset, DataLoader # 假设我们的原始数据 # user_id, item_id, age, expense, other_feat1, other_feat2, label # 其中 age, expense 是原始连续值 class CTRDataset(Dataset): def __init__(self, data_array): self.data data_array def __len__(self): return len(self.data) def __getitem__(self, idx): sample self.data[idx] # 假设数据列顺序为上述 user_id int(sample[0]) item_id int(sample[1]) age float(sample[2]) expense float(sample[3]) other_dense np.array([sample[4], sample[5]], dtypenp.float32) label int(sample[6]) return { user_id: user_id, item_id: item_id, age: age, expense: expense, other_dense: other_dense, label: label } # 关键步骤连续特征归一化 # AutoDis的MLP输入是原始值但数值范围差异过大会影响训练稳定性。 # 建议对 age 和 expense 进行标准化或归一化。 def normalize_features(data): ages data[:, 2].astype(np.float32) expenses data[:, 3].astype(np.float32) age_mean, age_std ages.mean(), ages.std() expense_mean, expense_std expenses.mean(), expenses.std() # 标准化: (x - mean) / std data[:, 2] (ages - age_mean) / (age_std 1e-8) data[:, 3] (expenses - expense_mean) / (expense_std 1e-8) return data, (age_mean, age_std, expense_mean, expense_std) # 加载和预处理数据 raw_data np.loadtxt(your_data.csv, delimiter,) # 示例 normalized_data, norm_stats normalize_features(raw_data) dataset CTRDataset(normalized_data) dataloader DataLoader(dataset, batch_size256, shuffleTrue) # 初始化模型、损失函数、优化器 model CTRModelWithAutoDis(num_users10000, num_items5000, embedding_dim32, auto_dis_dim32, # 设置为和embedding_dim相同 auto_dis_h10) criterion nn.BCELoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) # 训练循环 for epoch in range(10): total_loss 0 for batch in dataloader: user_id batch[user_id] item_id batch[item_id] age batch[age] expense batch[expense] other_dense batch[other_dense] label batch[label].float() optimizer.zero_grad() pred model(user_id, item_id, age, expense, other_dense) loss criterion(pred, label) loss.backward() optimizer.step() total_loss loss.item() print(fEpoch {epoch}, Avg Loss: {total_loss/len(dataloader):.4f})提示在实际工业场景中age和expense的归一化参数均值和标准差需要在训练集上计算并保存下来用于对验证集、测试集以及线上实时请求的数据进行同样的变换保证数据分布的一致性。4. 实战中的调优策略与避坑指南把AutoDis的代码跑通只是第一步。要让它在你的业务数据上真正发挥出超越硬分桶的效果还需要一些实战经验和技巧。下面是我在几个真实项目中应用AutoDis后总结出的关键点。4.1 元嵌入数量H与维度d的选择Hnum_meta_embeddings这个参数不再像分桶数那样敏感。论文和实验表明H在10到20之间通常就能取得很好的效果。设置得太小如5可能限制模型的表达能力设置得太大如50虽然不会像硬分桶那样直接导致过拟合但会增加相关性网络的学习负担可能让权重分布变得过于平均化失去区分度。建议从H10开始作为一个稳定的默认值。如果你的特征非常复杂可以尝试增加到15或20并通过验证集AUC/Bias等指标观察变化。dmeta_embedding_dim这个维度决定了最终连续特征Embedding的表达能力。它应该与你模型中其他稀疏特征的Embedding维度对齐或相当。例如你的user_idEmbedding是32维那么AutoDis的输出也设为32维是合理的。如果维度不匹配可以在拼接所有特征后通过一个线性投影层统一维度。一个经验法则是d的取值可以与你的主要稀疏特征的Embedding维度相同。4.2 相关性网络的结构与温度系数网络结构论文中使用了一个简单的两层MLP。在实践中这个网络不宜过于复杂。一个包含1到2个隐藏层、激活函数为ReLU的MLP已经足够。过于复杂的网络可能会让模型过度关注如何从单个数值x计算权重而忽略了元嵌入本身的学习甚至可能引入过拟合。保持简洁有效是关键。可以加入Dropout如0.1-0.3来增强泛化能力。温度系数Temperature这是控制权重分布“尖锐”或“平滑”的重要超参数。在softmax中温度T越低logits/T输出的概率分布越尖锐接近one-hot温度越高分布越平滑接近均匀分布。如果温度太低权重会接近one-hotAutoDis就退化成了另一种形式的“硬分配”虽然分配是可学习的可能失去软聚合的优势。如果温度太高权重趋于均匀所有元嵌入贡献几乎相同最终Embedding会趋近于所有元嵌入的均值丢失了输入x的特异性信息。建议从T1.0标准softmax开始尝试。如果你发现模型学习缓慢或效果不佳可以尝试稍微调高温度如1.5让训练初期更平滑或者调低温度如0.7让模型做出更“果断”的权重分配。这是一个值得微调的超参数。4.3 连续特征的预处理标准化与非线性变换AutoDis的输入是原始连续值。数据的尺度直接影响相关性网络MLP的学习。标准化/归一化是必须的像“消费金额”这种可能从0到数万的特征如果不做处理巨大的数值范围会导致MLP的梯度不稳定。务必对每个连续特征进行标准化减均值除方差或归一化缩放到[0,1]或[-1,1]区间。这能大幅提升训练速度和模型稳定性。考虑非线性变换对于一些具有特定分布的特征如幂律分布直接对原始值x建模可能效率不高。可以尝试先对x进行非线性变换如log(1x)将大范围的值压缩到一个较小的区间再输入给AutoDis。这相当于给了模型一个更强的先验有时能加速收敛。这是一个特征工程的技巧可以与AutoDis结合使用。4.4 与模型其他部分的协同AutoDis生成的Embedding最终要和其他特征的Embedding拼接在一起送入后续的深度网络。这里有几个细节维度对齐确保AutoDis的输出维度d与其他Embedding维度一致方便拼接。如果不一致可以添加一个nn.Linear层进行投影。梯度流AutoDis的整个通路都是可微的梯度会同时更新元嵌入ME和相关性网络MLP的参数。要关注这两部分参数的学习率是否合适。通常使用统一的优化器如Adam即可它们会自动适应。可视化分析在训练后期可以取出autodis_age和autodis_expense的权重w对一批样本进行统计分析。例如画出不同年龄x对应的权重分布H个权重随x变化的曲线。这能帮你直观理解模型学到了什么样的“软分桶”模式也是一种模型可解释性的体现。4.5 可能遇到的“坑”与解决方案问题训练初期不稳定loss震荡大。排查首先检查连续特征是否做了标准化。其次检查元嵌入ME的初始化。代码中使用的是torch.randn * 0.02这是一个较小的随机初始化。如果问题依旧可以尝试使用Xavier或Kaiming初始化。解决尝试调高softmax的温度系数T如设为2.0让初始权重分布更均匀降低梯度方差。也可以在第一个训练周期使用较大的T然后逐渐退火到1.0。问题模型效果提升不明显甚至略低于精心调参的硬分桶。排查AutoDis的优势在于自动化和对连续性的保持。如果你的硬分桶是业务专家经过大量实验得出的“黄金分桶”那么它本身已经编码了很强的先验知识。AutoDis需要从数据中重新学习这个结构。解决给模型更多的时间和数据。AutoDis的潜力在于其灵活性可能需要在更大的数据集上训练更长时间才能超越强先验的硬分桶。也可以尝试用硬分桶的边界信息来初始化相关性网络例如让MLP初始输出在对应桶的权重更高进行“热启动”。问题线上推理延迟增加。分析相比硬分桶一次查表AutoDis需要做一次小型MLP的前向计算几层全连接和一次加权求和。这会增加一些计算开销。优化这个MLP非常小输入1维输出H维H通常20其计算开销在现代CPU/GPU上几乎可以忽略不计尤其是在批量推理时。如果确实成为瓶颈可以考虑将MLP的计算合并或查找表化但绝大多数场景下无需担心。5. 效果对比与业务场景适配性分析为了更直观地感受AutoDis的价值我们可以在一个公开数据集如Criteo Display Ads上设计一个对比实验。我们构建三个结构相同的DNN模型唯一区别在于处理连续特征的方式模型A基准使用等频分桶比如100个桶然后接Embedding层。模型B改进使用对数变换后等频分桶。模型CAutoDis使用AutoDis层H10,d32输入为标准化后的连续值。在相同的训练集、验证集划分下使用相同的优化器、学习率和迭代轮数进行训练。我们关注两个核心指标验证集AUC衡量排序能力和模型收敛速度达到稳定AUC所需的epoch数。在我的复现实验中通常观察到以下模式初期模型B带手工变换的硬分桶可能因为先验知识强而领先。中期模型CAutoDis开始快速追赶因为它的“软学习”机制能更精细地捕捉特征信息。后期模型C的AUC往往会持平或略微超越模型B0.5%到2%的AUC提升是常见的并且收敛曲线更平滑稳定。模型A简单分桶通常表现最差。业务场景适配性分析AutoDis并非银弹它在以下场景中优势最为明显特征价值密度高需要精细建模如金融风控中的用户收入、信贷评分广告系统中的实时出价bid内容推荐中的视频完播率。这些特征的微小差异可能带来业务指标的显著变化AutoDis的连续性保持能力至关重要。缺乏先验知识或分桶规则复杂对于新兴业务或难以定义清晰阈值的新特征如某种复杂的用户行为指数人工设计分桶规则成本高、效果差。AutoDis的“自动学习”特性可以节省大量特征工程时间。线上特征分布动态变化如果特征的分布随着时间漂移如人均消费水平逐年上涨硬分桶的边界可能需要定期调整。AutoDis的MLP具有一定的外推和自适应能力对分布变化的鲁棒性更强。而在以下场景简单的硬分桶可能更合适特征本身具有明确的业务分段如“用户等级”青铜、白银、黄金这本身就是离散的无需AutoDis。极度追求线上推理效率虽然AutoDis开销很小但在某些超低延迟1ms的极端场景一次查表仍比一次微型MLP计算更有优势。数据量非常小AutoDis相比硬分桶有更多的参数H*d MLP参数。在数据量不足时可能更容易过拟合。此时强先验的硬分桶作为正则化手段可能效果更好。总而言之AutoDis为我们处理深度学习中的连续特征提供了一个强大、优雅且自动化的新选择。它将我们从繁琐且不优雅的“调桶”工作中解放出来把如何从连续值中学习有效表征这个任务交给了模型本身。在实际项目中我建议可以将它作为处理连续特征的默认方案进行尝试并与精心优化的硬分桶方案进行A/B测试让数据来决定最终的选择。它的出现无疑是特征工程迈向自动化、智能化方向上的坚实一步。