针对初学者的循环神经网络介绍

共 5183字,需浏览 11分钟

 ·

2021-06-19 17:29

Python部落(python.freelycode.com)组织翻译,欢迎转发。

简单介绍什么是RNN,它们如何运行,以及如何用Python从头构建一个RNN。

循环神经网络(RNN)是一种专门处理序列的神经网络。它们通常用于自然语言处理(NLP)任务,因为它们在处理文本方面非常有效。在本文中,我们将探索什么是RNN,了解它们是如何工作的,并使用Python从头构建一个真正的RNN(仅使用numpy库)。


这篇文章假设你有神经网络的基本知识。我对神经网络的介绍涵盖了你需要知道的一切,所以我建议你先读一下(https://victorzhou.com/blog/intro-to-neural-networks/  )。

我们开始吧!


1. 为什么有用

标准神经网络(以及CNN)的一个问题是,它们只能处理预先确定的大小: 它们接受固定大小的输入并产生固定大小的输出。RNN是有用的,因为它允许我们使用可变长度的序列作为输入和输出。下面是一些RNN的例子:


输入为红色,RNN本身为绿色,输出为蓝色。来源:Andrej Karpathy


这种处理序列的能力使RNN非常有用。例如:


  • 机器翻译(比如谷歌翻译)是通过“多对多”RNN完成的。原始文本序列被输入一个RNN,然后该RNN会生成翻译文本作为输出。

  • 情感分析 (例如,这是一个积极的还是消极的评论?)通常是通过“多对一” 的RNN完成的。要分析的文本被输入一个RNN,然后该RNN会生成一个单独的输出分类(例如,这是一个积极的评论)。


在本文的后面,我们将从头构建一个“多对一”的RNN来执行基本的情感分析。


2. 工作原理

我们假设有一个带有输入x0,x1,...xn的“多对多”的RNN,我们希望它产生输出y0,y1,...yn。这些xi 和yi是向量,可以有任意的维数。


RNN利用以任意给定的步长t反复地更新一个隐藏状态h来运行,h是一个向量,也可以有任意的维数。


  1. 下一个隐藏状态ht是使用前一个隐藏状态ht -1和下一个输入xt进行计算的。

  2. 下一个输出yt是使用ht进行计算的。


一个多对多RNN


这就是使RNN循环的东西: 它对每个步骤使用相同的权重。具体来说,一个典型的标准RNN只使用3组权重来进行计算:


  • Wxh, 用于所有的 xt → ht 连接。

  • Whh, 用于所有的 ht-1 → ht 连接。

  • Why, 用于所有的 ht → yt 连接。


我们也对我们的RNN使用两个偏差:


  • bh,计算ht时加上。

  • by, 计算yt时加上。


我们用矩阵表示权重,用向量表示偏差。这3个权重和2个偏差就构成了整个RNN!


下面是把所有东西放在一起的方程式:


不要略过这些方程。停下来,盯着这个方程看一分钟。另外,记住权重是矩阵,其它变量是向量。


所有的权重都使用矩阵乘法进行应用,并将偏差加到结果乘积中。然后,我们使用tanh作为第一个方程的激活函数(其它激活函数像sigmoid也可以使用)。


不知道什么是激活函数?请认真阅读我之前提到的神经网络介绍。


3. 要解决的问题

我们来动手干吧!我们将从头实现一个RNN来执行一个简单的情感分析任务: 确定给定的文本字符串是积极的还是消极的。


下面是我为本文收集的小数据集中的一些例子:





4. 计划

由于这是一个分类问题,我们将使用“多对一" RNN。这与我们前面讨论的“多对多”RNN类似,只不过它只使用最终的隐藏状态来产生一个输出y:

一个多对一RNN


每个xi都是一个向量,表示文本中的一个单词。输出y将是一个包含两个数字的向量,一个表示积极的,另一个表示消极的。我们将使用Softmax将这些值转换为概率,并最终在积极的/消极的之间进行决定。


我们来开始构建我们的RNN!


5. 预处理

我前面提到的数据集由两个Python字典组成:


True=积极的,False=消极的


我们必须做一些预处理才能把数据转换成可用的格式。首先,我们将构造一个包含我们数据中所有单词的词汇表:



vocab现在包含了至少在一个训练文本中出现的所有单词的列表。接下来,我们将分配一个整数索引来表示vocab中的每个单词。



我们现在可以用对应的整数索引表示任意给定的单词!这是必要的,因为RNN不能理解单词——我们必须给它们提供数字。


最后,回想一下RNN的每个输入xi都是一个向量。我们将使用one-hot向量,它除了包含一个1之外,其余值都是0。每个one-hot向量中的“1”将位于这个单词对应的整数索引处。


由于我们的词汇表中有18个唯一的单词,每个xi将是一个18维的one-hot向量。



稍后,我们将使用createInputs()来创建向量输入,并将其传入我们的RNN。


6. 正向阶段

是时候开始实现我们的RNN了!我们将从初始化我们的RNN所需要的3个权重和2个偏差开始:


注意:我们除以1000是为了减小权重的初始方差。这并不是初始化权重的最佳方法,但它很简单,适合本文。


我们使用np.random.randn()从标准正态分布来初始化我们的权重。


接下来,我们来实现RNN的正向传递。还记得我们之前看到的这两个方程吗?



下面是这些同样的方程被写入代码中的形式:



很简单,对吧?注意,我们在第一步中将h初始化为零向量,因为此时没有可以供我们使用的前一个h。


让我们来试试:


如果你需要复习一下Softmax,请阅读我对Softmax的简要说明。


我们的RNN可以运行,但还不是很有用。我们来改变这一点……


7. 逆向阶段

为了训练我们的RNN,我们首先需要一个损失函数。我们将使用交叉熵损失函数,它通常与Softmax配对使用。我们是这样计算它的:

其中pc 是我们的RNN对正确类(积极的或消极的)的预测概率。例如,如果一个积极的文本被我们的RNN预测为90%的积极度,则损失为:

现在我们有了一个损失,我们将使用梯度下降训练我们的RNN来最小化损失。这意味着是时候推导一些梯度了!


⚠️以下部分假设你有多变量微积分的基本知识。如果你愿意,你可以跳过它,但我建议即使你不太明白也要略读一下。我们将在推导结果时逐步编写代码,即使表面的理解也会有所帮助。


如果你想了解这部分的额外背景知识,我建议你先阅读我的《神经网络介绍》中的《训练神经网络》部分。此外,这篇文章的所有代码都在Github上,所以如果你愿意,你可以follow它。


准备好了吗?我们开始吧。


7.1 定义

首先,我们来看一些定义:


  • 让 y代表来自我们RNN的原始输出。

  • 让p代表最终的概率:p=softmax(y).

  • 让c 指代一个特定文本例子的真实标签,也可以说是“正确的”类。 

  • 让L代表交叉熵损失:L=-ln(pc)

  • 让Wxh 、Whh 和Why代表我们的RNN中的3个权重矩阵。

  • 让bh和by 代表我们的RNN中的两个偏差向量。


7.2设置

接下来,我们需要编辑正向阶段来缓存一些数据,以便在反向阶段中使用。在此过程中,我们还将为反向阶段设置骨架。它是这样的:



想知道我们为什么要进行缓存吗?请阅读我在我的CNN介绍的训练概述中的说明。我在其中做了同样的事情。


7.3梯度

是用到数学的时候了!我们从计算∂L/∂y开始。我们知道:

我将把使用链式法则推导∂L/∂y的过程留给你,但是推导出来的结果是漂亮的:

例如,如果我们有p = [0.2, 0.2, 0.6],并且其正确的类是c=0,那么我们就会得到 ∂L/∂y=[−0.8,0.2,0.6] 。这转换称代码也是很容易的:



漂亮!下一步,我们来为Why和by尝试一下梯度,它们的梯度只用与将最终的隐藏状态转换为RNN的输出。我们有:


式中,hn是最终的隐藏状态,因此,

类似地,

现在,我们可以开始实现 backprop()!



提醒: 我们之前在forward()中创建了self.last_hs。


最后,我们需要Whh 、Wxh 和bh的权重,它们将在RNN中的每一步使用。我们有:

因为改变Wxh会影响每一个ht ,而每一个ht都会影响y,并最终影响L。为了完全计算Wxh的梯度,我们需要反向传播所有的步长,这也被称为随时间反向传播(BPTT):



随时间进行的反向传播


Wxh被用于所有的xt  —> ht 正向连接,因此我们必须反向传播回这些连接的每一个。


一旦我们到达了一个给定的步长t,我们需要计算∂ht/∂Wxh

tanh的推导是众所周知的:

我们和平常一样使用链式法则:

类似地,

我们需要的最后一个东西是∂y/∂ht,我们可以递归地计算它:

我们将从最后的隐藏状态开始,并逆向运行来实现BPTT,这样当我们想要计算∂y/∂ht+1的时候我们就已经有了∂y/∂ht!最后的隐藏状态hn是一个例外:

现在我们有了最终实现BPTT和完成backprop()所需要的所有东西:


一些需要注意的东西:


  • 为了方便起见,我们已经将(∂L/∂y)*(∂y/∂h)合并到 ∂L/∂h中了。

  • 我们需要不断地更新一个保存最近的∂L/∂ht+1值的变量d_h,我们计算∂L/∂ht需要用到这个值。

  • 在完成BPTT之后,我们使用np.clip()截取小于-1或大于1的梯度值。这有助于缓解梯度爆炸问题,这是因为有很多相乘项时,梯度就会变的非常大。对于普通的RNN来说,梯度爆炸或梯度消失是很有问题的——像LSTM这样更复杂的RNN通常能够很好地处理它们。

  • 一旦所有的梯度被计算出,我们就使用梯度下降来更新权重和偏差。


我们做到了!我们的RNN是完整的。


8. 高潮

终于到了我们一直等待的时刻——我们来测试我们的RNN!


首先,我们将编写一个辅助函数来使用我们的RNN处理数据:



现在,我们可以编写训练循环:



运行main.py应该会输出如下内容:



从结果来看我们自己建立的RNN还不错。


想自己尝试或修改这段代码吗?请在浏览器中运行这个RNNhttps://repl.it/@vzhou842/A-RNN-from-scratch)。你也可以在Github上找到它。(https://github.com/vzhou842/rnn-from-scratch)


9. 结尾

就是这样!在这篇文章中,我们完成了一个循环神经网络的一个演示,包括它们是什么,它们是如何工作的,它们为什么有用,如何训练它们,以及如何实现一个。虽然如此,你还有很多事情可以做:


  • 学习长短期记忆网络,一个更强大更流行的RNN架构,或者学习门控循环单元(GRU),一个著名的LSTM变体。

  • 使用合适的ML库(比如Tensorflow、 Keras或 PyTorch)对更大/更好的RNN进行实验。

  • 阅读关于双向RNN的内容。它会正向和反向处理序列,因此,有更多的信息对于输出层来说是可用的。

  • 尝试词嵌入(比如 GloVe 或 Word2Vec),你可以使用它们将单词转换成更有用的向量表示形式。 

  • 尝试自然语言工具集(NLTK),一个流行的处理人类语言数据的Python库。


我写了很多关于机器学习的文章,所以如果你有兴趣从我这里获得前沿的ML内容,请订阅我的时事通讯。

英文原文:https://victorzhou.com/blog/intro-to-rnns/
···  END  ···
推荐阅读:
一、Number(数字)
Python基础之数字(Number)超级详解
Python随机模块22个函数详解
Python数学math模块55个函数详解
二、String(字符串)
Python字符串的45个方法详解
Pandas向量化字符串操作
三、List(列表)
超级详解系列-Python列表全面解析
Python轻量级循环-列表推导式
四、Tuple(元组)
Python的元组,没想象的那么简单
五、Set(集合)
全面理解Python集合,17个方法全解,看完就够了
六、Dictionary(字典)
Python字典详解-超级完整版
七、内置函数
Python初学者必须吃透这69个内置函数!
八、正则模块
Python正则表达式入门到入魔
笔记 | 史上最全的正则表达式
八、系统操作
Python之shutil模块11个常用函数详解
Python之OS模块39个常用函数详解
九、进阶模块
【万字长文详解】Python库collections,让你击败99%的Pythoner
高手如何在Python中使用collections模块

扫描关注本号↓

浏览 35
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报