讲道理,想要处理语音这种时间信号,最适合RNN或者SNN这种神经网络来进行识别,传统的方法是基于GMM+HMM的方式进行声学模型以及语言模型的建模。现在的语音识别往往引入神经网络,进行端到端(end-to-end)的模型建立与训练。
这篇博客主要对语音信号处理必须面对的基本问题进行阐述,详解MFCC梅尔倒谱这种分析方法的由来,以及具体实现。
前期理论介绍,网上已经有很多资料了,但我觉得这位大神讲的可以说非常好,他还有很多其他的博客也都很不错。
https://blog.csdn.net/zouxy09/article/details/9156785
在任意一个Automatic speech recognition 系统中,第一步就是提取特征。换句话说,我们需要把音频信号中具有辨识性的成分提取出来,然后把其他的乱七八糟的信息扔掉,例如背景噪声啊,情绪啊等等。
搞清语音是怎么产生的对于我们理解语音有很大帮助。人通过声道产生声音,声道的shape(形状?)决定了发出怎样的声音。声道的shape包括舌头,牙齿等。如果我们可以准确的知道这个形状,那么我们就可以对产生的音素phoneme进行准确的描述。声道的形状在语音短时功率谱的包络中显示出来。而MFCCs就是一种准确描述这个包络的一种特征。
MFCCs(Mel Frequency Cepstral Coefficents)是一种在自动语音和说话人识别中广泛使用的特征。它是在1980年由Davis和Mermelstein搞出来的。从那时起。在语音识别领域,MFCCs在人工特征方面可谓是鹤立鸡群,一枝独秀,从未被超越啊(至于说Deep Learning的特征学习那是后话了)。
好,到这里,我们提到了一个很重要的关键词:声道的形状,然后知道它很重要,还知道它可以在语音短时功率谱的包络中显示出来。哎,那什么是功率谱?什么是包络?什么是MFCCs?它为什么有效?如何得到?下面咱们慢慢道来。
我们处理的是语音信号,那么如何去描述它很重要。因为不同的描述方式放映它不同的信息。那怎样的描述方式才利于我们观测,利于我们理解呢?这里我们先来了解一个叫声谱图的东西。
这里,这段语音被分为很多帧,每帧语音都对应于一个频谱(通过短时FFT计算),频谱表示频率与能量的关系。在实际使用中,频谱图有三种,即线性振幅谱、对数振幅谱、自功率谱(对数振幅谱中各谱线的振幅都作了对数计算,所以其纵坐标的单位是dB(分贝)。这个变换的目的是使那些振幅较低的成分相对高振幅成分得以拉高,以便观察掩盖在低幅噪声中的周期信号)。
我们先将其中一帧语音的频谱通过坐标表示出来,如上图左。现在我们将左边的频谱旋转90度。得到中间的图。然后把这些幅度映射到一个灰度级表示(也可以理解为将连续的幅度量化为256个量化值?),0表示黑,255表示白色。幅度值越大,相应的区域越黑。这样就得到了最右边的图。那为什么要这样呢?为的是增加时间这个维度,这样就可以显示一段语音而不是一帧语音的频谱,而且可以直观的看到静态和动态的信息。优点稍后呈上。
这样我们会得到一个随着时间变化的频谱图,这个就是描述语音信号的spectrogram声谱图。
下图是一段语音的声谱图,很黑的地方就是频谱图中的峰值(共振峰formants)。
那我们为什么要在声谱图中表示语音呢?
首先,音素(Phones)的属性可以更好的在这里面观察出来。另外,通过观察共振峰和它们的转变可以更好的识别声音。隐马尔科夫模型(Hidden Markov Models)就是隐含地对声谱图进行建模以达到好的识别性能。还有一个作用就是它可以直观的评估TTS系统(text to speech)的好坏,直接对比合成的语音和自然的语音声谱图的匹配度即可。
下面是一个语音的频谱图。峰值就表示语音的主要频率成分,我们把这些峰值称为共振峰(formants),而共振峰就是携带了声音的辨识属性(就是个人身份证一样)。所以它特别重要。用它就可以识别不同的声音。
既然它那么重要,那我们就是需要把它提取出来!我们要提取的不仅仅是共振峰的位置,还得提取它们转变的过程。所以我们提取的是频谱的包络(Spectral Envelope)。这包络就是一条连接这些共振峰点的平滑曲线。
我们可以这么理解,将原始的频谱由两部分组成:包络和频谱的细节。这里用到的是对数频谱,所以单位是dB。那现在我们需要把这两部分分离开,这样我们就可以得到包络了。
那怎么把他们分离开呢?也就是,怎么在给定log X[k]的基础上,求得log H[k] 和 log E[k]以满足log X[k] = log H[k] + log E[k]呢?
为了达到这个目标,我们需要Play a Mathematical Trick。这个Trick是什么呢?就是对频谱做FFT。在频谱上做傅里叶变换就相当于逆傅里叶变换Inverse FFT (IFFT)。需要注意的一点是,我们是在频谱的对数域上面处理的,这也属于Trick的一部分。这时候,在对数频谱上面做IFFT就相当于在一个伪频率(pseudo-frequency)坐标轴上面描述信号。
由上面这个图我们可以看到,包络是主要是低频成分(这时候需要转变思维,这时候的横轴就不要看成是频率了,咱们可以看成时间),我们把它看成是一个每秒4个周期的正弦信号。这样我们在伪坐标轴上面的4Hz的地方给它一个峰值。而频谱的细节部分主要是高频。我们把它看成是一个每秒100个周期的正弦信号。这样我们在伪坐标轴上面的100Hz的地方给它一个峰值。
在实际中咱们已经知道log X[k],所以我们可以得到了x[k]。那么由图可以知道,h[k]是x[k]的低频部分,那么我们将x[k]通过一个低通滤波器就可以得到h[k]了!没错,到这里咱们就可以将它们分离开了,得到了我们想要的h[k],也就是频谱的包络。
x[k]实际上就是倒谱Cepstrum(这个是一个新造出来的词,把频谱的单词spectrum的前面四个字母顺序倒过来就是倒谱的单词了)。而我们所关心的h[k]就是倒谱的低频部分。h[k]描述了频谱的包络,它在语音识别中被广泛用于描述特征。
那现在总结下倒谱分析,它实际上是这样一个过程:
1)将原语音信号经过傅里叶变换得到频谱:X[k]=H[k]E[k];
只考虑幅度就是:|X[k] |=|H[k]||E[k] |;
2)我们在两边取对数:log||X[k] ||= log ||H[k] ||+ log ||E[k] ||。
3)再在两边取逆傅里叶变换得到:x[k]=h[k]+e[k]。
这实际上有个专业的名字叫做同态信号处理。它的目的是将非线性问题转化为线性问题的处理方法。对应上面,原来的语音信号实际上是一个卷性信号(声道相当于一个线性时不变系统,声音的产生可以理解为一个激励通过这个系统),第一步通过卷积将其变成了乘性信号(时域的卷积相当于频域的乘积)。第二步通过取对数将乘性信号转化为加性信号,第三步进行逆变换,使其恢复为卷性信号。这时候,虽然前后均是时域序列,但它们所处的离散时域显然不同,所以后者称为倒谱频域。
总结下,倒谱(cepstrum)就是一种信号的傅里叶变换经对数运算后再进行傅里叶反变换得到的谱。它的计算过程如下:
好了,到这里,我们先看看我们刚才做了什么?给我们一段语音,我们可以得到了它的频谱包络(连接所有共振峰值点的平滑曲线)了。但是,对于人类听觉感知的实验表明,人类听觉的感知只聚焦在某些特定的区域,而不是整个频谱包络。
而Mel频率分析就是基于人类听觉感知实验的。实验观测发现人耳就像一个滤波器组一样,它只关注某些特定的频率分量(人的听觉对频率是有选择性的)。也就说,它只让某些频率的信号通过,而压根就直接无视它不想感知的某些频率信号。但是这些滤波器在频率坐标轴上却不是统一分布的,在低频区域有很多的滤波器,他们分布比较密集,但在高频区域,滤波器的数目就变得比较少,分布很稀疏。
人的听觉系统是一个特殊的非线性系统,它响应不同频率信号的灵敏度是不同的。在语音特征的提取上,人类听觉系统做得非常好,它不仅能提取出语义信息, 而且能提取出说话人的个人特征,这些都是现有的语音识别系统所望尘莫及的。如果在语音识别系统中能模拟人类听觉感知处理特点,就有可能提高语音的识别率。
梅尔频率倒谱系数(Mel Frequency Cepstrum Coefficient, MFCC)考虑到了人类的听觉特征,先将线性频谱映射到基于听觉感知的Mel非线性频谱中,然后转换到倒谱上。
将普通频率转化到Mel频率的公式是:
由下图可以看到,它可以将不统一的频率转化为统一的频率,也就是统一的滤波器组。
在Mel频域内,人对音调的感知度为线性关系。举例来说,如果两段语音的Mel频率相差两倍,则人耳听起来两者的音调也相差两倍。
我们将频谱通过一组Mel滤波器就得到Mel频谱。公式表述就是:log X[k] = log (Mel-Spectrum)。这时候我们在log X[k]上进行倒谱分析:
1)取对数:log X[k] = log H[k] + log E[k]。
2)进行逆变换:x[k] = h[k] + e[k]。
说白了就是流程还是一般的倒谱分析,只是在中间过程中,加入了梅尔三角滤波器组进行了一次过滤,而这个滤波器组设计是为了更加仿生,提高效果,按照上面那样设计滤波器组。于是相较于一般的倒谱分析,将频率坐标轴标成了梅尔频率坐标轴(代码中往往直接过渡到离散的滤波用的梅尔频率上,可能是为了降维吧,干脆就直接离散到梅尔中心频率上了。。),并且对应的频率进行了滤波,幅值有变化。
进一步理解,卷积等价于傅里叶变换,频率乘积等价于时域卷积,滤波的话就是频率乘积,时域卷积。。这样一想,神经网络也是一种维度变化,或者特征(表征)的不断进化。。
于是在Mel频谱上面获得的倒谱系数h[k]就称为Mel频率倒谱系数,简称MFCC。
现在咱们来总结下提取MFCC特征的过程:(具体的数学过程网上太多了,这里就不想贴了)
1)先对语音进行预加重、分帧和加窗;
2)对每一个短时分析窗,通过FFT得到对应的频谱;
3)将上面的频谱通过Mel滤波器组得到Mel频谱;
4)在Mel频谱上面进行倒谱分析(取对数,做逆变换,实际逆变换一般是通过DCT离散余弦变换来实现,取DCT后的第2个到第13个系数作为MFCC系数),获得Mel频率倒谱系数MFCC,这个MFCC就是这帧语音的特征;
这时候,语音就可以通过一系列的倒谱向量来描述了,每个向量就是每帧的MFCC特征向量。
这样就可以通过这些倒谱向量对语音分类器进行训练和识别了。
[1]这里面还有一个比较好的教程:
http://practicalcryptography.com/miscellaneous/machine-learning/guide-mel-frequency-cepstral-coefficients-mfccs/
[2]本文主要参考:cmu的教程:
http://www.speech.cs.cmu.edu/15-492/slides/03_mfcc.pdf
[3] C library for computing Mel Frequency Cepstral Coefficients (MFCC)
http://code.google.com/p/libmfcc/
以上就是原文,其实还有一个批注版:
https://www.cnblogs.com/BaroC/p/4283380.html
可以结合我的一个组会PPT进行更深入理解。。
20180529VAD&TIMIT.pptx
MFCC与语音分析.txt
需要的留言给我邮箱。。
这里我还想补充一些东西,进一步进入代码实现阶段。
https://blog.csdn.net/qq_39516859/article/details/80815369
程序:
fs=8000;
fl=0; fh=fs/2;
bl=1125*log(1+fl/700);%将频率转换为Mel频率
bh=1125*log(1+fh/700);
p=24;%滤波器个数
nfft=256;%FFT点数
B=bh-bl;
y=linspace(0,B,p+2);%产生0到B之间p+2个数
Fb=700*(exp(y/1125)-1);%将Mel频率转换为频率
W2=nfft/2+1;%fs/2内对应的FFT点数
df=fs/nfft;
freq=(0:W2-1)*df;%采样频率值
bank=zeros(24,W2);%生成一个24行W2列的全零数组
for k=2:p+1%why从2开始?因为k-1
f1=Fb(k-1); f2=Fb(k+1); f0=Fb(k);
n1=floor(f1/df)+1;%f(m-1)在频域中的谱线索引号
n2=floor(f2/df)+1;%f(m+1)在频域中的谱线索引号
n0=floor(f0/df)+1;%f(m)在频域中的谱线索引号。f(m)是从0开始,而在MATLAB中数组的索引是从1开始,所以要加1,否则会出现index=0的错误
for i=1 : W2
if i>=n1 & i<=n0
bank(k-1,i)=(i-n1)/(n0-n1);
elseif i>n0 & i<=n2
bank(k-1,i)=(n2-i)/(n2-n0);
end
end
plot(freq,bank(k-1,:),'r','linewidth',2); hold on
end
grid;
程序:
import numpy as np
import pylab as plt
fs = 8000
fl = 0
fh = fs/2
bl = 1125*np.log(1+fl/700) # 把 Hz 变成 Mel
bh = 1125*np.log(1+fh/700)
p = 24
NFFT=256
B = bh-bl
y = np.linspace(0,B,p+2)# 将梅尔刻度等间隔
#print(y)
Fb = 700*(np.exp(y/1125)-1)# 把 Mel 变成 Hz
#print(Fb)
W2 = int(NFFT / 2 + 1)
df = fs/NFFT
freq = []#采样频率值
for n in range(0,W2):
freqs = int(n*df)
freq.append(freqs)
bank = np.zeros((24,W2))
for k in range(1,p+1):
f1 = Fb[k-1]
f2 = Fb[k+1]
f0 = Fb[k]
n1=np.floor(f1/df)
n2=np.floor(f2/df)
n0=np.floor(f0/df)
for i in range(1,W2):
if i>=n1 and i<=n0:
bank[k-1,i]=(i-n1)/(n0-n1)
elif i>n0 and i<=n2:
bank[k-1,i]=(n2-i)/(n2-n0)
# print(k)
# print(bank[k-1,:])
plt.plot(freq,bank[k-1,:],'r')
plt.show()
我这里也还有另外一份matlab代码,可以供参考学习,需要的全套的,一样可以留言给我邮箱。
%% clear
clear all; close all; clc;
%% Pre-process
% parameter definition
filename = 'sx438.wav';
frame_length_ms = 25;
frame_shift_ms = 10;
dc_offset = 300;
pre_emphasis_coef = 0.97;
rand_noise_factor = 0.6;
add_rand_noise = true;
% step 1 : read wav
wav = audioread(filename, 'native');
figure;
subplot(2,1,1); plot(wav); title('waveform'); hold on;
% step 2 : add dc offset
wav = wav + dc_offset;
subplot(2,1,2); plot(wav); title('add dc offset'); hold on;
% step 3 : extract frame
info = audioinfo(filename); info
num_of_frames = floor((info.Duration * 1000 - frame_length_ms) / frame_shift_ms); % avoid out of range
num_of_per_frame = (info.SampleRate * frame_length_ms / 1000);
frames = zeros(num_of_frames, num_of_per_frame);
for f = 1:num_of_frames
idx = (f - 1) * (frame_shift_ms * info.SampleRate / 1000) + 1; % addrees start from 1
frames(f,:) = wav(idx:idx+num_of_per_frame-1)';
if add_rand_noise
for i = 1:num_of_per_frame
frames(f,i) = frames(f,i) - 1.0 + 2 * rand(); % rand [-1, 1)
end
end
end
figure;
subplot(3,1,1); plot(frames(51,:)); title('pre-sub-mean'); hold on;
% step 4 : sub mean
for f = 1:num_of_frames
me = mean(frames(f,:));
frames(f,:) = frames(f,:) - me;
end
subplot(3,1,2); plot(frames(51,:)); title('post-sub-mean'); hold on;
% step 5 : pre-emphasis
for f = 1:num_of_frames
for i = num_of_per_frame:-1:2
frames(f,i) = frames(f,i) - pre_emphasis_coef * frames(f,i-1);
end
end
subplot(3,1,3); plot(frames(51,:)); title('post-pre-emph'); hold on;
% step 6 : hamming window
hamming = zeros(1, num_of_per_frame);
for i = 1:num_of_per_frame
hamming(1,i) = 0.54 - 0.46*cos(2*pi*(i-1) / (num_of_per_frame-1));
end
figure;
plot(hamming); title('Hamming Window Function'); hold on;
% step 7 : add window
figure;
subplot(2,1,1); plot(1:400, frames(51,:),'r', 1:400, hamming, 'g'); title('pre-add-win'); hold on;
for f = 1:num_of_frames
frames(f,:) = frames(f,:) .* hamming;
end
subplot(2,1,2); plot(1:400, frames(51,:)); title('post-add-win'); hold on;
%% FbankFilter Analysis
% fft
NFFT = 2^nextpow2(num_of_per_frame); % Next power of 2 from length of num_of_per_frame
frames_fft = complex(zeros(num_of_frames, NFFT));
for f = 1:num_of_frames
frames_fft(f,:) = fft(frames(f,:), NFFT); % pad 0
end
freq = info.SampleRate / 2 * linspace(0, 1, NFFT/2 + 1); % frequency of fft
figure; plot(freq, 2*abs(frames_fft(1,1:NFFT/2+1))); hold on;
xlabel('Frequency(Hz)'); ylabel('|frame\_fft|');
% compute energy
energy_frames = zeros(num_of_frames, NFFT/2);
for f = 1:num_of_frames
for i = 1:NFFT/2
energy_frames(f,i) = abs(frames_fft(f,i));
end
end
% mel filter coef
num_of_bins = 40;
low_freq = 20; high_freq = info.SampleRate / 2;
fft_bin_width = info.SampleRate / NFFT;
mel_low_freq = 2595*log10(1 + low_freq / 700);
mel_high_freq = 2595*log10(1 + high_freq / 700);
mel_freq_delta = (mel_high_freq - mel_low_freq) / (num_of_bins + 1);
mel_coef = zeros(num_of_bins, (NFFT/2)+3);
for b = 1:num_of_bins
left_mel = mel_low_freq + (b - 1) * mel_freq_delta;
center_mel = mel_low_freq + (b) * mel_freq_delta;
right_mel = mel_low_freq + (b + 1) * mel_freq_delta;
first_index = -1;
last_index = -1;
for f = 1:(NFFT/2)
tmp_freq = (fft_bin_width * (f - 1)) + 1;
tmp_mel_freq = 2595*log10(1 + tmp_freq / 700);
if tmp_mel_freq > left_mel && tmp_mel_freq < right_mel
weight = 0;
if tmp_mel_freq < center_mel
weight = (tmp_mel_freq - left_mel) / (center_mel - left_mel);
elseif tmp_mel_freq < right_mel
weight = (right_mel - tmp_mel_freq) / (right_mel - center_mel);
end
if first_index == -1
first_index = f;
end
last_index = f;
mel_coef(b, f) = weight;
end
end
mel_coef(b, (NFFT/2)+2) = first_index;
mel_coef(b, (NFFT/2)+3) = last_index;
end
figure;
for b = 1:num_of_bins
plot(mel_coef(b,1:(NFFT/2))); hold on;
end
xlabel('Frequency'); ylabel('mel_coef');
% take mel filter
mel_frames = zeros(num_of_frames, num_of_bins);
for f = 1:num_of_frames
for i = 1:num_of_bins
mel_frames(f,i) = sum(energy_frames(f,:) .* mel_coef(i,1:NFFT/2));
end
end
% take log
fbank_frames = log(mel_frames);
%% MFCC
N = num_of_bins; M = num_of_bins; % assume M == N == num_of_bins
mfcc_frames = zeros(num_of_frames, M);
dct_mat = discrete_cos_trans(N, M);
for f = 1:num_of_frames
mfcc_frames(f,:) = sqrt(2/N) * fbank_frames(f,:) * (dct_mat');
end
% mean
m = mean(mfcc_frames);
% variance
v = sqrt(mean(power(mfcc_frames, 2)) - power(m,2) + 0.0001); % avoid div 0
% cmvn
for f = 1:num_of_frames
mfcc_frames(f,:) = (mfcc_frames(f,:) - m) ./ v;
end
读者可以比较上面两个matlab实现的相同点与不同点。
更多学习请参考:
https://blog.csdn.net/richard2357/article/details/17147249
https://blog.csdn.net/qq_39516859/article/details/80505981
https://labrosa.ee.columbia.edu/matlab/rastamat/