我最早做传统机器学习项目的时候,经常被问一句话:"你花那么多时间搞特征工程,为什么不直接上深度学习?"后来我开始大量使用深度神经网络(deep neural networks),又被另一句话追着跑:"特征工程不是很重要吗,怎么到了神经网络这里就全靠网络自己学?"
这两句话的背后,恰好是整个数据科学领域对 Feature Engineering 的两种典型态度:手工构造特征(hand-crafted),以及深度网络自动学习特征。做表格数据、图像、文本序列,都绕不开这个选择题。这篇笔记我想把这两条路线放在一起掰开聊,把各自的原理、适用场景、实操方法和配合方式讲透。如果你正在纠结该把精力放在构造特征还是调网络,或者想搞懂神经网络内部到底在"学什么",这篇应该能给你一个比较完整的坐标系。
1. 先弄清问题:特征工程究竟是给模型"喂饭"还是"做饭"
很多人对特征工程的理解停留在"造变量"这个动作上,但我更愿意把它定义为:把原始数据翻译成模型更容易学、更符合问题先验的形式。这个翻译过程决定了模型能从数据里“看到”什么,也就决定了效果的天花板。
1.1 数据和特征决定效果上限,算法只是逼近上限
业界流传一句话:数据和特征决定了机器学习的上限,模型和算法只是逼近这个上限。这句话不是鸡汤,是大量项目验证过的经验。同样是用户下单预测,如果只给模型一个“注册时间”,它很难学到有意义的规律;但如果我手工构造出“注册到首次购买的间隔”“近30天活跃天数”“午夜时段的操作次数占比”,模型在同样的算法下AUC往往能涨一大截。这不是模型变聪明了,而是特征把信息放到了模型容易抓住的位置。
我用一个生活化类比来解释:模型就像一个被蒙住眼睛的厨师,特征工程是提前把食材切好、腌好、摆到盘子里,模型只需要按顺序下锅,炒出来的菜自然好吃。神经网络这条路则是给厨师一副眼睛,让他自己看食材、自己决定怎么切,但前提是你得有足够多的食材让他练习。
1.2 手工特征与深度网络的两条路线差异
手工特征工程的本质是先验知识驱动。你把业务经验、领域规律显式地编码成特征,交给逻辑回归、GBDT甚至神经网络去学习。它的优点是可控、可解释、小样本下也稳定;缺点是费人力、依赖领域经验、很难覆盖所有可能的模式。
深度神经网络的特征学习则相反,是数据驱动。它把原始数据(像素、文本、用户行为序列)直接丢进网络,通过反向传播自动调整每一层的权重,逐层提取从低级到高级的特征。优点是省人力、能够发现人类容易忽略的复杂交互;缺点是数据量要求高、可解释性差、训练成本大。
这两条路线不是替代关系,而是分工关系。理解这一点,是后面所有方案选型的前提。
2. 手工特征工程的核心操作,逐个拆给你看
手工特征工程并不是“凭感觉造变量”,它有一批经过验证的常规操作,每种操作背后都有明确的动机。下面按特征类型拆开讲。
2.1 数值型特征:变换、裁剪与标准化的分寸
数值型特征最常见的三个操作是对数变换、异常值裁剪和标准化。先说对数变换,它主要应付长尾分布。像用户累计消费金额、点击次数这类字段,往往几个头部用户占用大部分数值量,直接用原始值喂给模型,会让分裂点或距离计算被极值主导。取log后能把量级从百万级压缩到十级以内,分布更接近对称,线性模型和神经网络都会更稳。
我试过对同一份电商数据做下单金额预测,原始金额直接入模时,GBDT也能跑,但残差集中在高消费人群;取对数变换后整体残差更均匀,RMSE降了将近十个点。不过要注意,对数变换不是万能药。对于树模型,单调变换几乎不改变分裂相对顺序,所以提升有限;真正受益的是线性模型、SVM、神经网络这类对数值尺度敏感的模型。
异常值裁剪的逻辑更直接:把超过p99的值强制压到p99。有人觉得“裁剪会丢信息”,但实际上极端异常值在业务上往往是错误数据或一次性事件,保留它对模型是噪声大于信号。裁剪点选的保守一点就行,p95到p99之间都可以,交叉验证里试一轮就有数。
标准化则是神经网络和线性模型的前置条件。把特征缩放到均值0、方差1,可以避免某些特征因为数值范围大而主导梯度更新。注意树模型根本不依赖标准化,所以不需要给GBDT做z-score。如果训练数据量不大,你完全可以把“对数变换只对网络模型做、标准化只对网络模型做、树模型用原始字段”这种差异化成默认流程。
2.2 分箱:给线性模型注入非线性
分箱在传统特征工程里的地位很高,尤其是逻辑回归时代。逻辑回归的决策边界天然是线性的,面对"年龄对购买意愿的影响先升后降"这种倒U型关系,直接拿年龄的原始数值进去学是学不出来的。但把年龄分成[18-25]、[26-35]、[36-45]、[46-60]等几个桶,再对每个桶做独热编码,逻辑回归就能学到分段常数的非线性效应。
分箱有等宽、等频和基于标签的分箱几种方式。等宽分箱实现简单,但遇到偏态分布会出现某些箱子几乎没样本;等频分箱让每个箱子的样本量相同,但边界值可能落在同一个数值上,造成同一个值被拆进不同箱子;有标签时还可以用决策树分箱或卡方分箱,让分箱边界和标签分布对齐,效果通常更好。
我常用的判断标准是:如果模型是树模型,基本不用刻意分箱,树自己就能找到分裂点;如果是逻辑回归或者要强解释性的场景,分箱是标配。分箱数一般在5到15之间,太多会让每个箱子的统计意义变弱,太少又丢精度,用交叉验证选一次就行。
2.3 类别特征编码:从One-Hot到统计编码再到Embedding
类别特征是表格数据里最麻烦的一类。基数低的时候,One-Hot简单直接;但高基数类别比如用户ID、商品ID,One-Hot会直接导致维度爆炸和稀疏矩阵,大多数模型在这种输入下学不到有效信息。
于是有了统计编码。频率编码用类别的出现次数替代类别本身,比如“商品被购买过多少次”,树模型能从频次里学到“热门商品更可能被再次购买”一类的规律。Target Encoding用类别对应目标均值做编码,效果通常不错,但很容易跑出数据泄漏,必须用out-of-fold方式在当前折内统计标签均值,否则验证集得分虚高到没有参考价值。
这里有个值得注意的过渡:Embedding本质上可以理解为神经网络自动完成的高基数类别编码。你把类别ID映射成一个可学习的高维向量,让网络在训练中自己调整向量的方向。这个思路就是深度网络把“类别特征工程”内化的体现——不需要人手动设计相似度规则,网络自己学到“哪些商品在行为上更接近”。
2.4 交互特征与时间窗口:业务直觉的落点
交互特征是把两个或多个字段组合成新特征,最常见的是比值和乘积。点击率CTR=点击/曝光,比单独用点击次数和曝光次数更符合业务直觉,同时天然消除了量纲差异。另一个例子是金额与件数组合成“客单价”,单独看总金额或总件数都有盲区,客单价直接刻画消费层级。
时间窗口聚合是风控和推荐系统里的家常便饭。对于用户行为序列,我会构造“近7天下单笔数”“近30天活跃天数”“距上次点击的间隔”这类特征。核心思路是近期行为比全量历史更有预测力,时间衰减越明显,窗口就要设得越短。实际操作里可以同时跑3天、7天、30天多个窗口,再让模型去选,比拍脑袋定一个窗口要稳。
做交互特征最大的坑是组合爆炸。三个特征能乘出三个二阶组合,十个特征就是四十五个,全造出来既耗时又容易过拟合。我试过用基于树的特征组合筛选,或者直接用GBDT叶子节点做特征映射,让模型自动找有用交互,再把筛选出的交互特征显式构造出来。这种方式既保留人工特征的稳定性,又不浪费自动化筛选的能力。
3. 深度神经网络究竟在"自动学习"什么特征
很多人的误区是,深度神经网络似乎凭空“变出”了特征。实际上它并没有摆脱特征工程,而是把人的精力从“计算特征数值”转移到了“设计网络结构”。这个视角能让很多问题豁然开朗。
3.1 端到端与表示学习:从原始特征到抽象特征
神经网络的每一层都在做特征变换,只是这个变换是自动学出来的。图像识别里,浅层卷积学到边缘、线条,中层学到纹理、形状,深层学到物体部件甚至整体语义。这个过程叫表示学习,深度模型把“原始输入-特征-输出”三阶段整合成了端到端的学习。
在表格数据里也一样。假设你有年龄、收入、历史购买次数三个字段,DNN的第一层可能学到“收入高且购买次数多”这个隐含组合,第二层又把这种组合和年龄做二阶交互。如果按传统手工做法,这个二阶交互特征需要靠人想到、算出来再喂给模型;而DNN在用足够的样本自动逼近它。
但自动学习是有代价的。一是需要的数据量大,否则学出一堆噪声模式;二是不可解释,出了问题很难定位到具体是哪个特征在起作用。我在实际项目中见过不少团队在样本量只有两三万的情况下强上深层模型,效果反而不如GBDT加几个手工特征,原因就在于此。
3.2 网络结构本身就是"人工先验"
CNN适合图像,不是因为“图像就适合CNN”,而是因为CNN结构里内置了局部性和平移等变性两个先验。卷积核只看局部区域,不管物体出现在图像左上角还是右下角,都能提取到同样的局部模式。这个先验不需要人显式计算,而是用网络结构表达出来了。
循环神经网络和Transformer同理。RNN假设序列依赖沿时间方向传播,注意力机制则假设序列里任意两个位置都可能直接相关。这些先验如果换成手工特征工程的做法,对应的是“滑动窗口统计量”“滞后项”“共现矩阵”等操作。只不过深度网络把这些特征放在了结构里,由优化算法自动适配。
Embedding是所有表格数据深度模型里最典型的手工特征替代物。传统做法里你可能费劲计算类别之间的相似度、共现频率,步Embedding把类别映射到向量空间,直接让距离近的类别共享相似表示。本质上这就是一个大规模自动化的类别特征工程模块。
3.3 上DNN不代表可以不做预处理
这是很多人最会误会的点。我见过直接把包含缺失值、异常字符串、不同量纲的原始表丢给PyTorch模型的项目,结果模型训练根本收敛不了,还反过来怪深度学习“不适合表格数据”。实际上,即便深度网络能自动学特征,基本的清洗、缺失值填充、缩放、类别编码仍然必要。
举一个实际例子:对稀疏的类别ID直接做Embedding之前,至少要保证只知道出现频次过低的ID能被映射为统一的UNK向量。对数值型特征,标准化仍然能明显加速收敛。深度网络确实能“自动学”很多交互关系,但对脏数据和坏尺度没有免疫力。用一句话总结:自动特征学习替代的是“特征构造”环节,不是“数据清洗”环节。
4. 落地方案:什么时候手工,什么时候自动,怎么组合
方案选型没有绝对答案,但有可复用的判断标准。我结合一线项目的经验整理了一套评估维度,按表里的思路走,绝大多数场景不会选错。
| 判断维度 | 侧重手工特征(hand-crafted) | 侧重深度网络自动学习 |
|---|---|---|
| 数据规模 | 小样本(千级到数万级)更稳 | 大数据(十万级以上)才有优势 |
| 可解释性 | 强,特征含义明确 | 弱,几乎没有直接解释性 |
| 训练成本 | 低,CPU也能跑 | 高,通常要GPU且调参周期长 |
| 数据形态 | 表格、强业务逻辑字段 | 图像、文本、序列、高维稀疏 |
| 先验依赖 | 高度依赖领域专家 | 更依赖数据和网络结构 |
4.1 选型判断标准与场景对照
如果你处理的是图像或自然语言,不需要犹豫,直接上深度网络,手工特征只做辅助。图像里旋转、裁剪、颜色增强属于数据层面的预处理,不是手工特征。文本里可以用TF-IDF、词频统计作为浅层基线,但Transformer类的模型能自动学出更丰富的语义表示。
如果你处理的是表格数据且样本量不大,手工特征加GBDT几乎是最稳健的起点。很多竞赛里的表格数据冠军方案至今仍然是特征工程加树模型集成,不是深度学习。样本量一旦超过十万且特征之间存在复杂交互,比如推荐系统中的用户行为序列、广告点击日志,深度模型才真正有机会超过树模型。
还有一类重要场景是混合模式:一方面你手工构造一部分强先验特征,另一方面把其他原始数据也喂进网络,让网络自己补全剩余模式。这既保留了业务知识的作用,又给了数据驱动模型发挥的空间。例如在OCR识别任务里,我会额外输入字符宽度、图像对比度、行距这些手工特征给模型;在推荐任务里,把专家构造的用户活跃度特征和序列Embedding拼接后一起输入DNN。
4.2 手工特征与DNN配合的实操流程
我的建议是在项目启动时先搭一个相对完整的方案框架,不要一上来就纠结“到底用哪条路”。一个实用的标准流程是:先做基础清洗和探测性分析,再手工构造一批低风险的基础特征,然后分别训练一个GBDT基线和一个DNN基线,最后选择效果更好的一方或者做融合。
下面这段伪代码是从我负责的一个用户复购预测项目里简化出来的,可以作为落地模板参考:
# 1. 基础清洗:缺失值、异常值、类型修正 df = clean_raw_data(df) # 2. 手工特征块 df["purchase_interval_days"] = (df["last_purchase_date"] - df["first_purchase_date"]).dt.days df["active_ratio_last_30d"] = df["active_days_30d"] / max(df["obs_days"], 1) df["avg_order_value"] = df["total_amount"] / df["order_cnt"].clip(lower=1) # 3. 训练/验证集拆分必须放在特征统计之前 train, valid = train_test_split(df, test_size=0.2, random_state=42) # 4. 手工特征同时支持树模型和DNN X_train_tree = train[hand_features] X_train_dnn = build_nn_input(train, hand_features, embed_cols) # 5. 分别训练后再加权融合 pred = 0.5 * gbdt_model.predict(X_train_tree) + 0.5 * dnn_model.predict(X_train_dnn)这个方案的好处是:树模型作为强基线,能快速判断特征工程做得到不到位;DNN在特征量足够大时有机会超出基线。融合也不止加权平均一种,可以拿树模型的预测概率作为DNN的新增特征,或者反向操作,让两个模型的优势互补。实际操作时我会先跑一个纯手工特征的GBDT,再跑一个纯DNN,最后跑融合版,三档效果对比清晰,复盘时也容易定位问题出在特征还是模型。
5. 常见问题与排查技巧实录
特征工程里的坑,绝大多数不是理论知识不够,而是实操细节没处理干净。下面几条都是我实际踩过、也帮别人排查过的坑。
5.1 特征泄漏:我踩得最重的一个坑
第一次我自己做目标编码的时候,在整份训练集上计算了类别均值,然后把编码后的特征喂给模型。线下验证集的AUC高得惊人,我一度以为找到了制胜特征,结果线上效果一塌糊涂。后来排查原因才意识到,我在划分训练集和验证集之前就已经计算了编码,验证集的信息通过类别均值泄漏到了训练过程里,等于考试前先看到了答案。
正确的做法是确保任何基于标签的统计,包括目标编码、分箱边界、标准化参数,都只能在训练集内拟合,然后应用到验证集。更稳妥的办法是用K折交叉验证做out-of-fold编码,让每折都用其他折的统计信息来生成当前折的特征。这个教训后来被我写进了团队的特征工程规范里,凡是涉及标签的统计量,一律禁止在全量数据上计算。
5.2 编码与变换里的隐蔽问题
高基数类别直接One-Hot是新手最容易踩的坑。我曾经接过一个商品推荐项目,类目ID有二十多万个,直接One-Hot之后特征矩阵稀疏得吓人,GBDT训练时间暴涨,效果还很差。后来改成频率编码加Top-K独热,只给出现次数最多的前几百个类目单独建特征,其余归入"其他"类,训练时间降了一个数量级,效果反而提升了。
另一个隐蔽问题是时序数据里的标准化。金融场景的时间序列如果按全样本标准化,后半个时间段的数据信息会被前半个时间段“偷走”。正确的做法是只用训练期窗口的均值方差去做标准化,线上推理时沿用同一组参数,不能跟着新数据滚动更新。这个问题导致的泄漏不显眼,但一旦出现,回测成绩会失真。
5.3 深度模型下的特征陷阱速查表
| 常见问题 | 典型表现 | 解决办法 |
|---|---|---|
| 原始ID直接做Embedding | 低频ID的向量训练不充分,效果差 | 低频ID统一映射为UNK,或用频次过滤 |
| 数值特征未标准化 | DNN收敛慢,loss震荡 | 训练前用训练集统计量做z-score |
| 高基数类别做One-Hot再进DNN | 维度爆炸,内存溢出 | 换Embedding层或统计编码 |
| 把树模型特征直接喂给DNN | 稀疏度过高,网络学不动 | 先稠密化,或改用DNN适配的特征表示 |
| 手工特征和原始特征同时堆给DNN | 特征尺度不一致,梯度不稳 | 分组处理后拼接输入 |
这张表里的问题我基本都遇到过。尤其是DNN输入那段,手工特征如果已经做过对数变换和分箱,数值范围通常比较温和,而原始数值特征可能动辄几百万,直接拼接会让网络训练早期被大尺度特征主导。解决办法是每种特征单独预处理之后再做拼接,或者给不同特征组配置不同的Embedding维度和归一化方式。
最后聊一个实际体会
折腾了几年特征工程,我最大的体会是:不要把手工特征和深度网络看成对立面,它们在绝大多数项目里是接力关系。手工特征负责把业务先验固化下来,深度网络负责在更广阔的假设空间里做补充搜索。哪怕是纯图像项目,我也会保留一部分手工统计特征作为辅助信号,比如目标尺寸分布、对比度、密集程度,这些信息经常能帮网络少走弯路。
如果你正处在“该不该做手工特征工程”的纠结里,我的建议是先花两天把手边最有业务直觉的特征做出来,哪怕只是最简单的比率和时间窗口,然后跑一个GBDT基线。这个基线一方面给你一个可信的参考点,另一方面能让你对数据的信息量建立体感。之后再决定要不要上DNN,或者怎样把两条路线结合起来,思路会清晰很多。