LSTM前向计算
在keras里训练完成一个模型后保存到本地,然后读取模型参数做前向计算,为C++移植做准备
关于LSTM的原理网上很多,被引用最多的应该就是这篇博客了Understanding LSTM Networks,它的译文也是遍布网上各个角落
使用LSTM分类mnist数据集的数字,数字图片大小
28∗28
28
∗
28
,每一步输入一行数据,总共28步输入图片的所有行,即lstm的timesteps为28,LSTM在最后一步输出结果,
模型训练完成后,model.summery()如下:
model.summary()
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
lstm_1 (LSTM) (None, 128) 80384
_________________________________________________________________
dense_1 (Dense) (None, 10) 1290
=================================================================
Total params: 81,674
Trainable params: 81,674
Non-trainable params: 0
_________________________________________________________________
model.input_shape
Out[4]: (None, 28, 28)
贴几张经典图片介绍一下,图片出自博客Understanding LSTM Networks
LSTM每一步有图中红色标记1、2、3、4的四个控制门,这四个门都分别是一个全连接网络,例如这个例子中的LSTM单元数为128,则这四个门就都是有128个单元的MLP。每一个绿色框表示一个cell,这三个cell表示同一个cell在不同的时刻,那一个LSTM的参数个数有多少呢,以这个mnist例子来算一下,
LSTM units = 128,则
ht−1
h
t
−
1
维度也为128
每一步输入
xt
x
t
维度为28
这四个门的输入都是
xt
x
t
跟
ht−1
h
t
−
1
的拼接,则四个门的输入维度为
(128+28)=156
(
128
+
28
)
=
156
前面说到四个门都是有units个单元的MLP,那每个门的参数就是
(28+128)∗128
(
28
+
128
)
∗
128
,四个门就乘以4再加上各自的bias,就是
(28+128)∗128∗4+128∗4=80384
(
28
+
128
)
∗
128
∗
4
+
128
∗
4
=
80384
。
翻上去一看,答案跟model.summary()的结果一样!
检查完了参数个数,那就开始前向计算了,其实就是四个门的更新公式,程序按照公式写就可以了
先看看keras保存的weights长什么样,
weights = model.get_weights()
weights[0].shape
Out[6]: (28, 512)
weights[1].shape
Out[7]: (128, 512)
weights[2].shape
Out[8]: (512,)
可以看到weight[0]、weights[1]就是前面分析的(28+128)*128*4这项,weights[2]就是bias的128*4这项了,分析了这个,程序就简单了,weights中是把四个门的系数放到一个矩阵里了,那计算的时候我们取这个矩阵中的相应部分就行了,例如bias取法如下
bias_i = weights[2][:128]
bias_f = weights[2][128:256]
bias_C = weights[2][256:384]
bias_o = weights[2][384:]
先将 xt和ht−1 x t 和 h t − 1 与四个门的系数矩阵相乘再拼接
input_xt = np.dot(x0[:,i,:],weights[0])
input_ht_1 = np.dot(ht_1,weights[1])
input = input_xt+input_ht_1
LSTM层计算步骤如下
forget gate
ft = sigmoid(input[:,128:256]+bias_f)
input gate:
it = sigmoid(input[:, :128] + bias_i)
计算cell状态:
Ct = tanh(input[:,256:384]+bias_C)
更新cell状态
Ct = ft*C_t_1+it*Ct
output gate
ot = sigmoid(input[:,384:]+bias_o)
ht = ot*tanh(Ct)
最后,更新一下 ht−1 h t − 1 和 Ct−1 C t − 1 以便进入下一次递归
ht_1 = ht
C_t_1 = Ct
其实,前向计算就是按照公式写,没什么难度,需要注意的问题有以下两点(也是我踩过的坑)
- keras模型里参数矩阵与这四个门对应的先后顺序
- keras里activation与recurrent activation的关系,recurrent activation用在了 i、f、o i 、 f 、 o 的计算中(keras里默认是hard_sigmoid),activation用在了 Ct C t 和 ht h t 的计算中(keras里默认是tanh)
关于这两个问题,可以参考博客Keras之LSTM源码阅读笔记,别人分析的更深入
参考资料:
附上测试本地模型的代码
import keras
import numpy as np
from keras.models import load_model
model = load_model('lstm-model.h5')
weights = model.get_weights()
weights = np.array(weights)
x0 = np.load('x0.npy')
y0 = model.predict(x0,1)
def sigmoid(x):
y = 1/(1+np.exp(-1*x))
return y
def tanh(x):
y = 2*sigmoid(2*x)-1
# y = (np.exp(x)-np.exp(-1*x))/(np.exp(x)+np.exp(-1*x))
return y
def softmax(x):
y = np.exp(x)/np.sum(np.exp(x))
return y
ht_1 = np.zeros([1,128]).astype('float32')
# ht_1.dtype = 'float32'
C_t_1 = np.zeros([1,128]).astype('float32')
bias_i = weights[2][:128]
bias_f = weights[2][128:256]
bias_C = weights[2][256:384]
bias_o = weights[2][384:]
for i in range(x0.shape[1]):
input_xt = np.dot(x0[:,i,:],weights[0])
input_ht_1 = np.dot(ht_1,weights[1])
input = input_xt+input_ht_1
it = sigmoid(input[:, :128] + bias_i)
ft = sigmoid(input[:,128:256]+bias_f)
Ct = tanh(input[:,256:384]+bias_C)
Ct = ft*C_t_1+it*Ct
ot = sigmoid(input[:,384:]+bias_o)
ht = ot*tanh(Ct)
ht_1 = ht
C_t_1 = Ct
output = softmax(np.dot(ht,weights[3])+weights[4])
print('haha')