Stanford2021年春季课程CS231N:Convolutional Neural Networks for Visual Recognition的一些作业笔记,这门课的作业围绕视觉相关的任务,需要从底层手动实现一大批经典机器学习算法和神经网络模型,本文是作业的第五部分,包含了RNN/LSTM的实现和在Image Captioning任务上的应用
Image Captioning
Image Captioning是一个经典的多模态任务,需要结合CV和NLP的知识共同完成,这类任务也可以叫图片描述,看图写话,即给定一张图像,我们要生成符合这张图像的描述性话语,这就涉及到一个问题,那就是如何在训练过程中,同时利用图像和文本的信息得到合适的模型,因此Image Captioning任务需要将CV和NLP的知识相结合。
本实验中使用的数据集是著名的COCO数据集,同时使用了预训练好的VGG模型进行降维之后的图像表示向量作为图像的特征,文本处理部分则需要我们自己实现RNN/LSTM
RNN
RNN的结构
- RNN指的是循环神经网络,是一种设计出来用于处理序列化数据(比如自然语言,时序数据)的神经网络,RNN可以保留一定的上下文信息并将序列前面的信息不断向后面传递。
- RNN的架构如下图所示,主要包含一个输入层,一个隐层和一个输出层,隐层单元的隐状态
也会不断向下一个隐层单元传递。
- RNN的隐状态更新公式可以表示为:
- 这里的可学习参数包括
,分别是两个权重矩阵和一个bias向量,而激活函数通常使用双曲正切tanh函数,而最终的输出结果的计算方式是:
RNN的前向传播及实现
- 从RNN的架构可以看出,我们输入RNN的数据是一个序列
,这个序列需要在RNN中按照顺序逐渐前向传播,最终得到一个隐状态的序列 ,而每一个单元内的前向传播过程可以用函数 rnn_step_forward
来描述
1 | def rnn_step_forward(x, prev_h, Wx, Wh, b): |
- 而整个序列的前向传播需要使用一个循环来完成,因此RNN的训练不能并行化,这也是RNN的一个重大缺点。我们使用一个函数来描述整个序列在RNN中的前向传播过程:
1 | def rnn_forward(x, h0, Wx, Wh, b): |
- RNN的前向传播总体来说比较简单。
RNN的反向传播及实现
- RNN的反向传播和CNN等传统神经网络不同,是一种“时间”上的反向传播,也就是说在计算梯度并更新参数的时候,不仅要考虑从最上层的loss函数中传递下来的梯度,也要考虑从后面一个隐层单元(从前向传播的时间来看要更迟,所以被称为时间反向传播)传递下来的梯度
- 而对于各个参数,其梯度的计算方式如下(注意tanh函数导数的特殊性):
- 根据这些公式和梯度的链式法则,我们可以写出一个隐层单元中的反向传播过程,用一个函数
rnn_step_backward
来表示
1 | def rnn_step_backward(dnext_h, cache): |
- 而RNN整体的反向传播也需要一个序列来完成,同时考虑从输出层的loss函数传递下来的梯度和从后一个隐藏单元传递下来的梯度,并使用函数
rnn_step_backward
来完成单个隐状态的更新。
1 | def rnn_backward(dh, cache): |
RNN的缺点
- 从上面的代码实现中可以看出,RNN一个很大的缺点就是对于一个序列必须串行化训练,不能并行训练,造成训练的速度非常缓慢,同时RNN也存在梯度爆炸和梯度消失的问题,对于长距离的依赖关系也缺乏学习的能力,这些问题的具体分析可以在CS224N的相关笔记中看到
- 拥有门控单元的RNN可以在一定程度上解决这个问题,比如LSTM
LSTM
LSTM的基本架构
- LSTM是
Long Short Term Memory
(长短期记忆模型),是一种加入了门控单元的RNN,具有更长的“记忆能力”,可以捕捉到序列化输入中的长距离依赖关系(比如一个文本中距离很远的两个单词的某种特定关系) - LSTM的组成单元是在RNN的组成单元的基础上加上了若干个控制单元(也叫做门)对整个单元进行控制,有输入门,输出门,遗忘门等等,同时又有一系列
memory cell
作为“长期记忆单元”,这些cell也要传递到下一个隐层中,用于保存当前输入的长期记忆,其基本组成单元的架构如下图所示:
LSTM的计算过程
- 三个门控单元的结果计算
- 生成新的记忆单元
- 按照门控单元的内容来控制输出结果,生成新的
- 生成最终的隐状态:
- 这里的三个门控单元和记忆单元分别起到如下作用:
- 记忆单元:暂时生成一个当前输入内容的记忆
- 输入门:评估当前的序列输入
的重要程度,将其加权反映到最终记忆单元中 - 遗忘门:评估上一个单元输入的记忆单元的重要程度,将其加权反映到最终记忆单元中
- 输出门:将最终的记忆单元和隐状态区分开,因为最终记忆单元中的很多序列信息是没有必要暴露到隐藏状态中的,输出门就是来评估有多少信息可以从最终记忆单元传递到隐状态中
- 整个计算过程可以用一张图来表示:
LSTM的前向传播实现
我们发现跟RNN相比,LSTM还多了三个门控单元和一个记忆单元需要处理,并且这些组件的值都需要用输入的
因此LSTM单元的前向传播可以用下面这个函数lstm_step_forward
来实现:
1 | def lstm_step_forward(x, prev_h, prev_c, Wx, Wh, b): |
- 而LSTM整体的前向传播和普通的RNN没有什么区别,这里就不重复了。
LSTM的反向传播的实现
LSTM的反向传播过程中,依然遵循RNN的反向传播的基本形式,即时间反向传播,在计算梯度的时候需要考虑后一个隐状态传递回来的梯度,并且需要对
1 | def lstm_step_backward(dnext_h, dnext_c, cache): |
- LSTM的整个序列的反向传播和RNN也类似,这里就不放基本重复的代码了,至此我们完成了RNN和LSTM的基本单元的前向传播和反向传播的底层代码的实现。
CaptioningRNN的实现
事实上我们遵循和前面MLP/CNN一样的范式,在__init__
中定义所有的参数,并在loss
中实现神经网络的前向传播和反向传播,具体的实现代码如下:
1 | class CaptioningRNN: |
训练的结果如下图所示,模型可以进行一些简单的看图说话功能: