PyTorch 系列:MLP 模型
這是 Deep Learning 系列的第一篇,旨在為大家介紹如何使用 PyTorch 這個強大的框架來建立你自己的深度學習模型。我們將從最基礎的張量(Tensor)概念講起,逐步深入到如何構建一個簡單的多層感知器(Multi-Layer Perceptron, MLP)模型,並了解其背後的運作原理和訓練基礎。
🎯 什麼是張量 (Tensor)?
在 Deep Learning 領域,張量是處理數據的核心單位。你可以把它想像成一個多維度的數據陣列。理解張量對於使用 PyTorch 至關重要,因為所有的輸入、輸出、以及模型參數都是張量。
- 純量 (Scalar):0 維張量,只包含一個數值,例如
5
。 - 向量 (Vector):1 維張量,一串數值,例如
[1, 2, 3]
。在模型中可能代表一個 feature set。 - 矩陣 (Matrix):2 維張量,有行和列的數值表格,例如
[[1, 2], [3, 4]]
。這是最常見的數據形式,例如一個 batch 的 tabular data。 - 更高維度的張量:例如一張彩色圖片可以是
[batch size, channel, height, width]
的 4 維張量。
PyTorch 中的所有操作都圍繞著 torch.Tensor
物件進行,它非常類似 NumPy 的 ndarray
,但額外支援 GPU 加速(大大提升計算效率)和自動微分(autograd
)功能,這對於 Deep Learning 模型的訓練至關重要。
🏗️ PyTorch 的自訂模型:nn.Module
在 PyTorch 中,所有神經網絡模組(例如層、激活函數等)都繼承自 torch.nn.Module
這個基類。要建立你自己的神經網絡模型,你也需要繼承它。nn.Module
是 PyTorch 構建任何複雜神經網絡的基礎。
每個 nn.Module
子類通常會包含兩個重要的方法:
__init__(self, ...)
:- 在這個方法中,你會定義模型的所有「層」(layers)和其他子模組。這些層通常是
torch.nn
模組中提供的預設層,例如nn.Linear
、nn.Conv2d
、nn.BatchNorm2d
等。這些層本身也是nn.Module
的實例。 - 這是模型「藍圖」的定義階段。
- 在這個方法中,你會定義模型的所有「層」(layers)和其他子模組。這些層通常是
forward(self, x)
:- 這個方法定義了數據如何通過你定義的層。
- 它接收一個輸入張量
x
,並執行一系列操作(例如線性變換、激活函數、捲積等)來產生輸出張量。 - 這就是模型進行「前向傳播」(forward pass)的過程。當你調用
model(input_data)
時,實際上就是執行了forward
方法。
🤔 什麼是 MLP 模型?
多層感知器(Multi-Layer Perceptron, MLP) 是最基礎且廣泛應用的神經網絡架構之一。它屬於前饋神經網絡 (Feedforward Neural Network) 的一種,意味著數據從輸入層單向流動到輸出層,中間不包含任何循環或迴路。
MLP 的核心組成部分是:
- 輸入層 (Input Layer):接收原始的特徵數據。
- 一個或多個隱藏層 (Hidden Layer(s)):這些是 MLP 的「大腦」。每個隱藏層由多個神經元(或單元)組成。每個神經元都與前一層的所有神經元連接(這就是為什麼它們也被稱為全連接層 (Fully Connected Layers) 或 密集層 (Dense Layers))。在 PyTorch 中,這些全連接層通常由
nn.Linear
模組實現。 - 輸出層 (Output Layer):產生模型的最終預測結果。輸出的形式取決於任務類型(例如,分類問題輸出每個類別的機率,迴歸問題輸出一個連續值)。
關鍵特性:
- 線性變換:在每個隱藏層和輸出層,數據首先會經過一個線性變換。這與我們稍後會詳細介紹的
nn.Linear
層的功能相同 ()。 - 非線性激活:僅僅堆疊線性層並不能讓模型學習複雜的模式,因為多個線性變換的組合仍然是線性的。因此,在每個隱藏層的線性變換之後,通常會應用一個非線性激活函數 (Activation Function),例如 ReLU。這個非線性是 MLP 能夠學習和表示複雜非線性關係的關鍵。
簡單來說,一個 MLP 就是由多個「線性層 + 激活函數」的組合堆疊而成,最後連接到一個輸出層。透過調整這些層中的權重和偏差,MLP 能夠學習從輸入到輸出的複雜映射關係。
🔗 線性層 (nn.Linear
)
nn.Linear
是神經網絡中最基礎也是最常用的層之一,它實現了線性變換,也是構成 MLP 的基本單元。
數學上,一個線性層的操作可以表示為:
其中:
- 是輸入張量。
- 是輸出張量。
- 是權重矩陣 (weights matrix)。
- 是偏差向量 (bias vector)。
nn.Linear
層會自動為你初始化 和 這些參數,並在模型訓練過程中學習它們的值。這就是為什麼它被稱為「可學習層」(learnable layer)。
例如,一個 nn.Linear(in_features=10, out_features=5)
的層會將一個包含 10 個 feature 的輸入轉換為一個包含 5 個 feature 的輸出。如果你的輸入 x
的形狀是 [Batch_Size, 10]
,那麼 W
的形狀將會是 [5, 10]
,b
的形狀是 [5]
。經過 Wx + b
的矩陣乘法和向量加法後,輸出的 y
的形狀將會是 [Batch_Size, 5]
。
🧠 激活函數 (Activation Functions)
如果神經網絡只由線性層組成,那麼無論有多少層,整個網絡的輸出仍然是輸入的線性組合。這意味著它只能學習線性關係,無法處理複雜的非線性數據模式,這大大限制了模型的表達能力。
激活函數就是用來引入非線性 (non-linearity) 的關鍵。它們在線性層的輸出之後應用,為網絡增加了處理複雜模式的能力,使得神經網絡能夠學習到更複雜的非線性映射。
ReLU (Rectified Linear Unit):
- 最常用也最簡單的激活函數之一。
- 定義為
f(x) = max(0, x)
。 - 它將所有負值設為零,正值保持不變。
- 簡單、計算效率高,且有助於解決早期深度網絡中常見的梯度消失(vanishing gradient)問題。
Softmax:
- 主要用於分類問題的輸出層。
- 它將一個實數向量(通常是線性層的輸出,稱為 logits)轉換為一個機率分佈,使得所有輸出值的總和為 1。
- 例如,如果你有 3 個類別,Softmax 會將
[2.0, 1.0, 0.1]
這樣的 logits 轉換為像[0.7, 0.2, 0.1]
這樣的機率。這使得輸出可以直接解釋為每個類別的預測機率。
📊 MLP 分類模型
多層感知器(MLP)是 Deep Learning 中最基本的網絡架構之一,由多個全連接層(線性層)組成,並在層之間穿插非線性激活函數。現在,讓我們結合前面學到的概念,建立一個簡單的分類模型。
import torch
import torch.nn as nn
import torch.nn.functional as F
# 定義我們自訂的神經網絡模型 (分類器)
class MLPClassifier(nn.Module):
def __init__(self, input_dim, hidden_dim, output_dim):
"""
初始化模型中的所有層。
input_dim: 輸入特徵的數量 (e.g., 數據的 column 數量)
hidden_dim: 隱藏層的單元數量。一個常見的做法是選擇一個比 input_dim 和 output_dim 大的數值。
output_dim: 輸出類別的數量 (e.g., 最終分為 3 類)
"""
super().__init__() # 呼叫父類的 __init__ 方法,這是定義任何 nn.Module 子類的必要步驟
# 定義第一個全連接層 (Linear Layer)
# 將輸入數據從 input_dim 維度映射到 hidden_dim 維度
self.fc1 = nn.Linear(input_dim, hidden_dim)
# 定義激活函數 (ReLU)
# 在第一個線性層的輸出之後應用,引入非線性
self.relu = nn.ReLU()
# 定義第二個全連接層
# 將隱藏層的輸出從 hidden_dim 維度映射到 output_dim 維度
# 這裡的輸出是每個類別的 "logits" (未歸一化的分數)
self.fc2 = nn.Linear(hidden_dim, output_dim)
# 注意:Softmax 通常在 forward 函數中應用,或由損失函數 (例如 nn.CrossEntropyLoss) 自動處理。
# 對於分類任務,如果你的損失函數是 nn.CrossEntropyLoss,那麼模型最後一層**不需要** Softmax。
# 因為 nn.CrossEntropyLoss 內部會自動執行 LogSoftmax + NLLLoss。
def forward(self, x):
"""
定義數據在前向傳播時如何通過模型。
x: 輸入數據張量。通常形狀為 [Batch_Size, input_dim]。
"""
# 數據首先通過第一個全連接層
x = self.fc1(x)
# 然後通過 ReLU 激活函數
x = self.relu(x)
# 最後通過第二個全連接層,得到每個類別的 logits
x = self.fc2(x)
# 返回模型的原始輸出 (logits)。
# 如果需要直接輸出機率分佈 (例如用於推理或可視化),可以在這裡手動應用 Softmax:
# probabilities = F.softmax(x, dim=1)
# return probabilities
return x # 訓練時通常返回 logits
# ----------------- 示範如何使用 MLPClassifier -----------------
# 假設我們的輸入數據有 10 個 features
input_features_cls = 10
# 我們希望模型有一個包含 64 個單元的隱藏層
hidden_units_cls = 64
# 我們希望模型將數據分為 3 個類別
output_classes_cls = 3
# 建立一個 MLPClassifier 模型實例
classifier_model = MLPClassifier(input_features_cls, hidden_units_cls, output_classes_cls)
print("--- 分類 MLP 模型結構 ---")
print(classifier_model)
# 創建一些虛擬輸入數據 (Batch size 為 1, features 為 10)
# 在實際訓練中,Batch size 會是多個樣本,例如 torch.randn(32, input_features_cls)
dummy_input_cls = torch.randn(1, input_features_cls)
print("\n--- 分類模型虛擬輸入數據形狀 ---")
print(dummy_input_cls.shape)
# 將虛擬數據傳入模型進行前向傳播
output_logits_cls = classifier_model(dummy_input_cls)
print("\n--- 分類模型輸出 (logits) 形狀 ---")
print(output_logits_cls.shape)
print("分類模型輸出 (logits):\n", output_logits_cls)
# 如果想看到機率分佈 (通常在模型預測時需要),可以手動應用 Softmax
probabilities_cls = F.softmax(output_logits_cls, dim=1)
print("\n--- 分類模型輸出 (機率分佈) ---")
print(probabilities_cls)
# 檢查機率總和是否為 1
print("機率總和 (應為 1):", probabilities_cls.sum().item())
結構圖:分類 MLP 模型
這個 Mermaid 圖清晰地展示了我們 MLPClassifier
模型的數據流和各層的連接關係:
小貼士
對於分類問題,當使用 nn.CrossEntropyLoss
作為損失函數時,你的模型最後一層不需要包含 Softmax
激活函數。nn.CrossEntropyLoss
會自動處理內部計算 LogSoftmax
和其後的負對數似然損失 (Negative Log Likelihood Loss, NLLLoss)。直接輸出 logits
會更數值穩定。
📈 MLP 迴歸模型
迴歸模型旨在預測一個連續的數值,而非離散的類別。例如,預測房價、股票價格或氣溫等。迴歸模型與分類模型在架構上非常相似,主要區別在於模型的輸出層和損失函數。
import torch
import torch.nn as nn
import torch.nn.functional as F
# 定義我們自訂的神經網絡模型 (迴歸器)
class MLPRegressor(nn.Module):
def __init__(self, input_dim, hidden_dim, output_dim=1): # 迴歸通常輸出一個數值,所以 output_dim 默認為 1
"""
初始化模型中的所有層。
input_dim: 輸入特徵的數量
hidden_dim: 隱藏層的單元數量
output_dim: 輸出值的數量 (對於單一數值迴歸,通常是 1)
"""
super().__init__()
self.fc1 = nn.Linear(input_dim, hidden_dim)
self.relu = nn.ReLU() # 隱藏層依然需要非線性
self.fc2 = nn.Linear(hidden_dim, output_dim) # 輸出層
# 注意:對於迴歸任務,輸出層通常沒有激活函數,或使用如 Sigmoid/Tanh (如果輸出有特定範圍)
# 但最常見的是沒有激活函數,直接輸出原始數值。
def forward(self, x):
"""
定義數據在前向傳播時如何通過模型。
x: 輸入數據張量。通常形狀為 [Batch_Size, input_dim]。
"""
x = self.fc1(x)
x = self.relu(x)
x = self.fc2(x) # 直接輸出數值
return x
# ----------------- 示範如何使用 MLPRegressor -----------------
# 假設我們的輸入數據有 5 個 features
input_features_reg = 5
# 我們希望模型有一個包含 32 個單元的隱藏層
hidden_units_reg = 32
# 迴歸模型通常輸出一個數值 (例如房價),所以 output_dim 為 1
output_values_reg = 1
# 建立一個 MLPRegressor 模型實例
regressor_model = MLPRegressor(input_features_reg, hidden_units_reg, output_values_reg)
print("\n--- 迴歸 MLP 模型結構 ---")
print(regressor_model)
# 創建一些虛擬輸入數據 (Batch size 為 1, features 為 5)
dummy_input_reg = torch.randn(1, input_features_reg)
print("\n--- 迴歸模型虛擬輸入數據形狀 ---")
print(dummy_input_reg.shape)
# 將虛擬數據傳入模型進行前向傳播
predicted_value_reg = regressor_model(dummy_input_reg)
print("\n--- 迴歸模型輸出形狀 ---")
print(predicted_value_reg.shape)
print("迴歸模型預測值:\n", predicted_value_reg)
結構圖:迴歸 MLP 模型
迴歸 MLP 的結構與分類 MLP 相似,但輸出層不同:
小貼士
迴歸模型的輸出層通常不使用激活函數(例如 Softmax 或 Sigmoid),因為我們希望直接輸出任意範圍的數值。如果你的預測值有特定範圍(例如必須在 0 到 1 之間),你可以在輸出層之後添加 nn.Sigmoid()
;如果預測值必須是正數,可以考慮 nn.ReLU()
或 F.softplus()
。
⚙️ 模型訓練基礎
構建模型只是第一步,真正的魔術發生在訓練 (Training) 過程中。訓練的目標是讓模型學會如何從輸入數據中提取有用的模式,以便做出準確的預測。具體來說,就是學習模型內部所有權重 (weights) 和偏差 (biases) 的最佳數值。
隨機初始化與迭代優化:
- 在訓練開始時,模型的權重和偏差通常是隨機初始化的。這意味著模型一開始的預測是完全隨機且不準確的。
- 訓練是一個迭代 (iterative) 的過程,在每個迭代 (通常稱為一個 epoch 或 step) 中,模型會根據輸入數據和當前的權重進行預測(forward pass),然後與真實值比較並計算誤差,接著調整權重以減少預測誤差。這個過程會不斷重複精煉 (refine) 權重值,使其越來越接近「正確」的值。
梯度下降 (Gradient Descent):
- 這是優化神經網絡最核心的演算法。
- 想像一下你在一個山谷裡,目標是找到最低點(這代表損失函數的最小值)。梯度下降會告訴你每次應該往哪個方向(坡度最陡峭的反方向)走一小步,直到走到最低點。
- 這個「坡度」就是損失函數對模型中每個可學習參數(權重和偏差)的梯度 (gradient)。梯度指出了損失函數增長最快的方向。因此,我們朝梯度的反方向移動,就能讓損失下降。
損失函數 (Loss Function / Cost Function):
- 損失函數用於衡量模型預測結果與真實值之間的差異。差異越大,損失值越高。
- 模型的目標就是最小化這個損失值。
- 均方誤差 (Mean Squared Error, MSE):
- 常用於迴歸 (Regression) 問題。
- 計算預測值與真實值之差的平方平均值。
- PyTorch 中對應的損失函數是
nn.MSELoss()
。
- 交叉熵損失 (Cross Entropy Loss):
- 常用於分類 (Classification) 問題,特別是多類別分類。
- 它衡量兩個機率分佈之間的差異。
- PyTorch 中對應的損失函數是
nn.CrossEntropyLoss()
。如前所述,它會自動處理輸入logits
的LogSoftmax
轉換。
優化器 (Optimizer):
- 優化器負責根據損失函數計算出的梯度來更新模型的權重。可以把它想像成負責「如何下山」的策略。
- PyTorch 提供了多種優化器,例如最基礎的
torch.optim.SGD
(Stochastic Gradient Descent) 和目前非常流行且效果普遍較好的torch.optim.Adam
。 - 在訓練迴圈中,你通常會執行以下步驟:
optimizer.zero_grad()
:清空之前計算的梯度。這是因為梯度會累積,如果不清空,新的梯度會疊加到舊的梯度上。loss.backward()
:執行反向傳播 (Backpropagation)。這是 PyTorchautograd
引擎的核心功能,它會自動計算當前批次數據所有權重和偏差相對於損失函數的梯度。optimizer.step()
:根據loss.backward()
計算出的梯度和優化器的演算法,更新模型的權重和偏差。這就是權重被「精煉」的步驟,使其朝著損失更小的方向移動。
每次迭代都會根據梯度微調權重,逐步讓模型學習到數據的底層模式,以降低損失函數的值。這個過程會一直重複,直到模型性能不再提升或達到預設的訓練次數。通過不斷地調整權重,模型從一個隨機的狀態逐漸學習到如何精確地映射輸入到輸出,這正是深度學習的精髓。
🚀 簡單訓練迴圈範例 (Training Loop)
現在我們已經定義了模型,接下來讓我們看一個非常基礎的訓練迴圈,了解模型是如何學習的。我們將使用前面定義的 MLPClassifier
。這個範例展示了分類模型的訓練。對於迴歸模型,主要的改變會是損失函數(例如 nn.MSELoss()
)和目標數據的格式。
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
class MLPClassifier(nn.Module):
def __init__(self, input_dim, hidden_dim, output_dim):
super().__init__()
self.fc1 = nn.Linear(input_dim, hidden_dim)
self.relu = nn.ReLU()
self.fc2 = nn.Linear(hidden_dim, output_dim)
def forward(self, x):
x = self.fc1(x)
x = self.relu(x)
x = self.fc2(x)
return x
# --- 1. 準備數據 (Dummy Data) ---
# 假設輸入數據有 10 個 features
input_features = 10
# 模型有一個包含 64 個單元的隱藏層
hidden_units = 64
# 模型將數據分為 3 個類別
output_classes = 3
# 批次大小 (Batch Size)
batch_size = 16
# 訓練樣本數量
num_samples = 100
# 創建虛擬輸入數據 X (100 個樣本, 每個樣本 10 個 features)
X_train = torch.randn(num_samples, input_features)
# 創建虛擬目標標籤 y (100 個樣本, 每個樣本的類別是 0, 1, 或 2)
# nn.CrossEntropyLoss 期望的目標標籤是 LongTensor 類型,且不應是 one-hot 編碼
y_train = torch.randint(0, output_classes, (num_samples,))
# --- 2. 實例化模型、損失函數和優化器 ---
# 建立模型
# (確保 MLPClassifier 類別已定義)
# model = MLPClassifier(input_features, hidden_units, output_classes)
# 為了使範例可直接運行,我們在這裡重新使用前面定義的 classifier_model 參數
model = MLPClassifier(input_features_cls, hidden_units_cls, output_classes_cls)
# 定義損失函數 (CrossEntropyLoss 適用於多類別分類)
criterion = nn.CrossEntropyLoss()
# 定義優化器 (Adam 是一個常用的選擇)
# model.parameters() 會告訴優化器哪些張量是需要優化的 (即可學習的權重和偏差)
optimizer = optim.Adam(model.parameters(), lr=0.001) # lr 是學習率 (learning rate)
# --- 3. 訓練迴圈 ---
num_epochs = 50 # 訓練的總輪次
print("\n--- 開始訓練分類模型 ---")
model.train() # 將模型設置為訓練模式
for epoch in range(num_epochs):
# 模擬將數據分成批次 (mini-batches)
# 在真實場景中,你會使用 DataLoader 來處理數據批次化和打亂
permutation = torch.randperm(num_samples) # 打亂數據順序
for i in range(0, num_samples, batch_size):
indices = permutation[i:i+batch_size]
inputs = X_train[indices]
labels = y_train[indices]
# 1. 清空梯度 (Zero gradients)
# 在每個批次的梯度計算之前,必須清除先前計算的梯度
optimizer.zero_grad()
# 2. 前向傳播 (Forward pass)
# 將輸入數據傳入模型,得到預測的 logits
outputs = model(inputs)
# 3. 計算損失 (Compute loss)
# 比較模型的預測 (outputs) 和真實標籤 (labels)
loss = criterion(outputs, labels)
# 4. 反向傳播 (Backward pass)
# 計算損失相對於模型所有可學習參數的梯度
loss.backward()
# 5. 更新權重 (Update weights)
# 優化器根據計算出的梯度來調整模型的權重
optimizer.step()
# 每個 epoch 結束後打印一次損失值
if (epoch + 1) % 10 == 0:
print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')
print("--- 訓練完成 ---")
# --- 4. 進行預測 (Inference/Prediction) ---
# 訓練完成後,可以使用模型進行預測
# 創建一些新的虛擬測試數據
X_test = torch.randn(5, input_features_cls) # 5 個測試樣本
# 將模型設置為評估模式 (evaluation mode)
# 這會關閉 Dropout 和 BatchNorm 等在訓練和評估時行為不同的層
model.eval()
# 在評估模式下,我們不需要計算梯度
with torch.no_grad(): # 禁用梯度計算,可以節省內存並加速計算
test_outputs = model(X_test)
# 輸出的 test_outputs 是 logits,可以通過 Softmax 轉換為機率
probabilities = F.softmax(test_outputs, dim=1)
# torch.max 返回 (values, indices),我們需要 indices 作為預測的類別
_, predicted_classes = torch.max(probabilities, 1)
print("\n--- 測試數據預測 ---")
print("測試輸入數據形狀:", X_test.shape)
print("模型輸出 (logits):\n", test_outputs)
print("模型輸出 (機率):\n", probabilities)
print("預測類別:\n", predicted_classes)
📝 總結
到目前為止,你已經了解了 PyTorch 的基本構建塊以及如何利用它們來構建簡單的 MLP 模型:
- 張量 (Tensor) 作為數據表示的基礎。
nn.Module
作為自訂模型的神經網絡藍圖。nn.Linear
層的線性變換原理及其背後的矩陣運算。- 激活函數(如 ReLU 和 Softmax) 引入非線性,賦予模型學習複雜模式的能力。
- 如何建立一個基於線性層和激活函數的分類 MLP 模型。
- 如何建立一個基於線性層和激活函數的迴歸 MLP 模型。
- 以及模型訓練的基本概念,包括梯度下降、損失函數(MSE 和交叉熵)、優化器以及
backward()
函數在計算梯度和更新權重中的作用。 - 一個簡單的訓練迴圈範例,展示了模型如何從數據中學習。
這只是深度學習和 PyTorch 的入門。接下來,你可以探索更複雜的模型架構、不同的層類型、更高級的優化技巧以及如何處理真實世界的數據集。
📚 參考資料 (References)
- PyTorch 官方文件 (Official Documentation):
- torch.Tensor - 張量的詳細文檔。
- torch.nn - 神經網絡模組 (包括
nn.Module
,nn.Linear
, 激活函數, 損失函數)。 - torch.optim - 優化器模組 (例如
Adam
,SGD
)。 - Autograd: Automatic Differentiation -
autograd
引擎的運作原理。
- PyTorch 官方教學 (Official Tutorials):
- What is PyTorch? - PyTorch 基礎入門,包括張量操作。
- Autograd: Automatic Differentiation -
autograd
實例教學。 - Neural Networks - 如何定義神經網絡、計算損失及更新權重。
- Learning PyTorch with Examples - 使用 PyTorch 實現不同神經網絡模型的範例。