從0到1:訓練自己的第一個CNN模型

S.H.H
TryTech
Published in
19 min readJul 18, 2020

本文出處是筆者上網看Stanford的”Convolutional Neural Networks for Visual Recognition”這門課的一點心得紀錄,本篇內容主要討論其中有關Training Neural Networks的課程筆記,加上一些筆者的經驗,應能幫助初學者更加了解該如何訓練一個卷積神經網路,將以Pytorch進行一些有關課程觀念的實作,從0到1建置一個卷積神經網路(convolutional neuron network, CNN),也可以參考官方的教學,連結附在下方。
https://pytorch.org/tutorials/beginner/blitz/cifar10_tutorial.html

(一)訓練前準備

環境安裝

現在深度學習框架的Document越寫越好,可能發展也趨於成熟,所以基本上直接上官網照你的作業環境條件下去安裝,應該就妥當了!附上傳送門:https://pytorch.org,但建議是安裝最新版(v1.5.1),待會使用的範例程式都確認能運行在該版本上,但我猜舊版本也不會有問題哈哈。
(註:本文範例不需要用到GPU加速,若沒有需求可以跳過cuda或顯卡驅動等安裝設定)

擷取自Pytorch官網(https://pytorch.org)

本文將以深度學習中相當於是Hello World等級的題目,MNIST手寫數字辨識為例進行實作,但資料來源利用https://www.kaggle.com/c/digit-recognizer上提供的train.csv,之後再用test.csv中的input製作符合格式的submission,讓讀者跟著實作完後也可以實際上傳他們的judge系統測試自己模型的表現,應該也比較有趣吧!

資料處理

載下來後的csv檔有很多處理方式,最簡單的可能就是用如pandas的套件,可以參考待會的程式碼,這邊簡單帶過,主要討論需要作的前處理:

Normalization

為了讓不同維度的資料有相同的尺度,以避免模型學習走偏,應將各個維度資料數值分佈在1到0之間,或是做減去平均的動作,讓資料對稱於0,才再做除以標準差來達到標準化,這些動作除了消弭尺度造成的雜訊,也有防止梯度爆炸跟加速模型收斂等效用。

擷取自課程CS231n: Convolutional Neural Networks for Visual Recognition

由於這次使用的影像資料比較簡單(?),且為灰階影像,這邊簡單除以255就可以有不錯的表現囉,因為原始RGB的數值範圍為0到255,其他比較複雜的任務,如物件偵測、影像切割等等,可能會採用減去平均再除以標準差的方式,但這種作法在資料量太小時反而會有所偏頗(即不夠robust),常見的做法是直接採用ImageNet計算出來的數值:

means = [0.485, 0.456, 0.406]
stds = [0.229, 0.224, 0.225]

import pandas as pd
import matplotlib.pyplot as plt
df = pd.read_csv('./train.csv')
image = df.iloc[0, 1:].values.reshape((28, 28))
plt.imshow(image) #確認是否成功
# input images
train_x = (df.iloc[:, 1:].values/255)
# 因為載入的資料被攤平成784維,為了餵入CNN則再動點小手腳轉回28x28的圖片
data_length = train_x.shape[0]
train_x = train_x.reshape(data_length, 1, 28, 28) # gray-scale
# labels
train_y = df.label.values
若出現這個圖代表讀取成功。

建立模型

在開始堆疊自己的卷積神經網路前,你應該要知道一些基本元素像是卷積層(convolutional layer)、池化層(pooling layer)、激勵函數(activation)和全連接層(fully-connected layer)等等,其他還有一些沒提到但等等會直接用於案例示範中的,應該都可以很輕易的Google搜尋到,就不再贅述。

在Pytorch中要定義自己的模型要先繼承他的nn.Module類別,並實作至少constructorforward兩個方法,這邊參考Pytorch在官網教學中使用的網路,並就我們的案例作微調:

from torch import nn
import torch.nn.functional as F
class FirstConvNet(nn.Module):
# constructor
def __init__(self):
super(FirstConvNet, self).__init__()
self.conv1 = nn.Conv2d(1, 6, 3)
self.pool = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(6, 16, 3)
self.fc1 = nn.Linear(16 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 10)
self.lsx = nn.LogSoftmax(dim=1)

def forward(self, x):
x = self.pool(F.relu(self.conv1(x))) # output 13x13
x = self.pool(F.relu(self.conv2(x))) # output 5x5
x = x.view(-1, 16 * 5 * 5)
x = F.relu(self.fc1(x))
x = self.fc2(x)
x = self.lsx(x)
return x
model = FirstConvNet()

在Pytorch要加入卷積層只需要呼叫nn.Conv2d,並設定有關卷積運算中的輸入維度(in_channel)、輸出維度(out_channel)、kernal_sizestridepadding等等,其中的維度和濾波器(filter)的數量有關,跟圖片長寬無關,也可以發現我們並沒有告訴卷積層輸入圖片大小,那是因為這會根據剛剛提到那最後三項有關,至於相關的計算細節還是交給Google來解答了…

損失函數 Loss Function

損失函數可以說是深度學習模型的靈魂,因為他可是一切反向傳播的起點,不同任務所定義的函數表示便不同,來看看Pytorch定義好了哪些loss function:https://pytorch.org/docs/stable/nn.html#loss-functions。由於我們實作的是手寫數字辨識,說是辨識其實就是將影像進行分類,而分類的對象就是0–9共10個類別,這邊採用nn.CrossEntropyLoss()作為示範,就不介紹詳細的計算方式,只能說這種多分類題目一般採用交叉熵(CrossEntropyLoss)的效果都不錯,有興趣的一樣可以找到相關的介紹:

loss_fn = nn.CrossEntropyLoss()

優化器 Optimizer

有了損失函數後,每次輸入模型的輸出和真值(label)作梯度的反向傳播,這個梯度值再拿來更新模型的權重,而如何更新則和優化器的種類有關,本文案例是使用隨機梯度下降法(Stochastic gradient descent, SGD),其他Adagrade, RMSprop, Adam等,主要想法差異是他們如何加權得到的梯度來變化現有權重,而隨機梯度下降經實驗在許多影像相關的模型訓練都有助於收斂且計算速度更快。

from torch import optimoptimizer = optim.SGD(model.parameters(), lr=0.0005, momentum=0.9)

(二)開始訓練!

看來準備就緒了,該馬上開始讓我們的模型來學習啦!但先別急~我們還有一些參數可以調整跟設定👌

訓練用超參數(Hyper-parameters)

一些常見的可能如輸入的批次資料數量(batch size)和要訓練的次數,可能會用iteration或epoch,前者是輸入批次資料的次數,後者則是輸入整個資料集的次數,例如使用的MNIST資料共有42000筆,當batch size = 32,而200個iterations代表模型只看到200*32=6400筆,「1」個epoch就代表模型已看完整個資料集42000筆的資料「1」次,其他參數如learning rate的已在優化器設定的階段即完成。

import torch# iteration
iterations = 20000
batch_size = 32
total_size = len(train_y)
for i in range(iterations):
optimizer.zero_grad()
end = min((i + batch_size)%total_size, total_size)
start = end - batch_size

batch_data = torch.Tensor(train_x[start:end])
output = model(batch_data)
target = torch.Tensor(train_y[start:end]).long()
loss = loss_fn(output, target)
if i % 1000 == 0:
print('Iteration %s: Loss= %s'%(i, loss.item()))
loss.backward()
optimizer.step()

得到的Output:

Iteration 0: Loss= 2.307753801345825
Iteration 1000: Loss= 1.3364770412445068
Iteration 2000: Loss= 0.27987733483314514
Iteration 3000: Loss= 0.08904282003641129
Iteration 4000: Loss= 0.11222745478153229
Iteration 5000: Loss= 0.13293687999248505
Iteration 6000: Loss= 0.04844062030315399
Iteration 7000: Loss= 0.07967690378427505
Iteration 8000: Loss= 0.06679101288318634
Iteration 9000: Loss= 0.02613155171275139
Iteration 10000: Loss= 0.021366586908698082
Iteration 11000: Loss= 0.056416213512420654
Iteration 12000: Loss= 0.11734075099229813
Iteration 13000: Loss= 0.20440557599067688
Iteration 14000: Loss= 0.11024016886949539
Iteration 15000: Loss= 0.009949509985744953
Iteration 16000: Loss= 0.033364810049533844
Iteration 17000: Loss= 0.08049172163009644
Iteration 18000: Loss= 0.023416034877300262
Iteration 19000: Loss= 0.14210675656795502

如何確保模型學習

一般機器學習訓練可概括為下圖所示流程。主要就是需要學會去看loss和採用的metric數值變化,並比較在訓練資料和驗證資料的表現,進而去調整可能影響的超參數,再不然就是要動到模型主體或是資料集本身,那這部分就非訓練上的議題,不列入考量。

模型訓練及調整流程圖

啊變化圖看起來都大同小異,到底要怎麼看呢?在機器學習中,不管資料集或大或小,模型或簡單或複雜,我們的訓練工作就是讓模型學會”收斂”,收斂就是讓loss function的值越小越好,也就是模型的權重慢慢更新到最後吐給你的輸出跟你的給定的標籤一樣,但收斂的對象僅止於訓練的資料,整個過程就像下圖漫畫的意思,所以最怕的就是過擬合(overfitting),等於只是讓機器解出跟國小會有的連連看題目一樣,通常真的看到loss變0是開心不起來的…要馬資料量太少不然就是模型設定太複雜,若是不想更動到原方法或是增加資料,則可以適當地調整超參數,來達到更好的學習效果,接下來將介紹比較常見的圖形和代表意義,以及可以針對不同情況可以如何調整訓練參數。

original comic by sandserif

從課程中給的下兩張圖,我認為已很好的說明大部分的訓練情況,可以從training loss的變化看出目前模型對樣本的收斂好壞(不管模型的泛用性),因為理論上若梯度反向傳播正常,權重變化理應往資料特性收斂,而在確定模型能有效收斂(=學習)後,才再從驗證資料(validation set)的損失變化或準確率表現等確認模型的學習狀況,但本文案例並沒有將資料集分出一部分當作驗證資料,這邊額外做一個簡單的說明,驗證資料通常不參與訓練,才能確保模型沒有將權重更新以”迎合”資料特性,一般來說在訓練模型時,因為沒有像本文練習的案例有所謂的test資料可以上傳測試,大部分也只能用驗證資料來衡量模型效能,甚至是作為所謂的測試資料集,去揣測於實際使用上的準確度,畢竟就算資料集再大,也只能說是逼近世界可能存在的資料分佈。

擷取自課程CS231n: Convolutional Neural Networks for Visual Recognition
擷取自課程CS231n: Convolutional Neural Networks for Visual Recognition

其他還有很多可以延伸學習的地方,到後來若想用的模型很大或是要處理的任務比較複雜時,可能訓練個兩三天都還沒個像樣的成果,所以這時候比較常採用遷移式學習(transfer learning)或微調(fine-tune)別的大神測好訓練好的模型,就不用讓權重從混沌狀態找尋目標,其他訓練的技巧像是regularization、early stopping、learning rate scheduler等就留給讀者自行摸索。為了提升模型效能,除了替換網路結構或對資料做一些特徵工程,也可能是需要改變這些超參數,雖然給人種換湯不換藥的感覺,但其實都是很重要的技巧,畢竟有些paper光是把這些trick弄好,模型就可以獲得大幅度的提升。

上傳Kaggle,看看模型表現吧

為了上傳給Kaggle評分,只需要將test.csv作同樣的處理,並餵入剛剛訓練好的模型,再照格式將答案填一填,整個流程可以參考下方的程式碼:

import numpy as npdf_test = pd.read_csv('./test.csv')test_x = df_test.values/255
data_length = test_x.shape[0]
test_x = test_x.reshape(data_length, 1, 28, 28)
with torch.no_grad():
output = model(torch.Tensor(test_x))
print(output.shape)
with open('submit.csv', 'w+') as f:
f.write('ImageId,Label\n')
for i, r in enumerate(np.argmax(output, axis=1).numpy()):
f.write('%s,%s\n'%(i+1, r))
上傳Kaggle的結果

結果拿到了0.95485的分數,主要原因筆者認為是訓練資料集和測試資料集本身樣本差異性就不大,若模型訓練過程正常,其實使用一般DNN模型也可以拿到很高的分數,這邊就留給大家自己去調整模型的參數,看看對結果有什麼影響,例如,拿掉一開始除以255的步驟,透過這樣實際上傳測試,應可以更好地感受差異,但一天只能上傳5次就是了。

完整程式碼附在下面,懶得一步步複製的可以直接一口氣打包貼上:)

import pandas as pd
import matplotlib.pyplot as plt
df = pd.read_csv('./train.csv')
image = df.iloc[0, 1:].values.reshape((28, 28))
plt.imshow(image) #確認是否成功
# input images
train_x = (df.iloc[:, 1:].values/255)
# 因為載入的資料被攤平成784維,為了餵入CNN則再動點小手腳轉回28x28的圖片
data_length = train_x.shape[0]
train_x = train_x.reshape(data_length, 1, 28, 28) # gray-scale
# labels
train_y = df.label.values
from torch import nn
import torch.nn.functional as F
class FirstConvNet(nn.Module):
# constructor
def __init__(self):
super(FirstConvNet, self).__init__()
self.conv1 = nn.Conv2d(1, 6, 3)
self.pool = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(6, 16, 3)
self.fc1 = nn.Linear(16 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 10)
self.lsx = nn.LogSoftmax(dim=1)

def forward(self, x):
x = self.pool(F.relu(self.conv1(x))) # output 13x13
x = self.pool(F.relu(self.conv2(x))) # output 5x5
x = x.view(-1, 16 * 5 * 5)
x = F.relu(self.fc1(x))
x = self.fc2(x)
x = self.lsx(x)
return x

model = FirstConvNet()

loss_fn = nn.CrossEntropyLoss()
from torch import optim
optimizer = optim.SGD(model.parameters(), lr=0.0005, momentum=0.9)
import torch
# iteration
iterations = 20000
batch_size = 32
total_size = len(train_y)
for i in range(iterations):
optimizer.zero_grad()
end = min((i + batch_size)%total_size, total_size)
start = end - batch_size

batch_data = torch.Tensor(train_x[start:end])
output = model(batch_data)
target = torch.Tensor(train_y[start:end]).long()
loss = loss_fn(output, target)
if i % 1000 == 0:
print('Iteration %s: Loss= %s'%(i, loss.item()))
loss.backward()
optimizer.step()

import numpy as np
df_test = pd.read_csv('./test.csv')
test_x = df_test.values/255
data_length = test_x.shape[0]
test_x = test_x.reshape(data_length, 1, 28, 28)
with torch.no_grad():
output = model(torch.Tensor(test_x))
print(output.shape)
with open('submit.csv', 'w+') as f:
f.write('ImageId,Label\n')
for i, r in enumerate(np.argmax(output, axis=1).numpy()):
f.write('%s,%s\n'%(i+1, r))

一切就緒,分享你的模型

到這一步你應該就能訓練出符合你期望的模型,但文章並沒有包含存/讀模型的程式,可以再另外寫一個test的程式來包含這些內容,並設計一些接口來輸入自己的照片給模型,並輸出結果看看預測的如何,這樣才是訓練模型的最終目的嘛。希望這篇文章能給剛接觸CNN的人一個很好的實作指引,了解需要注意的地方和有效提升模型精度,也能稍微練習用Pytorch建置神經網路,若對文章有疑慮或觀念錯誤的地方,可以在下面留言指正/討論,若對你有所幫助可以幫忙按一下👏給點牡蠣哈哈。

--

--