梯度值与参数更新optimizer.zero_grad(),loss.backward、和optimizer.step()、lr_scheduler.step原理解析

在用pytorch训练模型时,通常会在遍历epochs的过程中依次用到optimizer.zero_grad(),loss.backward、和optimizer.step()、lr_scheduler.step()四个函数,使用如下所示:

train_loader=DataLoader(
    train_dataset,
    batch_size=2,
    shuffle=True
)
model=myModel()   
criterion=nn.CrossEntropyLoss()                                                                                                                           
optimizer=torch.optim.SGD(model.parameters(),lr=1e-6,momentum=0.9,weight_decay=2e-4)     #设置优化器参数
lr_scheduler=lr_scheduler.StepLR(optimizer,step_size=3,gamma=0.1) #学习率优化模块
for epoch in range(epochs):
      model.train()
      for  i,(inputs,labels) in enumerate(train_loader):
             outputs=model(inputs)
             loss=criterion(outputs,labels)
             optimizer.zero_grad()
             loss.backward()
             optimizer.step()
      lr_scheduler.step()
      

optimizer.zero_grad() # 梯度值清零
loss.backward() # 反向传播计算参数梯度值
optimizer.step() # 通过梯度下降更新参数
lr_scheduler.step() # 根据迭代epoch更新学习率
四个函数作用是先将梯度值归零(optimizer.zero_grad()),然后反向传播计算每个参数的梯度值(loss.backward()),通过梯度下降进行参数更新(optimizer.step()),最后根据opeoch训练轮数更新学习率(lr_scheduler.step())。

接下来通过源码对四个函数进行分析。在此之前说明函数中常见的参数变量。
param_groups:Optimizer类在实例化时会创建一个param_groups列表,列表中有num_groups(num_groups取决于你定义optimizer时传入了几组参数)个长度为6的param_group字典,每个param_group包含了[‘param’,‘lr’,‘momentum’,‘dampening’,‘weight_decay’,‘nesterov’]这6组键值对。
params(iterable)—待优化参数w、b 或者定义了参数组的dict
lr(float,可选)—学习率
momentum(float,可选,默认0)—动量因子
weight_decay(float,可选,默认0)—权重衰减
dampening (float, 可选) – 动量的抑制因子(默认:0)
nesterov (bool, 可选) – 使用Nesterov动量(默认:False)
param_group[‘param’]:由传入的模型参数组成的列表,即实例化Optimizer类时传入该group的参数,如果参数没有分组,则为整个模型的参数model.parameters(),每个参数是一个torch.nn.parameter.Parameter对象。
一、优化器torch.optim**
pytorch中用来优化模型权重的类是torch.optim.Optimizer,其他各种优化器是基于Optimizer这个基类的子类,下面谈谈如何构建一个模型的优化器对象实例。
所有的优化器都继承torch.optim.Optimizer类

CLASS torch.optim.Optimizer(params,defaults)

params的传入数据类型有2种,参数传入方式有2种
传入的数据类型有2种:

  1. 可迭代的torch.tensor
  2. 字典(dict),字典中的键(key)为“params”那一项的值(value)必须为可迭代的torch.tensor
  3. 如果有多个dict传入,则要放入一个列表中
  4. List item

参数传入方式有2种:
1.模型的权重共享一个学习率,直接将模型参数传入。

self.optimizer=torch.optim.SGD(params=self.model.parameter(),lr=args.lr)
#这种方式模型所有参数使用同一个学习率lr调整

2.模型权重不共享同一个学习率

#首选需要将模型参数分类,每一类有不同学习率
def get_one_lr_params(self):
    modules=[self.backbone]
    for i in range(len(modules)):
         for m in modules[i].named_modules():
             if isinstance(m[1],nn.Conv2d) or isinstance(m[1],SynchronizedBatchNorm2d) or isinstance(m[1],nn.batchNorm2d):
             for p in m[1].parameters():
                 if p.requires_grad:
                    yield p
def get_two_lr_params(self):
    modules=[self.aspp,self.decoder]
    for i in range(len(modules)):
        for m in modules[i].named_modules():
            if isinstance(m[1],nn.Conv2d) or isinstance(m[1],SynchronizedBatchNorm2d) isinstance(m[1],nn.batchNorm2d):
             for p in m[1].parameters():
                 if p.requires_grad:
                    yield p
#构建优化参数列表
def optim_parameters(self,args):
    return [{'params': self.get_one_lr_params,'lr':,args.learning_rate},
    {'params': self.get_two_lr_params,'lr': 10*args.learning_rate}]
#传入模型参数,构建优化器
self.optimizer=torch.optim.SGD(params=self.model.optim_parameters(args))  

动态调整优化器的学习率
每一个optimizer的实例都存在一个optimizer.param_groups的属性,其中一个list 类型,列表中的每个元素都以dict的类型存放,只需要调整dict的‘lr’项值即可。
构建一个动态调整学习率的函数方法

#计算学习率
def lr_poly(base_lr,iter,max_iter,power):
    return base_lr*((1-float(iter)/max_iter)**(power))
#调整学习率,传入学习率参数
def adjust_learning_rate(optimizer,i_iter):
    lr = lr_poly(args.learning_rate, i_iter, args.num_steps, args.power)
    optimizer.param_groups[0]['lr]=lr
    optimizer.param_groups[1]['lr]=10*lr

二、损失函数
损失函数loss function)是用来估量模型的预测值f(x)与真实值Y的不一致程度,它是一个非负实值函数,通常使用L(Y, f(x))来表示,损失函数越小,模型的鲁棒性就越好。损失函数是经验风险函数的核心部分,也是结构风险函数重要组成部分。基本用法:

criterion=LossCriterion() #构造函数
loss=criterion(x,y)       #调用标准

后面章节将介绍损失函数种类及原理。
三、梯度置零optimizer.zero_grad()
optimizer_zero_grad()意思是把梯度清零,把loss关于weight的导数变成0.

def zero_grad(self):
     for group in self.param_groups:
         for p in group['params']:
              if p.grad in not None:
                 p.grad.detach()
                 p.grad.zero()

optimizer.zero_grad()函数会遍历模型的所有参数,这里所说的参数都是之前总述中所叙述过的torch.nn.parameter.Parameter类型变量。也就是之后的p。通过p.grad.detach_()方法截断反向传播的梯度流,再通过p.grad.zero_()函数将每个参数的梯度值设为0,即上一次的梯度记录被清空。

因为训练的过程通常使用mini-batch方法,调用backward()函数之前都要将梯度清零,因为如果梯度不清零,pytorch中会将上次计算的梯度和本次计算的梯度累加。
**优势:**硬件限制无法使用更大batchsize,代码构造成多个batchsize进行一次optimizer.zero_grad()函数调用,这样就可以使用多次计算较小的bachsize的梯度平均值来代替。
**缺点:**每次都要清零梯度:进来一个batch的数据,计算一次梯度,更新一次网络。
总结
常规情况下,每个batch需要调用一次optimizer.zero_grad()函数,把参数的梯度清零;
也可以多个batch只调用一次optimizer.zero_grad()函数,这样相当于增大了batch_size。
四、反向传播 loss.backward()
PyTorch的反向传播(即tensor.backward())是通过autograd包来实现的,autograd包会根据tensor进行过的数学运算来自动计算其对应的梯度。

具体来说,torch.tensor是autograd包的基础类,如果你设置tensor的requires_grads为True,就会开始跟踪这个tensor上面的所有运算。

如果做完运算后使用tensor.backward(),所有的梯度就会自动运算,tensor的梯度将会累加到它的.grad属性里面去。如果没有进行tensor.backward()的话,梯度值将会是None,因此loss.backward()要写在optimizer.step()之前。
五、更新参数 optimizer.step()
在看源码之前请理解几个参数使用:
lr(float,可选)—学习率
momentum(float,可选,默认0)—动量因子
weight_decay(float,可选,默认0)—权重衰减
momentum
“冲量”这个概念源自于物理中的力学,表示力对时间的积累效应。
在普通的梯度下降法x+=v
中,每次x的更新量v为v=−dx∗lr,其中dx为目标函数func(x)对x的一阶导数,。
当使用冲量时,则把每次x的更新量v考虑为本次的梯度下降量−dx∗lr与上次x的更新量v乘上一个介于[0,1][0,1]的因子momentum的和,即
v=−dx∗lr+v∗momemtum

当本次梯度下降- dx * lr的方向与上次更新量v的方向相同时,上次的更新量能够对本次的搜索起到一个正向加速的作用。 
当本次梯度下降- dx * lr的方向与上次更新量v的方向相反时,上次的更新量能够对本次的搜索起到一个减速的作用。

weight_decay
在使用梯度下降法求解目标函数func(x) = x * x的极小值时,更新公式为x += v,其中每次x的更新量v为v = - dx * lr,dx为目标函数func(x)对x的一阶导数。可以想到,如果能够让lr随着迭代周期不断衰减变小,那么搜索时迈的步长就能不断减少以减缓震荡。学习率衰减因子由此诞生:
lri=lrstart∗1.0/(1.0+decay∗i)

decay越小,学习率衰减地越慢,当decay = 0时,学习率保持不变。 
decay越大,学习率衰减地越快,当decay = 1时,学习率衰减最快。
class SGD(Optimizer):
    def __init__(self,params,lr=required,momentum=0.9,dampening=0,weight_decay=0,nesterov=False):
    def__setstate__(self,state):
        super(SGD,self).__setstate__(state)
        for group in self.param_groups:
            group_setdefault('nesterov',False)
	def step(self,closure=None):
    	loss=None
	    if closure is not None:
	       loss=closure()
	    for group in self.param_groups:
	        weight_decay=group['weight_decay']
	        momentum=group['momentum']
	        dampening=group['dampening']
	        nesterov=group['nesterov']
	        for p in group['params']:
	            if p.grad is None:
	               continue
	            d_p=p.grad.data
	            if weight_decay!=0:
	               d_p=d_p.add_(p.data,weight_decay) #d_p=d_p+weigt_decap*p.data
	            if momentum!=0:
	               param_state=self.state[p]
	               if 'momentum_uffer' not in param_state:
	                   buf=param_sate['momentum_buffer']=torch.zeros_like(p.data)   
	                   buf.mul_(momentum).add_(1-dampening,d_p) #buf=momentum*buf+(1-dampening)*d_p     
	                   if nesterov:
                        d_p = d_p.add(momentum, buf)# d_p = d_p + momentum * buf
                    else:
                        d_p = buf
                  p.data.add_(-group['lr'], d_p)# p = p - lr * d_p
        return loss            

六、参数更新lr_scheduler.step()
训练时需我们通过一定机制来调整学习率,这个时候可以借助于torch.optim.lr_scheduler类来进行调整;torch.optim.lr_scheduler模块提供了一些根据epoch训练次数来调整学习率(learning rate)的方法。一般情况下我们会设置随着epoch的增大而逐渐减小学习率从而达到更好的训练效果。
下面介绍了一种调整策略机制:StepLR机制;

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

参数:
optimizer (Optimizer):要更改学习率的优化器;
step_size(int):每训练step_size个epoch,更新一次参数;
gamma(float):更新lr的乘法因子;
last_epoch (int):最后一个epoch的index,如果是训练了很多个epoch后中断了,继续训练,这个值就等于加载的模型的epoch。默认为-1表示从头开始训练,即从epoch=1开始。
更新策略:
每过step_size个epoch,做一次更新:

new_lr =lr×gamma
其中new_lr是得到的新的学习率,lr是初始的学习率,step_size是参数step_size,

举例子说明:

import torch
import torch.nn as nn
from torch.optim.lr_scheduler import StepLR
import itertools
initial_lr = 0.1
class model(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(in_channels=3, out_channels=3, kernel_size=3)
    def forward(self, x):
        pass
net_1 = model()
optimizer_1 = torch.optim.Adam(net_1.parameters(), lr = initial_lr)
scheduler_1 = StepLR(optimizer_1, step_size=3, gamma=0.1)
print("初始化的学习率:", optimizer_1.defaults['lr']) 
for epoch in range(1, 11):
    # train
    optimizer_1.zero_grad()
    optimizer_1.step()
    print("第%d个epoch的学习率:%f" % (epoch, optimizer_1.param_groups[0]['lr']))
    scheduler_1.step()

输出为:

初始化的学习率: 0.11个epoch的学习率:0.1000002个epoch的学习率:0.1000003个epoch的学习率:0.1000004个epoch的学习率:0.0100005个epoch的学习率:0.0100006个epoch的学习率:0.0100007个epoch的学习率:0.0010008个epoch的学习率:0.0010009个epoch的学习率:0.00100010个epoch的学习率:0.000100
  • 15
    点赞
  • 59
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值