Fastai, Lesson 2:Data cleaning and production, SGD from scratch (上篇)

Chenyu Tsai
UXAI
Published in
18 min readOct 3, 2019

透過 fastai 來清理資料、運算和做 SGD

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

Lesson1 大綱:

  • 泰迪熊分類器
  • 訓練模型的注意事項
  • 什麼是 loss
  • SGD

泰迪熊分類器 feat. Google Images

這堂課會深入講解電腦中圖像的應用,我們會用自己的圖片來打造一個自己的分類器,以泰迪熊的分類來做示範,如何分辨泰迪熊、灰熊和黑熊。

第一步:找到每個圖片類別的 URL

我們直接到 google images 上去找這三個類別,在搜尋泰迪熊、灰熊跟黑熊就好,可以考慮以英文來搜尋,Teedy bear, grizzly bear, black bear,找到之後往下滑,找到一群覺得還不錯的照片後,接下來我們要弄出一個 URLs lists,先按 Ctrl + Shift +J (Windows) 或 Cmd Opt J (Mac),接著在視窗中輸入 (記得關掉 Adblock 之類的插件):

urls = Array.from(document.querySelectorAll('.rg_di .rg_meta')).map(el=>JSON.parse(el.textContent).ou);
window.open('data:text/csv;charset=utf-8,' + escape(urls.join('\\n')));

接著就會讓我們下載這些圖片,之後我們再依照相同的步驟來下載黑熊跟灰熊。

第二步:下載圖片

因為我們是在 jupyter notebook 中下載,所以要先 import fastai 的 lib:

from fastai import *
from fastai.vision import *

接著我們設定一下三種熊的資料夾名稱和檔案路徑:

folder = 'black'
file = 'urls_black.txt'
folder = 'teddys'
file = 'urls_teddys.txt'
folder = 'grizzly'
file = 'urls_grizzly.txt'

接著跑這段程式碼來建立資料夾:

path = Path('data/bears')
dest = path/folder
dest.mkdir(parents=True, exist_ok=True)

注意不能依序往下按,這樣他最後只會建立灰熊的資料夾,要先跑上面的設定路徑,才能跑下面的建立程式碼,總共要跑三輪,當然也可以把建立資料夾的程式碼都夾在每一個設定路徑後,只是會變得比較麻煩而已,或是也可以用 for loop 解決,端看個人喜好。

第三步:建立 DataBunch

接下來我們要移除一些不是圖檔的東西,從 google 上下載難免會有一些損毀的檔案或無法下載的,這時要用 verify_images 來確認所有在路徑中的照片有沒有問題,如果參數 delete=True,程式就會自動幫我們刪除。

for c in classes:
print(c)
verify_images(path/c, delete=True, max_workers=8)

這樣我們就有三種熊的資料夾了,從另一種角度說,就是一個可以拿來建立 DataBunch 的基本架構來做 deep learning 了。

大家可能會注意到,我們好像沒有準備 validation set,然而 validation set 在訓練中是相當重要的,不能缺少,不過我們不必特地分,在這個情況下,我們可以用「 . 」來告訴程式 training set 就在這個資料夾,然後我想要分個 20% 出來做 validation set,程式就會自動幫我們從 training set 中挑選 20% 出來了。

另外儘管我們喜歡讓模型的訓練實驗有隨機性,讓我們在面對各種沒看過的資料時仍然能保持穩定。不過在 validation set 的部分,則希望能夠保持一致,因為有時候我們需要微調一些超參數,同時調整超參數和 validation set 會導致實驗結果難以看出是否是超參數的影響,所以講者提到他總是在建立 DataBunch 之前設置一個 random seed

np.random.seed(42)data = ImageDataBunch.from_folder(path, 
train=".",
valid_pct=0.2,
ds_tfms=get_transforms(),
size=224,num_workers=4)
.normalize(imagenet_stats)

有了 DataBunch 之後就可以來用 data.classes 看一下裡面長怎樣:

data.classes['black', 'grizzly', 'teddys']

接著用 show_batch 來看裡面的照片,有些圖片看起來可能會有點詭異:

data.show_batch(rows=3, figsize=(7,8))

訓練模型之前,我們來做一次確認,看看我們的資料分佈為何,可以看到我們training set 有 473, validation set 有 140。

data.classes, data.c, len(data.train_ds), len(data.valid_ds)(['black', 'grizzly', 'teddys'], 3, 473, 140)

第四步:訓練模型

再來就可以訓練模型拉:

learn = create_cnn(data, models.resnet34, metrics=error_rate)learn.fit_one_cycle(4)Total time: 00:26
epoch train_loss valid_loss error_rate
1 0.957604 0.199212 0.045045
2 0.556265 0.093994 0.036036
3 0.376028 0.082099 0.036036
4 0.273781 0.076548 0.027027

錯誤率只有 2 %,還不錯!接下來把模型存起來:

learn.svae('stage-1')

然後 unfreeze

learn.unfreeze()

跑 learning rate finder 後印成圖

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

在 learning rate 中,我們要找到下降坡度最陡的一段,所以我們決定拿 3e-5 當作起始點,3e-4 當作終點,除了這個案例,大部分案例在這個坡段都還不錯,不過今天也會看一下例外的案例。

learn.fit_one_cycle(2, max_lr=slice(3e-5,3e-4))Total time: 00:28
epoch train_loss valid_loss error_rate
1 0.107059 0.056375 0.028369
2 0.070725 0.041957 0.014184

最後利用 google images 得到一個錯誤率 1.4% 的模型,ㄧ樣把它存起來

learn.save('stage-2')

Interpretation

我們一樣可以用 Classification Interpretation 來看看發生什麼事:

learn.load('stage-2')interp = ClassificationInterpretation.from_learner(learn)
interp.plot_confusion_matrix()

從圖中可以看到我們錯把一個黑熊分成灰熊了,這有可能是我們的資料中有些雜訊,畢竟 google 總不可能每次都給我們正確的照片,那麼多張總有幾張可能是錯的,例如同時出現灰熊黑熊的圖等等,接下來我們要來把它清除。

from fastai.widgets import *db = (ImageList.from_folder(path)
.split_none()
.label_from_folder()
.transform(get_transforms(), size=224)
.databunch()
)
learn_cln = cnn_learner(db, models.resnet34, metrics=error_rate)learn_cln.load('stage-2');ds, idxs = DatasetFormatter().from_toplosses(learn_cln)

上圖中第二個明顯不是熊,所以我們要刪除掉他。關於製作圖形化介面 (Graphical User Interface) 的指南可以看這裡

上線我們的模型

清除掉不必要的資料後,我們可以重新訓練模型,不過模型進步的幅度可能不會太大,因為資料雜訊其實不太多,而我們的模型對於處理這些為數不大的隨機性雜訊資料還算可以,但是當雜訊一多或是很偏頗時,這問題就會很嚴重了,例如我們在黑熊的資料夾中塞幾張泰迪熊的圖片等。

在要上線模型前,有個建議是,用 CPU 來處理預測就好,因為我們的模型已經訓練好了,不需要再依賴 GPU 運算的廣度。我們很難一次要去分類大量的圖片,也不會從各個不同使用者中累積到一定量的圖片才預測,這樣太沒效率了,所以我們只要每次都包一包丟給 CPU 就可以了,雖然運算可能會慢一點點,但不是在做訓練的話大概就是 0.2 秒跟 0.01 秒的差別,除非我們的網站流量非常大了,再來考慮要怎麼更改。

最一開始,我們要輸出我們的 learner:

learn.export

首先我們要先知道我們擁有的類別,除了要知道類別外,還要知道他們的順序,保險起見我們利用:

data.classes['black', 'grizzly', 'teddys']

當我們要辨識一個新照片的時候,要把它存在一個變數 img 中,接著利用 open_images 來打開照片:

img = open_image(path/'black'/'00000021.jpg')
img

在應用環境中建立一個 learner,要確保路徑中有剛剛存的 ‘export.pkl’ 檔。

learn = load_learner(path)pred_class, pred_idx, outputs = learn.predict(img)
pred_class
Category black

這裡的範例是 Starlette 的網頁框架,若是有用過如 Flask 之類的網頁框架看起來也會有點類似,整個模型在網頁中的運作大概會長這樣:

@app.route("/classify-url", methods=["GET"])
async def classify_url(request):
bytes = await get_bytes(request.query_params["url"])
img = open_image(BytesIO(bytes))
_,_,losses = learner.predict(img)
return JSONResponse({
"predictions": sorted(
zip(cat_learner.data.classes, map(float, losses)),
key=lambda p: p[1],
reverse=True
)
})

訓練模型的注意事項

通常根據經驗得到的規則都有不錯的效果,但是總有情況之外的事情,接下來要談談我們在訓練模型時可能會遇到的問題:

  • 學習率太高或太低
  • Epochs 數太高或太低

學習率 learning rate 太高

假如我們把泰迪熊偵測器的 lr 調超高,預設的是 0.003,我們把他調到 0.5 試試,差不多是 150 倍左右,從結果可以看到我們的 validation loss 會變得非常高,通常我們會希望 loss 可以在 1 以下,今後有遇到高於 1 的情況,都是需要去調整模型的學習參數的。

learn = create_cnn(data, models.resnet34, metrics=error_rate)learn.fit_one_cycle(1, max_lr=0.5)

out:

Total time: 00:13
epoch train_loss valid_loss error_rate
1 12.220007 1144188288.000000 0.765957 (00:13)

學習率 learning rate 太低

那如果我們把學習率從 0.003 調到 1e-5 (0.00001 )呢?

learn = create_cnn(data, models.resnet34, metrics=error_rate)

我們之前的結果是:

Total time: 00:57
epoch train_loss valid_loss error_rate
1 1.030236 0.179226 0.028369 (00:14)
2 0.561508 0.055464 0.014184 (00:13)
3 0.396103 0.053801 0.014184 (00:13)
4 0.316883 0.050197 0.021277 (00:15)

大概在第一個 epoch 我們就得到了 2% 多得錯誤率,假如學習率很低的話呢?

learn.fit_one_cycle(5, max_lr=1e-5)Total time: 01:07
epoch train_loss valid_loss error_rate
1 1.349151 1.062807 0.609929 (00:13)
2 1.373262 1.045115 0.546099 (00:13)
3 1.346169 1.006288 0.468085 (00:13)
4 1.334486 0.978713 0.453901 (00:13)
5 1.320978 0.978108 0.446809 (00:13)

模型還是會進步,但是進步的幅度非常的慢。

我們一樣能把圖印出來,利用 learn.recorder 來記錄裡面的數據,之後利用 plot_losses 來印出 validation 和 training loss,我們可以看到兩者下降得非常緩慢,所以我們可以試著提升 10 倍甚至 100 倍來嘗試。另外我們永遠不希望 training loss 高於 validation loss,這樣表示我們的模型擬合地不夠,有可能就是來自於學習率太低或是 epochs 數太低,所以假如我們遇到類似的狀況,就調高學習率或 epochs。

Epochs 太少

如果 epoch 只有 1 的話會發生什麼事?從數據中可看看到,training loss 比 validation loss 還要高,這表示著模型訓練得還不夠, 假如我們都訓練個 20 epochs 了,情況還是沒改善,那麼就要調整學習率了。

learn = create_cnn(data, models.resnet34, metrics=error_rate, pretrained=False)learn.fit_one_cycle(1)

out:

Total time: 00:14
epoch train_loss valid_loss error_rate
1 0.602823 0.119616 0.049645 (00:14)

Epochs 太多

太多 epochs 就會造成一種叫做 overfitting 的問題,他會變得非常擅長辨識我們給的那些圖片,但是出了那些圖片的樣貌,模型就會變得不容易辨識了。Overfitting 的特徵就是當錯誤率前幾次增加了,但是之後開始變差,這就是唯一判斷是否 overfit 的方式。

任何正確訓練的模型,他的 training loss 都會低於 validation loss。

有些人會說 training loss 比 validation loss 低的話,那就是模型 overfit 了,但是那只是一個沒有正確訓練模型的訊號。

我們把所有對數據的變化都取消來訓練到 overfitting 看看:

np.random.seed(42)
data = ImageDataBunch.from_folder(path, train=".", valid_pct=0.9, bs=32,
ds_tfms=get_transforms(do_flip=False, max_rotate=0, max_zoom=1, max_lighting=0, max_warp=0
),size=224, num_workers=4).normalize(imagenet_stats)
learn = create_cnn(data, models.resnet50, metrics=error_rate, ps=0, wd=0)
learn.unfreeze()
learn.fit_one_cycle(40, slice(1e-6,1e-4))

out:

Total time: 06:39
epoch train_loss valid_loss error_rate
1 1.513021 1.041628 0.507326 (00:13)
2 1.290093 0.994758 0.443223 (00:09)
3 1.185764 0.936145 0.410256 (00:09)
4 1.117229 0.838402 0.322344 (00:09)
5 1.022635 0.734872 0.252747 (00:09)
6 0.951374 0.627288 0.192308 (00:10)
7 0.916111 0.558621 0.184982 (00:09)
8 0.839068 0.503755 0.177656 (00:09)
9 0.749610 0.433475 0.144689 (00:09)
10 0.678583 0.367560 0.124542 (00:09)
11 0.615280 0.327029 0.100733 (00:10)
12 0.558776 0.298989 0.095238 (00:09)
13 0.518109 0.266998 0.084249 (00:09)
14 0.476290 0.257858 0.084249 (00:09)
15 0.436865 0.227299 0.067766 (00:09)
16 0.457189 0.236593 0.078755 (00:10)
17 0.420905 0.240185 0.080586 (00:10)
18 0.395686 0.255465 0.082418 (00:09)
19 0.373232 0.263469 0.080586 (00:09)
20 0.348988 0.258300 0.080586 (00:10)
21 0.324616 0.261346 0.080586 (00:09)
22 0.311310 0.236431 0.071429 (00:09)
23 0.328342 0.245841 0.069597 (00:10)
24 0.306411 0.235111 0.064103 (00:10)
25 0.289134 0.227465 0.069597 (00:09)
26 0.284814 0.226022 0.064103 (00:09)
27 0.268398 0.222791 0.067766 (00:09)
28 0.255431 0.227751 0.073260 (00:10)
29 0.240742 0.235949 0.071429 (00:09)
30 0.227140 0.225221 0.075092 (00:09)
31 0.213877 0.214789 0.069597 (00:09)
32 0.201631 0.209382 0.062271 (00:10)
33 0.189988 0.210684 0.065934 (00:09)
34 0.181293 0.214666 0.073260 (00:09)
35 0.184095 0.222575 0.073260 (00:09)
36 0.194615 0.229198 0.076923 (00:10)
37 0.186165 0.218206 0.075092 (00:09)
38 0.176623 0.207198 0.062271 (00:10)
39 0.166854 0.207256 0.065934 (00:10)
40 0.162692 0.206044 0.062271 (00:09)

到這裡為止介紹了如何建立自己的資料集並訓練、如何清理數據,和當模型訓練不佳時,可以從哪四個方面著手來嘗試改善模型,下一篇我們會從介紹什麼是 loss 開始,講一些很基本的數學和深度學習模型的基礎 SGD。

--

--