3. 语音识别基础:语音信号处理流程、关键词唤醒(KWS)与命令词识别(ASR)的基本原理
好,咱们进入第三章。这一章聊语音识别,但我不打算跟你扯太深的理论。说白了,在嵌入式设备上做语音识别,跟云端那套完全是两码事。资源有限、算力吃紧,你得学会“戴着镣铐跳舞”。
我个人习惯把语音识别拆成两大块:关键词唤醒(KWS)和命令词识别(ASR)。前者是让设备“听到你叫它”,后者是让设备“听懂你在说什么”。嗯,咱们一个一个来。
3.1 语音信号处理流程:从麦克风到数字信号
你想想看,麦克风采集到的是一段连续的模拟信号。但MCU只认0和1。所以第一步,就是把这玩意儿变成数字信号。
流程大致是这样的:
- 采样:把连续时间信号变成离散时间信号。我建议采样率至少8kHz,做语音识别的话16kHz比较稳妥。
- 量化:把连续的幅度值变成离散的二进制数。16位量化是主流。
- 预加重:提升高频分量。为什么?因为语音信号的高频能量通常偏弱,不处理的话容易被噪声淹没。
- 分帧:语音信号是非平稳的,但短时间(20-30ms)内可以认为是平稳的。所以切成帧来处理。
- 加窗:每帧乘以一个窗函数(比如汉明窗),减少频谱泄露。
- 特征提取:把时域信号转成频域特征,最常用的是MFCC(梅尔频率倒谱系数)。
核心要点:特征提取这一步,直接决定了后续识别的准确率。我在项目中遇到过,有人直接用原始波形做识别,结果噪声一上来就崩了。后来换成MFCC,效果立竿见影。
这里给一段简单的特征提取伪代码,帮你理解流程:
// 伪代码:MFCC特征提取
function extractMFCC(audioFrame):
// 1. 预加重
preEmphasized = audioFrame - 0.97 * shift(audioFrame, 1)
// 2. 加汉明窗
windowed = preEmphasized * hammingWindow(len(audioFrame))
// 3. FFT
spectrum = FFT(windowed)
// 4. Mel滤波器组
melSpectrum = melFilterBank(spectrum)
// 5. 取对数
logMel = log(melSpectrum)
// 6. DCT(离散余弦变换)
mfcc = DCT(logMel)
return mfcc[0:13] // 通常取前13维系数
我的经验:MFCC的维数不是越高越好。在嵌入式设备上,13维足够了。我曾经试过取26维,结果识别率没提升多少,内存倒是翻了一倍。
3.2 关键词唤醒(KWS):让设备“听到你叫它”
关键词唤醒,说白了就是让设备一直监听,直到听到某个特定的词(比如“小爱同学”、“Hey Siri”)。
为什么需要KWS?因为你不能让设备一直做完整的语音识别——那太耗电了。KWS是一个轻量级的检测器,它只做一件事:判断当前音频里有没有唤醒词。
常见的KWS方案有:
- 基于模板匹配:把唤醒词的声学特征存成模板,实时音频的特征跟模板做相似度计算。简单,但鲁棒性差。
- 基于DNN(深度神经网络):训练一个二分类模型,输出“是唤醒词”或“不是唤醒词”。准确率高,但需要一定算力。
- 基于TCN(时序卷积网络):轻量级,适合MCU。我最近在项目里用的就是这种。
避坑指南:我曾经在KWS上踩过一个坑——误唤醒率太高。设备在电视声、音乐声里频繁被唤醒。后来加了语音活动检测(VAD)做前置过滤,只对有人声的片段做KWS,误唤醒率直接降了80%。
KWS的典型工作流程:
- 麦克风持续采集音频,送入环形缓冲区。
- 每20ms取一帧,做VAD判断是否有人声。
- 如果有人声,提取MFCC特征。
- 把特征送入KWS模型,计算唤醒概率。
- 如果概率超过阈值(比如0.8),触发唤醒事件。
3.3 命令词识别(ASR):让设备“听懂你在说什么”
唤醒之后,设备开始听你的命令。比如“打开灯”、“播放音乐”。这就是命令词识别。
注意,这里说的ASR不是那种大词库的连续语音识别(比如云端那种能识别几千个词的)。嵌入式ASR通常是有限词表的,一般就几十个到几百个命令词。
为什么?因为资源限制。你想想看,一个Cortex-M4的MCU,RAM可能只有256KB,跑一个完整的语音识别模型?不现实。
常见的嵌入式ASR方案:
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| DTW(动态时间规整) | 把待识别语音与模板做对齐匹配 | 无需训练,实现简单 | 对噪声敏感,词表大了性能下降 |
| HMM(隐马尔可夫模型) | 用状态转移概率建模语音 | 鲁棒性较好 | 训练复杂,需要大量数据 |
| 端到端DNN | 直接映射声学特征到文本 | 准确率高 | 需要较大算力和内存 |
我个人比较推荐DTW + 小词表的组合。为什么?因为简单、可靠、资源占用低。我在一个智能灯控项目里就用DTW,词表只有20个命令词,识别率能做到95%以上。
关键点:DTW的核心是计算两个序列的最短距离。你只需要把用户说的命令词跟每个模板做DTW匹配,选距离最小的那个作为识别结果。
DTW的简化实现思路:
// 伪代码:DTW距离计算
function dtwDistance(testSeq, templateSeq):
n = len(testSeq)
m = len(templateSeq)
// 初始化距离矩阵
dtw = matrix(n+1, m+1, INF)
dtw[0][0] = 0
// 动态规划填充
for i = 1 to n:
for j = 1 to m:
cost = euclideanDistance(testSeq[i], templateSeq[j])
dtw[i][j] = cost + min(dtw[i-1][j], dtw[i][j-1], dtw[i-1][j-1])
return dtw[n][m]
我的建议:实际项目中,DTW的模板不要只录一次。我习惯每个命令词录3-5次,取平均或者做聚类,这样能覆盖不同人的发音差异。
3.4 从KWS到ASR的联动
好,现在把KWS和ASR串起来。整个流程是这样的:
- 设备处于休眠监听状态,只跑KWS。
- KWS检测到唤醒词,触发中断。
- 设备进入命令接收状态,启动ASR。
- ASR识别出命令词,执行对应操作(比如开灯)。
- 执行完毕后,回到休眠监听状态。
这里有个细节:唤醒后的语音段怎么截取?我见过有人用固定时长(比如2秒),但这样容易截断或包含多余噪声。更好的做法是用端点检测(VAD)来自动判断语音的起止点。
曾经踩过的坑:有一次,KWS唤醒后ASR一直识别失败。排查了半天,发现是唤醒词末尾的尾音被截到了命令词的开头。后来我在KWS和ASR之间加了一个静音检测,等用户说完唤醒词、停顿一下再说命令词,问题就解决了。
3.5 资源优化建议
嵌入式语音识别,资源永远是瓶颈。给你几个我常用的优化技巧:
- 特征降维:MFCC只取前8-13维,别贪多。
- 模型量化:把浮点模型转成8位整型,体积缩小4倍,速度提升2-3倍。
- 帧移加大:KWS阶段帧移可以设到40ms(正常是10ms),减少计算量。
- 分级唤醒:先用简单的能量检测过滤掉大部分噪声,再跑KWS模型。
嗯,这一章的内容就到这。语音识别在嵌入式上是个“戴着镣铐跳舞”的活儿,但掌握了KWS和ASR的基本原理,你就能在资源受限的环境下做出好用的产品。下一章咱们聊网络通信,把语音识别的结果通过网络发出去,实现真正的联动控制。