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

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

from torch.optim import lr_scheduler

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

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

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

# 设置随机数种子
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)

在优化器创建完成之后,我们可以使用.state_dict()方法查看优化器状态。

optimizer.state_dict()
#{'state': {},
# 'param_groups': [{'lr': 0.01,
#   'momentum': 0,
#   'dampening': 0,
#   'weight_decay': 0,
#   'nesterov': False,
#   'params': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]}]}

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

optimizer.state_dict()['param_groups']
#[{'lr': 0.01,
#  'momentum': 0,
#  'dampening': 0,
#  'weight_decay': 0,
#  'nesterov': False,
#  'params': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]}]
optimizer.state_dict()['param_groups'][0]
#{'lr': 0.01,
# 'momentum': 0,
# 'dampening': 0,
# 'weight_decay': 0,
# 'nesterov': False,
# 'params': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]}
optimizer.state_dict()['param_groups'][0]['lr']
#0.01

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

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

list(tanh_model1.parameters())
# [Parameter containing:
#  tensor([[ 0.2365, -0.1118, -0.3801,  0.0275,  0.4168],
#          [-0.1995, -0.1456,  0.3497, -0.0622, -0.1708],
#          [-0.0901,  0.0164, -0.3643, -0.1278,  0.4336],
#          [-0.0959,  0.4073, -0.1746, -0.1799, -0.1333]], requires_grad=True),
#  Parameter containing:
#  tensor([-0.3999, -0.2694,  0.2703, -0.3355], requires_grad=True),
#  Parameter containing:
#  tensor([1., 1., 1., 1.], requires_grad=True),
#  Parameter containing:
#  tensor([0., 0., 0., 0.], requires_grad=True),
#  Parameter containing:
#  tensor([[ 0.1708,  0.4704, -0.0635,  0.2187],
#          [ 0.2336, -0.3569, -0.1928, -0.1566],
#          [ 0.4825, -0.4463,  0.3027,  0.4696],
#          [ 0.3953,  0.2131,  0.2226, -0.0267]], requires_grad=True),
#  Parameter containing:
#  tensor([ 0.2516,  0.4558, -0.1608,  0.4831], requires_grad=True),
#  Parameter containing:
#  tensor([1., 1., 1., 1.], requires_grad=True),
#  Parameter containing:
#  tensor([0., 0., 0., 0.], requires_grad=True),
#  Parameter containing:
#  tensor([[ 0.0795, -0.3507, -0.3589,  0.1764]], requires_grad=True),
#  Parameter containing:
#  tensor([-0.0705], requires_grad=True)]# 验证带训练参数个数
len(list(tanh_model1.parameters()))
#10tanh_model2 = net_class3(act_fun= torch.tanh, in_features=5, BN_model='pre')optimizer1 = torch.optim.SGD(tanh_model2.parameters(), lr=0.05)
optimizer1.state_dict()
#{'state': {},
# 'param_groups': [{'lr': 0.05,
#   'momentum': 0,
#   'dampening': 0,
#   'weight_decay': 0,
#   'nesterov': False,
#   'params': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]}]}len(list(tanh_model2.parameters()))
#14

模型本地保存与读取方法

  同时,借助state_dict()方法,我们可以实现模型或优化器的本地保存与读取。此处以模型为例,优化器的本地保存相关操作类似。
  对于模型而言,其实也有state_dict()方法。通过该方法的调用,可以查看模型全部参数信息。

值得注意的是,模型的训练和保存,本质上都是针对模型的参数。而模型的state_dict()则包含了模型当前全部的参数信息。因此,保存了模型的state_dict()就相当于是保存了模型。

# 设置随机数种子
torch.manual_seed(24)  # 实例化模型
tanh_model1 = net_class2(act_fun= torch.tanh, in_features=5, BN_model='pre')tanh_model1.state_dict()
# OrderedDict([('linear1.weight',
#               tensor([[ 0.2365, -0.1118, -0.3801,  0.0275,  0.4168],
#                       [-0.1995, -0.1456,  0.3497, -0.0622, -0.1708],
#                       [-0.0901,  0.0164, -0.3643, -0.1278,  0.4336],
#                       [-0.0959,  0.4073, -0.1746, -0.1799, -0.1333]])),
#              ('linear1.bias', tensor([-0.3999, -0.2694,  0.2703, -0.3355])),
#              ('normalize1.weight', tensor([1., 1., 1., 1.])),
#              ('normalize1.bias', tensor([0., 0., 0., 0.])),
#              ('normalize1.running_mean', tensor([0., 0., 0., 0.])),
#              ('normalize1.running_var', tensor([1., 1., 1., 1.])),
#              ('normalize1.num_batches_tracked', tensor(0)),
#              ('linear2.weight',
#               tensor([[ 0.1708,  0.4704, -0.0635,  0.2187],
#                       [ 0.2336, -0.3569, -0.1928, -0.1566],
#                       [ 0.4825, -0.4463,  0.3027,  0.4696],
#                       [ 0.3953,  0.2131,  0.2226, -0.0267]])),
#              ('linear2.bias', tensor([ 0.2516,  0.4558, -0.1608,  0.4831])),
#              ('normalize2.weight', tensor([1., 1., 1., 1.])),
#              ('normalize2.bias', tensor([0., 0., 0., 0.])),
#              ('normalize2.running_mean', tensor([0., 0., 0., 0.])),
#              ('normalize2.running_var', tensor([1., 1., 1., 1.])),
#              ('normalize2.num_batches_tracked', tensor(0)),
#              ('linear3.weight',
#               tensor([[ 0.0795, -0.3507, -0.3589,  0.1764]])),
#              ('linear3.bias', tensor([-0.0705]))])

首先,我们可以将该存有模型全部参数信息的字典对象赋给某个变量。

t1 = tanh_model1.state_dict()
t1
# OrderedDict([('linear1.weight',
#               tensor([[ 0.2365, -0.1118, -0.3801,  0.0275,  0.4168],
#                       [-0.1995, -0.1456,  0.3497, -0.0622, -0.1708],
#                       [-0.0901,  0.0164, -0.3643, -0.1278,  0.4336],
#                       [-0.0959,  0.4073, -0.1746, -0.1799, -0.1333]])),
#              ('linear1.bias', tensor([-0.3999, -0.2694,  0.2703, -0.3355])),
#              ('normalize1.weight', tensor([1., 1., 1., 1.])),
#              ('normalize1.bias', tensor([0., 0., 0., 0.])),
#              ('normalize1.running_mean', tensor([0., 0., 0., 0.])),
#              ('normalize1.running_var', tensor([1., 1., 1., 1.])),
#              ('normalize1.num_batches_tracked', tensor(0)),
#              ('linear2.weight',
#               tensor([[ 0.1708,  0.4704, -0.0635,  0.2187],
#                       [ 0.2336, -0.3569, -0.1928, -0.1566],
#                       [ 0.4825, -0.4463,  0.3027,  0.4696],
#                       [ 0.3953,  0.2131,  0.2226, -0.0267]])),
#              ('linear2.bias', tensor([ 0.2516,  0.4558, -0.1608,  0.4831])),
#              ('normalize2.weight', tensor([1., 1., 1., 1.])),
#              ('normalize2.bias', tensor([0., 0., 0., 0.])),
#              ('normalize2.running_mean', tensor([0., 0., 0., 0.])),
#              ('normalize2.running_var', tensor([1., 1., 1., 1.])),
#              ('normalize2.num_batches_tracked', tensor(0)),
#              ('linear3.weight',
#               tensor([[ 0.0795, -0.3507, -0.3589,  0.1764]])),
#              ('linear3.bias', tensor([-0.0705]))])

其次,我们也可以通过torch.save来将该参数保存至本地。

torch.save(tanh_model1.state_dict(), 'tanh1.pt')

  对于torch.save函数来说,第一个参数是需要保存的模型参数,而第二个参数则是保存到本地的文件名。一般来说可以令其后缀为.pt.pth。而当我们需要读取保存的参数结果时,则可以直接使用load_state_dict方法w。该方法的使用我们稍后就会谈到。
  接下来进行模型训练,也就是模型参数调整。回顾此前学习内容,当我们进行模型训练时,实际上就是借助损失函数和反向传播机制进行梯度求解,然后利用优化器根据梯度值去更新各线性层参数。

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()
# OrderedDict([('linear1.weight',
#               tensor([[ 0.0436, -0.3587, -0.3227,  0.0310,  0.4388],
#                       [-0.0870, -0.1146,  0.4255, -0.0052, -0.3548],
#                       [-0.0154,  0.1517, -0.4181, -0.0605,  0.4350],
#                       [-0.0627,  0.5445,  0.0345, -0.1221,  0.1262]])),
#              ('linear1.bias', tensor([-0.3999, -0.2694,  0.2703, -0.3355])),
#              ('normalize1.weight', tensor([1.0497, 0.9741, 1.0267, 1.0508])),
#              ('normalize1.bias', tensor([ 0.0358, -0.1734, -0.1451,  0.0043])),
#              ('normalize1.running_mean',
#               tensor([-0.3789, -0.2839,  0.2689, -0.3484])),
#              ('normalize1.running_var',
#               tensor([0.3839, 0.2907, 0.3761, 0.2507])),
#              ('normalize1.num_batches_tracked', tensor(42)),
#              ('linear2.weight',
#               tensor([[ 0.1514,  0.5047, -0.0870,  0.1669],
#                       [ 0.2090,  0.0034, -0.3558, -0.4330],
#                       [ 0.4056, -0.3937,  0.3199,  0.5734],
#                       [ 0.3083,  0.3801, -0.0587, -0.2878]])),
#              ('linear2.bias', tensor([ 0.2516,  0.4558, -0.1608,  0.4831])),
#              ('normalize2.weight', tensor([1.0229, 0.4936, 0.2831, 0.7715])),
#              ('normalize2.bias', tensor([-0.0817, -1.2150, -1.1698,  0.8213])),
#              ('normalize2.running_mean',
#               tensor([ 0.2384,  0.4661, -0.1415,  0.4703])),
#              ('normalize2.running_var',
#               tensor([0.0720, 0.1388, 0.6376, 0.0972])),
#              ('normalize2.num_batches_tracked', tensor(42)),
#              ('linear3.weight',
#               tensor([[-0.3395, -1.3164, -1.1326,  0.8836]])),
#              ('linear3.bias', tensor([4.8350]))])

我们发现模型的参数已经发生了变化。当然,此时t1也随之发生了变化

t1
# OrderedDict([('linear1.weight',
#               tensor([[ 0.0436, -0.3587, -0.3227,  0.0310,  0.4388],
#                       [-0.0870, -0.1146,  0.4255, -0.0052, -0.3548],
#                       [-0.0154,  0.1517, -0.4181, -0.0605,  0.4350],
#                       [-0.0627,  0.5445,  0.0345, -0.1221,  0.1262]])),
#              ('linear1.bias', tensor([-0.3999, -0.2694,  0.2703, -0.3355])),
#              ('normalize1.weight', tensor([1.0497, 0.9741, 1.0267, 1.0508])),
#              ('normalize1.bias', tensor([ 0.0358, -0.1734, -0.1451,  0.0043])),
#              ('normalize1.running_mean',
#               tensor([-0.3789, -0.2839,  0.2689, -0.3484])),
#              ('normalize1.running_var',
#               tensor([0.3839, 0.2907, 0.3761, 0.2507])),
#              ('normalize1.num_batches_tracked', tensor(0)),
#              ('linear2.weight',
#               tensor([[ 0.1514,  0.5047, -0.0870,  0.1669],
#                       [ 0.2090,  0.0034, -0.3558, -0.4330],
#                       [ 0.4056, -0.3937,  0.3199,  0.5734],
#                       [ 0.3083,  0.3801, -0.0587, -0.2878]])),
#              ('linear2.bias', tensor([ 0.2516,  0.4558, -0.1608,  0.4831])),
#              ('normalize2.weight', tensor([1.0229, 0.4936, 0.2831, 0.7715])),
#              ('normalize2.bias', tensor([-0.0817, -1.2150, -1.1698,  0.8213])),
#              ('normalize2.running_mean',
#               tensor([ 0.2384,  0.4661, -0.1415,  0.4703])),
#              ('normalize2.running_var',
#               tensor([0.0720, 0.1388, 0.6376, 0.0972])),
#              ('normalize2.num_batches_tracked', tensor(0)),
#              ('linear3.weight',
#               tensor([[-0.3395, -1.3164, -1.1326,  0.8836]])),
#              ('linear3.bias', tensor([4.8350]))])

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

torch.load('tanh1.pt')
# OrderedDict([('linear1.weight',
#               tensor([[ 0.2365, -0.1118, -0.3801,  0.0275,  0.4168],
#                       [-0.1995, -0.1456,  0.3497, -0.0622, -0.1708],
#                       [-0.0901,  0.0164, -0.3643, -0.1278,  0.4336],
#                       [-0.0959,  0.4073, -0.1746, -0.1799, -0.1333]])),
#              ('linear1.bias', tensor([-0.3999, -0.2694,  0.2703, -0.3355])),
#              ('normalize1.weight', tensor([1., 1., 1., 1.])),
#              ('normalize1.bias', tensor([0., 0., 0., 0.])),
#              ('normalize1.running_mean', tensor([0., 0., 0., 0.])),
#              ('normalize1.running_var', tensor([1., 1., 1., 1.])),
#              ('normalize1.num_batches_tracked', tensor(0)),
#              ('linear2.weight',
#               tensor([[ 0.1708,  0.4704, -0.0635,  0.2187],
#                       [ 0.2336, -0.3569, -0.1928, -0.1566],
#                       [ 0.4825, -0.4463,  0.3027,  0.4696],
#                       [ 0.3953,  0.2131,  0.2226, -0.0267]])),
#              ('linear2.bias', tensor([ 0.2516,  0.4558, -0.1608,  0.4831])),
#              ('normalize2.weight', tensor([1., 1., 1., 1.])),
#              ('normalize2.bias', tensor([0., 0., 0., 0.])),
#              ('normalize2.running_mean', tensor([0., 0., 0., 0.])),
#              ('normalize2.running_var', tensor([1., 1., 1., 1.])),
#              ('normalize2.num_batches_tracked', tensor(0)),
#              ('linear3.weight',
#               tensor([[ 0.0795, -0.3507, -0.3589,  0.1764]])),
#              ('linear3.bias', tensor([-0.0705]))])
tanh_model1.load_state_dict(torch.load('tanh1.pt'))
#<All keys matched successfully>
tanh_model1.state_dict()
# OrderedDict([('linear1.weight',
#               tensor([[ 0.2365, -0.1118, -0.3801,  0.0275,  0.4168],
#                       [-0.1995, -0.1456,  0.3497, -0.0622, -0.1708],
#                       [-0.0901,  0.0164, -0.3643, -0.1278,  0.4336],
#                       [-0.0959,  0.4073, -0.1746, -0.1799, -0.1333]])),
#              ('linear1.bias', tensor([-0.3999, -0.2694,  0.2703, -0.3355])),
#              ('normalize1.weight', tensor([1., 1., 1., 1.])),
#              ('normalize1.bias', tensor([0., 0., 0., 0.])),
#              ('normalize1.running_mean', tensor([0., 0., 0., 0.])),
#              ('normalize1.running_var', tensor([1., 1., 1., 1.])),
#              ('normalize1.num_batches_tracked', tensor(0)),
#              ('linear2.weight',
#               tensor([[ 0.1708,  0.4704, -0.0635,  0.2187],
#                       [ 0.2336, -0.3569, -0.1928, -0.1566],
#                       [ 0.4825, -0.4463,  0.3027,  0.4696],
#                       [ 0.3953,  0.2131,  0.2226, -0.0267]])),
#              ('linear2.bias', tensor([ 0.2516,  0.4558, -0.1608,  0.4831])),
#              ('normalize2.weight', tensor([1., 1., 1., 1.])),
#              ('normalize2.bias', tensor([0., 0., 0., 0.])),
#              ('normalize2.running_mean', tensor([0., 0., 0., 0.])),
#              ('normalize2.running_var', tensor([1., 1., 1., 1.])),
#              ('normalize2.num_batches_tracked', tensor(0)),
#              ('linear3.weight',
#               tensor([[ 0.0795, -0.3507, -0.3589,  0.1764]])),
#              ('linear3.bias', tensor([-0.0705]))])

至此,我们就完成了模型训练与保存的基本过程。当然,除了模型可以按照上述方法保存外,优化器也可以类似进行本地存储。

当然,结合此前介绍的深拷贝的相关概念,此处我们能否通过深拷贝的方式将模型参数保存在当前操作空间内然后再替换训练后的模型参数呢?同学们可以自行尝试

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

二、LambdaLR基本使用方法

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

lr_lambda = lambda epoch: 0.5 ** epoch

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

# 第一轮迭代时
lr_lambda(0)
#1.0
# 第二轮迭代时
lr_lambda(1)
#0.5

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

  在准备好一个匿名函数之后,接下来我们需要实例化一个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()
#{'state': {},
# 'param_groups': [{'lr': 0.05,
#   'momentum': 0,
#   'dampening': 0,
#   'weight_decay': 0,
#   'nesterov': False,
#   'params': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]}]}
# 创建学习率调度器
scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda)

注意,LambdaLR学习率调度器的创建必须要输入一个lambda函数和与之关联的优化器。一旦优化器创建完成,我们即可继续观察优化器optimizer的状态。

optimizer.state_dict()
#{'state': {},
# 'param_groups': [{'lr': 0.05,
#   'momentum': 0,
#   'dampening': 0,
#   'weight_decay': 0,
#   'nesterov': False,
#   'initial_lr': 0.05,
#   'params': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]}]}

  此时优化器的参数簇中多了’initial_lr’元素。该元素代表初始学习率,也就是我们在实例化优化器时输入的学习率。而优化器中的lr,则仍然表示下一次迭代时的学习率。

对于LambdaLR学习调度来说,优化器中的lr伴随模型迭代相应调整的方法如下:
lr=lr_lambda(epoch)∗initial_lrlr = lr\_lambda(epoch) * initial\_lrlr=lr_lambda(epoch)∗initial_lr
  并且,第一次实例化LambdaLR时epoch取值为0时,因此此时优化器的lr计算结果如下:lr0=0.50∗0.05=0.05lr_0 = 0.5^0 * 0.05 = 0.05lr0​=0.50∗0.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()
scheduler.step()

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

optimizer.state_dict()
#{'state': {},
# 'param_groups': [{'lr': 0.025,
#   'momentum': 0,
#   'dampening': 0,
#   'weight_decay': 0,
#   'nesterov': False,
#   'initial_lr': 0.05,
#   'params': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]}]}

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

optimizer.zero_grad()
optimizer.step()
scheduler.step()lr_lambda = lambda epoch: 0.5 ** epoch
lr_lambda(2) * 0.05
#0.0125
optimizer.state_dict()
#{'state': {},
# 'param_groups': [{'lr': 0.0125,
#   'momentum': 0,
#   'dampening': 0,
#   'weight_decay': 0,
#   'nesterov': False,
#   'initial_lr': 0.05,
#   'params': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]}]}
ss = scheduler.state_dict()
ss
#{'base_lrs': [0.05],
# 'last_epoch': 2,
# '_step_count': 3,
# 'verbose': False,
# '_get_lr_called_within_step': False,
# '_last_lr': [0.0125],
# 'lr_lambdas': [None]}

不出意外,在第三次scheduler.step()时,匿名函数输出结果为0.520.5^20.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']
#0.05
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学习率调度实验

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
# 第一轮迭代时
lr_lambda(0)
#1.0
# 第二轮迭代时
lr_lambda(1)
#0.95
lr_lambda(100)
#0.0059205292203339975

相当于每迭代一轮学习率衰减5%。

# 设置随机数种子
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)
# 创建学习率调度器
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()
#{'state': {},
# 'param_groups': [{'lr': 0.002303489949347597,
#   'momentum': 0,
#   'dampening': 0,
#   'weight_decay': 0,
#   'nesterov': False,
#   'initial_lr': 0.05,
#   'params': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]}]}
lr_lambda(60) * 0.05
#0.002303489949347597

当然,我们也可以继续进行实验,对比恒定学习率时计算结果

  • 对比恒定学习率为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')
plt.plot(train_l3, label='train_l3')
plt.plot(train_l1, label='train_l1')
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)
#Do you want to continue the iteration? [y/n] y
#Number of epochs: 30
#Update learning rate: 0.03
#Do you want to continue the iteration? [y/n] y
#Number of epochs: 30
#Update learning rate: 0.01
#Do you want to continue the iteration? [y/n] n
plt.plot(train_l, label='train_l')
plt.plot(train_mse, label='train_mse')
plt.xlabel('epochs')
plt.ylabel('MSE')
plt.legend(loc = 1)


很明显,由于上一节的模型是0.03学习率模型和0.01学习率模型简单叠加结果,在恒定学习率模型效果均不如本节模型的情况下,上一节课中的模型学习率调度策略也无法有更好的表现。
  但是,令人惊讶的是,在训练了60轮之后,LambdaLR模型最终学习率在0.002附近,相比上述0.01学习率模型而言学习率更小。但从上述的实验中我们发现,恒定学习率时从恒定0.03到恒定0.01的过程,模型准确率已经发生了明显的下降,但在如果是采用动态调整学习率的策略,则可以在一个最终更小的学习率取值的情况下取得一个更好的模型结果。

lr_lambda(60) * 0.05
#0.002303489949347597

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

Lesson 15.2 学习率调度在PyTorch中的实现方法相关推荐

  1. Lesson 15.1 学习率调度基本概念与手动实现方法

    Lesson 15.1 学习率调度基本概念与手动实现方法   从本节开始,我们将介绍深度学习中学习率优化方法.学习率作为模型优化的重要超参数,在此前的学习中,我们已经看到了学习率的调整对模型训练在诸多 ...

  2. PyTorch中的topk方法以及分类Top-K准确率的实现

    PyTorch中的topk方法以及分类Top-K准确率的实现 Top-K 准确率 在分类任务中的类别数很多时(如ImageNet中1000类),通常任务是比较困难的,有时模型虽然不能准确地将groun ...

  3. Lesson 4.5 梯度下降优化基础:数据归一化与学习率调度

    Lesson 4.5 梯度下降优化基础:数据归一化与学习率调度 在上一小节中,我们讨论了关于随机梯度下降和小批量梯度下降的基本算法性质与使用流程.我们知道,在引入了一定的样本随机性之后,能够帮助参数点 ...

  4. Pytorch中的学习率调整方法

    在梯度下降更新参数的时,我们往往需要定义一个学习率来控制参数更新的步幅大小,常用的学习率有0.01.0.001以及0.0001等,学习率越大则参数更新越大.一般来说,我们希望在训练初期学习率大一些,使 ...

  5. 【深度学习】图解 9 种PyTorch中常用的学习率调整策略

    learning rate scheduling 学习率调整策略 01 LAMBDA LR 将每个参数组的学习率设置为初始lr乘以给定函数.当last_epoch=-1时,将初始lr设置为初始值. t ...

  6. pytorch中调整学习率的lr_scheduler机制

    pytorch中调整学习率的lr_scheduler机制 </h1><div class="clear"></div><div class ...

  7. pytorch中的学习率与优化器【lr_scheduler与optimizer】

    pytorch中优化器的使用流程大致为: for input, target in dataset:optimizer.zero_grad()output = model(input)loss = l ...

  8. Pytorch中的学习率调整lr_scheduler,ReduceLROnPlateau

    Pytorch中的学习率调整:lr_scheduler,ReduceLROnPlateau torch.optim.lr_scheduler:该方法中提供了多种基于epoch训练次数进行学习率调整的方 ...

  9. Lesson 16.5 在Pytorch中实现卷积网络(上):卷积核、输入通道与特征图在PyTorch中实现卷积网络(中):步长与填充

    卷积神经网络是使用卷积层的一组神经网络.在一个成熟的CNN中,往往会涉及到卷积层.池化层.线性层(全连接层)以及各类激活函数.因此,在构筑卷积网络时,需从整体全部层的需求来进行考虑. 1 二维卷积层n ...

最新文章

  1. 批处理如何做到等待用户输入一个值(dos命令行)
  2. 基于机器视觉的缺陷检测汽车零部件
  3. 《Adobe Illustrator CS6中文版经典教程(彩色版)》—第0课0.11节创建与应用图案...
  4. p_re 实验数据库ViPER和ETHZ的下载地址和实验评价标准
  5. 电子书下载:The C# Programming Language, 4th Edition
  6. Oracle执行计划中 并行和BUFFER SORT的问题
  7. 鸟哥学习笔记---网络驱动器设备iSCSI
  8. html5--6-13 CSS3中的颜色表示方式
  9. 写一个SFTP工具类实现连接
  10. Windows常用密码的破解方法
  11. 电容或电感的电压_如何通俗的理解电流,电压,电阻,电容和电感?
  12. 夺命雷公狗—玩转SEO---38---百度快照投诉从而更新快照
  13. JavaWeb-文件上传和下载
  14. iPhone/iPad怎么进入恢复模式?
  15. 使用Python开发一个恐龙跳跳小游戏,玩起来
  16. 美妆科技:改变美容行业的未来
  17. FTP服务器的搭建与配置
  18. 使用TinyProxy搭建代理服务器
  19. 奶茶和咖啡,二者终有一战
  20. 物理学家眼中的世界:编程的未来

热门文章

  1. python dump函数_python 处理 json 四个函数dumps、loads、dump、load的区别
  2. java字符串10_十个最常见的Java字符串问题
  3. c语言画图 钟表模拟程序,图形模拟时钟C语言课程设计
  4. 运行时权限+读取系统联系人
  5. Button控件的基本使用(初识封装、减少代码冗余性、代码优化)
  6. LeanCloud数据更新不同步问题
  7. 29. Leetcode 19. 删除链表的倒数第 N 个结点 (链表-双指针)
  8. python 笔记:time calendar
  9. 产品经理经验谈50篇(二):如何写好一份规范的数据分析报告?我想你需要知道这些
  10. 数据中台应用实战50篇(一)-企业级数据中台的建设方法架构和技术栈