昨天我們證明了單層感知器的完整數學推導,而在今天我們將把這些理論知識轉換成對應的程式碼。這個過程在學習深度學習技術時是至關重要的一步,因為我們今天所做的事情是所有深度學習程式的基本概念,當你理解這些內容後,你在去接觸到其他深度學習的函式庫時,就能更好地理解它們的原理和運作方式。
我們知道邏輯閘是一種二元分類的元件,因此非常適合用來作為單層感知器的資料型態。因此今天在撰寫程式碼時,我們將使用AND邏輯閘作為範例,示範如何建立一個單層感知器並進行模型的訓練,現在讓我們來看看建立模型的步驟
在建立模型時,通常會將會變動的參數放在 __init__
方法中,並在這個步驟中進行基本的隨機初始化,這樣子做的好處是當模型訓練完畢後其參數的變化將會被保存在這個類別中使我們能夠快速地讀取與儲存。
因此在這一步驟中,我會先在這裡宣告偏移量及對應的權重,同時由於我們在更新權重時會使用梯度下降法,這還需要一個學習率參數,因此我也會在 __init__
中設定它。
import numpy as np
class Perceptron:
def __init__(self, input_shape, bias=0, learning_rate=0.1):
# 初始化權重
self.weights = np.random.randn(input_shape)
# 初始化偏移量
self.bias = bias
# 初始化學習率
self.learning_rate = learning_rate
在這個步驟中,由於我們的權重會根據輸入資料的數量而有所變化,因此我們需要傳入一個input_shape
參數,以幫助模型建立正確的資料維度。在深度學習的模型中,如果資料計算的維度錯誤,就會導致模型發生Shape Error的問題。這一點是在剛學習深度學習程式時最容易遇到的錯誤。
而在深度學習的第一步中,就是要定義它的前向傳播方式。這個過程也就是昨日提到的 wx+b
這個數學公式,並搭配階梯函數來轉換其類別,因此對於該方法的定義如下所示:
def forward(self, x):
# 前向傳播公式 wx+b
z = np.dot(x, self.weights) + self.bias
# 階梯函數轉換結果
y_hat = self.step_function(z)
return y_hat
def step_function(self, z):
return (z >= 0).astype(int)
在這裡要注意的是,我們使用的程式碼採用矩陣相乘的方式,因此需要調用 np.dot
來進行運算。此外在階梯函數的部分,我們設定了一個條件式 z >= 0
。該條件式會將符合條件的結果轉換為 True
,不符合的則為 False
。而這正好符合我們需要的 0 與 1 類別,不過我們需要將其轉換成 int 型態即可。
在這一步驟中由於我們已經將損失函數和反向傳播證明簡化完畢,因此不需要先計算損失值的部分,而是直接依照昨日公式定義的反向傳播方法,同時進行參數優化。不過我們在這邊還是選擇保留損失函數的計算,因為可以通過該損失值來判斷模型當前的訓練效果。
def backward(self, x, y, y_hat):
# 計算梯度
grad = (y - y_hat)
# 優化器更新參數
self.weights += self.learning_rate * grad * x
self.bias += self.learning_rate * grad
def loss_function(self, y_hat, y):
# MSE計算損失值
return 0.5 * (y - y_hat) ** 2
最後當我們訓練完模型後,需要一個方法來調用訓練好的參數。此時我們可以簡單地用前向傳播方式進行包裝。
def predict(self, x):
# 預測時直接調用訓練好的前向傳播函數
return self.forward(x)
如此一來我們就完成模型的建立了。
模型的訓練方式非常簡單,而且通常不會有過多的變化。在這裡我們通常會以一個週期(Epoch)
為單位。在每個週期中,我們會將資料集拆分成多個批次(Batch)
。這樣做的原因是因為計算時需要適當的記憶體空間或GPU空間,如果一次給予過大的資料,就會導致記憶體不足(Out of memory, OOM)
的問題。在這裡我們也可以調用模型的損失函數來查看每個週期的損失值變化。
def training(model, x_train, y_train, epochs=10):
for epoch in range(epochs): # 週期
total_loss = 0 # 紀錄每個周期的損失值
for x, y in zip(x_train, y_train): # 批量
y_hat = model.forward(x)
loss = model.loss_function(y_hat, y)
total_loss += loss
model.backward(x, y, y_hat)
print(f'Epoch {epoch}, Loss: {total_loss:.5f}')
print('訓練完畢!')
首先,我們需要為模型提供訓練數據。在這裡,我們的目標是模擬 AND 閘的邏輯,因此我們需要 AND 閘的輸入和對應的輸出結果。這裡我們以兩個輸入作為範例,當然你也可以使用更多個輸入並相對應地調整標籤與輸入。
x_train = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y_train_AND = np.array([0, 0, 0, 1])
接下來我們需要建立一個單層感知器模型,在這裡我們使用剛剛建立Perceptron
並輸入對應的input_shape
即可完成模型的建立,在這裡我們可以使用x_train.shape[1]
取的其資料的第二軸維度,以此獲取輸入資料的大小
x_train
的資料維度是(4, 2)
,分別對應(batch_size, input_shape)
,這也代表我們的輸入資料有4筆,每筆包含2個特徵。
# 建立單層感知器模型
model = Perceptron(input_shape=x_train.shape[1], learning_rate=0.1)
最後我們只需要調用訓練用的函數 training
,就能更完整地實現前向傳播與反向傳播來更新這些可變動的參數了。在這裡我們要注意 epochs
的次數,若設定得太少則會導致訓練不完全,設定得太多則會導致過度擬合(Overfitting)
的問題。不過在這裡,由於訓練資料比較簡單且只有訓練資料,因此不必考量過度擬合的問題,所以我們可以將 epochs
的次數設定高一些。
# 訓練模型
training(model, x_train, y_train_AND, epochs=20)
最後我們將使用訓練好的權重來預測這些邏輯閘的效果。在這裡我們可以看到其預測的類別已經能完美地呈現AND邏輯閘的功能。當然我們也可以重新調整參數或更換成不同的邏輯閘,來觀察模型的訓練效果。我非常建議大家對這部分進行調整與實驗,這樣你更能理解這些超參數的實際用意。
# 測試模型預測結果
print("\n測試訓練模型:")
for x in x_train:
print(f'輸入: {x}, 預測輸出: {model.predict(x)}')
#-----輸出-----
輸入: [0, 0], 預測輸出: 0
輸入: [0, 1], 預測輸出: 0
輸入: [1, 0], 預測輸出: 0
輸入: [1, 1], 預測輸出: 1
現在你學會了如何使用單層感知器來模擬不同的邏輯閘,並在這過程中理解了感知器的基本原理與訓練過程。透過實作前向與反向傳播、梯度下降優化,以及訓練過程的方式,你在基礎上就對深度學習有了深刻的印象。這樣在後續的章節中,你將能夠更好地銜接不同的模型與數學公式。