【實務系列】練習用 Tensorflow 打造一個神經網路!

追客 Fredrick Hong
程式愛好者
Published in
13 min readJun 3, 2022

去年剛踏入機器學習的領域時,就有想分享實務相關的文章,然而原本的計畫是想先把李老師2021的影片看完,再來提升 coding 方面的經驗。但依照目前的進度來看,李老師的影片看完今年也過完了,所以就兩邊一起進行了。

目前工作上使用 keras 其實就很夠用了,但最近因為種種因素,讓我想試試看 tensorflow、pytorch…等等其他框架。

我把自己練習的過程分享出來,一方面自己釐清每個步驟,另一方面也希望可以幫助到剛要入門的人。因為在找範例時,不知道是不是關鍵字打得不夠精準,找不太到中文的文章,所以這篇的範例是直接拿國外的範例來練習。

此篇範例是完全按照底下這篇文章所練習的,只是多了一些對於程式碼的解釋:https://adventuresinmachinelearning.com/python-tensorflow-tutorial/

TensorFlow Logo From Wikipedia

前情提要

因為是要打造一個神經網路,所以程式碼基本上就是將神經網路的基本運作實現出來,只是在框架底下,可以省略很多底層的運算,但對於神經網路還是需要有基礎的認識。

練習開始

第一步:匯入套件跟資料集

先匯入需要的套件以及資料集,tensorflow 內建有手寫資料集可以用。

x_train.shape = (60000, 28, 28)
y_train.shape= (10000, 28, 28)
x_test.shape = (60000,)
y_test.shape = (10000,)

訓練資料是黑白的手寫圖片,像素非常低,因此只需要用 (28, 28) 就可以代表一張圖片。
從上面的 shape 可以得知,我們的訓練資料有6萬筆,測試資料有1萬筆。

我們要的目標是要讓神經網路能夠分辨手寫數字 0~9,因此這是一個 label 有10個的 multi-classification 的任務。

import tensorflow as tf
import numpy as np
from tensorflow.keras.datasets import mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()

第二部:參數設定

再來將訓練的參數設定好,並且將 data 的數值縮放到 0~1 之間(正規化),並將 x_test 轉換成 tensor object(才能餵進神經網路)。

這個步驟不一定要在這時候做,我目前的步驟是順著參考的範例做下來。

epochs = 10
batch_size = 100
x_train = x_train / 255.0 # scaler to 0~1
x_test = x_test / 255.0 # scaler to 0-1
x_test = tf.Variable(x_test)

第三部:切分 batch

在訓練網路時將訓練資料切分成小資料集的 batch,在模型的綜合評估上是優於把一整包訓練集丟下去訓練的,因此這邊先定義了一個切分資料集的 function,兩者的比較可以參考這篇李老師的影片筆記

def get_batch(x_data, y_data, batch_size):
idxs = np.random.randint(0, len(y_data), batch_size)
return x_data[idxs,:,:], y_data[idxs]
x_data, y_data = get_batch(x_train, y_train, batch_size=100)
print(y_data.shape)
print(x_data.shape)

(100, 28, 28)
(100,)

第四部:打造神經網路

架構

我們要打造的神經網路架構如下,非常簡單,只有三層 layer。
input layer:784 (28x28) 個神經元。
hidden layer:300 個神經元,activation function 選擇用 relu。
output layer:10 個神經元,activation function 選擇用 softmax (會讓輸出的結果總和為1,通常用在 multi-classification 的任務輸出層)。

參數量

一個神經網路的的公式可以簡化成:y = (Wx + b) * activation function,透過公式可以了解我們接下來要設定的 weights 和 bias 的數量。
input layer:0
hidden layer:784x300(weights) + 300(bias) = 235500
output layer:300*10(weights) + 10(bias) = 3010

Tensorflow

我們可以透過 tf.random.normal 來隨機創造一個常態分佈的矩陣,並且可以自行設定平均數 (mean) 跟標準差 (stddev)。

根據上面 weights, bias 的數量,所有的 weights 和 bias 就可以透過下面的程式碼創建出來。

W1 = tf.Variable(tf.random.normal([784,300], stddev=0.03), name='W1')
b1 = tf.Variable(tf.random.normal([300]), name='b1')
W2 = tf.Variable(tf.random.normal([300,10], stddev=0.03), name='W2')
b2 = tf.Variable(tf.random.normal([10]), name='b2')
  1. 再來是定義網路的架構,首先必須先把 input data 攤平成784的形狀才能餵進網路中。
  2. tf.matmul(a, b) 是讓 a, b 兩個矩陣相乘,且 a, b 兩個矩陣的資料型態必須相同。
    tf.add(a, b) 是讓 a, b 兩個矩陣相加。
    因此第二行的程式碼就是在實現 y = Wx + b 這件事情。
  3. 接著計算出 Wx + b 後,我們需要再用一個 activation function 做轉換。
  4. 然後輸出層再做同樣的事情一次。
def nn_model(x_input, W1, b1 ,W2, b2):
x_input = tf.reshape(x_input, shape=(x_input.shape[0], -1))
# y = (Wx + b) * activation function
x = tf.add(tf.matmul(tf.cast(x_input, tf.float32), W1), b1)
x = tf.nn.relu(x)
outputs = tf.add(tf.matmul(x, W2), b2)
outputs = tf.nn.softmax(outputs)
return outputs

Keras

上述是以 Tensorflow 來架一個 nn 的步驟,為了確定參數量設定得正確,這邊用 Keras 架了一個相同架構的 nn,來比較一下。

input_layer = Input(shape=(784,), name='input_layer')
dense = Dense(300, activation='relu', name='hidden_layer')(input_layer)
output_layer = Dense(10, activation='softmax', name='output_layer')(dense)
model_ = Model(inputs=input_layer, outputs=output_layer)
model_.summary()
model.summary()

看起來參數量設定正確!

第五部:定義 loss function

範例使用了 Tensorflow 內建的給手寫辨識任務的 loss function,參數就是 label 和 predict outputs,當然也可以選擇用其他 loss function。

def loss_fn(outputs, labels):
cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=labels, logits=outputs))
return cross_entropy

第六部:定義 optimizer

上一篇分享李老師的文章,正好就是在介紹各種 optimizer,但這邊是直接用 keras 內建好的 Adam,省去一些功。若要自己寫出來,網路上也有蠻多中文的文章。

optimizer = tf.keras.optimizers.Adam()

第七部:訓練網路

訓練的過程是我認為範例所有步驟裡比較需要花時間理解的,如果覺得觀看範例還是有點抽象,可以把變數印出來觀察,對於理解或許會有幫助。

前處理

  • total_batch 是在計算在一個 epoch 中需要跑幾次 iteration。
  • 先將 avg_loss 在進行batch 的 iteration 開始前設定成0。
  • 開始進行一次 batch 的訓練,首先先把資料集用先前定義的 function 切分成 batch。(get_gatch(x_train, y_train, batch_size=batch_size))
  • 再來需要將 batch_x, batch_y (ndarray) 轉換成 tensor 的物件才能順利餵進網路中。
  • 並且需要將 batch_y (shape=(100,)) 做 one-hot (shape=(100, 10)) 才會符合網路輸出層的形狀。

餵進神經網路

  • GraidentTape 是一個能夠紀錄神經網路正向傳播的物件,輸入 loss 跟網路參數後,也能夠計算出梯度(gradient)。因為我基本上也是前幾天第一次看到這個東西,所以想了解的人建議可以看看說明文件,或者網路上其他文章
  • 首先我們把 batch_x(100, 28, 28) 作為 input 輸入到 nn_model 中,可以得到 prediction(100, 10) 再把 prediction 跟 batch_y (100, 10) 放入 loss_fn 中,又可以計算出這個 batch 的 loss(一個數值)。
  • 接著透過 tape.gradient,將 loss, weights, bias 數入進去,就可以得到 gradients,這行 code 是在計算 ∂L/∂W, ∂L/∂b(對 loss 和參數做微分)。
  • 透過 optimizer.apply_gradients 可以直接透過反向傳播法(Backpropagation)來更新參數。
  • 最後將 avg_loss 加上這次的 loss / total_batch。
  • 到這邊一個 batch 就跑完了,總共跑了 total_batch 個 iteration 後,1個 epoch 就算完成。

驗證測試資料

  • 在進行了 total_batch 個 iteration 後,我們再來驗證測試資料。
  • 原本的範例僅計算測試資料的 accuracy,但我想模擬 keras 的訓練過程,因此也計算了訓練資料的 accuracy,以及測試資料的 loss。
  • 計算 accuracy 的方式,兩種資料集我都用同樣方式進行。雖然再前面已經訓練過所有的訓練資料,到這個步驟再去預測一次不是好的做法,但目前我先這種方式來計算。
  • train_pred 是將測試資料全部餵進網路後所得到的輸出。
  • 我們透過 tf.argmax 取得每一筆資料在10個種類中最大值的 index(用法跟 np.argmax 相同)。
  • train_max_idxs 是 tf.argax 輸出後的 tensor,因此需要轉換成 ndarray,才能跟 y_train 去比對。
  • 當 a, b 都是形狀相同的 ndarray 時,a==b 會回傳一個相同形狀的 boolean 的矩陣,再用 np.sum 包起來後就可以得到所有 true 的 value。
  • 測試資料也是用相同的方式來驗證。
total_batch = int(len(y_train) / batch_size)
for epoch in range(epochs):
avg_loss = 0
for i in range(total_batch):
batch_x, batch_y = get_batch(x_train, y_train, batch_size=batch_size)
batch_x = tf.Variable(batch_x)
# batch_y.shape = (100,)
batch_y = tf.Variable(batch_y)
batch_y = tf.one_hot(batch_y, 10)
with tf.GradientTape() as tape:
pred = nn_model(batch_x, W1, b1, W2, b2)
loss = loss_fn(outputs=pred, labels=batch_y)
gradients = tape.gradient(loss, [W1, b1, W2, b2])
optimizer.apply_gradients(zip(gradients, [W1, b1, W2, b2]))
avg_loss += loss / total_batch
# validate training data
train_pred = nn_model(x_train, W1, b1, W2, b2)
train_max_idxs = tf.argmax(train_pred, axis=1)
acc = np.sum(train_max_idxs.numpy()==y_train) / len(y_train)
# validate testing data
test_pred = nn_model(x_test, W1, b1, W2, b2)
y_test_one_hot = tf.one_hot(y_test, 10)
test_loss = loss_fn(outputs=test_pred, labels=y_test_one_hot)
max_idxs = tf.argmax(test_pred, axis=1)
test_acc = np.sum(max_idxs.numpy()==y_test) / len(y_test)
print(f"Epoch: {epoch+1}, loss={avg_loss:.3f}, , acc={acc:.3f}, val_loss={test_loss:.3f}, val_acc:{test_acc*100:.3f}")
print("Training complete!")
  • 印出訓練結果
訓練結果

附上完整程式碼

結論

在練習的過程中,感覺其實是更踏實的,因為相較於 Keras 幾行程式碼就寫完建模跟訓練的步驟,這種方式讓人有種踏實感,也感覺是在實現之前課堂上看到的數學公式。

當然 Tensorflow 的優勢當然不只是這樣,因為較 Keras 更底層,因此在運算上也會更快速。而關於客製化的特性,我個人不認為 Keras 就沒辦法客製化神經網路,搭配 Tensorflow 來運用,反而更具有靈活性,但前提肯定是得熟悉兩項工具才有辦法用得靈巧。

若程式碼或觀念有錯誤歡迎告知,也希望這篇文章有幫助到想要入門 Tensorflow 的人!如果喜歡我的筆記,歡迎給個clap或留下留言!

--

--

追客 Fredrick Hong
程式愛好者

畢業後就到在數位廣告業打滾,之前是廣告優化師,目前則是在數據團隊任職。