了解 PyTorch 学习率调整和迁移学习
本文介绍了深度学习中的学习率调整策略与迁移学习技术。学习率调整包括有序调整(StepLR、CosineAnnealingLR等)、自适应调整(ReduceLROnPlateau)和自定义调整方法。迁移学习利用预训练模型(如ResNet)加速训练,通过冻结初始层、修改输出层并逐步解冻微调来实现。文章提供了PyTorch代码示例,展示了如何加载预训练模型、调整网络结构、设置优化器和学习率调度器,以及实
一、学习率调整策略
1.1 学习率调整的重要性
学习率是深度学习模型训练中的关键超参数。较大的学习率能使模型快速收敛,但也可能导致震荡或不收敛;较小的学习率虽然稳定,但收敛速度慢。在实际训练中,我们通常希望在训练初期使用较大学习率加速收敛,在训练后期使用较小学习率精细调优。
常用的学习率值包括0.1、0.01和0.001等。PyTorch提供了丰富的学习率调整策略,主要通过torch.optim.lr_scheduler接口实现。
1.2 PyTorch学习率调整方法分类
PyTorch提供了三种主要的学习率调整方法:
(1)有序调整策略
这类策略按照预定的计划调整学习率:
- StepLR(等间隔调整):每经过固定epoch数调整一次
- MultiStepLR(多间隔调整):在指定epoch集合处调整
- ExponentialLR(指数衰减):学习率按指数函数衰减
- CosineAnnealingLR(余弦退火):学习率按余弦函数变化
代码示例:
# StepLR:每5个epoch学习率减半
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5)
# MultiStepLR:在第10、30、80个epoch调整学习率
scheduler = torch.optim.lr_scheduler.MultiStepLR(optimizer, milestones=[10, 30, 80], gamma=0.1)
# ExponentialLR:指数衰减,底数为0.9
scheduler = torch.optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.9)
# CosineAnnealingLR:余弦退火,周期为50个epoch
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50, eta_min=0)
(2)自适应调整策略
根据训练过程中的指标变化动态调整学习率:
- ReduceLROnPlateau:当监控指标(如loss或accuracy)不再改善时调整学习率
代码示例:
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
optimizer,
mode='min', # 监控指标最小化
factor=0.1, # 学习率调整倍数
patience=10, # 容忍的epoch数
threshold=0.0001, # 变化阈值
threshold_mode='rel', # 相对变化模式
cooldown=0, # 调整后的冷却期
min_lr=0, # 最小学习率
eps=1e-08 # 数值稳定性参数
)
(3)自定义调整策略
通过Lambda函数自定义学习率调整规则,特别适合为不同网络层设置不同的学习率:
- LambdaLR:使用自定义函数计算学习率调整倍数
1.3 学习率调整的实际应用
在训练过程中,学习率调整通常与优化器配合使用:
# 定义优化器
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
# 定义学习率调度器
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.5)
# 训练循环中更新学习率
for epoch in range(num_epochs):
# 训练步骤...
train(...)
# 更新学习率
scheduler.step()
# 评估步骤...
test(...)
二、迁移学习技术
2.1 迁移学习概述
迁移学习是指利用在大型数据集上预训练好的模型,将其知识迁移到新的目标任务中。这种方法可以显著加快模型训练速度,提高模型性能,特别是在数据稀缺的情况下表现尤为突出。
迁移学习的主要优势:
- 减少训练时间:预训练模型已学习通用特征
- 提升模型性能:利用大规模数据集训练的知识
- 解决数据不足:在小数据集上也能取得良好效果
2.2 迁移学习的实施步骤
实施迁移学习通常包括以下五个关键步骤:
2.2 迁移学习的实施步骤
实施迁移学习通常包括以下五个关键步骤:
第一步:选定预训练模型并确定微调层级
- 选择在大规模图像数据集(如ImageNet)上预训练的模型,如VGG、ResNet等
- 根据新任务特点选择微调层:低级特征任务用浅层,高级特征任务用深层
第二步:冻结预训练模型的初始参数
保持预训练模型权重不变,只训练新增层或微调部分层,避免在新数据集上过拟合:
# 冻结所有参数
for param in model.parameters():
param.requires_grad = False
第三步:针对新数据集训练新增网络层
在冻结预训练模型参数的情况下,训练新增加的层,使模型适应新任务:
# 修改最后一层全连接层
in_features = model.fc.in_features
model.fc = nn.Linear(in_features, num_classes) # 新任务类别数
第四步:渐进式解冻并微调预训练层
在新层训练完成后,可以解冻部分预训练层进行微调,进一步提高模型性能:
# 解冻最后几层进行微调
for param in model.layer4.parameters():
param.requires_grad = True
第五步:评估模型性能并进行策略调优
使用测试集评估模型性能,根据结果调整超参数或微调策略。
2.3 ResNet网络与迁移学习实践
2.3.1 ResNet网络介绍
ResNet(Residual Network)由微软研究院的何凯明等人于2015年提出,在当年的ImageNet竞赛中获得分类任务第一名。ResNet通过引入残差结构解决了深度神经网络中的退化问题。
传统CNN存在的问题:
- 梯度消失和梯度爆炸:
- 梯度消失:当每层梯度小于1时,反向传播中梯度趋近于0
- 梯度爆炸:当每层梯度大于1时,反向传播中梯度越来越大
- 退化问题:随着网络加深,准确率不升反降
2.3.2 ResNet的解决方案
-
Batch Normalization:解决梯度消失/爆炸问题
- 使所有feature map满足均值为0、方差为1的分布
- 提高训练稳定性和收敛速度
-
残差结构(Residual Block):解决退化问题
- 使用shortcut连接方式,让特征矩阵隔层相加
- 公式: H ( x ) = F ( x ) + x H(x) = F(x) + x H(x)=F(x)+x,其中 F ( x ) F(x) F(x)为残差映射
- 确保 F ( x ) F(x) F(x)和 x x x形状相同,进行逐元素相加
三、 调整学习率与迁移学习 (ResNet) 的实际案例
3.1 迁移学习与预训练模型加载
import torch
from PIL import Image
from torch import nn
from torchvision import models
from torchvision.transforms import transforms
from torch.utils.data import Dataset, DataLoader
# 加载预训练的ResNet-18模型
resnet_model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
代码解析:
models.resnet18():创建ResNet-18模型结构,包含18个卷积层weights=models.ResNet18_Weights.DEFAULT:使用在ImageNet数据集上预训练好的权重初始化模型参数- 预训练模型已学习到丰富的图像特征,适合迁移到新的视觉任务
3.2 模型参数冻结与全连接层调整
# 冻结所有模型参数
for param in resnet_model.parameters():
param.requires_grad = False
# 获取原始模型的全连接层输入特征数
in_features = resnet_model.fc.in_features
# 重新设计全连接层以适应20个食物类别
resnet_model.fc = nn.Sequential(
nn.Linear(in_features, 512), # 新增层:将特征维度从in_features降到512
nn.ReLU(inplace=True), # 激活函数引入非线性
nn.Dropout(0.5), # 防止过拟合
nn.Linear(512, 20) # 输出层:将512维降到20个类别
)
代码解析:
param.requires_grad = False:冻结卷积层参数,保持预训练特征不变- 仅新添加的全连接层参数需要训练,大大减少参数量和训练时间
- Dropout层随机失活神经元,增强模型泛化能力
3.3 优化器选择与学习率调整策略
device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
model = resnet_model.to(device)
loss_fn = nn.CrossEntropyLoss()
# 仅收集需要训练的参数(全连接层参数)
params_to_update = []
for param in resnet_model.parameters():
if param.requires_grad == True:
params_to_update.append(param)
# 使用Adam优化器替代SGD
optimizer = torch.optim.Adam(params_to_update, lr=0.001) # 设置较低的学习率
# 学习率调度器:每5个epoch将学习率减半
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5)
# 替代方案:基于验证损失调整学习率
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
optimizer,
mode='min',
factor=0.1,
patience=10,
threshold=0.0001,
threshold_mode='rel',
cooldown=0,
min_lr=0,
eps=1e-08
)
代码解析:
- StepLR调度器:固定周期衰减策略,
step_size=5表示每5个epoch,gamma=0.5表示学习率减半 - ReduceLROnPlateau:自适应衰减策略,当验证损失在
patience=10个epoch内不再下降threshold=0.0001时,学习率乘以factor=0.1 - Adam优化器:自适应学习率优化算法,适合非平稳目标和非稀疏梯度
3.4 训练与评估流程
def train(dataLoader, model, loss_fn, optimizer):
model.train()
for X, y in dataLoader:
X, y = X.to(device), y.to(device)
pred = model.forward(X)
loss = loss_fn(pred, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
def test(dataloader, model, loss_fn):
global best_acc
size = len(dataloader.dataset)
num_batches = len(dataloader)
model.eval()
test_loss, correct = 0, 0
with torch.no_grad():
for X, y in dataloader:
X, y = X.to(device), y.to(device)
pred = model.forward(X)
test_loss = loss_fn(pred, y)
correct += (pred.argmax(1) == y).type(torch.float).sum().item()
test_loss /= num_batches
correct /= size
print(f"Test result:\n Accuracy:{(100 * correct):.2f}%, Avg loss: {test_loss}")
if correct > best_acc:
best_acc = correct
# 主训练循环
epochs = 10
best_acc = 0
acc_s = []
loss_s = []
for t in range(epochs):
print(f"Epoch {t + 1}\n---")
train(train_dataloader, model, loss_fn, optimizer)
scheduler.step() # 更新学习率
test(test_dataloader, model, loss_fn)
print(f"Best accuracy: {best_acc}")
代码解析:
- 训练模式切换:
model.train()启用dropout和batch normalization训练模式 - 评估模式切换:
model.eval()关闭dropout,使用训练好的统计量 - 梯度清零:
optimizer.zero_grad()避免梯度累加 - 学习率更新:每个epoch后调用
scheduler.step()执行学习率调整策略
3.5 完整代码与结果展示
import torch
from PIL import Image
from torch import nn
from torchvision import models
from torchvision.transforms import transforms
from torch.utils.data import Dataset, DataLoader
resnet_model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
for param in resnet_model.parameters():
param.requires_grad = False
in_features = resnet_model.fc.in_features
resnet_model.fc = nn.Sequential(
nn.Linear(in_features, 512), # 新增层:将特征维度从in_features降到512
nn.ReLU(inplace=True), # 激活函数
nn.Dropout(0.5), # 可选的dropout层,防止过拟合
nn.Linear(512, 20) # 输出层:将512维降到20个类别
)
params_to_update = []
for param in resnet_model.parameters():
if param.requires_grad == True:
params_to_update.append(param)
data_transforms = {
'train':
transforms.Compose([
transforms.Resize([300, 300]),
transforms.RandomRotation(45),
transforms.CenterCrop(256),
transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomVerticalFlip(p=0.5),
transforms.ColorJitter(
brightness=0.2,
contrast=0.1,
saturation=0.1,
hue=0.1
),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]
)
]),
'valid':
transforms.Compose([
transforms.Resize([256, 256]),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]
)
]),
}
class FoodDataset(Dataset):
def __init__(self, file_path, transform=None):
self.file_path = file_path
self.imgs = []
self.labels = []
self.transform = transform
with open(self.file_path) as f:
samples = [x.strip().split(' ') for x in f.readlines()]
for img_path, label in samples:
self.imgs.append(img_path)
self.labels.append(int(label))
def __len__(self):
return len(self.imgs)
def __getitem__(self, idx):
image = Image.open(self.imgs[idx])
if self.transform:
image = self.transform(image)
label = self.labels[idx]
label = torch.tensor(label, dtype=torch.long)
return image, label
training_data = FoodDataset(file_path='./train.txt', transform=data_transforms['train'])
test_data = FoodDataset(file_path='./test.txt', transform=data_transforms['valid'])
# 创建数据加载器
train_dataloader = DataLoader(training_data, batch_size=64, shuffle=True)
test_dataloader = DataLoader(test_data, batch_size=64, shuffle=True)
device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
# print(f"Using {device} device")
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
self.conv1 = nn.Sequential(
nn.Conv2d(
in_channels=3,
out_channels=16,
kernel_size=5,
stride=1,
padding=2,
),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2),
)
self.conv2 = nn.Sequential(
nn.Conv2d(16, 32, 5, 1, 2),
nn.ReLU(),
nn.Conv2d(32, 32, 5, 1, 2),
nn.ReLU(),
nn.MaxPool2d(2),
)
self.conv3 = nn.Sequential(
nn.Conv2d(32, 128, 5, 1, 2),
nn.ReLU(),
)
self.out = nn.Linear(128 * 64 * 64, 20)
def forward(self, x):
x = self.conv1(x)
x = self.conv2(x)
x = self.conv3(x)
x = x.view(x.size(0), -1)
output = self.out(x)
return output
model = resnet_model.to(device) # 为什么不需要加括号,之前是model = CNN().to(device)
# 因为resnet_model已经是一个实例化的模型对象,直接调用.to(device)即可
model.eval()
loss_fn = nn.CrossEntropyLoss()
# 替换SGD为Adam
optimizer = torch.optim.Adam(params_to_update, lr=0.001) # 降低学习率
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5)
def train(dataLoader, model, loss_fn, optimizer):
model.train()
for X, y in dataLoader:
X, y = X.to(device), y.to(device)
pred = model.forward(X)
loss = loss_fn(pred, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
scheduler.step(loss)
best_acc = 0
def test(dataloader, model, loss_fn):
global best_acc
size = len(dataloader.dataset)
num_batches = len(dataloader)
model.eval()
test_loss, correct = 0, 0
with torch.no_grad():
for X, y in dataloader:
X, y = X.to(device), y.to(device)
pred = model.forward(X)
test_loss = loss_fn(pred, y)
correct += (pred.argmax(1) == y).type(torch.float).sum().item()
a = (pred.argmax(1) == y)
b = (pred.argmax(1) == y).type(torch.float)
test_loss /= num_batches
correct /= size
print(f"Test result:\n Accuracy:{(100 * correct):.2f}%, Avg loss: {test_loss}")
if correct > best_acc:
best_acc = correct
# scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
# optimizer,
# mode='min',
# factor=0.1,
# patience=10,
# threshold=0.0001,
# threshold_mode='rel',
# cooldown=0,
# min_lr=0,
# eps=1e-08
# )
# train(train_dataloader, model, loss_fn, optimizer)
# test(test_dataloader, model, loss_fn)
epochs = 10
acc_s = []
loss_s = []
for t in range(epochs):
print(f"Epoch {t + 1}\n---")
train(train_dataloader, model, loss_fn, optimizer)
scheduler.step()
test(test_dataloader, model, loss_fn)
print(best_acc)
- 未调整学习率与使用 ResNet 的训练结果

- 调整学习率与使用 ResNet 的训练结果

更多推荐


所有评论(0)