在这节课中,我们将从零开始构建我们自己的图像分类器,看看是否能够取得顶级的准确率。就让我们一探究竟吧!
我们将使用由 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中时
利用函数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。