5、优化方法:随机梯度下降法
5.1 介绍
在上衣章,我们介绍了图像分类任务中的两个关键部分:
- 一个参数化的评分函数将原始图像像素映射到类得分(例如线性函数)
- 一个损失函数,它测量预测的分类得分与实际的分类之间的一致程度,来量化一组特定参数集的质量。我们看到有很多方法和版本(例如SoftMax、SVM)。
回顾一下,线性评分函数是这样的:f(Xi,W)=WXi,SVM损失的公式为:
对于样例xi,如果给一组参数W,得到的预测结果与实际标记Yi一致,则损失L趋低。我们现在将介绍第三个,也是最后一个关键部件:最优化。优化是寻找最小化损失函数的参数W集合的过程。
预告:一旦我们理解这三个核心组件是如何相互作用的,我们将重新审视第一个组件(参数化函数映射),并将其扩展到比线性映射复杂得多的函数:先是完整的神经网络,然后是卷积神经网络。损失函数和优化过程则保持相对稳定。
5.2 可视化损失函数
损失函数通常是在非常高维空间上定义的(例如在CIFAR-10 中,线性分类器权重矩阵的大小为10×3073),总共有30730个参数,它们难以可视化。然而,我们可以通过沿着光线(1维)或沿着平面(2个维度)对高维空间做切面来获一些直观映像。例如,我们可以生成随机权重矩阵W(它对应于空间中的单个点),然后沿着某个方向变化,并沿途记录损失函数值。也就是说,我们可以生成一个随机W1,然后不断变化a,计算损失L(W+W1)。这个过程产生一个简单的曲线图,值为X轴,损失函数的值为Y轴。也可以用两个维度,通过改变来计算损失值,从而给出二维的图像。下图,分别用x和y轴表示,而损失函数的值用颜色变化表示
使用CIFAR-10数据集中一个样本(左,中)和一百个样本(右)的多分类SVM(无正则化)的损失图示。左:只有一个变量a的一维损失。中,右:二维损失切面,蓝色=低损失,红色=高损失。注意损失函数的分段线性结构。多个实例的损失则取平均值,因此右边的碗形是许多分段线性碗的平均值(例如中间图所示的那个)。
我们可以通过检验数学来解释损失函数的分段线性结构。对于一个样例,我们有:
从等式中可以清楚地看出,每个样例的损失是一系列关于W的线性函数值(不小于零)的总和。此外,每一行W(即Wj),有时在它前面有一个正符号(当它对应于错误的分类时),有时是一个负号(当它对应于正确的分类时)。举例来说,考虑一个包含三个一维点和三个分类的简单数据集,全部的SVM损失函数(没有正则化)就是是这样的:
由于这些样本是一维的,所以数据Xi和权重Wj是一个数字。例如,W0,上式中一些项是W0的线性函数,并且其值都被钳位在零处。如下图所示:
1维的数据损失图。X轴是单权重,Y轴是损失。数据损失是多个部分求和而成。其中每个部分要么与权重无关(等于0),要么是该权重的线性函数。完整的SVM数据损失就是这个形状的30730维版本。
顺便提一句,您可能已经从它的碗状外观猜出,SVM损失函数是凸函数的一个例子。有大量的文献致力于有效地最小化这种类型的函数(凸优化)。但是一旦我们将评分函数F扩展到神经网络,我们的目标函数将变成非凸的了,上面的可视化将不再是碗状,而是复杂的、崎岖不平的样子。
不可微损失函数。请注意在损失函数中的扭结(由于最大操作)处,是不可微的。因为在这些扭结处,梯度没有被定义。然而,次梯度仍然存在。本课程我们将交替使用梯度和次梯度术语。
5.3 最优化
损失函数允许我们量化任何特定的权重集合W的质量。最优化的目标是找到最小化损失函数的W。我们现在要开发一种优化损失函数的方法。如果你有一些经验,这一部分可能看起来很奇怪,因为我们将使用的例子(SVM损失)是一个凸函数问题,但是请记住,我们最终还要优化神经网络,在那里我们不能很容易地使用任何在凸优化中开发的工具。
策略 1:一个非常糟糕的解决方案:随机搜索
检查一组给定的参数W有多好很简单,所以最先想到的(非常糟糕的)想法是简单地尝试许多不同的随机权重,并跟踪哪一组是最有效的。这个过程可以如此这般:
# 假设X_train的每一列都是一个数据样本(比如3073 x 50000)
# 假设Y_train是数据样本的类别标签(比如一个长50000的一维数组)
# 假设函数L对损失函数进行评价
bestloss = float("inf")
for num in range(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)
# 输出:
# in attempt 0 the loss was 9.401632, best 9.401632
# in attempt 1 the loss was 8.959668, best 8.959668
# in attempt 2 the loss was 9.044034, best 8.959668
# in attempt 3 the loss was 9.278948, best 8.959668
# in attempt 4 the loss was 8.857370, best 8.857370
# in attempt 5 the loss was 8.943151, best 8.857370
# in attempt 6 the loss was 8.605604, best 8.605604
# ... (trunctated: continues for 1000 lines)
在上面的代码中,我们尝试了1000个随机权重向量W,其中一些工作比其他更好。我们可以用这个搜索找到的最佳权重并在测试集上试用:
# 假设X_test尺寸是[3073 x 10000], Y_test尺寸是[10000 x 1]
# 10 x 10000, 所有测试样本的类得分
scores = Wbest.dot(Xte_cols)
# 找到在每列中评分值最大的索引(即预测的分类)
Yte_predict = np.argmax(scores, axis = 0)
# 计算准确率
np.mean(Yte_predict == Yte)
# 返回 0.1555
用最好的W,这给出了大约15.5%的准确度。也还不是一个非常糟糕的结果,比完全脑残式的随机的猜测要高,因为后者仅为10% !
核心思想:迭代求精。当然,事实证明我们可以做得更好。核心思想是找到最好的权重集W是一个非常困难甚至是不可能的问题(特别是一旦W包含了整个复杂神经网络的权值),但是提炼一组特定的权重W的问题要稍微好一些。换句话说,我们的方法是从随机W开始,然后迭代前进,使它每次都比上一次稍微好一点。
我们的策略是从随机权重开始,并随着时间的推移反复求好,以获得更低的损失。
蒙眼的徒步旅行者的比喻。你可以想象在一个丘陵地带徒步旅行,蒙上眼睛,试图到达山谷底部。在CIFAR-10的例子中,小山是30730维的,因为W的维度是10×3073。在山上的每一个点上,我们都会得到一个特别的损失(地形的高度)。
策略 2:随机局部搜索
你可以想到的第一个策略是试着把一只脚伸到一个随机的方向,然后只在下坡时前进一步。具体来说,我们将从随机W开始,对它产生随机扰动δW,如果扰动W+δW的损失较低,我们就更新W。代码如下:
# 生成随机初始W
W = np.random.randn(10, 3073) * 0.001
bestloss = float("inf")
step_size = 0.0001
for i in xrange(1000):
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)
采用与前述相同的损失函数(迭代1000次),该方法达到了21.4%的测试集分类精度。稍微好了一些了,但仍然浪费和计算昂贵。
策略 3:跟随梯度
在前一节中,我们试图在权重空间中找到一个方向来改进我们的权重向量(给我们一个更低的损失)。事实证明,没有必要随机地寻找一个好的方向:我们可以计算最佳方向,这就是从数学上计算出最陡峭的方向(至少在步长趋近于零的范围内)。这个方向将与损失函数的梯度有关。在我们徒步旅行的比喻中,这种方法大致相当于能感觉到我们脚下的山的坡度,并且朝着感觉最陡的方向下山。
在一维函数中,斜率是函数在任何点上的瞬时变化率。梯度是函数的斜率的一般化表达,它不是一个数字而是一个向量,向量中的每个数代表了输入空间中每个维度的斜率(导数)。一个一维函数导数与其输入的数学表达式为:
当函数的变量是一个向量而不是单个数时,我们称导数为偏导数,而梯度就是其中每个维度的偏导数组成的向量。
5.4 梯度计算
有两种计算梯度的方法:一种缓慢、近似但简单的方法(数值梯度),以及一种快速、精确但更容易出错的方法,需要微积分(解析梯度)。下面分别阐述这两种方法。
5.4.1 用有限差分计算数值梯度
上面给出的公式允许我们数值地计算梯度。这里是一个通用的函数,它取函数f,向量x来计算梯度,并返回f在x处的梯度:
def eval_numerical_gradient(f, x):
"""
一个f在x处的数值梯度法的简单实现
- f是只有一个参数的函数
- x是计算梯度的点
"""
fx = f(x) # 在初始点计算函数值
#梯度初始化为0
grad = np.zeros(x.shape)
h = 0.00001
# 对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]
# 增加 h
x[ix] = old_value + h
# 计算f(x + h)
fxh = f(x)
# 存到前一个值中 (非常重要)
x[ix] = old_value
# 计算偏导数 坡度
grad[ix] = (fxh - fx) / h
# 到下个维度
it.iternext()
return grad
按照上面给出的梯度公式,上面的代码逐个遍历所有维度,在每一个维度做一个小的变化h,并通过观察函数的变化程度来计算损失函数沿该维度的偏导数。最后,全部梯度都保存在变量grad里面了。
实践考量:注意在数学公式中,h的取值是趋近于0的,然而在实际中,用一个很小的数值(比如例子中的1e-5)就足够了。在不产生数值计算出错的理想前提下,建议使用尽可能小的h。还有,实践中用中心差值公式(centered difference formula) [f(x+h) - f(x-h)]/2h 的效果会更好。
我们可以使用上面给出的函数来计算任意点和任意函数的梯度。下面我们来计算CIFAR-10损失函数在权重空间任意点上的梯度:
# 要使用上面的代码我们需要一个只有一个参数的函数
# 在这里这个参数就是权重W,所以我们隐蔽地使用了 X_train和Y_train
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)
# 输出:
# original loss: 2.200718
# for step size 1.000000e-10 new loss: 2.200652
# for step size 1.000000e-09 new loss: 2.200057
# for step size 1.000000e-08 new loss: 2.194116
# for step size 1.000000e-07 new loss: 2.135493
# for step size 1.000000e-06 new loss: 1.647802
# for step size 1.000000e-05 new loss: 2.844355
# for step size 1.000000e-04 new loss: 25.558142
# for step size 1.000000e-03 new loss: 254.086573
# for step size 1.000000e-02 new loss: 2539.370888
# for step size 1.000000e-01 new loss: 25392.21403
在负梯度方向上进行更新。在上面的代码中,请注意要计算W_new
,我们在梯度df的负方向上进行更新,因为我们希望我们的损失函数减少,而不是增加。
步长的影响。梯度告诉我们函数具有最陡增长率的方向,但它并没有告诉我们应该沿着这个方向走多远。正如我们将在后面看到的,选择步长(也称为学习率)将成为训练神经网络中最重要的(也是最头痛的)超参数设置之一。在我们的蒙眼下山类比中,我们感觉脚下的山在某个方向下降,但是我们应该采取的步长是不确定的。如果我们小心翼翼,我们可以取得连续的但非常小的进步(这相当于有一个小的步长)。相反,我们可以选择做一个大而有信心的步骤,试图更快地下降,但结果可能不尽如人意。正如你在上面的代码示例中所看到的,在某个时候,采取更大的步骤会带来更高的损失,因为我们“跨过”了最低点。
可视化步长的影响。我们从某个特定的点W开始,评估梯度(或者更确切地说,它的负方向-白色箭头),它告诉我们损失函数中最陡下降的方向。小步骤可能导致一致但缓慢的进展。大步骤可以带来更好的进步,但风险更大。请注意,最终,对于一个大的步长,我们将跨过最低点,使损失更糟。步长(或者我们以后称之为学习速率)将成为我们必须仔细调整的最重要的超参数之一。
效率问题。您可能已经注意到,计算数值梯度的复杂性和参数的量线性相关。在我们的例子中,我们总共有30730个参数,因此每一次更新,都必须计算30731次来计算损失函数的梯度。现代神经网络可以很容易地拥有数以千万计的参数,这个问题只会变得更糟。显然,这种策略可伸缩性查,我们需要更好的策略。
5.4.2 使用微积分计算梯度
使用有限差分近似计算,数值梯度计算比较简单,但缺点是它是近似的(因为我们必须选择一个小的h值,而真正的梯度被定义为h的极限为零),并且它在计算上是非常昂贵的。计算梯度的第二种方法是使用微积分来分析,这使得我们能够得到一个直接的公式来计算梯度(无近似),这也是非常快的计算。然而,与数值梯度不同的是,它可能更容易出错。为了解决这个问题,实践中常常将分析梯度法的结果和数值梯度法的结果作比较,以此来检查其实现的正确性,这个步骤叫做梯度检查。
单个数据点的支持向量机损失函数:
可以对函数进行微分。比如,对Wyi进行微分得到:
其中1是一个指示函数,如果括号中的条件为真,那么函数值为1,如果为假,则函数值为0。虽然上述公式看起来复杂,但在代码实现的时候比较简单:只需要计算没有满足边界值的分类的数量(他们对损失函数产生了贡献),然后乘以就是梯度了。注意,这个梯度只是对应正确分类的W的行向量的梯度,那些j≠yi的梯度是:
一旦导出了梯度表达式,就直接执行表达式并使用它们执行梯度更新即可。
5.5 梯度下降法
现在我们可以计算损失函数的梯度,反复计算梯度然后执行参数更新的过程称为梯度下降法。其香草版如下:
# 普通的梯度下降
while True:
weights_grad = evaluate_gradient(loss_fun, data, weights)
# 进行梯度更新
weights += - step_size * weights_grad
这个简单的循环是所有神经网络库的核心。也有其他方式的优化方法(例如LBFGS),但梯度下降是目前为止最常见和公认的优化神经网络损失函数的方式。我们后续会在这个循环的基础上,做一些细节的升级(例如更新方程的具体细节),但是核心思想不变,那就是我们一直跟着梯度走,直到结果不再变化。
小批量梯度下降。在大规模应用(例如ILVRC竞赛)中,训练数据可能会有数百万个样本。因此,执行单个参数更新时,在整个训练集上计算完全损失似乎是浪费的。解决这一挑战的一个非常普遍的方法是在训练数据的批次(一部分数据)上计算梯度。例如,在当前的最先进的卷积神经网络中,典型的批次只从全部120万个训练数据中取256个样本。然后使用该批数据执行参数更新:
# 普通的小批量数据梯度下降
while True:
# 256个数据
data_batch = sample_training_data(data, 256)
weights_grad = evaluate_gradient(loss_fun, data_batch, weights)
# 参数更新
weights += - step_size * weights_grad
这个方法之所以效果不错,是因为训练集中的数据都是相关的。为了看到这一点,考虑极端情况下,ILSVRC 中的所有120万个图像实际上是由只有1000个不同图像的重复(每个类别1张图片,每张图片有1200张复制)组成。显然,我们计算所有1200个相同拷贝的梯度都是相同的,并且当我们在所有120万个图像上平均数据损失时,我们将得到完全相同的损失,就好像我们只对1000的小子集进行评估。在实践中,数据集不会包含重复图像,那么小批量数据的梯度就是对整个数据集梯度的一个近似。因此,通过计算小批量梯度可以在实践中实现更快的收敛,并以此来进行更频繁的参数更新。
当这个小批量只包含一个样本时,这个过程被称为随机梯度下降(SGD,或在线梯度下降)。这种策略在实际情况中相对少见,因为向量化操作的代码一次计算100个数据 比100次计算1个数据要高效很多。即使SGD在技术上是指每次使用1个数据来计算梯度,你还是会听到人们使用SGD来指代小批量数据梯度下降(或者用MGD来指代Minibatch Gradient Descent,而BGD来指代Batch gradient descent)。小批量数据的大小是一个超参数,但是一般并不需要通过交叉验证来调参。它一般由存储器的限制来决定的,或者干脆设置为同样大小,比如32,64,128等。之所以使用2的指数,是因为在实际中许多向量化操作实现的时候,如果输入数据量是2的倍数,那么运算更快。
5.5 本章小结
信息流概述。作为训练数据集的(x,y)时给定和固定的。权重开始的时候时随机数,是可以改变的。在正向传递中,评分函数计算类得分,存储在向量F中。损失函数包含两个分量:数据损失计算得数F与实际标签Y之间的一致性性。正则化损失仅是权重参数的函数。在梯度下降期间,我们计算权重上的梯度(并且如果我们愿意的话,也计算数据上的梯度),并使用它们在梯度下降期间执行参数更新。
本章:
- 我们将损失函数比作一个在高维度上的山地,并尝试到达它的最底部。最优化的工作过程可以看做一个蒙着眼睛的徒步者希望摸索着走到山的底部。例中,我们看SVM的损失函数是分段线性的,并且是碗状的。
- 提出了迭代优化的思想,从一个随机的权重开始,然后一步步地优化他们,指导让损失值变得最小。
- 我们看到函数的梯度给出了最陡峭的上升方向。介绍了利用有限差分法来近似计算梯度的方法,该方法实现简单但是效率较低。
- 我们看到,参数更新需要设置一个棘手的超参数步长(或学习率):如果太低,进度稳定,但缓慢。如果太高,进度可能会更快,但风险更大。我们将在后续的章节中更详细地探讨这种权衡。
- 我们讨论了数值梯度和微分梯度之间的折衷。数值梯度是简单的,但它是近似的和昂贵的计算。解析梯度是精确的,计算快速,但更容易出错,因为它需要用数学推导梯度。因此,在实践中,我们总是使用解析梯度,然后执行梯度检查,即将解析梯度与数值梯度进行比较。
- 我们引入了梯度下降算法,迭代地计算梯度,并在循环中执行参数更新。
预告:本章的核心内容是:理解并能计算损失函数关于权重的梯度,是设计、训练和理解神经网络的核心能力。下节中,将介绍如何使用链式法则来高效地计算梯度,也就是通常所说的反向传播机制。该机制能够对包含卷积神经网络在内的几乎所有类型的神经网络的损失函数进行高效的最优化。
斯坦福大学计算机视图课程,青星人工智能研究中心 翻译整理
原文地址 CS231n Convolutional Neural Networks for Visual Recognition