3、特征提取入门:MFCC特征提取全流程、FBank与MFCC的对比、特征归一化技巧
各位同学,欢迎来到第三章。
前两章我们聊了语音唤醒的整体架构和前端信号处理。今天要讲的,是整个唤醒系统里最核心的环节之一——特征提取。
说白了,就是把麦克风采集到的原始波形,变成计算机能听懂、能建模的数学特征。你想想看,原始音频每秒16000个采样点,直接扔给神经网络?那计算量谁也扛不住。所以我们需要一套方法,把有用的信息提炼出来,把没用的噪音扔掉。
我个人习惯把特征提取比作「厨师备菜」。食材(原始音频)再好,不经过切配、腌制、调味,直接下锅炒,味道肯定不对。MFCC和FBank,就是两种经典的「备菜刀法」。
3.1 MFCC特征提取全流程
MFCC,全称Mel频率倒谱系数。名字听着唬人,其实拆开看就三件事:模拟人耳听觉 → 取对数 → 做DCT变换。
为什么模拟人耳?因为人耳对频率的感知不是线性的。低频区域我们分辨得很细,高频区域则比较粗糙。Mel刻度就是用来模拟这个特性的。
完整的MFCC提取流程如下:
- 预加重:提升高频分量,补偿口唇辐射造成的衰减。公式很简单:
y[n] = x[n] - 0.97 * x[n-1]。0.97这个系数,我一般取0.97到0.98之间,具体看采样率。 - 分帧:把长信号切成短帧,每帧20-40ms。帧移通常取帧长的一半。比如帧长25ms,帧移10ms。
- 加窗:常用汉明窗,减少频谱泄露。嗯,这里要注意,窗函数的选择会影响旁瓣抑制效果。
- FFT:对每帧做快速傅里叶变换,得到频谱。
- Mel滤波器组:将频谱通过一组三角滤波器,每个滤波器对应一个Mel刻度。滤波器个数通常取20-40个。
- 取对数:对每个滤波器的输出取log,模拟人耳对响度的非线性感知。
- DCT:对log能量做离散余弦变换,得到倒谱系数。通常保留前12-13个系数。
- 动态特征:加上一阶差分和二阶差分,捕捉帧间变化。
核心要点:MFCC的精华在于DCT这一步。它把高度相关的Mel滤波器输出解相关,让特征更紧凑。这在GMM-HMM时代是神技,但在深度学习时代,这个解相关操作反而可能丢掉信息。
我曾经在一个车载项目里,为了追求极致的唤醒率,把MFCC的DCT系数从13个加到26个。结果呢?唤醒率没提升多少,模型大小倒是翻了一倍。后来我反思,其实对于深度神经网络来说,保留更多原始信息往往比「解相关」更重要。
3.2 FBank与MFCC的对比
FBank,全称Filter Bank,也叫滤波器组特征。它和MFCC的前半段流程完全一样,直到Mel滤波器组输出log能量后,不做DCT,直接作为特征使用。
所以两者的区别,说白了就是一句话:MFCC = FBank + DCT。
| 对比维度 | FBank | MFCC |
|---|---|---|
| 特征维度 | 较高(40-80维) | 较低(13-26维) |
| 相关性 | 相邻维度高度相关 | 维度间解相关 |
| 计算量 | 较小(少一步DCT) | 稍大(多一步DCT) |
| 适用模型 | DNN/CNN/Transformer | GMM-HMM/传统方法 |
| 噪声鲁棒性 | 一般 | 较好(DCT有去噪效果) |
我个人建议:做语音唤醒,优先选FBank。为什么?因为现在的唤醒模型基本都是DNN或CNN,它们不害怕特征维度之间的相关性。相反,FBank保留了完整的频谱结构,模型能学到更丰富的模式。
我的经验:在安静环境下,MFCC和FBank的唤醒率差距不大。但在车载这种强噪声场景,FBank往往比MFCC高出2-3个百分点的唤醒率。我猜是因为DCT丢掉了某些对噪声敏感的细节信息。
当然,如果你用的是传统GMM-HMM模型,或者特征维度有严格限制(比如某些低功耗芯片),那MFCC还是更合适的选择。
3.3 特征归一化技巧
特征提取完了,直接扔给模型?别急,还有一步关键操作——归一化。
归一化的目的,是让特征的分布更稳定。你想想看,不同人说话音量不一样,不同麦克风灵敏度不一样,不同环境噪声底噪不一样。如果不做归一化,模型就得自己去适应这些变化,训练难度会大很多。
常用的归一化方法有三种:
- 均值归一化(Mean Normalization):每个特征维度减去均值。简单粗暴,能消除静态偏移。
- 方差归一化(Variance Normalization):每个特征维度除以标准差。让所有维度的动态范围一致。
- 全局归一化(Global Normalization):对整个特征矩阵做归一化。适合数据分布比较均匀的场景。
在语音唤醒里,我最常用的是在线均值方差归一化。具体做法是:维护一个滑动窗口,实时计算当前帧前N帧的均值和方差,然后用这个统计量对当前帧做归一化。
# 伪代码示例:在线均值方差归一化
class OnlineNormalizer:
def __init__(self, window_size=100):
self.window = []
self.window_size = window_size
def normalize(self, feature):
self.window.append(feature)
if len(self.window) > self.window_size:
self.window.pop(0)
mean = np.mean(self.window, axis=0)
std = np.std(self.window, axis=0) + 1e-6
return (feature - mean) / std
避坑指南:我曾经在量产项目里踩过一个坑——用全局统计量做归一化。结果呢?在安静环境采集的统计量,到了高速场景完全不适用,特征分布偏移严重,唤醒率直接掉了10个点。后来改成在线归一化,问题才解决。
另外,还有一个容易被忽略的细节:归一化应该在特征提取之后、模型推理之前做。千万别在分帧或FFT阶段就做归一化,那会破坏信号的物理意义。
嗯,这里再补充一点。对于FBank特征,我建议只做均值归一化,不做方差归一化。因为FBank的每个维度代表不同频带的能量,方差本身携带了重要的动态信息。强行拉平方差,反而会抹掉不同频带之间的能量对比关系。
好了,这一章的内容就到这里。特征提取是语音唤醒的「地基」,地基打不牢,后面的模型再花哨也没用。下一章,我们会进入声学模型的世界,聊聊怎么用神经网络把特征变成唤醒决策。
记住:好的特征 + 合适的归一化 = 唤醒系统成功的一半。