fastai 1.0入門:如何訓練簡單影象分類器
來源:論智
編譯:weakish
大半個月前,fast.ai在部落格上宣佈fastai 1.0版釋出,之後很快在GitHub上釋出了 ofollow,noindex">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依靠神經網路區分敵我)。

資料集
要做這樣一個分類器,首先需要資料集。這很簡單,網路上鐵血戰士和異形的圖片太多了。不過,在自己動手蒐集圖片之前,我們先檢查下有沒有人做過類似的工作。
這裡安利下Google的資料集搜尋,找資料集很方便: https:// toolbox.google.com/data setsearch/
用“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
使用的是一種週期性學習率,從較小的學習率開始學習,緩慢提高至較高的學習率,然後再慢慢下降,周而復始,每個週期的長度略微縮短,在訓練的最後部分,允許學習率比之前的最小值降得更低。這不僅可以加速訓練,還有助於防止模型落入損失平面的陡峭區域,使模型更傾向於尋找更平坦的極小值,從而緩解過擬合現象。

這種學習率規劃方案是Lesile Smith等最近一年剛提出的。 fit_one_cycle
體現了fastai的一個知名的特性: 讓最新的研究成果易於使用 。
先跑個epoch看看效果:
learn.fit_one_cycle(1)
結果:
epochtrain_lossvalid_lossaccuracy 10.4007880.5206930.835106
嗯,看起來相當不錯。一般而言,先跑一個epoch是個好習慣,可以快速檢查是否在編碼時犯了一些低階錯誤(比如形狀弄反之類的)。但是fastai這麼簡潔,這麼自動化,犯低階錯誤的機率相應也低了不少,也讓先跑一個epoch體現價值的機會少了很多。;-)
再多跑幾個epoch看看:
learn.fit_one_cycle(3)
結果:
epochtrain_lossvalid_lossaccuracy 10.2216890.3644240.888298 20.1644950.2095410.909574 30.1329790.1816890.930851
有興趣的讀者可以試著多跑幾個epoch,表現應該還能提升一點。不過,我對這個精確度已經很滿意了。我們可以對比下Kaggle上的PyTorch實現跑3個epoch的表現:
Epoch 1/3 ---------- train loss: 0.5227, acc: 0.7205 validation loss: 0.3510, acc: 0.8400 Epoch 2/3 ---------- train loss: 0.3042, acc: 0.8818 validation loss: 0.2759, acc: 0.8800 Epoch 3/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)。