训练神经网络或大型深度学习模型是一项困难的优化任务。
训练神经网络的经典算法叫做随机梯度下降。众所周知,通过使用在训练期间变化的学习率,可以在某些问题上提高性能并加快训练速度。
在这篇文章中,您将了解什么是学习率调度,以及如何在 PyTorch 中为神经网络模型使用不同的学习率调度。
阅读本文后,你将了解:
- 学习率调度在模型训练中的作用
- 如何在 PyTorch 训练循环中使用学习率调度
- 如何设置您自己的学习率调度
想开始使用PyTorch进行深度学习吗?
立即参加我的免费电子邮件速成课程(附示例代码)。
点击注册,同时获得该课程的免费PDF电子书版本。
让我们开始吧。

在 PyTorch 训练中使用学习率调度
照片作者:Cheung Yin。部分权利保留。
概述
这篇博文分为三部分;它们是:
- 用于训练模型的学习率调度
- 在 PyTorch 训练中应用学习率调度
- 自定义学习率调度
用于训练模型的学习率调度
梯度下降是一种数值优化算法。它的作用是使用以下公式更新参数:
$$
w := w – \alpha \dfrac{dy}{dw}
$$
在这个公式中,$w$ 是参数,例如神经网络中的权重,而 $y$ 是目标,例如损失函数。它的作用是将 $w$ 移动到可以最小化 $y$ 的方向。方向由微分 $\dfrac{dy}{dw}$ 提供,但 $w$ 的移动量由学习率 $\alpha$ 控制。
一个简单的开始是在梯度下降算法中使用恒定的学习率。但是,通过学习率调度,您可以做得更好。调度是为了使学习率适应梯度下降优化过程,从而提高性能并缩短训练时间。
在神经网络训练过程中,数据以批次的形式输入网络,每个时期(epoch)包含许多批次。每个批次会触发一次训练步骤,梯度下降算法会更新一次参数。然而,通常学习率调度是每个训练时期才更新一次。
您可以每步都更新学习率,但通常每个时期更新一次,因为您需要了解网络的性能才能确定学习率应如何更新。通常,模型会根据验证数据集每时期进行一次评估。
有多种使学习率自适应的方法。在训练开始时,您可能更喜欢较大的学习率,这样可以粗略地改进网络以加快进度。在非常复杂的神经网络模型中,您可能还希望在开始时逐渐增加学习率,因为您需要网络在预测的不同维度上进行探索。然而,在训练结束时,您总是希望学习率更小。因为那时,您即将从模型中获得最佳性能,并且如果学习率较大,很容易发生过拟合。
因此,在训练过程中调整学习率最简单也是也许最常用的方法是随着时间的推移降低学习率的技术。这些技术的好处是在训练过程的开始阶段,当使用较大的学习率值时,可以进行大的改动,并在训练过程的后期将学习率降低,从而对权重进行更小的改动。
这使得在早期可以快速学习到好的权重,并在后期进行微调。
接下来,我们看看如何在 PyTorch 中设置学习率调度。
通过我的《用PyTorch进行深度学习》一书来启动你的项目。它提供了包含可用代码的自学教程。
在 PyTorch 训练中应用学习率调度
在 PyTorch 中,模型由优化器更新,学习率是优化器的参数。学习率调度是一种用于更新优化器中学习率的算法。
下面是一个创建学习率调度的示例
1 2 3 4 5 |
import torch import torch.optim as optim import torch.optim.lr_scheduler as lr_scheduler scheduler = lr_scheduler.LinearLR(optimizer, start_factor=1.0, end_factor=0.3, total_iters=10) |
PyTorch 在 torch.optim.lr_scheduler
子模块中提供了许多学习率调度器。所有调度器都需要优化器作为第一个参数进行更新。根据调度器的不同,您可能需要提供更多参数来设置。
让我们从一个示例模型开始。下面是一个用于解决电离层二分类问题的模型。这是一个小型数据集,您可以从 UCI 机器学习存储库下载。将数据文件放在您的工作目录中,文件名设置为 ionosphere.csv
。
电离层数据集非常适合练习神经网络,因为所有输入值都是较小的数值,且尺度相同。
构建了一个小型神经网络模型,包含一个具有 34 个神经元的隐藏层,使用 ReLU 激活函数。输出层有一个神经元,并使用 sigmoid 激活函数,以便输出类似概率的值。
使用纯随机梯度下降算法,学习率为固定的 0.1。模型训练 50 个时期。优化器的状态参数可以在 optimizer.param_groups
中找到;学习率是 optimizer.param_groups[0]["lr"]
中的一个浮点数值。在每个时期结束时,会打印出优化器的学习率。
完整的示例如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
import numpy as np import pandas as pd import torch import torch.nn as nn import torch.optim as optim from sklearn.preprocessing import LabelEncoder from sklearn.model_selection import train_test_split # 加载数据集,分割为输入(X)和输出(y)变量 dataframe = pd.read_csv("ionosphere.csv", header=None) dataset = dataframe.values X = dataset[:,0:34].astype(float) y = dataset[:,34] # 将类别值编码为整数 编码器 = LabelEncoder() 编码器。fit(y) y = encoder.transform(y) # 转换为 PyTorch 张量 X = torch.tensor(X, dtype=torch.float32) y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1) # 用于模型评估的训练-测试分割 X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.7, shuffle=True) # 创建模型 model = nn.Sequential( nn.Linear(34, 34), nn.ReLU(), nn.Linear(34, 1), nn.Sigmoid() ) # 训练模型 n_epochs = 50 batch_size = 24 batch_start = torch.arange(0, len(X_train), batch_size) lr = 0.1 loss_fn = nn.BCELoss() optimizer = optim.SGD(model.parameters(), lr=lr) model.train() for epoch in range(n_epochs): for start in batch_start: X_batch = X_train[start:start+batch_size] y_batch = y_train[start:start+batch_size] y_pred = model(X_batch) loss = loss_fn(y_pred, y_batch) optimizer.zero_grad() loss.backward() optimizer.step() print("Epoch %d: SGD lr=%.4f" % (epoch, optimizer.param_groups[0]["lr"])) # 训练后评估准确率 model.eval() y_pred = model(X_test) acc = (y_pred.round() == y_test).float().mean() acc = float(acc) print("Model accuracy: %.2f%%" % (acc*100)) |
运行此模型会产生:
1 2 3 4 5 6 7 8 9 10 11 12 |
Epoch 0: SGD lr=0.1000 Epoch 1: SGD lr=0.1000 Epoch 2: SGD lr=0.1000 Epoch 3: SGD lr=0.1000 Epoch 4: SGD lr=0.1000 ... Epoch 45: SGD lr=0.1000 Epoch 46: SGD lr=0.1000 Epoch 47: SGD lr=0.1000 Epoch 48: SGD lr=0.1000 Epoch 49: SGD lr=0.1000 Model accuracy: 86.79% |
您可以确认在整个训练过程中学习率没有变化。让我们让训练过程以较大的学习率开始,并以较小的学习率结束。要引入学习率调度器,您需要在训练循环中运行其 step()
函数。上面的代码修改如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
import numpy as np import pandas as pd import torch import torch.nn as nn import torch.optim as optim import torch.optim.lr_scheduler as lr_scheduler from sklearn.preprocessing import LabelEncoder from sklearn.model_selection import train_test_split # 加载数据集,分割为输入(X)和输出(y)变量 dataframe = pd.read_csv("ionosphere.csv", header=None) dataset = dataframe.values X = dataset[:,0:34].astype(float) y = dataset[:,34] # 将类别值编码为整数 编码器 = LabelEncoder() 编码器。fit(y) y = encoder.transform(y) # 转换为 PyTorch 张量 X = torch.tensor(X, dtype=torch.float32) y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1) # 用于模型评估的训练-测试分割 X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.7, shuffle=True) # 创建模型 model = nn.Sequential( nn.Linear(34, 34), nn.ReLU(), nn.Linear(34, 1), nn.Sigmoid() ) # 训练模型 n_epochs = 50 batch_size = 24 batch_start = torch.arange(0, len(X_train), batch_size) lr = 0.1 loss_fn = nn.BCELoss() optimizer = optim.SGD(model.parameters(), lr=lr) scheduler = lr_scheduler.LinearLR(optimizer, start_factor=1.0, end_factor=0.5, total_iters=30) model.train() for epoch in range(n_epochs): for start in batch_start: X_batch = X_train[start:start+batch_size] y_batch = y_train[start:start+batch_size] y_pred = model(X_batch) loss = loss_fn(y_pred, y_batch) optimizer.zero_grad() loss.backward() optimizer.step() before_lr = optimizer.param_groups[0]["lr"] scheduler.step() after_lr = optimizer.param_groups[0]["lr"] print("Epoch %d: SGD lr %.4f -> %.4f" % (epoch, before_lr, after_lr)) # 训练后评估准确率 model.eval() y_pred = model(X_test) acc = (y_pred.round() == y_test).float().mean() acc = float(acc) print("Model accuracy: %.2f%%" % (acc*100)) |
输出结果为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Epoch 0: SGD lr 0.1000 -> 0.0983 Epoch 1: SGD lr 0.0983 -> 0.0967 Epoch 2: SGD lr 0.0967 -> 0.0950 Epoch 3: SGD lr 0.0950 -> 0.0933 Epoch 4: SGD lr 0.0933 -> 0.0917 ... Epoch 28: SGD lr 0.0533 -> 0.0517 Epoch 29: SGD lr 0.0517 -> 0.0500 Epoch 30: SGD lr 0.0500 -> 0.0500 Epoch 31: SGD lr 0.0500 -> 0.0500 ... Epoch 48: SGD lr 0.0500 -> 0.0500 Epoch 49: SGD lr 0.0500 -> 0.0500 Model accuracy: 88.68% |
在上面的代码中,使用了 LinearLR()
。它是一个线性学习率调度器,接受三个附加参数:start_factor
、end_factor
和 total_iters
。您将 start_factor
设置为 1.0,end_factor
设置为 0.5,total_iters
设置为 30,因此它会在 10 个等步长中使乘数因子从 1.0 减少到 0.5。在 10 步之后,因子将保持在 0.5。然后将此因子乘以优化器中的原始学习率。因此,您将看到学习率从 $0.1\times 1.0 = 0.1$ 降低到 $0.1\times 0.5 = 0.05$。
除了 LinearLR()
,您还可以使用 ExponentialLR()
,其语法为:
1 |
scheduler = lr_scheduler.ExponentialLR(optimizer, gamma=0.99) |
如果用这个替换 LinearLR()
,您将看到学习率更新如下:
1 2 3 4 5 6 7 8 9 10 11 |
Epoch 0: SGD lr 0.1000 -> 0.0990 Epoch 1: SGD lr 0.0990 -> 0.0980 Epoch 2: SGD lr 0.0980 -> 0.0970 Epoch 3: SGD lr 0.0970 -> 0.0961 Epoch 4: SGD lr 0.0961 -> 0.0951 ... Epoch 45: SGD lr 0.0636 -> 0.0630 Epoch 46: SGD lr 0.0630 -> 0.0624 Epoch 47: SGD lr 0.0624 -> 0.0617 Epoch 48: SGD lr 0.0617 -> 0.0611 Epoch 49: SGD lr 0.0611 -> 0.0605 |
其中学习率通过在每个调度器更新时乘以一个常数因子 gamma
来更新。
自定义学习率调度
没有一般规则说明哪种学习率调度器效果最好。有时,您可能需要一个 PyTorch 未提供的特殊学习率调度器。可以使用自定义函数定义自定义学习率调度。例如,您希望学习率在时期 $n$ 时为
$$
lr_n = \dfrac{lr_0}{1 + \alpha n}
$$
其中 $lr_0$ 是初始学习率(时期 0 时),而 $\alpha$ 是一个常数。您可以实现一个函数,该函数给定时期 $n$ 来计算学习率 $lr_n$。
1 2 3 4 5 |
def lr_lambda(epoch): # LR 为 0.1 * (1/1+0.01*epoch) base_lr = 0.1 factor = 0.01 return base_lr/(1+factor*epoch) |
然后,您可以设置一个 LambdaLR()
来根据此函数更新学习率:
1 |
scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda) |
通过修改之前的示例来使用 LambdaLR()
,您将得到以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
import numpy as np import pandas as pd import torch import torch.nn as nn import torch.optim as optim import torch.optim.lr_scheduler as lr_scheduler from sklearn.preprocessing import LabelEncoder from sklearn.model_selection import train_test_split # 加载数据集,分割为输入(X)和输出(y)变量 dataframe = pd.read_csv("ionosphere.csv", header=None) dataset = dataframe.values X = dataset[:,0:34].astype(float) y = dataset[:,34] # 将类别值编码为整数 编码器 = LabelEncoder() 编码器。fit(y) y = encoder.transform(y) # 转换为 PyTorch 张量 X = torch.tensor(X, dtype=torch.float32) y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1) # 用于模型评估的训练-测试分割 X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.7, shuffle=True) # 创建模型 model = nn.Sequential( nn.Linear(34, 34), nn.ReLU(), nn.Linear(34, 1), nn.Sigmoid() ) def lr_lambda(epoch): # LR 为 0.1 * (1/1+0.01*epoch) base_lr = 0.1 factor = 0.01 return base_lr/(1+factor*epoch) # 训练模型 n_epochs = 50 batch_size = 24 batch_start = torch.arange(0, len(X_train), batch_size) lr = 0.1 loss_fn = nn.BCELoss() optimizer = optim.SGD(model.parameters(), lr=lr) scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda) model.train() for epoch in range(n_epochs): for start in batch_start: X_batch = X_train[start:start+batch_size] y_batch = y_train[start:start+batch_size] y_pred = model(X_batch) loss = loss_fn(y_pred, y_batch) optimizer.zero_grad() loss.backward() optimizer.step() before_lr = optimizer.param_groups[0]["lr"] scheduler.step() after_lr = optimizer.param_groups[0]["lr"] print("Epoch %d: SGD lr %.4f -> %.4f" % (epoch, before_lr, after_lr)) # 训练后评估准确率 model.eval() y_pred = model(X_test) acc = (y_pred.round() == y_test).float().mean() acc = float(acc) print("Model accuracy: %.2f%%" % (acc*100)) |
这会产生:
1 2 3 4 5 6 7 8 9 10 11 |
Epoch 0: SGD lr 0.0100 -> 0.0099 Epoch 1: SGD lr 0.0099 -> 0.0098 Epoch 2: SGD lr 0.0098 -> 0.0097 Epoch 3: SGD lr 0.0097 -> 0.0096 Epoch 4: SGD lr 0.0096 -> 0.0095 ... Epoch 45: SGD lr 0.0069 -> 0.0068 Epoch 46: SGD lr 0.0068 -> 0.0068 Epoch 47: SGD lr 0.0068 -> 0.0068 Epoch 48: SGD lr 0.0068 -> 0.0067 Epoch 49: SGD lr 0.0067 -> 0.0067 |
请注意,尽管提供给 LambdaLR()
的函数假定参数为 epoch
,但它并不与训练循环中的时期绑定,而只是计算您调用 scheduler.step()
的次数。
使用学习率调度的技巧
本节列出了一些在使用神经网络学习率调度时需要考虑的技巧。
- 增加初始学习率。因为学习率很可能会降低,所以从一个较大的值开始以供降低。较大的学习率将在开始时导致权重的变化更大,从而使您能够受益于后期的微调。
- 使用较大的动量。许多优化器可以考虑动量。使用较大的动量值将有助于优化算法在学习率缩小到较小值时继续朝着正确的方向进行更新。
- 尝试不同的调度器。目前尚不清楚哪种学习率调度器最好。尝试几种不同的配置选项,看看哪种最适合您的问题。另外,尝试指数变化的调度器,甚至是对模型在训练集或测试集上准确率做出响应的调度器。
进一步阅读
以下是有关在 PyTorch 中使用学习率的更多详细文档:
- 如何调整学习率,来自 PyTorch 文档
总结
在这篇文章中,您学习了神经网络模型的学习率调度。
阅读本文后,您将了解:
- 学习率如何影响您的模型训练
- 如何在 PyTorch 中设置学习率调度
- 如何创建自定义学习率调度
这篇文章写得很好,但语法错误很多,有时让人难以理解。
感谢 Tanner 的反馈!
lr_lambda 不需要最后乘以 base_lr。如果您查看 PyTorch 的源代码,LambdaLR.get_lr 会为您处理这个问题。
感谢 FelixHao 的澄清!
这个学习率调度器也可以与 Adam 一起使用吗?
你好 SuBele... 使用 Adam 时,这会是不必要的。
https://ai.stackexchange.com/questions/35041/do-learning-rate-schedulers-conflict-with-or-prevent-convergence-of-the-adam-opt#:~:text=Because%20Adam%20manages%20learning%20rates,causing%20model%20convergence%20to%20worsen.
关于 LinearLR 中的 ‘total_iter’ 参数,您提到(引自文本):
您将 start_factor 设置为 1.0,end_factor 设置为 0.5,total_iters 设置为 30,因此它将在 10 个等步长中使乘数因子从
1.0 减少到 0.5。在 10 步之后,因子将保持在 0.5。
我认为应该是
您将 start_factor 设置为 1.0,end_factor 设置为 0.5,total_iters 设置为 30,因此它将在 10 个等步长中使乘数因子从
1.0 减少到 0.5,在 30 个等步长中。在 30 步之后,因子将保持在 0.5。
这正确吗?
是的,您完全正确——文本中的引用关于步数是不正确的。
以下是准确的解释:
当使用
torch.optim.lr_scheduler.LinearLR
时,学习率在start_factor
和end_factor
之间线性插值,持续时间由total_iters
指定。在这些步数之后,学习率将保持在end_factor
的恒定值。因此,如果您设置:
* start\_factor = 1.0
* end\_factor = 0.5
* total\_iters = 30
那么 PyTorch 将在 30 步内逐渐将学习率因子从 1.0 降低到 0.5——而不是 10 步。在第 30 步之后,因子将保持在 0.5。
总结
* 正确:学习率在 30 步内降低
* 错误:说它在 10 步内降低
感谢您指出这一点——您的修正完全正确。