pytorch 15.2 学习率调度在PyTorch中的实现方法

发布于:2025-05-29 ⋅ 阅读:(21) ⋅ 点赞:(0)


  学习率调度作为模型优化的重要方法,也集成在了PyTorch的 optim模块中。我们可以通过下述代码将学习率调度模块进行导入。

from torch.optim import lr_scheduler

  接下来,我们从较为基础的学习率调度方法入手,熟悉PyTorch中实现学习率调度的基本思路与流程。

一、优化器与状态字典(state_dict)

  在此前的模型训练过程中,我们已经基本了解了PyTorch中的模型优化器的基本使用方法。模型优化器是求解损失函数的函数,其中包含了模型训练的诸多关键信息,包括模型参数、模型学习率等,同时在进行模型训练时,我们也是通过优化器调整模型参数、归零模型梯度。
  而在学习率调度过程中,由于我们需要动态调整学习率,而学习率又是通过传入优化器进而影响模型训练的,因此在利用PyTorch进行学习率调度的时候,核心需要考虑的问题是如何让优化器内的学习率随着迭代次数增加而不断变化
  为做到这一点,首先我们需要补充关于优化器状态字典内容。

1.1 优化器相关参数介绍

# 设置随机数种子
torch.manual_seed(420)  
# 创建最高项为2的多项式回归数据集
features, labels = tensorGenReg(w=[2, -1, 3, 1, 2], bias=False, deg=2)
# 进行数据集切分与加载
train_loader, test_loader = split_loader(features, labels, batch_size=50)

# 设置随机数种子
torch.manual_seed(24)  
# 实例化模型  
tanh_model1 = net_class2(act_fun= torch.tanh, in_features=5, BN_model='pre')
# 创建优化器
optimizer = torch.optim.SGD(tanh_model1.parameters(), lr=0.01)
# 查看优化器状态
optimizer.state_dict()

在这里插入图片描述

在优化器创建完成之后,我们可以使用.state_dict()方法查看优化器状态。该方法会返回一个包含优化器核心信息的字典,目前为止该字典包含两个元素,第一个是优化器状态(state),第二个是优化器相关参数簇(param_groups)。
(1)学习率 lr
其中,目前为止核心需要关注的是参数簇中的lr对象,该对象代表着下一次模型训练的时候所带入的学习率。当然,我们可以通过如下方法提取lr对应的value

optimizer.state_dict()['param_groups']
optimizer.state_dict()['param_groups'][0]
optimizer.state_dict()['param_groups'][0]['lr']

在这里插入图片描述

参数簇中其他参数包括动量系数、特征权重、是否采用牛顿法及待训练参数索引

(2)模型参数索引
另外,params表示训练参数个数(其中一个矩阵算作一个参数),可以通过如下方式进行简单验证。

list(tanh_model1.parameters())
# 验证带训练参数个数
len(list(tanh_model1.parameters()))

在这里插入图片描述在这里插入图片描述

1.2 模型的本地保存与读取方法

  借助state_dict()方法,可以实现模型或优化器的本地保存于读取。此处以模型为例,优化器的本地保存相关操作类似。模型的训练和保存,本质上都是针对模型的参数。而模型的state_dict()则包含了模型当前全部的参数信息。因此,保存了模型的state_dict()就相当于是保存了模型。

# 设置随机数种子
torch.manual_seed(24)  

# 实例化模型  
tanh_model1 = net_class2(act_fun= torch.tanh, in_features=5, BN_model='pre')

# 通过`torch.save`将模型参数保存至本地
# 第一个参数:需要保存的模型参数,第二个参数:保存到本地的文件路径及名称
tanh_model1.state_dict()
torch.save(tanh_model1.state_dict(), 'tanh1.pt')

在这里插入图片描述
在这里插入图片描述

  进行模型训练,即模型参数调整。通过损失函数和反向传播机制进行梯度求解,利用优化器根据梯度值去更新各线性层参数。

criterion = nn.MSELoss()
optimizer = torch.optim.SGD(tanh_model1.parameters(), lr=0.05)
for X, y in train_loader:
    yhat = tanh_model1.forward(X)
    loss = criterion(yhat, y)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
# 训练完一轮之后,查看模型状态
tanh_model1.state_dict()

在这里插入图片描述
在这里插入图片描述

我们发现模型的参数已经发生了变化。此时,如果我们想还原tanh_model1中原始参数,我们只能考虑通过使用load_state_dict方法,将本次保存的原模型参数替换当前的tanh_model1中参数,具体方法如下:

# 读取(还原)保存的模型参数结果,使用load_state_dict方法和`torch.load`函数
tanh_model1.load_state_dict(torch.load('tanh1.pt'))
tanh_model1.state_dict()

在这里插入图片描述
在这里插入图片描述

除了模型可以按照上述方法保存外,优化器也可以类似进行本地存储。

接下来,我们通过调用optim模块中lr_scheduler相关函数,来实现优化器中学习率的动态调整

二、LambdaLR基本使用方法

  让优化器动态调整学习率的类,也被我们称为学习率调度器类,该类实例化的对象也被称为学习率调度器。在所有的学习率调度器中,LambdaLR类是实现学习率调度最简单灵活、同时也是最通用的一种方法。
(1)lambda匿名函数
  要使用LambdaLR来完成学习率调度,首先需要准备一个lambda匿名函数,例如:

lr_lambda = lambda epoch: 0.5 ** epoch
# 第一轮迭代时
lr_lambda(0)
# 第二轮迭代时
lr_lambda(1)

在这里插入图片描述

此处我们通过lambda创建了一个匿名函数。该函数需要输入一个参数,一般来说我们会将该参数视作模型迭代次数。当然上述匿名函数是个非常简单的匿名函数,输出结果就是0.5的epoch次方。

此处需要注意,一般来说epoch取值从0开始,并且用于学习率调度的匿名函数参数取值为0时,输出结果不能为0。

(2)LambdaLR使用
  在准备好一个匿名函数之后,接下来我们需要实例化一个LambdaLR学习率调度器。

# 设置随机数种子
torch.manual_seed(24)  

# 实例化模型  
tanh_model1 = net_class2(act_fun= torch.tanh, in_features=5, BN_model='pre')
# 创建优化器
optimizer = torch.optim.SGD(tanh_model1.parameters(), lr=0.05)
# 查看优化器信息
optimizer.state_dict()

# 创建学习率调度器
# 参数包括与之关联的优化器和一个lambda函数
lr_lambda = lambda epoch: 0.5 ** epoch
scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda)
# 查看优化器信息
optimizer.state_dict()

在这里插入图片描述

  此时优化器的参数簇中多了 initial_lr元素,代表初始学习率,在实例化优化器时输入的学习率。而优化器中的lr,则仍然表示下一次迭代时的学习率。对于LambdaLR学习调度来说,优化器中的lr伴随模型迭代相应调整的方法如下:
l r = l r _ l a m b d a ( e p o c h ) ∗ i n i t i a l _ l r lr = lr\_lambda(epoch) * initial\_lr lr=lr_lambda(epoch)initial_lr
  并且,第一次实例化LambdaLR时epoch取值为0时,因此此时优化器的lr计算结果如下: l r 0 = 0.5 0 ∗ 0.05 = 0.05 lr_0 = 0.5^0 * 0.05 = 0.05 lr0=0.500.05=0.05
而在后续计算过程中,每当我们调用一次scheduler.step(),epoch数值就会+1。当一轮训练完成时,我们可通过scheduler.step()来更新下一轮迭代时的学习率。

for X, y in train_loader:
    yhat = tanh_model1.forward(X)
    loss = criterion(yhat, y)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
# 更新下一轮(epoch)迭代时的学习率
scheduler.step()
# 查看优化器信息
optimizer.state_dict()

在这里插入图片描述

需要注意,在上述模型训练的代码中,之所以将学习率调度器放在模型小批量梯度下降循环的外侧,也是因为一般来说遍历一次完整训练集(一个epoch)才会对学习率进行一次更新,而不是每次计算完一个小批数据就对模型学习率进行更新。

而此时lr的取值0.025,则是由lr_lambda当epoch取值为1时的输出结果和initial_lr相乘之后的结果。也就是 l r = 0.5 1 ∗ 0.05 = 0.025 lr = 0.5^1 * 0.05 = 0.025 lr=0.510.05=0.025。而如果把上述过程封装为一个循环(也就是此前定义的fit函数),则下次模型训练时学习率就调整为了0.025。至此,我们也就知道了scheduler.step()的真实作用——令匿名函数的自变量+1,然后令匿名函数的输出结果与initial_lr相乘,并把计算结果传给优化器,作为下一次优化器计算时的学习率
  当然,我们也能简单的重复optimizer.step()与scheduler.step(),即可一次次完成计算新学习率、并将新学习率传输给优化器的过程。

optimizer.zero_grad()
optimizer.step()
scheduler.step()        # lr 0.025 --> 0.0125

lr_lambda = lambda epoch: 0.5 ** epoch
optimizer.state_dict()  # 优化器 lr 0.0125
scheduler.state_dict()  # 学习率调度器 lr 0.0125

在这里插入图片描述

不出意外,在第三次scheduler.step()时,匿名函数输出结果为 0.5 2 0.5^2 0.52,再与initial_lr相乘之后结果为0.0125。此处需要注意,PyTorch中要求先进行优化器的step,再进行学习率调度的step,此处需要注意先后顺序。 另外,上述过程之所以提前将优化器内保存的模型参数清零,也是为了防止上述实验过程最终导致模型参数被修改(梯度为0时模型无法修改参数)当然,每一轮epoch都让模型学习率衰减50%其实是非常激进的。我们可以通过绘制图像观察学习率衰减情况。

# 创建优化器
optimizer = torch.optim.SGD(tanh_model1.parameters(), lr=0.05)
# 创建学习率调度器
scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda)
optimizer.state_dict()['param_groups'][0]['lr']
lr_l = [0.05]
for i in range(10):
    optimizer.step()
    scheduler.step()
    lr = optimizer.state_dict()['param_groups'][0]['lr']
    lr_l.append(lr)
plt.plot(lr_l)
plt.xlabel('epoch')
plt.ylabel('Learning rate')

在这里插入图片描述

接下来,我们放缓学习率衰减速率,进行学习率调度建模实验。

三、LambdaLR学习率调度实验

3.1 前期准备与匿名函数定义

  在实验开始前,我们需要将之前定义的fit_rec函数再次进行改写,新函数需要包含学习率调度相关方法。

def fit_rec_sc(net, 
               criterion, 
               optimizer, 
               train_data,
               test_data,
               scheduler,
               epochs = 3, 
               cla = False, 
               eva = mse_cal):
    """加入学习率调度后的模型训练函数(记录每一次遍历后模型评估指标)
    
    :param net:待训练的模型 
    :param criterion: 损失函数
    :param optimizer:优化算法
    :param train_data:训练数据
    :param test_data: 测试数据 
    :param scheduler: 学习率调度器
    :param epochs: 遍历数据次数
    :param cla: 是否是分类问题
    :param eva: 模型评估方法
    :return:模型评估结果
    """
    train_l = []
    test_l = []
    for epoch  in range(epochs):
        net.train()
        for X, y in train_data:
            if cla == True:
                y = y.flatten().long()          # 如果是分类问题,需要对y进行整数转化
            yhat = net.forward(X)
            loss = criterion(yhat, y)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
        scheduler.step()
        net.eval()
        train_l.append(eva(train_data, net).detach())
        test_l.append(eva(test_data, net).detach())
    return train_l, test_l

同样,该函数需要写入torchLearning.py文件中。接下来,我们定义一个衰减速度更加缓慢的学习率调度器。

lr_lambda = lambda epoch: 0.95 ** epoch   # 相当于每迭代一轮学习率衰减5%
torch.manual_seed(24)  
# 创建优化器
optimizer = torch.optim.SGD(tanh_model1.parameters(), lr=0.05)
# 创建学习率调度器
lr_lambda = lambda epoch: 0.95 ** epoch  # 相当于每迭代一轮学习率衰减5%
scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda)

lr_l = [0.05]
for i in range(100):
    optimizer.step()
    scheduler.step()
    lr = optimizer.state_dict()['param_groups'][0]['lr']
    lr_l.append(lr)
plt.plot(lr_l)
plt.xlabel('epoch')
plt.ylabel('Learning rate')

在这里插入图片描述

# 设置随机数种子
torch.manual_seed(24)  

# 实例化模型  
tanh_model1 = net_class2(act_fun=torch.tanh, in_features=5, BN_model='pre')
# 创建优化器
optimizer = torch.optim.SGD(tanh_model1.parameters(), lr=0.05)
# 创建学习率调度器
lr_lambda = lambda epoch: 0.95 ** epoch   # 相当于每迭代一轮学习率衰减5%
scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda)
### 3.模型训练与结果比较
# 进行模型训练
train_l, test_l = fit_rec_sc(net = tanh_model1, 
                             criterion = nn.MSELoss(), 
                             optimizer = optimizer, 
                             train_data = train_loader,
                             test_data = test_loader,
                             scheduler = scheduler,
                             epochs = 60, 
                             cla = False, 
                             eva = mse_cal)
plt.plot(train_l, label='train_mse')
plt.xlabel('epochs')
plt.ylabel('MSE')
plt.legend(loc = 1)

在这里插入图片描述

# 简单验证学习率最终调整结果。
optimizer.state_dict()
lr_lambda(60) * 0.05

在这里插入图片描述
当然,我们也可以继续进行实验,对比恒定学习率时计算结果

  • 对比恒定学习率为0.03时模型训练结果
# 设置随机数种子
torch.manual_seed(24)  

# 实例化模型  
tanh_model1 = net_class2(act_fun= torch.tanh, in_features=5, BN_model='pre')
train_l3, test_l3 = fit_rec(net = tanh_model1, 
                            criterion = nn.MSELoss(), 
                            optimizer = optim.SGD(tanh_model1.parameters(), lr = 0.03), 
                            train_data = train_loader,
                            test_data = test_loader,
                            epochs = 60, 
                            cla = False, 
                            eva = mse_cal)
plt.plot(train_l, label='train_l')
plt.plot(train_l3, label='train_l3')
plt.xlabel('epochs')
plt.ylabel('MSE')
plt.legend(loc = 1)

在这里插入图片描述

我们发现,相比恒定学习为0.03的模型,加入学习率调度策略的模型(蓝色线条),模型收敛效果更好、迭代更加平稳,且收敛速度较快。

  • 对比恒定学习率为0.01时模型训练结果
# 设置随机数种子
torch.manual_seed(24)  

# 实例化模型  
tanh_model1 = net_class2(act_fun= torch.tanh, in_features=5, BN_model='pre')
train_l1, test_l1 = fit_rec(net = tanh_model1, 
                            criterion = nn.MSELoss(), 
                            optimizer = optim.SGD(tanh_model1.parameters(), lr = 0.01), 
                            train_data = train_loader,
                            test_data = test_loader,
                            epochs = 60, 
                            cla = False, 
                            eva = mse_cal)
                            
plt.plot(train_l, label='train_l')		# lr 0.05 学习率调节器,迭代优化
plt.plot(train_l3, label='train_l3')  	# lr 0.03
plt.plot(train_l1, label='train_l1')	# lr 0.01
plt.xlabel('epochs')
plt.ylabel('MSE')
plt.legend(loc = 1)

在这里插入图片描述

我们发现,相比恒定学习率为0.01的模型,拥有学习率调度的模型结果更优秀。

  • 对比Lesson 15.1节中学习率调度模型
# 设置随机数种子
torch.manual_seed(24)  

# 实例化模型  
tanh_model = net_class2(act_fun=torch.tanh, in_features=5, BN_model='pre')
# 创建用于保存记录结果的空列表容器
train_mse = []
test_mse = []

# 创建可以捕捉手动输入数据的模型训练流程
while input("Do you want to continue the iteration? [y/n]") == "y":    # 询问是否继续迭代
    epochs = int(input("Number of epochs:"))                           # 下一轮迭代遍历几次数据
    lr = float(input("Update learning rate:"))                        # 设置下一轮迭代的学习率
    train_l0, test_l0 = fit_rec(net = tanh_model, 
                                criterion = nn.MSELoss(), 
                                optimizer = optim.SGD(tanh_model.parameters(), lr = lr), 
                                train_data = train_loader,
                                test_data = test_loader,
                                epochs = epochs, 
                                cla = False, 
                                eva = mse_cal)
    train_mse.extend(train_l0)
    test_mse.extend(test_l0)
plt.plot(train_l, label='train_l')			# 60 0.05迭代优化
plt.plot(train_mse, label='train_mse')		# 30 0.03, 30 0.01
plt.xlabel('epochs')
plt.ylabel('MSE')
plt.legend(loc = 1)

在这里插入图片描述

很明显,由于上一节的模型是0.03学习率模型和0.01学习率模型简单叠加结果,在恒定学习率模型效果均不如本节模型的情况下,上一节课中的模型学习率调度策略也无法有更好的表现。

在这里插入图片描述
  但是,令人惊讶的是,在训练了60轮之后,LambdaLR模型最终学习率在0.002附近,相比上述0.01学习率模型而言学习率更小。但从上述的实验中我们发现,恒定学习率时从恒定0.03到恒定0.01的过程,模型准确率已经发生了明显的下降,但在如果是采用动态调整学习率的策略,则可以在一个最终更小的学习率取值的情况下取得一个更好的模型结果

  这其实说明损失函数在超平面空间的图像比一般的想象要复杂的多,很多时候并不是越靠近全域最小值点附近的通道就越窄,会导致迭代过程落入局部最小值陷阱的学习率大小取值也只是绝对概念。正是由于损失函数的复杂性,才导致很多时候我们认为神经网络的内部训练是个“黑箱”,才进一步导致神经网络的模型训练往往以模型结果为最终依据,这也是神经网络优化算法会诞生诸多基本原理层面比较扎实,但却找不到具体能够证明优化效果的理论依据的方法。
  不过,针对此类方法,和此前介绍的Batch Normalization一样,尽管理论层面无法具体整体优化效果,但对于使用者来说仍然需要在了解其底层原理基础上积累使用经验或者调参经验。因此在后续的课程中,我们将在继续介绍其他学习率优化方法的同时,通过大量的实践来快速积累使用经验,并且在更多事实的基础上找到解释和理解的角度。


网站公告

今日签到

点亮在社区的每一天
去签到