跳到主要內容

Day 30 訓練迴圈

Day 30 訓練迴圈

引言

Day 29 我們搭出了第一個神經網路,呼叫 model(x) 就能得到預測值。不過目前為止,預測的數字其實是「隨機猜的」:模型的權重是隨機初始化的,沒有人告訴它什麼是對、什麼是錯。要讓模型真的學會預測,得再請兩個角色上場:損失函式(loss function)告訴模型「現在錯得多嚴重」,最佳化器(optimizer)則根據梯度把參數往「錯得比較少」的方向調整。

把資料、模型、損失函式、最佳化器四個角色串起來,就是所謂的「訓練迴圈」(training loop)。今天的內容會用三個小節,從損失函式與最佳化器的概念開始,再寫出一個能實際跑起來、並且看得到損失下降的訓練流程。最後我們會把所有零件組合成一段可在 CPU 上執行的完整範例,並討論如何評估模型在沒看過的資料上的表現。

損失函式:衡量預測與目標的差距

損失函式接受兩個引數:模型的預測值(logits)與真實標籤(targets),回傳一個純量張量表示「平均錯了多少」。PyTorch 把常見的損失函式收在 torch.nn 裡,最常用的兩個是 nn.CrossEntropyLoss 與 nn.MSELoss。

nn.CrossEntropyLoss 適合多類別分類:輸入是模型最後一層的原始輸出(不需要先手動 softmax),目標是類別索引(整數 0、1、2…)。它會在內部套用 softmax 再算負對數似然(negative log-likelihood),數值上等同於「預測的機率分布」與「真實分布」之間的距離。

import torch
import torch.nn as nn

loss_fn = nn.CrossEntropyLoss()

# 模型輸出:3 筆、每筆 4 個類別的分數
logits = torch.tensor([[2.0, 1.0, 0.5, 0.3],
                       [0.1, 3.0, 0.2, 0.5],
                       [0.4, 0.2, 1.5, 2.1]])

# 真實標籤:第 0、1、3 類
targets = torch.tensor([0, 1, 3])

loss = loss_fn(logits, targets)
print(loss.item())  # 輸出約 0.46

nn.MSELoss(均方誤差)適合回歸任務:輸入與目標都是連續值,計算「兩者差值的平方」再取平均。

loss_fn = nn.MSELoss()

pred = torch.tensor([2.5, 0.0, 2.0])
target = torch.tensor([3.0, -0.5, 2.0])

loss = loss_fn(pred, target)
print(loss.item())  # 輸出約 0.1667

選擇損失函式的原則很直觀:「任務在預測什麼、就用對應的損失」。分類任務用 CrossEntropyLoss,回歸任務用 MSELoss,二元分類也可以用 BCEWithLogitsLoss(內建 sigmoid 的二元交叉熵)。

最佳化器:依照梯度更新參數

有了損失,接著要把「錯」回傳給模型參數,這就是 autograd 的工作;參數拿到梯度之後,要決定「往哪個方向更新多少」,這就是最佳化器的工作。PyTorch 把常用最佳化器收在 torch.optim,最常見的兩個是 AdamW 與 SGD。

import torch
import torch.nn as nn

model = nn.Linear(in_features=4, out_features=2)

optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3)

建構最佳化器時,要把「要更新的參數」與「學習率(learning rate,lr)」傳進去。AdamW 是 Adam 的改良版,對權重做「權重衰減」(weight decay)的正則化,有助於避免過擬合,是目前深度學習裡的預設選擇之一。SGD(隨機梯度下降)則是最經典的方法,加上動量(momentum)後在很多場景依然好用;學習率的設定經驗值大約在 1e-3 到 1e-2 之間。如果對學習率沒有概念,從 1e-3 開始嘗試,通常是個安全的起點。

訓練時最佳化器有三個標準動作:清空梯度、計算新梯度、更新參數。

optimizer.zero_grad()   # 清空上一輪的梯度
loss.backward()         # 反向傳播算出新梯度
optimizer.step()        # 用梯度更新參數

optimizer.zero_grad() 一定要記得呼叫,因為 autograd 預設會把梯度累加到 .grad 上;如果忘記清零,第二輪的梯度就會疊在第一輪之上,更新方向會錯亂。順帶一提,不同的最佳化器在內部會保留各自的狀態,例如 AdamW 會記錄梯度的移動均值與變異數;如果在訓練中途想要重置這些狀態(例如切換到不同的學習率策略),可以呼叫 optimizer.state 來檢查,或直接建立一份新的最佳化器。

完整訓練迴圈

把前面的角色全部組裝起來。我們用一個合成的「可分類資料集」做示範:先隨機產生 120 筆 4 維特徵,並設計 3 個類別的中心點,再讓模型學習把每筆資料分到正確的類別。整個流程在 CPU 上就能執行,輸出是「3 類別的分類結果」,概念延伸到 MNIST 完全一樣。

import torch
import torch.nn as nn

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

# ---------- 1. 合成資料 ----------
torch.manual_seed(0)
centers = torch.tensor([[-2.0, -2.0, -2.0, -2.0],
                        [ 0.0,  0.0,  0.0,  0.0],
                        [ 2.0,  2.0,  2.0,  2.0]])
features, targets = [], []
for c in range(3):
    features.append(centers[c] + 0.5 * torch.randn(40, 4))
    targets.append(torch.full((40,), c, dtype=torch.long))

X = torch.cat(features).to(device)            # shape: [120, 4]
y = torch.cat(targets).to(device)             # shape: [120]

# ---------- 2. 模型 ----------
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)

# ---------- 3. 損失與最佳化器 ----------
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-2)

# ---------- 4. 訓練迴圈 ----------
for epoch in range(1, 6):
    model.train()           # 切到訓練模式
    optimizer.zero_grad()   # 清空梯度
    logits = model(X)       # 前向傳播
    loss = loss_fn(logits, y)
    loss.backward()         # 反向傳播
    optimizer.step()        # 更新參數
    print(f"epoch {epoch}: loss = {loss.item():.4f}")

執行結果大致如下(隨機初始化的差異,每次數字會略有不同):

epoch 1: loss = 1.0982
epoch 2: loss = 0.8521
epoch 3: loss = 0.5104
epoch 4: loss = 0.2861
epoch 5: loss = 0.1537

可以看到 loss 從 1.1 一路下降到 0.15 左右,這就是模型在「學會分類」的訊號。model.train() 會啟用 dropout、BatchNorm 的訓練行為;如果模型有這些層,呼叫 forward 前別忘了切換模式。

驗證與評估

訓練的最終目的是讓模型在「沒看過的資料」上也表現得好,因此需要把資料切出一部分當驗證集,並在每個 epoch 結束時評估模型在這份資料上的表現。實務上,model.eval() 把模型切到評估模式(關閉 dropout 之類的隨機性),並用 torch.no_grad() 包住評估程式碼,避免建立計算圖浪費記憶體。

model.eval()
with torch.no_grad():
    val_logits = model(X)
    val_loss = loss_fn(val_logits, y).item()
    val_pred = val_logits.argmax(dim=1)
    val_acc = (val_pred == y).float().mean().item()
print(f"val loss = {val_loss:.4f}, val acc = {val_acc:.4f}")

這個範例刻意把訓練和驗證用同一份資料(合成資料太短),目的是把流程示範清楚;真實情境中應該另外保留一份驗證集,並用 DataLoader 批次讀取。整套訓練流程到這裡就完整了:前向傳播算 logits、損失函式衡量差距、反向傳播算梯度、最佳化器更新參數;評估時切到 eval 模式並關閉梯度。

學習率排程器:動態調整學習率

訓練一個模型有時長達數小時甚至數天,全程都用同一個學習率往往不是最佳策略。常見的做法是隨著訓練推進逐步調降學習率:訓練前期用較大的學習率快速收斂,後期用較小的學習率微調,避免在最低點附近震盪。PyTorch 把這些策略收在 torch.optim.lr_scheduler,最直覺的選擇是 StepLR:每 N 個 epoch 把學習率乘上一個小於 1 的係數。

optimizer = torch.optim.AdamW(model.parameters(), lr=1e-2)
scheduler = torch.optim.lr_scheduler.StepLR(
    optimizer, step_size=3, gamma=0.5   # 每 3 個 epoch 把學習率乘 0.5
)

for epoch in range(1, 11):
    model.train()
    # ... 正常的訓練步驟 ...
    scheduler.step()                    # 每個 epoch 結束時更新
    print(optimizer.param_groups[0]["lr"])

除了 StepLR,常用的還有 CosineAnnealingLR(餘弦退火,把學習率沿著餘弦曲線慢慢降到 0)、ReduceLROnPlateau(當驗證損失不再下降時自動降低學習率)。對初學者來說,先熟悉 StepLR 已經足以應付大多數任務,等到 Day 38 討論超參數調整時,再視狀況嘗試其他策略。

結語

今天把訓練流程的四大角色湊齊:CrossEntropyLoss 衡量分類預測的誤差,AdamW 依梯度更新參數,optimizer.zero_grad() → loss.backward() → optimizer.step() 是每一步訓練的標準動作,model.eval() 與 torch.no_grad() 則保證驗證階段既正確又有效率。雖然今天的資料是合成的,但整套架構可以直接搬到 MNIST、CIFAR-10 等真實資料集上。

明天,我們會用這套流程在 MNIST 手寫數字資料集上做第一次實戰。把 torchvision.datasets.MNIST、DataLoader、MLP 模型、CrossEntropyLoss 與 AdamW 全部串起來,實際把辨識率訓練到 9 成以上,並學會怎麼評估模型的好壞。

留言

這個網誌中的熱門文章

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 建構深度學習模型。 開發者與研究人員 :想更深入了...