当前位置:   article > 正文

LSTM源码解析_lstm代码

lstm代码

作为CV从业者,大部分时间都是在跟CNN打交道,但RNN在图像方面的应用也越来越多,比如在文本检测算法CTPN中就有用到LSTM,因为文本是有顺序和前后语义联系的,用到LSTM也合情合理。相比于卷积的过程,LSTM更加复杂,虽然看了一些博客后了解了抽象概念,但还是想通过送入实际的值,调试代码来观察整个流程中的每一步来加深对其的理解。

本文以tensorflow-1.10版本为例,在eager模式下来进行lstm源码的调试。在阅读本文前,建议先阅读Understanding LSTM Networks了解LSTM的原理,这篇文章介绍的非常详细,本文就不再介绍具体原理了,只是对照这篇文章按照tensorflow中lstm的实际调用过程进行源码解析。

首先自己造一个输入,在CTPN中,一张图片首先经过多层卷积、池化操作,在输入LSTM前的shape为(batch_size, height, width, channel),首先将其reshape成(b×h, w, c),因为任务是检测图片中的文本行,因此这里的w就是lstm中的max_time。代码如下

  1. import tensorflow as tf
  2. import numpy as np
  3. tf.enable_eager_execution()
  4. net = np.ones((2, 2, 2, 3), dtype=np.float32) # (batch_size, h, w, c)
  5. net = tf.convert_to_tensor(net)
  6. shape = tf.shape(net)
  7. N, H, W, C = shape[0], shape[1], shape[2], shape[3]
  8. net = tf.reshape(net, [N * H, W, C]) # (4,2,3) (batch_size, time_step, embedding_size)
  9. lstm_fw_cell = tf.nn.rnn_cell.LSTMCell(num_units=2, initializer=tf.ones_initializer, state_is_tuple=True)
  10. output, final_state = tf.nn.dynamic_rnn(lstm_fw_cell, net, dtype=tf.float32)

其中num_units就是经过LSTM后的输出维度,在CNN中卷积后的输出维度(通道数)由卷积核的个数决定,具体可以查看这篇文章BasicLSTMCell中num_units参数解释。tf.ones_initializer将权重矩阵初始化为全1矩阵,方便调试计算。tf.nn.dynamic_rnn是用来封装lstm_cell的,具体步骤还是在LSTMCell中实现的。

tf.nn.dynamic_rnn的函数定义在../tensorflow/python/opts/rnn.py的455行。dynamic_rnn的入参有个time_major,默认为False。当input.shape = (batch_size, max_time, depth) 时,time_major=False。当Input.shape = (max_time, batch_size, depth) 时,time_major=True。当time_major=False时,因为我们input的shape是前者,在第583行,代码将其转成 (max_time, batch_size, depth) 格式。代码如下

  1. if not time_major:
  2. # (B,T,D) => (T,B,D)
  3. flat_input = [ops.convert_to_tensor(input_) for input_ in flat_input]
  4. flat_input = tuple(_transpose_batch_time(input_) for input_ in flat_input)

如上图所示是一个lstm cell,当time_step=0也就是在第一个时间步时,需要初始化一个C_{t-1}h_{t-1},由dynamic_rnn的入参initial_state传入,默认值为None,当initial_state不传值时必须传入dtype这个参数,否则会报错。在第600行代码会根据dtype创建一个全0的初始state。注意shape都是(batch_size, max_time)即(4, 2)。代码如下

  1. if initial_state is not None:
  2. state = initial_state
  3. else:
  4. if not dtype:
  5. raise ValueError("If there is no initial_state, you must give a dtype.")
  6. state = cell.zero_state(batch_size, dtype)
  7. print(state)
  8. LSTMStateTuple(c=<tf.Tensor: id=35, shape=(4, 2), dtype=float32, numpy=
  9. array([[0., 0.],
  10. [0., 0.],
  11. [0., 0.],
  12. [0., 0.]], dtype=float32)>, h=<tf.Tensor: id=40, shape=(4, 2), dtype=float32, numpy=
  13. array([[0., 0.],
  14. [0., 0.],
  15. [0., 0.],
  16. [0., 0.]], dtype=float32)>)

然后第624行调用函数_dynamic_rnn_loop,主要的入参包括cell、inputs和state,cell就是最开始代码中创建的lstm_fw_cell,inputs是transpose成(T, B, D)后的,state就是上面创建的全0初始状态。

接着进入函数_dynamic_rnn_loop中,快进到823行,通过control_flow_ops.while_loop循环调用函数_time_step,每一次调用是一个时间步,一共调用max_time次。

接着进入函数_time_step,其中在786行定义call_cell,在800行调用call_cell,代码如下

  1. call_cell = lambda: cell(input_t, state)
  2. (output, new_state) = call_cell()

这里的cell就是我们最开始创建的lstm_fw_cell,之前一开始也说过dynamic_rnn只是个封装函数,lstm的具体实现在LSTMCell里面,也就是在这里开始一个lstm cell的流程。

接着就要进入到../tensorflow/python/ops/rnn_cell_impl.py的652行的类LSTMCell中,实际调用的是807行的call函数,注释第一行就是Run one step of LSTM。

首先看到上面四个图就是LSTM一个time_step的过程,代码837行,(c_prev, m_prev) = state,state在上面的代码中已经打印出来了,是两个shape=(4, 2)的全零矩阵,这里的c_prev和m_prev就是上面代码中的c和h,对应上面图中的C_{t-1}h_{t-1}

接着第846行

  1. # i = input_gate, j = new_input, f = forget_gate, o = output_gate
  2. lstm_matrix = math_ops.matmul(
  3. array_ops.concat([inputs, m_prev], 1), self._kernel)
  4. lstm_matrix = nn_ops.bias_add(lstm_matrix, self._bias)
  5. i, j, f, o = array_ops.split(
  6. value=lstm_matrix, num_or_size_splits=4, axis=1)
  7. ############
  8. print(inputs)
  9. tf.Tensor(
  10. [[1. 1. 1.]
  11. [1. 1. 1.]
  12. [1. 1. 1.]
  13. [1. 1. 1.]], shape=(4, 3), dtype=float32)
  14. print(array_ops.concat([inputs, m_prev], 1))
  15. tf.Tensor(
  16. [[1. 1. 1. 0. 0.]
  17. [1. 1. 1. 0. 0.]
  18. [1. 1. 1. 0. 0.]
  19. [1. 1. 1. 0. 0.]], shape=(4, 5), dtype=float32)
  20. print(self._kernel)
  21. <tf.Variable 'rnn/lstm_cell/kernel:0' shape=(5, 8) dtype=float32, numpy=
  22. array([[1., 1., 1., 1., 1., 1., 1., 1.],
  23. [1., 1., 1., 1., 1., 1., 1., 1.],
  24. [1., 1., 1., 1., 1., 1., 1., 1.],
  25. [1., 1., 1., 1., 1., 1., 1., 1.],
  26. [1., 1., 1., 1., 1., 1., 1., 1.]], dtype=float32)>
  27. print(self._bias)
  28. <tf.Variable 'rnn/lstm_cell/bias:0' shape=(8,) dtype=float32, numpy=array([0., 0., 0., 0., 0., 0., 0., 0.], dtype=float32)>
  29. print(lstm_matrix)
  30. tf.Tensor(
  31. [[3. 3. 3. 3. 3. 3. 3. 3.]
  32. [3. 3. 3. 3. 3. 3. 3. 3.]
  33. [3. 3. 3. 3. 3. 3. 3. 3.]
  34. [3. 3. 3. 3. 3. 3. 3. 3.]], shape=(4, 8), dtype=float32)

首先看inputs,原始输入shape=(4,2,3),是(B,T,D)格式,之前有讲过dynamic_rnn一开始就将输入transpose成了(T,B,D)也就是(2,4,3)的格式,然后这里是一个time_step,故这里的inputs.shape=(4,3)。然后将其与m_prev拼接,shape变成(4,5),结果如代码中打印的所示。

接着773行可以看到self._kernel.shape=[input_depth + h_depth, 4 * self._num_units]=[3+2, 4*2],input_depth=3就是输入shape中的D,h_depth=self._num_units=2,这是我们一开始传入LSTMCell的参数num_units,之前我们提过num_units就是lstm_cell的输出维度,这里需要注意第二个维度之所以要乘以4,是因为如上面四张图中,有四个权重矩阵与输入相乘的式子W\cdot \left [ h_{t-1},x_{t} \right ]+b,区别在于W和b的下标不同,代码中将这四个不同的W拼接了起来一起计算,然后再split成了4个输出。

然后到859行

  1. c = (sigmoid(f + self._forget_bias) * c_prev + sigmoid(i) * self._activation(j))
  2. print(c)
  3. tf.Tensor(
  4. [[0.9478634 0.9478634]
  5. [0.9478634 0.9478634]
  6. [0.9478634 0.9478634]
  7. [0.9478634 0.9478634]], shape=(4, 2), dtype=float32)

其中self._forget_bias取默认值1.0,self._activation是tanh函数,其它值上面都已经print出来了,大家可以自己手动算一下最终结果是不是和上面打印的一样。这里的c就是上面第三张图中的C_{t}

最后869行

  1. m = sigmoid(o) * self._activation(c)
  2. print(m)
  3. tf.Tensor(
  4. [[0.7037754 0.7037754]
  5. [0.7037754 0.7037754]
  6. [0.7037754 0.7037754]
  7. [0.7037754 0.7037754]], shape=(4, 2), dtype=float32)

这里的m就是上面第四张图中计算的h_{t},至此LSTM的一个time_step计算完毕,源码中的c和m对应图中的C_{t}h_{t}

接着c和m组合成state传入下一个time_step中,重复同样的计算步骤,大家可以自己手动计算一下,第二个时间步的c和m结果如下

  1. tf.Tensor(
  2. [[1.9313017 1.9313017]
  3. [1.9313017 1.9313017]
  4. [1.9313017 1.9313017]
  5. [1.9313017 1.9313017]], shape=(4, 2), dtype=float32)
  6. tf.Tensor(
  7. [[0.9472957 0.9472957]
  8. [0.9472957 0.9472957]
  9. [0.9472957 0.9472957]
  10. [0.9472957 0.9472957]], shape=(4, 2), dtype=float32)

因为我们的输入max_time=2,所以第二个时间步整个流程就结束了。接着我们打印dynamic_rnn的返回output和final_state,结果如下

  1. print(output)
  2. tf.Tensor(
  3. [[[0.7037754 0.7037754]
  4. [0.9472957 0.9472957]]
  5. [[0.7037754 0.7037754]
  6. [0.9472957 0.9472957]]
  7. [[0.7037754 0.7037754]
  8. [0.9472957 0.9472957]]
  9. [[0.7037754 0.7037754]
  10. [0.9472957 0.9472957]]], shape=(4, 2, 2), dtype=float32)
  11. print(final_state)
  12. LSTMStateTuple(c=<tf.Tensor: id=154, shape=(4, 2), dtype=float32, numpy=
  13. array([[1.9313017, 1.9313017],
  14. [1.9313017, 1.9313017],
  15. [1.9313017, 1.9313017],
  16. [1.9313017, 1.9313017]], dtype=float32)>, h=<tf.Tensor: id=158, shape=(4, 2), dtype=float32, numpy=
  17. array([[0.9472957, 0.9472957],
  18. [0.9472957, 0.9472957],
  19. [0.9472957, 0.9472957],
  20. [0.9472957, 0.9472957]], dtype=float32)>)

在实际应用中,我们只需要output,final_state用不到。注意output是两个time_step的m值,即图中的向上的箭头h_{t},且因为一开始将输入由(B,T,D)转成了(T,B,D),最后要再转回(B,T,D)再返回。最终output.shape=(4,2,2),分别对应batch_size,max_time,num_units。

双向LSTM

在CTPN中实际用的是双向LSTM,代码中LSTMCell不变,封装函数由tf.nn.dynamic_rnn改为tf.nn.bidirectional_dynamic_rnn,其内部实现也很简单,就是正向和反向调用两次dynamic_rnn。但是在反向dynamic_rnn前需要将inputs先reverse一下,为了方便观察,将原始input由全1矩阵改为随机矩阵,然后inputs和inputs_reverse如下所示

  1. print(inputs)
  2. tf.Tensor(
  3. [[[0.84839684 0.21003267 0.49825752]
  4. [0.17281447 0.92418146 0.70772856]]
  5. [[0.7951453 0.31010404 0.15164271]
  6. [0.03229304 0.3272632 0.12064549]]
  7. [[0.02255895 0.512737 0.10098135]
  8. [0.0386815 0.1329508 0.68645036]]
  9. [[0.55390334 0.5705598 0.38108754]
  10. [0.82101023 0.92697096 0.77738845]]], shape=(4, 2, 3), dtype=float32)
  11. print(inputs_reverse)
  12. tf.Tensor(
  13. [[[0.17281447 0.92418146 0.70772856]
  14. [0.84839684 0.21003267 0.49825752]]
  15. [[0.03229304 0.3272632 0.12064549]
  16. [0.7951453 0.31010404 0.15164271]]
  17. [[0.0386815 0.1329508 0.68645036]
  18. [0.02255895 0.512737 0.10098135]]
  19. [[0.82101023 0.92697096 0.77738845]
  20. [0.55390334 0.5705598 0.38108754]]], shape=(4, 2, 3), dtype=float32)

其实很好理解,就是将inputs在max_time维度进行反转,所谓的反向就是时间序列这个维度的反向。

然后反向dynamic_rnn的输出再沿max_time维度反转回去,和正向的输出保持一致。将输入再改回全1矩阵,打印出最终输出如下

  1. (<tf.Tensor: id=151, shape=(4, 2, 2), dtype=float32, numpy=
  2. array([[[0.7037754, 0.7037754],
  3. [0.9472957, 0.9472957]],
  4. [[0.7037754, 0.7037754],
  5. [0.9472957, 0.9472957]],
  6. [[0.7037754, 0.7037754],
  7. [0.9472957, 0.9472957]],
  8. [[0.7037754, 0.7037754],
  9. [0.9472957, 0.9472957]]], dtype=float32)>, <tf.Tensor: id=276, shape=(4, 2, 2), dtype=float32, numpy=
  10. array([[[0.9472957, 0.9472957],
  11. [0.7037754, 0.7037754]],
  12. [[0.9472957, 0.9472957],
  13. [0.7037754, 0.7037754]],
  14. [[0.9472957, 0.9472957],
  15. [0.7037754, 0.7037754]],
  16. [[0.9472957, 0.9472957],
  17. [0.7037754, 0.7037754]]], dtype=float32)>)

从上面可以看出,因为输入是全1矩阵,且初始state都是全0初始化,因此前向和反向lstm的输出是一样的,为了将两个输出按照时间序列对应起来,将反向输出按max_time维度进行反转就得到了最终的结果。

参考

http://colah.github.io/posts/2015-08-Understanding-LSTMs/#fn1

BasicLSTMCell中num_units参数解释_notHeadache的博客-CSDN博客_lstm中的units

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/168102
推荐阅读
相关标签
  

闽ICP备14008679号