前期回顾:
Pytorch学习笔记(1):基本概念、安装、张量操作、逻辑回归
Pytorch学习笔记(2):数据读取机制(DataLoader与Dataset)
Pytorch学习笔记(3):图像的预处理(transforms)
Pytorch学习笔记(4):模型创建(Module)、模型容器(Containers)、AlexNet构建
Pytorch学习笔记(5):torch.nn---网络层介绍(卷积层、池化层、线性层、激活函数层)
一、优化器
1.1 优化器的介绍
pytorch的优化器:管理并更新模型中可学习参数的值,使得模型输出更接近真实标签
导数:函数在指定坐标轴上的变化率
方向导数:指定方向上的变化率
梯度:一个向量,方向为方向导数取得最大值的方向
梯度下降:朝着梯度下降的负方向去变化,下降是最快的
1.2 optimizer的属性
- defaults:优化器的超参数(存储一些学习率,momentum的值,weight_decay等)
- state:参数的缓存,如momentum的缓存(使用前几次梯度进行平均)
- param_groups:管理的参数组, 这是个列表,每一个元素是一个字典,在字典中有key,key里面的值才是我们真正的参数(这个很重要, 进行参数管理)
- _step_count:记录更新次数,学习率调整中使用。 比如迭代100次之后更新学习率的时候,就得记录这里的100。
1.3 optimizer的方法
基本方法
• zero_grad():清空所管理参数的梯度, 这里注意Pytorch有一个特性就是张量梯度不自动清零
• step():执行一步更新
class Optimizer(object): def zero_grad(self): for group in self.param_groups: for p in group['params']: if p.grad is not None: p.grad.detach_() p.grad.zero_()
class Optimizer(object): def __init__(self, params, defaults): self.defaults = defaults self.state = defaultdict(dict) self.param_groups = []
• add_param_group():添加参数组,不同的组有不同的超参数。
例如:fine_tune中,我们对模型前面这些特征提取部分的权重参数,希望学习率小一些,更新的慢一些,将其设置为一组参数;而在后面的全连接层,我们希望其学习率大一些,更新的快一些,这些设置为一组参数。
class Optimizer(object): def add_param_group(self, param_group): for group in self.param_groups: param_set.update(set(group['params’])) self.param_groups.append(param_group)
• state_dict():获取优化器当前状态信息字典
• load_state_dict() :加载状态信息字典
这两个方法的作用是回复断点的训练,例如,一个模型需要训练十天,而在第三天的时候因为断电等问题停止了训练,而我们重新启动训练时就不需要从0开始了,直接从断点处加载状态信息,继续训练。
class Optimizer(object): def state_dict(self): return { 'state': packed_state, 'param_groups': param_groups, } def load_state_dict(self, state_dict):
pytorch特性:张量梯度不自动清零
下面我们学习一下优化器具体使用方法:
(1)构建可学习参数和学习率
代码:
set_seed(1) # 设置随机种子 #构建可学习参数 weight = torch.randn((2, 2), requires_grad=True) weight.grad = torch.ones((2, 2)) #传入可学习参数,学习率设置为1 optimizer = optim.SGD([weight], lr=0.1)
(2)step(): 一次梯度下降更新参数
代码:
# ----------------------------------- step ----------------------------------- print("weight before step:{}".format(weight.data)) optimizer.step() # 修改lr=1 0.1观察结果 print("weight after step:{}".format(weight.data))
输出结果:
(3)zero_grad:更新梯度,将梯度清零
代码:
# ----------------------------------- zero_grad ----------------------------------- print("weight before step:{}".format(weight.data)) optimizer.step() # 修改lr=1 0.1观察结果 print("weight after step:{}".format(weight.data)) print("weight in optimizer:{}\nweight in weight:{}\n".format(id(optimizer.param_groups[0]['params'][0]), id(weight))) print("weight.grad is {}\n".format(weight.grad)) optimizer.zero_grad() print("after optimizer.zero_grad(), weight.grad is\n{}".format(weight.grad))
输出结果:
(4)add_param_groups:增加组参数
代码:
# ----------------------------------- add_param_group ----------------------------------- print("optimizer.param_groups is\n{}".format(optimizer.param_groups)) w2 = torch.randn((3, 3), requires_grad=True) optimizer.add_param_group({"params": w2, 'lr': 0.0001}) print("optimizer.param_groups is\n{}".format(optimizer.param_groups))
输出结果:
(5)state_dict:保存优化器的状态信息
代码:进行十次更新
# ----------------------------------- state_dict ----------------------------------- optimizer = optim.SGD([weight], lr=0.1, momentum=0.9) opt_state_dict = optimizer.state_dict() print("state_dict before step:\n", opt_state_dict) for i in range(10): optimizer.step() print("state_dict after step:\n", optimizer.state_dict()) #保存状态信息 torch.save(optimizer.state_dict(), os.path.join(BASE_DIR, "optimizer_state_dict.pkl"))
输出结果:
(6)load_state_dict:加载状态信息
在之前的十次更新后继续更新,而非从头更新。
举个栗子:假设我们训练一个模型,训练10次,此时停电了,该怎么办?难道要像个大冤种一样从头开始吗?漏!state_dict()方法里面保存了优化器的各种状态信息,我们通过torch.save就可以将这些状态保存到文件(.pkl),然后就可以通过load_state_dict()来导入这个状态信息,让优化器在这个基础上进行训练,而不用从头开始啦!
代码:
# -----------------------------------load state_dict ----------------------------------- optimizer = optim.SGD([weight], lr=0.1, momentum=0.9) state_dict = torch.load(os.path.join(BASE_DIR, "optimizer_state_dict.pkl")) print("state_dict before load state:\n", optimizer.state_dict()) optimizer.load_state_dict(state_dict) print("state_dict after load state:\n", optimizer.state_dict())
输出结果:
1.4 常用优化器
(1) torch.optim.SGD
主要功能:随机梯度下降法
主要参数:
- params:管理的参数组list,list的元素是字典,key = params主要用来管理模型实际参数
- lr:初始学习率
- momentum:动量系数,β
- weight_decay:L2正则化系数
- nesterov:是否采用NAG梯度下降方法,布尔变量
常用优化器:
- optim.SGD: 随机梯度下降法
- optim.Adagrad: 自适应学习率梯度下降法
- optim.RMSprop: Adagrad的改进
- optim.Adadelta: Adagrad的改进
- optim.Adam: RMSprop结合Momentum
- optim.Adamax: Adam增加学习率上限
- optim.SparseAdam: 稀疏版的Adam
- optim.ASGD: 随机平均梯度下降
- optim.Rprop: 弹性反向传播
- optim.LBFGS: BFGS的改进
二、学习率
2.1 学习率介绍
学习率(Learning rate):作为监督学习以及深度学习中重要的超参,其决定着目标函数能否收敛到局部最小值以及何时收敛到最小值。合适的学习率能够使目标函数在合适的时间内收敛到局部最小值。
这里我们以梯度下降为例,来观察一下不同的学习率对代价函数的收敛过程的影响:
当学习率设置的过小时,收敛过程如下:
当学习率设置的过大时,收敛过程如下:
由上图可以看出来,当学习率设置的过小时,收敛过程将变得十分缓慢。而当学习率设置的过大时,梯度可能会在最小值附近来回震荡,甚至可能无法收敛。
我们通过代码进一步理解一下:
代码:
# ------------------------------ gradient descent ------------------------------ #记录loss和迭代次数用来画loss曲线 iter_rec, loss_rec, x_rec = list(), list(), list() lr = 0.5 # /1. /.5 /.2 /.1 /.125 max_iteration = 4 # /1. 4 /.5 4 /.2 20 200 for i in range(max_iteration): y = func(x) y.backward() print("Iter:{}, X:{:8}, X.grad:{:8}, loss:{:10}".format( i, x.detach().numpy()[0], x.grad.detach().numpy()[0], y.item())) x_rec.append(x.item()) x.data.sub_(lr * x.grad) # x -= x.grad 数学表达式意义: x = x - x.grad # 0.5 0.2 0.1 0.125 x.grad.zero_() iter_rec.append(i) loss_rec.append(y.detach().numpy()) plt.subplot(121).plot(iter_rec, loss_rec, '-ro') plt.xlabel("Iteration") plt.ylabel("Loss value") x_t = torch.linspace(-3, 3, 100) y = func(x_t) plt.subplot(122).plot(x_t.numpy(), y.numpy(), label="y = 4*x^2") plt.grid() y_rec = [func(torch.tensor(i)).item() for i in x_rec] plt.subplot(122).plot(x_rec, y_rec, '-ro') plt.legend() plt.show()
首先,我们设置学习率为1,看一下输出结果:
我们可以看上面的图像,loss是不断上升的,这说明这个跨度是有问题的
接着,我们修改学习率,设置为0.1,再看一下输出结果:
我们可以看到,loss减少了
然后,我们把迭代次数设置的大一点,看loss是否还会继续降低
经过多次尝试,我们发现当lr设置为0.125,loss值就可以一步到达0
我们发现,当loss上升不降的时候,有可能是学习率的问题,所以我们一般会尝试一个小的学习率。 慢慢的去进行优化。
最后,我们设置多个学习率来观察不同大小的lr对loss 的影像
代码:
# ------------------------------ multi learning rate ------------------------------ iteration = 100 num_lr = 10 lr_min, lr_max = 0.01, 0.2 # .5 .3 .2 lr_list = np.linspace(lr_min, lr_max, num=num_lr).tolist() loss_rec = [[] for l in range(len(lr_list))] iter_rec = list() for i, lr in enumerate(lr_list): x = torch.tensor([2.], requires_grad=True) for iter in range(iteration): y = func(x) y.backward() x.data.sub_(lr * x.grad) # x.data -= x.grad x.grad.zero_() loss_rec[i].append(y.item()) for i, loss_r in enumerate(loss_rec): plt.plot(range(len(loss_r)), loss_r, label="LR: {}".format(lr_list[i])) plt.legend() plt.xlabel('Iterations') plt.ylabel('Loss value') plt.show()
输出结果:
综上所述,lr 从来控制更新的步伐,设置学习率时,不能设置的太大,如0.3,0.5,此时的loss值是激增,容易引发梯度爆炸;也不能设置太小,设置太小就很难收敛。
2.2 为什么要调整学习率
学习率是可以控制更新的步伐的。 我们在训练模型的时候,一般开始的时候学习率会比较大,这样可以以一个比较快的速度到达最优点的附近,然后再把学习率降下来, 缓慢的去收敛到最优值。
举个栗子:以打高尔夫为例,开始的时候击球的力度会大一些,让高尔夫球落在洞口附近。然后来到洞口,这次就需要小一点的击球力度,让球逐渐靠近洞口。
再看看函数的例子:
在模型的训练过程中,需要调整学习率,设置较大的学习率,让loss快速下降,此时曲线是震荡的
如果继续采用较大的学习率,那loss就无法继续下降。学习率前期要大,后期要小。
2.3 pytorch的六种学习率调整策略
(1)StepLR
功能:等间隔调整学习率
主要参数:
- step_size:调整间隔数(每个n个epoch调整学习率)
- gamma:调整系数
调整方式:lr = lr * gamma
代码:
torch.manual_seed(1) LR = 0.1 iteration = 10 max_epoch = 200 # ------------------------------ fake data and optimizer ------------------------------ weights = torch.randn((1), requires_grad=True) target = torch.zeros((1)) optimizer = optim.SGD([weights], lr=LR, momentum=0.9)
# ------------------------------ 1 Step LR ------------------------------ scheduler_lr = optim.lr_scheduler.StepLR(optimizer, step_size=50, gamma=0.1) # 设置学习率下降策略 lr_list, epoch_list = list(), list() for epoch in range(max_epoch): # 获取当前lr,新版本用 get_last_lr()函数,旧版本用get_lr()函数,具体看UserWarning lr_list.append(scheduler_lr.get_last_lr()) epoch_list.append(epoch) for i in range(iteration): loss = torch.pow((weights - target), 2) loss.backward() optimizer.step() optimizer.zero_grad() scheduler_lr.step() plt.plot(epoch_list, lr_list, label="Step LR Scheduler") plt.xlabel("Epoch") plt.ylabel("Learning rate") plt.legend() plt.show()
输出:
(2)MultiStepLR
功能:按给定间隔调整学习率
主要参数:
- milestones:设定调整时刻数 eg:自定义间隔数[50,125,180]
- gamma:调整系数
调整方式:lr = lr * gamma
代码:
# ------------------------------ 2 Multi Step LR ------------------------------ milestones = [50, 125, 160] scheduler_lr = optim.lr_scheduler.MultiStepLR(optimizer, milestones=milestones, gamma=0.1) lr_list, epoch_list = list(), list() for epoch in range(max_epoch): lr_list.append(scheduler_lr.get_lr()) epoch_list.append(epoch) for i in range(iteration): loss = torch.pow((weights - target), 2) loss.backward() optimizer.step() optimizer.zero_grad() scheduler_lr.step() plt.plot(epoch_list, lr_list, label="Multi Step LR Scheduler\nmilestones:{}".format(milestones)) plt.xlabel("Epoch") plt.ylabel("Learning rate") plt.legend() plt.show()
输出:
(3)ExponentialLR
功能:按指数衰减调整学习率
主要参数:
- gamma:指数的底(一般设置为接近1的一个数)
调整方式:lr = lr * gamma ** epoch
代码:
# ------------------------------ 3 Exponential LR ------------------------------ gamma = 0.95 scheduler_lr = optim.lr_scheduler.ExponentialLR(optimizer, gamma=gamma) lr_list, epoch_list = list(), list() for epoch in range(max_epoch): lr_list.append(scheduler_lr.get_lr()) epoch_list.append(epoch) for i in range(iteration): loss = torch.pow((weights - target), 2) loss.backward() optimizer.step() optimizer.zero_grad() scheduler_lr.step() plt.plot(epoch_list, lr_list, label="Exponential LR Scheduler\nmilestones:{}".format(gamma)) plt.xlabel("Epoch") plt.ylabel("Learning rate") plt.legend() plt.show()
输出:
(4)CosineAnnealingLR
功能:余弦周期调整学习率
主要参数:
- T_max:下降周期,半个周期(整个周期为两倍的T_max)只是往下的那一块。
- eta_min:学习率下限(通常设置为0)
代码:
# ------------------------------ 4 Cosine Annealing LR ------------------------------ t_max = 50 scheduler_lr = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=t_max, eta_min=0.) lr_list, epoch_list = list(), list() for epoch in range(max_epoch): lr_list.append(scheduler_lr.get_lr()) epoch_list.append(epoch) for i in range(iteration): loss = torch.pow((weights - target), 2) loss.backward() optimizer.step() optimizer.zero_grad() scheduler_lr.step() plt.plot(epoch_list, lr_list, label="CosineAnnealing LR Scheduler\nmilestones:{}".format(t_max)) plt.xlabel("Epoch") plt.ylabel("Learning rate") plt.legend() plt.show()
输出:
(5)ReduceLRonPlateau
功能:监控指标,当指标不再变化则调整。可以监控loss或者准确率,当不再变化的时候,我们再去调整。
主要参数:
- mode:min/max 两种模式
- min:如果监控指标不再下降则调整学习率,一般用来监控loss
- max:如果监控指标不再上升则调整学习率,一般用来监控accuracy
- factor:调整系数
- patience:“耐心”,接受几次不变化
- 监控指标连续n次没有下降/增长,则调整学习率
- cooldown:“冷却时间”,停止监控一段时间
- 调整完学习率后一段时间内不再监控指标
- verbose:是否打印日志
- min_lr:学习率下限
- eps:学习率衰减最小值
代码:
# ------------------------------ 5 Reduce LR On Plateau ------------------------------ loss_value = 0.5 accuray = 0.9 factor = 0.1 mode = "min" patience = 10 # 连续10次损失不下降时执行一次lr=lr*0.1 cooldown = 10 # 先冷却10个epoch,这时候不监控 min_lr = 1e-4 verbose = True # 会打印多少次信息,更新多少次学习率 scheduler_lr = optim.lr_scheduler.ReduceLROnPlateau(optimizer, factor=factor, mode=mode, patience=patience, cooldown=cooldown, min_lr=min_lr, verbose=verbose) for epoch in range(max_epoch): for i in range(iteration): # train(...) optimizer.step() optimizer.zero_grad() #if epoch == 5: # loss_value = 0.4 #放入监控标量 scheduler_lr.step(loss_value)
输出:
(6)LambdaLR
功能:自定义调整策略
主要参数:
- lr_lambda:function or list
代码:
# ------------------------------ 6 lambda ------------------------------ lr_init = 0.1 weights_1 = torch.randn((6, 3, 5, 5)) weights_2 = torch.ones((5, 5)) optimizer = optim.SGD([ {'params': [weights_1]}, {'params': [weights_2]}], lr=lr_init) lambda1 = lambda epoch: 0.1 ** (epoch // 20) lambda2 = lambda epoch: 0.95 ** epoch scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=[lambda1, lambda2]) lr_list, epoch_list = list(), list() for epoch in range(max_epoch): for i in range(iteration): # train(...) optimizer.step() optimizer.zero_grad() scheduler.step() lr_list.append(scheduler.get_lr()) epoch_list.append(epoch) print('epoch:{:5d}, lr:{}'.format(epoch, scheduler.get_lr())) plt.plot(epoch_list, [i[0] for i in lr_list], label="lambda 1") plt.plot(epoch_list, [i[1] for i in lr_list], label="lambda 2") plt.xlabel("Epoch") plt.ylabel("Learning Rate") plt.title("LambdaLR") plt.legend() plt.show()
输出:
下面我们总结一下学习率:
(1)有序调整:Step、MultiStep、Exponential和CosineAnnealing
(2)自适应调整:ReduceLROnPlateau
(3)自定义调整:Lambda
三、动量
momentum(动量、冲量):结合当前梯度与上一次更新信息,用于当前更新
(图片来源:翻滚的小@强)
指数加权平均:在时间序列中经常使用的求取平均值的方法,求取当前时刻的平均值,距离平均值越近的参数值,参考性越大,所占的权重也越大。
代码:
#构建权重的计算公式,观察每一个时刻的权重 def exp_w_func(beta, time_list): return [(1 - beta) * np.power(beta, exp) for exp in time_list] beta = 0.9 num_point = 100 time_list = np.arange(num_point).tolist()
# ------------------------------ exponential weight ------------------------------ weights = exp_w_func(beta, time_list) plt.plot(time_list, weights, '-ro', label="Beta: {}\ny = B^t * (1-B)".format(beta)) plt.xlabel("time") plt.ylabel("weight") plt.legend() plt.title("exponentially weighted average") plt.show() print(np.sum(weights))
输出:
当time = 0,时权重为0.1,越往前权重越小,距离当前时刻越远,对当前时刻平均值的影响就越小
设置不同的β,看一下权重的变化 :
# ------------------------------ multi weights ------------------------------ beta_list = [0.98, 0.95, 0.9, 0.8] w_list = [exp_w_func(beta, time_list) for beta in beta_list] for i, w in enumerate(w_list): plt.plot(time_list, w, label="Beta: {}".format(beta_list[i])) plt.xlabel("time") plt.ylabel("weight") plt.legend() plt.show()
输出结果:
β可以理解为记忆周期的概念,β值越小,记忆周期越短。
举个栗子:
- β = 0.8时,记忆周期就很多,仅在20左右
- β = 0.98时,记忆周期就比较长
通常,我们会将β设置为0.9,它可以更关注当前时刻十天左右的情况。10 = 1/(1-β))
了解了指数加权平均,我们再来看momentum。其实所谓的momentum梯度下降, 基本的想法是计算梯度的指数加权平均数,并利用该梯度更新权重 。
梯度下降(原来的梯度下降方式):
pytorch中更新公式(加入动量之后的梯度下降方式):
这里lr乘的不是梯度了,而是变化量,当前的变化量也会考虑到之前的梯度
接下来我们看看带momentum的loss变化情况:
代码:
# ------------------------------ SGD momentum ------------------------------ def func(x): return torch.pow(2*x, 2) # y = (2x)^2 = 4*x^2 dy/dx = 8x iteration = 100 m = 0.9 # .9 .63 lr_list = [0.01, 0.03] momentum_list = list() loss_rec = [[] for l in range(len(lr_list))] iter_rec = list() for i, lr in enumerate(lr_list): x = torch.tensor([2.], requires_grad=True) momentum = 0. if lr == 0.03 else m momentum_list.append(momentum) optimizer = optim.SGD([x], lr=lr, momentum=momentum) for iter in range(iteration): y = func(x) y.backward() optimizer.step() optimizer.zero_grad() loss_rec[i].append(y.item()) for i, loss_r in enumerate(loss_rec): plt.plot(range(len(loss_r)), loss_r, label="LR: {} M:{}".format(lr_list[i], momentum_list[i])) plt.legend() plt.xlabel('Iterations') plt.ylabel('Loss value') plt.show()
输出结果:
lr设置为0.01和0.03,首先两个都不加momentum
可以看到,学习率大的收敛更快
接着我们给学习率0.01的这个加一个动量momentum=0.9, 再看看效果:
输出结果:
在最开始的低点,是比lr = 0.03更快的,但是又开始震荡, 这说明动量设置的太大了。前面梯度一旦大小变化,这里就会震荡,当然会发现震荡会越来越小最后趋于平缓,这是因为不断平均的梯度越来越多。
接着我们给将动量设置小一点吧,动量momentum=0.65, 再看看效果:
输出结果:
可以看到,小学习率下降的更快了
由此可知,合适的动量可以加速loss的下降
本文参考: