引言
在上篇文章cs231n 2023春季课程理解——lecture_7中,我们讲解了如何去训练一个卷积神经网络,比如说如何选取激活函数,怎么进行数据预处理等。接下来我们来熟悉一下深度学习中的另一个神经网络——循环神经网络(Recurrent Neural Networks)
循环神经网络(Recurrent Neural Networks)
前面我们了解了卷积神经网络,它其实是神经网络中的一部分,属于单个输入单个输出(例如图像分类,输入一张图像,输出对应的类别)。而除此之外,神经网络其实还有着不同种形式,比如说单个输入多个输出(例如图像描述,输入一张图像,输出一句文本)、多个输入单个输出(例如动作预测,从视频帧之中预测动作类别)以及多个输入多个输出(例如视频描述,输入多帧图片,输出描述这个视频的文本等)。而这些形式就是我们接下来要了解的循环神经网络
概念
如上图所示,循环神经网络与卷积神经网络最大的不同在于其有一个反复出现的小循环模块。将输入x输送到这个模块中,它能计算得到内部的hidden state(主要我也不晓得怎么翻好),并且会在每次读取新的输入时进行更新。对于一个输入向量x,其hidden state更新的公式为:
h
t
=
f
w
(
h
t
−
1
,
x
t
)
h_t=f_w(h_{t-1},x_t)
ht=fw(ht−1,xt)其中,
h
t
h_t
ht是新状态,
f
w
f_w
fw是一些和参数w有关的函数,
h
t
−
1
h_{t-1}
ht−1是更新前的状态,
x
t
x_t
xt是在一些时刻输入的数据(当然,对于每个循环模块,他们的函数和参数的设置都是一样的)。如果想对某个时刻的状态进行输出,那么其公式为:
y
t
=
f
W
h
y
(
h
t
)
y_t=f_{W_{hy}}(h_t)
yt=fWhy(ht)其中,
y
t
y_t
yt是输出,
f
W
h
y
f_{W_{hy}}
fWhy是与参数w有关的另一个函数,
h
t
h_t
ht是新的状态。例如,在普通的循环神经网络中,它的隐藏态
h
t
h_t
ht以及输出公式为:
h
t
=
t
a
n
h
(
W
h
h
h
t
−
1
+
W
x
h
x
t
)
h_t=tanh(W_{hh}h_{t-1}+W_{xh}x_t)
ht=tanh(Whhht−1+Wxhxt)
y
t
=
W
h
y
h
t
y_t=W_{hy}h_t
yt=Whyht
计算图
在学习卷积神经网络时,我们了解过它的计算图形式。现在,我们也来看看循环神经网络的计算图是什么样子的。
如上图所示,循环神经网络的计算流程首先是对于初始隐藏状态
h
0
h_0
h0与输入
x
1
x_1
x1进行一个计算(在带有参数W的函数下计算,例如普通循环神经网络中的tanh函数),最终得到隐藏状态
h
1
h_1
h1。然后再一直重复这个过程。而由于这些参数W和与参数相关的函数f都是一样的,因此在最终反向传播计算梯度时,只需要将每个时刻的梯度累加起来就可以得到最终的W的梯度。
多对多
循环神经网络的计算图大致就是上面所展示的,此外,对于不同的形式(比如多对多,多对一等)会有一些细微的区别。比如说多对多的,它会有多个输入,并产生多个输出。此外,会对每个输出使用一个损失函数来进行损失值的计算(如果这些输出都有真实标签的话),这样的话,最终的损失值也可以使用这些损失累加。
多对一
类似的,循环神经网络还可以进行多对一的任务,比如说情感分析。在这种任务重,模型经过迭代之后,会将所有的信息包含在最后一个隐藏态
h
t
h_t
ht中。
一对多
如果我们想接收固定大小的输入然后产生一个可变大小的输出,那么使用的应该是一对多形式的循环神经网络。这种情况下,会将输入项用来初始化隐藏态
h
0
h_0
h0,然后RNN对每个输出单元进行处理,最终得到一个不定长的输出序列。其计算图如下。
序列对序列
这个其实也是多对多,但与前面讲的多对多不同的是,前面的多对多其实是输入与输出一一对应,但是序列对序列却不是。序列对序列可以看做是由多对一和一对多两种形式组合而成。也就是说,它对于一个不定长的输入x,首先先将其编码成一个个独立的向量,然后将它们作为输入,并使用多对一形式的循环神经网络,得到一个类似的定长的表达,最终再将这个表达输入到一对多形式的循环神经网络中,得到不定长的输出。之后,网络再对这个进行预测(比如说接下来应该是什么词汇等)。其计算图如下:
举例
语言模型
对于循环神经网络,我们经常将其用到语言模型里面。语言模型是指我们让模型读取大量语料语句,然后让这个网络了解如何生成自然语言的顺序,这可能是发生在字符级(即对语料中的内容以字符粒度进行学习),比如循环神经网络读取一串字符序列,然后模型需要预测这个字符流的下一个字符是什么。接下来,我们来看一个简单的例子:现在有一个很小的字符表[h,e,l,o]和一个训练序列"hello",现在使用的参数函数为tanh,即循环公式为:
h
t
=
t
a
n
h
(
W
h
h
h
t
−
1
+
W
x
h
x
t
)
h_t=tanh(W_{hh}h_{t-1}+W_{xh}x_t)
ht=tanh(Whhht−1+Wxhxt)在语言模型的训练阶段,将训练序列作为输入项,并将其分解为独立的向量,即每一个时刻输入一个字符。这时,我们首先需要做的应该是让循环神经网络能够表示这个序列,如下图所示。
在上图中,我们使用了一个长度为4的向量来表示每个字符(字符对应的位置为1,其他的为0)。那么在第一个时刻(time step)中,循环神经网络收到了第一个输入的字符向量h;然后将其输送到循环模块中,得到输出
y
1
y_1
y1,作为对接下来的字符的一个预测,即网络模型认为接下来应该输入的字符。之后再继续上述步骤。如果在预测中预测错误,那么可以使用softmax来计算损失值,以此来度量对结果的满意程度,并进行优化。而在测试的时候,我们可以使用一个句子的第一个字符来进行输入,然后由模型来进行输出预测,并将输出作为接下来的输入预测,以此类推。
截断反向传播(Truncated Backpropagation)
循环神经网络在前向传播的时候需要遍历整个序列来累加损失,同样地,在反向传播中,它也需要遍历整个序列来计算梯度。面对这种情况,如果在训练语言模型时,语料库比较大(比如说维基百科的所有内容),那么它花费的时间和内存的占用都是非常巨大的(见下面第一张图)。因此,在实际应用中,一般都会使用沿时间的截断反向传播(Truncated Backpropagation),这样输入的序列就可以接近无穷。即在前向传播时不使用整个序列计算损失,而是用其中的一小部分,比如说50个间隔(timestep),来计算损失值,同时通过反向传播来计算梯度。然后再循环这个操作(见下面第二张图),从而使得开销大大减小。这个过程的代码详见min-char-rnn.py。
应用
生成的语言模型,有着许多的应用。比如说,用莎士比亚的文集来训练语言模型,最终可以生成能带有其风格的文章;生成一些公式;生成C代码等。
过程解释
之前我们知道,卷积神经网络中有隐藏层。其实,循环神经网络(RNN)中也有隐藏向量,并且这些隐藏向量在每一个时间步结束之后都会进行更新(即隐藏态
h
t
h_t
ht),而模型在训练的过程中就需要在这些隐藏向量中寻找可解释的单元。
在我们开始寻找的时候,这些隐藏向量看起来就是杂乱无章的,因此我们需要选择隐藏向量中的一个元素,并跟进它,利用RNN来训练之后,现在每个元素上的颜色(比如蓝色对应数值比较低,红色是数值高)对应于每个时间步的隐藏向量中单个元素的大小,所以你可能能看到这些隐藏状态中的向量似乎不太容易解释。此时语言模型感觉上在做一个低长度的低级语言建模,弄清楚接下来该使用哪些字符。由于其中有些字符的最终效果非常好,因此模型会寻找这些向量来进行引用定位。最终会变成在每句开始时数值都比较低,然后慢慢变红,之后再进行换行。具体流程见下图。
看图说话(Image Captioning)
之前提到过,循环神经网络可以定长输入,而输出则不定长。这其中的一个经典的应用就是看图说话。即在循环神经网络中输入是一张图像,输出是关于它的自然语言描述。
如上图所示,看图说话的模型一般都是先使用一个卷积神经网络来对图像进行处理,从而生成一个向量(特征向量,表示图像的特征),之后再将其输入到循环神经网络之中,最终得到关于这张图像的自然语言描述。
其他例子
除了语言模型以及看图说话之外,循环神经网络还能实现根据图片回答问题(两个输入,一个是图片,另一个是问题,最终得到一个结果);根据图片进行对话等。
小结
RNN的优点:
- 可以处理任意长度的输入。
- 可以使用之前许多步(间隔,timestep)的信息来计算当前时刻t(理论上)。
- 模型的大小并不会因为输入比较长而增大。
- 在每个间隔都应用相同的权重,因此输入的处理方式是对称的。
RNN的不足:
- 循环计算非常慢
- 实际上,从前面的一些间隔(timestep)来获取信息是非常困难的。
其它形式的RNN
多层RNN(Multilayer RNNs)
上面我们讲述的RNN,其隐藏层只有一层,但是,实际上隐藏层可能有多层(和卷积神经网络中感知机和多层感知机类似),这就是多层循环神经网络。此时,运行一次多层RNN得到的是一个隐藏态的序列,并将其当做是第二个隐藏态的输入。如下图所示:
长短期记忆网络(Long Short Term Memory, LSTM)
普通RNN梯度流
现在,我们先来看看普通RNN的梯度流是如何计算的。在之前我们知道了
h
t
h_t
ht的计算,现在我们对其进行形式上的变换,有:
h
t
=
t
a
n
h
(
W
h
h
h
t
−
1
+
W
x
h
x
t
)
=
t
a
n
h
(
(
W
h
h
W
x
h
)
(
h
t
−
1
x
t
)
)
=
t
a
n
h
(
W
(
h
t
−
1
x
t
)
)
\begin{aligned}h_t&=tanh(W_{hh}h_{t-1}+W_{xh}x_t)\\ &=tanh(\begin{pmatrix} W_{hh} &W_{xh}\end{pmatrix}\begin{pmatrix} h_{t-1} \\ x_{t}\end{pmatrix}) \\ &=tanh(W\begin{pmatrix} h_{t-1} \\ x_{t}\end{pmatrix})\end{aligned}
ht=tanh(Whhht−1+Wxhxt)=tanh((WhhWxh)(ht−1xt))=tanh(W(ht−1xt))即反向传播时,梯度流从
h
t
h_t
ht到
h
t
−
1
h_{t-1}
ht−1需要乘以W(实际上是
W
h
h
T
W_{hh}^T
WhhT)。那么当RNN之中的隐藏向量比较多时,梯度从最后一个隐藏态传到第一个隐藏态需要乘以多次权重(如下图所示)。这就会导致,如果累计相乘的值大于1,那么它最终可能导致梯度爆炸(可能会变成无穷大)。如果累计相乘的值小于1,最终可能导致梯度消失(因为一直乘以一个小于1的数,得到的数会变小,知道接近0)。
LSTM
在前面我们看到了普通的RNN在计算梯度的时候的不足。不过,如果产生了梯度爆炸的现象,其实我们可以设置一个阈值,通过使用L2范式来给超过这个阈值的梯度进行降低,这样就可以在一定程度上避免梯度爆炸。那么梯度消失呢?一个比较好的办法就是更换RNN的网络架构,比如说LSTM。LSTM的计算如下所示:
(
i
f
o
g
)
=
(
σ
σ
σ
t
a
n
h
)
W
(
h
t
−
1
x
t
)
c
t
=
f
⊙
c
t
−
1
+
i
⊙
g
h
t
=
o
⊙
t
a
n
h
(
c
t
)
\begin{aligned}\begin{pmatrix} i\\f\\o\\g\end{pmatrix}&=\begin{pmatrix} \sigma\\\sigma\\\sigma\\tanh\end{pmatrix}W\begin{pmatrix}h_{t-1}\\x_t\end{pmatrix}\\c_t&=f\odot c_{t-1}+i\odot g\\h_t&=o\odot tanh(c_t)\end{aligned}
ifog
ctht=
σσσtanh
W(ht−1xt)=f⊙ct−1+i⊙g=o⊙tanh(ct)其中,
i
i
i是输入门(input gate),表示有多少内容被写到单元态。
f
f
f是遗忘门(forget gate),表示对之前的单元状态的遗忘程度。
o
o
o是输出门(output gate),表示但愿状态输出多少给隐藏态。
g
g
g是gete gate(我也不清楚这个是啥,应该如何翻译),它控制写入到单元状态的信息。
⊙
\odot
⊙表示逐元素相乘。
在LSTM中,每一步都会有两个隐藏态,其中
h
t
h_t
ht和一般的RNN一样,另外还有一个单元状态向量
c
t
c_t
ct,它是保留在LSTM内部的隐藏态,不会完全暴露到外面。从上面的公式我们可以看出,LSTM会使用输入和之前的隐藏态来更新四个组成
c
t
c_t
ct的门,然后使用
c
t
c_t
ct来更新
h
t
h_t
ht。其反向传播如下图所示,从
c
t
c_t
ct到
c
t
−
1
c_{t-1}
ct−1只需要乘以
f
f
f(f的值在不同时刻会不同),而不需要再乘以W了,这样就能在一定程度上避免梯度消失。
其他RNN
除了LSTM之外,还有这许多学者研究了一些其他的RNN,也有很好的效果。
总结
在本篇文章中,简单地介绍了一下循环神经网络,同时介绍了一下它的应用,如语言模型,看图说话等。此外,还介绍了一下LSTM。
注
本文所有图片均来自于cs231公开课的网站之中。