3、语音特征提取(下):梅尔频率倒谱系数(MFCC)详解、滤波器组(Fbank)特征、动态特征(Δ、ΔΔ)

上一讲我们把 FFT 和梅尔滤波器组聊透了。今天咱们继续往下走,把语音特征提取最后三板斧讲清楚:MFCC、Fbank,还有那些带「Δ」的动态特征。

说实话,这三个东西在实际工程里天天见。我刚开始做语音识别时,总觉得 MFCC 和 Fbank 差不多,随便选一个就行。后来被坑过一次——嗯,具体哪次后面说。总之,它们各有各的脾气,选错了真会掉点。

3.1 滤波器组(Fbank)特征

Fbank,全称 Filter Bank,中文叫滤波器组特征。说白了,它就是梅尔滤波器组输出的能量值。

还记得上节课我们怎么做的吗?把频谱通过一组三角滤波器,每个滤波器输出一个能量值。把这些能量值取个对数,就是 Fbank 特征。

Fbank 特征提取流程:

  1. 预加重 → 分帧 → 加窗
  2. FFT → 功率谱
  3. 梅尔滤波器组滤波
  4. 取对数(log)

输出:每帧一个 M 维向量(M 是滤波器个数,通常 40 或 80)

为什么取对数?两个原因。第一,人耳对声音强度的感知是对数的——响度翻倍,你感觉没翻那么多。第二,取对数能把乘性噪声变成加性噪声,后面做归一化好处理。

我的经验:Fbank 特征在深度学习时代越来越流行。因为 DNN 不需要 MFCC 那步 DCT 去相关,原始 Fbank 信息更完整。我在做唤醒词检测时,用 Fbank 比 MFCC 高了 2 个点。当然,这跟任务有关,不是绝对的。

3.2 梅尔频率倒谱系数(MFCC)

MFCC 就是在 Fbank 基础上多了一步:离散余弦变换(DCT)。

为什么要做 DCT?你想想看,Fbank 各个维度之间是相关的——相邻滤波器有重叠,能量自然有冗余。DCT 能把它们去相关,让信息更紧凑地集中在前几个系数上。

# MFCC 提取核心步骤(Python 伪代码)
import numpy as np
from scipy.fftpack import dct

# 假设 fbank 已经是 log 后的梅尔能量,形状 (帧数, 滤波器数)
fbank = np.log(mel_energies + 1e-10)  # 加小常数防 log(0)

# DCT 变换,通常保留 2-13 阶系数
mfcc = dct(fbank, type=2, axis=1, norm='ortho')
mfcc = mfcc[:, 1:13]  # 去掉第 0 阶(直流分量),保留 12 维

这里有个细节:为什么去掉第 0 阶?第 0 阶代表整帧的平均能量,它受麦克风增益、说话人距离影响很大。去掉它,模型对录音条件的鲁棒性会好很多。

我曾经踩过的坑:有次做说话人识别,MFCC 的 DCT 阶数我选了 20。结果模型在测试集上表现很好,一上线就崩。后来发现,高阶 DCT 系数对噪声特别敏感,泛化能力差。建议语音识别用 12-13 阶,说话人识别可以适当多几阶,但别超过 20。

3.3 MFCC vs Fbank:怎么选?

很多新手会问:到底用哪个?我直接给结论:

对比维度 Fbank MFCC
信息完整性 高(保留全部能量分布) 低(DCT 丢弃了高阶信息)
维度大小 40-80 维 12-13 维
去相关性 弱(相邻维度相关) 强(DCT 去相关)
适用模型 DNN / CNN / Transformer GMM-HMM(传统方法)
噪声鲁棒性 中等 较好(去除了冗余)

我个人习惯:如果是深度学习模型,优先用 Fbank。信息更全,模型自己会学去相关。如果是传统 GMM-HMM 系统,或者特征维度敏感的场景,用 MFCC。

3.4 动态特征:Δ 和 ΔΔ

静态特征只描述当前帧,但语音是连续的——前一帧和后一帧有关系。Δ 特征就是用来捕捉这种时序变化的。

Δ 的计算很简单:当前帧减去前一帧,或者用更平滑的回归公式。

# Δ 特征计算(一阶差分)
def delta(feats, window=2):
    """feats: (帧数, 特征维度)"""
    num_frames = feats.shape[0]
    delta_feats = np.zeros_like(feats)
    
    for t in range(num_frames):
        # 用前后 window 帧做线性回归
        numerator = 0
        denominator = 0
        for w in range(1, window + 1):
            if t - w >= 0:
                numerator += w * feats[t - w]
                denominator += w * w
            if t + w < num_frames:
                numerator -= w * feats[t + w]
                denominator += w * w
        if denominator > 0:
            delta_feats[t] = numerator / denominator
    
    return delta_feats

# ΔΔ 就是再对 Δ 做一次差分
delta_delta = delta(delta_feats)

为什么需要 Δ 和 ΔΔ?你想想看,静态 MFCC 只告诉你「这帧长什么样」,Δ 告诉你「它在往哪个方向变」,ΔΔ 告诉你「变化的速度是快还是慢」。三个合起来,模型才能感知到语音的动态模式。

标准做法:最终特征 = [静态特征, Δ, ΔΔ],维度变成原来的 3 倍。

例如:13 维 MFCC → 39 维(13 + 13Δ + 13ΔΔ)

40 维 Fbank → 120 维(40 + 40Δ + 40ΔΔ)

3.5 完整特征提取流程

把今天讲的串起来,完整的流程是这样的:

语音特征提取完整流程 原始语音 预加重 + 分帧 + 加窗 FFT → 功率谱 梅尔滤波器组 取对数 (log) Fbank 特征 DCT → MFCC Δ + ΔΔ 计算 最终特征:静态 + Δ + ΔΔ 分叉点:log 之后

实用建议:实际工程中,我通常这样配:

  • 语音识别:40 维 Fbank + Δ + ΔΔ = 120 维
  • 说话人识别:20 维 MFCC + Δ + ΔΔ = 60 维
  • 情感识别:80 维 Fbank + Δ + ΔΔ = 240 维(情感需要更细的频谱信息)

3.6 避坑指南

最后分享几个我踩过的坑,希望你能绕开:

  • 归一化顺序:先做特征提取,再做全局归一化。千万别在分帧前归一化——每帧的均值方差会变,特征就不一致了。
  • Δ 窗口大小:默认 window=2 够用。窗口太大,动态信息被平滑掉了;窗口太小,噪声会被放大。
  • DCT 阶数选择:别贪多。12 阶 MFCC 在大多数任务上已经够用。多出来的阶数往往是噪声。
  • 采样率匹配:16kHz 语音用 40 个滤波器,8kHz 语音用 20-25 个。滤波器个数要和频率范围匹配。

好了,特征提取这块就讲完了。这三种特征——Fbank、MFCC、动态特征——是语音算法的基本功。你把它吃透了,后面无论是做识别、唤醒还是声纹,都会顺手很多。