3. 数字信号处理核心
各位同学,欢迎来到特征提取实战这一章。说实话,特征提取是语音算法里最「实在」的一环——你模型再花哨,喂进去的特征不行,结果就是白搭。我当年刚入行时,花了两周调模型结构,结果发现是MFCC的参数没设对,真是欲哭无泪。
这一章我们不讲虚的,直接上手。你会亲手把一段音频变成MFCC、Fbank、Pitch、Delta这些特征,再学会怎么归一化和增强。嗯,咱们开始吧。
核心观点:特征提取不是「选一个最好的」,而是「根据任务组合最合适的」。语音识别用MFCC+Fbank,说话人识别用Fbank+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)
具体步骤:
- 对每帧做 FFT,得到幅度谱
- 用 Mel 滤波器组(通常 40 个三角滤波器)加权求和
- 取对数(模拟人耳的对数感知)
结果就是 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 的权重。