☰
结构化数据深度学习实战:Embedding与注意力机制的工程落地
2026/9/30 11:33:51 网站建设 项目流程

这个系列写到现在,前两篇我们聊了结构化数据在深度学习里为什么难搞,也搭了一个最基础的MLP基线。说实话,那个基线在不少场景下是被LightGBM按在地上摩擦的,这是事实。但问题在于,我们之所以还在探索深度学习路线,是因为业务场景里总有一些GBDT啃不动的硬骨头——高基数类别特征、超大规模数据、在线学习、以及需要端到端融合文本或序列信息的需求。这一篇我不打算再炒“深度学习能不能打赢GBDT”的冷饭,而是把重心放到工程落地:当你决定用深度学习建模结构化数据时,模型该怎么设计、特征该怎么喂进去、训练有哪些细节,以及踩过哪些坑之后我才总结出来的稳定方案。

这篇要分享的内容,适合已经会用PyTorch搭基础网络、但面对表格数据总觉得无从下手的同学。同时也适合那些手里有GBDT基线、想在特定业务上继续压指标的团队。我会从特征编码讲起,一直讲到注意力结构在表格数据上的正确用法,最后给出一套可以直接复制的训练配置和排查手册。

1. 结构化数据建模的核心矛盾与破局思路

很多人在表格数据上用深度学习,第一步就走错了。他们把结构化数据当成图像或文本,直接扔进全连接网络里,跑几个epoch发现效果不如XGBoost,于是得出结论“深度学习不适合表格数据”。这个结论太早了。问题在于,结构化数据有两个特性,决定了它不能简单套用通用网络结构。

1.1 表格数据的两个关键特性:类别稀疏与数值非平稳

第一,类别特征的稀疏性。一个“用户ID”可能有一千万个取值,一个“商品类目”可能有几万个取值。如果做One-hot编码,输入维度爆炸,而且绝大多数维度是0,深度学习模型很难在这种极度稀疏的输入上学到有效特征。GBDT这种树模型却天然擅长处理稀疏离散特征,因为它只需要在分裂时遍历有限的类目取值即可。

第二,数值特征的分布不确定性。表格里的数值特征不像图像像素那样范围稳定,同一个字段可能是“年龄”这种近似均匀分布的,也可能是“交易金额”这种长尾到离谱的。更麻烦的是,特征之间往往存在复杂的非线性交互——比如“年龄段”和“城市等级”组合起来才能体现消费能力,单独看任何一个特征都没什么信号。

这两个特性直接决定了模型结构的选择。GBDT在表格数据上的统治力,本质上是它通过贪心分裂自动完成了特征交叉。深度学习想在这个赛道上赢,就得用结构化的方式把“稀疏离散信息”压缩成稠密向量,再通过网络层去捕捉交互。

1.2 深度模型在表格场景下真正有价值的四个方向

在继续往下讲之前,我得先明确一个态度:这个系列不是告诉你“深度学习在所有表格场景都优于GBDT”,而是告诉你“在哪些条件下深度学习值得用”。根据我自己的项目经验,以下四类场景深度学习效果明显:

  • 高基数类别特征主导的数据:比如电商场景下的“用户ID+商品ID”,类别取值几十万上百万,GBDT处理起来内存和时间消耗很大,而Embedding天然适合这种高基数稀疏输入。
  • 超大规模训练数据:树模型在海量数据下训练时间线性增长,而深度学习可以配合分布式训练和GPU加速,效果上限更高。
  • 在线学习或增量更新需求:深度模型可以用增量训练方式快速吸收新数据,GBDT通常需要定时全量重建。
  • 多模态融合:表格数据旁边还带着文本描述、图片、时序序列,这种场景下深度学习可以端到端打通,树模型做不到。

如果你的项目不占其中任何一条,老老实实用GBDT,别折腾。深度学习在这里不是技术信仰问题,是投入产出比问题。

1.3 正确的建模思路:先做特征编码,再谈网络结构

基于上面的分析,我认为在表格数据上用深度学习的正确姿势,不是找什么“终极网络结构”,而是先把特征编码做好。特征编码决定了信息能不能被网络有效读取。文本和图像领域早已证明Embedding的重要性,表格数据同样如此——只不过嵌入的对象是类别ID和分箱后的数值区间。

具体来说,主线方案分三步:

  1. 类别特征全部映射为可学习的Embedding向量;
  2. 数值特征经标准化或分箱后,与类别Embedding向量拼接;
  3. 拼接后的稠密向量输入多层全连接网络,配合LayerNorm、残差和正则化完成训练。

这个思路在业界被称为Entity Embedding,它不是什么新东西,但绝大多数团队在实际落地时并没有把细节做到位。接下来我详细拆每一步。

2. 特征嵌入的细节:从One-hot到Embedding的完整演进

2.1 为什么One-hot在深度学习里是死路

先说个简单的类比。One-hot相当于给每个类别发一个独享的“开关”,开关数量等于类别数量。一万个类目就是一万个开关,但每个样本只打开其中几个。神经网络在反向传播时,没被激活的开关对应的权重根本得不到更新,大量参数在训练中处于“沉睡”状态。这既浪费参数,又容易让模型在尾类别上学不到东西。

Embedding的做法完全不同。它给每个类别分配一个低维稠密向量(比如64维或128维),向量里的每个维度都是可学习的。两个类别之间的相似性,会通过向量内积体现出来。这相当于把“ID编号”变成了“语义空间中的坐标”,模型可以从坐标中推断出类别的隐含属性。实测中,Embedding对高基数类别的泛化能力提升是断崖式的。

注意:Embedding并不是只能用在类别特征上。数值特征经过离散化分箱后,也可以把“箱号”当成类别ID再做Embedding,这种方式能够捕捉数值特征的非线性分布。

2.2 Embedding维度的经验法则与选择逻辑

Embedding维度选多少是个高频问题。行业里流传的经验法是 min(16, (类别数+1) // 2),意思是类别数量少的时候维度取小值,类别数量上去了维度也最多到16就封顶。这个公式在实践中对中小数据集基本够用,但我个人建议在高基数场景下放宽到32或64。

原因在于,Embedding维度本质上决定了每个类别向量的表达能力。一级类目几百个取值,16维足够表达类目间的差异;但用户ID这种百万级取值的场景,16维能承载的信息就太少了,模型很难通过维度间的交互学到精细的用户偏好。不过维度也不是越大越好,过大会让Embedding表本身变成一个巨大的参数矩阵,在小数据集上会严重过拟合。

我自己的经验公式是:

  • 类别数 < 100:维度取8或16;
  • 类别数 100 ~ 10000:维度取32;
  • 类别数 > 10000:维度取64,并在训练中配合Embedding Dropout防止过拟合。

2.3 数值特征的处理方案:标准化与分箱Embedding的组合

数值特征在表格数据里的占比通常很高。把数值特征直接拼到Embedding向量后面,是最常见的做法,但这里有几个关键细节。

第一,数值特征必须先做标准化。原始数值的尺度差异过大会导致网络初期梯度不稳定。Z-score标准化(减均值除标准差)是最稳妥的选择,如果特征严重偏态,建议先做log变换再标准化。偏态特征直接标准化,会把绝大多数样本压到很窄的区间里,模型很难区分差异。

第二,数值特征可以考虑分箱Embedding。做法是把连续值切成若干区间,每个区间分配一个Embedding向量,然后与原始数值一起喂入网络。这相当于给模型同时提供“离散语义”(哪个区间)和“连续精确值”(具体数值)。在金融风控这类特征非线性极强的场景里,这个组合效果非常明显。

实操建议:不要把所有数值特征都做分箱,先做特征重要性筛选,只对Top K个特征进行分箱Embedding。全量分箱会让输入维度膨胀,训练速度明显下降,收益却不成比例。

3. 网络主体设计:残差、归一化与激活函数的正确组合

3.1 全连接层堆叠之外,还需要残差和LayerNorm

早期表格数据深度模型就是简单堆叠全连接层。但结构化数据的特征并不像图像那样有天然的局部相关性,网络一深,梯度消失、协变量偏移的问题立刻暴露。残差连接和LayerNorm是解决这两个问题的核心组件。

Residual连接让梯度可以在反向传播时“抄近路”直达浅层,避免深层网络训练初期更新缓慢。LayerNorm在表格数据上尤其重要——它按特征维度做归一化,能够缓解内部协变量偏移,让我在调学习率时不用战战兢兢。我见过不少团队在表格模型里不加归一化,然后把学习率调到1e-4还震荡,最后怀疑人生。加了LayerNorm之后,1e-3的学习率都能稳定跑。

3.2 激活函数的选择:ReLU之外的可选项

在表格数据上,ReLU依然是最稳的起点,但它有一个问题:负区间梯度恒为0,会导致部分神经元死亡。对于结构化数据这种特征维度不算高、但每个维度都承载着业务含义的场景,神经元死亡意味着某些特征的梯度永远消失,模型能力受影响。

我实测下来,SELU和GELU在表格数据上表现更好。SELU自带归一化特性,在“自归一化网络”结构里不需要额外加BatchNorm;GELU在Transformer相关结构中常用,与残差和LayerNorm搭配很和谐。如果你不想纠结,默认选GELU,调参空间更大。

3.3 经典基线模型结构示例(PyTorch伪代码)

下面这个结构是我常用的基线模型,包含了Embedding、数值特征拼接、多层全连接、残差与LayerNorm。代码不复杂,但结构上每个组件都有它存在的理由。

import torch import torch.nn as nn class EmbeddingModel(nn.Module): def __init__(self, cat_dims, num_dim, embed_dim=32, hidden_dims=[256, 128, 64]): super().__init__() self.embeddings = nn.ModuleList([ nn.Embedding(dim, min(embed_dim, (dim + 1) // 2)) for dim in cat_dims ]) self.emb_dropout = nn.Dropout(0.1) total_dim = sum(e.embedding_dim for e in self.embeddings) + num_dim layers = [] prev_dim = total_dim for h in hidden_dims: layers.append(nn.Linear(prev_dim, h)) layers.append(nn.LayerNorm(h)) layers.append(nn.GELU()) layers.append(nn.Dropout(0.2)) prev_dim = h layers.append(nn.Linear(prev_dim, 1)) self.mlp = nn.Sequential(*layers) def forward(self, x_cat, x_num): embs = [emb(x_cat[:, i]) for i, emb in enumerate(self.embeddings)] emb = torch.cat(embs, dim=1) emb = self.emb_dropout(emb) x = torch.cat([emb, x_num], dim=1) return self.mlp(x).squeeze(-1)

这段代码只是基线,但它能覆盖70%以上的结构化数据场景。在此基础上,下一节我再讲如何引入注意力机制,进一步提升特征交互能力。

4. 引入注意力机制:TabTransformer的实践与边界

注意力机制在结构化数据上不是万能神药,但在某些场景下确实能带来明显的收益。这一节我把TabTransformer和FT-Transformer的思路讲清楚,再告诉你什么情况下上它才划算。

4.1 TabTransformer的结构与动机

TabTransformer的出发点很简单:类别特征经过Embedding后,通过多层Transformer Encoder进行交互,再与数值特征拼接,最后送入全连接层做预测。这么做的好处是,Transformer的Self-Attention可以显式建模“类别特征之间”和“类别与上下文之间”的依赖关系。

在电商和广告场景中,类别特征之间确实存在非常强的交互。比如“用户所在城市”与“用户偏好类目”的组合,单独的Embedding向量无法直接表达这种交叉关系,但Attention机制可以通过Query-Key的匹配动态地加权不同特征的贡献。这在GBDT里相当于“自动寻找高阶交叉”。

4.2 什么场景收益最大,什么场景别折腾

TabTransformer有没有收益,很大程度上取决于数据里类别特征的比例和交互强度。我总结出两个判断维度:

  • 类别特征占主导、且类别间交互明显:收益最大。比如“用户ID+广告位ID+素材ID”这种纯ID特征组合的CTR预估场景。
  • 数值特征占主导、类别特征是少量辅助维度:收益有限。此时Transformer层主要在处理稀疏的类别交互,对数值特征的拟合不如直接全连接来得高效。

如果项目属于后者,我建议不要上Transformer,把精力花在数值特征的分箱Embedding上,效果可能更好。

4.3 FT-Transformer的补充思路

FT-Transformer(Feature Tokenizer Transformer)在TabTransformer的基础上做了改进:它把所有特征(包括数值特征)都“Token化”。数值特征不再是直接拼一个标量,而是通过一个线性层或分箱Embedding映射成一个向量,然后所有特征向量一起过Transformer。

这个思路理论上是完备的,但代价是参数量大幅上升,训练时间也随之增加。在我自己的实验中,FT-Transformer在中等规模数据上比TabTransformer没明显优势,反而更容易过拟合。所以如果团队训练资源有限,我建议先用TabTransformer,看Attention有没有实际收益再考虑升级。

5. 实操全流程:从数据到指标对比的一体化配置

代码和结构只是模型的一部分,真正决定效果的还有数据处理和训练配置。这一节我按自己的实操习惯,完整走一遍流程,并给出可直接使用的默认参数组合。

5.1 数据预处理细节:频次截断与缺失值填充

类别特征处理上,我强烈建议做低频截断。把出现次数少于阈值(比如5次或10次)的类别统一替换为<UNK>,再参与Embedding。否则那些只出现过几次的ID会得到非常不稳定的Embedding向量,训练时它们自身权重更新极少,预测时却在测试集上频繁出现,导致线上效果飘忽。

数值特征缺失值用中位数填充比均值更稳,因为数值特征往往是长尾分布,均值会被极端值带偏。填充后统一做标准化,再决定哪些特征需要额外做分箱Embedding。

5.2 训练配置的推荐组合

我调试过大量表格深度模型后,攒了一套相对稳定的默认配置。不要盲目追求大batch和超大模型,表格数据性能曲线和图像不一样。

配置项推荐值说明
优化器AdamW比Adam多了权重衰减解耦,配合正则化效果更好
学习率1e-3配合LayerNorm可以稳定收敛;若震荡则降至5e-4
Batch Size256或512表格数据不需要像视觉那样动辄上千
轮数30~60配合早停,用小验证集监控过拟合
Dropout0.2~0.3隐藏层;Embedding层另用0.1的Embedding Dropout
权重衰减1e-5 ~ 1e-4主要为了防止高维Embedding参数过拟合
学习率调度Cosine退火后期平稳,配合SWA效果更好

训练过程中我会记录验证集AUC和Loss两个指标。AUC适合看排序能力,Loss适合看拟合程度。Loss下不去但AUC在涨,说明模型在学排序;两个都不动,就该检查特征编码是不是有问题。

5.3 二阶段训练技巧:类别特征先学,还是全量一起训

一个容易被忽略但很实用的技巧是二阶段训练。第一阶段先冻结全连接层,只训练Embedding层和数值特征编码器,跑几个epoch;第二阶段再解锁所有参数联合训练。

这么做的好处是,Embedding层在第一阶段能快速从随机初始化状态稳定下来,避免一开始就被全连接层的随机梯度带着乱跑。尤其在高基数类别场景下,Embedding参数占据网络参数的大部分,如果一开始就和主网络一起大规模更新,训练初期的震荡会让Embedding学到非常“脏”的向量。二阶段训练之后,模型收敛速度明显加快,最终指标通常也有小幅提升。

5.4 实测对比:GBDT、MLP与Embedding+Attention方案

为了直观展示效果差异,我给出一组基于公开数据集(如Porto Seguro车险预测及电商用户行为预测)的典型对比数据。数据量在百万级、类别特征占比约40%、数值特征60%:

方案AUC训练耗时备注
LightGBM(调优后)0.762约30分钟(CPU)强基线
普通MLP(One-hot+标准化)0.743约20分钟(GPU)基线较弱
Embedding+MLP0.774约40分钟(GPU)超过GBDT
TabTransformer0.778约2小时(GPU)类别交互收益有限
FT-Transformer0.779约4小时(GPU)收益与成本不成比例

这组数据不是精确复现,但趋势非常稳定。Embedding+MLP往往已经能超过GBDT,而Transformer类结构的增量收益和成本需要团队自己掂量。

6. 常见问题与排查技巧实录

这部分是我在实际项目中反复遇到的坑,单独拎出来当速查表用。每条都是我踩过的、或帮别人排查代码时遇到的真实问题。

6.1 Embedding维度到底怎么定

不要固定用16,要根据类别数量取经验值,参考我在前面给的公式。如果验证集Loss在训练早期就不降,把Embedding维度减半试试。如果过拟合严重,除了增大Dropout,另一个直接有效的做法是削减Embedding维度——它直接降低参数总量。

6.2 训练Loss震荡不收敛

先确认LayerNorm有没有加。表格数据模型没有归一化层,大概率在训练到中途开始震荡。LayerNorm加上之后,再看学习率是不是超过1e-3。如果仍然震荡,把学习率降到5e-4,并把Batch Size从256提到512。反过来,如果收敛速度慢,先提学习率而不是加大模型。

6.3 数值特征标准化后效果反而变差

某些业务特征本质上不是线性的,比如“距上次登录天数”这种特征,0天和1天的差异远比100天和101天的差异重要。这种情况下纯标准化会破坏语义。试着把特征先做log变换再标准化,或者直接走分箱Embedding通道,让网络自己学习分箱内的非线性关系。

6.4 类别特征基数过大导致显存爆掉

类别Embedding表的参数量等于“类别数乘维度”,几百万用户ID配上64维,参数轻松上亿。这时有两个选择:一是降维度到32甚至16;二是把高频ID单独映射,低频统一归入<UNK>,从源头缩减类别数。不要硬扛,嵌入式设备的显存经不起这种浪费。

6.5 测试集出现训练时没见过的类别

这是一个非常隐蔽但后果很严重的坑。线上预测时,新用户、新商品会产生未知ID,模型只能随机初始化或者置零。规避方式有两个:训练时留出<UNK>槽位,任何新ID都映射到它;或者对类别特征增加一层“哈希分桶”,先把大基数压缩到固定桶数再Embedding。

6.6 二阶段训练后Embedding向量仍不稳定

如果二阶段后Embedding向量变化依然剧烈,检查是否使用了过大的学习率。Embedding的梯度更新是稀疏的——只有当前Batch内出现的类别会被更新,其余类别的向量原地不动。学习率过大会让那些偶现类别剧烈波动。我的做法是把Embedding参数单独挂一个稍小的学习率,比如主网络1e-3,Embedding用5e-4。

7. 这个系列还能往哪个方向扩展

我先把话放这儿:结构化数据深度学习这个方向,真正值得探索的不是“继续堆网络层数”,而是怎么把业务先验注入模型。比如在Embedding向量上直接拼接人工规则特征,比如把预训练模型产出的向量作为额外Embedding输入。我自己正在尝试的方向,是把LLM对类别特征的语义理解蒸馏成Embedding,再喂进表格模型——这类跨模态融合,才是深度学习相对GBDT的长期优势所在。

如果你按这篇文章搭出了自己的基线模型,我建议你第一件事不是调参,而是把训练好的Embedding向量可视化出来,看看相近的类别是不是被映射到了邻近区域。这一步能帮你快速判断模型学到的语义是否符合业务直觉,比盯着AUC死磕更有价值。后续我会在这个系列里继续更新关于结构化数据的预训练、增量学习与部署实践,欢迎持续关注。

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

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

立即咨询