跳到主要內容

Day 39 模型評估與視覺化

Day 39 模型評估與視覺化

引言

昨天把模型訓練流程架好了,今天要談的是機器學習專案最容易被忽略、卻也最能決定專案成敗的一環——模型評估與視覺化。拿到一個 95% 正確率的模型之後,事情並沒有結束:我們還要回答「這 95% 究竟代表什麼」、「模型在哪些地方出錯」、「出錯的原因是資料還是模型本身」這些問題。

這一篇會從分類指標的計算出發,介紹混淆矩陣(Confusion Matrix)、分類報告(Classification Report)、錯誤樣本分析等技巧。我們會用 Day 37 建立的 SVHN 模型當作範例,把抽象的數字轉換成可以實際行動的洞察。學完今天,你會有一套評估影像分類模型的完整工具箱。對剛接觸機器學習的人來說,這塊往往會被跳過——大家只想趕快看到模型「訓練完」,但真正專業與業餘的差別,常常就在評估這一步的細緻程度。

在測試集上做最終評估

訓練與驗證結束後,模型的真正考試是測試集。測試集在整個訓練過程中都不能拿來調參,否則評估結果會失去意義。下方示範一個標準的測試流程:載入最佳權重、把所有 batch 跑過一次、累計預測結果。

import torch
import numpy as np
from sklearn.metrics import accuracy_score

def evaluate_test(model, test_loader, device):
    model.eval()
    all_preds, all_labels = [], []
    with torch.no_grad():
        for x, y in test_loader:
            x = x.to(device)
            preds = model(x).argmax(dim=1).cpu().numpy()
            all_preds.append(preds)
            all_labels.append(y.numpy())
    preds = np.concatenate(all_preds)
    labels = np.concatenate(all_labels)
    print(f"測試集 Accuracy: {accuracy_score(labels, preds):.4f}")
    return preds, labels

# preds, labels = evaluate_test(model, test_loader, device)

這支函式除了回傳整體正確率,也會把每筆預測結果保留起來,方便後續做更細的分析。注意我們用 model.eval() 關閉 dropout 與 BatchNorm 的訓練模式,並用 torch.no_grad() 關閉梯度計算,這兩個動作都能讓推論更穩定、記憶體更省。

順帶提醒一個小坑:把 batch 一個一個丟進模型時,numpy 的 concatenate 是必要的步驟,因為每個 batch 回傳的是「該批次的預測結果」,要拼起來才會是完整的測試集預測。如果忘記 concatenate,後續呼叫 accuracy_score 時,shape 對不上會直接報錯。另一個常見錯誤是忘了把 GPU 上的 tensor 搬到 CPU(.cpu()),numpy 也沒辦法直接讀 GPU tensor。

分類報告:精確率、召回率、F1-score

在多類別任務裡,單看 Accuracy 很容易被誤導。例如 SVHN 的類別分布大致均勻,Accuracy 大致等於「平均表現」;但若遇到 9 遠多於其他數字的偏斜資料集,模型只要狂猜 9 就能拿到很高的 Accuracy,實際上其他類別幾乎完全沒預測到。

這時就要看「每個類別的精確率(Precision)、召回率(Recall)、F1-score」。三者的意義簡單來說:

  • Precision:模型說是某類的樣本中,真的屬於該類的比例。精確率太低代表「誤報」太多。
  • Recall:所有真的屬於該類的樣本中,模型抓到的比例。召回率太低代表「漏抓」太多。
  • F1-score:Precision 與 Recall 的調和平均數,適合當作綜合指標。

scikit-learn 提供現成的分類報告,只要一行就能看到所有類別的三項指標。

from sklearn.metrics import classification_report

def show_report(labels, preds, class_names=None):
    if class_names is None:
        class_names = [str(i) for i in range(10)]
    print(classification_report(labels, preds, target_names=class_names, digits=4))

# show_report(labels, preds)

執行後會看到類似下表的輸出(示意):

              precision    recall  f1-score   support
           0     0.9750    0.9821    0.9785      2744
           1     0.9812    0.9755    0.9783      5099
           2     0.9680    0.9730    0.9705      4827
...
    accuracy                         0.9683     26032
   macro avg     0.9691    0.9683    0.9687     26032
weighted avg     0.9684    0.9683    0.9683     26032

「macro avg」是每個類別指標的簡單平均,適合評估「模型對每個類別是否一視同仁」;「weighted avg」則用支援數(support)當權重,會偏向資料量大的類別。在類別平衡的 SVHN 上,兩者差異不大;在偏斜資料上,這兩個平均值會差很多,這時 macro avg 通常更值得參考。

另一個在實務上很常見的狀況,是某些類別的 Precision 與 Recall 差距很大。例如某個少見類別 Precision 很高(模型說是某類時幾乎不會錯),但 Recall 很低(真正屬於該類的樣本幾乎都被誤判成別的類別)。這種情況通常意味著模型在「不確定」的時候預設不猜該類。這時候可以考慮用 threshold 調整、class weight、或 focal loss 來強化少數類別。

混淆矩陣:一眼看出誰被誤判成誰

分類報告雖然提供了數字,但很難看出「模型最容易把誰誤判成誰」。這時就用混淆矩陣——把預測類別當作欄、真實類別當作列,每一格 (i, j) 代表「真實是 i 但被預測成 j」的數量。對角線上越多越深色,代表模型越準;非對角線的熱點,則是模型最容易搞混的類別。

import matplotlib.pyplot as plt
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay

def plot_confusion_matrix(labels, preds, class_names=None, normalize=None):
    cm = confusion_matrix(labels, preds, normalize=normalize)
    disp = ConfusionMatrixDisplay(cm, display_labels=class_names)
    fig, ax = plt.subplots(figsize=(8, 8))
    disp.plot(ax=ax, cmap="Blues", colorbar=False, xticks_rotation=0)
    plt.title("Confusion Matrix" if normalize is None else "Normalized Confusion Matrix")
    plt.tight_layout()
    plt.savefig("checkpoints/confusion_matrix.png", dpi=120)
    print("已存到 checkpoints/confusion_matrix.png")

# plot_confusion_matrix(labels, preds, normalize="true")

normalize 參數很實用:設成 "true" 後,每一列會被縮放到 0~1,顯示「真實是 i 的樣本中,有多少比例被預測成 j」,這在類別分布不均勻時特別好用。實務上建議同時存「原始數量」與「列標準化」兩個版本,前者看絕對數量、後者看比例。

錯誤樣本分析:看模型到底錯在哪

指標與矩陣告訴我們「錯了多少」,但還不足以告訴我們「為什麼錯」。直接畫出模型預測錯誤的樣本,往往比看任何指標都更有啟發性。

def show_misclassified(images, labels, preds, n=12):
    mis_idx = np.where(labels != preds)[0]
    if len(mis_idx) == 0:
        print("沒有錯誤樣本!")
        return
    sample_idx = mis_idx[:n]
    fig, axes = plt.subplots(2, n // 2, figsize=(12, 4))
    for ax, i in zip(axes.flat, sample_idx):
        img = images[i].transpose(1, 2, 0)  # (C, H, W) -> (H, W, C)
        img = (img - img.min()) / (img.max() - img.min() + 1e-8)
        ax.imshow(img)
        ax.set_title(f"真:{labels[i]} / 預:{preds[i]}", fontsize=10)
        ax.axis("off")
    plt.tight_layout()
    plt.savefig("checkpoints/misclassified.png", dpi=120)
    print(f"已存 {len(sample_idx)} 個錯誤樣本到 checkpoints/misclassified.png")

# 注意:要把測試集的原始影像(未 Normalize)另外保留
# show_misclassified(test_images, labels, preds)

這個小工具非常實用,常常一畫出來就能看到一些規律:例如 SVHN 容易把「6 跟 8」、「3 跟 9」搞混(連人都很難分辨的低解析度樣本);或模型對某些特殊角度或陰影特別敏感。當你能看出模型失敗的模式,就能回頭設計針對性的資料增強、或調整模型架構。這也是影像分類專案除錯的核心環節。

如果時間充裕,建議把錯誤樣本依「誤判類型」分組統計:例如「3 被誤判成 8」有幾個、「6 被誤判成 0」有幾個。這種統計能幫你快速看到「最該處理的失敗模式」。在資料清理階段,也可以把這些錯誤樣本拿來檢查——很多時候是標註本身就錯了,例如原始資料把「1」標成「7」。當錯誤樣本中有 1% 到 2% 是標註錯誤,這時與其改模型,不如把標籤修一修,成效可能更大。

用 TensorBoard 視覺化訓練過程

除了測試集結果,訓練過程中的 loss 與驗證曲線也值得視覺化。PyTorch 與 TensorBoard 整合得很好,只要裝 tensorboard 套件就能用。

from torch.utils.tensorboard import SummaryWriter

writer = SummaryWriter("runs/svhn_resnet18")

for epoch in range(epochs):
    train_loss = run_one_epoch(model, train_loader, criterion, optimizer, device)
    val_acc = evaluate(model, val_loader, device)

    writer.add_scalar("Loss/train", train_loss, epoch)
    writer.add_scalar("Acc/val", val_acc, epoch)
    for name, param in model.named_parameters():
        writer.add_histogram(name, param, epoch)

writer.close()

# 啟動方式:在終端機輸入 tensorboard --logdir=runs,再用瀏覽器開啟 http://localhost:6006

TensorBoard 不只會畫 loss 與 accuracy 曲線,還能視覺化權重與梯度的分佈、模型運算圖、資料樣本圖等。在做實驗的時候,把每一次 trial 的 log 放在不同子目錄(例如 runs/trial_001/、runs/trial_002/),就能在 TensorBoard 介面上把多組實驗疊在一起比較,這對超參數搜尋特別有幫助。

另一個小提醒:TensorBoard 對深度學習的支援相當完整,但對小型專案來說,matplotlib + pandas 的組合往往更直覺。把訓練指標寫成 CSV,再畫幾張折線圖,一樣能達到「監控訓練過程」的效果。選擇哪個工具,取決於團隊的習慣與專案的規模——大團隊通常會用 TensorBoard 或 MLflow;個人小專案則從 matplotlib 開始就好。

結語

這一篇把模型評估與視覺化從「看數字」推進到「看模式、找原因」。當你能在指標之外進一步分析錯誤樣本、用混淆矩陣找出系統性的失敗模式、再用 TensorBoard 把訓練過程留底,整個專案的成熟度會明顯提升。這些動作也能幫助你在報告中說明「為什麼這個模型可以上線」,而不只是丟一個 95% 的數字。

明天,我們會把這三天的成果整合起來:用 torch.onnx.export 把模型匯出成 ONNX,用 Streamlit 做一個簡單的展示介面,並為整個 40 天系列做一個完整的回顧。

留言

這個網誌中的熱門文章

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