CV Day 20 現代語意分割:DeepLabV3+、FPN 與 smp
執行需求:Colab T4 可跑。本篇在 Colab 免費 T4(16 GB VRAM)上以 PASCAL VOC 2012 segmentation 子集(每類抽 200 張訓練、50 張驗證、約 21 類)跑 3 個 smp 模型:DeepLabV3+、U-Net 與 FPN,每個模型 5 epoch 訓練約 8–12 分鐘(含預訓練權重下載);驗證 mIoU 通常 DeepLabV3+ 最高(0.65 以上,實際數字會略有不同)、U-Net 次之、FPN 接近 U-Net。CPU 也能跑,但每個 epoch 會從 1 分鐘拖到 8–12 分鐘,不建議。完整 VOC segmentation 訓練(10K+ 影像、30+ epoch)約 2 小時,建議用 Colab Pro 或自架 GPU。
引言
昨天的內容中,我們自寫了一個小型 U-Net,在合成的幾何形狀資料集上把 mIoU 推到 0.87。今天接著處理兩個問題:第一,真實世界的影像(例如 PASCAL VOC)有 20 個前景類別加上背景共 21 類,類別之間外觀差異大、影像尺寸不一,自寫 U-Net 從零訓練很難達到 SOTA;第二,U-Net 只有四層下採樣,捕捉大物件語意與小物件細節時會拉扯。現代語意分割透過兩個關鍵設計解決這些問題:DeepLabV3+ 引入「空洞卷積(Atrous/Dilated Convolution)」與「ASPP(Atrous Spatial Pyramid Pooling)」模組,讓骨幹網路能在不降低解析度的情況下擴大感受野;FPN(Feature Pyramid Network)則把骨幹不同階段的特徵用 top-down 與 lateral connection 串起來,讓模型同時利用深層語意與淺層細節。
本篇的主角是 segmentation_models_pytorch(簡稱 smp,版本 0.3.x,pip 套件名也是 segmentation_models_pytorch)。smp 是社群在 2024 年中期仍在積極維護的開源套件,把 U-Net、DeepLabV3+、FPN、Unet++、PAN、MANet、Linknet 等 9 種以上的主流分割架構包成統一介面,每個架構的 encoder 都可以換成 ImageNet 預訓練的 ResNet、EfficientNet、ConvNeXt、Swin Transformer 等十幾種 backbone。這意味著你只要改一個字串參數(encoder_name),就能在 ResNet34 與 EfficientNet-B5 之間切換,省下自己寫骨幹與載入權重的時間。本篇會示範 smp 0.3 的三個 API 簽名:smp.DeepLabV3Plus(encoder_name="resnet34", encoder_weights="imagenet", classes=21, activation=None)、smp.Unet(...)、smp.FPN(...),並在 VOC 子集上比較三者的 mIoU。
貫穿專案的角度:Day 25 會在 MVTec AD 上做完整的瑕疵分割實戰,但 MVTec AD 公開的標註是「每張影像對應一個二值遮罩」(瑕疵 vs 背景),比 VOC 的 21 類簡單。今天先把 VOC 21 類的分割管線跑通,到了 Day 25 就可以把 backbone(DeepLabV3+)、損失(Dice + CE)、評估(mIoU)整套搬到 MVTec AD 上,只把 classes 從 21 換成 1 即可。
空洞卷積、ASPP 與 FPN 的設計動機
傳統 CNN 在 encoder 階段反覆用 2×2 maxpool 降低解析度,雖然能擴大感受野,但每次下採樣都會丟掉空間資訊。對於「道路與行人」這種需要像素級精度的任務,這個取捨太過犧牲。空洞卷積(atrous convolution,又稱 dilated convolution)解決了這個兩難:在卷積核的權重之間插入「空洞」,等效於在不增加參數量的前提下擴大 kernel 的空間覆蓋。例如一個 3×3、dilation=2 的卷積,雖然只用到 9 個權重,但這 9 個權重作用在 5×5 的範圍上(每兩個權重間隔一個像素);dilation=4 時則作用在 9×9 範圍上。這個特性讓 DeepLabV3+ 的骨幹在最後兩個 stage 用空洞卷積取代標準卷積,把輸出解析度從原圖的 1/32 維持在 1/16,保留更多空間細節。
ASPP(Atrous Spatial Pyramid Pooling)是 DeepLabV3+ 的招牌模組。它把同一份深層特徵分別用不同 dilation rate 的空洞卷積(常見是 1、6、12、18)平行處理,再加上一個全域平均池化(image pooling)分支,最後把所有分支的特徵沿通道維度拼接。這個設計的動機是「同一個物件在不同影像中可能佔據不同大小」——大物件需要大感受野(dilation=12、18),小物件需要密集採樣(dilation=1),把多個尺度並排處理能同時照顧兩者。實作上 ASPP 對應到 smp 內部的 ASPP 模組,使用者不需要自己寫,但了解原理能幫助除錯。
FPN(Feature Pyramid Network)則是一個通用設計,最初是為了物件偵測提出的,但語意分割也廣泛採用。FPN 的核心是「top-down + lateral connection」:把骨幹最後一層的深層特徵(高語意、低解析度)做上採樣,與前一層的中層特徵(中等語意、中等解析度)在加權相加後融合,再對這個結果上採樣與淺層融合。這樣最終得到的「金字塔特徵」每一層都同時含有強語意與合理解析度。在語意分割中,FPN 通常用於 decoder 階段,讓最終輸出能同時對大物件(靠深層語意)與小物體(靠淺層細節)有好的預測。smp 的 FPN 把這些邏輯包成一個獨立模型,使用者只要給 encoder_name 即可。
三者放在一起的取捨是:U-Net 結構簡單、容易理解、訓練穩定;DeepLabV3+ 在 VOC、Cityscapes 等 benchmark 上長期 SOTA,特別擅長處理大物件與多類別;FPN 則介於兩者之間,骨幹可以自由替換,當你想換成 EfficientNet、Swin Transformer 時 smp.FPN 的彈性最大。實務上若你已有 PyTorch 訓練流程的經驗,建議直接從 smp.DeepLabV3Plus 開始;若類別只有 2 個(瑕疵 vs 背景)且資料量小,smp.Unet 更穩定。
smp 0.3 的 API 與預訓練權重
smp 0.3.x 把所有模型包成同一個建構介面,最常用的四個參數是 encoder_name、encoder_weights、classes、activation。encoder_name 是一個字串,指定 ImageNet 預訓練的骨幹,例如 resnet34、efficientnet-b3、timm-efficientnet-b5、swin-tiny-patch4-window7-224。smp 內建了 timm 作為可選 backbone,這是 smp 0.3 開始的重要功能:讓使用者能直接用 timm 1.0.x 維護的數十種現代 backbone。encoder_weights 通常設為 "imagenet"(使用 ImageNet 預訓練權重)或 "None"(從頭訓練)。對 VOC 這種中小型資料集,從 ImageNet 預訓練轉移過來幾乎總是比較好的起點。
classes 是分割類別數,VOC 21 類(含背景)就寫 classes=21。activation 參數要特別注意:訓練時一律設成 None,讓模型輸出 logits(未經 softmax),這樣可以搭配 CrossEntropyLoss 內部做的 softmax 取得數值穩定性;若 activation 設成 "softmax2d"(多類)或 "sigmoid"(二元),模型會在最後一層做完 softmax/sigmoid 才輸出,這時再餵 CrossEntropyLoss 就會報錯(重複 softmax)。實務上的慣例是「訓練用 None、推論用 softmax2d」。
對於二元分割,smp 內建的 DiceLoss 與 SoftCrossEntropyLoss(對 label smoothing 友善)值得特別注意:smp.losses.DiceLoss(mode="binary") 用於前景 vs 背景,mode="multiclass" 用於多類分割。實務上 VOC 21 類會把 DiceLoss(mode="multiclass") 與 nn.CrossEntropyLoss 加權相加作為最終損失,比例約 0.5/0.5 或 1.0/0.5(Dice 權重略高)。
完整實作:用 smp 0.3 訓練 DeepLabV3+、U-Net 與 FPN
以下範例在 Colab T4 上跑約 30–40 分鐘(3 模型 × 5 epoch)。我們會下載 PASCAL VOC 2012 segmentation 子集、寫一個 VOC Dataset、用 smp 0.3 載入三個模型、在同一份訓練設定下跑 5 epoch、比較驗證集 mIoU。執行前需要:pip install segmentation-models-pytorch==0.3.3 timm==1.0.9 albumentations==1.4.10(smp 0.3 對應 albumentations 1.4 與 timm 1.0)。
# 1. 下載 PASCAL VOC 2012 segmentation(官方學術授權,無需註冊)
# trainval 包含 2,905 張 segmentation 標註;devkit 提供標籤色彩定義
mkdir -p /content/datasets/voc2012
cd /content/datasets/voc2012
wget -q http://host.robots.ox.ac.uk/pascal/VOC/voc2012/VOCtrainval_11-May-2012.tar
tar -xf VOCtrainval_11-May-2012.tar
ls VOCdevkit/VOC2012
# 輸出:Annotations ImageSets JPEGImages SegmentationClass SegmentationObject
這段把 VOC 2012 segmentation 的 trainval 解壓到 /content/datasets/voc2012/VOCdevkit/VOC2012。SegmentationClass/ 是 21 類(含背景)的 PNG 標註,ImageSets/Segmentation/ 提供官方 train.txt 與 val.txt(1,464 張訓練 + 1,449 張驗證)。為了讓 Colab 免費版能在合理時間內跑完三個模型,我們下面會再從 train.txt 與 val.txt 各抽 200 與 50 張做為子集;這對 DeepLabV3+ 來說仍是足夠的相對評估,但絕對 mIoU 會比全量訓練低 5–10 個百分點,這是預期內的取捨。
# 2. VOC Segmentation Dataset(從 ImageSets/Segmentation/ 抽 200 train + 50 val)
import numpy as np
from pathlib import Path
from PIL import Image
import torch
from torch.utils.data import Dataset
VOC_ROOT = Path("/content/datasets/voc2012/VOCdevkit/VOC2012")
IMG_DIR = VOC_ROOT / "JPEGImages"
MASK_DIR = VOC_ROOT / "SegmentationClass"
SPLIT_DIR = VOC_ROOT / "ImageSets" / "Segmentation"
N_TRAIN, N_VAL = 200, 50
def _sample_ids(file, n, seed=42):
ids = Path(file).read_text().split()
rng = np.random.default_rng(seed)
chosen = rng.choice(len(ids), size=min(n, len(ids)), replace=False)
return [ids[i] for i in sorted(chosen)]
train_ids = _sample_ids(SPLIT_DIR / "train.txt", N_TRAIN)
val_ids = _sample_ids(SPLIT_DIR / "val.txt", N_VAL)
class VOCSeg(Dataset):
"""讀 VOC JPEG 影像與 SegmentationClass PNG(21 類索引)。"""
CLASSES = 21
def __init__(self, ids, img_size=256):
self.ids = ids
self.img_size = img_size
def __len__(self):
return len(self.ids)
def __getitem__(self, idx):
iid = self.ids[idx]
img = Image.open(IMG_DIR / f"{iid}.jpg").convert("RGB").resize(
(self.img_size, self.img_size)
)
mask = Image.open(MASK_DIR / f"{iid}.png").resize(
(self.img_size, self.img_size), Image.NEAREST
)
# 影像:float32 / 255 並用 ImageNet mean/std 標準化
img_np = np.asarray(img, dtype=np.float32) / 255.0
mean = np.array([0.485, 0.456, 0.406], dtype=np.float32)
std = np.array([0.229, 0.224, 0.225], dtype=np.float32)
img_np = (img_np - mean) / std
img_t = torch.from_numpy(img_np.transpose(2, 0, 1)).float()
# mask:uint8 long tensor,21 類索引
mask_t = torch.from_numpy(np.asarray(mask, dtype=np.int64)).long()
return img_t, mask_t
print(f"train: {len(train_ids)} 張, val: {len(val_ids)} 張")
print(f"第一張 train mask 的唯一值:{torch.unique(VOCSeg(train_ids)[0][1]).tolist()[:8]}...")
# 輸出:train: 200 張, val: 50 張
# 輸出:第一張 train mask 的唯一值:[0, 1, 2, 8, 15, 20]...
這段建立 VOCSeg 類別讀取 VOC 的 JPEG 影像與 SegmentationClass PNG。VOC 的 SegmentationClass 是 8 位元索引 PNG,每個像素值就是類別索引(0=背景、1=飛機、…、20=其他類別),不需要做色彩解碼——這跟 PASCAL VOC Detection Challenge 的 SegmentationClass 是同一份檔案。影像預處理用了 ImageNet mean/std,這是 smp 預訓練權重預期的輸入分佈;如果你用 torchvision.transforms 預設的 Normalize 也可以,但 smp 內建的 preprocessing_fn 與 ImageNet mean/std 一致,直接寫死比較直觀。Mask 用最近鄰 resize 以避免引入無效標籤,並轉成 long tensor 對應 CrossEntropyLoss。
# 3. smp 0.3 三個模型:DeepLabV3+、U-Net、FPN,共享 ResNet34 encoder
import segmentation_models_pytorch as smp
ENCODER = "resnet34"
WEIGHTS = "imagenet"
CLASSES = 21
models = {
"DeepLabV3+": smp.DeepLabV3Plus(
encoder_name=ENCODER, encoder_weights=WEIGHTS,
classes=CLASSES, activation=None,
),
"U-Net": smp.Unet(
encoder_name=ENCODER, encoder_weights=WEIGHTS,
classes=CLASSES, activation=None,
),
"FPN": smp.FPN(
encoder_name=ENCODER, encoder_weights=WEIGHTS,
classes=CLASSES, activation=None,
),
}
for name, m in models.items():
n = sum(p.numel() for p in m.parameters()) / 1e6
print(f"{name:14s} 參數量 {n:6.2f} M")
# 輸出:
# DeepLabV3+ 參數量 22.43 M
# U-Net 參數量 24.45 M
# FPN 參數量 23.14 M
這段示範 smp 0.3 最有代表性的兩個特性。第一,三個模型共享同一個 encoder(ResNet34 + ImageNet 預訓練權重),差別只在 decoder 設計:DeepLabV3+ 是 ASPP + 簡單上採樣、U-Net 是四層 skip connection、FPN 是 top-down 金字塔。第二,每個模型都約 22–24 M 參數,比昨天自寫 U-Net(7.8 M)大三倍,但因為有 ImageNet 預訓練,從小資料集收斂的速度反而快很多。第一個模型 DeepLabV3+ 載入時 smp 會自動從 https://github.com/qubvel/segmentation_models.pytorch/releases 下載 ResNet34 的 encoder 權重(首次執行約 80 MB)。
# 4. 損失:DiceLoss(multiclass) + CrossEntropyLoss,並定義 mIoU
import torch.nn as nn
import torch.nn.functional as F
class DiceCE(nn.Module):
def __init__(self, w_dice=0.5, w_ce=0.5, n_classes=21):
super().__init__()
self.dice = smp.losses.DiceLoss(mode="multiclass", from_logits=True)
self.ce = nn.CrossEntropyLoss()
self.w_dice, self.w_ce, self.n_classes = w_dice, w_ce, n_classes
def forward(self, logits, targets):
# logits: (B, C, H, W);targets: (B, H, W) long
dice_l = self.dice(logits, targets)
ce_l = self.ce(logits, targets)
return self.w_dice * dice_l + self.w_ce * ce_l
@torch.no_grad()
def mIoU_per_class(logits, targets, n_classes=21):
preds = logits.argmax(dim=1) # (B, H, W)
ious = []
for c in range(n_classes):
p = (preds == c)
t = (targets == c)
inter = (p & t).sum().item()
union = (p | t).sum().item()
ious.append(float("nan") if union == 0 else inter / union)
valid = [v for v in ious if not np.isnan(v)]
return float(np.mean(valid)) if valid else 0.0
print("DiceCE 損失 + per-class mIoU 評估函式就緒")
# 輸出:DiceCE 損失 + per-class mIoU 評估函式就緒
這段是 VOC 21 類分割的標準損失搭配。DiceLoss(mode="multiclass", from_logits=True) 是 smp 內建的損失,mode="multiclass" 會對每個類別算 Dice 再平均,from_logits=True 表示輸入是 logits(未經 softmax)。CrossEntropyLoss 內部會對 logits 做 softmax 再算 NLL,所以兩個損失可以直接相加。mIoU_per_class 對 21 類逐類算 IoU 再取平均;遇到「這個 batch 完全沒出現某類別」時把那類 IoU 設為 nan(不計入平均),這是分割評估的標準處理,比直接算 0 更公平。
# 5. 訓練一個模型(示範 DeepLabV3+;U-Net 與 FPN 重複同樣程式碼即可)
from torch.utils.data import DataLoader
from torch.optim import Adam
from torch.optim.lr_scheduler import CosineAnnealingLR
device = "cuda"
train_loader = DataLoader(VOCSeg(train_ids), batch_size=8, shuffle=True, num_workers=2)
val_loader = DataLoader(VOCSeg(val_ids), batch_size=8, shuffle=False, num_workers=2)
def train_one(model, name, epochs=5):
model = model.to(device)
opt = Adam(model.parameters(), lr=1e-3)
sched = CosineAnnealingLR(opt, T_max=epochs)
loss_fn = DiceCE()
best_iou = 0.0
for ep in range(1, epochs + 1):
model.train()
total = 0.0
for img, mask in train_loader:
img, mask = img.to(device), mask.to(device)
logits = model(img)
loss = loss_fn(logits, mask)
opt.zero_grad(); loss.backward(); opt.step()
total += loss.item() * img.size(0)
sched.step()
model.eval()
iou_sum, n = 0.0, 0
with torch.no_grad():
for img, mask in val_loader:
img, mask = img.to(device), mask.to(device)
iou_sum += mIoU_per_class(model(img), mask) * img.size(0)
n += img.size(0)
val_iou = iou_sum / n
print(f"[{name}] ep {ep}/{epochs} loss={total / len(train_loader.dataset):.4f} val_mIoU={val_iou:.4f}")
best_iou = max(best_iou, val_iou)
return best_iou
print(f"準備訓練 DeepLabV3+({sum(p.numel() for p in models['DeepLabV3+'].parameters()) / 1e6:.1f} M)")
# 輸出:準備訓練 DeepLabV3+(22.4 M)
這段把訓練流程包成 train_one 函式,方便三個模型重複使用。DataLoader 用 batch=8 是 Colab T4 的安全設定(DeepLabV3+ 在 batch=8、256×256 約 3 GB VRAM);若想加速可以把 batch 拉到 16 或 24,但要注意 ResNet34 的 encoder 在 256×256 解析度下 VRAM 不會爆炸,瓶頸在 ASPP 與 decoder 的 1/16 解析度特徵。Adam + CosineAnnealingLR 是 smp 官方建議的起點,學習率 1e-3 適合 ImageNet 預訓練的模型;如果改用 SGD(lr=0.01, momentum=0.9) 也是 VOC 的常見選擇,但 Adam 對小資料集更友善。
# 6. 依序訓練三個模型並收集最佳 mIoU
results = {}
for name in ["DeepLabV3+", "U-Net", "FPN"]:
print(f"\n=== {name} ===")
results[name] = train_one(models[name], name, epochs=5)
print("\n各模型最佳驗證 mIoU:")
for name, score in results.items():
print(f" {name:14s} {score:.4f}")
# 輸出(實際數字會略有不同):
# === DeepLabV3+ ===
# [DeepLabV3+] ep 1/5 loss=1.4521 val_mIoU=0.4123
# [DeepLabV3+] ep 2/5 loss=1.0823 val_mIoU=0.5204
# [DeepLabV3+] ep 3/5 loss=0.8732 val_mIoU=0.5881
# [DeepLabV3+] ep 4/5 loss=0.7451 val_mIoU=0.6238
# [DeepLabV3+] ep 5/5 loss=0.6648 val_mIoU=0.6456
# === U-Net ===
# [U-Net] ep 1/5 loss=1.5102 val_mIoU=0.3982
# [U-Net] ep 5/5 loss=0.7104 val_mIoU=0.6125
# === FPN ===
# [FPN] ep 1/5 loss=1.4881 val_mIoU=0.3842
# [FPN] ep 5/5 loss=0.7402 val_mIoU=0.6018
訓練 5 epoch 後三個模型的驗證 mIoU 大致為 DeepLabV3+ ≈ 0.64、U-Net ≈ 0.61、FPN ≈ 0.60(實際數字會略有不同)。這個排序與 VOC benchmark 的文獻結論一致:DeepLabV3+ 略優於 U-Net 與 FPN。差距看起來不大(約 3–4 個百分點),但放在全量 VOC(10K+ 影像)與 30+ epoch 的設定下,DeepLabV3+ 通常能比 U-Net 多 5–10 個百分點。本篇的子集較小、訓練輪數較少,所以差距被壓縮;如果想更公平比較,可以把 epoch 拉長到 20 或使用全量 VOC。
# 7. 用最佳模型(DeepLabV3+)對單張影像推論,視覺化 VOC 21 類預測
import matplotlib.pyplot as plt
model = models["DeepLabV3+"].to(device).eval()
COLORS = (np.random.rand(21, 3) * 255).astype(np.uint8) # 21 類隨機配色
img_t, mask_t = VOCSeg(val_ids, img_size=256)[2]
with torch.no_grad():
pred = model(img_t.unsqueeze(0).to(device)).argmax(dim=1)[0].cpu().numpy()
gt = mask_t.numpy()
fig, axes = plt.subplots(1, 3, figsize=(12, 4))
axes[0].imshow(np.clip((img_t.permute(1, 2, 0).numpy() * np.array([0.229, 0.224, 0.225]) + np.array([0.485, 0.456, 0.406])), 0, 1))
axes[0].set_title("原圖"); axes[0].axis("off")
axes[1].imshow(COLORS[gt]); axes[1].set_title("GT (21 類)"); axes[1].axis("off")
axes[2].imshow(COLORS[pred]); axes[2].set_title("DeepLabV3+ 預測"); axes[2].axis("off")
plt.tight_layout()
plt.savefig("/content/voc_deeplabv3plus_pred.png", dpi=120)
print("預測結果已存到 /content/voc_deeplabv3plus_pred.png")
# 輸出:預測結果已存到 /content/voc_deeplabv3plus_pred.png
這段把驗證集第 3 張影像拿來視覺化:左邊是反標準化後的原圖、中間是 ground truth 的 21 類彩色遮罩、右邊是 DeepLabV3+ 的預測。從圖中可以看到 DeepLabV3+ 大致抓到了物件的主要輪廓(飛機、車、人等),但小物體(背景中的腳踏車)有時會被合併到大類別或遺漏。這也是 Day 24 會深入討論的「分割評估與後處理」議題——除了 mIoU,還需要看混淆矩陣與每一類別的 IoU 才能完整診斷模型。
常見錯誤與踩雷
錯誤一:activation 設成 "softmax2d" 又餵 CrossEntropyLoss。smp 0.3 的 activation 預設是 None(輸出 logits),這時可以安全地餵 nn.CrossEntropyLoss 與 smp 的 DiceLoss(from_logits=True)。如果你寫成 activation="softmax2d",模型會在最後一層做 softmax 才輸出,CrossEntropyLoss 內部又做一次 softmax,就會發生「重複 softmax → 數值不穩定、loss 變 nan」的情況。對應排查方向:訓練時 activation=None;推論時再用 nn.functional.softmax(model(x), dim=1) 手動加 softmax,或者輸出 logits 後用 argmax(dim=1) 直接取類別。
錯誤二:encoder_weights="imagenet" 但網路不通。smp 首次載入時會從 GitHub releases 下載 encoder 的 pretrained weights(ResNet34 約 80 MB、EfficientNet-B5 約 120 MB)。Colab 環境通常網路通暢,但如果你在中國大陸網路或受限企業網路下執行,可能會被 GitHub releases 擋住,模型會變成「隨機初始化」且 mIoU 永遠卡在 0.1 附近。對應排查方向:執行 smp.encoders.get_encoder("resnet34", encoder_weights="imagenet") 時若有 URLError,先把權重下載到 ~/.cache/torch/hub/checkpoints/ 或自己架 mirror。
錯誤三:把 ImageSets/Segmentation/train.txt 與 ImageSets/Main/train.txt 搞混。VOC 2012 同時提供 detection 用的 ImageSets/Main/train.txt(含 17,125 張「有標 bounding box 的影像」,但很多沒有 segmentation 標註)與 segmentation 用的 ImageSets/Segmentation/train.txt(1,464 張「同時有 segmentation 標註的影像」)。如果用錯清單,Image.open(... / "SegmentationClass" / f"{iid}.png") 會找不到檔案而拋 FileNotFoundError。對應排查方向:永遠用 ImageSets/Segmentation/train.txt 與 val.txt 來讀 segmentation 資料集。
錯誤四:mask 忘記轉成 long。CrossEntropyLoss 預期 target 是 long 型別(類別索引);如果你的 mask_t 是 float 或 uint8,會直接報 RuntimeError: expected scalar type Long but found Float。對應排查方向:在 __getitem__ 裡明確寫 torch.from_numpy(np.asarray(mask, dtype=np.int64)).long()。np.int64 與 PyTorch 的 long 對應。
錯誤五:用 timm 的 backbone 但 timm 版本太舊。smp 0.3 要求 timm 版本 ≥ 0.9;如果你裝了 timm 0.6 等舊版本,encoder_name="tu-resnest50d" 這類 timm-only encoder 會直接報 ValueError: Unknown encoder。對應排查方向:pip install timm==1.0.9,並用 smp 0.3 文件列出的 encoder 名稱(多數帶 "tu-" 前綴表示 timm 版本)。
效能與實務提醒
在 Colab T4 上用 200 張 VOC 子集(256×256、batch=8)訓練 5 epoch:DeepLabV3+ 約 8 分鐘、U-Net 約 10 分鐘、FPN 約 8 分鐘。三個模型加起來約 30 分鐘,仍在 Colab 免費版的單次 session 範圍內。如果把 batch 拉到 16 或 24,DeepLabV3+ 每 epoch 可從 100 秒壓到 55–65 秒,整體訓練時間可降到 20 分鐘以內。
實務上 VOC 21 類分割的標準訓練設定是「全量 1,464 張訓練影像、batch=16、30 epoch、AdamW(lr=1e-3) + cosine」,在 Colab T4 上約 2.5 小時可達 mIoU 0.78(DeepLabV3+ 與 ResNet101 encoder 的組合);若換成 EfficientNet-B5 encoder,mIoU 可推到 0.80 但 VRAM 會逼近 10 GB。對於「瑕疵分割」(二元)場景,Day 25 會用 smp.DeepLabV3Plus + ResNet34 在 MVTec AD 上訓練,預期 mIoU 可以達到 0.85+。
另一個工程上的提醒:smp 內建的 DiceLoss 對於極度不平衡的資料(例如瑕疵只佔 1%)會回傳 nan 或 0;這時可以在 DiceLoss 內加 smooth=1.0(預設)並搭配 ignore_index=255 過濾 VOC 的邊界標籤。Day 25 處理 MVTec AD 時會示範這個處理方式。
小結
今天用 smp 0.3 在 PASCAL VOC 2012 segmentation 子集上比較了 DeepLabV3+、U-Net、FPN 三個模型。DeepLabV3+ 透過 ASPP 與空洞卷積在不降低解析度的前提下擴大感受野,FPN 透過 top-down 金字塔融合不同尺度的特徵,smp 把這三個現代分割架構包成統一介面(encoder_name、encoder_weights、classes、activation=None),讓我們能在一行程式碼內切換模型與 backbone。200 張訓練 + 5 epoch 的設定下三個模型的 mIoU 分別約 0.64、0.61、0.60,排序與文獻一致。明天會把視角從語意分割切到實例分割,用 torchvision 的 maskrcnn_resnet50_fpn 微調同一份 VOC 子集,學習如何同時輸出邊界框與像素遮罩,並比較 Mask R-CNN 與 Faster R-CNN 的差異。
結語
今天的重點是「把 U-Net 升級到現代語意分割」。我們從空洞卷積、ASPP、FPN 三個關鍵設計開始,理解 DeepLabV3+ 為什麼能在不犧牲解析度的前提下擴大感受野,以及 FPN 如何融合多尺度特徵;接著在 VOC 2012 segmentation 子集上用 smp 0.3 跑 DeepLabV3+、U-Net、FPN 三個模型,在同一個訓練設定下比較 mIoU(DeepLabV3+ ≈ 0.64 > U-Net ≈ 0.61 ≈ FPN ≈ 0.60)。讀完這篇你應該能回答:smp 的 activation=None 為什麼對訓練時的 CrossEntropyLoss 很重要?空洞卷積如何讓骨幹維持解析度?ASPP 的多分支設計解決了什麼問題?明天,我們會把視角從「每個像素分類」轉到「每個實例分割」,用 torchvision 的 maskrcnn_resnet50_fpn 在 VOC 上微調,學習如何同時輸出邊界框與遮罩,並比較 Mask R-CNN 與 Day 15 的 Faster R-CNN 在 API、訓練與輸出上的差異。
延伸資源
- Chen 等人,2018,Encoder-Decoder with Atrous Separable Convolution for Semantic Image Segmentation(DeepLabV3+ 論文,ECCV 2018):
https://arxiv.org/abs/1802.02611,ASPP 與 decoder 設計的原始定義。 - Lin 等人,2017,Feature Pyramid Networks for Object Detection(FPN 原始論文,CVPR 2017):
https://arxiv.org/abs/1612.03144,top-down 與 lateral connection 的通用設計。 - PASCAL VOC 2012 Segmentation 官方網站(自訂學術用途授權):
http://host.robots.ox.ac.uk/pascal/VOC/voc2012/,21 類語意分割資料集與 SegmentationClass 標註規範。 - segmentation_models_pytorch 官方文件(0.3.x,2024):
https://github.com/qubvel/segmentation_models.pytorch,DeepLabV3+、U-Net、FPN 等 9 種以上架構的統一介面與 backbone 清單。 - timm 官方文件(1.0.x,2024):
https://github.com/huggingface/pytorch-image-models,smp 0.3 透過"tu-"前綴可選用的數十種現代 backbone。 - Yu 等人,2017,Dilated Residual Networks(CVPR 2017):
https://arxiv.org/abs/1705.09914,空洞卷積在分類網路中的設計經驗,是 DeepLabV3+ 的骨幹選擇依據。
留言
張貼留言