欢迎光临
我们一直在努力

深度学习入门:迁移学习与 ResNet

深度学习入门:迁移学习与 ResNet

前言:上一篇我们学习了模型保存与加载,以及学习率动态调整。在实际项目中,从零训练一个深度网络往往需要大量数据和算力,普通开发者很难做到。本篇我们将学习迁移学习——利用已经在大规模数据集上训练好的模型,快速应用到自己的任务上。同时介绍迁移学习中最常用的网络结构——ResNet(残差网络)。

目录

  • 一、为什么需要迁移学习
  • 二、迁移学习的基本流程
  • 三、ResNet 与残差结构
  • 四、Batch Normalization
  • 五、实战:用 ResNet 做迁移学习
  • 六、总结

一、为什么需要迁移学习

1.1 从零训练的问题

问题说明
数据量大 训练一个 ImageNet 级别的模型需要上百万张图片
算力要求高 从零训练 ResNet 需要多张高端 GPU,耗时数天
小数据集困难 自己的数据集只有几百张图,从零训练效果很差

1.2 迁移学习的思路

迁移学习是指利用已经训练好的模型,在新的任务上进行微调。核心思想是:模型在大规模数据集上学到的特征提取能力,可以迁移到新任务上。

1.3 迁移学习的优势

优势说明
加快训练 不用从零开始,几轮就能收敛
提高性能 预训练模型已学到通用特征,效果更好
数据稀缺 即使数据集小,也能取得不错的效果

二、迁移学习的基本流程

迁移学习的完整步骤如下:

步骤说明
1. 选择预训练模型和层 通常选择在 ImageNet 上预训练的模型(VGG、ResNet 等)
2. 冻结预训练模型参数 保持预训练权重不变,只训练新增的层
3. 训练新增的层 在冻结的状态下训练新的分类层,让模型适应新任务
4. 微调预训练模型的层 解冻部分层,用较小学习率继续微调
5. 评估和测试 用测试集评估,效果不好可调整超参数或微调层

2.1 冻结与微调的理解

操作含义适用层
冻结 参数不更新 浅层(边缘、纹理等通用特征)
微调 参数继续更新 深层(类别相关的专属特征)

三、ResNet 与残差结构

3.1 深层网络的问题

卷积神经网络通过卷积层和池化层叠加组成。理论上网络越深表达能力越强,但实验发现:

问题说明
梯度消失 每层误差梯度小于 1,反向传播时越深梯度越趋近 0
梯度爆炸 每层误差梯度大于 1,反向传播时越深梯度越大
退化问题 网络加深后,训练误差反而上升,效果变差

3.2 ResNet 的解决方案

ResNet(残差网络)由何凯明等人在 2015 年提出,斩获当年 ImageNet 竞赛分类、目标检测双料冠军。

两个关键解决方案:

问题解决方案
梯度消失/爆炸 数据预处理 + 使用 BN(Batch Normalization)层
退化问题 引入残差结构(跨层连接)

3.3 残差结构

残差结构的核心是捷径(shortcut),让特征层相加:

F

(

x

)

+

x

\\mathcal{F}(x) + x

F(x)+x

部分说明
x 输入(identity,恒等映射)
F(x) 经过两层权重层的输出
F(x) + x 两者相加,再经过 ReLU

注意:F(x) 和 x 的形状必须相同才能相加。如果不相同,需要用 1×1 卷积调整通道数。

3.4 ResNet 网络结构

以 ResNet-18 为例:

层级输出尺寸操作
输入 3×224×224 输入图像
conv1 64×112×112 7×7 卷积,stride=2
maxpool 64×56×56 3×3 最大池化,stride=2
conv2_x 64×56×56 2 个残差块
conv3_x 128×28×28 2 个残差块
conv4_x 256×14×14 2 个残差块
conv5_x 512×7×7 2 个残差块
全局平均池化 512×1×1 将每个特征图的平均值作为最终向量
全连接层 1000 输出 1000 类(ImageNet 类别数)

不同版本的 ResNet:

版本层数
ResNet-18 18 层
ResNet-34 34 层
ResNet-50 50 层
ResNet-101 101 层
ResNet-152 152 层

四、Batch Normalization

4.1 BN 的作用

Batch Normalization(批归一化) 的目的是让所有的 feature map 满足均值为 0、方差为 1 的分布规律。

4.2 BN 的好处

好处说明
加速训练 每层输入分布稳定,梯度更稳定
防止梯度问题 缓解梯度消失和梯度爆炸
允许更大学习率 训练更快,收敛更好
轻微正则化 减少对 Dropout 的依赖

4.3 BN 的实现

ResNet 中每个卷积层后都紧接 BN 层:

nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding, bias=False),
nn.BatchNorm2d(out_channels), # BN 层
nn.ReLU(inplace=True),
)

五、实战:用 ResNet 做迁移学习

5.1 场景说明

假设我们要做一个 20 类的食物分类任务,数据集规模较小(几百张图)。直接从头训练 CNN 效果差,使用 ResNet-18 预训练模型进行迁移学习是更好的选择。

5.2 完整代码

import torch
import torch.nn as nn
from torchvision import models, transforms
from torch.utils.data import Dataset, DataLoader
from PIL import Image
import numpy as np

# ==================== 数据预处理 ====================
data_transforms = {
'trainda':
transforms.Compose([
transforms.Resize((224, 224)),
transforms.RandomRotation(45),
transforms.CenterCrop(224),
transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomVerticalFlip(p=0.5),
transforms.ColorJitter(0.2, 0.1, 0.1, 0.1),
transforms.RandomGrayscale(p=0.1),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
]),
'valid':
transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
]),
}

# ==================== 自定义数据集 ====================
class FoodDataset(Dataset):
def __init__(self, file_path, transform=None):
self.imgs = []
self.labels = []
self.transform = transform
with open(file_path) as f:
samples = [x.strip().split(' ') for x in f.readlines() if x.strip()]
for img_path, label in samples:
self.imgs.append(img_path)
self.labels.append(label)

def __len__(self):
return len(self.imgs)

def __getitem__(self, idx):
image = Image.open(self.imgs[idx]).convert('RGB')
if self.transform:
image = self.transform(image)
label = torch.from_numpy(np.array(self.labels[idx], dtype=np.int64))
return image, label

training_data = FoodDataset('./train.txt', data_transforms['trainda'])
test_data = FoodDataset('./test.txt', data_transforms['valid'])

train_dataloader = DataLoader(training_data, batch_size=16, shuffle=True)
test_dataloader = DataLoader(test_data, batch_size=16, shuffle=False)

# ==================== 自定义迁移学习网络 ====================
class NewNet(nn.Module):
def __init__(self, num_classes=20):
super(NewNet, self).__init__()
# 加载 ResNet-18 预训练模型
self.resnet = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
# 新增分类层,将原来的 1000 类改为自定义的 20 类
self.fc = nn.Linear(1000, num_classes)

def forward(self, x):
x = self.resnet(x) # 经过 ResNet 提取特征,输出 (batch_size, 1000)
x = self.fc(x) # 经过新的全连接层,输出 (batch_size, 20)
return x

# ==================== 训练与测试 ====================
device = "cuda" if torch.cuda.is_available() else "cpu"
model = NewNet(num_classes=20).to(device)

def train(dataloader, model, loss_fn, optimizer):
model.train()
total_loss = 0
for X, y in dataloader:
X, y = X.to(device), y.to(device)
pred = model(X)
loss = loss_fn(pred, y)

optimizer.zero_grad()
loss.backward()
optimizer.step()

total_loss += loss.item()
print(f"训练平均损失: {total_loss / len(dataloader):.4f}")

def test(dataloader, model, loss_fn):
size = len(dataloader.dataset)
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(X)
test_loss += loss_fn(pred, y).item()
correct += (pred.argmax(1) == y).type(torch.float).sum().item()
test_loss /= len(dataloader)
correct /= size
print(f"测试结果: 准确率 {(100 * correct):.2f}%, 平均损失 {test_loss:.4f}")

loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

epochs = 10
for t in range(epochs):
print(f"Epoch {t + 1}\\n====================================")
train(train_dataloader, model, loss_fn, optimizer)
test(test_dataloader, model, loss_fn)
print("Done!")

5.3 关键改动说明

改动说明
输入尺寸 从 256×256 改为 224×224,因为 ResNet 预训练时使用 224×224
加载预训练模型 models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
修改分类层 将 1000 类改为 20 类:nn.Linear(1000, 20)
batch_size 改为 16,因为 ResNet 参数量更大,显存占用更高

5.4 进阶:冻结与微调

如果数据量极小,可以先冻结 ResNet 参数,只训练新增的 fc 层:

class NewNet(nn.Module):
def __init__(self, num_classes=20):
super(NewNet, self).__init__()
self.resnet = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)

# 冻结 ResNet 的所有参数
for param in self.resnet.parameters():
param.requires_grad = False

self.fc = nn.Linear(1000, num_classes)

def forward(self, x):
x = self.resnet(x)
x = self.fc(x)
return x

训练几轮后,再解冻部分层进行微调:

# 解冻 ResNet 的最后两层
for param in model.resnet.layer4.parameters():
param.requires_grad = True

# 用更小的学习率微调
optimizer = torch.optim.Adam(model.parameters(), lr=0.0001)

六、总结

核心知识点速查

知识点关键概念
迁移学习 利用预训练模型在新任务上微调
冻结参数 param.requires_grad = False
微调 解冻部分层,用较小学习率继续训练
ResNet 残差网络,通过跨层连接解决退化问题
残差结构 F(x) + x,让特征层相加
Batch Normalization 让 feature map 满足均值 0、方差 1 的分布

核心 API 一览

用途对应模块 / 方法
加载预训练模型 models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
冻结参数 param.requires_grad = False
修改分类层 nn.Linear(in_features, num_classes)
BN 层 nn.BatchNorm2d(channels)

注意事项

要点说明
输入尺寸 ResNet 预训练用 224×224,迁移学习时保持一致
batch_size ResNet 参数量大,显存不足时可调小
学习率 微调时用更小的学习率(如 0.0001)
冻结策略 数据少时先冻结,数据多时可解冻部分层
归一化参数 使用 ImageNet 的均值和标准差

系列直达

  • 上篇:深度学习入门:模型保存与加载
  • 本篇:深度学习入门:迁移学习与 ResNet(本文)
  • 下篇:敬请期待
赞(0)
未经允许不得转载:171主机测评 » 深度学习入门:迁移学习与 ResNet
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址