预训练模型
知识点回顾:
- 预训练的概念
- 常见的分类预训练模型
- 图像预训练模型的发展史
- 预训练的策略
- 预训练代码实战:resnet18
训练就是不断更新参数,那么如果参数初始值比较好,不仅可以减少未来训练次数,也可以避免未来训练陷入局部最优解的可能,这就引入了预训练模型------即指在和当前目标数据集类似的大规模数据集上训练好的模型。需要处理任务类似很好理解,这里需要大规模数据集是因为提取的是通用特征,所以如果数据集数据少、尺寸小,就很难支撑复杂任务学习通用的数据特征。这种思想叫做迁移学习,预训练的过程叫做上游任务,在目标训练集微调参数的过程叫做下游任务
经典CNN架构预训练模型
使用预训练模型有几个要点:
- 需要resize图片让其可以适配模型,因为模型的输入尺寸是固定的
- 需要修改最后的全连接层以适应数据集,因为下游任务的类别数通常不同,而全连接层是任务相关的,需替换
- 训练过程中,最开始往往先冻结住特征提取器的参数,单独训练全连接层,大约在5-10个epoch后再解冻训练微调全部网络,因为这样不仅更快速而且直接微调所有层可能导致预训练特征被破坏
对于当前数据集的预处理和数据加载不赘述了,以ResNet18作为预训练模型,先修改创建(使用creat_resnet() 函数对原始ResNet18进行修改以适配当前任务)
python
# 导入ResNet模型
from torchvision.models import resnet18
# 定义ResNet18模型(支持预训练权重加载)
def create_resnet18(pretrained=True, num_classes=10):
# 加载预训练模型(ImageNet权重)
model = resnet18(pretrained=pretrained)
# 修改最后一层全连接层,适配CIFAR-10的10分类任务
in_features = model.fc.in_features # model.fc是ResNet18最后的全连接层,in_features获取原全连接层的输入特征维度
model.fc = nn.Linear(in_features, num_classes)
# 将模型转移到指定设备(CPU/GPU)
return model.to(device)
# 创建ResNet18模型(这里不创建也行,后面也会创建的)
model = create_resnet18(pretrained=True, num_classes=10)
定义一个 freeze_model() 函数进行冻结/解冻模型的控制
python
def freeze_model(model, freeze=True):
"""冻结或解冻模型的卷积层参数"""
# 冻结/解冻除fc层外的所有参数
for name, param in model.named_parameters():
if 'fc' not in name:
param.requires_grad = not freeze # 这里有个取反,捋一下逻辑,冻结参数,不计算梯度;解冻参数,计算梯度
# 打印冻结状态
frozen_params = sum(p.numel() for p in model.parameters() if not p.requires_grad) # 计算所有不计算梯度的参数的元素总数,即统计被冻结的参数总数量
total_params = sum(p.numel() for p in model.parameters())
if freeze:
print(f"已冻结模型卷积层参数 ({frozen_params}/{total_params} 参数)")
else:
print(f"已解冻模型所有参数 ({total_params}/{total_params} 参数可训练)")
return model
接下来就是训练过程的函数封装了,除了开头部分加入了冻结/解冻卷积层的控制,剩下部分和之前一样
python
def train_with_freeze_schedule(model, train_loader, test_loader, criterion, optimizer, scheduler, device, epochs, freeze_epochs=5):
"""
前freeze_epochs轮冻结卷积层,之后解冻所有层进行训练
"""
train_loss_history = []
test_loss_history = []
train_acc_history = []
test_acc_history = []
all_iter_losses = []
iter_indices = []
# 初始冻结卷积层
if freeze_epochs > 0:
model = freeze_model(model, freeze=True)
for epoch in range(epochs):
# 解冻控制:在指定轮次后解冻所有层
if epoch == freeze_epochs:
model = freeze_model(model, freeze=False)
# 解冻后调整优化器(可选)
optimizer.param_groups[0]['lr'] = 1e-4 # 降低学习率防止过拟合
model.train() # 设置为训练模式
running_loss = 0.0
correct_train = 0
total_train = 0
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
output = model(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
# 记录Iteration损失
iter_loss = loss.item()
all_iter_losses.append(iter_loss)
iter_indices.append(epoch * len(train_loader) + batch_idx + 1)
# 统计训练指标
running_loss += iter_loss
_, predicted = output.max(1)
total_train += target.size(0)
correct_train += predicted.eq(target).sum().item()
# 每100批次打印进度
if (batch_idx + 1) % 100 == 0:
print(f"Epoch {epoch+1}/{epochs} | Batch {batch_idx+1}/{len(train_loader)} "
f"| 单Batch损失: {iter_loss:.4f}")
# 计算 epoch 级指标
epoch_train_loss = running_loss / len(train_loader)
epoch_train_acc = 100. * correct_train / total_train
# 测试阶段
model.eval()
correct_test = 0
total_test = 0
test_loss = 0.0
with torch.no_grad():
for data, target in test_loader:
data, target = data.to(device), target.to(device)
output = model(data)
test_loss += criterion(output, target).item()
_, predicted = output.max(1)
total_test += target.size(0)
correct_test += predicted.eq(target).sum().item()
epoch_test_loss = test_loss / len(test_loader)
epoch_test_acc = 100. * correct_test / total_test
# 记录历史数据
train_loss_history.append(epoch_train_loss)
test_loss_history.append(epoch_test_loss)
train_acc_history.append(epoch_train_acc)
test_acc_history.append(epoch_test_acc)
# 更新学习率调度器
if scheduler is not None:
scheduler.step(epoch_test_loss)
# 打印 epoch 结果
print(f"Epoch {epoch+1} 完成 | 训练损失: {epoch_train_loss:.4f} "
f"| 训练准确率: {epoch_train_acc:.2f}% | 测试准确率: {epoch_test_acc:.2f}%")
# 绘制损失和准确率曲线
plot_iter_losses(all_iter_losses, iter_indices)
plot_epoch_metrics(train_acc_history, test_acc_history, train_loss_history, test_loss_history)
return epoch_test_acc # 返回最终测试准确率
刚才过程都是函数封装,需要一个顶层控制函数 main() 调用这些封装函数完成从参数设置、模型创建、优化器定义到训练执行的完整流程
python
def main():
# 参数设置
epochs = 40 # 总训练轮次
freeze_epochs = 5 # 冻结卷积层的轮次
learning_rate = 1e-3 # 初始学习率(Adam优化器常用值)
weight_decay = 1e-4 # 权重衰减,Adam优化器的L2正则化参数(防止过拟合)
# 创建ResNet18模型(加载预训练权重)
model = create_resnet18(pretrained=True, num_classes=10)
# 定义优化器和损失函数
optimizer = optim.Adam(model.parameters(), lr=learning_rate, weight_decay=weight_decay)
criterion = nn.CrossEntropyLoss()
# 定义学习率调度器
scheduler = optim.lr_scheduler.ReduceLROnPlateau(
optimizer, mode='min', factor=0.5, patience=2, verbose=True
)
# 开始训练(前5轮冻结卷积层,之后解冻)
final_accuracy = train_with_freeze_schedule(
model=model,
train_loader=train_loader,
test_loader=test_loader,
criterion=criterion,
optimizer=optimizer,
scheduler=scheduler,
device=device,
epochs=epochs,
freeze_epochs=freeze_epochs
)
print(f"训练完成!最终测试准确率: {final_accuracy:.2f}%")
# # 保存模型
# torch.save(model.state_dict(), 'resnet18_cifar10_finetuned.pth')
# print("模型已保存至: resnet18_cifar10_finetuned.pth")