#
#作者:韦访
#博客:https://blog.csdn.net/rookie_wei
#微信:1007895847
#添加微信的备注一下是CSDN的
#欢迎大家一起学习
#

16、Bi-RNN网络

数据准备好了,接着就应该搭建网络了,我们这里使用Bi-RNN网络,前面的博客中没有介绍这个网络,所以现在先来介绍一下这个网络。

Bi-RNN网络,又叫双向RNN网络,它采用了两个方向的RNN网络,如下图所示,

RNN网络擅长处理连续的数据,所以将正反两个方向的网络结合,就不仅可以学习它的正向规律,还可以学习它的反向规律,这样就比单个循环网络拥有更高的拟合度。

Bi-RNN跟RNN网络非常类似,只是在正向传播的基础上,再进行一次反向传播,且这两个都连接同一个输出层。

17、CTC

还得插讲一下其他内容,直接上代码的话会一脸懵逼。CTC(Connectionist Temporal Classification)是语音识别中的一个关键技术,通过增加一个额外的Symbol代表NULL来解决叠字的问题。

在基于连续的时间序列分类任务中,常用CTC的方法。

该方法主要体现在处理loss值上,通过对序列对不上的label添加blank(空)的方式,将预测的输出值与给定的label值在时间序列上对齐,再求出具体损失。如果以后用机会我再专门的研究这个,大家也可以自行百度,我们这里只要知道怎么使用它就可以了。

18、CTC loss

计算CTC loss在Tensorflow中封装成了ctc_loss函数,该函数的作用就是按照序列来处理输出标签和标注标签之间的损失。函数原型如下,

ctc_loss(labels, inputs, sequence_length,
        preprocess_collapse_repeated=False,
        ctc_merge_repeated=True,
        ignore_longer_outputs_than_inputs=False, time_major=True)

其中,

labels:是一个int32类型的稀疏矩阵张量(SparseTensor)。什么是稀疏矩阵等下再讲。

inputs:经过RNN后输出的标签预测值,是三维的浮点型张量,如果time_major=True,则它的形状为[max_time,batch_size,num_classes],否则为[batch_size,max_time,num_classes]。

sequence_lenght:序列长度

preprocess_collapse_repeated:是否需要预处理,将重复的label合并成一个label。

ctc_merge_repeated:在计算时,是否将每个non_blank重复的label当成单独的label来解释。

当取批次样本进行训练时,还需要对ctc_loss的返回值求均值,这个才是最终的loss。

上面参数中,需要注意的是inputs参数中的num_classes,如果样本中有classes个分类,那么,num_classes=classes+1,即num_classes要比classes多出一个分类,用来存放blank类。在后面实现的代码中就知道这点了。

19levenshtein距离

Levenshtein距离,也叫编辑距离(Edit Distance),指两个字符串之间,由一个转成另一个所需要的最少的编辑操作次数。编辑操作指的是,将一个字符替换成另一个字符、插入或者删除一个字符。编辑距离越小,说明两个字符串之间的相似度最大。

在Tensorflow中,编辑距离的计算被封装成对两个稀疏矩阵的操作,函数原型如下,

edit_distance(hypothesis, truth, normalize=True, name="edit_distance")

其中,

hypothesis:SparseTensor类型,为预测的序列结果。

truth:SparseTensor类型,为真实的序列结果。

normalize:求出来的编辑距离除以真实序列长度。

name:名字

返回值:R-1维的DenseTensor,包含每个序列的编辑距离。

20、CTC decoder

虽然输入ctc_loss中的inputs是我们的预测结果,但是这个结果却是带有空标签的(blank),而且是一个与时间序列强对应的输出。实际上我们需要的是一个转化好的,类似原始标注标签一个的输出。这时,我们可以使用CTC decoder,经过它对预测结果加工后,就可以与标准标签进行损失loss的运算了。

TensorFlow中,CTC decoder有两个函数,如下所示,

 

21、定义占位符

现在可以开始搭建网络模型了,我们将其封装成BiRNN类,我们在构造函数中保存一些传进来的参数并定义占位符,代码如下,

class BiRNN():
    def __init__(self, features, contexts, batch_size, hidden, cell_dim, stddev, keep_dropout_rate, relu_clip, character, save_path, learning_rate):
        self.features = features
        self.batch_size = batch_size
        self.contexts = contexts
        self.hidden = hidden
        self.stddev = stddev
        self.keep_dropout_rate = keep_dropout_rate
        self.relu_clip = relu_clip
        self.cell_dim = cell_dim
        self.learning_rate = learning_rate
        
        # input 为输入音频数据,由前面分析可知,它的结构是[batch_size, amax_stepsize, features + (2 * features * contexts)]
        #其中,batch_size是batch的长度,amax_stepsize是时序长度,n_input + (2 * features * contexts)是MFCC特征数,
        #batch_size是可变的,所以设为None,由于每一批次的时序长度不固定,所有,amax_stepsize也设为None
        self.input = tf.placeholder(tf.float32, [None, None, features + (2 * features * contexts)], name='input')
       
        # label 保存的是音频数据对应的文本的系数张量,所以用sparse_placeholder创建一个稀疏张量
        self.label = tf.sparse_placeholder(tf.int32, name='label')

        #seq_length保存的是当前batch数据的时序长度
        self.seq_length = tf.placeholder(tf.int32, [None], name='seq_length')

        #keep_dropout则是dropout的参数
        self.keep_dropout = tf.placeholder(tf.float32, name='keep_dropout')

22、构建网络模型

网络模型的话,先使用3个全连接层网络,然后经过一个Bi-RNN网络,最后再连接两个全连接层,且都带有dropout层。激活函数的话,使用带截断的Relu,截断值设置为20。

模型的shape变换有点多,我们输入的数据的结构是3维的,

[batch_size, amax_stepsize, n_input + (2 * n_input * n_context)]

我们要将它变成2维的,才能传入全连接层,

[amax_stepsize * batch_size, n_input + 2 * n_input * n_context]

全连接层到Bi-RNN网络时,又得转成3维的,

[amax_stepsize, batch_size, 2*n_cell_dim]

然后又得转成2维的,传入全连接层,

[amax_stepsize * batch_size, 2 * n_cell_dim]

最后,又得将2维的转成3维的输出,

[amax_stepsize, batch_size, n_character]

代码如下,

def network_init(self, input, character):
        # batch_x_shape: [batch_size, amax_stepsize, n_input + 2 * n_input * contexts]
        batch_x_shape = tf.shape(input)
    
        # 将输入转成时间序列优先
        input = tf.transpose(input, [1, 0, 2])
        # 再转成2维传入第一层
        # [amax_stepsize * batch_size, n_input + 2 * n_input * contexts]
        input = tf.reshape(input, [-1, self.features + 2 * self.features * self.contexts])
        
        # 使用clipped RELU activation and dropout.
        # 1st layer
        with tf.name_scope('fc1'):
            b1 = variable_on_cpu('b1', [self.hidden], tf.random_normal_initializer(stddev=self.stddev))        
            h1 = variable_on_cpu('h1', [self.features + 2 * self.features * self.contexts, self.hidden],
                                tf.random_normal_initializer(stddev=self.stddev))
            layer_1 = tf.minimum(tf.nn.relu(tf.add(tf.matmul(input, h1), b1)), self.relu_clip)
            layer_1 = tf.nn.dropout(layer_1, self.keep_dropout)
        
        # 2nd layer
        with tf.name_scope('fc2'):
            b2 = variable_on_cpu('b2', [self.hidden], tf.random_normal_initializer(stddev=self.stddev))
            h2 = variable_on_cpu('h2', [self.hidden, self.hidden], tf.random_normal_initializer(stddev=self.stddev))
            layer_2 = tf.minimum(tf.nn.relu(tf.add(tf.matmul(layer_1, h2), b2)), self.relu_clip)
            layer_2 = tf.nn.dropout(layer_2, self.keep_dropout)
    
        # 3rd layer
        with tf.name_scope('fc3'):
            b3 = variable_on_cpu('b3', [2 * self.hidden], tf.random_normal_initializer(stddev=self.stddev))
            h3 = variable_on_cpu('h3', [self.hidden, 2 * self.hidden], tf.random_normal_initializer(stddev=self.stddev))
            layer_3 = tf.minimum(tf.nn.relu(tf.add(tf.matmul(layer_2, h3), b3)), self.relu_clip)
            layer_3 = tf.nn.dropout(layer_3, self.keep_dropout)
    
        # 双向rnn
        with tf.name_scope('lstm'):
            # Forward direction cell:
            lstm_fw_cell = tf.contrib.rnn.BasicLSTMCell(self.cell_dim, forget_bias=1.0, state_is_tuple=True)
            lstm_fw_cell = tf.contrib.rnn.DropoutWrapper(lstm_fw_cell,
                                                        input_keep_prob=self.keep_dropout)
            # Backward direction cell:
            lstm_bw_cell = tf.contrib.rnn.BasicLSTMCell(self.cell_dim, forget_bias=1.0, state_is_tuple=True)
            lstm_bw_cell = tf.contrib.rnn.DropoutWrapper(lstm_bw_cell,
                                                        input_keep_prob=self.keep_dropout)
    
            # `layer_3`  `[amax_stepsize, batch_size, 2 * cell_dim]`
            layer_3 = tf.reshape(layer_3, [-1, batch_x_shape[0], 2 * self.cell_dim])
    
            outputs, _ = tf.nn.bidirectional_dynamic_rnn(cell_fw=lstm_fw_cell,
                                                                    cell_bw=lstm_bw_cell,
                                                                    inputs=layer_3,
                                                                    dtype=tf.float32,
                                                                    time_major=True,
                                                                    sequence_length=self.seq_length)
    
            # 连接正反向结果[amax_stepsize, batch_size, 2 * n_cell_dim]
            outputs = tf.concat(outputs, 2)
            # to a single tensor of shape [amax_stepsize * batch_size, 2 * n_cell_dim]
            outputs = tf.reshape(outputs, [-1, 2 * self.hidden])
    
        with tf.name_scope('fc5'):
            b5 = variable_on_cpu('b5', [self.hidden], tf.random_normal_initializer(stddev=self.stddev))
            h5 = variable_on_cpu('h5', [(2 * self.hidden), self.hidden], tf.random_normal_initializer(stddev=self.stddev))
            layer_5 = tf.minimum(tf.nn.relu(tf.add(tf.matmul(outputs, h5), b5)), self.relu_clip)
            layer_5 = tf.nn.dropout(layer_5, self.keep_dropout)
    
        with tf.name_scope('fc6'):
            # 全连接层用于softmax分类
            b6 = variable_on_cpu('b6', [character], tf.random_normal_initializer(stddev=self.stddev))
            h6 = variable_on_cpu('h6', [self.hidden, character], tf.random_normal_initializer(stddev=self.stddev))
            layer_6 = tf.add(tf.matmul(layer_5, h6), b6)
    
        # 将2维[amax_stepsize * batch_size, character]转成3维 time-major [amax_stepsize, batch_size, character].
        self.pred = tf.reshape(layer_6, [-1, batch_x_shape[0], character], name='pred')

23、定义损失函数和优化器

前面也说了,语音识别属于时序分类任务,要使用ctc_loss来计算损失。

    #损失函数
    def loss_init(self):
        # 使用ctc loss计算损失
        self.loss = tf.reduce_mean(ctc_ops.ctc_loss(self.label, self.pred, self.seq_length))

 而优化器还是使用梯度下降法AdamOptimizer。

    #优化器
    def optimizer_init(self):
        # 优化器        
        self.optimizer = tf.train.AdamOptimizer(learning_rate=self.learning_rate).minimize(self.loss)

24、使用CTC decoder和计算编辑距离

这里使用ctc_beam_search_decoder函数对预测结果进行解码,它返回值decoded是一个只有一个元素的数组,所以,使用edit_distance函数计算编辑距离时,我们应该传入的是decoded[0]。最后,对编辑距离取均值,求平均错误率,代码如下,

def accuracy_init(self):
        # 使用CTC decoder
        with tf.name_scope("decode"):
            self.decoded, _ = ctc_ops.ctc_beam_search_decoder(self.pred, self.seq_length, merge_repeated=False)
            
        # 计算编辑距离
        with tf.name_scope("accuracy"):
            distance = tf.edit_distance(tf.cast(self.decoded[0], tf.int32), self.label)
            # 计算label error rate (accuracy)
            self.label_error_rate = tf.reduce_mean(distance, name='label_error_rate')

25、建立session

我们先在构造函数调用上面定义的各种初始化函数,然后再创建session,并且实现如果我们在训练模型过程中中断了训练,再次运行程序时,它还能从我们保存的模型中继续训练的功能。代码如下,

        #创建会话
        self.sess = tf.Session()

        #需要保存模型,所以获取saver
        self.saver = tf.train.Saver(max_to_keep=1)

        #模型保存地址
        self.save_path = save_path
        #如果该目录不存在,新建
        if os.path.exists(self.save_path) == False:
            os.mkdir(self.save_path)

        #初始化
        self.sess.run(tf.global_variables_initializer())

        # 没有模型的话,就重新初始化
        cpkt = tf.train.latest_checkpoint(self.save_path)
        
        self.start_epoch = 0
        if cpkt != None:
            self.saver.restore(self.sess, cpkt)
            ind = cpkt.find("-")
            self.start_epoch = int(cpkt[ind + 1:])

26、run函数

接着,我们在run函数中实现训练模型的代码,并且验证模型的准确率,和边训练边保存模型的功能,代码如下,

def run(self, batch, source, source_lengths, sparse_labels, words, epoch):
        feed = {self.input: source, self.seq_length: source_lengths, self.label: sparse_labels, 
                    self.keep_dropout: self.keep_dropout_rate}

        # loss optimizer ;
        loss, _ = self.sess.run([self.loss, self.optimizer], feed_dict=feed)
        
        # 验证模型的准确率,比较耗时,我们训练的时候全力以赴,所以这里先不跑
        # if (batch + 1) % 1 == 0:
            
        #     feed2 = {self.input: source, self.seq_length: source_lengths, self.label: sparse_labels, self.keep_dropout: 1.0}
        
        #     decoded, label_error_rate = self.sess.run([self.decoded[0], self.label_error_rate], feed_dict=feed2)        
        #     dense_decodeds = tf.sparse_tensor_to_dense(decoded, default_value=0).eval(session=self.sess)
        #     dense_original_labels = sparse_tuple_to_text(sparse_labels, words)
        
        #     counter = 0            
        #     print('Label err rate: ', label_error_rate)
        #     for dense_original_label, dense_decoded in zip(dense_original_labels, dense_decodeds):
        #         # convert to strings
        #         decoded_str = dense_to_text(dense_decoded, words)                 
        #         print('Original: {}'.format(dense_original_label))
        #         print('Decoded:  {}'.format(decoded_str))
        #         print('------------------------------------------')
        #         counter = counter + 1
                

        #每训练100次保存一下模型
        if (batch + 1) % 100 == 0:
            self.saver.save(self.sess, os.path.join(self.save_path + "birnn_speech_recognition.cpkt"), global_step=epoch)

        return loss

27、train

一切都准备好了,我们现在就来调用它们实现模型的训练,这一步就比较简单了,我就直接上代码了,代码如下,

 

from audio_processor import AudioProcessor
import tensorflow as tf
import time
import numpy as np
from birnn import BiRNN
import os
# 梅尔倒谱系数的个数
features = 26
# 对于每个时间序列,要包含上下文样本的个数
contexts = 9
# batch大小
batch_size = 8

stddev = 0.046875

 
hidden = 1024
cell_dim = 1024

keep_dropout_rate = 0.95
relu_clip = 20

wav_path = 'dataset/data_thchs30/train'
tran_path = 'dataset/data_thchs30/data'

save_path = 'model/'

#迭代次数
epochs = 200

learning_rate = 0.001

def main(argv=None):

    if not os.path.exists(wav_path) or not os.path.exists(tran_path):
        print('目录', wav_path, '或', tran_path, "不存在!")
        return

    processor = AudioProcessor(wav_path, tran_path, features, contexts)
    words, words_size = processor.get_property()

    birnn = BiRNN(features, contexts, batch_size, hidden, cell_dim, stddev, keep_dropout_rate, relu_clip, words_size+1, save_path, learning_rate)
        
    print('Run training epoch')
    start_epoch = birnn.get_property()
    

    for epoch in range(epochs):  # 样本集迭代次数
        epoch_start_time = time.time()
        if epoch < start_epoch:
            continue
 
        print("epoch start:", epoch, "total epochs= ", epochs)        
        batches_per_epoch = processor.batches_per_epoch(batch_size)
        print("total loop ", batches_per_epoch, "in one epoch,", batch_size, "items in one loop")

        next_index = 0        
        #######################run batch####
        for batch in range(batches_per_epoch):  # 一次batch_size,取多少次            
            next_index, source, source_lengths, sparse_labels = processor.next_batch(next_index, batch_size)
            batch_loss = birnn.run(batch, source, source_lengths, sparse_labels, words, epoch)

            epoch_duration = time.time() - epoch_start_time
 
            log = 'Epoch {}/{}, batch:{}, batch_loss: {:.3f}, time: {:.2f} sec'
            print(log.format(epoch, epochs, batch, batch_loss, epoch_duration))

if __name__ == '__main__':
    tf.app.run()

 运行上面的代码,运行结果如下,

这样就开始训练模型了,我们可以看到,损失batch_loss在慢慢的下降,说明模型正在“学习”了,静等代码执行完毕吧,这个运行时间因机器而异,尽量使用高性能的显卡,我会上传我训练好的模型,大家可以在这基础上继续训练,或者测试。

运行结果:

可以看到,loss达到了个位数,甚至零点几,这个效果已经不错了。我们将训练时注释了的这段代码打开,再运行看看,

        # 验证模型的准确率,比较耗时,我们训练的时候全力以赴,所以这里先不跑
        if (batch + 1) % 1 == 0:            
            feed2 = {self.input: source, self.seq_length: source_lengths, self.label: sparse_labels, self.keep_dropout: 1.0}        
            decoded, label_error_rate = self.sess.run([self.decoded[0], self.label_error_rate], feed_dict=feed2)        
            dense_decodeds = tf.sparse_tensor_to_dense(decoded, default_value=0).eval(session=self.sess)
            dense_original_labels = sparse_tuple_to_text(sparse_labels, words)        
            counter = 0            
            print('Label err rate: ', label_error_rate)
            for dense_original_label, dense_decoded in zip(dense_original_labels, dense_decodeds):
                # convert to strings
                decoded_str = dense_to_text(dense_decoded, words)                 
                print('Original: {}'.format(dense_original_label))
                print('Decoded:  {}'.format(decoded_str))
                print('------------------------------------------')
                counter = counter + 1

运行结果,

 

28、历史遗留问题

如果以前看过我这篇语音识别博客的就知道,当时训练结束以后有如下问题,

--------------------------------补充-------------------------

20181102

训练了两天两夜,贴上训练结果,

可以看到,准确率还是可以了,但是,句子后面怎么有一堆的“龚龚龚龚龚龚龚龚龚龚龚龚龚龚龚龚龚龚龚龚龚龚龚龚龚龚龚龚龚龚龚龚龚龚龚”呢??先留下疑问,后续找到问题我们再更新博客。

-------------------------------------------------------------

其实出现这个问题并不是模型的问题,而是因为以前代码中,我在调用TensorFlow的tf.sparse_tensor_to_dense函数将批量的稀疏矩阵还原为稠密矩阵时,出现了像上一篇博客中,我们将稀疏表示还原为文字时出现的问题,截图如下,

(在稀疏表示还原成文字的时候,“盎然”后面多了几个空格,这是因为批量处理时,矩阵的shape是统一大小的,所以对于比较短的句子,就会在句子后面“补零”了。)

而我在调用TensorFlow的tf.sparse_tensor_to_dense函数时将默认值设置成了-1,这样的话就导致稀疏矩阵中的“零值”都被设置成了-1。而“龚”字又刚好在我们words列表中的最后一个字,

所以在进行批量语音识别时才会出现一堆“龚”字了。

解决的方法就是将default_value设为0,这样的话对于比较短的句子,它后面补的也只是空格而已。

29、完整代码

完整代码链接如下,

https://mianbaoduo.com/o/bread/ZZyWm58=

下一讲,我们将上面训练好的模型进行固化和应用。

 

 

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐