在这篇文章里,我们将讨论GRU/LSTM的代码实现。在这里,我们仍然沿用RNN学习笔记(五)-RNN 代码实现里的例子,使用GRU/LSTM网络建立一个2-gram的语言模型。
项目源码:https://github.com/rtygbwwwerr/RNN
参考项目:https://github.com/dennybritz/rnn-tutorial-gru-lstm
为了解决当词典中的words数量很大时,输入向量过长的问题,我们在输入层和隐层之间引入了Embedding Layer,通过该层,输入的one-hot将被转换为word的Embedding vector。
略。
这里我们重点讨论bptt部分(*:“ ⊙ ”表示elemwise乘法运算)。对于GRU网络来说,有
softmax(x)′=softmax(x)[1−softmax(x)]
输出层节点的输入值 z(o)k(t) 导数如下:
δ(o)k(t)=∂Lt∂z(o)k(t)
=ok(t)−1
写成向量形式为:
δ(o)(t)=o(t)−1
∂Lt∂V=δ(o)k(t)∂z(o)k(t)∂V=[ok(t)−1]⊙st
可以看到,这一层的导数与常规RNN是一致的。
从隐层开始,导数将有所不同,我们先来看下单个GRU网络节点结构:
这里,先对符号做一下约定:
iz(t)=xtUz+s(t−1)Wz :t时刻update gate 对应的输入
ir(t)=xtUr+s(t−1)Wr :t时刻rest gate 对应的输入
ih(t)=xtUh+(s(t−1)⊙r(t))Wh :t时刻隐单元对应的输入
io(t)=(1−z(t))⊙h(t)+z(t)⊙s(t−1) :t时刻output gate对应的输入
f(io(t))=io(t)=s(t)=(1−z(t))⊙h(t)+z(t)⊙s(t−1)
δo(t)=∂Lt∂io(t)=δ(o)(t)∂z(o)(t)∂s(t)∂s(t)∂io(t)=δ(o)(t)∂z(o)(t)∂s(t)∂f(io(t))∂io(t)
=δ(o)(t)⋅V⋅1
δz(t)
=δo(t)∂io(t)∂z(t)∂z(t)∂iz(t)
=δo(t)⊙[s(t−1)−h(t)]⊙σ(iz(t))′
这里我们设:
f(x)=0.2∗x+0.5
σ(x)=⎧⎩⎨f(x)010≤f(x)≤1f(x)<0f(x)>1
σ(x)′=⎧⎩⎨0.2000≤f(x)≤1f(x)<0f(x)>1
δh(t)
=δo(t)∂io(t)∂h(t)∂h(t)∂ih(t)
=δo(t)⊙(1−z(t))⊙[1−tanh(ih(t))2]
=δo(t)⊙(1−z(t))⊙[1−h(t)2]
δr(t)
=δh(t)∂ih(t)∂r(t)∂r(t)∂ir(t)
=[δh(t)⊙s(t−1)]⋅Wh
根据复合函数的求导法则,前一时刻的输出门误差 δo(t−1) 由四部分的偏导数组成(对应图中的4条蓝线)
δo(t−1)=∂Lt∂io(t−1)
=δz(t)∂iz(t)∂s(t−1)+δr(t)∂ir(t)∂s(t−1)+δh(t)∂ih(t)∂s(t−1)+δo(t)∂io(t)∂s(t−1)
=δz(t)⋅Wz+δr(t)⋅Wr+(δh(t)⊙r(t))⋅Wh+δo(t)⊙z(t)
得到了 δo(t−1) 依次带入前边的公式,即可求出 δz(t−1),δr(t−1),δh(t−1)
而对于t时刻的输入 x(t) 求导,可得:
δx(t)=∂Lt∂x(t)
=δz(t)∂iz(t)∂x(t)+δr(t)∂ir(t)∂x(t)+δh(t)∂ih(t)∂x(t)
=δz(t)⋅Uz+δr(t)⋅Ur+δh(t)⋅Uh
当存在多层GRU的时候,上一层的输出等于当前层的输入,即: s(l−1)(t)=x(l)(t)
所以就有:
δ(l−1)o(t)=δ(l)x(t)=δ(l)z(t)⋅U(l)z+δ(l)r(t)⋅U(l)r+δ(l)h(t)⋅U(l)h
以此为基础,可以按上边的方法推导出 δ(l−1)z(t),δ(l−1)r(t),δ(l−1)h(t)
的计算公式,这里就不一一推导了。
Output Layer的梯度:
ΔV(t)=∂Lt∂V=δ(o)(t)∂z(o)(t)∂V=[o(t)−1]⊙s(t)
Δc(t)=∂Lt∂c=δ(o)(t)∂z(o)(t)∂c=o(t)−1
Hidden Layer:
ΔWr(t)=δr(t)∂ir(t)∂Wr=δr(t)⊙s(t−1)
ΔWh(t)=δh(t)∂ih(t)∂Wh=δh(t)⊙s(t−1)⊙r(t)
ΔWz(t)=δz(t)∂iz(t)∂Wz=δz(t)⊙s(t−1)
ΔUr(t)=δr(t)∂ir(t)∂Ur=δr(t)⊙x(t)
ΔUh(t)=δh(t)∂ih(t)∂Uh=δh(t)⊙x(t)
ΔUz(t)=δz(t)∂iz(t)∂Uz=δz(t)⊙x(t)
对于Embedding层, E 为Nh×Ni的矩阵, E 的每一列可以看做词典中一个单词的Embedding向量表示
ix(t)=E⋅word(t)
ΔE(t)=δ(1)x(t)∂ix(t)∂E=δ(1)x(t)word(t)=δ(1)x(t)
word(t)为单词的one-hot向量,其中的分量只有一个为1,其余为0:
[w1,w2,...,wi,...,wNi]=[0,0,...,1,...,0] ,其中只有分量 wi=1 .所以 E⋅word(t) 的结果相当于取出 E 的第i列。在使用numpy的时候,为了实现的方便,使用了一个lookup Table,输入的为非零向量的索引 i ,通过取E矩阵的第i列直接得出word的Embedding向量 x :
x = self.E[:,word];
bias项的梯度(上边的公式中省略了,例如:iz(t)=xtUz+st−1Wz+bz)
Δbr(t)=δr(t)∂ir(t)∂br=δr(t)
Δbh(t)=δh(t)∂ih(t)∂bh=δh(t)
Δbz(t)=δz(t)∂iz(t)∂bz=δz(t)
对应源码:
def bptt(self, x, y):
word = x
T = len(y)
o, s, z, r, h = self.forward_propagation(word)
x = self.E[:,word];
dLdE = np.zeros(self.E.shape)
dLdU = np.zeros(self.U.shape)
dLdV = np.zeros(self.V.shape)
dLdW = np.zeros(self.W.shape)
dLdb = np.zeros(self.b.shape)
dLdc = np.zeros(self.c.shape)
#self.bptt_truncate
delta_o = o
delta_o[np.arange(len(y)), y] -= 1.
for t in np.arange(T)[::-1]:
dLdV += np.outer(delta_o[t], s[t][0].T)
dLdc += delta_o[t]
delta_x = 0
for l in np.arange(self.layer_num)[::-1]:
# Initial delta calculation
delta_ho = self.V.T.dot(delta_o[t])
for bptt_step in np.arange(max(0, t-self.bptt_truncate), t+1)[::-1]:
delta_func = map(delta_hard_sigmoid, z[bptt_step][l])
delta_hz = delta_ho * (s[bptt_step - 1][l] - h[bptt_step][l]) * delta_func
delta_hh = delta_ho * (1 - z[bptt_step][l]) * (1-(h[bptt_step][l]**2))
delta_hr = self.W[l][1].T.dot(delta_hh * s[bptt_step - 1][l])
delta_x = self.U[l][0].T.dot(delta_hz) + self.U[l][1].T.dot(delta_hr) + self.U[l][2].T.dot(delta_hh)
#dW^z
dLdW[l][0] += delta_hz * s[bptt_step-1][l]
#dW^r
dLdW[l][1] += delta_hr * s[bptt_step-1][l]
#dW^h
dLdW[l][2] += delta_hh * s[bptt_step-1][l] * r[bptt_step][l]
input = (x[:, bptt_step] if l < 1 else (s[bptt_step][l - 1]))
#dU^z
dLdU[l][0] += delta_hz * input
#dU^r
dLdU[l][1] += delta_hr * input
#dU^h
dLdU[l][2] += delta_hh * input
#db^z
dLdb[l][0] += delta_hz
#db^r
dLdb[l][1] += delta_hr
#db^h
dLdb[l][2] += delta_hh
#while is the first hidden layer,you need to count the dE
if l < 1:
dLdE[:, word[t]] += delta_x
delta_ho = self.W[l][0].T.dot(delta_hz) + self.W[l][1].T.dot(delta_hr) + self.W[l][2].T.dot(delta_hh * r[bptt_step][l]) + delta_ho * z[bptt_step][l]
return [dLdE, dLdU, dLdV, dLdW, dLdb, dLdc]