本博客是基于吴恩达老师深度学习视频所做的笔记,没有任何代码也无需强大的数学基础,主要是对一些概念的理解。
如有错误欢迎指出~
上面这些例子中,不过X和Y的长度并不都是相同的,也可能不都是序列。在后面的学习中我们就会碰见各种情况。
假设我们对一个句子进行人名判断,我们会用 x < t > x^{
我们如何来表示一个句子里的每个单词呢?通常我们会做一张词表(也称为词典)。如下图左侧所示,假设我们词表共有10000个单词,单词a在位置1,单词and在位置367,单词harry在4075,单词potter在6830……而我们输入一个句子时,可以利用One-Hot编码,即输入句子里的Harry映射为一个向量,该向量在4075位置值为1,其他位置值都为0,这样就可以用来表示Harry单词。最终,这句有9个单词的句子会用10000x9的二维向量来表示。【后面会说更方便的编码方式】
为什么要用循环神经网络?想象一下,如果我们使用标准神经网络来处理人名判断该怎么做?
按照前面的例子,有9个单词作为输入,我们可能采取One-Hot编码,因此输入的是9个编码向量,再经过一些隐藏层,最终输出9个值,可能是0或者1来表示每个单词是否为人名。
这个过程中主要存在两个问题:
循环神经网络RNN可以解决上面两个问题。
1、首先我们会读取到句子的第一个单词,将其送到神经网络中的一个隐藏层,对其尝试进行预测,判断是否属于人名的一部分。
2、循环神经网络做的事就是,当我们读取到第二个单词时,它不会直接就利用输入值来预测,还会利用到来自时间步1的信息(一个激活函数的值)。
3、以此类推,每次都会利用到之前的信息。当然,这个例子中输入和输出的长度是相同的,如果长度不同结构会采取一些改变,后面会说。在零时刻,我们会有一个初始激活值,通常是零向量。图中的 w a a 、 w a x 、 w y a w_{aa}、w_{ax}、w_{ya} waa、wax、wya是权重参数矩阵,每个时间步都是相同的,在后面会说明它们的含义。
因此,在RNN中,假设我们预测 y < 3 > y^{<3>} y<3>,我们不仅会使用 x < 3 > x^{<3>} x<3>还会使用 a < 2 > a^{<2>} a<2>,这样就利用到了 x < 1 > x^{<1>} x<1>和 x < 2 > x^{<2>} x<2>的信息。
当然,这样做也还是有缺陷,我们只是利用到了过去的信息,没有利用到未来的信息。
就像下面两个句子,只看前三个单词我们是无法确切判断Teddy是否为人名的,在第一个句子中Teddy Roosevelt组合起来我们知道Teddy是人名的一部分,而第二个句子Teddy bears是泰迪熊的意思,Teddy就不属于人名的一部分。因此我们还需要结合未来的信息。
我们会在之后的双向循环神经网络(Bi-RNN)中说明如何解决这个问题。
假设现在给出了一句话,我们通过RNN对这句话里单词进行人名判断。
我们会一个词一个词输入到神经网络中,同时每次输入的词在计算的时候都会利用到之前的信息(即 a < t − 1 > a^{
可以把这里的公式化简一下, a < t > = t a n h ( w a [ x < t > , a < t − 1 > ] + b a ) a^{
假设 a < t − 1 > a^{
为了计算反向传播,我们需要定义损失函数。
我们先定义一个单词位置上的损失函数(或者叫一个时间步的损失函数):
L < t > ( y ^ < t > , y < t > ) = − y < t > l o g y ^ < t > − ( 1 − y < t > ) l o g ( 1 − y ^ < t > ) L^{
那么整个序列的损失函数,就是对每个位置的损失函数求和: L ( y ^ , y ) = ∑ t = 1 T L < t > ( y ^ < t > , y < t > ) L(\hat y, y)=\sum\limits_{t=1}^T L^{
有了损失函数,就可以如我们所知道的那样,在相反的方向上进行计算和传递信息,就像上图的红色箭头。通过导数相关的参数,用梯度下降算法来更新参数。这个算法在RNN中也称为通过时间的反向传播(Backpropagation through time)。
下面是一个单元的内部结构:
正如开头所说,像下面的音乐生成,我们输入甚至可以是空集,输出得到一段序列,而对于情感分析输入一段文本序列,输出是一个评分。它们输入和输出的长度并不相同,对应的RNN结构也就有所区别。
RNN结构大致分为下列几种:
就像我们前面的举例,对一段文本的人名进行判断,我们一个单词就是一个输入,同时每个输入对应这一个输出,判断这个单词是否是人名。【这种是输入和输出长度相同的情况】
如果输入和输出长度不同,例如机器翻译,可能我们输入的是法语,输出的是英语,它们都是表示同一个意思的序列,但它们长度可能不同。
此时的结构如下图所示,先把法语的句子一次读完(这部分叫编码器),然后再输出英语内容(这部分叫解码器)。
对于像文本情感分类或是电影评分之类的,就属于多对一,many-to-one。我们输入一段文本,包含了多个单词,最终输出的可能是0/1判断文本是正向还是负向,或是输出0~5来评价电影分数。这种结构就不需要每次输入都有一个输出,只需要在最后输出一个结果就行。
这个结构就和我们了解的小型的标准神经网络,输入X,得到输出Y。
类似音乐生成就是一对多的问题,我们可以输入一个整数来表示想要某种音乐风格,然后就没有输入了,或是初始就不输入使得初始状态是零向量。最终会得到多个输出,即多个音符。
当然,这里面其实有一个技术细节,我们会把第一个输出值喂给下一层,依次进行,所以最终得到如下图所示结构。
什么是语言模型?比如我们在做一个语音识别系统时,我们听到一个句子【The apple and pear salad was delicious.】,其中【pear】可能会听成【pair】(读音相同),但一个是苹果和梨的萨拉,一个是苹果和一对沙拉,显然我们应该是想表达第一个。而语音识别系统对于这种情况就需要判断我们到底希望表达什么意思,它会去计算这两句话各自的可能性。
例如:
语言模型所做的就是告诉我们某个特定的句子它出现的概率是多少,它是语音识别系统、机器翻译系统等系统的基本组成部分。
那么如何来建立一个语言模型呢?首先我们需要一个很大的文本语料库,语料库是自然语言处理的一个专有名词,意思就是很长的或者说数量众多的英文句子组成的文本。
假如说我们在训练集中得到这么一句话"猫一天睡15个小时"。
我们要做的第一件事就是将这个句子标记化,就像前面那样建立一个字典,把每个单词转换成对应的One-Hot编码。
其次,我们还可能需要设定一个句子的结尾,例如在末尾增加
注意,这里是把单词用 y < t > y^{
如果句子中有些词并不在我们的初始语料库中,我们会把它们当做
下面我们使用”Cats average 15 hours of sleep a day.
如果我们使用很大的训练集来训练这个RNN,我们可以通过开头一系列单词像是"Cats average 15"来预测之后单词的概率。假设现在有个只包含三个单词的新句子, y < 1 > 、 y < 2 > 、 y < 3 > y^{<1>}、y^{<2>}、y^{<3>} y<1>、y<2>、y<3>,现在要计算出整个句子中各个单词的概率。
在你训练一个序列模型之后,要想了解到这个模型学到了什么,一种非正式的方法就是进行一次新序列采样,来看看到底应该怎么做。
假设现在有一个已经经过莎士比亚文章训练的模型,我们想要检验下它到底学到了些什么,让它生成一篇内容。
np.random.choice
,来根据向量中这些概率的分布进行采样,这样就实现了对第一个词的采样。
基本的RNN算法是有一个很大的问题的,也就是梯度消失问题。
我们知道RNN模型的结构如下:
假设有个句子:The cat, which already ate …, was full.,这里cat是单数,所以后面用的was来保持一致。如果前面我们是用The cats,那么后面就应该用were。显然,对于中间部分的长度可以很长,这就会存在一个长期依赖,即最前面的单词会对后面单词造成影响。然而目前我们见到的RNN是不擅长处理这种长期依赖的。为什么?
学神经网络时我们知道,当一个网络层数很深时,它的反向传播就很难传播回去。对于RNN也是如此,我们一开始前向传播从左到右计算,然后进行反向传播从右到左,越往前面就会越难传播,由于梯度消失问题,后面层的输出误差很难影响到前面。
这就会导致我们很难让一个RNN意识到它前面记住的单词是单数还是复数,然后在序列后面生成依赖的was或是were。
同时,我们在反向传播的时候随着层数的增多,梯度不仅可能指数型下降,还可能指数型上升。也就是会存在梯度爆炸问题,不过梯度爆炸很明显,因为指数级大的梯度会让参数变得非常大,从而网络参数直接崩溃,所以很容易发现梯度爆炸。解决方法也很简单,可以使用梯度修剪,即观测到梯度大于某个阀值时缩放梯度向量就好。而想要解决梯度消失就比较麻烦,这也是我们后面需要解决的问题。
GRU(Gated Recurrent Unit,门控循环单元)改变了RNN的隐藏层,使其可以更好的捕捉深层连接,并改善了梯度消失问题。
从前面的学习中,我们知道RNN的隐藏层单元是下面这样的,其中
a < t > = t a n h ( w a [ x < t > , a < t − 1 > ] + b a ) , y < t > = s o f t m a x ( a < t > w y + b y ) a^{
从前面RNN的梯度消失问题中我们知道,对于句子”The cat, which already ate…, was full“,我们需要RNN记得最开始是单数的猫cat。
在GRU单元中,会有一个新的变量叫c,称为记忆细胞cell,它可以提供记忆能力,例如记住猫是单数还是复数。在时间t处,有记忆细胞 c < t > c^{
重点来了,在GRU中最重要的思想就是门,我们用符号 Γ u \Gamma_u Γu表示, Γ u = s i g m o i d ( w u [ c < t − 1 > , x < t > ] + b u ) \Gamma_u=sigmoid(w_u[c^{
现在,我们把上面的情况用公式来表示: c < t > = Γ u ∗ c ~ < t > + ( 1 − Γ u ) ∗ c < t − 1 > c^{
理解了原因,下面我们来看看GRU单元的结构图:
我们可以通过门决定在某个时间步中是要更新记忆细胞还是维持记忆细胞,只要维持细胞非常简单,只要门是0,这样最终输出的就会是输入值 c < t − 1 > c^{
上面我们描述的是简化版的GRU,它只包含了更新门,完整的GRU还会有重置门 Γ r \Gamma_r Γr(r表示reset重置),它的表达式和更新门表达式一样,只是参数矩阵和偏置不同: Γ r = s i g m o i d ( w r [ c < t − 1 > , x < t > ] + b r ) \Gamma_r=sigmoid(w_r[c^{
重置门用于决定过去有多少信息需要遗忘,有了更新记忆细胞的公式就改变了 c ~ < t > = t a n h ( w c [ Γ r ∗ c < t − 1 > , x < t > ] + b r ) \tilde{c}^{
看看下面的结构图:
LSTM(Long short term memory)也是一种RNN结构,它比GRU出现得更早,是一个比GRU更加强大和通用的坂本,相当于GRU单元是LSTM的简化版。GRU只含有重置门 Γ r \Gamma_r Γr和更新门 Γ u \Gamma_u Γu,而LSTM含有更新门 Γ u \Gamma_u Γu、遗忘门 Γ f \Gamma_f Γf、输出门 Γ o \Gamma_o Γo,它的公式如下:
对比一下GRU:
可以发现,LSTM里 c < t > ≠ a < t > c^{
我们什么时候应该用GRU什么时候用LSTM?这其实没有统一的准则,GRU的优点是模型简单,它只有两个门,所以更容易创建一个大型网络,在计算性方面运行得比较快,但LSTM更加强大和灵活,因为它有三个门。无论是GRU还是LSTM,我们都可以用它们来构建更加深层连接的神经网络。
前面我们已经了解了大部分RNN模型的关键构件,普通RNN、GRU单元、LSTM单元,还有两个方法可以提升我们的模型,分别是双向RNN模型和深层RNN。先从双向RNN开始吧~
依然沿用前面的例子,当我们需要判断人名时,如果是下面两句话,无论是普通RNN,还是用GRU单元、LSTM单元,它们都是从左往右依次处理。这样就会出现一个问题,对于"Teddy"这个词,它在第一句话里是泰迪熊,而第二句话里是人名的一部分,但这个结果需要我们考虑到第四个单词。也就是说,如果我们仅仅是从左往右来判断,可能无法知道第三个单词到底是不是人名。
那么对于一个双向的RNN是如何解决这个问题的呢?请看下面的图。
这里每个单元都是普通的RNN,如果我们只执行蓝色部分,那么就是单向的普通RNN进行预测。图中红色部分的四个单元就是反向循环层,它们是从右往左执行的,先输入beas到 a < 4 > a^{<4>} a<4>进行计算,然后将信息和Teddy传给 a < 3 > a^{<3>} a<3>,依次类推。
对于这个双向的RNN来说,执行流程是:我们先获取到完整的句子,然后正向序列执行,反向序列执行,最后正向的激活值结果要和反向的激活值结果进行一个预测处理。也就是说,不像之前单向那样,一个单元计算完就会出预测值,而是要等正向和反向全部执行完,才会预测每个单元的值。
预测公式: y ^ < t > = s i g m o i d ( w y [ a ← < t > , a → < t > ] + b y ) \hat{y}^{
现在来预测单词“Teddy”时,我们会有过去的信息(来自 a < 2 > a^{<2>} a<2>、 a < 1 > a^{<1>} a<1>)也会有未来的信息(来自 a < 4 > a^{<4>} a<4>),这样就能准确预测了。图中只是普通的RNN,我们可以用GRU或者LSTM来替换。
不过,对于双向的RNN显然是有一个缺点的,那就是我们必须要有完整的数据序列,假如我们想构建一个语音识别系统,我们用双向RNN去实现就需要等人把整句话说完,然后获取整个语音序列才能进行处理。对于多数NLP应用,只要我们能获取整个句子,那么双向RNN总是比较高效的。
前面所说的无论是普通RNN、GRU单元、LSTM还是双向RNN,它们都是简单的一层。如果要学习非常复杂的函数,通常会把RNN的多个层堆叠在一起构建更深的模型。
这是一层的RNN,每个单元根据输入以及之前的信息直接进行预测,左上角角标表示层:
这是两层的RNN,第一层的激活值作为第二层的输入:
对于RNN来说不会像CNN那样有很多层,通常对于深层RNN最多也就到三层,因为RNN是带有时间维度的。不过可以在预测值上面堆叠隐藏层,就像下面这样,顶部的两层是没有水平方向连接的。:
当然,上面的单元都是标准的RNN单元,通常会是GRU单元或者LSTM等,也可以构建双向RNN。不过深层的RNN训练需要很多计算资源,虽然这看起来没有多少层。