iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 24
0

上兩篇文我們從一般線性分類一直到多層感知器,把幾個重要函數定義了,我們先複習一下:

整個模型的最後輸出用 Softmax 做分類問題:
https://chart.googleapis.com/chart?cht=tx&chl=%5Chat%7By%7D%20%3D%20%7Bsoftmax%7D(W_y%20%7Bh%7D_n%20%2B%20b_y) , 而 https://chart.googleapis.com/chart?cht=tx&chl=%24%24%5Ctext%7Bsoftmax%7D(%7Bz%7D)%20%3D%20%5Cfrac%7Be%5E%7B%7Bz%7D%7D%20%7D%7B%5Csum_%7Bi%3D1%7D%5Ek%20e%5E%7Bz_i%7D%7D%24%24

用交叉熵來計算損失函數 :

https://chart.googleapis.com/chart?cht=tx&chl=%5Cell(%5CTheta%7D)%20%3D%20-(1%2Fn)%20%20%5Csum_%7Bi%3D1%7D%5En%20%5Clog%20%5Chat%20y_%7By%5E%7B(i)%7D%7D%5E%7B(i)%7D

為了引入非線性,我們每個隱藏層的每個單元加上 激活函數 ReLU :

https://chart.googleapis.com/chart?cht=tx&chl=%5Ctext%7BReLU%7D(x)%20%3D%20%5Cmax(x%2C%200)

這樣我們已經有材料可以開發出訓練模型了,事不宜遲,今天就開始實作,我們用深度學習的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(…)) 就可以帶入上面公式。
https://chart.googleapis.com/chart?cht=tx&chl=%5Ctext%7Blog%7D%7B(%5Chat%20y_j)%7D%20%3D%20%5Ctext%7Blog%7D%5Cleft(%20%5Cfrac%7Be%5E%7Bz_j%7D%7D%7B%5Csum_%7Bi%3D1%7D%5E%7Bn%7D%20e%5E%7Bz_i%7D%7D%5Cright)%20 = https://chart.googleapis.com/chart?cht=tx&chl=%20%5Ctext%7Blog%7D%7B(e%5E%7Bz_j%7D)%7D-%5Ctext%7Blog%7D%7B%5Cleft(%20%5Csum_%7Bi%3D1%7D%5E%7Bn%7D%20e%5E%7Bz_i%7D%20%5Cright)%7D%20 = https://chart.googleapis.com/chart?cht=tx&chl=z_j%20-%5Ctext%7Blog%7D%7B%5Cleft(%20%5Csum_%7Bi%3D1%7D%5E%7Bn%7D%20e%5E%7Bz_i%7D%20%5Cright) (公式二)

可結合上述(公式一)與(公式二)這個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專案】


上一篇
深度學習的 Hello World - 多層感知機 MLP
下一篇
由 MLP 看機器學習導入策略
系列文
深度學習所需入門知識--一位初學者的認知31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言