如果你也跟我一样,白天调试了一整天代码,晚上还想知道自己到底打不打鼾、打多久,大概率想到的办法是拿手机录音。手机录音确实方便,但整晚亮着屏幕录音,一觉醒来机身发烫不说,几十个音频文件堆在那里,靠人耳去听根本不现实,更别提在鼾声发生的那几秒做点干预。我这次想验证的是另一条路:用一块MCU开发板放在床头,持续采集声音并做鼾声识别,所有推理都在本地完成,不往云端传原始音频。选来选去,最后用到的是手头这块英飞凌CY8CKIT-062S2-AI评估板,把从录音、特征提取、模型训练、量化部署到板端实测的完整流程走了一遍。这篇就把整个实操过程写清楚,不绕弯子,该给的步骤、参数和踩坑记录都会给,适合想用MCU做音频类TinyML项目、又不想花太多时间啃英文文档的人。
1. 为什么我一定要用MCU做鼾声识别
1.1 隐私、功耗和实时性,三个不能只靠手机的理由
先说隐私。鼾声识别本质上是音频事件检测,而音频比文字敏感得多。夜里卧室里录到的可不止鼾声,还有翻身的声音、被子摩擦的声音,甚至偶尔的对话。如果把整晚音频传到云端再做识别,不管用户嘴上说不说,心里都会犯嘀咕。MCU本地推理的好处是把整个分析过程限制在板子上,需要传出去的时候只传一个类似“今晚鼾声总时长38分钟、鼾声事件12次”的统计结果。这一点对做成家用小设备来说,几乎是刚需。
然后是功耗。手机整夜录音,哪怕开飞行模式,一晚上掉电也比较明显,而且机身贴着枕头或者床头柜,发热本身就会影响睡眠。MCU不一样,真正适合音频事件检测的工作方式是:平时休眠,麦克风采集到一定能量阈值后唤醒,再做特征提取和模型推理。就算不做休眠优化,全程连续推理,一块低功耗MCU的整机功耗也比手机亮屏低好几个数量级。鼾声又不是每秒钟都有,事件驱动可以把平均功耗压得更低。
最后是实时性。如果后续想接止鼾设备——比如震动枕、智能床垫、蓝牙耳机提示——就不能等云端返回结果。云端方案一来一回最少几百毫秒,网络一抖马上卡顿。本地推理从音频帧到达开始算,到输出一个判决结果,控制在几十毫秒到一百多毫秒以内,实时性才有保证。
1.2 “窄任务”是MCU能跑鼾声识别的关键前提
MCU不是万能的,能把鼾声识别跑起来,根本原因是这个任务足够“窄”。鼾声就是一类特定的呼吸振动声,类别少、频谱结构相对稳定,不需要识别自然语言,也不需要区分几十种指令词。这种窄任务可以让模型做得很小,小到MCU的内存和Flash装得下。
我给自己定的硬性约束是:int8量化后的模型不要超过150KB,推理过程中的临时buffer控制在几十KB以内。这个约束反过来帮我筛掉了所有花哨的结构。如果一上来就拿一个几MB的语音模型说“剪一剪也许能跑”,那大概率是浪费时间。做嵌入式AI,先圈定资源边界,再设计模型,和大算力平台上“先搭大模型再压缩”的路数是完全相反的。
2. 开箱与听声:CY8CKIT-062S2-AI的环境准备
2.1 板载麦克风和音频通路的首次测试
拿到板子第一步不是急着跑AI,而是先把音频通路调通。CY8CKIT-062S2-AI板上有现成的麦克风,不需要外接音频模块,这对做音频识别来说省了很大事。先给板子上电,装好驱动,确认系统能识别到调试接口,然后用官方例程把麦克风采集的PCM数据通过串口回传。
我的做法是写一个最简单的采集程序,以16kHz采样率、16位精度持续往外发PCM数据,PC端用脚本把串口数据流攒下来封成WAV文件,再用音频工具看一下波形。这一步看起来基础,但非常关键:你需要在动手训练模型之前,就确认录音的通路是干净的、增益是合理的。
第一次测试时我踩了个小坑:串口回传的PCM数据是连续字节流,没有帧边界。脚本收数据时多收了一个字节,整个WAV文件后半段就变成一片高频噪声,波形完全乱掉。后来我是按照“块头+块内样本数”的方式自行组帧才解决。这种问题在PC上几乎遇不到,但MCU侧很常见,建议大家一开始就把数据结构设计好。
2.2 开发工具链版本带来的第一个坑
英飞凌这块板子的开发环境是官方IDE加SDK的方式,例程仓库里包含了针对这块评估板的BSP配置。听起来很顺,但工具链版本不对的时候,编译一个最简单的例程都会报一堆语义错误,而且报错信息指向的库文件和你实际打开的代码完全对不上。
我遇到的问题是IDE和SDK包版本不匹配:IDE是较新版,例程却是旧版分支拉的,结果一编译就到库内部报错。后来发现官方例程项目的“目标BSP版本”可以统一调整,把项目BSP版本切到与SDK配套的版本,重新编译,整个例程一次通过。我的经验是,拿到板子先别看功能代码,先把例程编译跑通,并且确认版本配套关系,再往里加自己的代码。否则环境问题会掺杂到你的业务逻辑里,排查起来特别痛苦。
2.3 算力边界:这块板子能跑多大的模型
在启动模型训练之前,我先想清楚一个问题:这块板子的算力边界到底在哪。CY8CKIT-062S2-AI的主控是双核Cortex-M架构,带DSP和AI加速相关扩展,Flash和RAM的单位都是KB到MB级别。这种量级决定了它适合的参数规模。
按照我的经验,一个针对“二分类鼾声事件”设计的小型CNN,参数量往往就在几万到几十万之间,int8量化后体积几十到一百多KB。这个规模对评估板来说是比较从容的。真正需要仔细规划的是推理时的临时buffer和特征存储:MFCC特征矩阵要不要全部放在RAM里?能不能环形缓存?这些细节后面会专门讲。
一句话总结:这块板子不是拿来跑大模型的,它是一个“刚好够用”的端侧推理平台,所有设计都要围绕“资源受控”来做。
3. 把鼾声变成特征:声学原理与特征提取方案
3.1 鼾声在频域长什么样,和说话声差在哪
模型不认识音频波形,它只能认识特征。要选对特征,得先知道鼾声在声学上是什么东西。简单说,鼾声是睡眠时上呼吸道部分变窄、气流经过狭窄通道引起组织振动产生的声音。它的能量大多集中在低频段,尤其是50到1000Hz这一片,而且振动的周期性比较明显,听起来像一串低频的“突突突”。这和说话声完全不同:说话声频谱铺得比较开,高频谐波丰富,还经常夹着爆破音、摩擦音。正因为两类声音在频域的分布差异明显,鼾声识别这类任务用窄带特征就足够了,完全不需要大而全的语音模型。
3.2 MFCC与Mel谱图的选择,我最后怎么定的
音频特征里最常见的两套方案是MFCC和Mel-spectrogram。MFCC是把梅尔频谱再做一次离散余弦变换,得到一组倒谱系数,维度低、去相关,传统语音识别用得非常多。Mel-spectrogram则保留了更多的原始频谱结构,作为2D图像喂给CNN的时候,模型能学到更丰富的时频模式。
我一开始在PC上对比过用这两种特征训练同一个CNN的效果。Mel谱图方案在验证集上的准确率稍微高一点,但它的维度更大,MCU上做特征提取的循环更多。MFCC的问题是维度低、丢了一些高频细节,但对鼾声这种窄带声音来说,丢失的细节恰好不太重要。权衡之后,我最终选了40维MFCC,并且不是单帧判别,而是把连续20帧的MFCC堆成一个“40×20的特征图”输入给CNN,对应大约1秒的音频段。
这1秒长度的选择不是拍脑袋。鼾声事件最短通常也在0.5秒以上,1秒窗口能覆盖一次完整鼾声事件,又不会让判决延迟太明显。窗口再长,鼾声之间的静音会被大量包进来,反而稀释特征。你只需要记住一个点:单帧分类容易抖动,多帧组成的短片段才能稳定反映声音事件的时间连续性。
3.3 特征提取在MCU上跑起来的注意事项
PC上提取MFCC用现成的音频库一行调完,MCU上没那么轻松。我实际部署时的基本链路是:预加重 -> 分帧加窗 -> FFT -> 梅尔滤波器组 -> 取对数 -> DCT得到MFCC。其中FFT在Cortex-M上可以用DSP库加速,效率很高;真正耗时间的反而不是FFT,而是梅尔滤波器组那几十个频带对能量谱的加权累加,以及最后的log和DCT。
为了省时间,我做了三件事。第一,梅尔滤波器的系数表预先算好,存成常量数组,不放在RAM里动态生成。第二,log操作改用查表加线性插值,避免调用标准库的浮点log函数。第三,DCT矩阵也常量化,MCU上只做一个矩阵乘向量操作,能省掉大量重复计算。实测下来,1秒音频的特征提取时间从最初的两三百毫秒降到几十毫秒,模型推理本身反而成了增速的受益者。
另一个容易被忽略的点:枕头摩擦声、被子摩擦声这类瞬时噪声,在频谱上往往表现出很宽的高频能量,和鼾声的低频窄带特征完全不同,但如果麦克风增益太高,瞬态噪声的频谱会往低频蔓延,干扰分类。所以我在特征层面对高频分量做了一点抑制,相当于把模型注意力更集中到鼾声所在的低频区间。
4. 数据比模型重要:样本采集、标注与增强
4.1 鼾声数据的来源和标注的“粒度”问题
模型能学成什么样,七成看数据,三成看模型结构。这句话在鼾声识别上尤其成立。数据来源我是两条腿走路:自己录的卧室音频,加上网上能找到的公开鼾声片段。公开片段的好处是正样本形态多样,坏处是录音距离、麦克风型号和卧室环境跟你的目标场景可能差很远。所以我的建议是:公开数据可以作为补充训练集,但最终评判模型是不是好用,必须拿自己环境里的录音来测。
标注是这里面最容易被做砸的一步。很多人拿着音频编辑器,看到一串鼾声就把整段标记为正样本“鼾声”,这其实有问题。鼾声常常是一串一串出现的,串与串之间只有零点几秒的停顿。如果整串标成一个正样本,模型会学到“鼾声之间的微小静音也是鼾声”,部署之后就容易在静音和呼吸声上误报。
正确做法是逐次标注:每一下鼾声的开始时间和结束时间都单独标出来,中间的停顿不标,周边的呼吸声不标,翻身声、被子摩擦声、闹钟声统统归入负样本。标注粒度越细,模型学到的边界越准确。
4.2 数据增强我只用了三种,但效果很明显
我训练时只用了三种数据增强,没有搞得很复杂。第一种是随机音量缩放,把音频幅度乘以0.7到1.3之间的随机系数,模拟麦克风离人远一点近一点的变化。第二种是加环境噪声,混入白噪声、空调底噪、电视背景声等,信噪比控制在5到15dB,逼迫模型去学“噪声背景下也要抓鼾声”。第三种是时间偏移和随机裁剪,让鼾声出现在窗口中的不同位置,避免模型错误地学到“鼾声必须居中”。
这三种增强看起来简单,效果却很直接。加环境噪声之后,模型对卧室底噪的容忍度明显提升;随机裁剪之后,同样一段鼾声出现在窗口不同位置都能被正确定位。有一点要特别注意:时间偏移超过鼾声边界时,新的标签可能不再是“鼾声”了,所以增强时必须同步更新标签,而不是移动完不管。
4.3 不要把同一条录音既当训练又当验证
这是我在这个项目里交过的一份学费。第一版模型训练出来,验证集准确率95%以上,我兴冲冲跑到板子上实测,结果一整晚录音里误报多到没法看。排查了半天,问题出在数据划分。
我一开始是把每条录音随机切成片段,然后整体随机打散成训练集和验证集。这样来自同一个录音文件、完全相同噪声条件、相同距离的片段会被同时分到两边,验证集看到的其实都是训练集相近的场景,准确率当然虚高。后来的改法是按录音文件来划分:一个晚上的录音要么全部进训练集,要么全部进验证集,不让同一环境的数据出现在两边。这样一改,验证集准确率立刻降到真实水平,但也终于能反映夜间实拍的效果了。
如果你也在做类似项目,千万别偷懒用随机切分,一定要按录音来源、按场景划分数据。
5. 训练到部署:把模型塞进板子的完整路径
5.1 模型结构和训练参数:小、稳、低误报
我的模型结构非常简单。输入是40×20的MFCC特征矩阵,我先把它当成40维的特征序列来对待,也就是20个时间步、每步40维。第一层是1D卷积,8个卷积核,核长5;第二层1D卷积,16个卷积核,核长5;中间加ReLU激活,最后经过全局平均池化,接一个全连接层输出2类概率。参数量不大,int8量化后大概70到100KB,非常适合MCU。
训练参数没有特别之处:batch size取32,Adam优化器,初始学习率1e-3,每训练几轮不下降就降一半,同时做早停。损失函数用二分类交叉熵。
这里有个经验:做这种事件检测,不要只盯准确率,更要看“误报率”。如果阈值定得很松,模型把每一声翻身的摩擦声都当成鼾声,准确率也许还是很高,因为负样本占比大,但实际体验完全没法用。我训练时更多观察的是验证集上“误报次数/小时”和“鼾声事件召回率”,这两个指标比整体准确率更能反映夜间使用的真实感受。
5.2 量化前后对比:模型小了,准确率也动了
模型在PC上训练好是浮点权重,直接部署到MCU上能做推理,但浮点运算慢且占用Flash大。更好的方案是转成int8量化模型。量化过程把权重和激活从float转到8位整数,配合MCU的加速单元,推理速度和体积都会有明显改善。
我记录过一组对比数值(不同场景下会有差异,但趋势基本一致):
| 模型形式 | 体积 | 验证集F1 | 实测推理耗时 |
|---|---|---|---|
| float32 | 约310KB | 0.93 | 相对较慢 |
| int8量化 | 约85KB | 0.90 | 明显更快 |
量化后准确率掉了一点,但体积只有原来的四分之一左右,推理时间大幅缩短。有意思的是,量化后模型在夜间实测里的误报率反而低了一些,我猜测是量化操作起到了类似正则化的作用,让一些过拟合的细节响应被抹平了。当然这不是必然现象,如果量化后效果变差,通常要从校准集和归一化上面找原因,后面章节我会专门讲。
5.3 部署流程与转换工具链
把模型从PC端搬到板端,整个流程大概是这样的:在训练框架里导出float模型,然后用量化工具指定校准集、生成int8模型,再把模型文件转成C数组,在MCU工程里加一个运行时解释器,然后把自研的特征提取代码和推理代码接起来。
这中间最容易翻车的环节是输入输出的对齐。模型在训练时对特征做了什么归一化、均值和方差是多少,部署时必须原样复制。我踩过的坑是:PC上写归一化用的是整个数据集的统计均值,部署后却用实时样本临时计算的均值,两者量级对不上,导致模型输出概率漂移。解决办法非常简单——把归一化的均值和缩放系数直接写死成常量,板上任何时刻都用同一套参数去变换特征,模型的输入分布就固定住了。
另外,模型转换这一步强烈建议做一个“PC端仿真部署”验证:把要板上运行的特征提取代码和推理代码先在PC上编译运行,喂一段标注好的音频,对比输出和训练时的结果。如果这一步通过了,再往板子上搬,能省掉大量烧录调试时间。
6. 板端实测结果和调试策略
6.1 我的测试集结果:哪些指标比准确率更重要
模型部署到CY8CKIT-062S2-AI之后,我用自己录制的两晚音频做了测试。说结论:帧级准确率在92%左右,鼾声事件的召回率达到95%上下,误报控制在每小时不到两次。这个水平足够我自己日常观察了,但要强调一句:这组数字是在我卧室的麦克风摆放位置、环境底噪条件下得到的,换一个房间、换一种床具,指标都会有变化,千万别当成通用结果。
在这里我更想说的不是数字本身,而是怎么看数字。做鼾声识别,用户最先感知到的是误报:没打鼾却被提示“鼾声中”。所以帧级准确率再高,如果误报频繁,实际体验照样很糟。我建议评估时重点记录三个指标:帧级准确率、误报次数/小时、鼾声事件的召回率。前两个反映“乱报”的程度,最后一个反映“漏报”的程度。
6.2 决策层的滑动窗口与事件计数
模型输出的是一个鼾声概率,每一秒一个结果。直接拿这个概率和阈值比,结果会很抖:同一段鼾声中间可能因为一次吸气声让概率掉到阈值以下,导致判决断断续续。我的办法是在决策层加一个滑动窗口做后处理。
具体逻辑是:连续5秒输出里,至少2秒的鼾声概率超过阈值,才判定当前时刻进入“鼾声状态”。这样可以平滑掉瞬时抽风。进入鼾声状态之后,再维护一个事件冷却时间:如果连续两次鼾声状态间隔小于5秒,就合并为同一次鼾声事件;只有间隔超过5秒才生成新事件。这样最终上报的是“今晚发生了多少次鼾声事件、每次持续多久、总共多少分钟”,而不是一条每秒都在变化的0/1时间线。
这套后处理逻辑对最终体验影响很大。同样一个模型,不做滑动窗口时的误报率可能高得没法用,加了窗口之后马上就变得可接受。建议做音频事件检测的朋友别在模型之后直接上阈值,一定留出一个决策层来。
6.3 从实测中发现的误报来源
实测一晚之后,我对着误报时间点去听了对应的录音,发现主要来源有三类。第一类是翻身时的床板吱呀声和被子摩擦声,这类声音瞬态能量大,频谱宽,很容易踩中模型的敏感区。第二类是手机通知声和闹钟声,它们的谐波结构在MFCC上跟鼾声有一定的相似度。第三类是空调压缩机启动或变频切换时的低频轰鸣,频率低、持续长,和鼾声的能量分布相当接近,最容易骗过模型。
针对这三类误报,我的处理是三层防线。第一层,只依赖模型的概率,而是在特征提取之前先加一个能量门限:这一帧RMS太低就根本不进模型推理,直接从源头排除大量底噪和轻微摩擦声。第二层,在模型输出的特征层面限制高频响应,让瞬态摩擦声的影响变小。第三层,在训练数据里专门加入更多翻身声、闹钟声、空调声样本,让模型亲眼见过这些负样本。三层下来,误报率比最开始下降了一个数量级。
7. 复盘:几个真会劝退人的问题与我的解决办法
7.1 为什么量化后模型输出恒为某一类
整个项目里最让我头疼的问题是:int8量化模型部署到板上后,不管输入什么声音,输出几乎恒定为“鼾声”这一类。一时间我以为是板上代码有bug,排查了两天没有结果,最后回到PC端做对比才发现是量化过程的问题。
根因有两层。第一层是校准集不具代表性,我当时为了省事,只用了几段夜间静音音频来统计量化参数,导致量化时对网络中间层的激活分布估计严重不准。第二层是输入归一化没有对齐,使得量化后的实时输入数值范围超出校准阶段见过的范围,被大量截断,把模型推到了一个极端输出状态。
解决方法是先修正校准集,确保里面同时覆盖静音、鼾声、说话声、摩擦声和空调声,每个类别几十条片段;然后在PC上让量化模型跑一遍和板上完全相同的输入数据,和浮点模型输出做对比,偏差超过可接受范围就回去调整校准集或逐层检查。我加了这个对比验证步骤之后,问题十分钟内就定位到了。
7.2 一键提升夜间识别稳定性:麦克风增益与能量门限
也不知道是不是只有我这样,最初录音时宁愿把增益调大,怕弱鼾声采不到。结果就是夜间底噪和微风声都被放大得跟鼾声差不多,模型误报直线上升。后来我把增益调低,再用能量门限去卡静音,效果立刻好了很多。
更关键的一点是关掉自动增益控制。板载麦克风如果走自动增益,环境声音一大会自动压小,环境安静又会放大,这样模型看到的输入幅度比例在整晚都在变,没法建立一个稳定的判别基准。正确做法是固定一个适中的增益,保证安静环境下底噪离最高值还有充足的余量,然后用能量门限过滤掉底噪。
能量门限既可以放在预处理前,也可以直接集成到事件检测的唤醒逻辑里:只有帧RMS高于门限才做特征提取和推理,否则直接判为非鼾声。这个门限看着简单,实际上对功耗和误报都有立竿见影的作用。
7.3 个人经验和最后的小技巧
整个项目做到最后,我最深的一条经验是:数据质量确实大于模型结构。第一次训练时我用复杂的模型、更多的参数,效果反而不如后来这个小而稳的模型配上一套认真划分的数据。把时间花在录音、标注和增强上,回报远比折腾模型结构来得高。
再分享一个调试技巧。在MCU上跑AI,一定不要只看到“最终是否识别”这个0/1结果,要主动输出中间量。我把串口调试信息设计成周期性打印当前帧的RMS、特征均值和模型原始输出概率,这样一旦测试出问题,我能很快分辨是麦克风采集的锅、特征提取的锅还是模型推理的锅。只看最终结果的话,信号链路上任何一环出错,表现都是“识别不对”,排查效率会很低。
如果让我重新做一遍这件事,我会在数据采集阶段多花一倍时间,把不同床具、不同睡姿、不同房间底噪都录进去;模型结构和部署流程反而是小事,真正决定这个鼾声识别项目能不能用的,永远是你在数据上付出的耐心。