神经网络优化方法

多层神经网络的学习能力比单层网络强得多,但也更难训练。实际中最常用的方法就是 BP(反向传播)

它的过程可以理解成两步:

  1. 正向传播:数据从输入层一路传到输出层,算出预测值,再和真实值比较,得到误差。
  2. 反向传播:根据误差从后往前计算各层参数的梯度,再更新权重和偏置,让预测越来越接近真实值。

反向传播(BP算法)

通过反向传播算法计算出损失函数对所有参数的梯度,然后优化器根据这些梯度,通过梯度下降规则来更新网络参数,从而逐步最小化损失函数。

展开查看更多

反向传播概念

前向传播:指的是数据输入到神经网络中,逐层向前传输,一直运算到输出层为止。

反向传播(Back Propagation):利用损失函数ERROR值,从后往前,结合梯度下降算法,依次求各个参数的偏导,并进行参数更新。

在网络的训练过程中经过前向传播后得到的最终结果跟训练样本的真实值总是存在一定误差,这个误差便是损失函数 ERROR。想要减小这个误差,就用损失函数 ERROR,从后往前,依次求各个参数的偏导,这就是反向传播(Back Propagation)

反向传播详解

反向传播算法利用链式法则对神经网络中的各个节点的权重进行更新

【举个栗子🌰:】

如下图是一个简单的神经网络用来举例:激活函数为sigmoid

image-20210129103846076

前向传播运算

image-20210129104612230

接下来是反向传播(求网络误差对各个权重参数的梯度):

我们先来求最简单的,求误差E对w5的导数。首先明确这是一个链式法则的求导过程,要求误差E对w5的导数,需要先求误差E对$out_{o1}$的导数,再求$out_{o1}$对$net_{o1}$的导数,最后再求$net_{o1}$对$w_5$的导数,经过这个链式法则,我们就可以求出误差E对$w_5$的导数(偏导),如下图所示:

image-20210129104646474

导数(梯度)已经计算出来了,下面就是反向传播与参数更新过程

image-20210129104718140

如果要想求误差E对w1的导数,误差E对w1的求导路径不止一条,这会稍微复杂一点,但换汤不换药,计算过程如下所示:

image-20210129104756053

至此,反向传播算法的过程就讲完了啦!

梯度下降优化方法【掌握】

image-20260201164804610

BP 负责算梯度,梯度下降负责用梯度更新参数。可以把损失函数看成一个“碗”,模型每次都沿着负梯度方向往下走,目标就是让损失越来越小。

几个涉及的基础概念

这里还有 3 个基础概念:

  1. Epoch:把整个训练集完整训练 1 遍
  2. Batch:每次送进模型的一小批样本
  3. Iteration:模型用 1 个 Batch 完成 1 次参数更新

例如:训练集有 50000 条数据,Batch Size = 256

  • 1 个 Epoch 要看完 50000 条数据
  • 1 个 Epoch 大约有 196Batch
  • 所以 1 个 Epoch 也大约有 196Iteration
  • 训练 10Epoch,大约就是 1960Iteration

学习率 η 太小,下降会很慢;太大,又可能直接跨过最优点。不同梯度下降方式的一个核心区别,就是每次用多少样本来算梯度,也就是 Batch Size 不同。

梯度下降优化算法中,可能会碰到以下情况:

  • 碰到平缓区域,梯度值较小,参数优化变慢
  • 碰到 “鞍点” ,梯度为0,参数无法优化
  • 碰到局部最小值,参数不是最优

对于这些问题, 出现了一些对梯度下降算法的优化方法,例如:MomentumAdaGradRMSpropAdam

指数加权平均

算术平均是每个值权重都一样;移动平均只看最近一段数据;指数加权平均则是越近的数据权重越大,越远的数据权重越小

所以,指数移动加权平均本质上就是一种更看重“最近变化”的平均方式。

展开查看更多

比如:明天气温怎么样,和昨天气温有很大关系,而和一个月前的气温关系就小一些。

计算公式可以用下面的式子来表示: $ S_t= \begin{cases} Y_1, & t=0 \\ \beta \cdot S_{t-1} + (1-\beta)\cdot Y_t, & t>0 \end{cases} $

  • $S_t$ 表示指数加权平均值;
  • $Y_t$ 表示t时刻的值;
  • β 调节权重系数,该值越大平均数越平缓。

第100天的指数加权平均值为:

1735177998337

下面通过代码来看结果,随机产生 30 天的气温数据:

指数加权平均演示代码
import torch
import matplotlib.pyplot as plt


ELEMENT_NUMBER = 30


# 1. 实际平均温度
def test01():

    # 固定随机数种子
    torch.manual_seed(0)
    # 产生30天的随机温度
    temperature = torch.randn(size=[ELEMENT_NUMBER,]) * 10
    print(temperature)
    # 绘制平均温度
    days = torch.arange(1, ELEMENT_NUMBER + 1, 1)
    plt.plot(days, temperature, color='r')
    plt.scatter(days, temperature)
    plt.show()


# 2. 指数加权平均温度
def test02(beta=0.9):
    
    # 固定随机数种子
    torch.manual_seed(0)
    # 产生30天的随机温度
    temperature = torch.randn(size=[ELEMENT_NUMBER,]) * 10
    print(temperature)

    exp_weight_avg = []
    # idx从1开始
    for idx, temp in enumerate(temperature, 1):
        # 第一个元素的 EWA 值等于自身
        if idx == 1:
            exp_weight_avg.append(temp)
            continue
        # 第二个元素的 EWA 值等于上一个 EWA 乘以 β + 当前气温乘以 (1-β)
        # idx-2:2-2=0,exp_weight_avg列表中第一个值的下标值
        new_temp = exp_weight_avg[idx - 2] * beta + (1 - beta) * temp
        exp_weight_avg.append(new_temp)

    days = torch.arange(1, ELEMENT_NUMBER + 1, 1)
    plt.plot(days, exp_weight_avg, color='r')
    plt.scatter(days, temperature)
    plt.show()


if __name__ == '__main__':
    test01()
    test02(0.5)
    test02(0.9)

1734144867552

从程序运行结果可以看到:

  • 指数加权平均绘制出的气温变化曲线更加平缓
  • β 的值越大,则绘制出的折线越加平缓,波动越小(1-β越小,t时刻的$S_t$越不依赖$Y_t$的值)
  • β 值一般默认都是 0.9

动量算法Momentum

当梯度下降碰到 “峡谷” 、”平缓”、”鞍点” 区域时, 参数更新速度变慢。 Momentum 通过指数加权平均法,累计历史梯度值,进行参数更新,越近的梯度值对当前参数更新的重要性越大。

    # 创建优化器对象,作用:1- 计算梯度;2- 自动更新权重
    optimizer = torch.optim.SGD([w],lr=0.1,momentum=0.9)
展开查看更多

image-20260201165617920

梯度计算公式

​ $s_t=βs_{t−1}+(1−β)g_t$

参数更新公式

​ $w_t=w_{t−1}−ηs_t$

$s_t$是当前时刻指数加权平均梯度值

$s_{t-1}$是历史指数加权平均梯度值

$g_t$是当前时刻的梯度值

β 是调节权重系数,通常取 0.9 或 0.99

η是学习率

$w_t$是当前时刻模型权重参数

image-20250814095850665

咱们举个例子假设权重 β 为 0.9例如
第一次梯度值s1 = g1 = w1 
第二次梯度值s2 = 0.9*s1 + g2*0.1 
第三次梯度值s3 = 0.9*s2 + g3*0.1 
第四次梯度值s4 = 0.9*s3 + g4*0.1 
1. w 表示初始梯度
2. g 表示当前轮数计算出的梯度值
3. s 表示历史梯度移动加权平均值

梯度下降公式中梯度的计算就不再是当前时刻t的梯度值而是历史梯度值的指数移动加权平均值
公式修改为
Wt = Wt-1 - η*St
Wt当前时刻模型权重参数
St当前时刻指数加权平均梯度值
η学习率

Monmentum 优化方法是如何一定程度上克服 “平缓”、”鞍点”、”峡谷” 的问题呢?

  • 当处于鞍点位置时,由于当前的梯度为 0,参数无法更新。但是 Momentum 动量梯度下降算法已经在先前积累了一些梯度值,很有可能使得跨过鞍点。
  • 由于 mini-batch 普通的梯度下降算法,每次选取少数的样本梯度确定前进方向,可能会出现震荡,使得训练时间变长。Momentum 使用移动加权平均,平滑了梯度的变化,使得前进方向更加平缓,有利于加快训练过程。一定程度上有利于降低 “峡谷” 问题的影响。

在pytorch中动量梯度优化法编程实践如下:

import torch
import torch.nn

def momentum_demo():
    # 初始化权重值
    w = torch.tensor([1.0],requires_grad=True,dtype=torch.float32)

    # 自定义损失函数
    criterion = (w**2)/2

    # 创建优化器对象,作用:1- 计算梯度;2- 自动更新权重
    optimizer = torch.optim.SGD([w],lr=0.1,momentum=0.9)

    # 反向传播计算梯度
    # 梯度清零
    optimizer.zero_grad()
    # 反向传播
    criterion.sum().backward()
    # 更新权重
    optimizer.step()
    print(f"第1次,梯度的结果{w.grad}")

    # 第二次
    criterion = (w ** 2) / 2
    # 反向传播计算梯度
    # 梯度清零
    optimizer.zero_grad()
    # 反向传播
    criterion.sum().backward()
    # 更新权重
    optimizer.step()
    print(f"第2次,梯度的结果{w.grad}")

if __name__ == '__main__':
    # 动量法
    momentum_demo()

AdaGrad

AdaGrad 通过对不同的参数分量使用不同的学习率,AdaGrad 的学习率总体会逐渐减小,这是因为 AdaGrad 认为:在起初时,我们距离最优目标仍较远,可以使用较大的学习率,加快训练速度,随着迭代次数的增加,学习率逐渐下降。

    # 2 实例化优化方法:adagrad优化方法
    optimizer = torch.optim.Adagrad([w], lr=0.01)
展开查看详情

其计算步骤如下:

  1. 初始化学习率 η、初始化参数w、小常数 σ = 1e-10

  2. 初始化梯度累计变量 s = 0

  3. 从训练集中采样 m 个样本的小批量,计算梯度$g_t$

  4. 累积平方梯度: $s_t$ = $s_{t-1}$ + $g_t$ ⊙ $g_t$,⊙ 表示各个分量相乘

  5. 学习率 η 的计算公式如下:

    ​ η = $η\over\sqrt{s_t}+σ$

  6. 权重参数更新公式如下:

    ​ $w_t$ = $w_{t-1}$ - $η\over\sqrt{s_t}+σ$ * $g_t$

  7. 重复 3-7 步骤

AdaGrad 缺点是可能会使得学习率过早、过量的降低,导致模型训练后期学习率太小,较难找到最优解。

在PyTorch中AdaGrad优化法编程实践如下:

def test02():
    # 1 初始化权重参数
    w = torch.tensor([1.0], requires_grad=True, dtype=torch.float32)
    loss = ((w ** 2) / 2.0).sum()
    # 2 实例化优化方法:adagrad优化方法
    optimizer = torch.optim.Adagrad([w], lr=0.01)
    # 3 第1次更新 计算梯度,并对参数进行更新
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    print('第1次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))
    # 4 第2次更新 计算梯度,并对参数进行更新
    # 使用更新后的参数机选输出结果
    loss = ((w ** 2) / 2.0).sum()
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    print('第2次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))

结果显示:

第1次: 梯度w.grad: 1.000000, 更新后的权重:0.990000
第2次: 梯度w.grad: 0.990000, 更新后的权重:0.982965

RMSProp

RMSProp 优化算法是对 AdaGrad 的优化。最主要的不同是,其使用指数加权平均梯度替换历史梯度的平方和。

    # 2 实例化优化方法:RMSprop算法,其中alpha对应beta
    optimizer = torch.optim.RMSprop([w], lr=0.01, alpha=0.9)
展开查看详情

其计算过程如下:

  1. 初始化学习率 η、初始化权重参数w、小常数 σ = 1e-10

  2. 初始化梯度累计变量 s = 0

  3. 从训练集中采样 m 个样本的小批量,计算梯度 $g_t$

  4. 使用指数加权平均累计历史梯度,⊙ 表示各个分量相乘,公式如下:

    ​ $s_t$ = β$s_{t-1}$ + (1-β)$g_t$⊙$g_t$

  5. 学习率 η 的计算公式如下:

    ​ η = $η\over\sqrt{s_t}+σ$

  6. 权重参数更新公式如下:

    ​ $w_t$ = $w_{t-1}$ - $η\over\sqrt{s_t}+σ$ * $g_t$

  7. 重复 3-7 步骤

RMSProp 与 AdaGrad 最大的区别是对梯度的累积方式不同,对于每个梯度分量仍然使用不同的学习率。

RMSProp 通过引入衰减系数β,控制历史梯度对历史梯度信息获取的多少. 被证明在神经网络非凸条件下的优化更好,学习率衰减更加合理一些。

需要注意的是:AdaGrad 和 RMSProp 都是对于不同的参数分量使用不同的学习率,如果某个参数分量的梯度值较大,则对应的学习率就会较小,如果某个参数分量的梯度较小,则对应的学习率就会较大一些。

在PyTorch中RMSprop梯度优化法,编程实践如下:

def test03():
    # 1 初始化权重参数
    w = torch.tensor([1.0], requires_grad=True, dtype=torch.float32)
    loss = ((w ** 2) / 2.0).sum()
    # 2 实例化优化方法:RMSprop算法,其中alpha对应beta
    optimizer = torch.optim.RMSprop([w], lr=0.01, alpha=0.9)
    # 3 第1次更新 计算梯度,并对参数进行更新
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    print('第1次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))
    # 4 第2次更新 计算梯度,并对参数进行更新
    # 使用更新后的参数机选输出结果
    loss = ((w ** 2) / 2.0).sum()
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    print('第2次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))

结果显示:

第1次: 梯度w.grad: 1.000000, 更新后的权重:0.968377
第2次: 梯度w.grad: 0.968377, 更新后的权重:0.945788

Adam

  • Momentum:用指数加权平均来平滑梯度
  • AdaGradRMSProp:根据梯度变化自动调整学习率
  • Adam:可以看成是把前两类思路结合起来
    • 一边用指数加权平均修正梯度
    • 一边根据梯度平方的指数加权平均动态调整学习率

所以,Adam 本质上就是一种同时“修正梯度 + 自适应调学习率”的优化方法

    # 2 实例化优化方法:Adam算法,其中betas是指数加权的系数
    optimizer = torch.optim.Adam([w], lr=0.01, betas=[0.9, 0.99])
展开查看详情
  • 梯度计算公式

    ​ $m_t=β_1m_{t−1}+(1−β_1)g_t$

    ​ $s_t=β_2s_{t−1}+(1−β_2)gt^2$

    ​ $\hat{m_t}$ = $m_t\over1−β_1^t$, $\hat{s_t}$=$s_t\over1−β_2^t$

  • 权重参数更新公式:

    ​ $w_t$ = $w_{t−1}$ − $η\over\sqrt{\hat{s_t}}+ϵ$$\hat{m_t}$

其中,$m_t$ 是梯度的一阶矩估计,$s_t$ 是梯度的二阶矩估计,$ \hat{m_t}$和 $\hat{s_t}$ 是偏差校正后的估计。

在PyTroch中,Adam梯度优化法编程实践如下:

def test04():
    # 1 初始化权重参数
    w = torch.tensor([1.0], requires_grad=True)
    loss = ((w ** 2) / 2.0).sum()
    # 2 实例化优化方法:Adam算法,其中betas是指数加权的系数
    optimizer = torch.optim.Adam([w], lr=0.01, betas=[0.9, 0.99])
    # 3 第1次更新 计算梯度,并对参数进行更新
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    print('第1次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))
    # 4 第2次更新 计算梯度,并对参数进行更新
    # 使用更新后的参数机选输出结果
    loss = ((w ** 2) / 2.0).sum()
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    print('第2次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))

结果显示:

第1次: 梯度w.grad: 1.000000, 更新后的权重:0.990000 
第2次: 梯度w.grad: 0.990000, 更新后的权重:0.980003

小结

展开查看详情
优化算法 优点 缺点 适用场景
SGD 简单、容易实现。 收敛速度较慢,容易震荡,特别是在复杂问题中。 用于简单任务,或者当数据特征分布相对稳定时。
Momentum 可以加速收敛,减少震荡,特别是在高曲率区域。 需要手动调整动量超参数,可能会在小步长训练中过度更新。 用于非平稳优化问题,尤其是深度学习中的应用。
AdaGrad 自适应调整学习率,适用于稀疏数据。 学习率会在训练过程中逐渐衰减,可能导致早期停滞。 适合稀疏数据,如 NLP 或推荐系统中的特征。
RMSProp 解决了 AdaGrad 学习率过早衰减的问题,适应性强。 需要选择合适的超参数,更新可能会过于激进。 适用于动态问题、非平稳目标函数,如深度学习训练。
Adam 结合了 Momentum 和 RMSProp 的优点,适应性强且稳定。 需要调节更多的超参数,训练过程中可能会产生较大波动。 广泛适用于各种深度学习任务,特别是非平稳和复杂问题。
  • 简单任务和较小的模型:SGD 或 Momentum
  • 复杂任务或有大量数据:Adam 是最常用的选择,因其在大部分任务上都表现优秀
  • 需要处理稀疏数据或文本数据:Adagrad 或 RMSProp