一、特征工程概述与重要性
我做特征工程这么多年,有个很深的体会:模型的上限往往不取决于你用多深的网络,而取决于你喂进去的特征质量。说白了,再好的算法也救不了垃圾特征——嗯,这句话是我在项目里摔过好几次跟头之后才真正理解的。
1.1 特征工程到底在做什么?
概念上,特征工程就是把原始数据转换成模型能理解的、能抓住问题本质的数值化表示。你想想看,一个深度学习模型只能吃数字,但现实世界的数据可能是文本、图片、时间序列、甚至是一堆空的单元格。特征工程就是那个“翻译器”。
核心公式(我习惯这么记)
模型效果 = 有效信息量 × 模型容量 × 特征表示质量
其中“特征表示质量”是唯一能通过人工干预大幅提升的变量。
模型效果 = 有效信息量 × 模型容量 × 特征表示质量
其中“特征表示质量”是唯一能通过人工干预大幅提升的变量。
我个人习惯把特征工程分成三个层次:
- 原始层:处理缺失值、异常值、数据格式统一——保证数据能吃下去。
- 语义层:提取业务含义,比如从用户购买时间推断“是否工作日”、“是否促销时段”。
- 结构层:构造交叉特征、分桶特征、时序滑窗——让模型能发现复杂模式。
1.2 特征工程对模型性能的影响到底有多大?
直接上案例。我曾经参与过一个点击率预估项目,一开始只用原始特征(用户ID、广告位ID、时间戳),AUC 只有 0.62。后来花了两周时间做了三类特征构建:
- 统计类:用户过去7天对同类广告的点击率
- 交叉类:用户年龄段 × 广告行业类别
- 时序类:用户上一次点击同品类广告距今小时数
模型没换,参数没动,AUC 直接跳到 0.78。你看,特征工程带来的提升比换个网络结构可观得多。
避坑提醒: 我曾经在一个电信欺诈检测项目里,拼命加了几十个衍生特征,结果模型过拟合得一塌糊涂。后来复盘才发现,其中有几个特征包含“未来信息”——比如用当天的通话记录去预测当天的欺诈标签。所以做特征工程时,时间一致性是绝对不能碰的红线。
为什么特征工程这么关键?我总结了几点:
| 因素 | 说明 |
|---|---|
| 特征冗余 | 高维稀疏特征让模型学习效率极低,需要降维或合并 |
| 特征噪音 | 异常点或缺失值会直接拉偏梯度,尤其对基于距离的模型 |
| 特征关系 | 线性模型无法自动捕捉交叉关系,需要人工构造 |
| 特征量纲 | 不同尺度的特征会主导梯度更新方向,必须归一化 |
1.3 特征工程的完整流程
下面这张流程图画的是我这些年摸索出来的标准套路,每一个环节都可能成为瓶颈:
每个步骤背后的坑,我挑几个重点说说:
- 数据清洗:很多项目 70% 的时间花在这里。我曾遇到过一个传感器数据集,50% 的缺失值用均值填充后模型直接崩了——因为缺失不是随机的,是传感器故障导致的偏态分布。后来改用按时间窗口的中位数填充才稳住。
- 特征构建:交叉特征不是越多越好。我的经验是先基于业务逻辑做 5~10 个高潜力的交叉,再通过单特征重要性排序逐步扩充,而不是一上来就做笛卡尔积。
- 特征缩放:对于使用 BatchNorm 的深度网络,理论上可以不做全局缩放,但实际我发现先做 Z-score 标准化有时能让收敛速度提升 3 倍。
实操小技巧: 我经常在特征工程阶段就会跑一个轻量的 LightGBM 基线模型,用它的
gain 重要性快速筛选出 TOP 20 特征,再把这些特征作为后续深度模型的主干输入。这一步往往能帮我砍掉 60% 的冗余特征。
1.4 一个简单的代码示例
下面这个片段展示了我处理时序特征时常用的一个操作——构建“上次行为距离”特征,说白了就是算一算用户当前动作距离上一次同类动作过了多少秒。
import pandas as pd
import numpy as np
def build_time_since_last_event(df, user_col, event_col, time_col):
"""
构建距离上次事件的时间间隔特征
df: DataFrame, 按时间升序排列
user_col: 用户标识列名
event_col: 事件类型列名
time_col: 时间戳列名(datetime)
返回: 添加 'time_since_last' 列的副本
"""
df = df.sort_values([user_col, time_col]).copy()
# 按用户和事件分组,shift(1)取上一次时间
df['last_time'] = df.groupby([user_col, event_col])[time_col].shift(1)
# 计算时间差(秒)
df['time_since_last'] = (df[time_col] - df['last_time']).dt.total_seconds()
# 首次事件没有上一次,填充为一个大常数(例如 999999)
df['time_since_last'] = df['time_since_last'].fillna(999999)
return df.drop(columns=['last_time'])
# 使用示例(伪数据)
# df = build_time_since_last_event(raw_df, 'user_id', 'action_type', 'timestamp')
# 该特征可直接用于特征列,模型能学到"用户频繁操作"的模式
注意: 填充值为 999999 只是一种简单做法。实际项目中我更喜欢用
fillna(df.groupby(user_col)[time_col].transform('median')) 来动态填充,避免引入过大值影响梯度。
嗯,以上是我对特征工程第一章节的理解。记住一句话:好特征是模型的上限,调参只是在贴地板。后面的章节我们会深入到每一个具体环节的 GPU 优化和实战技巧。