跳到主要內容

Day 29 建立第一個神經網路

Day 29 建立第一個神經網路

引言

前兩天我們理解了 autograd 怎麼算梯度,也學會了用 Dataset 與 DataLoader 把資料送進模型。不過目前為止,模型這端還是空白的:沒有「可學習的參數」、沒有「從輸入走到輸出的運算流程」。今天要把這塊補上,帶大家用 PyTorch 的 torch.nn 模組搭出第一個神經網路。

PyTorch 把神經網路抽象成 nn.Module 這個類別。只要繼承它,定義好每一層(layer)與 forward(),模型就自動擁有兩件重要的事:一是所有可學習參數的清單,可以交給最佳化器更新;二是呼叫模型就像呼叫函式一樣直覺,輸入張量、得到預測。今天會分三個小節,從最基礎的類別骨架開始,逐步加入全連接層、激活函式,並用一個實際可執行的範例示範前向傳播。

nn.Module:把模型寫成類別

PyTorch 的模型都繼承自 torch.nn.Module。最基本的寫法是:在建構式(__init__)裡宣告每一層,在 forward() 裡定義資料如何流過這些層。完成後只要寫 model(x),就會自動呼叫 forward() 並回傳預測值。

我們先寫一個最陽春的版本:一個輸入層直接接到輸出層,沒有任何隱藏層或激活函式。

import torch
import torch.nn as nn

class LinearModel(nn.Module):
    def __init__(self):
        super().__init__()
        # 一個全連接層:輸入 4 維、輸出 2 維
        self.fc = nn.Linear(in_features=4, out_features=2)

    def forward(self, x):
        return self.fc(x)

device = "cuda" if torch.cuda.is_available() else "cpu"
model = LinearModel().to(device)
print(model)

# 輸出:
# LinearModel(
#   (fc): Linear(in_features=4, out_features=2, bias=True)
# )

這段程式有三個關鍵點:super().__init__() 必須呼叫,否則內部的參數註冊機制不會啟動;self.fc 是 nn.Linear 的實例,模組會自動把它內部的權重與偏差註冊成可學習參數;.to(device) 把整個模型搬到指定裝置,之後輸入張量也要在同一個裝置上才能運算。

想知道模型有哪些參數,可以用 parameters() 或 named_parameters()。

for name, param in model.named_parameters():
    print(name, param.shape)

# 輸出:
# fc.weight torch.Size([2, 4])
# fc.bias   torch.Size([2])

fc.weight 的形狀是 [輸出數, 輸入數],符合線性變換的習慣:y = x · Wᵀ + b。這個清單稍後會交給 torch.optim 裡的最佳化器,由它負責在每一步訓練中更新這些參數。

nn.Linear 與激活函式

只靠一層線性變換,無論疊幾層,輸出都是輸入的線性組合,這樣的模型沒辦法表達真實世界中複雜的非線性關係。要讓網路「變聰明」,必須在每一層之間插入激活函式(activation function),把線性結果再經過一次非線性映射。最常見的選擇是 ReLU。

class MLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(in_features=4, out_features=8)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(in_features=8, out_features=2)

    def forward(self, x):
        x = self.fc1(x)
        x = self.relu(x)
        x = self.fc2(x)
        return x

這個小型 MLP(Multi-Layer Perceptron,多層感知器)有兩個全連接層,中間夾了一個 ReLU:先把 4 維的輸入升維到 8 維,通過 ReLU 後再壓回 2 維。ReLU 的公式是 f(x) = max(0, x),把所有負數歸零、正數保留;它計算簡單、收斂穩定,是深度學習裡最常用的激活函式之一。

其他常見的激活函式還包括 nn.Sigmoid(把值壓到 0 到 1 之間,常用於二元分類的輸出層)、nn.Tanh(把值壓到 −1 到 1 之間)、nn.LeakyReLU(讓負數保留一個小斜率,避免神經元「死亡」)。實務上,隱藏層先用 ReLU 通常是安全的起點。

用前向傳播產生預測

模型定義好之後,送進資料就能得到預測值。我們用隨機產生的 3 筆輸入示範,並把中間層的數值印出來,幫助理解資料在網路中怎麼流動。

import torch

device = "cuda" if torch.cuda.is_available() else "cpu"

class MLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(in_features=4, out_features=8)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(in_features=8, out_features=3)

    def forward(self, x):
        x = self.fc1(x)
        x = self.relu(x)
        x = self.fc2(x)
        return x

model = MLP().to(device)

# 3 筆輸入、每筆 4 個特徵
x = torch.randn(3, 4).to(device)
y = model(x)

print("輸入形狀:", x.shape)   # torch.Size([3, 4])
print("輸出形狀:", y.shape)   # torch.Size([3, 3])
print(y)

輸出形狀的變化是 [3, 4] → [3, 8] → [3, 3],第一個維度維持「批次大小」不變,後面的維度隨層的設定改變。這個規則在之後的 CNN、RNN 都一樣:批次軸永遠在最前面,這也是為什麼 DataLoader 預設會沿著第一個維度堆疊資料。

預測出來的數值還沒經過 softmax 之類的轉換,純粹是線性層的輸出;如果要做多類別分類,後面會再接 nn.CrossEntropyLoss,它會在內部自動套用 softmax,免去自己額外處理的麻煩(細節會在 Day 30 提到)。

用 GPU 跑一次前向傳播

前面介紹過 device = "cuda" if torch.cuda.is_available() else "cpu" 這行,這是 PyTorch 在 2024 年最常見的裝置切換寫法。我們可以把這個模型實際搬到 GPU 上跑一次,確認 tensor 與 model 都在同一個裝置上。

import torch
import torch.nn as nn

device = "cuda" if torch.cuda.is_available() else "cpu"

class MLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(4, 16)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(16, 3)

    def forward(self, x):
        return self.fc2(self.relu(self.fc1(x)))

model = MLP().to(device)
x = torch.randn(2, 4, device=device)
y = model(x)

print(next(model.parameters()).device)   # 輸出:cuda:0 或 cpu
print(y.device)                          # 與模型參數相同

注意:nn.Module 不會自動把輸入張量搬到正確的裝置,所以輸入的 x 必須明確地放到 device 上。常見的寫法是 x = x.to(device),在 Day 28 的 DataLoader 迴圈裡會大量出現。如果忘了搬,會出現 RuntimeError: Expected all tensors to be on the same device 這種錯誤訊息,這也是初學者常見的 bug 之一。

參數初始化的小知識

PyTorch 的 nn.Linear 在建立時會自動用 Kaiming Uniform 初始化權重(適合搭配 ReLU),這對大多數任務來說已經是合理的起點。如果想自訂初始化方式,可以在 __init__ 之後用 nn.init 模組覆寫。

import torch.nn.init as init

linear = nn.Linear(8, 16)
init.xavier_uniform_(linear.weight)   # 改用 Xavier 初始化
init.zeros_(linear.bias)              # 偏差通常初始化為 0

不過在大多數情況下,使用 PyTorch 的預設初始化就夠了,等到遇到「loss 一直是 NaN」或「模型完全不收斂」這類特殊狀況,再回頭檢查初始化即可。Day 38 會更深入討論模型設計與超參數調整。

用 nn.Sequential 快速拼模型

如果模型的層與層之間只是「依序串接」,PyTorch 提供更簡潔的寫法:nn.Sequential。把每一層依序放進它的建構式,forward() 就會自動依序執行,不需另外定義。這種寫法適合用於結構單純的分類器或特徵萃取器;但碰到需要分支、殘差連接等複雜結構時,仍要回到 nn.Module 自己寫 forward()。

import torch.nn as nn

seq_model = nn.Sequential(
    nn.Linear(4, 16),
    nn.ReLU(),
    nn.Linear(16, 8),
    nn.ReLU(),
    nn.Linear(8, 3),
)

x = torch.randn(2, 4)
print(seq_model(x).shape)   # torch.Size([2, 3])

可以看到 nn.Sequential 版本的程式碼更短、更直觀;但少了 named_parameters() 容易閱讀的層級名稱(例如 fc1.weight vs 2.weight),debug 時稍微不便。實務上可以混搭:外層用 nn.Module 管理整個模型,內部某些重複區塊用 nn.Sequential 簡化寫法,兼顧可讀性與彈性。

結語

今天我們把模型這一端補齊了。nn.Module 提供統一的骨架,讓我們能把每一層宣告在 __init__ 裡、把資料流向寫在 forward() 裡;nn.Linear 負責線性變換,nn.ReLU 之類的激活函式則讓網路有能力學會非線性關係。三層的 MLP 雖然簡單,卻已經具備深度學習模型的關鍵元素:可學習參數、前向傳播、與 .to(device) 的裝置切換。

明天,我們會把資料、模型、損失函式、最佳化器四個角色湊齊,寫出第一個能實際「學習」的訓練迴圈。會看到損失從一開始的高數字一步步收斂,模型的預測也跟著穩定下來。

留言

這個網誌中的熱門文章

Day 2 變數與資料型別

Day 2 變數與資料型別 引言 寫程式的過程中,變數與資料型別是處理資料的基礎。變數是存放資料的容器,資料型別則決定這筆資料有哪些特性、可以進行哪些操作。學會定義變數、認識各種資料型別,是學好 Python 的關鍵一步。 這篇文章會帶你了解 Python 中變數的觀念、如何定義變數,以及常見的資料型別,包括整數、浮點數、字串、布林值,還有串列、元組、字典與集合等容器型別。我們也會介紹變數的命名規則與撰寫風格建議,以及如何用 type() 檢查資料型別。 什麼是變數?如何在 Python 中定義變數 變數是在程式執行時用來存放資料的名稱。透過定義變數,我們可以給一筆資料一個名字,並在程式的其他地方用這個名字取用該筆資料。在 Python 中,變數不需要事先宣告型別,因為 Python 是動態型別語言,變數的型別由指定給它的值決定。 定義變數的基本語法 在 Python 中定義變數非常簡單,只要用賦值符號 = 把值指定給變數即可。例如: x = 5 # 定義變數 x,並把整數 5 賦值給它 name = "Alice" # 定義變數 name,並把字串 "Alice" 賦值給它 在這裡,x 是一個變數,被賦予整數 5;name 是另一個變數,被賦予字串 "Alice"。 變數的更新與覆寫 變數的值可以修改,也就是說,我們可以在程式的不同地方給同一個變數新的值。例如: x = 10 # x 最初被賦予 10 x = 15 # x 的值現在被更新為 15 這樣就能依照需求,在程式執行過程中靈活調整變數的值。 Python 的動態型別系統 Python 和某些靜態型別語言不同,定義變數時不需要宣告型別。賦值時,Python 會根據值自動判斷變數的型別。例如: x = 5 # x 是整數 x = 3.14 # x 變成浮點數 x = "Hi" # x 變成字串 同一個變數在程式執行過程中可以存放不同型別的值,這是 Python 的彈性之一。 常見資料型別 在 Python 中,資料型別決定我們可以對變數進行哪些操作...

Day 1 Python 簡介與環境設定

Day 1 Python 簡介與環境設定 引言 在現在的科技環境裡,程式設計已經是一項重要技能。無論你是對資料科學有興趣、想成為開發者,或是想踏入人工智慧(AI)領域,學會寫程式都能明顯提升你的競爭力。在眾多程式語言中,Python 因為語法簡單、功能強大、應用範圍廣泛,成為許多人進入程式世界的第一選擇。這篇文章會帶你認識 Python 的背景與優勢,並一步步教你在不同系統上安裝與設定 Python 開發環境,最後寫出第一支 Python 程式。 為什麼選擇 Python? Python 是一種高階程式語言,由 Guido van Rossum 在 1991 年發布。Python 的設計哲學強調程式碼的可讀性,並用縮排來定義程式區塊,這點和許多使用大括號的語言不同。簡潔的語法讓它成為初學者的理想選擇;就算是經驗豐富的開發者,也能用它完成複雜的專案。 Python 的優勢如下: 簡單易學 :Python 的語法清楚、結構簡潔,初學者很快就能上手。和其他語言相比,學習曲線相對平緩,不需要先弄懂一堆複雜觀念,就能開始寫程式。 應用範圍廣泛 :從資料科學、網頁開發、人工智慧、機器學習、自動化測試到網路爬蟲,Python 都有大量開源函式庫與工具支援,而且在這些領域都扮演關鍵角色。 豐富的函式庫與框架 :Python 的函式庫生態系非常龐大。做資料分析有 NumPy、Pandas;開發網站有 Django、Flask;做深度學習有 TensorFlow、PyTorch。各種需求幾乎都能找到對應的套件,讓開發更有效率。 跨平台支援 :Python 支援 Windows、macOS、Linux 等作業系統,程式通常不需要太多修改就能跨平台執行,讓開發與部署更有彈性。 活躍的社群 :Python 擁有龐大的開發者社群。學習或開發上遇到問題,幾乎都能在社群與論壇(例如 Stack Overflow)找到答案,對初學者來說是很強的後盾,也能減少卡關時的挫折感。 Python 的應用領域 Python 的流行與強大功能,讓許多領域都開始大量使用它。以下是幾個常見的應用方向: 資料科學 :隨著大數據與人工智慧興起,資料科學大量使用 Python。NumPy、Pandas 與 Matplotlib 等工具能處理和分析龐...

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門 隨著人工智慧(AI)與深度學習(Deep Learning)快速發展,越來越多人對這些技術產生興趣。不論你是想踏入 AI 領域的初學者,還是已經有程式基礎的開發者,學好 Python 與深度學習框架(例如 PyTorch),都能為你打開更多可能。 為什麼選擇 Python? Python 已經是資料科學與人工智慧領域的首選語言。它的語法簡潔、容易上手,而且擁有龐大的生態系與大量開源函式庫。無論是資料處理、資料視覺化,還是建立機器學習與深度學習模型,Python 都能勝任。對想進入 AI 或資料科學領域的人來說,它幾乎是必備工具。 PyTorch 是什麼? PyTorch 是由 Meta(原 Facebook)AI 研究團隊開發的開源深度學習框架,以易用、靈活和動態計算圖著稱,是許多 AI 研究人員與開發者的首選。相較於其他框架,PyTorch 的寫法更貼近原生 Python,對初學者相對友善。無論是簡單的實驗,還是複雜的深度學習模型,PyTorch 都能提供強大的支援。 這個系列能帶給你什麼? 這個系列會從 Python 的基礎開始,帶你一步一步學習,最後能自己用 PyTorch 建立深度學習模型。即使你完全沒有寫過程式,也能跟著文章的節奏累積技能,理解 AI 與深度學習的核心觀念。 本系列涵蓋的主題 Python 基礎:從變數、條件判斷到函式與模組。 資料處理工具:用 NumPy 與 Pandas 有效率地操作資料。 資料視覺化:用 Matplotlib 與 Seaborn 把資料畫成圖表。 深度學習的數學基礎:線性代數、微積分與機率。 PyTorch 入門:理解張量、模型建構與 GPU 加速。 基礎深度學習模型:CNN 與 RNN 的實作應用。 深度學習專案實戰:從資料前處理到模型部署的端到端流程。 誰適合這個系列? 程式初學者 :如果你對 AI 充滿好奇,卻還沒寫過程式,系列的第一部分會帶你快速上手 Python,並幫助你理解深度學習的基本觀念。 資料科學愛好者 :如果你已經熟悉一些資料處理方法,進階部分會教你如何用 PyTorch 建構深度學習模型。 開發者與研究人員 :想更深入了...