RNN是最简单的循环神经网络,称谓Simple-RNN,它是LSTM的基础。下面看结构:
输入层到隐含层的权重用U表示,隐含层到隐含层的权重用W表示,隐含层到输出层的权重用V表示。
1. 前向传播
可以将网络看成是一个三层结构,与普通BP神经网络不同的是,输入层有两个部分,一个是上一时刻的隐层向量(ht-1);一个是本次的输入向量(xt)。隐含层(ht)也会有两个走向,一个作为本次输出到达输出层;同时ht也作为下一时刻的输入。
公式如下,在时间步t时:
ht = f(U.xt + W.ht-1)
at = g(V.ht)
其中f是隐含层激活函数,一般是tanh。g是输出层激活函数,一般是softmax。所以:
h
t
=
t
a
n
h
(
U
x
t
+
W
h
t
−
1
)
a
t
=
s
o
f
t
m
a
x
(
V
h
t
)
h_t=tanh(Ux_t+Wh_{t-1}) \\a_t=softmax(Vh_t)
ht=tanh(Uxt+Wht−1)at=softmax(Vht)
损失函数,这里我们选择交叉熵:
L
o
s
s
=
−
∑
i
=
1
n
y
t
(
i
)
ln
a
t
(
i
)
Loss =- \sum_{i=1}^{n}y_t(i)\ln a_t(i)
Loss=−i=1∑nyt(i)lnat(i)
其中
y
t
(
i
)
y{_t}(i)
yt(i)表示真实值(标签)的第
i
i
i个值,
a
t
(
i
)
a_t(i)
at(i)表示输出值的第
i
i
i个值,
n
n
n表示输出向量的长度。
2. 反向传播
反向传播是将误差通过输出层->隐含层->输入层逐层反传,并通过激活函数的导函数将误差分摊给各层的所有单元,从而获得各层单元的修正信号,并以信号作为依据修正各单元的权重。RNN的反向传播是从最后一个时间将累积的残差传递回来,所以梯度是从最后一个时刻开始计算,然后依次往前更新。
从结构图可以看出,Simple-RNN一共包含三个权重:
隐含层到输出层权重:V
输入层到隐含曾权重:U,W
这里将上面前向传播的式子拆开:
l
a
y
e
r
1
t
=
U
x
t
+
W
h
t
−
1
h
t
=
t
a
n
h
(
l
a
y
e
r
1
t
)
l
a
y
e
r
2
t
=
V
h
t
a
t
=
s
o
f
t
m
a
x
(
l
a
y
e
r
2
t
)
layer1_t=Ux_t+Wh_{t-1} h_t=tanh(layer1_t)\\layer2_t=Vh_t a_t=softmax(layer2_t)
layer1t=Uxt+Wht−1 ht=tanh(layer1t)layer2t=Vht at=softmax(layer2t)
对于时刻
t
t
t,先看最简单的权重
V
V
V,它的更新只与输出层的误差有关,说白了就是一个全连接层。关于softmax和交叉熵的求导过程可以参考这篇博客: 简单易懂的softmax交叉熵损失函数求导
在
t
t
t时刻,总的
L
o
s
s
Loss
Loss对
l
a
y
e
r
2
i
layer2_i
layer2i的偏导为(这里下标
i
i
i表示输出向量的第
i
i
i项):
α
L
o
s
s
α
l
a
y
e
r
2
i
=
∑
j
=
1
n
(
α
L
o
s
s
j
α
a
j
α
a
j
α
l
a
y
e
r
2
i
)
\frac{\alpha Loss}{\alpha layer2_i}=\sum_{j=1}^{n}(\frac{\alpha Loss_j}{\alpha a_j}\frac{\alpha a_j}{\alpha layer2_i})
αlayer2iαLoss=j=1∑n(αajαLossjαlayer2iαaj)
首先看
L
o
s
s
j
Loss_j
Lossj对
a
j
a_j
aj的偏导,
α
L
o
s
s
j
α
a
j
=
−
y
j
1
a
j
\frac{\alpha Loss_j}{\alpha a_j} = -y_j\frac{1}{a_j}
αajαLossj=−yjaj1
然后看
a
j
a_j
aj对
l
a
y
e
r
2
i
layer2_i
layer2i的偏导。注意这里是
j
j
j对
i
i
i
α
a
j
α
l
a
y
e
r
2
i
=
{
a
i
(
1
−
a
i
)
,
if
i
=
j
−
a
j
a
i
,
else
\frac{\alpha a_j}{\alpha layer2_i} = \begin{cases} a_i(1-a_i), & \text{if $i=j$} \\ -a_ja_i, & \text{else} \end{cases}
αlayer2iαaj={ai(1−ai),−ajai,if i=jelse
∇
l
a
y
e
r
2
i
=
α
L
o
s
s
α
l
a
y
e
r
2
i
=
∑
j
≠
i
n
−
y
j
1
a
j
(
−
a
j
a
i
)
+
(
−
y
i
1
a
i
)
(
a
i
(
1
−
a
i
)
)
=
∑
j
≠
i
n
y
j
a
i
+
y
i
a
i
−
y
i
=
a
i
∑
j
=
1
n
y
j
−
y
i
\nabla layer2_i=\frac{\alpha Loss}{\alpha layer2_i}=\sum_{j \neq i}^{n}-y_j\frac{1}{a_j}(-a_ja_i) + (-y_i\frac{1}{a_i})(a_i(1-ai)) \\=\sum_{j \neq i}^{n}y_ja_i+y_ia_i-y_i \\=a_i\sum_{j=1}^{n}y_j-yi
∇layer2i=αlayer2iαLoss=j̸=i∑n−yjaj1(−ajai)+(−yiai1)(ai(1−ai))=j̸=i∑nyjai+yiai−yi=aij=1∑nyj−yi
针对分类问题,真值y是一个独热码,只有一位为1其余均为0,所以上述梯度为:
∇
l
a
y
e
r
2
i
=
a
i
−
y
i
\nabla layer2_i=a_i-y_i
∇layer2i=ai−yi
总的这一层的梯度为:
∇
l
a
y
e
r
2
=
−
α
L
o
s
s
α
l
a
y
e
r
2
=
−
[
∇
l
a
y
e
r
2
1
,
.
.
.
,
∇
l
a
y
e
r
2
n
]
\nabla layer2=-\frac{\alpha Loss}{\alpha layer2}= -[\nabla layer2_1,...,\nabla layer2_n]
∇layer2=−αlayer2αLoss=−[∇layer21,...,∇layer2n]
这里加上一个负号,是为了让后面更新权重的时候用加法。如果这里不加负号,后面更新权重就用减法。最后
∇
l
a
y
e
r
2
\nabla layer2
∇layer2其实是一个向量
∇
l
a
y
e
r
2
=
−
[
a
1
,
a
2
,
.
.
.
,
(
a
k
−
1
)
,
.
.
.
,
a
n
]
\nabla layer2=-[a_1,a_2,...,(a_k-1),...,a_n]
∇layer2=−[a1,a2,...,(ak−1),...,an],这里
k
k
k就是标签中独热码为1的那一位,也就是除了这一位其它位的梯度都一样是softmax的值本身。
∇
V
=
−
α
L
o
s
s
α
l
a
y
e
r
2
α
l
a
y
e
r
2
α
V
=
h
t
T
∇
l
a
y
e
r
2
\nabla V= -\frac{\alpha Loss}{\alpha layer2}\frac{\alpha layer2}{\alpha V}=h_t^T\nabla layer2
∇V=−αlayer2αLossαVαlayer2=htT∇layer2
然后,再来看
W
和
U
W和U
W和U,由于它们的梯度依赖于之前的状态,所以需要从后向前依次求解:
首先是
t
a
n
h
(
x
)
tanh(x)
tanh(x)的梯度:
α
t
a
n
h
(
x
)
α
x
=
1
−
t
a
n
h
(
x
)
2
\frac{\alpha tanh(x)}{\alpha x}=1-tanh(x)^2
αxαtanh(x)=1−tanh(x)2
然后
∇
l
a
y
e
r
1
=
(
1
−
h
t
2
)
∗
(
V
T
∇
l
a
y
e
r
2
+
W
T
∇
l
a
y
e
r
1
n
e
x
t
)
\nabla layer1 = (1-h_t^2)*(V^T\nabla layer2+W^T\nabla layer1_{next})
∇layer1=(1−ht2)∗(VT∇layer2+WT∇layer1next)
∇
W
=
h
t
−
1
T
∇
l
a
y
e
r
1
∇
U
=
x
t
∇
l
a
y
e
r
1
\nabla W=h_{t-1}^T \nabla layer1 \\\nabla U=x_t \nabla layer1
∇W=ht−1T∇layer1∇U=xt∇layer1
这里
∇
l
a
y
e
r
1
n
e
x
t
\nabla layer1_{next}
∇layer1next是下一时刻的
∇
l
a
y
e
r
1
\nabla layer1
∇layer1,最后时刻
∇
l
a
y
e
r
1
n
e
x
t
\nabla layer1_{next}
∇layer1next初始化为0,然后每向前计算一次,
∇
l
a
y
e
r
1
n
e
x
t
\nabla layer1_{next}
∇layer1next就更新为这次的
∇
l
a
y
e
r
1
\nabla layer1
∇layer1。
对每一个时刻,累积计算增量:
V
u
p
d
a
t
e
+
=
∇
V
W
u
p
d
a
t
e
+
=
∇
W
U
u
p
d
a
t
e
+
=
∇
U
V_{update} += \nabla V\\ W_{update}+=\nabla W \\ U_{update}+=\nabla U
Vupdate+=∇VWupdate+=∇WUupdate+=∇U
最后,更新权重:
V
+
=
a
l
p
h
a
∗
V
u
p
d
a
t
e
W
+
=
a
l
p
h
a
∗
W
u
p
d
a
t
e
U
+
=
a
l
p
h
a
∗
U
u
p
d
a
t
e
V+=alpha*V_{update}\\ W+=alpha*W_{update}\\ U+=alpha*U_{update}
V+=alpha∗VupdateW+=alpha∗WupdateU+=alpha∗Uupdate
其中
a
l
p
h
a
alpha
alpha是学习率。好了,至此RNN的反向传播就结束了。下面放出详细代码:(代码中的权重和层的乘积是右乘,上面公式中写的左乘,不过相信这应该不算什么)
import numpy as np
class RNN:
def __init__(self, in_shape, unit, out_shape):
'''
:param in_shape: 输入x向量的长度
:param unit: 隐层大小
:param out_shape: 输出y向量的长度
'''
self.U = np.random.random(size=(in_shape, unit))
self.W = np.random.random(size=(unit, unit))
self.V = np.random.random(size=(unit, out_shape))
self.in_shape = in_shape
self.unit = unit
self.out_shape = out_shape
self.start_h = np.random.random(size=(self.unit,)) # 初始隐层状态
@staticmethod
def tanh(x):
return (np.exp(x)-np.exp(-x))/(np.exp(x)+np.exp(-x))
@staticmethod
def tanh_der(y):
return 1 - y*y
@staticmethod
def softmax(x):
tmp = np.exp(x)
return tmp/sum(tmp)
@staticmethod
def softmax_der(y, y_):
j = np.argmax(y_)
tmp = y[j]
y = -y[j]*y
y[j] = tmp*(1-tmp)
return y
@staticmethod
def cross_entropy(y, y_):
'''
交叉熵
:param y:预测值
:param y_: 真值
:return:
'''
return sum(-np.log(y)*y_)
@staticmethod
def cross_entropy_der(y, y_):
j = np.argmax(y_)
return -1/y[j]
def inference(self, x, h_1):
'''
前向传播
:param x: 输入向量
:param h_1: 上一隐层
:return:
'''
h = self.tanh(np.dot(x, self.U) + np.dot(h_1, self.W))
y = self.softmax(np.dot(h, self.V))
return h, y
def train(self, x_data, y_data, alpha=0.1, steps=100):
'''
训练RNN
:param x_data: 输入样本
:param y_data: 标签
:param alpha: 学习率
:param steps: 迭代伦次
:return:
'''
for step in range(steps): # 迭代伦次
print("step:", step+1)
for xs, ys in zip(x_data,y_data): # 每个样本
h_list = []
h = self.start_h # 初始化初始隐层状态
h_list.append(h)
y_list = []
losses = []
for x, y_ in zip(xs, ys): # 前向传播
h, y = self.inference(x, h)
loss = self.cross_entropy(y=y, y_=y_)
h_list.append(h)
y_list.append(y)
losses.append(loss)
print("loss:", np.mean(losses))
V_update = np.zeros(shape=self.V.shape)
U_update = np.zeros(shape=self.U.shape)
W_update = np.zeros(shape=self.W.shape)
next_layer1_delta = np.zeros(shape=(self.unit,))
for i in range(len(xs))[::-1]: # 反向传播
layer2_delta = -self.cross_entropy_der(y_list[i], ys[i])*self.softmax_der(y_list[i], ys[i]) # 输出层误差
# 当前隐层梯度 = 下一隐层梯度 * 下一隐层权重 + 输出层梯度 * 输出层权重
layer1_delta = self.tanh_der(h_list[i+1])*(np.dot(layer2_delta, self.V.T) + np.dot(next_layer1_delta, self.W.T))
V_update += np.dot(np.atleast_2d(h_list[i+1]).T, np.atleast_2d(layer2_delta)) # V增量
W_update += np.dot(np.atleast_2d(h_list[i]).T, np.atleast_2d(layer1_delta)) # W增量
U_update += np.dot(np.atleast_2d(xs[i]).T, np.atleast_2d(layer1_delta)) # U增量
next_layer1_delta = layer1_delta # 更新下一隐层的梯度等于当前隐层的梯度
self.W += W_update * alpha
self.V += V_update * alpha
self.U += U_update * alpha
# print(self.W,self.V,self.U)
def predict(self, xs, return_sequence=False):
'''
RNN预测
:param xs: 单个样本
:param return_sequence: 是否返回整个输出序列
:return:
'''
y_list = []
h_list = []
h = self.start_h
for x in xs:
h, y = self.inference(x,h)
y_list.append(y)
h_list.append(h)
if return_sequence:
return h_list, y_list
else:
return h_list[-1], y_list[-1]
然后做一个简单地测试:
预测26个字母中的下一个字母
这里偷懒只写了前面9个字母"abcdefghi",相对需要的样本比较少。
class RNNTest:
def __init__(self, hidden_num, all_chars):
'''
创建一个rnn
:param hidden_num: 隐层数目
:param all_chars: 所有字符集
'''
self.all_chars = all_chars
self.len = len(all_chars)
self.rnn = RNN(self.len, hidden_num, self.len)
def str2onehots(self, string):
'''
字符串转独热码
:param string:
:return:
'''
one_hots = []
for char in string:
one_hot = np.zeros((self.len,),dtype=np.int)
one_hot[self.all_chars.index(char)] = 1
one_hots.append(one_hot)
return one_hots
def vector2char(self, vector):
'''
预测向量转字符
:param vector:
:return:
'''
return self.all_chars[int(np.argmax(vector))]
def run(self, x_data, y_data, alpha=0.1, steps=100):
x_data_onehot = [self.str2onehots(xs) for xs in x_data]
y_data_onehot = [self.str2onehots(ys) for ys in y_data]
self.rnn.train(x_data_onehot, y_data_onehot, alpha=alpha, steps=steps) # 训练
vector_f = self.rnn.predict(self.str2onehots("f"), False)[1] # 预测f下一个字母
vector_ab = self.rnn.predict(self.str2onehots("ab"), False)[1] # 预测ab的下一个字母
print("f.next=",self.vector2char(vector_f))
print("ab.next=",self.vector2char(vector_ab))
# 测试:下一个字母
x_data = ["abc","bcd","cdef","fgh","a","bc","abcdef"]
y_data = ["bcd","cde","defg","ghi","b","cd","bcdefg"]
all_chars = "abcdefghi"
rnn_test = RNNTest(10, all_chars)
rnn_test.run(x_data,y_data)
测试运行把这两份代码,放在一个文件就好了。如果预测结果不对,就再跑一次,样本有限有时候收敛的不好。放个结果图: