本代码模拟yolov5的学习率调整,深度解析其中torch.optim.lr_scheduler在yolov5的使用方法,有助于提高我们对该代码的理解。
为了简单实现模拟yolov5的学习率调整策略,在此代码中我使用resnet18网络,yolov5则使用的是darknet网络骨架。
在yolov5代码训练的过程中,作者对不同的层使用不同的学习率调整方法,分别分为权重层weight,偏执层bais,和BN层,单独调整不同层的学习率可以使得模型训练的更好。
另外,yolov5在对学习率更新也采用了warmp-up的的方法预热学习率。在warmp-up阶段,采用的是一维线性插值来进行对每次迭代的学习率进行更新,在warmp-up阶段以后采用的余弦退火算法来对学习率进行更新。所以接下来我们会分别详细介绍一下代码的组成以及其中所用到的方法:
如下是我模拟yolov5训练代码学习率调整的方法:
1.我定义了训练的总epoch是100,样本总数量是600个,训练的batch_size是20。
import matplotlib.pyplot as plt
import torch
import torch.optim as optim
from torch.optim.lr_scheduler import LambdaLR
from torchvision.models import resnet18
import seaborn as sns
import math
import torch.nn as nn
import numpy as np
num_epochs = 100
nums = 600
batch_size = 20
n = nums/batch_size
#定义10分类网络
model = resnet18(num_classes=10)
# optimizer parameter groups 设置了个优化组:权重,偏置,其他参数
pg0, pg1, pg2 = [], [], []
for k, v in model.named_parameters():
v.requires_grad = True
if '.bias' in k:
pg2.append(v) # biases
elif '.weight' in k and '.bn' not in k:
pg1.append(v) # apply weight decay
else:
pg0.append(v) # all else
optimizer = optim.SGD(pg0, lr=0.01,momentum=0.937, nesterov=True)
#给optimizer管理的参数组中增加新的组参数,
#可为该组参数定制lr,momentum,weight_decay 等在finetune 中常用。
optimizer.add_param_group({
'params': pg1,'weight_decay':0.0005 }) # add pg2 (biases)
optimizer.add_param_group({
'params': pg2}) # add pg2 (biases)
lf = lambda x: ((1 + math.cos(x * math.pi / num_epochs)) / 2) * (1 - 0.2) + 0.2
scheduler = LambdaLR(
optimizer=optimizer,
lr_lambda=lf, #传入一个函数或一个以函数为元素列表,作为学习率调整的策略
)
start_epoch=0
scheduler.last_epoch = start_epoch - 1
lr0,lr1,lr2, epochs = [], [], [] ,[]
optimizer.zero_grad()
for epoch in range(start_epoch,num_epochs):
for i in range(n):
#训练的迭代次数
ni = i + n * epoch
# Warmup 热身的迭代次数
if ni <= 1000:
xi = [0, 1000]
for j, x in enumerate(optimizer.param_groups):
#一维线性插值
x['lr'] = np.interp(ni, xi, [0.1 if j == 2 else 0.0, 0.01 * lf(epoch)])
if 'momentum' in x:
x['momentum'] = np.interp(ni, xi, [0.8, 0.937])
pass # iter and train here
# Scheduler 学习率衰减
lr = [x['lr'] for x in optimizer.param_groups]
lr0.append(lr[0])
lr1.append(lr[1])
lr2.append(lr[2])
#学习率更新
scheduler.step()
epochs.append(epoch)
plt.figure()
plt.subplot(221)
plt.plot(epochs, lr0, color="r",label='l0')
plt.legend()
plt.subplot(222)
plt.plot(epochs, lr1, color="b",label='l1')
plt.legend()
plt.subplot(223)
plt.plot(epochs, lr2,color="g",label='l2')
plt.legend()
plt.show()
如下图所示我分别绘制出训练100个epoch时的学习率变化情况。
如果想看懂上面的学习率调整算法,我们得需要理解余弦退火算法,它的公式和解释如下:
n e w − l r = e t a − m i n + ( i n i t i a l − l r − e t a − m i n ) ∗ ( ( 1 + c o s ( c u r − e p o c h T − m a x ∗ π ) ) / 2 ) new_-lr=eta_-min+(initial_-lr-eta_-min)*((1+cos(\frac{cur_-epoch}{T_-max}*\pi ))/2) new−lr=eta−min+(initial−lr−eta−min)∗((1+cos(T−maxcur−epoch∗π))/2)
new_-lr:新得到的学习率。
initial_lr:初始学习率。
eta_min:表示最小学习率。
cur_epoch:代表当前训练到某个epoch对应的值。
T_max: 代表训练的总epoch数。
last_epoch:最后一个epoch的index,如果是训练了很多个epoch后中断了,继续训练,这个值就等于加载的模型的epoch。默认为-1表示从头开始训练,即从epoch=1开始。
比如当我们initial_-lr
为0.01,eta_-min
为0.002,epoch
为200,T_max
为也就是200时,可以绘制如下的学习率图。
那么这个函数的具体含义是什么呢?它的结构为什么是这个形式呢?其实只看函数很简单,它就是由一个余弦函数cos和一些简单的加法乘法组合而成,接下来我来深度剖析这个函数的具体含义:
为了方便计算和展示函数的意义,假设初始学习率为1,最小学习率为0.2。我们一层一层的对函数进行解析,它的内部是cos函数,而cos函数的取值范围为[-1,1]。
我们绘制出 c o s ( c u r − e p o c h T − m a x ∗ π ) cos(\frac{cur_-epoch}{T_-max}*\pi ) cos(T−maxcur−epoch∗π)的曲线如图一所示:
( 1 + c o s ( c u r − e p o c h T − m a x ∗ π ) ) (1+cos(\frac{cur_-epoch}{T_-max}*\pi )) (1+cos(T−maxcur−epoch∗π))函数的曲线如图2所示:
( ( 1 + c o s ( c u r − e p o c h T − m a x ∗ π ) ) / 2 ) ((1+cos(\frac{cur_-epoch}{T_-max}*\pi ))/2) ((1+cos(T−maxcur−epoch∗π))/2)函数的曲线绘制如图3所示:
( i n i t i a l − l r − e t a − m i n ) ∗ ( ( 1 + c o s ( c u r − e p o c h T − m a x ∗ π ) ) / 2 ) (initial_-lr-eta_-min)*((1+cos(\frac{cur_-epoch}{T_-max}*\pi ))/2) (initial−lr−eta−min)∗((1+cos(T−maxcur−epoch∗π))/2)如图4所示:
e t a − m i n + ( i n i t i a l − l r − e t a − m i n ) ∗ ( ( 1 + c o s ( c u r − e p o c h T − m a x ∗ π ) ) / 2 ) eta_-min+(initial_-lr-eta_-min)*((1+cos(\frac{cur_-epoch}{T_-max}*\pi ))/2) eta−min+(initial−lr−eta−min)∗((1+cos(T−maxcur−epoch∗π))/2)如图5所示。
就这么简单,我们一步一步的解刨了函数的具体含义,接下类我们来解析一下预热过程中所使用的线性插值方法来更新学习率
它的定义是将一维分段线性插值返回给具有给定离散数据点(xp,fp)且在x处求值的函数。
x:待插入数据的横坐标.
xp:原始数据点的横坐标
fp:原始数据点的y坐标,与xp的长度相同。
返回值
浮点数或复数(对应于fp值)或ndarray. 插入数据的纵坐标,和x
形状相同。
例1:可以看到当x=2.5的时候,得到的y值是1。
import numpy as np
import matplotlib.pyplot as plt
x = 2.5
xp = [1, 2, 3]
fp = [3, 2, 0]
y = np.interp(x, xp, fp) # 1.0
plt.plot(xp, fp,'ro')
plt.plot(x, y, 'x')
plt.show()
如下图所示,其中蓝色的点是插值后的点。
例2:在红色的十个点之间进行插入50个蓝色标记的点如下所示:
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(0, 2 * np.pi, 10)
y = np.sin(x)
xvals = np.linspace(0, 2 * np.pi, 50)
yinterp = np.interp(xvals, x, y)
plt.plot(x, y, 'ro')
plt.plot(xvals, yinterp, 'x')
plt.show()