学习率衰减优化方法(理解)

手动调整学习率的策略相对前面梯度下降优化算法自动调整的方式,比较少用。原因是实际工作神经网络的结构是比较复杂,手动很难调整到比较好的状态

为什么要进行学习率优化

在训练神经网络时,一般情况下学习率都会随着训练而变化。这主要是由于,在神经网络训练的后期,如果学习率过高,会造成loss的振荡,但是如果学习率减小的过慢,又会造成收敛变慢的情况。

展开查看更多

运行下面代码,观察学习率设置不同对网络训练的影响:

观察不同学习率对训练的影响
"""
案例:
    通过代码 观察不同的学习率 对 参数更新 的影响.
"""

import torch
import matplotlib.pyplot as plt

# x看成是权重,y看成是loss,下面通过代码来理解学习率的作用
def func(x_t):
    return torch.pow(2*x_t, 2)  # y = 4 x ^2

# 采用较小的学习率,梯度下降的速度慢
# 采用较大的学习率,梯度下降太快越过了最小值点,导致不收敛,甚至震荡
def dm01(lr=0.3):

    x = torch.tensor([2.], requires_grad=True)
    # 记录loss迭代次数,画曲线
    iter_rec, loss_rec, x_rec = list(), list(), list()

    # 实验学习率: 0.01 0.02 0.03 0.1 0.2 0.3 0.4
    # lr = 0.1    # 正常的梯度下降
    # lr = 0.125      # 当学习率设置0.125 一下子求出一个最优解
                    # x=0 y=0 在x=0处梯度等于0 x的值x=x-lr*x.grad就不用更新了
                    # 后续再多少次迭代 都固定在最优点

    # lr = 0.3      # x从2.0一下子跨过0点,到了左侧负数区域
    # lr = 0.3      # 梯度越来越大 梯度爆炸
    max_iteration = 40
    for i in range(max_iteration):
        y = func(x)   # 得出loss值
        y.backward()  # 计算x的梯度
        print("Iter:{}, X:{:8}, X.grad:{:8}, loss:{:10}".format(
            i, x.detach().numpy()[0], x.grad.detach().numpy()[0], y.item()))
        x_rec.append(x.item())      # 梯度下降点 列表
        # 更新参数
        x.data.sub_(lr * x.grad)    # x = x - x.grad
        x.grad.zero_()
        iter_rec.append(i)          # 迭代次数 列表
        loss_rec.append(y.item())  # 损失值 列表,这里将y改为y.item()以获取标量值
    # 迭代次数-损失值 关系图
    plt.subplot(121).plot(iter_rec, loss_rec, '-ro')
    plt.grid()
    plt.xlabel("Iteration X")
    plt.ylabel("Loss value Y")
    # 函数曲线-下降轨迹 显示图
    x_t = torch.linspace(-3, 3, 100)
    y = func(x_t)
    plt.subplot(122).plot(x_t.detach().numpy(), y.detach().numpy(), label="y = 4*x^2")
    y_rec = [func(torch.tensor(i)).item() for i in x_rec]
    print('x_rec--->', x_rec)
    print('y_rec--->', y_rec)
    # 指定线的颜色和样式(-ro:红色圆圈,b-:蓝色实线等)
    plt.subplot(122).plot(x_rec, y_rec, '-ro')
    plt.grid()
    plt.legend()
    plt.show()

if __name__ == '__main__':
    # dm01(0.01) # 学习率过小,梯度下降缓慢,需要迭代计算多次
    # dm01(0.1)
    dm01(0.2) # 学习率设置的不合理,会出现梯度震荡
    # dm01(0.3) # 学习率过大,会出现梯度爆炸

运行效果图如下:

可以看出:采用较小的学习率,梯度下降的速度慢;采用较大的学习率,梯度下降太快越过了最小值点,导致震荡,甚至不收敛(梯度爆炸)。

image-20220313172050673

等间隔学习率衰减

等间隔学习率衰减方式如下所示:

image-20220313172716591

在PyTorch中实现时使用:

#   step_size:调整间隔数=50
#   gamma:调整系数=0.5
#   调整方式:lr = lr * gamma
optim.lr_scheduler.StepLR(optimizer, step_size, gamma=0.5)

具体使用方式如下:

StepLR 使用示例
import matplotlib.pyplot as plt
import torch
import torch.nn

def demo():
    # 定义真实值、特征值、权重值
    y_true = torch.tensor([0])
    x = torch.tensor([1.0],dtype=torch.float32)
    w = torch.tensor([1.0],requires_grad=True,dtype=torch.float32)

    # 梯度下降优化器
    optimizer = torch.optim.SGD([w],lr=0.1,momentum=0.9)

    # 学习率等间隔下降策略
    """
        step_size:每隔多少个轮次更新一次学习率
        gamma:学习率的更新系数
    """
    scheduler = torch.optim.lr_scheduler.StepLR(optimizer,step_size=50,gamma=0.5)

    # 训练的总轮次
    epochs = 200

    iteration = 10

    epoch_list = [] # 记录循环到哪个轮次了
    lr_list = [] # 记录对应轮次的学习率
    for epoch in range(epochs):
        for i in range(iteration):
            # 获得预测值
            y_pred = w*x

            # 计算损失值
            loss_value = (y_pred-y_true)**2

            # 梯度清零、反向传播、更新权重
            optimizer.zero_grad()
            loss_value.sum().backward()
            optimizer.step()

        # 记录对应轮次和对应的学习率
        epoch_list.append(epoch)
        lr_list.append(scheduler.get_last_lr())
        print(f"第{epoch}轮次,学习率是多少{scheduler.get_last_lr()}")
        # 按照等间隔(epoch的等间隔)更新学习率
        scheduler.step()

    # 绘制折线图
    plt.plot(epoch_list,lr_list)
    plt.xlabel("Epoch")
    plt.ylabel("lr")
    plt.show()


if __name__ == '__main__':
    demo()

指定间隔学习率衰减

指定间隔学习率衰减的效果如下:

image-20220313172915819

在PyTorch中实现时使用:

# milestones:设定调整轮次:[50, 125, 160]
# gamma:调整系数
# 调整方式:lr = lr * gamma
optim.lr_scheduler.MultiStepLR(optimizer, milestones, gamma=0.1)    

具体使用方式如下所示:

MultiStepLR 使用示例
# 导包
import torch
import torch.optim as optim
import matplotlib.pyplot as plt

# 2. 定义函数, 演示: 如何实现 指定间隔学习率优化.
def dm02():
    # 1. 定义变量, 记录: 训练的总轮数, 初始学习率, 每轮的迭代器个数.
    epochs, lr, iteration = 200, 0.1, 10

    # 2. 定义变量, 记录: 真实值.
    y_true = torch.tensor([0], dtype=torch.float32)

    # 3. 定义变量, 记录: w(权重), x(特征)
    x = torch.tensor([1.0], dtype=torch.float32)
    w = torch.tensor([1.0], dtype=torch.float32, requires_grad= True)

    # 4. 创建优化器对象, 采用: 动量法.
    optimizer = optim.SGD([w], lr=lr, momentum=0.9)
    # 5. 创建 学习率衰减优化对象.
    # 思路1: 等间隔学习率衰减优化对象, 设置 50轮/次, 更新学习率.
    # 参1: 优化器对象.   参2: 衰减的轮数间隔.  参3: 衰减系数, 即: lr新 = lr旧 * gamma
    # scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=50, gamma=0.5)   # [0.1, 0.05, 0.0125...]

    # 思路2: 指定间隔学习率衰减优化对象, 设置第50, 125, 160轮 更新学习率.
    # milestones = [24, 50, 60, 90, 125, 130, 160]
    milestones = [50, 125, 160]
    # 参1: 优化器对象.   参2: 衰减的(指定)轮数.  参3: 衰减系数, 即: lr新 = lr旧 * gamma
    scheduler = optim.lr_scheduler.MultiStepLR(optimizer, milestones=milestones, gamma=0.5)

    # 6. 定义列表, 分别记录: 训练的轮数, 该轮的学习率.
    epoch_list, lr_list = [], []

    # 7. 具体的 每轮 训练过程
    # 7.1 逐轮训练.
    for epoch in range(epochs):     # epoch: 0 ~ 200, 包左不包右
        # 7.2 添加轮数, 以及本轮的 学习率
        epoch_list.append(epoch)
        lr_list.append(scheduler.get_last_lr())

        # 7.3 每轮的训练过程, 即: 每轮有 10个 迭代器.
        for i in range(iteration):
            # 7.4 计算预测值.
            y_pred = w * x
            # 7.5 计算损失值.
            loss = (y_pred - y_true) ** 2
            # 7.6 反向传播.
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

        # 7.7 走这里,说明: 本轮(一轮)训练完成. 更新本轮的学习率.
        scheduler.step()

    # 8. 打印下: 每轮的 学习率
    print(f'epoch_list: {epoch_list}')
    print(f'lr_list: {lr_list}')

    # 9. 画图, 绘制: 训练的轮数 以及 每轮的 学习率
    plt.plot(epoch_list, lr_list)
    plt.xlabel('Epoch')
    plt.ylabel('Learning Rate')
    plt.show()


# 测试
if __name__ == '__main__':
    dm02()

按指数学习率衰减

按指数衰减调整学习率的效果如下:

image-20220313173110595

在PyTorch中实现时使用:

# gamma:指数的底
# 调整方式
# lr= lr∗gamma^epoch
optim.lr_scheduler.ExponentialLR(optimizer, gamma)

具体使用方式如下所示:

ExponentialLR 使用示例
"""
案例:
    演示 学习率衰减优化方式.

背景:
    之前学的 AdaGrad, RMSProp, Adam这些梯度下降优化方式, 都可以修改 学习率, 但是底层无法人为精准控制, 例如: *轮 更新一次, 制定轮更新等...
    针对于此, PyTorch中加入了 学习率衰减优化方式, 可以人为的控制学习率的变化.

方式:
    1. 等间隔学习率优化.
        间隔制定的轮数, 优化(更新)学习率.
        lr新 = lr旧 * gamma
    2. 指定间隔学习率优化.
        设置指定的轮数, 当达到指定的轮数后, 即可自动优化 学习率.
        lr新 = lr旧 * gamma
    3. 指数学习率优化.
        lr新 = lr旧 * gamma ^ epoch
"""

# 导包
import torch
import torch.optim as optim
import matplotlib.pyplot as plt

# 3. 定义函数, 演示: 如何实现 指数学习率优化.
def dm03():
    # 1. 定义变量, 记录: 训练的总轮数, 初始学习率, 每轮的迭代器个数.
    epochs, lr, iteration = 200, 0.1, 10

    # 2. 定义变量, 记录: 真实值.
    y_true = torch.tensor([0], dtype=torch.float32)

    # 3. 定义变量, 记录: w(权重), x(特征)
    x = torch.tensor([1.0], dtype=torch.float32)
    w = torch.tensor([1.0], dtype=torch.float32, requires_grad= True)

    # 4. 创建优化器对象, 采用: 动量法.
    optimizer = optim.SGD([w], lr=lr, momentum=0.9)
    # 5. 创建 学习率衰减优化对象.
    # 思路1: 等间隔学习率衰减优化对象, 设置 50轮/次, 更新学习率.
    # 参1: 优化器对象.   参2: 衰减的轮数间隔.  参3: 衰减系数, 即: lr新 = lr旧 * gamma
    # scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=50, gamma=0.5)   # [0.1, 0.05, 0.0125...]

    # 思路2: 指定间隔学习率衰减优化对象, 设置第50, 125, 160轮 更新学习率.
    # milestones = [24, 50, 60, 90, 125, 130, 160]
    # milestones = [50, 125, 160]
    # 参1: 优化器对象.   参2: 衰减的(指定)轮数.  参3: 衰减系数, 即: lr新 = lr旧 * gamma
    # scheduler = optim.lr_scheduler.MultiStepLR(optimizer, milestones=milestones, gamma=0.5)

    # 思路3: 指数衰减学习率衰减优化对象.  计算公式: lr新 = lr旧 * gamma ^ epoch
    scheduler = optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.9)

    # 6. 定义列表, 分别记录: 训练的轮数, 该轮的学习率.
    epoch_list, lr_list = [], []

    # 7. 具体的 每轮 训练过程
    # 7.1 逐轮训练.
    for epoch in range(epochs):     # epoch: 0 ~ 200, 包左不包右
        # 7.2 添加轮数, 以及本轮的 学习率
        epoch_list.append(epoch)
        lr_list.append(scheduler.get_last_lr())

        # 7.3 每轮的训练过程, 即: 每轮有 10个 迭代器.
        for i in range(iteration):
            # 7.4 计算预测值.
            y_pred = w * x
            # 7.5 计算损失值.
            loss = (y_pred - y_true) ** 2
            # 7.6 反向传播.
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

        # 7.7 走这里,说明: 本轮(一轮)训练完成. 更新本轮的学习率.
        scheduler.step()

    # 8. 打印下: 每轮的 学习率
    print(f'epoch_list: {epoch_list}')
    print(f'lr_list: {lr_list}')

    # 9. 画图, 绘制: 训练的轮数 以及 每轮的 学习率
    plt.plot(epoch_list, lr_list)
    plt.xlabel('Epoch')
    plt.ylabel('Learning Rate')
    plt.show()


# 测试
if __name__ == '__main__':
    dm03()

小结

方法 等间隔学习率衰减 (Step Decay) 指定间隔学习率衰减 (Exponential Decay) 指数学习率衰减 (Exponential Moving Average Decay)
衰减方式 固定步长衰减 指定步长衰减 平滑指数衰减,历史平均考虑
实现难度 简单易实现 相对简单,容易调整 需要额外历史计算,较复杂
适用场景 大型数据集、较为简单的任务 对训练平稳性要求较高的任务 高精度训练,避免过快收敛
优点 直观,易于调试,适用于大批量数据 易于调试,稳定训练过程 平滑且考虑历史更新,收敛稳定性较强
缺点 学习率变化较大,可能跳过最优点 在某些情况下可能衰减过快,导致优化提前停滞 超参数调节较为复杂,可能需要更多的计算资源