如何用 Python 和 fast.ai 做影象深度遷移學習?

本文帶你認識一個優秀的新深度學習框架,瞭解深度學習中最重要的3件事。
框架
看到這個題目,你可能會疑惑:
老師,你不是講過如何用深度學習做影象分類了嗎?遷移學習好像也講過了啊!
說得對!我要感謝你對我專欄的持續關注。我確實講過深度學習做影象分類,以及遷移學習這兩項內容。
寫這篇文章,是因為最近因為科研的關係,發現了 fast.ai 這款框架。我希望把它介紹給你。
你可能會不解,之前介紹過的 TuriCreate, Tensorflow, tflearn 和 Keras 好像都挺好用的啊!
我想問問,你在實際的科研工作裡,用過哪一個呢?
大多數的讀者,只怕基本上都沒真正用它們跑過實際的任務。
為什麼呢?
因為對普通使用者(例如我經常提到的“文科生”),這些框架要麼用起來很簡單,但是功能不夠強大;要麼功能很強大,但是不夠易用。
例如蘋果的 TuriCreate ,我給你演示過,直接零基礎上手都沒問題。但當你希望對模型進行構造調整的時候,馬上就會發現困難重重。因為其專長在於快速產生模型,並且部署到蘋果移動裝置,因此文件裡面底層細節的介紹是有欠缺的。而且有些模型,非蘋果平臺目前還不能相容。

至於某著名框架,直到推出3年後,在各方壓力下,不得已才把好用的 Eager Execution 作為主要使用模式。其間充分體現了那種技術人員獨有的傲慢和固執。另外,就連程式員和資料科學家們都把吐槽“看不懂”它的官方文件當作了家常便飯。這些軼事,由於公開發佈會招致口水仗,所以我只寫在了知識星球專屬語雀團隊《發現了一套非常棒的(該框架名稱)視訊教程》一文中。感興趣的話,不妨去看看。
原本我認為, Keras 已經是把功能和易用性做到了最佳平衡了。直到我看到了 Jeremy Howard,也就是 fast.ai 創始人提出的評判標準——如果一個深度學習框架需要寫個教程給你,那它的易用性還不夠好。
我看了之後,可以用感動來形容。
Jeremy 說這話,不是為了誇自己——因為他甚至做了個 MOOC 出來。他自己評價,說目前 fast.ai 的易用性依然不算成功。但在我看來, fast.ai 是 目前 把易用性和功能都做到了 極致 的深度學習框架。
它的門檻極低。如同 TuriCreate 一樣,你可以很輕易用幾句話寫個圖片分類模型出來,人人都能立即上手。
它的天花板又很高。因為它只是個包裹了 Pytorch 的程式碼庫。
你可能也聽說了,在過去的一年裡,Pytorch 在學術界大放異彩,就是因為它的門檻對於科研人員來說,已經足夠友好了。如果你有需求,可以非常方便地通過程式碼的修改和複用,敏捷構造自己的深度學習模型。
這種積木式的組合方式,使得許多新論文中的模型,可以第一時間被複現驗證。如果你在這個過程中有了自己的靈感和心得,可以馬上實踐。
且慢,fast.ai 的作者不是已經做了自己的 MOOC 了嗎?那寫這篇文章,豈不是多此一舉?
不是的。
首先,作者每年迭代一個 MOOC 的版本,因為 MOOC 一共包括三門課程,分別是:
- ofollow,noindex">Practical Deep Learning for Coders
- Cutting Edge Deep Learning for Coders
- Introduction to Machine Learning for Coders
但現在你能看到的深度學習基礎課,還是去年錄的。今年10月,伴隨著 Pytorch 1.0 的推出, fast.ai 做了一次顯著的大版本(1.0)更新。如果你去看去年的課程,會發現和目前的 fast.ai 程式碼有很多區別。在完成同一個功能時,你願意再跑去學舊的過時內容嗎?特別是,如果搞混了,還很容易出錯。
可是,想看到這個版本課程的免費視訊,你至少得等到明年1月。因為目前正式學員們也才剛剛開課。

而且,那視訊,也是 英文 的。
正因如此,我覺得有必要給你講講,如何用最新的 fast.ai 1.0 版本,來完成影象深度遷移學習。
資料
Jeremy 在 MOOC 中提到,如果你打算讓機器通過資料來學習,你需要提供3樣東西給它,分別是:
- 資料(Data)
- 模型結構(Architecture)
- 損失度量(Loss Metrics)
模型結構,是根據你的具體問題走的。例如說,你需要讓機器做圖片分類,那麼就需要使用卷積神經網路(Convolutional Neural Network)來表徵圖片上的畫素資訊構成的特徵。如果你需要做自然語言處理,那麼就可以使用迴圈神經網路(Recurrent Neural Network)來捕捉文字或者字元的順序關聯資訊。
損失衡量,是指你提供一個標準,衡量機器對某項任務的處理水平。例如說對於分類效果如何,你可以使用交叉熵(Binary Cross Entropy)來評判。這樣,機器會嘗試最小化損失結果,從而讓分類表現越來越好。
至於資料,因為我們這裡的任務是做分類。因此需要有標註的訓練資料。
我已經把本文需要用到的資料放到了 這個 github 專案 上。

開啟其中的 imgs
資料夾,你會看見3個子資料夾,分別對應訓練(train),驗證(valid)和測試(test)。
開啟 train
資料夾看看。
你沒猜錯,我們用的圖片還是哆啦A夢(doraemon)和瓦力(walle)。

因為這樣不僅可以保持教程的一慣性,而且也可以保證結果對比的公平。
開啟哆啦A夢的目錄看看:

展示其中第一個檔案內容。

好熟悉,是不是?
你可以瀏覽一下其他的哆啦A夢照片,然後別忘了去瓦力的資料夾裡面掃上一眼。

這就是我們的資料集了。
環境
為了執行深度學習程式碼,你需要一個 GPU 。但是你不需要去買一個,租就好了。最方便的租用方法,就是雲平臺。
fast.ai 官方,給出了以下5種雲端計算平臺使用選項:
其中,我推薦你使用的,是 Google Compute Platform 。原因很簡單,首先它成本低,每小時只需要 0.38 美元。更重要的是,如果你是新使用者, Google 會先送給你 300美金 ,1年內有效。算算看,這夠你執行多久深度學習?

原先,fast.ai 上面的設定 Google Compute Platform 教程寫得很簡略。於是我寫了個一步步的教程,請使用這個連結訪問。

不過,我發現 fast.ai 的迭代速度簡直驚人,短短几天時間,新的教程就出來了,而且詳盡許多。因此你也可以點選這裡檢視官方的教程。其中如果有跳步,你可以回看我的教程,作為補充。
因此,Google Compute Platform 中間步驟,咱們就不贅述了。當你的終端裡面出現這樣的提示的時候,就證明一切準備工作都就緒了。

下面,你需要下載剛剛在 github 上面的程式碼和資料集。
git clone https://github.com/wshuyi/demo-image-classification-fastai.git

之後,就可以呼叫 jupyter 出場了。
jupyter lab

注意因為你是在 Google Compute Platform 雲端執行 jupyter ,因此瀏覽器不會自動彈出。
你需要開啟 Firefox 或者 Chrome,在其中輸入這個連結( http:// localhost:8080/lab ? )。

開啟左側邊欄裡面的 demo.ipynb
。

本教程全部的程式碼都在這裡了。當然,你如果比較心急,可以選擇執行 Run->Run All Cells
,檢視全部執行結果。

但是,跟之前一樣,我還是建議你跟著教程的說明,一步步執行它們。以便更加深刻體會每一條語句的含義。
載入
我們先要載入資料。第一步是從 fast.ai 讀入一些相關的功能模組。
from fastai import * from fastai.vision import * from fastai.core import *
接著,我們需要設定資料所在資料夾的位置,為 imgs
目錄。

執行:
path = Path('imgs')
下面,我們讓 fast.ai 幫我們載入全部的資料。這時我們呼叫 ImageDataBunch
類的 from_folder
函式,結果儲存到 data
中:
data = ImageDataBunch.from_folder(path, test='test', ds_tfms=get_transforms(), size=224)
注意這裡,我們不僅讀入了資料,還順手做了2件事:
- 我們進行了資料增強(augmentation),也就是對資料進行了翻轉、拉伸、旋轉,弄出了很多“新”訓練資料。這樣做的目的,是因為資料越多,越不容易出現過擬合(over-fitting),也就是模型死記硬背,矇混考試,卻沒有抓住真正的規律。
- 我們把圖片大小進行了統一,設定成了 224 x 224 ,這樣做的原因,是我們需要使用遷移學習,要用到預訓練模型。預訓練模型是在這樣大小的圖片上面訓練出來的,因此保持大小一致,效果更好。
下面,檢查一下資料載入是否正常:
data.show_batch(rows=3, figsize=(10,10))

沒問題。圖片和標記都是正確的。
訓練
用下面這 一條 語句,我們把“資料”、“模型結構”和“損失度量”三樣資訊,一起餵給機器。
learn = ConvLearner(data, models.resnet34, metrics=accuracy)
資料就不說了,模型我們採用的是 resnet34
這樣一個預訓練模型作為基礎架構。至於損失度量,我們用的是準確率(accuracy)。
你可能會納悶,這就完了?不對呀!
沒有告訴模型類別有幾個啊,沒有指定任務遷移之後接續的幾個層次的數量、大小、啟用函式……
對,不需要。
因為 fast.ai 根據你輸入的上述“資料”、“模型結構”和“損失度量”資訊, 自動 幫你把這些閒七雜八的事情默默搞定了。
下面,你需要用一條指令來訓練它:
learn.fit_one_cycle(1)
注意,這裡我們要求 fast.ai 使用 one cycle policy 。如果你對細節感興趣,可以點選這個連結瞭解具體內容。

5秒鐘之後,訓練結束。
驗證集準確率是,100%。
注意,你“拿來”的這個 resnet34
模型當初做訓練的時候,可從來沒有見識過哆啦A夢或者瓦力。
看了100多張形態各異,包含各種背景噪聲的圖片,它居然就能 100% 準確分辨了。
之前我們講過機器學習的可解釋性很重要。沒錯,fast.ai 也幫我們考慮到了這點。
preds,y = learn.get_preds() interp = ClassificationInterpretation(data, preds, y, loss_class=nn.CrossEntropyLoss)
執行上面這兩行語句,不會有什麼輸出。但是你手裡有了個解釋工具。
我們來看看,機器判斷得最不好的9張圖片都有哪些?
interp.plot_top_losses(9, figsize=(10,10))

因為準確率已經 100% 了,所以單看數值,你根本無法瞭解機器判斷不同照片的時候,遇到了哪些問題。但是這個直譯器卻可以立即讓你明白,哪些圖片,機器處理起來,底氣(信心)最為不足。
我們還能讓直譯器做個混淆矩陣出來:
interp.plot_confusion_matrix()

不過這個混淆矩陣好像沒有什麼意思。反正全都判斷對了。
評估
我們的模型,是不是已經 完美 了?
不好說。
因為我們剛才展示的,只是驗證集的結果。這個驗證集,機器在迭代模型引數的時候每一回都拿來嘗試。所以要檢驗最為真實的效能,我們需要讓機器看從來沒有看到過的圖片。
你可以到 test 目錄下面,看看都有什麼。

注意這裡一共6張圖片,3張哆啦A夢的,3張瓦力的。
這次,我們還會使用剛才用過的 get_preds
函式。不過區別是,我們把 is_test
標記設定為 True
,這樣機器就不會再去驗證集裡面取資料了,而是看測試集的。
preds,y = learn.get_preds(is_test=True)
注意目錄下面看到的檔案順序,是依據名稱排列的。但是 fast.ai 讀取資料的時候,其實是做了隨機洗牌(randomized shuffling)。我們得看看實際測試集裡面的檔案順序。
data.test_dl.dl.dataset.ds.x

好了,我們自己心裡有數了。下面就看看機器能不能都判斷正確了。
preds

這都啥玩意兒啊?
彆著急,這是模型預測時候,根據兩個不同的分類,分別給出的傾向數值。數值越大,傾向程度越高。
左側一列,是哆啦A夢;右側一列,是瓦力。
我們用 np.argmax
函式,把它簡化一些。
np.argmax(preds, axis=1)

這樣一來,看著就清爽多了。
我們來檢查一下啊:瓦力,瓦力,哆啦A夢,哆啦A夢,哆啦A夢,哆啦A夢……
不對呀!
最後這一張, walle.113.jpg
,不應該判斷成瓦力嗎?
開啟看看。

哦,難怪。另一個機器人也出現在圖片中,圓頭圓腦的,確實跟哆啦A夢有相似之處。
要不,就這樣了?
微調
那哪兒行?!
我們做任務,要講究精益求精啊。
遇到錯誤不要緊,我們嘗試改進模型。
用的方法,叫做微調(fine-tuning)。
我們剛剛,不過是移花接木,用了 resnet34
的身體,換上了一個我們自定義的頭部層次,用來做哆啦A夢和瓦力的分辨。

這個訓練結果,其實已經很好了。但是既然鎖定了“身體”部分的全部引數,只訓練頭部,依然會遇到判斷失誤。那我們自然想到的,就應該是連同“身體”,一起調整訓練了。
但是這談何容易?
你調整得動作輕微,那麼效果不會明顯;如果你調整過了勁兒,“身體”部分的預訓練模型通過海量資料積累的引數經驗,就會被破壞掉。
兩難啊,兩難!
好在,聰明的研究者提出了一個巧妙的解決之道。這非常符合我們不只一次提及的“第一性原理”,那就是返回到事情的本源,問出一句:
誰說調整的速度,要全模型都一致?!
深度卷積神經網路,是一個典型的層次模型。
模型靠近輸入的地方,捕獲的是底層的特徵。例如邊緣形狀等。
模型靠近輸出的地方,捕獲的是高層特徵,例如某種物體的形貌。
對於底層特徵,我們相信哆啦A夢、瓦力和原先訓練的那些自然界事物,有很多相似之處,因此應該少調整。
反之,原先模型用於捕獲貓、狗、兔子的那些特徵部分,我們是用不上的,因此越靠近輸出位置的層次,我們就應該多調整。
這種不同力度的調整,是通過學習速率(learning rate)來達成的。具體到我們的這種區分,專用名詞叫做“歧視性學習速率”(discriminative learning rate)。
你可能想放棄了,這麼難!我不玩兒了!
且慢,看看 fast.ai 怎麼實現“歧視性學習速率”。
learn.unfreeze() learn.fit_one_cycle(3, slice(1e-5,3e-4))
對,只需在這裡指定一下,底層和上層,選擇什麼不同的起始速率。搞定。
沒錯,就是這麼 不講道理地智慧化 。

這次,訓練了3個迴圈(cycle)。
注意,雖然準確率沒有變化(一直是100%,也不可能提升了),但是損失數值,不論是訓練集,還是驗證集上的,都在減小。
這證明模型在努力地學東西。
你可能會擔心:這樣會不會導致過擬合啊?
看看就知道了,訓練集上的損失數值,一直 高於 驗證集,這就意味著,沒有過擬合發生的徵兆。
好了,拿著這個微調優化過後的模型,我們再來試試測試集吧。
首先我們強迫症似地看看測試集檔案順序有沒有變化:
data.test_dl.dl.dataset.ds.x

既然沒有變,我們就放心了。
下面我們執行預測:
preds,y = learn.get_preds(is_test=True)
然後,觀察結果:
np.argmax(preds, axis=1)

如你所見,這次全部判斷正確。
可見,我們的微調,是真實有用的。
小結
本文為你介紹瞭如何用 fast.ai 1.0 框架進行影象深度遷移學習。可以看到, fast.ai 不僅簡潔、功能強大,而且足夠智慧化。所有可以幫使用者做的事情,它全都替你代勞。作為研究者,你只需要關注“資料”、“模型結構”和“損失度量”這3個關鍵問題,以改進學習效果。
我希望你不要滿足於把程式碼跑下來。用你獲得的300美金,換上自己的資料跑一跑,看看能否獲得足夠滿意的結果。
祝(深度)學習愉快!
喜歡請點贊和打賞。還可以微信關注和置頂我的公眾號 “玉樹芝蘭”(nkwangshuyi) 。
如果你對 Python 與資料科學感興趣,不妨閱讀我的系列教程索引貼《 如何高效入門資料科學? 》,裡面還有更多的有趣問題及解法。