说明:
本系列文章翻译斯坦福大学的课程:Convolutional Neural Networks for Visual Recognition的课程讲义 原文地址:http://cs231n.github.io/。 最好有Python基础(但不是必要的),Python的介绍见该课程的module0。
本节的code见地址:
https://github.com/anthony123/cs231n/tree/master/module1-3如果在code中发现bug或者有什么不清楚的地方,可以及时给我留言,因为code没有经过很严格的测试。
这节课的主要内容:
简介
损失函数可视化
-
优化
- 策略1: 随机搜索
- 策略2: 随机局部搜索
- 策略3: 沿着梯度
-
计算梯度
- 有限差异的数值分析
- 微分分析
梯度下降
总结
简介
在上一节中,我们介绍了图像分类任务的两种主要的组成部分:
- (参数化)分数函数,它将图像的原始像素映射到类别分数(比如:线性函数)
- 损失函数,可以通过比较训练集中预测的分数函数与真实类别分数的一致性来测量参数的质量,我们已经见过很多版本(比如: Softmax/SVM)
具体来说,线性函数有如下形式:
而SVM有如下形式:
我们知道,使得我们对Xi的预测与真实类别yi一致的W,也会产生一个非常低的损失L。我们将会介绍第三个也是最后一个组成部分:优化。 优化是指寻找最小化损失值的过程。
剧透
一旦我们知道了这些主要的组成部分如何交互,我们会再访问第一个组成部分(参数化的映射函数), 并把它扩展为比线性函数更复杂的形式:首先是整个神经网络,然后是卷积神经网络。损失函数和优化过程几乎保持不变。
可视化损失函数
这门课的损失函数一般都是定义在高维度空间内(比如,CIFAR-10,线性分类器的权重矩阵为[10x3073], 所以有30730个参数),这使得它们很难可视化。然而,我们可以通过把高维切割成一条线或一个平面来直观地感受下损失函数。比如,我们可以生成一个随机的权重矩阵W(对应于空间中的一个点),然后沿着一条射线改变它的值,并记录损失值的改变。也就是说,我们可以随机生成一个方向W1,然后沿着这个方向,通过计算L(W + aW1),来测量沿着这个方向的损失值变化情况(通过改变a的值)。这个过程可以在一条直线上表示,其中 以a的值为x坐标,损失值为y坐标。或者,我们也可以在二维坐标上进行,通过计算L(W + aW1 + bW2)来评估损失值(通过改变a和b的值)。我们也可以用图表示,其中a和b分别对应于x坐标和y坐标,而损失值可以用颜色来表示,如下图
我们可以通过数学的方法来解释下损失函数的结构:
从上面的式子我们可以看出每张图像的损失值是W的线性和(以0为阈值,因为max(0,-)函数)。而且,W的每一行(比如Wj)有时候会添加一个正号在它的前面(当它表示错误类别时),有时候会添加一个负号在其前面(当它代表正确类别时)。为了更加清楚的表达,考虑一个简单的例子:包含三个一维点和三个类别。其完整的SVM损失(没有正则项)变成
因为这个例子的数据都是一维的,所以数据xi 和 权重 Wj都是数字。比如,对W0而言,上面的一些参数是w0的线性函数,每一个都被限制在零以上。通过下图,我们可以更直观的理解我讲的内容
顺便说一句,从它的碗装图形你可以推测出SVM损失函数是一个凸函数。关于凸函数的最小化,有非常多的研究,你也可以上斯坦福的一门关于这个主题的课程(凸优化)。一旦我们将函数f扩展到神经网络,我们的目标函数将会变成非凸函数,那么我们图将不会是像碗那样,而是复杂的,崎岖的山路。
不可微的损失函数
一个函数弯折(kinks)的地方是不可微的,所以在此处梯度是不存在的,但是子梯度是存在的。所以我们通常使用子梯度。在后续的课程中,梯度和子梯度可以交替使用。
优化
再次重申一遍,损失函数让我们将对W的评估量化,优化的目的是找到一个W,可以最小化损失函数。我们将会慢慢的找出优化损失函数的方法。对于那些之前学过凸函数优化的同学,这个过程看起来会有点奇怪,因为SVM损失就是一个凸函数优化的问题,但是记住我们最终的目的在于优化神经网络,而这种优化问题不能用到任何凸优化的方法。
策略1: 第一个非常糟糕的解决方案: 随机搜索
由于评估W的好坏非常简单,第一个想到的方法(非常糟糕)就是简单的试出许多不同的随机权重,并记录下最好的结果,这个过程可以用代码实现如下:
#X_train 是训练的数据,每一列是一张图像(比如, 3073x50,000)
#Y_train 是标记(比如,一维数组, 50,000)
#L函数为损失函数
bestloss = float("inf"); #首先将最优损失初始化一个最大整数
for num in xrange(1000):
W = np.random.randn(10, 3073)*0.0001 #生成一个初始化随机参数
loss = L(X_train, Y_train, W) #计算损失值
if loss < bestloss:
bestloss = loss;
bestW = W;
print 'in attempt %d the loss was %f, best %f' %(num, loss, bestloss)
在上面的代码中, 我们试了各种不同的随机权重W,有些表现的更好一些。我们记录其中最好的权重W,并在测试集中测试。
#X_test 为 [3073x1000], Y_test 为 [1000x1]
score = Wbest.dot(Xte_cols) #10x10000 所有测试数据的类别分数
#找出每一列最高分数的索引值
Yte_predict = np.argmax(scores, axis = 0)
#计算准确率
np.mean(Yte_predict == Yte)
最好的W,准确率为15.5%。 随机猜的准确率为10%。对于这样一个随机搜索的方法,这不是一个坏的结果。
核心想法: 迭代优化
当然,我们可以做的比这更好。现在主要的问题在于一下子找到一个最好的权重W非常难,甚至是不可能(特别是当W为一个完整的神经网络的权重集合时)。但是稍微优化权重W却是一个相对简单的问题。也就是说,我们的方法会首先使用一个随机的W,然后迭代优化,使得每一次都比上一次更好。
我们的策略是首先随机初始化权重值,然后迭代优化,获得更低的损失值
蒙眼徒步者的类比
一个可能会帮助你理解的类比就是把自己比作一个崎岖山区的蒙眼徒步者,并且试图到达山脚。在CIFAR-10的例子中,这座山是30,730维度,因为W的维度为3073x10。在山中的每一点,我们都会得到一个损失值(山的高度)。
策略2: 随机局部搜索
你可能想到的第一个策略是试着在一个随机的方向伸出你的一条腿,如果它会导致下坡,那么就跨出这一步。具体来说,我们开始于一个随机的W,然后产生一个随机的小变化量aW, 如果W+aW更低,那么我们就更新,实现这个过程的代码如下:
w = np.random.randn(10,3073)*0.001 #随机产生一个W
bestloss = float("inf")
for i in xrange(1000):
step_size = 0.0001
Wtry = W + np.random.randn(10, 3073)*step_size
loss = L(Xtr_cols, Ytr, Wtry)
if loss < bestloss:
W = Wtry
bestloss = loss
print 'iter %d loss is %f' %(i, bestloss)
使用同一个损失函数,这个方法的准确率为21.4%。这个结果更好,但是还是比较浪费并且计算花费很大。
策略3: 沿着梯度
在上一节中,我们试着在W的空间内寻找一个方向,来提高权重向量(即给我们更低的损失)。事实上我们不需要随机寻找一个好的方向,我们可以计算出这个最好的方向,这个方向和损失函数的梯度有关。在我们徒步者的类比中,这个方法和以下做法的原理是一样的:即我们感受山的斜度,并且沿着下降最快的那个方向跨出一步。
在一维函数中,斜率是指在某一点的瞬时变化速率。梯度是斜率的一般化,它的自变量不是一个数字,而是一个向量。所以,梯度是由各个维度的斜率(经常称之为导数)组成的向量。一维函数的导数数学表达式为:
当一个函数的输入不是一个数字,而是一个向量的话,那我们称这些导数为偏导数, 梯度就是各个维度的偏导数。
计算梯度
有两种计算梯度的方法,一种是计算比较慢,而且是近似值,但是非常简单(数值梯度),一种非常快,准确,但是更加容易出错(分析梯度),这两种方式我都会逐一讲解。
计算有限差异的数值梯度
上述的计算公式允许我们计算数值梯度。下面的函数输入一个函数f和一个向量x,并计算数值梯度,并返回f在x处的梯度。
def eval_numberical_gradient(f,x):
"""
数值型梯度的一个简单实现
-f 函数,其输入参数个数为1个
-x 目标点的坐标
"""
fx = f(x) #计算x处的函数值
grad = np.zeros(x.shape)
h = 0.0001
#迭代x中所有的索引值
it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
while not it.finished:
#计算在 x+h 处的函数值
ix = it.multi_index
old_value = x[ix]
x[ix] = old_value+h
fxh = f(x)
x[ix] = old_value
#计算偏导数
grad[ix] = (fxh - fx) / h
it.iternext()
return grad
根据我们上面给出的公式,一个个迭代所有的维度,在每个维度上做一个小的变化h,并计算出该维度的偏导数。最终,变量grad中存储了整个梯度。
实践考虑
注意到在数学公式中,梯度定义在h趋近于0的过程中,但是实际上,使用一个很小的值就足够了(比如1e-5)。而且,在实践中,我们使用不引起数值问题的尽可能小的步伐。同时,中心差异公式在实践中往往有比较好的表现,它的表达式如下:[f(x+h) - f(x)]/2h 。
我们可以使用上面的函数计算任何函数在任何点的梯度。我们来计算一下CIFAR-10损失函数在权重空间的一些点的梯度。
#计算CIFAR-10的损失函数
def CIFAR10_loss_fun(W):
return L(X_train, Y_train, W)
W = np.random.rand(10,3073)*0.001 #随机权重向量
df = eval_numerical_gradient(CIFAR10_loss_fun, W)
梯度告诉我们各个维度的斜率,我们可以用它们来更新权重值
loss_original = CIFAR10_loss_fun(W) #原始损失
print 'original loss: %f', %(loss_original,)
#看不同的步长产生的效果
for step_size_log in [-10, -9, -8, -7, -6, -5, -4, -3,-2,-1]:
step_size = 10**step_size_log
W_new = W - step_size*df #更新后的权重矩阵
loss_new = CIFAR10_loss_fun(W_new)
print 'for step size %f new loss: %f' %(step_size, loss_new)
在梯度的负方向更新权重值
在上面的代码中 我们发现我们在df的负方向上更新权重,因为我们希望我们损失值减少,而不是增加。
步长的效果
梯度告诉我们函数增长最快的方向,但是没有告诉我们这个方向的步长。我们会在之后的课程中看到,一个合适的步长(也称之为学习速率),是神经网络训练过程中一个非常重要的超参数(也是最令人头疼的)。在我们那个蒙眼下山的比喻中,我们能感受到在某个方向的下降,但是我们应该使用的步长是不确定的。如果我们迈的步子很小,我们可以得到很好的结果,但是我们的速度会很慢,如果我们期望下降速度快,迈的步子很大,但是有时却不能得到我们想要的结果。从上面代码运行的结果我们可以看出,使用一个大的步长,有时候会导致损失值上升。我们把这种现象称之为 overstep。
效率问题
你可能已经发现计算数值型梯度的复杂度与参数的个数呈线性增长关系。在我们的例子中,我们有30730个参数,所以我们需要计算30731次来更新一个参数,当我们使用神经网络时,这种结果将会变得非常糟糕。所以,显然,数值型梯度可扩展性不好,我们需要一个更好的策略。
使用微积分计算分析梯度
数值梯度计算过程非常简单,但是缺点是它是近似值(因为虽然h的值很小,但是真实的梯度定义在h趋近于0的时候),而是它的计算量非常大。第二种计算梯度的方法是使用微积分的知识分析,它能得出一个计算梯度的公式(不是近似值),而且能非常快速计算。但是,不像数值梯度,分析梯度更容易出错。所以在实践中,人们更常见的做法是同时计算分析梯度和数值梯度,并验证分析梯度的正确性,这称之为梯度检验。
我们来看一下一个点的SVM损失函数的例子:
我们可以求上式中关于Wyi的梯度,并且可以得到:
其中 1 是个指示函数,如果条件为真,那么其值为1;否则其值为0。 初看这个公式,你可能会觉得有点吓人,但是在代码中实现的时候,你只需要计算没有达到预期边界值要求(产生损失值)的数值, 那么这一项的梯度值就等于该值与xi的梯度的积。注意这仅仅是对W中正确类别的梯度计算,对其他错误的类别,其梯度计算公式为:
一旦你推导出了梯度的表达式,那么实现起来就非常简单。
梯度下降
现在我们知道如何计算损失函数的梯度,通过反复计算梯度,然后进行参数更新的过程称之为梯度下降。它的简单的版本如下所示:
#简单的梯度下降
while true:
weight_grad = evaluate_gradient(loss_fun, data, weights)
weight += (-step_size*weight_grad) #参数更新
这个简单的循环是所有神经网络库的核心。 当然,还有其他优化方法(比如 LBFGS),但是梯度下降是目前最常见,也是最成熟的神经网络优化方法。在整个课的过程中,我们会慢慢讲解其中的细节, 但是整个思想是不变的,即我们沿着梯度慢慢更新权重值,直到我们获得满意的结果为止。
小批量梯度下降
在大规模的应用中(比如ILSVRC挑战),训练数据有百万之众,为了更新一个参数,需要在整个训练集上更新计算损失函数,这看起来非常浪费。一个常见的解决方法是只在训练集的一个小的批量上计算梯度。比如,对于ConvNets, 一个典型的批量为在120万图片集中选出256个例子。这个小的批量就用来更新梯度:
#简单的小批量梯度下降
while True:
data_batch = sample_training_data(data, 256)
weights_grad = evaluate_gradient(loss_fun, data_batch, weights)
weights += -(step_size*weights_grad)
这样做能够获得好的结果是因为训练集中的数据都是相关的。为了更清楚的说明这点,考虑一个极端的例子,在120万张ILSVRC数据集中,只有1000不同的图片(每个类别有1200张相同的图片)。由于计算1200张相同图片的梯度然后取平均值与计算一张图片的梯度是一样的。扩展到整个数据集中,计算1000张图片的平均梯度和计算整个120万张图片的平均梯度是一样的。当然在实践中,数据集不可能包含两种相同的图片,但是,小批量梯度是对整个图片集梯度的一个非常好的估计值。在实践中,也能更快地实现聚合。
小批量的一个极端情况是批量中图片的个数为1个。这种情况下的梯度下降过程称之为随机梯度下降(Stochastic Gradient Descent SGD)(有时候也称之为在线梯度下降)。但是在实践中,这种情况比较少见,因为在向量化的代码中,同时计算100个图片的梯度比计算100次不同图像的梯度更有效。尽管SGD指的是一次使用一个图像来计算梯度,但是人们经常使用SGD指代小批量梯度下降。(比如: 提到小批量梯度下降(MGD)和批量梯度下降(BGD)就非常少)。小批量的大小是一个超参数,但是一般不使用交叉验证来评估它。它经常基于内存限制(如果有的话)或者设为一些特定的值,比如 32, 64或128。在实践中我们使用2的幂数,因为很多向量化的操作在代码中实现会更快。
总结
在这一节中,
- 我们把损失函数比作是一个多维度优化的地形(high-dimensional optimization landscape),我们期望到达它的底部。我们把整个优化过程比作为想要到达山脚的盲眼徒步者。特别地,我们发现SVM损失函数是一个分段线性且呈碗状的形状。
- 我们把整个优化过程看成是一个迭代优化的过程,我们开始于随机权重,然后优化直到损失达到最小。
- 我们发现函数的梯度指出了最快下降的方向,而且我们还讨论了一个简单但不是很有效的方法,即数值梯度。
- 我们发现参数更新需要设置一个适当的步长(或学习速率),如果它的值太小,则优化过程稳定但是会很慢,如果它的值太大,则优化过程更快,但是更冒险。在后面的章节中我们将会讲到如何达到二者的平衡。
- 我们讨论了数值梯度和分析梯度的利弊。数值梯度简单, 但是它只是近似梯度而且运算花费高;分析梯度准确,快速,但是更容易出错(因为需要使用数学公式推导)。所以,在实践中,我们经常使用分析梯度,然后进行梯度检验,即将分析梯度与数值梯度作比较。
- 我们介绍了梯度下降算法,它通过迭代计算梯度并进行参数更新。
预告
这节课主要讲的是如何计算损失函数的梯度,而且它也是设计,训练和理解神经网络的关键。在下一节中,我们将介绍使用链式法则计算分析梯度的方法,同时我们也会提到反向传播(backpropagation)。 这允许我们更有效地优化各种出现在包括卷积神经网络在内的各种神经网络的损失函数。