keras框架下GRU+CTC语音识别的笔记

导入框架

  • 首先是一个经常使用的代码
%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的数值。
代码地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,997评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,603评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,359评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,309评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,346评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,258评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,122评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,970评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,403评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,596评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,769评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,464评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,075评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,705评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,848评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,831评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,678评论 2 354