时空图神经网络2——RNN和GRU
系列文章目录
文章目录
前言
我们的目标是学习时空图神经网络,关于图神经网络的一些基本知识前面已经讲过了,满足了“空”的要求,现在要学习一下循环神经网络RNN及变体GRU的知识满足“时”的要求。
另外,我是外行,这些内容都是我临时自学并基于自己的理解写的,可能会有很多不足。
一、RNN
循环神经网络RNN相关的讲解在网上可以找到很多,但是我个人不理解的点可能跟大家讲的重点不一样,尤其是我不做NLP,我是做时序数据的,所以我重点按照自己的思路来写。从应用的角度出发,原理要跟应用相结合。
1. RNN基本原理

1.1 RNN的NN
这一部分很好理解,直接看上图中的右图红框部分。这就是一个简单的线性层, x t x_{t} xt是输入层, U U U是对 x t x_{t} xt线性变换的矩阵, s t s_{t} st是隐藏层, V V V是对 s t s_{t} st线性变换的矩阵,得到输出 o t o_{t} ot。
1.2 RNN的R
RNN用来处理时序数据,所以我们要考虑过去时间的信息。线性层只接收当前值显然不能实现目的。
把包含前一个时间点信息的
s
t
−
1
s_{t-1}
st−1用到当前时间计算隐藏层
s
t
s_{t}
st的过程中,就能得到上一个时间点的信息,也就是说
s
t
s_{t}
st融合了
x
t
−
1
x_{t-1}
xt−1和
x
t
x_{t}
xt的信息。我们由此可以推测,
s
t
+
1
s_{t+1}
st+1融合了
x
t
+
1
x_{t+1}
xt+1和
s
t
s_{t}
st的信息,也就是把
x
t
−
1
x_{t-1}
xt−1、
x
t
x_{t}
xt和
x
t
+
1
x_{t+1}
xt+1的信息全都融合了。
既然
s
t
s_{t}
st还接收一个经过
W
W
W线性变换的
s
t
−
1
s_{t-1}
st−1,实际上可以通过公式
s
t
=
f
(
W
s
t
−
1
+
U
x
t
)
s_{t}=f(Ws_{t-1}+Ux_{t})
st=f(Wst−1+Uxt)表示。当
t
=
0
t=0
t=0时,我们可以输入一个全为0的初始化隐藏层。
注意:
W
W
W和
U
U
U都是共享矩阵,在处理任意一个时间点的数据时,都是一样的。
2. torch.nn.RNN
为了能用起来,只看原理是不够的,我们来看官网截图(原文链接):
注意: 不考虑双向RNN的情况

2.1 初始化

- input_size: 输入数据x的特征数,比如对于股票的预测来说,可能有开盘价、收盘价、成交量等特征
- hidden_size: 隐藏层的特征数,没有实际意义,但是包含了输入x所有特征的信息
- num_layers: 循环层数,相当于是从线性层变成了
MLP默认是1层。当超过1层时,就变成了DRNN - nonlinearity: 设置非线性层,默认
tanh,可选relu - bias: 线性层是否增加偏置
- batch_first: 默认输入的结构是
[seq_length, batch_size, num_features],如果输入True,输入结构变成[batch_size, seq_length, num_features] - dropout: 默认是0,如果不是0,就在RNN层的每个输出引入Droupout层(除了输出层)
- bidirectional: 默认是False,True代表使用双向
RNN
上面这些内容都是在初始化模型的时候需要指定的。

2.2 模型输入
- input: 输入,在不分批次的情况下,张量的形状是
[seq_length, num_features];在分批次的情况下,若batch_first=False,张量形状为[seq_length, batch_size, num_features],若batch_first=True,张量形状为[batch_size, seq_length, num_features]。 - hx: 初始隐藏层,在不分批次的情况下张量尺寸是
[num_layers, hidden_size],在批次的情况下,张量形状为[num_layers, batch_size, hidden_size]。我们知道,在计算 t − 1 {t-1} t−1时刻的隐藏层时需要上一时刻的隐藏层,那么第一个时刻没有前面的隐藏层可以用,就要给一个初始化的隐藏层,默认是0。
模型的输入

2.3 模型输出
- output: 在不分批次的情况下,张量形状为
[seq_length, hidden_size];分批次的情况下,张量形状分别为[batch_size, seq_length, hidden_size]和[seq_length, batch_size, hidden_size]。注意: 这里的输出实际上还是一个隐藏层,是所有时间步(seq_length)的最后一个隐藏层(hidden_size)。 如果我们想预测未来n个时间点的数据,需要加一个线性层变换到需要的形状。 - h_n: 看符号就知道明显是一个隐藏层,表示的是最后一个时间步的所有隐藏层,所以形状是
[num_layers, hidden_layers]。默认情况num_layers是1,但是在DRNN中会有多层。在分批次的情况下,可以通过h_n将前一个批次的信息传递到后面批次。
使用pytorch实现RNN的代码展示如下:
import torch.nn as nn
class SimpleRNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(SimpleRNN, self).__init__()
self.rnn = nn.RNN(input_size, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x, h_0):
out, h_n = self.rnn(x, h_0) # 运用RNN层
out = self.fc(out) # 运用全连接层
return out
RNN小结
通过上述内容,可以在了解原理的同时知道如何去用这个算法,知道算法的输入输出是什么含义。其他的很多讲解提到的many to many结构和many to one结构等等,可以通过输出加一个线性层得到相应的尺度,也可以用最后一次的输出当作下一次的输入预测再下一次的结果,这样的问题在于误差会累积。
二、GRU
前言
虽然RNN能够处理不同长度的序列数据,能够捕捉序列中的时间依赖关系。但是对长序列的记忆能力较弱,可能出现梯度消失或梯度爆炸问题,另外,训练可能相对复杂和时间消耗大。
原因其实很好理解,反向传播的时候需要计算梯度,时间序列越长,对隐藏层进行线性变换的矩阵
W
W
W会一直累乘。我们知道,大于1的数累乘会变得很大,而小于1的数累乘会趋近0,这就是梯度消失和爆炸的原因。
长短期记忆神经网络LSTM通过适当“忘记”一些信息,保留重要信息,避免了梯度在反向传播时因简单的连乘操作而出现爆炸或消失的情况,让模型训练更加稳定。门控循环单元GRU在LSTM的基础上结构更简单,效率更高。在这里简单放一段pytorch实现LSTM的代码,可以发现跟RNN使用起来基本上完全一样,除了多了个
c
c
c,但是我们不展开讲(因为我没仔细看):
import torch.nn as nn
class LSTM(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(LSTM, self).__init__()
self.lstm = nn.LSTM(input_size, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x, (h_0, c_0)):
out, (h_n, c_n) = self.lstm(x, (h_0, c_0)) # 运用LSTM层
out = self.fc(out) # 运用全连接层
return out
1.GRU基本原理
GRU比LSTM结构简单,计算效率更高,更适合因此工业场景在线运行的要求,所以我重点展开讲这一部分。

总体上观察一下这个图,看起来好像很复杂(确实很复杂),但是跟RNN其实有些相似,毕竟是从RNN的基础上发展过来的。区别在于计算隐藏层的部分更复杂了,不是前一时刻的隐藏层
h
t
−
1
h_{t-1}
ht−1(上文是
s
t
−
1
s_{t-1}
st−1)和当前时刻的输入
x
t
x_{t}
xt只经过线性变换就能得到了。
但是我们依然用的到他们,只是逻辑更复杂。
假设我们有深度学习的基础知识,比如你已经完全明白MLP,下面的式子肯定是可以直接用pytorch写出来的。其中
⊙
\odot
⊙是Hadamard积,表示矩阵对应位置的元素相乘。
r
t
=
σ
(
W
i
r
x
t
+
b
i
r
+
W
h
r
h
t
−
1
+
b
h
r
)
(
1
)
r_t = \sigma(W_{ir}x_t+b_{ir}+W_{hr}h_{t-1}+b_{hr}) (1)
rt=σ(Wirxt+bir+Whrht−1+bhr)(1)
u
t
=
σ
(
W
i
u
x
t
+
b
i
u
+
W
h
u
h
t
−
1
+
b
h
u
)
(
2
)
u_t = \sigma(W_{iu}x_t+b_{iu}+W_{hu}h_{t-1}+b_{hu})(2)
ut=σ(Wiuxt+biu+Whuht−1+bhu)(2)
c
t
=
t
a
n
h
(
W
i
c
x
t
+
b
i
c
+
r
t
⊙
(
W
h
c
h
t
−
1
+
b
h
c
)
)
(
3
)
c_t = tanh(W_{ic}x_t+b_{ic}+r_t\odot(W_{hc}h_{t-1}+b_{hc}))(3)
ct=tanh(Wicxt+bic+rt⊙(Whcht−1+bhc))(3)
h
t
=
(
1
−
u
t
)
⊙
c
t
+
u
t
⊙
h
t
−
1
(
4
)
h_t = (1-u_t)\odot c_t+u_t\odot h_{t-1}(4)
ht=(1−ut)⊙ct+ut⊙ht−1(4)
不过终究还是要展开解释一下的。
r
t
r_t
rt是重置门reset_gate,
u
t
u_t
ut是更新门update_gate,
c
t
c_t
ct是候选隐藏层,
h
t
h_t
ht是隐藏层。我们按下面的思路一步一步看:
- 隐藏层
h
t
h_t
ht与
RNN中的隐藏层一样,都是要传递给下一个时间 t + 1 t+1 t+1 的,都是本次的输出。 - 候选隐藏层
c
t
c_t
ct为什么叫“候选” “隐藏层”?先看式(3),
c
t
c_t
ct已经包含了当前输入和上一个时间隐藏层的信息(之前
RNN没提偏置 b b b,有没有都不影响原理,我们在GRU也不提),与RNN计算的隐藏层不同之处在于对 h t − 1 ⊙ h_{t-1}\odot ht−1⊙了一个 r t r_{t} rt,从这个角度看 c t c_t ct已经具备了隐藏层的特性了。然而他不是传递给下一层的真正隐藏层,还要经过式(4),通过更新门调节 c t c_t ct与 h t − 1 h_{t-1} ht−1在 h t h_t ht中的占比,且 u t = 0 u_t=0 ut=0时 c t c_t ct就是隐藏层,所以他是候选隐藏层。 - 关于候选隐藏层还要补充一点:对历史信息 h t − 1 h_{t-1} ht−1乘一个(0, 1)的数,是对历史信息的选择性记忆,这能够剔除长期依赖中冗余的中间状态;还能保证当前信息完整地保存下来(相当于乘 1)。
- 为什么要经过式(4)才能得到最终的隐藏层?这一步的作用是实现信息的选择性保留和更新。这怎么理解?而且更新的形式是累加,避免了RNN中使用累乘导致梯度消失或爆炸。另外也重新考虑是否一定要保留当前信息。在计算候选隐藏层的时候,我们对历史信息
⊙
\odot
⊙了
r
t
r_{t}
rt,而把当前信息完整地保留了。如果说当前的信息不太重要,我们可能要舍弃,但是在式(3)和
RNN中做不到。在式(4)中,可以通过 u t = 1 u_{t}=1 ut=1把当前信息完全抛弃,保留没有 ⊙ r t \odot r_{t} ⊙rt的原始的历史信息。反过来说,如果我们的数据翻篇了,历史数据不需要了,完全可以通过 r t = 0 r_t=0 rt=0 和 u t = 0 u_t=0 ut=0 保留全部的当前信息。所以说式(3)和式(4)二者缺一不可,二者共同实现了对过去信息和当前信息的精准整合。
注意:公式中的权重矩阵也都是共享矩阵。
2. torch.nn.GRU
在我们知道RNN怎么用了以后,非常easy,初始化、输入和输出完全一样!可以查看链接。
总结
起码完全解答了我一开始不懂的地方,我觉得很好!
更多推荐
所有评论(0)