3、音频特征提取(MFCC)
好,咱们进入正题。音频特征提取这一步,说白了就是把声音信号变成模型能看懂的数字。我刚开始做唤醒词的时候,也想过直接用原始波形扔给模型,结果效果惨不忍睹。后来才明白,特征提取这一步做不好,后面再牛的模型也白搭。
3.1 MFCC原理简介
MFCC,全称是梅尔频率倒谱系数。名字挺唬人,其实核心思想很简单:人耳对声音频率的感知不是线性的。低频段我们分得很细,高频段就相对粗糙。MFCC就是模拟这种听觉特性。
具体怎么做的呢?大致分几步:
- 预加重:提升高频部分,补偿语音信号中高频能量的衰减。我习惯用系数0.97,这个值比较通用。
- 分帧:把音频切成一小段一小段,每段25ms左右。帧与帧之间重叠一半,保证连续性。
- 加窗:每帧乘一个汉明窗,减少频谱泄露。嗯,这一步不能省。
- FFT:把时域信号转到频域,得到频谱。
- 梅尔滤波器组:用一组三角滤波器把频谱映射到梅尔刻度上。梅尔刻度和频率的换算公式是:
mel = 2595 * log10(1 + f/700)。 - 取对数:对滤波器输出取log,模拟人耳对响度的对数感知。
- DCT:离散余弦变换,把频谱系数压缩成倒谱系数。通常取前13个。
你看,其实每一步都有物理意义,不是随便拍脑袋定的。
核心要点:MFCC提取的是语音的「音色」特征,对说话内容敏感,对说话人身份和环境噪声有一定鲁棒性。这就是为什么唤醒词模型普遍用它。
3.2 使用librosa提取MFCC特征
librosa这个库,我用了好多年。它把上面那一整套流程封装得特别干净。来看看代码:
import librosa
# 加载音频,采样率统一到16kHz
audio, sr = librosa.load('wake_word.wav', sr=16000)
# 提取13维MFCC
mfcc = librosa.feature.mfcc(
y=audio,
sr=sr,
n_mfcc=13,
n_fft=512,
hop_length=160,
win_length=400
)
print(mfcc.shape) # 输出: (13, 帧数)
这里几个参数我解释一下:
n_mfcc=13:取前13个系数。这是最常用的维度。n_fft=512:FFT窗口大小。对应32ms(512/16000*1000)。hop_length=160:帧移10ms。160个采样点。win_length=400:窗长25ms。400个采样点。
我在项目中遇到过一个问题:不同音频的采样率不一致。有的16k,有的8k,有的44.1k。如果不统一采样率,提取出来的特征维度会乱套。所以我的习惯是,所有音频进来先重采样到16kHz。
小技巧:librosa的load函数默认会重采样到22.05kHz。做唤醒词的话,建议手动指定sr=16000,因为16k对语音来说足够了,还能减少计算量。
3.3 特征维度选择:13维还是39维?
这个问题,我经常被问到。13维是基础MFCC,39维是加了动态特征。
具体来说:
| 维度 | 组成 | 特点 |
|---|---|---|
| 13维 | 13个静态MFCC系数 | 计算快,模型小,适合资源受限设备 |
| 39维 | 13静态 + 13一阶差分 + 13二阶差分 | 包含动态信息,准确率更高,但计算量大 |
差分特征怎么理解?说白了就是看MFCC的变化趋势。一阶差分反映变化速度,二阶差分反映加速度。你想想看,一个词从发音到结束,声学特征是在连续变化的,差分特征正好捕捉了这种变化。
我个人建议:
- 做轻量级唤醒词(比如资源只有几百KB),用13维就够了。
- 做高精度唤醒词,或者有充足算力,用39维。
- 如果拿不准,可以先试13维,效果不满意再加差分。
注意:不要盲目追求高维度。我曾经试过提取52维MFCC,结果模型反而过拟合了。特征不是越多越好,够用就行。
3.4 特征归一化技巧
这一步特别重要,但很多人会忽略。原始MFCC的值范围差异很大,有的系数在-100到100之间,有的只有-5到5。如果不归一化,模型训练时大数值的特征会主导梯度更新,小数值的特征就学不到了。
常用的归一化方法有两种:
方法一:均值方差归一化(Z-score)
import numpy as np
# 假设mfcc形状为 (13, T)
mean = np.mean(mfcc, axis=1, keepdims=True)
std = np.std(mfcc, axis=1, keepdims=True)
mfcc_norm = (mfcc - mean) / (std + 1e-8)
这种方法让每个维度的均值为0,方差为1。我比较推荐这个,因为它保留了特征的相对关系。
方法二:全局归一化
# 对整个数据集统计均值和方差
global_mean = np.mean(all_mfcc_data, axis=(0, 2), keepdims=True)
global_std = np.std(all_mfcc_data, axis=(0, 2), keepdims=True)
mfcc_norm = (mfcc - global_mean) / (global_std + 1e-8)
这种方法的好处是,训练和测试时用同一套统计量,特征分布更稳定。
避坑指南:我曾经犯过一个错误——对每一条音频单独做归一化。结果训练时模型学到的不是语音特征,而是「这条音频的MFCC相对大小」。换到测试集上,效果直接崩了。正确的做法是:用训练集的统计量,去归一化训练集和测试集。
嗯,还有一点要注意。归一化应该在提取完MFCC之后做,而不是在提取之前。因为MFCC的log操作对数值范围敏感,先归一化再取log会破坏特征分布。
最后总结一下我的经验:MFCC提取这件事,参数调好了,模型训练事半功倍。参数没调好,后面怎么折腾都救不回来。所以,花点时间把这一步做扎实,绝对值得。