cs231n笔记(二) 最优化方法

回顾上一节中,介绍了图像分类任务中的两个要点:git

  1.  假设函数。该函数将原始图像像素映射为分类评分值。 
  2. 损失函数。该函数根据分类评分和训练集图像数据实际分类的一致性,衡量某个具体参数集的质量好坏。

如今介绍第三个要点,也是最后一个关键部分:最优化Optimization。最优化是寻找能使得损失函数值最小化的参数 W 的过程,一旦理解了这三个部分是如何相互运做的,咱们将会回到第一个要点,而后将其拓展为一个远比线性函数复杂的函数:首先是神经网络,而后是卷积神经网络。而损失函数和最优化过程这两个部分将会保持相对稳定。github

损失函数可视化本节讨论的损失函数通常都是定义在高维度的空间中(好比,在 CIFAR-10 中一个线性分类器的权重矩阵大小是 $[10 \times 3073]$ ,就有 30730 个参数),这样要将其可视化就很困难。然而办法仍是有的,在 1 个维度或者 2 个维度的方向上对高维空间进行切片,就能获得一些直观感觉。例如,随机生成一个权重矩阵 W ,将其看作向量,该矩阵就与高维空间中的一个点对应。而后沿着某个维度方向前进的同时记录损失函数值的变化。换句话说,就是生成一个随机的方向 $W_1$ 而且沿着此方向计算损失值,计算方法是根据不一样的 $a$ 值来计算 $L(W + aW_1)$ 。这个过程将生成一个图表,其 $x$ 轴是 $a$ 值,$y$ 轴是损失函数值。一样的方法还能够用在两个维度上,经过改变 $a,b$ 来计算损失值 $L(W + aW_1 + bW_2)$ ,从而给出二维的图像。在图像中,$a,b$ 能够分别用 $x,y$ 轴表示,而损失函数的值能够用颜色变化表示:

svm1d

上图左边与中间为一个数据且无正则化的多类 SVM 的损失函数的图示。右边是 CIFAR-10 中的 100 个数据。: $a$ 值变化在某个维度方向上对应的的损失值变化。中和右:两个维度方向上的损失值切片图,蓝色部分是低损失值区域,红色部分是高损失值区域。注意损失函数的分段线性结构。多个样本的损失值是整体的平均值,因此右边的碗状结构是不少的分段线性结构的平均(好比中间这个就是其中之一)。能够经过数学公式来解释损失函数的分段线性结构,对于一个单独的数据,损失函数的计算公式以下:算法

\[L_i = \sum_{j \ne y_i} max (0,w_j^Tx_i –w_{y_i} ^T x_i + 1)\]数组

经过公式可见,每一个样本的数据损失值是以 W 为参数的线性函数的总和(零阈值来源于 $max(0,-)$ 函数)。W 的每一行(即 $w_j$ ),有时候它前面是一个正号(好比当它对应错误分类的时候),有时候它前面是一个负号(好比当它是是正确分类的时候)。为进一步阐明,假设有一个简单的数据集,其中包含有 3 个只有 1 个维度的点,数据集数据点有 3 个类别。那么完整的无正则化 SVM 的损失值计算以下:网络

\begin{aligned}
L_0 &= max (0,w_1^Tx_0 -w_0 ^T x_0 + 1) + max (0,w_2^Tx_0 - w_0 ^T x_0 + 1)\\
L_1 &= max (0,w_0^Tx_1 -w_1 ^T x_1 + 1) + max (0,w_2^Tx_1 - w_1 ^T x_1 + 1)\\
L_2 &= max (0,w_0^Tx_2 -w_2 ^T x_2 + 1) + max (0,w_1^Tx_2 - w_2 ^T x_2 + 1)\\
L   &=\frac{1}{3} (L_0  + L_1 + L_2)
\end{aligned}
dom

由于这些例子都是一维的,因此数据 $x_i$ 和权重 $w_j$ 都是数字。观察 $w_0$ ,能够看到上面的式子中一些项是 $w_0$ 的线性函数,且每一项都会与 0 比较,取二者的最大值。可做图以下:ide

svmbowl

从一个维度方向上对数据损失值的展现。 $x$ 轴方向就是一个权重, $y$ 轴就是损失值。数据损失是多个部分组合而成。其中每一个部分要么是某个权重的独立部分,要么是该权重的线性函数与 0 阈值的比较。完整的 SVM 数据损失就是这个形状的 30730 维版本。函数

须要多说一句的是,SVM 的损失为一个凸函数,可是一旦咱们将 $f$ 函数扩展到神经网络,目标函数就就再也不是凸函数了,图像也不会像上面那样是个碗状,而是凹凸不平的复杂地形形状。学习

须要注意到:因为 $\max$ 操做,损失函数中存在一些不可导点这些点使得损失函数不可微,由于在这些不可导点,梯度是没有定义的。可是次梯度(subgradient)依然存在且经常被使用。在本课中,咱们将交换使用 次梯度 梯度 两个术语。测试

最优化 Optimization损失函数能够量化某个具体权重集 W 的优略。而最优化的目标就是找到可以最小化损失函数值的 W 。咱们如今就朝着这个目标前进,实现一个可以最优化损失函数的方法。对于有一些经验的同窗,这节课看起来有点奇怪,由于使用的例子(SVM 损失函数)是一个凸函数问题。可是要记得,最终的目标是不只仅对凸函数作最优化,而是可以最优化一个神经网络,而对于神经网络是不能简单的使用凸函数的最优化技巧的。

策略#1随机搜索

既然确认参数集 W 的好坏蛮简单的,那第一个想到的(差劲)方法,就是能够随机尝试不少不一样的权重,而后看其中哪一个最好。过程以下:

# 假设X_train的每一列都是一个数据样本(好比3073 x 50000)
# 假设Y_train是数据样本的类别标签(好比一个长50000的一维数组)
# 假设函数L对损失函数进行评价

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)

# 输出:
# 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)
View Code
在上面的代码中,咱们尝试了若干随机生成的权重矩阵 W ,其中某些的损失值较小,而另外一些的损失值大些。咱们能够把此次随机搜索中找到的最好的权重 W 取出,而后去跑测试集:
# 假设X_test尺寸是[3073 x 10000], Y_test尺寸是[10000 x 1]
scores = Wbest.dot(Xte_cols) # 10 x 10000, the class scores for all test examples
# 找到在每列中评分值最大的索引(即预测的分类)
Yte_predict = np.argmax(scores, axis = 0)
# 以及计算准确率
np.mean(Yte_predict == Yte)
# 返回 0.1555
View Code

验证集上表现最好的权重 W跑测试集的准确率是 15.5% ,而彻底随机猜的准确率是 10% ,如此看来,这个准确率对于这样一个不通过大脑的策略来讲,还算不错嘛!

迭代优化固然,咱们确定能作得更好些。核心思路是:虽然找到最优的权重 W 很是困难,甚至是不可能的(尤为当 W 中存的是整个神经网络的权重的时候),但若是问题转化为:对一个权重矩阵集 W 取优,使其损失值稍微减小。那么问题的难度就大大下降了。换句话说,咱们的方法从一个随机的 W 开始,而后对其迭代取优,每次都让它的损失值变得更小一点。咱们的策略是从随机权重开始,而后迭代取优,从而得到更低的损失值。一个助于理解的比喻是把你本身想象成一个蒙着眼睛的徒步者,正走在山地地形上,目标是要慢慢走到山底。在 CIFAR-10 的例子中,这山是 30730 维的(由于 W 是 3073x10 )。咱们在山上踩的每一点都对应一个的损失值,该损失值能够看作该点的海拔高度。

策略#2:随机本地搜索

第一个策略能够看作是每走一步都尝试几个随机方向,若是某个方向是向山下的,就向该方向走一步。此次咱们从一个随机 W 开始,而后生成一个随机的扰动 $\delta W$ ,只有当 $W + \delta W$ 的损失值变低,咱们才会更新。这个过程的具体代码以下:

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)
View Code

使用一样的数据(1000),这个方法能够获得 21.4%的分类准确率。这个比策略一好,可是依然过于浪费计算资源。

策略#3:跟随梯度

前两个策略中,咱们是尝试在权重空间中随机找到一个方向,沿着该方向能下降损失函数的损失值。其实不须要随机寻找方向,由于能够直接计算出最好的方向,这就是从数学上计算出最陡峭的方向。这个方向就是损失函数的梯度(gradient)。在蒙眼徒步者的比喻中,这个方法就比如是感觉咱们脚下山体的倾斜程度,而后向着最陡峭的降低方向下山。

在一维函数中,斜率是函数在某一点的瞬时变化率。梯度是函数的斜率的通常化表达,它不是一个值,而是一个向量。在输入空间中,梯度是各个维度的斜率组成的向量(或者称为导数 derivatives )。对一维函数的求导公式以下:

\[\frac{df(x)}{dx} = \lim _{h \rightarrow 0} \frac{f(x + h) – f (x)}{h}\]

当函数有多个参数的时候,咱们称导数为偏导数。而梯度就是在每一个维度上偏导数所造成的向量。

梯度的计算计算梯度有两种方法:一个是数值梯度法,实现相对简单。另外一个分析梯度法,计算迅速,结果精确,可是实现时容易出错,且须要使用微分。如今对两种方法进行介绍:

上节中的公式已经给出数值计算梯度的方法。下面代码是一个输入为函数 $f$ 和向量 $x$ ,经过数值梯度法计算 $f$ 梯度的通用函数,它返回函数 $f$ 在点 $x$ 处的梯度,:

def eval_numerical_gradient(f, x):
  """  
  一个f在x处的数值梯度法的简单实现
  - f是只有一个参数的函数
  - x是计算梯度的点
  """ 
  fx = f(x) # 在原点计算函数值
  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]
    x[ix] = old_value + h # 增长h
    fxh = f(x) # 计算f(x + h)
    x[ix] = old_value # 存到前一个值中 (很是重要)

    # 计算偏导数
    grad[ix] = (fxh - fx) / h # 坡度
    it.iternext() # 到下个维度

  return grad
View Code

根据上面的梯度公式,代码对全部维度进行迭代,在每一个维度上产生一个很小的变化 h ,经过观察函数值变化,计算函数在该维度上的偏导数。最后,全部的梯度存储在变量 grad 中。

注意在数学公式中, $h$ 的取值是趋近于0的,然而在实际中,用一个很小的数值(好比例子中的 $1^{-5}$ )就足够了。在不产生数值计算出错的理想前提下,你会使用尽量小的 $h$ 。还有,实际中用中心差值公式(centered difference formula)效果较好:

\[ \frac{f(x)}{dx} = \lim _{h \rightarrow 0}  = \frac{f(x+ h ) – f(x - h)}{2h}\]

可使用上面这个公式来计算任意函数在任意点上的梯度。下面计算权重空间中的某些随机点上,CIFAR-10 损失函数的梯度:

# 要使用上面的代码咱们须要一个只有一个参数的函数
# (在这里参数就是权重)因此也包含了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) # 获得梯度
View Code

梯度告诉咱们损失函数在每一个维度上的斜率,以此来进行更新:

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.214036
View Code

在梯度负方向上更新:在上面的代码中,为了计算 W_new,要注意咱们是向着梯度 df 的负方向去更新,这是由于咱们但愿损失函数值是在下降而不是升高。

梯度指明了函数在哪一个方向是变化率最大的,可是没有指明在这个方向上应该走多远。在后续的课程中能够看到,选择步长(也叫做学习率)将会是神经网络训练中最重要也是最头痛的超参数设定之一。仍是用蒙眼徒步者下山的比喻,这就比如咱们能够感受到脚朝向的不一样方向上,地形的倾斜程度不一样。可是该跨出多长的步长呢?不肯定。若是谨慎地小步走,状况可能比较稳定可是进展较慢(这就是步长较小的状况)。相反,若是想尽快下山,那就大步走吧,但结果也不必定尽如人意。在上面的代码中就能看见反例,在某些点若是步长过大,反而可能越过最低点致使更高的损失值。

stepsize

将步长效果视觉化的图例。从某个具体的点 W 开始计算梯度(白箭头方向是负梯度方向),梯度告诉了咱们损失函数降低最陡峭的方向。小步长降低稳定但进度慢,大步长进展快可是风险更大。采起大步长可能致使错过最优势,让损失值上升。步长(后面会称其为学习率)将会是咱们在调参中最重要的超参数之一。

数值梯度的效率问题:你可能已经注意到,计算数值梯度的复杂性和参数的量线性相关。在本例中有 30730 个参数,因此损失函数每走一步就须要计算 30731 次损失函数的梯度。现代神经网络很容易就有上千万的参数,所以这个问题只会愈加严峻。显然这个策略不适合大规模数据,咱们须要更好的策略。

微分分析计算梯度使用有限差值近似计算梯度比较简单,但缺点在于终究只是近似(由于咱们对于 $h$ 值是选取了一个很小的数值,但真正的梯度定义中 $h$ 趋向 0 的极限),且耗费计算资源太多。第二个梯度计算方法是利用微分来分析,能获得计算梯度的公式(不是近似),用公式计算梯度速度很快,惟一很差的就是实现的时候容易出错。为了解决这个问题,在实际操做时经常将分析梯度法的结果和数值梯度法的结果做比较,以此来检查其实现的正确性,这个步骤叫作梯度检查

用SVM的损失函数在某个数据点上的计算来举例:

\[L_i = \sum_{j \ne y_i} max(0, w_j^T x_i – w_{y_i}^T x_i + \Delta)\]

能够对函数进行微分。好比,对 $w_{y_i}$ 进行微分获得:

\[\nabla_{w_{y_i}} L_i = - \left ( \sum_{j \ne y_i} \mathbb{I}(w_j^Tx_i - w_{y_i}^T x_i + \Delta > 0) \right )x_i\]

其中 $\mathbb{I}$ 是一个示性函数,若是括号中的条件为真,那么函数值为 1 ,若是为假,则函数值为 0 。虽然上述公式看起来复杂,但在代码实现的时候比较简单:只须要计算没有知足边界值的分类的数量(所以对损失函数产生了贡献),而后乘以 $x_i$ 就是梯度了。注意,这个梯度只是对应正确分类的 W 的行向量的梯度,那些 $j \ne y_i$ 行的梯度是:
\[\nabla_{w_j} L_i = \mathbb{I}(w_j^Tx_i - w_{y_i}^T x_i + \Delta > 0 ) x_i\]

一旦将梯度的公式微分出来,代码实现公式并用于梯度更新就比较顺畅了。

梯度降低

如今能够计算损失函数的梯度了,程序重复地计算梯度而后对参数进行更新,这一过程称为梯度降低,他的普通版本是这样的:

# 普通的梯度降低

while True:
  weights_grad = evaluate_gradient(loss_fun, data, weights)
  weights += - step_size * weights_grad # 进行梯度更新
View Code

这个简单的循环在全部的神经网络核心库中都有。虽然也有其余实现最优化的方法(好比 L-BFGS ),可是到目前为止,梯度降低是对神经网络的损失函数最优化中最经常使用的方法。课程中,咱们会在它的循环细节增长一些新的东西(好比更新的具体公式),可是核心思想不变,那就是咱们一直跟着梯度走,直到结果再也不变化。

小批量数据梯度降低( Mini-batch gradient descent :在大规模的应用中(好比 ILSVRC 挑战赛),训练数据能够达到百万级量级。若是像这样计算整个训练集,来得到仅仅一个参数的更新就太浪费了。一个经常使用的方法是计算训练集中的小批量(batches)数据。例如,在目前最高水平的卷积神经网络中,一个典型的小批量包含 256 个例子,而整个训练集是多少呢?一百二十万个。这个小批量数据就用来实现一个参数更新:

# 普通的小批量数据梯度降低

while True:
  data_batch = sample_training_data(data, 256) # 256个数据
  weights_grad = evaluate_gradient(loss_fun, data_batch, weights)
  weights += - step_size * weights_grad # 参数更新
 
View Code

这个方法之因此效果不错,是由于训练集中的数据都是相关的。要理解这一点,能够想象一个极端状况:在 ILSVRC 中的 120 万个图像是 1000 张不一样图片的复制(每一个类别 1 张图片,每张图片有 1200 张复制)。那么显然计算这 1200 张复制图像的梯度就应该是同样的。对比 120 万张图片的数据损失的均值与只计算 1000 张的子集的数据损失均值时,结果应该是同样的。实际状况中,数据集确定不会包含重复图像,那么小批量数据的梯度就是对整个数据集梯度的一个近似。所以,在实践中经过计算小批量数据的梯度能够实现更快速地收敛,并以此来进行更频繁的参数更新。

小批量数据策略有个极端状况,那就是每一个批量中只有 1 个数据样本,这种策略被称为随机梯度降低(Stochastic Gradient Descent 简称 SGD ,有时候也被称为在线梯度降低。这种策略在实际状况中相对少见,由于向量化操做的代码一次计算 100 个数据 比 100 次计算 1 个数据要高效不少。即便 SGD 在技术上是指每次使用 1 个数据来计算梯度,你仍是会听到人们使用 SGD 来指代小批量数据梯度降低(或者用 MGD 来指代小批量数据梯度降低,而 BGD 来指代则相对少见)。小批量数据的大小是一个超参数,可是通常并不须要经过交叉验证来调参。它通常由存储器的限制来决定的,或者干脆设置为一样大小,好比 32,64,128 等。之因此使用 2 的指数,是由于在实际中许多向量化操做实现的时候,若是输入数据量是 2 的倍数,那么运算更快。

dataflow

信息流的总结图例。数据集中的 $(x_i,y_i)$ 是给定的。权重从一个随机数字开始,且能够改变。在前向传播时,评分函数计算出类别的得分并存储在向量 $f$ 中。损失函数包含两个部分:数据损失和正则化损失。其中,数据损失计算的是分类评分 $f$ 和实际标签 $y$ 之间的差别,正则化损失只是一个关于权重的函数。在梯度降低过程当中,咱们计算权重的梯度(若是愿意的话,也能够计算数据上的梯度),而后使用它们来实现参数的更新。

在本节中:

  • 将损失函数比做了一个高维度的最优化地形,并尝试到达它的最底部。最优化的工做过程能够看作一个蒙着眼睛的徒步者但愿摸索着走到山的底部。在例子中,可见 SVM 的损失函数是分段线性的,而且是碗状的。

  • 提出了迭代优化的思想,从一个随机的权重开始,而后一步步地让损失值变小,直到最小。

  • 函数的梯度给出了该函数最陡峭的上升方向。介绍了利用有限的差值来近似计算梯度的方法,该方法实现简单可是效率较低(有限差值就是 $h$,用来计算数值梯度)。

  • 参数更新须要有技巧地设置步长。也叫学习率。若是步长过小,进度稳定可是缓慢,若是步长太大,进度快可是可能有风险。

  • 讨论权衡了数值梯度法和分析梯度法。数值梯度法计算简单,但结果只是近似且耗费计算资源。分析梯度法计算准确迅速可是实现容易出错,并且须要对梯度公式进行推导的数学基本功。所以,在实际中使用分析梯度法,而后使用梯度检查来检查其实现正确与否,其本质就是将分析梯度法的结果与数值梯度法的计算结果对比。

  • 介绍了梯度降低算法,它在循环中迭代地计算梯度并更新参数。

参考:

https://zhuanlan.zhihu.com/p/21360434

http://cs231n.github.io/optimization-1/