1. 从人耳到算法:为什么需要梅尔倒谱系数
做语音相关项目的人,迟早会撞上MFCC这个词。不管你是做语音识别、声纹比对、情感计算,还是只是想做个简单的关键词唤醒,翻开源码或者论文,MFCC几乎无处不在。但很多人第一次看到"梅尔倒谱系数"这五个字的时候,脑子里大概是一片空白——梅尔是什么?倒谱又是什么?为什么偏偏是它成了语音特征提取的默认选项?
我刚开始接触语音信号处理的时候也有同样的困惑。那时候我拿到一段音频,第一反应是直接上FFT看频谱,觉得频域信息已经够用了。结果拿去做分类,效果一塌糊涂。后来才明白,问题出在"人耳不是频谱分析仪"这件事上。人耳对频率的感知是非线性的,它对低频的变化非常敏感,对高频的变化则相对迟钝。换句话说,同样是从200Hz变到300Hz,和从8000Hz变到8100Hz,前者人耳能明显察觉,后者几乎听不出区别。而FFT给出的频谱是线性的,它把这两个变化同等对待,这就导致特征和人耳的感知严重不匹配。
MFCC做的事情,本质上就是模拟人耳的这套非线性感知机制,把原始频谱"压缩"成一组更符合听觉特性的系数。它的核心思路可以拆成三步:先把线性频率映射到梅尔刻度上,然后在梅尔刻度上做滤波,最后通过倒谱分析把频谱的包络和细节分离开。这三步每一步都有它的道理,缺一不可。
这篇文章我打算从工程实操的角度,把MFCC的来龙去脉讲清楚。不是那种堆公式的教科书式讲法,而是按照一个从业者在实际项目中会遇到的顺序来展开:先搞明白每一步在干什么、为什么这么干,再动手把整个流程实现一遍,最后聊聊实际调参时踩过的坑和总结出来的经验。适合已经有一点信号处理基础、但还没系统做过语音特征提取的读者,也适合那些用过现成库但想搞清楚底层原理的人。
2. 梅尔刻度与倒谱:两个核心概念的前世今生
2.1 梅尔刻度到底在模拟什么
梅尔刻度(Mel Scale)这个概念最早来自心理声学实验。研究者让听音者对不同频率的音调做主观判断,找出"听起来等距"的频率点,然后拟合出一条映射曲线。这条曲线大致是这样的:在1000Hz以下,梅尔值和线性频率差不多是线性关系;超过1000Hz之后,频率每翻一倍,梅尔值的增量越来越小。
常用的换算公式有好几个版本,工程上最常用的是这个:
mel = 2595 * log10(1 + f / 700)反过来:
f = 700 * (10^(mel / 2595) - 1)这里的700这个常数不是随便拍的,它决定了曲线的弯曲程度。你可以把它理解成一个"拐点参数"——在700Hz附近,线性频率和梅尔频率的对应关系开始明显偏离线性。
我个人的理解是,梅尔刻度本质上是一种"感知加权"。它把低频区域拉伸、高频区域压缩,使得在梅尔域上等距的间隔,在人耳听来也大致等距。这就为后续的特征提取提供了一个和人耳感知对齐的坐标系。
注意:不同工具库用的梅尔公式可能略有差异,比如有的用自然对数,有的用log10,常数也可能不同。做跨库对比实验时一定要确认公式版本,否则特征值会有系统性偏差。
2.2 倒谱的直觉理解:把卷积变成加法
倒谱(Cepstrum)这个词本身就是spectrum的前四个字母倒过来拼的,中文翻译成"倒谱"也是取了"倒"的意思。它的定义是:对信号频谱取对数后,再做一次逆傅里叶变换。
为什么要这么绕?核心原因在于,语音信号可以近似看作激励源和声道滤波器的卷积。激励源是声带振动产生的周期脉冲(浊音)或湍流噪声(清音),声道滤波器则是由口腔、鼻腔等构成的共振系统。在时域上,这两者是卷积关系;到了频域,卷积变成乘法;再取对数,乘法就变成加法。
这一步"取对数"是整个倒谱分析的精髓。一旦变成加法,我们就可以用线性方法把两个分量分离开。具体来说,声道滤波器的频谱包络变化比较缓慢,对应倒谱域的低时间索引部分;而激励源的精细结构变化剧烈,对应倒谱域的高时间索引部分。MFCC只取前面的低阶系数,实际上就是在提取声道包络信息,丢弃激励源的细节。
这个思路在工程上非常实用。因为对于语音识别这类任务,我们关心的是"说了什么",也就是声道形状的变化,而不是"怎么说的",也就是声带的具体振动方式。倒谱分析恰好帮我们做了这个分离。
2.3 为什么MFCC能成为经典
在MFCC出现之前,人们也用过很多其他特征,比如线性预测系数(LPC)、感知线性预测(PLP)等。MFCC之所以能脱颖而出,我觉得有几个原因。
第一,它的计算复杂度适中。整个流程就是FFT、滤波、取对数、DCT,全是标准操作,没有迭代求解,非常适合实时系统。第二,它的维度可以灵活控制。你可以取13维,也可以取20维,甚至40维,根据任务需求调整。第三,它对人耳的模拟虽然粗糙,但在大量实验中证明了有效性。第四,DCT这一步还有一个额外好处——它能把滤波器组输出的相关性去掉,使得各维特征近似独立,这对后面的高斯混合模型等分类器非常友好。
当然,MFCC也不是没有缺点。它对噪声比较敏感,在低信噪比环境下性能下降明显。后来出现的PNCC、GFCC等特征在一定程度上就是为了解决这个问题。但在大多数安静或中等噪声场景下,MFCC依然是性价比最高的选择。
3. 完整流程拆解:从一段音频到一组特征向量
3.1 预加重:为什么要先给高频"加个buff"
预加重的操作很简单,就是一个一阶高通滤波器:
y[n] = x[n] - α * x[n-1]α通常取0.95到0.97之间。它的目的是补偿语音信号在发声过程中被声道辐射所衰减的高频分量。人的声带振动产生的信号,高频部分的能量天然比低频弱,如果不做补偿,高频段的信噪比会更差,后续的频谱分析就不够准确。
我试过不做预加重直接往下走,结果就是高频段的梅尔滤波器输出值普遍偏小,取对数之后数值范围被压缩,特征的区分度明显下降。加上预加重之后,频谱整体更平坦,各频段的贡献更均衡。
实操心得:α的取值不是死的。如果你的音频采样率比较低(比如8kHz),高频本身就有限,α可以取小一点,比如0.95;如果是16kHz或更高,0.97更合适。我一般会先用0.97跑一版,看看频谱平坦度,再决定要不要调。
3.2 分帧与加窗:把非平稳信号切成短时平稳
语音信号整体上是非平稳的,它的统计特性随时间变化。但在很短的时间尺度内(比如20到30毫秒),可以近似认为它是平稳的。所以标准做法是把长音频切成一个个短帧,每帧做一次特征提取。
帧长一般取25毫秒,帧移取10毫秒。这两个数字是怎么来的?25毫秒大约覆盖了一个音素的核心段,足够捕捉到声道的一个稳定状态;10毫秒的帧移保证了相邻帧之间有足够的重叠,避免信息丢失。以16kHz采样率为例,25毫秒就是400个采样点,10毫秒就是160个采样点。
分帧之后要加窗,最常用的是汉明窗:
w[n] = 0.54 - 0.46 * cos(2πn / (N-1))加窗的目的是减少帧边缘的突变造成的频谱泄漏。如果不加窗,直接对截断的信号做FFT,帧两端的跳变会在频谱上产生大量虚假的高频分量,干扰后续分析。
这里有个细节值得注意:加窗会让帧边缘的信息被削弱,所以帧移不能太大,否则边缘信息就浪费了。但帧移也不能太小,否则计算量会急剧增加。10毫秒是一个经过大量实践验证的平衡点。
3.3 梅尔滤波器组:整个流程的灵魂
这是MFCC最核心的一步。具体做法是:在梅尔刻度上均匀放置一组三角形滤波器,然后把每个滤波器的输出能量累加起来。
滤波器的数量通常取26个左右,覆盖从0到奈奎斯特频率的范围。每个三角形滤波器的中心频率在梅尔刻度上是等距的,但映射回线性频率后就不是等距了——低频处密集,高频处稀疏。这正是我们想要的效果。
每个滤波器的输出计算方式是:
S[m] = Σ (|X[k]|^2 * H_m[k])其中X[k]是FFT结果,H_m[k]是第m个滤波器的频率响应。这一步做完之后,我们就得到了26个能量值,它们代表了信号在26个梅尔频带上的能量分布。
注意:滤波器组的下限频率一般不从0开始,而是从几十Hz开始,因为极低频段主要是直流分量和环境噪声,对语音识别没有帮助。上限频率一般取奈奎斯特频率,但如果采样率很高,也可以截断到8000Hz左右,因为语音的主要能量集中在这个范围内。
3.4 取对数与DCT:从能量到倒谱系数
滤波器组输出的是能量值,动态范围可能很大。取对数有两个作用:一是压缩动态范围,使得特征的数值分布更接近高斯分布,有利于后续建模;二是前面提到的,把频域的乘法关系变成加法关系,为倒谱分析做准备。
取完对数之后,做离散余弦变换(DCT):
c[n] = Σ log(S[m]) * cos(πn(m+0.5)/M)DCT的结果就是倒谱系数。通常我们只取前12到13个系数,再加上第0个系数(代表帧能量)。后面的高阶系数对应的是滤波器组输出的快速变化部分,也就是激励源的细节,对语音识别帮助不大,反而可能引入噪声。
这里有个经验:第0个系数有时候会被单独处理,因为它代表的是整体能量,和音量相关。如果你的应用场景对音量敏感(比如声纹识别),可能需要把它去掉或者做归一化;如果对音量不敏感(比如语音识别),保留它反而有帮助。
3.5 差分与拼接:让特征带上时间动态信息
单独的MFCC只描述了每一帧的静态频谱特性,但语音是一个动态过程,相邻帧之间的关系同样重要。所以标准做法是计算MFCC的一阶差分(Delta)和二阶差分(Delta-Delta),然后把静态、一阶、二阶拼在一起。
一阶差分的计算很简单:
d[n] = (c[n+1] - c[n-1]) / 2实际实现中会用更平滑的版本,考虑前后几帧的加权。二阶差分就是对一阶差分再做一次差分。
拼接之后,如果原始MFCC是13维,加上一阶和二阶就是39维。这39维特征就构成了一个比较完整的语音特征表示。
4. 动手实现:从零写一个MFCC提取器
4.1 环境准备与参数设定
我用Python来实现,依赖numpy和scipy就够了。先定义好所有参数:
import numpy as np from scipy.fftpack import dct sample_rate = 16000 frame_size = 0.025 frame_stride = 0.010 num_filters = 26 num_ceps = 13 pre_emphasis = 0.97 NFFT = 512这些参数不是随便定的。frame_size取25毫秒是行业惯例;num_filters取26是因为26个滤波器在16kHz采样率下能较好地覆盖语音频带;num_ceps取13是因为前13个倒谱系数已经能捕捉大部分声道信息;NFFT取512是因为它大于帧长400,能提供足够的频率分辨率,同时是2的幂次,FFT效率高。
4.2 逐步实现核心流程
先做预加重和分帧:
def pre_emphasis(signal, coeff=0.97): return np.append(signal[0], signal[1:] - coeff * signal[:-1]) def framing(signal, sample_rate, frame_size, frame_stride): frame_length = int(round(frame_size * sample_rate)) frame_step = int(round(frame_stride * sample_rate)) num_frames = int(np.ceil(float(np.abs(len(signal) - frame_length)) / frame_step)) + 1 pad_length = num_frames * frame_step + frame_length padded = np.append(signal, np.zeros(pad_length - len(signal))) indices = np.tile(np.arange(0, frame_length), (num_frames, 1)) + \ np.tile(np.arange(0, num_frames * frame_step, frame_step), (frame_length, 1)).T return padded[indices.astype(np.int32, copy=False)]然后是加窗和FFT:
def compute_spectrum(frames, NFFT=512): frames = frames * np.hamming(frames.shape[1]) mag = np.abs(np.fft.rfft(frames, NFFT)) pow_spec = (1.0 / NFFT) * (mag ** 2) return pow_spec接下来是梅尔滤波器组的构建,这是最关键的一步:
def mel_filterbank(num_filters, NFFT, sample_rate, low_freq=0, high_freq=None): high_freq = high_freq or sample_rate / 2 low_mel = 2595 * np.log10(1 + low_freq / 700) high_mel = 2595 * np.log10(1 + high_freq / 700) mel_points = np.linspace(low_mel, high_mel, num_filters + 2) hz_points = 700 * (10 ** (mel_points / 2595) - 1) bins = np.floor((NFFT + 1) * hz_points / sample_rate).astype(int) fbank = np.zeros((num_filters, int(NFFT / 2 + 1))) for m in range(1, num_filters + 1): f_m_minus = bins[m - 1] f_m = bins[m] f_m_plus = bins[m + 1] for k in range(f_m_minus, f_m): fbank[m - 1, k] = (k - f_m_minus) / (f_m - f_m_minus) for k in range(f_m, f_m_plus): fbank[m - 1, k] = (f_m_plus - k) / (f_m_plus - f_m) return fbank最后是取对数、DCT和差分:
def mfcc(signal, sample_rate=16000, num_ceps=13): emphasized = pre_emphasis(signal) frames = framing(emphasized, sample_rate, 0.025, 0.010) pow_spec = compute_spectrum(frames, NFFT) fbank = mel_filterbank(num_filters, NFFT, sample_rate) filter_banks = np.dot(pow_spec, fbank.T) filter_banks = np.where(filter_banks == 0, np.finfo(float).eps, filter_banks) filter_banks = 20 * np.log10(filter_banks) mfcc_coeffs = dct(filter_banks, type=2, axis=1, norm='ortho')[:, 1:(num_ceps + 1)] return mfcc_coeffs4.3 参数选择的计算依据
这里重点说一下滤波器数量和NFFT的关系。滤波器数量不能太多,否则每个滤波器的带宽太窄,在低频段可能覆盖不到一个FFT bin。以16kHz采样率、NFFT=512为例,频率分辨率是16000/512≈31.25Hz。第一个梅尔滤波器在低频段的带宽可能只有几十Hz,如果滤波器数量取到40个,低频段的滤波器就会窄于频率分辨率,导致某些滤波器输出为零。
一般来说,滤波器数量取20到40之间比较合理。26是一个常用的默认值,它在频率分辨率和特征维度之间取得了较好的平衡。如果你做的是音乐相关的任务,可能需要更多滤波器来捕捉更精细的频谱结构;如果是窄带语音(比如电话语音,8kHz采样率),滤波器数量可以适当减少。
NFFT的选择也有讲究。它必须大于帧长,否则会丢失信息。但也不能太大,否则计算量增加而频率分辨率提升有限。512对于16kHz采样率、25毫秒帧长来说是一个很合适的值。
5. 实际调参中的坑与经验总结
5.1 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| 特征值全为负无穷 | 滤波器组输出为零后取对数 | 检查滤波器数量和NFFT是否匹配 |
| 识别率明显低于预期 | 预加重系数不合适 | 尝试0.95到0.97之间的不同值 |
| 低频段特征区分度差 | 滤波器下限频率设得太低 | 将下限频率提高到50到100Hz |
| 特征维度太高导致过拟合 | 差分阶数过多 | 只保留一阶差分,或减少倒谱系数数量 |
| 不同音频长度特征不一致 | 分帧时边界处理不当 | 检查padding逻辑和帧数计算 |
5.2 我踩过的几个典型坑
第一个坑是滤波器组的频率范围。我一开始把下限频率设成了0,结果第一个滤波器的输出几乎全是直流分量,取对数后数值异常大,把其他维度的特征都压下去了。后来把下限调到50Hz,问题就解决了。这个细节在很多教程里不会提,但实际做的时候很容易忽略。
第二个坑是DCT之后的系数选择。我一开始取了前13个系数,包括第0个。结果发现第0个系数的数值范围比其他系数大很多,做归一化的时候它主导了整体缩放。后来我把第0个系数单独拿出来做能量归一化,或者直接去掉,特征的分布就正常多了。
第三个坑是差分计算时的边界处理。差分需要用到前后帧,但第一帧没有前一帧,最后一帧没有后一帧。我一开始直接用0填充,结果边界帧的差分值异常。正确的做法是用边缘复制或者反射填充,保持差分的连续性。
实操心得:做MFCC之前,一定要先听一下音频,确认采样率、位深、声道数都符合预期。我遇到过好几次因为音频是立体声但代码按单声道处理,导致特征完全错乱的情况。另外,如果音频有直流偏移,最好先做一次去均值,否则会影响低频段的特征。
5.3 什么时候该考虑替代方案
MFCC不是万能的。如果你在低信噪比环境下做识别,或者你的信号不是语音而是音乐、环境声,MFCC可能不是最优选择。这时候可以考虑以下几种替代或补充方案。
一是PNCC(Power Normalized Cepstral Coefficients),它在MFCC的基础上增加了功率归一化和噪声抑制步骤,对噪声更鲁棒。二是GFCC(Gammatone Frequency Cepstral Coefficients),它用Gammatone滤波器组替代梅尔滤波器组,更接近人耳耳蜗的响应特性。三是直接用滤波器组输出(Filter Bank Energies),不做DCT,保留更多原始信息,适合深度学习模型直接端到端学习。
我个人的经验是,如果做传统机器学习(比如GMM-HMM),MFCC加差分是首选;如果做深度学习,滤波器组输出或者原始频谱往往效果更好,因为神经网络可以自己学习到合适的特征变换。但MFCC依然是一个很好的基线,值得先跑一版看看效果。
6. 从特征到应用:MFCC在下游任务中的表现
6.1 语音识别中的经典搭配
在深度学习普及之前,语音识别的主流方案是GMM-HMM,而MFCC就是它的标准输入特征。GMM负责对每一帧的MFCC特征建模,HMM负责建模时序关系。这个组合在安静环境下能跑到不错的识别率,但在噪声环境下性能下降很快。
后来DNN兴起,人们发现用MFCC训练DNN也能work,但滤波器组输出往往更好。原因是DNN有足够的容量自己学习特征变换,而MFCC的DCT和截断步骤可能丢失了一些对分类有用的信息。不过MFCC的维度低、计算快,在资源受限的场景下依然有优势。
6.2 声纹识别中的注意事项
声纹识别和语音识别对特征的要求不太一样。语音识别关心的是"说了什么",所以要去掉说话人相关的信息;声纹识别关心的是"谁说的",所以反而要保留说话人的个性特征。
在声纹识别中,MFCC的第0个系数(能量)通常会被去掉,因为它和音量相关,而音量不是说话人的固有属性。另外,倒谱系数的阶数可以适当增加,比如取20维甚至更高,以捕捉更多声道细节。差分特征也很重要,因为说话人的语速和韵律也是身份的一部分。
6.3 情感计算中的特征选择
情感计算任务中,MFCC通常不是唯一的特征。因为情感不仅体现在频谱特性上,还体现在基频、能量、语速等多个维度。常见的做法是把MFCC和基频、能量、过零率等特征拼接在一起,形成一个更全面的特征向量。
我做过一个情感分类的项目,单纯用MFCC的准确率大概在60%左右,加上基频和能量特征之后提升到了70%以上。这说明MFCC虽然强大,但在某些任务中需要和其他特征配合使用。
6.4 实时系统中的优化策略
如果你的应用场景是实时语音处理,MFCC的计算速度就很关键了。几个优化方向:一是用FFT的实数版本(rfft)而不是复数版本,计算量减半;二是预计算梅尔滤波器组矩阵,避免每帧重复计算;三是用DCT的快速算法,或者直接用矩阵乘法代替逐元素计算;四是如果帧移和帧长有重叠,可以利用重叠部分的FFT结果,减少重复计算。
我在一个嵌入式项目里做过测试,优化前每帧MFCC提取需要大约2毫秒,优化后降到了0.5毫秒左右,完全能满足实时性要求。关键就是把滤波器组矩阵和DCT矩阵都预计算好,运行时只做矩阵乘法。
7. 写在最后
MFCC这个东西,看起来公式一堆,但拆开来看,每一步的动机都很直观。预加重是为了补偿高频衰减,分帧加窗是为了把非平稳信号局部平稳化,梅尔滤波器组是为了对齐人耳感知,取对数和DCT是为了分离声道和激励。把这些逻辑串起来,整个流程就不再是一堆孤立的步骤,而是一个有内在一致性的设计。
我在实际项目中的体会是,不要迷信默认参数。不同的采样率、不同的应用场景、不同的下游模型,最优参数可能都不一样。最好的办法是先跑通流程,然后拿一小批数据做参数扫描,看看特征分布和下游指标的变化。这个过程可能有点繁琐,但比起盲目调参,效率高得多。
另外,如果你刚开始接触语音信号处理,建议不要一上来就用librosa或者python_speech_features这些库。先自己手写一遍MFCC,哪怕写得慢一点、代码丑一点,你对整个流程的理解会完全不一样。等自己写过了,再用库,你就知道每个参数在背后做了什么,遇到问题也能快速定位。
最后分享一个小技巧:可视化。把原始波形、频谱、梅尔滤波器组输出、MFCC系数都画出来,对照着看。很多时候,特征出了问题,看一眼图就能发现。比如滤波器组输出在某个频段全为零,或者MFCC系数某一维的方差特别大,这些都是信号,提示你某个环节可能有问题。图看多了,直觉就出来了。