深度学习笔记(Practical Deep Learning for Coders, v3):1.快速建立一个CNN模型

1,042 阅读7分钟

在这节课中,我们将从零开始构建我们自己的图像分类器,看看是否能够取得顶级的准确率。就让我们一探究竟吧!

我们将使用由 O. M. Parkhi et al., 2012引用的Oxford-IIIT Pet Dataset数据集,这个数据集中有12个品种的猫和25个品种的狗。我们的模型需要学会正确区分这37个品种。根据上述学者的论文描述,他们能取得的最佳分类准确率是59.21%。这个结果是基于一个专门检测宠物品种的复杂模型得到的,这个模型对宠物的照片分别建立了“肖像”、“头部”以及“躯干”的独立模型。让我们来看看,使用深度学习能够达到什么样的准确率吧!

1.浏览一下数据

项目代码将使用Jupyter Notebook编写,关于jupyter的使用请见fastai官方Git中的00_notebook_tutorial.ipynb

每一个notebook都由下面三行开始;它们确保你对库代码进行任何编辑,这些代码都将自动重新加载,并且任何图表或图片能在notebook中展示。

# 自动重新加载修改后的库文件
%reload_ext autoreload
%autoreload 2
# 一个notebook中只需要运行一次,则之后用matplotlib库作图不需要plt.show()即可把图展示出来。
%matplotlib inline

我们首先导入所有需要的包,然后就可以使用构建于Pytorch 1.0之上的fastai库。fastai库提供了大量有用的函数,可以帮助我们简单快捷地构建神经网络,并且训练出我们的模型。

import torch
# fastai中的CV库
from fastai.vision import *
# 误差评估
from fastai.metrics import error_rate
# 设置batch size(批次大小)
bs = 64

关于batch size,epoch和迭代(iteration)的区别,举一个例子就可以简单的理解:

如果一个数据集有20000个样本,一次将这些样本全部放入神经网络中是不现实的,所以我们把它分成4部分(即4个batch),这时,模型的batch size=5000。

如果用这个模型进行梯度下降优化,则数据集中的所有样本参与一次梯度下降为一个epoch。因为数据集被分成了4部分,所以该模型完成一个epoch需要4次迭代(iteration)。

详情参考:机器之心

图片分类数据处理方式的最主要区别是标签存储方式。在这个数据集中,标签本身就存在于文件名之中。如图:

我们需要将标签信息提取出来,从而将这些图片分门别类。幸运的是,fastai库提供了一个非常好用的函数来实现这一点,ImageDataBunch.from_name_re函数通过使用正则表达式从文件名中提取标签信息。

get_transforms():对图片进行缩放和剪裁等操作。

将图片大小设置为224的原因:CNN的最后一层是7个节点,GPU运算2的整数次幂比较快速,因此使用7*2^5=224。

# 正则表达式
pat = r'/([^/]+)_\d+.jpg$'
# 将原数据打包为databutch,shape(3, 224, 224),分成训练集和验证集
# normalize,特征归一化
data = ImageDataBunch.from_name_re(path_img, fnames, pat, ds_tfms=get_transforms(), size=224, bs=bs
                                  ).normalize(imagenet_stats)

上面正则表达式在源码中匹配的是形如'/american_pit_bull_terrier_159.jpg'这样的字符串路径,其实是fnames(文件路径)去掉了path_img(文件夹路径)。

# 上面正则表达式的使用
# ^表示文件开头,是非的意思,如[^/]表示非/的前n个字符
import re
result = re.match(r'/([^/]+)_(\d+).jpg$', '/american_pit_bull_terrier_159.jpg')
result.group(1)
# out 'american_pit_bull_terrier'

通过这个函数,我们把图片和标签分开了:

# 打印标签
print(data.classes)
# out ['Abyssinian', 'Bengal', 'Birman', 'Bombay', 'British_Shorthair', 'Egyptian_Mau', 'Maine_Coon', 'Persian', 'Ragdoll', 'Russian_Blue', 'Siamese', 'Sphynx', 'american_bulldog', 'american_pit_bull_terrier', 'basset_hound', 'beagle', 'boxer', 'chihuahua', 'english_cocker_spaniel', 'english_setter', 'german_shorthaired', 'great_pyrenees', 'havanese', 'japanese_chin', 'keeshond', 'leonberger', 'miniature_pinscher', 'newfoundland', 'pomeranian', 'pug', 'saint_bernard', 'samoyed', 'scottish_terrier', 'shiba_inu', 'staffordshire_bull_terrier', 'wheaten_terrier', 'yorkshire_terrier']
# 标签的类别数
len(data.classes)
# out 37

2.训练

现在我们将要开始训练模型了。我们将使用一个卷积神经网络作为主干结构,衔接一个单隐藏层的全连接头部,构成分类器模型。不理解这些是什么意思吗?不用担心,我们在接下来的课程中会做更深入的讲解。当下,你只需要理解,我们正在构建一个模型,这个模型接收图片作为输入,并且能够输出各个品种的预测概率(在我们这个案例中,共有37个数)。

2.1 使用frozen model进行训练

这里使用了RESNET34(残差网络),已经经过了ImageNet预训练。

关于卷积神经网络(CNN)的详细信息。

# 创建模型
learn = cnn_learner(data, models.resnet34, metrics=error_rate)
# 模型训练&验证
learn.fit_one_cycle(4)

可以看到,经过4个epoch,我们获得了一个只有7%错误率的模型。

显示损失最大的9个样本

# 显示损失最大的n个样本
interp.plot_top_losses(9, figsize=(15,11))

结果:

如果去调查一下,会发现这些识别误差最大的样本是人也很难分辨的。

混淆矩阵:

2.2 模型解冻

既然我们的模型表现符合我们的预期,我们将解冻模型并继续训练。

所谓frozen model即冻结了大部分layer权重和结构的神经网络模型,而一旦解冻,则代表该神经网络所有的layer都可以在epoch中改变。

# 模型解冻
learn.unfreeze()
# 模型训练
learn.fit_one_cycle(1)

这里训练的效果反而比解冻前的更差,其原因是一些无需更改权重的layer(例如前几个layer)被频繁的更改了,这对于模型的准确率是无益的。

无需更改的layer大概长这样:

对于CNN来说,前面的层是用来定位简单的线条特征(如圆圈,直角),所以这些layer的权重是无需修改的,因为大部分的图片都会有类似的特征。卷积神经网络的不同层代表着不同的语义复杂性,由底层到顶层对应着复杂性由简到繁。 详情请见paper:Visualizing and Understanding Convolutional Networks

2.3 模型调优

fastai库中内置了learning rate finder,通过调用一个函数即可实现。

# learning rate finder:寻找最优学习率
learn.lr_find()
# 画出学习率-损失曲线
learn.recorder.plot()

结果:

可以看到在超过红叉标注位置后损失就一直增大了。
而我们查看模型训练函数的文档可以知道,默认的学习率是0.03,这显然不合适。

选取合适的学习率:

# 选取合适的学习率(默认为3e-3,显然不合适)
learn.fit_one_cycle(2, max_lr=slice(1e-6,1e-4))

这里的slice为python中的切片对象,设置lr的范围应至少相差10倍以上。 slice:获取一个切片对象,可以在切片操作中传递

例如:

list1 = [1, 2, 3, 4, 5]
s = slice(0, 2)
list1[s] = [1, 2]

结果:

可以看到准确率的提升

3.模型用单个图片进行预测

在训练好一个模型以后,如果想用单个的图片进行预测,该怎么办呢?

先看看要预测的图片:

# 展示图片
from PIL import Image
fn = '/home/zhy/test.jpg'
im = Image.open(fn)
im

进行预测:

该函数返回的分别是预测的类别,类别所属的标签,所有类别的可能性。

# 预测
predicted_class, label, probabilities = learn.predict(open_image(fn))

显然这是一只波斯猫,结果:

4.其他将数据集打包为databunch的方式

在本文第一章中,我们使用了ImageDataBunch.from_name_re函数配合正则表达式的方式将数据集打包为databunch,这是因为数据的标签在文件名中。

接下来介绍另外的几种情况:

  • 文件夹名为标签时

利用data = ImageDataBunch.from_folder(path, ds_tfms=tfms, size=26)函数可以直接获取databunch。

  • 当标签存放在csv/df中时

csv文件中需要有文件名列和标签列。

利用函数data = ImageDataBunch.from_csv(path, ds_tfms=tfms, size=28, csv_labels='labels.csv')data = ImageDataBunch.from_df(path, df, ds_tfms=tfms, size=24)可以直接获取databunch。

  • 使用函数从文件名中获取标签

利用函数data = ImageDataBunch.from_name_func(path, fn_paths, ds_tfms=tfms, size=24, label_func = func)

这时需要构造文件名路径fn_paths=[PosixPath('/home/zhy/.fastai/data/mnist_sample/train/3/7463.png'), PosixPath('/home/zhy/.fastai/data/mnist_sample/train/3/21102.png') ... ]from_name_func函数可以在label_func处传递函数,形参是fn_paths,返回值是标签。最好传递匿名函数lambda。

定义匿名函数:lambda 形参1, 形参2: 返回值

  • 先将标签提取到列表中,然后打包为databunch
# labels = ['3', '3', '7' ...]
labels = [('3' if '/3/' in str(x) else '7') for x in fn_paths]
data = ImageDataBunch.from_lists(path, fn_paths, labels=labels, ds_tfms=tfms, size=24)

fastai的文档可以在jupyter中直接运行,位置在github中的doc_src。