大半个月前,fast.ai在博客上宣布fastai 1.0版发布,之后很快在GitHub上发布了1.0.5测试版,半个月前发布1.0.6正式版。由于刚发布不久,网上关于fastai 1.0的教程极少,因此,我们编写了这篇入门教程,以一个简单的图像分类问题(异形与铁血战士)为例,带你领略fastai这一高层抽象框架惊人的简洁性。
注意,fastai最近的更新节奏很疯狂:
好在按照语义化版本的规矩,动的都是修订号,所以这篇教程在这些版本上应该都适用。不过,以防万一,给出我们使用的版本号供参考:
fastai 1.0.15
pytorch-nightly-cpu 1.0.0.dev20181014
安装
建议通过conda安装。如果用的是最近的Nvidia显卡,可以安装GPU版本:
conda install -c pytorch pytorch-nightly cuda92
conda install -c fastai torchvision-nightly
conda install -c fastai fastai
显卡不给力的话,可以安装CPU版本:
conda install -c pytorch pytorch-nightly-cpu
conda install -c fastai torchvision-nightly-cpu
conda install -c fastai fastai
不用担心,在迁移学习的加持下,本教程中的模型,即使是性能较低的机器,不到一小时也可训练完毕。当然,如果使用GPU版,就更快了,几分钟搞定。
当然也可以通过pip安装,甚至直接从源代码编译,详见官方仓库的README。
注意,不管是GPU版本,还是CPU版本,都需要python 3.6以上,如果是GPU版本,还需要正确配置显卡驱动。
安装好之后,我们可以通过以下语句引入fastai:
import fastai
from fastai import *
from fastai.vision import *
严格来说最后两行并不必要,不过,加载模块中的所有定义,可以使代码看起来更简洁,所以我们这里就加上了。
图像分类示例
MNIST
深度学习入门的经典例子是MNIST手写数字分类,实际上fastai的官方文档开篇就是MNIST的例子:
path = untar_data(URLs.MNIST_SAMPLE)
data = ImageDataBunch.from_folder(path)
learn = create_cnn(data, models.resnet18, metrics=accuracy)
learn.fit(1)
只有四行!事实上,其中还有两行是加载数据,然后最后一行是训练,真正搭建模型只用一行!如果你接触过其他深度学习框架,也许立刻就会意识到fastai恐怖的简洁性。反正我第一次看到的反应就是:“我靠!这也行!?”
不过MNIST分类实在是太过经典,几乎每篇入门教程都用,说不定有些人已经看吐了。而且,黑白手写数字分类,在当前背景下,太过古老,体现不了近年来深度学习在计算机视觉领域的突飞猛进。所以,我们还是换一个酷炫一点的例子吧。
异形大战铁血战士
换个什么例子呢?最近上映了一部新的《铁血战士》,我们不如做个铁血战士和异形的分类器吧(以前还有部《异形大战铁血战士》,不妨假想一下,铁血战士的HUD依靠神经网络区分敌我)。
图片来源: frolic.media
数据集
要做这样一个分类器,首先需要数据集。这很简单,网络上铁血战士和异形的图片太多了。不过,在自己动手搜集图片之前,我们先检查下有没有人做过类似的工作。
这里安利下Google的数据集搜索,找数据集很方便:https://toolbox.google.com/datasetsearch/
用“alien predator”一搜,还真有,第一个结果就是Kaggle上的Alien vs. Predator images:
这些图像是通过Google图像搜索搜集的JPEG缩略图(约250×250像素),训练集、验证集的每个分类各有347、100张图像样本。
从Kaggle下载数据后,我们将validation文件夹改名为valid,得到以下的目录结构:
|-- train
|-- alien
|-- predator
|-- valid
|-- alien
|-- predator
这一目录结构符合fastai组织数据的惯例。
数据预处理
之前MNIST的例子中,我们是这样加载数据的:
path = untar_data(URLs.MNIST_SAMPLE)
data = ImageDataBunch.from_folder(path)
我们直接使用URLs.MNIST_SAMPLE,fastai会自动下载数据集并解压,这是因为MNIST是fastai的自带数据集。fastai自带了MNIST、CIFAR10、Wikitext-103等常见数据集,详见fastai官网:https://course.fast.ai/datasets
而我们要使用的是非自带数据集,所以只需像之前提到的那样,在相关路径准备好数据,然后直接调用ImageDataBunch.from_folder加载即可。
不过,上面的MNIST例子中,出于简单性考虑,没有进行预处理。这是因为MNIST图像本身比较齐整,而且MNIST非常简单,所以不做预处理也行。我们的异形和铁血战士图片则需要做一些预处理。
首先,大多数卷积神经网络的输入层形状都是28、32、64、96、224、384、512之类的数字。而数据集中的图片边长约为250像素,这就需要缩放或者裁切一下。
其次,绝大多数图像分类任务,都需要做下数据增强。一方面增加些样本,以充分利用数量有限的样本;另一方面,也是更重要的一方面,通过平移、旋转、缩放、翻转等手段,迫使模型学习图像更具概括性的特征,缓解过拟合问题。
如果你平时积累了一些不依赖框架的图像增强函数(比如,基于numpy和scipy定义),那图像增强不算太麻烦。不过,你也许好奇,fastai有没有内置图像增强的功能?
有的。实际上,上面说了一大堆,体现到代码就是一句话:
data = ImageDataBunch.from_folder('data', ds_tfms=get_transforms(), size=224)
前面MNIST的例子中,我们看到,fastai只需一个语句就可以完成加载数据集的任务,这已经足够简洁了。现在我们加上预处理,还是一个语句,只不过多了两个参数!
现在我们回过头来,再看看from_folder这个方法,它根据路径参数获取数据集目录,然后根据目录结构区分训练集、验证集、分类集,根据目录名称获取样本的分类标签。这种API的设计极为简洁,避免了很多冗余的“模板代码”。类似地,fastai的ImageDataBunch类还有from_csv、from_df等方法,从CSV文件或DataFrame加载数据。
size参数指定了形状,ds_tfms指定了预处理逻辑,两个参数完成预处理工作。get_transforms()则是fastai的内置方法,提供了适用于大多数计算机视觉任务的默认数据增强方案:
以0.5的概率随机水平翻转
以0.75的概率在-10与10度之间旋转
以0.75的概率在1与1.1倍之间随机放大
以0.75的概率随机改变亮度和对比度
以0.75的概率进行随机对称扭曲
get_transforms()充分体现了fastai的高层抽象程度。fastai的使用者考虑的是我需要应用常见的数据增强方案,而不是具体要对图像进行哪些处理。
在设计模型之前,我们先简单地检查下数据加载是否成功:
data.show_batch(rows=3, figsize=(6,6))
看起来没问题。
顺便提下,如果使用GPU版本,建议再传入一个bs参数,指定下batch大小,比如bs=32,以便充分利用GPU的并行特性,加速之后的训练。
化神经网络为平常
之前提到,MNIST例子中的核心语句(指定网络架构)其实只有一行:
learn = create_cnn(data, models.resnet18, metrics=accuracy)
其实我们这个异形、铁血战士的模型架构也只需一行:
learn = create_cnn(data, models.resnet50, metrics=accuracy)
几乎和MNIST一模一样,只是把模型换成了表达力更强、更复杂的ResNet-50网络,毕竟,异形和铁血战士图像要比黑白手写数字复杂不少。
正好,提供异形、铁血战士数据集的Kaggle页面还提供了分类器的Keras实现和PyTorch实现。我们不妨把网络架构部分的代码抽出来对比一下。
首先,是以API简洁著称的Keras:
conv_base = ResNet50(
include_top=False,
weights='imagenet')
for layer in conv_base.layers:
layer.trainable = False
x = conv_base.output
x = layers.GlobalAveragePooling2D()(x)
x = layers.Dense(128, activation='relu')(x)
predictions = layers.Dense(2, activation='softmax')(x)
model = Model(conv_base.input, predictions)
optimizer = keras.optimizers.Adam()
model.compile(loss='sparse_categorical_crossentropy',
optimizer=optimizer,
metrics=['accuracy'])
然后,是以易用性著称的PyTorch:
device = torch.device("cuda:0"if torch.cuda.is_available() else"cpu")
model = models.resnet50(pretrained=True).to(device)
for param in model.parameters():
param.requires_grad = False
model.fc = nn.Sequential(
nn.Linear(2048, 128),
nn.ReLU(inplace=True),
nn.Linear(128, 2)).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.fc.parameters())
对比一下,很明显,论简洁程度,PyTorch并不比一直打广告说自己简洁的Keras差。然后,虽然写法有点不一样,但这两个框架的基本思路都是差不多的。首先指定模型的冻结部分,然后指定后续层,最后指定损失函数、优化算法、指标。
然后我们再看一遍fastai:
learn = create_cnn(data, models.resnet50, metrics=accuracy)
你大概能体会文章开头提到的惊呼“这样也行!?”的心情了吧。看起来我们只是指定了模型种类和指标,很多东西都没有呀。
实际上,如果你运行过开头提到的MNIST的代码,就会发现一个epoch就达到了98%以上的精确度。很明显,用了迁移学习,否则不会学得这么快。和Keras、PyTorch需要明确指出继承权重、预训练不同,fastai里迁移学习是默认配置。同理,后续层的层数、形状、激活函数,损失函数,优化算法,都不需要明确指定,fastai可以根据数据的形状、模型种类、指标,自动搞定这些。
fastai的口号是“makeing neural nets uncool again”(化神经网络为平常),真是名不虚传。
训练和解读
定下模型后,只需调用fit就可以开始训练了,就像MNIST例子中写的那样。
不过,这次我们打算转用fit_one_cycle方法。fit_one_cycle使用的是一种周期性学习率,从较小的学习率开始学习,缓慢提高至较高的学习率,然后再慢慢下降,周而复始,每个周期的长度略微缩短,在训练的最后部分,允许学习率比之前的最小值降得更低。这不仅可以加速训练,还有助于防止模型落入损失平面的陡峭区域,使模型更倾向于寻找更平坦的极小值,从而缓解过拟合现象。
图片来源:sgugger.github.io
这种学习率规划方案是Lesile Smith等最近一年刚提出的。fit_one_cycle体现了fastai的一个知名的特性:让最新的研究成果易于使用。
先跑个epoch看看效果:
learn.fit_one_cycle(1)
结果:
epoch train_loss valid_loss accuracy
1 0.400788 0.520693 0.835106
嗯,看起来相当不错。一般而言,先跑一个epoch是个好习惯,可以快速检查是否在编码时犯了一些低级错误(比如形状弄反之类的)。但是fastai这么简洁,这么自动化,犯低级错误的几率相应也低了不少,也让先跑一个epoch体现价值的机会少了很多。;-)
再多跑几个epoch看看:
learn.fit_one_cycle(3)
结果:
epoch train_loss valid_loss accuracy
1 0.221689 0.364424 0.888298
2 0.164495 0.209541 0.909574
3 0.132979 0.181689 0.930851
有兴趣的读者可以试着多跑几个epoch,表现应该还能提升一点。不过,我对这个精确度已经很满意了。我们可以对比下Kaggle上的PyTorch实现跑3个epoch的表现:
Epoch1/3
----------
train loss: 0.5227, acc: 0.7205
validation loss: 0.3510, acc: 0.8400
Epoch2/3
----------
train loss: 0.3042, acc: 0.8818
validation loss: 0.2759, acc: 0.8800
Epoch3/3
----------
train loss: 0.2181, acc: 0.9135
validation loss: 0.2405, acc: 0.8950
fastai的表现与之相当,但是,相比PyTorch实现需要进行的编码(已经很简洁了),我们的fastai实现可以说是毫不费力。
当然,也不能光看精确度——有的时候这会形成偏差。让我们看下混淆矩阵吧。
interp = ClassificationInterpretation.from_learner(learn)
interp.plot_confusion_matrix()
看起来很不错嘛!
再查看下最让模型头疼的样本:
interp.plot_top_losses(9, figsize=(10,10))
我们看到,这个模型还是有一定的可解释性的。我们可以猜想,人脸、亮度过暗、画风独特、头部在画幅外或较小,都可能干扰分类器的判断。如果我们想进一步提升模型的表现和概括能力,可以根据我们的猜想再收集一些样本,然后做一些针对性的试验加以验证。查明问题后,再采取相应措施加以改进。
灵活性
本文的目的是展示fastai API的简洁性和高层抽象性。不过,我们最后需要指出的一点是,fastai并没有因此放弃灵活性和可定制性。
比如,之前为了符合fastai数据集目录结构的惯例,我们在下载数据集后将validation重命名为valid。实际上,不进行重命名也完全可以,只需在调用ImageDataBunch的from_folder方法时,额外将validation传入对应的参数即可。
再比如,如果我们的数据集目录中,子目录名作为标签,但没有按训练集、验证集、测试集分开,需要随机按比例划分。另外,我们还想手动指定数据增强操作列表。那么可以这样加载数据集:
# 手动指定数据增强操作
tfms = [rotate(degrees=(-20,20)), symmetric_warp(magnitude=(-0.3,0.3))]
data = (ImageFileList.from_folder(path)
.label_from_folder() # 根据子目录决定标签
.random_split_by_pct(0.1) # 随机分割10%为验证集
.datasets(ImageClassificationDataset) # 转换为图像分类数据集
.transform(tfms, size=224) # 数据增强
.databunch()) # 最终转换
我们上面明确罗列了数据增强操作。如果我们只是需要对默认方案进行微调的话,那么get_transforms方法其实有一大堆参数可供调整,比如get_transforms(flip_vert=True, max_rotate=20)意味着我们同时进行上下翻转(默认只进行水平翻转),并且增加了旋转的角度范围(默认为-10到10)。
-
图像分类
+关注
关注
0文章
90浏览量
11916 -
数据集
+关注
关注
4文章
1208浏览量
24694
原文标题:fastai 1.0入门教程:如何训练简单图像分类器
文章出处:【微信号:jqr_AI,微信公众号:论智】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论