循环神经网络
一、感知机与神经网络
1 感知机
1.1感知机的概念
感知机(Perceptron),又称神经元(Neuron,对生物神经元进行了模仿)是神 经网络(深度学习)的起源算法,可以接收多个输入信号,产生一个输出信号
1.2感知机的功能
作为分类器(多个离散值)/回归器(一个连续值),实现自我学习
实现逻辑运算,包括逻辑和(AND)、逻辑或(OR)
1.3感知机的缺陷
感知机的局限在于无法处理“异或”问题(非线性问题)
结构简单,完成的功能有限
多层感知机
多层感知机(Multi-Layer Perceptron ,MLP):通过多个感知机组合来解决异或问题
2 神经网络
2.1神经网络
和多层感知机有什么区别?
多层前馈神经网络(Multi-layer Feedforward Neural Networks):将多个感知机连在一起,形成一个级联网络结构。
前馈是指将前一层的输出作为后一层的输入的逻辑结构,每一层神经元仅与下一层的神经元全连接,同一层内或者跨层之间的神经元彼此不连接。
2.2神经网络的功能
用近似定理去模拟目标函数,前馈神经网络在理论上可近似解决任何问题
近似定理:对于任意复杂度的连续波莱尔可测函数(Borel Measurable Function)f,仅仅需要一个隐含层,只要这个隐含层包括足够多的神经元,前馈神经网络使用挤压函数(Squashing Function)作为激活函数,就可以以任意精度来近似模拟f。如果想增加f的近似精度,单纯依靠增加神经元的数目即可实现。
通用近似定理:
神经网络结构的另一个进化方向:纵深,减少单层的神经元数量,增加神经网络的层数,这会显著提升神经网络系统的学习性能
多层神经网络计算公式:
3 MLP
MLP:多层感知机(Multilayer Perceptron),最基本的人工神经网络模型之一,由多个神经元组成,每个神经元层与下一层全连接。
通常由一个输入层、一个或多个隐藏层以及一个输出层组成。
输入层(Input Layer):接收原始数据输入的层。
隐藏层(Hidden Layers):对输入数据进行非线性变换的层,使得MLP可以学习非线性关系。
输出层(Output Layer):产生最终预测结果的层。根据问题不同有不同的激活函数,回归问题用线性激活函数。
MLP通过反向传播算法进行训练,利用梯度下降等优化算法来最小化预测值与真实标签之间的误差。它在许多领域都得到了广泛的应用,如图像识别、自然语言处理、预测分析等。
MLP和感知器模型效果对比
原数据-第一线性模块的输出-第一非线性模块输出-第二线性模块输出改变了空间
感知器不行的原因在于:没有额外的层来修正数据的形状。
MLP实现
MLP模型-模型实例化-测试
4 DNN模型
DNN(Deep Neural Network,深度神经网络)是一类人工神经网络,包含多个隐藏层。
与传统的浅层神经网络(eg只有一个隐藏层的多层感知器MLP)相比,DNN通过增加隐藏层的数量,可以学习和表示数据中更加复杂和抽象的特征。
关键特点:
深度:DNN的“深度”指的是网络中隐藏层的数量。一个网络层数越多,其能够捕捉的特征越复杂。
层次化特征表示:DNN通过逐层抽象和变换数据,逐步提取低级、中级和高级特征。例如,在图像识别任务中,前几层可能提取边缘和纹理,中间几层可能提取局部形状和图案,后几层则可能提取整体的物体结构和类别信息。
非线性变换:每个隐藏层通常由线性变换和非线性激活函数组成。这些非线性激活函数(如ReLU、sigmoid、tanh等)使得DNN可以学习非线性映射。
组成部分:
输入层:接收原始数据输入。
隐藏层:包括多个隐藏层,每个层包含若干个神经元,这些神经元通过线性变换和非线性激活函数
处理输入数据。
输出层:生成最终的预测结果。输出层的形式和激活函数取决于具体任务(如分类或回归)。
4.1训练过程
DNN的训练过程通常使用反向传播(backpropagation)算法和优化算法(如梯度下降、Adam等)。反向传播算法通过计算每个参数的梯度来最小化损失函数,从而调整网络的权重和偏置。
4.2DNN存在的问题
标准DNN假设数据之间是独立的,在处理前后依赖、序列问题(语音、文本、视频等)力不从心。
文本数据对文字前后次序非常敏感,和图像数据差别非常大。
传统的深度神经网络(DNN)由于其结构的固有限制无法捕捉前置时间的信息,通过在网络中引入新的参数和结构来处理时序数据。
传统的MLP通过不断学习调节Win和Wout来适应任务。
在传统的多层感知器(MLP)中引入时间特征,通过堆叠的方式传递前置时间的信息,引入新的参数Ws调节前置时间信息的传递。为简化计算,所有的时间权重参数都可以使用同一个参数矩阵Ws。网络在每次更新中都包含了上一时刻的状态St-1和权重参数Ws,能够更好地捕捉时序数据中的前置时间信息,有效地拓展了传统DNN网络的应用范围,能够处理更复杂的时序数据任务,例如时间序列预测、语音识别等。
传统的DNN网络可以表示为:
$$
S=f(W_{in}X+b)
$$
引入时间特征后的网络更新规则:
$$
S_t =f(W_{in}X+W_sS_{t-1}+b)
$$
5 激活函数
激活函数(activation function):在神经网络中,将输入信号的总和转换为输出信号的函数
将多层感知机输出转换为非线性,使得神经网络可以任意逼近任何非线性函数,是区别多层感知机和神经网络(使用连续函数作为激活函数)的区别。
5.1常见激活函数
5.1.1阶跃函数(Step Function)
一种特殊的连续时间函数,从0跳变到1的过程
$$
f(x)=\begin{cases}1 (x\geq 0)\\0 (x<0) \end{cases}
$$
5.1.2sigmoid函数
Logistic函数,用于隐层神经元输出,取值范围(0,1),可以将一个实数映射到(0,1)区间用来做二分类
$$
f(x)=\frac{1}{1+e^{-x}}
$$
优点:平滑,易于求导
缺点:激活函数计算量大,反向传播求误差梯度时求导涉及除法容易出现梯度消失的情况,无方完成深层网络的训练
5.1.3tanh双曲正切函数
$$
tanh(x)=\frac{1-e^{-2x}}{1+e^{-2x}}
$$
优点:平滑,易于求导,输出均值为0,收敛速度比sigmoid快,可以减少迭代次数
缺点:梯度消失
常用于NLP中
5.1.4ReLU(Rectified Linear Units,修正线性单元)
$$
f(x)=\begin{cases}x (x> 0)\\0 (x≤0) \end{cases}
$$
优点:更加有效率的梯度下降以及反向传播,避免了梯度爆炸和梯度消失的问题
计算简单
缺点:≤0的部分梯度为0,会导致部分神经元死亡
常用于图像
5.2 softmax函数
softmax函数将多分类的输出数值转化为累计和为1的相对概率,常用于神经网络的输出层。
ReLU一般用在隐藏层,主要作用是引入非线性使得网络能够处理复杂的任务,来激活金额调整中间特征
softmax一般用在输出层,将模型输出转化为概率分布,用于分类任务的最终决策,预测每个类别的概率
二、RNN模型
1 先导
循环神经网络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处理一个序列时,它会在每个时间步接受一个输入,并更新其隐藏状态。这个更新过程依赖于当前的输入和之前的隐藏状态,
$$
S_{t}=f(Ux_{t}+WS_{t-1})
$$
$$
S_{t}:表示在时间步t的隐藏状态;x_{t}:当前时间步的输入;U和W:输入到隐藏状态和隐藏状态到隐藏状态的权重矩阵
$$
f:非线性函数,例如tanh或者ReLU,引入非线性特性并帮助网络学习复杂的模式
RNN的输出在每个时间步也可以计算出来,这依赖于当前的隐藏状态:
$$
O_t= g(V_{st} )
$$
其中 Ot是时间步t的输出,V是从隐藏状态到输出层的权重矩阵,g是另一个非线性函数,常用于输出层
通过这种逐步处理序列并在每一步更新隐藏状态的方式,RNN能够在其内部维持一个随时间变化的“记忆”。这使得它能够对之前序列元素的信息做出响应,并据此影响后续的输出。这种特性对于诸如语言模型、文本生成、语音识别等许多序列处理任务至关重要。
2.3RNN内部结构
tanh(x):
优点:平滑,易于求导;输出均值为0,收敛速度要比sigmoid快,可以减少迭代次数
缺点:梯度消失
用途:常用于NLP中
2.4 RNN模型输入输出关系对应模式
通过改变RNN的结构,调整输入输出的数量和形式,可以让它适应各种不同的任务
一对一/一对多:接受单个输入并产生一系列输出,常用于看图说话,分类问题。RNN结构首先对输入图片进行编码,然后根据这个编码连续生成文本序列中的词语。
多对一:接受一系列输入并产生单个输出,常用于文本分类和情感分析。模型需要阅读和理解整个文本(一系列输入),然后决定文本属于哪个类别(单个输出)。在图片生成的上下文中,这种结构可以通过分析一系列的特征或指令来生成单个图片输出。
多对多:既接受一系列输入,也产生一系列输出。在输入输出均为序列的任务中非常有用,例如机器翻译,词性标注,小说生成。模型需要读取一个语言的文本(一系列输入),然后生成另一种语言的对应文本(一系列输出)。
3 RNN实现
batch size(批量大小):在一次前向传播或反向传播过程中同时处理的样本数量。
Sequence Length (序列长度):输入数据中每个样本的连续时间步(或词、字符)的数量。
Input Size (输入大小):每个时间步输入向量的特征维度。在处理文本时,如果每个词都被表示为一个固定维度的向量,那么input size就是这个词向量的维度。
Hidden Size (隐藏层大小):RNN单元内部隐藏状态(Hidden State)的维度。
在每个时间步,RNN都会根据当前输入和上一时间步的隐藏状态来计算新的隐藏状态,新隐藏状态的维度就是hidden size。
隐藏层大小的选择会影响到模型的学习能力和表示能力,同时也影响到模型的计算资源消耗。
在决定隐藏层大小时,通常需要结合具体任务的特点、数据集规模、计算资源等因素进行合理选择,并通过交叉验证、网格搜索等方式进行超参数调优,以找到最优的隐藏层大小以及其他超参数组合。
Output Size (输出大小):与特定任务相关。
对于一般的RNN,每个时间步的输出大小与hidden size相同,即输出也是一个隐藏状态维度的向量。
在分类任务中,最后一层可能通过一个全连接层映射到类别数目,这时最后一个时间步的输出大小可能是类别数目的维度。
如果是多层或双向RNN,输出也可能经过额外的处理(如拼接、池化等),最终的输出大小会根据具体应用需求来确定。
4 RNN的训练方法——BPTT
BPTT(back-propagation through time)算法是常用的训练RNN的方法,本质还是BP算法(梯度下降法),要基于时间反向传播,故叫随时间反向传播。沿着需要优化的参数的负梯度方向不断寻找更优的点直至收敛,求各个参数的梯度便成了此算法的核心。
5 RNN模型存在的问题
5.1RNN中的梯度消失和爆炸
随着时间序列的不断深入,小数的累乘就会导致梯度越来越小,直到接近于0,这就是梯度消失现象。
在深层神经网络中,有时候多加神经元数量可能会比多加深度好。
tanh函数相对于sigmoid函数来说梯度较大,收敛速度更快且引起梯度消失更慢。
是sigmoid函数还有一个缺点,Sigmoid函数输出不是零中心对称。sigmoid的输出均大于0,这就使得输出不是0均值,称为偏移现象,这将导致后一层的神经元将上一层输出的非0均值的信号作为输入。关于原点对称的输入和中心对称的输出,网络会收敛地更好。
解决“梯度消失“的方法主要有:
1、选取更好的激活函数——一般选用ReLU函数作为激活函数,ReLU函数的左侧导数为0,右侧导数恒为1,这就避免了“梯度消失“的发生。但恒为1的导数容易导致梯度爆炸,但设定合适的阈值可以解决这个问题。还有一点就是如果左侧横为0的导数有可能导致把神经
元学死,不过设置合适的步长(学习率)也可以有效避免这个问题的发生。
2、改变传播结构——LSTM结构
sigmoid函数的缺点:
1、导数值范围为(0,0.25],反向传播时会导致“梯度消失“。tanh函数导数值范围更大,相对好一点。
2、sigmoid函数不是0中心对称,tanh函数是,可以使网络收敛的更好。
5.2远距离依赖
循环神经网络(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中的梯度消失或爆炸等问题。
细胞状态是LSTM的中心概念,它可以被视为一条信息的高速公路,沿着序列传递信息。细胞状态的设计使得信息可以几乎不受阻碍地在序列间流动,这解决了传统RNN中梯度消失问题导致的信息传递障碍。
LSTM有通过精心设计的称作“门”的结构来去除或者增加信息到细胞状态的能力。
门是一种让信息选择式通过的方法。他们包含一个sigmoid神经网络层和一个pointwise**乘法操作。**
Sigmoid层输出0到1之间的数值,描述每个部分有多少量可以通过。
0代表“不许任何量通过”
1代表“允许任何量通过”
LSTM 拥有三个门,来保护和控制细胞状态。
2 门控机制
2.1遗忘门
遗忘门(Forget Gate):决定细胞状态中要保留的信息。它通过一个sigmoid函数来输出一个0到1之间的值,表示要忘记(0)或保留(1)的程度。1 代表“完全保留这个”,而 0 代表“完全摆脱这个”。
函数的整体目的是使用当前输入和前一时间步的隐藏状态来计算一个门控信号,该信号决定细胞状态中的哪些信息应该被保留或丢弃。这是LSTM的关键特性之一,它允许网络在处理序列数据时学习长期依赖关系。
2.2输入门
输入门(Input Gate):决定要从输入中更新细胞状态的哪些部分。它结合了输入数据和先前的细胞状态,利用sigmoid函数来确定更新的量,并通过tanh函数来产生新的候选值,然后结合遗忘门确定最终的更新。
2.3状态更新
这个更新规则使得LSTM能够在不同时间步考虑遗忘旧信息和添加新信息,是它在处理序列数据时记忆长期依赖信息的关键。
2.4 输出门
输出门(Output Gate):决定在特定时间步的输出是什么。它利用当前输入和先前的细胞状态来计算一个输出值,然后通过sigmoid函数来筛选。
这种结构允许LSTM单元控制信息的流动,它可以通过输出门来控制有多少记忆单元的信息会被传递到隐藏状态和网络的下一个时间步。
3 代码实现
4 序列池化
在自然语言处理 (NLP) 中,序列池化(sequence pooling)是一种将变长序列转换为固定长度表示的方法。这个过程对于处理可变长度的输入(如句子或文档)特别有用,因为许多深度学习模型(如全连接层)需要固定长度的输入。
序列池化的主要方法包括:
1.最大池化(Max Pooling):
对序列中的每个特征维度,选择该维度的最大值作为输出。
适用于突出序列中特定特征的最大激活值。
nn.AdaptiveMaxPool1d(1) 中的 1 表示输出的特征长度。自适应最大池化层会根据输入的特征长度自动调整池化的参数,以确保输出的特征长度为 1 。这意味着,无论输入的特征长度是多少,经过这个层后,输出的特征长度始终会被压缩到 1 。
2.平均池化(Average Pooling):
对序列中的每个特征维度,计算该维度的平均值作为输出。
适用于希望保留序列中所有特征的总体信息。
3.注意力池化(Attention Pooling):
使用注意力机制对序列进行加权平均,根据每个时间步的重要性分配权重。
适用于希望模型能够根据输入内容自适应地分配注意力权重。
注意力池化的实现通常涉及一个注意力权重计算模块和一个对这些权重进行加权平均的模块
5 梯度消失
LSTM(长短期记忆网络)是一种特殊的RNN(循环神经网络),设计初衷就是为了解决传统RNN在长序列数据上训练时出现的梯度消失和梯度爆炸问题。然而,尽管LSTM相较于普通的RNN在处理长序列数据时表现得更好,但它仍然有可能在某些情况下出现梯度消失和梯度爆炸的问题。
5.1梯度消失问题
梯度消失(Vanishing Gradient)主要在于反向传播过程中,梯度在多层传播时会逐渐减小,导致前面层的参数更新非常缓慢,甚至完全停滞。L
长期依赖问题:如果序列特别长,即使是LSTM也可能无法有效地记住早期的信息,因为梯度会在很长的时间步长内持续衰减。
不适当的权重初始化:如果权重初始化不合理,可能会导致LSTM的各个门在初始阶段就偏向于某种状态(如过度遗忘或完全记住),从而影响梯度的有效传播。
激活函数的选择:尽管LSTM通常使用tanh和sigmoid激活函数,这些函数在某些输入值下可能会导致梯度的进一步缩小。
5.2梯度爆炸问题
梯度爆炸(Exploding Gradient)则是在反向传播过程中,梯度在多层传播时会指数级增长,导致前面层的参数更新过大,模型难以收敛。
过长的序列长度:即使是LSTM,在非常长的序列上仍然可能遇到梯度爆炸,因为梯度在反向传播时会不断累积,最终可能变得非常大。
不适当的学习率:过高的学习率可能会导致梯度爆炸,因为参数更新的步伐太大,使得模型参数偏离最优解。
不适当的权重初始化:与梯度消失类似,权重初始化也可能导致梯度爆炸。如果初始权重过大,梯度在反向传播过程中会不断放大。
5.3解决办法
梯度裁剪:在每次反向传播后,将梯度裁剪到某个阈值范围内,防止梯度爆炸。
适当的权重初始化:使用标准的初始化方法,如Xavier初始化或He初始化,确保权重在初始阶段不至于过大或过小。
调整学习率:选择合适的学习率,或者使用自适应学习率算法,如Adam、RMSprop等,动态调整学习率。
正则化技术:如L2正则化、Dropout等,防止过拟合并平滑梯度。
批归一化(Batch Normalization):在网络层之间使用批归一化技术,可以加速训练并稳定梯度。
四、中文情绪分析案例
import os import torch import random import numpy as np import torch from torch.utils.data import TensorDataset,DataLoader from sklearn.model_selection import train_test_split import torch.nn as nn from tqdm import tqdm mydict={} code=1 #文件路径 data_file = os.path.relpath(os.path.join(os.path.dirname(__file__),'data/hotel_discuss2.csv')) # 字典路径 dict_file = os.path.relpath(os.path.join(os.path.dirname(__file__),'data/dict.txt')) # 编码后存放的路径 encoding_file = os.path.relpath(os.path.join(os.path.dirname(__file__),'data/encoding.txt')) # 需要过滤的字符(例如标点符号) puncts = '。' # 设备切换 device = "cuda" if torch.cuda.is_available() else "cpu" # 学习率 lr = 1e-4 # 训练轮次 epochs = 40 with open(data_file,'r',encoding='utf-8-sig') as f: lines=f.readlines() for line in lines: line=line.strip() for word in line: if word not in puncts: if word not in mydict: mydict[word]=code code +=1 mydict['<unk>']=code with open(dict_file,'w',encoding='utf-8-sig') as f: f.write(str(mydict)) def load_dict(): with open(dict_file, 'r', encoding='utf-8-sig') as file: return eval(file.read()) new_dict = load_dict() with open(data_file,'r', encoding='utf-8-sig') as f1: # 写入编码文件 with open(encoding_file, 'w', encoding='utf-8-sig') as f2: lines=f1.readlines() #将原始数据的每行数据进行编码 for line in lines: line = line.strip() label = line[0] content = line[2:] print(label) print(content) for ch in content: print(ch) if ch not in puncts: f2.write(str(new_dict[ch])) f2.write(',') f2.write('\t'+str(label)+'\n') #存放训练数据 values = [] #存放真实数据 labels = [] #预设每次输入的长度是256,不够就补0,超过的部分则不要 max_lens = 256 with open(encoding_file,'r',encoding='utf-8-sig') as f: lines=f.readlines() random.shuffle(lines) for line in lines: data, label = line.split('\t') val = [int(i) for i in data[0:-1].split(',')] #评论长度不够256的就补0 for i in range(max_lens - len(val)): val.append(0) val = val[:max_lens]#评论长度超过256的只保留前面256个 values.append(val) labels.append(int(label)) print(values) print(labels) values = np.array(values, dtype='int64') labels = np.array(labels, dtype='int64') # 数据集的划分 train_x, test_x, train_y, test_y = train_test_split(values, labels, test_size=0.2, random_state=1) # 将数据转换为tensor train_data = TensorDataset(torch.from_numpy(train_x), torch.from_numpy(train_y)) test_data = TensorDataset(torch.from_numpy(test_x), torch.from_numpy(test_y)) train_data = DataLoader(dataset=train_data, batch_size=64, shuffle=True) test_data = DataLoader(dataset=test_data, batch_size=64, shuffle=True) class LSTM(nn.Module): def __init__(self, input_size, output_size, embedding_dim=512,hidden_size=512): super(LSTM, self).__init__() self.hidden_size = hidden_size self.embedding = nn.Embedding(input_size, embedding_dim, device=device) self.lstm = nn.LSTM(embedding_dim, hidden_size, num_layers=4) self.fc = nn.Linear(hidden_size, output_size) def forward(self, x): embed = self.embedding(x) permute_ = embed.permute(1, 0, 2) # h0 = torch.zeros(1, permute_.size(0), self.hidden_size).to(device) # c0 = torch.zeros(1, permute_.size(0), self.hidden_size).to(device) out, _ = self.lstm(permute_) out = self.fc(out[-1, :, :]) return out #get_dict_size=len(new_dict) model = LSTM(len(new_dict),2) # model.train() model.to(device) optimizer = torch.optim.AdamW(model.parameters(), lr=0.001) loss_fc = nn.CrossEntropyLoss() loss_old = 100 # 初始化保存训练和测试结果的列表 train_result = [] test_result = [] # 初始化保存训练和测试损失、精度的列表 train_losses = [] train_accuracies = [] test_losses = [] test_accuracies = [] # 循环训练每个周期 for epoch in range(1, epochs + 1): # 创建一个进度条以显示训练数据的处理进度 pbar = tqdm(train_data) # 初始化每个周期的总损失和总准确度 loss_all = 0 acc_all = 0 # 遍历训练数据集的每一个批次 for step, (x, y) in enumerate(pbar): # 将标签数据移动到指定的设备(如GPU) y = y.to(device) # 将输入数据移动到指定的设备(如GPU) x = x.to(device) # 计算每个序列的实际长度(非零元素的数量) # lengths = torch.sum(x != 0, dim=-1).cpu().long() # 使用模型进行前向传播,得到输出 out = model(x) # 计算损失 loss = loss_fc(out, y) # 反向传播计算梯度 loss.backward() # 更新模型参数 optimizer.step() # 将梯度归零 optimizer.zero_grad() # 累加损失 loss_all += loss.item() # 计算当前批次的平均损失 loss_time = loss_all / (step + 1) # 计算当前批次的准确度 acc = torch.mean((y == torch.argmax(out, dim=-1)).float()) # 累加准确度 acc_all += acc # 计算当前批次的平均准确度 acc_time = acc_all / (step + 1) # 构建进度条的描述信息 s = "train => epoch:{} - step:{} - loss:{:.4f} - loss_time:{:.4f} - acc:{:.4f} - acc_time:{:.4f}".format(epoch, step, loss, loss_time, acc, acc_time) # 设置进度条的描述 pbar.set_description(s) # 将描述信息添加到训练结果列表中 train_result.append(s + "\n") # 记录当前epoch的训练损失和精度 train_losses.append(loss_time) train_accuracies.append(acc_time.item()) # #每一轮比较,保留准确率最高的一个 # if train_acc<train_acc1: # train_acc=train_acc1 # torch.save(model.state_dict(), f"model.pkl") # 禁用梯度计算,因为我们只需要前向传播进行预测 with torch.no_grad(): # 创建一个进度条以显示测试数据的处理进度 pbar = tqdm(test_data) # 初始化每个周期的总损失和总准确度 loss_all = 0 acc_all = 0 # 遍历测试数据集的每一个批次 for step, (x, y) in enumerate(pbar): # 将标签数据移动到指定的设备(如GPU) y = y.to(device) # 将输入数据移动到指定的设备(如GPU) x = x.to(device) # 计算每个序列的实际长度(非零元素的数量) lengths = torch.sum(x != 0, dim=-1).cpu().long() # 使用模型进行前向传播,得到输出 out = model(x) # 计算损失 loss = loss_fc(out, y) # 累加损失 loss_all += loss.item() # 计算当前批次的平均损失 test_loss_time = loss_all / (step + 1) # 计算当前批次的准确度 acc = torch.mean((y == torch.argmax(out, dim=-1)).float()) # 累加准确度 acc_all += acc # 计算当前批次的平均准确度 acc_time = acc_all / (step + 1) # 构建进度条的描述信息 s = "test => epoch:{} - step:{} - loss:{:.4f} - loss_time:{:.4f} -acc:{:.4f} - acc_time:{:.4f}".format(epoch, step, loss, test_loss_time, acc, acc_time) # 设置进度条的描述 pbar.set_description(s) # 将描述信息添加到测试结果列表中 test_result.append(s + "\n") # 记录当前epoch的测试损失和精度 test_losses.append(test_loss_time) test_accuracies.append(acc_time.item()) # 将训练结果写入文件 with open(f"train_result.txt", "w") as f: f.writelines(train_result) # 将测试结果写入文件 with open(f"test_result.txt", "w") as f: f.writelines(test_result) # 保存模型,如果当前测试损失小于之前记录的最小损失 if loss_old > test_loss_time: loss_old = test_loss_time torch.save(model.state_dict(), f"model.pkl")