原文地址:http://cs231n.github.io/optimization-1/
标题:Optimization: Stochastic Gradient Descent
随手翻译,不当之处请指正
前面的章节中我们介绍了图像分类任务的两个关键部分:
线性方程的形式为 f(xi,W)=Wxi ,我们公式化的SVM分类器为:
伏笔: 一旦我们了解了这三个部分之间如何交互,我们就会回到第一个部分(参数化的映射函数),将它扩展为一个比线性映射复杂的多的函数。首先是完整的神经网络,然后是卷积神经网络。而损失函数和优化过程则相对的保持不变。
我们将要看到的损失函数一般都存在在高维空间中(比如CIFAR-10数据集中,一个线性分类器权重矩阵有[10x3073],也就是30730个参数),这使它非常难以可视化地表达出来。但是我们仍旧可以通过将高维空间限制(映射)在一维数轴或者二维平面中得到一些启发。比如,我们随机生成一个权重矩阵W(代表高维空间中的一个点),然后沿着数轴前进,记录损失函数的值。我们可以生成一个随机方向 W1 ,根据不同的 a 值用L(W+aW1)沿着这个方向计算损失。这个过程生成了一个简单的以 a 值为x轴、以损失值为y轴的简单的图。或者我们也可以将这个过程展现在二维空间中,也就是计算L(W+aW1+bW2),也就是改变 a,b 。这个时候, a,b 分别依赖x,y轴,而损失函数的值则用颜色表达。
CIFAR-10数据集中,多类SVM分类器(未正则化)损失函数的可视化结果,左边和中间是一个样本的结果,右边是100个样本的结果。左:只刻画a的一维损失。中,右:二维损失,蓝色代表数值低,红色代表数值高。注意到损失函数的逐段线性结构。多样本的损失以平均值的形式叠加在一起,所以右图中的碗形是许多逐段线性碗形(中)平均叠加而成的。
我们通过数学方法理解一下逐段线性的损失函数。对于一个样本的损失,我们有:
一维数据损失图例。x轴是简单的权重,y轴是损失。数据损失是多个表达式的和,每个表达式不是独立的特定的权重,就是被钳位在0的线性函数。完整的SVM数据损失是上述图像的30730维版本
从这个碗形的图像可以猜测得到,SVM损失函数(这里原文为cost function)是凸函数。大量的文献致力于高效地寻找这类函数的最小值,你也可以上一门叫做凸优化的斯坦福课程。一旦我们将评分函数 f 扩展到神经网络,我们的目标函数变得非凸,而可视化的结果并不是碗形,而是复杂且崎岖的。
不可微分损失函数 由于max函数的缘故,损失函数中存在一些转折点,这些转折点不可微分,因为这种地方没有梯度的定义。然而次梯度仍然存在且我们比较常用。这节课里我们会交替使用梯度和次梯度。
再次强调,损失函数允许我们对任意权重集W量化以及评价。优化的目的是找到让损失函数最小的权重集 W 。现在我们慢慢地开始“开发”优化损失函数的方法。在座的如果有些经验,会感觉非常古怪,我们使用的损失(SVM损失)是一个凸问题。但是注意的是,我们最后希望优化神经网络,而不可能简单的使用我们讲述的凸函数优化方法。
由于检查一个参数集W到底好不好是很简单的,浮现在脑海中的第一个想法就是随机地尝试不同的权重,跟踪一下哪个权重集表现最好,这个过程如下:
# assume X_train is the data where each column is an example (e.g. 3073 x 50,000)
# assume Y_train are the labels (e.g. 1D array of 50,000)
# assume the function L evaluates the loss function
bestloss = float("inf") # Python assigns the highest possible float value
for num in xrange(1000):
W = np.random.randn(10, 3073) * 0.0001 # generate random parameters
loss = L(X_train, Y_train, W) # get the loss over the entire training set
if loss < bestloss: # keep track of the best solution
bestloss = loss
bestW = W
print 'in attempt %d the loss was %f, best %f' % (num, loss, bestloss)
# prints:
# 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:
从上面的代码中看出,我们尝试了几个随机的权重向量 W ,其中一些比另一些表现的好。我们把这几次尝试中最好的权重W取出来,放在测试集里:
# Assume X_test is [3073 x 10000], Y_test [10000 x 1]
scores = Wbest.dot(Xte_cols) # 10 x 10000, the class scores for all test examples
# find the index with max score in each column (the predicted class)
Yte_predict = np.argmax(scores, axis = 0)
# and calculate accuracy (fraction of predictions that are correct)
np.mean(Yte_predict == Yte)
# returns 0.1555
表现最好的权重 W 最终给出的精度为大约15.5%。考虑到如果纯随机猜测的话,精度大约是10%,这种方法居然比死脑筋的随机略胜一筹!
核心思想:迭代求精。当然,事实表明我们完全可以做的更好。核心思想就是寻找最好的权重W是非常困难,甚至不可能的问题(特别是当 W 包括了整个复杂神经网络的时候),但是让具体的权重W稍微优化一些的求精问题就明显简单很多。也就是说,我们的方法从一个随机的权重 W 开始,迭代地对其求精,每次让他的表现稍微好一点。
我们的策略是从随机权重开始,迭代的对其求精,以使得损失降低
被蒙上眼的远足者(这翻译好愚蠢,原谅)。一个可能帮助理解的比喻就是把自己想象成一个被蒙上眼的远足者,在丘陵地带远足,并且想到达最低的地方。在CIFAR-10的例子中,这些“丘陵”是30730维的,丘陵上每个点都代表着一个特定的损失。
第一个策略可以理解成朝随机方向迈一脚,但只有海拔会降低的时候才前进。我们从随机生成的W开始,生成随机扰动 δW ,如果扰动结果 W+δW 损失变小,我们进行更新,这些步骤的过程如下:
W = np.random.randn(10, 3073) * 0.001 # generate random starting 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)
用前面的损失函数评估值(1000),这个方法在测试分类中分类精度达到21.4%。这已经有进步了,但是仍然很浪费计算成本。
前节我们尝试找到权重空间中的一个方向,希望可以优化我们的权重向量(并且给出更低的损失)。事实上我们不需要随机搜索好的方向:我们可以沿着数学证明的下降最快的方向调整我们的权重向量。这个方向与损失函数的梯度相关。在我们的比喻中,这种方法类似沿着斜坡向下,也就是找到下降最快的方法。
在一维函数中,这个倾斜就是任何一点的斜率。而梯度下降就是将一个数字的输入泛化到以一个向量输入函数中。另外,梯度就是一个矢量,输入空间中每个维度对应一个倾斜(度)(也就是导数)。一维导数定义:
计算梯度有两种方法:一个非常慢、近似计算但是很简单的方法(数值梯度),和一个速度快,精确,但是易错的微分方法(分析梯度)。两这里种方法都会提到。
上面给的式子可以计算数值梯度。这里是一个简单的输入函数f
,向量x
计算梯度,并返回f
在x
处梯度的函数:
def eval_numerical_gradient(f, x):
"""
a naive implementation of numerical gradient of f at x
- f should be a function that takes a single argument
- x is the point (numpy array) to evaluate the gradient at
"""
fx = f(x) # evaluate function value at original point
grad = np.zeros(x.shape)
h = 0.00001
# iterate over all indexes in x
it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
while not it.finished:
# evaluate function at x+h
ix = it.multi_index
old_value = x[ix]
x[ix] = old_value + h # increment by h
fxh = f(x) # evalute f(x + h)
x[ix] = old_value # restore to previous value (very important!)
# compute the partial derivative
grad[ix] = (fxh - fx) / h # the slope
it.iternext() # step to next dimension
return grad
上面的代码在所有维度上迭代,改变量为h
,通过函数值改变大小,计算该维度上损失函数的偏导数。变量grad
最终保存了所有梯度。
实际操作中的思考 注意到在数学表达式中,梯度的定义限制在h趋近0的情况下,但是实际上一个非常小的值就已经很充分了(比如例中的1e-5)。理想的情况下,应该使用不导致数值问题的最小步长。另外,在实际中一般用中心差分公式 [f(x+h)−f(x−h)]/2h 计算梯度。
上面的函数可以在任何点计算任何函数的梯度。下面在权重空间里计算CIFAR-10的损失函数在一些随机点处的梯度。
# to use the generic code above we want a function that takes a single argument
# (the weights in our case) so we close over X_train and Y_train
def CIFAR10_loss_fun(W):
return L(X_train, Y_train, W)
W = np.random.rand(10, 3073) * 0.001 # random weight vector
df = eval_numerical_gradient(CIFAR10_loss_fun, W) # get the gradient
梯度告诉我们损失函数在每个维度上的斜率,所以我们更新一下代码:
loss_original = CIFAR10_loss_fun(W) # the original loss
print 'original loss: %f' % (loss_original, )
# lets see the effect of multiple step sizes
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 # new position in the weight space
loss_new = CIFAR10_loss_fun(W_new)
print 'for step size %f new loss: %f' % (step_size, loss_new)
# prints:
# 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.214036
副梯度方向更新 上面的代码中我们用梯度df
的负值更新W_new
值,因为我们希望我们的损失函数下降,而不是上升。
步长的影响 梯度告诉我们哪个方向上上升最快,但是没有告诉我们要前进多长的距离。我们后面会看到选择步长(也叫学习率)会是训练神经网络中一个非常重要的(也是非常令人头疼的)超参数设置。在我们的比喻中,方向使我们要考虑的,但是步长是不确定的。如果小心地随机下脚我们会有连续的但是很微小的进步(因为步长很小)。相反地,可以选择大的步长,自信可以让梯度下降得更快,但是这可能适得其反。像上面看到的例子,有些地方我们选择了较大的步长,损失也会增大,这称作“overstep”。
可视化的步长影响。我们从 W 的特定点开始评估告诉我们损失函数下降最快的方向梯度(或者说是梯度的负,也就是白色的箭头)。小步长可以获得连续的,较小的进步。大步长可能过程比较好,但是有风险。大步长可能越过了下降,让损失更差了。步长(后面称为学习率)会成为后面调整超参数最重要的一环之一。
效率问题 你可能注意到,评估数值梯度复杂度与参数数目线性相关。在例子中我们有30370个参数,因此更新一个参数时我们需要进行30731次损失函数梯度评估。这个问题可能更严重,因为现代神经网络随随便便就达到几千万参数。显然,这个东西并不好,我们需要更好的办法。
数值梯度可以通过差分逼近的方法很简单地实现,但是坏处就是近似(因为我们必须选择一个非常小的值h,然而真正的梯度定义是h趋向于0),并且计算成本非常高。第二种方法是使用微分方法计算分析梯度,我们可以为梯度导出一个粗暴的方程,计算起来非常快。但是不像数值梯度,它实现起来很容易出错。所以在实际运作中,常常先计算分析梯度,再使用数值梯度检查是否正确实现。这就是叫梯度检查。
我们对一个数据点使用SVM损失函数做例子: