语音识别-特征提取(Python实现)

学习主题:语音信号处理及特征提取

  • 1. 数字信号处理基础
    • 1.1 数字信号处理基础
    • 1.2 频率混叠
    • 1.3 奈奎斯特采样定理
    • 1.4 离散傅里叶变换(DFT
    • 1.5 DFT的性质
  • 2. 常用特征提取流程
    • step1:预加重
    • step2:加窗分帧
    • step3:傅里叶变换
    • step4:梅尔滤波器组和对数操作
    • step5:动态特征计算
    • step6:能量计算
  • 3. 实践
    • 3.1 实验代码
    • 3.2 实验结果
    • 3.3 实验分析:

1. 数字信号处理基础

1.1 数字信号处理基础

在科学和工程中遇到的大多数信号都是连续模拟信号,例如电压随着时间变化,一天中温度的变化等等,而计算机智能处理离散的信号,因此必须对这些连续的模拟信号进行转化。通过采样–量化来转换成数字信号。
以正弦波为例:

x(t)= sin(2πf0t)

其中f0表示信号本身的频率,单位是Hz,首先对正弦波进行采样,每t秒一次采样,并使用一定范围的离散数值来表示采样值,得离散信号x(n):

x(n)= sin(2πf0nt)

其中t为采样周期;f=1/t为采样频率或者采样率,表示1s内采样的点数,n表示离散整数序列
语音识别-特征提取(Python实现)_第1张图片

1.2 频率混叠

由于采样信号频谱发生变化,而出现高、低频成分发生混淆的一种现象。抽样时频率不够高,抽样出来的点既代表了信号中的低频信号的样本值,也同时代表高频信号样本值,在信号重建的时候,高频信号被低频信号代替,两种波形完全重叠在一起,形成严重失真。

1.3 奈奎斯特采样定理

采样频率大于信号中最大频率的两倍!

fs/2≥fmax

即在原始信号的一个周期内至少要采样两个点才能有效杜绝频率混叠问题 )

1.4 离散傅里叶变换(DFT

DFT将时域离散且周期的信号的时域变换到频域,分析信号中的频率成分,若是非周期的离散信号需要进行周期延拓再进行DFT
语音识别-特征提取(Python实现)_第2张图片
由此看出只有DFT在时域和频域上都具有离散和周期的特点,因此也只有DFT可以用计算机来处理

DFT定义: 给定一个长度为N的离散信号,DFT定义了对应的离散频域序列X(m 在这里插入图片描述 语音识别-特征提取(Python实现)_第3张图片
根据欧拉公式,DFT公式还可以为:
在这里插入图片描述
DFT本质上是一个线性变换:
语音识别-特征提取(Python实现)_第4张图片

1.5 DFT的性质

性质1. 对称性,对于实数信号有X(m)=X*(N-m)
语音识别-特征提取(Python实现)_第5张图片
DFT之后的离散频率序列的幅度具有对称性,因此在进行N点DFT之后,只需要保留钱N/2+1个点,语音信号特征提取时,一般使用512个点DFT,由于对称性只需要提取257个有效点。
性质2:X(m)实际上表示的是“谱密度”,如果对一个幅度为A实正弦波进行N点DFT,则DFT之后对应频率M和A之间的关系为:M=AN/2
性质3:DFT的线性
在这里插入图片描述
性质4:时移性,对x(n)左移k个采样点进行DFT得到

在这里插入图片描述
DFT的频率轴:频率分辨率 fs/N 表示最小的频率间隔 当N越大时,频率分辨率越高,在频域上第m个点所表示的分析频率为:
在这里插入图片描述
从这个角度我们可以理解为X(m)的幅值体现了原信号中频率成为为(m/N)fs Hz的信号的强度

为了提高频率分辨率,我们可以将时域长度为N的信号x(n)补0来增加信号的长度从而提高频率分辨率,对信号进行补0的操作不会影响DFT的结果,在这FFT(快速傅里叶变换)中和语音信号分析中非常常见,比如在语音特征提取阶段对于16k采样率的信号,一帧语音信号长度为400采样点,为了进行512点的FFT,通常将400个点补0,得到512个采样点,最后只需要前257个点。

2. 常用特征提取流程

Fbank和MFCC提取流程
语音识别-特征提取(Python实现)_第6张图片

step1:预加重

· 提高信号高频部分能量
· 预加重滤波器是一个一阶高通滤波器,给定时域输入信号x[n],预加重之后的信号为
y[n] = x[n] - ax[n-1] ,其中0.9≤a≤1.0

step2:加窗分帧

· 语音信号为非平稳信号,其统计属性是随着时间变化的
· 语音信号又具有短时平稳的属性,在进行语音识别的时候,对于一句话,识别的过程也是以较小的发音单元(音素、字音素或者字、字节)为单位进行识别,因此用滑动窗来提取短时片段
**·**帧长、帧移、窗函数,对于采样率为16kHz的信号,帧长、帧移一般为25ms、10ms即400和160个采样点
· 分帧的过程,在时域上即是用一个窗函数和原始信号进行相乘 y[n]=w[n]x[n],w[n]为窗函数,常用矩形窗和汉明窗。在加窗的过程中一般不直接使用矩形窗,实际上是在时域上将信号截断,窗函数与信号在时域相乘,就等于对应的频域表示进行卷积(),矩形窗主瓣窄但是旁瓣较大(红色部分),将其与原信号的频域表示进行卷积就会导致频率泄露。

step3:傅里叶变换

经过上一步分帧之后的语音帧,已经从时域变换到了频域,取DFT系数的模,得到谱特征。(语谱图的生成)

step4:梅尔滤波器组和对数操作

· DFT得到了每个频带上信号的能量,但是人耳对频率的感知不是等间隔的,近似于对数函数
· 将线性频率转换为梅尔频率,梅尔频率和线性频率的转换关系是:mel=2595log10(1+f/700)
· 梅尔三角滤波器组:根据起始频率、中间频率和截止频率,确定各滤波系数
**·**梅尔滤波器组设计
确定滤波器组个数P
根据采样率fs,DFT点数N,滤波器个数P,在梅尔域上等间隔的产生每个滤波器的起始频率,中间频率和截止频率,注意,上一个滤波器的中间频率为下一个滤波器的起始频率(存在overlap)
将梅尔域上每个三角滤波器的起始、中间和截止频率转换线性频率域,并对DFT之后的谱特征进行滤波,得到P个滤波器组能量,进行log操作,得到FBank特征
· MFCC特征在FBank特征的基础上继续进行IDFT变换等操作

step5:动态特征计算

· 一阶差分(△) △t =( c(t+1) - c(t-1))/2 (类比速度)
· 二阶差分(△△) △△t = (△(t+1) - △(t -1)) /2

step6:能量计算

e = ∑x²[n]

MFCC特征总结:一般常用的MFCC特征是39维,包括12维原始MFCC+12维一阶差分+12维二阶差分—+1维原始能量+一维一阶能量+一维二阶能量

MFCC特征一般用于对角GMM训练,各维度之间相关性小;FBank特征一般用于DNN训练

3. 实践

给定一段音频,取12维MFCC特征以及最终的FBank和MFCC特征

3.1 实验代码

import librosa
import numpy as np
from scipy.fftpack import dct
# If you want to see the spectrogram picture
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
def plot_spectrogram(spec, note,file_name):
    """Draw the spectrogram picture
        :param spec: a feature_dim by num_frames array(real)
        :param note: title of the picture
        :param file_name: name of the file
    """
    fig = plt.figure(figsize=(20, 5))
    heatmap = plt.pcolor(spec)
    fig.colorbar(mappable=heatmap)
    plt.xlabel('Time(s)')
    plt.ylabel(note)
    plt.tight_layout()
    plt.savefig(file_name)


#preemphasis config 
alpha = 0.97

# Enframe config
frame_len = 400      # 25ms, fs=16kHz
frame_shift = 160    # 10ms, fs=15kHz
fft_len = 512

# Mel filter config
num_filter = 23
num_mfcc = 12

# Read wav file
wav, fs = librosa.load('./test.wav', sr=None)

# Enframe with Hamming window function
def preemphasis(signal, coeff=alpha):
    """perform preemphasis on the input signal.

        :param signal: The signal to filter.
        :param coeff: The preemphasis coefficient. 0 is no filter, default is 0.97.
        :returns: the filtered signal.
    """
    return np.append(signal[0], signal[1:] - coeff * signal[:-1])

def enframe(signal, frame_len=frame_len, frame_shift=frame_shift, win=np.hamming(frame_len)):
    """Enframe with Hamming widow function.

        :param signal: The signal be enframed
        :param win: window function, default Hamming
        :returns: the enframed signal, num_frames by frame_len array
    """
    
    num_samples = signal.size
    num_frames = np.floor((num_samples - frame_len) / frame_shift)+1
    frames = np.zeros((int(num_frames),frame_len))
    for i in range(int(num_frames)):
        frames[i,:] = signal[i*frame_shift:i*frame_shift + frame_len] 
        frames[i,:] = frames[i,:] * win

    return frames

def get_spectrum(frames, fft_len=fft_len):
    """Get spectrum using fft
        :param frames: the enframed signal, num_frames by frame_len array
        :param fft_len: FFT length, default 512
        :returns: spectrum, a num_frames by fft_len/2+1 array (real)
    """
    cFFT = np.fft.fft(frames, n=fft_len)
    valid_len = int(fft_len / 2 ) + 1
    spectrum = np.abs(cFFT[:,0:valid_len])
    return spectrum

def fbank(spectrum, num_filter = num_filter):
    """Get mel filter bank feature from spectrum
        :param spectrum: a num_frames by fft_len/2+1 array(real)
        :param num_filter: mel filters number, default 23
        :returns: fbank feature, a num_frames by num_filter array 
        DON'T FORGET LOG OPRETION AFTER MEL FILTER!
    """
    low_mel_freq = 0
    high_mel_freq = 2595 * np.log10(1+(fs /2)/700) #转到梅尔尺度上
    mel_filters_points = np.linspace(low_mel_freq,high_mel_freq,num_filter+2)
    freq_filters_pints = (700 * (np.power(10.,(mel_filters_points/2595))-1))
    freq_bin = np.floor(freq_filters_pints / (fs /2)*(fft_len /2 + 1))
    feats=np.zeros((int(fft_len/2+1), num_filter))
    for  m in range(1,num_filter+1):
        bin_low = int(freq_bin[m-1])
        bin_medium = int(freq_bin[m])
        bin_high = int(freq_bin[m+1])
        for k in range(bin_low,bin_medium):
            feats[k,m-1]=(k-freq_bin[m-1])/(freq_bin[m]-freq_bin[m-1])
        for k in range(bin_medium,bin_high):
            feats[k,m-1]=(freq_bin[m+1]-k)/(freq_bin[m+1]-freq_bin[m])
    feats = np.dot(spectrum,feats)
    feats = 20 *np.log10(feats)
    return feats

def mfcc(fbank, num_mfcc = num_mfcc):
    """Get mfcc feature from fbank feature
        :param fbank: a num_frames by  num_filter array(real)
        :param num_mfcc: mfcc number, default 12
        :returns: mfcc feature, a num_frames by num_mfcc array 
    """

    #feats = np.zeros((fbank.shape[0],num_mfcc))
    mfcc = dct(fbank, type=2, axis=1, norm='ortho')[:, 1:(num_mfcc+1)]
    return mfcc

def write_file(feats, file_name):
    """Write the feature to file
        :param feats: a num_frames by feature_dim array(real)
        :param file_name: name of the file
    """
    f=open(file_name,'w')
    (row,col) = feats.shape
    for i in range(row):
        f.write('[')
        for j in range(col):
            f.write(str(feats[i,j])+' ')
        f.write(']\n')
    f.close()


def main():
    wav, fs = librosa.load('./test.wav', sr=None)
    signal = preemphasis(wav)
    frames = enframe(signal)
    spectrum = get_spectrum(frames)
    fbank_feats = fbank(spectrum)
    mfcc_feats = mfcc(fbank_feats)
    plot_spectrogram(fbank_feats, 'Filter Bank','fbank.png')
    write_file(fbank_feats,'./test.fbank')
    plot_spectrogram(mfcc_feats.T, 'MFCC','mfcc.png')
    write_file(mfcc_feats,'./test.mfcc')


if __name__ == '__main__':
    main()

3.2 实验结果

FBank
语音识别-特征提取(Python实现)_第7张图片
MFCC
语音识别-特征提取(Python实现)_第8张图片

3.3 实验分析:

你可能感兴趣的:(python,语音识别,MFCC)