1️⃣ RNN介绍
前馈神经网络(CNN,全连接网络)的流程是前向传播、反向传播和参数更新,存在以下不足:
- 无法处理时序数据:时序数据长度一般不固定,而前馈神经网络要求输入的维度是固定的,不能改变
- 缺少记忆:前馈神经网络没有机制去记忆和处理
之前的输入数据
,因此无法处理像语言、股票走势或天气预报等 序列化、时间依赖性强的数据
针对前馈神经网络上述问题,RNN引入以下机制:
- 不同时间步的隐藏层之间是相连的
- 在时刻t,隐藏层的输入包括两部分,当前时刻的输入 x t x_t xt和上一个时间步隐藏层的输出 s t − 1 s_{t-1} st−1
通过这两条机制,模型能够记忆之前的输入数据,捕捉序列的上下文信息
看完这几句话你一定在想,这说的是个啥?太晕了,没关系,慢慢往下看
多说一句,RNN在很久之前就提出了,Jordan RNN于1986被提出,Elman RNN于1990年提出。
2️⃣ 原理介绍
接下来,讲讲具体原理,解决一下上面的迷惑。看下面这张图,分析一下
o
t
o_t
ot的表达式:
- x t x_t xt是t时刻的输入
- s t s_t st是t时刻的记忆, s t = f ( U ⋅ x t + W ⋅ s t − 1 ) s_t=f(U\cdot x_t+W\cdot s_{t-1}) st=f(U⋅xt+W⋅st−1),f表示激活函数, s t − 1 s_{t-1} st−1表示t-1时刻的记忆
- o t o_t ot是t时刻的输出, o t = s o f t m a x ( V ⋅ s t ) o_t=softmax(V\cdot s_t) ot=softmax(V⋅st)
看完上面这张图,对于W是什么疑惑很大,我一开始学习的时候也是这样,W到底是啥呢?来看下面这张图:
看完这张图,对于W的描述一目了然。W是在不同的时间步
隐藏层之间
递归的权重
。在RNN中,不同时间步使用相同的W,为了保证信息能够传递下去。
其实这里还有一个疑惑,按照我之前的认知,神经网络可训练的参数w和b都是在神经元上的,例如下面这张图。那么问题来了,RNN隐藏层神经元上参数是啥样的呢?
虽然下面的左图是这样画的,搞得好像参数U,W,V“漂浮在空中一般”,实际上,它们都在神经元上。准确的来说应该是右图的形式,U和W都在隐藏层神经元上,V在输出层神经元上。所以之前理解的神经元是一个神经元上只有一种参数。对于RNN来说,隐藏层神经元上有两种参数U和W。终于搞懂了,爽!
分析完RNN中参数的具体含义,来看看参数的尺寸:
U
=
隐藏层神经元个数
×
输入尺寸
W
=
隐藏层神经元个数
×
隐藏层神经元个数
V
=
输出尺寸
×
隐藏层神经元个数
U=隐藏层神经元个数×输入尺寸\\ W=隐藏层神经元个数×隐藏层神经元个数\\ V=输出尺寸×隐藏层神经元个数
U=隐藏层神经元个数×输入尺寸W=隐藏层神经元个数×隐藏层神经元个数V=输出尺寸×隐藏层神经元个数
这样最简单的RNN就分析完了。
3️⃣ RNN结构
1. N to N结构
输入序列和输出序列长度一样,即一个输入x对应一个输出y。常用于视频帧分类、词性标注等
公式可以表示为:
h
i
=
f
(
U
⋅
x
i
+
W
⋅
h
i
−
1
)
y
i
=
g
(
V
⋅
h
i
)
h_{i}=f(U\cdot x_{i}+W\cdot h_{i-1})\\y_{i}=g(V\cdot h_{i})
hi=f(U⋅xi+W⋅hi−1)yi=g(V⋅hi)
2. N to 1结构
整个输入序列被压缩成一个固定的输出,通常用来处理那些输入序列产生一个整体的输出的场景。例如序列分类(一段文本→判断积极or消极)、时间序列预测(给定过去 10 天的股价,预测第 11 天的股价)、异常检测(根据历史数据输出一个二分类结果)
公式可以表示为:
h
N
=
f
(
U
⋅
x
N
+
W
⋅
h
N
−
1
)
y
N
=
g
(
V
⋅
h
N
)
h_{N}=f(U\cdot x_{N}+W\cdot h_{N-1})\\y_{N}=g(V\cdot h_{N})
hN=f(U⋅xN+W⋅hN−1)yN=g(V⋅hN)
3. 1 to N结构
1 to N 结构RNN,表示一个输入数据对应输出一个序列的模型,输入位置有两种形式,适用于图生文任务:
- 只在首个时刻输入
h i = { f ( W ⋅ h i − 1 ) , i > 1 f ( U ⋅ x 1 + W ⋅ h 0 ) , i = 1 y i = g ( V ⋅ h i ) \begin{aligned} &\left.h_{i}=\left\{\begin{matrix}{f(W\cdot h_{i-1}) ,i>1}\\{f(U\cdot x_{1}+W\cdot h_{0}) ,i=1}\\\end{matrix}\right.\right.\\ &y_{i}=g(V\cdot h_{i}) \end{aligned} hi={f(W⋅hi−1),i>1f(U⋅x1+W⋅h0),i=1yi=g(V⋅hi) - 在每个时刻均输入
h i = f ( U ⋅ x 1 + W ⋅ h i − 1 ) y i = g ( V ⋅ h i ) h_{i}=f(U\cdot x_{1}+W\cdot h_{i-1})\\y_{i}=g(V\cdot h_{i}) hi=f(U⋅x1+W⋅hi−1)yi=g(V⋅hi)
注意里面是 x 1 x_1 x1
4. N to M结构RNN模型(encoder-decoder、seq2seq)
输入与输出序列不等长的结构。N和M分别为输入序列长度及输出序列长度,该结构我们采用一个N to 1结构作为encoder,一个1 to M结构作为decoder来实现。
主要实现原理为在encoder和decoder之间,增加了一个上下文向量
c
c
c,向量
c
c
c中包含着输入系列的语义信息和序列化信息。在上图中,
c
c
c作为decoder的输入数据。此外,
c
c
c还可以作为decoder的隐层初始变量,如下图所示:
此外,还有第三种结构:
通过N to M结构RNN模型,可以适应各类序列处理任务,常见的如机器翻译、语音识别、文本摘要及阅读理解等任务。由于输入输出都是序列,该模型也称为seq2seq模型
。向量
c
c
c的计算方法包括以下几种:
c
=
h
N
c
=
g
(
h
N
)
c
=
g
(
h
1
:
:
h
N
)
\begin{aligned} &\mathbf{c}=h_{N} \\ &\mathbf{c}=g(h_N) \\ &\mathbf{c}=g(h_1::h_N) \end{aligned}
c=hNc=g(hN)c=g(h1::hN)
第一种计算方法:上下文向量
c
c
c就是Encoder最后一个时间步的隐藏状态
h
N
h_N
hN
第二种计算方法:增加一个非线性变换函数
第三种计算方法:使用整个序列的隐藏状态
h
1
,
h
2
,
…
,
h
N
h_1,h_2,\ldots,h_N
h1,h2,…,hN来计算上下文向量
c
c
c
局限性: 编码和解码之间的唯一联系是固定长度的上下文向量 c c c。编码时,整个序列的信息需要被压缩进一个固定长度的 c c c中,这可能导致信息丢失或覆盖。因此,对于较长的输入序列,解码效果可能会受到影响
解决办法: 提出了注意力(Attention)机制
4️⃣ 代码
接下来看一下最简单的代码:
import torch
import torch.nn as nn
# 参数设置
input_size = 2 # 每个时间步的特征维度
hidden_size = 5 # 隐层神经元数量
num_layers = 1 # RNN层数
output_size = 3 # 假设输出的维度
# RNN对象实例化
rnn = nn.RNN(input_size, hidden_size, num_layers, batch_first=True)
# U:输入到隐藏状态的权重矩阵
U = rnn.weight_ih_l0 # 输入到隐藏状态的权重矩阵
print("矩阵 U 的大小 (输入到隐藏层):", U.shape) # 应为 (hidden_size, input_size)
# W:隐藏状态到隐藏状态的权重矩阵
W = rnn.weight_hh_l0 # 隐藏状态之间的递归权重矩阵
print("矩阵 W 的大小 (隐藏层到隐藏层):", W.shape) # 应为 (hidden_size, hidden_size)
# V:输出层权重矩阵
# 在 PyTorch 中没有直接实现,可以添加一个 Linear 层来模拟
V_layer = nn.Linear(hidden_size, output_size) # 定义线性层
V = V_layer.weight # V 就是隐藏状态到输出层的权重矩阵
print("矩阵 V 的大小 (隐藏层到输出层):", V.shape) # 应为 (output_size, hidden_size)
输出:
矩阵 U 的大小 (输入到隐藏层): torch.Size([5, 2])
矩阵 W 的大小 (隐藏层到隐藏层): torch.Size([5, 5])
矩阵 V 的大小 (隐藏层到输出层): torch.Size([3, 5])
5️⃣ 总结
-
标准的RNN存在
梯度消失
和梯度爆炸
问题,无法捕捉长时间序列的关系。因此LSTM和GRU被提出关于梯度消失和爆炸的原理,看我的另一篇博客