1.前记
LSTM系列的前面两篇文章《LSTM反向传播详解Part1》《LSTM反向传播详解Part2》将LSTM的理论梳理了一遍,本文主要着重用代码实现LSTM,其实也是对之前两篇文章的验证。文末贴出下载链接。
2.代码内容
代码一共分为三个文件,分别为样本生成文件,采用tensorflow训练LSTM参数的文件,自编写LSTM参数训练文件。
2.1样本生成
样本生成文件主要含有LSTM的前向传播,其核心代码如下:
def forward(x_pre, h_pre, c_pre, Wi, Wc, Wf, Wo, Bi, Bc, Bf, Bo): # 传入行矩阵
xh = np.column_stack((x_pre, h_pre)) # 行矩阵合并为行
# ft = xh.dot(Wf) + Bf
# ft = ft+forget_bias
# ft = sigmoid(ft)
ft = sigmoid(xh.dot(Wf) + Bf + forget_bias)#forget_bias主要是tensorflow中的LSTM模型中有这个参数
it = sigmoid(xh.dot(Wi) + Bi)
ot = sigmoid(xh.dot(Wo) + Bo)
ct_ = tanh(xh.dot(Wc) + Bc)
ct = np.multiply(ft, c_pre) + np.multiply(it, ct_)
ht = np.multiply(ot, tanh(ct))
return ct, ht
每个时刻都调用一次forward()函数,并依次递归。
h_pre = np.zeros((1, num_units))
c_pre = h_pre
x_pre = np.array([X[i, 0, :]])
c1, h1 = forward(x_pre, h_pre, c_pre, Wi, Wc, Wf, Wo, Bi, Bc, Bf, Bo)
x_pre = np.array([X[i, 1, :]])
c2, h2 = forward(x_pre, h1, c1, Wi, Wc, Wf, Wo, Bi, Bc, Bf, Bo)
在此回答《LSTM反向传播详解Part2》展望部分问题4,“我该如何“自编”样本数据,Part1文中使用one-hot分类标签(最后通过
s
o
f
t
m
a
x
softmax
softmax层分类)能得到正确结果吗?”,一开始编写代码的时候是将最后时刻的输出
h
τ
h_{\tau}
hτ通过
s
o
f
t
m
a
x
softmax
softmax后,然后根据其最大值所在的下标为1生成one-hot输出标签。但是这样子训练的结果很难接近我自定义的矩阵参数
W
f
,
W
i
,
W
o
,
W
c
,
B
f
,
B
i
,
B
o
,
B
c
Wf,Wi,Wo,Wc,Bf,Bi,Bo,Bc
Wf,Wi,Wo,Wc,Bf,Bi,Bo,Bc。
想一想原因或许是这样子的,比如你生成样本时,softmax序列输出为
a
=
[
0.55
,
0.5
,
0.45
]
a = [0.55,0.5,0.45]
a=[0.55,0.5,0.45],其标签标记为
[
1
,
0
,
0
]
[1,0,0]
[1,0,0],而通过偏导数公式
∂
L
/
∂
h
τ
=
V
T
⋅
(
a
−
y
)
\partial L/\partial h_{\tau} = V^T\cdot(a-y)
∂L/∂hτ=VT⋅(a−y)(在本实验中相当于
V
V
V为单位矩阵),当你的训练结果标签为正确值
[
1
,
0
,
0
]
[1,0,0]
[1,0,0]时,
a
−
y
a-y
a−y依旧有残差值。这样子就很难准确训练到与原定义模型的参数值。因此代码中采用
h
τ
h_{\tau}
hτ作为数据输出值,训练模型采用最小误差平方和做数据回归。话说这是我第一次觉得二范数这么有用,因为各种书籍中描述的方法都大量说最小二范数误差有各种问题(比如台大的机器学习在将LR回归时,最小二范数误差在偏离目标点的初始点时,训练会很慢,在比如adaboost方法中损失函数采用最小二范数同样不可取,见《统计学习基础 Trevor Hastie著》10.6节-损失函数和健壮性)。个人认为平方误差损失函数效果好,是因为本实验是验证自定义的精准数学模型。而在实际项目中,是很少有标准的回归模型的,比如判断是不是猫打标签肯定是0或1而很难说0.65是猫。
2.2通过tensorflow训练
训练代码很简单,主要围绕以下两句核心代码展开:
tf.nn.rnn_cell.BasicLSTMCell(num_units=h_dimens, forget_bias=forget_bias, state_is_tuple=True)
tf.nn.dynamic_rnn(cell=cell, dtype=tf.float64, inputs=inputx)
定义的损失函数为最小二范数误差:
tf.reduce_mean(tf.square(y_label - last_h))
参数训练方法采用tf.train.AdamOptimizer(lr),采用梯度下降方法训练时,没有得到正确的结果。通过训练,查看LSTM中的模型参数与样本生成时定义的模型参数结果是一致的。
2.3自编写LSTM训练代码
自编写的训练代码,其思路完全按照系列文章的前面两篇完成。能够从代码中查找到对应的一个个公式。本文不做过多解释,仅通过代码讲解一下《LSTM反向传播详解Part2》文中提到的几个问题。
2.3.1 问题1
1.自编写LSTM训练代码.Part1《LSTM反向传播详解Part1》中
∂
L
/
∂
h
t
\partial L/\partial h_{t}
∂L/∂ht维度显然为
[
(
h
_
d
i
m
e
n
s
+
x
_
d
i
m
e
n
s
)
×
1
]
[(h\_dimens+x\_dimens)\times1]
[(h_dimens+x_dimens)×1],本文中应该为
[
h
_
d
i
m
e
n
s
×
1
]
[h\_dimens\times1]
[h_dimens×1],否则
∂
L
/
∂
o
t
=
∂
L
/
∂
h
t
⊙
tanh
(
c
t
)
\partial L/\partial o_{t} = \partial L/\partial h_{t}\ \odot \tanh(c_{t})
∂L/∂ot=∂L/∂ht ⊙tanh(ct)表达式将会出错。这是怎么一回事?
回答: 很简单,直接截断即可。在代码中其实现为:
dh = np.dot(Wo.T, (tanh(ct) * (1 - ot) * ot * dh))
dh = dh + np.dot(Wf.T, ct_1 * (1 - ft) * ft * dc)
dh = dh + np.dot(Wi.T, ct_ * (1 - it) * it * dc)
dh = dh + np.dot(Wc.T, it * (1 - ct_ * ct_) * dc)
dh = dh[0:h_dimens] # 公式(10_12)
上述代码就是参考文献《LSTM反向传播详解Part1》中的公式(10_12),最后一行代码就是截断原来对
[
h
t
;
x
t
]
[h_t;x_t]
[ht;xt]的偏导数为对
h
t
h_t
ht的偏导数。在代码中也能找到Part1文中公式(13_14)dc = dc * ft + dh * ot_1 * (1 - tanh(ct_1) * tanh(ct_1)) # 公式(13_14)
2.3.2 问题2
2.本文公式中出现大量
i
t
,
c
t
−
1
,
f
t
,
o
t
,
c
t
^
i_t,c_{t-1},f_t,o_t,\hat{c_t}
it,ct−1,ft,ot,ct^等变量,在程序中要怎么实现?
回答: 在实现反向传播前,其实是需要利用当前的参数做前向传播,并且记录所有时刻的各个变量值。具体代码如下:
def batch_forward(X): # X[batch,steps,x_dimens]
Xt = np.transpose(X, (0, 2, 1)) # 转换为[batch,x_dimens,steps]
for i in range(batchs):
h_pre = np.zeros((h_dimens, 1))
c_pre = h_pre
for j in range(time_steps):
x_pre = Xt[i, :, j]
x_pre = x_pre.reshape((len(x_pre), 1))
xh = np.row_stack((h_pre, x_pre))
ft = sigmoid(Wf.dot(xh) + Bf + forget_bias)
it = sigmoid(Wi.dot(xh) + Bi)
ot = sigmoid(Wo.dot(xh) + Bo)
ct_ = tanh(Wc.dot(xh) + Bc)
ct = np.multiply(ft, c_pre) + np.multiply(it, ct_)
ht = np.multiply(ot, tanh(ct))
F[i, :, j] = ft.reshape(-1)
I[i, :, j] = it.reshape(-1)
O[i, :, j] = ot.reshape(-1)
C_[i, :, j] = ct_.reshape(-1)
C[i, :, j] = ct.reshape(-1)
H[i, :, j] = ht.reshape(-1)
h_pre = ht
c_pre = ct
HX = np.column_stack((H, Xt))
return F, I, O, C_, C, H, HX
注意样本生成代码中采用的前向传播和这里的代码稍微有点点不一样,样本生成时
[
x
t
−
1
,
h
t
−
1
]
[x_{t-1},h_{t-1}]
[xt−1,ht−1]为行向量,这里
[
h
t
−
1
;
x
t
−
1
]
[h_{t-1};x_{t-1}]
[ht−1;xt−1]为列向量,且
h
t
−
1
h_{t-1}
ht−1在前。之所以有两种不同的排列,是因为前一种方法是tensorflow中的排列顺序,而我在公式推导中还是习惯列向量,代码为了与前面两文中公式对应,就采用了列向量的方式。
代码中的如下矩阵,存储了当前批量样本根据目前的矩阵参数
W
f
,
W
i
,
W
o
,
W
c
,
B
f
,
B
i
,
B
o
,
B
c
Wf,Wi,Wo,Wc,Bf,Bi,Bo,Bc
Wf,Wi,Wo,Wc,Bf,Bi,Bo,Bc生成的各个时刻的
i
t
,
c
t
,
f
t
,
o
t
,
c
t
^
,
h
t
i_t,c_{t},f_t,o_t,\hat{c_t},h_t
it,ct,ft,ot,ct^,ht值。
F[i, :, j] = ft.reshape(-1)
I[i, :, j] = it.reshape(-1)
O[i, :, j] = ot.reshape(-1)
C_[i, :, j] = ct_.reshape(-1)
C[i, :, j] = ct.reshape(-1)
H[i, :, j] = ht.reshape(-1)
2.3.3 问题3
3.本文的公式都是针对单个样本推导的,当每个batch中有大量样本时,怎么办?不同时刻的
h
t
,
c
t
h_t,c_t
ht,ct的偏导数都有对模型参数的链接,要如何实现?
回答: 每次都是计算批量样本范围内的样本误差平方和的平均,因此多个样本时,将每个样本得到的偏导数求平均即可。至于不同时刻都有对
W
f
,
W
i
,
W
o
,
W
c
,
B
f
,
B
i
,
B
o
,
B
c
Wf,Wi,Wo,Wc,Bf,Bi,Bo,Bc
Wf,Wi,Wo,Wc,Bf,Bi,Bo,Bc矩阵变量的偏导数,因此需要计算每个时刻时的
h
t
,
c
t
h_t,c_t
ht,ct对这些矩阵变量的偏导数求和。代码如下所示:
dbf = np.sum(dBf, 0) # batch合一
dbf = np.sum(dbf, 1) # time_steps合一
dbf = dbf / batchs
dbf = dbf.reshape((len(dbf), 1))
2.3.4 问题4
4.如果理论一切正常,你相信你能得到正确的结果吗?比如参数更新采用梯度下降还是Adam?我该如何“自编”样本数据,Part1文中使用ont-hot分类标签(最后通过
s
o
f
t
m
a
x
softmax
softmax层分类)能得到正确结果吗?
回答: 在tensorflow中训练都没法正确的通过梯度下降方法得到模型参数,自编写的代码就更没信心了。本代码是通过采用Adam方法训练参数。参考花书《深度学习》8.5.3节Adam算法中伪代码实现。
def Adamgrad(dB, t, m, v, lr=learn_rate, beta1=0.9, beta2=0.999, epsilon=1e-08):
m = beta1 * m + (1 - beta1) * dB
v = beta2 * v + (1 - beta2) * (dB ** 2)
mb = m / (1 - beta1 ** t) # t is step number
vb = v / (1 - beta2 ** t)
detB = lr * mb / (np.sqrt(vb) + epsilon)
return m, v, detB
最后一问在本文的2.1节已经阐述过,这里不再重复。
3.总结
最终通过代码得到了生成样本时定义的模型参数,不过速度比tensorflow的训练方法慢的多,结果的精度当然也没有那么高,我发现大部分模型参数误差大约在0.4%左右,误差最大的一个参数在Bi的最后一项,误差为1.82%
注意前面所述,样本生成与反向传播推导时
[
h
t
,
x
t
]
[h_{t},x_t]
[ht,xt]前后顺序不一致,所以训练结果
W
f
,
W
i
,
W
o
,
W
c
Wf,Wi,Wo,Wc
Wf,Wi,Wo,Wc中与
h
t
h_{t}
ht部分和
x
t
x_t
xt部分对应的参数要对调一下才能与样本生成时一致。
一开始的想法就是想尝试推导一下LSTM,后来想想如果不通过代码实现又怎么能够说明自己推导的是对的了。至少通过实验表明结果还是令人满意的。
4.附加说明
其实代码中没使用什么过多的语言技巧,只要照着前面两篇文章中的公式阅读代码还是比较简单的。毕竟不是工程代码所以注释什么的确实也比较简单,见谅。
代码链接:LSTM反向传播代码实现(通过tensorflow和自编写代码实现)
generateSamples.py为样本生成代码
train.py 为通过tensorflow训练代码
LstmBP.py 为根据系列文章编写的反向传播的实现代码