本系列文章面向深度学习研发者,希望通过Image Caption Generation,一个有意思的具体任务,深入浅出地介绍深度学习的知识。本系列文章涉及到很多深度学习流行的模型,如CNN,RNN/LSTM,Attention等。本文为第11篇。

作者:李理 
目前就职于环信,即时通讯云平台和全媒体智能客服平台,在环信从事智能客服和智能机器人相关工作,致力于用深度学习来提高智能机器人的性能。

相关文章: 

李理:从Image Caption Generation理解深度学习(part I) 
李理:从Image Caption Generation理解深度学习(part II) 
李理:从Image Caption Generation理解深度学习(part III) 
李理:自动梯度求解 反向传播算法的另外一种视角 
李理:自动梯度求解——cs231n的notes 
李理:自动梯度求解——使用自动求导实现多层神经网络

李理:详解卷积神经网络

李理:Theano tutorial和卷积神经网络的Theano实现 Part1

李理:Theano tutorial和卷积神经网络的Theano实现 Part2

李理:卷积神经网络之Batch Normalization的原理及实现

上文介绍了Batch Normalization技术。Batch Normalization是加速训练收敛速度的非常简单但又好用的一种实用技术,下面我们介绍可以提高模型的泛化能力的DropOut技术。

4. Dropout

4.1 Dropout简介

dropout是一种防止模型过拟合的技术,这项技术也很简单,但是很实用。它的基本思想是在训练的时候随机的dropout(丢弃)一些神经元的激活,这样可以让模型更鲁棒,因为它不会太依赖某些局部的特征(因为局部特征有可能被丢弃)。

图片描述

上图a是标准的一个全连接的神经网络,b是对a应用了dropout的结果,它会以一定的概率(dropout probability)随机的丢弃掉一些神经元。

4.2 Dropout的实现

实现Dropout最直观的思路就是按照dropout的定义来计算,比如上面的3层(2个隐藏层)的全连接网络,我们可以这样实现:

""" 最原始的dropout实现,不推荐使用 """p = 0.5 # 保留一个神经元的概率,这个值越大,丢弃的概率就越小。def train_step(X): 

  H1 = np.maximum(0, np.dot(W1, X) + b1)
  U1 = np.random.rand(*H1.shape) < p # first dropout mask
  H1 *= U1 # drop!
  H2 = np.maximum(0, np.dot(W2, H1) + b2)
  U2 = np.random.rand(*H2.shape) < p # second dropout mask
  H2 *= U2 # drop!
  out = np.dot(W3, H2) + b3  # 反向梯度计算,代码从略def predict(X):
  H1 = np.maximum(0, np.dot(W1, X) + b1) * p # NOTE: scale the activations
  H2 = np.maximum(0, np.dot(W2, H1) + b2) * p # NOTE: scale the activations
  out = np.dot(W3, H2) + b3

我们看函数 train_step,正常计算第一层的激活H1之后,我们随机的生成dropout mask数组U1。它生成一个0-1之间均匀分布的随机数组,然后把小于p的变成1,大于p的变成0。极端的情况,p = 0,则所有数都不小于p,因此U1全是0;p=1,所有数都小于1,因此U1全是1。因此越大,U1中1越多,也就keep的越多,反之则dropout的越多。 
然后我们用U1乘以H1,这样U1中等于0的神经元的激活就是0,其余的仍然是H1。 
第二层也是一样的道理。

predict函数我们需要注意一下。因为我们训练的时候会随机的丢弃一些神经元,但是预测的时候就没办法随机丢弃了【我个人觉得也不是不能丢弃,但是这会带来结果会不稳定的问题,也就是给定一个测试数据,有时候输出a有时候输出b,结果不稳定,这是实际系统不能接受的,用户可能认为你的模型有”bug“】。那么一种”补偿“的方案就是每个神经元的输出都乘以一个p,这样在”总体上“使得测试数据和训练数据是大致一样的。比如一个神经元的输出是x,那么在训练的时候它有p的概率keep,(1-0)的概率丢弃,那么它输出的期望是p x+(1-p) 0=px。因此测试的时候把这个神经元乘以p可以得到同样的期望。

但是这样测试的时候就需要多一次乘法,我们对于训练的实时性要求没有测试那么高。所以更为常见的做法是如下面的代码:

p = 0.5 # 保留一个神经元的概率,这个值越大,丢弃的概率就越小。def train_step(X):
  H1 = np.maximum(0, np.dot(W1, X) + b1)
  U1 = (np.random.rand(*H1.shape) < p) / p # first dropout mask. Notice /p!
  H1 *= U1 # drop!
  H2 = np.maximum(0, np.dot(W2, H1) + b2)
  U2 = (np.random.rand(*H2.shape) < p) / p # second dropout mask. Notice /p!
  H2 *= U2 # drop!
  out = np.dot(W3, H2) + b3def predict(X):
  # ensembled forward pass
  H1 = np.maximum(0, np.dot(W1, X) + b1) # no scaling necessary
  H2 = np.maximum(0, np.dot(W2, H1) + b2)
  out = np.dot(W3, H2) + b3

上面的代码在训练的时候给U1的每个元素除以p,相当于给H1放大1/p倍,那么预测的时候,那么后面层的参数学到的就相当于没有dropout的情况,因此预测的时候就不需要再乘以p了。比如第1层的输出是100个神经元,假设每一个神经元的输出都是0.5。如果没有dropout,这100个神经元都会连接到第二层的第一个神经元,假设第二层的第一个神经元的参数都是1,那么它的累加和是50。如果使用了0.5的概率保留,则第二层第一个神经元的累加和变成了25。但是上面的算法,我们对每个神经元的输出都先除以0.5。也就是第一层每个输出都是1,一个100个输入给第二层的第一个神经元,然后又以0.5的概率丢弃,那么最终累加的结果还是50。这样就”相当于“补偿”了第二层的“损失”。

4.3 实现

我们打开作业2的Dropout.ipynb。

4.3.1 cell1-2

和之前的一样的代码

4.3.2 cell3

打开layers.py,把dropout_forward里缺失的代码实现如下:

  if mode == 'train':    ###########################################################################
    # TODO: Implement the training phase forward pass for inverted dropout.   #
    # Store the dropout mask in the mask variable.                            #
    ###########################################################################
    [N,D] = x.shape
    mask = (np.random.rand(N,D) < (1-p))/(1-p)
    out = x * mask    ###########################################################################
    #                            END OF YOUR CODE                             #
    ###########################################################################
  elif mode == 'test':    ###########################################################################
    # TODO: Implement the test phase forward pass for inverted dropout.       #
    ###########################################################################
    out = x    ###########################################################################
    #                            END OF YOUR CODE                             #
    ###########################################################################

代码非常简单,不过注意的是这里的p是丢弃的概率,所以保留的概率是(1-p)。

运行这个cell的结果如下图:

图片描述

注意使用不同的dropout的结果是差不多的,因为我们实现的方法是“补偿”,如果您使用第一种实现,那么均值应该随着p而变化

4.3.3 cell4

接下来是实现dropout_backward,这也非常简单,只有一行代码:

if mode == ‘train’:

    ###########################################################################
    # TODO: Implement the training phase backward pass for inverted dropout.  #
    ###########################################################################
    dx = mask * dout    ###########################################################################
    #                            END OF YOUR CODE                             #
    ###########################################################################

然后我们运行cell4进行gradient check

图片描述

4.3.4 cell5

然后我们需要修改fc_net.py来增加dropout的支持,具体来说,如果dropout参数不是空,那么每一个relu的输出都需要增加一个dropout。

4.3.4.1 loss函数

init里代码已经处理好了,不需要任何修改,我们只需要修改loss的forward和backward部分。

首先是loss函数的forward部分:

    dropout_cache = {}
    for i in range(1, self.num_layers):
      keyW = 'W' + str(i)
      keyb = 'b' + str(i)      if not self.use_batchnorm:
        current_input, affine_relu_cache[i] = affine_relu_forward(current_input, self.params[keyW], self.params[keyb])      else:
        key_gamma = 'gamma' + str(i)
        key_beta = 'beta' + str(i)
        current_input, affine_bn_relu_cache[i] = affine_bn_relu_forward(current_input, self.params[keyW],                                                                        self.params[keyb],                                                                        self.params[key_gamma], self.params[key_beta],                                                                        self.bn_params[i - 1])      if self.use_dropout:
        current_input, dropout_cache[i] = dropout_forward(current_input,self.dropout_param)

和之前的代码比其实就增加了两行,如果self.use_dropout。 
当然还要记得在外面定义dropout_cache这个dict

然后是backward部分:

    for i in range(self.num_layers - 1, 0, -1):      if self.use_dropout:
        affine_dx = dropout_backward(affine_dx, dropout_cache[i])      if not self.use_batchnorm:
        affine_dx, affine_dw, affine_db = affine_relu_backward(affine_dx, affine_relu_cache[i])      else:
        affine_dx, affine_dw, affine_db, dgamma, dbeta = affine_bn_relu_backward(affine_dx, affine_bn_relu_cache[i])
        grads['beta' + str(i)] = dbeta
        grads['gamma' + str(i)] = dgamma

在for循环的最上面增加 if self.use_dropout这两行。 
注意顺序,我们是把dropout放到激活函数之后,因此反向求梯度是要最先计算affine_dx

完成代码后我们执行这个cell坚持梯度是否正确计算:

图片描述

4.3.5 cell6-7

As an experiment, we will train a pair of two-layer networks on 500 training examples: one will use no dropout, and one will use a dropout probability of 0.75. We will then visualize the training and validation accuracies of the two networks over time.

接下来我们做一个实验,我们会用500个数据训练一对2层的网络:其中一个使用dropout(0.75的丢弃概率),一个不用。

训练数据上的准确率

图片描述

验证数据上的准确率

图片描述

从上面两个图可以看出:不使用dropout,训练数据很少时会过拟合,在训练数据上准确率100%,但是验证数据上只有28%。而使用了dropout之后,训练数据90%,但是验证数据上能提高到30%以上。

这说明dropout确实能缓解过拟合的问题。

本篇文章介绍了Dropout技术,在下一篇文章中,我们将深入讲解一个CNN网络的具体实现。