ResNet 残差网络:迁移学习实现食物分类实战

在计算机视觉领域,残差网络(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. 解决梯度消失:捷径连接为梯度反向传播提供了"直通道",梯度可以直接从深层传回浅层,避免了梯度随层数增加而衰减;

  2. 缓解模型退化:残差学习降低了网络的学习难度,深层网络仍能保持性能提升;

  3. 轻量化高效:1×1卷积的使用大幅降低了计算量,同等性能下ResNet的参数更少;

  4. 泛化能力强:预训练的ResNet能学习到通用的视觉特征(如边缘、纹理、形状),是迁移学习的理想骨干网络。

4. 经典ResNet系列版本

ResNet有多个经典版本,核心区别是网络层数和残差块类型,适配不同算力和精度需求:

ResNet版本 网络层数 残差块类型 适用场景
ResNet18 18 基础块 轻量级任务、端侧设备、小数据集迁移学习
ResNet34 34 基础块 平衡性能与算力,中等规模视觉任务
ResNet50 50 瓶颈块 主流视觉任务(分类/检测/分割),工业级应用
ResNet101/152 101/152 瓶颈块 高精度要求的竞赛、大型视觉项目

其中ResNet18因轻量、易训练、迁移效果好,成为自定义小任务的首选。

三、迁移学习核心思想:站在预训练模型的"肩膀上"

迁移学习是将预训练在大规模数据集(如ImageNet,1000类、百万级图片) 上的模型参数,迁移到自定义的小任务中,核心优势是:

  • 无需从零训练,大幅减少训练时间和算力消耗;

  • 利用预训练模型的通用视觉特征,解决小数据集下的过拟合问题;

  • 提升模型的泛化能力,自定义任务的精度更高。

针对ResNet的迁移学习主要分为两步:

  1. 冻结主干网络:ResNet的卷积层部分学习到了通用视觉特征,冻结其参数不参与训练,避免破坏优质特征;

  2. 替换并训练分类头:将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%左右),充分体现了迁移学习的优势。

若想进一步提升准确率,可做以下优化:

  1. 增加训练轮数,结合早停(Early Stopping) 避免过拟合;

  2. 解冻ResNet18的最后1-2个卷积层,进行微调(设置较小的学习率);

  3. 优化数据增强策略,加入随机裁剪、颜色抖动等;

  4. 使用学习率预热、余弦退火等更优的学习率调度策略;

  5. 增加数据集规模,对样本进行均衡处理。

五、ResNet迁移学习的通用技巧

掌握以下技巧,可让ResNet在各类自定义视觉任务中发挥更好的效果:

  1. 输入尺寸匹配:ResNet默认输入为224×224,若使用其他尺寸,需保证卷积层的步长和填充适配,或通过自适应池化层统一输出维度;

  2. 归一化严格匹配:必须使用ImageNet的均值[0.485,0.456,0.406]和标准差[0.229,0.224,0.225],否则会破坏预训练特征;

  3. 参数冻结与微调策略:

    1. 小数据集(万级以下):完全冻结主干网络,仅训练分类头;

    2. 大数据集(万级以上):解冻最后2-4个卷积层,分类头用较大学习率(如0.001),解冻层用较小学习率(如0.0001);

  4. 优化器选择:优先使用Adam(自适应学习率,收敛快),微调时可使用SGD+动量(泛化能力更好);

  5. 批大小设置:根据硬件显存调整,GPU显存不足时可减小批大小(如32、16),并使用梯度累积;

  6. 模型保存:训练过程中保存最优准确率的模型,而非最后一轮,避免过拟合模型。

六、总结

ResNet通过残差学习和捷径连接突破了深层网络的训练瓶颈,成为计算机视觉领域的基础骨干网络;而迁移学习则最大化挖掘了ResNet预训练模型的价值,让小数据集、低算力场景下的高精度视觉建模成为可能。

本文从原理到实战,完整讲解了ResNet的核心设计和基于ResNet18的迁移学习流程,实现了20类食物分类任务的落地。事实上,ResNet不仅适用于图像分类,还可作为目标检测、语义分割、人脸识别等各类视觉任务的骨干网络,只需在其基础上添加对应的任务头,即可实现快速迁移。

掌握ResNet和迁移学习的结合使用,是计算机视觉工程落地的核心技能,无论是科研竞赛还是工业级应用,都能大幅提升开发效率和模型性能。

相关推荐
AI品信智慧数智人2 小时前
以科技为载体,以文化为核心,以游客为中心
人工智能
瑞和数智2 小时前
案例分享 | 瑞和数智助力某农商行打造标签管理平台
大数据·人工智能·科技·金融
科技前瞻观察2 小时前
技术自主、量产突围、产业链协同:宇树科技、优艾智合领衔具身智能TOP20领跑全球
大数据·人工智能·科技
前端不太难2 小时前
OpenClaw:AI 权限治理的核心问题
人工智能·状态模式
hans汉斯2 小时前
《人工智能与机器人研究》期刊推介&征稿指南
人工智能·机器人
电商API&Tina2 小时前
比价 / 选品专用:京东 + 淘宝 核心接口实战(可直接复制运行)
大数据·数据库·人工智能·python·json·音视频
love530love2 小时前
Windows 开源项目部署评估与决策清单(完整版)
人工智能·windows·python·开源·github
HyperAI超神经2 小时前
数据集汇总丨英伟达/OpenAI及多所科研机构开源推理数据集,覆盖数学/全景空间/Wiki问答/科研任务/视觉常识等
人工智能·深度学习·机器学习·数据集·ai编程·llama·图像合成
intcube2 小时前
从“数”到“智”——智达方通EPM如何推动企业韧性增长与创新?
大数据·人工智能·全面预算管理·财务规划·商业智能