FAST.AI 图像分类实践
计算机视觉是深度学习中最常见的应用领域,其中主要有:图像分类、图像生成,对象检测、目标跟踪、语义分割、实例分割等。FAST.AI 作为一款基于 PyTorch 开发的快速深度学习工具,自然也就包含有大量更便利的图像处理模块和方法。接下来,我们将以最常见的图像分类为例,使用 FAST.AI 进行实践。
图像分类
图像分类是最为常见的一项深度学习任务,一般情况下,完成该类任务会有 3 个重要步骤。
首先,我们需要对原始数据进行处理,将图像数据转换为深度学习工具能够支持的张量数据。这一步骤往往就是制作相应的数据加载器。当然,FAST.AI 也有自己对应的数据加载器 DataBunch 对象,这部分内容已在前面章节完成学习。
接下来,就是构建深度神经网络模型。图像处理相关的任务,大部分都会使用卷积神经网络模型。卷积神经网络是一种非常擅长解决计算机视觉任务的神经网络模型。当然,无论是 PyTorch,还是 TensorFlow,构建一个神经网络模型的难度不高,我们往往只需要调用相应深度神经网络框架完成层堆叠即可。
最后,就是神经网络训练的部分。这部分代码一般是最为复杂的,我们需要对数据进行适当地处理,以正确的方式输入到神经网络。最后,对神经网络的输出进行处理和评估。神经网络训练的部分需要有一定的构建经验才能完成,尤其是在 PyTorch 的应用过程中,相对于 TensorFlow 更为复杂。
FAST.AI 基于 PyTorch 开发,实际上我认为其最大的改进之处就是优化了 PyTorch 训练神经网络复杂的过程。接下来,我们将通过一个图像分类示例,来学习使用 FAST.AI 完成一个完整的图像分类任务。
数据处理
接下来,我们选择前面接触过的 MNIST 数据集进行演示,MNIST 是一个 10 个类别的图像分类任务,数据体积较小,非常适合作为工具使用方法的示例数据。首先,我们加载数据,并构建 DataBunch 对象,这部分内容实际上已经学习过了。
from fastai.datasets import untar_data, URLs, download_data
from fastai.vision import ImageDataBunch
# 因原数据集下载较慢,从蓝桥云课服务器下载数据,本次实验时无需此行代码
download_data("https://labfile.oss.aliyuncs.com/courses/1445/mnist_png")
mnist_path = untar_data(URLs.MNIST)
mnist_data = ImageDataBunch.from_folder(mnist_path, 'training', 'testing')
mnist_data
模型构建
构建完数据加载器 DataBunch 之后,接下来就可以开始构建模型了。一般情况下,构建一个图像分类模型有 2 种思路,分别是从头构建和迁移学习。从头构建,即意味着由你自己设计模型的结构和参数。而迁移学习则是利用一些在经典神经网络结构上预训练的模型进行学习。
首先,我们选择从头构建模型。FAST.AI 提供了一个非常友好的接口 fastai.vision.simple_cnn
来快速实现卷积神经网络的构建。该 API 包含 4 个参数:
actns
:定义卷积模块的数量和输入输出大小。
kernel_szs
:定义卷积核大小,默认为 3。
strides
:定义卷积步长大小,默认为 2。
bn
:是否包含批量归一化操作,布尔类型。
接下来,我们就调用该接口来快速定义一个卷积神经网络。
from fastai.vision import simple_cnn
model = simple_cnn(actns=(3, 16, 16, 10))
model
如上所示,我们只是定义了卷积神经网络包含的卷积模块数量和输入输出大小。该参数主要注意输入和输出尺寸,其中,(3, 16, 16, 10) 表示有 4 个卷积模块。因为前面的 DataBunch 对象尺寸为 (3, 28, 28),即为 3 个通道图像,所以第一层卷积操作的输入尺寸为 3。由于是 10 分类问题,所以最后一个数字是 10。中间层的尺寸可以自定义,我们选择了 16。fastai.vision.simple_cnn 最终会自动构建为 PyTorch 支持的 Sequential 顺序模型。
训练评估
有了模型之后,我们就可以开始第三步,也就是训练过程。FAST.AI 的模型训练过程会用到其核心类 fastai.vision.Learner。最简单的情况下,我们只需要将数据 DataBunch,模型和评估指标传入,即可开始训练。
from fastai.vision import Learner, accuracy
# 传入数据,模型和准确度评估指标
learner = Learner(mnist_data, model, metrics=[accuracy])
learner
如上所示,我们定义的 Learner 选择了 accuracy 准确度作为评估指标。你可以通过 Learner 的输出看到其他相关的默认参数设置。例如优化器 opt_func 选择了 Adam,损失函数 loss_func 选择了交叉熵。
加下来,我们可以调用 Learner 完成最终的训练,训练方法为 Learner.fit,传入迭代次数 Epoch 即可。
learner.fit(1) # 数据集上训练迭代 1 次
最终,Learner 会打印出最终的训练损失,验证损失,准确度和训练所用时长。至此,我们就使用 FAST.AI 完成了一次针对 MNIST 的图像分类过程。我们整理上面的完整代码如下:
mnist_path = untar_data(URLs.MNIST)
mnist_data = ImageDataBunch.from_folder(mnist_path, 'training', 'testing')
model = simple_cnn(actns=(3, 16, 16, 10))
learner = Learner(mnist_data, model, metrics=[accuracy])
learner.fit(1)
你可以看出,使用 FAST.AI 完成 MNSIT 分类我们只使用了 5 行代码,而相比于 PyTorch 需要的数十行代码和复杂的构建过程,FAST.AI 中的 FAST 是显而易见的。
迁移学习
上面,我们从头构建了一个卷积神经网络并针对 MNSIT 进行了训练。由于 MNSIT 数据集本身就质量较高,背景纯净,数据规范,所以最终准确度还是不错的。如果你将上方 Learner 训练迭代次数调至 3~5 次,准确度还会有一定的提升,并最终超过 90%。但对于一些复杂的任务,尤其是样本数据不规范的情况下,从头开始训练并不是一个很明智的选择。所以,很多时候我们会使用预训练模型做迁移学习。
迁移学习是一种站在巨人的肩膀上的训练方法。我们可以沿用一些经典神经网络在大型数据集训练好的模型,使用自定义数据集继续更新其中部分层的权重。最终,可以在较少的时间下取得不错的训练效果。
FAST.AI 提供的预训练模型大部分直接来自于 PyTorch,你可以通过 此页面 浏览这些模型。接下来,我们以 ResNet18 为例,针对上面的 MNIST 数据完成一次迁移学习过程。ResNet18 是 ResNet 精简结构在 ImageNet 数据集上得到的预训练模型,首先载入该模型并查看结构。
from fastai.vision import models
models.resnet18()
可以看出,相比于我们之前自行搭建的 CNN 结构,ResNet18 要复杂很多。解析来的训练过程需要利用 fastai.vision.cnn_learner 类来构建 Learner,这一点也与上面有所不同。你只需要记住,如果是从头开始就使用 fastai.vision.Learner,如果是迁移学习就使用 fastai.vision.cnn_learner 即可。
from fastai.vision import cnn_learner
# 构建基于 ResNet18 的 Learner 学习器
learner = cnn_learner(mnist_data, models.resnet18, metrics=[accuracy])
learner.fit(1) # 训练迭代 1 次
你可以看到 Learner 会自动下载 ResNet18 的 .pth 预训练权重文件,然后开始训练迭代过程。训练过程相对于上方会更长一些,原因是模型复杂度更高。最终,使用 ResNet18 完成 1 次迭代的准确度,应该会比上方我们自定义的模型高一些。
所以,当我们使用 FAST.AI 执行迁移学习时,代码可以进一步精简至 4 行。这对于使用 PyTorch 和 TensorFlow 是不可想象的简单,也体现了高阶 API 的优势。
mnist_path = untar_data(URLs.MNIST)
mnist_data = ImageDataBunch.from_folder(mnist_path, 'training', 'testing')
learner = Learner(mnist_data, models.resnet18, metrics=[accuracy])
learner.fit(1)
虽然我们的准确度已经达到了 90% 以上,但模型仍然对部分验证数据无法准确区分。接下来,我们可以通过 FAST.AI 提供的 fastai.vision.ClassificationInterpretation
方法来对结果进行进一步分析。
from fastai.vision import ClassificationInterpretation
# 载入学习器
interp = ClassificationInterpretation.from_learner(learner)
interp
首先,我们可以输出那些被分类器预测错误的样本进行观察。直接通过 interp.plot_top_losses
方法输出损失最大的 9 个验证样本,并比对它们本来的标签和预测结果。
interp.plot_top_losses(9, figsize=(9, 9))
上面依次输出了预测标签,真实标签,损失和预测概率。你可以看到,部分样本的确人眼都很难完成辨识,当然也有一些人眼可辨识样本被错误分类。
除了比对图像,FAST.AI 还提供了一个非常方便的方法 interp.plot_confusion_matrix。通过该方法,我们可以直接绘制出真实标签和预测标签之间的混淆矩阵。
interp.plot_confusion_matrix(figsize=(5, 5), dpi=100)
混淆矩阵展示了真实标签和预测标签对应样本的数量。可以看出,0-9 这 10 类样本在分布上没有明显的倾斜。你也可以进一步看出,到底哪些样本更容易被预测错误,以及被错误预测的标签结果。
数据扩增
数据在神经网络训练过程中伴有很大的左右,如果符合要求的数据越多,往往训练的结果也更好。所以,很多时候我们会对现有数据进行一些旋转、变换、镜像、归一化等操作。这些操作不仅可以在一定程度上起到数据扩增的效果,能够对模型训练带来一些帮助。
FAST.AI 提供了一个非常方便的函数 fastai.vision.get_transforms
用于对图像进行变换,该函数的主要参数有:
do_flip
:如果为 True
,则以 0.5 的概率应用随机翻转。
flip_vert
:应用水平翻转。如果 do_flip=True
时,则可以垂直翻转图像或旋转 90 度。
max_rotate
:如果不为 None
,则在 -max_rotate
和 max_rotate
度之间随机旋转,概率为 p_affine
。
max_zoom
:如果不是 1 或小于 1,则在 1 之前进行随机缩放,并以 p_affine
概率应用 max_zoom
。
max_lighting
:如果不为 None
,则以 max_lighting
概率 p_lighting
施加由 max_lighting
控制的随机噪声和对比度变化。
max_warp
:如果不是 None
,则以概率 p_affine
施加 -max_warp
和 maw_warp
之间的随机对称扭曲。
p_affine
:应用每个仿射变换和对称扭曲的概率。
p_lighting
:应用每个照明变换的概率。
xtra_tfms
:您想要应用的其他变换的列表。
接下来,通过一个直观的例子来演示数据变换扩增的效果。我们读取 MNIST 训练数据中第一个样本:
img, label = mnist_data.train_ds[0]
img.show(title=f'{label}')
然后,我们尝试对该数据进行随机旋转变换操作。为了更加方便地演示旋转后的效果,这里定义一个辅助绘图函数 plots_f
。
from fastai.vision import get_transforms
from matplotlib import pyplot as plt
%matplotlib inline
# 辅助绘图函数,参考自 FAST.AI 官方文档
def plots_f(rows, cols, width, height, **kwargs):
[img.apply_tfms(tfms[0], **kwargs).show(ax=ax) for i, ax in enumerate(plt.subplots(
rows, cols, figsize=(width, height))[1].flatten())]
接下来,定义变换操作并应用绘图。
# 定义变换操作,最大 [-25, 25] 度之间的随机旋转
tfms = get_transforms(max_rotate=25)
# 绘制样本变换后图像
plots_f(2, 4, 12, 6, size=224)
可以看到,样本被执行了 -25 度到 25 度之间的随机旋转操作。不过,上面的示例有一定的缺陷。因为对于手写字符,较大幅度的旋转或镜像图像会严重影响样本所反映的内容,甚至变成完全不是数字的样子。所以,对于 MNIST 这类数据,我们往往只能应用小幅度的旋转、添加噪声等变换,以避免对样本本身含义的影响。但是,对于如下所示的动物图像,更大幅度的变换对数据集扩增更有意义。
fastai.vision.get_transforms
操作一般会直接添加至 DataBunch 对象生成过程中,这样就可以将变换操作应用于样本数据。
# 示例,制作 DataBunch 对象时添加 get_transforms 操作
tfms = get_transforms(max_rotate=25)
tfms_data = ImageDataBunch.from_folder(mnist_path, 'training', 'testing', ds_tfms=tfms)
tfms_data.show_batch(rows=3, figsize=(5,5))
CIFAR10 图像分类挑战
前面的挑战中,我们已经熟悉了 CIFAR10 数据集,并将其处理成 FAST.AI 支持的 DataBunch 对象。本次挑战中,我们同样需读取 CIFAR10 数据集,并添加针对数据集变换的预处理过程。
接下来,请将 CIFAR10 数据集处理成 DataBunch 对象。挑战要求,将 train 文件夹中数据分离 20% 作为验证集,剩下数据作为训练集。test 文件夹下数据作为测试集。同时,加入 get_transforms 变换,应用[−30,30] 度之间的随机旋转变换。
from fastai.datasets import untar_data, URLs, download_data
from fastai.vision import ImageDataBunch, get_transforms
download_data("http://labfile.oss.aliyuncs.com/courses/1445/cifar10")
data_path = untar_data(URLs.CIFAR)
# 针对数据集变换
tfms = get_transforms(max_rotate=30)
data_bunch = ImageDataBunch.from_folder(data_path, train='train', test='test',
valid_pct=0.2, ds_tfms=tfms)
接下来,请使用 FAST.AI 提供的建模方法,应用卷积神经网络对 CIFAR10 完成分类和评估。你可以自由选择「从头开始训练」或「迁移学习方法」。迁移学习所使用的预训练模型也可以通过阅读官方文档自由选择。
挑战最终要求,验证集上的分类准确度不得低于 70%。由于训练时间较长,你可以在恰当的时候中止训练。
from fastai.vision import models, cnn_learner, accuracy
models.resnet18()
# 构建基于 ResNet18 的 Learner 学习器
learner = cnn_learner(data_bunch, models.resnet18, metrics=[accuracy])
learner.fit(15) # 训练迭代 15 次
仅供参考,accuracy 最终大于 70% 即可。