深度学习 自然语言处理(RNN) day_02
1. 感知机与神经网络
1.1 感知机
生物神经元:
1.1.1 感知机的概念
感知机(Perceptron),又称神经元(Neuron,对生物神经元进行了模仿)是神 经网络(深度学习)的起源算法,1958年由康奈尔大学心理学教授弗兰克·罗森布拉 特(Frank Rosenblatt)提出,它可以接收多个输入信号,产生一个输出信号。
神经元更通用的图形表示和表达式:
1.2.1 感知机的功能
作为分类器/回归器,实现自我学习:
实现逻辑运算,包括逻辑和(AND)、逻辑或(OR):
1.3.1 感知机的缺陷
多层感知机:
1.2 神经网络
1.2.1 什么是神经网络
感知机由于结构简单,完成的功能十分有限。可以将若干个感知机连在一起,形成 一个级联网络结构,这个结构称为“多层前馈神经网络”(Multi-layer Feedforward Neural Networks)。所谓“前馈”是指将前一层的输出作为后一 层的输入的逻辑结构。每一层神经元仅与下一层的神经元全连接。但在同一层之内, 神经元彼此不连接,而且跨层之间的神经元,彼此也不相连。
1.2.2 神经网络的功能
1989年,奥地利学者库尔特·霍尼克(Kurt Hornik)等人发表论文证明,对于任意复杂度的连续波莱尔可测函数(Borel Measurable Function)f,仅仅需要一个隐含层,只要这个隐含层包括足够多的神经元,前馈神经网络使用挤压函数(Squashing Function)作为激活函数,就可以以任意精度来近似模拟f。如果想增加f的近似精度,单纯依靠增加神经元的数目即可实现。
这个定理也被称为通用近似定理(Universal Approximation Theorem), 该定理表明,前馈神经网在理论上可近似解决任何问题。
通用近似定理:
其实,神经网络的结构还有另外一个“进化”方向,那就是朝着“纵深”方向发展,也就是说,减少单层的神经元数量,而增加神经网络的层数,也就是“深”而“瘦”的网络模型。
微软研究院的科研人员就以上两类网络性能展开了实验,实验结果表明:增加网络的层数会显著提升神经网络系统的学习性能。
多层神经网络计算公式:
以下是一个多层神经网络及其计算公式:
1.3 MLP
"MLP"通常指的是"多层感知器"(Multilayer Perceptron),是一种最基本的人工神经网络模型之一。它由多个神经元层组成,每个神经元层与下一层全连接。MLP通常由一个输入层、一个或多个隐藏层以及一个输出层组成。
- 输入层(Input Layer):接收原始数据输入的层。
- 隐藏层(Hidden Layers):对输入数据进行非线性变换的层。这些层的存在使得MLP可以学习非线性关系。
- 输出层(Output Layer):产生最终预测结果的层。根据问题的不同,输出层可以具有不同的激活函数,比如用于二分类问题的sigmoid函数,用于多分类问题的softmax函数,或用于回归问题的线性激活函数。
MLP通过反向传播算法进行训练,利用梯度下降等优化算法来最小化预测值与真实标签之间的误差。它在许多领域都得到了广泛的应用,如图像识别、自然语言处理、预测分析等。
MLP和感知器模型效果对比:
1.3.1 MLP的pytorch实现:
- 先写出MLP的模型;
- 对模型进行实例化;
- 用随机输入测试MLP。
import torch
import torch.nn as nn
import torch.nn.functional as Fx_input = torch.randn(2, 3, 10)
# print(x_input)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))outputs = self.fc2(intermediate)outputs = F.softmax(outputs, dim=2)return outputsmodel = MLP(10, 20, 5)
x_output = model(x_input)
print(x_output)
1.4 DNN模型
DNN(Deep Neural Network,深度神经网络)是一类人工神经网络,其特点是包含多个隐藏层。与传统的浅层神经网络(如只有一个隐藏层的多层感知器MLP)相比,DNN通过增加隐藏层的数量,可以学习和表示数据中更加复杂和抽象的特征。
关键特点:
-
深度:DNN的“深度”指的是网络中隐藏层的数量。一个网络层数越多,其能够捕捉的特征越复杂。
-
层次化特征表示:DNN通过逐层抽象和变换数据,逐步提取低级、中级和高级特征。例如,在图像识别任务中,前几层可能提取边缘和纹理,中间几层可能提取局部形状和图案,后几层则可能提取整体的物体结构和类别信息。
-
非线性变换:每个隐藏层通常由线性变换和非线性激活函数组成。这些非线性激活函数(如ReLU、sigmoid、tanh等)使得DNN可以学习非线性映射。
组成部分:
-
输入层:接收原始数据输入。
-
隐藏层:包括多个隐藏层,每个层包含若干个神经元,这些神经元通过线性变换和非线性激活函数处理输入数据。
-
输出层:生成最终的预测结果。输出层的形式和激活函数取决于具体任务(如分类或回归)。
DNN网络可以表示为:;其中 S表示网络的输出,X是输入数据,{W}_{in} 是输入层到隐藏层的权重参数,b 是偏置项,f是激活函数。
1.4.1 训练过程
DNN的训练过程通常使用反向传播(backpropagation)算法和优化算法(如梯度下降、Adam等)。反向传播算法通过计算每个参数的梯度来最小化损失函数,从而调整网络的权重和偏置。
1.4.2 DNN存在的问题
传统的深度神经网络(DNN)在处理时序数据时存在一些问题,主要是由于其结构的固有限制而无法捕捉前置时间的信息。为了解决这个问题,可以通过在网络中引入新的参数和结构来处理时序数据。
传统MLP形如上图,通过不断学习,调节Win和Wout,从而适应任务。
在传统的多层感知器(MLP)中引入时间特征,并通过堆叠的方式传递前置时间的信息。这样的结构就引入了新的参数Ws,并且利用这些参数来调节前置时间信息的传递。同时,为了简化计算,所有的时间权重参数都可以使用同一个参数矩阵Ws。
通过这样的修改,网络在每次更新中都包含了上一时刻的状态 {S}_{t-1} 和权重参数 Ws,从而能够更好地捕捉时序数据中的前置时间信息。这种方法有效地扩展了传统DNN网络的应用范围,使其能够处理更复杂的时序数据任务,例如时间序列预测、语音识别等。
具体而言,传统的DNN网络可以表示为:;其中S表示网络的输出,X是输入数据,Ws 是输入层到隐藏层的权重参数,b 是偏置项,f是激活函数。
在引入时间特征之后,网络的更新规则可以修改为 :;其中
表示在时间 t的输出,
表示前一时刻的状态,Ws 是用来传递前置时间信息的权重参数。
这样就可以看到,在每次更新中,都包含了上一时刻的状态和权重参数Ws
2. RNN 模型
2.1 先导
2.1.1 为什么需要循环神经网络 RNN
上图是一幅全连接神经网络图,我们可以看到输入层-隐藏层-输出层,他们每一层之间是相互独立地,(框框里面代表同一层),每一次输入生成一个节点,同一层中每个节点之间又相互独立的话,那么我们每一次的输入其实跟前面的输入是没有关系地。这样在某一些任务中便不能很好的处理序列信息。
2.1.2 时序数据
对于不同类型的数据和任务,理解数据的顺序和前后关系的重要性是至关重要的。对于图像识别这样的任务来说,图片的前后顺序并不会影响识别结果,因为图片中的像素是独立且无序的。但是,当涉及到文本、股票、天气、语音等具有时间顺序或逻辑顺序的数据时,顺序的变化会对结果产生显著影响。
举例来说,对于一句话"我吃苹果",如果改变顺序为"苹果吃我",意思完全变了。这种语言的表达方式对顺序和词语的关联有着极高的敏感性。而对于股票和天气数据,先后顺序也具有重要意义,因为后续的数据可能受到之前数据的影响,如股票市场的走势或天气的变化趋势。
在文本理解方面,顺序也至关重要。例如,对于经典的古诗"床前明月光,疑是地上霜。举头望明月,低头思故乡。",如果改变了诗句的顺序,那么整首诗的意境和情感也将随之改变,这就是所谓的"蝴蝶效应"。一个微小的改变可能会导致整体结果的巨大变化。
因此,在处理具有时间或逻辑顺序的数据时,我们必须考虑到顺序的重要性,并设计相应的模型来充分利用数据的前后关系,以获得更准确和有意义的结果。这也是循环神经网络(RNN)等模型在这些任务中被广泛使用的原因之一,因为它们能够捕捉到数据的顺序信息,并根据前面的输入来预测后续的输出。
2.2 RNN 原理
2.2.1 概述
循环神经网络(Recurrent Neural Network,RNN)是一种神经网络结构,专门用于处理序列数据。与传统的前馈神经网络不同,RNN 在内部具有反馈连接,允许信息在网络内部传递。这种结构使得 RNN 能够对序列数据的历史信息进行建模,并在一定程度上具有记忆能力。
在自然语言处理领域,RNN 被广泛应用于语言建模、机器翻译、情感分析等任务。通过捕捉单词之间的上下文信息,RNN 能够更好地理解语言的含义和结构。
同时,RNN 也在时间序列预测领域发挥着重要作用,比如股票价格预测、天气预测等。通过学习序列数据的模式和趋势,RNN 能够提供有用的预测信息,帮助人们做出决策。
然而,传统的 RNN 存在一些问题,例如难以处理长期依赖关系、梯度消失或梯度爆炸等。为了解决这些问题,出现了一些改进的 RNN 变种,如长短期记忆网络(LSTM)和门控循环单元(GRU)。这些变种结构能够更有效地捕捉长期依赖关系,并且在训练过程中更加稳定。
2.2.2 模型架构
循环神经网络(RNN)是深度学习中的一种架构,专门设计来处理序列数据,例如时间序列数据或自然语言文本。RNN的核心特征在于它能够在处理序列的每个元素时保留一个内部状态(记忆),这个内部状态能够捕捉到之前元素的信息。这种设计使得RNN特别适合处理那些当前输出依赖于之前信息的任务。
常见的RNN架构如下图两种:
左图可以理解为,先将每一层的网络简化,再将网络旋转90度得到的简化图,而右边两种类型,可以理解为,再将左图继续进行简化。
在RNN的经典架构中,网络通过一个特殊的循环结构将信息从一个处理步骤传递到下一个。这个循环结构通常被称为“隐藏层状态”或简单地称为“隐藏状态”。隐藏状态是RNN的记忆部分,它能够捕获并存储关于已处理序列元素的信息。
当RNN处理一个序列时,它会在每个时间步接受一个输入,并更新其隐藏状态。这个更新过程依赖于当前的输入和之前的隐藏状态,从而使得网络能够“记住”并利用过去的信息。这个过程可以通过以下数学公式简化表达:;在这个公式中,
表示在时间步t的隐藏状态,
是当前时间步的输入,U 和 W分别是输入到隐藏状态和隐藏状态到隐藏状态的权重矩阵。函数f通常是一个非线性函数,如tanh或ReLU,用于引入非线性特性并帮助网络学习复杂的模式。
RNN的输出在每个时间步也可以计算出来,这依赖于当前的隐藏状态:;其中
是时间步t的输出,V是从隐藏状态到输出层的权重矩阵,g是另一个非线性函数,常用于输出层。
RNN也是传统的神经网络架构,但是他里面包含了一个“盒子”,这个盒子里记录了输入时网络的状态,在下一次输入时,必须要考虑“盒子”。随着不断的输入,盒子里也会更新,那么这个盒子就是“隐藏态”。
假设为一个包含三个单词的句子,将模型展开,即为一个三层的网络结构,可以理解为,为第一个词,
为第二个词,
为第三个词。
图中参数含义:
-
表示第t步的输入。比如
为第二个词的词向量(
为第一个词);
-
为隐藏层的第t步的状态,它是网络的记忆单元。
-
根据当前输入层的输出与上一时刻隐藏层的状态
进行计算,如下所示。
-
其中,U是输入层的连接矩阵,W是上一时刻隐含层到下一时刻隐含层的权重矩阵,f(·)一般是非线性的激活函数,如tanh或ReLU。
-
-
{O}_{t}是第t步的输出。输出层是全连接层,即它的每个节点和隐含层的每个节点都互相连接,V是输出层的连接矩阵,g(·)一是激活函数。
-
-
带入可以得到
-
通过这种逐步处理序列并在每一步更新隐藏状态的方式,RNN能够在其内部维持一个随时间变化的“记忆”。这使得它能够对之前序列元素的信息做出响应,并据此影响后续的输出。这种特性对于诸如语言模型、文本生成、语音识别等许多序列处理任务至关重要。
2.2.3 RNN的内部结构
2.2.4 RNN模型输入输出关系对应模式
通过改变RNN的结构,即调整其输入和输出的数量和形式,可以让它适应各种不同的任务。以下是几种常见的RNN结构调整示例,以及它们各自适用的任务类型:
-
一对多(One-to-Many):这种结构的RNN接受单个输入并产生一系列输出。这种模式常用于“看图说话”的任务,即给定一张图片(单个输入),RNN生成一段描述该图片的文本(一系列输出)。在这种情况下,RNN的结构被调整为首先对输入图片进行编码,然后根据这个编码连续生成文本序列中的词语。
-
多对一(Many-to-One):与一对多相反,多对一的RNN结构接受一系列输入并产生单个输出。这种结构适用于如文本分类和情感分析等任务,其中模型需要阅读和理解整个文本(一系列输入),然后决定文本属于哪个类别(单个输出)。在图片生成的上下文中,这种结构可以通过分析一系列的特征或指令来生成单个图片输出。
-
多对多(Many-to-Many):这种结构的RNN既接受一系列输入,也产生一系列输出。这在需要输入和输出均为序列的任务中非常有用,例如机器翻译,其中模型需要读取一个语言的文本(一系列输入),然后生成另一种语言的对应文本(一系列输出)。另一个例子是小说生成,其中RNN可以基于给定的开头或主题(一系列输入),连续生成故事的后续内容(一系列输出)。
2.3 RNN 代码实现
重要参数含义:
-
Batch Size (批量大小):
-
Batch size指的是在一次前向传播或反向传播过程中同时处理的样本数量。
-
例如,在文本处理中,如果一批数据包含100个句子,那么batch size就是100。
-
-
Sequence Length (序列长度):
-
Sequence length是指输入数据中每个样本的连续时间步(或词、字符)的数量。
-
例如,在一个句子级别的任务中,一个句子可能包含10个单词,那么序列长度就是10。
-
-
Input Size (输入大小):
-
Input size是指每个时间步输入向量的特征维度。
-
在处理文本时,如果每个词都被表示为一个固定维度的向量,那么input size就是这个词向量的维度。
-
如在情感分析任务中,每个词可能被嵌入为一个100维的向量,那么input size就是100。
-
-
Hidden Size (隐藏层大小):
-
Hidden size是指RNN单元内部隐藏状态(Hidden State)的维度。
-
在每个时间步,RNN都会根据当前输入和上一时间步的隐藏状态来计算新的隐藏状态,新隐藏状态的维度就是hidden size。
-
例如,如果我们设置hidden size为256,那么每个时间步产生的隐藏状态就是一个256维的向量。
-
根据实验和模型复杂度的要求自由选择隐藏层大小,它并不是通过特定计算得出的数值。
-
隐藏层大小的选择会影响到模型的学习能力和表示能力,同时也影响到模型的计算资源消耗。
-
实践中,较小的隐藏层大小可能会限制模型的表达能力,而过大的隐藏层大小则可能导致过拟合、训练时间增加等问题。
-
在决定隐藏层大小时,通常需要结合具体任务的特点、数据集规模、计算资源等因素进行合理选择,并通过交叉验证、网格搜索等方式进行超参数调优,以找到最优的隐藏层大小以及其他超参数组合。
-
-
Output Size (输出大小):
-
Output size通常与特定任务相关。
-
对于一般的RNN,每个时间步的输出大小与hidden size相同,即输出也是一个隐藏状态维度的向量。
-
在分类任务中,最后一层可能通过一个全连接层映射到类别数目,这时最后一个时间步的输出大小可能是类别数目的维度。
-
如果是多层或双向RNN,输出也可能经过额外的处理(如拼接、池化等),最终的输出大小会根据具体应用需求来确定。
-
在最简单的单向单层循环神经网络(RNN)中,输出大小(output size)的计算通常比较直接:如果目的是为了获取每个时间步(time step)的隐藏状态表示,并且不进行额外的转换操作,那么每个时间步的输出大小(output size)就等于您设定的隐藏层大小(hidden size)。
如果是在做序列到序列(Sequence-to-Sequence)的任务,比如机器翻译,最后的时间步的隐藏状态通常会通过一个线性层映射到目标词汇表大小,这样输出大小就会是目标词汇表的大小。
例如,如果设置的隐藏层大小(hidden size)是256,那么在每个时间步,RNN的输出也将是一个256维的向量。
如果在RNN之后添加了其他层(如全连接层或分类层)来进行进一步的处理,比如进行分类任务,那么输出大小取决于这些后续层的设计。例如,如果您接下来是一个Softmax层用于做多分类,且类别数是10,则输出大小将会是10,表示每个样本的概率分布。
-
具体的单层单向RNN示例来说明维度变换过程:假设正在处理一个文本分类任务,每个单词已经被嵌入为一个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)
。 -
输出维度(output size): 因为这里我们假设没有在RNN后添加额外的层(例如分类层),所以每个时间步的输出大小就等于隐藏层大小,也就是128维。但是,由于输出是针对每一个时间步的,所以整个输出序列的维度为
(batch size, sequence length, hidden size)
,即(32, 50, 128)
。
如果后续需要进行分类,比如这是一个二分类问题,我们会把最后一个时间步的隐藏状态(128
维)通过一个全连接层(Dense Layer)映射到类别数目的维度,如2维,此时输出大小将变为 (32, 2)
,表示32个样本的二维概率分布。
2.3.1 原生代码
import numpy as np# 原始数据 一句话 3个单词 每个2个维度
x = np.random.rand(3, 2)# RNN参数
input_size = 2
hidden_size = 3
# 词表大小是输出维度 6
output_size = 6# 初始化权重矩阵
# 输入权重矩阵
Wx = np.random.rand(input_size, hidden_size)
# 隐藏权重矩阵
Wh = np.random.rand(hidden_size, hidden_size)
# 输出权重矩阵
Wout = np.random.rand(hidden_size, output_size)# 偏置
bh = np.zeros((hidden_size,))
bout = np.zeros((output_size,))# 激活函数
def tanh(x):return np.tanh(x)# 初始化隐藏状态
H_prev = np.zeros((hidden_size,))# 前向传播
# 时间步1 第一个单词
x1 = x[0, :]
H1 = tanh(np.dot(x1, Wx) + np.dot(H_prev, Wh) + bh)
Y = np.dot(H1, Wout) + bout# 时间步2 第二个单词
x2 = x[1, :]
H2 = tanh(np.dot(x2, Wx) + np.dot(H1, Wh) + bh)
Y2 = np.dot(H2, Wout) + bout# 时间步3 第三个单词
x3 = x[2, :]
H3 = tanh(np.dot(x3, Wx) + np.dot(H2, Wh) + bh)
Y3 = np.dot(H3, Wout) + bout# 输出结果
print("时间步1的隐藏状态:", H1)
print("时间步2的隐藏状态:", H2)
print("时间步3的隐藏状态:", H3)
print("时间步1的输出结果:", Y)
print("时间步2的输出结果:", Y2)
print("时间步3的输出结果:", Y3)
np.dot
是 NumPy 库中用于计算两个数组的点积(dot product)的函数。它的行为取决于输入数组的维度:
-
对于二维数组,
np.dot
计算的是矩阵乘积。这意味着如果 A 是一个 m×n 矩阵,B 是一个 n×p 矩阵,那么np.dot(A, B)
的结果将是一个 m×p 矩阵,其中每个元素由 A 中的行与 B 中的列之间的点积计算得出。 -
对于一维数组,
np.dot
计算的是向量的内积(inner product)。即,如果 a 和 b 是长度相同的两个一维数组,那么np.dot(a, b)
就是 a 和 b 对应元素相乘之后的结果之和。 -
A
是一维数组(shape 为(n,)
),B
是二维数组(shape 为(n, m)
),相当于:👉 将
A
视为一个行向量(1×n),与矩阵B
(n×m)相乘,得到一个 1×m 的结果(即 shape 为(m,)
的一维数组)。
2.3.2 基于 RNNCell 代码实现
import torch
from torch import nn# 创建数据 词嵌入之后的维度
x = torch.randn(10, 6, 5)hidden_size = 8# 利用rnncell创建RNN模型
class RNN(nn.Module):def __init__(self, batch_size, hidden_size, batch_flag=True):super(RNN, self).__init__()# nn.RNNCell 返回的是隐藏层 参数 第一个维度是输入维度(词嵌入后的维度) 第二个是隐藏层状态的维度self.rnncell = nn.RNNCell(input_size=5, hidden_size=hidden_size)# self.fc = nn.Linear(20, 10)self.batch_size = batch_sizeself.hidden_size = hidden_size# 标识符 类型为布尔类型self.batch_flag = batch_flag# 初始化隐藏状态def intialize_hidden(self):self.h_prev = torch.zeros(self.batch_size, self.hidden_size)return self.h_prevdef forward(self, x, h_prev=None):"""注意传递进来的数据格式 一般经过词嵌入格式(B,S,D) 但是在rnn中 需要的数据格式(S,B,D)词数开头:param x: 如果self.batch_flag是true就代表传入的数据格式是(B,S,D):return:"""if self.batch_flag:batch_size, seq_len, dim = x.size(0), x.size(1), x.size(2)# 数据处理为(S,B,D)x = x.permute(1, 0, 2)else:batch_size, seq_len, dim = x.size(0), x.size(1), x.size(2)hiddens = []if h_prev is None:init_hidden = self.intialize_hidden()else:init_hidden = h_prev# 循环遍历每一个时间步for t in range(seq_len):# 输入数据x_t = x[t]# 在t时刻获取隐藏层状态 封装的是H1 = tanh(np.dot(x1,Wx) + np.dot(H_prev,Wh)+bh)# 初始化隐藏层状态可以省略 init_hiddenhidden_t = self.rnncell(x_t, init_hidden)hiddens.append(hidden_t)# 所有的时间步堆叠成新的张量output = torch.stack(hiddens)# 处理数据if self.batch_flag:output = output.permute(1, 0, 2)return outputmodel = RNN(10, hidden_size)
output = model(x)
print(output.shape)
`nn.RNNCell` 本质上只返回隐藏状态,它没有单独的输出结果。一般在 `RNN` 中,隐藏状态既可以被视为输出,也可以通过一个线性层将隐藏状态转化为实际的输出。
2.3.3 基于 pytorch API 代码实现
官方代码细节:https://pytorch.org/docs/stable/_modules/torch/nn/modules/rnn.html#RNN
官方文档解释:https://pytorch.org/docs/stable/generated/torch.nn.RNN.html#rnn
import torch
from torch import nn# 初始数据
x = torch.rand(10, 6, 5)# 模型参数
hidden_size = 8
batch_size = 10
sen_len = 6
input_size = 5
size_len = 20class RNNModule(nn.Module):def __init__(self, hidden_size, batch_size, input_size):super(RNNModule, self).__init__()self.hidden_size = hidden_sizeself.batch_size = batch_sizeself.input_size = input_sizeself.rnn = nn.RNN(input_size=input_size, hidden_size=hidden_size,bidirectional=True, batch_first=True)# 如果bidirectional=True,全连接层输入需要 *2self.fc = nn.Linear(hidden_size*2, size_len)def forward(self, x):out, h_last = self.rnn(x) # h_last得到是最后一层print(out.shape)print(h_last.shape)out = self.fc(out)return outmodel = RNNModule(hidden_size=hidden_size, batch_size=batch_size, input_size=input_size)
out = model(x)
print(out.shape)
-
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_output
和state_final
代表着循环神经网络在当前时间步的输出和最终的隐藏状态。-
rnn_output
:代表当前时间步的 RNN 输出。对于很多序列模型而言,每个时间步都会有一个输出。这个输出可能会被用于下一时间步的计算,或者作为模型的最终输出。 -
state_final
:代表 RNN 模型在最后一个时间步的隐藏状态。这个隐藏状态通常被认为是对整个序列的编码或总结,它可能会被用于某些任务的最终预测或输出。
-
1. 单向、单层RNN
(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)
,即单一隐藏状态向量。这个隐藏状态可以用于下个时间步的预测或者作为整个序列的编码。
2. 双向、单层RNN
双向单层RNN(Recurrent Neural Network)是一种特殊类型的循环神经网络,它能够在两个方向上处理序列数据,即正向和反向。这使得网络在预测当前输出时,能够同时考虑到输入序列中当前元素之前的信息和之后的信息。双向单层RNN由两个独立的单层RNN组成,一个负责处理正向序列(从开始到结束),另一个负责处理反向序列(从结束到开始)。
主要特点:
-
双向处理: 最显著的特点是双向结构,使得模型能够同时学习到序列中某一点前后的上下文信息,这对于很多序列任务来说是非常有价值的,比如自然语言处理中的文本理解、语音识别等。
-
单层结构: “单层”指的是在每个方向上,网络结构只有一层RNN,即每个方向上只有一层循环单元(如LSTM单元或GRU单元)。虽然是单层的,但由于其双向特性,实际上每个时间点都有两个循环单元对信息进行处理。
-
定义一个双向循环神经网络(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为双向的,这意味着对于每个时间步,除了向前传递的信息外,还会考虑向后传递的信息,从而能够捕捉序列中前后依赖关系。
-
-
创建输入数据张量:
input = torch.randn(1, 2, 4)
这行代码生成了一个随机张量作为双向RNN的输入,其形状仍为
(batch_size, sequence_length, feature_size)
,即(1, 2, 4)
。这表示有一个样本(batch_size=1),序列长度为2,每个时间步有4个特征。 -
对输入数据进行前向传播:
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)
,分别对应正向和反向隐藏状态各一个。每个隐藏状态向量都是相应方向上整个序列信息的汇总。
-
2.4 RNN的训练方法——BPTT
BPTT(back-propagation through time)算法是常用的训练RNN的方法,其实本质还是BP算法,只不过RNN处理时间序列数据,所以要基于时间反向传播,故叫随时间反向传播。BPTT的中心思想和BP算法相同,沿着需要优化的参数的负梯度方向不断寻找更优的点直至收敛。综上所述,BPTT算法本质还是BP算法,BP算法本质还是梯度下降法,那么求各个参数的梯度便成了此算法的核心。
其中L是损失函数,对于多分类问题,我们使用的是多元交叉熵损失函数,也称为分类交叉熵。
再次拿出这个结构图观察,需要寻优的参数有三个,分别是U、V、W。与BP算法不同的是,其中W和U两个参数的寻优过程需要追溯之前的历史数据,参数V相对简单只需关注目前,那么我们就来先求解参数V的偏导数。
这个式子看起来简单但是求解起来很容易出错,因为其中嵌套着激活函数函数,是复合函数的求道过程。
RNN的损失也是会随着时间累加的,所以不能只求t时刻的偏导。
W和U的偏导的求解由于需要涉及到历史数据,其偏导求起来相对复杂,我们先假设只有三个时刻,那么在第三个时刻 L对W的偏导数为:
相应的,L在第三个时刻对U的偏导数为:
可以观察到,在某个时刻的对W或是U的偏导数,需要追溯这个时刻之前所有时刻的信息,这还仅仅是一个时刻的偏导数,上面说过损失也是会累加的,那么整个损失函数对W和U的偏导数将会非常繁琐。虽然如此但好在规律还是有迹可循,我们根据上面两个式子可以写出L在t时刻对W和U偏导数的通式:
整体的偏导公式就是将其按时刻再一一加起来。
2.5 RNN模型存在的问题
2.5.1 RNN中的梯度消失和爆炸
前面说过激活函数是嵌套在里面的,如果我们把激活函数放进去,拿出中间累乘的那部分:
我们会发现累乘会导致激活函数导数的累乘,进而会导致“梯度消失“和“梯度爆炸“现象的发生。至于为什么,我们先来看看sigmoid函数的函数图和导数图这是和tanh函数的函数图和导数图。
它们二者是何其的相似,都把输出压缩在了一个范围之内。他们的导数图像也非常相近,我们可以从中观察到,sigmoid函数的导数范围是(0,0.25],tanh函数的导数范围是(0,1],他们的导数最大都不大于1。
这就会导致一个问题,在上面式子累乘的过程中,如果取sigmoid函数作为激活函数的话,那么必然是一堆小数在做乘法,结果就是越乘越小。随着时间序列的不断深入,小数的累乘就会导致梯度越来越小直到接近于0,这就是“梯度消失“现象。其实RNN的时间序列与深层神经网络很像,在较为深层的神经网络中使用sigmoid函数做激活函数也会导致反向传播时梯度消失,梯度消失就意味消失那一层的参数再也不更新,那么那一层隐层就变成了单纯的映射层,毫无意义了,所以在深层神经网络中,有时候多加神经元数量可能会比多家深度好。
梯度爆炸(每天进一步一点点,N天后,你就会腾飞 每天堕落一点点,N天后,你就彻底完蛋)
之前说过我们多用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函数是,可以使网络收敛的更好。
2.5.2 远距离依赖
循环神经网络(RNN)是自然语言处理和其他序列数据任务中广泛使用的一种神经网络架构,它通过在网络中引入循环来处理序列数据,使得网络能够保持一定程度的序列信息。RNN的设计让它在处理如文本和语音等顺序数据时表现出色,因为它能够在每个时间步上接收输入,并保持一个内部状态,该状态包含了之前时间步的信息。
然而,RNN在处理长序列数据时面临一个重大挑战,即长期依赖性问题。长期依赖问题指的是当序列非常长时,RNN难以学习并保持序列早期时间步的信息。这是因为在RNN的训练过程中,使用反向传播算法进行梯度更新时,梯度往往会随着传播到更早的层而指数级衰减(梯度消失)或者指数级增长(梯度爆炸)。这导致了序列中较早时间步的信息对模型输出的影响变得微乎其微,从而使得模型难以学习到这些信息对序列后续部分的影响。
以一个具体的例子来说明,假设有一个叙述故事的序列:
“张三昨天下午本想去运动,但突然接到公司的急事,需要他紧急处理,随后他处理完去______________”。
在这个例子中,填空处的词与序列中较早出现的“运动”一词之间存在关联。然而,如果使用标准的RNN来预测空白处的词,由于长序列中的长期依赖问题,RNN可能无法有效地捕捉到“运动”这一关键信息,导致预测结果不准确。
长期依赖问题是RNN架构的一个根本性缺陷,它限制了RNN在处理具有重要长期依赖关系的长序列任务中的效能。因此,虽然RNN在处理较短序列时表现良好,但在涉及长距离时间依赖的复杂任务中,RNN的性能会大幅下降。
3. LSTM 模型
3.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 拥有三个门,来保护和控制细胞状态。
3.2 门控机制
3.2.1 遗忘门
遗忘门(Forget Gate):决定细胞状态中要保留的信息。它通过一个sigmoid函数来输出一个0到1之间的值,表示要忘记(0)或保留(1)的程度。1 代表“完全保留这个”,而 0 代表“完全摆脱这个”。
这个函数是遗忘门(forget gate)的公式,用于确定哪些信息应当从单元的状态中移除
-
[
]:这是一个连接的向量,包括前一时间步的隐藏状态
和当前时间步的输入
。它们被合并起来,以便遗忘门可以考虑当前的输入和先前的隐藏状态来做出决策。
-
:这是遗忘门的权重矩阵,用于从输入[
]中学习什么信息应该被遗忘。
-
:这是遗忘门的偏置项,它被加到权重矩阵和输入向量的乘积上,可以提供额外的调整能力,确保即使在没有输入的情况下遗忘门也能有一个默认的行为。
-
:这是sigmoid激活函数,它将输入压缩到0和1之间。在这里,它确保遗忘门的输出也在这个范围内,表示每个状态单元被遗忘的比例。
-
:这是在时间步 ( t ) 的遗忘门的输出,它是一个向量,其中的每个元素都在0和1之间,对应于细胞状态中每个元素应该被保留的比例。
函数的整体目的是使用当前输入和前一时间步的隐藏状态来计算一个门控信号,该信号决定细胞状态中的哪些信息应该被保留或丢弃。这是LSTM的关键特性之一,它允许网络在处理序列数据时学习长期依赖关系。
3.2.2 输入门
输入门(Input Gate):决定要从输入中更新细胞状态的哪些部分。它结合了输入数据和先前的细胞状态,利用sigmoid函数来确定更新的量,并通过tanh函数来产生新的候选值,然后结合遗忘门确定最终的更新。
-
输入门的激活
:
-
候选细胞状态
:
解释如下:
-
表示时间步 ( t ) 的输入门激活值,是一个向量。这个向量通过sigmoid函数产生,将值限定在 0 和 1 之间。它决定了多少新信息会被加入到细胞状态中。
-
是输入门的权重矩阵,用于当前时间步的输入
和前一个时间步的隐藏状态
。
-
[
] 是前一个隐藏状态和当前输入的串联。
-
是输入门的偏置向量。
-
是候选细胞状态,它是通过tanh函数产生的,可以将值限定在 -1 和 1 之间。它与输入门
相乘,决定了将多少新的信息添加到细胞状态中。
-
是控制候选细胞状态的权重矩阵。
-
是对应的偏置向量。
3.2.3 状态更新
在每个时间步,LSTM单元都会计算这两个值,并结合遗忘门的值更新细胞状态
。这样,LSTM能够记住长期的信息,并在需要的时候忘记无关的信息。
在计算新的细胞状态 () 时使用的更新规则:
这里:
-
是当前时间步的细胞状态。
-
是上一个时间步的细胞状态。
-
是遗忘门的激活值,通过sigmoid函数计算得到。它决定了多少之前的细胞状态应该被保留。
-
是输入门的激活值,也是通过sigmoid函数得到的。它决定了多少新的信息应该被存储在细胞状态中。
-
是当前时间步的候选细胞状态,通过tanh函数得到。它包含了潜在的新信息,可以被添加到细胞状态中。
符号 * 代表元素间的乘积,意味着和
分别与
和
相乘的结果然后相加,得到新的细胞状态
。这个更新规则使得LSTM能够在不同时间步考虑遗忘旧信息和添加新信息,是它在处理序列数据时记忆长期依赖信息的关键。
3.2.4 输出门
输出门(Output Gate):决定在特定时间步的输出是什么。它利用当前输入和先前的细胞状态来计算一个输出值,然后通过sigmoid函数来筛选。
这个函数描述了LSTM(长短期记忆)网络的输出门和隐藏状态的计算。
-
输出门
的计算:
;
-
隐藏状态 {h}_{t} 的计算:
具体来说:
-
是输出门的激活值。这是通过将前一时间步的隐藏状态
和当前时间步的输入
连接起来,并应用权重矩阵
以及偏置项
,然后通过sigmoid函数
来计算的。Sigmoid函数确保输出值在0和1之间。
-
是当前时间步的细胞状态,这是在之前的步骤中计算的。
-
是细胞状态的tanh激活,这个激活函数将值压缩到-1和1之间。这是因为细胞状态
可以有很大的值,而tanh函数有助于规范化这些值,使它们更加稳定。
-
是当前时间步的隐藏状态,通过将输出门
的值与细胞状态的tanh激活相乘来得到。这个元素级别的乘法(Hadamard乘法)决定了多少细胞状态的信息将被传递到外部作为当前的隐藏状态输出。
这种结构允许LSTM单元控制信息的流动,它可以通过输出门来控制有多少记忆单元的信息会被传递到隐藏状态和网络的下一个时间步。
3.2.5 LSTM总结
3.3 代码实现
注意:矩阵乘法规则:矩阵 A 和向量 v 做点积运算时,A 的列数必须与 v 的长度一致。
3.3.1 原生代码
多对一的任务:
import numpy as npclass LSTM:def __init__(self, input_size, hidden_size, out_size):""":param input_size: 词嵌入之后的向量维度:param hidden_size: 隐藏层的维度:param out_size: 输出的分类数"""self.input_size = input_sizeself.hidden_size = hidden_sizeself.out_size = out_size# 权重矩阵和偏置# 遗忘门self.W_f = np.random.randn(self.input_size + self.hidden_size, self.hidden_size)self.b_f = np.zeros((self.hidden_size,))# 输入门self.W_i = np.random.randn(self.input_size + self.hidden_size, self.hidden_size)self.b_i = np.zeros((self.hidden_size,))# 候选状态self.W_c = np.random.randn(self.input_size + self.hidden_size, self.hidden_size)self.b_c = np.zeros((self.hidden_size,))# 输出门self.W_o = np.random.randn(self.input_size + self.hidden_size, self.hidden_size)self.b_o = np.zeros((self.hidden_size,))# 输出层self.W_out = np.random.randn(self.hidden_size, self.out_size)self.b_out = np.zeros((self.out_size,))def tanh(self, x):return np.tanh(x)def sigmoid(self, x):return 1 / (1 + np.exp(-x))def forward(self, x):# 初始细胞状态和隐藏状态h_t = np.zeros((self.hidden_size,))c_t = np.zeros((self.hidden_size,))h_all = [] # 存储每个时间步的隐藏状态c_all = [] # 存储每个时间步的细胞状态for t in range(x.shape[0]):x_t = x[t] # 获取时间步# 拼接输入和隐藏状态x_t = np.concatenate((x_t, h_t), axis=0)# 遗忘门f_t = self.sigmoid(np.dot(x_t, self.W_f) + self.b_f)# 输入门i_t = self.sigmoid(np.dot(x_t, self.W_i) + self.b_i)# 候选状态c_ti = self.tanh(np.dot(x_t, self.W_c) + self.b_c)# 更新细胞状态c_t = f_t * c_t + i_t * c_ti# 输出门o_t = self.sigmoid(np.dot(x_t, self.W_o) + self.b_o)# 更新隐藏状态h_t = o_t * self.tanh(c_t)# 保留每个时间步的结果h_all.append(h_t)c_all.append(c_t)# 输出层 多对一的任务 最后一个时间步的隐藏层状态作为输入 进行分类y_t = np.dot(h_t, self.W_out) + self.b_outreturn y_t, h_all, c_all# 测试
if __name__ == '__main__':# 数据输入x = np.random.rand(3, 2) # 三个单词 每个单词两个维度 注意:在forward方法for循环中x.shapr[0]的表示是什么lstm = LSTM(input_size=2, hidden_size=5, out_size=10)y_t, h_all, c_all = lstm.forward(x)print(y_t)print(h_all)print(c_all)
多对多:
import torch
from torch import nnclass LSTMModule(nn.Module):def __init__(self, hidden_size, input_size, out_size):super(LSTMModule, self).__init__()self.hidden_size = hidden_sizeself.out_size = out_sizeself.input_size = input_sizeself.lstm = nn.LSTM(input_size=input_size, hidden_size=hidden_size, batch_first=True)self.fc = nn.Linear(hidden_size, out_size)def forward(self, x):out, (h_last, c_last) = self.lstm(x)# 多对多的输出out = self.fc(out)# 多对一的输出# out = torch.mean(out, dim=1)# out = self.fc(out)return out# 测试
if __name__ == '__main__':x = torch.randn(10, 6, 2)lstm = LSTMModule(input_size=2, hidden_size=5, out_size=8)y_t = lstm.forward(x)print(y_t.shape)
一对多:在一对多的任务中,尽管输入序列的大小只有一个,但模型仍然可以生成多个输出。这通常涉及以下几个方面:
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.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)
,那么hn
和cn
的形状将是(num_layers * num_directions, batch_size, hidden_size)
单层 LSTM:如果
num_layers
= 1,LSTM 网络将只有一个层。这意味着输入序列直接通过这个单层 LSTM 进行处理。多层 LSTM:如果
num_layers
> 1,LSTM 网络将有多层。输入序列首先通过第一层 LSTM,第一层的输出作为输入传递给第二层,以此类推,直到最后一层。 -
这些状态可以用于初始化下一个序列的 LSTM,特别是在处理长序列或多个批次的序列数据时。
3.4 序列池化
在自然语言处理 (NLP) 中,序列池化(sequence pooling)是一种将变长序列转换为固定长度表示的方法。这个过程对于处理可变长度的输入(如句子或文档)特别有用,因为许多深度学习模型(如全连接层)需要固定长度的输入。
序列池化的主要方法包括:
-
最大池化(Max Pooling):
-
对序列中的每个特征维度,选择该维度的最大值作为输出。
-
适用于突出序列中特定特征的最大激活值。
-
例如,如果输入是长度为 5 的序列,且每个时间步的特征维度为 10,最大池化会对每个特征维度取最大值,输出形状为
(batch_size, feature_size)
。
-
-
平均池化(Average Pooling):
-
对序列中的每个特征维度,计算该维度的平均值作为输出。
-
适用于希望保留序列中所有特征的总体信息。
-
同样,对于长度为 5 的序列,特征维度为 10,平均池化会对每个特征维度取平均值,输出形状为
(batch_size, feature_size)
。import torch from torch import nn# [b,s,d] x = torch.randn(10, 6, 5) # 调用平均池化方法 # pool = nnAvgPool1d(2, stride=2) pool = nn.AdaptiveAvgPool1d(1)# 调整形状去匹配池化输入 --->[b,d,s] x = x.permute(0, 2, 1) out = pool(x) out = out.squeeze(2) print(out) print(out.shape)
3.注意力池化(Attention Pooling):
-
使用注意力机制对序列进行加权平均,根据每个时间步的重要性分配权重。
-
适用于希望模型能够根据输入内容自适应地分配注意力权重。
-
注意力池化的实现通常涉及一个注意力权重计算模块和一个对这些权重进行加权平均的模块。
-
3.5 梯度消失
LSTM(长短期记忆网络)是一种特殊的RNN(循环神经网络),设计初衷就是为了解决传统RNN在长序列数据上训练时出现的梯度消失和梯度爆炸问题。然而,尽管LSTM相较于普通的RNN在处理长序列数据时表现得更好,但它仍然有可能在某些情况下出现梯度消失和梯度爆炸的问题。原因可以归结为以下几个方面:
3.5.1 梯度消失问题
梯度消失(Vanishing Gradient)主要在于反向传播过程中,梯度在多层传播时会逐渐减小,导致前面层的参数更新非常缓慢,甚至完全停滞。LSTM尽管通过门控机制(输入门、遗忘门和输出门)缓解了这个问题,但仍然可能出现梯度消失,特别是在以下情况下:
- 长期依赖问题:如果序列特别长,即使是LSTM也可能无法有效地记住早期的信息,因为梯度会在很长的时间步长内持续衰减。
- 不适当的权重初始化:如果权重初始化不合理,可能会导致LSTM的各个门在初始阶段就偏向于某种状态(如过度遗忘或完全记住),从而影响梯度的有效传播。
- 激活函数的选择:尽管LSTM通常使用tanh和sigmoid激活函数,这些函数在某些输入值下可能会导致梯度的进一步缩小。
3.5.2 梯度爆炸问题
梯度爆炸(Exploding Gradient)则是在反向传播过程中,梯度在多层传播时会指数级增长,导致前面层的参数更新过大,模型难以收敛。LSTM在以下情况下可能出现梯度爆炸:
-
过长的序列长度:即使是LSTM,在非常长的序列上仍然可能遇到梯度爆炸,因为梯度在反向传播时会不断累积,最终可能变得非常大。
-
不适当的学习率:过高的学习率可能会导致梯度爆炸,因为参数更新的步伐太大,使得模型参数偏离最优解。
-
不适当的权重初始化:与梯度消失类似,权重初始化也可能导致梯度爆炸。如果初始权重过大,梯度在反向传播过程中会不断放大。
解决方法:为了解决或缓解LSTM中的梯度消失和梯度爆炸问题,可以采取以下措施:
-
梯度裁剪:在每次反向传播后,将梯度裁剪到某个阈值范围内,防止梯度爆炸。
-
适当的权重初始化:使用标准的初始化方法,如Xavier初始化或He初始化,确保权重在初始阶段不至于过大或过小。
-
调整学习率:选择合适的学习率,或者使用自适应学习率算法,如Adam、RMSprop等,动态调整学习率。
-
正则化技术:如L2正则化、Dropout等,防止过拟合并平滑梯度。
-
批归一化(Batch Normalization):在网络层之间使用批归一化技术,可以加速训练并稳定梯度。
尽管LSTM通过其结构在一定程度上缓解了梯度消失和爆炸问题,但理解并应用这些技术和方法仍然是确保模型训练稳定和高效的关键。