1 minute read

1. CS231n课程

CS231n是斯坦福大学李飞飞团队的一门关于卷积神经网络CNN的课程,这个课程从KNN和线性分类器讲到普通的神经网络,再将到卷积神经网络的实现,以及一些实用的技术如Dropout、Batch Normalization等,整个课程下来后会对CNN有个比较全面的了解。这个课程通俗易懂,是入门深度学习的良心课程,不仅图文结合阐述了网络正向和反向传播的过程,还会介绍一些在实用中的应用的trick如训练过程的梯度检查、如何判断学习率大小、如何判断过拟合等等,以及相应的解决方法

官方网站:http://cs231n.github.io/ 知乎中文翻译:https://zhuanlan.zhihu.com/p/21930884

1.2 Assignment 2 使用

配套CS231n课程的有两次作业,作业2主要有四部分内容:全连接层的实现、Batch Normalization、Dropout和卷积层的实现,CS231n有作业的初始模板,可以在此下载,模板里已经填好了实现这些网络的部分代码,学习者只需要在特定的方法中填写自己的代码即可,例如下面全连接层的forward方法,只需理解了课程笔记后按照提示在TODO中填写代码,填写完后,模板中还提供了方法来检查你代码的正确性:运行IPython notebook,这个notebook会引导你如何填写代码并测试代码,最后会在CIFAR-10数据集上测试你的CNN的准确性。

def affine_forward(x, w, b):
  """
  Computes the forward pass for an affine (fully-connected) layer.

  The input x has shape (N, d_1, ..., d_k) and contains a minibatch of N
  examples, where each example x[i] has shape (d_1, ..., d_k). We will
  reshape each input into a vector of dimension D = d_1 * ... * d_k, and
  then transform it to an output vector of dimension M.

  Inputs:
  - x: A numpy array containing input data, of shape (N, d_1, ..., d_k)
  - w: A numpy array of weights, of shape (D, M)
  - b: A numpy array of biases, of shape (M,)
  
  Returns a tuple of:
  - out: output, of shape (N, M)
  - cache: (x, w, b)
  """
  out = None
  #############################################################################
  # TODO: Implement the affine forward pass. Store the result in out. You     #
  # will need to reshape the input into rows.                                 #
  #############################################################################
  pass
  #############################################################################
  #                             END OF YOUR CODE                              #
  #############################################################################
  cache = (x, w, b)
  return out, cache

本文对作业中填写的代码参照了CS231n (winter 2016) : Assignment2 - 简书,完整的代码位于Github,运行前请参考CS231n的Readme,Readme中详细介绍了环境的搭建、数据集的下载,如何使用IPython,以及相应的编译过程(如果要用Cpython加速训练过程的话)

请先下载初始模板,搭建Python环境,下载数据集,运行IPython,然后参考notebook中的提示,填写相应代码。由于各个部分的代码在简书和Github中已有,不再累述。下面主要说一些在CIFAR-10数据库上通过改变CNN网络结构参数提高准确率的心得

2. 在CIFAR-10上的表现

2.1 简单两层神经网络

当完成Part 1:全连接神经网络后,notebook会提示在CIFAR-10上跑一下,此时若代码实现正常,在测试集上基本上可以达到50%-55%的准确率,网络结构为简单两层,具体请见Github中的classifiers/fc_net.py,下面总结实战中的一些小的trick:

Trick 1: 如何测试我们实现的模型有效性:小数据集过拟合: 如何判断我们的方法到能不能work,根据notebook里的提示,可以在一个很小的数据集上跑几个epoch,观察我们的模型是否能够对训练集很好的过拟合,而测试集准确率很低,具体是:在训练集这个100张图片里,是否能够实现99%-100%准确率的判断,而在测试集中另外的100张图片有比较低的准确率(10%左右),这里的模型指我们手动实现的算法

Trick 2: 如何判断对层的实现是否正确:梯度检查: 在将我们的算法应用到正式的数据集上之前,需要对实现的层进行解析梯度和数值梯度的比较,具体方法在notebook里,数值梯度是微调1e-6参数获得的差值除以改变量1e-6得到的,而解析梯度是我们实现代码反向传播的输出(因为我们对层的更新都是根据求导法则来的,所以是梯度的解析值),将这两个梯度值比较,观察相对误差,能判断对这个层的实现有没有问题,相对误差在1e-7或者更小是很好的结果,若相对误差达到了1e-2,通常你的实现就有问题。但是,网络越深,相对误差会累计,在10层的网络里若有1e-2的相对误差,那也是可以的。除了梯度检查,notebook中提供了参考值用来检查你实现的权值更新准则如SGD+Momentum、RMSProp、Adam

Trick 3: 如何判断权值和偏移量参数的初始化是否正确: 初始化对训练过程也是非常重要的,所以我们需要对训练过程进行检查,当正则强度为0时,对于CIFAR-10的softmax输出的分类器(初始权值w为0.01量级的随机数,初始偏移b为0),一般初始的loss function的值为2.302,这是因为初始时分类器对每一类的概率期望为0.1(共有10类),因此softmax损失函数值为对于正确分类概率的负对数:-ln(0.1)=2.302

Trick 4: 如何判断学习率是否合适: 在训练过程中,需要对loss值进行实时地打印,可以判断当前训练的状态:高的学习率高会使损失值下降很快,然后停止在一个比较高的位置(相对最优),这是因为参数每次更新过大,导致在最优点附近震荡,但始终无法达到最优点,而过高的学习率会直接使损失值递增。过于低的学习率会导致损失值下降很慢,训练过程太长,引用笔记中的一张图来理解:

这里写图片描述

Trick 5: 如何判断模型的过拟合程度: 在训练过程中,我们还需要对每个epoch中的训练集和测试集的准确率进行打印,能够确定模型是否过拟合或者欠拟合,若训练集准确率一直大幅度高于验证集,说明此时模型过拟合,对训练集有过好的分类能力导致无法在验证集上进行比较好的分类,解决的方法可以增大正则化强度,如增大L2正则惩罚,增加dropout的随机失活率等。如果训练集一直小幅度低于验证集,说明此时稍微过拟合,而如果训练集和验证集的准确率不相上下,说明此时模型有点欠拟合,没有很好地学习到特征,此时可以调整模型参数如层的深度等,引用笔记中的一张图说明:

这里写图片描述

Trick 6: 如何判断训练中出现的梯度消散问题: 我们知道,当网络的层数过于深以后,会出现梯度消散的情况,也就是回传到前几层的梯度值很小,导致前面几层的参数无法更新。对此,我们可以打印前几层网络权值参数w的更新比例,经验结论是这个更新比例在1e-3比较比较好,若这个值太大,说明学习率太高;若这个值很小到1e-7,说明参数w基本上不会变,发生了梯度消散,解决方法为:1)使用Batch Normalization归一化每层之间的输出,2)激活函数改用线性ReLU,3)还有可能是学习率太低,4)减少网络层数

Trick 7: 如何判断训练过程是否稳定和有效: 若数据为图像数据,那么可以把第一层的权重进行可视化,观察模型是否学习到了比较的好的特征,notebook里内置了相关可视化的方法,若特征图中颜色杂乱无规律且充满噪音,说明训练过程未收敛(学习率太高)或者正则化惩罚不够,引用笔记中的图来解释,下图中的右图为比较好的特征,平滑而且种类繁多,说明训练过程有效且稳定

这里写图片描述

Trick 8: 如何进行有效的数据预处理: 在实际应用中CNN比较多的是减均值法和归一化,其他的处理方法为PCA和白化(Whitening):PCA能消除数据的相关性,使数据的分布在基准值上;白化则可看成是把数据在各个特征方向上进行拉伸压缩变化,使之服从均值为零的高斯分布,具体参考[知乎](https://zhuanlan.zhihu.com/p/21560667?refer=intelligentunit)

2.2 卷积神经网络

2.2.1 三层简单CNN

在写Part 2:卷积神经网络之前,会先完成Dropout和Batch Normalization这两部分。在完成了Part 2后,notebook会用classifiers/cnn.py 中一个三层的简单的卷积神经网络来跑CIFAR-10,最终的表现在测试集上达到55-59%这样一个结果,比普通的神经网络高了几个百分点,这个网络结构如下:

这里写图片描述

2.2.2 稍微复杂点的CNN

基于这个naive的CNN,我再加入一个卷积层和一个全连接层,去掉了Pool层,因为size为2的Pool层会使图像压缩至四分之一,而FICAR的图像大小为3232,经过一个Pool后变成了1616,信息损失太大,所以去掉Pool,考虑使用卷积层的stride=2或者3来压缩图像:

 INPUT --> [CONV --> RELU]*2 --> [FC --> RELU]*2 --> FC/OUT

此时在测试集上的精度大概能达到60-65%的程度,然后各种修改卷积层的padding,stride,filter_num参数,大概能提高到67%左右,而训练集精度基本上可以达到90%,说明模型有点过拟合,下一步考虑使用Dropout。

2.2.3 多层小卷积层CNN+Dropout

2.上述网络卷积层的过滤器尺寸始终未6或者7,相对于32*32的图像来说确实是一个比较大的尺寸,然而多层的小size的卷积层效果要比大的size的卷积层好:

现在,我们以3个3x3的卷积层和1个7x7的卷积层为例,加以对比说明。从下图可以看出,这两种方法最终得到的activation map大小是一致的,但3个3x3的卷积层明显更好: 1)、3层的非线性组合要比1层线性组合提取出的特征具备更高的表达能力; 2)、3层小size的卷积层的参数数量要少,3x3x3<7x7; 3)、同样的,为了便于反向传播时的梯度计算,我们需要保留很多中间梯度,3层小size的卷积层需要保留的中间梯度更少。 这里写图片描述 来自简书

因此,我使用小的卷积层,两个卷积层的过滤器都为filter_size=(3,3),使用stride=2来压缩图像;同时在输出层前一层加入了Dropout防止过拟合,随机失活率p=0.8,网络结构如下:

 INPUT --> [CONV --> RELU]*2 --> FC --> RELU --> FC --> RELU --> DropOut --> FC/OUT

此时在测试集上的精度大概能达到65-70%左右的程度,训练集精度在很多次epoch后还是维持在80%以上,这时的调整包括再次增加一个全连接层,但是精度还是不能很好提高,遇到了瓶颈,这时可以考虑加入Batch Normalization了。

2.2.4 分析大杀器ResNets

然后我去CIFAR数据集查好解决方案,看到了2015年最好的网络结构ResNets,能达到93%+的精度,这个网络最深能达到110层,而且在20层的时候就能达到91%了。我们考虑20层简单的情况:

INPUT --> [CONV --> BatchNorm --> RELU]*19 --> POOL --> FC/OUT

这个网络的特点为:每一卷积层都使用小的过滤器filter_size=(3,3),分阶段调整stride步长值:分别在第8层和第13层调整stride=2来压缩图像,其他卷积层的步长stride都1,而且每层卷积层后都会跟一个BatchNormal防止梯度弥散。通过分析这个网络,可以看出:

  1. 其实在stride>1的时候,stride跟pool一样,只保留了上个网络部分的信息,能起到压缩图像内容。

  2. zero-padding的作用不只是起到一个折中的方案:填补空白区域使卷积过程能够顺利进行,方便从过滤器能从初始位置以步长为单位可以刚好滑倒末尾位置,它的另外一个作用是为了保持图像的尺寸不变,根据公式output_size=(input_size+2padding-filter_size)/stride+1,我们以input_size=32, padding=1, filter_size=3, stride=1 来计算卷积后的图像大小output_size=(32-21-3)/1+1=32,可以看出输入图像和卷积后的图像大小并没有改变,这也就是ResNets为什么能在32*32这么小的图像上卷积100多次的原因了,而且只靠stride=2来压缩图像两次

对于ImageNet这样256*256图像的数据来说,怎么设置stride、padding、filter_size可能没有这么讲究,但是对于CIFAR小图像来说,如何巧妙地设计这些参数就有很大用处了,是深层网络必须的考虑的事

2.2.5 最后的挣扎

根据上面的分析,我最后挣扎了一下,因为机器不可能跑这么多层网络,还是采用了经典的CNN网络模型:

INPUT --> [CONV --> RELU --> CONV --> RELU --> POOL]*2 --> [FC --> RELU] --> DROPOUT --> [FC --> RELU] --> FC/OUT

四个卷积层三个全连接,每个卷积层fliter_size=(3,3), stride=1,padding=1,重新使用pool压缩图像,以下是我的最终参数,可以在classfiers/cnn_custom 里查看:

weight_scale=0.01, L2 regularization=0.0005, dropout=0.8, batch_size=100, optimizer=adam, learning_rate=0.001

INPUT: input_dim=(3,32,32)
CONV1: filters=64, filter_size=(3,3),stride=1, pad=1
CONV2: filters=64, filter_size=(3,3), stride=1, pad=1
POOL2: pool_height= 2, pool_width= 2, stride= 2
CONV3: filters=64, filter_size=(3,3),stride=1, pad=1
CONV4: filters=64, filter_size=(3,3), stride=1, pad=1
POOL4: pool_height= 2, pool_width= 2, stride= 2
FC5: 512 neurons
FC6: 64 neurons
FC7: 10 outputs

最终结果能拿到77%左右的测试集精度,90%+的训练集精度,未来的提高点在于每层加入Batch Normalization,因为BN需要额外的计算量还挺大的(自己实现的话),所以没有加上。最好的方案是跟ResNets一样,用小卷积层并扩展深度至20层以上。

这里写图片描述

下面给出我第一层卷积层权值w的可视化,因为是3*3,好像并看不出来什么,但是相比刚开始训练的时候要好的很多,不信你可以在训练完1个epoch时就可视化看看

这里写图片描述