1、简介
梯度下降是一种求函数最小值的优化算法。从函数上的一个随机点开始,沿着函数梯度的负方向移动,以达到局部/全局最小值。
损失函数让我们量化任何特定的权重集合 的质量。优化的目标是找到使损失函数最小化的 。
策略1:随机搜索
由于时检查给定的参数 的好坏,所以可能想到的第一个想法是简单地尝试许多不同的随机权重,并跟踪哪个最有效。
bestloss = float("inf")
for num in range(1000):
W = np.random.randn(10, 3073) * 0.0001 # generate random parameters
loss = Loss(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
上述策略是一个非常糟糕的方法,因为随机充满不确定性,可能不包含全局最优 。
核心理念:迭代细化。其核心思想是,找到最佳的权值 集是一个非常困难甚至不可能的问题(特别是当 包含了整个复杂神经网络的权值时),但将特定的权值 集细化到稍好一点的问题就明显不那么困难了。换句话说,将从随机的 开始,然后迭代地优化它,使它每次都稍微好一点。
策略将是从随机权重开始,随着时间的推移迭代改进它们,以降低损失。
蒙眼下山类比:把自己想象成在一个丘陵地带徒步旅行,蒙上眼睛,并试图到达底部。在山上的每一个点上,都有一个特殊的损失(地形的高度)。
策略2:随机局部搜索
具体来说,将从一个随机 开始,对其产生随机扰动 ,如果扰动 处的损失较低,将执行更新。这个过程的代码如下:
W = np.random.randn(10, 3073) * 0.001 # generate random starting W
bestloss = float("inf")
for i in range(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
策略3:遵循梯度
在上述中,试图在权重空间中找到一个方向,以改善权重向量(并降低损失)。
事实证明不需要通过随机搜索寻找一个好方向:可以通过数学保证的下降最陡的方向作为最好的方向计算权向量(至少在步长趋于0时是这样)。
这个方向与损失函数的梯度有关。在徒步旅行类比中,这种方法大致相当于感觉脚下的山坡坡度,然后沿着感觉最陡的方向往下走。
在一维函数中,斜率是函数在任何感兴趣的点上的瞬时变化率。梯度是斜率的泛化对于那些不接受单个数字而是一个数字向量的函数。
此外,梯度只是输入空间中每个维度的斜率向量(通常称为导数)。一维函数对其输入求导的数学表达式为:
当感兴趣的函数取一个向量而不是一个数时,称这些导数为偏导数,而梯度就是各维上的偏导数的向量。
2、梯度计算
有两种计算梯度的方法:
- 一种缓慢,近似但简单的方法(数值梯度)
-
用有限差分进行梯度数值计算
上面给出的公式允许用数值方法计算梯度。根据上面的公式,可以对梯度进行数值计算。这是一个泛型函数,它取一个函数 ,一个向量 来计算梯度,并返回 在 处的梯度:
def eval_numerical_gradient(f, x):
"""
计算 f 在 x 处的数值梯度
- f 只有一个输入变量
- x 是需要计算梯度的点
"""
y = f(x)
grad = np.zeros(x.shape)
h = 1e-5
# 迭代遍历 x 的索引
it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
while not it.finished:
# 计算不同维度的偏差数
ix = it.multi_index
yh = f(x[ix]+ h) # 计算 f(x + h)
# 计算维度 ix 的偏导数
grad[ix] = (yh - y) / h # 斜率
it.iternext() # 下一个维度
return grad
根据上面给出的梯度公式,上面的代码在所有维度上逐一迭代,沿着这个维度对 做一个小的改变,然后通过观察函数的改变量来计算损失函数沿着这个维度的偏导数。变量 在最后保持完整的梯度。
注意,在数学公式中,梯度定义为 趋于 时的极限,但在实践中,通常使用一个非常小的值(如示例中所示的1e-5)就足够了。
理想情况下,使用不会导致数值问题的最小步长。此外,在实践中,使用中心差分公式计算数值梯度通常效果更好。
可以用上面给出的函数来计算任意点和任意函数的梯度:W = np.random.rand(10, 3073) * 0.001 # random weight vector
grad = eval_numerical_gradient(loss_fun, W) # get the gradient
梯度说明损失函数沿每个维度的斜率,可以用它来进行更新:
for step_size in [-10, -9, -8, -7, -6, -5,-4,-3,-2,-1]:
step = 10 ** step_size
W_new = W - step * grad # 新的权重
loss_new = loss_fun(W_new)
以负梯度方向更新。 在上面的代码中,为了计算 ,在梯度 的负方向上进行更新,因为希望损失函数减少,而不是增加。
步长影响。 梯度说明函数的最大增长率的方向,但它没有说明沿着这个方向应该走多远。选择步长(也称为学习率)将成为训练神经网络中最重要的(也是最令人头疼的)超参数设置之一。
在蒙眼下山类比中,我们感觉脚下的山在向某个方向倾斜,但我们应该走的步长是不确定的。如果小心地拖着脚走,我们可以期望取得一致但非常小的进步(这相当于有一个小的步长)。相反地,也可以选择迈一大步,自信地走一步,试图更快地下降,但这可能不会有回报。正如在上面的代码示例中所看到的,在某些情况下,当越界时,采取更大的步骤会带来更大的损失。
效率问题。 计算数值梯度的复杂度与参数的数量成线性关系。在示例中,损失函数每次执行一个参数更新,如有上万个参数则要执行上万次。这个问题只会变得更糟,因为现代神经网络可以轻易地拥有数千万个参数。显然,这种策略是不可扩展的,需要更好的策略。用微积分解析计算梯度
数值梯度是非常简单的使用有限差分近似计算,但缺点是它是近似(因为必须选择一个小 的价值,而真正的梯度定义为 趋于零的极限),而且它的计算非常昂贵的计算。
计算梯度的第二种方法是解析微积分,它允许推导一个梯度的直接公式(没有近似),计算起来也非常快。与数值梯度不同的是,它在实现时可能更容易出错,这就是为什么在实践中,计算解析梯度并将其与数值梯度进行比较,以检查实现的正确性是非常常见的。这叫做梯度检查。梯度下降
现在可以计算损失函数的梯度,重复计算梯度然后进行参数更新的过程称为梯度下降。它的普通版本如下:
# Vanilla Gradient Descent
while True:
grad = evaluate_gradient(loss_fun, data, weights)
weights += - step_size * grad # perform parameter update
这个简单的循环是所有神经网络库的核心。还有其他的优化方法(如LBFGS),但梯度下降是目前最常用和最成熟的优化神经网络损失函数的方法。
Mini-batch梯度下降法。 在大规模应用程序(如ILSVRC挑战)中,训练数据可以有数百万个示例。因此,为了只执行一个参数更新,在整个训练集上计算完整的损失函数似乎很浪费。解决这一问题的一种非常常见的方法是计算分批训练数据的梯度。例如,在当前最先进的ConvNets中,一个典型的批处理包含整个120万个训练集中的256个示例。然后使用该批处理执行参数更新。# Vanilla Minibatch Gradient Descent
while True:
data_batch = sample_training_data(data, 256) # sample 256 examples
grad = evaluate_gradient(loss_fun, data_batch, weights)
weights += - step_size * grad # perform parameter update
这种方法之所以有效,是因为训练数据中的示例是相关的。当某些样本相同的时候,将得到完全相同的损失。在实践中,数据集不会包含重复的图像,一个小批量的梯度是全目标梯度的一个很好的近似。因此,在实际应用中,通过评估小批量梯度来进行更频繁的参数更新,可以实现更快的收敛速度。
极端情况是,minibatch只包含一个示例。这个过程称为随机梯度下降(SGD)(有时也称为在线梯度下降)。这种情况比较少见,因为在实践中,由于向量化代码优化,计算100个例子的梯度比计算一个例子100次的梯度要高效得多。