第四章:输入数据抗干扰——传感器数据滤波与鲁棒性增强
各位同学,咱们接着往下聊。上一章讲了模型本身的防护,这一章咱们把目光投向数据入口——传感器。说实话,我在嵌入式AI项目里踩过的坑,有一半都出在传感器数据上。你模型训练得再好,推理代码写得再漂亮,传感器给你喂一堆毛刺数据,那结果就是灾难。
我有个习惯:做AI部署之前,先花一周时间把传感器数据摸透。为什么?因为STM32的资源有限,你不可能在芯片上跑复杂的去噪算法。所以,数据抗干扰的第一步,是在数据进入模型之前,把脏东西过滤掉。
4.1 传感器数据滤波算法
滤波算法有很多,但在STM32上,我常用的就两种:中值滤波和卡尔曼滤波。别贪多,把这两个吃透,能解决90%的传感器噪声问题。
4.1.1 中值滤波——简单粗暴但有效
中值滤波的原理很简单:取连续N个采样值,排序后取中间那个。它对付脉冲噪声特别管用。比如你用一个超声波传感器测距,偶尔会蹦出一个离谱的值——明明前面是墙,它给你报个10米。这种就是典型的脉冲噪声。
我在项目里遇到过:一个扫地机器人的避障模块,超声波数据经常跳变。一开始我用均值滤波,结果发现均值滤波会把异常值“平均”到正常值里,导致避障反应迟钝。换成中值滤波后,问题立刻解决。
核心要点:中值滤波的窗口大小很关键。窗口太小(比如3个点),滤不掉噪声;窗口太大(比如15个点),延迟太高。我个人建议:对于10Hz的传感器,窗口取5~7个点比较合适。
// STM32上的中值滤波实现(窗口大小5)
#define MEDIAN_WINDOW_SIZE 5
float median_filter(float new_sample) {
static float buffer[MEDIAN_WINDOW_SIZE] = {0};
static uint8_t index = 0;
float temp[MEDIAN_WINDOW_SIZE];
float median;
// 更新环形缓冲区
buffer[index] = new_sample;
index = (index + 1) % MEDIAN_WINDOW_SIZE;
// 拷贝到临时数组并排序
memcpy(temp, buffer, sizeof(temp));
// 简单的冒泡排序(窗口小,够用)
for(int i = 0; i < MEDIAN_WINDOW_SIZE - 1; i++) {
for(int j = 0; j < MEDIAN_WINDOW_SIZE - 1 - i; j++) {
if(temp[j] > temp[j+1]) {
float t = temp[j];
temp[j] = temp[j+1];
temp[j+1] = t;
}
}
}
// 取中值
median = temp[MEDIAN_WINDOW_SIZE / 2];
return median;
}
小技巧:如果你对实时性要求高,可以用“快速中值滤波”算法——维护一个有序链表,每次插入新值、删除旧值。不过对于STM32F4以上的芯片,5个点的冒泡排序完全够用,别过度优化。
4.1.2 卡尔曼滤波——状态估计的利器
卡尔曼滤波听起来高大上,其实说白了就是:用上一刻的预测值 + 当前测量值,加权平均得到最优估计。它特别适合处理高斯噪声,比如加速度计、陀螺仪的数据。
我记得第一次在STM32上实现卡尔曼滤波时,被那些矩阵运算搞得头大。后来发现,对于一维传感器(比如温度、距离),卡尔曼滤波可以简化成几个公式,代码量很少。
// 一维卡尔曼滤波实现
typedef struct {
float Q; // 过程噪声协方差
float R; // 测量噪声协方差
float x; // 估计值
float P; // 估计误差协方差
float K; // 卡尔曼增益
} KalmanFilter_t;
void kalman_init(KalmanFilter_t* kf, float init_value) {
kf->Q = 0.01f; // 根据传感器特性调整
kf->R = 0.1f; // 根据传感器数据手册调整
kf->x = init_value;
kf->P = 1.0f;
}
float kalman_update(KalmanFilter_t* 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越大,滤波器越相信预测值。我一般先用传感器数据手册里的噪声方差作为R的初始值,然后现场微调。
4.2 异常值检测与剔除
滤波能处理噪声,但处理不了“坏数据”。比如传感器断线了,输出一个固定值;或者受到强电磁干扰,数据直接飞了。这时候就需要异常值检测。
我常用的方法有两种:3σ原则和滑动窗口阈值法。
4.2.1 3σ原则——基于统计的检测
假设传感器数据服从正态分布,那么99.7%的数据会落在均值±3σ范围内。超出这个范围的数据,大概率是异常值。
嗯,这里要注意:3σ原则要求你预先知道数据的均值和标准差。对于静态场景(比如固定位置的温度监测),你可以离线统计。但对于动态场景(比如运动中的加速度),均值和标准差是变化的,需要在线更新。
// 在线3σ异常检测
typedef struct {
float mean;
float std;
uint32_t count;
float m2; // 用于计算方差的中间变量
} OnlineStats_t;
void stats_update(OnlineStats_t* s, float value) {
s->count++;
float delta = value - s->mean;
s->mean += delta / s->count;
float delta2 = value - s->mean;
s->m2 += delta * delta2;
if(s->count > 1) {
s->std = sqrtf(s->m2 / (s->count - 1));
}
}
int is_outlier_3sigma(OnlineStats_t* s, float value) {
if(s->count < 10) return 0; // 样本太少,不判断
float diff = fabsf(value - s->mean);
return (diff > 3.0f * s->std) ? 1 : 0;
}
避坑指南:我曾经在一个项目中直接用3σ检测,结果发现传感器刚上电时数据波动大,把正常数据也剔除了。后来加了一个“预热期”——前100个样本只统计不剔除,问题就解决了。
4.2.2 滑动窗口阈值法——简单实用
如果你不想搞复杂的统计计算,可以用滑动窗口阈值法。原理是:当前值与前一个值的差值超过阈值,就认为是异常。
这个方法在STM32上特别实用,因为计算量小,而且不需要存储大量历史数据。
// 滑动窗口阈值法
#define THRESHOLD_RATE 0.2f // 允许的变化率(20%)
int is_outlier_threshold(float current, float previous) {
if(previous == 0.0f) return 0;
float rate = fabsf((current - previous) / previous);
return (rate > THRESHOLD_RATE) ? 1 : 0;
}
经验之谈:阈值率要根据传感器特性来设。温度传感器变化慢,阈值率可以设小一点(5%~10%);加速度传感器变化快,阈值率可以设大一点(30%~50%)。我一般先看传感器数据手册里的“最大变化率”,然后乘以1.5作为阈值。
4.3 数据归一化与鲁棒性增强
数据进模型之前,归一化是必须的。但很多人只做了“减均值除方差”这一步,忽略了鲁棒性。说白了,你的归一化参数本身可能被异常值污染。
4.3.1 鲁棒归一化——用中位数和四分位距
标准归一化(Z-score)用的是均值和标准差。但均值和标准差对异常值非常敏感——一个异常值就能把均值拉偏。我推荐用鲁棒归一化:用中位数代替均值,用四分位距(IQR)代替标准差。
| 归一化方法 | 中心化参数 | 缩放参数 | 对异常值的鲁棒性 |
|---|---|---|---|
| Z-score | 均值 | 标准差 | 差 |
| Min-Max | 最小值 | 最大值-最小值 | 极差 |
| 鲁棒归一化 | 中位数 | 四分位距 | 好 |
// 鲁棒归一化(在线更新)
typedef struct {
float median;
float iqr; // 四分位距
float buffer[100];
uint8_t count;
} RobustNormalizer_t;
void robust_normalize(RobustNormalizer_t* rn, float* value) {
if(rn->count < 100) {
// 收集样本阶段
rn->buffer[rn->count++] = *value;
return;
}
// 计算中位数和IQR(每100个样本更新一次)
if(rn->count == 100) {
// 排序
float temp[100];
memcpy(temp, rn->buffer, sizeof(temp));
// 用快速选择算法找中位数和四分位数
// 这里简化处理,实际项目中用nth_element
rn->median = temp[50];
rn->iqr = temp[75] - temp[25];
rn->count = 101; // 标记已初始化
}
// 归一化
if(rn->iqr > 0.001f) {
*value = (*value - rn->median) / rn->iqr;
}
}
注意:归一化参数(中位数、IQR)需要在模型训练时从训练集计算,然后固化到STM32的代码中。不要在推理时在线更新归一化参数,否则模型的行为会随时间漂移。
4.3.2 数据增强——让模型自己学会抗干扰
除了在数据预处理阶段做滤波,你还可以在训练阶段做数据增强。说白了,就是给训练数据加一些“模拟噪声”,让模型学会在噪声中提取特征。
我在一个振动监测项目里用过这招:原始数据是干净的,我故意加了高斯噪声、脉冲噪声、甚至丢包模拟,然后训练模型。结果模型在真实传感器数据上的准确率提升了12%。
具体做法:
- 加高斯噪声:模拟传感器热噪声,标准差取传感器数据手册中的噪声水平
- 加脉冲噪声:随机将5%~10%的数据点替换为极大值或极小值
- 时间扭曲:对时间轴进行轻微的拉伸或压缩,模拟采样时钟抖动
- 丢包模拟:随机丢弃1%~3%的数据点,用前一个值填充
你想想看,模型在训练时见过各种“脏数据”,到了STM32上遇到真实噪声,它自然就不慌了。这就是所谓的鲁棒性增强——不是让数据变干净,而是让模型变“皮实”。
我的习惯:数据增强的参数不要设得太极端。我一般先设一个较小的幅度(比如噪声标准差为0.01),然后逐步加大,直到模型在验证集上的性能开始下降为止。这个“临界点”就是最优的数据增强强度。
好了,这一章的内容就到这里。总结一下:传感器数据进模型之前,先过三关——滤波、异常检测、鲁棒归一化。这三步做好了,你的模型在STM32上才能稳定运行。下一章咱们聊聊模型量化与部署中的安全陷阱,到时候见。