从原始气体阻值到异常评分:环境基线算法的第一版设计

📅 2026/7/10 6:27:32
从原始气体阻值到异常评分:环境基线算法的第一版设计
传感器接上以后最容易做错的一步是直接拿一个数字下结论。比如气体阻值突然变了就想把它标成异常读数回落了又马上标成正常。这样做在页面上看起来很灵敏但实际很容易误判。BME690 的气体阻值会受很多因素影响刚上电的预热、温湿度变化、空气流动、传感器附近的人体气流、酒精或清洁剂、厨房油烟都会让读数变化。早期算法如果太激进页面会频繁跳状态如果太迟钝又看不出变化。所以我先做了一个简单可解释的版本原始值、平滑值、环境基线、相对偏离、持续时间最后再合成异常评分。第一件事是别把原始值当结论传感器原始值必须保留但不能直接当最终状态。原始值最大的价值是追溯问题当页面显示异常时能回头看真实数据到底发生了什么。我会把几个字段分开json{ gas_raw: 126.4, gas_smooth: 122.8, gas_baseline: 98.5, deviation: 0.247, duration_seconds: 12, score: 0.68}这些字段分别承担不同任务。gas_raw用来追溯gas_smooth用来减少尖峰影响gas_baseline表示当前环境的日常参考deviation表示偏离程度duration_seconds表示持续了多久score才是页面更容易理解的状态指标。如果只保留一个最终分数调试会很难。分数高了不知道是原始值真的变了还是平滑太敏感或者基线更新策略不合适。预热阶段不要参与判断BME690 刚启动时读数不会马上稳定。如果上电后立刻建立基线后面的判断很容易被带偏。我的做法是引入warmup状态预热阶段只采集和显示不参与异常评分也不更新长期基线。伪代码可以这样写cif (state.warmup) { state.score 0.0f; state.duration_seconds 0; return;}这个逻辑简单但很重要。它能避免一种常见误判设备刚上电读数快速变化页面马上显示异常。实际这不是环境异常只是传感器还没稳定。平滑是为了少被尖峰带跑环境数据会有尖峰。一次气流、一次手靠近传感器、一次短暂扰动都可能让原始值跳一下。如果直接用原始值判断页面会频繁闪。第一版可以用简单指数平滑cfloat smooth_value(float old_value, float new_value){ const float alpha 0.15f; if (old_value 0.0f) { return new_value; } return old_value * (1.0f - alpha) new_value * alpha;}alpha 越大反应越快也越容易跟着噪声跳alpha 越小曲线越稳但响应更慢。这个参数没有万能值需要结合采样频率和实际测试调整。这里不要追求一开始就完美。先把原始值和平滑值同时显示出来看它们在真实环境下怎么变化再决定参数。![读数不是直接报警中间还要经过基线和评分](https://i-blog.csdnimg.cn/direct/34e0b3ca66994c518218c27f0cdc25e3.png)pspan stylecolor:#1f6feb;strongemu基线不是固定常数/u/em/strong/span/p很多人会想设一个阈值比如大于某个数就是异常。但环境传感器不太适合这么做。不同房间、不同温湿度、不同摆放位置基础读数都可能不同。一个固定阈值很容易在某些环境里太敏感在另一些环境里又完全没反应。所以我更倾向于维护一个环境基线。基线表示“当前环境最近一段时间的大致正常水平”。它应该缓慢跟随日常变化但不能被突发扰动马上带走。伪代码可以这样写cfloat update_baseline(float baseline, float smooth, bool abnormal){ if (baseline 0.0f) { return smooth; } if (abnormal) { return baseline; } const float beta 0.02f; return baseline * (1.0f - beta) smooth * beta;}这里的关键是异常期间不要快速更新基线。否则持续异常会被系统慢慢当成新的正常最后分数反而降下来。pspan stylecolor:#1f6feb;strongemu只看偏离还不够还要看持续时间/u/em/strong/span/p一次短暂波动和一次持续偏离意义不同。比如有人从传感器旁边经过读数可能会瞬间变化但如果变化持续几十秒就更值得关注。第一版可以用相对偏离和持续时间一起判断cfloat calc_deviation(float smooth, float baseline){ if (baseline 0.0f) { return 0.0f; } return fabsf(smooth - baseline) / baseline;}void update_duration(env_state_t *s){ if (s-deviation 0.25f) { s-duration_seconds 1; } else { s-duration_seconds 0; }}这里的 0.25 只是示例阈值不应该直接当成通用答案。实际项目里要根据采样频率、传感器位置和样本数据调整。pspan stylecolor:#1f6feb;strongemu异常评分要能解释/u/em/strong/span/p我不喜欢一上来做一个黑盒分数。早期版本的评分最好能解释清楚为什么分数变高为什么还没报警为什么短暂扰动被忽略。一个简单评分可以这样写cfloat calc_score(float deviation, int duration_seconds){ float deviation_score deviation / 0.5f; if (deviation_score 1.0f) deviation_score 1.0f; float duration_score duration_seconds / 30.0f; if (duration_score 1.0f) duration_score 1.0f; return deviation_score * 0.6f duration_score * 0.4f;}这个公式不复杂但适合早期调试。偏离程度和持续时间分别占一部分页面上也能把两个因素展示出来。读者看到分数时不会觉得它是凭空冒出来的。页面一定要显示中间值如果页面只显示“正常/异常”调试会很难。我会把中间值也放出来包括原始值、平滑值、基线、偏离比例、持续时间和数据来源。这样做有两个好处。第一读者能看懂状态是怎么来的。第二自己调参数时不用反复接串口。如果页面显示异常但偏离值很低就说明评分公式可能有问题如果偏离值很高但持续时间为 0说明持续时间逻辑可能没累计如果基线一路跟着异常往上走说明基线更新太快。参数别在一次测试里定死平滑系数、偏离阈值、持续时间阈值都不应该靠一次测试拍脑袋定下来。比较稳的办法是保存几批数据用同一套算法回放。这样你能看到参数变化对结果的影响。比如同一批数据里把偏离阈值从 0.2 调到 0.3看看短时扰动有没有被过滤掉把持续时间从 10 秒调到 30 秒看看系统是不是变得太迟钝。参数不是越敏感越好读者实际使用时更怕频繁误触发。调参数时最好一次只改一个值。平滑系数、偏离阈值和持续时间一起改结果可能看起来变好了但你不知道是哪一个参数起作用。把改动拆开虽然慢一点但更容易形成可复用的经验。如果有条件还可以把不同参数下的结果记录成表格。比如同一批数据分别记录触发次数、最长持续时间、误触发片段。这样后面调整就不是凭感觉而是有对比依据。算法早期最有价值的不是“看起来聪明”而是能被自己解释清楚。解释不清的分数后面很难让人信任。所以第一版宁愿简单一些也要把每个中间值亮出来。能解释才能继续调。等这些简单逻辑在真实数据里跑稳再往复杂方向走风险会低很多。否则算法越复杂越难知道问题出在哪里。不要把一次安全样本说成结论测试时可以用安全方式制造一些变化比如改变通风、让传感器靠近普通日用品气味源、短时间观察读数变化。但这些测试只能说明传感器对环境变化有反应不能说明它已经能识别某种具体气味。这点要一直提醒自己。早期算法的目标是发现相对日常基线的持续变化不是做专业安全判断。页面上的文案也要克制不要把“异常评分”写成“危险判断”。写到这里第一版算法重点是可解释第一版环境基线算法不追求复杂。它的价值在于把原始值、平滑、基线、偏离和持续时间拆开让每一步都能被看懂、被调试。如果你也在做类似项目建议先别急着上复杂模型。先把中间值显示出来拿几批真实数据看看它们怎么变化。等你能解释为什么某次变化被判成异常、为什么某次扰动被忽略再考虑更复杂的识别方法。