RNN(Recurrent Neural Network)循环神经网络
一、概述
-
RNN是一种学习序列数据的算法
BP、CNN算法与RNN同属于神经网络,但BP与CNN的输出都是只考虑单个输入的影响,如单个物体和手写字的识别。当输入端给出一段连续的,隐含有上下文关系的输入(或者叫做与时间先后有关的输入),比如视频的下一时刻的预测,文档前后文内容的预测等,这些算法的表现就不尽如人意了。
RNN根据"人的认知是基于过往的经验和记忆"这一观点提出,它不仅考虑前一时刻的输入,而且赋予了网络对前面的内容的一种’记忆’功能,也就是当前的输出与之前的输出相关。具体的表现形式为网络会对前面的信息进行记忆并应用于当前输出的计算中,也就是说,隐藏层之间的节点是有连接的,并且隐藏层的输入不仅包括输入层的输出还包括上一时刻隐藏层的输出。
二、RNN模型结构
普通神经网络结构
RNN网络结构
可以看到在上图中存在多个时刻的网络结构,每个时刻的网络结构不仅仅有来自本层的输入,还有来自上一时刻隐藏层的输入。
除去层级内的权重,在每一层之间也有权重,我们将其记作
W
W
W 。一般层级之间共享此权重,这样可以有效减少训练参数。
我们还可以将上面的三维示意图简化为更加简单的二维表示:
三、RNN的前向传播
隐藏层输出 $ s_i $ 计算公式:
s
i
=
f
(
∑
n
N
(
U
i
⋅
x
n
i
+
b
i
)
)
s_i = f(\sum_{n}^N(U^i \cdot x_n^i + b_i))
si=f(n∑N(Ui⋅xni+bi))
在上式中,$ U $为输入层到隐藏层的权重矩阵;
$ x_n $为每一层的输入;
$ b_i 为每层的偏置;式中的函数 为每层的偏置; 式中的函数 为每层的偏置;式中的函数f$为激活函数,一般常用 tanh 或 relu
或者写成矩阵形式:
s
=
f
(
U
x
+
b
1
)
s = f(Ux + b_1)
s=f(Ux+b1)
由于RNN的每层输出需要考虑前一时刻的输出,那么,在t时刻的隐藏层输出表示为:
s
t
=
f
(
U
x
t
+
W
s
t
−
1
+
b
1
)
s_t = f(Ux_t + Ws_{t-1} + b_1)
st=f(Uxt+Wst−1+b1)
- 如此一来,不同时刻的隐藏层就将进行迭代
每层输出
o
i
o_i
oi计算:
o
i
=
g
(
V
s
i
+
b
2
)
o_i = g(Vs_i + b_2)
oi=g(Vsi+b2)
四、RNN的反向传播
在每一次的输出
o
i
o_i
oi中都会产生一个误差值
e
i
e_i
ei 那么总的误差可以使用下式表示:
E
=
(
∑
i
e
i
)
E = (\sum_ie_i)
E=(i∑ei)
- 损失函数可以使用交叉熵损失函数也可以使用平方误差损失函数
将输出端的误差值反向传递,运用梯度下降法进行更新。
参数梯度分别为:
∇
U
=
∂
E
∂
U
=
∑
i
∂
e
i
∂
U
\nabla U = \frac{\partial{E}}{\partial{U}} = \sum_i{\frac{\partial e_i}{\partial U}}
∇U=∂U∂E=i∑∂U∂ei
∇
V
=
∂
E
∂
V
=
∑
i
∂
e
i
∂
V
\nabla V = \frac{\partial{E}}{\partial{V}} = \sum_i{\frac{\partial e_i}{\partial V}}
∇V=∂V∂E=i∑∂V∂ei
∇
W
=
∂
E
∂
W
=
∑
i
∂
e
i
∂
W
\nabla W = \frac{\partial{E}}{\partial{W}} = \sum_i{\frac{\partial e_i}{\partial W}}
∇W=∂W∂E=i∑∂W∂ei
此处以W第i个时刻的偏导数为例(将此时的误差记作
E
i
E_i
Ei):
∂
E
i
∂
W
=
∂
E
i
∂
o
i
∂
o
i
∂
s
i
∂
s
i
∂
W
\frac{\partial{E_i}}{\partial{W}} = \frac{\partial{E_i}}{\partial{o_i}} \frac{\partial{o_i}}{\partial{s_i}} \frac{\partial{s_i}}{\partial{W}}
∂W∂Ei=∂oi∂Ei∂si∂oi∂W∂si
由于每一时刻的
s
i
s_i
si都与前一时刻的
s
i
−
1
s_{i-1}
si−1有关,故$ \frac{\partial{s_i}}{\partial{W}} $能够继续展开
∂
s
i
∂
W
=
∂
s
i
∂
W
+
∂
s
i
∂
s
i
−
1
∂
s
i
−
1
∂
W
+
∂
s
i
∂
s
i
−
1
∂
s
i
−
1
∂
s
i
−
2
∂
s
i
−
2
∂
W
+
.
.
.
\frac{\partial{s_i}}{\partial{W}} = \frac{\partial{s_i}}{\partial{W}} + \frac{\partial{s_i}}{\partial{s_{i-1}}} \frac{\partial{s_{i-1}}}{\partial{W}} + \frac{\partial{s_i}}{\partial{s_{i-1}}} \frac{\partial{s_{i-1}}}{\partial{s_{i-2}}} \frac{\partial{s_{i-2}}}{\partial{W}} +...
∂W∂si=∂W∂si+∂si−1∂si∂W∂si−1+∂si−1∂si∂si−2∂si−1∂W∂si−2+...
因此,我们可以将$ \frac{\partial{s_i}}{\partial{W}} $写为如下形式:
∂
E
i
∂
W
=
∑
i
=
0
n
∂
E
i
∂
o
i
∂
o
i
∂
s
i
∏
j
=
i
+
1
n
∂
s
i
∂
s
i
−
1
∂
s
i
∂
W
\frac{\partial{E_i}}{\partial{W}} = \sum_{i=0}^n \frac{\partial{E_i}}{\partial{o_i}} \frac{\partial{o_i}}{\partial{s_i}} \prod_{j=i+1}^n \frac{\partial{s_i}}{\partial{s_{i-1}}} \frac{\partial{s_i}}{\partial{W}}
∂W∂Ei=i=0∑n∂oi∂Ei∂si∂oij=i+1∏n∂si−1∂si∂W∂si
- 在 ∏ j = i + 1 n ∂ s j ∂ s j − 1 \prod_{j=i+1}^n \frac{\partial{s_j}}{\partial{s_{j-1}}} ∏j=i+1n∂sj−1∂sj处,若不采用任何激活函数将变成 n − j − 1 n-j-1 n−j−1 个 W W W 连乘,将导致梯度的爆炸或消失(具体是哪种情况取决于 W W W 的大小)。但添加激活函数不能够完全避免这个问题。
五、pytroch RNN代码实现
import torch
import numpy as np
import matplotlib.pyplot as plt
from torch import nn
# 定义RNN模型(可以类别下方RNN简单测试代码理解)
class Rnn(nn.Module):
def __init__(self, input_size):
super(Rnn, self).__init__()
# 定义RNN网络
## hidden_size是自己设置的,貌似取值都是32,64,128这样来取值
## num_layers是隐藏层数量,超过2层那就是深度循环神经网络了
self.rnn = nn.RNN(
input_size=input_size,
hidden_size=32,
num_layers=1,
batch_first=True # 输入形状为[批量大小, 数据序列长度, 特征维度]
)
# 定义全连接层
self.out = nn.Linear(32, 1)
# 定义前向传播函数
def forward(self, x, h_0):
r_out, h_n = self.rnn(x, h_0)
# print("数据输出结果;隐藏层数据结果", r_out, h_n)
# print("r_out.size(), h_n.size()", r_out.size(), h_n.size())
outs = []
# r_out.size=[1,10,32]即将一个长度为10的序列的每个元素都映射到隐藏层上
for time in range(r_out.size(1)):
# print("映射", r_out[:, time, :])
# 依次抽取序列中每个单词,将之通过全连接层并输出.r_out[:, 0, :].size()=[1,32] -> [1,1]
outs.append(self.out(r_out[:, time, :]))
# print("outs", outs)
# stack函数在dim=1上叠加:10*[1,1] -> [1,10,1] 同时h_n已经被更新
return torch.stack(outs, dim=1), h_n
TIME_STEP = 10
INPUT_SIZE = 1
LR = 0.02
model = Rnn(INPUT_SIZE)
print(model)
# 此处使用的是均方误差损失
loss_func = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=LR)
h_state = None # 初始化h_state为None
for step in range(300):
# 人工生成输入和输出,输入x.size=[1,10,1],输出y.size=[1,10,1]
start, end = step * np.pi, (step + 1)*np.pi
# np.linspace生成一个指定大小,指定数据区间的均匀分布序列,TIME_STEP是生成数量
steps = np.linspace(start, end, TIME_STEP, dtype=np.float32)
# print("steps", steps)
x_np = np.sin(steps)
y_np = np.cos(steps)
# print("x_np,y_np", x_np, y_np)
# 从numpy.ndarray创建一个张量 np.newaxis增加新的维度
x = torch.from_numpy(x_np[np.newaxis, :, np.newaxis])
y = torch.from_numpy(y_np[np.newaxis, :, np.newaxis])
# print("x,y", x,y)
# 将x通过网络,长度为10的序列通过网络得到最终隐藏层状态h_state和长度为10的输出prediction:[1,10,1]
prediction, h_state = model(x, h_state)
h_state = h_state.data
# 这一步只取了h_state.data.因为h_state包含.data和.grad 舍弃了梯度
# print("precision, h_state.data", prediction, h_state)
# print("prediction.size(), h_state.size()", prediction.size(), h_state.size())
# 反向传播
loss = loss_func(prediction, y)
optimizer.zero_grad()
loss.backward()
# 更新优化器参数
optimizer.step()
# 对最后一次的结果作图查看网络的预测效果
plt.plot(steps, y_np.flatten(), 'r-')
plt.plot(steps, prediction.data.numpy().flatten(), 'b-')
plt.show()
六、补充
1、RNN的缺陷
在传递过程中会产生信息缺失,求解过程中有梯度消失的问题
2、双向循环神经网络:
单项循环神经网络只能作为与前一个数据建立连接,双向循环神经网络则可以同后一个数据建立连接。
记隐藏层中反向计算的权重矩阵为 $ V_i^{‘}
(在图中为
(在图中为
(在图中为A_i^{’}$)
则输出的计算方法可写作:
o
i
=
g
(
V
s
i
+
V
i
′
s
i
′
+
b
2
)
o_i = g(Vs_i + V_i^{'}s_i^{'} + b_2)
oi=g(Vsi+Vi′si′+b2)
- 双向循环正向计算与反向计算方式相同但不共享权重(即正向矩阵记作 V i V_i Vi反向矩阵记作 V i ′ V_i^{'} Vi′
3、激活函数的选择
Sigmoid函数
S
i
g
m
o
i
d
(
x
)
=
1
1
+
e
−
x
Sigmoid(x) = \frac{1}{1+e^{-x}}
Sigmoid(x)=1+e−x1
S
i
g
m
o
i
d
Sigmoid
Sigmoid 图像如下:
- S i g m o i d Sigmoid Sigmoid 的取值在 [ 0 , 1.0 ] [0, 1.0] [0,1.0] 之间。当 s i s_i si 很大或者很小的时候,该函数的导数 $f^{'}(s_i)= f(s_i)(1-f(s_i)) $ 的值都会趋向于0,造成梯度消失的情况。
tanh函数
t
a
n
h
(
x
)
=
e
x
−
e
−
x
e
x
+
e
−
x
tanh(x) = \frac{e^x-e^{-x}}{e^x+e^{-x}}
tanh(x)=ex+e−xex−e−x
tanh及其导函数图像如下:
- t a n h tanh tanh 的取值在 [ − 1.0 , 1.0 ] [-1.0, 1.0] [−1.0,1.0] 之间; t a n h ′ tanh^{'} tanh′ 的取值在 [ 0 , 1.0 ] [0, 1.0] [0,1.0] 之间。
加上激活函数后的
∂
s
j
∂
s
j
−
1
\frac{\partial{s_j}}{\partial{s_{j-1}}}
∂sj−1∂sj :
∂
s
j
∂
s
j
−
1
=
t
a
n
h
′
(
W
s
j
−
1
+
U
x
j
)
W
\frac{\partial{s_j}}{\partial{s_{j-1}}} = tanh^{'}(Ws_{j-1} + Ux_j)W
∂sj−1∂sj=tanh′(Wsj−1+Uxj)W
在添加了tanh激活函数的情况下,仍然无法完全避免梯度爆炸或消失问题。比如当
W
W
W 过大,即使是
t
a
n
h
′
tanh^{'}
tanh′ 区间最大为1,也会产生梯度爆炸的情况;或者
W
W
W 也落在
[
−
1.0
,
1.0
]
[-1.0, 1.0]
[−1.0,1.0] 之间,如果时间步长过长的话,梯度将逐渐消失(趋于0),权重将不再变化。
Relu函数和LRelu函数
R
e
l
u
(
x
)
=
{
x
,
x
>
0
0
,
x
≤
0
Relu(x) = \begin{cases} x,\,\,x>0 \\ 0 ,\,\, x \le 0 \end{cases}
Relu(x)={x,x>00,x≤0
R
e
l
u
Relu
Relu 图像如下:
- R e l u Relu Relu 的取值在 [ 0 , ∞ ) [0, \infty) [0,∞) 之间。
相较于
S
i
g
m
o
i
d
Sigmoid
Sigmoid 函数和
T
a
n
h
Tanh
Tanh 函数,ReLu函数是利用阈值来进行因变量的输出,计算复杂度较低;其非饱和性可以有效地解决梯度消失的问题,提供相对宽的激活边界;单侧抑制(部分为0,使得部分神经元可能输出为0),提供了网络的稀疏表达能力(相当于在本次优化中,一些神经元不参与,进而破坏了全部神经元联动优化的陷阱?,使得模型更加的鲁棒),但这同时也导致了训练过程中神经元死亡的问题即若某神经元梯度一直为负值则该神经元参数将永不更新。在实际训练中,如果学习率(Learning Rate)设置较大,会导致超过一定比例的神经元不可逆死亡,进而参数梯度无法更新,整个训练过程失败。
由于上述问题,设计了
R
e
L
u
ReLu
ReLu 函数的变体即
L
e
a
k
y
R
e
L
u
(
L
R
e
L
u
)
Leaky \,ReLu(LReLu)
LeakyReLu(LReLu) :
L
R
e
l
u
(
x
)
=
{
x
,
x
>
0
a
x
,
x
≤
0
LRelu(x) = \begin{cases} x,\,\,x>0 \\ ax ,\,\, x \le 0 \end{cases}
LRelu(x)={x,x>0ax,x≤0
L
R
e
l
u
LRelu
LRelu 图像如下:
- 在 x < 0 x<0 x<0 时变为斜率为 a a a 的线性函数,一般 a a a 为一个很小的正常数, 这样既实现了单侧抑制,又保留了部分负梯度信息以致不完全丢失。
- 将负轴部分斜率a作为网络中一个可学习的参数,进行反向传播训练,与其他含参数网络层联合优化为 P R e L U ( P a r a m e t r i c R e L U ) PReLU(Parametric\,ReLU) PReLU(ParametricReLU) ;另一个LReLU的变种增加了“随机化”机制,具体地,在训练过程中,斜率a作为一个满足某种分布的随机采样;测试时再固定下来。 R a n d o m R e L U ( R R e L U ) Random\,ReLU(RReLU) RandomReLU(RReLU) 在一定程度上能起到正则化的作用。(这部分没有查阅更加详细的资料)