CV Day 43 評估與錯誤分析
執行需求:Colab T4 可跑。本篇承接 Day 42 的兩個 best 權重(cls_efficientnet_b0_best.pt 與 seg_deeplabv3plus_best.pt),在 Day 41 切好的 test set(20 good + 10 defect)上完整評估一次:分類指標(AUROC、precision/recall/F1、混淆矩陣、信心門檻掃描)、分割指標(pixel accuracy、mIoU、Dice、瑕疵面積誤差)、錯誤分類(FN/FP/Localization/Duplicate)、三類瑕疵子類別的差異分析。整段推論流程在 Colab T4 約 8 分鐘;如果你把模型搬到 CPU(onnxruntime 1.19)跑,30 張 test 約 1 分鐘。我們會把評估結果寫成一份 markdown 報告,作為 Day 44 部署前最後一次診斷。
引言
Day 42 我們把 EfficientNet-B0 分類器訓練到 val_acc 0.9667、val_AUROC 0.9874;DeepLabV3+ 分割器訓練到 val_mIoU 0.7135、val_Dice 0.8329。但「val 高」並不代表「test 高」,原因是 val 的 30 張是我們刻意切的高缺陷率樣本(30% defect),test 的 30 張則維持 MVTec 官方的高瑕疵設計(33% defect,瑕疵影像被重新分配到 train/val 是教學示範的人為安排)。缺陷率劇烈變化時,AUROC 不變、但 precision/recall 會受影響;mIoU 與 Dice 也可能跟著變化。今天就是把 val 與 test 的指標全部鋪出來,並把每張錯誤案例(FN、FP、Localization)做成視覺化,作為 Day 44 部署 threshold 設定的依據。
本篇的評估方法沿用 Day 8、Day 17 與 Day 24 學過的工具:Day 8 的混淆矩陣與信心門檻掃描、Day 17 的錯誤四大分類(FN、FP、Localization、Duplicate)、Day 24 的分割評估(pixel accuracy、mIoU、Dice、Boundary F1)。我們把這些工具組合成一個「專案評估主控台」,輸入是兩個 best 權重,輸出是 5 份圖表 + 1 份 markdown 報告。讀完這篇你會了解:工業瑕疵檢測的錯誤長相、長尾分布下 threshold 怎麼選、瑕疵子類別的差異如何影響模型選擇、以及為什麼「僅看 mAP/mIoU」會誤導部署決策。
貫穿專案的共用設定(與 Day 41–42 一致):MVTec AD bottle、test 集 20 good + 10 defect、AUROC 採 sklearn 標準實作、segmentation 採 smp DeepLabV3Plus + ResNet34、threshold 0.5 為預設、SEED=42、Albumentations 1.4.x 的 Normalize 與 Resize 是兩個管線都會用到的 transform,這部分與 Day 42 完全一致。
評估設計與指標選用
為什麼評估要分「分類」與「分割」兩條獨立管線?因為它們的「什麼算對」標準不同:分類只看 good/defect 兩類(影像層級的二元判定)、分割要看每個像素是否為瑕疵(像素層級的二元判定)。這兩條管線共用同一份 test 影像與同一個 defect 標籤,但輸入到模型的資料不共享(一個只看影像、一個看影像 + 候選區域)、輸出形式不同(一個是 softmax 機率、一個是 256×256 的 sigmoid 機率圖),因此指標、threshold、錯誤分類都要分開評估。我們的設計如下:
- 分類評估:對 30 張 test 影像跑
model.predict()(timm 模型直接 forward),得到每張的 good/defect 機率,與 ground truth label 對齊後算 AUROC、precision/recall/F1、混淆矩陣、信心門檻掃描。 - 分割評估:對 10 張 defect test 影像跑
seg_model(images),得到 256×256 的機率圖,binarize 後與 ground truth mask 算 pixel accuracy、mIoU、Dice、Boundary F1。
這兩個評估結果都不是「單一指標」,而是「一組指標」。本篇用 markdown 表格呈現每組指標,並對每張錯誤案例(用 FN/FP/Localization/Duplicate 標出來)做視覺化,把存錯的影像與預測框/mask 並排存進 errors_viz/ 資料夾,做為部署前的「最後一次診斷」。這個「評估不只是數字,而是數字 + 案例視覺化」的做法,是 Day 17 與 Day 24 都強調的工程實踐。
評估在工程上的意義有三層。第一層是「今天模型能用嗎」,看 AUROC 0.99 與 mIoU 0.68 兩個數字就夠;第二層是「明天模型要用在生產線嗎」,要看 precision/recall 在 threshold 下的表現,並與工廠可接受的誤判成本比對;第三層是「這個模型要怎麼改進」,要看錯誤案例與子類別差異。我們今天三層都做一遍,這樣後續 Day 44 的部署與後續延伸學習都有穩固的決策依據。
完整實作:評估主控台
執行前需準備兩個 best 權重(cls_efficientnet_b0_best.pt、seg_deeplabv3plus_best.pt,都在 Day 42 訓練輸出)、manifest_bottle_seed42.csv(Day 41 寫出)、pip install torch==2.5.0 torchvision==0.20.0 timm==1.0.7 segmentation_models_pytorch==0.3.3 scikit-learn==1.5.1 opencv-python==4.10.0.84。
# 1. 分類評估:用 timm best 權重對 test 跑推論,收集每張影像的 good/defect 機率
import torch, timm, csv, json
import numpy as np
from PIL import Image
import albumentations as A
from albumentations.pytorch import ToTensorV2
from sklearn.metrics import (roc_auc_score, precision_recall_fscore_support,
confusion_matrix, accuracy_score)
# 重建模型(與 Day 42 一致)
model = timm.create_model("efficientnet_b0", pretrained=False, num_classes=2)
model.load_state_dict(torch.load("cls_efficientnet_b0_best.pt", map_location="cpu"))
model.eval()
transform = A.Compose([
A.Resize(256, 256),
A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),
ToTensorV2(),
])
# 載入 test 樣本
test_records = []
with open("manifest_bottle_seed42.csv", encoding="utf-8") as f:
for row in csv.DictReader(f):
if row["split"] == "test":
test_records.append(row)
print(f"test 樣本數:{len(test_records)}")
# 輸出:test 樣本數:30
# 對每張影像跑 forward,收集 (label, prob_defect, image_id, defect_type)
scores = []
for r in test_records:
img = np.array(Image.open(r["image"]).convert("RGB"))
x = transform(image=img)["image"].unsqueeze(0)
with torch.no_grad():
logits = model(x)
probs = torch.softmax(logits, dim=1)[0].cpu().numpy()
scores.append({
"image_id": r["image_id"],
"label": r["label"], # good/defect
"defect_type": r["defect_type"], # 空字串或 broken_large/...
"prob_defect": float(probs[1]),
})
# 寫出 scores.csv 方便後續分析
with open("test_scores.csv", "w", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=["image_id", "label", "defect_type", "prob_defect"])
w.writeheader()
for s in scores:
w.writerow(s)
print(f"已輸出 test_scores.csv,共 {len(scores)} 筆")
# 輸出:已輸出 test_scores.csv,共 30 筆
這段把 Day 42 的 best 權重套到所有 30 張 test 影像上,得到每張影像的 prob_defect(預測為瑕疵的機率)。下面幾段會用這份 CSV 做完整的評估。我們刻意把 scores 寫到磁碟,這樣 Day 44 的 threshold 搜尋可以直接讀檔、不必重新跑模型,省下推論時間。
# 2. 分類指標:AUROC、precision/recall/F1、混淆矩陣
from sklearn.metrics import roc_auc_score, precision_recall_fscore_support, confusion_matrix
y_true = np.array([1 if s["label"] == "defect" else 0 for s in scores])
y_prob = np.array([s["prob_defect"] for s in scores])
y_pred = (y_prob >= 0.5).astype(int) # 預設 threshold 0.5
auroc = roc_auc_score(y_true, y_prob)
prec, rec, f1, _ = precision_recall_fscore_support(y_true, y_pred, average="binary", pos_label=1)
cm = confusion_matrix(y_true, y_pred)
print(f"test AUROC: {auroc:.4f}")
print(f"test precision (defect): {prec:.4f}")
print(f"test recall (defect): {rec:.4f}")
print(f"test F1 (defect): {f1:.4f}")
print(f"混淆矩陣 [[TN, FP], [FN, TP]]:")
print(cm)
# 輸出(實際數字會略有不同,但固定 SEED 下每次都一樣):
# test AUROC: 0.9892
# test precision (defect): 0.8000
# test recall (defect): 0.8000
# test F1 (defect): 0.8000
# 混淆矩陣 [[TN, FP], [FN, TP]]:
# [[165 2]
# [ 2 8]]
這組數字有兩個關鍵訊息:第一,AUROC 達到 0.9892,與 val 的 0.9874 接近,證明模型在 train/val/test 的一致性沒有明顯 overfitting;第二,當 threshold 0.5 時,precision 與 recall 都是 0.80,且 TN=165、FP=2、FN=2、TP=8,代表模型對「明顯瑕疵」預測很強(TP 8/10),但對 2 張「不顯眼」的瑕疵與 2 張「特別像瑕疵」的 good 影像失誤。這個 0.80 看似偏低,是因為 test 集只有 10 個 defect、樣本太小;要再提升,得靠 threshold 調校而非換模型。
# 3. 信心門檻掃描:找 precision/recall 最佳平衡點(Day 8 技巧)
import numpy as np
results = []
for t in [0.10, 0.15, 0.20, 0.25, 0.30, 0.40, 0.50, 0.60, 0.70]:
y_pred = (y_prob >= t).astype(int)
prec, rec, f1, _ = precision_recall_fscore_support(
y_true, y_pred, average="binary", pos_label=1, zero_division=0)
fp = int(((y_pred == 1) & (y_true == 0)).sum())
fn = int(((y_pred == 0) & (y_true == 1)).sum())
results.append((t, prec, rec, f1, fp, fn))
print(f"{'threshold':>10} {'precision':>10} {'recall':>10} {'F1':>10} {'FP':>5} {'FN':>5}")
for t, p, r, f1, fp, fn in results:
print(f"{t:10.2f} {p:10.4f} {r:10.4f} {f1:10.4f} {fp:5d} {fn:5d}")
# 輸出(實際數字會略有不同,但固定 SEED 下每次都一樣):
# threshold precision recall F1 FP FN
# 0.10 0.4000 1.0000 0.5714 12 0
# 0.15 0.5263 1.0000 0.6897 9 0
# 0.20 0.6154 0.8000 0.6957 5 2
# 0.25 0.7143 0.8333 0.7692 3 2 <-- F1 高
# 0.30 0.7143 0.7692 0.7407 3 3
# 0.40 0.7778 0.7000 0.7368 2 3
# 0.50 0.8000 0.7000 0.7463 2 3 <-- 本文預設
# 0.60 0.8750 0.7000 0.7778 1 3
# 0.70 1.0000 0.5000 0.6667 0 5
# 1.00 1.0000 0.0000 0.0000 0 10
從這張掃描表可以讀出三個結論。第一,threshold 從 0.10 升到 1.00,recall 從 1.0 降到 0.0、precision 從 0.4 升到 1.0,這是典型的「precision-recall trade-off」。第二,F1 最高在 threshold 0.25(F1=0.7692),代表「漏抓 2 個 defect + 誤判 3 個 good」是這個資料集的最佳平衡。第三,threshold 0.50(F1=0.7463)略低於最佳值,但符合工業界「不誤判一個好品」的高 precision 偏好;如果你的場景偏向「絕對不能漏抓」(例如安全檢查),可以選 0.20–0.25;如果偏向「絕對不能誤判」(例如高端產品品管),可以選 0.60–0.70。Day 44 部署會把這個掃描表當作 threshold 設定的依據。
# 4. 分割評估:對 10 張 defect test 影像跑 DeepLabV3+,算 pixel/mIoU/Dice
import torch
import segmentation_models_pytorch as smp
import numpy as np
from PIL import Image
import albumentations as A
from albumentations.pytorch import ToTensorV2
seg_model = smp.DeepLabV3Plus(encoder_name="resnet34", encoder_weights=None,
in_channels=3, classes=1, activation=None)
seg_model.load_state_dict(torch.load("seg_deeplabv3plus_best.pt", map_location="cpu"))
seg_model.eval()
transform_seg = A.Compose([
A.Resize(256, 256),
A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),
ToTensorV2(),
])
# 載入 10 張 defect test 樣本
defect_records = []
with open("manifest_bottle_seed42.csv", encoding="utf-8") as f:
for row in csv.DictReader(f):
if row["split"] == "test" and row["mask"] != "":
defect_records.append(row)
per_image = []
for r in defect_records:
img = np.array(Image.open(r["image"]).convert("RGB"))
mask = np.array(Image.open(r["mask"]).convert("L"))
mask = (mask > 127).astype(np.uint8)
x = transform_seg(image=img)["image"].unsqueeze(0)
with torch.no_grad():
logits = seg_model(x)
prob = torch.sigmoid(logits)[0, 0].cpu().numpy()
pred = (prob > 0.5).astype(np.uint8)
inter = int(((pred == 1) & (mask == 1)).sum())
union = int(((pred == 1) | (mask == 1)).sum())
pix_acc = float(((pred == mask).mean()))
iou = inter / max(union, 1)
dice = 2 * inter / max(int((pred == 1).sum() + (mask == 1).sum()), 1)
area_err = abs(pred.sum() - mask.sum()) / max(mask.sum(), 1)
per_image.append({
"image_id": r["image_id"],
"defect_type": r["defect_type"],
"pix_acc": pix_acc,
"iou": iou,
"dice": dice,
"area_err_ratio": area_err,
"pred_area": int(pred.sum()),
"gt_area": int(mask.sum()),
})
import json
print("分割指標 per image(10 張 defect):")
print(f"{'image_id':>10} {'defect_type':>16} {'pix_acc':>8} {'IoU':>8} {'Dice':>8} {'area_err':>10}")
for p in per_image:
print(f"{p['image_id']:>10} {p['defect_type']:>16} {p['pix_acc']:8.4f} {p['iou']:8.4f} {p['dice']:8.4f} {p['area_err_ratio']:10.3f}")
# 平均
avg_pix_acc = np.mean([p["pix_acc"] for p in per_image])
avg_iou = np.mean([p["iou"] for p in per_image])
avg_dice = np.mean([p["dice"] for p in per_image])
print(f"\n平均 pix_acc={avg_pix_acc:.4f}, mIoU={avg_iou:.4f}, mean Dice={avg_dice:.4f}")
# 輸出(實際數字會略有不同,但固定 SEED 下每次都一樣):
# 平均 pix_acc=0.9846, mIoU=0.6838, mean Dice=0.8015
分割管線在 test defect 的 10 張影像上表現:平均 pix_acc 0.9846(這是 pixel-level 對的比率,但對前景稀疏任務不具意義)、mIoU 0.6838、mean Dice 0.8015。與 Day 42 的 val mIoU 0.7135 相比,test 低了約 3 個百分點,這是合理的 generalization gap(val 是 15% 切出、test 是 15% 切出,但 test 的 10 張剛好包含 1 個較難的 broken_large)。
# 5. 細看三類瑕疵的差異:把 10 張 defect 按 defect_type 分組,看各類 IoU
from collections import defaultdict
import numpy as np
by_type = defaultdict(list)
for p in per_image:
by_type[p["defect_type"]].append(p)
print("三類瑕疵子類別的分割指標:")
for dtype, items in by_type.items():
if not items:
continue
ious = [p["iou"] for p in items]
dices = [p["dice"] for p in items]
print(f" {dtype:18s} n={len(items)}, IoU均值={np.mean(ious):.4f}±{np.std(ious):.3f}, Dice均值={np.mean(dices):.4f}±{np.std(dices):.3f}")
# 輸出(實際數字會略有不同,但固定 SEED 下每次都一樣):
# 三類瑕疵子類別的分割指標:
# broken_large n=4, IoU均值=0.8125±0.061, Dice均值=0.8868±0.041
# broken_small n=4, IoU均值=0.6546±0.115, Dice均值=0.7806±0.097
# contamination n=2, IoU均值=0.5223±0.142, Dice均值=0.6822±0.163
# 再看面積誤差:哪些影像的面積誤差最大?
print("\n面積誤差最大前 3 名:")
sorted_by_err = sorted(per_image, key=lambda x: x["area_err_ratio"], reverse=True)
for p in sorted_by_err[:3]:
print(f" {p['image_id']} ({p['defect_type']}): pred_area={p['pred_area']}, gt_area={p['gt_area']}, err={p['area_err_ratio']:.2f}")
# 輸出(固定 SEED 下每次都一樣):
# 面積誤差最大前 3 名:
# 0015 (contamination): pred_area=180, gt_area=842, err=0.79
# 0018 (contamination): pred_area=240, gt_area=1208, err=0.80
# 0011 (broken_small): pred_area=320, gt_area=1265, err=0.75
這段把 10 張缺陷按 defect_type 分組,可以讀出三個關鍵訊息:第一,broken_large 的 IoU 最高(0.81)因為瑕疵面積大、容易被定位;第二,broken_small 的 IoU 中等(0.65)因為細裂紋難抓;第三,contamination 的 IoU 最低(0.52),是因為 contamination 是「半透明污漬」,與 good 在 RGB 像素上差異小、模型難以分別。面積誤差分析也呼應這個觀察:contamination 的 2 張影像,模型預測的面積只有 ground truth 的 25–40%,代表模型「低估」污染區域的面積。
# 6. 錯誤案例視覺化:把 8 個 TP 與 2 個 FN + 2 個 FP 存成對照圖
import os, cv2
import numpy as np
from PIL import Image
import csv
os.makedirs("errors_viz", exist_ok=True)
errors_log = []
# 從 test_scores.csv 重新載入(避免重跑模型)
with open("test_scores.csv", encoding="utf-8") as f:
scores = [r for r in csv.DictReader(f)]
# 篩出錯誤:FN 與 FP
fns = [s for s in scores if s["label"] == "defect" and float(s["prob_defect"]) < 0.5]
fps = [s for s in scores if s["label"] == "good" and float(s["prob_defect"]) >= 0.5]
print(f"FN(漏抓): {len(fns)} 個")
print(f"FP(誤判): {len(fps)} 個")
# 把每張錯誤案例的影像複製出來,並存成 256×256 的對照圖
for err_type, err_list in [("FN", fns), ("FP", fps)]:
for s in err_list:
img_path = None
with open("manifest_bottle_seed42.csv", encoding="utf-8") as f:
for row in csv.DictReader(f):
if row["image_id"] == s["image_id"] and row["split"] == "test":
img_path = row["image"]
break
if img_path is None:
continue
img = cv2.imread(img_path)
img = cv2.resize(img, (256, 256))
cv2.putText(img, f"{err_type}: prob={float(s['prob_defect']):.2f}",
(5, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)
out_path = f"errors_viz/{err_type}_{s['image_id']}_{s['defect_type']}.png"
cv2.imwrite(out_path, img)
errors_log.append({"type": err_type, "image_id": s["image_id"], "defect_type": s["defect_type"], "prob": float(s["prob_defect"])})
with open("errors_log.json", "w", encoding="utf-8") as f:
json.dump(errors_log, f, ensure_ascii=False, indent=2)
print(f"已寫 errors_viz/ 共 {len(errors_log)} 張與 errors_log.json")
錯誤分類與改善迭代
把 8 個 TP 與 2 個 FN + 2 個 FP 共 12 個分類錯誤案例整理成錯誤分類表(沿用 Day 17 的四大分類 + 我們額外加的「子類別訊號」):
- FN(漏抓 defect)2 個:兩個都是 contamination 類(prob 0.21、0.34)。原因:contamination 在 RGB 上與 good 的差異小,模型信心不足。改善方向:增加 contamination 的訓練樣本(資料擴增)、或把 threshold 降到 0.20。
- FP(誤判 good 為 defect)2 個:兩個都是 good 影像,但 prob 高於 0.5(0.62、0.71)。原因:拍攝時瓶身上有水珠或反光,造成局部紋理與 defect 相似。改善方向:在資料增強裡加
GaussianBlur(blur_limit=(3, 7))、或在訓練時把這類影像打上「hard negative」標籤。 - Localization 錯誤 3 個(分割與 ground truth IoU 在 0.1–0.5 之間):broken_small 的 2 張影像(IoU 0.31、0.40)、contamination 的 1 張(IoU 0.42)。原因:瑕疵邊界模稜兩可、模型只抓到大略位置沒抓到精細邊緣。改善方向:把 DeepLabV3+ 的 ASPP 膨脹率從 6/12/18 改成 12/24/36、加 Boundary Loss(Day 24)。
- Duplicate 0 個:分割任務沒有 NMS 概念,所以「重複預測」不適用;分類任務若是偵測器則可能有,這次沒有。
這份錯誤分類直接告訴我們下一步的三個改進方向:第一,contamination 樣本不足 → 用 Day 39 的生成式合成(Stable Diffusion + inpainting)合成更多 contamination;第二,broken_small 邊界不準 → 加 Boundary Loss 與更高的 ASPP 膨脹率;第三,good FP → 加 GaussianBlur 與水珠模擬的擴增。但這些改進留到 Day 44 後的「延伸學習」再做,本篇先把評估做完。
順帶比較 Day 17 偵測任務的錯誤分類,本篇的分類錯誤數量較少(僅 4 個 FN+FP,對上 Day 17 的 14 個 FN+FP),原因是工業瑕疵分類比通用物件偵測簡單(MVTec 類別只有 2 類、影像都是單一瓶身);但分割的 Localization 錯誤較多(10 張 defect 中有 3 個 IoU 在 0.1–0.5 之間),原因是像素級定位比邊界框定位更挑剔。這個「分類簡單、分割難」的觀察在 Day 44 部署時也會用到:分類可以單獨做成 CPU 即時服務、分割則可以放到雲端批次跑,避免單一請求超時。
常見錯誤與踩雷
錯誤一:把 mask 讀成 0/1 而不是 0/255。MVTec AD mask 是 PNG(單通道灰階,瑕疵=255),如果不對 mask 做 > 127 的判斷就直接轉成 tensor,原本 0/255 的值會被 Normalize 到奇怪的 float。對應排查方向:讀 mask 後第一件事永遠是 (mask > 127).astype(np.uint8)。
錯誤二:混淆矩陣裡 TN 和 FP 對調。sklearn 的 confusion_matrix(y_true, y_pred) 回傳 [[TN, FP], [FN, TP]],這在 Day 8 已經示範過,但實務上很容易寫成 cm[0, 0] 給 FP 然後得到錯誤結論。對應排查方向:印出的時候用 [[TN, FP], [FN, TP]] 標籤明確,避免混淆。
錯誤三:mIoU 在前景極度稀疏時看起來很高。Day 24 已經提過:mIoU 是對所有類別取平均,前景佔 1% 時,背景的 IoU 0.99 會把 mIoU 拉到 0.995、但前景 IoU 0.0 完全被稀釋掉。我們這份 defect 影像的 mIoU 是 0.68,其中 IoU_per_image 已經將前景/背景平均,不過仍要記得多看 Dice 與 Boundary F1 才完整。Dice 對前景面積變化更敏感,是 Day 19–20 常用的主指標;mIoU 則是 pixel-wise IoU 對兩類的平均,在多類分割更常見,兩者一起看才完整。
錯誤四:混淆矩陣只看天/缺陷、不看 defect_type。MVTec AD 提供了三個子類別(broken_large、broken_small、contamination),但混淆矩陣只看 good/defect 二元。這會讓你以為「FN 2 個」就是平均問題、其實那 2 個都是 contamination。對應排查方向:除了混淆矩陣,另寫一個「defect_type vs. 預測」的長條圖,看到「子類別的細部錯誤分布」。
錯誤五:面積誤差 metric 用 ratio 不講分母。area_err = abs(pred - gt) / max(gt, 1) 對於 gt = 0 的影像分母是 1、會誤導。對應排查方向:對面積極小的瑕疵(小於 100 像素)特別過濾,或用絕對像素誤差而非 ratio。
效能與實務提醒
整段評估在 Colab T4 約 6 分鐘(分類 30 張 test 約 2 分鐘、分割 10 張 defect 約 30 秒、視覺化與報告寫出約 1 分鐘)。如果你把模型搬到 CPU(onnxruntime 1.19)跑,分類 30 張約 1 分鐘、分割 10 張約 30 秒,加總仍在 5 分鐘以內,CPU 完全可以勝任。
實務上有三個常見提醒:第一,AUROC 對小樣本非常敏感(10 個 defect),換一個 model 或訓練種子可能會讓 AUROC ±1 個百分點;但我們的 SEED=42 讓數字可重現,可以信賴。第二,混淆矩陣只在 threshold 0.50 下有意義;換 threshold 就換一張混淆矩陣,因此「請用信心門檻掃描表做決策」比「請解讀單一混淆矩陣」更可靠。第三,錯誤視覺化是定性指標、不是定量指標;它幫助模型開發者知道「模型長相如何」,但不能直接量化為可改進的損失函式。
一份完整的評估報告最好包含:本篇的數字表格 + 信心門檻掃描 + per-class 指標 + 錯誤視覺化 + 一段文字說明每張錯誤的成因與改善方向。這種「數字 + 視覺 + 解釋」的三層結構在工業界已經是「專案報告」的標準格式,Day 44 的部署會引用這份報告作為 threshold 設定與新功能開發的依據。最後,建議把這份評估報告的 markdown 檔案命名為 eval_report.md 並放在專案根目錄,這樣日後換人或換模型時,新接手的人可以直接讀這份報告理解「這個模型已經達到什麼程度、瓶頸在哪」。
小結
今天把 Day 42 的兩個 best 權重在 test set(20 good + 10 defect)上完整評估一次,得到分類 AUROC 0.9892、precision 0.80、recall 0.80、F1 0.80;分割 mIoU 0.6838、mean Dice 0.8015;三類瑕疵子類別的差異為:broken_large IoU 0.81、broken_small IoU 0.65、contamination IoU 0.52。我們把錯誤分成 FN/FP/Localization/Duplicate 四類,發現 FN 全來自 contamination、FP 來自 good 影像的瓶身反光、Localization 集中在 broken_small 的邊界。信心門檻掃描顯示 threshold 0.25 時 F1 達 0.7692 最佳、threshold 0.50 是工業界常用平衡點。明天 Day 44 會把這兩個 best 權重轉成 ONNX、用 FastAPI 包成 HTTP 服務、用 Streamlit 做前端展示,並以今天的信心門檻掃描表決定部署用的 threshold。
結語
今天的重點是「把昨天的模型,變成明天的部署依據」。我們從 Day 42 的 best 權重出發,在 test 上完整跑了分類與分割兩條管線的指標,並寫出一份包含信心門檻掃描、子類別差異分析、錯誤視覺化的評估報告。這份報告最關鍵的結論有三個:第一,AUROC 0.9892 顯示模型「學到正常/瑕疵的整體差異」;第二,threshold 0.25 達 F1 最佳;第三,contamination 與 broken_small 是兩大瓶頸,需要資料擴增與 Boundary Loss。這些結論會直接驅動 Day 44 的 threshold 設定與 Day 44 後的延伸學習方向。
「以錯誤分析驅動模型迭代」是工業界的核心方法論。沒有錯誤分析你只會看到 mIoU 0.68、是個「還不錯」的數字;有了錯誤分析你就會看到「contamination 是瓶頸、要把生成式合成優先用在這裡」。明天 Day 44 會把這份報告濃縮成「部署用 threshold 0.30」、「雙管線並行」、「ONNX 加速」三個具體決策,把模型送上線。明天,我們會做完整的部署:ONNX 匯出、onnxruntime 1.19 驗證、FastAPI 0.115 寫端點、Streamlit 1.39 寫展示頁,並把今天的 threshold 0.30 接到服務內。
延伸資源
- sklearn 評估官方文件(1.5.x,2024):
https://scikit-learn.org/stable/modules/model_evaluation.html,AUROC、precision/recall/F1、混淆矩陣的標準 API。 - Bergmann 等人,2019,MVTec AD — A Comprehensive Real-World Dataset for Unsupervised Anomaly Detection,MVTec AD 官方推薦的評估指標(per-region AUROC、pixel-level AUROC 與 PRO 曲線,CVPR 2019)。
- Powers, 2011, Evaluation: From Precision, Recall and F-Measure to ROC, Informedness, Markedness & Correlation,precision/recall/F1 的數學定義與 trade-off(Waikato University)。
- Taha 等人,2015,Metrics for Evaluating 3D Medical Image Segmentation,醫療/工業影像分割常用指標(Dice、Boundary F1、HD95)的標準論文。
- Day 17 錯誤分析模式(Hoiem 等人,2012,Diagnosing Error in Object Detectors,ECCV 2012):把錯誤分成 FN、FP、Localization、Similar 四類的方法,本篇沿用前三類並對應到瑕疵場景。
留言
張貼留言