3. 特征工程与特征选择实战
特征工程不是“把数据洗一遍”这么简单,它决定了模型能看到什么信息、以什么尺度看到、以什么形式组合。本节以一个可复现的教学数据集为例,把特征提取、归一化、离散化、PCA 降维和基于模型的重要性排序串成一条可执行的特征管道,并说明每一步的输入、参数、输出以及如何判断结果是否合理。
一、教学对象与原始特征
假设我们有一份设备运行状态数据 deviceData,共 1200 条样本,包含以下字段:
vib_x、vib_y、vib_z:三轴振动加速度均方根值,单位 m/s²,量级约 0.5~8;temp:轴承温度,单位 ℃,量级约 35~95;current:电机电流,单位 A,量级约 2~18;runHours:累计运行小时数,量级约 100~9000;loadLevel:负载等级,取值 low/medium/high;fault:标签,0 表示正常,1 表示异常。
原始特征存在三个典型问题:量纲差异大(runHours 与 vib_x 相差三个数量级)、类别特征无法直接进入数值模型、部分特征之间存在强相关(三轴振动往往同向变化)。特征管道要解决的就是这些问题,并且保证训练集与测试集使用完全一致的变换参数。
二、特征提取:从原始信号到可建模特征
如果原始数据是时序振动信号,而不是已经算好的均方根值,就需要先做特征提取。常用做法是按固定窗口切分,例如窗口长度 1024 点、重叠 50%,对每个窗口计算时域与频域特征:
winLen = 1024; overlap = 0.5;
step = round(winLen * (1 - overlap));
for k = 1:step:(length(sig) - winLen + 1)
seg = sig(k : k + winLen - 1);
feat.rms(k) = rms(seg);
feat.kurt(k) = kurtosis(seg);
feat.p2p(k) = max(seg) - min(seg);
Y = fft(seg .* hann(winLen));
P = abs(Y(1:winLen/2+1)).^2;
f = (0:winLen/2)' * fs / winLen;
feat.bandEnergy(k) = sum(P(f >= 50 & f <= 500));
end
这里 rms 反映能量水平,kurtosis 对冲击成分敏感,p2p 反映峰峰值,bandEnergy 提取 50~500 Hz 频带能量。提取后的特征矩阵行数为窗口数,列数为特征数。判断提取是否合理,可以观察同一工况下特征的稳定性:若某特征方差极大且与标签无明显关系,应考虑剔除或更换窗口参数。
三、归一化与离散化
归一化把不同量纲的特征拉到同一尺度。对近似正态分布的特征用 z-score 标准化:x' = (x - μ) / σ;对存在明显偏态或离群点的特征用 min-max 归一化:x' = (x - min) / (max - min)。关键原则是:μ、σ、min、max 只能从训练集计算,再应用到测试集,否则会造成信息泄漏。
离散化把连续特征转成类别或有序区间。等宽离散化按值域均分,适合分布均匀的特征;等频离散化按分位数切分,适合偏态特征。以 temp 为例,按训练集 25%、50%、75% 分位数切成四段,得到 temp_bin 取值 0~3。离散化的价值在于引入非线性、增强对异常值的鲁棒性,但会损失部分数值信息,因此通常保留原始特征与离散特征并存,由模型自行选择。
四、PCA 降维
三轴振动特征往往高度相关,PCA 可以把它们压缩成少数几个正交主成分。做法是:先对训练集特征标准化,再计算协方差矩阵的特征值,按累计方差贡献率选择主成分个数。例如累计贡献率达到 95% 时保留前 k 个主成分。需要注意:PCA 对尺度敏感,必须先归一化;主成分的物理含义不再直观,解释性会下降,因此常用于降维提速或去相关,而不是用于需要逐特征解释的场景。
五、基于模型的特征重要性排序
训练一个随机森林或梯度提升树后,可以直接读取特征重要性。常用两种口径:
- 不纯度减少:某特征在所有树节点分裂中带来的不纯度下降总和,偏向高基数特征;
- 排列重要性:打乱某特征后观察验证集指标下降幅度,更贴近模型真实依赖,但计算量更大。
实践中建议两者都看:若某特征在不纯度重要性中排名靠前、但排列重要性很低,说明它可能只是被高基数“刷”上去的,应谨慎保留。排序后可按累计重要性保留前 80% 的特征,或直接设定阈值剔除末尾特征。
六、构建特征管道
把上述步骤串成管道,核心是保证“训练集拟合、测试集变换”的一致性。MATLAB 中可用 fitrnet、fitcensemble 配合 normalize、pca 手动组织,也可用 pipeline 思路封装。下面给出一个可执行的结构示例:
% 1. 划分训练/测试集
cv = cvpartition(y, 'HoldOut', 0.3);
XTrain = X(training(cv), :); yTrain = y(training(cv), :);
XTest = X(test(cv), :); yTest = y(test(cv), :);
% 2. 训练集拟合归一化参数
mu = mean(XTrain); sigma = std(XTrain);
XTrainN = (XTrain - mu) ./ sigma;
XTestN = (XTest - mu) ./ sigma; % 用训练集参数
% 3. 训练集拟合 PCA
[coeff, score, latent] = pca(XTrainN);
k = find(cumsum(latent)/sum(latent) >= 0.95, 1);
XTrainP = score(:, 1:k);
XTestP = (XTestN - mean(XTrainN)) * coeff(:, 1:k);
% 4. 训练模型并查看重要性
mdl = fitcensemble(XTrainP, yTrain, 'Method', 'Bag');
imp = predictorImportance(mdl);
[~, idx] = sort(imp, 'descend');
disp(idx(1:min(10, numel(idx))));
管道执行后,应检查三件事:测试集准确率是否与训练集接近(差距过大说明过拟合或泄漏)、PCA 保留的主成分数是否合理、重要性排序是否与领域知识一致。若某特征重要性异常高但业务上不应如此,需要回查归一化参数是否误用了全量数据。
七、结果判断与常见问题
管道跑通后,重点看三个信号。第一,训练集与测试集指标差距。若训练准确率 0.98、测试 0.72,说明模型记住了训练集噪声,应减少特征数或增加正则。第二,PCA 主成分数。若 k 接近原始特征数,说明降维收益有限,可检查特征是否已经高度独立。第三,重要性排序的稳定性。可对训练集做多次重采样,观察同一特征是否稳定排在前列;若排名剧烈波动,说明样本量不足或特征冗余严重。
把上述步骤封装成函数或脚本后,每次更换数据集只需替换输入矩阵和标签,归一化、降维、重要性排序会自动按同一逻辑执行。特征管道的价值不在于单步多复杂,而在于每一步都可复现、可检查、可替换,让特征工程从“凭感觉调”变成“按流程验证”。