特征工程:特征提取、特征选择、时间序列特征构造
特征工程这步,说白了就是给原始数据“化妆”——让模型能看清楚规律。我在早期做异常检测时,总以为把原始指标直接扔进模型就完事了,结果预测结果一塌糊涂。后来才明白,特征构造省掉的功夫,都会在后续调试中加倍还回来。下面我按三个核心环节来讲:提取、选择、时序构造。
特征提取:从原始数据里“挖”出信号
特征提取的目的,是把原始时间点上的分散值,转化成能反映系统状态的高层特征。我在监控服务器 CPU 时,如果直接拿 1 秒粒度的 CPU 利用率做特征,模型根本抓不住慢速漂移。通常我会用以下几种方法。
| 方法 | 说明 | 典型场景 |
|---|---|---|
| 滑动窗口统计 | 计算窗口内均值、方差、最大值、最小值、中位数 | 检测均值漂移、方差突变 |
| 频域特征 | FFT 后提取主要频率、能量、功率谱密度 | 周期性异常(如网络流量抖动) |
| 差分与平滑 | 一阶差分消除趋势,移动平均去噪 | 非平稳时序预处理 |
| 自回归系数 | 用 AR 模型系数代表过去对当前的影响 | 有记忆性的传感器数据 |
我的一点经验:窗口大小别拍脑袋。之前做过一个磁盘 I/O 异常检测,窗口设 5 分钟漏报了一堆突发毛刺,后来用经验半周期法,设窗口为正常周期的 1.5 倍才稳定。建议先用领域知识给个初值,再通过网格搜索调整。
import pandas as pd
import numpy as np
def extract_features(series, window=50):
df = pd.DataFrame({'raw': series})
df['mean'] = df['raw'].rolling(window).mean()
df['std'] = df['raw'].rolling(window).std()
df['max'] = df['raw'].rolling(window).max()
df['min'] = df['raw'].rolling(window).min()
df['slope'] = df['raw'].diff()
return df.dropna()
特征选择:别让垃圾特征污染模型
特征不是越多越好。我记得有一次给一个风电齿轮箱做异常检测,手工造了 200 多个特征,模型训练时间爆炸,过拟合还严重。特征选择就是做减法——留下真正有用的。常用的做法分三大类。
- 过滤法(Filter):统计独立性或相关性筛选,例如 Pearson 相关系数、卡方检验、互信息。速度快,但忽略特征间组合效应。
- 包裹法(Wrapper):用目标模型做搜索,例如递归特征消除(RFE)、前向/后向选择。效果准,但计算开销大。
- 嵌入法(Embedded):在模型训练时内置选择,如 L1 正则化(Lasso)、树模型特征重要性。平衡效率与精度。
我曾踩过的坑:在过滤法里直接删除了与标签低相关的特征,结果把季节性弱信号也去掉了。后来改用互信息 + 包裹法混合,先用互信息砍掉一半冗余,再用 RFE 精调,效果好很多。别迷信单一种方法。
| 方法 | 优点 | 缺点 |
|---|---|---|
| 过滤法 | 计算快,适合高维 | 忽略特征交互,可能误删有用组合 |
| 包裹法 | 考虑模型偏好,精度高 | 计算量大,容易过拟合 |
| 嵌入法 | 自动选择,兼顾效率 | 依赖模型假设,解释性不如过滤 |
时间序列特征构造:让模型“看懂”时间依赖
普通特征构造只管数值,时间序列特征还要管顺序和周期。我在做服务器内存泄漏检测时,只用了原始内存使用量,模型死活分不清正常回收和泄漏趋势。后来加了差分和滚动分位数,效果才出来。
滞后特征(Lag Features)
就是拿过去时刻的值当当前特征。比如昨天下午 3 点的 CPU 利用率,可能对今天下午 3 点有参考意义。我用 shift() 生成滞后 1、2、3 步的特征。注意别把未来信息带进去,训练测试要严格对齐。
def add_lag_features(df, col, lags=[1,2,3,6]):
for lag in lags:
df[f'{col}_lag_{lag}'] = df[col].shift(lag)
return df
滚动统计特征
滚动均值、滚动标准差、滚动偏度、滚动极差等。这些特征能捕捉局部趋势和波动性。举个例子:滚动方差突然增大,往往意味着异常开始。
差分与季节分解
一阶差分能消除线性趋势,使序列平稳。对于有强周期的数据(如每日 CPU 负载),我常用 seasonal_decompose 拆出趋势、季节、残差,然后把残差单独做特征。
重点:时间序列特征构造最容易犯的错——窗口数据泄漏。我有一个惨痛教训:用整个时间序列的滚动均值做训练,导致模型学到了未来信息,线上 AUC 暴跌 20%。正确做法:每个时间点的特征只能用该点之前的数据计算,不支持“双向滚动”。
周期性编码
对于小时、星期、月份等周期性变量,如果直接用 0-23 的数字编码,模型会误以为 23 和 0 差很远。我会同时生成 sin 和 cos 两个分量来平滑周期。
def cyclical_encode(hour, period=24):
sin_feat = np.sin(2 * np.pi * hour / period)
cos_feat = np.cos(2 * np.pi * hour / period)
return sin_feat, cos_feat
实践要点:从项目里总结的几条建议
- 先画时间线,再动代码:在异常检测中,特征必须保持时间因果关系。我每次都会先画出特征相对于标签的时间对齐图,确认没有超前数据。
- 特征监控与漂移:部署后特征的分布会变(如均值偏移)。我习惯在特征流水线里加统计校验,一旦 KL 散度超过阈值就告警,提醒重新构造或选择特征。
- 尽量保留可解释性:深层的自动特征提取(如自编码器)效果好但难 debug。初期先用统计特征打底,等到系统稳定了再考虑复杂特征。
一个小习惯:每次添加新特征,我会同步记下它的业务含义和期望效果。半年后再看代码,会发现这些笔记比任何注释都管用。毕竟你我不可能记住每个窗口大小的计算公式,对吧?