如何用 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 一共包括三門課程,分別是:

  • 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種雲計算平臺使用選項:

  • Paperspace Gradient
  • Salamander
  • SageMaker
  • Google Compute Platform
  • Amazon Web Services EC2

其中,我推薦你使用的,是 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美金,換上自己的數據跑一跑,看看能否獲得足夠滿意的結果。

祝(深度)學習愉快!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章