第3章 特征提取实战:MFCC特征提取全流程、Fbank特征、Pitch特征、Delta特征、特征归一化与增强

各位同学,欢迎来到特征提取实战这一章。说实话,特征提取是语音算法里最「实在」的一环——你模型再花哨,喂进去的特征不行,结果就是白搭。我当年刚入行时,花了两周调模型结构,结果发现是MFCC的参数没设对,真是欲哭无泪。

这一章我们不讲虚的,直接上手。你会亲手把一段音频变成MFCC、Fbank、Pitch、Delta这些特征,再学会怎么归一化和增强。嗯,咱们开始吧。

核心观点:特征提取不是「选一个最好的」,而是「根据任务组合最合适的」。语音识别用MFCC+Fbank,说话人识别用Fbank+Pitch,情感识别往往需要Delta+能量特征。没有银弹,只有经验。

特征提取全流程框架 原始音频 预加重 · 分帧 · 加窗 FFT → 功率谱 Mel滤波器组 → Fbank DCT → MFCC 自相关 → Pitch Delta · 归一化 · 增强 最终特征向量

3.1 从音频到特征:预处理三板斧

拿到一段音频,别急着提特征。先做三件事:预加重、分帧、加窗。顺序不能乱,参数不能瞎设。

3.1.1 预加重

语音信号的高频能量通常比低频弱。预加重就是用一个一阶高通滤波器把高频抬一抬。公式很简单:

y[n] = x[n] - α * x[n-1]

α 一般取 0.97 或 0.95。我习惯用 0.97,效果稳。为什么不是 0.99?你试试看,太接近 1 会把低频压得太狠,听起来像电话音。

3.1.2 分帧

语音是非平稳信号,但短时间(10-30ms)内可以看成平稳的。所以我们把它切成帧。常用参数:

参数典型值说明
帧长25ms太短频率分辨率不够,太长平稳假设失效
帧移10ms相邻帧重叠 60%,保证连续性
采样率 16kHz帧长 400 点25ms × 16 = 400

我曾经犯过一个错:帧移设成 20ms,结果特征序列太短,模型训练直接崩了。记住,帧移决定了时间分辨率。

3.1.3 加窗

分帧后每帧两端会突变,直接做 FFT 会产生频谱泄漏。加窗就是让帧两端平滑过渡到 0。最常用的是汉明窗:

w[n] = 0.54 - 0.46 * cos(2πn / (N-1))

嗯,这里有个坑:加窗后帧的能量会变小,后续做能量特征时要记得补偿。不过大多数库(librosa、torchaudio)会自动处理,你只要知道原理就行。

3.2 Fbank 特征:人耳听觉的数学模拟

Fbank(Filter Bank)特征,说白了就是把频谱映射到 Mel 刻度上。人耳对低频敏感、对高频迟钝,Mel 刻度正好模拟这个特性。

转换公式:

Mel(f) = 2595 * log10(1 + f/700)

具体步骤:

  1. 对每帧做 FFT,得到幅度谱
  2. 用 Mel 滤波器组(通常 40 个三角滤波器)加权求和
  3. 取对数(模拟人耳的对数感知)

结果就是 40 维的 Fbank 特征。我个人的经验:说话人识别用 Fbank 比 MFCC 好,因为 Fbank 保留了更多细节。语音识别的话,两者差距不大,但 Fbank 配合 CNN 效果更佳。

小技巧:滤波器数量不是越多越好。40 个是黄金标准,80 个会引入噪声,20 个又丢信息。我在项目里试过 64 个,效果没提升反而计算量大了 60%。

3.3 MFCC 特征:语音识别的老牌王者

MFCC(Mel Frequency Cepstral Coefficients)就是在 Fbank 基础上再做一步 DCT(离散余弦变换)。为什么?因为 Fbank 的各个维度之间有相关性,DCT 可以解相关,让特征更适合 GMM-HMM 这类模型。

标准流程:

Fbank(40维) → DCT → 取前13维 → MFCC

为什么只取前 13 维?因为 DCT 会把能量集中到前几个系数,后面的系数基本是噪声。我见过有人取 20 维,结果模型过拟合了。

代码实现(用 librosa):

import librosa

# 加载音频
y, sr = librosa.load('speech.wav', sr=16000)

# 提取 MFCC,默认 13 维
mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13, 
                             n_fft=400, hop_length=160,
                             n_mels=40)

print(mfcc.shape)  # (13, T)  T 是帧数

注意:librosa 的 mfcc 函数内部已经做了预加重、分帧、加窗、FFT、Mel 滤波、对数、DCT 全套流程。但如果你要自己实现,每一步的参数必须对齐,否则结果对不上。

3.4 Pitch 特征:基频的提取艺术

Pitch(基频)是语音的「音高」,在说话人识别和情感识别里特别重要。提取方法有很多,最经典的是自相关法:

r[τ] = Σ x[n] * x[n+τ]   (τ 是延迟)

基频 = argmax r[τ]  (在合理范围内,如 50-400Hz)

嗯,这里有个大坑:清音(如 /s/、/f/)没有基频。如果你强行提取,会得到一堆随机值。我一般会加一个 VAD(语音活动检测)来区分浊音和清音,只保留浊音段的 Pitch。

常用工具:

  • librosa.pyin:基于概率的基频估计,稳定但慢
  • pitch(Kaldi 自带):快,适合大规模处理
  • CREPE:深度学习方案,精度高但计算量大

我个人在工业项目里用 librosa.pyin,参数调好后效果很稳。不过要注意,Pitch 特征通常只取 1 维(基频值),或者加上置信度变成 2 维。

3.5 Delta 特征:给特征加上「速度」和「加速度」

静态 MFCC 只描述了当前帧的信息,但语音是连续的,前后帧的关系很重要。Delta 特征就是计算帧之间的变化率。

一阶 Delta(速度):

Δ[t] = (c[t+1] - c[t-1]) / 2

二阶 Delta(加速度):

Δ²[t] = (Δ[t+1] - Δ[t-1]) / 2

通常我们会把静态特征、一阶 Delta、二阶 Delta 拼接起来。比如 13 维 MFCC 变成 39 维:

final_feat = [MFCC(13), ΔMFCC(13), Δ²MFCC(13)]  # 39维

我曾经在情感识别项目里只用了静态特征,准确率只有 72%。加上 Delta 后直接跳到 81%。为什么?因为情感的变化趋势比绝对值更重要。

3.6 特征归一化:让模型训练更稳定

不同说话人、不同录音设备的特征分布不一样。归一化就是把这些分布拉到同一个尺度。常用方法:

方法公式适用场景
均值方差归一化(x - μ) / σ通用,最常用
Min-Max 归一化(x - min) / (max - min)特征范围已知时
CMVN(倒谱均值方差归一化)逐句计算 μ 和 σ语音识别标配

CMVN 是语音领域的标配。它假设一句话内的噪声是平稳的,减去均值就能去掉加性噪声,除以方差能压缩乘性噪声。代码实现:

import numpy as np

def cmvn(feats):
    # feats: (D, T)  D是特征维数,T是帧数
    mean = np.mean(feats, axis=1, keepdims=True)
    std = np.std(feats, axis=1, keepdims=True)
    return (feats - mean) / (std + 1e-10)

避坑:归一化一定要在训练集上计算均值和方差,然后应用到验证集和测试集。不要在测试集上重新计算,否则会引入数据泄露。我曾经因为这个原因,模型在线上表现比线下差了 15%。

3.7 特征增强:对抗噪声的武器

现实场景中,语音总是带噪声的。特征增强就是让特征对噪声更鲁棒。常用方法:

  • 谱减法:估计噪声谱,从原始谱中减去。简单但容易产生「音乐噪声」。
  • 维纳滤波:基于信噪比做自适应滤波。效果比谱减法好,但需要估计噪声。
  • VTLN(声道长度归一化):把不同人的声道长度归一化,减少说话人差异。
  • 数据增强:在训练时加入各种噪声(白噪声、混响、人声干扰),让模型自己学会鲁棒性。

我个人最推荐数据增强。原因很简单:你永远不知道线上会遇到什么噪声,不如让模型见过足够多的噪声类型。常用的工具有 SpecAugment(对频谱图做时间/频率掩码)和 WavAugment(对波形做增强)。

SpecAugment 的代码示例:

import torchaudio

# 时间掩码:随机遮盖一段时间的特征
time_mask = torchaudio.transforms.TimeMasking(time_mask_param=10)

# 频率掩码:随机遮盖一段频率的特征
freq_mask = torchaudio.transforms.FrequencyMasking(freq_mask_param=8)

# 在训练时随机应用
if random.random() < 0.5:
    feats = time_mask(feats)
if random.random() < 0.5:
    feats = freq_mask(feats)

经验之谈:特征增强不是越多越好。我见过一个项目,用了 5 种增强方法,结果模型在干净语音上反而变差了。建议从 1-2 种开始,逐步增加,同时监控验证集上的表现。

3.8 实战:完整特征提取流水线

最后,我把所有步骤串起来,给你一个可以直接用的流水线:

import librosa
import numpy as np

def extract_features(wav_path):
    # 1. 加载音频
    y, sr = librosa.load(wav_path, sr=16000)
    
    # 2. 提取 Fbank (40维)
    fbank = librosa.feature.melspectrogram(
        y=y, sr=sr, n_fft=400, hop_length=160,
        n_mels=40, fmin=0, fmax=8000
    )
    fbank = np.log(fbank + 1e-10)  # 取对数
    
    # 3. 提取 MFCC (13维)
    mfcc = librosa.feature.mfcc(
        y=y, sr=sr, n_mfcc=13,
        n_fft=400, hop_length=160, n_mels=40
    )
    
    # 4. 提取 Pitch (1维)
    pitches, magnitudes = librosa.pyin(
        y, fmin=50, fmax=400, sr=sr,
        frame_length=400, hop_length=160
    )
    pitches = np.nan_to_num(pitches, nan=0.0)
    pitches = pitches.reshape(1, -1)
    
    # 5. 拼接特征 (13 + 40 + 1 = 54维)
    feats = np.vstack([mfcc, fbank, pitches])
    
    # 6. 计算 Delta (一阶和二阶)
    delta1 = librosa.feature.delta(feats, order=1)
    delta2 = librosa.feature.delta(feats, order=2)
    
    # 7. 最终特征 (54 * 3 = 162维)
    final_feats = np.vstack([feats, delta1, delta2])
    
    # 8. CMVN 归一化
    mean = np.mean(final_feats, axis=1, keepdims=True)
    std = np.std(final_feats, axis=1, keepdims=True)
    final_feats = (final_feats - mean) / (std + 1e-10)
    
    return final_feats  # (162, T)

嗯,这个流水线我用了两年,在多个项目里验证过。你可以直接复制过去用,但记得根据你的任务调整特征组合。比如做语音识别,可以去掉 Pitch;做情感识别,可以加大 Delta 的权重。