一、前言
写这部分的文章很耗费精力。因为我自己是医学信息工程专业的,主攻方向其实是医学影像处理(主要是图像的快采集算法和后期图像质量优化)而非人工智能,甚至都不是纯科班出身,需要钻研的地方有很多。一是需要自己找书和文章看,二是还得想怎么把晦涩难懂的内容尽量讲解地通俗易懂。
但写作的过程也确实让我懂得了很多东西,我也很喜欢学习人工智能相关的知识。接下来可能会转战我的老本行——Matlab编程和MRI方向了,但出于个人兴趣,人工智能方向的博文还会更新。还是不多说了,直接上全文吧。
这里贴一下前几篇博文:
[深度学习实战]基于PyTorch的深度学习实战(上)[变量、求导、损失函数、优化器]
[深度学习实战]基于PyTorch的深度学习实战(中)[线性回归、numpy矩阵的保存、模型的保存和导入、卷积层、池化层]
[深度学习实战]基于PyTorch的深度学习实战(下)[Mnist手写数字图像识别]
二、RNN和LSTM基本原理
在讲解前,首先我们需要明白:我们并不是每时每刻都从一片空白的大脑开始思考。比如,在你阅读这篇博文的时候,你都是基于自己已经拥有的对先前所见词的理解来推断当前词的真实含义,而不是在读这篇博文前还得从汉字的意思学起。这是后文RNN和LSTM的关键!我们不会将所有的东西都全部丢弃,然后用空白的大脑进行思考,因为我们的思想拥有持久性。
我们一直以来习以为常的事情在传统的神经网络看来,简直难如登天,而这也是其最大的弊端。
例如,假设你希望对电影中的每个时间点的时间类型进行分类。传统的神经网络应该很难来处理这个问题——使用电影中先前的事件推断后续的事件。
而 RNN 则解决了这个问题。 RNN 是包含循环的网络,允许信息的持久化。
RNN 本质上是与序列和列表相关的,对于这类数据的最自然的神经网络架构。在过去几年中,应用 RNN 在语音识别,语言建模,翻译,图片描述等问题上已经取得一定成功。而这些成功应用的关键之处就是LSTM 的使用,这是一种特别的 RNN,比标准的 RNN 在很多的任务上都表现得更好。几乎所有的令人振奋的关于RNN 的结果都是通过 LSTM 达到的。
2.1 长期依赖问题
RNN 的关键点之一就是它们可以用来连接先前的信息到当前的任务上,例如使用过去的视频段来推测对当前段的理解。如果 RNN 可以完美地做到的话,他们就可以称得上优秀了。但是,真的可以么?很可惜的,想要完美地连接先前的信息到当前的任务还有很多依赖因素。有时候,我们仅仅需要知道先前的信息来执行当前的任务。而不需要了解先前的信息,但需要记忆化处理任务的场合还是很多的。
在理论上,RNN 可以处理这样的长期依赖问题。人们可以仔细挑选参数来解决这类问题中的最初级形式。但理论终归是理论,在实践中,RNN 肯定不能够成功学习到这些知识。然而,幸运的是,LSTM 并没有这个问题!
2.2 LSTM 网络
Long Short Term 网络—— 一般叫做 LSTM,是一种 RNN 特殊的类型,可以学习长期依赖信息。LSTM 由 Hochreiter & Schmidhuber (1997)提出,并在近期被 Alex Graves 进行了改良和推广。在很多问题上,LSTM都取得相当巨大的成功,并得到了广泛的使用。
LSTM 通过刻意的设计来避免长期依赖问题。记住长期的信息在实践中是LSTM的默认行为,而非需要付出很大代价才能获得的能力。所有 RNN 都具有一种重复神经网络模块的链式的形式。在标准的RNN中,这个重复的模块只有一个非常简单的结构,例如一个tanh层。
LSTM 同样是这样的结构,但是重复的模块拥有一个不同的结构。不同于单一神经网络层,这里有四个,以一种非常特殊的方式进行交互。
2.3 LSTM 的核心思想
LSTM 的关键就是细胞状态,水平线在图上方贯穿运行。细胞状态类似于传送带。直接在整个链上运行,只有一些少量的线性交互。信息在上面流传保持不变会很容易。
LSTM 有通过精心设计的称作为“门”的结构来去除或者增加信息到细胞状态的能力。门是一种让信息选择式通过的方法。他们包含一个sigmoid 神经网络层和一个 pointwise 乘法操作。
Sigmoid 层输出 0 到 1 之间的数值,描述每个部分有多少量可以通过。0代表“不许任何量通过”,1 就指“允许任意量通过”。
LSTM 拥有三个门,来保护和控制细胞状态。
2.4 逐步理解 LSTM
在 LSTM 中的第一步是决定我们会从细胞状态中丢弃什么信息。这个决定通过一个称为忘记门层完成。该门会读取 h_{t-1} 和 x_t,输出一个在 0 到 1 之间的数值给每个在细胞状态 C_{t-1} 中的数字。1 表示“完全保留”,0 表示“完全舍弃”。
下一步是确定什么样的新信息被存放在细胞状态中。这里包含两个部分。第一,sigmoid 层称 “输入门层” 决定我们将要更新什么值。然后,一个 tanh 层创建一个新的候选值向量——\tilde{C}_t 会被加入到状态中。下一步,我们会讲这两个信息来产生对状态的更新。
现在是更新旧细胞状态的时间了,C_{t-1} 更新为 C_t。前面的步骤已经决定了将会做什么,我们现在就是实际去完成。
我们把旧状态与 f_t 相乘,丢弃掉我们确定需要丢弃的信息。接着加上i_t * \tilde{C}_t。这就是新的候选值,根据我们决定更新每个状态的程度进行变化。
最终,我们需要确定输出什么值。这个输出将会基于我们的细胞状态,但是也是一个过滤后的版本。首先,我们运行一个 sigmoid 层来确定细胞状态的哪个部分将输出出去。接着,我们把细胞状态通过 tanh 进行处理(得到一个在 -1 到 1 之间的值)并将它和 sigmoid 门的输出相乘,最终我们仅仅会输出我们确定输出的那部分。
2.5 LSTM 的变体
几乎所有包含 LSTM 的论文都采用了微小的变体,而其中一个流形的 LSTM 变体,就是由 Gers & Schmidhuber (2000) 提出的,增加了 “peephole connection”。是说,我们让门层也会接受细胞状态的输入。
另一个变体是通过使用 coupled 忘记和输入门。不同于之前是分开确定什么忘记和需要添加什么新的信息,这里是一同做出决定。我们仅仅会当我们将要输入在当前位置时忘记,而仅仅输入新的值到那些我们已经忘记旧的信息的那些状态。
2.5.1 coupled 忘记门和输入门
另一个改动较大的变体是Gated Recurrent Unit (GRU),这是由 Cho, etal. (2014) 提出。它将忘记门和输入门合成了一个单一的更新门。同样还混合了细胞状态和隐藏状态,和其他一些改动。最终的模型比标准的LSTM 模型要简单,也是非常流行的变体。
2.5.2 GRU
这里只是部分流行的 LSTM 变体。当然还有很多其他的,如 Yao, et al.(2015) 提出的 Depth Gated RNN。还有用一些完全不同的观点来解决长期依赖的问题,如 Koutnik, et al. (2014) 提出的 Clockwork RNN。
三、PyTorch中的LSTM
LSTM 是深度学习中比较难以理解的模型了。我们还是以实践为主,主要学习怎么在 pytorch 中使用 LSTM,现阶段可以不必关注 LSTM 的内部实现细节,就好像我们天天在使用电脑,但也不是非要去搞清楚操作系统是怎么编写出来的,一样的道理,专业的事情交给更专业的人去做。
pytorch 中使用 nn.LSTM 类来搭建基于序列的循环神经网络。本段详细介绍 LSTM 编程接口的正确打开姿势。
类的全称为 torch.nn.LSTM,它的构造函数有以下几个参数:
input_size:输入数据X的特征值的数目。
hidden_size:隐藏层的神经元数量,也就是隐藏层的特征数量。
num_layers:循环神经网络的层数,默认值是 2。
bias:默认为 True,如果为 false 则表示神经元不使用 bias 偏移参数。
batch_first:如果设置为 True,则输入数据的维度中第一个维度就是 batch 值,默认为 False。默认情况下第一个维度是序列的长度,第二个维度才是 batch,第三个维度是特征数目。
dropout:如果不为空,则表示最后跟一个 dropout 层抛弃部分数据,抛弃数据的比例由该参数指定。
==bidirectional ==:布尔型,设置为 True 那么 LSTM 就是一个双向的RNN 网络(双向是指同时可以往后影响参数),默认是 False。
其中 input_size,hidden_size 和 num_layers 是前三个参数,也是需要手工设置的参数,num_layers 一般取默认值 2,这个值我们不必动它,就用 2 好了,它的含义是说隐藏层配置几层。LSTM 中最主要的参数是input_size 和 hidden_size,这两个参数务必要搞清楚。其余的参数通常不用设置,LSTM 采用默认值就可以了。
下面这个图就是 num_layers 等于 2 的示意图,意思是说隐藏层有 2 个。
如果隐藏层只有一个的话则是下面这个图的样子。
num_layers 大部分情况下一般取默认值 2,我们在使用过程中也使用2吧。
LSTM 的用法形式是这样的:
output,(h_n,c_n)=LSTM(input,(h0,c0))
下面解释各个参数:
输入参数:input, (h_0, c_0)
input (seq_len, batch, input_size):输入数据 input 是一个三维向量,第一个维度是序列长度,第二个维度是 batch,也就是一批同时训练多少条数据,第三个维度是特征数目。如果实际数据的长度达不到序列长度 seq_len 的值,则可以先用torch.nn.utils.rnn.pack_padded_sequence() 方法进行填充,这就是变长序列的由来。
h_0 (num_layers * num_directions, batch, hidden_size):隐藏层的初始权重,num_directions 一般为 1。
c_0 (num_layers * num_directions, batch, hidden_size):隐藏层的初始状态,num_directions 一般为 1。
输出数据:output, (h_n, c_n)
output (seq_len, batch, hidden_size * num_directions):输出数据。
h_n (num_layers * num_directions, batch, hidden_size):隐藏层的输出权重。
c_n (num_layers * num_directions, batch, hidden_size):隐藏层的输出状态。
示例代码:
rnn = nn.LSTM(10, 20, 2) input = Variable(torch.randn(5, 3, 10)) h0 = Variable(torch.randn(2, 3, 20)) c0 = Variable(torch.randn(2, 3, 20)) output, hn = rnn(input, (h0, c0))
输入 input 的维度是 (5,3,10),其中 5 是序列长度,3 是 batch,10 是特征数目。
output 的维度是 (5,3,20),是输出数据。
hn 是隐藏层的参数,是一个 tuple。hn 的长度是 2:len(hn)=2。其中hn[0].size()=(2,3,20);hn[1].size()=(2,3,20)。hn[0] 是隐藏层的权重值,hn[1] 是隐藏层的状态值。对应关系如下:
hn[0] ==> h0 hn[1] ==> c0
我们说 LSTM 做深度学习模型,其实质是基于这样一个道理。在日常生活中,我们通常表示一句话的含义会有多种不同的表述方式,比如“我饿了”,“我要吃饭了”等不同的话,其意义是一样的,都是“吃饭”。那么我们就可以将“我饿了”,“我要吃饭了”,“我现在好饿啊”作为输入数据,将“吃饭”作为结果标签,通过 LSTM 模型自动学习输入数据和结果标签之间的关联关系,自动提取特征值,模型训练之后会对诸如“我马上就要饿了”等语句自动分类到“吃饭”这个类别。这就是 LSTM 的现实意义。
四、Embedding层
在自然语言处理和文本分析的问题中,词袋(Bag of Words, BOW)和词向量(Word Embedding)是两种最常用的模型。更准确地说,词向量只能表征单个词,如果要表示文本,需要做一些额外的处理。下面就简单聊一下两种模型的应用。
所谓 BOW,就是将文本 /Query 看作是一系列词的集合。由于词很多,所以咱们就用袋子把它们装起来,简称词袋。至于为什么用袋子而不用筐(basket)或者桶(bucket),这咱就不知道了。举个例子:
文本1:苏宁易购/是/国内/著名/的/B2C/电商/之一。
这是一个短文本。“/”作为词与词之间的分割。从中我们可以看到这个文本包含“苏宁易购”,“B2C”,“电商”等词。换句话说,该文本的的词袋由“苏宁易购”,“电商”等词构成。就像这样:
但计算机不认识字,只认识数字,那在计算机中怎么表示词袋模型呢?
其实很简单,给每个词一个位置/索引就可以了。例如,我们令“苏宁易购”的索引为0,“电商”的索引为 1,其他以此类推。则该文本的词袋就变成了:
这时,词袋变成了一串数字的(索引)的集合,这样计算机就能读懂了。
说白了,就是将意思相近的词的向量值安排的靠近一些,将意思不同的词的距离靠近的远一些。其原理大致就是在一段文本中的词意思一般来说是相近的,在不同的文本中的词意思大致是不同的,根据这个原理来计算Embedding 模型。
五、后记
感谢各位一直以来的支持,《基于PyTorch的深度学习实战》到这里就告一段落了。接下来我会继续学习人工智能和算法相关的知识并持续努力创作的!感谢大家!!!