【PyTorch】动态调整学习率 torch.optim.lr_scheduler.StepLR 调度器

发布于:2024-12-19 ⋅ 阅读:(12) ⋅ 点赞:(0)

在深度学习中,学习率调度器(Learning Rate Scheduler) 是用来动态调整学习率的工具。它的主要目的是在训练过程中自动调整学习率,以提高训练的效率和效果。之所以称其为“调度器”,是因为它控制着学习率的调整和更新,类似于调度一个过程或者任务,它按照某种策略和规则来“调度”学习率,本文将详细介绍pytorch中动态调整学习率方法之一 torch.optim.lr_scheduler.StepLR

官方文档链接:
https://pytorch.ac.cn/docs/stable/generated/torch.optim.lr_scheduler.StepLR.html#torch.optim.lr_scheduler.StepLR

1. torch.optim.lr_scheduler.StepLR 官方文档详解

官方文档定义:

class torch.optim.lr_scheduler.StepLR(optimizer, step_size, gamma=0.1, last_epoch=-1, verbose='deprecated')

每隔 step_size 个 epochs 将每个参数组的学习率衰减 gamma 倍。
请注意,这种衰减可能与来自此调度程序外部的学习率的其他更改同时发生。当 last_epoch=-1 时,将初始 lr 设置为 lr。

参数:

  • o p t i m i z e r optimizer optimizer (优化器) : 包装的优化器
  • s t e p _ s i z e ( i n t ) step\_size(int) step_size(int) : 学习率衰减周期
  • g a m m a ( f l o a t ) gamma (float) gamma(float) : 学习率衰减的乘法因子, 默认值:0.1
  • l a s t _ e p o c h ( i n t ) last\_epoch (int) last_epoch(int) : 最后一个 epoch 的索引, 默认值:-1 表示从头开始
  • v e r b o s e ( b o o l ∣ s t r ) verbose (bool | str) verbose(boolstr): 如果为 True,则为每次更新打印一条消息到标准输出, 默认值:False。

注意:

  • 自版本 2.2 起不推荐使用: v e r b o s e verbose verbose 已弃用。请使用 g e t _ l a s t _ l r ( ) get\_last\_lr() get_last_lr()访问学习率

其它:

  • g e t _ l a s t _ l r ( ) get\_last\_lr() get_last_lr() : 返回当前调度程序计算的最后一个学习率, 返回类型 L i s t [ f l o a t ] List[float] List[float]
  • g e t _ l r ( ) get\_lr() get_lr() : 计算每个组的学习率
  • l o a d _ s t a t e _ d i c t ( s t a t e _ d i c t ) load\_state\_dict(state\_dict) load_state_dict(state_dict) : 加载调度程序的状态,参数: s t a t e _ d i c t ( d i c t ) state\_dict (dict) state_dict(dict) 调度程序状态,应为 s t a t e _ d i c t ( ) state\_dict() state_dict() 调用返回的对象。
  • p r i n t _ l r ( i s _ v e r b o s e , g r o u p , l r , e p o c h = N o n e ) print\_lr(is\_verbose, group, lr, epoch=None) print_lr(is_verbose,group,lr,epoch=None) : 显示当前学习率。
  • s t a t e _ d i c t ( ) state\_dict() state_dict() : 将调度程序的状态作为 d i c t dict dict 返回,它包含 s e l f . _ _ d i c t _ _ self.\_\_dict\_\_ self.__dict__ 中每个变量的条目,这些变量不是优化器。
  • s t e p ( e p o c h = N o n e ) step(epoch=None) step(epoch=None) : 执行一步操作,即更新一次学习率

注意:

  • 自版本 2.4 起不推荐使用: p r i n t _ l r ( ) print\_lr() print_lr() 已弃用。请使用 g e t _ l a s t _ l r ( ) get\_last\_lr() get_last_lr() 访问学习率。

2. 使用示例

2.1 官方提供使用示例

  • 假设初始 l r = 0.05 lr = 0.05 lr=0.05 s t e p _ s i z e = 30 step\_size=30 step_size=30 g a m m a = 0.1 gamma=0.1 gamma=0.1,即每 30 个 epoch后,将学习率乘以 0.1
  • e p o c h < 30 epoch< 30 epoch<30 时, l r = 0.05 lr = 0.05 lr=0.05 ,即 l r = l r lr=lr lr=lr
  • 30 < = e p o c h < 60 30 <= epoch < 60 30<=epoch<60 l r = 0.005 lr = 0.005 lr=0.005,即 l r n e w = l r o l d ∗ g a m m a = 0.05 ∗ 0.1 = 0.005 lr_{new}=lr_{old}*gamma=0.05*0.1=0.005 lrnew=lroldgamma=0.050.1=0.005
  • 60 < = e p o c h < 90 60 <= epoch < 90 60<=epoch<90 l r = 0.0005 lr = 0.0005 lr=0.0005,即 l r n e w = l r o l d ∗ g a m m a = 0.005 ∗ 0.1 = 0.0005 lr_{new}=lr_{old}*gamma=0.005*0.1=0.0005 lrnew=lroldgamma=0.0050.1=0.0005
# Assuming optimizer uses lr = 0.05 for all groups
# lr = 0.05     if epoch < 30
# lr = 0.005    if 30 <= epoch < 60
# lr = 0.0005   if 60 <= epoch < 90
# ...
scheduler = StepLR(optimizer, step_size=30, gamma=0.1)
for epoch in range(100):
    train(...)
    validate(...)
    scheduler.step()

2.2 自己写代码测试方法

2.2.1 get_last_lr() 方法

  • 用于返回调度器计算的 最后一个学习率。这个学习率是在调度器(scheduler)调整之后的当前学习率
  • 如果优化器中有多个参数组, g e t _ l a s t _ l r ( ) get\_last\_lr() get_last_lr() 返回的是一个列表,每个元素对应一个参数组的学习率
  • 如果优化器中只有一个参数组, g e t _ l a s t _ l r ( ) get\_last\_lr() get_last_lr() 返回一个只有一个元素的列表
import torch
from torch import nn
from torch import optim
net = nn.Linear(3,4)
def train():
    optimizer = optim.Adam(net.parameters(), lr=0.1)
    scheduler = optim.lr_scheduler.StepLR(optimizer, step_size = 1, gamma=0.1)
    for epoch in range(0, 10):
        print(epoch, scheduler.get_last_lr())
        optimizer.step()
        scheduler.step()
if __name__ == "__main__":
    train()

代码输出:

0 [0.1]
1 [0.010000000000000002]
2 [0.0010000000000000002]
3 [0.00010000000000000003]
4 [1.0000000000000004e-05]
5 [1.0000000000000004e-06]
6 [1.0000000000000005e-07]
7 [1.0000000000000005e-08]
8 [1.0000000000000005e-09]
9 [1.0000000000000006e-10]

2.2.2 state_dict() 方法

  • s t a t e _ d i c t ( ) state\_dict() state_dict() 是一个非常重要的函数,它能够返回一个包含模型或优化器状态的字典(dict)。对于学习率调度器来说, s t a t e _ d i c t ( ) state\_dict() state_dict() 返回调度器的状态,包括它的参数和变量,用于保存和恢复调度器的状态等

代码示例:

import torch
from torch import nn
from torch import optim

net = nn.Linear(3,4)

def train():
    optimizer = optim.Adam(net.parameters(), lr=0.1)
    scheduler = optim.lr_scheduler.StepLR(optimizer, step_size = 1, gamma=0.1)
    
    for epoch in range(10):
        print(epoch, scheduler.state_dict())
        optimizer.step()
        scheduler.step()

if __name__ == "__main__":
    train()

代码输出:

0 {'step_size': 1, 'gamma': 0.1, 'base_lrs': [0.1], 'last_epoch': 0, 'verbose': False, '_step_count': 1, '_get_lr_called_within_step': False, '_last_lr': [0.1]}
1 {'step_size': 1, 'gamma': 0.1, 'base_lrs': [0.1], 'last_epoch': 1, 'verbose': False, '_step_count': 2, '_get_lr_called_within_step': False, '_last_lr': [0.010000000000000002]}   
2 {'step_size': 1, 'gamma': 0.1, 'base_lrs': [0.1], 'last_epoch': 2, 'verbose': False, '_step_count': 3, '_get_lr_called_within_step': False, '_last_lr': [0.0010000000000000002]}  
3 {'step_size': 1, 'gamma': 0.1, 'base_lrs': [0.1], 'last_epoch': 3, 'verbose': False, '_step_count': 4, '_get_lr_called_within_step': False, '_last_lr': [0.00010000000000000003]} 
4 {'step_size': 1, 'gamma': 0.1, 'base_lrs': [0.1], 'last_epoch': 4, 'verbose': False, '_step_count': 5, '_get_lr_called_within_step': False, '_last_lr': [1.0000000000000004e-05]} 
5 {'step_size': 1, 'gamma': 0.1, 'base_lrs': [0.1], 'last_epoch': 5, 'verbose': False, '_step_count': 6, '_get_lr_called_within_step': False, '_last_lr': [1.0000000000000004e-06]} 
6 {'step_size': 1, 'gamma': 0.1, 'base_lrs': [0.1], 'last_epoch': 6, 'verbose': False, '_step_count': 7, '_get_lr_called_within_step': False, '_last_lr': [1.0000000000000005e-07]} 
7 {'step_size': 1, 'gamma': 0.1, 'base_lrs': [0.1], 'last_epoch': 7, 'verbose': False, '_step_count': 8, '_get_lr_called_within_step': False, '_last_lr': [1.0000000000000005e-08]} 
8 {'step_size': 1, 'gamma': 0.1, 'base_lrs': [0.1], 'last_epoch': 8, 'verbose': False, '_step_count': 9, '_get_lr_called_within_step': False, '_last_lr': [1.0000000000000005e-09]} 
9 {'step_size': 1, 'gamma': 0.1, 'base_lrs': [0.1], 'last_epoch': 9, 'verbose': False, '_step_count': 10, '_get_lr_called_within_step': False, '_last_lr': [1.0000000000000006e-10]}

输出详解:

  • step_size: 该值表示每隔多少个 epoch 学习率就会发生变化。在例子中, s t e p _ s i z e = 1 step\_size = 1 step_size=1,意味着每经过 1 个 epoch,学习率都会更新一次
  • gamma: 这是学习率更新的衰减因子。每次调用 s c h e d u l e r . s t e p ( ) scheduler.step() scheduler.step() 时,当前的学习率将会乘以 g a m m a gamma gamma。例子中, g a m m a = 0.1 gamma = 0.1 gamma=0.1,意味着每次更新学习率时,学习率将减少为原来的 10%
  • base_lrs: 这是每个参数组的初始学习率(在调度器调整之前的学习率)。这里的 [ 0.1 ] [0.1] [0.1]表示模型的初始学习率是 0.1。如果有多个参数组,这里会是一个列表,列出每个参数组的初始学习率
  • last_epoch: 这是上一个 epoch 的编号,用来确定学习率更新时的参考点。 l a s t _ e p o c h = 0 last\_epoch = 0 last_epoch=0 表示调度器刚刚初始化,学习率还没有更新过。通常,last_epoch 用来恢复训练时从哪个 epoch 开始更新学习率
  • verbose: 该参数控制调度器是否在学习率更新时打印详细信息。 v e r b o s e = F a l s e verbose = False verbose=False 表示调度器在更新学习率时不会打印信息。如果设置为 T r u e True True,则每次更新学习率时都会打印一条日志。
  • _step_count: 这个内部变量跟踪调度器已经调用了多少次 step()。在例子中, _ s t e p _ c o u n t = 1 \_step\_count = 1 _step_count=1,表示调度器已经调用过一次 s t e p ( ) step() step(),即更新过一次学习率
  • _get_lr_called_within_step: 是一个内部标志,表示是否在 step() 方法内部调用了 get_lr()。通常不需要关注这个值,它帮助调度器管理内部逻辑
  • _last_lr: 这是调度器最近一次计算的学习率。这个列表保存了每个参数组的学习率。在你的例子中, _ l a s t _ l r = [ 0.1 ] \_last\_lr = [0.1] _last_lr=[0.1],表示当前学习率是 0.1。这会在 scheduler.step() 后更新为新的学习率。

2.2.3 load_state_dict() 保存和加载调度器

可以将 state_dict() 保存到文件中,然后在以后恢复。如下是一个保存和加载学习率调度器状态的例子:

保存调度器状态:

# 保存调度器的 state_dict
torch.save(scheduler.state_dict(), 'scheduler_state.pth')

加载调度器状态:

# 加载调度器的 state_dict
scheduler.load_state_dict(torch.load('scheduler_state.pth'))

通过这种方式,可以在训练中断后恢复学习率调度器的状态,并继续进行训练。

保存调度器状态示例:

epoch = 5 时,保存调度器状态:

import torch
from torch import nn
from torch import optim
net = nn.Linear(3,4)
def train():
    optimizer = optim.Adam(net.parameters(), lr=0.1)
    scheduler = optim.lr_scheduler.StepLR(optimizer, step_size = 1, gamma=0.1)

    for epoch in range(10):
        if epoch == 5:
            torch.save(scheduler.state_dict(), 'scheduler_state.pth')
            break
        optimizer.step()
        scheduler.step()

if __name__ == "__main__":
    train()

加载调度器状态示例:

import torch
from torch import nn
from torch import optim

net = nn.Linear(3,4)

def train():
    optimizer = optim.Adam(net.parameters(), lr=0.1)
    scheduler = optim.lr_scheduler.StepLR(optimizer, step_size = 1, gamma=0.1)
    scheduler.load_state_dict(torch.load("scheduler_state.pth"))
    for epoch in range(3):
        print(epoch, scheduler.state_dict())
        optimizer.step()
        scheduler.step()

if __name__ == "__main__":
    train()

代码输出:

0 {'step_size': 1, 'gamma': 0.1, 'base_lrs': [0.1], 'last_epoch': 5, 'verbose': False, '_step_count': 6, '_get_lr_called_within_step': False, '_last_lr': [1.0000000000000004e-06]}
1 {'step_size': 1, 'gamma': 0.1, 'base_lrs': [0.1], 'last_epoch': 6, 'verbose': False, '_step_count': 7, '_get_lr_called_within_step': False, '_last_lr': [0.010000000000000002]}
2 {'step_size': 1, 'gamma': 0.1, 'base_lrs': [0.1], 'last_epoch': 7, 'verbose': False, '_step_count': 8, '_get_lr_called_within_step': False, '_last_lr': [0.0010000000000000002]}

可以看到:

  • last_epoch 从 5 开始
  • _step_count 从6 开始
  • _last_lr 从 1e-6 开始

说明是从上次终断的状态继续运行

3. 思考

3.1 为什么需要state_dict()

  • 保存和恢复训练: 当你希望在训练中断后恢复训练时,你可以保存模型和调度器的 state_dict(),然后在恢复时加载它们,确保学习率调度器从上次停止的地方继续工作,而不是从头开始。
  • 调试和分析:通过 state_dict() 可以查看学习率的变化,帮助你调试和分析训练过程中调度器的行为。

3.2 get_lr() 与 get_last_lr() 的输出不一致问题

测试代码:

import torch
from torch import nn
from torch import optim

net = nn.Linear(3,4)

def train():
    optimizer = optim.Adam(net.parameters(), lr=0.1)
    scheduler = optim.lr_scheduler.StepLR(optimizer, step_size = 1, gamma=0.1)
    for epoch in range(3):
        print(epoch, scheduler.get_lr())
        print(epoch, scheduler.get_last_lr())
        print([group["lr"] for group in optimizer.param_groups])
        print("==========================================")
        optimizer.step()
        scheduler.step()

if __name__ == "__main__":
    train()

代码输出:

0 [0.1]
0 [0.1]
[0.1]
==========================================
1 [0.0010000000000000002]
1 [0.010000000000000002]
[0.010000000000000002]
==========================================
2 [0.00010000000000000003]
2 [0.0010000000000000002]
[0.0010000000000000002]
==========================================

不知道为什么从 epoch > 0 之后,get_lr() 每次都比 get_last_lr() 提前一步更新,但是通过查看 optimizer.param_groups 的学习率与 get_last_lr() 一致。

get_lr()函数源码:

def get_lr(self):
    if not self._get_lr_called_within_step:
        warnings.warn("To get the last learning rate computed by the scheduler, "
                      "please use `get_last_lr()`.", UserWarning)

    if (self.last_epoch == 0) or (self.last_epoch % self.step_size != 0):
        return [group['lr'] for group in self.optimizer.param_groups]
    return [group['lr'] * self.gamma
            for group in self.optimizer.param_groups]

我们可以看到在源码中当:if (self.last_epoch == 0) or (self.last_epoch % self.step_size != 0),返回值为[group['lr'] for group in self.optimizer.param_groups], 否则的话返回值为[group['lr'] * self.gamma for group in self.optimizer.param_groups],因此可以很好的解释上面的现象,但是Pytorch为什么要这样做呢??目前没有找到相关资料,可以评论区留言讨论!

get_last_lr() 函数源码:

def get_last_lr(self):
   """ Return last computed learning rate by current scheduler.
   """
   return self._last_lr