一、学习率调整策略

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. 梯度消失和梯度爆炸
    • 梯度消失:当每层梯度小于1时,反向传播中梯度趋近于0
    • 梯度爆炸:当每层梯度大于1时,反向传播中梯度越来越大
  2. 退化问题:随着网络加深,准确率不升反降
2.3.2 ResNet的解决方案
  1. Batch Normalization:解决梯度消失/爆炸问题

    • 使所有feature map满足均值为0、方差为1的分布
    • 提高训练稳定性和收敛速度
  2. 残差结构(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 的训练结果
    在这里插入图片描述
Logo

脑启社区是一个专注类脑智能领域的开发者社区。欢迎加入社区,共建类脑智能生态。社区为开发者提供了丰富的开源类脑工具软件、类脑算法模型及数据集、类脑知识库、类脑技术培训课程以及类脑应用案例等资源。

更多推荐