Learning Objectives
- 區分 epoch、批次與更新次數,算出一輪更新幾次
- 用 MLPClassifier 訓練手寫數字分類,並和基準比較
- 從損失曲線判斷學習率太大、太小與過擬合
- 說明提前停止與正則化怎麼用,以及它們的限制
- 把 MLPClassifier 的參數對應到 PyTorch 的寫法
從能算出梯度到訓練得起來
上一課已經能算出梯度。實際訓練還要決定幾件事,而且這些設定常常決定了模型學不學得起來:
- 每次更新用幾筆資料(批次)?
- 每次走多遠(學習率)?
- 要訓練多久、什麼時候停(過擬合與提前停止)?
這一課不再手寫迴圈,改用 scikit-learn 的 MLPClassifier,在真實資料上看這些設定的影響;最後看同樣的東西用 PyTorch 怎麼寫。
這一課的資料是 sklearn.datasets.load_digits():真實的公開資料,來自 UCI 機器學習資料庫的手寫數字資料集(Optical Recognition of Handwritten Digits),是它測試集的副本。原始手寫圖片經過前處理,縮成 8×8 的格子,每格是 0 到 16 的整數,共 1797 筆,答案是 0 到 9。
逐步看:手寫數字資料長什麼樣
每張圖攤平成 64 個數字,一列一張圖。把其中一列還原成 8×8,就是一張圖。
epoch、批次與更新次數
上一課每次更新都用全部資料算梯度(全批次)。資料多時這樣太慢,實際上是每次取一小批:
| 名詞 | 意思 |
|---|---|
| 批次(batch) | 一次算梯度用的資料,常見 32 到 256 筆 |
| 更新(update) | 用一個批次的梯度更新一次權重 |
| 一輪(epoch) | 全部訓練資料都看過一次 |
每輪更新次數 = 訓練筆數 ÷ 批次大小(有餘數就進位)
用小批次的梯度下降叫小批次梯度下降(mini-batch gradient descent)。每個批次的梯度只是估計值,所以損失會小幅跳動,但一輪可以更新很多次。
MLPClassifier 的主要預設值(scikit-learn 1.6):
| 參數 | 預設值 | 意思 |
|---|---|---|
hidden_layer_sizes |
(100,) |
一個隱藏層、100 個神經元 |
activation |
'relu' |
隱藏層的活化函數 |
solver |
'adam' |
更新方法,會自動調整每個參數的步伐 |
learning_rate_init |
0.001 |
學習率 |
batch_size |
'auto' |
min(200, 訓練筆數) |
max_iter |
200 |
最多跑幾輪(用 sgd、adam 時是 epoch 數) |
alpha |
0.0001 |
L2 正則化強度 |
early_stopping |
False |
不提前停止 |
類別超過兩個時,輸出層改成每個類別一個神經元,用 softmax 轉成總和為 1 的機率。
逐步看:MLPClassifier 分類手寫數字
先算基準:全猜最多的數字能對多少。再訓練一個 64-32-10 的網路,最後看損失曲線。
逐步看:批次大小的影響
為了只比較批次,這裡改用單純的 solver="sgd"(scikit-learn 預設帶 momentum=0.9)、學習率相同,三種批次都只跑 6 輪。partial_fit 每呼叫一次跑一輪。
學習率與過擬合
接下來用小工具看兩件最常遇到的事。資料是雜訊較多的月牙,只有一半拿來訓練,另一半是驗證資料(validation data):不參與訓練、用來觀察模型的表現與決定什麼時候停。
看損失曲線可以判斷很多事:
| 曲線長什麼樣 | 可能的原因 | 怎麼辦 |
|---|---|---|
| 下降得很慢 | 學習率太小 | 調大 |
| 上下亂跳、甚至上升 | 學習率太大 | 調小 |
| 一直很高、下不來 | 模型太簡單(欠擬合)或卡住 | 加神經元、換初始權重 |
| 訓練一直降,驗證先降後升 | 過擬合 | 提前停止、正則化、更多資料 |
過擬合的三種對策
小工具裡訓練損失幾乎降到 0、驗證損失卻一路上升:模型把訓練資料裡的雜訊也記住了。常用的對策:
| 對策 | 做法 | 在 MLPClassifier 裡 |
|---|---|---|
| 提前停止(early stopping) | 切一部分訓練資料當驗證資料,驗證分數連續幾輪沒有進步就停,留下最好的權重 | early_stopping=True,預設切 10%(validation_fraction),等 10 輪(n_iter_no_change) |
| L2 正則化 | 在損失加上權重平方的懲罰,權重比較不容易變得很大 | alpha,越大懲罰越強 |
| 更多訓練資料 | 雜訊比較難被記住 | 不是參數,要真的去蒐集 |
這些對策有沒有用要實測,不是加了就會變好。下面在手寫數字上比較。
逐步看:哪一種對策比較有效
訓練資料故意只用 120 筆或 600 筆(其餘都是測試資料),網路較大(128 個神經元)。每一列比較「跑了幾輪」「訓練準確率」「測試準確率」。
怎麼解讀上面的結果
這裡的訓練準確率是用全部訓練資料算的;提前停止時有 10% 被切去當驗證資料,所以訓練時實際上看不到那些資料。
上面的結果不代表提前停止沒有用,而是說明:
- 它要犧牲一部分資料當驗證資料,資料少時這個代價很明顯;
- 驗證資料太少時分數雜訊大,
n_iter_no_change太小就會太早停; - 這個資料集、這個網路本身過擬合得不嚴重,對策能幫上的本來就不多。小工具裡雜訊大的月牙,就看得到明顯的效果。
而且這只是切一次的結果。實際比較設定時要用第 14 課的交叉驗證。
同樣的東西用 PyTorch 怎麼寫
MLPClassifier 只適合小型的全連接網路,也不能用 GPU。要做影像、語音或更大的模型,一般改用 PyTorch 這類深度學習框架。PyTorch 不能在這個頁面的瀏覽器環境執行,下面這段只是對照,不會在這裡跑,要自己裝在電腦或 Colab 上試。
import torch
from torch import nn
from torch.utils.data import DataLoader, TensorDataset
# X_train、y_train 同上面的手寫數字資料
X_t = torch.tensor(X_train, dtype=torch.float32)
y_t = torch.tensor(y_train, dtype=torch.long)
loader = DataLoader(TensorDataset(X_t, y_t), batch_size=200, shuffle=True)
model = nn.Sequential(nn.Linear(64, 32), nn.ReLU(), nn.Linear(32, 10))
loss_fn = nn.CrossEntropyLoss() # 輸入是沒有經過 softmax 的分數(logits)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
for epoch in range(100):
model.train()
for xb, yb in loader:
loss = loss_fn(model(xb), yb) # 前向傳播
loss.backward() # 反向傳播:算出所有梯度
optimizer.step() # 更新權重
optimizer.zero_grad() # 清掉梯度,否則下一次會累加上去
model.eval()
with torch.no_grad(): # 預測時不需要梯度
pred = model(torch.tensor(X_test, dtype=torch.float32)).argmax(dim=1)
差別是 PyTorch 把訓練迴圈交給你自己寫,你也就能看到前兩課做的事:
| 這幾課學的 | MLPClassifier |
PyTorch |
|---|---|---|
| 網路結構 | hidden_layer_sizes=(32,)、activation='relu' |
nn.Sequential(nn.Linear(64, 32), nn.ReLU(), nn.Linear(32, 10)) |
| 前向傳播 | fit 裡面做 |
model(xb) |
| 損失 | 自動(log-loss) | nn.CrossEntropyLoss() |
| 反向傳播 | fit 裡面做 |
loss.backward() |
| 更新方法與學習率 | solver='adam'、learning_rate_init=0.001 |
torch.optim.Adam(model.parameters(), lr=0.001) |
| 批次 | batch_size=200 |
DataLoader(..., batch_size=200, shuffle=True) |
| 訓練幾輪 | max_iter |
外層 for epoch 迴圈 |
| 預測 | predict |
model.eval()、torch.no_grad()、argmax |
注意 PyTorch 的提前停止、驗證資料都要自己寫,下面的實作題就是這段邏輯。
實作:寫提前停止的判斷
完成兩個函數:
early_stop(val_losses, patience):val_losses是每一輪的驗證損失(第 0 輪開始)。一路記下損失最低的一輪;如果目前這一輪距離最低的那一輪已經有patience輪,而且這一輪沒有更低,就在這一輪停。回傳(最好的一輪, 停下來的一輪)。一直沒觸發時,停在最後一輪。updates_per_epoch(n, batch_size):一輪更新幾次(有餘數要進位)。
小測驗:訓練神經網路
訓練資料 1000 筆,批次大小 100,跑 5 輪。總共更新幾次權重?下面程式會印出什麼?
n, batch, epochs = 1000, 100, 5
print((n // batch) * epochs)
損失曲線上下亂跳,有時還比一開始高。最先該試什麼?
訓練準確率 1.0、測試準確率 0.89,測試準確率遠高於基準。下面哪一句最恰當?
MLPClassifier(solver="adam") 的 max_iter=200 是什麼意思?
PyTorch 的訓練迴圈裡,哪一行計算出所有參數的梯度?
上面的實驗裡,600 筆資料開提前停止,測試準確率比不開低。最合理的解讀是?
重點整理
- epoch=全部資料看一次;批次=一次更新用的資料;每輪更新次數=訓練筆數 ÷ 批次大小(進位)。
MLPClassifier預設一層 100 個神經元、ReLU、Adam、學習率 0.001、最多 200 輪;同樣要做尺度處理、要和基準比較、要看測試資料。- 學習率太小下降很慢,太大亂跳甚至發散;看損失曲線判斷。
- 過擬合時訓練損失降、驗證損失升。對策有提前停止、L2 正則化與更多資料,有沒有用要實測;在手寫數字的實驗裡,增加資料的效果最明顯。
- 提前停止要用驗證資料決定,不能用測試資料。
- PyTorch 把訓練迴圈交給你寫:
model(xb)、loss.backward()、optimizer.step()、optimizer.zero_grad(),正好對應前兩課的前向、反向與更新。 - 下一課是專題:把整門課用在校園單車的借用量預測,從切分、特徵、模型比較一路做到評估報告。