基于迁移学习训练自己的图像分类模型

2023年 9月 26日 68.1k 0

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

beginning

   
如果给你一个小型数据集让你实现图像分类任务,为了尽可能的提高准确度,你会怎么做呢?有过训练模型经验的小伙伴们肯定会选择将在大型公共数据集上训练得到的模型作为自己任务的预训练模型。但是小伙伴们有没有想过,为什么一个模型在解决一个问题上表现出色,它在处理一个不同的任务时也同样优秀?其实,这就是迁移学习的思想。它赋予了模型“经验”的能力,使得它们能在一个领域中学到的知识,对另一个领域的学习产生积极的影响(是不是和上期的知识蒸馏有异曲同工之妙腻)。今天咱们就来学习一下迁移学习,并通过代码实战在ImageNet预训练图像分类模型基础上,对自己的图像分类数据集进行迁移学习-微调训练,得到自己的分类模型。废话不多说啦,让我们一起愉快的学习叭🎈🎈🎈

pic03.png

1.迁移学习简介

   
当我们学习新东西时,先前的经验常常会派上用场。比如,学会了骑自行车后,学会摩托车会容易许多,因为你已经掌握了保持平衡的技能。这就是迁移学习的核心思想:利用在一个任务上学到的知识,来改善在另一个相关任务上的表现🌈🌈🌈

   
在计算机领域,迁移学习也是类似的。假设我们有一个预训练的卷积神经网络,它在ImageNet数据集上训练过,可以识别1000个不同的物体类别(如狗、猫、汽车等)。现在,我们想要在医学图像中识别疾病,比如肺部X射线中的结节。但是,我们手头上没有足够的标记的医学图像来训练一个深度学习模型。这时候,迁移学习就发挥作用了——我们可以利用在大型数据集上预训练得到的CNN,通过微调模型或者共享一些特征,来让它更快地学会识别医学图片。因为预训练网络已经学会了许多通用的特征,如边缘检测、纹理等,这些对于医学图像识别也是非常有用的,使得我们在相对较小的医学图像数据集上也能训练一个强大的模型(迁移学习思想是不是超好理解腻)🌞🌞🌞

pic05.png

   
迁移学习算法按照分类标准不同,可以分为不同的方法,比如按照数据来源分类,可以分为同源迁移和异源迁移;按照知识传递方式分类,可以分为基于实例、特征、模型的迁移......我这里介绍几个咱们经常使用的迁移学习策略:

  • 特征提取🎈🎈🎈:将预训练模型的前几层(通常是卷积层)冻结,只训练定制的分类器来适应新任务。例如,在图像识别中,可以使用在大型图像数据集上训练过的模型,然后冻结前面的卷积层,只训练全连接层以适应新的分类任务。
  • Fine-tuning🎈🎈🎈:允许模型在新任务上微调整个或部分层。例如可以解冻预训练模型的一部分,如后几层的卷积层,以便在新任务上微调特定的特征。
  • 领域自适应:通过将知识从一个领域迁移到另一个领域来解决分布不匹配的问题。例如,在将一个在城市场景中训练的交通标志识别模型应用到乡村场景中。
  • 元学习🎈🎈🎈:这是一种“学习如何学习”的范式,学习如何在新任务上进行学习,使模型能够快速适应新任务。
  • 模型蒸馏🎈🎈🎈:通过将大型模型的知识传递给小型模型来提高小型模型的性能。这在资源受限的环境中很有用,例如在移动设备上部署深度学习模型(这不就是我上期讲过的知识蒸馏嘛,看来知识学来学去也是一个圈)
  • 2.图像分类实战

       
    好啦,迁移学习的思想小伙伴们差不多都明白啦。为了避免一听就懂,一上手就废,咱们以pytorch图像分类为例,赶紧来看看迁移学习是怎么在实战代码中具体使用的叭(盆友们不用担心会很难喔,都是基础知识构成的,新手友好)🌟🌟🌟

    2.1安装配置环境

  • 安装各类库:
  • !pip install numpy pandas matplotlib seaborn plotly requests tqdm opencv-python pillow wandb -i https://pypi.tuna.tsinghua.edu.cn/simple
    
  • 下载中文字体文件:
  • !wget https://zihao-openmmlab.obs.cn-east-3.myhuaweicloud.com/20220716-mmclassification/dataset/SimHei.ttf --no-check-certificate
    
  • 创建目录:
  • import os
    # 存放结果文件
    os.mkdir('output')
    
    # 存放训练得到的模型权重
    os.mkdir('checkpoint')
    # 特别注意,不要把文件夹命名成 checkpoints (不要在最后加 s) ,否则在 Jupyter 中打不开,只能压缩后下载压缩包到本地才能打开
    
    # 存放生成的图表
    os.mkdir('图表')
    
  • 设置matplotlib中文字体(以下二选一):
  • # # windows操作系统
    # plt.rcParams['font.sans-serif']=['SimHei']  # 用来正常显示中文标签 
    # plt.rcParams['axes.unicode_minus']=False  # 用来正常显示负号
    
    # Linux操作系统,例如 云GPU平台:https://featurize.cn/?s=d7ce99f842414bfcaea5662a97581bd1
    # 如果报错 Unable to establish SSL connection.,重新运行本代码块即可
    !wget https://zihao-openmmlab.obs.cn-east-3.myhuaweicloud.com/20220716-mmclassification/dataset/SimHei.ttf -O /environment/miniconda3/lib/python3.7/site-packages/matplotlib/mpl-data/fonts/ttf/SimHei.ttf --no-check-certificate
    !rm -rf /home/featurize/.cache/matplotlib
    
    import matplotlib
    import matplotlib.pyplot as plt
    %matplotlib inline
    matplotlib.rc("font",family='SimHei') # 中文字体
    plt.rcParams['axes.unicode_minus']=False  # 用来正常显示负号
    
  • 测试成功与否:
  • plt.plot([1,2,3], [100,500,300])
    plt.title('matplotlib中文字体测试', fontsize=25)
    plt.xlabel('X轴', fontsize=15)
    plt.ylabel('Y轴', fontsize=15)
    plt.show()
    

       
    如果没有进行中文字体设置的话,matplotlib是写不了中文字体的,遇到中文会写成方框喔。测试成功的话会显示如下。这一步非常关键啊,任何编程、跑任何的代码库,最重要也是最难的一步就是安装配置环境(是不是深有感触腻)✨✨✨

    pic01.png

    2.2准备数据集

    # 下载数据集压缩包
    !wget https://zihao-openmmlab.obs.cn-east-3.myhuaweicloud.com/20220716-mmclassification/dataset/fruit30/fruit30_split.zip
    # 解压
    !unzip fruit30_split.zip >> /dev/null
    # 删除压缩包
    !rm fruit30_split.zip
    

    查看数据集目录结构:

    !sudo snap install tree
    !tree fruit30_split -L 2
    

    pic02.png

       
    今天的教程我们使用的是水果数据集(b站up主的fruit30水果图像分类数据集)。除此之外你也可以使用自己的数据集喔,怎么方便怎么来。我们运行代码之后可以看到数据集fruit30_split分为train和val,两个文件夹下各自有30个水果图像文件,就是说我们已经划分好了训练集和测试集,用训练集中的图像去训练,然后拿训练得到的模型在测试集上去评估它的效果😁😁😁

    2.3代码准备

  • 导入工具包:
  • import time
    import os
    
    import numpy as np
    from tqdm import tqdm
    
    import torch
    import torchvision
    import torch.nn as nn
    import torch.nn.functional as F
    
    import matplotlib.pyplot as plt
    %matplotlib inline
    
    # 忽略烦人的红色提示
    import warnings
    warnings.filterwarnings("ignore")
    
  • 获取计算硬件:
  • # 有 GPU 就用 GPU,没有就用 CPU
    device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
    print('device', device)
    
  • 图像预处理:
  • from torchvision import transforms
    
    # 训练集图像预处理:缩放裁剪、图像增强、转 Tensor、归一化
    train_transform = transforms.Compose([transforms.RandomResizedCrop(224),
                                          transforms.RandomHorizontalFlip(),
                                          transforms.ToTensor(),
                                          transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
                                         ])
    
    # 测试集图像预处理-RCTN:缩放、裁剪、转 Tensor、归一化
    test_transform = transforms.Compose([transforms.Resize(256),
                                         transforms.CenterCrop(224),
                                         transforms.ToTensor(),
                                         transforms.Normalize(
                                             mean=[0.485, 0.456, 0.406], 
                                             std=[0.229, 0.224, 0.225])
                                        ])
    

       
    在图像预处理中,对训练集和测试集的图像分别设置预处理方法。对训练集的图像我们先进行随机的缩放裁剪,然后再进行一些图像增强,比如随机的水平翻转,转成pytorch的tensor,进行归一化,Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])分别是RGB通道的均值和方差。对测试集的图像呢,就不需要图像增强了。先把任意一张图像缩放到256×256的正方形图像,然后从中心裁剪出一个224×224的正方形小块,转成tensor张量,再进行归一化✨✨✨

  • 载入图像分类数据集:
  • # 数据集文件夹路径
    dataset_dir = 'fruit30_split'
    train_path = os.path.join(dataset_dir, 'train')
    test_path = os.path.join(dataset_dir, 'val')
    print('训练集路径', train_path)
    print('测试集路径', test_path)
    
    from torchvision import datasets
    
    # 载入训练集
    train_dataset = datasets.ImageFolder(train_path, train_transform)
    
    # 载入测试集
    test_dataset = datasets.ImageFolder(test_path, test_transform)
    
    print('训练集图像数量', len(train_dataset))
    print('类别个数', len(train_dataset.classes))
    print('各类别名称', train_dataset.classes)
    

       
    我们指定文件夹的路径是fruit30_split,获取它的训练集和测试集路径;然后导入torchvision中的datasets模块,模块里面有一个非常好用的工具ImageFolder,直接传入训练集、测试集的文件夹路径,和相应预处理的方式,那么它就帮我们构建出了训练集和测试集。最后可以用len()函数查看图片数量和类别,运行之后看到训练集有4376张,有30个水果类别,比如哈密瓜、圣女果、山竹......✨✨✨

  • 类别和索引号一一对应:
  • # 各类别名称
    class_names = train_dataset.classes
    n_class = len(class_names)
    
    # 映射关系:类别 到 索引号
    train_dataset.class_to_idx
    
    # 映射关系:索引号 到 类别
    idx_to_labels = {y:x for x,y in train_dataset.class_to_idx.items()}
    
    # 保存为本地的 npy 文件
    np.save('idx_to_labels.npy', idx_to_labels)
    np.save('labels_to_idx.npy', train_dataset.class_to_idx)
    

       
    类别是按照哈密瓜......黄瓜的顺序排列的,所以索引号呢,哈密瓜为0,黄瓜为29,总共30个类别。我们就把这个字典记下来,会在多个地方用到,既有类别到索引号的映射,也有索引号到类别的映射。最后我们把idx_to_labels和labels_to_idx保存为npy文件,但凡后面涉及到类别和索引号的名称相互查询的时候就会用到这两个字典✨✨✨

  • 定义数据加载器DataLoader:
  • from torch.utils.data import DataLoader
    BATCH_SIZE = 32
    
    # 训练集的数据加载器
    train_loader = DataLoader(train_dataset,
                              batch_size=BATCH_SIZE,
                              shuffle=True,
                              num_workers=4
                             )
    
    # 测试集的数据加载器
    test_loader = DataLoader(test_dataset,
                             batch_size=BATCH_SIZE,
                             shuffle=False,
                             num_workers=4
                            )
    

       
    接下来就是pytorch中非常经典的数据加载器了,即在训练的时候,模型是如何给它一口一口喂数据的,每一口称之为一个batch,一口里面有32个数据✨✨✨

    2.4迁移学习微调训练

  • 导入训练使用的工具包:
  • from torchvision import models
    import torch.optim as optim
    

       
    接下来就是迁移学习的微调范式啦,我们可以选择三种策略。不同的迁移学习策略取决于你的数据分布,你自己的数据集分布和ImageNet这个大型数据集的分布到底有多大差异。比如我们的水果图像和ImageNet图像是非常类似的,甚至ImageNet里可能就有很多这30类水果的图片,所以我们可以选择下列的第一种——只微调模型训练最后一层,把前边模型权重都冻结。相当于我们换了一下最后的分类层,原来是1000个类别的输出,现在变成30个类别的输出。如果你的任务数据集和源数据集的分布很相似,这是比较推荐的一种迁移方式,就可以充分复用大规模数据集预训练的权重和特征🌴🌴🌴

  • 选择迁移学习策略:
  • 选择一:只微调训练模型最后一层(全连接分类层)

    model = models.resnet18(pretrained=True) # 载入预训练模型
    
    # 修改全连接层,使得全连接层的输出与当前数据集类别数对应
    # 新建的层默认 requires_grad=True
    model.fc = nn.Linear(model.fc.in_features, n_class)
    model.fc
    
    # 只微调训练最后一层全连接层的参数,其它层冻结
    optimizer = optim.Adam(model.fc.parameters())
    

       
    我们先载入resnet18(也可以换成Mobilenet、VGG等)的预训练模型,把它最后一层分类层从1000分类改成30分类,那么最后一层分类层它输入的是512维的特征,输出的是30个类别的logit分数。训练的话,我们只训练最后一层新加的分类层,前面层都冻结,用的优化器是Adam🌟🌟🌟

    选择二:微调训练所有层

    model = models.resnet18(pretrained=True) # 载入预训练模型
    
    model.fc = nn.Linear(model.fc.in_features, n_class)
    
    optimizer = optim.Adam(model.parameters())
    

       
    如果你的数据集和大型数据集不太像,就比如前几期的工业缺陷检测数据集,和猫儿狗儿分布不一致,那么可以选择微调训练所有层。仍然以resnet18预训练模型作为初始化权重,改动分类层之后,对所有层上的权重都进行微调。当然这个微调也是以预训练模型作为起点的,仍然可以一部分地复用大型数据集上的权重特征🌟🌟🌟

    选择三:随机初始化模型全部权重,从头训练所有层

    model = models.resnet18(pretrained=False) # 只载入模型结构,不载入预训练权重参数
    
    model.fc = nn.Linear(model.fc.in_features, n_class)
    
    optimizer = optim.Adam(model.parameters())
    

       
    如果你的数据和大型源数据完全不一样,从图像的尺寸、光照、环境等都不一样(比如用显微镜、望远镜拍摄的图像),那么我们可以只载入模型结构,不载入预训练权重参数,即载入一个随机初始化的resnet18,改动分类层之后,从头去训练模型权重的所有层。这样就相当于我们不管ImageNet数据集了,只在新数据集上重新训练resnet18🌟🌟🌟

  • 训练配置:
  • model = model.to(device)
    
    # 交叉熵损失函数
    criterion = nn.CrossEntropyLoss() 
    
    # 训练轮次 Epoch
    EPOCHS = 20
    
  • 运行完整训练:
  • # 遍历每个 EPOCH
    for epoch in tqdm(range(EPOCHS)):
    
        model.train()
    
        for images, labels in train_loader:  # 获取训练集的一个 batch,包含数据和标注
            images = images.to(device)
            labels = labels.to(device)
    
            outputs = model(images)           # 前向预测,获得当前 batch 的预测结果
            loss = criterion(outputs, labels) # 比较预测结果和标注,计算当前 batch 的交叉熵损失函数
            
            optimizer.zero_grad()
            loss.backward()                   # 损失函数对神经网络权重反向传播求梯度
            optimizer.step()   
    

       
    别看上述完整训练的代码很简短,其实也是pytorch通用的模板啦。首先遍历所有EPOCH,每一个epoch开始之后把模型调成训练模式。从train_loader中迭代的获取一个一个batch和对应的标签,把数据和标签转到设备device里,然后把数据喂到模型中执行前向预测,获得预测结果。接着由预测结果outputs和标注labels来计算交叉熵损失函数。最后是反向传播三部曲——优化器梯度清零、反向传播求梯度和优化器迭代更新。就是这么简单🧐🧐🧐

  • 在测试集上初步测试:
  • model.eval()
    with torch.no_grad():
        correct = 0
        total = 0
        for images, labels in tqdm(test_loader): # 获取测试集的一个 batch,包含数据和标注
            images = images.to(device)
            labels = labels.to(device)
            outputs = model(images)              # 前向预测,获得当前 batch 的预测置信度
            _, preds = torch.max(outputs, 1)     # 获得最大置信度对应的类别,作为预测结果
            total += labels.size(0)
            correct += (preds == labels).sum()   # 预测正确样本个数
    
        print('测试集上的准确率为 {:.3f} %'.format(100 * correct / total))
    

       
    首先把模型的模式从train训练改成eval评估,不再回传和计算梯度,从test_loader迭代地获取每一个batch的数据和标注,还是把数据输入到模型中执行前向预测,得到当前batch每一个数据的预测结果,用torch.max()获得当前batch置信度最高的预测类别。如果预测类别和标注类别是一样的,那么就把correct+1,我们就计算出了测试集中,总共的图像数量和预测正确的图像数量,把预测正确的数量除以总共数量就得到了测试集上的准确率。运行上述代码之后,得到准确率85.913%,如下所示。

    pic04.png

  • 保存模型:
  • torch.save(model, 'checkpoint/fruit30_pytorch_C1.pth')
    

       
    用torch.save()把模型保存下来,保存成一个.pth格式的文件。我们今天的目标就是要得到这个模型文件,大功告成咯✨✨✨

    ending

       
    看到这里相信盆友们都对迁移学习有了一个全面深入的了解啦,并且也学会了用代码实现三种迁移学习策略🌴🌴🌴很开心能把学到的知识以文章的形式分享给大家。如果你也觉得我的分享对你有所帮助,please一键三连嗷!!!下期见

    迷之自信.jpeg

    相关文章

    服务器端口转发,带你了解服务器端口转发
    服务器开放端口,服务器开放端口的步骤
    产品推荐:7月受欢迎AI容器镜像来了,有Qwen系列大模型镜像
    如何使用 WinGet 下载 Microsoft Store 应用
    百度搜索:蓝易云 – 熟悉ubuntu apt-get命令详解
    百度搜索:蓝易云 – 域名解析成功但ping不通解决方案

    发布评论