4. 数据预处理与特征工程:滑动窗口滤波、归一化、FFT特征提取、时域/频域特征选择

传感器数据进了ADC,拿到了原始值,然后呢?直接扔给模型?

别急。我见过太多新手把原始ADC值直接塞进神经网络,结果模型要么不收敛,要么推理结果飘忽不定。说白了,传感器数据是脏的、乱的、带噪声的。你得先把它收拾干净,再提炼出有用的特征,模型才能好好干活。

这一章,我们就来聊聊数据预处理和特征工程。这是从原始数据到有效特征的关键一步,也是嵌入式AI部署中最容易被低估的环节。

核心观点: 好的特征工程,比调模型结构更能提升推理效果。在资源受限的MCU上,这一点尤其明显。
数据预处理与特征工程流程 原始ADC数据 滑动窗口滤波 归一化 特征提取 时域特征 频域特征 特征选择 → 模型输入 关键:保留有效信息 去除冗余与噪声

4.1 滑动窗口滤波:把毛刺磨平

传感器数据最烦人的是什么?噪声。尤其是加速度计、陀螺仪这类MEMS传感器,手一抖,数据就跳得跟心电图似的。

滑动窗口滤波,说白了就是取最近N个点的平均值,代替当前点的值。简单、高效、适合MCU。

我的经验: 窗口大小选多少?我一般从N=5开始试。太小了滤不掉噪声,太大了信号延迟严重。对于100Hz的采样率,N=5到N=15是比较常见的范围。

来看代码实现。这是我在STM32上实际跑过的版本:

// 滑动窗口滤波 - 环形缓冲区实现
#define WINDOW_SIZE 10

typedef struct {
    float buffer[WINDOW_SIZE];
    uint8_t index;
    float sum;
    uint8_t count;
} SlidingWindowFilter;

void filter_init(SlidingWindowFilter *f) {
    f->index = 0;
    f->sum = 0.0f;
    f->count = 0;
    memset(f->buffer, 0, sizeof(f->buffer));
}

float filter_update(SlidingWindowFilter *f, float new_sample) {
    // 减去最旧的值
    f->sum -= f->buffer[f->index];
    // 存入新值
    f->buffer[f->index] = new_sample;
    f->sum += new_sample;
    f->index = (f->index + 1) % WINDOW_SIZE;
    if (f->count < WINDOW_SIZE) f->count++;
    // 返回平均值
    return f->sum / f->count;
}

为什么用环形缓冲区?因为每次移动窗口只需要做一次加法和一次减法,计算量恒定。你想想看,如果每次重新求和,窗口越大计算量越大,MCU扛不住的。

注意: 滑动窗口滤波会引入相位延迟。延迟量 = (N-1)/2 个采样点。如果你的应用对实时性要求极高(比如电机控制),窗口别太大。

4.2 归一化:把数据拉到同一个尺度

归一化,就是把不同量纲的数据映射到同一个范围。比如加速度计输出是±2g,温度传感器输出是0-100°C,你不归一化,模型就会偏向数值大的特征。

常用的归一化方法有两种:

方法 公式 适用场景 MCU开销
Min-Max归一化 x' = (x - min) / (max - min) 数据范围已知且固定 低(只需乘加)
Z-score标准化 x' = (x - μ) / σ 数据分布未知,有异常值 中(需预存μ和σ)

在嵌入式场景下,我强烈推荐Min-Max归一化。为什么?因为传感器的量程是固定的,min和max可以预先算好,存成常量。运行时只需要一次减法和一次乘法,连除法都可以优化成移位。

// Min-Max归一化 - 定点数优化版本
// 假设加速度计范围 ±2g,输出 0-4095 (12位ADC)
#define ACCEL_MIN 0
#define ACCEL_MAX 4095
#define SCALE_FACTOR (1.0f / (ACCEL_MAX - ACCEL_MIN))

float normalize_accel(uint16_t raw) {
    // 归一化到 [0, 1]
    return (raw - ACCEL_MIN) * SCALE_FACTOR;
}

// 如果需要归一化到 [-1, 1]
float normalize_accel_signed(uint16_t raw) {
    float normalized = (raw - ACCEL_MIN) * SCALE_FACTOR;  // [0, 1]
    return normalized * 2.0f - 1.0f;                      // [-1, 1]
}
关键点: 归一化的参数(min、max、μ、σ)必须在训练集上计算,然后固定下来。推理时直接用这些参数,不要再重新计算。我曾经见过有人每次开机都重新算归一化参数,结果模型表现时好时坏——因为每次的数据分布不一样。

4.3 FFT特征提取:看到时域看不到的东西

有些特征在时域里是看不出来的。比如一个振动信号,时域上看就是一堆上下跳动的点,但转到频域,你就能看到它在50Hz和120Hz有峰值——这可能是工频干扰和机械共振。

FFT(快速傅里叶变换)就是把信号从时域变换到频域的工具。在STM32上做FFT,我一般用CMSIS-DSP库,它针对Cortex-M内核做了优化,比你自己手写的快得多。

我的习惯: FFT点数选多少?我通常选256或512。点数太少频率分辨率不够,点数太多MCU算不过来。对于1024点FFT,STM32F4大概要花几毫秒,还能接受。

CMSIS-DSP库的FFT使用示例:

#include "arm_math.h"

#define FFT_SIZE 256
#define SAMPLING_RATE 1000.0f  // 1kHz采样率

// FFT输入输出缓冲区
float32_t input[FFT_SIZE * 2];  // 实部+虚部交错存储
float32_t output[FFT_SIZE / 2]; // 幅值谱

void compute_fft(float32_t *time_domain_data) {
    // 1. 填充输入缓冲区(实部放数据,虚部清零)
    for (int i = 0; i < FFT_SIZE; i++) {
        input[2 * i] = time_domain_data[i];      // 实部
        input[2 * i + 1] = 0.0f;                 // 虚部
    }
    
    // 2. 执行FFT
    arm_cfft_f32(&arm_cfft_sR_f32_len256, input, 0, 1);
    
    // 3. 计算幅值谱
    arm_cmplx_mag_f32(input, output, FFT_SIZE / 2);
    
    // 4. 归一化(幅值除以N/2)
    for (int i = 0; i < FFT_SIZE / 2; i++) {
        output[i] /= (FFT_SIZE / 2);
    }
}

// 获取某个频率对应的幅值
float get_frequency_magnitude(float freq) {
    int bin = (int)(freq * FFT_SIZE / SAMPLING_RATE);
    if (bin >= FFT_SIZE / 2) return 0.0f;
    return output[bin];
}

从FFT结果中,我们可以提取哪些特征?

  • 频谱峰值:最大幅值对应的频率
  • 频谱质心:能量集中的频率区域
  • 频带能量:特定频段(如低频、中频、高频)的能量和
  • 频谱平坦度:信号是集中在几个频率还是分布广泛
避坑指南: 我曾经在做一个电机故障诊断项目时,直接拿原始FFT幅值谱的256个点作为特征输入模型。结果模型在STM32上推理一次要200多毫秒,根本跑不动。后来我把256个点压缩成8个频带能量特征,推理时间降到5毫秒,准确率只掉了2%。特征不是越多越好,够用就行。

4.4 时域/频域特征选择:去粗取精

特征提取完了,你可能得到几十甚至上百个特征。但MCU的资源有限,不能全用。特征选择就是从中挑出最有用的几个。

常用的时域特征:

特征 计算公式 物理含义
均值 μ = (1/N) Σx_i 信号的直流分量
方差 σ² = (1/N) Σ(x_i - μ)² 信号的波动程度
均方根 RMS = sqrt((1/N) Σx_i²) 信号的有效能量
峰值因子 Crest = peak / RMS 信号是否有冲击
峭度 Kurtosis = (1/N) Σ((x_i - μ)/σ)⁴ 信号分布的尖锐程度

常用的频域特征:

特征 计算方法 适用场景
频谱峰值频率 幅值最大处对应的频率 旋转机械故障诊断
频带能量比 某频段能量 / 总能量 区分不同运行模式
频谱熵 -Σ(p_i * log(p_i)),p_i为归一化幅值 信号复杂度评估

特征选择的方法,我推荐两种适合MCU的:

  1. 方差过滤法:计算每个特征的方差,方差太小的特征直接扔掉(因为所有样本都差不多,没有区分度)。
  2. 相关性分析法:计算特征与目标变量之间的相关系数,只保留相关性高的特征。
我的建议: 在PC上用Python做特征选择,把选好的特征列表固化到C代码里。不要在MCU上跑特征选择算法,那太奢侈了。PC上算好,MCU上直接用,这是嵌入式AI的黄金法则。

最终,你的特征向量可能只有5-10个维度。别嫌少,我做过一个手势识别项目,只用6个特征(3个时域+3个频域),在STM32F103上跑KNN,准确率达到了94%。

嗯,数据预处理和特征工程就聊到这里。记住一句话:垃圾进,垃圾出。花时间把数据收拾干净、把特征选好,后面的模型训练和部署会轻松很多。