最近在折腾低功耗语音唤醒方案,把音频流喂给一组脉冲神经元而不是传统的深度网络。很多人听到脉冲神经网络(SNN)第一反应是“类脑研究、发论文”,但真正落到语音识别这种产品化任务上,尤其语音唤醒这种需要 7x24 小时挂在麦克风上的场景,SNN 的稀疏计算优势一下就体现出来了。这篇文章把从原理理解、前端编码、网络搭建到部署实测的整个链路整理一遍,也会把训练阶段踩过的坑和参数联动关系说清楚,适合正在做低功耗语音方案、或者对脉冲神经网络在具体任务上落地感兴趣的读者。文章不堆公式,但该有的推导细节都会保留。
1. 语音唤醒的任务边界比想象中更苛刻
1.1 语音唤醒到底在干什么
语音唤醒(Keyword Spotting,KWS)本质上是一个门卫任务:麦克风永远开着,系统要在环境噪声、电视声、陌生人说话声、音乐声里,识别出那个特定唤醒词。一旦识别到,立即把后续的语音数据交给完整的语音识别链路,或者触发设备进入工作状态。
这个任务和普通语音识别最大的区别在于“永远在听”。普通语音识别是用户按下按钮后开始识别,唤醒系统没有开始按钮,它必须自己判断哪一段音频包含唤醒词。代价在于:只要判断错了,后面所有逻辑都会走错。如果把唤醒词漏掉,用户喊了两三次都没反应;如果把普通对话误判成唤醒,设备反复被点亮,用户很快就会关掉这个功能。
所以语音唤醒不是“二分类准确率高”这么简单,它同时考验响应速度、误报率、功耗三项指标。
1.2 评价唤醒系统的四个核心指标
我习惯把唤醒系统的指标拆成四块:
唤醒率:用户说唤醒词时,系统成功唤醒的概率。这个指标直观,大家都很关注,但单看它没有意义,因为把阈值调低,唤醒率很容易接近 100%,代价是误唤醒暴增。
误唤醒率:每小时内,在没有唤醒词的情况下系统被触发的次数。业内通常用“次/24小时”或者“次/小时”来统计。这个指标是体验的生死线,测试时必须在空调声、键盘声、电视声、多人说话混叠的情况下反复跑。
响应延迟:从用户说完唤醒词到系统给出“已唤醒”反馈的时间。语音唤醒一般要求 300ms 以内,越短越好。延迟主要来自音频分帧长度、特征计算、网络推理和端到端链路的调度。
平均功耗:在持续监听模式下消耗的功率。对电池供电的TWS耳机、智能手表、助听器来说,这一项往往比唤醒率还要关键。传统 DNN 做唤醒平均功耗很难压下去,因为每帧音频都要做完整矩阵乘法,哪怕没有任何语音输入,计算量也是一样的。
1.3 为什么唤醒比“小规模识别”难度更高
有人觉得唤醒词就那么两三个字,网络又小,做起来很简单。真不是。唤醒词检测面对的数据分布极度失衡:负样本(非唤醒词、噪声、任意语音)占比超过 99.9%,正样本只有寥寥几个词的音频。系统必须在极低误报率的条件下保证高唤醒率,这比普通分类任务难得多。
再加上麦克风采集到的声音里混着各种噪声:空调恒定噪声、冰箱启动、窗外车流、电视里的人声,还有用户边说边走动带来的多普勒效应。这些小概率事件在持续监听模式下都会被放大。一个在测试集上唤醒率 98% 的模型,放到客厅里跑 24 小时,误唤醒次数可能就是灾难级别的。
在我转向脉冲神经网络方案之前,用传统 CNN 和 LSTM 也做过几版唤醒系统,准确率其实都不差。真正让我下决心换方案的是功耗和持续监听的矛盾:设备只要开着,DSP 或者 MCU 就得不停做乘加运算,电池掉电速度肉眼可见。这种场景天然适合事件驱动的计算方式,于是我把目光投向了脉冲神经网络。
2. 脉冲神经网络的核心逻辑:不是模拟人脑,是找稀疏时序表达
2.1 从生物神经元到 LIF 模型
脉冲神经网络的基本单元是脉冲神经元,最常用的是 LIF(Leaky Integrate-and-Fire,漏电积分发放)模型。它比传统 LSTM 单元简单得多,却保留了最关键的时间行为。
LIF 的连续性描述可以写成:
tau * dv/dt = -v + I(t)
其中v是膜电位,tau是膜时间常数,I(t)是输入电流。当膜电位累积到阈值Vth时,神经元发放一个脉冲,然后膜电位回落到静息值或减去阈值。
用生活化的类比就是接水杯:水龙头持续往杯子里倒水,但杯子底部有个小漏洞,水会慢慢漏掉。只有当水龙头进水速度超过漏水的速度,水位才会涨到杯沿并溢出。溢出的那一刻就是神经元发放了一个脉冲。
这个“漏电”特性很关键,它让神经元天然具备了时间选择性。一个脉冲输入序列持续作用时,膜电位会累积;但如果输入只是偶尔一个脉冲,膜电位很快就漏光了,不会触发输出。这和语音信号的包络变化有天然的对应关系。
2.2 脉冲编码方式:用离散脉冲表示音频特征
SNN 的输入必须是离散脉冲序列,但麦克风采集到的音频是连续实值波形,所以第一步要把音频信号转成脉冲序列。编码方式直接决定后续网络能学到什么,这里值得多说两句。
我最早尝试的是直接在原始波形上做“阈值穿越编码”:波形超过某个正阈值输出一个正脉冲,低于负阈值输出一个负脉冲。实测效果很差,因为原始波形上的高频噪声会产生大量冗余脉冲,而且语音的许多判别信息在频率维而不是时间维上。脉冲虽然很“生物”,但直接硬编码原始波形并不是好思路。
后来我采用了一条更务实的路线:先提取语音特征,再对特征做脉冲编码。流程是:
- 对 16kHz 采样率的音频分帧,帧长 32ms,帧移 16ms。
- 提取 80 维 log-mel 滤波器组特征,或者直接用 MFCC(梅尔频率倒谱系数)。
- 对特征序列做一阶差分,当差分值超过正负阈值时,在对应频率通道上产生一个正/负脉冲。
这样做的好处是:频率通道之间有区分度,脉冲密度天然稀疏,而且保留了语音在时间维上的变化趋势。像元音起始段、塞音爆破段、共振峰转移这些对唤醒词识别特别重要的瞬态信息,在差分脉冲序列中有明确的表征。
2.3 SNN 真正解决的核心问题:稀疏、时序、脉冲累加
很多人问,SNN 对比普通 DNN 到底赢在哪里。抛开生物学争议不谈,从工程角度看有三点很实在:
稀疏计算:DNN 中,每一层每一个神经元都要参与计算,无论输入是否有信息。SNN 中,一个神经元只有在膜电位累积到阈值时才发放脉冲,输出脉冲本身就是稀疏的。实际测试中,一个训练良好的 SNN 唤醒网络,平均发放率控制在 5% 到 20% 之间,这意味着大部分神经元大部分时间都在“休息”。
时间维度是内建的:DNN 处理时序需要用 RNN、LSTM 或者 Transformer 的位置编码来显式建模时间关系。SNN 的膜电位累积天然提供了时间记忆,脉冲序列的前后顺序和间隔本身就能表达信息,不需要额外的时序建模模块。
乘加运算变成累加运算:DNN 的卷积和全连接层核心是乘法累加(MAC),SNN 的输入是 0/1 脉冲,当脉冲到达时只需要把对应权重累加到膜电位上,也就是纯累加(AC)。在实际芯片上,AC 的能耗只有 MAC 的几分之一,这正是低功耗方案最看重的优势。
3. 搭建一套 SNN 语音唤醒系统的完整流程
3.1 数据准备:正负样本的比例和增强方式
没有数据,再好的网络结构都是空谈。语音唤醒的数据准备比普通语音分类更讲究,因为正负样本极度不平衡。
正样本是唤醒词的音频。一个唤醒词通常包含 2 到 4 个音节,比如“你好小明”“Hey Buddy”。我建议不是只录完整的唤醒词,而是把每个音节单独切出来、再把相邻音节组合起来,模拟用户在真实场景中的语速变化。这样即使用户说得快一点或者慢一点,系统也能识别。
负样本的构成更重要。我收集了四类负样本:
- 其他中文/英文日常短语,特别是包含唤醒词中音节组合的词组,比如唤醒词里有“小”字,就要多收集“上午”“学校”这类发音相近的词。
- 各类噪声:白噪声、粉红噪声、空调声、厨房声、街道声、电视节目声、音乐声。
- 多人同时说话的混叠音频。
- 用户自己的呼吸声、触碰麦克风的摩擦声、手机提示音。
训练时我会以 1:2 到 1:5 的比例在线混合正负样本,并做数据增强:随机加减音量、随机叠加噪声、轻微时移、随机改变音高,目的是让模型不要过拟合某个人的音色。
3.2 前端脉冲编码的实现示意
我在实际项目中用 Python 做前端编码验证,下面是一段简化版代码,演示如何从 log-mel 特征生成脉冲序列。这里不贴完整工程源码,只把核心逻辑讲清楚:
import numpy as np def feature_to_spikes(feats, pos_thresh=0.5, neg_thresh=-0.5): # feats shape: (T, F), T是帧数, F是特征维数 diff = np.diff(feats, axis=0) # 一阶差分, 得到相邻帧变化量 spikes = np.zeros_like(diff, dtype=np.int8) spikes[diff > pos_thresh] = 1 # 正脉冲 spikes[diff < neg_thresh] = -1 # 负脉冲 return spikes这段代码的核心思想是:只有特征变化足够明显的地方才产生脉冲。平稳段没有脉冲,瞬态变化段脉冲密集。阈值0.5是我在 80 维 log-mel 特征上试出来的经验值,如果特征范围差异大,可以对每个频率维度单独做min-max归一化后再用统一阈值,效果会更稳定。
如果不想自己写编码,也可以直接用开源的bsf(Ben's Spiker Algorithm)或者SpikingJelly里的编码器组件。但我的经验是:语音领域直接套用图像领域的泊松编码效果并不好,因为泊松编码的随机采样会破坏语音帧之间本来就很重要的时间相关性。语音更适合“确定性差分编码”而不是“随机采样编码”。
3.3 网络结构:一个小型时序 SNN 分类器
唤醒任务不需要很大的网络。我使用的网络结构分四部分:
- 输入层:接收 F 维脉冲序列,F 通常 80 或 128,时间步长 T 对应 30 到 50 帧。
- 脉冲卷积层:用 3x3 或 1x3 的小卷积核做时频特征提取,卷积权重是实值,但激活是脉冲发放。
- 脉冲全连接层:把卷积输出的脉冲积分成全局特征,这一层同时充当“累积证据”的角色。
- 读出层:不发放脉冲,而是把最后一层神经元的膜电位直接取出来做 softmax 分类。
之所以最后用膜电位而不是脉冲次数,是因为膜电位包含更细粒度的累积信息,直接用脉冲计数会丢失大量阈值以下的信号。这是很多初学者容易忽略的细节。
判决策略上,我采用滑动窗口加积分抑制:每 16ms 滑一步,维护一个 30 帧宽的窗口,窗口内唤醒类别的累计得分超过预设阈值才触发唤醒。触发后进入 2 秒的“冷却期”,这 2 秒内不会再触发第二次,避免唤醒词连续重复导致设备反复闪烁。
3.4 为什么要用很小的模型
有人会问:既然要做唤醒,为什么不用一个大一点的 SNN?唤醒场景的约束是“永远在线”,模型每大一倍,功耗和延迟都会上升。唤醒模型的目标不是代表 SOTA 准确率,而是在功耗、延迟、准确率之间找到平衡点。
我测试过参数量从 10 万到 300 万的几种 SNN,最终选型的依据不是测试集准确率,而是“每 24 小时持续监听下的总功耗和误唤醒次数”。一个 300 万参数的模型唤醒率可能比 50 万参数的高 0.5%,但功耗翻了 3 倍,电池续航从 20 小时掉到 6 小时,这就得不偿失。
4. 训练阶段最容易翻车的三个细节
4.1 代理梯度:脉冲不可导怎么办
SNN 的最大训练难点是脉冲发放函数是阶跃函数,梯度几乎处处为 0,无法直接用反向传播。解决办法是代理梯度(Surrogate Gradient):前向传播仍然使用硬阈值发放脉冲,反向传播时用一个平滑的替代函数来逼近阶跃函数的导数。
我常用的替代函数是一个矩形函数:
当 |v - Vth| < alpha 时,导数取 1/alpha;否则取 0。
代码层面,用snnTorch或者SpikingJelly都能轻松实现。关键超参数是alpha,它控制代理梯度的宽度。alpha 太小,梯度近似太尖锐,训练不稳定;alpha 太大,梯度过于平滑,网络学出来的膜电位分布会很糊。我测试下来,alpha 取 0.3 到 0.5 之间对语音特征比较合适。
训练 LIF 网络时还有一个坑:梯度会沿时间维传播,如果时间步长过长,很容易出现梯度爆炸或者衰减。我建议先用 8 到 16 步训练,稳定之后再慢慢加长时间步。
4.2 从零训练 vs 从 ANN 转换
业内训练 SNN 有两条路线:一是直接训练原生 SNN,二是先训练一个普通 ANN,再把激活函数替换成脉冲发放。
从 ANN 转换的优势是可以复用成熟的训练技巧和大规模预训练模型,但转换后的 SNN 通常需要很长的时间步才能逼近原 ANN 的精度。唤醒这类实时任务对延迟敏感,时间步太长无法接受。我实测过把一个小型 CNN 转成 SNN,精度接近需要 128 个时间步,每步都要做完整推理,延迟直接飙到几百毫秒,完全不适合唤醒场景。
所以我偏向原生训练 SNN。原生训练的收敛速度稍慢,但模型可以在很短的 8 到 16 个时间步内稳定工作,实际推理延迟很小。表 4-1 是我在自建唤醒集上的对比:
| 方案 | 时间步数 | 唤醒率 | 误唤醒/24h | 单次推理延迟 |
|---|---|---|---|---|
| ANN转SNN | 128 | 0.95 | 4 | 约 18ms |
| 原生SNN | 16 | 0.96 | 2 | 约 4ms |
| 原生SNN | 32 | 0.97 | 1 | 约 7ms |
虽然原生 32 步的方案精度最高,但部署时我仍然选了 16 步,因为延迟和功耗更可控,误唤醒次数也完全可以接受。
4.3 阈值、漏电常数和时间步长是联动的
这是我踩得最深的一个坑,单独拿出来说。很多人在 SNN 调参时,把膜电位阈值Vth、漏电常数tau、时间步长T当成互相独立的超参数来调,结果怎么调都调不好。实际上这三者强耦合:
阈值决定敏感度:阈值太高,神经元几乎不发放,梯度也很小,模型学不动;阈值太低,神经元饱和,所有输入都发放,脉冲序列失去区分度。理想的初始阈值是1.0,配合权重初始化使膜电位在阈值附近波动。
漏电常数决定记忆长度:tau越大,神经元对历史输入的记忆越长,但也会累积噪声;tau越小,神经元越“健忘”,只能捕捉最近的输入。语音音节的时长在 200ms 到 500ms 之间,如果时间步长是 16ms,一个音节能持续 10 到 30 步。按这个推算,tau设在 8 到 12 之间比较合适,太短的记忆抓不住音节整体信息。
时间步长决定时间分辨率:时间步长越长,序列建模越精细,但训练难度增大、推理延迟增大。语音唤醒一般不需要超过 50ms 的分辨率,建议时间步长保持在 8ms 到 20ms 之间。
调参时我的经验是:先固定时间步长为 16,只调阈值和漏电常数;等模型收敛后,再优化时间步长。不要一开始就抱着三个参数一起搜索,那会消耗大量实验时间。
5. 部署到边缘设备上的实测情况
5.1 稀疏率带来的硬件收益
SNN 在边缘设备上最有说服力的优势,是计算量高度依赖输入稀疏度。传统 DNN 不管输入的音频是安静还是嘈杂,计算量恒定;SNN 则在安静环境下脉冲密度显著下降,计算量也随之减少。
我的实现中,脉冲卷积层实际只统计非零输入对应的权重累加。在安静起居室场景下,平均脉冲密度约为 8%,在嘈杂街道场景下约为 18%。换算下来,同样的硬件平台,SNN 平均算力占用比同规模 DNN 低 60% 到 70%。
这个数据不一定精确,但趋势非常明显:环境越安静,SNN 省电越多。而语音唤醒的常态恰恰是大部分时间环境安静,只有少数时刻有人在说话,这让 SNN 的功耗模型非常契合实际使用。
5.2 部署时踩过的坑:噪声鲁棒性和双唤醒词矛盾
第一个坑是:SNN 对脉冲密度过于敏感,环境噪声一大,脉冲数量暴增,膜电位被噪声推高,误唤醒概率显著上升。解决思路有两个方向。一个是前端做噪声鲁棒处理,比如在编码前加一个简单的自适应增益控制,让强噪声环境下的特征幅度不至于过大。另一个是在训练阶段特意混入高噪声样本,让网络学会忽略持续噪声脉冲。
第二个坑是双唤醒词。我一开始想在一个模型里同时支持中文和英文两个唤醒词,直接把输出类别设为 3 类(中文词、英文词、其他)。训练时发现两个唤醒词互相干扰,误唤醒率明显上升。后来把决策层改成“共享权重特征提取 + 两套独立的读出层”,相当于两个唤醒词在底层共用声学特征,在顶层各自判断,问题才解决。
这个经验对多语种唤醒特别重要,后面章节我会再展开。
5.3 与传统 DNN 方案的功耗对比
我分别在 ARM Cortex-M4 和一个小型 FPGA 上做了对比实验。没有用最新的专用神经形态芯片,因为手头没有 Loihi 或者 Speck 这类硬件,但普通 MCU 的结果已经能说明问题。
在 Cortex-M4 上,一个 30 万参数的 DNN 唤醒模型持续监听功耗大约是 42mW,同样参数量的 SNN 模型在静音场景下降到 12mW,在连续语音场景约 22mW。唤醒率两者相近,但误唤醒率 SNN 略高,需要多花一些功夫在阈值调节上。
FPGA 上的差距更明显,因为 FPGA 可以充分利用 SNN 的稀疏性做资源调度,DNN 的密集矩阵乘则很难优化。差距大约是 4 倍。
这里要强调一下:SNN 不是银弹。如果任务本身输入总是密集的,比如持续处理满负荷语音流,SNN 的功耗优势会大幅缩小。语音唤醒的特殊价值在于“大多数时间输入稀疏”,这一点决定了它比图像识别等任务更适合用 SNN。
6. 多语种语音唤醒的扩展思考
6.1 语种差异对脉冲编码的影响
多语种语音识别是最近的热词,但放到语音唤醒上,首先要面对的是语言差异对前端的影响。中文唤醒词以单音节和双音节为主,每个音节有明确的声母、韵母和声调;英文唤醒词以多音节单词为主,重音模式是重要特征。
在差分脉冲编码这个浅层特征层面,中英文差异其实没有想象中那么大,因为 log-mel 特征本身是音调频率层面的,共振峰转移、音节边界这些信息都能表达。真正的问题是训练数据覆盖:同一个英文唤醒词,美国人、印度人、中国人的发音节奏和重音位置差异很大,如果只用一个地区的发音训练,到另一个地区误唤醒率会暴涨。
解决思路是给每个语种准备足够的说话人和口音样本,并且在训练时做跨语言的数据混洗。如果一个模型要同时支持中英文唤醒,建议中文和英文唤醒词的时长分布尽量匹配,不要出现中文词很紧凑、英文词很拖沓的极端差异。
6.2 共享前端和迁移学习的思路
多语种场景下,重新从零训练一个 SNN 相当浪费。更好的做法是:
- 先在大规模多语种语音数据上训练一个“脉冲自编码器”,让前端特征提取层学会表达语种无关的声学结构。
- 固定前端层,只训练每个语种对应的读出层。
- 新增加一种语言时,只需要收集该语言唤醒词的少量样本,微调读出层即可,不需要重新训练整个网络。
我在实验中用这个思路把英文模型迁移到一个新的中文唤醒词上,只需要大约 1/10 的标注数据就能达到和从零训练相近的唤醒率。原因是 SNN 前端提取的时频瞬态特征在不同语言之间是共享的,语言差异主要体现在高层组合模式,正好由不同的读出层来处理。
6.3 从“唤醒词”走向“多意图触发”
多语种唤醒再往下走一步,就是不只支持“唤醒词”,而是支持一组指令词:比如“小助手,播放音乐”“小助手,打开灯”。这种场景本质上是一个小规模命令词识别,网络结构和唤醒模型非常接近,只需要把读出层从 2 类改成 N 类,同时在训练数据里加入更丰富的负样本,防止不同指令之间互相误触。
从功耗角度看,指令词识别的计算量和唤醒词相当,用户不用先唤醒再下达指令,交互路径更短。实测下来,用同样的 SNN 结构支撑 10 个指令词,误唤醒率会比单一唤醒词稍高,但控制在可接受范围。对这个方向有兴趣的话,可以从“把唤醒词扩展成短语槽位”的角度来设计数据,而不是简单地在分类层加类别。
一路做下来,我的体会是:脉冲神经网络不是一个为了“类脑”而存在的概念,它在语音唤醒这个具体任务上有真实可量化的优势,只是很多人被训练难度和工具链门槛拦住了。先做一个小的可运行系统,把编码、训练、部署的最小闭环跑通,再逐步调参数,远比把每个理论细节都搞明白再动手高效得多。如果你想在低功耗语音产品上做差异化,SNN 值得你亲手试一遍。