[转载] 机器学习中的语音处理:滤波器组、MFCC 及其间的取舍
原文出处: https://haythamfayek.com/2016/04/21/speech-processing-for-machine-learning.html 作者: Haytham M. Fayek 原文发布日期: 2016-04-21 许可协议: 原文未声明显式开源协议,本文为其中文翻译,所有权利归原文作者所有。
无论面对的是自动语音识别(ASR)、说话人识别还是其他任何系统,语音处理都扮演着重要的角色。
很长一段时间里,Mel 频率倒谱系数(MFCCs)都是非常流行的特征;但近来,滤波器组(filter banks)正变得越来越受欢迎。
在这篇文章里,我将讨论滤波器组和 MFCC,以及为什么滤波器组正变得越来越流行。
计算滤波器组和 MFCC 的流程其实大体相同——两种情况下都要先算出滤波器组,再额外经过几个步骤就能得到 MFCC。简而言之:信号先经过一个预加重滤波器;接着被切分成(相互重叠的)帧,并对每一帧施加一个窗函数;随后对每一帧做傅里叶变换(更准确地说是短时傅里叶变换),计算功率谱;紧接着计算滤波器组。要得到 MFCC,还需对滤波器组做离散余弦变换(DCT),保留其中若干系数、丢弃其余的。两种流程的最后一步都是均值归一化。
准备工作
本文使用的是 Open Speech Repository 提供的一个 16-bit PCM wav 文件,名为 OSR_us_000_0010_8k.wav,采样频率为 8000 Hz。这是一个干净的语音信号,由单一说话人说出若干句子、句子之间有停顿。为简化起见,我只取信号的前 3.5 秒,大致对应 wav 文件里的第一句话。
我使用 Python 2.7.x、NumPy 和 SciPy。本文用到的部分代码基于一份公开的参考实现。
import numpy
import scipy.io.wavfile
from scipy.fftpack import dct
sample_rate, signal = scipy.io.wavfile.read('OSR_us_000_0010_8k.wav') # File assumed to be in the same directory
signal = signal[0:int(3.5 * sample_rate)] # Keep the first 3.5 seconds
原始信号在时域中的形态如下:

预加重
第一步是对信号施加预加重滤波器,以放大高频分量。预加重滤波器有几方面的好处:(1)平衡频谱,因为高频分量的幅度通常比低频小;(2)避免傅里叶变换运算中的数值问题;(3)还可能改善信噪比(SNR)。
预加重滤波器可以用下面这个一阶滤波器作用于信号 x 来实现:
y(t) = x(t) − α·x(t−1)
用下面这一行就能轻松实现,其中滤波器系数 α 的典型取值为 0.95 或 0.97,本例 pre_emphasis = 0.97:
emphasized_signal = numpy.append(signal[0], signal[1:] - pre_emphasis * signal[:-1])
在现代系统中,预加重的效果有限,主要是因为当初引入预加重的大部分动机都可以通过均值归一化(本文稍后讨论)来实现——除了「避免傅里叶变换的数值问题」这一点,但在现代 FFT 实现里这也不成问题了。
经过预加重后,信号在时域中的形态如下:

分帧
预加重之后,需要把信号切分成短时帧。这一步的理由是:信号中的频率是随时间变化的,所以在大多数情况下对整段信号做傅里叶变换没有意义——那样会丢失信号频率随时间变化的轮廓。为了避免这一点,我们可以合理地假设信号中的频率在很短的一段时间内是平稳的(stationary)。因此,对这样的短时帧做傅里叶变换,再把相邻帧拼接起来,就能得到信号频率轮廓的良好近似。
语音处理中典型的帧长在 20 ms 到 40 ms 之间,相邻帧之间有 50%(±10%)的重叠。常用的设置是帧长 25 ms,frame_size = 0.025,帧移(stride)10 ms(即 15 ms 重叠),frame_stride = 0.01。
frame_length, frame_step = frame_size * sample_rate, frame_stride * sample_rate # Convert from seconds to samples
signal_length = len(emphasized_signal)
frame_length = int(round(frame_length))
frame_step = int(round(frame_step))
num_frames = int(numpy.ceil(float(numpy.abs(signal_length - frame_length)) / frame_step)) # Make sure that we have at least 1 frame
pad_signal_length = num_frames * frame_step + frame_length
z = numpy.zeros((pad_signal_length - signal_length))
pad_signal = numpy.append(emphasized_signal, z) # Pad Signal to make sure that all frames have equal number of samples without truncating any samples from the original signal
indices = numpy.tile(numpy.arange(0, frame_length), (num_frames, 1)) + numpy.tile(numpy.arange(0, num_frames * frame_step, frame_step), (frame_length, 1)).T
frames = pad_signal[indices.astype(numpy.int32, copy=False)]
加窗
把信号切成帧之后,我们对每一帧施加一个窗函数,比如汉明窗(Hamming window)。汉明窗的形式如下:
w[n] = 0.54 − 0.46 · cos( 2πn / (N−1) )
其中 0 ≤ n ≤ N−1,N 为窗口长度。把上面这个式子画出来就是下面这张图:

之所以要给帧加窗,有几方面原因,尤其是为了抵消 FFT 所做的「数据是无限的」这一假设,并减少频谱泄漏(spectral leakage)。
frames *= numpy.hamming(frame_length)
# frames *= 0.54 - 0.46 * numpy.cos((2 * numpy.pi * n) / (frame_length - 1)) # Explicit Implementation **
傅里叶变换与功率谱
现在可以对每一帧做 N 点 FFT 来计算频谱,这也被称为短时傅里叶变换(STFT),其中 N 通常取 256 或 512,本例 NFFT = 512;然后用下面这个式子计算功率谱(periodogram,周期图):
P = |FFT(xᵢ)|² / N
其中 xᵢ 是信号 x 的第 i 帧。这可以用下面几行实现:
mag_frames = numpy.absolute(numpy.fft.rfft(frames, NFFT)) # Magnitude of the FFT
pow_frames = ((1.0 / NFFT) * ((mag_frames) ** 2)) # Power Spectrum
计算滤波器组的最后一步,是把三角滤波器(通常是 40 个,nfilt = 40)按 Mel 尺度施加到功率谱上,以提取频带。Mel 尺度旨在模仿人耳对声音非线性的感知——在低频处更具区分力,在高频处区分力较弱。我们可以在赫兹(f)与 Mel(m)之间用下面两个式子相互转换:
m = 2595 · log₁₀( 1 + f/700 )
f = 700 · ( 10^(m/2595) − 1 )
滤波器组里的每个滤波器都是三角形的,在中心频率处响应为 1,向两侧线性衰减到 0,直到触及两个相邻滤波器的中心频率时响应变为 0,如下图所示:

这可以用下面这个式子来建模:
⎧ 0 k < f(m−1)
⎪ (k − f(m−1)) / (f(m) − f(m−1)) f(m−1) ≤ k < f(m)
H_m(k) = ⎨ 1 k = f(m)
⎪ (f(m+1) − k) / (f(m+1) − f(m)) f(m) < k ≤ f(m+1)
⎩ 0 k > f(m+1)
low_freq_mel = 0
high_freq_mel = (2595 * numpy.log10(1 + (sample_rate / 2) / 700)) # Convert Hz to Mel
mel_points = numpy.linspace(low_freq_mel, high_freq_mel, nfilt + 2) # Equally spaced in Mel scale
hz_points = (700 * (10**(mel_points / 2595) - 1)) # Convert Mel to Hz
bin = numpy.floor((NFFT + 1) * hz_points / sample_rate)
fbank = numpy.zeros((nfilt, int(numpy.floor(NFFT / 2 + 1))))
for m in range(1, nfilt + 1):
f_m_minus = int(bin[m - 1]) # left
f_m = int(bin[m]) # center
f_m_plus = int(bin[m + 1]) # right
for k in range(f_m_minus, f_m):
fbank[m - 1, k] = (k - bin[m - 1]) / (bin[m] - bin[m - 1])
for k in range(f_m, f_m_plus):
fbank[m - 1, k] = (bin[m + 1] - k) / (bin[m + 1] - bin[m])
filter_banks = numpy.dot(pow_frames, fbank.T)
filter_banks = numpy.where(filter_banks == 0, numpy.finfo(float).eps, filter_banks) # Numerical Stability
filter_banks = 20 * numpy.log10(filter_banks) # dB
把滤波器组施加到信号的功率谱(周期图)上后,得到如下频谱图:

如果 Mel 尺度滤波器组就是我们要的特征,那就可以直接跳到均值归一化那一步了。
Mel 频率倒谱系数(MFCCs)
事实上,上一步算出的滤波器组系数是高度相关的(highly correlated),这在某些机器学习算法里会带来问题。因此,我们可以对滤波器组系数施加离散余弦变换(DCT)来去相关,并得到滤波器组的一个压缩表示。对于自动语音识别(ASR),通常会保留第 2 到第 13 个倒谱系数,其余丢弃;本例 num_ceps = 12。之所以丢弃其余系数,是因为它们代表的是滤波器组系数中快速变化的部分,而这些细节对 ASR 没有贡献。
mfcc = dct(filter_banks, type=2, axis=1, norm='ortho')[:, 1 : (num_ceps + 1)] # Keep 2-13
还可以对 MFCC 施加正弦提升(sinusoidal liftering)¹,以削弱高阶 MFCC,据说能改善噪声信号下的语音识别效果。
(nframes, ncoeff) = mfcc.shape
n = numpy.arange(ncoeff)
lift = 1 + (cep_lifter / 2) * numpy.sin(numpy.pi * n / cep_lifter)
mfcc *= lift #*
得到的 MFCC 如下:

均值归一化
如前所述,为了平衡频谱、改善信噪比(SNR),我们可以简单地从所有帧中减去每个系数的均值。
filter_banks -= (numpy.mean(filter_banks, axis=0) + 1e-8)
均值归一化后的滤波器组:

MFCC 也同样处理:
mfcc -= (numpy.mean(mfcc, axis=0) + 1e-8)
均值归一化后的 MFCC:

滤波器组 vs MFCCs
到目前为止,我们从动机和实现两个角度讨论了计算滤波器组和 MFCC 的各步。值得注意的是:计算滤波器组所需的全部步骤,其动机都来自语音信号本身的性质以及人耳对这类信号的感知。相反,计算 MFCC 所需的那些额外步骤,动机则来自某些机器学习算法的局限。需要 DCT 是为了给滤波器组系数去相关,这个过程也叫白化(whitening)。尤其是当高斯混合模型 - 隐马尔可夫模型(GMMs-HMMs)盛行时,MFCC 非常流行,并且 MFCC 与 GMMs-HMMs 共同演化,成为了做自动语音识别(ASR)的标准做法²。随着深度学习进入语音系统,人们可能会质疑 MFCC 是否仍是正确的选择——毕竟深度神经网络对高度相关的输入不那么敏感,因此 DCT 也不再是必要的步骤。值得注意的是,DCT 是一种线性变换,因而不太理想,因为它会丢弃语音信号(高度非线性)中的一部分信息。
同样合理的问题是:傅里叶变换本身是不是必要的操作。既然傅里叶变换本身也是一种线性操作,那么忽略它、直接在时域上从信号中学习,或许是有益的。事实上,已有一些近期工作尝试了这一点并报告了正面结果。然而,傅里叶变换是一个难以学习的操作,可能会增加达到同样性能所需的数据量和模型复杂度。而且,做短时傅里叶变换(STFT)时,我们已假设信号在这样短的时间内是平稳的,因此傅里叶变换的线性性并不会构成关键问题。
结论
在这篇文章里,我们探索了计算 Mel 尺度滤波器组和 Mel 频率倒谱系数(MFCCs)的流程,讨论了流程中每一步的动机与实现,也论证了相比于 MFCC,滤波器组日益流行的原因。
太长不看(tl;dr):如果机器学习算法对高度相关的输入不敏感,就用 Mel 尺度滤波器组;如果算法对相关输入敏感,就用 MFCCs。
引用:
@misc{fayek2016,
title = "Speech Processing for Machine Learning: Filter banks, Mel-Frequency Cepstral Coefficients (MFCCs) and What's In-Between",
author = "Haytham M. Fayek",
year = "2016",
url = "https://haythamfayek.com/2016/04/21/speech-processing-for-machine-learning.html"
}