大半個月前,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文章
93瀏覽量
11956 -
數據集
+關注
關注
4文章
1209瀏覽量
24838
原文標題:fastai 1.0入門教程:如何訓練簡單圖像分類器
文章出處:【微信號:jqr_AI,微信公眾號:論智】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論