文章目录
-
- 一、学习率调整策略
-
- [1.1 学习率调整的重要性](#1.1 学习率调整的重要性)
- [1.2 PyTorch学习率调整方法分类](#1.2 PyTorch学习率调整方法分类)
- [1.3 学习率调整的实际应用](#1.3 学习率调整的实际应用)
- 二、迁移学习技术
-
- [2.1 迁移学习概述](#2.1 迁移学习概述)
- [2.2 迁移学习的实施步骤](#2.2 迁移学习的实施步骤)
- [2.2 迁移学习的实施步骤](#2.2 迁移学习的实施步骤)
- [2.3 ResNet网络与迁移学习实践](#2.3 ResNet网络与迁移学习实践)
-
- [2.3.1 ResNet网络介绍](#2.3.1 ResNet网络介绍)
- [2.3.2 ResNet的解决方案](#2.3.2 ResNet的解决方案)
- [三、 调整学习率与迁移学习 (ResNet) 的实际案例](#三、 调整学习率与迁移学习 (ResNet) 的实际案例)
-
- [3.1 迁移学习与预训练模型加载](#3.1 迁移学习与预训练模型加载)
- [3.2 模型参数冻结与全连接层调整](#3.2 模型参数冻结与全连接层调整)
- [3.3 优化器选择与学习率调整策略](#3.3 优化器选择与学习率调整策略)
- [3.4 训练与评估流程](#3.4 训练与评估流程)
- [3.5 完整代码与结果展示](#3.5 完整代码与结果展示)
一、学习率调整策略
1.1 学习率调整的重要性
学习率是深度学习模型训练中的关键超参数。较大的学习率能使模型快速收敛,但也可能导致震荡或不收敛;较小的学习率虽然稳定,但收敛速度慢。在实际训练中,我们通常希望在训练初期使用较大学习率加速收敛,在训练后期使用较小学习率精细调优。
常用的学习率值包括0.1、0.01和0.001等。PyTorch提供了丰富的学习率调整策略,主要通过torch.optim.lr_scheduler接口实现。
1.2 PyTorch学习率调整方法分类
PyTorch提供了三种主要的学习率调整方法:
(1)有序调整策略
这类策略按照预定的计划调整学习率:
- StepLR(等间隔调整):每经过固定epoch数调整一次
- MultiStepLR(多间隔调整):在指定epoch集合处调整
- ExponentialLR(指数衰减):学习率按指数函数衰减
- CosineAnnealingLR(余弦退火):学习率按余弦函数变化
代码示例:
python
# 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)不再改善时调整学习率
代码示例:
python
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 学习率调整的实际应用
在训练过程中,学习率调整通常与优化器配合使用:
python
# 定义优化器
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等
- 根据新任务特点选择微调层:低级特征任务用浅层,高级特征任务用深层
第二步:冻结预训练模型的初始参数
保持预训练模型权重不变,只训练新增层或微调部分层,避免在新数据集上过拟合:
python
# 冻结所有参数
for param in model.parameters():
param.requires_grad = False
第三步:针对新数据集训练新增网络层
在冻结预训练模型参数的情况下,训练新增加的层,使模型适应新任务:
python
# 修改最后一层全连接层
in_features = model.fc.in_features
model.fc = nn.Linear(in_features, num_classes) # 新任务类别数
第四步:渐进式解冻并微调预训练层
在新层训练完成后,可以解冻部分预训练层进行微调,进一步提高模型性能:
python
# 解冻最后几层进行微调
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 迁移学习与预训练模型加载
python
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 模型参数冻结与全连接层调整
python
# 冻结所有模型参数
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 优化器选择与学习率调整策略
python
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 训练与评估流程
python
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 完整代码与结果展示
python
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 的训练结果
