Tensorflow API中LSTM的参数提取及手工复现模型推理(不使用API复现)

Tensorflow API中LSTM的参数提取及手工复现模型推理(不使用API复现)

对于深度学习任务,使用Tensorflow的API进行训练,推理是目前主流的实现方式。但是训练模型和运用模型进行推理,往往处于不同的工作场景。比如训练模型使用服务器的GPU集群进行加速训练,而通常希望运用模型的场景是在嵌入式设备上,ARM、FPGA、或者像我的需求一样,需要设计ASIC来进行加速推理(反正应用场景就是没有GPU也多半装不上TensorFlow )。
附:本文不提供LSTM模型训练的教程,本文适用于需要从Tensorflow 模型中提取LSTM参数,并且以及需要手动复现的读者。代码由Python编写。提取的模型适用于TensorFlow 1.14及之前的版本。介绍的两种LSTM API手动复现时偏置计算时有细微区别,请读者注意。

LSTM的结构

由于这些设备上往往只能使用训练好的参数进行计算,但是成本限制或者产品需求不能使用API,因此必须要搞清楚网络结构,并且手动复现这个计算过程,以及提取出模型保存好的参数来进行推理。首先介绍LSTM的基本结构:
[结构图引自博客 ]https://blog.csdn.net/kami0116/article/details/94749564.
Tensorflow API中LSTM的参数提取及手工复现模型推理(不使用API复现)_第1张图片
其中的c_prev和h_prev是前一时刻的状态,c,h是当前时刻的输出。通常LSTM中会有多个隐藏层,隐藏层可以理解成多个LSTM的串接,前一个LSTM的输出作为下一个LSTM的输入。但是在实际应用中,如FPGA或者ASIC实现,从硬件思维来考虑,由于LSTM对于时间连续性的要求,使得多个LSTM和单个LSTM在算一路数据而言,其耗时相当。c_prev和h_prev是一组向量,而第一个LSTM单元的c_prev和h_prev被默认为全为0的向量值。
Wf,Wi,Wj,Wo则是LSTM的权重信息,j 在文献中通常会用C来表示,为了区分以及方便对照着这个结构图写代码,此处用 j 表示。

Tensorflow API中LSTM的参数提取及手工复现模型推理(不使用API复现)_第2张图片
公式中括号的[x,hprev]表示向量的拼接,[x,hprev] · Wf ,代表矩阵乘法,硬件中的操作是MAC(乘累加),*则是矩阵中相同位置的数字相乘没有累加这一操作。σ是sigmoid激活函数,tanh也是激活函数。

LSTM结构介绍完毕,代码分析。

TensorFlow的API中,有两个构造LSTM的函数:
1:

tf.contrib.rnn.BasicLSTMCell() 

2:

tf.contrib.cudnn_rnn.CudnnLSTM()

这两个LSTM的结构相同,但是细节不同,具体细节可以查阅TensorFlow的API指南。
我们复现的时候更多关心的不过是怎么调用这个API,以及哪些接口是有用的。对于第一种结构而言,使用了多少层LSTM就需要在构造图的时候申明多少次(注意,这个层不是隐藏层)。
使用第一种结构构造一个双向LSTM的代码:

			  # 前向
            lstm_fw_cell = tf.contrib.rnn.BasicLSTMCell(n_cell_dim, forget_bias=1.0, state_is_tuple=True)
            lstm_fw_cell = tf.contrib.rnn.DropoutWrapper(lstm_fw_cell,
                                                         input_keep_prob=keep_dropout)
            # 后向
            lstm_bw_cell = tf.contrib.rnn.BasicLSTMCell(n_cell_dim, forget_bias=1.0, state_is_tuple=True)
            lstm_bw_cell = tf.contrib.rnn.DropoutWrapper(lstm_bw_cell,
                                                         input_keep_prob=keep_dropout)

            # `layer_3`  `[n_steps, batch_size, 2*n_cell_dim]`
            layer_3 = tf.reshape(layer_3, [-1, batch_x_shape[0], n_hidden_3])
            outputs, output_states = 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=seq_length)

            # 连接正反向结果[n_steps, batch_size, 2*n_cell_dim]
            outputs = tf.concat(outputs, 2)

使用第二种结构构造单层LSTM

   cudnn_lstm_cells = tf.contrib.cudnn_rnn.CudnnLSTM(num_layers = 1,num_units = n_cell_dim,input_mode=cudnn_rnn.CUDNN_INPUT_LINEAR_MODE,
            #                                            direction=cudnn_rnn.CUDNN_RNN_UNIDIRECTION,dropout=keep_dropout_rate,dtype=tf.float32)
            
    outputs,output_state = cudnn_lstm_cells(inputs=lstm_in, initial_state=None,  time_major=True,sequence_lengths=seq_length,training=True)

第一种结构的代码参考自github的https://github.com/xxbb1234021/speech_recognition
第二种结构是设计需要的结构,虽然结构不一样,但仅是API的不同运用而已。
可以从入口参数看出,BasicLSTMCell有一个 forget_bias=1.0,这也是两种结构在推理时的唯一区别。
在推理时,有用的接口仅有2个,inputs和num_units,这两个函数的这两个参数完全相同(可以直接替换API的意思)。
值得注意的是:inputs 是一个(Batch,Time,Depth)形式的数据类型,在送入default mode 的API时需要转换成(Time,Batch,Depth)的形式,也可以改变API的工作模式不转换数据类型。API的调用请查看官方手册。seq_lenths是时间序列的长度,这个不太好解释,需要在实际任务中理解。这个参数来自于input中的 input[0] 也就是B,T,D中的T。
num_units 是隐藏层的个数,这个需要作为参数入口。
output同样是(Batch,Time,Depth)形式的数据类型,但是Depth等于隐藏单元的数目。(隐藏层和LSTM层数初学者可能不太好理解。隐藏层大概可以理解成LSTM的宽度,LSTM层可以理解成深度,比如双向LSTM就是2层LSTM)

The inference without API

不用API来进行模型推理,根据上文输入输出接口的叙述,仅有2个接口在推理时起计算的作用(待计算数据,隐藏层大小)。
首先读取模型:

import tensorflow as tf
import numpy as np
from tensorflow.python.tools.inspect_checkpoint import print_tensors_in_checkpoint_file
Model_Save_Path = "./model/model-1"   

def lstm_layer(input = None,num_units = None): #num_units =the number of  hidden_cells
    reader = tf.compat.v1.train.NewCheckpointReader(Model_Save_Path) 
    all_variables = reader.get_variable_to_shape_map() 
    print_tensors_in_checkpoint_file("./model/model-8", tensor_name=None, all_tensors=False, all_tensor_names=True,count_exclude_pattern="")
    lstm_b= reader.get_tensor("rnn/lstm_cell/bias")
    lstm_k_array= reader.get_tensor("rnn/lstm_cell/kernel")

    weights = lstm_k_array
    bias = lstm_b
    batch_x = input  #B T D
    Batch_shape = batch_x.shape  #取出batch 

input 是模型输入,格式为Batch ,Time_squence,Depth,但是由于是验证推理。因此Batch = 1,如果有多个Batch需要修改代码

注意:模型参数名需要可以通过print_tensors_in_checkpoint_file()进行打印,然后读取参数。
接下来取参数

	temp_len = len(weights)   #矩阵长度
    Wf = np.zeros((temp_len,num_units),dtype = np.float32)
    Wi = np.zeros((temp_len,num_units),dtype = np.float32)
    Wj = np.zeros((temp_len,num_units),dtype = np.float32)
    Wo = np.zeros((temp_len,num_units),dtype = np.float32)
    
    Wi = weights[:temp_len,:num_units]
    Wj = weights[:temp_len,num_units:num_units*2]
    Wf = weights[:temp_len,num_units*2:num_units*3]
    Wo = weights[:temp_len,num_units*3:]

    print('Wf:shape')# 
    print(Wf.shape)
    Bf = np.zeros((num_units),dtype = np.float32)
    Bi = np.zeros((num_units),dtype = np.float32)
    Bj = np.zeros((num_units),dtype = np.float32)
    Bo = np.zeros((num_units),dtype = np.float32)
    
    print('Bf:shape')# 
    print(Bf.shape)
    for x in range(num_units):
            Bi[x] = bias[x]
            Bj[x] = bias[num_units*1 +x]
            Bf[x] = bias[num_units*2 +x]
            Bo[x] = bias[num_units*3 +x]

LSTM在模型中以矩阵的形式保存的参数,其保存格式如上,矩阵每一行保存了4个权重信息,分别是i,j,f,o;

开始计算:

    def calculate(Input,w,b): 
        re = np.zeros((num_units,1),dtype = np.float32)   
        Input = np.transpose(Input)  #1*622
        re = np.dot(Input,w) + b + 1.0 #矩阵乘及加法 BasicLSTMCell,CudnnLSTM去掉 +1.0
        return re


    def cell(temp,h,c):
        fo = sigmoid(calculate(temp,Wf,Bf))
        #print('fo')
        io = sigmoid(calculate(temp,Wi,Bi))
        #print('io')
        jo = np.tanh(calculate(temp,Wj,Bj))
       # print('jo')
        o =  sigmoid(calculate(temp,Wo,Bo))
       # print('o')

        c = (c*fo) + (io * jo)

        h = np.tanh(c) * o
        return h,c
    h = np.zeros(num_units,dtype = np.float32) 
    c = np.zeros(num_units,dtype = np.float32)   

    output = np.zeros((Batch_shape[1],num_units),dtype = np.float32) 
    temp = np.zeros(temp_len,dtype = np.float32)   

    for x in range(Batch_shape[1]): #时间序列优先 225   
        temp = np.hstack((batch_x[0][x],h))
        h,c = cell(temp,h,c)
        output[x] = h    

    return output

代码结构完全与图中结构一致,调用numpy的库计算矩阵可以算的更快,也可以手写循环进行计算,但是会慢很多,实测慢100倍。其他API功能可以根据需要进行增减,核心功能如上所述。

你可能感兴趣的:(Tensorflow API中LSTM的参数提取及手工复现模型推理(不使用API复现))