本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
beginning
如果给你一个小型数据集让你实现图像分类任务,为了尽可能的提高准确度,你会怎么做呢?有过训练模型经验的小伙伴们肯定会选择将在大型公共数据集上训练得到的模型作为自己任务的预训练模型。但是小伙伴们有没有想过,为什么一个模型在解决一个问题上表现出色,它在处理一个不同的任务时也同样优秀?其实,这就是迁移学习的思想。它赋予了模型“经验”的能力,使得它们能在一个领域中学到的知识,对另一个领域的学习产生积极的影响(是不是和上期的知识蒸馏有异曲同工之妙腻)。今天咱们就来学习一下迁移学习,并通过代码实战在ImageNet预训练图像分类模型基础上,对自己的图像分类数据集进行迁移学习-微调训练,得到自己的分类模型。废话不多说啦,让我们一起愉快的学习叭🎈🎈🎈
1.迁移学习简介
当我们学习新东西时,先前的经验常常会派上用场。比如,学会了骑自行车后,学会摩托车会容易许多,因为你已经掌握了保持平衡的技能。这就是迁移学习的核心思想:利用在一个任务上学到的知识,来改善在另一个相关任务上的表现🌈🌈🌈
在计算机领域,迁移学习也是类似的。假设我们有一个预训练的卷积神经网络,它在ImageNet数据集上训练过,可以识别1000个不同的物体类别(如狗、猫、汽车等)。现在,我们想要在医学图像中识别疾病,比如肺部X射线中的结节。但是,我们手头上没有足够的标记的医学图像来训练一个深度学习模型。这时候,迁移学习就发挥作用了——我们可以利用在大型数据集上预训练得到的CNN,通过微调模型或者共享一些特征,来让它更快地学会识别医学图片。因为预训练网络已经学会了许多通用的特征,如边缘检测、纹理等,这些对于医学图像识别也是非常有用的,使得我们在相对较小的医学图像数据集上也能训练一个强大的模型(迁移学习思想是不是超好理解腻)🌞🌞🌞
迁移学习算法按照分类标准不同,可以分为不同的方法,比如按照数据来源分类,可以分为同源迁移和异源迁移;按照知识传递方式分类,可以分为基于实例、特征、模型的迁移......我这里介绍几个咱们经常使用的迁移学习策略:
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('图表')
# # 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是写不了中文字体的,遇到中文会写成方框喔。测试成功的话会显示如下。这一步非常关键啊,任何编程、跑任何的代码库,最重要也是最难的一步就是安装配置环境(是不是深有感触腻)✨✨✨
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
今天的教程我们使用的是水果数据集(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文件,但凡后面涉及到类别和索引号的名称相互查询的时候就会用到这两个字典✨✨✨
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%,如下所示。
torch.save(model, 'checkpoint/fruit30_pytorch_C1.pth')
用torch.save()把模型保存下来,保存成一个.pth格式的文件。我们今天的目标就是要得到这个模型文件,大功告成咯✨✨✨
ending
看到这里相信盆友们都对迁移学习有了一个全面深入的了解啦,并且也学会了用代码实现三种迁移学习策略🌴🌴🌴很开心能把学到的知识以文章的形式分享给大家。如果你也觉得我的分享对你有所帮助,please一键三连嗷!!!下期见