当前位置: 首页 > news >正文

循环神经网络

一、RNN模型

1、先导

1.1 为什么需要循环神经网络RNN

上图是一幅全连接神经网络图,我们可以看到输入层-隐藏层-输出层,他们每一层之间是相互独立地,(框框里面代表同一层),每一次输入生成一个节点,同一层中每个节点之间又相互独立的话,那么我们每一次的输入其实跟前面的输入是没有关系地。这样在某一些任务中便不能很好的处理序列信息。

1.2 时序数据

对于不同类型的数据和任务,理解数据的顺序和前后关系的重要性是至关重要的。对于图像识别这样的任务来说,图片的前后顺序并不会影响识别结果,因为图片中的像素是独立且无序的。但是,当涉及到文本、股票、天气、语音等具有时间顺序或逻辑顺序的数据时,顺序的变化会对结果产生显著影响。

举例来说,对于一句话"我吃苹果",如果改变顺序为"苹果吃我",意思完全变了。这种语言的表达方式对顺序和词语的关联有着极高的敏感性。而对于股票和天气数据,先后顺序也具有重要意义,因为后续的数据可能受到之前数据的影响,如股票市场的走势或天气的变化趋势。

在文本理解方面,顺序也至关重要。例如,对于经典的古诗"床前明月光,疑是地上霜。举头望明月,低头思故乡。",如果改变了诗句的顺序,那么整首诗的意境和情感也将随之改变,这就是所谓的"蝴蝶效应"。一个微小的改变可能会导致整体结果的巨大变化。

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

2、RNN原理

2.1 概述

循环神经网络(Recurrent Neural Network,RNN)是一种神经网络结构,专门用于处理序列数据。与传统的前馈神经网络不同,RNN 在内部具有反馈连接,允许信息在网络内部传递。这种结构使得 RNN 能够对序列数据的历史信息进行建模,并在一定程度上具有记忆能力。

在自然语言处理领域,RNN 被广泛应用于语言建模、机器翻译、情感分析等任务。通过捕捉单词之间的上下文信息,RNN 能够更好地理解语言的含义和结构。

同时,RNN 也在时间序列预测领域发挥着重要作用,比如股票价格预测、天气预测等。通过学习序列数据的模式和趋势,RNN 能够提供有用的预测信息,帮助人们做出决策。

然而,传统的 RNN 存在一些问题,例如难以处理长期依赖关系、梯度消失或梯度爆炸等。为了解决这些问题,出现了一些改进的 RNN 变种,如长短期记忆网络(LSTM)和门控循环单元(GRU)。这些变种结构能够更有效地捕捉长期依赖关系,并且在训练过程中更加稳定。

2.2 模型架构

循环神经网络(RNN)是深度学习中的一种架构,专门设计来处理序列数据,例如时间序列数据或自然语言文本。RNN的核心特征在于它能够在处理序列的每个元素时保留一个内部状态(记忆),这个内部状态能够捕捉到之前元素的信息。这种设计使得RNN特别适合处理那些当前输出依赖于之前信息的任务。

常见的RNN架构如下图两种:

左图可以理解为,先将每一层的网络简化,再将网络旋转90度得到的简化图

而右边两种类型,可以理解为,再将左图继续进行简化

在RNN的经典架构中,网络通过一个特殊的循环结构将信息从一个处理步骤传递到下一个。这个循环结构通常被称为“隐藏层状态”或简单地称为“隐藏状态”。隐藏状态是RNN的记忆部分,它能够捕获并存储关于已处理序列元素的信息。

当RNN处理一个序列时,它会在每个时间步接受一个输入,并更新其隐藏状态。这个更新过程依赖于当前的输入和之前的隐藏状态,从而使得网络能够“记住”并利用过去的信息。这个过程可以通过以下数学公式简化表达:

在这个公式中,St表示在时间步t的隐藏状态,xt是当前时间步的输入,UW分别是输入到隐藏状态和隐藏状态到隐藏状态的权重矩阵。函数f通常是一个非线性函数,如tanh或ReLU,用于引入非线性特性并帮助网络学习复杂的模式。

RNN的输出在每个时间步也可以计算出来,这依赖于当前的隐藏状态:

其中Ot是时间步t的输出,V是从隐藏状态到输出层的权重矩阵,g是另一个非线性函数,常用于输出层。

RNN也是传统的神经网络架构,但是他里面包含了一个“盒子”,这个盒子里记录了输入时网络的状态,在下一次输入时,必须要考虑“盒子”。随着不断的输入,盒子里也会更新,那么这个盒子就是“隐藏态”

假设为一个包含三个单词的句子,将模型展开,即为一个三层的网络结构

可以理解为,xt-1为第一个词,xt为第二个词,xt+1为第三个词

图中参数含义:

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

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

    • Ht根据当前输入层的输出与上一时刻隐藏层的状态Ht-1进行计算,如下所示。

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

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

  • 带入可以得到

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

2.3 RNN的内部结构

2.4 RNN模型输入输出关系对应模式

通过改变RNN的结构,即调整其输入和输出的数量和形式,可以让它适应各种不同的任务。以下是几种常见的RNN结构调整示例,以及它们各自适用的任务类型:

  1. 一对多(One-to-Many):这种结构的RNN接受单个输入并产生一系列输出。这种模式常用于“看图说话”的任务,即给定一张图片(单个输入),RNN生成一段描述该图片的文本(一系列输出)。在这种情况下,RNN的结构被调整为首先对输入图片进行编码,然后根据这个编码连续生成文本序列中的词语。

  2. 多对一(Many-to-One):与一对多相反,多对一的RNN结构接受一系列输入并产生单个输出。这种结构适用于如文本分类和情感分析等任务,其中模型需要阅读和理解整个文本(一系列输入),然后决定文本属于哪个类别(单个输出)。在图片生成的上下文中,这种结构可以通过分析一系列的特征或指令来生成单个图片输出。

  3. 多对多(Many-to-Many):这种结构的RNN既接受一系列输入,也产生一系列输出。这在需要输入和输出均为序列的任务中非常有用,例如机器翻译,其中模型需要读取一个语言的文本(一系列输入),然后生成另一种语言的对应文本(一系列输出)。另一个例子是小说生成,其中RNN可以基于给定的开头或主题(一系列输入),连续生成故事的后续内容(一系列输出)。

3、RNN代码实现

重要参数含义

  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)的任务,比如机器翻译,最后的时间步的隐藏状态通常会通过一个线性层映射到目标词汇表大小,这样输出大小就会是目标词汇表的大小。

具体的单层单向RNN示例来说明维度变换过程:

假设正在处理一个文本分类任务,每个单词已经被嵌入为一个100维的向量,我们的序列长度(sequence length)是50(即最长句子有50个单词),批量大小(batch size)是32(一次处理32个句子),我们设定的隐藏层大小(hidden size)是128。

  1. 输入维度(input size): 每个时间步(每个单词)的输入向量维度是100,所以整个输入张量的维度是 (batch size, sequence length, input size),即 (32, 50, 100)

  2. 隐藏层计算: RNN会对每个时间步的输入进行处理,并基于上一时间步的隐藏状态生成当前时间步的隐藏状态。隐藏状态的维度由我们设定,这里是128维,所以每个时间步的隐藏状态和输出的维度都是 (batch size, hidden size),即 (32, 128)

  3. 输出维度(output size): 因为这里我们假设没有在RNN后添加额外的层(例如分类层),所以每个时间步的输出大小就等于隐藏层大小,也就是128维。但是,由于输出是针对每一个时间步的,所以整个输出序列的维度为 (batch size, sequence length, hidden size),即 (32, 50, 128)

如果后续需要进行分类,比如这是一个二分类问题,我们会把最后一个时间步的隐藏状态(128维)通过一个全连接层(Dense Layer)映射到类别数目的维度,如2维,此时输出大小将变为 (32, 2),表示32个样本的二维概率分布。

3.1 原生代码

import numpy as np# 假设输入数据,3个时间步,每个时间步2个特征
X = np.random.rand(3, 2)
# print(X)# 定义RNN的参数
input_size = 2  # 输入特征维度
hidden_size = 3  # 隐藏层大小
output_size = 4  # 输出层大小# 初始化权重和偏置(为了简单起见,这里使用小的随机值)
Wxh = np.random.randn(input_size, hidden_size)  # 输入到隐藏层的权重矩阵,形状应该是 (input_size, hidden_size)
Whh = np.random.randn(hidden_size, hidden_size)  # 隐藏层到隐藏层的权重矩阵
Why = np.random.randn(hidden_size, output_size)  # 隐藏层到输出层的权重矩阵bh = np.zeros((hidden_size,))  # 隐藏层的偏置
by = np.zeros((output_size,))  # 输出层的偏置# 激活函数
def tanh(x):return np.tanh(x)# 初始化隐藏状态
H_prev = np.zeros((hidden_size,))
# print(H_prev.shape)# 进行前向传播
# 时间步1
X1 = X[0, :]  # 取时间步1的输入特征
# np.dot() 是一个强大的函数,可以用于多种类型的点积和矩阵乘法运算,根据输入数组的维度自动选择合适的计算方式。
H1 = tanh(np.dot(X1, Wxh) + np.dot(H_prev, Whh) + bh)  # 在时间步1,没有上一个隐藏状态,所以只使用H_prev
O1 = np.dot(H1, Why) + by  # 计算输出# 时间步2
X2 = X[1, :]  # 取时间步2的输入特征
H2_input = np.dot(X2, Wxh) + np.dot(H1, Whh) + bh  # 组合当前输入和上一个隐藏状态
H2 = tanh(H2_input)  # 计算隐藏状态
O2 = np.dot(H2, Why) + by  # 计算输出# 时间步3
X3 = X[2, :]  # 取时间步3的输入特征
H3_input = np.dot(X3, Wxh) + np.dot(H2, Whh) + bh  # 组合当前输入和上一个隐藏状态
H3 = tanh(H3_input)  # 计算隐藏状态
O3 = np.dot(H3, Why) + by  # 计算输出# 输出结果
print("时间步1的隐藏状态H1:", H1)
print("时间步1的输出O1:", O1)
print("时间步2的隐藏状态H2:", H2)
print("时间步2的输出O2:", O2)
print("时间步3的隐藏状态H3:", H3)
print("时间步3的输出O3:", O3)

注释说明:

H1 = tanh(np.dot(X1, Wxh) + H_prev)

输入特征(X1):取时间步1的输入特征,通常是一个向量,形状为(输入维度,)

矩阵乘法(np.dot)

  • np.dot(X1, Wxh):这里将输入特征X1(形状为(输入维度,))与权重矩阵Wxh(形状为(输入维度, 隐藏层维度))进行点积,结果是一个隐藏层状态的线性组合,形状为(隐藏层维度,)

  • H_prev在时间步1的值通常为零(或初始化为零),所以np.dot(X1, Wxh) + H_prev仍是一个形状为(隐藏层维度,)的向量。

假设有以下示例:

激活函数(tanh)H1 = tanh(...)应用非线性激活函数,将线性组合转换为隐藏层的状态,形状保持为(隐藏层维度,)

输出计算(O1)

  • np.dot(H1, Why):将隐藏层状态H1(形状为(隐藏层维度,))与输出权重矩阵Why(形状为(隐藏层维度, 输出维度))进行点积,结果为一个输出向量,形状为(输出维度,)

  • 最后,加上偏置by,得到最终输出O1

3.2 基于RNNCell代码实现

import torch
import torch.nn as nnx_input = torch.randn(2, 3, 10)  # 创建一个形状为 (2, 3, 10) 的随机张量,2是批量大小,3是序列长度,10是特征维度# 定义一个继承自nn.Module的RNN类
class RNN(nn.Module):def __init__(self, input_size, hidden_size, batch_first=False):super(RNN, self).__init__()self.rnn_cell = nn.RNNCell(input_size, hidden_size)  # 初始化RNNCell,输入大小为input_size,隐藏层大小为hidden_sizeself.batch_first = batch_first  # 标识输入张量的第一个维度是否为批次大小self.hidden_size = hidden_size  # 保存隐藏层大小def _initialize_hidden(self, batch_size):# 初始化隐藏状态,形状为 (batch_size, hidden_size),全为零return torch.zeros((batch_size, self.hidden_size))def forward(self, inputs, initial_hidden=None):# 如果 batch_first 为 True,那么 inputs 的维度是 (batch_size, seq_size, feat_size)if self.batch_first:batch_size, seq_size, feat_size = inputs.size()  # 获取输入张量的尺寸# permute 函数重新排列张量维度,将 (batch_size, seq_size, feat_size) 变为 (seq_size, batch_size, feat_size)inputs = inputs.permute(1, 0, 2)else:# 如果 batch_first 为 False,那么 inputs 的维度为 (seq_size, batch_size, feat_size)seq_size, batch_size, feat_size = inputs.size()hiddens = []  # 用于存储每个时间步的隐藏状态# 如果没有提供初始隐藏状态,则初始化一个全零的隐藏状态if initial_hidden is None:initial_hidden = self._initialize_hidden(batch_size)  # 初始化隐藏状态initial_hidden = initial_hidden.to(inputs.device)  # 将隐藏状态移动到与输入张量相同的设备上hidden_t = initial_hidden  # 设置初始隐藏状态# 循环遍历每个时间步for t in range(seq_size):# 在第t个时间步更新隐藏状态hidden_t = self.rnn_cell(inputs[t], hidden_t)# 将该时间步的隐藏状态添加到hiddens列表中hiddens.append(hidden_t)# 将所有时间步的隐藏状态堆叠成一个新的张量,增加一个维度hiddens = torch.stack(hiddens)# 如果 batch_first 为 True,则重新排列维度,将 (seq_size, batch_size, hidden_size) 变为 (batch_size, seq_size, hidden_size)if self.batch_first:hiddens = hiddens.permute(1, 0, 2)print(hiddens)  # 打印隐藏状态张量return hiddens  # 返回隐藏状态张量model = RNN(10, 15, batch_first=True)  # 创建 RNN 模型,输入维度为10,隐藏层大小为15,batch_first为Trueoutputs = model(x_input)  # 将输入张量传入模型,获取输出
print(outputs.shape)  # 打印输出张量的形状

nn.RNNCell 本质上只返回隐藏状态,它没有单独的输出结果。一般在 RNN 中,隐藏状态既可以被视为输出,也可以通过一个线性层将隐藏状态转化为实际的输出。

3.3 基于pytorch API代码实现

官方代码细节:https://pytorch.org/docs/stable/_modules/torch/nn/modules/rnn.html#RNN

官方文档解释:RNN — PyTorch 2.8 documentation

import torch
import torch.nn as nn# 设置超参数
bs, T = 2, 3  # 批大小,输入序列长度
input_size, hidden_size = 2, 3  # 输入特征大小,隐含层特征大小# 初始化随机输入特征序列
input = torch.randn(bs, T, input_size)# 初始化初始隐含状态,全零向量
h_prev = torch.zeros(bs, hidden_size)# 创建一个RNN实例,设置batch_first=True意味着输入数据的第一维是batch_size
rnn = nn.RNN(input_size, hidden_size, batch_first=True)# 使用RNN进行前向传播
rnn_output, state_final = rnn(input, h_prev.unsqueeze(0))# 输出RNN的输出结果及最终隐含状态
print(rnn_output)
print(state_final)
  • input 是一个形状为 (batch_size, sequence_length, input_size) 的张量,表示一批包含 T 个时间步长的序列,每个时间步长的输入特征维度为 input_size

  • h_prev 是所有序列共享的初始隐含状态,形状为 (batch_size, hidden_size)

  • h_prev.unsqueeze(0)h_prev 的批量维度增加一层,因为PyTorch RNN期望隐含状态作为一个元组 (num_layers, batch_size, hidden_size),在这里我们只有一个隐藏层,所以增加了一维使得形状变为 (1, batch_size, hidden_size)

  • rnn(input, h_prev.unsqueeze(0)) 执行RNN的前向传播,得到的 rnn_output 是整个序列的输出结果,形状为 (batch_size, sequence_length, hidden_size),而 state_final 是最后一个时间步的隐含状态,形状为 (num_layers, batch_size, hidden_size)

  • 两个返回值 rnn_outputstate_final 代表着循环神经网络在当前时间步隐藏状态的输出和最终的隐藏状态。

    • rnn_output:代表所有时间步隐藏状态的输出。也就是指{H}_{1}、{H}_{2}、{H}_{3}……{H}_{n}的集合,形状为(batch_size, sequence_length, hidden_size)

    • state_final:代表 RNN 模型在最后一个时间步的隐藏状态。这个隐藏状态通常被认为是对整个序列的编码或总结,它可能会被用于某些任务的最终预测或输出。形状为 (1,batch_size, hidden_size)

多对多的任务

import torch
import torch.nn as nn# 设置超参数
batch_size, seq_len, input_size = 10, 6, 5  # Input size 词向量大小
hidden_size = 3  # 隐藏层大小
num_classes = 18  # 输出类别数# 数据输入
x = torch.randn(batch_size, seq_len, input_size)# 初始化隐藏状态,全零向量
h_prev = torch.zeros(batch_size, hidden_size)# 创建一个RNN实例
rnn = nn.RNN(input_size, hidden_size, batch_first=True)# 创建线性层,将隐藏状态映射到18个类别
linear = nn.Linear(hidden_size, num_classes)# RNN的输出
output, state_final = rnn(x, h_prev.unsqueeze(0))# 将RNN的输出经过线性层,映射到18个类别
# output: [batch_size, seq_len, hidden_size]
output = linear(output)# output: [batch_size, seq_len, num_classes]
print(output.shape)
3.3.1 单向、单层RNN
import torch
import torch.nn as nnsignle_rnn = nn.RNN(4,3,1,batch_first=True)
input = torch.randn(1,2,4) # bs*sl*fs
output, h_n = signle_rnn(input)

1. 定义一个单层循环神经网络(RNN)实例:

signle_rnn = nn.RNN(4, 3, 1, batch_first=True)

 这行代码创建了一个RNN层,其参数含义如下:

   - `4` 表示输入序列的特征维度(feature size),即每个时间步的输入向量长度为4。
   - `3` 表示隐藏状态(hidden state)的维度,即RNN单元内部记忆的向量长度为3。
   - `1` 表示RNN层的数量,这里仅为单层。
   - `batch_first=True` 指定输入张量的第一个维度代表批次(batch),第二个维度代表时间步(sequence length),这对于处理批次数据时更容易理解。

2. 创建输入数据张量:

input = torch.randn(1, 2, 4)

这行代码生成了一个随机张量作为RNN的输入,它的形状为 (batch_size, sequence_length, feature_size),具体到这里的值是:

  • 1 表示批大小(batch size),即本次输入的数据样本数量。
  • 2 表示序列长度(sequence length),即每个样本的输入序列包含两个时间步。
  • 4 是每个时间步输入向量的特征维度,与RNN层设置一致。对输入数据进行前向传播:

3. 对输入数据进行前向传播:

output, h_n = signle_rnn(input)

这行代码将之前创建的随机输入数据送入RNN层进行前向计算。执行后得到两个输出:

  • output 是经过RNN处理后的输出序列,其形状通常为 (batch_size, sequence_length, num_directions * hidden_size)。在这个例子中,因为没有指定双向RNN,所以 num_directions=1。因此,output 的尺寸将是 (1, 2, 3),对应每个批次中的每个时间步输出一个维度为3的向量。

  • h_n 是最后一个时间步的隐藏状态(hidden state),它通常是最终时间步的隐藏状态或者是所有时间步隐藏状态的某种聚合(取决于RNN类型)。在这里,h_n 的形状是 (num_layers * num_directions, batch_size, hidden_size),但由于只有一层并且是无方向的RNN,所以形状会简化为 (1, 1, 3),即单一隐藏状态向量。这个隐藏状态可以用于下个时间步的预测或者作为整个序列的编码。

3.3.2 双向、单层RNN

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

主要特点

  1. 双向处理: 最显著的特点是双向结构,使得模型能够同时学习到序列中某一点前后的上下文信息,这对于很多序列任务来说是非常有价值的,比如自然语言处理中的文本理解、语音识别等。

  2. 单层结构: “单层”指的是在每个方向上,网络结构只有一层RNN,即每个方向上只有一层循环单元(如LSTM单元或GRU单元)。虽然是单层的,但由于其双向特性,实际上每个时间点都有两个循环单元对信息进行处理。

import torch
import torch.nn as nnbi_rnn = nn.RNN(4,3,1,batch_first=True,bidirectional=True)
input = torch.randn(1,2,4)
output, h_n = bi_rnn(input)

1. 定义一个双向循环神经网络(Bi-RNN)实例:

bi_rnn = nn.RNN(4, 3, 1, batch_first=True, bidirectional=True)

这行代码创建了一个具有双向连接的RNN层,参数含义如下:

  • 4 依然是输入序列的特征维度(每个时间步长的输入向量有4个元素)。

  • 3 表示的是单向隐藏状态(hidden state)的维度;由于设置了 bidirectional=True,实际上模型会同时维护正向和反向两个隐藏状态,因此总的隐藏状态维度将是 2 * 3

  • 1 表示RNN层的数量,这里也是单层。

  • batch_first=True 保持输入张量的批量维度在最前面。

  • bidirectional=True 指定该RNN为双向的,这意味着对于每个时间步,除了向前传递的信息外,还会考虑向后传递的信息,从而能够捕捉序列中前后依赖关系。

2. 创建输入数据张量:

input = torch.randn(1, 2, 4)

这行代码生成了一个随机张量作为双向RNN的输入,其形状仍为 (batch_size, sequence_length, feature_size),即 (1, 2, 4)。这表示有一个样本(batch_size=1),序列长度为2,每个时间步有4个特征。

3. 对输入数据进行前向传播:

output, h_n = bi_rnn(input)

将随机输入数据传入双向RNN进行前向计算。执行后获取的结果与单向RNN有所不同:

  • output 现在包含了正向和反向两个方向的输出,其形状为 (batch_size, sequence_length, num_directions * hidden_size),在本例中为 (1, 2, 2 * 3),即每个时间步有两个方向上的隐藏状态输出拼接而成的向量

  • h_n 包含了最后时间步的正向和反向隐藏状态,形状为 (num_layers * num_directions, batch_size, hidden_size),在本例中实际为 (2, 1, 3)分别对应正向和反向隐藏状态各一个。每个隐藏状态向量都是相应方向上整个序列信息的汇总。

4、RNN的训练方法——BPTT

BPTT(back-propagation through time)算法是常用的训练RNN的方法,其实本质还是BP算法,只不过RNN处理时间序列数据,所以要基于时间反向传播,故叫随时间反向传播。BPTT的中心思想和BP算法相同,沿着需要优化的参数的负梯度方向不断寻找更优的点直至收敛。综上所述,BPTT算法本质还是BP算法,BP算法本质还是梯度下降法,那么求各个参数的梯度便成了此算法的核心。

其中L是损失函数,对于多分类问题,我们使用的是多元交叉熵损失函数,也称为分类交叉熵

这个式子看起来简单但是求解起来很容易出错,因为其中嵌套着激活函数函数,是复合函数的求道过程。

RNN的损失也是会随着时间累加的,所以不能只求t时刻的偏导。

W和U的偏导的求解由于需要涉及到历史数据,其偏导求起来相对复杂,我们先假设只有三个时刻,那么在第三个时刻 L对W的偏导数为:

最终的梯度就是这些路径上的梯度和,它们构成了公式中的三个部分。每一部分都反映了梯度在不同路径上的传播和累计。

相应的,L在第三个时刻对U的偏导数为:

可以观察到,在某个时刻的对W或是U的偏导数,需要追溯这个时刻之前所有时刻的信息,这还仅仅是一个时刻的偏导数,上面说过损失也是会累加的,那么整个损失函数对W和U的偏导数将会非常繁琐。虽然如此但好在规律还是有迹可循,我们根据上面两个式子可以写出L在t时刻对W和U偏导数的通式:

整体的偏导公式就是将其按时刻再一一加起来。

5、RNN模型存在的问题

5.1 RNN中的梯度消失和爆炸

前面说过激活函数是嵌套在里面的,如果我们把激活函数放进去,拿出中间累乘的那部分:

我们会发现累乘会导致激活函数导数的累乘,进而会导致“梯度消失“和“梯度爆炸“现象的发生。

至于为什么,我们先来看看sigmoid函数的函数图和导数图这是和tanh函数的函数图和导数图。

它们二者是何其的相似,都把输出压缩在了一个范围之内。他们的导数图像也非常相近,我们可以从中观察到,sigmoid函数的导数范围是(0,0.25],tanh函数的导数范围是(0,1],他们的导数最大都不大于1。

这就会导致一个问题,在上面式子累乘的过程中,如果取sigmoid函数作为激活函数的话,那么必然是一堆小数在做乘法,结果就是越乘越小。随**着时间序列的不断深入,小数的累乘就会导致梯度越来越小直到接近于0,这就是“梯度消失“现象**。其实RNN的时间序列与深层神经网络很像,在较为深层的神经网络中使用sigmoid函数做激活函数也会导致反向传播时梯度消失,梯度消失就意味消失那一层的参数再也不更新,那么那一层隐层就变成了单纯的映射层,毫无意义了,所以在深层神经网络中,有时候多加神经元数量可能会比多家深度好。

梯度爆炸(每天进一步一点点,N天后,你就会腾飞 每天堕落一点点,N天后,你就彻底完蛋)

你可能会提出异议,RNN明明与深层神经网络不同,RNN的参数都是共享的,而且某时刻的梯度是此时刻和之前时刻的累加,即使传不到最深处那浅层也是有梯度的。这当然是对的,但如果我们根据有限层的梯度来更新更多层的共享的参数一定会出现问题的,因为将有限的信息来作为寻优根据必定不会找到所有信息的最优解。

之前说过我们多用tanh函数作为激活函数,那tanh函数的导数最大也才1啊,而且又不可能所有值都取到1,那相当于还是一堆小数在累乘,还是会出现“梯度消失“,那为什么还要用它做激活函数呢?原因是tanh函数相对于sigmoid函数来说梯度较大,收敛速度更快且引起梯度消失更慢。

还有一个原因是sigmoid函数还有一个缺点,Sigmoid函数输出不是零中心对称。sigmoid的输出均大于0,这就使得输出不是0均值,称为偏移现象,这将导致后一层的神经元将上一层输出的非0均值的信号作为输入。关于原点对称的输入和中心对称的输出,网络会收敛地更好。

RNN的特点本来就是能“追根溯源“利用历史数据,现在告诉我可利用的历史数据竟然是有限的,这就令人非常难受,解决“梯度消失“是非常必要的。解决“梯度消失“的方法主要有: 1、选取更好的激活函数 2、改变传播结构

关于第一点,一般选用ReLU函数作为激活函数,ReLU函数的图像为:

ReLU函数的左侧导数为0,右侧导数恒为1,这就避免了“梯度消失“的发生。但恒为1的导数容易导致“梯度爆炸“,但设定合适的阈值可以解决这个问题。还有一点就是如果左侧横为0的导数有可能导致把神经元学死,不过设置合适的步长(学习率)也可以有效避免这个问题的发生。

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

总结一下,sigmoid函数的缺点: 1、导数值范围为(0,0.25],反向传播时会导致“梯度消失“。tanh函数导数值范围更大,相对好一点。 2、sigmoid函数不是0中心对称,tanh函数是,可以使网络收敛的更好。

5.2 远距离依赖

循环神经网络(RNN)是自然语言处理和其他序列数据任务中广泛使用的一种神经网络架构,它通过在网络中引入循环来处理序列数据,使得网络能够保持一定程度的序列信息。RNN的设计让它在处理如文本和语音等顺序数据时表现出色,因为它能够在每个时间步上接收输入,并保持一个内部状态,该状态包含了之前时间步的信息。

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

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

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

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

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

二、LSTM模型

1、LSTM概述

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

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

所有循环神经网络都具有神经网络重复模块链的形式。在标准 RNN 中,这个重复模块将具有非常简单的结构,例如单个 tanh 层。

LSTM 也具有这种链式结构,但重复模块具有不同的结构。神经网络层不是单一的,而是四个(黄色矩形,四个激活函数,三个sigmod、一个tanh),以非常特殊的方式相互作用。

LSTM每个循环的模块内又有4层结构:3个sigmoid层,2个tanh层

基本状态

下图就是描述的关键部分:细胞状态C cell state

LSTM的关键是细胞状态CC,一条水平线贯穿于图形的上方,这条线上只有些少量的线性操作,信息在上面流传很容易保持。

细胞状态是LSTM的中心概念,它可以被视为一条信息的高速公路,沿着序列传递信息。细胞状态的设计使得信息可以几乎不受阻碍地在序列间流动,这解决了传统RNN中梯度消失问题导致的信息传递障碍。

LSTM有通过精心设计的称作“门”的结构来去除或者增加信息到细胞状态的能力。 门是一种让信息选择式通过的方法。他们包含一个sigmoid神经网络层和一个pointwise乘法操作。

Sigmoid层输出0到1之间的数值,描述每个部分有多少量可以通过。
0代表“不许任何量通过”
1代表“允许任何量通过”
LSTM 拥有**三个门**,来保护和控制细胞状态。

2、门控机制

2.1 遗忘门

遗忘门(Forget Gate):决定细胞状态中要保留的信息。它通过一个sigmoid函数来输出一个0到1之间的值,表示要忘记(0)或保留(1)的程度。1 代表“完全保留这个”,而 0 代表“完全摆脱这个”。

这个函数是遗忘门(forget gate)的公式,用于确定哪些信息应当从单元的状态中移除

  • [ ht-1, xt]:这是一个连接的向量,包括前一时间步的隐藏状态 ht-1和当前时间步的输入 xt。它们被合并起来,以便遗忘门可以考虑当前的输入和先前的隐藏状态来做出决策。

  • Wf:这是遗忘门的权重矩阵,用于从输入[ ht-1, xt]中学习什么信息应该被遗忘。

  • bf:这是遗忘门的偏置项,它被加到权重矩阵和输入向量的乘积上,可以提供额外的调整能力,确保即使在没有输入的情况下遗忘门也能有一个默认的行为。

  • :这是sigmoid激活函数,它将输入压缩到0和1之间。在这里,它确保遗忘门的输出也在这个范围内,表示每个状态单元被遗忘的比例。

  • ft :这是在时间步 ( t ) 的遗忘门的输出,它是一个向量,其中的每个元素都在0和1之间,对应于细胞状态中每个元素应该被保留的比例。

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

2.2 输入门

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

  1. 输入门的激活 (it ):

2. 候选细胞状态 ( Ct ):

解释如下:

  • it 表示时间步 ( t ) 的输入门激活值,是一个向量。这个向量通过sigmoid函数

    产生,将值限定在 0 和 1 之间。它决定了多少新信息会被加入到细胞状态中。

  • Wi 是输入门的权重矩阵,用于当前时间步的输入 xt 和前一个时间步的隐藏状态 ht-1。

  • [ ht-1, xt] 是前一个隐藏状态和当前输入的串联。

  • bi 是输入门的偏置向量。

  • Ct 是候选细胞状态,它是通过tanh函数产生的,可以将值限定在 -1 和 1 之间。它与输入门 it 相乘,决定了将多少新的信息添加到细胞状态中。

  • WC 是控制候选细胞状态的权重矩阵。

  • bC 是对应的偏置向量。

2.3 状态更新

在每个时间步,LSTM单元都会计算这两个值,并结合遗忘门f_t的值更新细胞状态C_t。这样,LSTM能够记住长期的信息,并在需要的时候忘记无关的信息。

在计算新的细胞状态 ( Ct ) 时使用的更新规则:

这里:

  • Ct 是当前时间步的细胞状态。

  • Ct-1 是上一个时间步的细胞状态。

  • ft 是遗忘门的激活值,通过sigmoid函数计算得到。它决定了多少之前的细胞状态应该被保留。

  • it 是输入门的激活值,也是通过sigmoid函数得到的。它决定了多少新的信息应该被存储在细胞状态中。

  • Ct 是当前时间步的候选细胞状态,通过tanh函数得到。它包含了潜在的新信息,可以被添加到细胞状态中。

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

2.4 输出门

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

这个函数描述了LSTM(长短期记忆)网络的输出门和隐藏状态的计算。

  1. 输出门 ot 的计算:

2. 隐藏状态 ht 的计算:

具体来说:

  • ot 是输出门的激活值。这是通过将前一时间步的隐藏状态 ht-1 和当前时间步的输入 xt 连接起来,并应用权重矩阵Wo以及偏置项 bo,然后通过sigmoid函数来计算的。Sigmoid函数确保输出值在0和1之间。

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

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

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

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

2.5 LSTM总结

3、代码实现

注意:

矩阵乘法规则:矩阵 A 和向量 v 做点积运算时,A 的列数必须与 v 的长度一致。

3.1 原生代码

多对一的任务
import numpy as np
import torchclass CustomLSTM:def __init__(self, input_size, hidden_size, output_size):self.input_size = input_size  # 输入特征维度self.hidden_size = hidden_size  # 隐藏层维度# 权重和偏置初始化self.W_f = np.random.randn(hidden_size, hidden_size + input_size)self.b_f = np.random.randn(hidden_size)self.W_i = np.random.randn(hidden_size, hidden_size + input_size)self.b_i = np.random.randn(hidden_size)self.W_c = np.random.randn(hidden_size, hidden_size + input_size)self.b_c = np.random.randn(hidden_size)self.W_o = np.random.randn(hidden_size, hidden_size + input_size)self.b_o = np.random.randn(hidden_size)self.W_y = np.random.randn(output_size, hidden_size)self.b_y = np.random.randn(output_size)def sigmoid(self, x):return 1 / (1 + np.exp(-x))def tanh(self, x):return np.tanh(x)def forward(self, X):h_t = np.zeros((self.hidden_size,))  # 初始隐藏状态c_t = np.zeros((self.hidden_size,))  # 初始细胞状态h_states = []  # 存储每个时间步的隐藏状态c_states = []  # 存储每个时间步的细胞状态for t in range(X.shape[0]):x_t = X[t]  # 当前时间步的输入# 将 h_t 和 x_t 拼接 垂直方向combined = np.concatenate((h_t, x_t))# 遗忘门f_t = self.sigmoid(np.dot(self.W_f, combined) + self.b_f)# 输入门i_t = self.sigmoid(np.dot(self.W_i, combined) + self.b_i)c_hat_t = self.tanh(np.dot(self.W_c, combined) + self.b_c)# 更新细胞状态c_t = f_t * c_t + i_t * c_hat_t# 输出门o_t = self.sigmoid(np.dot(self.W_o, combined) + self.b_o)# 更新隐藏状态h_t = o_t * self.tanh(c_t)# 保存每个时间步的隐藏状态和细胞状态h_states.append(h_t)c_states.append(c_t)# 输出层,分类类别y_t = np.dot(self.W_y, h_t) + self.b_y# 计算 Softmax torch.from_numpy()将一个 NumPy 数组 转换为 PyTorch 张量(Tensor) dim=0 是行维度output = torch.softmax(torch.from_numpy(y_t), dim=0)# 列表转数组return np.array(h_states), np.array(c_states), output# 假设输入数据,3个时间步,每个时间步2个特征
X = np.random.rand(3, 2)
# 假设LSTM的隐藏单元数量为5,分类类别为4
lstm = CustomLSTM(input_size=2, hidden_size=5, output_size=4)
# 前向传播
hidden_states, cell_states, output = lstm.forward(X)print("每个时间步的隐藏状态:")
print(hidden_states)
print("\n每个时间步的细胞状态:")
print(cell_states)
print("\n分类输出:")
print(output)

注释

举例说明形状为 (5, 7) 的矩阵和形状为 (7,) 的向量是如何通过 np.dot() 相乘得到结果的。

matrix = np.array([
    [1, 2, 3, 4, 5, 6, 7],  # 第一行
    [7, 6, 5, 4, 3, 2, 1],  # 第二行
    [1, 3, 5, 7, 9, 11, 13],# 第三行
    [2, 4, 6, 8, 10, 12, 14],# 第四行
    [3, 5, 7, 9, 11, 13, 15] # 第五行
])

vector = np.array([1, 2, 3, 4, 5, 6, 7])

最终结果:

所有行与向量的点积结果形状为 (5, ) :

[140,84,266,308,350]

多对多
 outputs = []   # 存储每个时间步的输出# 输出层,计算每个时间步的输出y_t = np.dot(self.W_y, h_t) + self.b_youtput = torch.softmax(torch.from_numpy(y_t), dim=0)  # 计算 Softmaxoutputs.append(output.numpy())  # 保存输出return np.array(h_states), np.array(c_states), np.array(outputs)
一对多

在一对多的任务中,尽管输入序列的大小只有一个,但模型仍然可以生成多个输出。这通常涉及以下几个方面:

1. 输入与时间步

  • 一对多任务通常指的是在某个时间点有一个输入,但模型在接下来的多个时间步中生成多个输出。例如,在图像生成或文本生成任务中,你可能在每次预测时只输入一个数据点(如一个单词或图像),然后模型根据这个输入生成多个输出。

2. 时间步的生成

  • 对于输入序列只有一个时间步的情况,模型可以通过在其内部实现循环来生成多个时间步的输出。通常在 LSTM 中,这通过反馈机制实现:

    • 在每次生成输出时,模型可以使用先前生成的输出作为下一步的输入。

3. 示例:文本生成

例如,在文本生成中,你可能会输入一个起始单词(例如 "Once"),然后 LSTM 根据这个单词生成接下来的多个单词:

  • 输入:["Once"]

  • 输出:["Once", "upon", "a", "time", "there", "was"]

在这种情况下,尽管输入只有一个时间步,但后续的多个输出是根据模型的状态和历史信息生成的。

4. 实现方式

在实现时,可以设置一个循环,在每个时间步中,模型接收前一个时间步的输出作为当前时间步的输入。这样,即使起始时只有一个输入,模型也可以生成多个输出。

output_sequence = []  hidden_state, cell_state, output = lstm.forward(input_char)output_sequence.append(np.argmax(output,axis=1))  # 选择概率最大的字符

3.2 基于pytorch API代码实现

在 LSTM 网络中,初始化隐藏状态 (h0) 和细胞状态 (c0) 是一个重要的步骤,确保模型在处理序列数据时有一个合理的起始状态。

h0 = torch.zeros(1, x.size(1), self.hidden_size)
c0 = torch.zeros(1, x.size(1), self.hidden_size)

1:指的是 LSTM 的层数。如果 num_layers > 1,那么这里应该是 num_layers

x.size(1):表示批次的大小 (batch_size)。这是输入 x 的第二个维度,因为 x 的形状为 (seq_len, batch_size, input_size)

self.hidden_size:表示 LSTM 隐藏层的单元数,即隐藏状态和细胞状态的维度。

在 PyTorch 中,使用 LSTM (长短期记忆网络) 进行序列数据的处理时,调用 self.lstm(x, (h0, c0)) 会返回两个值:out(hn, cn)

out:

  • out 是 LSTM 网络在所有时间步的输出。

  • 假设输入 x 的形状是 (seq_len, batch_size, input_size),那么 out 的形状将是 (seq_len, batch_size, num_directions * hidden_size),其中 num_directions 是 1 如果 LSTM 是单向的,2 如果是双向的。

  • 具体地,out 包含 LSTM 在每个时间步的输出,适用于后续处理(例如,将其传递给一个全连接层)。

(_)或(hn, cn):

  • hn 是最后一个时间步的隐状态(hidden state)。

  • cn 是最后一个时间步的细胞状态(cell state)。

  • 如果输入 x 的形状是 (seq_len, batch_size, input_size),那么 hncn 的形状将是 (num_layers * num_directions, batch_size, hidden_size)

    单层 LSTM:如果 num_layers = 1,LSTM 网络将只有一个层。这意味着输入序列直接通过这个单层 LSTM 进行处理。

    多层 LSTM:如果 num_layers > 1,LSTM 网络将有多层。输入序列首先通过第一层 LSTM,第一层的输出作为输入传递给第二层,以此类推,直到最后一层。

  • 这些状态可以用于初始化下一个序列的 LSTM,特别是在处理长序列或多个批次的序列数据时。

import torch
import torch.nn as nn# 定义一个简单的LSTM模型
class SimpleLSTM(nn.Module):def __init__(self, input_size, hidden_size, output_size):super(SimpleLSTM, self).__init__()self.hidden_size = hidden_sizeself.lstm = nn.LSTM(input_size, hidden_size)  # LSTM层self.fc = nn.Linear(hidden_size, output_size)  # 全连接层def forward(self, x):# 初始化隐藏状态和细胞状态h0 = torch.zeros(1, x.size(1), self.hidden_size)c0 = torch.zeros(1, x.size(1), self.hidden_size)# 前向传播out, _ = self.lstm(x, (h0, c0))  # _是最后一个时间步的隐藏状态和细胞状态out = self.fc(out[-1, :, :])  # 取最后一个时间步的输出return out# 测试模型
if __name__ == "__main__":# 定义模型参数input_size = 10hidden_size = 20output_size = 2# 创建模型实例lstm_model = SimpleLSTM(input_size, hidden_size, output_size)# 生成随机输入input_data = torch.randn(5, 3, 10)  # 序列长度为5,批量大小为3,输入特征维度为10# 运行模型output = lstm_model(input_data)# 打印输出和形状print("Output:\n", output)print("Output Shape:", output.shape)

4、序列池化

在自然语言处理 (NLP) 中,序列池化(sequence pooling)是一种将变长序列转换为固定长度表示的方法。这个过程对于处理可变长度的输入(如句子或文档)特别有用,因为许多深度学习模型(如全连接层)需要固定长度的输入。

序列池化的主要方法包括:

  1. 最大池化(Max Pooling)

    • 对序列中的每个特征维度,选择该维度的最大值作为输出。

    • 适用于突出序列中特定特征的最大激活值。

    • 例如,如果输入是长度为 5 的序列,且每个时间步的特征维度为 10,最大池化会对每个特征维度取最大值,输出形状为 (batch_size, feature_size)

    • nn.AdaptiveMaxPool1d(1)中的1表示输出的特征长度。自适应最大池化层会根据输入的特征长度自动调整池化的参数,以确保输出的特征长度为1。这意味着,无论输入的特征长度是多少,经过这个层后,输出的特征长度始终会被压缩到1

import torch
import torch.nn as nn# 假设输入数据
x = torch.randn(32, 10, 50)  # [batch_size, seq_len, feature_size]# 定义自适应最大池化层
max_pool = nn.AdaptiveMaxPool1d(1)# 调整形状以匹配池化层的输入要求
x = x.permute(0, 2, 1)  # 从 [batch_size, seq_len, feature_size] 变为 [batch_size, feature_size, seq_len]# 对输入数据进行平均池化
output = max_pool(x)# 去掉多余的维度
# output = output.squeeze(-1)  # [batch_size, feature_size]print(output.shape)  # 输出形状:[32, 50, 1] ->[32,50]
  1. 平均池化(Average Pooling)

  • 对序列中的每个特征维度,计算该维度的平均值作为输出。

  • 适用于希望保留序列中所有特征的总体信息。

  • 同样,对于长度为 5 的序列,特征维度为 10,平均池化会对每个特征维度取平均值,输出形状为 (batch_size, feature_size)

import torch
import torch.nn as nn# 假设输入数据
x = torch.randn(32, 10, 50)  # [batch_size, seq_len, feature_size]# 定义自适应平均池化层
avg_pool = nn.AdaptiveAvgPool1d(1)# 调整形状以匹配池化层的输入要求
x = x.permute(0, 2, 1)  # 从 [batch_size, seq_len, feature_size] 变为 [batch_size, feature_size, seq_len]# 对输入数据进行平均池化
output = avg_pool(x)# 去掉多余的维度
# output = output.squeeze(-1)  # [batch_size, feature_size]print(output.shape)  # 输出形状:[32, 50, 1]

3.注意力池化(Attention Pooling)

  • 使用注意力机制对序列进行加权平均,根据每个时间步的重要性分配权重。

  • 适用于希望模型能够根据输入内容自适应地分配注意力权重。

  • 注意力池化的实现通常涉及一个注意力权重计算模块和一个对这些权重进行加权平均的模块。

5、梯度消失

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

5.1 梯度消失问题

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

  • 长期依赖问题:如果序列特别长,即使是LSTM也可能无法有效地记住早期的信息,因为梯度会在很长的时间步长内持续衰减。

  • 不适当的权重初始化:如果权重初始化不合理,可能会导致LSTM的各个门在初始阶段就偏向于某种状态(如过度遗忘或完全记住),从而影响梯度的有效传播。

  • 激活函数的选择:尽管LSTM通常使用tanh和sigmoid激活函数,这些函数在某些输入值下可能会导致梯度的进一步缩小。

5.2 梯度爆炸问题

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

  • 过长的序列长度:即使是LSTM,在非常长的序列上仍然可能遇到梯度爆炸,因为梯度在反向传播时会不断累积,最终可能变得非常大。

  • 不适当的学习率:过高的学习率可能会导致梯度爆炸,因为参数更新的步伐太大,使得模型参数偏离最优解。

  • 不适当的权重初始化:与梯度消失类似,权重初始化也可能导致梯度爆炸。如果初始权重过大,梯度在反向传播过程中会不断放大。

解决方法

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

  • 梯度裁剪:在每次反向传播后,将梯度裁剪到某个阈值范围内,防止梯度爆炸。

  • 适当的权重初始化:使用标准的初始化方法,如Xavier初始化或He初始化,确保权重在初始阶段不至于过大或过小。

  • 调整学习率:选择合适的学习率,或者使用自适应学习率算法,如Adam、RMSprop等,动态调整学习率。

  • 正则化技术:如L2正则化、Dropout等,防止过拟合并平滑梯度。

  • 批归一化(Batch Normalization):在网络层之间使用批归一化技术,可以加速训练并稳定梯度。

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

http://www.dtcms.com/a/327347.html

相关文章:

  • THCV215一种高速视频数据收发器,采用低电压差分信号(LVDS)技术支持高速串行数据传输,支持1080p/60Hz高分辨率传输
  • 【[特殊字符][特殊字符] 协变与逆变:用“动物收容所”讲清楚 PHP 类型的“灵活继承”】
  • Gradle(二)Gradle的优势、项目结构介绍
  • 电商双11美妆数据分析(一)
  • Honeywell霍尼韦尔A205压力传感器HC41H106P060169419G固瑞克117764美国制造
  • Rust 项目编译故障排查:从 ‘onnxruntime‘ 链接失败到 ‘#![feature]‘ 工具链不兼容错误
  • KAQG:一种用于生成难度可控问题的知识图谱的增强的RAG系统(论文大白话)
  • 2025AI行业升级生态战:谁在“种树”?谁在“造林”?
  • 02-Ansible 基本使用
  • Visual Studio中VC++目录、C/C++和链接器配置的区别与最佳实践
  • Minst手写数字识别
  • python2操作neo4j
  • 非凸科技受邀参加Community Over Code Asia 2025 Rust分论坛
  • 上海AI实验室发布MinerU2:通专融合路线如何补齐AI-Ready数据的最后一公里
  • AutoAgent节点入门:解锁智能体的自主规划能力
  • Myqsl建立库表练习
  • 盲盒抽谷机小程序系统开发:解锁盲盒新玩法,开启潮玩社交新时代
  • 论答题pk小程序软件版权的
  • DeepSeek-R1与RAGflow本地部署全流程指南:从模型下载到个人知识库构建实战
  • 真实案例 | 如何用iFlyCode开发Webpack插件?
  • string 类运算符重载
  • LeetCode Day5 -- 栈、队列、堆
  • JavaScript 实现模块懒加载的几种方式
  • 如何轻松解除Facebook封锁
  • flinksql bug: Received resultset tuples, but no field str
  • 阿里云国际DDoS高防:添加网站配置指南
  • 腾讯codebuddy.ai 安装实测【从零开始开发在线五子棋游戏:完整开发记录】
  • 机械学习--TF-IDF实战--红楼梦数据处理
  • wordpress数据库导入时的#1044错误
  • Linux中使用计划任务和tar命令实现文件备份