数据预处理与特征工程:从原始信号到可用特征

大家好,我是你们的部署工程师。今天咱们聊聊数据预处理和特征工程。说实话,这部分工作占了我整个项目周期的60%以上。很多人觉得模型才是核心,但我告诉你——数据搞不好,模型再牛也白搭。

我在现场部署时遇到过太多次了:传感器数据哗哗地来,但直接扔进模型,结果一塌糊涂。为什么?因为原始数据太“脏”了。今天我就把这几年的实战经验掰开揉碎讲给你听。

数据清洗:去噪与去异常值

传感器数据,说白了就是一堆带噪声的测量值。温度传感器可能受电磁干扰,加速度计可能因为震动产生毛刺。这些噪声不处理,模型学到的就是噪声模式,而不是真实物理规律。

去噪:滑动窗口平均

我最常用的去噪方法是滑动窗口平均。简单、高效、在嵌入式上跑得飞快。

// C语言实现:滑动窗口平均去噪
#define WINDOW_SIZE 5
float buffer[WINDOW_SIZE] = {0};
int index = 0;

float denoise(float new_sample) {
    buffer[index] = new_sample;
    index = (index + 1) % WINDOW_SIZE;
    
    float sum = 0;
    for (int i = 0; i < WINDOW_SIZE; i++) {
        sum += buffer[i];
    }
    return sum / WINDOW_SIZE;
}

嗯,这里要注意:窗口大小选多少?我一般选3-7。太小去噪效果差,太大信号延迟大。我在一个振动监测项目里试过,窗口选5效果最好,既滤掉了高频噪声,又保留了冲击信号的上升沿。

去异常值:3σ原则

异常值比噪声更可怕。噪声是随机的,异常值可能是传感器故障、通信丢包导致的。我曾经遇到过,一个压力传感器突然跳变到满量程,差点让整个控制系统误动作。

我的做法是:先算均值和标准差,然后剔除超出3σ范围的点。

// 3σ异常值检测
int is_outlier(float value, float mean, float std) {
    float z_score = (value - mean) / std;
    return (z_score > 3.0f || z_score < -3.0f);
}
避坑指南: 我曾经在温度监测项目里直接用3σ剔除,结果发现夏天高温时段的数据全被剔除了。后来我改用滑动窗口的局部3σ,效果就好多了。记住:全局统计对非平稳信号不适用。

时域特征:均值、方差

清洗完数据,接下来就是提取特征。时域特征是最直观的,说白了就是看信号在时间轴上的统计特性。

均值:信号的直流分量

均值代表信号的“中心位置”。比如加速度计静止时,均值就是重力加速度的分量。我习惯用递推公式算均值,省内存。

float mean = 0;
int count = 0;

float update_mean(float x) {
    count++;
    mean = mean + (x - mean) / count;
    return mean;
}

方差:信号的波动程度

方差告诉你信号抖不抖。在故障诊断里,方差突然变大往往意味着设备出问题了。我做过一个电机轴承故障检测,正常运行时方差很小,一旦轴承磨损,方差直接翻倍。

// 递推计算方差(Welford算法)
float mean = 0, M2 = 0;
int count = 0;

void update_variance(float x) {
    count++;
    float delta = x - mean;
    mean += delta / count;
    float delta2 = x - mean;
    M2 += delta * delta2;
}

float get_variance() {
    return M2 / (count - 1);
}
我的经验: 在嵌入式设备上,尽量用递推算法。别一次性存所有数据再算,内存扛不住。我见过有人用数组存1000个点再算方差,结果MCU直接爆了。

频域特征:FFT

时域特征只能看到“信号有多大”,看不到“信号由哪些频率组成”。而很多故障特征恰恰藏在频率里。比如齿轮箱的啮合频率、电机的转频,这些在时域里根本看不出来。

FFT实战要点

FFT(快速傅里叶变换)是把时域信号变到频域的工具。但现场部署时,有几个坑你必须知道。

参数 说明 我的建议
采样率 决定最高分析频率 至少是目标频率的2.56倍
FFT点数 决定频率分辨率 选2的幂次,如256、512
窗函数 减少频谱泄漏 汉宁窗最常用
// 简易FFT实现(基2-FFT,仅作演示)
void fft(float* real, float* imag, int n) {
    // 位反转排序
    for (int i = 1, j = 0; i < n; i++) {
        int bit = n >> 1;
        for (; j & bit; bit >>= 1)
            j ^= bit;
        j ^= bit;
        if (i < j) {
            // 交换real[i]和real[j], imag[i]和imag[j]
        }
    }
    
    // 蝶形运算
    for (int len = 2; len <= n; len <<= 1) {
        float w_real = 1.0f, w_imag = 0.0f;
        float theta = -2 * M_PI / len;
        // ... 完整实现略
    }
}
关键点: 现场部署时,别自己手写FFT。用CMSIS-DSP库或者ARM的DSP库,人家是汇编优化的,比你手写的快10倍。我在STM32F4上测过,256点FFT只要0.3ms。

数据归一化与标准化

最后一步,也是很多人容易忽略的一步。不同传感器量纲不一样——温度可能是0-100℃,压力可能是0-10MPa。如果不归一化,模型会认为数值大的特征更重要,这显然是错的。

归一化:缩放到[0,1]

说白了就是把数据压到0和1之间。公式很简单:x' = (x - min) / (max - min)

// 在线归一化(更新min/max)
float min_val = 1e10, max_val = -1e10;

float normalize(float x) {
    if (x < min_val) min_val = x;
    if (x > max_val) max_val = x;
    return (x - min_val) / (max_val - min_val + 1e-8);
}
注意: 归一化有个大坑——如果新数据超出了之前的min/max范围,结果会大于1或小于0。我建议在部署前先用一批数据确定好min/max,部署后固定不变。或者用滑动窗口的min/max。

标准化:均值为0,方差为1

标准化公式:x' = (x - mean) / std。它不保证数据在某个区间内,但能消除量纲影响。

我个人习惯:如果后续用SVM、神经网络这类对尺度敏感模型,用标准化;如果用决策树、随机森林,其实归一化标准化都行,树模型不关心尺度。

// 标准化(使用预计算的mean和std)
float standardize(float x, float mean, float std) {
    return (x - mean) / (std + 1e-8);
}
我的选择: 在嵌入式部署时,我倾向于用归一化。为什么?因为标准化需要提前算好均值和标准差,而且如果数据分布变化,标准化后的值可能很大。归一化至少保证在[0,1]区间,对定点数运算更友好。

总结一下

数据预处理这条流水线,我建议你这样走:

  1. 原始数据 → 滑动窗口去噪 → 3σ去异常值
  2. 干净数据 → 提取时域特征(均值、方差)
  3. 时域特征 → FFT提取频域特征(幅值谱、功率谱)
  4. 所有特征 → 归一化或标准化 → 送入模型

你想想看,这一套流程走下来,数据质量就有了保障。我在现场部署时,这套流程帮我解决过振动分析、温度预测、压力监测等各种问题。说白了,数据预处理做得好,模型部署就成功了一半。

下一章咱们聊聊模型压缩和量化,那又是另一番天地了。