欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 美景 > 循环神经网络

循环神经网络

2025/2/23 15:21:48 来源:https://blog.csdn.net/m0_73723097/article/details/145469206  浏览:    关键词:循环神经网络

感知机与神经网络

感知机

生物神经元

感知机的概念 

        感知机(Perceptron),又称神经元(Neuron,对生物神经元进行了模仿)是神经网络(深度学习) 的起源算法,1958年由康奈尔大学心理学教授弗兰克·罗森布拉特(Frank Rosenblatt)提出,它可以 接收多个输入信号,产生一个输出信号

感知机的功能

  • 作为分类器/回归器,实现自我学习

  • 实现逻辑运算,包括逻辑和(AND)、逻辑或(OR)

感知机的缺陷

感知机的局限在于无法处理异或问题(非线性问题)

多层感知机

        多层感知机(Multilayer Perceptron,简称MLP)是一种前馈人工神经网络模型,它至少由三层组成:输入层、至少一个隐藏层输出层。每一层都包含多个神经元,并且除了输入层外,每一层的神经元都通过权重与下一层的神经元相连。

利用多层感知机组合来解决感知机无法解决异或(非线性问题)

神经网络

        感知机由于结构简单,完成的功能十分有限。可以将若干个感知机连在一起,形成 一个级联网络结构,这个结构称为“多层前馈神经网络”(Multi-layer Feedforward Neural Networks)。所谓“前馈”是 指将前一层的输出作为后一 层的输入的逻辑结构。每一层神经元仅与下一层的神经元全连接。但在同一 层之内, 神经元彼此不连接,而且跨层之间的神经元,彼此也不相连(全连接)。

神经网络的功能

        1989年,奥地利学者库尔特·霍尼克(Kurt Hornik)等人发表论文证明,对于任意复杂度连续波莱尔可测函数(Borel Measurable Function)f,仅仅需要一个隐含层,只要这个隐含层包括足够多的神经元前馈神经网络使用挤压函数(Squashing Function)作为激活函数,就可以以任意精度来近似模拟f。如果想增加f的近似精度,单纯依靠增加神经元的数目即可实现。

         这个定理也被称为通用近似定理(Universal Approximation Theorem), 该定理表明,前馈神经网在理论上可近似解决任何问题

        神经网络的结构还有另外一个“进化”方向,那就是朝着“纵深”方向发展,也就是说,减少单层的神经元数量,而增加神经网络的层数,也就是“深”而“瘦”的网络模型。

        微软研究院的科研人员就以上两类网络性能展开了实验,实验结果表明:增加网络的层数会显著提升神经网络系统的学习性能。

多层神经网络计算公式

  • 本质全连接

MLP(一个隐藏层)

        "MLP"通常指的是"多层感知器"(Multilayer Perceptron),是一种最基本的人工神经网络模型之一。 它由多个神经元层组成,每个神经元层与下一层全连接。MLP通常由一个输入层、一个或多个隐藏层以 及一个输出层组成

  • 输入层(Input Layer):接收原始数据输入的层。
  • 隐藏层(Hidden Layers):对输入数据进行非线性变换的层。这些层的存在使得MLP可以学习非 线性关系
  • 输出层(Output Layer):产生最终预测结果的层。根据问题的不同,输出层可以具有不同的激 活函数,比如用于二分类问题的sigmoid函数,用于多分类问题的softmax函数,或用于回归问题的线性激活函数

MLP通过反向传播算法进行训练,利用梯度下降等优化算法来最小化预测值真实标签之间的误差。它 在许多领域都得到了广泛的应用,如图像识别、自然语言处理、预测分析等。

MLP和感知器模型效果对比

import torch
import torch.nn as nn
import torch.nn.functional as F
x_input = torch.randn(2, 3, 10)
# torch.Size([2, 3, 10])
print(x_input.shape)
class MLP(nn.Module):def __init__(self, input_dim, hidden_dim, output_dim):super(MLP, self).__init__()self.fc1 = nn.Linear(input_dim, hidden_dim)self.fc2 = nn.Linear(hidden_dim, output_dim)def forward(self, inputs):intermediate = F.relu(self.fc1(inputs))# torch.Size([2, 3, 20])# print(intermediate.shape)outputs = self.fc2(intermediate)# torch.Size([2, 3, 5])# print(outputs.shape)"""tensor([[[0.1974, 0.2204, 0.2073, 0.1959, 0.1791],[0.2111, 0.1881, 0.2213, 0.2037, 0.1758],[0.1517, 0.2093, 0.1910, 0.2333, 0.2148]],[[0.1461, 0.2149, 0.2021, 0.2364, 0.2005],[0.1545, 0.2163, 0.2159, 0.2391, 0.1741],[0.1428, 0.2263, 0.2192, 0.2216, 0.1901]]],grad_fn=<SoftmaxBackward0>)"""# Softmax函数会将输入张量中的每个元素转换为介于0和1之间的值,并确保每个切片内所有元素的总和等于1。outputs = F.softmax(outputs, dim=2)# torch.Size([2, 3, 5])print(outputs)return outputs
model = MLP(10, 20, 5)
x_output = model(x_input)
# torch.Size([2, 3, 5])
print(x_output.shape)

DNN模型(多个隐藏层)

        DNN(Deep Neural Network,深度神经网络)是一类人工神经网络,其特点是包含多个隐藏层。与传统的浅层神经网络(如只有一个隐藏层多层感知器MLP)相比,DNN通过增加隐藏层的数量,可以学习和表示数据中更加复杂和抽象的特征

关键特点

  • 深度:DNN的“深度”指的是网络中隐藏层的数量。一个网络层数越多,其能够捕捉的特征越复杂
  • 层次化特征表示:DNN通过逐层抽象和变换数据,逐步提取低级、中级和高级特征。例如,在图像识别任务中,前几层可能提取边缘和纹理,中间几层可能提取局部形状和图案,后几层则可能提取整体的物体结构和类别信息。
  • 非线性变换:每个隐藏层通常由线性变换和非线性激活函数组成。这些非线性激活函数(如 ReLU、sigmoid、tanh等)使得DNN可以学习非线性映射

组成部分

  • 输入层:接收原始数据输入。
  • 隐藏层:包括多个隐藏层,每个层包含若干个神经元,这些神经元通过线性变换和非线性激活函数 处理输入数据。
  • 输出层:生成最终的预测结果。输出层的形式和激活函数取决于具体任务(如分类或回归)。

数学公式

其中S表示网络的输出,X是输入数据,W_{in} 是输入层到隐藏层的权重参数,b 是偏置项,f是激活函数。

改进过程

训练过程

DNN的训练过程通常使用反向传播(backpropagation)算法(计算每个参数的梯度来最小化损失函数,从而调整网络的权重和偏置+优化算法(如梯度下降、Adam 等)

存在问题

标准的DNN假设的数据是相互独立的,在处理前后依赖、序列问题(如语音、文本、视频)时就显得力不从心,其结构的固有限制导致无法捕捉前置时间的信息。

改进方法

其中,S_{t}表示在时间 t 的输出,S_{t-1}表示前一时刻的状态W_{s}是用来传递前置时间信息的权重参数。 这样就可以看到,在每次更新中,都包含了上一时刻的状态S_{t-1}和权重参数W_{s}

改进神经网络图

MLPDNN

激活函数

激活函数:在神经网络中,将输入信号的总和转换为输出信号的函数被称为激活函数(activation function)

使用原因

激活函数将多层感知机输出转换为非线性,使得神经网络可以任意逼近任何非线性函数,这样神经 网络就可以应用到众多的非线性模型中

如果一个多层网络,使用连续函数作为激活函数的多层网络,称之为“神经网络” ,否则称为“多层感知机” 。所以,激活函数是区别多层感知机神经网络的依据。

常见激活函数

阶跃函数

阶跃函数(Step Function)是一种特殊的连续时间函数,是一个从0变到1的过程,非0即1

sigmoid函数

sigmoid函数也叫做Logistic函数,用于隐藏神经元输入,取值范围为(0,1),他可以将一个实数映射到(0,1)之间,因此也叫做二分类函数,表达式:\sigma(x) = 1/(1+e^{-x})

优点:平滑、易于求导

缺点:激活函数计算量大,反向传播求误差梯度时,求导涉及除法,反向传播时,容易出现梯度消失情况,从而无法完成深层网络的训练

tanh双曲正切函数

优点:平滑、易于求导,输出均值为0收敛速度要比sigmoid块,从而可以减少迭代次数

缺点:梯度消失

用途:常用于NLP中

ReLU(Rectified Linear Units, 修正线性单元)

优点:更加有效的梯度下降以及反向传播,避免了梯度爆照和消失问题,计算过程简单

缺点:小于或等于0的部分梯度为0

用途:常用于图像

softmax函数

softmax函数属于多分类输出激活函数,通过softmax函数可以将多分类的输出数值转化为相对概率,而这些的概率值累计和为1,常用于神经网络输出层

注意

  • Softmax 函数和ReLU(Rectified Linear Unit)函数都是神经网络中常见的激活函数,但它们的作用和应用场景不同。
  • ReLU一般用在隐藏层,主要作用是引入非线性,使得网络能够处理复杂的任务
  • Softmax则用在输出层,用来将模型输出转化为概率分布,用于分类任务的最终决策。 在分类任务中,网络的最后一层通常会输出一组得分(logits),而 Softmax 可以将这些得分转化为类别的概率
  • ReLU 用在隐藏层来激活和调整中间特征,而 Softmax 是用在最后一层用于预测每个类别的概率。 ReLU 主要用于隐藏层的非线性激活
  • Softmax 主要用于输出层,将结果转化为概率用于分类任务

RNN模型

循环神经网络

时序数据

        对于不同类型的数据和任务,理解数据的顺序和前后关系的重要性是至关重要的。对于图像识别这样的任务来说,图片的前后顺序并不会影响识别结果,因为图片中的像素是独立且无序的。但是,当涉及到 文本、股票、天气、语音等具有时间顺序或逻辑顺序的数据时,顺序的变化会对结果产生显著影响,因此循环神经网络(RNN)等模型在这些任务中被广泛使用的原因之一,因为它们能够捕捉到数据的顺序信息,并根据前面的输入来预测后续的输出。。

RNN原理

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

在自然语言处理领域,RNN 被广泛应用于语言建模、机器翻译、情感分析等任务。通过捕捉单词之间的上下文信息,RNN 能够更好地理解语言的含义和结构。 同时,RNN 也在时间序列预测领域发挥着重要作用,比如股票价格预测、天气预测等。

通过学习序列数据的模式和趋势,RNN 能够提供有用的预测信息,帮助人们做出决策。 然而,传统的 RNN 存在一些问题,例如难以处理长期依赖关系梯度消失或梯度爆炸等。为了解决这 些问题,出现了一些改进的RNN 变种,如长短期记忆网络(LSTM)和门控循环单元(GRU)。这些变种结构能够更有效地捕捉长期依赖关系,并且在训练过程中更加稳定

模型架构

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

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

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

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

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

其中O_{_{t}}时间步t的输出,V是从隐藏状态到输出层的权重矩阵,g是另一个非线性函数,常用于输出层。 RNN也是传统的神经网络架构,但是他里面包含了一个“盒子”,这个盒子里记录了输入时网络的状态, 在下一次输入时,必须要考虑“盒子”。随着不断的输入,盒子里也会更新,那么这个盒子就是“隐藏态”

模型结构图

  • 加tanh双曲正切激活函数构成RNN的内部结构

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

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

  • 一对多(One-to-Many):这种结构的RNN接受单个输入并产生一系列输出。这种模式常用于“看 图说话”的任务,即给定一张图片(单个输入),RNN生成一段描述该图片的文本(一系列输 出)。在这种情况下,RNN的结构被调整为首先对输入图片进行编码,然后根据这个编码连续生成文本序列中的词语
  • 多对一(Many-to-One):与一对多相反,多对一的RNN结构接受一系列输入并产生单个输出。 这种结构适用于如文本分类和情感分析等任务,其中模型需要阅读和理解整个文本(一系列输 入),然后决定文本属于哪个类别(单个输出)。在图片生成的上下文中,这种结构可以通过分析 一系列的特征或指令来生成单个图片输出。
  • 多对多(Many-to-Many):这种结构的RNN既接受一系列输入,也产生一系列输出。这在需要输入和输出均为序列的任务中非常有用,例如机器翻译,其中模型需要读取一个语言的文本(一系 列输入),然后生成另一种语言的对应文本(一系列输出)。另一个例子是小说生成,其中RNN可 以基于给定的开头或主题(一系列输入),连续生成故事的后续内容(一系列输出)。

RNN代码实现

参数解读

1.Batch Size (批量大小):指的是在一次前向传播或反向传播过程中同时处理的样本数量

2.Sequence Length (序列长度):指输入数据中每个样本的连续时间步(或词、字符)的数量。

3.Input Size (输入大小):指每个时间步输入向量的特征维度。

4.Hidden Size (隐藏层大小)

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

5.Output Size (输出大小)

  • Output size通常与特定任务相关(一般几分类决定了output size)
  • 对于一般的RNN,每个时间步的输出大小与hidden size相同,即输出也是一个隐藏状态维度 的向量。
  • 分类任务中,最后一层可能通过一个全连接层映射到类别数目,这时最后一个时间步的输出大小可能是类别数目的维度。
  • 如果是多层或双向RNN,输出也可能经过额外的处理(如拼接、池化等),最终的输出大小会根据具体应用需求来确定。
  • 在最简单的单向单层循环神经网络(RNN)中,输出大小(output size)的计算通常比较直接
  • 如果目的是为了获取每个时间步(time step)的隐藏状态表示,并且不进行额外的转换操作,那么每个时间步的输出大小(output size)就等于您设定的隐藏层大小(hidden size)

案列解读

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

  • 输入维度(input size): 每个时间步(每个单词)的输入向量维度是100,所以整个输入张量的维 度是 (batch size, sequence length, input size),即 (32, 50, 100)
  • 隐藏层计算: RNN会对每个时间步的输入进行处理,并基于上一时间步的隐藏状态生成当前时间步 的隐藏状态。隐藏状态的维度由我们设定,这里是128维,所以每个时间步的隐藏状态和输出的维 度都是 (batch size, hidden size),即 (32, 128)(每次处理每个32个批次中的一个特征(1/50),并将输出作为下一时间步的隐藏状态的上一时间步的隐藏状态)
  • 输出维度(output size): 因为这里我们假设没有在RNN后添加额外的层(例如分类层),所以每 个时间步的输出大小就等于隐藏层大小,也就是128维。但是,由于输出是针对每一个时间步的, 所以整个输出序列的维度为 (batch size, sequence length, hidden size),即 (32,50,128) (全部处理完后)

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

原生代码

import numpy as np# 假设输入数据,3个时间步,每个时间步2个特征
X = np.random.rand(3, 2)
"""
[[0.9799354  0.94964632]
[0.03334772 0.93230107]
[0.98703732 0.2951229 ]]
"""
# print(X)# 定义RNN的参数
input_size = 2  # 输入特征维度
hidden_size = 3  # 隐藏层大小
output_size = 4  # 输出层大小
# 初始化权重和偏置(为了简单起见,这里使用小的随机值)
Wxh = np.random.randn(input_size, hidden_size)  # 输入到隐藏层的权重矩阵,形状应该是 (input_size, hidden_size)
# (2, 3)
# print(Wxh.shape)
Whh = np.random.randn(hidden_size, hidden_size)  # 隐藏层到隐藏层的权重矩阵
# (3, 3)
# print(Whh.shape)
Why = np.random.randn(hidden_size, output_size)  # 隐藏层到输出层的权重矩阵
# (3, 4)
# print(Why.shape)
bh = np.zeros((hidden_size,))  # 隐藏层的偏置
# [0. 0. 0.]
# print(bh)
by = np.zeros((output_size,))  # 输出层的偏置
# [0. 0. 0. 0.]
# print(by)# 激活函数
def tanh(x):return np.tanh(x)# 初始化隐藏状态
H_prev = np.zeros((hidden_size,))
# [0. 0. 0.]
# print(H_prev)
# 进行前向传播
# 时间步1
# 取第一行所有列
X1 = X[0, :]  # 取时间步1的输入特征
#  [0.22157873 0.74458033]
# print("输入特征X1:", X1)
# np.dot() 是一个强大的函数,可以用于多种类型的点积和矩阵乘法运算,根据输入数组的维度自动选择合适的计算方式。
# X1.shape = (2,), Wxh.shape = (2, 3),H_prev.shape = (3,),Whh.shape = (3, 3)
H1 = tanh(np.dot(X1, Wxh) + np.dot(H_prev, Whh) + bh)  # 在时间步1,没有上一个隐藏状态,所以只使用H_prev
# [ 0.0297402   0.78637309 -0.82153805]
# print("时间步1的隐藏状态H1:", H1)
# H1.shape = (3,) Why.shape = (3, 4)
O1 = np.dot(H1, Why) + by  # 计算输出
# [-1.06872514  0.74326332 -0.71305645 -0.53929714]
# print("时间步1的输出O1:", O1)
# 时间步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  # 计算输出
# 输出结果
"""
时间步1的隐藏状态H1: [ 0.89197341 -0.60439725  0.32905487]
时间步1的输出O1: [-1.06872514  0.74326332 -0.71305645 -0.53929714]
时间步2的隐藏状态H2: [ 0.9705254  -0.99936997  0.88211897]
时间步2的输出O2: [-0.7474593   0.31838459 -1.16844556 -0.98024948]
时间步3的隐藏状态H3: [ 0.88839807 -0.99730625  0.94699084]
时间步3的输出O3: [-0.58426143  0.1649201  -1.16021769 -0.99353336]
"""
print("时间步1的隐藏状态H1:", H1)
print("时间步1的输出O1:", O1)
print("时间步2的隐藏状态H2:", H2)
print("时间步2的输出O2:", O2)
print("时间步3的隐藏状态H3:", H3)
print("时间步3的输出O3:", O3)

基于RNNCell代码实现(了解)

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

import torch
import torch.nn as nn
x_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为True
outputs = model(x_input)  # 将输入张量传入模型,获取输出
print(outputs.shape)  # 打印输出张量的形状

基于 pytorch API 代码实现

官方代码细节: torch.nn.modules.rnn — PyTorch 2.6 documentation

官方文档解释:http:// https://pytorch.org/docs/stable/generated/torch.nn.RNN.html#rnn

import torch
import torch.nn as nn# 设置超参数
bs, T = 2, 3  # 批大小,输入序列长度
input_size, hidden_size = 2, 3  # 输入特征大小,隐含层特征大小
# 初始化随机输入特征序列
# (2, 3, 2)
input = torch.randn(bs, T, input_size)
# 初始化初始隐含状态,全零向量
# (2, 3)
h_prev = torch.zeros(bs, hidden_size)
# 创建一个RNN实例,设置batch_first=True意味着输入数据的第一维是batch_size
rnn = nn.RNN(input_size, hidden_size, batch_first=True)
# 使用RNN进行前向传播
# h_prev.unsqueeze(0)保持维度一致
rnn_output, state_final = rnn(input, h_prev.unsqueeze(0))
# 输出RNN的输出结果及最终隐含状态
"""
tensor([[[ 0.3224, -0.8209, -0.5784], [ 0.8212, -0.7772, -0.6485], [ 0.1148, -0.1676,  0.6050]],[[ 0.1462,  0.2260, -0.2316],[ 0.3889, -0.0495, -0.5054],[ 0.9416, -0.8148, -0.9502]]], grad_fn=<TransposeBackward1>)
tensor([[[ 0.1148, -0.1676,  0.6050],[ 0.9416, -0.8148, -0.9502]]], grad_fn=<StackBackward0>)
"""
print(rnn_output)
print(state_final)

多对多任务

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]
# torch.Size([10, 6, 18])
print(output.shape)

单向、单层RNN

import torch
import torch.nn as nn
"""
4 表示输入序列的特征维度(feature size),即每个时间步的输入向量长度为4。
3 表示隐藏状态(hidden state)的维度,即RNN单元内部记忆的向量长度为3。
1 表示RNN层的数量,这里仅为单层。
batch_first=True 指定输入张量的第一个维度代表批次(batch),第二个维度代表时间步
(sequence length),这对于处理批次数据时更容易理解。
"""
signle_rnn = nn.RNN(4,3,1,batch_first=True)
"""
1 表示批大小(batch size),即本次输入的数据样本数量。
2 表示序列长度(sequence length),即每个样本的输入序列包含两个时间步。
4 是每个时间步输入向量的特征维度,与RNN层设置一致。
"""
input = torch.randn(1,2,4) # bs*sl*fs
"""
output 是经过RNN处理后的输出序列,其形状通常为 (batch_size, sequence_length, num_directions * hidden_size)。
在这个例子中,因为没有指定双向RNN,所以 num_directions=1 。因此,步输出一个维度为3的向量。
output 的尺寸将是 (1, 2, 3),对应每个批次中的每个时间
h_n 是最后一个时间步的隐藏状态(hidden state),它通常是最终时间步的隐藏状态或者
是所有时间步隐藏状态的某种聚合(取决于RNN类型)。
在这里,
h_n 的形状是 (num_layers * num_directions, batch_size, hidden_size),
但由于只有一层并且是无方向的RNN,所以形状会简化为 (1, 1, 3),
即单一隐藏状态向量。这个隐藏状态可以用于下个时间步的预测或者作为整个序列的编码。
"""
output, h_n = signle_rnn(input)

双向、单层RNN

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

主要特点

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

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

结构图

代码解读
import torch
import torch.nn as nn
"""
bidirectional=True 指定该RNN为双向的,这意味着对于每个时间步,除了向前传递的信
息外,还会考虑向后传递的信息,从而能够捕捉序列中前后依赖关系。
"""
bi_rnn = nn.RNN(4,3,1,batch_first=True,bidirectional=True)
input = torch.randn(1,2,4)
"""
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),分别对应正向和反向隐藏状态各一个。每个隐藏状态向量都是相应方向上整个序列信息的汇总。
"""
output, h_n = bi_rnn(input)

RNN的训练方法——BPTT

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

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

多元交叉熵损失函数

RNN模型存在的问题

RNN中的梯度消失和爆炸

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

这就会导致一个问题,在上面式子累乘的过程中,如果取sigmoid函数作为激活函数的话,那么必然是一堆小数在做乘法,结果就是越乘越小。随着时间序列的不断深入,小数的累乘就会导致梯度越来越小直到接近于0,这就是“梯度消失“现象。

其实RNN的时间序列与深层神经网络很像,在较为深层的神经网 络中使用sigmoid函数做激活函数也会导致反向传播时梯度消失,梯度消失就意味消失那一层的参数再也不更新,那么那一层隐层就变成了单纯的映射层,毫无意义了,所以在深层神经网络中,有时候多加神经元数量可能会比多家深度好。

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

激活函数的选择

虽然tanh和sigmoid函数都是相当于一堆小数在累乘,还是会出现“梯度消失“,但是tanh函数相对于sigmoid函数来说梯度较大收敛速度更快且引起梯度消失更慢。 还有一个原因是sigmoid函数还有一个缺点,Sigmoid函数输出不是零中心对称。sigmoid的输出均大于 0,这就使得输出不是0均值,称为偏移现象,这将导致后一层的神经元将上一层输出的非0均值的信号作为输入。关于原点对称的输入和中心对称的输出,网络会收敛地更好。

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

sigmoid函数的缺点

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

远距离依赖

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

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

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

LSTM 模型

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层

基本状态

细胞状态(Ccell state)

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

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

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

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

门控机制

遗忘门

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

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

  • [h_{t-1}, x_{t}]:这是一个连接的向量,包括前一时间步的隐藏状态当前时间步的输入 。它们被合并起来,以便遗忘门可以考虑当前的输入先前的隐藏状态来做出决策。
  • W_{f}:这是遗忘门的权重矩阵,用于从输入[h_{t-1}, x_{t}]中学习什么信息应该被遗忘
  • b_{f}:这是遗忘门的偏置项,它被加到权重矩阵和输入向量的乘积上,可以提供额外的调整能力,确保即使在没有输入的情况下遗忘门也能有一个默认的行为
  • \sigma:这是sigmoid激活函数,它将输入压缩到0和1之间。在这里,它确保遗忘门的输出也在这个范围内,表示每个状态单元被遗忘的比例
  • f_{t}:这是在时间步 ( t ) 的遗忘门的输出,它是一个向量,其中的每个元素都在0和1之间,对应于细胞状态中每个元素应该被保留的比例

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

输入门

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

  • i_{t}表示时间步 ( t ) 的输入门激活值,是一个向量。这个向量通过sigmoid函数产生,将值限定在 0 和 1 之间。它决定了多少新信息会被加入到细胞状态中
  • W_{t}输入门的权重矩阵,用于当前时间步的输入 和前一个时间步的隐藏状态h_{t-1}
  • [h_{t-1},x_{t}]是前一个隐藏状态和当前输入的串联
  • b_{i}输入门的偏置向量
  • C_{t}候选细胞状态,它是通过tanh函数产生的,可以将值限定在 -1 和 1 之间。它与输入门相 乘,决定了将多少新的信息添加到细胞状态中
  • W_{c}控制候选细胞状态的权重矩阵。
  • b_{c}是对应的偏置向量

状态更新

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

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

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

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

输出门

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

  • o_{t}输出门的激活值。这是通过将前一时间步的隐藏状态和当前时间步的输入连接起 来,并应用权重矩阵W_{o}以及偏置项 b_{o},然后通过sigmoid函数来计算的。Sigmoid函数确保输出值在0和1之间。
  • C_{t}当前时间步的细胞状态,这是在之前的步骤中计算的。
  • C_{t}是细胞状态的tanh激活,这个激活函数将值压缩到-1和1之间。这是因为细胞状态C_{t}可以有很 大的值,而tanh函数有助于规范化这些值,使它们更加稳定。
  • h_{t}当前时间步的隐藏状态,通过将输出门的值与细胞状态的tanh激活相乘来得到。这个元素 级别的乘法(Hadamard乘法)决定了多少细胞状态的信息将被传递到外部作为当前的隐藏状态输 出

代码

import numpy as np
import torch
# input_size=2, hidden_size=5, output_size=4
class CustomLSTM:def __init__(self, input_size, hidden_size, output_size):# lstm = CustomLSTM(input_size=2, hidden_size=5, output_size=4)self.input_size = input_size  # 输入特征维度 2self.hidden_size = hidden_size  # 隐藏层维度 5# 权重和偏置初始化# self.W_f = (5, 7)self.W_f = np.random.randn(hidden_size, hidden_size + input_size) # self.b_f = (5,)self.b_f = np.random.randn(hidden_size)# self.W_i = (5, 7)self.W_i = np.random.randn(hidden_size, hidden_size + input_size)# self.b_i = (5,)self.b_i = np.random.randn(hidden_size)# self.W_c = (5, 7)self.W_c = np.random.randn(hidden_size, hidden_size + input_size)# self.b_c = (5,)self.b_c = np.random.randn(hidden_size)# self.W_o = (5, 7)self.W_o = np.random.randn(hidden_size, hidden_size + input_size)# self.b_o = (5,)self.b_o = np.random.randn(hidden_size)# self.W_y = (4, 5)self.W_y = np.random.randn(output_size, hidden_size)# self.b_y = (4,)self.b_y = np.random.randn(output_size)# sigmoid函数def sigmoid(self, x):return 1 / (1 + np.exp(-x))# tanh函数def tanh(self, x):return np.tanh(x)# X = np.random.rand(3, 2)def forward(self, X):h_t = np.zeros((self.hidden_size,))  # 初始隐藏状态# 初始隐藏状态h_t: [0. 0. 0. 0. 0.] (5,)# print("初始隐藏状态h_t:", h_t)c_t = np.zeros((self.hidden_size,))  # 初始细胞状态 (5,)h_states = []  # 存储每个时间步的隐藏状态c_states = []  # 存储每个时间步的细胞状态# X.shape[0]:3for t in range(X.shape[0]):# 依次取出输入的第一维数据(特征)x_t = X[t]  # 当前时间步的输入# 将 h_t 和 x_t 拼接 垂直方向combined = np.concatenate((h_t, x_t))#  [0.   0.   0.   0.   0.   0.00261591 0.01346005] (1,7)# print(combined)# 遗忘门# self.W_f = np.random.randn(hidden_size, hidden_size + input_size)# np.dot(self.W_f, combined) (5,7) (1,7)=>(5,)+(5,)(self.b_f)f_t = self.sigmoid(np.dot(self.W_f, combined) + self.b_f)# [0.45627695 0.1345999  0.46255267 0.04940347 0.41737901] (5,1)# print(5555555555555555, f_t)# (5,1)# print(f_t.shape)# 输入门 同理 (5,7)i_t = self.sigmoid(np.dot(self.W_i, combined) + self.b_i)# [0.74374338 0.34698128 0.5041858  0.57060708 0.49126998] (5,1)# print(i_t)c_hat_t = self.tanh(np.dot(self.W_c, combined) + self.b_c)# [0.74374338 0.34698128 0.5041858  0.57060708 0.49126998]# print(i_t)# 更新细胞状态 (5,7)*(5,)+(5,7)(5,7)# c_t = (1,5)*(1,5) + (1,5)*(1,5)c_t = f_t * c_t + i_t * c_hat_t# [ 0.47651912 -0.02464907 -0.23617022  0.34938658 -0.1880387 ] (1,5)# print(c_t)# 输出门o_t = self.sigmoid(np.dot(self.W_o, combined) + self.b_o)# [0.55258193 0.77237627 0.2649089  0.68104621 0.75720679] (1,5)# print(o_t)# 更新隐藏状态# (1,5) = (1,5)*(1,5)h_t = o_t * self.tanh(c_t)# 保存每个时间步的隐藏状态和细胞状态h_states.append(h_t)c_states.append(c_t)# 输出层,分类类别# self.W_y = (4, 5)# np.dot(self.W_y, h_t) (4,5)*(1,5)=>(1,4)y_t = np.dot(self.W_y, h_t) + self.b_y# 计算 Softmax torch.from_numpy()将一个 NumPy 数组 转换为 PyTorch 张量Tensor) dim=0 是行维度# (1,4)# 多对多output = torch.softmax(torch.from_numpy(y_t), dim=0)# 列表转数组# np.array(): 这是 NumPy 库中的一个函数,用于创建数组,将 h_states 转换为一个 NumPy 数组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)
"""
每个时间步的隐藏状态:
[[-0.03733028 -0.35349276  0.16567163  0.19512539 -0.06818017][-0.13198858 -0.34621915  0.08614014  0.19774609 -0.10965626][-0.19201421 -0.47469226  0.20348467  0.27649428 -0.11850828]]
"""
print("每个时间步的隐藏状态:")
print(hidden_states)
"""
每个时间步的细胞状态:
[[-0.12145711 -0.60546981  0.40412433  0.27709844 -0.08985773][-0.34780014 -0.8260018   0.11146379  0.30350921 -0.21136193][-0.57352514 -0.8733343   0.27496541  0.3747736  -0.16200148]]
"""
print("\n每个时间步的细胞状态:")
print(cell_states)
"""
分类输出:
tensor([0.0996, 0.5345, 0.1516, 0.2143], dtype=torch.float64)
"""
print("\n分类输出:")
print(output)

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

import numpy as np
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])
# [140  84 252 280 308]
print(np.dot(matrix, vector))# matrix的每一行乘以vector,得出的结果组成[140  84 252 280 308]

基于 pytorch API 代码实现

1.在 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 隐藏层的单元数,即隐藏状态和细胞状态的维度
     

2.在 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 ,LSTM 是单向的双向的
  • out 包含 LSTM 在每个时间步的输出,适用于后续处理(例如,将其传递给一个全连接层)。

(_)或(hn, cn)

  • hn 是最后一个时间步的隐状态(hidden state)
  • cn 是最后一个时间步的细胞状态(cell state)
  • 如果输入 x 的形状是 (seq_len, batch_size, input_size) ,那么 hn 和 cn 的形状将是
    (num_layers * num_directions, batch_size, hidden_size)
  • 如果 num_layers = 1,LSTM 网络将只有一个层。这意味着输入序列直接通过这
    个单层 LSTM 进行处理(单层LSTM)。
  • 如果 num_layers > 1,LSTM 网络将有多层。输入序列首先通过第一层 LSTM,
    第一层的输出作为输入传递给第二层,以此类推,直到最后一层(双层LSTM)。
  • 这些状态可以用于初始化下一个序列的 LSTM,特别是在处理长序列或多个批次的序列数据时。
代码实现
import torch
import torch.nn as nn
# 定义一个简单的LSTM模型
class SimpleLSTM(nn.Module):def __init__(self, input_size, hidden_size, output_size):# # lstm_model = SimpleLSTM(10,20,2)super(SimpleLSTM, self).__init__()self.hidden_size = hidden_size # 20self.lstm = nn.LSTM(input_size, hidden_size) # LSTM层self.fc = nn.Linear(hidden_size, output_size) # 全连接层def forward(self, x):# (5, 3, 10)# 初始化隐藏状态和细胞状态#  torch.Size([1, 3, 20])h0 = torch.zeros(1, x.size(1), self.hidden_size)# print(h0.shape)c0 = torch.zeros(1, x.size(1), self.hidden_size)#  torch.Size([1, 3, 20])# print(c0.shape)# 前向传播out, _ = self.lstm(x, (h0, c0)) # _是最后一个时间步的隐藏状态和细胞状态# torch.Size([5, 3, 20])# print(out.shape)out = self.fc(out[-1, :, :]) # 取最后一个时间步的输出return out
# 测试模型
if __name__ == "__main__":# 定义模型参数input_size = 10hidden_size = 20output_size = 2# 创建模型实例# lstm_model = SimpleLSTM(10,20,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)# 打印输出和形状"""tensor([[ 0.0244, -0.3516],[-0.1438, -0.2025],[-0.1747, -0.1571]], grad_fn=<AddmmBackward0>)"""print("Output:\n", output)# Output Shape: torch.Size([3, 2])print("Output Shape:", output.shape)

序列池化
 

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


最大池化(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)
print(output.shape) # 输出形状:[32, 50, 1] ->[32,50]
# 去掉多余的维度
output = output.squeeze(-1) # [batch_size, feature_size]
print(output.shape) # torch.Size([32, 50])

 平均池化(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)
# torch.Size([32, 50, 1])
print(output.shape)
# 去掉多余的维度
output = output.squeeze(-1) # [batch_size, feature_size]
print(output.shape) # 输出形状:[32, 50, 1]

 注意力池化(Attention Pooling)

  • 使用注意力机制对序列进行加权平均,根据每个时间步的重要性分配权重
  • 适用于希望模型能够根据输入内容自适应地分配注意力权重
  • 注意力池化的实现通常涉及一个注意力权重计算模块和一个对这些权重进行加权平均的模块

梯度消失

梯度消失(Vanishing Gradient 主要在于反向传播过程中,梯度在多层传播时会逐渐减小,导致前面层的参数更新非常缓慢,甚至完全停滞。LSTM 尽管通过门控机制(输入门、遗忘门和输出门)缓解了这 个问题,但仍然可能出现梯度消失,特别是在以下情况下:
  • 长期依赖问题:如果序列特别长,即使是LSTM也可能无法有效地记住早期的信息,因为梯度会在 很长的时间步长内持续衰减
  • 不适当的权重初始化:如果权重初始化不合理,可能会导致LSTM的各个门在初始阶段就偏向于某 种状态(如过度遗忘或完全记住),从而影响梯度的有效传播。
  • 激活函数的选择:尽管LSTM通常使用tanhsigmoid激活函数,这些函数在某些输入值下可能会导致梯度的进一步缩小。

梯度爆炸问题

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

 梯度消失问题

梯度爆炸(Exploding Gradient 则是在反向传播过程中,梯度在多层传播时会指数级增长,导致前面层的参数更新过大模型难以收敛。LSTM 在以下情况下可能出现梯度爆炸:
  • 过长的序列长度:即使是LSTM,在非常长的序列上仍然可能遇到梯度爆炸,因为梯度在反向传播 时会不断累积,最终可能变得非常大。
  • 不适当的学习率过高的学习率可能会导致梯度爆炸,因为参数更新的步伐太大,使得模型参数偏 离最优解。
  • 不适当的权重初始化:与梯度消失类似,权重初始化也可能导致梯度爆炸。如果初始权重过大,梯 度在反向传播过程中会不断放大。
解决方法
  • 梯度裁剪:在每次反向传播后,将梯度裁剪到某个阈值范围内,防止梯度爆炸。
  • 适当的权重初始化使用标准的初始化方法,如Xavier初始化或He初始化,确保权重在初始阶段不
  • 至于过大或过小。
  • 调整学习率:选择合适的学习率,或者使用自适应学习率算法,如AdamRMSprop等,动态调整
  • 学习率。
  • 正则化技术:如L2正则化、Dropout等,防止过拟合并平滑梯度。
  • 批归一化(Batch Normalization:在网络层之间使用批归一化技术,可以加速训练并稳定梯度。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词