在用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种:
参数传入方式有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.1
第1个epoch的学习率:0.100000
第2个epoch的学习率:0.100000
第3个epoch的学习率:0.100000
第4个epoch的学习率:0.010000
第5个epoch的学习率:0.010000
第6个epoch的学习率:0.010000
第7个epoch的学习率:0.001000
第8个epoch的学习率:0.001000
第9个epoch的学习率:0.001000
第10个epoch的学习率:0.000100