ASR中常用的語音特徵之FBank和MFCC(原理 + Python實現)

一步一步講解和實現ASR中常用的語音特徵——FBank和MFCC的提取,包括算法原理、代碼和可視化等。

完整Jupyter Notebook鏈接:https://github.com/Magic-Bubble/SpeechProcessForMachineLearning/blob/master/speech_process.ipynb

語音信號的產生

語音通常是指人說話的聲音。從生物學的角度來看,是氣流通過聲帶、咽喉、口腔、鼻腔等發出聲音;從信號的角度來看,不同位置的震動頻率不一樣,最後的信號是由基頻和一些諧波構成。

之後被設備接收後(比如麥克風),會通過A/D轉換,將模擬信號轉換爲數字信號,一般會有采樣、量化和編碼三個步驟,採樣率要遵循奈奎斯特採樣定律:fs>=2ffs >= 2f,比如電話語音的頻率一般在300Hz~3400Hz,所以採用8kHz的採樣率足矣。

下面採用一個30s左右的16比特PCM編碼後的語音wav爲例。

準備工作

1. 導包

import numpy as np
from scipy.io import wavfile
from scipy.fftpack import dct
import warnings
warnings.filterwarnings('ignore')
import matplotlib.pyplot as plt
%matplotlib inline

2. 繪圖工具

# 繪製時域圖
def plot_time(signal, sample_rate):
    time = np.arange(0, len(signal)) * (1.0 / sample_rate)
    plt.figure(figsize=(20, 5))
    plt.plot(time, signal)
    plt.xlabel('Time(s)')
    plt.ylabel('Amplitude')
    plt.grid()
# 繪製頻域圖
def plot_freq(signal, sample_rate, fft_size=512):
    xf = np.fft.rfft(signal, fft_size) / fft_size
    freqs = np.linspace(0, sample_rate/2, fft_size/2 + 1)
    xfp = 20 * np.log10(np.clip(np.abs(xf), 1e-20, 1e100))
    plt.figure(figsize=(20, 5))
    plt.plot(freqs, xfp)
    plt.xlabel('Freq(hz)')
    plt.ylabel('dB')
    plt.grid()
# 繪製頻譜圖
def plot_spectrogram(spec, note):
    fig = plt.figure(figsize=(20, 5))
    heatmap = plt.pcolor(spec)
    fig.colorbar(mappable=heatmap)
    plt.xlabel('Time(s)')
    plt.ylabel(note)
    plt.tight_layout()

3. 數據準備

sample_rate, signal = wavfile.read('./resources/OSR_us_000_0010_8k.wav')
signal = signal[0: int(3.5 * sample_rate)]  # Keep the first 3.5 seconds
print('sample rate:', sample_rate, ', frame length:', len(signal))

sample rate: 8000 , frame length: 28000

plot_time(signal, sample_rate)

在這裏插入圖片描述

plot_freq(signal, sample_rate)

在這裏插入圖片描述

預加重(Pre-Emphasis)

預加重一般是數字語音信號處理的第一步。語音信號往往會有頻譜傾斜(Spectral Tilt)現象,即高頻部分的幅度會比低頻部分的小,預加重在這裏就是起到一個平衡頻譜的作用,增大高頻部分的幅度。它使用如下的一階濾波器來實現:

y(t)=x(t)αx(t1),    0.95<α<0.99y(t) = x(t) - \alpha x(t-1), \ \ \ \ 0.95 < \alpha < 0.99

筆者對這個公式的理解是:信號頻率的高低主要是由信號電平變化的速度所決定,對信號做一階差分時,高頻部分(變化快的地方)差分值大,低頻部分(變化慢的地方)差分值小,達到平衡頻譜的作用。

pre_emphasis = 0.97
emphasized_signal = np.append(signal[0], signal[1:] - pre_emphasis * signal[:-1])
plot_time(emphasized_signal, sample_rate)

在這裏插入圖片描述

plot_freq(emphasized_signal, sample_rate)

在這裏插入圖片描述

從下面這個圖來看,確實起到了平衡頻譜的作用。

分幀(Framing)

在預加重之後,需要將信號分成短時幀。做這一步的原因是:信號中的頻率會隨時間變化(不穩定的),一些信號處理算法(比如傅里葉變換)通常希望信號是穩定,也就是說對整個信號進行處理是沒有意義的,因爲信號的頻率輪廓會隨着時間的推移而丟失。爲了避免這種情況,需要對信號進行分幀處理,認爲每一幀之內的信號是短時不變的。一般設置幀長取20ms~40ms,相鄰幀之間50%(+/-10%)的覆蓋。對於ASR而言,通常取幀長爲25ms,覆蓋爲10ms。

frame_size, frame_stride = 0.025, 0.01
frame_length, frame_step = int(round(frame_size * sample_rate)), int(round(frame_stride * sample_rate))
signal_length = len(emphasized_signal)
num_frames = int(np.ceil(np.abs(signal_length - frame_length) / frame_step)) + 1

pad_signal_length = (num_frames - 1) * frame_step + frame_length
z = np.zeros((pad_signal_length - signal_length))
pad_signal = np.append(emphasized_signal, z)

indices = np.arange(0, frame_length).reshape(1, -1) + np.arange(0, num_frames * frame_step, frame_step).reshape(-1, 1)
frames = pad_signal[indices]
print(frames.shape)

(349, 200)

加窗(Window)

在分幀之後,通常需要對每幀的信號進行加窗處理。目的是讓幀兩端平滑地衰減,這樣可以降低後續傅里葉變換後旁瓣的強度,取得更高質量的頻譜。常用的窗有:矩形窗、漢明(Hamming)窗、漢寧窗(Hanning),以漢明窗爲例,其窗函數爲:

w(n)=0.540.46cos(2πnN1)w(n) = 0.54 - 0.46 cos(\frac{2\pi n}{N-1})

這裏的0<=n<=N10<=n<=N-1NN是窗的寬度。

hamming = np.hamming(frame_length)
# hamming = 0.54 - 0.46 * np.cos(2 * np.pi * np.arange(0, frame_length) / (frame_length - 1))
plt.figure(figsize=(20, 5))
plt.plot(hamming)
plt.grid()
plt.xlim(0, 200)
plt.ylim(0, 1)
plt.xlabel('Samples')
plt.ylabel('Amplitude')

在這裏插入圖片描述

frames *= hamming
plot_time(frames[1], sample_rate)
plot_freq(frames[1], sample_rate)

在這裏插入圖片描述

快速傅里葉變換(FFT)

對於每一幀的加窗信號,進行N點FFT變換,也稱短時傅里葉變換(STFT),N通常取256或512,然後用如下的公式計算能量譜:

P=FFT(xi)2N P = \frac{|FFT(x_i)|^2}{N}

NFFT = 512
mag_frames = np.absolute(np.fft.rfft(frames, NFFT))
pow_frames = ((1.0 / NFFT) * (mag_frames ** 2))
print(pow_frames.shape)

(349, 257)

plt.figure(figsize=(20, 5))
plt.plot(pow_frames[1])
plt.grid()

FBank特徵(Filter Banks)

經過上面的步驟之後,在能量譜上應用Mel濾波器組,就能提取到FBank特徵。

在介紹Mel濾波器組之前,先介紹一下Mel刻度,這是一個能模擬人耳接收聲音規律的刻度,人耳在接收聲音時呈現非線性狀態,對高頻的更不敏感,因此Mel刻度在低頻區分辨度較高,在高頻區分辨度較低,與頻率之間的換算關係爲:

m=2595log10(1+f700)m = 2595 log_{10} (1 + \frac{f}{700})

f=700(10m/25951)f = 700(10^{m/2595} - 1)

Mel濾波器組就是一系列的三角形濾波器,通常有40個或80個,在中心頻率點響應值爲1,在兩邊的濾波器中心點衰減到0,如下圖:

在這裏插入圖片描述

具體公式可以寫爲:

最後在能量譜上應用Mel濾波器組,其公式爲:

Yt(m)=k=1NHm(k)Xt(k)2Y_t(m) = \sum_{k=1}^{N} H_m(k)|X_t(k)|^2

其中,k表示FFT變換後的編號,m表示mel濾波器的編號。

low_freq_mel = 0
high_freq_mel = 2595 * np.log10(1 + (sample_rate / 2) / 700)
print(low_freq_mel, high_freq_mel)

0 2146.06452750619

nfilt = 40
mel_points = np.linspace(low_freq_mel, high_freq_mel, nfilt + 2)  # 所有的mel中心點,爲了方便後面計算mel濾波器組,左右兩邊各補一箇中心點
hz_points = 700 * (10 ** (mel_points / 2595) - 1)
fbank = np.zeros((nfilt, int(NFFT / 2 + 1)))  # 各個mel濾波器在能量譜對應點的取值
bin = (hz_points / (sample_rate / 2)) * (NFFT / 2)  # 各個mel濾波器中心點對應FFT的區域編碼,找到有值的位置
for i in range(1, nfilt + 1):
    left = int(bin[i-1])
    center = int(bin[i])
    right = int(bin[i+1])
    for j in range(left, center):
        fbank[i-1, j+1] = (j + 1 - bin[i-1]) / (bin[i] - bin[i-1])
    for j in range(center, right):
        fbank[i-1, j+1] = (bin[i+1] - (j + 1)) / (bin[i+1] - bin[i])
print(fbank)

[[0. 0.46952675 0.93905351 … 0. 0. 0. ]
[0. 0. 0. … 0. 0. 0. ]
[0. 0. 0. … 0. 0. 0. ]

[0. 0. 0. … 0. 0. 0. ]
[0. 0. 0. … 0. 0. 0. ]
[0. 0. 0. … 0.14650797 0.07325398 0. ]]

filter_banks = np.dot(pow_frames, fbank.T)
filter_banks = np.where(filter_banks == 0, np.finfo(float).eps, filter_banks)
filter_banks = 20 * np.log10(filter_banks)  # dB
print(filter_banks.shape)

(349, 40)

plot_spectrogram(filter_banks.T, 'Filter Banks')

PS:“log mel-filter bank outputs”和“FBANK features”說的是同一個東西。

MFCC特徵(Mel-frequency Cepstral Coefficients)

前面提取到的FBank特徵,往往是高度相關的。因此可以繼續用DCT變換,將這些相關的濾波器組係數進行壓縮。對於ASR來說,通常取2~13維,扔掉的信息裏面包含濾波器組係數快速變化部分,這些細節信息在ASR任務上可能沒有幫助。

DCT變換其實是逆傅里葉變換的等價替代:

yt(n)=m=0M1log(Yt(m))cos(n(m+0.5)πM),     n=0,...,Jy_t(n) = \sum_{m=0}^{M-1}log(Y_t(m))cos(n(m + 0.5) \frac{\pi}{M}), \ \ \ \ \ n = 0, ..., J

所以MFCC名字裏面有倒譜(Cepstral)。

num_ceps = 12
mfcc = dct(filter_banks, type=2, axis=1, norm='ortho')[:, 1:(num_ceps+1)]
print(mfcc.shape)

(349, 12)

plot_spectrogram(mfcc.T, 'MFCC Coefficients')

在這裏插入圖片描述

一般對於ASR來說,對MFCC進行一個正弦提升(sinusoidal liftering)操作,可以提升在噪聲信號中最後的識別率:

MFCCi=wiMFCCiMFCC'_i = w_i MFCC_i

wi=D2sin(πiD)w_i = \frac{D}{2} sin(\frac{\pi * i}{D})

從公式看,猜測原因可能是對頻譜做一個平滑,如果DD取值較大時,會加重高頻部分,使得噪聲被弱化?

cep_lifter = 23
(nframes, ncoeff) = mfcc.shape
n = np.arange(ncoeff)
lift = 1 + (cep_lifter / 2) * np.sin(np.pi * n / cep_lifter)
mfcc *= lift
plot_spectrogram(mfcc.T, 'MFCC Coefficients')

在這裏插入圖片描述

FBank與MFCC比較

FBank特徵的提取更多的是希望符合聲音信號的本質,擬合人耳接收的特性。而MFCC特徵多的那一步則是受限於一些機器學習算法。很早之前MFCC特徵和GMMs-HMMs方法結合是ASR的主流。而當一些深度學習方法出來之後,MFCC則不一定是最優選擇,因爲神經網絡對高度相關的信息不敏感,而且DCT變換是線性的,會丟失語音信號中原本的一些非線性成分。

還有一些說法是在質疑傅里葉變換的使用,因爲傅里葉變換也是線性的。因此也有很多方法,設計模型直接從原始的音頻信號中提取特徵,但這種方法會增加模型的複雜度,而且本身傅里葉變換不太容易擬合。同時傅里葉變換是在短時上應用的,可以建設信號在這個短的時間內是靜止的,因此傅里葉變換的線性也不會造成很嚴重的問題。

結論就是:在模型對高相關的信號不敏感時(比如神經網絡),可以用FBank特徵;在模型對高相關的信號敏感時(比如GMMs-HMMs),需要用MFCC特徵。從目前的趨勢來看,因爲神經網絡的逐步發展,FBank特徵越來越流行。

其他特徵

  1. PLP(Perceptual Linear Prediction)

另外一種特徵,與MFCC相比有一些優勢,具體提取方式見下圖:

  1. 動態特徵

加入表現幀之間變化的特徵,用如下公式:

d(t)=c(t+1)c(t1)2d(t) = \frac{c(t+1) - c(t-1)}{2}

一般在ASR中使用的特徵(用於GMM相關的系統),是39維的;包括(12維MFCC+1維能量) + delta + delta^2

具體提取過程見下圖:

標準化

其目的是希望減少訓練集與測試集之間的不匹配。有三種操作:

  1. 去均值 (CMN)

爲了均衡頻譜,提升信噪比,可以做一個去均值的操作

filter_banks -= (np.mean(filter_banks, axis=0) + 1e-8)
plot_spectrogram(filter_banks.T, 'Filter Banks')

在這裏插入圖片描述

mfcc -= (np.mean(mfcc, axis=0) + 1e-8)
plot_spectrogram(mfcc.T, 'MFCC Coefficients')

在這裏插入圖片描述

  1. 方差歸一(CVN)

除以標準差,從而使得方差爲1

  1. 標準化(CMVN)

yt(j)=yt(j)μ(y(j))σ(y(j))y_t(j) = \frac{y_t(j) - \mu (y(j))}{\sigma (y(j))}

PS:這些操作,還可以針對speaker/channel做;在實時情景下,可以計算moving average。

總結

最後引用文末slide裏面的一個總結:

傳送門

Speech Processing for Machine Learning: Filter banks, Mel-Frequency Cepstral Coefficients (MFCCs) and What’s In-Between 一個很優質,講的很清楚的英文博客
Speech Signal Analysis 英國愛丁堡大學一門ASR課程的講義
python_speech_features 一個很成熟的python提取這些特徵的包
ASR中常用的語音特徵之FBank和MFCC(原理 + Python實現) 個人博客

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章