1. 项目概述最近在捣鼓一些边缘计算和嵌入式AI的应用发现TinyML这个领域真是越来越有意思了。简单来说它就是让那些原本只负责控制电机、读取传感器的微控制器MCU也能跑得动一些轻量级的机器学习模型实现本地的、实时的智能判断。这玩意儿最大的好处就是快、省电而且数据不用上传云端隐私性也更好。我这次折腾的项目就是一个挺实用的场景用一块Arduino开发板做一个能实时识别咳嗽声的小设备。你可能会想这有什么用其实应用场景还挺多的。比如在需要保持安静环境的图书馆或办公室它可以作为一个非侵入式的噪音监测节点再比如结合其他传感器它可以作为健康监测系统的一部分用于记录和分析咳嗽频率。当然它绝对不是一个医疗诊断设备不能也不应该用于判断任何健康状况比如文中提到的特定疫情。它的核心价值在于展示如何将音频分类的TinyML模型从数据采集到最终部署完整地跑在一个指甲盖大小的硬件上。整个项目的核心硬件是Arduino Nano 33 BLE Sense它自带一个麦克风正好用来采集声音。软件层面则主要依赖Edge Impulse这个在线平台它把数据标注、特征提取、模型训练和嵌入式代码生成这些繁琐的步骤都打包好了对开发者特别友好。下面我就把从零开始搭建这个“口袋咳嗽检测器”的详细过程、踩过的坑以及一些优化思路完整地分享出来。2. 核心硬件与平台选型解析2.1 为什么是Arduino Nano 33 BLE Sense选择这块板子不是随便抓的而是基于几个硬性需求做的权衡。首先音频输入是刚需。普通的Arduino Uno、Nano没有内置麦克风外接的话会增加电路复杂度和体积。Nano 33 BLE Sense板载了一个MP34DT05数字MEMS麦克风直接通过I2S接口与主控芯片通信省去了模拟信号调理电路的麻烦音质和抗干扰性也更好。其次算力和内存是关键。跑机器学习模型哪怕是轻量级的也需要一定的计算能力和存储空间。这块板子的核心是Nordic Semiconductor的nRF52840这是一颗Cortex-M4F内核的微控制器主频64MHz拥有1MB的Flash和256KB的RAM。相比传统的8位AVR单片机如Uno用的ATmega328P只有32KB Flash和2KB RAM它的资源要充裕得多足以承载一个经过优化的神经网络模型。再者生态支持很重要。Arduino官方为这块板子提供了完整的TinyML支持库如Arduino_LSM9DS1用于传感器PDM用于麦克风并且Edge Impulse平台对其有现成的部署模板。这意味着在数据采集和模型部署阶段你能省下大量底层驱动和适配的工作。注意市面上也有一些其他带麦克风的开发板比如Seeed Studio的XIAO BLE SensenRF52840或ESP32-S3-EYE。选择Arduino Nano 33 BLE Sense主要是看中其稳定的Arduino Core支持和Edge Impulse的一键部署兼容性。如果你是ESP32的深度用户也可以尝试但部分底层库和部署流程可能需要自行调整。2.2 Edge Impulse StudioTinyML的“快速开发套件”对于嵌入式开发者来说从头开始搞机器学习光是数据预处理、特征工程和模型训练就能劝退一大片。Edge Impulse的价值就在于它把这些门槛极高的步骤图形化、流程化了。你可以把它理解为一个在线的MLOps平台但专门为嵌入式设备优化。它主要解决了以下几个痛点数据采集与标注提供了手机App、CLI工具等多种方式可以直接从你的硬件设备如Arduino或手机录制数据并在线打标签管理数据集非常直观。特征工程自动化对于音频分类它内置了MFCC梅尔频率倒谱系数特征提取块。MFCC是语音识别领域的经典特征能很好地模拟人耳听觉特性用较少的维度表征声音的频谱特点。在Edge Impulse里你只需要点一下按钮它就能自动为所有音频样本计算MFCC特征无需自己写信号处理代码。模型训练与优化提供图形化的神经网络构建界面也支持专家模式的Keras代码。更重要的是它在后台会自动进行模型量化、剪枝等操作确保最终生成的模型足够小能塞进微控制器的有限内存里。一键部署训练好的模型可以直接导出为优化的C库Arduino Library格式里面包含了模型权重、推理引擎EON Compiler或TFLite Micro以及调用接口。你只需要在Arduino IDE里导入这个库调用几个简单的API就能完成推理。对于这个咳嗽检测项目Edge Impulse几乎是“唯一选择”因为它极大地简化了从音频数据到嵌入式固件的整个链路。自己手动实现MFCC提取和TFLite Micro的集成工作量会呈指数级增长。3. 数据集构建质量决定模型上限模型训练就像做饭食材数据不好再好的厨艺算法也白搭。构建一个高质量、有代表性的数据集是整个项目最耗时但也最重要的环节。3.1 数据采集策略与实操我们的目标是区分“咳嗽”和“噪声”背景音。理想情况下数据集应该尽可能覆盖真实场景下的多样性。1. 咳嗽样本采集来源文中提到可以录制自己或他人的咳嗽声或者使用网络上的咳嗽音频库。我强烈建议两者结合。自己录制能保证数据真实但数量有限、多样性不足通常只有你自己的咳嗽模式。使用公开数据集如AudioSet、COSWARA等项目中提取的咳嗽片段可以极大地丰富数据涵盖不同年龄、性别、力度的咳嗽声。实操方法使用Edge Impulse手机App在Edge Impulse Studio创建项目后进入“数据采集”页面。将标签设为“cough”采样长度设为10秒对于咳嗽这种短事件10秒足够包含多次咳嗽和间隔比原文的40秒更高效减少无效数据。点击“开始采样”用手机在相对安静的环境下播放预先准备好的咳嗽音频文件或者真人咳嗽。确保手机麦克风不被遮挡。数量与多样性目标至少50-100个咳嗽样本。尽量包含干咳、湿咳、重咳、轻咳以及不同人发出的声音。如果使用网络资源注意版权和格式转换通常需要转换为WAV格式16kHz采样率单声道。2. 噪声样本采集“噪声”的定义这里的“噪声”不是指刺耳的噪音而是指“非咳嗽的一切声音”。这包括环境音键盘敲击声、鼠标点击声、翻书声、空调风扇声。人声谈话声、笑声、清嗓子声、打喷嚏声。其他声音手机铃声、音乐声、开关门声。采集实操标签设为“noise”。拿着手机在你期望设备部署的环境如图书馆、办公室、家中走动录制各种背景声音。也可以在电脑前模拟办公场景录制键盘鼠标声。同样目标采集50-100个噪声样本覆盖尽可能多的场景。3. 数据集划分Edge Impulse会自动将你上传的数据按比例默认80/20划分为训练集和测试集。但更好的做法是主动管理。在采集时就规划好哪些数据用于训练哪些用于验证。例如你可以专门录制一些“测试专用”的音频片段。在Edge Impulse的“数据采集”页面上传时可以选择“别”为“Training”或“Testing”。确保测试集数据是模型在训练时从未“见过”的这样才能真实评估模型的泛化能力。实操心得数据平衡很重要。尽量让“cough”和“noise”两类样本的总时长相近。如果一类数据远多于另一类模型可能会偏向于预测数量多的类别。另外录音时注意增益控制避免声音过大削波或过小。Edge Impulse App的录音界面有电平表尽量让音量峰值保持在-3dB到-6dB之间。3.2 使用CLI工具批量导入数据用手机一个个录效率太低特别是当你已经有一批下载好的音频文件时。这时就需要用到Edge Impulse CLI上传工具。安装CLI确保电脑已安装Node.js然后在终端运行npm install -g edge-impulse-cli。登录运行edge-impulse-uploader --login按提示在浏览器中完成认证。准备数据这是最关键的一步。CLI工具要求音频文件必须配套一个.json的元数据文件。你需要将每个WAV文件转换如下假设你有一个cough_1.wav。你需要创建一个同名的cough_1.json内容如下{ version: 1, label: cough, sampleLengthMs: 10000, // 音频长度单位毫秒 frequency: 16000 // 采样率单位Hz }将所有.wav和对应的.json文件放入同一个文件夹如training。上传在终端导航到该文件夹的父目录运行edge-impulse-uploader --category training training/*.wav edge-impulse-uploader --category training training/*.json对于测试集同理使用--category testing。这个过程虽然前期需要写脚本批量生成.json文件可以用Python简单实现但一旦跑通对于管理成百上千个样本来说效率是碾压手动操作的。4. 模型设计与训练从特征提取到神经网络数据准备好后就可以在Edge Impulse里构建和训练模型了。这个过程涉及到信号处理和机器学习的基本原理。4.1 理解MFCC特征提取为什么用MFCC而不是直接把原始的音频波形数据喂给神经网络维度灾难1秒16kHz采样率的音频就有16000个数据点。直接处理计算量巨大且包含大量冗余信息如低频背景嗡嗡声。感知相关性人耳对声音频率的感知不是线性的对低频变化更敏感。MFCC通过梅尔滤波器组模拟了这一特性。表征能力MFCC提取的是声音的短时功率谱再经过一系列变换得到的倒谱系数它能很好地表征声音的“音色”特征对于咳嗽这种具有特定频谱模式的声音非常有效。在Edge Impulse的“创建脉冲”环节添加“Audio (MFCC)”处理块时你需要关注几个参数窗长通常设为0.02秒20ms到0.04秒40ms。太短则频率分辨率低太长则时间分辨率低。咳嗽是短时事件建议用20ms或25ms。窗重叠通常设为0.01秒10ms。重叠是为了确保不会在窗口边界丢失重要信息。MFCC特征数默认是13维。这个维度已经足够表征声音的主要特征增加维度会提升信息量但也会增加模型输入大小和计算量。对于咳嗽检测13维是合适的起点。点击“生成特征”后Edge Impulse会将每一段音频比如10秒按照上述窗长和重叠切成许多小片段对每个片段计算MFCC最终生成一个特征矩阵。这个矩阵就是神经网络的实际输入。4.2 构建与训练卷积神经网络在“学习块”中选择“神经网络Keras”。原文提到了切换到专家模式并替换代码。我们来深入理解一下这段代码的每一层在做什么model Sequential() # 输入层形状由MFCC特征决定例如 (X_train.shape[1], ) 可能是 (13 * 时间步数, ) model.add(InputLayer(input_shape(X_train.shape[1], ), namex_input)) # 重塑层将一维特征向量重塑为二维图像格式 (时间步, MFCC系数, 1通道)。 # 假设X_train.shape[1]是130除以13得到10个时间步。这相当于一个10x13的“声谱图”。 model.add(Reshape((int(X_train.shape[1] / 13), 13, 1), input_shape(X_train.shape[1], ))) # 第一层卷积使用10个5x5的卷积核激活函数ReLU采用“same”填充保持尺寸并施加最大范数约束防止过拟合。 model.add(Conv2D(10, kernel_size5, activationrelu, paddingsame, kernel_constraintMaxNorm(3))) # 平均池化2x2池化窗口进一步降低特征图尺寸提取更鲁棒的特征。 model.add(AveragePooling2D(pool_size2, paddingsame)) # 第二层卷积卷积核数量减少到5继续提取更高阶的特征。 model.add(Conv2D(5, kernel_size5, activationrelu, paddingsame, kernel_constraintMaxNorm(3))) model.add(AveragePooling2D(pool_size2, paddingsame)) # 展平层将二维特征图展平成一维向量为全连接层做准备。 model.add(Flatten()) # 输出层神经元数量等于类别数这里是2‘cough’和‘noise’使用softmax激活函数输出概率分布。 model.add(Dense(classes, activationsoftmax, namey_pred, kernel_constraintMaxNorm(3)))为什么用CNN处理音频虽然音频是一维时间序列但当我们把MFCC特征重塑为(时间步, 系数, 1)的格式后它就变成了一张二维的“图”横轴是时间纵轴是MFCC系数。卷积神经网络CNN擅长捕捉图像中的局部空间模式。在这里CNN可以学习到咳嗽声在时间和频率维度上特有的局部模式例如某个频段在短时间内能量的突然爆发和衰减。关键参数调整与经验学习率原文代码中设置为0.005。对于Adam优化器这是一个相对较高的值。如果训练过程中损失值震荡剧烈或无法下降可以尝试调低到0.001或0.0005。训练轮数设为9轮。你需要观察训练集和验证集的准确率曲线。如果验证集准确率在几轮后不再上升甚至开始下降过拟合就应该提前停止训练。Edge Impulse的图表能很好地展示这个过程。最小置信度在模型设置中将“最小置信度评级”设为0.70。这个阈值很重要它决定了模型做出预测需要多大的把握。阈值越高误报把噪声当咳嗽越少但漏报没检测到咳嗽可能增加。0.7是一个比较均衡的起点实际部署后可以根据串口打印的概率值进行调整。训练完成后平台会显示准确率、损失值并提供一个“模型测试”页面你可以上传新的音频片段来直观地测试模型性能。5. 嵌入式部署与代码解析模型训练满意后就可以把它部署到Arduino上了。5.1 生成与导入Arduino库在Edge Impulse的“部署”页面选择“Arduino库”。点击“构建”后会下载一个.zip文件。在Arduino IDE中通过“项目” - “加载库” - “添加.ZIP库…”将其导入。导入后你可以在“文件” - “示例” - “你的项目名 - Edge Impulse”下找到示例代码。最常用的是nano_ble33_sense_microphone。这个示例代码已经包含了从麦克风读取数据、执行推理、输出结果的全部框架。5.2 核心代码逻辑析与修改让我们仔细看看示例代码的关键部分以及如何修改它来实现咳嗽检测并触发LED警报。1. 初始化与设置#include your_project_name_inferencing.h // 自动生成的模型头文件 #include PDM.h // 麦克风库 // 定义音频缓冲区 static signed short sampleBuffer[2048]; static bool debug_nn false; // 设置为true以查看详细的NN调试信息 void setup() { Serial.begin(115200); // 等待串口连接方便调试 while (!Serial); ei_printf(Edge Impulse Inferencing Demo\n); // 初始化麦克风 if (!PDM.begin(1, 16000)) { // 1个通道16kHz采样率 ei_printf(Failed to start PDM!); while (1); } // 配置LED引脚 pinMode(LED_BUILTIN, OUTPUT); pinMode(4, OUTPUT); // 假设外接LED在D4引脚 }这部分代码进行硬件初始化。PDM.begin(1, 16000)是关键它设置了与模型训练时一致的音频采样率16kHz。2. 主循环与推理流程原始示例的loop()函数核心是不断采集音频凑够一次推理所需的数据长度由模型定义例如1秒然后调用run_inference()函数。3. 修改推理结果处理逻辑关键步骤原文提供的修改代码有误且不完整。EI_CLASSIFIER_LABEL_COUNT是标签总数循环应从0开始。更重要的是我们需要比较“cough”和“noise”的概率。假设你的标签顺序是[noise, cough]Edge Impulse按字母顺序或添加顺序排列请根据实际打印结果确认。正确的修改思路如下void loop() { // ... 音频采集和缓冲区填充代码示例代码中已有... // 信号处理、特征提取和分类推理 signal_t signal; // 将原始音频缓冲区转换为signal_t结构 numpy::int16_to_float(ei::get_signalbuf(), rawFeatures, EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE); signal.total_length EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE; signal.get_data raw_feature_get_data; // 运行推理 ei_impulse_result_t result {0}; EI_IMPULSE_ERROR res run_classifier(signal, result, debug_nn); if (res ! EI_IMPULSE_OK) { ei_printf(ERR: Failed to run classifier (%d)\n, res); return; } // 打印原始结果 ei_printf(Predictions (DSP: %d ms., Classification: %d ms., Anomaly: %d ms.): \n, result.timing.dsp, result.timing.classification, result.timing.anomaly); // 假设标签索引0noise, 1cough 请根据实际打印确认 float cough_confidence result.classification[1].value; float noise_confidence result.classification[0].value; ei_printf( NOISE: %.3f\n, noise_confidence); ei_printf( COUGH: %.3f\n, cough_confidence); // 决策逻辑如果咳嗽置信度高于阈值且高于噪声置信度则触发警报 float detection_threshold 0.65; // 可调阈值 if (cough_confidence detection_threshold cough_confidence noise_confidence) { ei_printf(*** COUGH DETECTED! ***\n); triggerAlarm(); // 调用警报函数 } } void triggerAlarm() { for (int i 0; i 3; i) { // 闪烁3次 digitalWrite(LED_BUILTIN, HIGH); digitalWrite(4, HIGH); // 外接LED delay(200); digitalWrite(LED_BUILTIN, LOW); digitalWrite(4, LOW); delay(200); } }修改要点解析明确标签索引不要依赖循环而是直接通过数组索引获取“cough”和“noise”的置信度分数。先运行一次未修改的代码查看串口打印的标签顺序。双重判断条件不仅要求cough_confidence threshold例如0.65还要求cough_confidence noise_confidence。这增加了判断的鲁棒性防止在噪声置信度也很高时误判。结构化警报函数将LED闪烁逻辑封装成函数使主循环更清晰。保留调试信息继续打印DSP处理和分类的时间这对于评估模型在设备上的实时性能至关重要。5.3 编译与上传修改完代码后选择正确的开发板Arduino Nano 33 BLE和端口点击上传。上传成功后打开串口监视器波特率115200你就能看到实时的推理结果输出。当有咳嗽声被识别时LED会闪烁报警。6. 系统优化与实战调试技巧项目能跑起来只是第一步要让它在实际环境中稳定可靠还需要一系列的优化和调试。6.1 性能分析与优化关注时序数据串口打印的result.timing.dsp和result.timing.classification分别代表特征提取和模型推理的耗时单位毫秒。它们的总和应小于你的音频窗口长度例如处理1秒音频的总时间应远小于1000ms否则无法实现实时流式处理。如果耗时太长需要回Edge Impulse简化模型减少层数、神经元数或调整MFCC参数减少特征数。内存占用在Arduino IDE编译时留意输出的“全局变量使用”和“动态内存”信息。确保模型没有耗尽RAM256KB。如果接近极限在Edge Impulse中训练时可以选择“量化感知训练”或使用“EON Compiler”进行更激进的优化它能生成比TensorFlow Lite Micro更小更快的模型。功耗考虑如果希望设备电池供电需要优化代码。在loop()中如果没有检测到声音可以增加一个短暂的delay()或让MCU进入低功耗休眠模式由麦克风或定时器中断唤醒。这需要更深入的嵌入式编程知识。6.2 提高检测准确性的实战技巧阈值动态调整固定的检测阈值如0.65可能不适应所有环境。可以设计一个简单的自适应机制例如持续监测一段时间内的平均“noise”置信度将咳嗽检测阈值设置为0.5 (平均噪声置信度 * 0.2)。这样在嘈杂环境下阈值会自动提高减少误报。后处理与去抖声音识别容易受到突发尖峰噪声的影响。可以引入一个简单的“去抖”逻辑连续N次推理例如3次都检测到咳嗽才最终判定为一次有效的咳嗽事件。这能过滤掉很多短暂的误报。int cough_detection_counter 0; const int DEBOUNCE_COUNT 3; // 在loop的决策部分 if (cough_confidence detection_threshold cough_confidence noise_confidence) { cough_detection_counter; if (cough_detection_counter DEBOUNCE_COUNT) { ei_printf(*** CONFIRMED COUGH DETECTED! ***\n); triggerAlarm(); cough_detection_counter 0; // 重置计数器 } } else { cough_detection_counter 0; // 条件不满足重置计数器 }数据增强如果发现模型对某些声音比如拍手声、咳嗽声容易混淆最好的办法是补充数据。回到Edge Impulse专门采集或导入这些易混淆的样本重新打标签并加入训练集然后重新训练模型。数据质量永远是第一位的。6.3 常见问题与排查实录问题上传代码时出错提示“内存不足”或编译失败。排查首先检查是否选择了正确的开发板型号Tools - Board - Arduino Nano 33 BLE。如果正确可能是模型太大。返回Edge Impulse在“部署”页面尝试选择“优化版EON Compiler”重新构建库它通常比“未优化版TensorFlow Lite for Microcontrollers”小很多。问题串口有输出但置信度分数永远不变或者对声音没反应。排查检查麦克风是否被遮挡或损坏。可以运行一个简单的PDM测试程序将原始音频数据打印到串口绘图器看看对着麦克风说话时波形是否有变化。检查PDM.begin()的采样率是否与Edge Impulse项目中设置的采样率默认为16kHz完全一致。确认音频缓冲区sampleBuffer的大小足够。推理函数run_classifier需要特定长度的数据由EI_CLASSIFIER_RAW_SAMPLE_COUNT定义确保你的采集循环能填满这个长度。问题模型在电脑上测试准确率很高但在设备上误报很多。排查这是典型的“领域偏移”问题。训练数据可能来自网络或特定环境录制与设备实际部署环境的声音特征不匹配。解决方法是进行现场数据采集和再训练。使用Edge Impulse的数据采集功能直接在最终要部署的Arduino设备上采集真实环境下的“cough”和“noise”样本用这些新数据对原有模型进行微调Transfer Learning可以显著提升实际性能。问题检测延迟感觉很高。排查查看串口打印的timing信息。如果总时间接近或超过音频窗口长度就是性能瓶颈。优化方法包括使用EON编译器、在Edge Impulse中创建更小的模型减少层数、使用MobileNetV1等高效架构、增加窗重叠以减少每次推理需要的新数据量但这会增加计算频率。这个项目麻雀虽小五脏俱全完整走通了TinyML应用开发的闭环需求定义、硬件选型、数据采集、模型训练、嵌入式部署和调试优化。它给你提供的不仅仅是一个咳嗽检测的固件更是一个可以复用的音频事件检测框架。你可以用完全相同的流程去训练识别门铃、婴儿啼哭、玻璃破碎、特定关键词等声音只需要更换数据集和调整模型参数即可。