Assignment 3 (神经网络) | 斯坦福CS231n-深度学习与计算机视觉课程

时间:2022-05-03
本文章向大家介绍Assignment 3 (神经网络) | 斯坦福CS231n-深度学习与计算机视觉课程,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

该笔记是以斯坦福cs231n课程的python编程任务为主线,展开对该课程主要内容的理解和部分数学推导。这篇文章是第三篇。

CS231n简介

CS231n的全称是CS231n: Convolutional Neural Networks for Visual Recognition,即面向视觉识别的卷积神经网络。该课程是斯坦福大学计算机视觉实验室推出的课程。需要注意的是,目前大家说CS231n,大都指的是2016年冬季学期(一月到三月)的最新版本。

课程描述 Information 计算机视觉在社会中已经逐渐普及,并广泛运用于搜索检索、图像理解、手机应用、地图导航、医疗制药、无人机和无人驾驶汽车等领域。而这些应用的核心技术就是图像分类、图像定位和图像探测等视觉识别任务。近期神经网络(也就是“深度学习”)方法上的进展极大地提升了这些代表当前发展水平的视觉识别系统的性能。 本课程将深入讲解深度学习框架的细节问题,聚焦面向视觉识别任务(尤其是图像分类任务)的端到端学习模型。在10周的课程中,学生们将会学习如何实现、训练和调试他们自己的神经网络,并建立起对计算机视觉领域的前沿研究方向的细节理解。最终的作业将包括训练一个有几百万参数的卷积神经网络,并将其应用到最大的图像分类数据库(ImageNet)上。我们将会聚焦于教授如何确定图像识别问题,学习算法(比如反向传播算法),对网络的训练和精细调整(fine-tuning)中的工程实践技巧,指导学生动手完成课程作业和最终的课程项目。

视频入口

Assignment 3

03

神经网络(Neural Networks)

  • 神经网络模型是由多个人工神经元构成的多层网络结构,而人工神经元的灵感来自人脑;相对于生物神经元,人工神经元只是一个十分粗糙的模型。下面给出一张生物神经元和它的数学模型的对比图:

CS231n Convolutional Neural Networks for Visual Recognition.png

  • 从上图的数学模型我们可以看出人工神经元的处理过程如下:
  • 输入x与权重w做内积 ----> 内积结果输入激活函数 ---> 从激活函数输出信号
  • 感知器(perceptron)和S型神经元(sigmoid neuron),是两个重要的人工神经元,承载了神经网络的关键思想(可以移步Michael Nielsen写的Neural Networks and Deep Learning)。

先介绍下S型神经元,上张图:

Neural Networks and Deep Learning.png

S 型神经元有多个输入x1,x2,x3,... ;对每个输入有权重w1,w2,...,和⼀个总的偏置b。输出output = σ(wx+b),这里σ被称为S型函数,定义为:

---------------> σ(z) = 1/(1+e-z) <-------------------

σ的函数曲线如下:

Neural Networks and Deep Learning.png

这个形状是阶跃函数平滑后的版本:

Neural Networks and Deep Learning.png

σ函数的平滑特性是它成为激活函数的关键因素, σ的平滑特性意味着权重和偏置的微小变化,Δwj和Δb,会通过神经元产生一个微小的输出变化Δoutput。实际上,Δoutput可以很好地近似表示为:

Neural Networks and Deep Learning.png

从公式可以看出,Δoutput是一个反映权重和偏置变化的线性函数。这一线性性质,使得我们可以很容易地选择小的权重和偏置的变化量,来获得任何想要的小的输出变化量。

下面介绍神经网络的结构(ps: 这里指前馈(feedforward)神经网络,网络中是没有回路的,信息总是向前传播,不反向回馈),神经网络通常有如下结构:

The Architecture of Neural Networks.png

上图是一个含有两个隐藏层的3-layer神经网络,层与层之间是全连接(fully-connected)的。输入层是图像数据(经过预处理后的),即该层的神经元数量等于输入图片的维数;神经网络的隐藏层可以是一层或多层,多层神经网络我们称为人工神经网络(ANN),其实最后一层隐藏层,我们可以看成是输入图像的特征向量;输出层神经元的数量等于需要分类的图像数据的类别数,输出值可以看成是在每个类别上的得分。

对于分类任务而言,根据损失函数(SVM loss function or softmax loss function)选择的不同,神经网络的输出层也可以看作是SVM层或Softmax层。神经网络的激活函数是非线性的,所以神经网络是一个非线性分类器。 ---> (ps: 神经网络的输出层神经元不含激活函数f)

神经网络的多层结构给它带来了非常强大的表达能力(层越深,神经元数量越多,表达能力越强),换句话说,神经网络可以拟合任意函数!具体的可视化证明可以移步这里。但是,隐藏层或神经元数量越多,越容易出现过拟合(overfitting)现象,这时我们需要使用规则化(L2 regularization, dropout等等)来控制过拟合。

接下来我们具体讨论神经网络的各个环节:

1. 激活函数的选择

之前我们已经介绍了S型函数,但是在实际应用中,我们基本不会使用它,因为它的缺陷较多。先看下σ的导数:

Neural Networks and Deep Learning.png

从图中我们可以看到,S型函数导数值在0到0.25之间。在进行反向传播的时候,σ′会和梯度相乘,前面层的梯度值等于后面层的乘积项,那么越往前梯度值越小,慢慢趋近于0,这就是梯度消失问题(vanishing gradient problem)。为了便于理解梯度为什么会消失,我们给出一个每层只有一个神经元的4-layer简化模型:

The Vanishing Gradient Problem.png

其中,C表示代价函数,aj = σ(zj)(注意,a4 = z4),zj = wjaj-1 + bj,我们称 zj是神经元的带权输入。现在我们要来研究一下第一个隐藏神经元的梯度∂C/∂b1,这里我们直接给出表达式(具体证明,请移步这里):

The Vanishing Gradient Problem.png

我们看出∂C/∂b1会是∂C/∂b3的1/16 或者更小,这其实就是梯度消失的本质原因。这会导致深层神经网络前面的隐藏层神经元学习速度慢于后面隐藏层神经元的学习速度,而且越往前越慢,最终无法学习。

---> ps: 对于这个问题,不论使用什么样的激活函数,都会出现,但是有些激活函数可以减轻这一问题。说到这里,不得不提一下Batch Normalization,这一方法在很大程度上缓解了梯度消散问题,bravo!

除此之外,sigmoid还有两个缺陷:

其一,当sigmoid的输入值很小或者很大的时候,它的导数会趋于0,在反向传播的时候梯度就会趋于0,那么神经元就不能很好的更新而提前饱和; 其二,sigmoid神经元输出值(激活值)是恒大于0的,那么问题来了,就以上面的4-layer简化模型为例,你会发现在反向传播时,梯度会恒正或恒负(取决于∂C/∂a4的正负)。换句话说,连接到同一个神经元的所有权重w(包括偏置b)会一起增加或者一起减少。这就有问题了,因为某些权重可能需要有不同方向的变化(虽然没有严格的证明,但这样更加合理)。所以,我们通常希望激活函数的输出值是关于0对称的。

下面列出一些相对于sigmoid性能更好的激活函数:

1、Tanh

tanh神经元使用双曲正切函数替换了S型函数,tanh函数的定义如下:

-------------> tanh(z) = (ex−e-x)/(ex+e-x) <----------

该公式也可以写成:tanh(z) = 2σ(2z)−1,所以tanh可以看做是sigmoid的缩放版,相对于sigmoid的好处是他的输出值关于0对称,其函数曲线如下:

Neural Networks and Deep Learning.png

2、修正线性单元(Rectified Linear Unit, ReLU)

ReLU是近几年在图像识别上比较受欢迎的激活函数,定义如下:

-----------------> f(z) = max(0, z) <---------------

其函数曲线如下:

Neural Networks and Deep Learning.png

ReLU的优点在于它不会饱和,收敛快(即能快速找到代价函数的极值点),而且计算简单(函数形式很简单);但是收敛快也使得ReLU比较脆弱,如果梯度的更新太快,还没有找到最佳值,就进入小于0的函数段,这会使得梯度变为0,无法更新梯度直接挂机了。所以,对于ReLU神经元,控制学习率(learning rate)十分重要。此外,它的输出也不是均值为零0的。

---> ps: 在assignment1里的神经网络部分,我们选择ReLU作为我们的激活函数。

3、Leaky ReLU(LReLU)

Leaky ReLU是ReLU的改进版,修正了ReLU的缺点,定义如下:

---------------> f(z)=max(αz, z) <----------------

其中,α为较小的正值(如0.01),函数曲线如下:

figure_4.png

4、Maxout

Maxout是ReLU和LReLU的一般化公式,公式如下:

--------------> max(z1, z2) <----------------

可以看出,该方法会使得参数数量增加一倍。

5、指数线性单元(Exponential Linear Units, ELU)

ELU的公式为:

ELU.png

函数曲线如下:

figure_5.png

ELU除了具有LReLu的优点外,还有输出结果接近于0均值的良好特性;但是,计算复杂度会提高。

---> ps: 通常我们在神经网络中只使用一种激活函数。


2. 数据预处理

和Part1部分一样,假设我们有一个图像训练集X,是一个大小为[N,D]的矩阵;其中,N表示样本的数量,D表示样本的维数。xi是X中的第i行,即第i个样本。y表示一个向量,大小为[1,N];yi表示第i个样本的真实类别,yi=1,2,3, ...,C。

**- 数据预处理的手段一般有: · 去均值(mean subtraction) · 规范化/归一化(normalization) · 主成分分析(PCA)和白化(whitening)

对于图像而言,我们一般只进行去均值处理(好处1:自然图像数据是平稳的,即数据每一个维度的统计都服从相同分布。去均值处理可以移除图像的平均亮度值,我们对图像的照度并不感兴趣,而更多地关注其内容;好处2:使数据关于0对称),X -= np.mean(X, axis=0)。或者我们可以进一步进行归一化,即每一维减去该维的标准差,X /= np.std(X, axis = 0)。但是,我们通常不会进行白化,因为计算代价太大(需要计算协方差矩阵的特征值)。有关数据预处理的详细内容可以参见UFLDL和课程笔记。

---> PS1: 其实我们还要进行一项预处理,就是将图像向量化,假设图像大小为[d1,d2],向量化之后大小为[1,D],D=d1d2。但是我们通常不会将其纳入预处理范畴。

---> PS2: 我们为什么要进行预处理?因为预处理可以增大数据分布范围,加速收敛,即可以帮助我们更快地找到代价函数的极(小)值点。便于大家直观理解,我绘制了下面这张图(以二维数据为例):

data preprocessing.png

此图以ReLU神经元为例,ReLU(wx+b) = max(wx+b,0),图中绿色和红色的线表示wx+b=0;我们发现只有红色的线对数据进行了分割,说明我们随机初始化的参数只有少部分发挥了作用,那么在反向传播时,收敛速度就会变得很慢;但是去均值后的数据被大多数线分割了,这样收敛速度也就会快很多了。


3. 权重初始化方式的选择

通常我们会将权重随机初始化为:均值为0,标准差为一个很小的正数(如0.001)的高斯分布,在numpy中可以写成:w = np.random.randn(n)。这样的初始化方式对于小型的神经网络是可以的(在assignment1的编程部分,我们就是使用这样的初始化方式)。

但是对于深度神经网络,这样的初始化方式并不好。我们以激活函数为tanh为例,如果标准差设置得较小,后面层的激活值将全部趋于0,反向传播时梯度也会变的很小;如果我们将标准差设置得大些,神经元就会趋于饱和,梯度将会趋于零。

为了解决这个问题,我们可以使用方差校准技术:

· 实践经验告诉我们,如果每个神经元的输出都有着相似的分布会使收敛速度加快。而上面使用的随机初始化方式,会使得各个神经元的输出值的分布产生较大的变化。 · 抛开激活函数,我们假设神经元的带权输入s=∑iwixi,则s和x的方差关系如下:

CS231n Convolutional Neural Networks for Visual Recognition.png

得到的结果显示,如果希望s与输入变量x同分布就需要使w的方差为1/n。即权重初始化方式改为:w = np.random.randn(n) / sqrt(n)。

但是当使用ReLU作为激活函数时,各层神经元的输出值分布又不一样了,对于这个问题这篇论文进行了探讨,并给出了修改:w = np.random.randn(n) * sqrt(2.0/n),解决了此问题。

至于偏置的初始化,我们可以简单地将其初始化为0。


4. Batch Normalization

Batch Normalization就是在每一层的wx+b和f(wx+b)之间加一个归一化(将wx+b归一化成:均值为0,方差为1;但在原论文中,作者为了计算的稳定性,加了两个参数将数据又还原回去了,这两个参数也是需要训练的。Assignment2部分我会详细介绍)层,说白了,就是对每一层的数据都预处理一次。方便直观感受,上张图:

Batch Normalization.png

这个方法可以进一步加速收敛,因此学习率可以适当增大,加快训练速度;过拟合现象可以得倒一定程度的缓解,所以可以不用Dropout或用较低的Dropout,而且可以减小L2正则化系数,训练速度又再一次得到了提升。即Batch Normalization可以降低我们对正则化的依赖程度。

现在的深度神经网络基本都会用到Batch Normalization。


5. 正则化的选择

这里,我们会继续使用L2正则化(关于L1正则化和最大范数约束,请看课程笔记)来惩罚权重W,控制过拟合现象的发生。在深度神经网络(如卷积神经网络,后续的Assignment2篇会讲到)中我们通常也是选择L2正则化,而且还会增加Dropout来进一步控制过拟合。关于Dropout,我们留到Assignment2部分再详细介绍。


6. 损失函数的选择

损失(代价)函数由data loss 和 regularization loss两部分组成,即L = 1/N ∑iLi + λR(W)。我们常用的损失函数是SVM的hinge loss和softmax的交叉熵损失(这里我们只针对数据集中样本只有一个正确类的情况,对于其它分类问题和回归问题,请看课程笔记),这里我们选择softmax的交叉熵损失作为我们的损失函数。


7. 反向传播计算梯度

我们以激活函数f为ReLU,损失函数为softmax的交叉熵损失的3-layer神经网络为例,给出完整的计算各层梯度的过程(由于图片分辨率较高,请在新的标签页打开图片并放大,或者下载后观看。下图中,W3 的 size 应该是 [H,C]):

compute the gradient.jpg


8. 参数更新策略

1)、Vanilla update 最简单的参数更新方式,即我们常说的SGD方法的标准计算形式。

2)、Momentum update (SGD+Momentum) 该方法是对Vanilla update的改进版,为了理解momentum 技术,我们可以把现梯度下降,类比于球滚向山谷的底部。momentum 技术修改了梯度下降的两处使之类似于这个物理场景。首先,引入一个称为速度(velocity)的概念。梯度的作用就是改变速度,而不是直接的改变位置,就如同物理学中的力改变速度,只会间接地影响位置;第二,momentum 方法引入了一种摩擦力的项,用来逐步地减少速度。具体的更新规则如下:

--------------) v --> v' = μv - λdx (-------------- ------------------) x --> x' = x + v' (---------------

其中,x表示需要更新的参数(W和b),v的初始值为0,μ是用来控制摩擦力的量的超参数,取值在(0,1)之间,最常见的设定值为0.9(也可以用交叉验证来选择最合适的μ值,一般我们会从[0.5, 0.9, 0.95, 0.99]里面选出最合适的)。

从公式可以看出,我们通过重复地增加梯度项来构造速度,那么随着迭代次数的增加,速度会越来越快,这样就能够确保momentum技术比标准的梯度下降运行得更快;同时μ的引入,保证了在接近谷底时速度会慢慢下降,最终停在谷底,而不是在谷底来回震荡。

---> ps: SGD+Momentum是最常见的参数更新方式,这里我们就使用此方法。

3)、Nesterov Momentum (SGD+Nesterov Momentum) 算是Momentum update的改良版,实际应用中的收敛效果也略优于momentum update。为了方便理解Nesterov Momentum,我们把Momentum update的更新规则合并如下:

------------) x --> x' = (x + μv) - λdx (-----------

从公式可以看出,(x + μv)其实就是x即将去到的下一个位置;但是这个公式在计算梯度的时候,仍然还在计算dx,而我们希望它能前瞻性地计算d(x + μv),这样我们的梯度能更快的下降。贴张辅助理解的图(图中大红点表示参数x的当前位置):

CS231n Convolutional Neural Networks for Visual Recognition.png

现在我们可以给出Nesterov Momentum的参数更新规则了:

----------------> x_ahead = x + μv <-------------- -----------> v = μv - λdx_ahead <---------------- -----------------------> x = x + v <----------------

在实际应用时,我们会稍作修改,对应代码如下:

v_prev = v                              
v = mu * v - learning_rate * dx         # 和 Momentum update 的更新方式一样x += -mu * v_prev + (1 + mu) * v        # 新的更新方式

如果你想深入了解Nesterov Momentum的数学原理,请看论文: · Advances in optimizing Recurrent Networks by Yoshua Bengio, Section 3.5. · Ilya Sutskever’s thesis, contains a exposition of the topic in section 7.2


8.1. 衰减学习率

在实际训练过程中,随着训练过程的推进,逐渐衰减学习率是很有必要的技术手段。这也很容易理解,我们还是以山顶到山谷为例,刚开始离山谷很远,我们的步长可以大点,但是快接近山谷时,我们的步长得小点,以免越过山谷。

常见的学习率衰减方式:

1)、步长衰减:每一个epoch(1 epoch = N/batch_size iterations)过后,学习率下降一些,数学形式为λ'=kλ,k可以取0.9/0.95,我们也可以通过交叉验证获得。

2)、指数衰减:数学形式为α=α0e−kt,其中α0,k为超参数,t是迭代次数。

3)、1/t衰减:数学形式为α=α0/(1+kt),其中α0,k为超参数,t是迭代次数。

在实际应用中,我们通常选择步长衰减,因为它包含的超参数少,计算代价低。

以上的讨论都是以全局使用同样的学习率为前提的,而调整学习率是一件很费时同时也容易出错的事情,因此我们一直希望有一种学习率自更新的方式,甚至可以细化到逐参数更新。下面简单介绍一下几个常见的自适应方法:

1)、Adagrad Adagrad是Duchi等在论文Adaptive Subgradient Methods for Online Learning and Stochastic Optimization中提出的自适应学习率算法,实现代码如下:

# Assume the gradient dx and parameter vector x
cache += dx**2
x += - learning_rate * dx / (np.sqrt(cache) + eps)

这种方法的好处是,对于高梯度的权重,它们的有效学习率被降低了;而小梯度的权重迭代过程中学习率提升了。要注意的是,这里开根号很重要。平滑参数eps是为了避免除以0的情况,eps一般取值1e-4 到1e-8。

2)、RMSprop RMSprop是一种高效但是还未正式发布的自适应调节学习率的方法,RMSProp方法对Adagrad算法做了一个简单的优化,以减缓它的迭代强度:

cache = decay_rate * cache + (1 - decay_rate) * dx**2x += - learning_rate * dx / (np.sqrt(cache) + eps)

其中,decay_rate是一个超参数,其值可以在 [0.9, 0.99, 0.999]中选择。

3)、Adam Adam有点像RMSProp+momentum,效果比RMSProp稍好,其简化版的代码如下:

m = beta1*m + (1-beta1)*dx
v = beta2*v + (1-beta2)*(dx**2)x += - learning_rate * m / (np.sqrt(v) + eps)

论文中推荐eps = 1e-8,beta1 = 0.9,beta2 = 0.999。 完整的Adam update还包括了一个偏差修正机制,以弥补m,v初始值为零的情况。

---> PS: 建议使用SGD+Nesterov Momentum 或 Adam来更新参数。

其它的一些方法:

·Adadelta by Matthew Zeiler ·Unit Tests for Stochastic Optimization

这里给出一些上述提到的多种参数更新方法下,损失函数最优化的示意图:

opt1.gif
opt2.gi
9. 超参数的优化

神经网络的训练过程中,我们需要对很多超参数进行优化,这个过程通常在验证集上进行,这里我们需要优化的超参数有:

·初始学习率 ·学习率衰减因子 ·正则化系数/惩罚因子(包括L2惩罚因子,dropout比例)

对于深度神经网络而言,我们训练一次需要很长的时间。所以,在此之前我们花一些时间去做超参数搜索,以确定最佳超参数。最直接的方式就是在框架实现的过程中,设计一个会持续变换超参数实施优化,并记录每个超参数在每一个epoch后,在验证集上状态和效果。实际应用中,神经网络里确定这些超参数,我们一般很少使用n折交叉验证,一般使用一份固定的交叉验证集就可以了。

对于初始学习率,通常的搜索序列是:learning_rate = 10 ** uniform(-6, 1),训练5 epoches左右,然后缩小范围,训练更多次epoches,最后确定初始学习率的大小,大概在1e-3左右;对于正则化系数λ,通常的搜索序列为[0.5, 0.9, 0.95, 0.99]。


10. 训练过程的可视化观察

1)、观察损失函数,来判断你设置的学习率好坏:

loss function.jpeg

但实际损失函数的变化没有上图光滑,会存在波动,下图是实际训练CIFAR-10的时候,loss的变化情况:

CIFAR10_loss.jpeg

大家可能会注意到上图的曲线有一些上下波动,这和设定的batch size有关系。batch size非常小的情况下,会出现很大的波动,如果batch size设定大一些,会相对稳定一点。 · 2)、观察训练集/验证集上的准确度,来判断是否发生了过拟合:

accuracies.jpeg