导入框架
- 首先是一个经常使用的代码
%confing interactive.ast_node_interactivity='all'
%pprint
这两行代码的作用是在notebook代码框同时输出多个结果时,可以全部显示(默认是显示最后一次的结果)。
- 导入框架
import tensorflow as tf
import keras
from keras.layers import Input, BatchNormalization, LSTM
from keras.layers import Reshape, Dense, Lambda, Dropout
from keras.layers.recurrent import GRU
from keras.layers.merge import add, concatenate
from keras.optimizers import Adam, SGD, Adadelta
from keras import backend as K
from keras.models import Model
import os
from collections import Counter
import numpy as np
import random
import scipy.io.wavfile as wav
import matplotlib.pyplot as plt
from python_speech_features import mfcc
from keras.preprocessing.sequence import pad_sequences
在keras中有两种搭建网络的模型,一种是Sequential()像搭建积木一样进行搭建,第二种是通过Model()进行搭建,此次是用的Model()。sequential和model两者的区别。
在tf中使用GPU进行计算
tf.debugging.set_log_device_placement(True)
这个需要安装的tensorflow是gpu版的,并且电脑上有独立显卡,可以通过start+r,键入cmd开启命令窗口,然后输入nvidia -smi -l 显示GPU的内存以及算力消耗,也可以通过任务管理器看GPU的消耗来确定是否GPU是否参与了计算。
音频文件处理
- 生成音频列表
#生成音频列表
"""
dirpath表示的是路径
dirname表示当前路径的文件夹名
filenames表示当前路径下所有的文件
filenames.endswith(".wav")表示以.wav为扩展名的文件
"""
def genwavlist(wavpath):
wavfiles = {} #字典,用来存储去掉扩展名的文件名和其对应的路径
fileids = [] #列表,存储去掉扩展名的文件名
for (dirpath, dirnames, filenames) in os.walk(wavpath):
for filename in filenames:
if filename.endswith('.wav'):
filepath = os.sep.join([dirpath, filename])
fileid = filename.strip('.wav')
wavfiles[fileid] = filepath
fileids.append(fileid)
return wavfiles,fileids
此时data_thchs30自带一个label文件,其形式为{去掉扩展名后的文件名:标签},wavfiles生成的是一个字典{去掉扩展名后的wav文件名:wav文件的路径}。fileids存储的是所有的去掉扩展名后的wav文件名,通过fileids的长度可以知道整个语音文件的个数。
- 提取mfcc特征
# 对音频文件提取mfcc特征
def compute_mfcc(file):
fs, audio = wav.read(file) #读取采样频率和具体的数值
mfcc_feat = mfcc(audio, samplerate=fs, numcep=26) #提取mfcc特征,出来的是一个矩阵,比如(910,26)
mfcc_feat = mfcc_feat[::3] #对采样后的矩阵,每三行取一行数(304,26)
mfcc_feat = np.transpose(mfcc_feat) #对矩阵进行转置,(26,304)
#对矩阵进行padding,在304行之后补0,最后补到500行,矩阵维度为(500,26)
mfcc_feat = pad_sequences(mfcc_feat, maxlen=500, dtype='float', padding='post', truncating='post').T
return mfcc_feat
"""
pad_sequences(sequence,maxlen,dtype,padding,truncating)
sequence: 就是需要进行pad的序列
maxlen:定义序列的最大长度
dtype:数据类型,有int和float
padding:表示padding的位置,主要有“post”和“pre”两个参数,分别表示在序列之前和在序列之后
truncating:表示阶截断的位置,参数有“post”“pre”两个参数,我这里理解的是,
如果在序列之前padding就是前面阶段,如果再序列后面padding就是后向截断
value:padding的数值,默认的是0
"""
标签文件处理
- 生成词典
# 利用训练数据生成词典
def gendict(textfile_path):
dicts = []
textfile = open(textfile_path,'r+')
for content in textfile.readlines():
content = content.strip('\n')
#这个表示以空格为分隔符,分为2个,取第2个,即只要label,不要文件名
content = content.split(' ',1)[1]
content = content.split(' ') #对选择的标签在用空格作为分割符分开
dicts += (word for word in content) #将分开后的词语添加到字典中,这里可能存在重复的
counter = Counter(dicts) #计算字典中每个词出现的次数
words = sorted(counter) #对字典进行排序,生成词典(声调为5表示轻声)
wordsize = len(words) #只有1176个词,已经包含了空格
word2num = dict(zip(words, range(wordsize))) #给每一个词增加索引,生成一个{词:索引}的字典
num2word = dict(zip(range(wordsize), words)) #生成一个{索引:词}的字典
return word2num, num2word
上述代码中textfile_path表示的就是data_thchs30自带的那个标签文件(之前的版本有,现在新下的数据集没有这个标签文件),最后生成两个词典,word2num表示{词:索引},num2word表示{索引:词}。
- 将label中的词用数字进行替代
# 文本转化为数字
"""
返回两个值,第一个值是一个字典:将每一个语音文件标签转换成长度为50的数字列表
第二个值是一个词典:{词语:索引}
"""
def text2num(textfile_path):
lexcion,num2word = gendict(textfile_path)
"""
dict.get(key,default)根据key的值返回对应的value
如果指定的键不存在就返回default得值
"""
word2num = lambda word:lexcion.get(word, 0)
textfile = open(textfile_path, 'r+')
content_dict = {}
for content in textfile.readlines():
content = content.strip('\n')
cont_id = content.split(' ',1)[0] #音频文件的名字
content = content.split(' ',1)[1] #对应的label
content = content.split(' ')
content = list(map(word2num,content)) #将content中所有的词语转成字典中对应的索引
"""
固定最后每一个语音的输出为50,不足50的补0
"""
add_num = list(np.zeros(50-len(content)))
content = content + add_num
content_dict[cont_id] = content
return content_dict,lexcion
数据格式处理
- 将数据整理成网络可以接受的格式
# 将数据格式整理为能够被网络所接受的格式,被data_generator调用
def get_batch(x, y, train=False, max_pred_len=50, input_length=500):
# X = np.expand_dims(x, axis=3) #这是将原始矩阵变成一个4维的矩阵
X = x # for model2
# labels = np.ones((y.shape[0], max_pred_len)) * -1 # 3 # , dtype=np.uint8
labels = y
input_length = np.ones([x.shape[0], 1]) * ( input_length - 2 )
# label_length = np.ones([y.shape[0], 1])
label_length = np.sum(labels > 0, axis=1) #只需要大于0的label,因为0表示的空格
label_length = np.expand_dims(label_length,1)
inputs = {'the_input': X,
'the_labels': labels,
'input_length': input_length,
'label_length': label_length,
}
outputs = {'ctc': np.zeros([x.shape[0]])} # dummy data for dummy loss function
return (inputs, outputs)
input_length=500是因为在mfcc特征提取时固定了帧长为500,max_pre_len=50是因为在标签文件处理时固定了标签的长度为50 。
- 数据生成器
# 数据生成器,默认音频为thchs30\train,默认标注为thchs30\train.syllable,被模型训练方法fit_generator调用
def data_generate(wavpath = 'J:\\thchs30\\train', textfile = 'J:\\thchs30\\train.syllable.txt', bath_size=4):
wavdict,fileids = genwavlist(wavpath)
#wavdict为{wav文件名:路径},fileids为[wav文件名]
content_dict,lexcion = text2num(textfile)
#content_dict为{wav文件名:数字label},lexcion为{词汇:索引}
genloop = len(fileids)//bath_size #wav文件个数除以batch_size,算出每一次epoch经历多少个batch
print("all loop :", genloop)
genloop_index=np.arange(genloop)
np.random.shuffle(genloop_index)
index=0
while True:
feats = []
labels = []
if index=genloop:
index=0
i = genloop_index[index]
index=index+1
for x in range(bath_size):
num = i * bath_size + x
fileid = fileids[num]
# 提取音频文件的特征
mfcc_feat = compute_mfcc(wavdict[fileid])
feats.append(mfcc_feat)
# 提取标注对应的label值
labels.append(content_dict[fileid])
# 将数据格式修改为get_batch可以处理的格式
feats = np.array(feats)
labels = np.array(labels)
# 调用get_batch将数据处理为训练所需的格式
inputs, outputs = get_batch(feats, labels)
yield inputs, outputs
yield的具体用法可以参考这个博客: yield的用法 。
此时inputs就有四个值,分别为:the_input,the_labels,input_length,label_length; outputs的值为一个ctc的输出,即batch_size的ctc_loss 。
构建模型
- 定义ctc_lambda函数
# 被creatModel调用,用作ctc损失的计算
def ctc_lambda(args):
labels, y_pred, input_length, label_length = args
y_pred = y_pred[:, :, :]
return K.ctc_batch_cost(labels, y_pred, input_length, label_length)
Keras中自带ctc损失函数,其输入有原始标签,原始标签的长度,预测输出,输入长度。
- 构建网络模型
# 构建网络结构,用于模型的训练和识别
def creatModel():
input_data = Input(name='the_input', shape=(500, 26))
layer_h1 = Dense(512, activation="relu", use_bias=True, kernel_initializer='he_normal')(input_data)
#layer_h1 = Dropout(0.3)(layer_h1)
layer_h2 = Dense(512, activation="relu", use_bias=True, kernel_initializer='he_normal')(layer_h1)
layer_h3_1 = GRU(512, return_sequences=True, kernel_initializer='he_normal', dropout=0.3)(layer_h2)
layer_h3_2 = GRU(512, return_sequences=True, go_backwards=True, kernel_initializer='he_normal', dropout=0.3)(layer_h2)
layer_h3 = add([layer_h3_1, layer_h3_2])
layer_h4 = Dense(512, activation="relu", use_bias=True, kernel_initializer='he_normal')(layer_h3)
#layer_h4 = Dropout(0.3)(layer_h4)
output= Dense(1176, activation="softmax", use_bias=True, kernel_initializer='he_normal')(layer_h4)
# output = Activation('softmax')(layer_h5)
model_data = Model(inputs=input_data, outputs=output)
#ctc
labels = Input(name='the_labels', shape=[50], dtype='float32')
input_length = Input(name='input_length', shape=[1], dtype='int64')
label_length = Input(name='label_length', shape=[1], dtype='int64')
loss_out = Lambda(ctc_lambda, output_shape=(1,), name='ctc')([labels, output, input_length, label_length])
model = Model(inputs=[input_data, labels, input_length, label_length], outputs=loss_out)
model.summary()
ada_d = Adadelta(lr=0.01, rho=0.95, epsilon=1e-06)
#model=multi_gpu_model(model,gpus=2)
model.compile(loss={'ctc': lambda y_true, output: output}, optimizer=ada_d)
#test_func = K.function([input_data], [output])
print("model compiled successful!")
# return model, model_data
return model,model_data
上述模型并不是采用keras经常使用的sequential()模型,而是用的Model()模型,此时输出的模型有两个,model里面包含计算损失的Lambda层,model_data不包含计算损失,应该是用来做预测的。
其中 model.compile(loss={'ctc': lambda y_true, output: output}, optimizer=ada_d)这一行代码的原理可以这么理解:在普通的神经网络中我们通过计算出来的预测值和真实值来算一个误差,但是在这里lambda函数,输入进去的是y_true,output,输出出来的还是output,这是为什么?因为我们在网络中加了一层Lambda,它的输出output其实就是一个误差了,所以我们直接就输出出来就可以了,并不需要额外的进行计算。那这里为什么要就这一行代码呢,不是多此一举么?我理解的是,因为model.compile中间集成了很多自带的loss,加一个这样的步骤就是代码按原样返回预测张量output来绕过Keras内置的损失函数。 对于'ctc'层(即Lambda),loss={'ctc':...被绕过了损耗。
ctc_loss的参考博客
训练模型
def train():
# 准备训练所需数据
yielddatas = data_generate()
# 导入模型结构,训练模型,保存模型参数
# model, model_data = creatModel()
model = creatModel()
model.fit_generator(yielddatas, steps_per_epoch=2000, epochs=1)
# model.save_weights('model.mdl')
# model_data.save_weights('model_data.mdl')
steps_per_epoch:整数,当生成器返回steps_per_epoch次数据时计一个epoch结束,执行下一个epoch。这里的step_per_epochs应该是要等于genloop的数值。
代码地址