目录
1、实现目标
原始数据为200个音频文件,分别为真实人说话的声音,和机器合成的声音。
实现目标为成功将两种声音进行分类。
此贴记录了,实现的整个流程包括每一部分的代码以及背后的数学原理和方法的简介,以及每一部分出现的问题和坑点解决办法以及我的一些理解和疑惑。
希望这个帖子可以给做LSTM网络的朋友们一些参考。
2、数据读取
读取音频文件,并将音频文件转换成mfcc特征
import scipy.io.wavfile as wav
import librosa
from python_speech_features import mfcc
"""
librosa和python_speech_features是语音识别领域最常用的两个包,但是librosa的速度相对比较慢,尤其在读取音频数据上,并且要慢不少,而wav.read比较快然而会报错,主要的一个错误是下面这个:
ValueError: Incomplete wav chunk.
查了一下这个主要是wav是经过后期格式转换或者处理过的情况,导致文件wav文件信息不完整,总之这里会报错。网上给的解决办法是将WAV文件转换成PCM文件,或者使用librosa读取。
这里我使用了一个异常处理的语句,首先使用更快的wav.read()方法读取文件,如果报错则转换为librosa.load()方法。
python_speech_features.mfcc主要用于将音频转换成mfcc特征。
python_speech_features.base.mfcc(signal, samplerate=16000, winlen=0.025, winstep=0.01, numcep=13, nfilt=26, nfft=512, lowfreq=0, highfreq=None, preemph=0.97, ceplifter=22, appendEnergy=True, winfunc=<function <lambda>>)
同样的使用librosa.features.mfcc也可以提取mfcc特征
librosa.features.mfcc(y=None,sr=22050,S=None,n_mfcc=20,dct_type=2,norm='ortho',**kwargs)
"""
def def_wav_read_mfcc(file_name):
try:
fs, audio = wav.read(file_name)
# return fs:采样频率, data:数据
processed_audio = mfcc(audio, samplerate=fs, nfft = 2048)
except ValueError:
audio, fs = librosa.load(file_name)
# return y:数据, sr:采样频率
"""
load(
path,
sr=22050,
mono=True,
offset=0.0,
duration=None,
dtype=np.float32,
res_type="kaiser_best",
):
"""
processed_audio = mfcc(audio, samplerate=fs, nfft = 2048)
return processed_audio
关于MFCC:
1、什么是MFCC?
MFCC:Mel频率倒谱系数(Mel Frequency Cepstrum Coefficient,MFCC)的缩写.Mel频率是基于人耳听觉特性提出来的,它与Hz频率成非线性对应关系.Mel频率倒谱系数(MFCC)则是利用它们之间的这种关系,计算得到的Hz频谱特征。
2、MFCC的过程和步骤?
这个网上有太多太多讲解了,详情请移步:
语音识别——MFCC理解
3、为什么要将语音信息的时间序列转换成MFCC序列?
学过《信号与系统》的都知道一般对于连续时间信号处理的时候都会对其进行傅里叶变换转换成频域的信号进行处理。转换到频域中进行处理是为了便于对信号进行分析和处理。至于这个是为什么,详见《信号与系统》;
个人的理解MFCC就是先将时间序列分割成一小段一小段的状态,然后再在每一段进行nfft然后在频域进行处理最后提取出特征;
形象一点说明,就类似:语音“hello”,将一个hello在时间维度上拆解成单音音素,h,e,l,l,o(当然每个音节长度肯定不是一样的,这里只是方便说明将其拆解成五个音素,实际上每个音素之间还由重叠的部分,因此MFCC的帧移也都是1/3或者1/2存在一定的重叠。),对于这五个因素,对于“h”这一段因素进行傅里叶变换,然后再在频域对其进行分析。
下面是mfcc函数中内部每个param的注解,以及函数的api
def mfcc(signal,
# 用来计算特征的音频信号,应该是一个N*1的数组
samplerate=16000,
# 音频的采样率
winlen=0.025,
# 分析窗口的长度,以秒为单位,默认值是0.025秒(25毫秒)
winstep=0.01,
# 连续窗口之间的步长(以秒为单位),缺省值是0.01秒(10毫秒)
numcep=13,
# 返回的倒频谱数,默认为13
nfilt=26,
# 过滤器数量,默认为26。
nfft=512,
# FFT的大小,默认是512。
lowfreq=0,
# MEL滤波器的最低带边。以Hz为单位,默认为0。
highfreq=None,
# MEL滤波器的最高带边。以Hz为单位,默认为samplerate/2
preemph=0.97,
# 采用以preemph为系数的预强调滤波器。0是无滤波器。默认是0.97。
ceplifter=22,
# 对最终倒谱系数应用一个提升器。默认是22。
appendEnergy=True,
# 如果这是真的,第0倒谱系数就被替换为总坐标系能量的对数。
winfunc=lambda x:numpy.ones((x,)))
# 应用到每一帧的分析窗口。默认情况下不应用任何窗口。可以在这里使用numpy窗口函数,例如winfunc=numpy.hamming
return feat
函数最后返回的是频率信号的特征。
举个例子,原来的信号是[x1,x2,…xn]
经过MFCC后的信号应该是 [ [f11,f12,…,f113], [f21,f22,…,f213]…[fm1,fm2,…,fm13] ] 的形式
这里m应该取决于你原来信号中n的大小,原来信号的采样频率和分析窗口的长度的大小,以及帧移等参数。例如:本文所使用的wav文件。
读取后长度为22k的采样率,大约400w的数据,双声道,经过MFCC转换后,变成了一个(18188. 13)的特征序列。
具体计算过程移步:
MFCC特征提取详细计算过程
3、数据预处理
预读取数据
在训练过程需要获取数据,训练数据的加载方法通常有两种,其一是形如样本集比较小的情况,可以先将数据全部加载,再进行训练,另外一种是一个batch加载,加载一个完毕后训练再加载,这种方式无论多少数据,只要设置batch合适就不会出现内存不够用的情况,也不会报错。
def get_batch_for_train(label):
Wav = []
for j in range(200):
wav = gf.def_wav_read_mfcc(data[j])
# 将音频信号转换成mfcc的格式
wav1 = wav[:10000]
# 为了保证输入数据一样大小,只取读取音频转换成mfcc后前10000个数据
# print(wav1,i)
Wav.append(wav1)
print("Conversion to MFCC:",j+1)
label_for_train = label[0:199]
return Wav, label_for_train
label = []
# label为外部手动标注后,csv文件导入
Wav,label_for_train = get_batch_for_train(label)
这里label是一个二维数组,里面对应200个一维数组,其中1代表音频为真人,0代表合成音。为了保证输入训练集的数据格式一致,取MFCC序列前10000个作为作为训练数据。
因为只是简单的分类,这里 label 是手动标注的然后储存再一个csv文件中,对应文件在文件夹的顺序。所以没有音频的 label 的抽取过程,关于如何给音频标注并且抽取label的方法请移步:
praat使用入门演示
结合Praat进行语音实验的步骤
根据标注区域提取需要部分的语音特征参数
上面演示的帖子用的HTK,个人推荐用praat。
LSTM参数设定
基本的LSTM参数设定,没必要再提。
n_inputs = 13
# 输入一行,一行有13个数据,是由mfcc变换得到的
max_time = 10000
# 每个训练数据取前10000个向量作为训练数据
lstm_size = 78
# 隐层单元
n_classes = 1
# 预测1个值
batch_size = 20
# 每批次1个样本
n_batch = len(Wav) // batch_size
# 计算一共有多少个批次
nums_samples = len(Wav)
n = 10000
数据预处理
# 对原始的mfcc数据进行归一化
for i in range(len(Wav)):
scaler = MinMaxScaler(feature_range=(0, 1))
Wav[i] = scaler.fit_transform(Wav[i])
print("scaler:",i)
for i in range(len(Wav)):
Wav[i] = Wav[i].tolist()
# 生成的 Wav[] 里面放的是 array(15000×13)因此用循环将向量转换成列表
Wav_tensor = tf1.convert_to_tensor(Wav)
label_tensor = tf1.convert_to_tensor(label_for_train)
print("Success construct Wav_tensor")
为什么要对MFCC序列进行归一化?
归一化是大多数深度学习都需要进行的一个步骤。
归一化后加快了梯度下降求最优解的速度且有可能提高训练出模型的精度。
如果不进行归一化,对于训练数据结果可能是很准确的,但是每次的测试效果并不好。同时,收敛速度很慢。
对于语音信号,转换成MFCC特征序列后,对各个维度进行均值归一化会起到信道补偿的作用。消除不同声音信号由于幅值,干扰等因素而造成过拟合的或者训练较慢。
关于语音信号信道补偿方面的知识有兴趣的可以查看论文。
常用的归一化方法?
1、min-max标准化(Min-Max Normalization)
对原始数据的线性变换,使结果值映射到[0 - 1]之间
2、Z-score标准化方法
原始数据的均值(mean)和标准差(standard deviation)进行数据的标准化。经过处理的数据符合标准正态分布,即均值为0,标准差为1。
关于不同归一化方法和该方法的优缺点,详见下表:
图片来自知乎匿名用户
其实大多数语音信号处理MFCC后接归一化一般都是Min-Max方法。甚至大多数循环神经网络进行训练的数据使用的归一化方法都是Min-Max方法
获取数据集
这一部分没什么好说的,标准操作。
x_batch, y_batch = get_batch(Wav_tensor, label_tensor, batch_size)
# 生成每一个批次用于训练的数据集
x_batch_test, y_batch_test = get_batch(Wav_tensor, label_tensor, 100)
# 生成每一个批次用于测试的数据集
x_batch = tf1.convert_to_tensor(x_batch)
y_batch = tf1.convert_to_tensor(y_batch)
x = tf1.placeholder(tf1.float32, [None, 10000, 13])
y = tf1.placeholder(tf1.float32, [None, 1])
4、LSTM网络
def lstm_model(X, weights, biases):
inputs = tf1.reshape(X, [-1, max_time, n_inputs])
lstm_cell_1 = tf.keras.layers.LSTMCell(lstm_size)
outputs_1, final_state_1= tf1.nn.dynamic_rnn(lstm_cell_1, inputs, dtype=tf1.float32)
lstm_cell_2 = tf.keras.layers.LSTMCell(lstm_size)
outputs_2, final_state_2= tf1.nn.dynamic_rnn(lstm_cell_2, outputs_1, dtype=tf1.float32)
lstm_cell_3 = tf.keras.layers.LSTMCell(lstm_size)
outputs_3, final_state_3= tf1.nn.dynamic_rnn(lstm_cell_3, outputs_2, dtype=tf1.float32)
lstm_cell_4 = tf.keras.layers.LSTMCell(13)
outputs, final_state= tf1.nn.dynamic_rnn(lstm_cell_4, outputs_3, dtype=tf1.float32)
result = tf.nn.sigmoid(tf1.matmul(final_state[0], weights) + biases)
return result
weights = tf1.Variable(tf1.truncated_normal([13, n_classes], stddev=0.1))
biases = tf1.Variable(tf1.constant(0.1, shape=[n_classes]))
prediction = lstm_model(x,weights,biases)
我设定的LSTM网络是一个四层的网络。LSTM网络中需要设置的最主要的参数为每个节点的单元数,和LSTM网络的层数。
关于层数
对于一个LSTM网络,通常为1~4层,对于比较复杂的信息,可以采用3/4层的网络结构。但是层数越多可能效果反而不好,例如,对于本文所涉及的音频信号,使用三层网络训练500轮后的准确率达到93%,而四层网络经过500轮训练后准确率只有86%。
关于网络层数选择问题一般网上给出的答案都是通过经验一点一点调整。
我的建议是对于比较复杂的信息,可以先构建一个两层的看看效果,再试试三层的网络;而对于不是很复杂的信息,可以直接使用单层的LSTM网络观察下效果
关于节点
每一层LSTM网络都要设定单元数,这个参数通常也是通过经验来选取,网上并没有太好的系统性的办法选择这个参数,看了不少帖子加上自己的尝试,最好设定每一层的单元数为输入数据维数的二倍或者四倍,以这个为基准逐渐测试调整。这里我的每一层单元数选择为78,输入MFCC为13,78为13的六倍。单元数越多,训练速度越慢,同时也不能保证一定提高训练模型的精度和准确度。我还测试了单元数为52和26的网络,经过500轮训练准确度分别为89%和85%(当然一次训练结果也不能说明什么,而且500轮的时候也没有完全收敛)。
关于网络结构
tf.keras.layers.LSTMCell(unit)
这个api是构建 LSTM 网络基本结构 cell 的函数。
每一层可以定义一个 cell,对于一个四层的网络就可以定义四个 cell。定义的 cell 只是一个单独的静态的节点。
需要使用
tf1.nn.dynamic_rnn(lstm_cell_1, inputs, dtype=tf1.float32)
进行迭代,将整个序列循环输入,该函数输出结果为outputs 和 final_state,分别为输入的每一个序列经过 LSTM_CELL 的值,也就是LSTM原理图中的 ht
5、网络优化训练方法
cross_entropy = tf1.reduce_mean(tf1.square(y - prediction))
# 使用AdamOptimizer进行优化
train_step = tf1.train.AdamOptimizer(1e-3).minimize(cross_entropy)
correct_prediction = tf1.equal(y,tf.round(prediction))#argmax返回一维张量中最大的值所在的位置
# 求准确率
accuracy = tf1.reduce_mean(tf1.cast(correct_prediction,tf1.float32))#把correct_prediction变为float32类型
# 初始化
# model.fit(x_batch,y_batch,epochs=epochs,batch_size = batch_size)
# loss_and_metrics = model.evaluate(x_batch_test,y_batch_test)
# print(loss_and_metrics)
init = tf1.global_variables_initializer()
config = tf1.ConfigProto()
config.gpu_options.allocator_type = "BFC"
saver = tf1.train.Saver()
with tf1.Session() as sess:
with tf1.device('/gpu:0'):
sess.run(init)
loss = []
checkpoint_steps = 100
for i in range(400):
x_batch_data = sess.run(x_batch)
y_batch_data = sess.run(y_batch)
x_batch_test_data = sess.run(x_batch_test)
y_batch_test_data = sess.run(y_batch_test)
sess.run(train_step, feed_dict={
x: x_batch_data, y: y_batch_data})
# 进行学习
# 本次学习的数据
pred_X1 = sess.run(prediction, feed_dict={
x: x_batch_data})
# 预测模型输入测试数据获得的结果
# pred_X = sess.run(prediction, feed_dict={x: x_batch_test_data})
# 预测模型输入训练数据获得的结果
pred_X1 = pred_X1[0]
pred_X1 = pred_X1[0]
if pred_X1 >= 0.5:
print("This sound is True.")
else:
print("This sound is False.")
y_batch_data = y_batch_data[0]
if y_batch_data[0] >= 0.5:
y_r = True
else:
y_r = False
print("prediction",i,":",prediction,"; ","real value:",y_batch_data,"; ","test result:",pred_X1)
print("test results:"