上兩篇文我們從一般線性分類一直到多層感知器,把幾個重要函數定義了,我們先複習一下:
整個模型的最後輸出用 Softmax 做分類問題:
, 而
用交叉熵來計算損失函數 :
為了引入非線性,我們每個隱藏層的每個單元加上 激活函數 ReLU :
這樣我們已經有材料可以開發出訓練模型了,事不宜遲,今天就開始實作,我們用深度學習的Hello World 常用的MNIST database 數字辨識來當作訓練目標,基本上它就是一張張圖形成的資料集,每張圖都是28x28 圖像的一個手寫數字,還有對應的標籤(Label) 作為監督學習的素材,我們列印出160張圖,大約長成這樣:
(取自 Wiki)
MNIST database 共包含了 6,000張訓練用圖;以及1000張測試模型準確度的用圖。機器學習的資料集通常至少會區隔開兩個集合:『訓練集』與『測試集』,比較講究的還會多增一個『驗證集』。以下取材自 Multilayer perceptrons from scratch
import mxnet as mx
import numpy as np
from mxnet import nd, autograd, gluon
data_ctx = mx.cpu()
model_ctx = mx.gpu()
#練習用GPU加速計算。改用mx.cpu(),以這麼小的數據,速度差不了多少
剛剛我們提到 MNIST 有圖,有對應數字 Label。那特徵呢?就只有 28x28 = 784 個點,每個點用 0到255代表灰階程度。通常機器學習的框架們,無論Tensorflow 還是 PyTorch 都會提供教學常用的資料集,Mxnet Gluon 亦然。這邊要關注的是 transform 這函數,它特徵值轉換為0到1的浮點數。
num_inputs = 784 #784=28x28
num_outputs = 10 #輸出為十個數字預測
batch_size = 64
num_examples = 60000
def transform(data, label):
return data.astype(np.float32)/255, label.astype(np.float32)
train_data = gluon.data.DataLoader(mx.gluon.data.vision.MNIST(train=True, transform=transform),
batch_size, shuffle=True)
test_data = gluon.data.DataLoader(mx.gluon.data.vision.MNIST(train=False, transform=transform),
batch_size, shuffle=False)
回憶先前我們做線性迴歸,要先給予所有參數初始值,後續在利用梯度下降,每個 Iteration 做更新參數,逐步讓模型趨向最佳化。我們進步到用MLP多層感知機,也不例外,一樣要做同樣的事,只是參數隨著層數多就變多了。後續我們再了解更多的深度學習模型,會發現都要做『參數初始化』,參數初始化這個也是一個很重要的課題,Choosing Weights: Small Changes, Big Differences談到如果一開始初始化做的不好,那模型訓練會無法收斂。我又要在講金髮姑娘原則,這篇 Deep Learning Best Practices (1) — Weight Initialization很容易理解。
#設定所有的隱藏層都是 256 個單元
num_hidden = 256
weight_scale = .01
#設定第一層隱藏層的參數與偏差,採正常分佈平均值0,隨機取值
W1 = nd.random_normal(shape=(num_inputs, num_hidden), scale=weight_scale, ctx=model_ctx)
b1 = nd.random_normal(shape=num_hidden, scale=weight_scale, ctx=model_ctx)
#設定第二層隱藏層的參數與偏差
W2 = nd.random_normal(shape=(num_hidden, num_hidden), scale=weight_scale, ctx=model_ctx)
b2 = nd.random_normal(shape=num_hidden, scale=weight_scale, ctx=model_ctx)
##設定輸出層的參數與偏差
W3 = nd.random_normal(shape=(num_hidden, num_outputs), scale=weight_scale, ctx=model_ctx)
b3 = nd.random_normal(shape=num_outputs, scale=weight_scale, ctx=model_ctx)
params = [W1, b1, W2, b2, W3, b3]
#要對參數自動計算梯度,須賦予空間給參數
for param in params:
param.attach_grad()
終於要把最開始談的數學公式變成我們的程式:
def relu(X):
return nd.maximum(X, nd.zeros_like(X))
定義 softmax 與 cross entropy 如下:
def softmax(y_linear):
exp = nd.exp(y_linear-nd.max(y_linear)) #解決 exp(很大的值) 會overflow
partition = nd.nansum(exp, axis=0, exclude=True).reshape((-1, 1))
return exp / partition
def cross_entropy(yhat, y):
return - nd.nansum(y * nd.log(yhat), axis=0, exclude=True)
這個在有極端值的時候,不是softmax 就是cross_entropy 計算過程會有 NaN (Not a Number),這是因為有 exp(…)這樣的運算,容易變成overflow。所以我們用nansum以防萬一,出現異常的時候我們可以介入手工處理。 這個基本上不太可行,我們不能一直守在旁邊觀察有無異常。
我們訓練時需要的是 cross_entropy(softmax(…)) 經過一番的LogSumExp,LSE,數學分析,主要是針對計算 LSE:
內有一個x值很大時,exp()運算會產生overflow 的解決方法。最終得到公式:
,而。 (公式一)
還沒忘記我們目的是要計算出交叉熵損失函數 。有了這個公式以後要計算 cross_entropy(softmax(…)) 就可以帶入上面公式。
= = (公式二)
可結合上述(公式一)與(公式二)這個Mxnet Gluon 提供了函數給我們呼叫,而不使用我們自己撰寫的兩個函數 cross_entropy(softmax(…))來計算,但是softmax 還是有存在意義的,如果我們要看預測出來的10個數字機率時,就需要 softmax:
def softmax_cross_entropy(yhat_linear, y):
return - nd.nansum(y * nd.log_softmax(yhat_linear), axis=0, exclude=True)
有了分類用的交叉熵損失函數,我們開始堆疊網路,幾乎是機械式反應:每一層都以參數計算出線性值,再以ReLU 做非線性轉換。
def net(X):
h1_linear = nd.dot(X, W1) + b1
h1 = relu(h1_linear)
h2_linear = nd.dot(h1, W2) + b2
h2 = relu(h2_linear)
yhat_linear = nd.dot(h2, W3) + b3
#這邊直接輸出線性值,後面訓練再用 softmax_cross_entropy計算損失
return yhat_linear
def SGD(params, lr):
for param in params:
param[:] = param - lr * param.grad
網路定義了兩層隱藏層,參數初始化了,訓練與測試用資料集也有 Data Loader 協助抓取,損失函數也定義了,現在又有了隨機梯度下降SGD幫忙更新參數,我們可以開始訓練網路:
epochs = 10
learning_rate = .001
smoothing_constant = .01
for e in range(epochs):
cumulative_loss = 0
for i, (data, label) in enumerate(train_data):
data = data.as_in_context(model_ctx).reshape((-1, 784))
label = label.as_in_context(model_ctx)
label_one_hot = nd.one_hot(label, 10)
with autograd.record():
output = net(data)
loss = softmax_cross_entropy(output, label_one_hot)
loss.backward()
SGD(params, learning_rate)
cumulative_loss += nd.sum(loss).asscalar()
print("Epoch %s. Loss: %s" % (e, cumulative_loss/num_examples))
這樣就完成可以訓練,更講究一點我們還可以定義『評估模型準確度函數』然後在訓練的每個回合都印出以訓練集與測試集來進行評估,目的在讓自己知道模型的收斂程度。我們先不談這個。
專案緣起記錄在 【UP, Scrum 與 AI專案】