多層感知器分類器的 Tensorflow 實現

數據準備

本文中以 0~9 數字圖片識別,首先準備圖片數據,這裏使用到的是 MNIST 數據集,藉助 Keras 實現:

import os
import tensorflow as tf # 導入 TF 庫
from tensorflow import keras # 導入 TF 子庫
from tensorflow.keras import layers, optimizers, datasets # 導入 TF 子庫
(x, y), (x_val, y_val) = datasets.mnist.load_data() # 加載數據集
x = 2*tf.convert_to_tensor(x, dtype=tf.float32)/255.-1 # 轉換爲張量,縮放到-1~1
y = tf.convert_to_tensor(y, dtype=tf.int32) # 轉換爲張量
y = tf.one_hot(y, depth=10) # one-hot 編碼
print(x.shape, y.shape)
train_dataset = tf.data.Dataset.from_tensor_slices((x, y)) # 構建數據集對象
train_dataset = train_dataset.batch(512) # 批量訓練

一張圖片我們用 shape 爲 [h, w] 的矩陣來表示,對於多張圖片來說,我們在前面添加一個數量維度(Dimension),使用 shape 爲 [𝑐,ℎ,𝑥],𝑐 代表了 batch size(批量);多張彩色圖片可以使用 shape 爲 [𝑐,ℎ,𝑥,𝑑] 的張量來表示,其中的𝑑表示通道數量(Channel)。

多輸入多輸出神經元

假如說輸入的維度爲 dind_{\text{in}},那麼使用線性激活函數(恆等激活函數)output=input\operatorname {output } = \operatorname {input} 實現的的感知器(Perceptron)的數學表達爲:

y=wTx+b=[w1,w2,w3,,wdn][x1x2x3xdin]+b y = \boldsymbol { w } ^ { T } \boldsymbol { x } + b = \left[ w _ { 1 } , w _ { 2 } , w _ { 3 } , \ldots , w _ { d _ { n } } \right] \cdot \left[ \begin{array} { c } x _ { 1 } \\ x _ { 2 } \\ x _ { 3 } \\ \dots \\ x _ { d _ { i n } } \end{array} \right] + b

現將這種 Perceptron 拓展到多輸入多輸出的神經元模型,假如輸入維度爲 dind_{\text{in}},輸出維度爲 doutd_{\text{out}},可以寫爲:

y=Wx+b \boldsymbol { y } = W \boldsymbol { x } + \boldsymbol { b }

其中 xRdin,bRdout,yRdout,WRdout×din\boldsymbol { x } \in R ^ { d _ { i n } } , \boldsymbol { b } \in R ^ { d _ { o u t } } , \boldsymbol { y } \in R ^ { d _ { o u t } } , W \in R ^ { d _ { o u t } \times d _ { i n } }

對於多輸出節點、批量訓練方式,加入批量數據的數據量爲 kk,現將模型寫成張量形式:

Y=X@W+b \mathrm { Y } = \mathrm { X } @ \mathrm { W } + \boldsymbol { b }

其中 XRk×din,bRdout,YRk×dout,WRdin×dout\mathrm X \in R ^ { k \times d _ { i n } } , \boldsymbol { b } \in R ^ { d _ { o u t } } , \mathrm { Y } \in R ^ { k \times d _ { o u t } } , \mathrm W \in R ^ { d _ { i n } \times d _ { o u t } },@符號表示矩陣相乘( Matrix Multiplication,matmul )。

現在假設輸入樣本數爲 2,輸入特徵長度爲 3,輸出維度爲 2:

[o11o21o12o22]=[x11x21x31x12x22x32][w11w12w21w22w31w32]+[b1b2] \left[ \begin{array} { l l } o _ { 1 } ^ { 1 } & o _ { 2 } ^ { 1 } \\ o _ { 1 } ^ { 2 } & o _ { 2 } ^ { 2 } \end{array} \right] = \left[ \begin{array} { l l l } x _ { 1 } ^ { 1 } & x _ { 2 } ^ { 1 } & x _ { 3 } ^ { 1 } \\ x _ { 1 } ^ { 2 } & x _ { 2 } ^ { 2 } & x _ { 3 } ^ { 2 } \end{array} \right] \left[ \begin{array} { l l } w _ { 11 } & w _ { 12 } \\ w _ { 21 } & w _ { 22 } \\ w _ { 31 } & w _ { 32 } \end{array} \right] + \left[ \begin{array} { l } b _ { 1 } \\ b _ { 2 } \end{array} \right]

上述公式對應的模型結構圖爲:
在這裏插入圖片描述
可以看到,通過張量形式表達網絡結構,更加簡潔清晰,同時也可充分利用張量計算的並行加速能力。那麼怎麼將圖片識別任務的輸入和輸出轉變爲滿足格式要求的張量形式呢?

輸入處理(one-hot 編碼)

一張圖片𝒚使用矩陣方式存儲,shape 爲:[ℎ,𝑥],𝑐 張圖片使用 shape
爲 [𝑐,ℎ,𝑥] 的張量 X 存儲。而現在的模型只能接受向量形式的輸入特徵向量,因此需要將 [ℎ,𝑥] 的矩陣形式圖片特徵平鋪成 [ℎ ∗ 𝑥] 長度的向量。以下圖爲例:

在這裏插入圖片描述

同時一般的數據標籤都是用一個數字來表示便籤信息,假如說 1、2、3、4 分別對應的標籤是貓、狗、魚、鳥,這裏的 1、2、3、4 只是表示的 ID 標籤,但是數字本身的大小關係迫使機器學習模型來學習這種大小約束關係。所以現在提出一種編碼方式:one-hot 編碼。那麼對於某數據集,總類別數爲 n,假設某個樣本屬於類別 𝑖,即圖片的中數字爲 𝑖,只需要一個長度爲 n 的向量 𝐲 ,向量 𝐲 的索引號爲 𝑖 的元素設置爲 1,其他位爲 0。

在這裏插入圖片描述

One-hot 編碼是非常稀疏 (Sparse) 的,相對於數字編碼來說,佔用較多的存儲空間,所以一般在存儲時還是採用數字編碼。

利用 TensorFlow 實現:

y = tf.constant([0,1,2,3]) # 數字編碼
y = tf.one_hot(y, depth=10) # one-hot 編碼
print(y)

其中 one_hot 中的參數 depth 表示的是 one-hot 編碼後的向量長度。

評價函數(損失函數)

對於分類問題來說,目標是最大化某個性能指標,比如準確度 ,但是把準確度當做損失函數去優化時,會發現是不可導的,無法利用梯度下降算法優化網絡參數。一般的做法是,設立一個平滑可導的代理目標函數,比如優化模型的輸出 與 One-hot 編碼後的真實標籤 y 之間的距離(Distance)。因此,相對迴歸問題而言,分類問題的優化目標函數和評價目標函數是不一致的。

模型的訓練目標是通過優化損失函數 L\mathcal L 來找到最優數值解 W,bW^∗, \boldsymbol b^∗

W,b=argminW,bL(o,y) \mathrm { W } ^ { * } , \boldsymbol { b } ^ { * } = { \mathop { \text {argmin} } \limits_ { W , \boldsymbol { b } } \mathcal { L } ( \boldsymbol { o } , \boldsymbol { y } ) }

這裏採用均方差損失函數,那麼對於𝑁個樣本的均方差損失函數爲:

L(o,y)=1Ni=1Nj=1n(ojiyji)2 \mathcal { L } ( \boldsymbol { o } , \boldsymbol { y } ) = \frac { 1 } { N } \sum _ { i = 1 } ^ { N } \sum _ { j = 1 } ^ { n} \left( o _ { j } ^ { i } - y _ { j } ^ { i } \right) ^ { 2 }

現在我們只需要採用梯度下降算法來優化損失函數得到 W,bW, \boldsymbol b 的最優解,利用求得的模型去預測未知的手寫數字圖片 xDtest\boldsymbol { x } \in \mathbb { D } ^ { \text {test} }

非線性激活函數

激活函數實際上是對輸入 x 的線性組合做了進一步的處理然後輸出,也就下式中的 σ\sigma

o=σ(Wx+b) \boldsymbol { o } = \sigma ( \boldsymbol { W } \boldsymbol { x } + \boldsymbol { b } )

而恆等激活函數就是對其直接輸出,即:

z=σ(z) z = \sigma (z)

但是有嚴格證明可以知道當使用線性激活函數時,無論經過多少層的神經元,其輸出仍然是輸入 x 的線性組合。所以需要使用一些非線性激活函數增強模型的能力(power)。常見的非線性激活函數有Sigmoid 函數(圖 a),ReLU 函數(圖 b)

在這裏插入圖片描述

ReLU 函數非常簡單,僅僅是在 𝑦 = 𝑥 在基礎上面截去了 𝑥 < 0 的部分,可以直觀地理解爲 ReLU 函數僅僅保留正的輸入部份,清零負的輸入。ReLU 函數的具體實現爲:

f(x)=max(0,x) f ( x ) = \max ( 0 , x )

雖然簡單,ReLU 函數卻有優良的非線性特性,而且梯度計算簡單,訓練穩定,是深度學習模型使用最廣泛的激活函數之一。下文中會嵌套 ReLU 函數將模型轉換爲非線性模型:

o=ReLU(Wx+b) \boldsymbol { o } = \operatorname { ReLU } ( \boldsymbol { W } \boldsymbol { x } + \boldsymbol { b } )

多層感知器堆疊

針對於模型的表達能力偏弱的問題,可以通過重複堆疊多次變換來增加其表達能力:

h1=ReLU(W1x+b1)h2=ReLU(W2h1+b2)o=W3h2+b3 \begin{array} { c } \boldsymbol { h } _ { \mathbf { 1 } } = \operatorname { ReLU } \left( \boldsymbol { W } _ { \mathbf { 1 } } \boldsymbol { x } + \boldsymbol { b } _ { \mathbf { 1 } } \right) \\ \boldsymbol { h } _ { \mathbf { 2 } } = \operatorname { ReLU } \left( \boldsymbol { W } _ { \mathbf { 2 } } \boldsymbol { h } _ { \mathbf { 1 } } + \boldsymbol { b } _ { 2 } \right) \\ \boldsymbol { o } = \boldsymbol { W } _ { 3 } \boldsymbol { h } _ { 2 } + \boldsymbol { b } _ { 3 } \end{array}

把第一層神經元的輸出值 h1h_1 作爲第二層神經元模型的輸入,把第二層神經元的輸出 h2h_2 作爲第三層神經元的輸入,最後一層神經元的輸出作爲模型的輸出 。

從下圖所示的網絡結構上看,函數的嵌套表現爲網絡層的前後相連,每堆疊一個(非)線性環節,網絡層數增加一層。我們把數據節點所在的層叫做輸入層,每一個非線性模塊的輸出 hih_i 連同它的網絡層參數 𝑾𝒋 和 𝒃𝒋 稱爲一層網絡層,特別地,對於網絡中間的層,叫做隱藏層,最後一層叫做輸出層。這種由大量神經元模型連接形成的網絡結構稱爲(前饋)神經網絡(Neural Network)。

在這裏插入圖片描述

優化方法

優化方法採用的是經典的反向傳播算法。具體推導在博文 機器學習技法 之 神經網絡(Neural Network) 中有介紹過。學過之後會發現,可以根據後一層的梯度值求前一層的梯度值,這樣就實現了反向傳播,並且可以得出一個可以用於任何網絡結構的計算範式,所以深度學習框架通過該範式自動獲得梯度的大小,從而方便模型的實現。

TensorFlow 實現

網絡搭建

對於前文提到的三層網絡,其輸入層的神經網絡模型由 256 個神經元組成,在 TensorFlow 中通過一行代碼即可實現:

layers.Dense(256, activation='relu')

使用 TensorFlow 的 Sequential 容器可以非常方便地搭建多層的網絡。對於 3 層網絡,對於前文提到的三層網絡,可以使用以下語句實現:

model = keras.Sequential([ # 3 個非線性層的嵌套模型
layers.Dense(256, activation='relu'),
layers.Dense(128, activation='relu'),
layers.Dense(10)])

模型訓練 得到模型輸出 後,通過 MSE 損失函數計算當前的誤差ℒ

with tf.GradientTape() as tape:
    # 打平操作,[batch, 28, 28] => [batch, 784]
    x = tf.reshape(x, (-1, 28*28))
    # Step1. 得到模型輸出output [batch, 784] => [batch, 10]
    out = model(x)
    # [batch] => [batch, 10]
    y_onehot = tf.one_hot(y, depth=10)
    # Step2. 計算均方誤差 [batch, 10] => [batch]
    loss = tf.reduce_sum(tf.square(out-y_onehot)) / x.shape[0]
    
acc_meter.update_state(tf.argmax(out, axis=1), y)

# Step3. 計算參數的梯度 w1, w2, w3, b1, b2, b3
grads = tape.gradient(loss, model.trainable_variables)
# w' = w - lr * grad, 更新網絡參數
optimizer.apply_gradients(zip(grads, model.trainable_variables))

最終實現

import  tensorflow as tf
from    tensorflow.keras import datasets, layers, optimizers, Sequential, metrics

# 設置GPU使用方式
# 獲取GPU列表
gpus = tf.config.experimental.list_physical_devices('GPU')
if gpus:
    try:
        for gpu in gpus:
            # 設置GPU爲增長式佔用
            tf.config.experimental.set_memory_growth(gpu, True)
    except RuntimeError as e:
        # 打印異常
        print(e)

# 導入數據
(x_train, y_train),(x_val, y_val) = datasets.mnist.load_data()
print('datasets:', x_train.shape, y_train.shape, x_train.min(), x_train.max())

# 將樣本屬性轉換爲張量
x_train = tf.convert_to_tensor(x_train, dtype=tf.float32) / 255.
x_val = tf.convert_to_tensor(x_val, dtype=tf.float32) / 255.

# 每批次的樣本個數
batch_size = 30

# 模型迭代次數
epochs = 30

# 序列模型 Sequential 適用於每層只有一個輸入張量和一個輸出張量的簡單層堆棧
model = Sequential([layers.Dense(256, activation='relu'),
                     layers.Dense(128, activation='relu'),
                     layers.Dense(10)]) 

# input_shape 爲輸入層的形狀參數 None 代表任意批次 28* 28 代表輸入參數維度
model.build(input_shape=(None, 28*28))

# 序列模型信息打印
model.summary()

# 隨機梯度下降法 優化器
optimizer = optimizers.Adam(lr=0.01)

# # Keras 中的一種訓練方式
# x_train = tf.reshape(x_train, (-1, 28*28))
# x_val = tf.reshape(x_val, (-1, 28*28))
# y_train = tf.one_hot(y_train, depth=10)
# y_val = tf.one_hot(y_val, depth=10)

# model.compile(
#     optimizer = tf.keras.optimizers.Adam(lr=0.01),
#     loss = tf.keras.losses.MeanSquaredError(),
#     metrics=[tf.keras.metrics.CategoricalAccuracy()])

# model.fit(x_train,y_train,batch_size,epochs,validation_data = (x_val, y_val))

# 將樣本屬性和標籤轉換爲張量
dataset_train = tf.data.Dataset.from_tensor_slices((x_train,y_train))

# 將樣本批次大小設置爲 batch_size,並將樣本數據複製 30 次
dataset_train = dataset_train.batch(batch_size).repeat(epochs)

# metrics 是用來判斷模型性能的
# 其中的準確率容器 Accuracy 判斷某批次中預測值與實際值是否相同(二值準確率) 
acc_meter = metrics.Accuracy()

def val_acc(x,y):
    # 打平操作,[batch, 28, 28] => [batch, 784]
    x = tf.reshape(x, (-1, 28*28))
   
    # Step1. 得到模型輸出output [batch, 784] => [batch, 10]
    out = model(x)

    # one-hot 編碼 [batch] => [batch, 10]
    y_onehot = tf.one_hot(y, depth=10)
    
    # Step2. 計算均方誤差 [batch, 10] => [1] 
    loss = tf.reduce_sum(tf.square(out-y_onehot)) / x.shape[0]

    acc_meter.reset_states()

    acc_meter.update_state(tf.argmax(out, axis=1), y)

    print('val_loss:', float(loss), 'val_acc:', acc_meter.result().numpy())

# 遍歷根據批次分割後數據樣本集
for step, (x,y) in enumerate(dataset_train):

    # 遍歷根據批次分割後數據樣本集
    with tf.GradientTape() as tape:

        # 打平操作,[batch, 28, 28] => [batch, 784]
        x = tf.reshape(x, (-1, 28*28))
       
        # Step1. 得到模型輸出output [batch, 784] => [batch, 10]
        out = model(x)

        # one-hot 編碼 [batch] => [batch, 10]
        y_onehot = tf.one_hot(y, depth=10)
        
        # Step2. 計算均方誤差 [batch, 10] => [1] 
        loss = tf.reduce_sum(tf.square(out-y_onehot)) / x.shape[0]

        # 向準確率容器中添加當前訓練樣本的預測值與實際值,用於更新當前批次準確率
        # 可以使用參數 sample_weight=[1, 1, 0, 0 ... 1] 用於計算加權準確率
        acc_meter.update_state(tf.argmax(out, axis=1), y)
        
        # Step3. 計算參數的梯度 w1, w2, w3, b1, b2, b3
        grads = tape.gradient(loss, model.trainable_variables)

        # Step4. 根據公式 w' = w - lr * grad 更新網絡參數
        optimizer.apply_gradients(zip(grads, model.trainable_variables))

        if step % 200==0:

            print(step, 'loss:', float(loss), 'acc:', acc_meter.result().numpy(),end=' ')
            val_acc(x_val,y_val)
            # 每 200 個數據量大小爲 batchsize 數據集作爲一個批次數據
            # 之後重置準確率容器中的緩存,從新計算準確率
            acc_meter.reset_states()
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章