政安晨:【深度学习处理实践】(五)—— 初识RNN-循环神经网络

RNN(循环神经网络)是一种在深度学习中常用的神经网络结构,用于处理序列数据与传统的前馈神经网络不同,RNN通过引入循环连接在网络中保留了历史信息。

RNN中的每个神经元都有一个隐藏状态,它会根据当前输入前一个时间步的隐藏状态来计算输出和下一个时间步的隐藏状态。

这种循环的结构使得RNN可以在序列数据的处理中考虑到上下文信息。对于每个时间步,RNN都会根据当前输入和前一个时间步的隐藏状态来计算当前时间步的输出和隐藏状态,然后将当前时间步的隐藏状态传递给下一个时间步。

RNN的具体计算可以用如下的公式表示: h_t = f(W_hh * h_{t-1} + W_xh * x_t) y_t = g(W_hy * h_t)

其中,h_t表示当前时间步的隐藏状态,h_{t-1}表示前一个时间步的隐藏状态,x_t表示当前时间步的输入,y_t表示当前时间步的输出。

W_hh、W_xh和W_hy分别表示隐藏状态到隐藏状态输入到隐藏状态隐藏状态到输出的权重矩阵。f和g是激活函数

RNN的优势在于可以处理变长的序列数据,并且能够捕捉到序列数据中的长期依赖关系。它被广泛用于自然语言处理、语音识别、时间序列分析等领域。

然而,标准的RNN在处理长序列时容易出现梯度消失或梯度爆炸的问题,为了解决这些问题,后续的研究提出了一些改进的RNN结构,如长短期记忆网络(LSTM)和门控循环单元(GRU)。这些改进的RNN结构在一定程度上缓解了梯度问题,并取得了更好的性能。

政安晨的个人主页政安晨

欢迎 👍点赞✍评论⭐收藏

收录专栏政安晨的机器学习笔记

希望政安晨的博客能够对您有所裨益,如有不足之处,欢迎在评论区提出指正!


理解RNN

目前我们见过的所有神经网络(比如密集连接网络和卷积神经网络)都有一个主要特征,那就是它们都没有记忆它们对每个输入都是单独处理的,在输入之间没有保存任何状态。这样的神经网络要想处理数据点的序列或时间序列,需要一次性将整个序列输入其中,即将整个序列转换为单个数据点。比如我们在密集连接网络示例中就是这样做的:将5天的数据展平为一个大向量,然后一次性处理。这种网络叫作前馈网络(feedforward network)

与此相对,当阅读这个句子时,你是在逐字阅读(或者更确切地说,是在逐行扫视),同时会记住前面的内容。这让你可以流畅地理解这个句子的含义。智能生物处理信息是渐进式的,保存一个关于所处理内容的内部模型,这个模型是根据过去的信息构建的,并随着新信息的进入而不断更新。

RNN采用相同的原理(不过是一个极其简化的版本)。它处理序列的方式是:遍历所有序列元素,同时保存一个状态(state),其中包含与已查看内容相关的信息。实际上,RNN是一种具有内部环路(loop)的神经网络,如下图所示(RNN:带有环路的神经网络):

在处理两个彼此独立的序列(比如批量中的两个样本)之间,RNN的状态会被重置,所以你仍然可以将一个序列看作单个数据点,即神经网络的单个输入。不同的是,这个数据点不再是一步处理完,相反,神经网络内部会对序列元素进行循环操作。

为了更好地解释环路和状态的概念,我们来实现一个简单RNN的前向传播这个RNN的输入是一个向量序列,我们将其编码成尺寸为(timesteps,input_features)的2阶张量。这个RNN对时间步进行遍历,在每个时间步t,它都会考虑t的当前状态和t的输入(形状为(input_features,)),并对二者计算得到t的输出。

然后,我们将下一个时间步的状态设置为上一个时间步的输出。对于第一个时间步,上一个时间步的输出没有定义,所以它没有当前状态。因此,我们将状态初始化为全零向量这叫作神经网络的初始状态。

RNN伪代码如下所示:

# t的状态
state_t = 0# 对序列元素进行遍历
for input_t in input_sequence:output_t = f(input_t, state_t)# 上一次的输出变为下一次迭代的状态state_t = output_t

你甚至可以给出具体的f函数:它是从输入和状态到输出的变换,其参数包括两个矩阵(W和U)和一个偏置向量,如下代码所示(更详细的RNN伪代码)

它类似于前馈网络中密集连接层所做的变换。

state_t = 0
for input_t in input_sequence:output_t = activation(dot(W, input_t) + dot(U, state_t) + b)state_t = output_t

为了将这些概念解释清楚,我们用NumPy来实现简单RNN的前向传播,代码如下所示:

import numpy as np# 输入序列的时间步数
timesteps = 100 # 输入特征空间的维度
input_features = 32# 输出特征空间的维度
output_features = 64# 输入数据:随机噪声,仅作为示例
inputs = np.random.random((timesteps, input_features))# 初始状态:全零向量
state_t = np.zeros((output_features,))# (本行及以下2行)创建随机的权重矩阵
W = np.random.random((output_features, input_features))U = np.random.random((output_features, output_features))b = np.random.random((output_features,))
successive_outputs = []# 对输入和当前状态(上一个输出)进行计算,得到当前输出。这里使用tanh来添加非线性(也可以使用其他激活函数)
for input_t in inputs:# input_t是形状为(input_features,)的向量output_t = np.tanh(np.dot(W, input_t) + np.dot(U, state_t) + b)# 将输出保存到一个列表中successive_outputs.append(output_t)# 更新网络状态,用于下一个时间步state_t = output_t# 最终输出是形状为(timesteps, output_features)的2阶张量
final_output_sequence = np.stack(successive_outputs, axis=0)

RNN实现起来很简单。总而言之,RNN是一个for循环,它重复使用循环上一次迭代的计算结果,仅此而已。当然,你可以构建不同的RNN,它们都能满足上述定义。这个例子展示的只是最简单的RNN。RNN的特征在于时间步函数,比如本例中的下面这个函数,如下图所示:

output_t = np.tanh(np.dot(W, input_t) + np.dot(U, state_t) + b)

一个简单的RNN,沿时间展开

注意 本例的最终输出是一个形状为(timesteps, output_features)的2阶张量,其中每个时间步长是循环在t时间步长的输出。输出张量中的每个时间步t都包含入序列中时间步0到t的信息,即关于过去的全部信息。在多数情况下,你并不需要这个完整的输出序列,而只需要最后一个输出(循环结束时的output_t),因为它已经包含了整个序列的信息。

Keras中的循环层

上面的NumPy简单实现对应一个实际的Keras层——SimpleRNN层。

不过,二者有一点小区别:SimpleRNN层能够像其他Keras层一样处理序列批量,而不是像NumPy示例中的那样只能处理单个序列。也就是说,它接收形状为(batch_size, timesteps, input_features)的输入,而不是(timesteps, input_features)。指定初始Input()的shape参数时,你可以将timesteps设为None,这样神经网络就能够处理任意长度的序列,代码如下所示:

能够处理任意长度序列的RNN层

num_features = 14
inputs = keras.Input(shape=(None, num_features))
outputs = layers.SimpleRNN(16)(inputs)

如果你想让模型处理可变长度的序列,那么这就特别有用。但是,如果所有序列的长度相同,那么我建议指定完整的输入形状,因为这样model.summary()能够显示输出长度信息,这总是很好的,而且还可以解锁一些性能优化功能(以后文章咱们详述)。

Keras中的所有循环层SimpleRNN层、LSTM层和GRU层)都可以在两种模式下运行:

一种是返回每个时间步连续输出的完整序列,即形状为(batch_size,timesteps, output_features)的3阶张量

另一种是只返回每个输入序列的最终输出,即形状为(batch_size, output_features)的2阶张量。这两种模式由return_sequences参数控制。我们来看一个SimpleRNN示例,它只返回最后一个时间步的输出,代码如下所示:

只返回最后一个时间步输出的RNN层

num_features = 14
steps = 120
inputs = keras.Input(shape=(steps, num_features))#  请注意,默认情况下使用return_sequences=False
outputs = layers.SimpleRNN(16, return_sequences=False)(inputs)print(outputs.shape)

打印出的形状为 (None, 16)

还有,如下代码给出的示例返回了完整的状态序列(返回完整输出序列的RNN层):

num_features = 14
steps = 120
inputs = keras.Input(shape=(steps, num_features))
outputs = layers.SimpleRNN(16, return_sequences=True)(inputs)
print(outputs.shape)

打印出的形状为 (None, 120, 16)

为了提高神经网络的表示能力,有时将多个循环层逐个堆叠也是很有用的。在这种情况下,你需要让所有中间层都返回完整的输出序列,代码如下所示:

RNN层堆叠

inputs = keras.Input(shape=(steps, num_features))
x = layers.SimpleRNN(16, return_sequences=True)(inputs)
x = layers.SimpleRNN(16, return_sequences=True)(x)
outputs = layers.SimpleRNN(16)(x)

我们在实践中很少会用到SimpleRNN层。

它通常过于简单,没有实际用途。特别是SimpleRNN层有一个主要问题在t时刻,虽然理论上来说它应该能够记住许多时间步之前见过的信息,但事实证明,它在实践中无法学到这种长期依赖。原因在于梯度消失问题,这一效应类似于在层数较多的非循环网络(前馈网络)中观察到的效应:随着层数的增加,神经网络最终变得无法训练。Yoshua Bengio等人在20世纪90年代初研究了这一效应的理论原因。

oshua Bengio, Patrice Simard, Paolo Frasconi. Learning Long-Term Dependencies with Gradient Descent Is Difficult. IEEE Transactions on Neural Networks 5, no. 2, 1994.

值得庆幸的是,SimpleRNN层并不是Keras中唯一可用的循环层,还有另外两个:LSTM层和GRU层,二者都是为解决这个问题而设计的。

我们来看LSTM层,其底层的长短期记忆(LSTM)算法由Sepp Hochreiter和Jürgen Schmidhuber在1997年开发,是二人研究梯度消失问题的重要成果。

Sepp Hochreiter, Jürgen Schmidhuber. Long Short-Term Memory. Neural Computation 9, no. 8, 1997.

LSTM层是SimpleRNN层的变体,它增加了一种携带信息跨越多个时间步的方式

假设有一条传送带,其运行方向平行于你所处理的序列。

序列中的信息可以在任意位置跳上传送带,然后被传送到更晚的时间步,并在需要时原封不动地跳回来。

这其实就是LSTM的原理:保存信息以便后续使用,从而防止较早的信号在处理过程中逐渐消失。

为了详细解释LSTM,我们先从SimpleRNN单元开始讲起,如下图所示:

因为有许多个权重矩阵,所以对单元中的W和U两个矩阵添加下标字母o(Wo和Uo),表示输出(output)

讨论LSTM层的出发点:SimpleRNN层

我们向上图中添加新的数据流,其中携带跨越时间步的信息。这条数据流在不同时间步的值称为c_t,其中c表示携带(carry)。这些信息会对单元产生以下影响:它将与输入连接和循环连接进行计算(通过密集变换,即与权重矩阵做点积,然后加上偏置,再应用激活函数),从而影响传递到下一个时间步的状态(通过激活函数和乘法运算)。从概念上来看,携带数据流可以调节下一个输出和下一个状态,如下图所示。到目前为止,内容都很简单。

从SimpleRNN到LSTM:添加携带数据流

下面来看一下这种方法的精妙之处,即携带数据流下一个值的计算方法。

它包含3个变换,这3个变换的形式都与SimpleRNN单元相同,如下所示:

y = activation(dot(state_t, U) + dot(input_t, W) + b)

但这3个变换都有各自的权重矩阵,我们分别用字母i、f、k作为下标。目前的模型如下代码所示:

LSTM架构的详细伪代码(1/2)

output_t = activation(dot(state_t, Uo) + dot(input_t, Wo) + dot(c_t, Vo) + bo)
i_t = activation(dot(state_t, Ui) + dot(input_t, Wi) + bi)
f_t = activation(dot(state_t, Uf) + dot(input_t, Wf) + bf)
k_t = activation(dot(state_t, Uk) + dot(input_t, Wk) + bk)

通过对i_t、f_t和k_t进行计算,我们得到了新的携带状态(下一个c_t),如下代码所示:

LSTM架构的详细伪代码(2/2)

c_t+1 = i_t * k_t + c_t * f_t

添加上述内容之后的模型如下图所示。这就是LSTM层,不算很复杂,只是稍微有些复杂而已。

详解LSTM架构

你可以解释每个运算的作用。

比如你可以说,将c_t和f_t相乘,是为了故意遗忘携带数据流中不相关的信息。

同时,i_t和k_t都包含关于当前时间步的信息,可以用新信息来更新携带数据流。

但归根结底,这些解释并没有多大意义,因为这些运算的实际效果是由权重参数决定的,而权重以端到端的方式进行学习,每次训练都要从头开始,因此不可能为某个运算赋予特定的意义。

RNN单元的类型(如前所述)决定了假设空间,即在训练过程中搜索良好模型配置的空间,但它不能决定RNN单元的作用,那是由单元权重来决定的。

相同的单元具有不同的权重,可以起到完全不同的作用。因此,RNN单元的运算组合最好被解释为对搜索的一组约束,而不是工程意义上的设计。

这种约束的选择(如何实现RNN单元)最好留给优化算法来完成(比如遗传算法或强化学习过程),而不是让人类工程师来完成。那将是未来我们构建模型的方式。

总之,你不需要理解LSTM单元的具体架构。作为人类,你不需要理解它,而只需记住LSTM单元的作用:允许过去的信息稍后重新进入,从而解决梯度消失问题。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/735885.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java EE之wait和notify

一.多线程的执行顺序 由于多个线程执行是抢占式执行,就会导致顺序不同,同时就会导致出现问题,就比如俩个线程同时对同一个变量进行修改,我们难以预知执行顺序。 但在实际开发中,我们希望代码按一定的逻辑顺序执行&am…

计算机视觉——P2PNet基于点估计的人群计数原理与C++模型推理

简介 人群计数是计算机视觉领域的一个核心任务,旨在估算静止图像或视频帧中的行人数量。在过去几十年中,研究人员在这个领域投入了大量的精力,并在提高现有主流基准数据集性能方面取得了显著进展。然而,训练卷积神经网络需要大规…

matlab阶段学习笔记小节2

syms定义符号变量 求极限 第二题 第三题 limit(y,n,inf);求的的函数y关于自变量n在无穷处(inf)的极限 exp(2)即指数为2,底为e,也就是e^2 求导数 第一题 log(x)默认是以e为底的指数函数,也就是ln(x). 使用diff(f)对函数进行求…

算法-双指针、BFS与图论-1113. 红与黑

题目 思路 本题相当于问BFS中的当前点所在的区域连通块有多少个 Flood Fill算法 (可参考以下链接:洪水覆盖算法(Flood Fill):颜色填充-CSDN博客)本题用DFS实现Flood Fill算法DFS是否需要恢复现场:(重要&am…

新IDEA电脑环境设置

1.设置UTF-8 2.Maven 3.JRE选对

【漏洞复现】华三用户自助服务产品dynamiccontent.properties.xhtml接口处存在RCE漏洞

免责声明:文章来源互联网收集整理,请勿利用文章内的相关技术从事非法测试,由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损失,均由使用者本人负责,所产生的一切不良后果与文章作者无关。该…

VUE+HBuilder的uniapp技术路线开发应用使用总结

使用总结 本来想做一个记录日常数据的应用,主要在Android端使用,后来发现在uniapp中使用sqllite数据库不是像原生中那样简单(所以当前准备去进行另一个路线,就是给我使用的电脑都安装一个portalble的服务端,用来记录数据&#xf…

java中使用rabbitmq

文章目录 前言一、引入和配置1.引入2.配置 二、使用1.队列2.发布/订阅2.1 fanout(广播)2.2 direct(Routing/路由)2.3 Topics(主题)2.4 Headers 总结 前言 mq常用于业务解耦、流量削峰和异步通信,rabbitmq是使用范围较广,比较稳定的一款开源产品,接下来我们使用springboot的sta…

角蜥优化算法 (Horned Lizard Optimization Algorithm ,HLOA)求解无人机路径优化

一、无人机路径规划模型介绍 无人机三维路径规划是指在三维空间中为无人机规划一条合理的飞行路径,使其能够安全、高效地完成任务。路径规划是无人机自主飞行的关键技术之一,它可以通过算法和模型来确定无人机的航迹,以避开障碍物、优化飞行时间和节省能量消耗。 二、算法介…

数据结构:图及相关算法讲解

图 1.图的基本概念2. 图的存储结构2.1邻接矩阵2.2邻接表2.3两种实现的比较 3.图的遍历3.1 图的广度优先遍历3.2 图的深度优先遍历 4.最小生成树4.1 Kruskal算法4.2 Prim算法4.3 两个算法比较 5.最短路径5.1两个抽象存储5.2单源最短路径--Dijkstra算法5.3单源最短路径--Bellman-…

Dataset 读取数据

Dataset 读取数据 from torch.utils.data import Dataset from PIL import Image import osclass Mydata(Dataset):def __init__(self,root_dir,label_dir):self.root_dir root_dir #根目录 dataset/trainself.label_dir label_dir #标签的后面链接目录 ants_ima…

windows系统玩游戏找不到d3dx9_43.dll缺失,无法启动此程序的解决方法

今日,我们要深入讨论d3dx9_43.dll文件的重要性及其缺失问题。最近,我也遇到了这个文件丢失的困扰,因此想借此机会与大家分享如何解决d3dx9_43.dll缺失的问题。 一.电脑d3dx9_43.dll丢失会提示什么? 关于电脑提示d3dx9_43.dll丢失…

神经网络实战前言

应用广泛 从人脸识别到网约车,在生活中无处不在 未来可期 无人驾驶技术便利出行医疗健康改善民生 产业革命 第四次工业革命——人工智能 机器学习概念 机器学习不等价与人工智能20世纪50年代,人工智能是说机器模仿人类行为的能力 符号人工智能 …

【环境配置】Linux MySQL8 忘记密码解决措施

本片博客介绍 Linux 操作系统 Ubuntu 下,MySQL8 忘记密码怎么重新设置,笔者亲测有效,分享给大家。 查看 MySQL 版本 $ mysql --version停止 MySQL 服务器,并查看状态是否变更为 Server shutdown complete # 等价命令sudo syste…

Django简易用户登入系统示例

Django简易用户登入系统示例 1)添加url和函数的对应关系(urls.py) urlpatterns [ path(login/, views.login), #login:url路径,views.login:对应的函数 ]2)添加视图函数(views.py) def login(req):if…

IDEA + Git + GitHub(保姆级教学)

文章目录 IDEA Git GitHub1.IDEA克隆远程仓库到本地仓库1.创建一个GitHub远程仓库test12.IDEA克隆仓库到本地1.复制远程仓库地址2.创建一个版本控制项目3.克隆到本地仓库4.克隆成功 2.IDEA将本地项目push到远程仓库1.在这个项目下新建一个java模块1.新建模块2.填写模块名3.在…

【CSP试题回顾】202006-1-线性分类器

CSP-202006-1-线性分类器 解题思路 线性分类问题,即根据给定的数据点和分类界限,判断是否存在一条线能够将属于不同类别的点完全分开。具体来说,数据点被分为两类,标记为A和B,我们要找出是否存在一个线性决策边界&…

gpt不能发送信息了?

文章目录 问题注意步骤解决后 问题 注意步骤 清理所有cookie和浏览数据,可保存密码 解决后

C++的类与对象(五):赋值运算符重载与日期类的实现

目录 比较两个日期对象 运算符重载 赋值运算符重载 连续赋值 日期类的实现 Date.h文件 Date.cpp文件 Test.cpp文件 const成员 取地址及const取地址操作符重载 比较两个日期对象 问题描述:内置类型可直接用运算符比较,自定义类型的对象是多个…

虚拟 DOM:前端性能优化的秘密

🤍 前端开发工程师、技术日更博主、已过CET6 🍨 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 🕠 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 🍚 蓝桥云课签约作者、上架课程《Vue.js 和 E…