跳转至

Optimization

约 6893 个字 143 行代码 预计阅读时间 36 分钟

在上一节中,我们介绍了图像分类任务中的两个关键部分:

  1. 基于参数的 评分函数。

评分函数的主要作用是将输入图像的像素映射到分类评分值,这些评分值用于判断图像属于哪个类别。最常见的评分函数是线性模型,其形式如下:

\(f(x;W,b)=Wx+b\)

  • x 是输入图像的像素值向量。
  • W 是权重矩阵,每一行对应一个类别。
  • b 是偏置向量。

这个函数会输出每个类别的评分值(即得分)。这些得分并不直接代表概率,而是用于决定哪个类别的得分最高,因而认为该图像属于该类别。

  1. 损失函数。损失函数用于评估模型的预测与实际标签之间的差异,并引导模型参数的更新。损失函数通过衡量分类评分和实际分类的一致性,来评估参数的好坏。两种常见的损失函数是 SVM LossSoftmax Loss

image-20240817154823376

SVM 损失函数的目标是最大化正确分类的得分与其他类别的得分之间的差距。SVM 损失函数通常表达为:

\[ L = \dfrac{1}{N}\sum_{i}\sum_{j!= y_i}[max(0, f(x_i, W)_j - f(x_i, W)_{y_i} + \Delta)] + \alpha R(W)\\ L_i = \sum_{j \neq y_i} \max(0, s_j - s_{y_i} + \Delta) \]

其中:

  • \(y_i\)是正确类别的索引。
  • \(s_j\)是第 \( j \) 类的得分。
  • \(\Delta\)是一个超参数,控制得分差距的容忍度。

SVM 损失函数希望正确类别的得分比其他类别的得分高至少 \(\Delta\),否则会产生损失。

Softmax 损失函数将得分转化为概率分布,并使用交叉熵来衡量预测概率与实际标签分布的差异。其表达式为:

\[ L_i = -\log \left(\dfrac{e^{s_{y_i}}}{\sum_j e^{s_j}} \right) \]
  • \(s_{y_i}\)是正确类别的得分。
  • \(\sum_j e^{s_j}\)是所有类别得分的指数和。

Softmax 损失函数希望正确类别的概率接近 1,同时其他类别的概率接近 0。

对于图像数据 \(x_i\),如果基于参数集 \(W\)做出的分类预测与真实情况比较一致,那么计算出来的损失值 \(L\)就很低。现在介绍第三个,也是最后一个关键部分:最优化Optimization。最优化是寻找能使得损失函数值最小化的参数$W$的过程。

铺垫:一旦理解了这三个部分是如何相互运作的,我们将会回到第一个部分(基于参数的函数映射),然后将其拓展为一个远比线性函数复杂的函数:首先是神经网络,然后是卷积神经网络。而损失函数和最优化过程这两个部分将会保持相对稳定。

4.1 损失函数可视化

在高维空间中,像图像分类任务中的损失函数是难以直接可视化的,因为它们涉及大量的参数。例如,对于 CIFAR-10 图像分类中的线性分类器,其权重矩阵有 30,730 个参数(对于每个类 3,073 个参数,共 10 个类)。然而,通过在低维空间中对高维损失函数进行切片,可以获取其结构的直观理解。这种方法能帮助我们了解损失函数的复杂性及其对不同参数的敏感度。

  1. 单维度的切片可视化

可以 选取一个随机方向 并沿该方向进行探索,生成一维图像。具体来说:

  • 首先,选择一个随机生成的权重矩阵 W 作为起点。
  • 然后,沿某个随机方向 \(W_1\)前进,记录损失值 \(L(W + aW_1)\)随 a 变化的情况。

左图 展示了这个过程:x 轴表示步长 a,y 轴表示对应的损失值。由于 SVM 损失函数的分段线性性质,我们会看到多个线性段连接形成的图像,反映了模型对不同权重配置的敏感度。

  1. 双维度的切片可视化

为了获得更丰富的结构信息,可以在两个维度上对损失函数进行切片:

  • 生成两个随机方向 \(W_1\)\(W_2\)
  • 计算不同 \(a, b\)组合下的损失值 \(L(W + aW_1 + bW_2)\)

中图和右图 展示了在二维平面上的切片结果。x 轴和 y 轴分别代表 a 和 b 的值,颜色代表损失值的大小。蓝色区域表示损失较低,红色区域表示损失较高。

img

一个无正则化的多类 SVM 的损失函数的图示。左边和中间只有一个样本数据,右边是 CIFAR-10 中的 100 个数据。:a 值变化在某个维度方向上对应的的损失值变化。中和右:两个维度方向上的损失值切片图,蓝色部分是低损失值区域,红色部分是高损失值区域。

当只使用一个样本进行计算(如左图和中图),损失函数具有明显的 分段线性特征,反映了 SVM 的性质。而当使用 更多样本 时(如右图),损失函数的图像变得更加平滑,因为每个样本的分段线性特征被平均化,形成了一个类似“碗状”的结构。这种平滑的“碗”结构有助于理解损失函数的最小值处于何处,即模型找到一个对所有样本都表现良好的权重配置


线性结构是由每个样本的损失值通过 max(0, -) 函数计算得出的,它将线性函数的结果与 0 进行比较,从而产生多个分段线性部分。

具体来说,对于单个数据点的损失函数 \(L_i\)而言,它是所有其他类别的得分减去正确类别得分再加上一个常数(通常为 1)的最大值的和。因此,损失函数是 W 的线性函数的总和,每个线性部分通过 max 操作被截断在零值以上。

对于一个单独的数据,有损失函数的计算公式如下:

\[ L_i = \sum_{j!= y_i}[max(0, w_j^Tx_i - w_{y_i}^Tx_i + 1)] \]

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

\[ L_0 = max(0, w^T_1x_0-w^T_0x_0+1)+max(0, w^T_2x_0-w^T_0x_0+1)\\ L_1 = max(0, w^T_0x_1-w^T_1x_1+1)+max(0, w^T_2x_1-w^T_1x_1+1)\\ L_2 = max(0, w^T_0x_2-w^T_2x_2+1)+max(0, w^T_1x_2-w^T_2x_2+1)\\ L =(L_0+L_1+L_2)/3 \]

因为这些例子都是一维的,所以数据 \(x_i\)和权重 \(w_j\)都是数字。观察 \(w_0\),可以看到上面的式子中一些项是 \(W_0\)的线性函数,且每一项都会与 0 比较,取两者的最大值。可作图如下:——————————————————————————————————————

img

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


在多维情况下,类似的分段线性结构会出现在每个维度上,从而构成高维空间中的凸形状。正因为如此,SVM 损失函数是凸函数,这使得其优化问题在数学上是可解的,并且可以应用有效的凸优化算法。但是一旦我们将函数 \(f\)扩展到神经网络,目标函数就就不再是凸函数了,图像也不会像上面那样是个碗状,而是凹凸不平的复杂地形形状。

4.2 最优化 Optimization

在凸优化中,像 SVM 损失函数这样的凸函数可以通过梯度下降或其他凸优化算法有效地找到全局最优解。然而,当我们转向神经网络时,损失函数通常是非凸的,这意味着它们可能包含多个局部极小值、鞍点以及复杂的地形。在这种情况下,标准的凸优化方法可能不再适用,我们需要更复杂的优化策略,如随机梯度下降(SGD)、动量方法、自适应学习率优化器(如 Adam)等。

随机搜索的策略:通过随机生成大量不同的权重矩阵,计算每个权重矩阵的损失值,并选取损失值最小的那个权重矩阵作为最终的模型。

# 假设 X_train 的每一列代表一个数据样本(形状为 3073 x 50000)
# 假设 Y_train 是对应的标签数组(长度为 50000)
# 假设函数 L 计算给定权重 W 的损失值

bestloss = float("inf")  # 初始化最小损失值为正无穷
for num in range(1000):  # 进行 1000 次随机搜索
    # 生成一个形状为 10x3073 的随机权重矩阵,并缩放。权重矩阵中的值由标准正态分布生成,并乘以一个很小的常数(0.0001)以确保初始权重较小
    W = np.random.randn(10, 3073) * 0.0001
    loss = L(X_train, Y_train, W)  # 计算当前权重 W 对整个训练集的损失值
    # 更新最佳损失和最佳权重
    if loss < bestloss:  # 如果当前损失值小于记录的最小损失值
        bestloss = loss  # 更新最小损失值
        bestW = W  # 更新最佳权重矩阵
    print('在第 %d 次尝试中的损失值为 %f,当前最小损失值为 %f' % (num, loss, bestloss))

# 输出示例:
# 在第 0 次尝试中的损失值为 9.401632,当前最小损失值为 9.401632
# 在第 1 次尝试中的损失值为 8.959668,当前最小损失值为 8.959668
# ... (此处省略,代码会持续输出 1000 行)

# 使用训练集上表现最好的权重矩阵 W 在测试集上进行预测
# 假设 X_test 形状为 3073 x 10000,Y_test 为长度为 10000 的标签数组
scores = bestW.dot(Xte_cols)  # 计算所有测试样本的分类得分(形状为 10 x 10000)
Yte_predict = np.argmax(scores, axis=0)  # 选择每个样本得分最高的类别作为预测结果
accuracy = np.mean(Yte_predict == Yte)  # 计算预测结果与真实标签的匹配比例(准确率)
# 返回的准确率为 0.1555

核心思路:迭代优化

  • 局限性:找到全局最优解非常困难,尤其是在参数空间维度非常高的情况下(如神经网络的权重)。
  • 改进思路将优化问题分解为一系列更小的、迭代的步骤,每一步都试图略微减少损失值,而不是试图直接找到最优解。通过逐步改进权重矩阵,损失值会逐渐减少,从而达到更好的性能。

4.2.2 策略 2:随机本地搜索

第一个策略可以看做是每走一步都尝试几个随机方向,如果某个方向是向山下的,就向该方向走一步。这个策略基于局部探索,通过微调初始随机权重矩阵来减少损失值。具体来说,从一个随机权重矩阵 \(W\)开始,然后添加一个小扰动 \(\delta W\),生成新的权重矩阵 \(W_{\text{try}} = W + \delta W\)。如果新权重的损 失值比当前最优值更小,就更新权重 \(W\)\(W_{\text{try}}\)。每次尝试随机移动一点,只在找到更低损失时更新位置。

W  = np.random.randn(10, 3073) * 0.001  # 生成一个随机的初始权重矩阵 W
bestloss = float("inf")  # 初始化最小损失值为正无穷

for i in range(1000):  # 进行 1000 次局部搜索
    step_size = 0.0001  # 设置步长,控制扰动的大小
    # 每次迭代时,生成一个与当前权重同维度的随机扰动 Wtry,并通过步长 step_size 来控制扰动的幅度。这种小幅度的扰动确保了模型在每一步中仅对参数进行微调。
    Wtry = W + np.random.randn(10, 3073) * step_size  # 在当前权重基础上添加随机扰动
    loss = L(Xtr_cols, Ytr, Wtry)  # 计算扰动后权重的损失值

    if loss < bestloss:  # 如果新的损失值比之前更小
        W = Wtry  # 更新权重矩阵为新的权重
        bestloss = loss  # 更新最小损失值

    print('第 %d 次迭代的损失值为 %f' % (i, bestloss))

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

4.2.3 策略 3:跟随梯度

在之前的策略中,我们随机寻找一个能降低损失的方向,而这里我们直接计算出最陡峭的下降方向——损失函数的 梯度。梯度告诉我们在多维空间中,各个方向上的变化率,通过沿着梯度方向更新参数,我们能够更快速地接近最优解。

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

\[ \dfrac{df(x)}{dx}= lim_{h \rightarrow 0}\dfrac{f(x+h)-f(x)}{h} \]

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

4.3 梯度计算

计算梯度有两种方法:一个是缓慢的近似方法(数值梯度法),但实现相对简单。另一个方法(分析梯度法)计算迅速,结果精确,但是实现时容易出错,且需要使用微分。现在对两种方法进行介绍:

4.3.1 数值梯度法 Numeric gradient

数值梯度法是一种通过有限差分近似计算梯度的方法。虽然相比分析梯度法它较慢,但实现简单且通用,适合用于调试和验证分析梯度的正确性。其核心思想是通过在每个维度上施加一个微小的扰动,然后观察函数值的变化,来近似计算该维度上的导数。

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

def eval_numerical_gradient(f, x):
    """
    计算函数 f 在点 x 处的数值梯度。

    参数:
    - f: 目标函数,输入为 x,输出为一个标量
    - x: numpy 数组,表示计算梯度的点

    返回:
    - grad: 与 x 形状相同的 numpy 数组,表示 f 在 x 处的梯度
    """
    fx = f(x)  # 计算函数在原点的值
    grad = np.zeros_like(x)  # 初始化与 x 形状相同的梯度数组,用于存储梯度
    h = 1e-5  # 设置一个非常小的数值,作为扰动

    # 使用 nditer 迭代 x 中的所有索引
    it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        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

迭代计算: 对于 x 中的每个维度,施加一个微小的正向扰动 h,计算扰动后函数的值 f(x + h)。通过比较原函数值 f(x) 和扰动后函数值的变化,计算该维度上的梯度。

实践考量:注意在数学公式中,h 的取值是趋近于 0 的,然而在实际中,用一个很小的数值(比如例子中的 1e-5)就足够了。在不产生数值计算出错的理想前提下,你会使用尽可能小的 h。

  • 中心差分法: 使用 f(x + h)f(x - h) 的中心差分公式来计算梯度,效果更好,可以减少误差。公式为:\(\dfrac{f(x + h) - f(x - h)}{2h}\)

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

# 定义一个封装的 CIFAR-10 损失函数,便于传递给数值梯度函数
def CIFAR10_loss_fun(W):
    return L(X_train, Y_train, W)

# 生成一个随机的权重矩阵 W
W = np.random.rand(10, 3073) * 0.001

# 计算损失函数在 W 处的梯度
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.214036
  • 梯度方向: 更新权重时,向着梯度的负方向移动,即 W_new = W - step_size * df,因为梯度指向的是损失函数上升最快的方向,向其反方向移动可以降低损失值。

  • 步长选择: 步长决定了每次更新权重时移动的距离。步长过小,下降稳定但进度慢;步长过大,可能越过最优点导致更高的损失值。

img

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

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

4.3.2 微分分析计算梯度 Analytic gradient

相比有限差值近似法,利用微分公式计算梯度更加精确且计算效率更高。我们可以直接从损失函数的公式推导出梯度的解析表达式,再用它进行优化。这种方法的优点是计算精度高、速度快;但缺点是推导复杂、容易出错。因此,实际操作中常用 梯度检查 来验证解析梯度的正确性。

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

\[ \sum_{j!= y_i}max(0, w^T_jx_i-w^T_{y_i}x_i+\Delta) \]

可以对函数进行微分。比如,对 \(w_{y_i}\)进行微分得到:

\[ \nabla {w_{y_i}}L_i =-(\sum_{j\not = y_i}1(w^T_jx_i-w^T_{y_i}x_i+Delta > 0))x_i \]

该梯度的计算涉及一个指示函数,当括号内的条件为真时,其值为 1,否则为 0。只需要计算没有满足边界值的分类的数量(因此对损失函数产生了贡献),然后乘以 \(x_i\)就是梯度了。注意,这个梯度只是对应正确分类的 W 的行向量的梯度,那些 \(j\not =y_i\)行的梯度是:

\[ \nabla_{w_j}L_i = 1(w^T_jx_i-w^T_{y_i}x_i+Delta > 0)x_i \]

4.3.3 梯度下降

梯度下降法的核心思想,即通过不断计算梯度并更新参数,使得损失函数逐渐降低。

# 普通的梯度下降
while True:
    weights_grad = evaluate_gradient(loss_fun, data, weights)  # 计算当前权重的梯度
    weights += -step_size * weights_grad  # 更新权重,沿梯度的负方向移动

梯度下降是一种迭代优化算法,其目标是找到一个使损失函数最小化的参数集合。通过沿着损失函数梯度的负方向更新参数,算法试图在每一步中尽可能地降低损失。

  • 梯度计算:在每次迭代中,通过 evaluate_gradient 函数计算损失函数相对于参数的梯度 weights_grad。梯度指向损失函数上升最快的方向。

  • 参数更新:权重 weights 按照学习率(learning rate) step_size 沿着负梯度方向更新。这样做是因为负梯度方向是损失减少最快的方向。

  • 这个循环结构会持续运行,直到满足某种停止条件。实际应用中,常见的停止条件包括:

    • 固定迭代次数:运行指定次数的迭代,例如 1000 次。
    • 损失变化小于某个阈值:当损失的变化小于设定的阈值时,认为已收敛。
    • 梯度范数很小:当梯度的范数(即梯度向量的长度)非常小时,说明已经接近极小值点。

Batch Gradient Descent 批量梯度下降

image-20240819104619162

小批量数据梯度下降(Mini-batch Gradient Descent, MBGD) 是介于批量梯度下降(Batch Gradient Descent, BGD)和随机梯度下降(Stochastic Gradient Descent, SGD)之间的一种优化策略。它在每次迭代时使用一部分(即小批量)数据来计算梯度并更新参数。这种方法结合了 BGD 和 SGD 的优点,在大规模机器学习应用中广泛使用。

# 普通的小批量数据梯度下降
while True:
    data_batch = sample_training_data(data, 256)  # 256个数据
    weights_grad = evaluate_gradient(loss_fun, data_batch, weights)  # 计算梯度
    weights += - step_size * weights_grad  # 参数更新
  • sample_training_data(data, 256):从数据集中随机选取 256 个样本,作为当前迭代的小批量数据。

  • evaluate_gradient(loss_fun, data_batch, weights):计算小批量数据上的损失函数梯度。

小批量数据梯度下降的工作原理

  • 数据批次:小批量数据的典型大小通常为 32、64、128 或 256 个样本。每次迭代时,从训练集中随机选取一个小批量数据来计算梯度。

参数数量的选择和选择参数是hyper parameter,但是影响不大

  • 梯度计算与更新:在每次迭代中,使用小批量数据计算梯度,随后更新模型参数。由于每次迭代使用的只是数据的一小部分,计算速度比全批量更新快得多,同时又比单个样本的更新更稳定。

小批量数据梯度下降的优势

  • 计算效率高:相比 BGD,MBGD 的计算开销更小,因为它不需要在每次迭代时计算整个数据集的梯度,而是从训练集中随机取出一部分来计算梯度,近似整个数据集的梯度。而与 SGD 相比,MBGD 的梯度估计更准确,减少了参数更新中的噪声。

  • 平衡收敛与计算开销:MBGD 结合了 BGD 的稳定性和 SGD 的快速收敛性,在计算成本和模型性能之间取得平衡。

小批量数据策略有个极端情况,那就是每个批量中只有 1 个数据样本,这种策略被称为 随机梯度下降(Stochastic Gradient Descent 简称 SGD),有时候也被称为在线梯度下降。这种策略在实际情况中相对少见,因为向量化操作的代码一次计算 100 个数据 比 100 次计算 1 个数据要高效很多。

小批量数据的大小是一个超参数,但是一般并不需要通过交叉验证来调参。它一般由存储器的限制来决定的,或者干脆设置为同样大小,比如 32,64,128 等。之所以使用 2 的指数,是因为在实际中许多向量化操作实现的时候,如果输入数据量是 2 的倍数,那么运算更快。并且在批量大小合适的情况下,可以充分利用计算

两个潜在问题:

  • 学习率如果过大,那么就会出现震荡的情况,学习率过小的话,虽然没有震荡情况出现,但是学习速度会很慢,这个问题在技术上有时候被称为具有高条件数的问题

image-20240819105344402

  • 存在局部最小点和鞍点 saddle point(鞍点指的是某些维度上的梯度为零,但其他维度仍需优化,使得模型无法继续向下优化),在某些维度上可能无法优化,也就是高维度下优化难题,这样模型无法达到最佳。

image-20240819105521626

或者可以使用动量法进行优化,尝试越过鞍点和局部最小点,会产生很不错的效果,同时还可以抑制震荡,是一种很有用的方法

动量法可以视为给梯度下降添加了“惯性”,使得更新方向不仅取决于当前的梯度,还受过去更新方向的影响。公式如下:

\[ v = \gamma v - \eta \nabla_\theta J(\theta)\\ \theta = \theta + v \]

其中:

  • \(\gamma\)是动量因子,通常设为 0.9。
  • \(v\)是累积的动量。
  • \(eta\)是学习率。
  • \(\nabla_\theta J(\theta)\)是当前梯度。
  • image-20240819105913771
  • image-20240819110142018

通过累积动量,模型能够跨越鞍点,并在陡峭的区域加速收敛,在平坦的区域减缓震荡。

4.3.4 AdaGrad

AdaGrad(Adaptive Gradient Algorithm)是一种自适应学习率的方法,它通过调整每个参数的学习率来提升优化的效率。与标准的梯度下降不同,AdaGrad 会根据历史梯度的累积值动态调整学习率,使得更新的幅度能够适应不同的参数。

原理:在每次迭代中,AdaGrad 会累加梯度的平方值,并将这个累积值用于调整学习率。由于梯度的平方值逐步增加,学习率会逐步减少,从而在训练过程中实现自适应调整。

grad_squared = 0
for t in range(num_steps):
    dw = compute_gradient(w)         # 计算梯度
    grad_squared += dw * dw          # 累加梯度的平方和
    w -= learning_rate * dw / (grad_squared.sqrt() + 1e-7)  # 自适应学习率更新

优点:

  • 适应稀疏数据:AdaGrad 特别适合处理稀疏数据,因为它可以根据历史梯度动态调整学习率,使得频繁更新的参数学习率逐渐减小,而不常更新的参数则保留较大的学习率。
  • 无需手动调整学习率:通过自动调节学习率,AdaGrad 减轻了手动调参的负担。

缺点:

  • 过度衰减:由于学习率随时间单调减少,AdaGrad 在训练的后期可能会导致学习率过小,进而使得模型难以进一步优化。

RMSprop 的改进:

为了解决 AdaGrad 的学习率不断下降问题,RMSprop 在计算梯度平方累积时引入了指数加权移动平均(decay rate),避免累积过多的历史信息,从而让学习率在训练过程中保持相对稳定。RMSprop 在处理非凸问题时更为有效。

grad_squared = 0
for t in range(num_steps):
    dw = compute_gradient(w)
    grad_squared = decay_date * grad_squared + 1 - decay_rate) * dw * dw
    w -= learning_rate * dw / (grad_squared.sqrt() + 1e-7)  # 自适应学习率更新

decay_rate: 这个参数控制历史梯度的衰减速度,使优化算法对新梯度信息更加敏感。常见值是 0.9。

Adam:RMSProp + Momentum

Adam 结合了 RMSprop 和动量(Momentum)的优点,不仅考虑梯度的均值,还考虑梯度的方差。Adam 通过一阶矩估计(动量)和二阶矩估计(梯度平方的指数移动平均)自适应地调整学习率,在处理非凸优化问题和噪声数据时非常稳健,并且通常不需要过多的超参数调整。

moment1 = 0  # 初始化一阶动量
moment2 = 0  # 初始化二阶动量
for t in range(num_steps):
    dw = compute_gradient(w)  # 计算当前参数的梯度
    moment1 = beta1 * moment1 + (1 - beta1) * dw  # 更新一阶动量(类似于动量)
    moment2 = beta2 * moment2 + (1 - beta2) * dw * dw  # 更新二阶动量(类似于RMSprop)
    w -= learning_rate * moment1 / (moment2.sqrt() + 1e-7)  # 使用自适应学习率更新参数
  • 一阶动量 (moment1): moment1 是梯度的指数加权移动平均,类似于动量优化中的累积梯度,旨在平滑梯度更新。beta1 控制着一阶动量的衰减率,通常取值为 0.9。

  • 二阶动量 (moment2): moment2 是梯度平方的指数加权移动平均,用于调整学习率。beta2 控制着二阶动量的衰减率,通常取值为 0.999。这可以看作是 RMSprop 中的均方根移动平均。

在 Adam 中,一般还需要对一阶和二阶动量进行偏差修正,特别是在前几次迭代时。偏差修正可以提高算法的收敛性能。

moment1 = 0
moment2 = 0
for t in range(1, num_steps + 1):
    dw = compute_gradient(w)
    moment1 = beta1 * moment1 + (1 - beta1) * dw
    moment2 = beta2 * moment2 + (1 - beta2) * dw * dw

    # 偏差修正
    m1_hat = moment1 / (1 - beta1**t)
    m2_hat = moment2 / (1 - beta2**t)

    w -= learning_rate * m1_hat / (m2_hat.sqrt() + 1e-7)

这种偏差修正的目的是减少初始阶段估计值的偏差,尤其是在迭代次数较少的情况下。

image-20240819112958840

4.4 小结


img

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


在本节课中:

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

通过迭代优化,从随机初始权重出发,逐步降低损失值。

  • 梯度表示函数在当前点最陡峭的上升方向,通过反向操作来更新权重,使得损失最小化。介绍了利用有限的差值来近似计算梯度的方法,该方法实现简单但是效率较低(有限差值就是h,用来计算数值梯度)。

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

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

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

预告:这节课的核心内容是:理解并能计算损失函数关于权重的梯度,是设计、训练和理解神经网络的核心能力。下节中,将介绍如何使用链式法则来高效地计算梯度,也就是通常所说的 反向传播( backpropagation)机制。该机制能够对包含卷积神经网络在内的几乎所有类型的神经网络的损失函数进行高效的最优化。