初识深度学习——迁移学习
一、引言:站在巨人的肩膀上
在之前的博客中,我们从零开始搭建CNN,用数据增强、学习率调度等技巧训练了一个食物分类模型。但你可能已经发现:训练一个从零开始的CNN需要大量数据、大量时间,而且效果不一定理想。
假如现在要做一个新的图像分类任务——比如识别100种不同的鸟类,但手头只有几千张图片,从头训练一个CNN几乎不可能达到高准确率。这时候,迁移学习(Transfer Learning) 就是你的救星。
迁移学习的核心思想:将在一个大规模数据集(如ImageNet,包含1000类、120万张图片)上训练好的模型,迁移到自己的小数据集任务上。就像一位已经学过“如何看图”的专家,只需要稍微学习一下新的类别,就能快速上手。
本篇博客将基于PyTorch,使用经典的ResNet18预训练模型,完成食物分类任务。我们将学习如何加载预训练权重、冻结参数、替换分类层,以及为什么这样做能大幅提升效率。
二、迁移学习核心概念
2.1 什么是迁移学习
迁移学习是指将一个任务(源任务)上学到的知识,应用到另一个相关任务(目标任务)上。在深度学习中,通常指:
-
源任务:在大规模数据集(如ImageNet)上训练一个模型
-
目标任务:将自己的数据集(如食物分类)交给这个模型微调
为什么有效?
-
浅层卷积层学到的特征(边缘、纹理、颜色)是通用的,适用于几乎所有图像任务
-
深层卷积层学到的特征(语义、物体部件)也大部分通用
-
只有最后的分类层需要根据新任务的类别数重新学习
2.2 迁移学习的三种策略
| 策略 | 做法 | 适用场景 |
|---|---|---|
| 特征提取 | 冻结所有卷积层,只训练新的分类层 | 数据量很小,目标任务与源任务相似 |
| 微调 | 冻结部分卷积层,训练剩余层+分类层 | 数据量中等,目标任务与源任务有一定差异 |
| 从头训练 | 不使用预训练权重,全部重新训练 | 数据量很大,目标任务与源任务差异极大 |
本代码采用的是特征提取策略:冻结ResNet18的全部卷积层,只训练最后替换的全连接层。
2.3 ResNet18简介
ResNet(残差网络) 由何恺明等人于2015年提出,通过残差连接解决了深层网络难以训练的问题。ResNet18是该系列中最轻量的版本,包含18层卷积层(含残差块),在ImageNet上达到了约70%的top-1准确率。
ResNet18结构:
-
1个7×7卷积 + 最大池化
-
4个残差块组(每个组包含2个BasicBlock)
-
全局平均池化
-
全连接层(输出1000类)
PyTorch的torchvision.models中提供了预训练的ResNet18,可直接加载。
三、代码逐段解析
3.1 加载预训练模型
import torchvision.models as models
resnet_model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
weights=models.ResNet18_Weights.DEFAULT:表示加载在ImageNet上预训练好的权重。这是PyTorch 1.13+的新写法,等价于旧版的pretrained=True。
为什么要加载预训练权重?
-
模型已经学会了如何提取通用图像特征
-
相比随机初始化,微调后准确率更高、收敛更快
3.2 冻结所有参数
for param in resnet_model.parameters():
param.requires_grad = False
requires_grad = False:将该参数的梯度计算关闭。在反向传播时,这些参数不会更新。
效果:所有卷积层的权重都被“冻结”,在训练中保持不变。这样做的好处是:
-
大幅减少计算量(不需要计算卷积层的梯度)
-
防止小数据集上过拟合
-
加快训练速度
3.3 替换分类层
in_features = resnet_model.fc.in_features
resnet_model.fc = nn.Linear(in_features, out_features=20)
resnet_model.fc:ResNet18最后的全连接层,原输出为1000类(ImageNet类别数)。
in_features:获取原全连接层的输入特征数(ResNet18为512)。
替换为nn.Linear(512, 20):新的全连接层输出20类(食物分类的类别数)。
关键点:新替换的fc层默认requires_grad=True,所以只有这一层会被训练。
3.4 收集需要训练的参数
params_to_update = []
for param in resnet_model.parameters():
if param.requires_grad == True:
params_to_update.append(param)
作用:将所有需要梯度的参数(即新的全连接层参数)收集到一个列表中。
为什么需要这个列表?
-
优化器只需要更新这些参数
-
如果直接把
resnet_model.parameters()传给优化器,虽然冻结的参数不会更新,但会浪费内存和计算 -
只传需要更新的参数更高效
3.5 数据准备
数据增强和自定义数据集与之前一致:
data_transforms = {
'trainda': transforms.Compose([
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.RandomGrayscale(p=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])
]),
}
注意:验证集不做数据增强,只做必要的尺寸调整和标准化。
3.6 模型移到设备
model = resnet_model.to(device)
为什么不需要加括号?
-
resnet_model已经是models.resnet18()返回的实例对象 -
而之前的
CNN()是先定义类,再实例化。这里直接使用已实例化的模型 -
.to(device)是实例方法,返回模型本身
3.7 优化器与调度器
optimizer = torch.optim.Adam(params_to_update, lr=0.001)
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5)
注意:优化器只传入params_to_update(新的全连接层参数),而不是所有模型参数。
StepLR:每5个epoch学习率乘以0.5,逐步衰减。
3.8 训练与测试函数
训练函数与之前类似,但不需要 scheduler.step(loss),因为StepLR的step不需要参数:
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()
测试函数记录准确率和损失,并保存最优准确率。
3.9 训练循环
epochs = 100
acc_s = []
loss_s = []
for t in range(epochs):
print(f"Epoch {t+1}")
train(train_dataloader, model, loss_fn, optimizer)
scheduler.step() # 每个epoch后更新学习率
test(test_dataloader, model, loss_fn)
print('最优训练结果为:', best_acc)
关键:scheduler.step()放在train()之后、test()之前或之后都可以,但通常放在epoch末尾。
四、迁移学习的优势与注意事项
4.1 优势
| 优势 | 说明 |
|---|---|
| 训练速度快 | 只训练最后一层,参数量从1100万降到1万左右 |
| 准确率高 | 预训练特征强大,小数据集也能达到高精度 |
| 防止过拟合 | 冻结大量参数,模型复杂度降低 |
| 节省数据 | 不需要海量数据即可取得好效果 |
4.2 注意事项
-
输入尺寸:ResNet18原输入为224×224,但代码中用了256×256。由于是全卷积结构(最后是全局池化),ResNet可以接受任意尺寸输入,但最好与预训练时接近。
-
标准化参数:必须使用ImageNet的均值和标准差,因为预训练模型是在这种标准化下训练的。
-
学习率:新全连接层的学习率可以设大一些(如0.001),如果微调卷积层,学习率要更小(如0.0001)。
-
冻结策略:数据量越小,冻结的层应越多;数据量越大,可以解冻更多层进行微调。
五、完整代码总结
import torch
import torchvision.models as models
from torch import nn
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms
from PIL import Image
import numpy as np
# 1. 加载预训练ResNet18
resnet_model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
# 2. 冻结所有参数
for param in resnet_model.parameters():
param.requires_grad = False
# 3. 替换全连接层
in_features = resnet_model.fc.in_features
resnet_model.fc = nn.Linear(in_features, 20)
# 4. 收集需要训练的参数
params_to_update = [p for p in resnet_model.parameters() if p.requires_grad]
# 5. 数据准备(与之前一致)
# ...
# 6. 设备、优化器、调度器
device = "cuda" if torch.cuda.is_available() else "cpu"
model = resnet_model.to(device)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(params_to_update, lr=0.001)
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5)
# 7. 训练与测试函数
# ...
# 8. 训练循环
epochs = 100
for t in range(epochs):
train(train_dataloader, model, loss_fn, optimizer)
scheduler.step()
test(test_dataloader, model, loss_fn)
六、总结
本篇博客通过一个完整的食物分类项目,系统讲解了迁移学习的核心技术:
| 知识点 | 核心内容 |
|---|---|
| 迁移学习 | 将预训练模型迁移到新任务 |
| ResNet18 | 经典残差网络,ImageNet预训练 |
| 冻结参数 | requires_grad = False |
| 替换分类层 | model.fc = nn.Linear(512, 20) |
| 仅训练新层 | 优化器只传params_to_update |
| 学习率调度 | StepLR逐步衰减 |
更多推荐



所有评论(0)