Fastai Lesson 1:Image Classification (下篇)

Chenyu Tsai
UXAI
Published in
14 min readSep 30, 2019

介紹如何使用 fastai 輕鬆完成圖片分類

這一系列文章是在學習 Fastai 時所記錄的筆記,內容有參考教學資源中其他同學所做的筆記,建議同時搭配影片和裡面的檔案學習,單看文章內容效果有限。

Lesson1 大綱:

  • 簡介
  • 前期準備
  • 資料 Data
  • 訓練 Training
  • 結果
  • 製作標籤

結果

接續上篇的結果,我們來看看混淆矩陣:

混淆矩陣 Confusion matrix

另一項有用的工具是一個叫混淆矩陣的東西,在下面的圖表中,我們可以看到每一個品種他預測的狀況,因為模型的準確率有 93 %,所以可以看到混淆矩陣的對角線很明顯,因為絕大多數都有預測對。

Most confused

接下來是一個很有趣的 function,most_confused 從名字就可以看出它會列出最讓人容易搞混的狀況:

interp.most_confused(min_val=2)

out:

[('american_pit_bull_terrier', 'staffordshire_bull_terrier', 5),
('Birman', 'Ragdoll', 5),
('english_setter', 'english_cocker_spaniel', 4),
('staffordshire_bull_terrier', 'american_pit_bull_terrier', 4),
('boxer', 'american_bulldog', 4),
('Ragdoll', 'Birman', 3),
('miniature_pinscher', 'chihuahua', 3),
('Siamese', 'Birman', 3)]

most_confused 會直接從 confusion matrix 中抓出資料,從上圖可以看出最常搞混的是 ‘american_pit_bull_terrier’‘staffordshire_bull_terrier’ 的辨認,若是合併下面的來算,搞錯這兩種狗的狀況有 9 次!所以我跑去估狗了一下,真的蠻難辨認的…

Unfreezing, fine-tuning, and learning rates

接下來要透過微調讓我們的模型再近一步提升,目前我們 fit 了四個 epochs,而且訓練速度還蠻快的,這是因為我們搭的模型架構和 ImageNet 的差不多,只是在他原有的模型後加幾層而已,這是能夠訓練得比較快的原因,因為他只要多跑後面我們所加的層數。

但我們希望他可以訓練整個模型,這就是為什麼我們要分兩階段來訓練了。一般我們會呼叫 fitfit_one_cycle 來訓練 ConvLearner,他就只是簡單的微調我們之後加入的層然後很快地訓練一下,也幾乎不會 overfit 就有很好的表現。為了以更好了表現,我們要使用 unfreeze function,這就是告訴電腦叫他訓練整個 model。

learn.unfreeze()
learn.fit_one_cycle(1)

out:

Total time: 00:26
epoch train_loss valid_loss error_rate
1 0.558166 0.314579 0.101489

恩…但是他變差了,為了搞懂為什麼,我們來從一張照片直觀地了解背後是如何運作的。

CNN

這張照片來自這個論文,這篇論文主要在展示如何將 CNN 層的內容視覺化,詳細的 CNN 計算之後會有教學,現在先理解在電腦上的照片是由代表紅藍綠的三層圖像,各以 0–255 的值來表示,最後併成一層我們平常看到的圖像。CNN 就是在第一層輸入這樣的圖來運算,然後經過一些運算後把第資料輸入到第二層,接著進入第三層,這樣持續下去,像是 ResNet34 就有 34 層,ResNet50 則有 50層。

我們來看看第一層,右圖是我們輸入的圖片,左圖是我們在第一層運算後得到的其中之一個圖,大家可以對照一下左有兩組的相似度。

第一個找到了這張圖有對角線的成分,第二張則找到不同方向的對角線,第三張則找了藍黃漸層,這就是 ImageNet 預訓練模型的過濾器 (filter),把圖像中類似的成分給過濾出來。

在第二層我們做了第二次運算,例如最右下角的,我們可以看到一個由上方和左方線條組成的夾角,觀察其他的圖也可以看到他們都比第一層來得複雜,第一層可能只能抓出一條線,第二層開始可以抓出連結的兩條線,一個圓或曲線等等,像是右下角的 filter 就比較擅長找類似窗戶的東西。

從這幾張圖可看到 CNN 會先找出圖像中小的基礎元素,之後再慢慢擴展成點線面,在第三層我們開始可以看到兩者個組合。

第四層又能再將第三層的東西加以組合,開始可以看到狗狗的臉或是鳥的腳,第五層又能得到更多的資訊,愈多層物體的輪廓就愈明顯,所以可以當 ResNet34 有 34 層時,就具備可以分辨貓狗品種的能力了。

所以當我們第一次訓練 (fine-tune) 預訓練模型時,我們讓這些層中的內容保持一樣,只訓練我們在後面新加上去的幾層,所以當我們要整個重新訓練時,就要思考一些事,像是第一層的簡單線條我們就不太需要去更改,因為這個對於判斷貓狗品種的影響可能不大,我們可能會比較想要調整在第五層時狗狗的臉。

回到我們剛剛嘗試的 fine-tune,他之所以沒什麼效果是因為他用一樣的速度來訓練所有的層,也就是他更新基本線條和清晰輪廓的速度是一樣的,所以我們要來調整一下,回到最一開始我們儲存的結果,利用:

learn.load('stage-1')

這個可以帶回我們第一次訓練的結果。

Learning rate finder

接下來要來看看 learning rate finder,這個在下次的課程會提到,現在先理解他是一個來決定我們最快可以 train 這個模型到什麼樣的速度。

learn.lr_find()
learn.recorder.plot()

這樣他就會印出 LR finder 的結果,learning rate 可以說是我們以多快的速度來更新模型中的參數,x 軸向右代表 lr 的增加,y 軸向上則是代表 loss 的增加,所以可以看到當 lr 到 10^-4 時,loss 開始變差,我們用 shift + tab 可以看一下 fit_one_cycle 中的 lr 預設是多少,0.003,這也就解釋了為什麼 loss 會那麼高。

因為我們現在是在微調,所以我們不能從那麼高的 lr 開始,根據這個圖表,我們決定從 1e-6 開始,但是我們不用全程都用 1e-6 這個速度,後因為我們可以看到,大概到 1e-4 時表現都還不錯,所以我們可以為 learn.fit_one_cycle 設定一個 lr 區間:

learn.unfreeze()
learn.fit_one_cycle(2, max_lr=slice(1e-6,1e-4))

out:

Total time: 00:53
epoch train_loss valid_loss error_rate
1 0.242544 0.208489 0.067659
2 0.206940 0.204482 0.062246

在這裡我們會用一個 Python 程式碼 slice,讓我們可以設定一個初始點和停止點,這樣在訓練第一層時會用 1e-6,在最後一層是會是 1e-4,中間的從會平均分配。

在 unfreeze 後如何選擇 learning rate

經驗告訴我們,當我們 unfreeze 之後,以 slice 的方式第一個傳入最大的 lr 參數,第二個則小於第一個十倍,我們的預設是 1e-3,所以他差不多會跑到 1e-4 的位置,接著看看 lr finder 的圖來看看 loss 是何時開始變糟。

大概是在 10^-4 的地方開始變糟,所以我們就再往前推十倍到 10^-5,這樣這個模型就有不錯的表現了。

ResNet 50

除了調整 learning rate 我們還可以用更多層的模型,我們就直接把 ResNet34 改成 ResNet50:

data = ImageDataBunch.from_name_re(path_img, fnames, pat, ds_tfms=get_transforms(), size=320, bs=bs//2)
data.normalize(imagenet_stats)
learn = ConvLearner(data, models.resnet50, metrics=error_rate)

因為 ResNet50 有 50 層,所以模型中的參數量非常的大,若是我們的記憶體容量不夠,就會出現一些錯誤的訊息,這時候我們就要調整 batch size,就是每次處理的資料量,像現在是 64,我們可以調成 32,雖然會訓練得比較慢,但至少跑得動。

learn.fit_one_cycle(8)

out:

Total time: 06:59
epoch train_loss valid_loss error_rate
1 0.548006 0.268912 0.076455 (00:57)
2 0.365533 0.193667 0.064953 (00:51)
3 0.336032 0.211020 0.073072 (00:51)
4 0.263173 0.212025 0.060893 (00:51)
5 0.217016 0.183195 0.063599 (00:51)
6 0.161002 0.167274 0.048038 (00:51)
7 0.086668 0.143490 0.044655 (00:51)
8 0.082288 0.154927 0.046008 (00:51)

經過幾輪訓練後我們得到 4.6% 的錯誤率。

再次 interpret 結果

interp = ClassificationInterpretation.from_learner(learn)
interp.most_confused(min_val=2)

out:

[('american_pit_bull_terrier', 'staffordshire_bull_terrier', 6),
('Bengal', 'Egyptian_Mau', 5),
('Bengal', 'Abyssinian', 4),
('boxer', 'american_bulldog', 4),
('Ragdoll', 'Birman', 4),
('Egyptian_Mau', 'Bengal', 3)]

我們可以再次看看結果,有興趣的話可以去搜尋一下這些品種的樣子,就知道為何模型會分錯了。

製作標籤

本次的作業是要找到自己的資料集來做訓練,所以有一些製作標籤的教學,最直接的方式是直接從 google 下載,在這裡先以一個有名的手寫資料集來做示範,首先先讀數入資料

path = untar_data(URLs.MNIST_SAMPLE); path
path.ls()

out:

['train', 'valid', 'labels.csv', 'models']

從上面可以看到現在已經有訓練和驗證集了。

情況 1:資料夾名為標籤

(path/'train').ls()

out:

['3', '7']

train 中分別有叫做 3 和 7 的資料夾,點進去可以看裡面就是一堆 3 和 7 的圖片,這是一種常見的做標記方式,也常被叫做 ImageNet 風格的資料集,因為 ImageNet 的資料就是這麼分布的,當資料以這種方式分佈時,我們可以用 from_folder:

tfms = get_transforms(do_flip=False)
data = ImageDataBunch.from_folder(path, ds_tfms=tfms, size=26)

這樣同樣會建立一個 DataBunch,同時也做好標籤了:

data.show_batch(rows=3, figsize=(5,5))

情況 2:CSV 檔

另一個範例是 CSV 檔,內容長這樣:

df = pd.read_csv(path/'labels.csv')
df.head()

圖中可以看到每一個圖檔所對應的標籤都記錄在檔案裡,不過不是以 3 或 7 的標籤呈現,因為我們這次是坐二分類,所以是 1 和 0,用來判斷是否為 7,假設我們拿到的資料型態是這樣的話,可以使用 from_csv:

data = ImageDataBunch.from_csv(path, ds_tfms=tfms, size=28)

假如檔名是 labels.csv 的話,就不用傳入檔案名稱。

data.show_batch(rows=3, figsize=(5,5))
data.classes

out:

[0, 1]

情況 3:用 regular expression

fn_paths = [path/name for name in df['name']]; fn_paths[:2]

out:

[PosixPath('/home/chenyu/.fastai/data/mnist_sample/train/3/7463.png'),
PosixPath('/home/chenyu/.fastai/data/mnist_sample/train/3/21102.png')]

跟一開始提取貓狗標籤一樣的方式,利用 regular expression 的方式:

pat = r"/(\d)/\d+\.png$"
data = ImageDataBunch.from_name_re(path, fn_paths, pat=pat, ds_tfms=tfms, size=24)
data.classes

out:

['3', '7']

情況 4:更複雜的方式

用 function 來從檔案名或路徑拿到 label,我們可以用 from_name_func:

data = ImageDataBunch.from_name_func(path, fn_paths, ds_tfms=tfms, size=24,
label_func = lambda x: '3' if '/3/' in str(x) else '7')
data.classes

情況 5:一些比較彈性的方式

若是需要更彈性的方式,我們可以建立一個標籤的 array,然後直接用 from_lists:

labels = [('3' if '/3/' in str(x) else '7') for x in fn_paths]
labels[:5]
data = ImageDataBunch.from_lists(path, fn_paths, labels=labels, ds_tfms=tfms, size=24)
data.classes

因為有點多,當忘記確切用法時,可以使用 doc 來看一下,例如:

doc(ImageDataBunch.from_name_re)

下一堂 Lesson 2 我們會講到資料清理,還有深度學習的基本原理之一 - SGD。

--

--