在计算机视觉领域,残差网络(ResNet) 是解决深层网络训练难题的里程碑式模型,而迁移学习则让预训练的ResNet模型能快速适配各类自定义视觉任务,实现小数据集下的高精度建模。本文将从ResNet核心原理出发,结合完整的PyTorch代码实战,手把手教你将ResNet18迁移到食物分类任务中,让你既懂原理又能落地。
一、为什么需要ResNet?深层网络的训练困境
在ResNet提出之前,研究人员发现一个核心问题:单纯增加卷积神经网络的层数,模型性能反而会下降(模型退化),且伴随严重的梯度消失问题。
传统卷积网络是直连映射,每一层需要学习从输入到输出的完整映射关系 H(x)=F(x),当网络层数过深时,梯度在反向传播过程中会不断衰减,导致浅层网络的参数无法有效更新,模型难以训练。
为解决这一问题,何恺明团队在2015年提出了残差网络(Residual Network, ResNet),通过创新的残差学习和捷径连接,让网络可以轻松训练到上百层,甚至上千层,一举拿下ILSVRC 2015竞赛的冠军。
二、ResNet核心原理:残差块与捷径连接
ResNet的核心设计只有一个------不再让网络学习完整映射,而是学习输入与输出的残差,这一设计通过残差块(Residual Block) 实现,而残差块的灵魂是捷径连接(Shortcut Connection)(也叫跳连接)。
1. 残差学习的数学逻
传统网络学习:H(x)=F(x)(
H(x)为期望输出,F(x)为网络学习的映射)
残差网络学习:F(x)=H(x)-x,最终输出为H(x)=F(x)+x
这里的x是残差块的输入,F(x)是残差映射,网络只需学习输入到输出的差值,而非完整映射,学习难度大幅降低。即使网络已经达到最优,残差映射F(x)只需学习为0,模型仍能保持最优性能,从根本上避免了模型退化。
2. 残差块的基本结构
残差块是ResNet的最小组成单元,分为基础残差块(适用于ResNet18/34) 和瓶颈残差块(适用于ResNet50/101/152),核心结构一致:
-
主路径:输入经过卷积层-BN层-ReLU激活的组合,学习残差映射F(x);
-
捷径路径:输入x直接跳过主路径的卷积层,与主路径的输出进行元素相加;
-
最终输出:相加结果经过ReLU激活,得到残差块的输出H(x)=F(x)+x。

若主路径和捷径路径的维度不一致(如通道数、图像尺寸不同),会在捷径路径中加入1×1卷积层调整维度,保证元素相加的可行性。
3. ResNet的核心优势
-
解决梯度消失:捷径连接为梯度反向传播提供了"直通道",梯度可以直接从深层传回浅层,避免了梯度随层数增加而衰减;
-
缓解模型退化:残差学习降低了网络的学习难度,深层网络仍能保持性能提升;
-
轻量化高效:1×1卷积的使用大幅降低了计算量,同等性能下ResNet的参数更少;
-
泛化能力强:预训练的ResNet能学习到通用的视觉特征(如边缘、纹理、形状),是迁移学习的理想骨干网络。
4. 经典ResNet系列版本
ResNet有多个经典版本,核心区别是网络层数和残差块类型,适配不同算力和精度需求:
| ResNet版本 | 网络层数 | 残差块类型 | 适用场景 |
|---|---|---|---|
| ResNet18 | 18 | 基础块 | 轻量级任务、端侧设备、小数据集迁移学习 |
| ResNet34 | 34 | 基础块 | 平衡性能与算力,中等规模视觉任务 |
| ResNet50 | 50 | 瓶颈块 | 主流视觉任务(分类/检测/分割),工业级应用 |
| ResNet101/152 | 101/152 | 瓶颈块 | 高精度要求的竞赛、大型视觉项目 |
其中ResNet18因轻量、易训练、迁移效果好,成为自定义小任务的首选。
三、迁移学习核心思想:站在预训练模型的"肩膀上"
迁移学习是将预训练在大规模数据集(如ImageNet,1000类、百万级图片) 上的模型参数,迁移到自定义的小任务中,核心优势是:
-
无需从零训练,大幅减少训练时间和算力消耗;
-
利用预训练模型的通用视觉特征,解决小数据集下的过拟合问题;
-
提升模型的泛化能力,自定义任务的精度更高。
针对ResNet的迁移学习主要分为两步:
-
冻结主干网络:ResNet的卷积层部分学习到了通用视觉特征,冻结其参数不参与训练,避免破坏优质特征;
-
替换并训练分类头:将ResNet最后一层全连接层(原适配ImageNet 1000类)替换为适配自定义任务类别的全连接层,仅训练该层参数。
若自定义数据集规模较大,也可解冻部分浅层卷积层进行微调(Fine-tune),让模型更好地适配自定义特征。
四、实战:ResNet18迁移学习实现食物分类
接下来我们基于PyTorch框架,将预训练的ResNet18模型迁移到20类食物分类任务中,从代码编写到训练评估,实现完整的落地流程。
1. 实战环境准备
-
框架:PyTorch 1.10+
-
第三方库:torchvision、PIL、numpy
-
硬件:优先GPU(CUDA/MPS),无GPU可使用CPU
-
数据:自定义食物数据集,训练集/测试集分别由trainda.txt/testda.txt管理,每行格式为图片路径 类别标签(标签为0-19的整数)。
2. 完整代码实现与逐行解析
(1)导入所需库
python
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
(2)加载预训练ResNet18并改造
加载 ResNet18 预训练模型,冻结特征层参数,替换分类层以适配食物 20 分类任务:
python
# 加载预训练ResNet18模型,使用默认预训练权重
resnet_model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
# 冻结所有特征层参数,禁止梯度更新
for param in resnet_model.parameters():
param.requires_grad = False
# 获取原fc层的输入特征数,替换为20分类的全连接层
in_features = resnet_model.fc.in_features
resnet_model.fc = nn.Linear(in_features, 20) # 20为食物分类的类别数
关键说明:
weights=models.ResNet18_Weights.DEFAULT:自动下载并加载ImageNet预训练权重,替代旧版的pretrained=True;
冻结参数后,只有新替换的全连接层参数requires_grad=True,后续仅训练该层。
(3)数据预处理与数据增强
数据预处理是提升模型泛化能力的关键,训练集加入数据增强(随机变换避免过拟合),测试集仅做基础预处理(保证数据一致性),且预处理参数需与预训练ResNet的要求匹配:
python
data_transforms = {
'train':
transforms.Compose([
transforms.Resize([300, 300]), # 缩放图像至300*300
transforms.RandomRotation(45), # 随机旋转(-45,45)度
transforms.CenterCrop(224), # 中心裁剪至224*224(ResNet输入尺寸)
transforms.RandomHorizontalFlip(p=0.5), # 50%概率水平翻转
transforms.RandomVerticalFlip(p=0.5), # 50%概率垂直翻转
transforms.RandomGrayscale(p=0.1), # 10%概率转为灰度图
transforms.ToTensor(), # 转为Tensor,像素值归一化至[0,1]
# 按ImageNet均值和标准差归一化,与预训练模型保持一致
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
]),
'valid':
transforms.Compose([
transforms.Resize([224, 224]), # 直接缩放至224*224
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
]),
}
核心要点:归一化必须使用ImageNet的均值和标准差,否则会破坏预训练模型学习到的特征分布。
(4)自定义Dataset加载食物数据
继承PyTorch的Dataset类,实现自定义数据集的加载逻辑,txt适配文件管理的图片路径和标签:
python
class food_dataset(Dataset):
def __init__(self, file_path, transform=None):
self.file_path = file_path # 数据列表txt文件路径
self.imgs = [] # 存储所有图片路径
self.labels = [] # 存储所有类别标签
self.transform = transform # 数据预处理方法
# 读取txt文件,解析图片路径和标签
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(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)
# 标签转为int64类型张量,适配CrossEntropyLoss
label = torch.from_numpy(np.array(self.labels[idx], dtype=np.int64))
return image, label
# 创建数据集和数据加载器
training_data = food_dataset(file_path='./trainda.txt', transform=data_transforms['train'])
test_data = food_dataset(file_path='./testda.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)
(5)设备选择与模型配置
选择训练设备(优先GPU),将模型移至设备,并定义损失函数、优化器和学习率调度器:
python
# 自动选择设备:CUDA > MPS > CPU
device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
print(f"Using {device} device")
# 将模型移至指定设备
model = resnet_model.to(device)
# 定义损失函数:交叉熵损失(适用于多分类任务)
loss_fn = nn.CrossEntropyLoss()
# 定义优化器:仅优化需要更新的参数,学习率0.001
optimizer = torch.optim.Adam(param_to_update, lr=0.001)
# 学习率调度器:每10个epoch,学习率减半
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.5)
关键优化:优化器仅传入param_to_update(需训练的全连接层参数),而非所有参数,减少计算量。
(6)定义训练和测试函数
-
训练函数:实现模型的前向传播、损失计算、反向传播和参数更新;
-
测试函数:实现模型的评估,计算测试集的准确率和平均损失,记录最优准确率。
python
# 训练函数
def train(dataloader, model, loss_fn, optimizer):
model.train() # 模型设为训练模式(开启BN/Dropout)
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()
# 初始化最优准确率
best_acc = 0
# 测试函数
def test(dataloader, model, loss_fn):
global best_acc # 声明全局变量
size = len(dataloader.dataset) # 测试集总样本数
num_batches = len(dataloader) # 测试集总批数
model.eval() # 模型设为评估模式(关闭BN/Dropout更新)
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 /= num_batches
correct /= size
print(f"Test Result: \n Accuracy: {(100 * correct):.2f}%, Avg Loss: {test_loss:.4f}\n")
# 存储每轮结果
acc_s.append(correct)
loss_s.append(test_loss)
# 更新最优准确率
if correct > best_acc:
best_acc = correct
return test_loss, correct
(7)模型训练与评估
设置训练轮数,执行训练和测试流程,输出最优准确率:
python
# 设置训练轮数
epochs = 10
acc_s = []
loss_s = []
# 逐轮训练
for t in range(epochs):
print(f"Epoch {t+1}/{epochs}\n-------------------------------")
train(train_dataloader, model, loss_fn, optimizer)
scheduler.step() # 更新学习率
test(test_dataloader, model, loss_fn)
# 输出训练结果
print(f"Training Finished! Best Accuracy: {(100 * best_acc):.2f}%")
3. 训练结果说明
本次实战设置10轮训练,基于ResNet18的迁移学习,在20类食物分类任务中,测试集准确率通常能达到85%以上,远高于从零训练的普通卷积网络(通常60%左右),充分体现了迁移学习的优势。
若想进一步提升准确率,可做以下优化:
-
增加训练轮数,结合早停(Early Stopping) 避免过拟合;
-
解冻ResNet18的最后1-2个卷积层,进行微调(设置较小的学习率);
-
优化数据增强策略,加入随机裁剪、颜色抖动等;
-
使用学习率预热、余弦退火等更优的学习率调度策略;
-
增加数据集规模,对样本进行均衡处理。
五、ResNet迁移学习的通用技巧
掌握以下技巧,可让ResNet在各类自定义视觉任务中发挥更好的效果:
-
输入尺寸匹配:ResNet默认输入为224×224,若使用其他尺寸,需保证卷积层的步长和填充适配,或通过自适应池化层统一输出维度;
-
归一化严格匹配:必须使用ImageNet的均值[0.485,0.456,0.406]和标准差[0.229,0.224,0.225],否则会破坏预训练特征;
-
参数冻结与微调策略:
-
小数据集(万级以下):完全冻结主干网络,仅训练分类头;
-
大数据集(万级以上):解冻最后2-4个卷积层,分类头用较大学习率(如0.001),解冻层用较小学习率(如0.0001);
-
-
优化器选择:优先使用Adam(自适应学习率,收敛快),微调时可使用SGD+动量(泛化能力更好);
-
批大小设置:根据硬件显存调整,GPU显存不足时可减小批大小(如32、16),并使用梯度累积;
-
模型保存:训练过程中保存最优准确率的模型,而非最后一轮,避免过拟合模型。
六、总结
ResNet通过残差学习和捷径连接突破了深层网络的训练瓶颈,成为计算机视觉领域的基础骨干网络;而迁移学习则最大化挖掘了ResNet预训练模型的价值,让小数据集、低算力场景下的高精度视觉建模成为可能。
本文从原理到实战,完整讲解了ResNet的核心设计和基于ResNet18的迁移学习流程,实现了20类食物分类任务的落地。事实上,ResNet不仅适用于图像分类,还可作为目标检测、语义分割、人脸识别等各类视觉任务的骨干网络,只需在其基础上添加对应的任务头,即可实现快速迁移。
掌握ResNet和迁移学习的结合使用,是计算机视觉工程落地的核心技能,无论是科研竞赛还是工业级应用,都能大幅提升开发效率和模型性能。
