初识RNN02——RNN基础

深入理解RNN及其变体LSTM和GRU

系列文章目录

初识RNN01——NLP基础



一、RNN模型

1.1 为什么需要RNN

在这里插入图片描述
上图是一幅全连接神经网络图,我们可以看到输入层-隐藏层-输出层,他们每一层之间是相互独立地,(框框里面代表同一层),每一次输入生成一个节点,同一层中每个节点之间又相互独立的话,那么我们每一次的输入其实跟前面的输入是没有关系地。这样在某一些任务中便不能很好的处理序列信息。
在这里插入图片描述
对于不同类型的数据和任务,理解数据的顺序和前后关系的重要性是至关重要的。例如涉及到文本、股票、天气、语音等具有时间顺序或逻辑顺序的数据时,顺序的变化会对结果产生显著影响。
在这里插入图片描述

因此,在处理具有时间或逻辑顺序的数据时,我们必须考虑到顺序的重要性,并设计相应的模型来充分利用数据的前后关系,以获得更准确和有意义的结果。这也是循环神经网络(RNN)等模型在这些任务中被广泛使用的原因之一,因为它们能够捕捉到数据的顺序信息,并根据前面的输入来预测后续的输出。

1.2 RNN原理

循环神经网络(Recurrent Neural Network,RNN)是一种神经网络结构,专门用于处理序列数据。。RNN的核心特征在于它能够在处理序列的每个元素时保留一个内部状态(记忆),这个内部状态能够捕捉到之前元素的信息。这种设计使得RNN特别适合处理那些当前输出依赖于之前信息的任务。
在这里插入图片描述
左图可以理解为,先将每一层的网络简化,再将网络旋转90度得到的简化图。而右边两种类型,可以理解为是左图的简化版本。

在RNN的经典架构中,网络通过一个特殊的循环结构将信息从一个处理步骤传递到下一个。这个循环结构通常被称为“隐藏层状态”或简单地称为“隐藏状态”。隐藏状态是RNN的记忆部分,它能够捕获并存储关于已处理序列元素的信息。
在这里插入图片描述
假设为一个包含三个单词的句子,将模型展开,即为一个三层的网络结构。可以理解为, x t − 1 {x}_{t-1} xt1为第一个词, x t {x}_{t} xt为第二个词, x t + 1 {x}_{t+1} xt+1为第三个词。

图中参数含义:

  • x t {x}_{t} xt表示第t步的输入。比如 x 1 {x}_{1} x1为第二个词的词向量( x 0 {x}_{0} x0为第一个词);

  • H t {H}_{t} Ht为隐藏层的第t步的状态,它是网络的记忆单元。

    • H t {H}_{t} Ht根据当前输入层的输出与上一时刻隐藏层的状态 H t − 1 {H}_{t-1} Ht1进行计算,如下所示。
      H t = f ( U ⋅ x t + W ⋅ H t − 1 ) H_t=f(\mathbf{U}·x_t+\mathbf{W}·H_{t-1}) Ht=f(Uxt+WHt1)

    • 其中,U是输入层的连接矩阵,W是上一时刻隐含层到下一时刻隐含层的权重矩阵,f(·)一般是非线性的激活函数,如tanh或ReLU。

  • O t {O}_{t} Ot是第t步的输出。输出层是全连接层,即它的每个节点和隐含层的每个节点都互相连接,V是输出层的连接矩阵,g(·)一是激活函数。

    • o t = g ( V ⋅ s t ) o_t=g(\mathbf{V}·s_t) ot=g(Vst)

    • 带入可以得到
      H t = f ( W i n X t + W s H t − 1 + b t ) = f ( W i n X t + W s f ( W i n X t − 1 + W s H t − 2 + b t − 1 ) + b t ) = f ( W i n X t + W s f ( W i n X t − 1 + W s f ( W i n X t − 2 + W s H t − 3 + b t − 2 ) + b t − 1 ) + b t ) = f ( W i n X t + W s f ( W i n X t − 1 + W s f ( W i n X t − 2 + W s ( . . . ) ) + b t − 2 ) + b t − 1 ) + b t ) \begin{align} H_t&=f(\mathbf{W}_{in}X_t+\mathbf{W}_{s}H_{t-1}+b_t)\\ &=f(\mathbf{W}_{in}X_t+\mathbf{W}_{s}f(\mathbf{W}_{in}X_{t-1}+\mathbf{W}_{s}H_{t-2}+b_{t-1})+b_{t})\\ &=f(\mathbf{W}_{in}X_t+\mathbf{W}_{s}f(\mathbf{W}_{in}X_{t-1}+\mathbf{W}_{s}f(\mathbf{W}_{in}X_{t-2}+\mathbf{W}_{s}H_{t-3}+b_{t-2})+b_{t-1})+b_t)\\ &=f(\mathbf{W}_{in}X_t+\mathbf{W}_{s}f(\mathbf{W}_{in}X_{t-1}+\mathbf{W}_{s}f(\mathbf{W}_{in}X_{t-2}+\mathbf{W}_{s}(...))+b_{t-2})+b_{t-1})+b_t) \end{align} Ht=f(WinXt+WsHt1+bt)=f(WinXt+Wsf(WinXt1+WsHt2+bt1)+bt)=f(WinXt+Wsf(WinXt1+Wsf(WinXt2+WsHt3+bt2)+bt1)+bt)=f(WinXt+Wsf(WinXt1+Wsf(WinXt2+Ws(...))+bt2)+bt1)+bt)

通过这种逐步处理序列并在每一步更新隐藏状态的方式,RNN能够在其内部维持一个随时间变化的“记忆”。这使得它能够对之前序列元素的信息做出响应,并据此影响后续的输出。这种特性对于诸如语言模型、文本生成、语音识别等许多序列处理任务至关重要。

内部结构如下:
在这里插入图片描述

1.3 RNN 代码实现

代码如下:

import torch
import torch.nn as nn

class SimpleRNN(nn.Module):
    def __init__(self, input_size, hidden_size, output_size, num_layers=1):
        """
        初始化RNN模型
        :param input_size: 输入特征的维度
        :param hidden_size: 隐藏层的大小
        :param output_size: 输出的维度
        :param num_layers: RNN的层数
        """
        super(SimpleRNN, self).__init__()
        self.hidden_size = hidden_size
        self.num_layers = num_layers
        
        # 使用PyTorch内置的RNN层,可以指定tanh或ReLU作为非线性激活函数
        self.rnn = nn.RNN(input_size, hidden_size, num_layers, batch_first=True)
        # 全连接层将RNN的输出映射到最终输出
        self.fc = nn.Linear(hidden_size, output_size)
    
    def forward(self, x, h0=None):
        """
        前向传播
        :param x: 输入张量,形状为(batch_size, seq_len, input_size)
        :param h0: 初始隐藏状态
        """
        # 如果没有提供初始隐藏状态,则创建全零的初始状态
        if h0 is None:
            h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device)
        
        # RNN前向传播,输出形状为(batch_size, seq_len, hidden_size)
        out, hn = self.rnn(x, h0)
        
        # 将RNN的输出通过全连接层
        # 通常我们取最后一个时间步的输出
        out = self.fc(out[:, -1, :])
        
        return out, hn

重要参数含义

  1. Batch Size (批量大小):

    • Batch size指的是在一次前向传播或反向传播过程中同时处理的样本数量。
    • 例如,在文本处理中,如果一批数据包含100个句子,那么batch size就是100。
  2. Sequence Length (序列长度):

    • Sequence length是指输入数据中每个样本的连续时间步(或词、字符)的数量。
    • 例如,在一个句子级别的任务中,一个句子可能包含10个单词,那么序列长度就是10。
  3. Input Size (输入大小):

    • Input size是指每个时间步输入向量的特征维度。
    • 在处理文本时,如果每个词都被表示为一个固定维度的向量,那么input size就是这个词向量的维度。
    • 如在情感分析任务中,每个词可能被嵌入为一个100维的向量,那么input size就是100。
  4. Hidden Size (隐藏层大小):

    • Hidden size是指RNN单元内部隐藏状态(Hidden State)的维度。
    • 在每个时间步,RNN都会根据当前输入和上一时间步的隐藏状态来计算新的隐藏状态,新隐藏状态的维度就是hidden size。
    • 例如,如果我们设置hidden size为256,那么每个时间步产生的隐藏状态就是一个256维的向量。
    • 根据实验和模型复杂度的要求自由选择隐藏层大小,它并不是通过特定计算得出的数值。
    • 隐藏层大小的选择会影响到模型的学习能力和表示能力,同时也影响到模型的计算资源消耗。
    • 实践中,较小的隐藏层大小可能会限制模型的表达能力,而过大的隐藏层大小则可能导致过拟合、训练时间增加等问题。
    • 在决定隐藏层大小时,通常需要结合具体任务的特点、数据集规模、计算资源等因素进行合理选择,并通过交叉验证、网格搜索等方式进行超参数调优,以找到最优的隐藏层大小以及其他超参数组合。
  5. Output Size (输出大小):

    • Output size通常与特定任务相关。

    • 对于一般的RNN,每个时间步的输出大小与hidden size相同,即输出也是一个隐藏状态维度的向量。

    • 在分类任务中,最后一层可能通过一个全连接层映射到类别数目,这时最后一个时间步的输出大小可能是类别数目的维度。

    • 如果是多层或双向RNN,输出也可能经过额外的处理(如拼接、池化等),最终的输出大小会根据具体应用需求来确定。

    • 在最简单的单向单层循环神经网络(RNN)中,输出大小(output size)的计算通常比较直接:

  • 如果目的是为了获取每个时间步(time step)的隐藏状态表示,并且不进行额外的转换操作,那么每个时间步的输出大小(output size)就等于您设定的隐藏层大小(hidden size)。例如,如果设置的隐藏层大小(hidden
    size)是256,那么在每个时间步,RNN的输出也将是一个256维的向量。

  • 如果在RNN之后添加了其他层(如全连接层或分类层)来进行进一步的处理,比如进行分类任务,那么输出大小取决于这些后续层的设计。例如,如果您接下来是一个Softmax层用于做多分类,且类别数是10,则输出大小将会是10,表示每个样本的概率分布。

  • 如果是在做序列到序列(Sequence-to-Sequence)的任务,比如机器翻译,最后的时间步的隐藏状态通常会通过一个线性层映射到目标词汇表大小,这样输出大小就会是目标词汇表的大小。

1.3.1 双向RNN

双向RNN(Recurrent Neural Network)是一种特殊类型的循环神经网络,它能够在两个方向上处理序列数据,即正向和反向。这使得网络在预测当前输出时,能够同时考虑到输入序列中当前元素之前的信息和之后的信息。

以单层的双向RNN为例,其由两个独立的单层RNN组成,一个负责处理正向序列(从开始到结束),另一个负责处理反向序列(从结束到开始)。
在这里插入图片描述
将一个单向RNN变为双向只需将direction设定为True即可。

# 单向RNN
bi_rnn = nn.RNN(4, 3, 1, batch_first=True)
# 双向RNN
bi_rnn = nn.RNN(4, 3, 1, batch_first=True, bidirectional=True)

1.4 BPTT

BPTT(back-propagation through time)算法是常用的训练RNN的方法,其实本质还是BP算法,只不过RNN处理时间序列数据,所以要基于时间反向传播,故叫随时间反向传播。BPTT的中心思想和BP算法相同,沿着需要优化的参数的负梯度方向不断寻找更优的点直至收敛。综上所述,BPTT算法本质还是BP算法,BP算法本质还是梯度下降法,那么求各个参数的梯度便成了此算法的核心。
在这里插入图片描述
由图可知,需要寻优的参数有三个,分别是U、V、W。与BP算法不同的是,其中W和U两个参数的寻优过程需要追溯之前的历史数据,参数V相对简单只需关注目前,那么我们就来先求解参数V的偏导数。

由于RNN的损失也是会随着时间累加的,所以不能只求t时刻的偏导。
在这里插入图片描述
W和U的偏导的求解由于需要涉及到历史数据,其偏导求起来相对复杂,我们先假设只有三个时刻,那么在第三个时刻 L对W的偏导数以及 L对U的偏导数为:
在这里插入图片描述
在这里插入图片描述
根据上面两个式子可以写出L在t时刻对W和U偏导数的通式:
在这里插入图片描述
整体的偏导公式就是将其按时刻再一一加起来。

1.5 RNN模型存在的问题

- 梯度消失和爆炸
由于激活函数是嵌套在里面的,故损失函数的偏导中的累乘会导致激活函数导数的累乘,进而会导致“梯度消失“和“梯度爆炸“现象的发生。
sigmoid函数和tanh函数的函数图和导数图
由[sigmoid函数和tanh函数的函数图和导数图可知,其导数最大都不大于1。在上面式子累乘的过程中,如果取sigmoid函数作为激活函数的话,那么必然是一堆小数在做乘法,结果就是越乘越小。随着时间序列的不断深入,小数的累乘就会导致梯度越来越小直到接近于0,这就是“梯度消失“现象

解决“梯度消失“的方法主要有:
1、选取更好的激活函数
2、改变传播结构

关于第一点,一般选用ReLU函数作为激活函数,ReLU函数的图像为:
在这里插入图片描述
ReLU函数的左侧导数为0,右侧导数恒为1,这就避免了“梯度消失“的发生。但恒为1的导数容易导致“梯度爆炸“,但设定合适的阈值可以解决这个问题。还有一点就是如果左侧横为0的导数有可能导致把神经元死亡,不过设置合适的步长(学习率)也可以有效避免这个问题的发生。

关于第二点,目前可以采用LSTM结构以解决这个问题。

  • 远距离依赖
    RNN在处理长序列数据时面临一个重大挑战,即长期依赖性问题。长期依赖问题指的是当序列非常长时,RNN难以学习并保持序列早期时间步的信息。这是因为在RNN的训练过程中,使用反向传播算法进行梯度更新时,梯度往往会随着传播到更早的层而指数级衰减(梯度消失)或者指数级增长(梯度爆炸)。这导致了序列中较早时间步的信息对模型输出的影响变得微乎其微,从而使得模型难以学习到这些信息对序列后续部分的影响。

以一个具体的例子来说明,假设有一个叙述故事的序列:

“张三昨天下午本想去运动,但突然接到公司的急事,需要他紧急处理,随后他处理完去______________”。

在这个例子中,填空处的词与序列中较早出现的“运动”一词之间存在关联。然而,如果使用标准的RNN来预测空白处的词,由于长序列中的长期依赖问题,RNN可能无法有效地捕捉到“运动”这一关键信息,导致预测结果不准确。

长期依赖问题是RNN架构的一个根本性缺陷,它限制了RNN在处理具有重要长期依赖关系的长序列任务中的效能。因此,虽然RNN在处理较短序列时表现良好,但在涉及长距离时间依赖的复杂任务中,RNN的性能会大幅下降。

二、LSTM

2.1 LSTM 概述

长短期记忆网络(Long Short-Term Memory,LSTM)是一种特别设计来解决长期依赖问题的循环神经网络(RNN)架构。在处理序列数据,特别是长序列数据时,LSTM展现出其独特的优势,能够有效地捕捉和记忆序列中的长期依赖性。这一能力使得LSTM在众多领域,如自然语言处理、语音识别、时间序列预测等任务中,成为了一个强大且广泛使用的工具。

LSTM的核心思想是引入了称为“细胞状态”(cell state)的概念,该状态可以在时间步长中被动态地添加或删除信息。LSTM单元由三个关键的门控机制组成,通过这些门控机制,LSTM可以在处理长序列数据时更有效地学习长期依赖性,避免了传统RNN中的梯度消失或爆炸等问题。

LSTM基本单元结构如下:
在这里插入图片描述在这里插入图片描述
LSTM每个循环的模块内又有4层结构:3个sigmoid层,2个tanh层。其关键是细胞状态C,一条水平线贯穿于图形的上方,这条线上只在每个细胞内部进行一些线性操作,信息在上面流传很容易保持。
在这里插入图片描述

2.2 门控机制

LSTM有通过精心设计的称作“门”的结构来去除或者增加信息到细胞状态的能力。门是一种让信息选择式通过的方法。LSTM 拥有三个门,来保护和控制细胞状态。

2.2.1 遗忘门

  • 遗忘门(Forget Gate):决定细胞状态中要保留的信息。它通过一个sigmoid函数来输出一个0到1之间的值,表示要忘记或保留的程度。1 代表“完全保留这个”,而 0 代表“完全摆脱这个”。
    在这里插入图片描述
    右边是遗忘门(forget gate)的公式,用于确定哪些信息应当从单元的状态中移除
  • [ h t − 1 {h}_{t-1} ht1, x t {x}_{t} xt]:这是一个连接的向量,包括前一时间步的隐藏状态 h t − 1 {h}_{t-1} ht1和当前时间步的输入 x t {x}_{t} xt。它们被合并起来,以便遗忘门可以考虑当前的输入和先前的隐藏状态来做出决策。选择拼接,而非与权重矩阵相除后相加,能够减少权重矩阵的个数。
  • W f {W}_{f} Wf:这是遗忘门的权重矩阵,用于从输入[$ {h}{t-1}$, $ {x}{t}$]中学习什么信息应该被遗忘。
  • b f {b}_{f} bf:这是遗忘门的偏置项,它被加到权重矩阵和输入向量的乘积上,可以提供额外的调整能力,确保即使在没有输入的情况下遗忘门也能有一个默认的行为。
  • $\sigma $:这是sigmoid激活函数,它将输入压缩到0和1之间。在这里,它确保遗忘门的输出也在这个范围内,表示每个状态单元被遗忘的比例。
  • f t {f}_{t} ft :这是在时间步 ( t ) 的遗忘门的输出,它是一个向量,其中的每个元素都在0和1之间,对应于细胞状态中每个元素应该被保留的比例。

函数的整体目的是使用当前输入和前一时间步的隐藏状态来计算一个门控信号,该信号决定细胞状态中的哪些信息应该被保留或丢弃。这是LSTM的关键特性之一,它允许网络在处理序列数据时学习长期依赖关系。

2.2.2 输入门

输入门(Input Gate):决定要从输入中更新细胞状态的哪些部分。它结合了输入数据和先前的细胞状态,利用sigmoid函数来确定更新的量,并通过tanh函数来产生新的候选值,然后结合遗忘门确定最终的更新。
在这里插入图片描述
解释如下:

  • i t {i} _{t} it 表示时间步 ( t ) 的输入门激活值,是一个向量。这个向量通过sigmoid函数$\sigma $产生,将值限定在 0 和 1 之间。它决定了多少新信息会被加入到细胞状态中。
  • W i {W}_{i} Wi 是输入门的权重矩阵,用于当前时间步的输入 x t {x}_{t} xt 和前一个时间步的隐藏状态 h t − 1 {h}_{t-1} ht1
  • [ h t − 1 {h}_{t-1} ht1, x t {x}_{t} xt] 是前一个隐藏状态和当前输入的串联。
  • b i {b}_{i} bi 是输入门的偏置向量。
  • ~ C t \widetilde{} {{C}_{t}} Ct 是候选细胞状态,它是通过tanh函数产生的,可以将值限定在 -1 和 1 之间。它与输入门 i t {i}_{t} it 相乘,决定了将多少新的信息添加到细胞状态中。
  • W C {W}_{C} WC 是控制候选细胞状态的权重矩阵。
  • b C {b}_{C} bC 是对应的偏置向量。

2.2.3 状态更新

在每个时间步,LSTM单元都会计算输入门的激活与候选细胞状态,并结合遗忘门f_t的值更新细胞状态C_t。这样,LSTM能够记住长期的信息,并在需要的时候忘记无关的信息。
在这里插入图片描述
这里:

  • C t {C}_{t} Ct 是当前时间步的细胞状态。
  • C t − 1 {C}_{t-1} Ct1 是上一个时间步的细胞状态。
  • f t {f}_{t} ft 是遗忘门的激活值,通过sigmoid函数计算得到。它决定了多少之前的细胞状态应该被保留。
  • i t {i}_{t} it 是输入门的激活值,也是通过sigmoid函数得到的。它决定了多少新的信息应该被存储在细胞状态中。
  • C t {C}_{t} Ct 是当前时间步的候选细胞状态,通过tanh函数得到。它包含了潜在的新信息,可以被添加到细胞状态中。

符号 * 代表元素间的乘积,意味着 f t {f}_{t} ft i t {i}_{t} it 分别与 C t − 1 {C}_{t-1} Ct1 C t {C}_{t} Ct 相乘的结果然后相加,得到新的细胞状态 C t {C}_{t} Ct 。这个更新规则使得LSTM能够在不同时间步考虑遗忘旧信息和添加新信息,是它在处理序列数据时记忆长期依赖信息的关键。

2.2.4 输出门

输出门(Output Gate):决定在特定时间步的输出是什么。它利用当前输入和先前的细胞状态来计算一个输出值,然后通过sigmoid函数来筛选。
在这里插入图片描述
具体来说:

  • o t {o}_{t} ot 是输出门的激活值。这是通过将前一时间步的隐藏状态 h t − 1 {h}_{t-1} ht1 和当前时间步的输入 x t {x}_{t} xt 连接起来,并应用权重矩阵W_o以及偏置项 b_o,然后通过sigmoid函数 $\sigma $
    来计算的。Sigmoid函数确保输出值在0和1之间。

  • C t {C}_{t} Ct 是当前时间步的细胞状态,这是在之前的步骤中计算的。

  • C t {C}_{t} Ct 是细胞状态的tanh激活,这个激活函数将值压缩到-1和1之间。这是因为细胞状态C_t可以有很大的值,而tanh函数有助于规范化这些值,使它们更加稳定。

  • h t {h}_{t} ht 是当前时间步的隐藏状态,通过将输出门 o t {o}_{t} ot 的值与细胞状态的tanh激活相乘来得到。这个元素级别的乘法(Hadamard乘法)决定了多少细胞状态的信息将被传递到外部作为当前的隐藏状态输出。

这种结构允许LSTM单元控制信息的流动,它可以通过输出门来控制有多少记忆单元的信息会被传递到隐藏状态和网络的下一个时间步。

2.3 LSTM总结

在这里插入图片描述

2.4 代码实现

import torch
import torch.nn as nn

class LSTMModel(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers, output_size, bidirectional=False):
        super(LSTMModel, self).__init__()
        self.hidden_size = hidden_size
        self.num_layers = num_layers
        self.bidirectional = bidirectional
        
        # 定义LSTM层
        self.lstm = nn.LSTM(input_size, hidden_size, num_layers, 
                           batch_first=True, bidirectional=bidirectional)  # batch_first=True表示输入形状为(batch_size, seq_len, input_size)
        
        # 定义输出层
        self.direction = 2 if bidirectional else 1
        self.fc = nn.Linear(hidden_size * self.direction, output_size)
    
    def forward(self, x):
        # 初始化隐藏状态和单元状态
        h0 = torch.zeros(self.num_layers * self.direction, 
                         x.size(0), self.hidden_size).to(x.device)
        c0 = torch.zeros(self.num_layers * self.direction, 
                         x.size(0), self.hidden_size).to(x.device)
        
        # 前向传播LSTM
        out, _ = self.lstm(x, (h0, c0))  # 输出形状: (batch_size, seq_len, hidden_size*directions)
        
        # 取最后一个时间步的输出
        out = out[:, -1, :]
        
        # 通过全连接层得到最终输出
        out = self.fc(out)
        return out

2.5 潜在问题

LSTM(长短期记忆网络)是一种特殊的RNN(循环神经网络),设计初衷就是为了解决传统RNN在长序列数据上训练时出现的梯度消失和梯度爆炸问题。然而,尽管LSTM相较于普通的RNN在处理长序列数据时表现得更好,但它仍然有可能在某些情况下出现梯度消失和梯度爆炸的问题。原因可以归结为以下几个方面:

2.5.1 梯度消失问题

梯度消失(Vanishing Gradient)主要在于反向传播过程中,梯度在多层传播时会逐渐减小,导致前面层的参数更新非常缓慢,甚至完全停滞。LSTM尽管通过门控机制(输入门、遗忘门和输出门)缓解了这个问题,但仍然可能出现梯度消失,特别是在以下情况下:

  • 长期依赖问题:如果序列特别长,即使是LSTM也可能无法有效地记住早期的信息,因为梯度会在很长的时间步长内持续衰减。
  • 不适当的权重初始化:如果权重初始化不合理,可能会导致LSTM的各个门在初始阶段就偏向于某种状态(如过度遗忘或完全记住),从而影响梯度的有效传播。
  • 激活函数的选择:尽管LSTM通常使用tanh和sigmoid激活函数,这些函数在某些输入值下可能会导致梯度的进一步缩小。

2.5.2 梯度爆炸问题

梯度爆炸(Exploding Gradient)则是在反向传播过程中,梯度在多层传播时会指数级增长,导致前面层的参数更新过大,模型难以收敛。LSTM在以下情况下可能出现梯度爆炸:

  • 过长的序列长度:即使是LSTM,在非常长的序列上仍然可能遇到梯度爆炸,因为梯度在反向传播时会不断累积,最终可能变得非常大。
  • 不适当的学习率:过高的学习率可能会导致梯度爆炸,因为参数更新的步伐太大,使得模型参数偏离最优解。
  • 不适当的权重初始化:与梯度消失类似,权重初始化也可能导致梯度爆炸。如果初始权重过大,梯度在反向传播过程中会不断放大。

解决方法

为了解决或缓解LSTM中的梯度消失和梯度爆炸问题,可以采取以下措施:

  • 梯度裁剪:在每次反向传播后,将梯度裁剪到某个阈值范围内,防止梯度爆炸。
  • 适当的权重初始化:使用标准的初始化方法,如Xavier初始化或He初始化,确保权重在初始阶段不至于过大或过小。
  • 调整学习率:选择合适的学习率,或者使用自适应学习率算法,如Adam、RMSprop等,动态调整学习率。
  • 正则化技术:如L2正则化、Dropout等,防止过拟合并平滑梯度。
  • 批归一化(Batch Normalization):在网络层之间使用批归一化技术,可以加速训练并稳定梯度。

尽管LSTM通过其结构在一定程度上缓解了梯度消失和爆炸问题,但理解并应用这些技术和方法仍然是确保模型训练稳定和高效的关键。

三、GRU

3.1 GRU概述

门控循环神经网络 (Gated Recurrent Neural Network,GRNN) 是LSTM的改进,它的提出,旨在更好地捕捉时间序列中时间步距离较大的依赖关系。它通过可学习的门来控制信息的流动。其中,门控循环单元 (Gated Recurrent Unit,GRU) 是一种常用的 GRNN。GRU 对LSTM 做了很多简化,同时却保持着和 LSTM 相同的效果。

3.2 GRU原理

GRU 有两个重大改进:

  1. 将三个门:输入门、遗忘门、输出门变为两个门:更新门 (Update Gate) 和 重置门 (Reset Gate)。
  2. 将 (候选) 单元状态 与 隐藏状态 (输出) 合并,即只有 当前时刻候选隐藏状态当前时刻隐藏状态

简化图:
在这里插入图片描述
内部结构:
在这里插入图片描述
在这里插入图片描述

3.2.1 重置门

在这里插入图片描述
r t = σ ( W r ⋅ [ h t − 1 , x t ] ) r_t = \sigma(W_r \cdot [h_{t-1}, x_t]) rt=σ(Wr[ht1,xt])
重置门决定在计算当前候选隐藏状态时,忽略多少过去的信息。
在这里插入图片描述

3.2.2 更新门

在这里插入图片描述
z t = σ ( W z ⋅ [ h t − 1 , x t ] ) z_t = \sigma(W_z \cdot [h_{t-1}, x_t]) zt=σ(Wz[ht1,xt])
更新当前时间步的信息,它使用前一时间步的隐藏状态 ( h_{t-1} ) 和当前输入 ( x_t ) 来计算得出。

3.2.3 候选隐藏状态

在这里插入图片描述
h ~ t = tanh ⁡ ( W ⋅ [ r t ∗ h t − 1 , x t ] ) \tilde{h}_t = \tanh(W \cdot [r_t * h_{t-1}, x_t]) h~t=tanh(W[rtht1,xt])
候选隐藏状态是当前时间步的建议更新,它包含了当前输入和过去的隐藏状态的信息。重置门的作用体现在它可以允许模型抛弃或保留之前的隐藏状态。

3.2.4 最终隐藏状态

在这里插入图片描述
h t = ( 1 − z t ) ∗ h t − 1 + z t ∗ h ~ t h_t = (1 - z_t) * h_{t-1} + z_t * \tilde{h}_t ht=(1zt)ht1+zth~t
最终隐藏状态是通过融合过去的隐藏状态和当前候选隐藏状态来计算得出的。更新门 ${Z}_{t} $控制了融合过去信息和当前信息的比例。

3.3 代码实现

import torch
import torch.nn as nn

class GRUModel(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers, output_size, bidirectional=False):
        super(GRUModel, self).__init__()
        self.hidden_size = hidden_size
        self.num_layers = num_layers
        self.bidirectional = bidirectional
        
        # 定义GRU层
        self.gru = nn.GRU(input_size, hidden_size, num_layers, 
                         batch_first=True, bidirectional=bidirectional)  # batch_first=True表示输入形状为(batch_size, seq_len, input_size)
        
        # 定义输出层
        self.direction = 2 if bidirectional else 1
        self.fc = nn.Linear(hidden_size * self.direction, output_size)
    
    def forward(self, x):
        # 初始化隐藏状态(GRU只有隐藏状态,没有细胞状态)
        h0 = torch.zeros(self.num_layers * self.direction, 
                         x.size(0), self.hidden_size).to(x.device)
        
        # 前向传播GRU
        out, _ = self.gru(x, h0)  # 输出形状: (batch_size, seq_len, hidden_size*directions)
        
        # 取最后一个时间步的输出
        out = out[:, -1, :]
        
        # 通过全连接层得到最终输出
        out = self.fc(out)
        return out

总结

本文系统阐述了循环神经网络(RNN)及其演进模型LSTM与GRU。RNN通过引入循环结构处理序列数据,但存在梯度消失/爆炸问题,难以学习长期依赖。LSTM通过精巧的输入门、遗忘门、输出门和细胞状态(Cell State)机制,有效控制了信息流动,解决了长期记忆难题。GRU作为LSTM的变体,将门控单元简化为重置门和更新门,在保持类似效果的同时降低了计算复杂度。文章详解了各模型的原理、门控机制、代码实现及优缺点,为理解和应用序列模型提供了清晰框架。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值