3. 数据采集与预处理:采样率设置、滤波去噪、数据归一化

好,咱们进入第三章。说实话,这一章是TinyML项目里最容易被忽视,但也是坑最多的地方。我见过太多人模型调得飞起,结果一上板子就崩,最后发现是数据预处理没做好。嗯,咱们今天就把这块彻底讲透。

3.1 采样率设置:别让数据骗了你

采样率这东西,说白了就是「你每隔多久看一眼传感器」。设得太快,MCU累死;设得太慢,信号细节全丢了。

核心原则:奈奎斯特采样定理

采样率必须大于信号最高频率的2倍。比如你要捕捉100Hz的振动,采样率至少得200Hz。但我个人习惯留3~5倍余量,因为实际信号往往有高频噪声。

实战经验:

我在做可穿戴设备时,采集人体加速度信号。理论上20Hz就够了,但我发现采样率低于50Hz时,走路时的步态峰值经常被漏掉。后来我设成100Hz,效果才稳定。

小技巧:

如果你不确定信号频率,可以先拿示波器看看波形,或者用FFT扫一下。别猜,要测。

常见传感器采样率参考:

传感器类型 典型应用 推荐采样率
加速度计 步态检测 50~100 Hz
陀螺仪 姿态估计 100~200 Hz
麦克风 语音识别 8~16 kHz
温度传感器 环境监测 1~10 Hz

注意:

采样率不是越高越好。MCU的ADC转换需要时间,数据量大了还会撑爆内存。我曾经在STM32上设了1kHz采样,结果DMA缓冲区直接溢出,数据全乱了。

3.2 滤波去噪:让信号干净起来

传感器数据天生带噪。电源纹波、机械振动、电磁干扰……你想想看,原始数据直接扔进模型,那效果能好吗?

3.2.1 移动平均滤波

这是最简单的滤波方法,也是我入行时用的第一个算法。说白了就是取最近N个点的平均值。

// 移动平均滤波 - C语言实现
#define WINDOW_SIZE 5

float buffer[WINDOW_SIZE] = {0};
int index = 0;
float sum = 0;

float moving_average(float new_sample) {
    sum -= buffer[index];          // 去掉最旧的值
    buffer[index] = new_sample;    // 存入新值
    sum += buffer[index];          // 更新总和
    index = (index + 1) % WINDOW_SIZE;
    return sum / WINDOW_SIZE;      // 返回平均值
}

优缺点:

  • 优点:计算量极小,适合MCU
  • 缺点:有延迟,窗口越大延迟越大

我的建议:

窗口大小一般取3~10。我习惯先看数据波形,如果噪声频率高,窗口就大一点;如果信号变化快,窗口就小一点。没有万能参数,得试。

3.2.2 卡尔曼滤波入门

卡尔曼滤波听起来高大上,其实核心思想很简单:用预测值修正测量值。它特别适合处理有噪声的传感器数据,比如IMU。

咱们先看一个一维卡尔曼滤波的例子,用于平滑温度数据:

// 一维卡尔曼滤波 - C语言实现
typedef struct {
    float Q;  // 过程噪声协方差
    float R;  // 测量噪声协方差
    float P;  // 估计误差协方差
    float K;  // 卡尔曼增益
    float X;  // 状态估计值
} KalmanFilter;

void kalman_init(KalmanFilter *kf, float init_value) {
    kf->Q = 0.01;    // 根据经验调整
    kf->R = 0.1;     // 根据传感器手册
    kf->P = 1.0;     // 初始值设大一点
    kf->X = init_value;
}

float kalman_update(KalmanFilter *kf, float measurement) {
    // 预测阶段
    kf->P = kf->P + kf->Q;
    
    // 更新阶段
    kf->K = kf->P / (kf->P + kf->R);
    kf->X = kf->X + kf->K * (measurement - kf->X);
    kf->P = (1 - kf->K) * kf->P;
    
    return kf->X;
}

避坑指南:

我曾经在无人机项目里直接用网上抄的卡尔曼参数,结果姿态估计完全发散。后来才明白,Q和R必须根据实际传感器调。Q越大,滤波器越相信测量值;R越大,滤波器越相信预测值。没有捷径,得一点点试。

什么时候用移动平均,什么时候用卡尔曼?

  • 移动平均:噪声平稳、计算资源紧张时用
  • 卡尔曼滤波:噪声变化、需要实时跟踪时用

3.3 数据归一化:让模型好受一点

归一化,说白了就是把不同量纲的数据拉到同一个尺度。加速度计输出±2g,陀螺仪输出±250°/s,温度输出25°C……你想想看,这些数据直接喂给模型,大数值的特征会主导训练,小数值的特征直接被忽略。

两种常用方法:

方法 公式 适用场景
Min-Max归一化 x' = (x - min) / (max - min) 数据范围已知,如传感器量程
Z-score标准化 x' = (x - μ) / σ 数据分布未知,可能有异常值
// Min-Max归一化 - 以加速度计为例
// 假设量程为 ±2g,即 [-2.0, 2.0]
float normalize_accel(float raw_value) {
    const float MIN_VAL = -2.0;
    const float MAX_VAL = 2.0;
    return (raw_value - MIN_VAL) / (MAX_VAL - MIN_VAL);
    // 输出范围 [0, 1]
}

我的习惯:

在TinyML项目中,我优先用Min-Max归一化。原因很简单:计算量小,而且传感器量程通常是已知的。Z-score需要在线计算均值和方差,对MCU来说有点吃力。

注意:

归一化的参数(min、max、μ、σ)必须在训练集上计算,然后固定下来。测试时直接用这些参数,不能重新算。否则模型在部署时会表现不一致。

3.4 实战流程总结

好了,咱们把这一章串起来。一个典型的数据采集与预处理流程是这样的:

  1. 确定采样率:根据信号频率和MCU性能,选一个合适的值
  2. 采集原始数据:用DMA或中断方式,避免阻塞
  3. 滤波去噪:先用移动平均快速去噪,如果效果不够再用卡尔曼
  4. 数据归一化:把数据映射到[0,1]或[-1,1]区间
  5. 存入缓冲区:准备好给模型推理的数据

嗯,这一章就到这里。下一章咱们会讲特征提取,到时候你会看到,预处理做得好,特征提取事半功倍。