跳到主要內容

Day 38 模型設計與調參

Day 38 模型設計與調參

引言

昨天把專案骨架與資料流都準備好了,今天要進入機器學習專案最讓人興奮、也最容易踩坑的一塊——模型設計與超參數調整(Hyperparameter Tuning)。我們會用 SVHN 影像分類任務做為範例,從挑選 backbone、調整容量、到設計超參數搜尋策略,一步步把模型從「能跑」推向「穩定且有效」。

這一篇的核心觀念是:模型設計不是追求越複雜越好,而是要在「偏差(bias)」與「變異(variance)」之間找到甜蜜點;超參數的選擇則影響訓練的速度與最終成效。掌握一些實務眉角後,你會發現:好模型往往不是設計出來的,是實驗迭代出來的。在學習階段,與其追求一次到位,不如把實驗流程做得紮實——把所有設定記錄下來、把每次結果留下來,時間一久就能看出哪種策略對哪種任務最有效。

挑選模型 backbone

面對 SVHN 這種 32×32 的彩色影像分類,有幾個常見的 backbone 選擇:

  • 自訂小型 CNN:3 到 4 層卷積搭配 BatchNorm 與 Dropout,參數量可控,訓練速度快。
  • ResNet 家族:從 ResNet18 到 ResNet50,殘差連接能緩解梯度消失,深度可以拉得更深。
  • 預訓練模型微調:用 torchvision 的 ResNet18 在 ImageNet 上預訓練的權重做遷移學習(Day 34 介紹過)。

資料量充足的話(SVHN 訓練集 7 萬多張),從頭訓練一個小型 CNN 就能交出不錯的成效。我們這裡選 ResNet18,從頭訓練當 baseline,後續再比較是否值得換成預訓練版本。

建立模型定義

把模型寫成獨立的 src/model.py,讓訓練、評估、部署都能呼叫同一份程式碼。為了能靈活調整容量,這裡把常見超參數寫成函式參數。

import torch
import torch.nn as nn
from torchvision import models

def build_model(arch: str = "resnet18", num_classes: int = 10, pretrained: bool = False):
    if arch == "resnet18":
        model = models.resnet18(weights=None if not pretrained else models.ResNet18_Weights.IMAGENET1K_V1)
        model.fc = nn.Linear(model.fc.in_features, num_classes)
    elif arch == "simple_cnn":
        model = SimpleCNN(num_classes=num_classes)
    else:
        raise ValueError(f"不支援的 arch: {arch}")
    return model

class SimpleCNN(nn.Module):
    """3 層卷積的小型 CNN,適合資料量不大或想快速實驗的場景"""
    def __init__(self, num_classes=10):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 32, 3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True),
            nn.MaxPool2d(2),
            nn.Conv2d(32, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True),
            nn.MaxPool2d(2),
            nn.Conv2d(64, 128, 3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True),
            nn.AdaptiveAvgPool2d(1),
        )
        self.classifier = nn.Sequential(
            nn.Flatten(),
            nn.Dropout(0.3),
            nn.Linear(128, num_classes),
        )

    def forward(self, x):
        return self.classifier(self.features(x))

寫成函式 + 模組化的好處,是之後想換 backbone(例如改成 resnet34 或 efficientnet_b0)只需要在設定檔改一行字串,不用動到訓練程式。SimpleCNN 也提供了「完全自訂」的對照組,當你想確認資料流沒有問題時,可以先用這個小模型快速驗證;如果連這麼小的 CNN 都學不起來,那問題八成出在資料或訓練流程,不會是模型本身的設計。

另一個值得提的眉角是 BatchNorm 的位置。在小型 CNN 裡,BatchNorm 幾乎一定要加在卷積層與激活函式之間,能讓訓練更穩定、收斂更快;但在 LSTM 之類的序列模型裡,BatchNorm 的擺放就比較微妙,常常需要實驗才有最佳解。這也是為什麼我們把 backbone 寫成可換的元件——不同的任務、不同的資料,適合的設計組合常常不一樣。

學習率與訓練演算法

學習率是深度學習裡最關鍵的超參數之一,太大會震盪、太小會卡住。一般實務建議:

  • 先設一個保守值(例如 1e-3)跑幾個 epoch,確認模型有在學習(loss 在下降)。
  • 如果 loss 震盪劇烈或發散,把學習率調小(例如 1e-4)。
  • 如果 loss 下降太慢,調高學習率,或換成 AdamW 等自適應演算法。

對 CNN 來說,Adam 或 AdamW 幾乎是預設選擇;對 LSTM/RNN 則可以試試看 SGD + momentum 或 AdamW。下方示範一個常見的寫法,並把 weight_decay(權重衰減)一起放進去。

import torch.optim as optim

def build_optimizer(model, lr: float = 1e-3, weight_decay: float = 5e-4):
    return optim.AdamW(
        model.parameters(),
        lr=lr,
        weight_decay=weight_decay,
    )

def build_scheduler(optimizer, step_size: int = 10, gamma: float = 0.5):
    return optim.lr_scheduler.StepLR(optimizer, step_size=step_size, gamma=gamma)

加入 lr_scheduler 是另一個實務眉角:訓練後期把學習率降下來,模型比較容易收斂到比較好的局部最佳解。常見策略有 StepLR(每 N 個 epoch 降一次)、CosineAnnealingLR(用餘弦曲線平滑下降)、ReduceLROnPlateau(看到驗證 loss 卡住就降)等。這裡先用 StepLR 當範例,後續實驗可以替換。

對學習率還有更進階的玩法是 warmup(熱身):前幾個 epoch 用很小的學習率,再慢慢升到目標值,最後再下降。這種 cosine schedule with warmup 對大型模型(像是 Transformer)特別有效,但對小型 CNN 來說,加了不一定有感。建議先用簡單的 StepLR 跑一輪,看到有需要再升級。

訓練迴圈與早停(Early Stopping)

訓練時除了存最佳權重,也可以加入早停機制——連續幾個 epoch 驗證指標沒有進步,就提前結束訓練,避免不必要的時間浪費,也避免模型在訓練集上過擬合。

def train(
    model, train_loader, val_loader, *, epochs, lr, weight_decay,
    device, patience=5, ckpt_path="checkpoints/best.pt",
):
    optimizer = build_optimizer(model, lr=lr, weight_decay=weight_decay)
    scheduler = build_scheduler(optimizer)
    criterion = nn.CrossEntropyLoss()

    best_acc = 0.0
    bad_epochs = 0
    for epoch in range(epochs):
        model.train()
        for x, y in train_loader:
            x, y = x.to(device), y.to(device)
            optimizer.zero_grad()
            loss = criterion(model(x), y)
            loss.backward()
            optimizer.step()

        val_acc = evaluate(model, val_loader, device)
        scheduler.step()
        print(f"Epoch {epoch+1}: val_acc={val_acc:.4f}")

        if val_acc > best_acc:
            best_acc = val_acc
            bad_epochs = 0
            torch.save(model.state_dict(), ckpt_path)
        else:
            bad_epochs += 1
            if bad_epochs >= patience:
                print(f"早停:連續 {patience} 個 epoch 沒有進步")
                break
    return best_acc

這個 train 函式把所有需要被調整的選項(epochs、lr、weight_decay、patience)都用 keyword argument 暴露,方便後續用超參數搜尋工具呼叫。bad_epochs 是「已經連續幾次沒進步」的計數器,一旦超過 patience 就停止訓練,這是早停最簡潔的寫法。

另一個實務細節是權重的保留策略:除了「目前最佳」之外,也可以選擇「最後一個 epoch 的權重」或「所有 epoch 的權重(ckpt)儲存」。前者方便部署,後者方便之後繼續訓練或做 ensemble。對個人小專案來說只存「最佳」通常就夠,但如果你想之後拿多個 epoch 的權重做測試時間增強(Test Time Augmentation),就要規劃好儲存方式。

超參數搜尋策略

當手上的超參數選項太多(學習率、weight_decay、batch_size、模型容量…),手動調整會非常沒效率。這時可以用一些搜尋策略:

  • Grid Search:把所有組合列出來,一個個跑。簡單暴力,但很貴。
  • Random Search:隨機抽組合,通常比 Grid Search 更有效率。
  • Bayesian Optimization:根據過去的結果預測下一組比較好的參數(例如 Optuna、Hyperopt)。

對學習階段的專案,建議先用「Random Search + 視覺化驗證曲線」就夠了。先用較小的 epoch 數篩掉明顯不好的組合,再對剩下幾組跑完整訓練。下方示範一個簡單的隨機搜尋腳本:

import random

SEARCH_SPACE = {
    "lr": [1e-3, 5e-4, 1e-4],
    "weight_decay": [1e-4, 5e-4, 1e-3],
    "batch_size": [64, 128],
}

def sample_config():
    return {k: random.choice(v) for k, v in SEARCH_SPACE.items()}

for trial in range(5):
    cfg = sample_config()
    print(f"=== Trial {trial+1}: {cfg} ===")
    train_loader, val_loader, _ = get_dataloaders("data", batch_size=cfg["batch_size"])
    model = build_model("resnet18").to(device)
    acc = train(model, train_loader, val_loader, epochs=5, **cfg, device=device)
    print(f"Trial {trial+1} 最佳正確率:{acc:.4f}")

每一次 trial 會隨機抽一組超參數,跑 5 個 epoch 後看驗證正確率。雖然只是粗篩,但足以找出「明顯有效」的範圍。把這些範圍內的設定,再用更長的 epoch 數跑完整實驗,往往就能拿到一個穩定的模型。

另外,建議把每一組 trial 的設定與結果寫成 JSON 或 CSV 留底,例如 {lr: 1e-3, weight_decay: 5e-4, val_acc: 0.952}。長期下來這些紀錄會變成專案最寶貴的資產——下次換一個類似任務時,可以直接參考過去的結果決定初始超參數,而不是每次都從零開始亂猜。

結語

這一篇從 backbone 的選擇、模型定義、訓練演算法與學習率、訓練迴圈與早停,一路談到超參數搜尋策略。整套流程的關鍵不在於「找到最好的模型」,而是建立一個可重現、可調整的實驗流程。當你能把模型設計與超參數調整的每一個動作寫成程式碼,整個專案的可維護性會大幅提升。

另外也別忘了,每次跑出比上一輪更好的結果時,先把當下的超參數、訓練曲線、評估指標記下來,再決定要不要繼續調。一旦專案累積了十幾輪的實驗記錄,之後不論是要寫報告、重現結果、或交接給同事,都會順暢很多。

明天,我們會進入評估與視覺化:除了正確率之外,還會看混淆矩陣、分類報告、錯誤樣本分析,學會從多個角度判斷模型是否真的可用。

留言

這個網誌中的熱門文章

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