政安晨:【深度学习处理实践】(五)—— 初识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单元的作用:允许过去的信息稍后重新进入,从而解决梯度消失问题。


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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SRS(Simple Realtime Server)

SRS(Simple Realtime Server - github) SRS 中文官网 docker安装srs ##(安全组放开1935端口、8080端口) docker run --rm -it -p 1935:1935 -p 1985:1985 -p 8080:8080 -p 8000:8000/udp -p 10080:10080/udp ossrs/srs:5推流 ## 不需要加端口 ffmpeg…

深度学习armv8/armv9 cache的原理

文章目录 1、为什么要用cache?2、背景:架构的变化?2、cache的层级关系 ––big.LITTLE架构(A53为例)3、cache的层级关系 –-- DynamIQ架构(A76为例)4、DSU / L3 cache5、L1/L2/L3 cache都是多大呢6、cache相关的术语介绍7、cache的分配策略(alocation,…

Python读取influxDB数据库

1. influxDB连接 首先用InfluxDBStudio软件连接influxDB数据库来查看所有表: 2. 写sql语句来查询数据 然后和平时写sql查询语句一样,先创建连接client,然后调用其query函数来查询获取数据 self.client influxdb.InfluxDBClient(hostinflu…

前端文件上传

文件上传方式 前端文件上传有两种方式,第一种通过二进制blob传输(formData传输),第二种是通过base64传输 文件相关的对象 file对象其实是blob的子类 blob对象的第一个参数必须是一个数组,你可以把一个file对象放进去…

HCS-华为云Stack-计算节点内部网络结构

HCS-华为云Stack-计算节点内部网络结构 图中表示的仅为计算节点是两网口的模式,如果是四网口模式,系统会再自动创建一个网桥出来 图中未画出存储平面和Internal Base平面,它们和tunnel bearing、External OM-样,都是通过trunk0的…

模型驱动架构MDA

MDE 模型驱动工程(MDE, Model-Driven Engineering)是软件工程的一个分支,它将模型与建模拓展到软件开发的所有方面,形成一个多维建模空间,从而将工程活动建立在这些模型的映射和转换之上。[1] MDE的基本原则是将模型视…

《量子计算:下一个大风口,还是一个热炒概念?》

引言 量子计算,作为一项颠覆性的技术,一直以来备受关注。它被认为是未来计算领域的一次革命,可能改变我们对计算能力和数据处理的理解。然而,随着技术的不断进步和商业应用的探索,人们开始思考,量子计算到底是一个即将到来的大风口,还是一个被过度炒作的概念? 量子计…

空间复杂度(数据结构)

概念: 空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度 。 空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟实践复…

软考73-上午题-【面向对象技术2-UML】-UML中的图4

一、构件图(组件图) 1-1、构件图的定义 展现了,一组构件之间的组织和依赖。 构件图专注于系统的静态实现图。 构件图与类图相关,通常把构件映射为一个、多个类、接口、协作。 【回顾】: 类图展示了一组对象、接口、…

C++之map与set的使用与原理+拓展avl树(详解)

前言:map和set是C里面的两种常用STL容器,他们被设计为关联式容器,这两个容器存储的元素都是唯一的,并且每个元素与键(key)相关联,简单来说就是两个存储不重复元素的容器。那就有人有疑问了&…

PostgreSQL中In, Exists在SQL查询中到底有无区别

前言 SQL查询当中,In和Exists子查询到底有无区别?记得很多年以前,确实是有相关的使用戒条的,或者说存在一些使用的惯用法。试图完全抹开两者的区别,就有点过了。 两者的主要区别: 从目的上讲&#xff0c…

微信小程序开发系列(二十七)·小程序生命周期详细介绍

目录 1. 小程序生命周期介绍 2. 应用生命周期 3. 页面生命周期 1. 小程序生命周期介绍 一个小程序完整的生命周期由 应用生命周期、页面生命周期 和 组件生命周期 三部分来组成。 应用生命周期:是指应用程序进程从创建到消亡的整个过程。 小程序的生命&#…

python——By.ID/By.NAME

一、通过元素的id属性来定位元素 from selenuim import webdriver from selenuim.webdriver.common.by import Bydriver webdriver.Chrome() driver.maximize_window() driver.get(http://xxx) time.sleep(3)# 通过By.ID找到唯一页面元素后,输入abcd driver.find_…

简析内部审计数字化转型的方法和路径

简析内部审计数字化转型的方法和路径 内部审计是一种独立的、客观的确认和咨询活动,包括鉴证、识别和分析问题以及提供管理建议和解决方案。狭义的数字化转型是指将企业经营管理和业务操作的各种行为、状态和结果用数字的形式来记录和存储,据此再对数据…

华为OD机试 - 模拟数据序列化传输(Java JS Python C C++)

题目描述 模拟一套简化的序列化传输方式,请实现下面的数据编码与解码过程 编码前数据格式为 [位置,类型,值],多个数据的时候用逗号分隔,位置仅支持数字,不考虑重复等场景;类型仅支持:Integer / String / Compose(Compose的数据类型表示该存储的数据也需要编码)编码后数…

VSCode安装C语言编译环境

目录 一、在vscode下载C/C扩展 二、配置gcc环境 1.访问网站:https://sourceforge.net/projects/mingw-w64/files/ 2.解压并复制bin目录 三、配置gcc环境 四、在cmd检查是否配置成功 五、vscode配置gcc环境 六、在vscode运行C文件 运行.c代码 七、在vscode运…

JVM-2

目录 1.虚拟机类加载机制 2.JVM常见回收算法 2.1标记清除算法 2.2标记整理算法 2.3标记复制算法 3.分代垃圾回收机制 新生代收集 第一次垃圾回收 第二次垃圾回收 第N此垃圾回收 老年代收集 4.补充 Stop The World Java的对象结构 1.虚拟机类加载机制 双亲委派模式…

理解STM32的低功耗模式

低功耗模式简介 TM32的低功耗模式是特别设计来减少微控制器在不活跃状态下的能耗。这些模式允许STM32在保持核心功能的同时尽可能减少电力消耗,适合用在电池供电或需长期运行的场景。理解各种低功耗模式如何节能,主要包括以下几个方面: 关闭…

DenseNet笔记

📒from ©实现pytorch实现DenseNet(CNN经典网络模型详解) - 知乎 (zhihu.com) 是什么之 DenseBlock 读图: x0是inputH1的输入是x0 (input)H2的输入是x0和x1 (x1是H1的输出) Summary: 传统卷积网,网…

C语言——函数指针——函数指针数组 (详解)

函数指针数组 函数指针数组的作用 函数指针数组是一个数组,其中的每个元素都是一个函数指针。函数指针是指向函数的指针变量,可以用来调用相应的函数。函数指针数组的作用是可以根据需要动态地选择并调用不同的函数。 函数指针数组的使用场景有很多&…