深度学习之第八课迁移学习(残差网络ResNet)

目录

简介

一、迁移学习

1.什么是迁移学习

[2. 迁移学习的步骤](#2. 迁移学习的步骤)

二、残差网络ResNet

1.了解ResNet

2.ResNet网络---残差结构

三、代码分析

[1. 导入必要的库](#1. 导入必要的库)

[2. 模型准备(迁移学习)](#2. 模型准备(迁移学习))

[3. 数据预处理](#3. 数据预处理)

[4. 自定义数据集类](#4. 自定义数据集类)

[5. 数据加载器](#5. 数据加载器)

[6. 设备配置](#6. 设备配置)

[7. 训练函数](#7. 训练函数)

[8. 测试函数](#8. 测试函数)

[9. 训练配置和执行](#9. 训练配置和执行)

整体流程总结


简介

经过长久的卷积神经网络的学习、我们学习了如何提高模型的准确率,但是最终我们的准确率还是没达到百分之八十。原因是因为我们本身模型的局限,面对现有很多成熟的模型,它们有很好的效果,都是经过多次训练选取了最佳的参数,那我们能不能去使用哪些大佬的模型呢?

答案是可以的,这就使用到迁移学习的知识。

深度学习之第五课卷积神经网络 (CNN)如何训练自己的数据集(食物分类)

深度学习之第六课卷积神经网络 (CNN)如何保存和使用最优模型

深度学习之第七课卷积神经网络 (CNN)调整学习率

一、迁移学习

1.什么是迁移学习

迁移学习是指利用已经训练好的模型,在新的任务上进行微调。迁移学习可以加快模型训练速度,提高模型性能,并且在数据稀缺的情况下也能很好地工作。

2. 迁移学习的步骤

1、选择预训练的模型和适当的层:通常,我们会选择在大规模图像数据集(如ImageNet)上预训练的模型,如VGG、ResNet等。然后,根据新数据集的特点,选择需要微调的模型层。对于低级特征的任务(如边缘检测),最好使用浅层模型的层,而对于高级特征的任务(如分类),则应选择更深层次的模型。

2、冻结预训练模型的参数:保持预训练模型的权重不变,只训练新增加的层或者微调一些层,避免因为在数据集中过拟合导致预训练模型过度拟合。

3、在新数据集上训练新增加的层:在冻结预训练模型的参数情况下,训练新增加的层。这样,可以使新模型适应新的任务,从而获得更高的性能。

4、微调预训练模型的层:在新层上进行训练后,可以解冻一些已经训练过的层,并且将它们作为微调的目标。这样做可以提高模型在新数据集上的性能。

5、评估和测试:在训练完成之后,使用测试集对模型进行评估。如果模型的性能仍然不够好,可以尝试调整超参数或者更改微调层。

太多概念,我们直接使用残差网络进行迁移学习。

二、残差网络ResNet

1.了解ResNet

ResNet 网络是在 2015年 由微软实验室中的何凯明等几位大神提出,斩获当年ImageNet竞赛中分类任务第一名,目标检测第一名。获得COCO数据集中目标检测第一名,图像分割第一名。

传统卷积神经网络存在的问题?

卷积神经网络都是通过卷积层和池化层的叠加组成的。 在实际的试验中发现,随着卷积层和池化层的叠加,学习效果不会逐渐变好,反而出现2个问题:

1、梯度消失和梯度爆炸 梯度消失:若每一层的误差梯度小于1,反向传播时,网络越深,梯度越趋近于0 梯度爆炸:若每一层的误差梯度大于1,反向传播时,网络越深,梯度越来越大

2、退化问题

如何解决问题?

为了解决梯度消失或梯度爆炸问题,论文提出通过数据的预处理以及在网络中使用 BN(Batch Normalization)层来解决。 为了解决深层网络中的退化问题,可以人为地让神经网络某些层跳过下一层神经元的连接,隔层相连,弱化每层之间的强联系。这种神经网络被称为 残差网络 (ResNets)。

实线为测试集错误率 虚线为训练集错误率

2.ResNet网络---残差结构

ResNet的经典网络结构有:ResNet-18ResNet-34ResNet-50ResNet-101ResNet-152几种,其中,ResNet-18和ResNet-34的基本结构相同,属于相对浅层的网络,后面3种的基本结构不同于ResNet-18和ResNet-34,属于更深层的网络。

不论是多少层的ResNet网络,它们都有以下共同点:

  • 网络一共包含5个卷积组,每个卷积组中包含1个或多个基本的卷积计算过程(Conv-> BN->ReLU)
  • 每个卷积组中包含1次下采样操作,使特征图大小减半,下采样通过以下两种方式实现:
    • 最大池化,步长取2,只用于第2个卷积组(Conv2_x)
    • 卷积,步长取2,用于除第2个卷积组之外的4个卷积组
  • 第1个卷积组只包含1次卷积计算操作,5种典型ResNet结构的第1个卷积组完全相同,卷积核均为7x7, 步长为均2
  • 第2-5个卷积组都包含多个相同的残差单元,在很多代码实现上,通常把第2-5个卷积组分别叫做Stage1、Stage2、Stage3、Stage4
  • 首先是第一层卷积使用kernel 7∗7,步长为2,padding为3。之后进行BN,ReLU和maxpool。这些构成了第一部分卷积模块conv1。
  • 然后是四个stage,有些代码中用make_layer()来生成stage,每个stage中有多个模块,每个模块叫做building block,resnet18= [2,2,2,2],就有8个building block。注意到他有两种模块**BasicBlock** 和Bottleneck 。resnet18和resnet34用的是BasicBlock ,resnet50及以上用的是Bottleneck 。无论BasicBlock 还是Bottleneck 模块,都用到了残差连接( shortcut connection)方式:

下图以ResNet18为例介绍一下它的网络模型

layer1

ResNet18 ,使用的是 BasicBlocklayer1,特点是没有进行降采样,卷积层的 stride = 1,不会降采样。在进行 shortcut 连接时,也没有经过 downsample 层。

layer2,layer3,layer4

layer2layer3layer4 的结构图如下,每个 layer 包含 2 个 BasicBlock,但是第 1 个 BasicBlock 的第 1 个卷积层的 stride = 2,会进行降采样。在进行 shortcut 连接时,会经过 downsample 层,进行降采样和降维

residual结构使用了一种shortcut的连接方式,也可理解为捷径。让特征矩阵隔层相加,注意F(X)和X形状要相同,所谓相加是特征矩阵相同位置上的数字进行相加。

一个残差块有2条路径 F(x)和 x,F(x) 路径拟合残差,可称之为残差路径; 路径为`identity mapping`恒等映射,可称之为`shortcut`。图中的⊕为`element-wise addition`,要求参与运算的F(x) 和 x的尺寸要相同。

其中关键技术 Batch Normalization是对每一个卷积后进行标准化

Batch Normalization目的:使所有的feature map满足均值为0,方差为1的分布规律

三、代码分析

1. 导入必要的库

python 复制代码
import torch
from torch.utils.data import DataLoader,Dataset  # 数据加载相关
from PIL import Image  # 图像处理
from torchvision import transforms  # 数据预处理
import numpy as np
from torch import nn  # 神经网络模块
import torchvision.models as models  # 预训练模型

2. 模型准备(迁移学习)

这部分是迁移学习的重点,

python 复制代码
# 加载预训练的ResNet-18模型
resnet_model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)

# 冻结所有预训练参数(迁移学习常用策略)
for param in resnet_model.parameters():
    print(param)  # 打印参数(实际应用中可删除)
    param.requires_grad = False  # 冻结参数,不参与训练

# 获取原模型最后一层的输入特征数
in_features = resnet_model.fc.in_features  # ResNet18的fc层输入是512

# 替换最后一层全连接层,输出类别数为20(根据实际任务调整)
resnet_model.fc = nn.Linear(in_features, 20)

# 收集需要更新的参数(只有新替换的全连接层参数)
params_to_update = []
for param in resnet_model.parameters():
    if param.requires_grad == True:
        params_to_update.append(param)

这里采用了迁移学习策略:冻结预训练模型的大部分参数,只训练最后一层的分类器,这样可以加快训练速度并提高效果。

  • models.resnet18():创建 ResNet-18 网络结构
  • weights=models.ResNet18_Weights.DEFAULT:使用在 ImageNet 数据集上预训练好的权重初始化模型
  • 迁移学习的关键操作:保留预训练模型学到的特征提取能力
  • requires_grad = False:告诉 PyTorch 不需要计算这些参数的梯度
  • 原 ResNet-18 用于 1000 类分类,这里替换为 20 类分类
  • 只训练新替换的全连接层参数,大大减少计算量

3. 数据预处理

python 复制代码
data_transforms = {
    'train': transforms.Compose([  # 训练集的数据增强
        transforms.Resize([300, 300]),  # 调整大小
        transforms.RandomRotation(45),  # 随机旋转
        transforms.CenterCrop(224),  # 中心裁剪
        transforms.RandomHorizontalFlip(p=0.5),  # 随机水平翻转
        transforms.RandomVerticalFlip(p=0.5),  # 随机垂直翻转
        transforms.ToTensor(),  # 转为Tensor
        # 归一化,使用ImageNet的均值和标准差
        transforms.Normalize([0.485, 0.456, 0.486], [0.229, 0.224, 0.225])
    ]),
    'valid': transforms.Compose([  # 验证集不做数据增强,只做必要处理
        transforms.Resize([224, 224]),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.486], [0.229, 0.224, 0.225])
    ]),
}

4. 自定义数据集类

python 复制代码
class food_dataset(Dataset):  # 继承Dataset类
    def __init__(self, file_path, transform=None):
        self.file_path = file_path
        self.imgs = []  # 存储图像路径
        self.labels = []  # 存储标签
        self.transform = transform
        
        # 从文件中读取图像路径和标签
        with open(file_path, 'r') 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)

        # 处理标签,转为Tensor
        label = self.labels[idx]
        label = torch.from_numpy(np.array(label, dtype=np.int64))
        return image, label

5. 数据加载器

python 复制代码
# 创建训练集和测试集
train_data = food_dataset(file_path='train.txt', transform=data_transforms['train'])
test_data = food_dataset(file_path='test.txt', transform=data_transforms['train'])  # 注意这里可能应该用'valid'

# 创建数据加载器,用于批量加载数据
train_dataloader = DataLoader(train_data, batch_size=32, shuffle=True)
test_dataloader = DataLoader(test_data, batch_size=32, shuffle=True)

6. 设备配置

python 复制代码
# 自动选择可用的计算设备(GPU优先)
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)

7. 训练函数

python 复制代码
def train(dataloader, model, loss_fn, optimizer):
    model.train()  # 切换到训练模式
    batch_size_num = 1
    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()  # 更新参数
        
        # 打印训练信息
        loss = loss.item()
        if batch_size_num % 64 == 0:
            print(f"loss: {loss:>7f} [number: {batch_size_num}]")
        batch_size_num += 1

8. 测试函数

python 复制代码
best_acc = 0  # 记录最佳准确率

def test(dataloader, model, loss_fn):
    size = len(dataloader.dataset)
    num_batches = len(dataloader)
    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 /= num_batches
    correct /= size
    print(f"Test result:\n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f}")
    
    # 保存最佳模型
    global best_acc
    if correct > best_acc:
        best_acc = correct
        torch.save(model, 'best3.pt')  # 保存整个模型

9. 训练配置和执行

python 复制代码
# 定义损失函数和优化器
loss_fn = nn.CrossEntropyLoss()  # 交叉熵损失,适用于分类任务
optimizer = torch.optim.Adam(params_to_update, lr=0.001)  # Adam优化器

# 学习率调度器,每10个epoch学习率乘以0.5
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.5)

# 训练轮次
epochs = 20
acc_s = []
loss_s = []

# 开始训练
for t in range(epochs):
    print(f"Epoch {t+1}\n-----------------------")
    train(train_dataloader, model, loss_fn, optimizer)
    test(test_dataloader, model, loss_fn)
    scheduler.step()  # 更新学习率
print("Done!")
print(f"最佳的结果:\n Accuracy: {(100*best_acc):>0.1f}%")

整体流程总结

  1. 加载预训练的 ResNet-18 模型并修改最后一层以适应新任务
  2. 定义数据预处理和增强方法
  3. 创建自定义数据集类来读取图像和标签
  4. 设置训练设备(GPU 或 CPU)
  5. 定义训练和测试函数
  6. 配置优化器、损失函数和学习率调度器
  7. 执行多轮训练,每轮结束后在测试集上评估并保存最佳模型

最后我们都结果可以达到百分之90左右,效果得到很大的提升。

相关推荐
NAGNIP3 小时前
一文搞懂深度学习中的通用逼近定理!
人工智能·算法·面试
冬奇Lab4 小时前
一天一个开源项目(第36篇):EverMemOS - 跨 LLM 与平台的长时记忆 OS,让 Agent 会记忆更会推理
人工智能·开源·资讯
冬奇Lab4 小时前
OpenClaw 源码深度解析(一):Gateway——为什么需要一个"中枢"
人工智能·开源·源码阅读
AngelPP8 小时前
OpenClaw 架构深度解析:如何把 AI 助手搬到你的个人设备上
人工智能
宅小年8 小时前
Claude Code 换成了Kimi K2.5后,我再也回不去了
人工智能·ai编程·claude
九狼8 小时前
Flutter URL Scheme 跨平台跳转
人工智能·flutter·github
ZFSS8 小时前
Kimi Chat Completion API 申请及使用
前端·人工智能
天翼云开发者社区9 小时前
春节复工福利就位!天翼云息壤2500万Tokens免费送,全品类大模型一键畅玩!
人工智能·算力服务·息壤
知识浅谈9 小时前
教你如何用 Gemini 将课本图片一键转为精美 PPT
人工智能
Ray Liang10 小时前
被低估的量化版模型,小身材也能干大事
人工智能·ai·ai助手·mindx