RNN及其变体
RNN简述
RNN(Recurrent Neural Network),中文称作循环神经网络,是一种专门用于处理序列数据的神经网络架构。一般以序列数据为输入,通过网络内部的结构设计有效捕捉序列之间的关系特征,一般也是以序列形式进行输出。它的特点是能够捕捉序列数据中的时间依赖关系,广泛应用于自然语言处理(NLP)、时间序列预测、语音识别等领域。
- 序列数据 (Sequential Data):指的是数据项之间存在顺序关系的数据,例如:
- 文本:句子中的单词顺序
- 语音:音频信号的时间序列
- 股票价格:随时间变化的股价
- 视频:连续的帧序列
- 循环连接 (Recurrent Connection):RNN的核心在于其循环连接,它允许信息在网络内部循环流动。这使得网络可以保留过去的信息,并将其用于处理当前的输入。
- 隐藏状态 (Hidden State):RNN在每个时间步都会维护一个隐藏状态,这个隐藏状态包含了过去的信息。它可以被认为是网络的"记忆"。
- 时间步 (Time Step):指的是序列数据中的每个元素,例如文本中的一个单词、语音中的一个音频帧。
基本工作原理
**输入:**RNN接收一个序列数据作为输入,每次输入序列中的一个元素 (时间步)。
**隐藏状态更新:**对于每个时间步,RNN根据当前的输入和上一个时间步的隐藏状态计算出新的隐藏状态。这个计算通常使用激活函数(如tanh或ReLU)。
- 隐藏状态更新公式
h t = σ ( W h h t − 1 + W x x t + b ) h_t=σ(W_hh_{t−1}+W_xx_t+b) ht=σ(Whht−1+Wxxt+b)
Wh,Wx |:权重矩阵
ht |:t时刻的隐藏状态
**输出:**RNN根据当前的隐藏状态计算出当前的输出。
**循环:**上述过程会在序列的每个时间步重复进行,直到序列结束。
📓 理解RNN的关键:按时间步对RNN进行展开
RNN模型分类
- 概览
类型 | 输入结构 | 输出结构 | 应用场景 |
---|---|---|---|
N vs N | 等长序列输入 | 等长序列输出 | 词性标注、逐点预测 |
N vs 1 | 序列输入 | 单个时间步输出 | 文本分类、情感分析 |
1 vs N | 单个时间步输入 | 序列输出 | 文本/音乐生成、图像描述生成 |
N vs M | 不等长序列输入 | 不等长序列输出 | 机器翻译、文本摘要 |
1.按输出和输出的结构分类
N vs N:输入和输出序列等长. 适用范围较小,可以用于生成等长度的合辙诗句
1 vs N:输入是一个时间步[ 输入(一个词/值/向量) ], 输出是一个序列数据, 常用于文本/音乐生成/图片描述生成
N vs 1:输入是一个序列数据,一个时间步[ 输入(一个词/值/向量) ], 常用于情感/文本分类任务
N V M模式: 输入输出都是序列数据, 但是不等长,常用于机器翻译/语音识别/文本摘要
这是一种不限输入输出长度的RNN结构,它由编码器和解码器两部分组成,两者的内部结构都是某类RNN,它也被称为seq2seq架构。输入数据首先通过编码器, 最终输出一个隐含变量c, 之后最常用的做法是使用这个隐含变量c作用在解码器进行解码的每一步上,以保证输入信息被有效利用。
seq2seq架构最早被提出应用于机器翻译,因为其输入输出不受限制,如今也是应用最广的RNN模型结构。在机器翻译、阅读理解、文本摘要等众多领域都进行了非常多的应用实践。
2.按RNN内部构造分类
概览
类型 传统RNN LSTM Bi-LSTM GRU Bi-GRU
1.传统RNN模型
概况
[内部结构示意图]
[解读] :在 t 时刻时(对应图片的中间),存在两部分输入:ht-1 和 Xt ,代表上一时间步的隐藏层输出以及此时间步的输入。
在进入RNN结构体后:
1.它们会"融合"到一起,形成新的张量[xt, ht−1]。
2.这个新的张量[xt, ht−1] 将通过一个全连接层(线性层),该层使用tanh作为激活函数,最终得到该时间步的输出ht,它将作为下一个时间步的输入和xt+1一起进入结构体。
即进行了以下运算:
h t = t a n h ( W t [ X t , h t − 1 ] + b t ) h_t = tanh(W_t[X_t,h_{t-1}] + b_t) ht=tanh(Wt[Xt,ht−1]+bt)
对这一过程在pytorch有另一种解读:
pytorch对这两部分输入:ht-1 和 Xt 分配了各自的权重和偏置项,并进行了以下运算(两次线性运算 + tanh激活)
h t = tanh ( x t W i h T + b i h + h t − 1 W h h T + b h h ) h_t = \tanh(x_t W_{ih}^T + b_{ih} + h_{t-1}W_{hh}^T + b_{hh}) ht=tanh(xtWihT+bih+ht−1WhhT+bhh)
- 重复前两步直到循环结束
- 将所有时间步的隐藏层输出{hi , i = 1,2,…}合并为output,将output的最后的张量(output[-1],即 hn)作为全连接层的输入,进行预测
- 计算损失,反向传播,参数更新
pytorch构建RNN模型
[API] : torch.nn.RNN
# torch.nn.RNN 的 __init__参数说明
__init__(input_size, hidden_size, num_layers=1, nonlinearity='tanh', bias=True, batch_first=False, dropout=0.0, bidirectional=False, device=None, dtype=None)
input_size
:输入特征的数量,即每个时间步输入的维度。[词向量维度]hidden_size
:隐藏状态的特征数量,即每个时间步隐藏状态的维度。num_layers
:循环层的数量。例如,设置num_layers=2
表示堆叠两个 RNN 层。默认值为 1。nonlinearity
:使用的激活函数,可以是'tanh'
或'relu'
。默认值为'tanh'
。bias
:如果为False
,则该层不使用偏置权重b_ih
和b_hh
。默认值为True
。batch_first
:如果为True
,则输入和输出张量的形状为(batch, seq, feature)
,否则为(seq, batch, feature)
。默认值为False
。dropout
:如果非零,则在每个 RNN 层(除了最后一层)的输出后引入一个 Dropout 层,丢弃概率为dropout
。默认值为 0。bidirectional
:如果为True
,则使用双向 RNN。默认值为False
。device
:指定模型所在的设备。dtype
:指定模型参数的数据类型。句子长度为1的基础RNN模型代码
def dm01():
"""
构建句子长度为 1 的基础 RNN模型
:return:None
"""
# 0- 手动设定随机种子方便后续复现
torch.manual_seed(12)
# 1- 构建 RNN模型
# input_size: 词向量维度
# hidden_size: 隐藏层维度
# num_layers: 隐藏层层数
rnn = nn.RNN(input_size=3, hidden_size=4, num_layers=1)
# 2- 定义输出向量
# size: (seq_len,batch_size,input_dim)
# seq_len:句子长度
# batch_size: 批次的样本数
# input_dim=input_size: 词向量维度
input = torch.randn(1, 2, 3)
# 3- 定义初始隐藏状态张量 h0
# size:(num_layer * 网络方向数,batch_size,hidden_size)
# num_layer: 隐藏层层数
# 网络方向数: 为 1 或 2 -> 1:代表是单向RNN 2:代表是双向RNN
# batch_size: 样本批次数
# hidden_size: 隐藏层维度
h0 = torch.zeros(1, 2, 4)
## 注意!! h0也可以不显式定义出来,默认就是全0初始化.
## 使用rnn模型时可以不传入h0,就是使用的隐式设置hidden层
### 示例:output, hn = rnn(input)
# 4- 使用RNN模型
output, hn = rnn(input, h0)
# 5- 查看模型输出
print('output->', output, output.shape)
print('hn->', hn, hn.shape)
# 结论: 若句子只有1个词,output输出结果等于hn
输出结果
output-> tensor( [[[-0.2506, -0.3442, -0.2307, 0.1333], [-0.1602, -0.1754, -0.5785, -0.3782]]] , grad_fn=<StackBackward0>) torch.Size([1, 2, 4]) hn-> tensor( [[[-0.2506, -0.3442, -0.2307, 0.1333], [-0.1602, -0.1754, -0.5785, -0.3782]]] , grad_fn=<StackBackward0>) torch.Size([1, 2, 4])
结论: 若句子只有1个词,output输出结果等于hn
- 句子长度大于 1 的RNN模型
def dm02():
"""
构建句子长度大于 1 的基础 RNN模型
:return:None
"""
# 0- 手动设定随机种子方便后续复现
torch.manual_seed(12)
# 1- 构建 RNN模型
# input_size: 词向量维度
# hidden_size: 隐藏层维度
# num_layers: 隐藏层层数
rnn = nn.RNN(input_size=4, hidden_size=3, num_layers=1)
# 2- 定义输出向量
# size: (seq_len,batch_size,input_dim)
# seq_len:句子长度
# batch_size: 批次的样本数
# input_dim=input_size: 词向量维度
input = torch.randn(3, 2, 4)
# 3- 定义初始隐藏状态张量 h0
# size:(num_layer * 网络方向数,batch_size,hidden_size)
# num_layer: 隐藏层层数
# 网络方向数: 为 1 或 2 -> 1:代表是单向RNN 2:代表是双向RNN
# batch_size: 样本批次数
# hidden_size: 隐藏层维度
h0 = torch.zeros(1, 2, 3)
## 注意!! h0也可以不显式定义出来,默认就是全0初始化.
## 使用rnn模型时可以不传入h0,就是使用的隐式设置hidden层
### 示例:output, hn = rnn(input)
# 4- 使用RNN模型
output, hn = rnn(input, h0)
# 5- 查看模型输出
print('output->', output, output.shape)
print('hn->', hn, hn.shape)
# 结论: 若句子由多个词组成, output的最后一组词向量(每个句子的最后一个词组成)等于h0
输出结果
output-> tensor( [[[ 0.0096, -0.4331, 0.0036], [-0.2697, -0.4088, -0.1225]], [[-0.4351, -0.8297, -0.5539], [-0.2669, -0.6056, 0.5748]], [[-0.3306, 0.0999, -0.7674], [-0.0076, -0.7076, 0.6473]]] ,grad_fn=<StackBackward0>) torch.Size([3, 2, 3]) hn-> tensor( [[[-0.3306, 0.0999, -0.7674], [-0.0076, -0.7076, 0.6473]]] , grad_fn=<StackBackward0>) torch.Size([1, 2, 3])
结论: 若句子由多个词组成, output的最后一组词向量(每个句子的最后一个词组成)等于h0
- 模拟 RNN模型循环机制
def dm03():
"""
模拟 RNN循环机制
:return:None
"""
# 0- 手动设定随机种子方便后续复现
torch.manual_seed(12)
# 1- 构建 RNN模型
# input_size: 词向量维度
# hidden_size: 隐藏层维度
# num_layers: 隐藏层层数
rnn = nn.RNN(input_size=4, hidden_size=3, num_layers=1)
# 2- 定义输出向量
# size: (seq_len,batch_size,input_dim)
# seq_len:句子长度
# batch_size: 批次的样本数
# input_dim=input_size: 词向量维度
input = torch.randn(3, 1, 4)
# print('input->', input)
# 3- 定义初始隐藏状态张量 h0
# size:(num_layer * 网络方向数,batch_size,hidden_size)
# num_layer: 隐藏层层数
# 网络方向数: 为 1 或 2 -> 1:代表是单向RNN 2:代表是双向RNN
# batch_size: 样本批次数
# hidden_size: 隐藏层维度
h0 = torch.zeros(1, 1, 3)
## 注意!! h0也可以不显式定义出来,默认就是全0初始化.
## 使用rnn模型时可以不传入h0,就是使用的隐式设置hidden层
### 示例:output, hn = rnn(input)
# 4- 一个词一个词的 给模型喂数据
for i in range(input.shape[0]):
tmp_x = input[0].unsqueeze(dim=0)
# print('tmp_x->', tmp_x)
output, hn = rnn(tmp_x, h0)
# 5- 查看模型输出
print(f'output_{i + 1}->', output, output.shape)
print(f'hn_{i + 1}->', hn, hn.shape)
print()
输出结果:
output_1-> tensor([[[ 0.1012, -0.1932, 0.0585]]], grad_fn=<StackBackward0>) torch.Size([1, 1, 3]) hn_1-> tensor([[[ 0.1012, -0.1932, 0.0585]]], grad_fn=<StackBackward0>) torch.Size([1, 1, 3]) output_2-> tensor([[[ 0.1012, -0.1932, 0.0585]]], grad_fn=<StackBackward0>) torch.Size([1, 1, 3]) hn_2-> tensor([[[ 0.1012, -0.1932, 0.0585]]], grad_fn=<StackBackward0>) torch.Size([1, 1, 3]) output_3-> tensor([[[ 0.1012, -0.1932, 0.0585]]], grad_fn=<StackBackward0>) torch.Size([1, 1, 3]) hn_3-> tensor([[[ 0.1012, -0.1932, 0.0585]]], grad_fn=<StackBackward0>) torch.Size([1, 1, 3])
- 隐藏层大于1的RNN模型
def dm04():
"""
构建隐藏层层数大于1的 RNN模型
:return:None
"""
# 0- 手动设定随机种子方便后续复现
torch.manual_seed(12)
# 1- 构建 RNN模型
# input_size: 词向量维度
# hidden_size: 隐藏层维度
# num_layers: 隐藏层层数
rnn = nn.RNN(input_size=3, hidden_size=3, num_layers=2)
# 2- 定义输出向量
# size: (seq_len,batch_size,input_dim)
# seq_len:句子长度
# batch_size: 批次的样本数
# input_dim=input_size: 词向量维度
input = torch.randn(1, 2, 3)
# 3- 定义初始隐藏状态张量 h0
# size:(num_layer * 网络方向数,batch_size,hidden_size)
# num_layer: 隐藏层层数
# 网络方向数: 为 1 或 2 -> 1:代表是单向RNN 2:代表是双向RNN
# batch_size: 样本批次数
# hidden_size: 隐藏层维度
h0 = torch.zeros(2, 2, 3)
## 注意!! h0也可以不显式定义出来,默认就是全0初始化.
## 使用rnn模型时可以不传入h0,就是使用的隐式设置hidden层
### 示例:output, hn = rnn(input)
# 4- 使用RNN模型
output, hn = rnn(input, h0)
# 5- 查看模型输出
print('output->', output, output.shape)
print('hn->', hn, hn.shape)
print('rnn模型->', rnn)
# 结论:若只有1个隐藏层,output输出结果等于hn
# 结论:如果有2个隐藏层,hn有2个,output输出结果等于最后1个隐藏层的hn
输出结果:
output-> tensor( [[[-0.3889, -0.1856, -0.5709], [-0.3924, -0.2318, -0.4879]]] , grad_fn=<StackBackward0>) torch.Size([1, 2, 3]) hn-> tensor( [[[-0.6897, -0.7110, 0.7299], [-0.8599, -0.6114, 0.4448]], [[-0.3889, -0.1856, -0.5709], [-0.3924, -0.2318, -0.4879]]] , grad_fn=<StackBackward0>) torch.Size([2, 2, 3]) rnn模型-> RNN(3, 3, num_layers=2)
结论:若只有1个隐藏层,output输出结果等于hn
结论:如果有2个隐藏层,hn有2个,output输出结果等于最后1个隐藏层的hn
关于output的维数,给出如下步骤方便理解
- 隐藏状态的维度信息
h i h_{i} hi:(num_layers * 网络方向数,batch_size,hidden_size) ,i=1,2…n
rnn层前向传播时,按照以下步骤执行:
1.每一个时间步会产生一个正向隐藏状态: h i f h^{f}_{i} hif,一个反向隐藏状态** h i b h^{b}_{i} hib**(如果是双向网络)
如果是此时rnn是双向网络则会执行2.0:
2.0 根据dim= -1 将这两个张量拼接(concat)起来
即** h i h_i hi = h i f , h i b {h^{f}_{i},h^{b}_{i}} hif,hib =>(num_layers,batch_size,hidden_size * 网络方向数)**
2.output会按照dim=0维度连接 h 0 h_0 h0/ h 1 h_1 h1…/ h n h_n hn 所有向量(concat操作,并不会增加维度)
因此output也是一个三维张量,最后output的维数 => (seq_len,batch_size,hidden_size * 网络方向数)
传统RNN模型的优点与不足
优点
- 结构简单,易于理解和实现:
- 传统RNN的结构非常直观,只有一个隐藏层和循环连接,因此容易理解其工作原理。
- 由于结构简单,其代码实现也相对容易。
- 适用于处理序列数据:
- 传统RNN的核心优势在于它能够处理具有顺序关系的数据,例如文本、语音、时间序列等。
- 通过循环连接,RNN可以将过去的信息传递到当前时刻,从而学习序列数据中的依赖关系。
- 能够处理可变长度的序列:
- 传统RNN可以处理长度不固定的序列,而无需提前固定输入数据的长度。
- 这使得它在处理自然语言、语音等具有可变长度数据的任务中非常灵活。
- 参数共享:
- 传统RNN在每个时间步都共享相同的参数(权重矩阵和偏置项),从而减少了模型参数的数量。
- 参数共享还允许RNN学习序列数据中跨时间步的通用模式。
- 计算效率较高 (相对于更复杂的RNN变体):
- 由于结构简单,传统 NN的计算复杂度相对较低,训练速度相对较快。
- 在一些计算资源有限的场景下,传统RNN是一个可行的选择。
- 结构简单,易于理解和实现:
缺点
- 梯度消失问题:
- 这是传统RNN最主要的缺点。在处理长序列时,梯度在反向传播过程中会逐渐衰减,导致网络无法学习到长距离依赖关系。
- 梯度消失使得模型难以捕捉输入序列中较早的信息,从而限制了其在长序列任务上的性能。
- 梯度爆炸问题:
- 与梯度消失相反,在某些情况下,梯度在反向传播过程中可能会呈指数级增长,导致训练不稳定甚至发散。
- 梯度爆炸也限制了传统RNN在某些任务中的应用。
- 难以捕捉长期依赖:
- 由于梯度消失问题,传统RNN很难学习长距离的依赖关系,即难以记住输入序列中较早的信息,并将其用于影响后面的输出。
- 这意味着在处理文本、语音等长序列时,传统RNN很难理解上下文的含义。
- 训练不稳定:
- 由于梯度消失和梯度爆炸问题,传统RNN的训练过程可能会不稳定,需要小心调整超参数。
- 无法并行计算:
- 由于RNN的循环结构,每个时间步的计算都依赖于前一个时间步的隐藏状态,因此无法进行并行计算,训练速度受到限制。
- 梯度消失问题: