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 实战流程总结
好了,咱们把这一章串起来。一个典型的数据采集与预处理流程是这样的:
- 确定采样率:根据信号频率和MCU性能,选一个合适的值
- 采集原始数据:用DMA或中断方式,避免阻塞
- 滤波去噪:先用移动平均快速去噪,如果效果不够再用卡尔曼
- 数据归一化:把数据映射到[0,1]或[-1,1]区间
- 存入缓冲区:准备好给模型推理的数据
嗯,这一章就到这里。下一章咱们会讲特征提取,到时候你会看到,预处理做得好,特征提取事半功倍。