循环神经网络
感知机与神经网络
感知机
生物神经元

感知机的概念
该模型(Perceptron)也可称为人工神经元(Neuron),其灵感来源于生物神经系统中的神经元结构。该模型可被视为神经网络体系中最早期也是最基础的算法之一。该模型于1958年被康奈尔大学心理学教授弗兰克·罗森布拉特提出,在其研究中他试图模仿生物神经系统中的信息传递机制。该模型能够接收多个输入信号,并能生成一个输出信号。

感知机的功能
- 作为分类器/回归器,实现自我学习

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

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

多层感知机
多层感知机(Multilayer Perceptron, 简称 MLP)主要是一种前馈的人工神经网络模型,在实际应用中通常包含三个或更多层次:包括输入、隐藏以及输出层次。每个层次都由许多神经单元构成,并且除了输入层次之外,在其他层次中的每一个单元都会通过加权连接到下一个层次的所有单元。
利用多层感知机组合来解决感知机无法解决异或(非线性问题)

神经网络
由于其结构较为单一 ,感知机所能执行的功能相对较为有限。因此可以通过串联多个感知机来构建一个能够完成复杂任务的级联网络架构——被称为多层前馈神经网络(Multi-layer Feedforward Neural Networks)。所谓‘前馈’即指从前一层输出传递给后一层输入的逻辑架构。具体而言,在每一层次中各神经元仅与下一层次的所有神经元实现全连接配置。然而在同一层次内部各节点间互不相连,并且不同层次之间的节点也互不相连——这即是典型的全连接设计特点。

神经网络的功能
在1989年时,奥地利学者库尔特·霍尼克(Kurt Hornik)等人为正式发表论文提供了证明。该研究明确指出:对于具有任意复杂程度的连续波莱尔可测函数f(Borel Measurable Function),仅需一个隐含层即可实现模拟效果。具体而言,在该隐含层中包含足够的神经元数量时,在采用挤压函数作为激活函数的前提下(Squashing Function),前馈神经网络将能够以任意精度地模拟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]],
24. [[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模型(多个隐藏层)
深度人工神经网络(即 Deep Neural Network 或者 深度神经网络)被定义为一类包含多个隐含层的人工神经网络模型。相对于传统的仅有单一层的多层感知器 MLP 来说,这类模型通过增加隐含层数量能够捕捉并表达数据中更为复杂和抽象的特征
关键特点
- 深度:DNN中的"深度"是指网络中所包含的隐藏层层级数 。当网络层数增加时,其能够捕获的数据特征也会更加复杂与深入 。
- 层次化特征表示:DNN通过逐步提取数据特征 ,实现了从基础到高层次信息的构建过程。具体而言,在图像识别任务中,前几个隐藏层主要负责提取边缘、纹理与局部图案 等基础信息;随后的几层则能够整合这些低层特征,形成更复杂的局部形状与整体物体结构与类别信息 。
- 非线性变换:每个隐藏层通常由一系列的线性运算与非线性激活函数组成 。这些非线性激活函数(如ReLU、sigmoid与tanh等)赋予了DNN强大的能力——能够学习和建模复杂的非线性关系。
组成部分
- 输入层:作为接收原始数据输入的有效手段。
- 隐藏层:由多个隐藏层构成,每一个隐藏层都包含一定数量的神经元。这些神经元通过线性变换与非线性激活函数共同作用于输入数据。
- 输出层:负责生成最终预测结果。输出层的形式与所采用的具体任务(例如分类或回归)直接相关。
数学公式

其中S表示网络的输出,X是输入数据,

是输入层到隐藏层的权重参数,b 是偏置项,f是激活函数。
改进过程

训练过程
DNN的训练过程通常利用**反向传播(backpropagation)机制(通过计算各个参数对应的梯度值以使损失函数达到最小值,并更新网络中的权重和偏置参数)与优化算法体系(包括梯度下降法、Adam等方法)**相结合。
存在问题
基于标准深度神经网络(DNN)的方法假设样本之间具有相互独立性,在面对包含前后依赖性的场景(如语音识别、自然语言处理及视频分析)时往往表现出较差的效果。这些场景中的前后依赖性问题会导致模型难以有效学习这些复杂关系。此外,其模型架构的固有局限性使得对时间滞后信息的捕捉能力不足。
改进方法

其中,

表示在时间 t 的输出,

表示前一时刻的状态 ,

它表示为涉及前置时间信息的权重参数。这样我们就能观察到,在每一次更新中都包括了上一时刻的状态。

和权重参数**

改进神经网络图
|

|

|
|---|---|
|MLP|DNN|
激活函数
激活函数:在神经网络领域中,在将输入信号之和转换为输出信号的过程中所使用的函数被称为激活函数(activation function)。
使用原因
激活函数通过映射多层感知机的输出至非线性域,并使神经网络具备在理论上完美地近似任何非线性函数的能力。这些方法在实际应用中表现出了极强的能力,并广泛应用于各种非线性模型中。
一个多层网络若采用连续函数作为激活函数的多层网络,则被称作"神经网络"。否则则被称作"多层感知机"。因此, 激活函数是区分"多层感知机"与"神经网络"的关键因素。
常见激活函数
阶跃函数
被称为阶跃函数的数学模型。其本质特征是作为一类具有独特动态特性的数学模型。其变化过程呈现出一种突变式的跳跃性转变。该函数仅取两个离散值。

sigmoid函数
该Sigmoid函数也被称作Logistic function,在处理隐藏层神经元的输入信号时具有重要应用价值;其输出值范围在(0,1)区间内,并能够将任意实数值映射至(0,1)区间内;该方法也被广泛应用于二分类问题中;数学表达式如下:
该Sigmoid函数也被称作Logistic function,在处理隐藏层神经元的输入信号时具有重要应用价值;其输出值范围在(0,1)区间内,并能够将任意实数值映射至(0,1)区间内;该方法也被广泛应用于二分类问题中;数学表达式如下:

**
优点:平滑、易于求导
缺点在于激活函数的计算开销较大,在反向传播过程中求取误差梯度时其中导数计算涉及除法运算,在反向传播过程中容易导致梯度消失的问题从而导致无法有效地完成深层网络模型的训练过程

tanh双曲正切函数
优点:光滑性好且易于计算梯度以加快训练速度,在输出端强制使均值设为零的情况下(即均值设为零),该算法相较于sigmoid函数而言具有更快的收敛速度,并且这种特性使得模型在训练过程中所需迭代次数得以显著降低
缺点:梯度消失
用途:常用于NLP中

ReLU(Rectified Linear Units, 修正线性单元)
该方法显著提高了梯度下降与反向传播的效率,并成功防止了梯度爆炸和消失现象;运算过程较为简洁。
缺点:小于或等于0的部分梯度为0
用途:常用于图像

softmax函数
softmax函数是一种多分类场景下的输出激活方式,在神经网络模型中能够实现将各类别的输入特征映射为对应概率值,并且这些对应的各个概率值总和等于1。在深度学习模型的设计中这一特性被广泛应用于神经网络的输出层环节。


注意
- 在神经网络中广泛使用的激活函数包括Softmax函数与ReLU(Rectified Linear Unit),它们各自承担着不同的作用与应用场景。
- ReLU常用于隐藏层中以引入非线性特性,并通过激活节点来调整中间特征的表现。
- 而ReLU在输出层则主要负责将数据映射为非负值,并通过这种非线性变换增强模型对复杂数据的学习能力。
- Softmax则专注于输出层的任务设定,在分类模型中将其输入转化为概率分布形式以支持分类任务的决策过程。
- 在分类模型架构中,默认情况下最后一层通常会生成一组无约束的得分值(logits),而Softmax的作用就是将这些得分转化为可解释的概率值。
- 因此,在构建神经网络时需要根据具体的任务需求合理选择合适的激活函数组合。
RNN模型
循环神经网络
时序数据
不同类型的变量或数据及任务类型中对数据顺序及其前后关系的理解程度直接影响结果
RNN原理
Recurrent Neural Network(RNN)是一种神经网络系统
注释:其中数字1-6分别代表上述六条技术指标
在自然语言处理领域中,RNN主要应用于语义模型训练、自动机翻译以及情感识别等多种任务。通过有效利用词语间的上下文关系来增强对文本语义的理解与把握。此外,在时间序列数据的研究方向上也发挥着重要作用,在金融市场的股价走势预测以及气象部门的天气状况预报等方面都有显著的应用价值。
通过掌握序列数据的特征和走向 ,RNN能够生成有价值的信息来辅助决策。然而,在传统 RNN中存在局限性:它们难以捕捉长期依赖关系、容易遭遇梯度消失或爆炸等问题。针对这些问题出现了改进型 RNN架构:包括长短期记忆网络(LSTM)以及门控循环单元(GRU)。这些架构设计上能够更加高效地提取长期依赖信息,并展现出训练过程中的优异稳定性。
模型架构
循环神经网络(RNN)属于深度学习中的一种特殊架构,并特意设计用于处理序列数据类型的信息。这些数据包括时间序列或自然语言文本等复杂结构的数据形式。其关键特性在于能够在逐个元素地解析序列的同时维持一个内部状态(即记忆),这种状态能够有效地捕获前面各要素所包含的重要信息。基于这一机制的设计使其特别适合应对那些当前输出结果与前一阶段所得信息紧密相关的任务类型。
在RNN的经典架构中,该网络借助特定的反馈机制将信息从前一个处理阶段传递给后续阶段。这一关键组件通常被称作「隐含层状态变量」或简称为「隐含状态变量」。这种机制不仅起到记忆的作用,并且能够有效保存与已处理序列元素相关的各种信息特征。
当RNN在处理序列时,在每个时间步接收一个输入(逐步计算每个特征)后会更新隐层状态]这一过程基于当前输入及前一时刻的状态]从而使其具备记忆能力,并能利用过去的信号(参考CNN原理)。通过以下数学公式可简明扼要地描述这一过程:

在这个公式中,

表示在时间步 t 的隐藏状态 ,

是代表当前时间步的信息 ,其中U为从输入层到隐含层的权重矩阵 ,而W则表示隐含层间的连接权重矩阵 。其通常采用双曲正切函数或ReLU激活函数 ,以引入非线性特性并提升模型的学习能力 。这些变换操作有助于网络提取复杂的特征模式 。
RNN的输出在每个时间步也可以计算出来 ,这依赖于当前的隐藏状态 :

其中

代表时间步t的输出结果,V代表从隐藏状态到输出层的权重参数矩阵,g通常是一个常用的非线性激活函数,作为标准配置的一部分,RNN属于传统的人工神经网络架构模型,其核心特征在于一个称为"记忆单元"(memory cell)的关键组件,该记忆单元捕获了系统在每个时间步的信息,在处理下一个输入样本时,需要考虑这一记忆单元的作用,随着时间的推移,记忆单元会不断更新其内部状态信息,从而反映系统当前的状态特征
模型结构图
- 加tanh双曲正切激活函数构成RNN的内部结构

RNN模型输入输出关系对应模式
通过调整RNN的架构及其输入与输出的规模和类型 ,可以让其更好地适应不同任务的需求。以下列举了几种常见的RNN结构调整方案,并分别对应不同类型的适用任务场景:
- 一对多(One-to-Many):这种RNN处理单个输入并生成一系列输出 。这种模式常用于语音识别等任务中即给定一张图片(单个输入),RNN通过编码机制将其转换为一段描述性文本(一系列输出)。在此过程中RNN的架构被设计成先从输入图片提取特征然后基于此特征连续生成文本序列中的每个词。
- 多对一(Many-to-One):与一对多相对应的是一个多到一的RNN架构接收一系列输入并生成单个输出 。这种架构适用于如文本分类与情感分析等任务其中模型需整合全部输入信息并对整个文本进行分析最终决定其类别归属(单个输出)。在图像生成领域该架构可通过对一系列特征或指令进行分析来实现单一图像的合成。
- 多对多(Many-to-Many):这种架构的RNN既接收并处理一系列输入也生成对应的一系列输出 在需要同步处理与生成的任务中表现尤为出色例如机器翻译中模型需读取源语言文本并通过解码器逐步生成目标语言译文;另一个应用案例是小说创作系统基于给定的前提条件或开篇段落模型能够持续产出故事情节的发展。

RNN代码实现
参数解读
1.Batch Size (批量大小):指的是在一次前向传播或反向传播过程中同时处理的样本数量 。
- Sequence Duration (序列持续时间):表示输入数据中每个样本所包含的连续时间段的数量(包括单词、字符或其他单位)。
3.Input Size (输入大小):指每个时间步输入向量的特征维度。
4.Hidden Size (隐藏层大小)
- 隐藏层大小指的是模型内部的状态向量维度。
- 在每个时间步,RNN会基于当前输入以及上一时间步的状态来 更新新的状态 ,而新状态的空间维数即为hidden size。
- 根据实验需求和模型复杂度的具体要求,人们可以选择不同的隐藏层规模,但这一规模并非由特定算法推导得出。
- 隐藏层规模的选择 直接影响着模型的学习能力与表示能力,同时也决定了其计算开销。
- 实践中,采用 较低 的隐维可能限制模型的表现,而较高隐维则可能导致过拟合 训练耗时增加 等问题。
- 在确定隐维大小时,通常需综合考虑具体任务的特点 数据量多少 和计算资源等因素来进行权衡选择 同时通过交叉验证与网格搜索等方式优化超参数组合 最终寻找到最佳隐维设置及其他最优超参数组合
5.Output Size (输出大小)
- Output dimensions typically correlate with specific task requirements (e.g., the number of categories in classification tasks).
- For a standard RNN, the output dimension at each time step is equal to the hidden state dimension, meaning it's a vector representing the hidden state.
- In classification tasks, the final layer often employs a fully connected layer to map features to category counts. As a result, the output dimension at the last time step might correspond to the number of categories.
- When dealing with more complex RNN architectures like stacked or bidirectional networks, additional processing steps (such as concatenation or pooling) are commonly applied. Consequently, the output dimension is determined by the specific application requirements.
- In a simple single-layer unidirectional RNN model, calculating output dimensions is straightforward.
- If our goal is to obtain hidden state representations at each time step without any further transformations, then each time step's output dimension will be equal to the specified hidden layer size.
案列解读
在进行文本分类任务时,在每个单词已经被嵌入为一个100维的向量的情况下,请注意以下参数设置:其中序列长度设置为50(即最长的句子包含50个单词),批量大小设定为32(一次处理32个句子)。此外,请记住将隐藏层大小设置为128(即网络内部采用的隐藏层宽度)。
- 输入空间(input space): 在每一个时间段内(对应于一个单词的位置),其对应的输入向量具有固定的长度为100。因此,在批处理的情况下(batch_size=32),整个输入张量的空间结构为 batch_size × sequence_length × input_size 即 (32×50×100)。
- 隐藏层运算机制: RNN模型会对每一个时间段内的输入进行深度学习运算,并基于上一时间段的状态生成当前时间段的状态表示。具体而言,在本模型中设定初始隐藏状态为128维向量,则该时段内操作后的隐态向量也是长度为128的一维向量表示。
- 输出空间结构: 在不增加额外全连接层的情况下,默认输出结果与最终隐态向量相同均为128维长度的一维向量表示。在序列数据的整体处理过程中,则会形成三维的空间张量结构 output_volume = batch_size × sequence_length × hidden_dimension 即 (32×50×128)。
如果后续需要执行分类任务,请注意例如这是一个二元分类问题,在RNN模型中通常会采用以下步骤处理:首先获取最后一个时间步的状态向量(此处为128维),然后将其通过一个全连接层(Dense Layer)映射到类别数目对应的维度空间上(如本例中为二维),最终输出形状将变为(32, 2),表示每个样本对应的二维概率分布
原生代码
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_size
self.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 代码实现
官方文档细节:PyTorch 2.6版本中的RNN模块
官方文档解释: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]],
23. [[ 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模型来完成对逆序输入信号的前馈计算。
主要特点
在双工处理方面:其核心优势在于采用独特的双工架构设计,在完成主要功能的同时还能兼顾辅助功能的实现。这种设计使得系统不仅能够高效运行主程序逻辑,还能灵活应对各种附加需求的处理工作。这种特性在众多需要精确捕捉前后关系的任务中都展现出巨大的价值
单层
结构图

代码解读
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(time-bounded backward propagation)是一种用于训练循环神经网络(RNN)的方法。实际上它本质上属于BP(backpropagation)算法系列。由于RNN处理的是时序数据特征,在实现这一特性时需要采用时序反向传播机制,并因此得名随时间反向传播方法。其核心思想与标准BP算法一致,在优化参数的过程中始终沿着优化方向即负梯度方向不断探索更优的目标点直至收敛过程完成。综上所述,BPTT与传统的BP方法在本质上有异曲同工之处,BP方法的本质依然是一个基于梯度下降原理的基本优化框架,因此求取各参数对应的梯度值成为了这一类优化方法的核心目标

在其中,在数学表达式中,
L被定义为损失函数;
针对多分类任务(即所谓的多标签分类问题),我们采用多元交叉熵损失函数作为目标;
该方法也被称为分类交叉熵损失函数。
多元交叉熵损失函数

RNN模型存在的问题
RNN中的梯度消失和爆炸

通过观察Sigmoid function 的曲线图形及其求导图形与Tanh function 的曲线图形及其求导图形进行对比分析发现两者极为相似 在一定程度上将输出值限制在一个特定范围内 而从其中能够观察到的是:Sigmoid function 的其导数值落在区间 (0, 0.25]内 而tanh function 则其导数值则覆盖区间 (0,1) 。
这样就会出现一个问题:在上述公式进行连乘运算时(或:当对上述公式进行连续性乘法运算时),如果选择sigmoid函数作为激活函数时(或:当采用sigmoid函数作为激活函数的情况下),必然会导致多个小于1的小数值进行相乘(或:必然会导致一系列小于1的小数参与连续相乘)。随着时间序列逐步深入发展(或:随着时间序列不断深入推进),这些小数值的连乘效应将导致最终计算出的梯度逐渐趋向于零值(或:最终所得出的梯度值将趋向于零)。这就是我们常说的‘梯度消失’现象(或:这就是所谓的‘梯度消失’现象)。
实际上RNN的时间序列结构与较深的神经网络架构具有许多相似之处。在较深的 neural network 中选择 sigmoid 函数作为 activation function 可能会导致在 backpropagation 过程中出现 gradient vanishing 的问题。当出现 gradient vanishing 现象时意味着该 layer 的 parameter 在整个 training 过程中将不再被优化更新。因此这一层次隐含单元的作用仅限于纯粹的信息传递功能。所以,在某些情况下适当增加单个隐藏 layer 中的 neural 元数量可能比单纯增加 layers 数量更为有效。
而值得注意的是,在RNN结构中与深层神经网络存在显著差异的地方在于:其参数均为共用而非独立设置,并且在计算某一层次上的梯度时需要综合考虑当前层次及之前所有层次的影响(即为各层次梯度之总和)。这种观点是正确的,在理论层面是成立的。然而,在实际应用中如果我们仅利用较浅层次获得的梯度信息来进行对更深层次参数(尤其是共享参数)进行更新,则会面临以下问题:即仅凭有限信息无法获得全局最优解。
激活函数的选择
虽然tanh与sigmoid函数都等价于一系列小数值相乘,在这种运算模式下仍然容易出现‘梯度消失’现象。相比之下,在同样情况下运行时的表现更为稳定——其收敛速度更快,并且更容易避免梯度消失。然而需要注意的是,在其他方面可能存在不足——例如其输出值并不围绕零点对称分布(即其所有Sigmoid单元的激活值都为正值)。由于所有Sigmoid单元的激活值都为正值,则意味着整体上存在偏差(偏差现象)。这意味着整体上存在偏差(偏差现象),这将导致下一层神经元接收到带有偏差信号作为输入——从而使得下一层神经元接收到带有偏差信号作为输入(即带有一定的偏移)。这种特性可能会干扰网络的学习过程并降低其性能表现——而这一特性也可能是导致网络难以训练的原因之一:即当网络试图学习具有某种特定属性的数据时,在这种情况下可能会遇到较大的困难——即当网络试图学习具有某种特定属性的数据时,在这种情况下可能会遇到较大的困难
而ReLU函数能够有效地解决这一问题,并且其左侧一阶导数值恒等于零而右侧一阶导数值保持恒定为1

sigmoid函数的缺点
- 导数值范围在(0, 0.25]区间内,在反向传播过程中会引发"梯度消失"现象。相比之下,tanh函数的导数范围更为宽广,其表现更为优异。
- sigmoid函数不具备关于原点的对称性,而tanh函数则不同地具备这种特性。因此,相比于sigmoid函数,tanh函数更适合用于神经网络中的激活层设计。
远距离依赖
循环神经网络(RNN)主要采用于自然语言处理以及其他类型的序列数据任务中,并被视为一种经典的神经网络架构。该架构通过在其中引入循环机制来处理序列数据,在此过程中能够持续地保持关于序列的信息。其设计使其在处理文本以及语音等顺序数据时展现出卓越的效果。因为其能够在每个时间步骤接收输入并维持内部状态,在每个时刻都能够基于当前输入与之前的记忆进行信息处理。这种状态机制使得其内部的状态包含了前一时间段内的相关信息。
然而,在处理长序列数据方面存在重大挑战。长期依赖性问题是指当序列长度显著增长时,在RNN中出现的学习困难现象。具体而言,在训练过程中,RNN遇到的问题是难以获取并维持早期时间步的信息。这一现象发生的原因在于,在反向传播过程中用于梯度更新的过程中,在训练过程中使用反向传播算法进行梯度更新时,在训练过程中使用反向传播算法进行梯度更新的过程中,在反向传播过程中的计算机制会导致某些情况下导致某些情况下导致某些情况下导致某些情况下导致某些情况下导致某些情况下导致某些情况下导致某些情况下信息的重要性逐渐减弱。具体来说,在这种情况下会导致这些信息的重要性逐渐减弱的情况有两种:一种是在这种计算机制下会导致这些信息的重要性逐渐减弱的情况是由于计算机制中出现的数值衰减现象;另一种则是由于数值放大现象所引起的相应后果。这两种情况共同作用的结果是使得这些较早时间步的信息对整个模型的输出结果影响甚微。因此必须解决这一问题才能更好地掌握这些信息对未来事件的影响程度。这一问题与网络架构的设计密切相关
长期依赖现象是RNN架构的一个本质缺陷, 它制约了RNN在处理具有重要长期依赖关系的长序列任务中的效能。因此, 虽然RNN适用于处理较短序列的任务, 但在涉及长距离时间依赖关系的复杂场景中, RNN的表现将显著降低
LSTM 模型
LSTM概述
长短期记忆网络(Long Short-Term Memory, LSTM)是一种专门设计来应对长期依赖问题的循环神经网络体系结构。它在处理序列数据中尤其擅长处理长序列数据,并展现出其独特的优势。该方法能够有效地记录和记忆序列中的长期依赖性,在多个研究领域中得到了广泛应用,并成为解决复杂问题的重要工具之一
LSTM的主要概念是引入了一个被称为"细胞状态"(cell state)的概念。这一状态能够灵活地在时间步长内动态地增减相关信息。其结构包含三个核心调节机制。借助这三个调节机制的作用,在处理长时间序列数据时,LSTM表现出更强的能力去捕捉长期依赖关系,并且能够有效避免传统RNN模型中常出现的问题如梯度消失或爆炸的情况。
所有Recurrent Neural Networks (RNNs)均被构建为具有神经网络重复结构链的形式,在标准RNN架构中,默认采用一个单一的双曲正切层作为这一重复结构。

LSTM模型也遵循链式结构模式,在处理序列数据时展现出独特的特性。然而其重复单元采用了独特的架构设计,在信息传递上与传统RNN有所不同。神经网络层并非单一构造,在模型架构中整合了多个功能模块:包括四个主要组件(黄框标识的四个激活函数部分),其中三个采用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)的公式,在神经网络架构中用于控制单元状态信息的更新与遗忘
:这是一个连接的向量 ,包括前一时间步的隐藏状态 和当前时间步的输入 。它们被合并起来,以便遗忘门 可以考虑当前的输入 和先前的隐藏状态 来做出决策。

:这是遗忘门的权重矩阵 ,用于从输入
中学习什么信息应该被遗忘 。
*

该层的遗忘偏置项...被用于将权重矩阵与输入向量进行相乘运算,并为这一过程提供了额外的调节能力。即使未提供任何输入信息,在此情况下遗忘门仍能维持其默认的行为模式。

它是逻辑门限单元(Logistic Unit)或S型激活函数(Sigmoid Activation Function) ,该函数通过数学运算将输入值映射至区间(0,1),从而实现对输入信号的非线性转换。在此过程中,在 forget gate 的输出也限定在这一范围内这一机制的作用下,在每个状态单元中都有一个确定的比率决定了信息会被遗忘的程度。

这对应于时间步t的位置,在此处计算出的是遗忘门所释放的内容。这一结果可以被视为一个向量,在其各个分量上取值范围限定为0到1之间。每一个分量的具体数值反映了细胞状态相应维度信息被持续保留的程度。

该函数的主要目标是通过当前输入与前一个时间步长处的状态向量来生成一个控制门限的信号(gates),这些控制门限决定了哪些特定的信息应当被模型所记住或遗忘。这一机制使得网络能够通过处理序列数据来捕捉长期依存关系(long-term dependencies)。
输入门
输入机制(Input Mechanism)


表示时间步 ( t ) 的输入门控制信号 ,是一个...型激活向量。该向量经sigmoid函数处理后其取值范围被限制在0至1之间,并决定了新信息以怎样的比例融入细胞状态中。

代表的是负责处理当前时刻输入特征以及上一时刻状态信息的权重参数矩阵

。
是前一个隐藏状态和当前输入的串联 。

是输入门的偏置向量 。
*

该文本代表了候选细胞的状态特征,在tanh函数的作用下形成了特定的数值范围,并在此基础上与输入门进行交互作用以实现信息的更新过程

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

是对应的偏置向量 。
状态更新

在每个时间段内(即每个时间步),LSTM单元都会计算出两个关键数值,并通过控制遗忘门f_t的值来调整当前时刻的状态信息。从而实现对历史信息的持久记忆,并根据需求舍弃多余的信息。
在计算新的细胞状态 ( ) 时使用的更新规则:


是当前时刻的候选细胞状态 ,经过tanh函数处理得到。该单元承载了潜在的新信息,并可被整合进细胞状态中。

是上一个时间步的细胞状态 。
*

该参数为遗忘门的activation value ,由sigmoid函数进行计算得出 。该activation value 用于决定在多大程度上应保持前一时刻的cell state信息

由输入门控制的激活程度也可视为通过sigmoid函数进行非线性变换得到的一个重要参数;它决定了新信息在一定程度上会被储存在细胞状态中。
符号 * 代表元素间的乘积,意味着

和

分别与

和

通过计算各元素的乘积后再进行累加运算,最终获得这一时刻的**细胞活性水平**.这一机制使得LSTM具备在各个时间步分别考虑到遗忘旧数据并及时补充新数据的能力,并且这种特性使其成为捕捉序列中长期相关性的重要手段.
输出门
输出门(Output Gate):负责确定在某个时间点产生的输出内容。它通过综合当前输入信号以及上一时间步的细胞状态来推导出一个潜在的输出值,并随后应用sigmoid函数来进行选择性地抑制或释放这一潜在输出值以生成最终的单元状态更新。


其输出门的激活值即为其计算结果。具体而言,该值通过将前一个时间步的状态信息与当前时刻输入信号相结合,并运用相应的权重矩阵进行处理而得到

以及偏置项

,然后通过sigmoid函数** 来计算的。Sigmoid函数确保输出值在0和1之间。
*

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

代表细胞状态的tanh激活函数会将数值限定在**-1至1之间**。这是因为这种激活函数能够有效反映细胞状态的变化范围。

可以有很 大的值,而tanh函数有助于规范化这些值,使它们更加稳定。
*

属于当前时间步的隐含状态具体来说,则是将输出门控制变量与细胞内态经过tanh激活函数后的结果进行相乘运算这种操作实际上决定了多少细胞信息会被传递到外部以构成当前隐藏状态
代码
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 # 输入特征维度 2
self.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]:3
for 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 代码实现
对于 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,则表示该LSTM层是双向结构。
- out记录了LSTM在每个时间步的状态信息,并可进一步用于后续处理(如传递给全连接层)。
(_)或(hn, cn)
*hn 被定义为最后一步的时间隐状态。
*cn 被定义为最后一步的时间细胞状态。
*假设输入x具有形状(seq_len, batch_size, input_size),则h和c的状态将具备具体形状(num_layers * num_directions, batch_size, hidden_size)。
当 layers数目为1时,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):
# # lstm_model = SimpleLSTM(10,20,2)
super(SimpleLSTM, self).__init__()
self.hidden_size = hidden_size # 20
self.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 = 10
hidden_size = 20
output_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)
序列池化
在自然语言处理领域中,序列池化方法通过将不同长度的输入序列映射为统一维度向量来提取特征。这一技术特别适用于处理长度不一的数据(如句子或文档),因为它能够提供恒定长度的表示。由于深度学习模型通常要求固定的输入维度(例如全连接层),因此这种池化方法在实际应用中具有重要意义。
最大池化(Max Pooling)
-
在序列中的每一个特征维度上选择该维的最大值作为输出结果。
-
特别适用于突出显示特定特征向量中的最大激活值。
-
如果输入是一个长度为5的序列,并且其中每个时间步对应的**特征向量大小均为10,则经过最大池化处理后,在每一维上都会选取其最大激活值,并最终得到一个大小为(batch_size, feature_size)的结果。
-
nn.AdaptiveMaxPool1d(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)通过其门控机制(包括输入门、遗忘门和输出门)在一定程度上缓解了这一问题,但仍有可能出现梯度消失现象,在以下特定条件下尤其明显:
- 长时依赖问题 :当序列特别长时(long sequences),即使基于长短时记忆单元(LSTM)的设计初衷是为了有效处理长期信息(long-term information),但由于梯度在较长的时间跨度内持续下降(gradually diminish over longer time spans),其仍然可能会面临长期依赖性的不足(insufficient capability to handle long-range dependencies)。
- 不当的权重初始化 :如果权重初始化选择不当(poor weight initialization),可能会导致各个门单元(gate units)在初始阶段倾向于某种特定的状态(例如过度遗忘或完全记忆),从而严重限制了梯度的有效传播效果(effectiveness of gradient propagation)。
- 激活函数的选择 :尽管LSTM模型通常采用tanh和sigmoid函数作为激活函数,在某些输入值下这些非线性激活函数可能会进一步缩小梯度的传播效果(reduce the effectiveness of gradient propagation)。
梯度爆炸问题
该模型被称为长短时记忆网络(LSTM),它是一种独特的循环神经网络(RNN),其主要目的是解决常规RNN在处理长序列数据时容易出现的梯度消失与梯度爆炸问题。然而,在多数情况下该模型确实能够更高效地处理长序列数据,并且虽然相比普通RNN具有更好的性能表现但它仍有可能在某些特定场景下出现上述问题
梯度消失问题
在反向传播过程中( Exploding Gradient ),当神经网络在多个层级进行参数更新时( 多层传播时会呈指数速度上升 ),会导致上一层的参数更新幅度过大( 模型难以达到稳定状态 )。LSTM可能会在以下几种情况下导致梯度爆炸:
- 过长的序列长度: 即使是基于长短时记忆网络(LSTM)的模型,在面对过长的输入序列时也可能面临导数趋于无限增大的问题,在反向传播过程中由于误差项沿着计算图传播并逐步积累, 最终可能导致导数值变得异常庞大。
- 不当的学习速率: 过高的学习速率可能导致训练过程中的不稳定, 因为模型参数以过快的速度被更新, 这种情况往往会使得优化后的模型无法准确地逼近最优状态。
- 不当的权重初始化: 类似于避免出现消失性导数的情况, 合理选择权重初始化值也很重要. 如果初始权重设定过大, 在反向传播的过程中导数值将经历几何级数地放大过程。
解决方法
- 梯度裁剪机制:完成反向传播后,在计算过程中将所有梯度限制在特定范围内以防止出现异常值。
- 适当的方法:采用标准化的参数初始化策略(如Xavier或He初始化),这有助于避免出现过大的数值情况。
- 学习率调节:根据训练需求选择合适的初始学习率参数;若有必要可采用自适应优化器算法(如Adam、RMSprop)来动态调节学习速率。
- 正则化措施:通过应用L2范数惩罚项以及Dropout技术等手段来实现模型的正则化;这不仅有助于防止模型过度拟合还能提升训练过程中的数值稳定性。
- 批归一化技术的应用:在各层之间实施批归一化操作;这不仅可以加速网络收敛过程还能有效稳定各层之间的梯度变化状态。
