3、特征提取入门:MFCC特征提取全流程、FBank与MFCC的对比、特征归一化技巧

各位同学,欢迎来到第三章。

前两章我们聊了语音唤醒的整体架构和前端信号处理。今天要讲的,是整个唤醒系统里最核心的环节之一——特征提取

说白了,就是把麦克风采集到的原始波形,变成计算机能听懂、能建模的数学特征。你想想看,原始音频每秒16000个采样点,直接扔给神经网络?那计算量谁也扛不住。所以我们需要一套方法,把有用的信息提炼出来,把没用的噪音扔掉。

我个人习惯把特征提取比作「厨师备菜」。食材(原始音频)再好,不经过切配、腌制、调味,直接下锅炒,味道肯定不对。MFCC和FBank,就是两种经典的「备菜刀法」。

3.1 MFCC特征提取全流程

MFCC,全称Mel频率倒谱系数。名字听着唬人,其实拆开看就三件事:模拟人耳听觉 → 取对数 → 做DCT变换

为什么模拟人耳?因为人耳对频率的感知不是线性的。低频区域我们分辨得很细,高频区域则比较粗糙。Mel刻度就是用来模拟这个特性的。

完整的MFCC提取流程如下:

  1. 预加重:提升高频分量,补偿口唇辐射造成的衰减。公式很简单:y[n] = x[n] - 0.97 * x[n-1]。0.97这个系数,我一般取0.97到0.98之间,具体看采样率。
  2. 分帧:把长信号切成短帧,每帧20-40ms。帧移通常取帧长的一半。比如帧长25ms,帧移10ms。
  3. 加窗:常用汉明窗,减少频谱泄露。嗯,这里要注意,窗函数的选择会影响旁瓣抑制效果。
  4. FFT:对每帧做快速傅里叶变换,得到频谱。
  5. Mel滤波器组:将频谱通过一组三角滤波器,每个滤波器对应一个Mel刻度。滤波器个数通常取20-40个。
  6. 取对数:对每个滤波器的输出取log,模拟人耳对响度的非线性感知。
  7. DCT:对log能量做离散余弦变换,得到倒谱系数。通常保留前12-13个系数。
  8. 动态特征:加上一阶差分和二阶差分,捕捉帧间变化。

核心要点: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的每个维度代表不同频带的能量,方差本身携带了重要的动态信息。强行拉平方差,反而会抹掉不同频带之间的能量对比关系。

好了,这一章的内容就到这里。特征提取是语音唤醒的「地基」,地基打不牢,后面的模型再花哨也没用。下一章,我们会进入声学模型的世界,聊聊怎么用神经网络把特征变成唤醒决策。

记住:好的特征 + 合适的归一化 = 唤醒系统成功的一半