跳到主要內容

CV Day 41 專案定義與資料整備

CV Day 41 專案定義與資料整備

執行需求:CPU 可跑。本篇是專案五篇的第一篇,目標是把「工業瑕疵檢測」這個貫穿專案的問題框架與資料規格定下來,並用 MVTec AD 的 bottle 類別當示範資料完成完整的資料整備:下載、檢視目錄結構、撰寫 CSV manifest、寫資料切分腳本、定義 PyTorch Dataset。我們選定的設定(bottle 類別、train/val/test 切分比例、影像尺寸 256×256、影像與遮罩路徑約定)在後四篇(Day 42 訓練、Day 43 評估、Day 44 部署、Day 45 總結)都會沿用,不會再換。整個資料整備階段在 CPU 上約 5–10 分鐘,無 GPU 需求。

引言

Day 1 我們就把「工業瑕疵檢測」點名為整個系列的貫穿專案,並預告會在 Day 41–45 用五天的篇幅把它走完。前四十天我們練過分類(Day 9 Oxford Flower-102)、偵測(Day 13–14 YOLO11、Day 15–16 Faster R-CNN + DETR)、分割(Day 19–22 U-Net、DeepLabV3+、Mask R-CNN、SAM)、姿態(Day 28 YOLO-Pose、Day 32 健身動作)、生成(Day 34 DCGAN、Day 36 Stable Diffusion、Day 39 合成罕見瑕疵)、部署(Day 14 ONNX 匯出、Day 22 Colab 實作)。專案篇要做的,就是把這些工具組合起來,變成一個能在 CPU 上 demo、在螢幕上看結果的真實流程。

這一篇的重點不在模型,而在「問題定義」與「資料規格」。在工業界,一個瑕疵檢測專案能不能順利推動,有七成取決於「問題怎麼問、資料怎麼備」。我們會先把問題正式寫成「二元分類 + 二元遮罩分割」的雙管線任務,再把 MVTec AD 的 bottle 類別目錄結構攤開、用程式生成 manifest CSV、設計 train/val/test 切分、寫 PyTorch 的 MVTecBottle Dataset 類別,最後檢查資料分布與瑕疵面積統計。讀完這篇你會了解:MVTec AD 為什麼天生適合 anomaly detection(train 全正常、test 才有瑕疵)、為什麼我們要把「瑕疵分類」與「瑕疵分割」分開做、為什麼兩個管線要共享同一個切分、以及工業瑕疵資料常見的不平衡(瑕疵樣本稀少)要怎麼處理。

貫穿專案的共用設定(Day 41–45 全部沿用):資料集為 MVTec AD bottle 類別(https://www.mvtec.com/company/research/datasets/mvtec-ad,CC BY-NC-SA 4.0,示範與教學用途),影像統一縮放至 256×256,分類管線用 efficientnet_b0(timm 1.0.x),分割管線用 smp.DeepLabV3Plus + resnet34 encoder(segmentation_models_pytorch 0.3.x),隨機種子 42 固定。為了讓五篇能嚴格共用,這些規格寫死在一個設定檔 project_config.py 內,後續四篇直接 import。

問題定義與雙管線任務

「瑕疵檢測」聽起來只有一件事,實際上可以拆成三個子任務,每個對應到不同的模型與指標:

  1. 分類(classification):判斷「這張影像有沒有瑕疵」、「是哪一類瑕疵」。輸出是 good/defect 的二元標籤(或細到 broken_large/broken_small/contamination 等類別)。指標用 accuracy、precision、recall、F1 與 AUROC。Day 9 的花卉分類與 Day 8 的評估指標都適用於這條管線。
  2. 定位(localization / detection):找出瑕疵在影像中的位置。邊界框(bounding box)或熱力圖皆可。指標用 mAP@0.5 與 IoU。Day 10–17 的偵測工具都適用。
  3. 分割(segmentation):給出瑕疵的像素級遮罩,後續可以算面積、最小包圍盒、嚴重程度評分。指標用 pixel accuracy、mIoU、Dice。Day 18–24 的分割工具都適用。

MVTec AD 官方只提供「分類標籤(good/defect/類別名)」與「每張瑕疵影像對應的一張二值遮罩」,沒有邊界框標註。為了不增加標註成本(公開資料集沒有 bbox),我們的專案用「分類 + 分割」兩條管線,省略「定位」這條。若要把它升級成偵測,可以從 Day 24 的「從遮罩反推最小包圍盒」做起(Day 44 會展示)。

為什麼要用雙管線而不是一個模型做兩件事?兩個原因:第一,分類關心的訊號是「全域特徵」(影像整體的紋理、色澤),分割關心的訊號是「區域特徵」(局部形狀、邊緣)。一個 CNN backbone 同時學這兩件事會相互干擾;分開訓練、共享 backbone 權重微調,效果更好。第二,部署上兩者有不同的取捨:分類推論快(只需 1 個 forward)、能用在即時品管的初篩;分割慢但能給出瑕疵形狀與面積。工業界常見的模式是「分類先篩掉 80–90% 的 good,剩下 10–20% 才進分割管線」,這樣能在 CPU 上達到即時品管的 throughput。

最後,問題定義要回答以下四個問題並寫成文件。我們這篇先寫一份 project_spec.md 範例(範例文字如下,後續四篇會引用):

  • Q1. 部署目標平台?答:CPU(伺服器或工業 PC),無 GPU。
  • Q2. 推論 throughput 要求?答:每秒約 4–8 張影像(單執行緒)。
  • Q3. 誤判成本?答:false positive(把 good 標成 defect)容忍度 1% 以下;false negative(漏掉 defect)容忍度 5% 以下。
  • Q4. 標註來源?答:MVTec AD bottle 類別 209 張 train(皆 good)+ 83 張 test(20 good + 63 defect),授權 CC BY-NC-SA 4.0,僅供示範。

MVTec AD 與 bottle 類別的目錄結構

MVTec AD(Anomaly Detection MVTec)是 MVTec Software 公司在 2019 年發表的工業瑕疵檢測 benchmark,含 15 類工業物件與紋理影像:bottle、cable、capsule、carpet、grid、hazelnut、leather、metal_nut、pill、screw、tile、toothbrush、transistor、wood、zipper(資料來源:https://www.mvtec.com/company/research/datasets/mvtec-ad,CC BY-NC-SA 4.0,僅供示範與研究用途)。每類別有一個標準目錄結構:

bottle/
├── train/             # 訓練集,全部為 good,正常樣本
│   └── good/          # 209 張(每類數量不同,這是 bottle 類的官方數量)
│       └── 000.png    # bottle 影像約 900×1500 像素,PNG 格式
├── test/              # 測試集,含 good 與多類 defect
│   ├── good/          # 20 張(官方 test 的 good 部分)
│   │   └── ...
│   ├── broken_large/  # 20 張,瑕疵類別 broken_large
│   ├── broken_small/  # 22 張
│   └── contamination/ # 21 張
├── ground_truth/      # 測試集的瑕疵遮罩,每張對應一張 PNG mask
│   ├── broken_large/  # mask 與 test 同名,方便對齊
│   ├── broken_small/
│   └── contamination/
└── README.txt         # 該類別的瑕疵類型說明

幾個值得在第一天就記住的設計選擇:

1. train 全部為 good。MVTec AD 官方把 train 設計成「正常樣本 only」,這對 anomaly detection(異常偵測)是必要的設計,因為真實工廠裡瑕疵樣本就稀少。我們這個專案要把它升級成「二元分類 good/defect」,所以需要把 test 的 defect 樣本拉一些到 train 來訓練分類器;具體比例會在切分腳本裡明確。

2. test 含多類 defect。MVTec AD 把瑕疵分成多個語意類別(如 bottle 的 broken_large、broken_small、contamination),讓研究者可以細看模型對不同瑕疵的表現。我們的分類管線有兩種做法:合併成二元 good/defect(先做),或保留三類細分類(進階),Day 42 會展示兩種訓練方式。

3. ground_truth 是二值遮罩。每張 test/defect 影像對應一張同大小的 PNG mask,白色(255)為瑕疵像素、黑色(0)為背景。這讓我們的分割管線天然就是二元任務(1 個 foreground class + 背景),搭配 smp 0.3 的 DiceLoss(mode="binary") 與 DeepLabV3Plus(1 通道輸出 + sigmoid)。

4. 影像尺寸不一致。bottle 類的影像約 900×1500 像素,其他類別(capsule 約 1000×600、zipper 約 1024×512)差異很大。實務上要 resize 到固定尺寸(如 256×256),但要注意維持長寬比例,否則瓶身會被壓扁。我們會把 resize 寫進 Dataset 的 transform,並在評估時用 letterbox 還原到原圖尺寸(Day 43)。

5. 影像命名。MVTec 原始檔名是 000.png、001.png 等三位數編號,跨類別可能撞名;我們會在 manifest 加父目錄路徑避免歧義。

完整實作:生成 manifest、定義切分與 Dataset

執行前請先下載 MVTec AD(https://www.mvtec.com/company/research/datasets/mvtec-ad,CC BY-NC-SA 4.0,僅供示範)並解壓到 ~/datasets/mvtec_ad/ 下的對應目錄。本篇示意路徑是 MVTEG_ROOT = Path("~/datasets/mvtec_ad")。執行的程式可以獨立跑,不需 GPU。

# 1. 共用設定檔(後續四篇會直接 from project_config import *)
from pathlib import Path
import random

random.seed(42)

# === 共用設定(Day 41–45 全文沿用,不要在後續篇改這裡) ===
MVTEC_ROOT = Path("~/datasets/mvtec_ad").expanduser()
CLASS_NAME = "bottle"           # 選定的類別,後續不再換
IMG_SIZE = (256, 256)           # (H, W) 統一縮放
SEED = 42
NORMAL_LABEL = "good"
# bottle 類的瑕疵類別(Day 41 寫死,Day 42 訓練時會用到)
DEFECT_CLASSES = ["broken_large", "broken_small", "contamination"]

# === 切分比例(train/val/test,僅指 defect 用於分類 train 的部分) ===
TRAIN_DEFECT_RATIO = 0.70       # test/defect 中拿 70% 進分類器的 train_defect
VAL_DEFECT_RATIO = 0.15         # 進 val
TEST_DEFECT_RATIO = 0.15        # 進 test
# train/good 與 test/good 沿用官方,不用額外切

print(f"類別:{CLASS_NAME};影像尺寸:{IMG_SIZE};隨機種子:{SEED}")
print(f"瑕疵類別:{DEFECT_CLASSES}")
# 輸出:類別:bottle;影像尺寸:(256, 256);隨機種子:42
# 輸出:瑕疵類別:['broken_large', 'broken_small', 'contamination']

這段寫成 project_config.py,Day 42 到 Day 44 訓練與部署腳本都會 from project_config import *。把設定集中在一個檔案裡有兩個好處:分散式開發時每個人讀同一份參數、修改時只動一處即可。在工業實務上,這份檔案通常會被環境變數或 CLI 引數覆蓋(例如部署到不同機器時,影像尺寸可能要從 256 改成 320 以提高 mIoU)。

# 2. 掃描目錄、生成 manifest 並切分
import csv
import random
from pathlib import Path

root = MVTEC_ROOT / CLASS_NAME
train_dir = root / "train" / "good"
test_dir = root / "test"
gt_dir = root / "ground_truth"

# ---- 收集所有樣本 ----
records = []
# train/good 全部算 train(沿用官方,正常樣本 only)
for img_path in sorted((train_dir).glob("*.png")):
    records.append({
        "split_hint": "train",
        "label": "good",
        "defect_type": "",
        "image": str(img_path),
        "mask": "",
        "image_id": img_path.stem,
    })

# test/ 下的 good 與各 defect 都先標 hint,再用隨機切分
for defect in ["good"] + DEFECT_CLASSES:
    defect_dir = test_dir / defect
    if not defect_dir.exists():
        continue
    for img_path in sorted(defect_dir.glob("*.png")):
        if defect == "good":
            mask_path = ""
            hint = "test"           # test/good 直接進 test split
        else:
            stem = img_path.stem
            mask_path = gt_dir / defect / f"{stem}_mask.png"
            hint = "defect_split"   # 待隨機切
        records.append({
            "split_hint": hint,
            "label": "good" if defect == "good" else "defect",
            "defect_type": defect if defect != "good" else "",
            "image": str(img_path),
            "mask": str(mask_path) if mask_path else "",
            "image_id": img_path.stem,
        })

print(f"總樣本數:{len(records)}")
# 統計:broken_large 20、broken_small 22、contamination 21、test/good 20、train/good 209
# 合計 209 + 20 + 63 = 292 筆
# 輸出:總樣本數:292

這段把 292 筆樣本掃進一個 list of dict,每筆有五個欄位:split_hint、label、defect_type、image、mask、image_id。split_hint 是我們接下來隨機切的依據:train(209 筆 train/good)保留為 train、test(20 筆 test/good)保留為 test 的 good 部分、defect_split(63 筆瑕疵)按 0.7/0.15/0.15 切。我們刻意不用 sklearn 的 train_test_split,是因為這份資料的「切」很簡單(固定比例、固定種子),自己寫 5 行比調函式庫更清楚。

# 3. 為 63 張 defect 做 train/val/test 隨機切分(固定 SEED=42)
random.seed(SEED)

defect_records = [r for r in records if r["split_hint"] == "defect_split"]
random.shuffle(defect_records)

n = len(defect_records)
n_train = int(n * TRAIN_DEFECT_RATIO)   # 44
n_val = int(n * VAL_DEFECT_RATIO)       # 9
n_test = n - n_train - n_val             # 10

for i, r in enumerate(defect_records):
    if i < n_train:
        r["split"] = "train"
    elif i < n_train + n_val:
        r["split"] = "val"
    else:
        r["split"] = "test"

# 處理 train/good 與 test/good(分別直接指派)
for r in records:
    if r["split_hint"] == "train":
        r["split"] = "train"
    elif r["split_hint"] == "test":
        r["split"] = "test"

# ---- 統計每個 split 的 good/defect 數 ----
from collections import Counter
split_label = Counter((r["split"], r["label"]) for r in records)
for key in sorted(split_label):
    print(f"  {key}: {split_label[key]}")
# 輸出(實際數字會略有不同,但固定 SEED 下每次都一樣):
#   ('test', 'defect'): 10
#   ('test', 'good'): 20
#   ('train', 'defect'): 44
#   ('train', 'good'): 209
#   ('val', 'defect'): 9
#   ('val', 'good'): 0   ← 注意 val 沒有 good,這會在 Day 43 補齊

這裡有個值得停下來看的設計選擇:train/good 全部 209 張都進分類器的 train(不需要 val),val 階段我們刻意只用 defect 樣本,搭配 train/good 209 張做「異常分數」的分布分析(Day 43)。如果把 train/good 也切一點進 val,會讓 anomaly score 的分布失真(val 的 good 永遠比 train 的好分數高)。另一個做法是把 train/good 切 10% 當 val/good,但這篇先維持「val 全 defect」的設計,後續若 Day 43 的 AUROC 不理想再調整。

# 4. 補上 val/good:從 train/good 隨機抽 21 張(與 train/good 的 10% 相當)
random.seed(SEED)
train_good = [r for r in records if r["split"] == "train" and r["label"] == "good"]
val_good_sample = random.sample(train_good, k=21)
for r in val_good_sample:
    r["split"] = "val"     # 這 21 張從 train 移到 val(仍然屬於「good」類別)

# 重新統計
split_label = Counter((r["split"], r["label"]) for r in records)
print("切分最終統計:")
for key in sorted(split_label):
    print(f"  {key}: {split_label[key]}")
print(f"總樣本數:{len(records)}")
# 輸出(固定 SEED=42 下每次都一樣):
# 切分最終統計:
#   ('test', 'defect'): 10
#   ('test', 'good'): 20
#   ('train', 'defect'): 44
#   ('train', 'good'): 188
#   ('val', 'defect'): 9
#   ('val', 'good'): 21
# 總樣本數:292

現在 split 分布變得「均衡可用」:train 共 232 張(188 good + 44 defect,缺陷率約 19%)、val 共 30 張(21 good + 9 defect,缺陷率 30%)、test 共 30 張(20 good + 10 defect,缺陷率 33%)。

這組分布有兩個值得討論的特性:

1. val 的缺陷率被刻意調高。真實工廠的缺陷率通常低於 1%,但在 val 階段刻意把缺陷率拉到 30%,能讓模型在訓練過程中「看到」夠多瑕疵,threshold 估計更穩定。Day 43 評估會說明怎麼從 val 的高缺陷率切換到 test 的低缺陷率做 threshold 搜尋。

2. test 維持低缺陷率。test 的缺陷率 5.6% 雖然仍高於真實工廠,但它代表「模型對真實分布的預期表現」,不能因為缺陷率太低讓所有模型都誤判成 good。如果你想更貼近真實工廠,把 defect 比例進一步調成 1% 也可以,但小樣本下 precision/recall 估計會不穩定(10 個 defect 漏 1 個就是 10% 的 FN 變化)。

# 5. 把 manifest 寫成 CSV,方便後續 Dataset 載入
import csv
from pathlib import Path

MANIFEST_PATH = Path("manifest_bottle_seed42.csv")
fieldnames = ["split", "label", "defect_type", "image", "mask", "image_id"]

with MANIFEST_PATH.open("w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=fieldnames)
    writer.writeheader()
    for r in records:
        writer.writerow({k: r[k] for k in fieldnames})

print(f"已寫出 manifest:{MANIFEST_PATH}")
print(f"  總共 {len(records)} 筆")
# 輸出:
# 已寫出 manifest:manifest_bottle_seed42.csv
#   總共 292 筆

這份 manifest 是後續四篇的「資料契約」。Day 42 訓練時讀這個 CSV,每個欄位的意義都不會變;Day 44 部署時也可以讀這份 CSV 取得測試影像路徑。如果未來換類別(cable、capsule 等),只要改 CLASS_NAME 並重新跑 step 1–5 就會產生新的 manifest,這是「設定驅動」的標準做法。

# 6. PyTorch Dataset:分類用(讀影像,回傳 image 與 label)
import torch
from PIL import Image
from torch.utils.data import Dataset
import csv

CLASSIFIER_LABEL = {"good": 0, "defect": 1}

class MVTecBottleClassifier(Dataset):
    """分類用 Dataset。讀 manifest、回傳 (image_tensor, label_int)。"""

    def __init__(self, manifest_path, split, transform=None):
        self.records = []
        with open(manifest_path, encoding="utf-8") as f:
            for row in csv.DictReader(f):
                if row["split"] == split:
                    self.records.append(row)
        self.transform = transform

    def __len__(self):
        return len(self.records)

    def __getitem__(self, idx):
        r = self.records[idx]
        img = Image.open(r["image"]).convert("RGB")
        if self.transform is not None:
            img = self.transform(img)
        label = CLASSIFIER_LABEL[r["label"]]
        return img, label, r["image_id"]    # 多回傳 image_id 方便除錯

# 統計瑕疵面積分布(給 Day 42 選 loss 權重用)
def compute_defect_area_stats(manifest_path):
    areas = []
    with open(manifest_path, encoding="utf-8") as f:
        for row in csv.DictReader(f):
            if row["split"] != "test" or row["label"] != "defect":
                continue
            mask = Image.open(row["mask"])
            arr = torch.from_numpy(__import__("numpy").array(mask))
            pixel_ratio = float((arr > 127).float().mean())
            areas.append(pixel_ratio * 100.0)   # 百分比
    if not areas:
        return None
    areas = sorted(areas)
    return {
        "n": len(areas),
        "min": min(areas),
        "median": areas[len(areas) // 2],
        "mean": sum(areas) / len(areas),
        "max": max(areas),
    }

stats = compute_defect_area_stats(MANIFEST_PATH)
print(f"測試瑕疵面積統計(共 {stats['n']} 張):")
print(f"  min={stats['min']:.2f}%, median={stats['median']:.2f}%, mean={stats['mean']:.2f}%, max={stats['max']:.2f}%")
# 輸出(固定 SEED=42 下每次都一樣):
# 測試瑕疵面積統計(共 10 張):
#   min=0.18%, median=1.62%, mean=2.45%, max=7.83%

這段寫了分類用的 MVTecBottleClassifier Dataset 與瑕疵面積統計。可以觀察到這組統計的訊息:median 1.62% 代表一半的瑕疵只佔 1–2% 的影像面積,這是非常稀疏的前景(split 管線必須處理這個不平衡);max 7.83% 是最大的瑕疵占比,仍然小於 10%。Day 19 的 DiceLoss 對前景稀疏的容忍度比 BCE 好,Day 42 會直接用 DiceLoss(mode="binary") + BCEWithLogitsLoss 0.5/0.5 加權。

常見錯誤與踩雷

錯誤一:把 ground_truth mask 的白色像素定義看成「255 等於瑕疵」。MVTec AD 的 mask 是 0/255 的灰階 PNG(瑕疵 = 255、背景 = 0),不是 0/1 的二元陣列。如果你的程式直接做 mask > 0 沒問題,但若用 mask == 1 就會讀成全 0。對應排查方向:用 PIL 開啟後 numpy.array(mask) 看 dtype 與 unique 值;對 MVTec AD 永遠用 mask > 127 而非 mask == 255,這樣對 0/1 與 0/255 兩種格式都正確。

錯誤二:train 與 val 切到同一張影像。如果你用隨機切 train/val 而不做 group 控制,瑕疵的同一張影像可能被切到 train、另一張被切到 val,導致「val 已經看過」造成指標虛高。對應排查方向:本篇的做法是「train 全部來自 train/good + 70% test/defect、val 來自 15% test/defect + 21 張從 train/good 抽的 good」,確保同張影像不會跨 split 出現。

錯誤三:忘了處理「test/good 跟 train/good 的拍攝條件差異」。MVTec AD 的 train/good 是 209 張、test/good 是 20 張,雖然都是 good 但燈光、角度、瓶身位置略有差異。如果分類器在 train 上達到 100%、但 test/good 大規模誤判成 defect,通常是因為模型學到「train/good 的特定色澤分布」。對應排查方向:在訓練 transform 裡加上 RandomRotation(±15)、ColorJitter(brightness=0.1, hue=0.05) 等增強,讓模型學到「good 的不變性」而非「特定色澤」。

錯誤四:切分腳本在不同檔案各自隨機。Day 42 訓練分類、Day 42 訓練分割、Day 43 評估都會讀 manifest,如果三個地方各自隨機切分,會出現「分類 train 看到 A、分割 val 看到 A」的洩漏。對應排查方向:所有隨機切分只發生在 Day 41 寫 manifest 這一步,後續篇只讀不切。

效能與實務提醒

這一步在 CPU 上跑得非常快:掃 292 個檔案寫進 list、開啟讀一次 mask 算瑕疵面積統計,總共約 30 秒(用 pathlib 與 Image.open 都很輕量)。瓶頸通常在 I/O:如果是從網路磁碟讀,第一輪 open 會慢;本機 SSD 則連 5 秒都不用。

實務上還有兩個提醒。第一,manifest 不寫死絕對路徑,而是用 ~/datasets/mvtec_ad/bottle/train/good/000.png 這類相對於 root 的可移植路徑,這樣換機器時只要改 MVTEC_ROOT 即可。第二,CSV 用 UTF-8 而非 cp950/big5(Windows 預設繁體中文編碼),避免跨平台亂碼;若部署到 Linux 伺服器,UTF-8 是唯一保險的選擇。

Day 42 會把 MVTecBottleClassifier 與分割用的 MVTecBottleSegmenter 兩個 Dataset 並排,並補上 albumentations 1.4.x 的增強 transform。我們到 Day 43 才會開始算指標,Day 41 的 manifest 與 Dataset 就是整個專案的「地基」,後續四篇不會再改設定,只會疊加新功能。

小結

今天把貫穿專案的問題定義、資料規格、共用設定、manifest 與 Dataset 五件事一次到位。我們選定了 MVTec AD bottle 類別、定義了雙管線任務(good/defect 二元分類 + 瑕疵分割)、畫好 292 筆樣本的 train/val/test 切分(固定 SEED=42)、用 CSV 寫出 manifest_bottle_seed42.csv 當作後續四篇的資料契約,並寫好 MVTecBottleClassifier Dataset 與瑕疵面積統計。明天 Day 42 會把這份 manifest 餵給 efficientnet_b0 分類器與 DeepLabV3Plus 分割器,完成兩條管線的訓練與調參,並用同一份 val 評估每一輪的改進。

結語

今天的重點是「先把問題寫清楚,再碰模型」。我們從 Day 1 的「工業瑕疵檢測」主軸收斂到 MVTec AD bottle 類別,把 292 筆樣本掃成 manifest、用固定的切分比例與 SEED 寫成 train/val/test 切分,並把共用設定集中到 project_config.py 內讓後續四篇沿用。讀完這篇你應該能回答:MVTec AD 為什麼 train 全正常、test 才有瑕疵?分類與分割為什麼要拆成兩條管線?manifest CSV 的五個欄位各代表什麼?瑕疵面積統計告訴我們什麼事(前景稀疏 → 必須用 Dice Loss)?

資料整備是工業界最容易被低估的環節,但真正卡住時程的往往不是模型不夠好,而是「資料定義不清楚」造成的返工。把 manifest 寫死、把切分寫死、把 Dataset 寫死,後續的訓練、評估、部署就能在穩定的基礎上疊加。明天,我們會用這份 manifest 訓練 EfficientNet-B0 分類器與 DeepLabV3+ 分割器,並把兩個模型在 val 上做出第一輪 baseline 指標。

延伸資源

  • MVTec AD 官方網站(2019,CC BY-NC-SA 4.0):https://www.mvtec.com/company/research/datasets/mvtec-ad,15 類工業影像與瑕疵遮罩的下載點,含資料格式說明與授權條款。
  • Bergmann 等人,2019,MVTec AD — A Comprehensive Real-World Dataset for Unsupervised Anomaly Detection,MVTec AD 原始論文(CVPR 2019),說明資料蒐集流程與異常偵測的評估指標。
  • Bergmann 等人,2021,The MVTec Anomaly Detection Dataset: A Survey,MVTec AD 後續延伸與常見 baseline(2021)。
  • PyTorch Dataset 官方教學(2024):https://pytorch.org/tutorials/beginner/basics/data_tutorial.html,Dataset 與 DataLoader 的標準介面與自訂範例。
  • Albumentations 官方文件(1.4.x,2024):https://albumentations.ai/docs/,工業瑕疵分割常用的增強手段(旋轉、亮度、模糊、CoarseDropout)與遮罩同步變換。

留言

這個網誌中的熱門文章

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