CS231n课程笔记(4) Optimization Note

本文转载自:https://zhuanlan.zhihu.com/p/21387326?refer=intelligentunit,原文:http://cs231n.github.io/optimization-1/

简介

图像分类任务中的两个关键部分:

  1. 基于参数的评分函数。该函数将原始图像像素映射为分类评分值(例如,一个线性函数)。
  2. 损失函数。该函数能够根据评分和训练集图像数据实际分类的一致性,衡量某个具体参数集的质量好坏。损失函数有多种版本和不同的实现方式(例如:Softmax或SVM)。
    上节,线性函数的形式是$f(x_i,W)=Wx_i$,而SVM实现的公式是:$$
    L=\frac {1}{N}\sum_i\sum_{j \neq y_i} [max(0,f(x_i;W)_j - f(x_i;W)_{y_i}+1)]+\alpha R(W)$$
    对于图像数据$x_i$,如果基于参数集W做出的分类预测与真实情况比较一致,那么计算出来的损失值L就很低。现在介绍第三个,也是最后一个关键部分:最优化Optimization。最优化是寻找能使得损失函数值最小化的参数W的过程。

    损失函数可视化

    课程讨论的损失函数一般都是定义在高维度的空间中,这样要将其进行可视化就很困难。然而办法还是有的,在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轴表示,而损失函数的值可以用颜色变化表示:

    mark

    一个无正则化的多类SVM的损失函数的图示。左边和中间只有一个样本数据,右边是CIFAR-10中的100个数据。:a值变化在某个维度方向上对应的的损失值变化。中和右:两个维度方向上的损失值切片图,蓝色部分是低损失值区域,红色部分是高损失值区域。注意损失函数的分段线性结构。多个样本的损失值是总体的平均值,所以右边的碗状结构是很多的分段线性结构的平均(比如中间这个就是其中之一)。

可以通过数学公式来解释损失函数的分段线性结构,对于一个单独的数据,有损失函数的计算公式如下:$$
L_i = \sum_{j \neq y_i}[max(0, W_j^Tx_i - W_{y_i}^Tx_i + 1)]
$$
通过公式可见,每个样本的数据损失值是以W为参数的线性函数的总和(0阈值来源于max(0,-)函数)。W的每一行(即$w_j$),有时候它前面是一个正号(比如当它对应错误分类的时候),有时候它前面是一个负号(比如当它是是正确分类的时候)。为进一步阐明,假设有一个简单的数据集,其中包含有3个只有1个维度的点,数据集数据点有3个类别。那么完整的无正则化SVM的损失值计算如下:$$
L_0 = max(0, w_1^Tx_0 - w_0^Tx_0 + 1)+max(0, w_2^Tx_0 - w_0^Tx_0 + 1)\\\\
L_1 = max(0, w_0^Tx_1 - w_1^Tx_1 + 1)+max(0, w_2^Tx_1 - w_1^Tx_1 + 1)\\\\
L_2 = max(0, w_0^Tx_2 - w_2^Tx_2 + 1)+max(0, w_1^Tx_2 -w_2^Tx_2 + 1)\\\\
L=(L_0+L_1+L_2)/3
$$
因为这些例子都是一维的,所以数据$x_i$和权重$w_j$都是数字。观察$w_0$,可以看到上面的式子中一些项是$w_0$的线性函数,且每一项都会与0比较,取两者的最大值。可作图如下:


mark

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

最优化

损失函数可以量化某个具体权重集W的质量,而最优化的目标就是找到能够最小化损失函数值的W。我们现在就朝着这个目标前进,实现一个能够最优化损失函数的方法。对于一些有经验的同学,这节课看起来有点奇怪,因为使用的例子(SVM损失函数)是一个凸函数。但是要记得,最终的目标是不仅仅对凸函数做最优化,而是能够最优化一个神经网络,而对于神经网络是不能简单的使用凸函数的最优化技巧的。
策略1:随机搜索
既然确认参数集W的好坏蛮简单的,那第一个想到的(差劲)方法,就是可以随机尝试很多不同的权重,然后看其中哪个最好。
蒙眼徒步者的比喻:一个助于理解的比喻是把你自己想象成一个蒙着眼睛的徒步者,正走在山地地形上,目标是要慢慢走到山底。在CIFAR-10的例子中,这山是30730维的(因为W是3073x10)。我们在山上踩的每一点都对应一个的损失值,该损失值可以看做该点的海拔高度。

策略2:随机本地搜索
第一个策略可以看做是每走一步都尝试几个随机方向,如果某个方向是向山下的,就向该方向走一步。这次我们从一个随机W开始,然后生成一个随机的扰动$\delta W$ ,只有当$W+\delta W$的损失值变低,我们才会更新。

策略3:梯度跟随
前两个策略中,我们是尝试在权重空间中找到一个方向,沿着该方向能降低损失函数的损失值。其实不需要随机寻找方向,因为可以直接计算出最好的方向,这就是从数学上计算出最陡峭的方向。这个方向就是损失函数的梯度(gradient)。在蒙眼徒步者的比喻中,这个方法就好比是感受我们脚下山体的倾斜程度,然后向着最陡峭的下降方向下山。
在一维函数中,斜率是函数在某一点的瞬时变化率。梯度是函数的斜率的一般化表达,它不是一个值,而是一个向量。在输入空间中,梯度是各个维度的斜率组成的向量(或者称为倒数)。对一维函数的求导公式如下:$$
\frac {df(x)}{dx} = \lim_{h\rightarrow0}\frac {f(x+h)-f(x)}{h}
$$
当函数有多个参数的时候,我们称导数为偏导数。而梯度就是在每个维度上偏导数所形成的向量。

梯度计算

计算梯度有两种方法:一个是缓慢的近似方法(数值梯度法),实现相对简单。另一个方法是(分析梯度法)计算迅速,结果精确,但是实现时容易出错,且需要使用微分。现在对这两种方法进行介绍:
利用有限差值计算梯度
上节中的公式已经给出数值计算梯度的方法。下面代码是一个输入为函数f和向量x,计算f的梯度的通用函数,它返回函数f在点x处的梯度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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

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

实践考虑:注意在数学公式中,h的取值是趋近于0的,然而在实际中, 用一个很小的数值就足够了。而不产生数值计算出错的理想前提下,使用尽可能小的h。还有,实际中用中心差值公式(centered difference formula)[f(x+h)-f(x-h)]/2h效果较好。

在扶梯度方向上更新:要注意我们是向着梯度df的负方向去更新,这是因为我们希望损失函数值是降低而不是升高。
步长的影响:梯度指明了函数在哪个方向是变化率最大的,但是没有指明在这个方向上应该走多远。选择步长(也叫作学习率)将会是神经网络训练中最重要(也是最头痛)的超参数设定之一。从某个具体的点W开始计算梯度(白箭头方向是负梯度方向),梯度告诉了我们损失函数下降最陡峭的方向。小步长下降稳定但进度慢,大步长进展快但是风险更大。采取大步长可能导致错过最优点,让损失值上升。步长(后面会称其为学习率)将会是我们在调参中最重要的超参数之一。

微分分析计算梯度
使用有限差值近似计算梯度比较简单,但缺点在于终究只是近似(因为我们对于h值是选取了一个很小的数值,但真正的梯度定义中h趋向0的极限),且耗费计算资源太多。第二个梯度计算方法是利用微分来分析,能得到计算梯度的公式(不是近似),用公式计算梯度速度很快,唯一不好的就是实现的时候容易出错。为了解决这个问题,在实际操作时常常将分析梯度法的结果和数值梯度法的结果作比较,以此来检查其实现的正确性,这个步骤叫做梯度检查
用SVM的损失函数在某个数据点上的计算来举例:$$
L_i = \sum_{j\neq y_i} [max(0, w_j^Tx_i - w_{y_i}^Tx_i)+\Delta]
$$
可以对函数进行微分,比如,对$w_{y_i}$进行微分得到:
$$
\nabla_{w_{y_i}}L_i = -(\sum_{j\neq y_i} 1(w_j^Tx_i-w_{y_i}^Tx_i+\Delta>0))x_i
$$
其中1是一个示性函数,如果括号中的条件为真,那么函数值为1,如果为假,则函数值为0。虽然上述公式看起来复杂,但在代码实现的时候比较简单:只需要计算没有满足边界值的分类的数量(因此对损失函数产生了贡献),然后乘以x_i就是梯度了。注意,这个梯度只是对应正确分类的W的行向量的梯度,那些$j\neq =y_i$行的梯度是:
$$
\nabla_{w_j}L_i=1(w_j^Tx_i-w_{y_i}^Tx_i+\Delta>0)x_i
$$
一旦将梯度的公式微分出来,代码实现公式并用于梯度更新就比较顺畅了。

梯度下降

现在可以计算损失函数的梯度了,程序重复地计算梯度然后对参数进行更新,这一过程称为梯度下降,他的普通版本是这样的:

1
2
3
4
# 普通的梯度下降
while True:
weights_grad = evaluate_gradient(loss_fun, data, weights)
weights += - step_size * weights_grad # 进行梯度更新

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

小批量数据梯度下降(Mini-batch gradient descent):在大规模的应用中(比如ILSVRC挑战赛),训练数据可以达到百万级量级。如果像这样计算整个训练集,来获得仅仅一个参数的更新就太浪费了。一个常用的方法是计算训练集中的小批量(batches)数据。这个方法之所以效果不错,是因为训练集中的数据都是相关的。

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

小结

  • 将损失函数比作了一个高维度的最优化地形,并尝试到达它的最底部。最优化的工作过程可以看做一个蒙着眼睛的徒步者希望摸索着走到山的底部。在例子中,可见SVM的损失函数是分段线性的,并且是碗状的。
  • 提出了迭代优化的思想,从一个随机的权重开始,然后一步步地让损失值变小,直到最小。
  • 函数的梯度给出了该函数最陡峭的上升方向。介绍了利用有限的差值来近似计算梯度的方法,该方法实现简单但是效率较低(有限差值就是h,用来计算数值梯度)。
  • 参数更新需要有技巧地设置步长。也叫学习率。如果步长太小,进度稳定但是缓慢,如果步长太大,进度快但是可能有风险。
  • 讨论权衡了数值梯度法和分析梯度法。数值梯度法计算简单,但结果只是近似且耗费计算资源。分析梯度法计算准确迅速但是实现容易出错,而且需要对梯度公式进行推导的数学基本功。因此,在实际中使用分析梯度法,然后使用梯度检查来检查其实现正确与否,其本质就是将分析梯度法的结果与数值梯度法的计算结果对比。
坚持原创技术分享,您的支持将鼓励我继续创作!