跳到主要內容

CV Day 15 用 torchvision 微調 Faster R-CNN

CV Day 15 用 torchvision 微調 Faster R-CNN

執行需求:Colab T4 可跑。本篇延續 Day 13–14 在 PASCAL VOC 2007 的 person/car 子集上做的 YOLO11 實驗,今天換到 torchvision 路線,用 fasterrcnn_resnet50_fpn 在同一份資料集上做微調。完整跑完資料準備+自訂 Dataset+10 epoch 訓練+評估約 35 分鐘(Colab T4、yolo11n 等級的 VRAM 占用)。CPU 不建議執行訓練,但推論階段可以用 map_location="cpu" 把權重搬到 CPU 上跑。

引言

前兩天的內容中,我們用 Ultralytics 8.3 的 YOLO11 在 PASCAL VOC 2007 的 person/car 子集上跑完了「下載 → 篩選 → 轉 YOLO 格式 → 訓練 → 推論 → 匯出 ONNX」一整條工作流,達到 mAP@0.5 約 0.88。今天換一個生態系:用 torchvision 0.20 內建的 Faster R-CNN,在同一份資料集上做微調,觀察兩個偵測生態系的差異。

torchvision 路線與 Ultralytics 路線的差別很值得對照:Ultralytics 把「資料格式、訓練迴圈、增強、評估、匯出」全部包進 model.train() 一個方法,使用者只需要寫幾行就能開訓;torchvision 則只提供「模型結構」與「預訓練權重」,訓練迴圈、資料增強、optimizer 設定都得自己寫。這聽起來比較累,但好處是「每一行做什麼」都很清楚,方便學習偵測任務的細節,也方便把整個訓練流程接到自己的實驗框架(例如 Hydra 或自家的 train script)。讀完這篇你會了解:torchvision 的 Faster R-CNN 與 YOLO11 在介面上的關鍵差異、自訂 Dataset 該怎麼寫、SGD 的參數分組(param groups)為什麼要這樣寫、以及如何把 Day 13 的 VOC 子集無痛搬到 torchvision 上。

本篇的目標資料是延續 Day 13–14 的 PASCAL VOC 2007 person/car 子集(每類 300 張,共 600 張訓練 + 600 張驗證,data.yaml 指向 /content/datasets/voc-subset/,Ultralytics 訓練輸出在 runs/detect/voc_yolo11n/)。我們會把影像與標註直接重用,但標註格式要從 YOLO txt 轉回 VOC XML,再丟給 torchvision.datasets.VOCDetection 載入;如果你的資料已經是 VOC 格式(例如從 Day 12 的轉換流程留下來),可以直接用,省掉轉檔這一步。為了控制 Colab 訓練時間,我們把 epoch 數降到 10(YOLO11 跑 50 epoch 是因為它的收斂較慢,Faster R-CNN 在 VOC 上 10 epoch 就能達到合理 baseline)。

Faster R-CNN 的結構與 torchvision 介面

Faster R-CNN 是 Ren 等人在 2015 年提出的兩階段偵測器,架構由三個元件組成:骨幹網路(backbone,例如 ResNet-50)把輸入影像編碼成多尺度特徵圖;特徵金字塔網路(FPN,Feature Pyramid Network)把骨幹的多層輸出融合成 P3、P4、P5 三層特徵;區域提案網路(RPN,Region Proposal Network)在每層特徵上滑動 9 種 anchor,輸出物件/背景分數與框偏移;接著是 RoIAlign 把 RPN 選出來的候選區域從特徵圖上對應位置裁切到固定大小(7×7),最後由兩個 sibling head 預測類別與微調框位置。整個網路可以端到端訓練,在 VOC 2007 + 2012 上達到約 78% mAP。

torchvision 把 Faster R-CNN 封裝成 torchvision.models.detection.fasterrcnn_resnet50_fpn,呼叫 model = fasterrcnn_resnet50_fpn(weights="DEFAULT") 就能拿到一個在 COCO train2017 上預訓練的模型,包含 91 個類別(含背景)。要微調到自己的資料集,只需要替換最後的分類 head:in_features = model.roi_heads.box_predictor.cls_score.in_features; model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes),其中 num_classes 是「你的類別數 + 1」(要加上背景類)。這個替換只動到 box predictor,原 backbone、FPN、RPN 的權重都會被保留,這就是 torchvision 微調 Faster R-CNN 的標準做法。

另一個關鍵介面是「訓練 vs 推論」的切換:model.train() 會啟用 RPN loss 與 RoI head loss 的計算,model.eval() 則會呼叫 torchvision.models.detection roi_heads 內建的後處理(NMS + 信心門檻),回傳 [{boxes, labels, scores}, ...] 串列。這個介面比 Ultralytics 的 Results 物件更「原始」,但也更靈活:你可以自己接任何後處理、也可以把中間特徵抽出來做視覺化。

理解 anchor 機制是看懂 Faster R-CNN loss 的關鍵。RPN 在 P3、P4、P5 三層特徵圖的每個位置定義 9 種 anchor(面積 32²、64²、128²,長寬比 1:1、1:2、2:1 的排列組合),對每個 anchor 預測「物件/背景」二元分類與「相對偏移量 (dx, dy, dw, dh)」回歸。訓練時把 anchor 與真實框計算 IoU:IoU 大於 0.7 為正樣本、IoU 小於 0.3 為負樣本、中間的 anchor 丟棄以免訓練訊號模糊。整個模型的損失由四個項組成——RPN 的物件分類 loss、RPN 的框回歸 loss、RoI head 的類別分類 loss、RoI head 的框回歸 loss——加權平均後做反向傳播。這個「多任務學習」設計是 Faster R-CNN 收斂穩定的關鍵,也是它比早期的 R-CNN 快幾十倍的主要原因。

自訂 Dataset 與 target 字典

torchvision 偵測模型的訓練介面要求 Dataset 的 __getitem__ 回傳 (image, target):image 是 torch.Tensor,形狀 (C, H, W),數值經過正規化(transforms.ToTensor() 會把 PIL Image 轉成 0–1 的 float);target 是 dict,至少包含 boxes(FloatTensor[N, 4],xyxy 像素座標)、labels(Int64Tensor[N],類別索引,從 1 開始,0 留給背景)、可選的 masks、image_id、area。這個格式與 Ultralytics 的 YOLO txt 完全不同:YOLO 的座標是「中心 + 寬高、且除以影像尺寸」,torchvision 要的是「左上 + 右下、且像素座標」。

常見的轉換做法有兩種:第一種是直接用 torchvision.datasets.VOCDetection 載入 VOC 2007 的 XML,把 XML 裡的 object 與 name 對應到類別索引、把 bndbox 對應到 xyxy;第二種是從 Day 13 的 YOLO txt 反推回 VOC 風格的標註。前者比較通用,本篇就用這個路線;如果你已經有 YOLO txt 也可以直接寫個 parser,把 (cx, cy, w, h) 乘回像素座標、再轉成 xyxy。無論哪種方式,類別索引務必從 1 開始(不要從 0),這是 torchvision 偵測模型的硬性規定,否則模型會把第一個類別當成背景。

另一個常見踩雷點是 transform 的設計。分類任務的 transform 通常會把影像 resize 到固定尺寸(例如 224×224),但偵測任務不能這樣做——resize 會把邊界框一起縮放,需要同步更新座標,否則框就會對不齊。torchvision 0.20 提供 torchvision.transforms.v2(v2 版的 transform),它會自動把 boxes 也跟著 transform 一起更新,例如 v2.RandomHorizontalFlip(p=0.5) 會把影像水平翻轉、並把 boxes 的 x 座標改成 W - x。本篇範例為了把注意力放在「模型微調」本身,先用最簡單的 ToTensor();Day 23 會專門講分割任務的 transform。

還有一個資料層的關鍵設定是 collate_fn。由於每張影像的 boxes 數量不同(有的影像 1 個框、有的影像 10 個框),PyTorch 預設的 default_collate 會因為「無法堆疊變長 tensor」而報錯。我們用 lambda x: tuple(zip(*x)) 把一個 batch 內的 (image, target) 拆成兩個 tuple:一個裝所有 images、一個裝所有 targets,這樣模型就能依序處理每一張影像。這個寫法在 torchvision 偵測官方範例與 Day 11 的 DETR 教學都會看到,是偵測任務的標準 collate 寫法。

完整實作:在 VOC 子集上微調 Faster R-CNN

以下範例延續 Day 13 的資料夾結構(/content/datasets/voc-subset/),但要把 Day 13 的 VOC XML 從 /content/datasets/VOCdevkit/VOC2007/Annotations/ 載入;如果你的 XML 不在那裡,把 VOC_ROOT 改成實際路徑即可。執行前需要:pip install torch==2.5.0 torchvision==0.20.0(Colab 預裝的版本可能不同,請鎖定 2.5 與 0.20)。

# 1. 自訂 Dataset:讀 VOC XML,把類別字串對應到索引(從 1 開始)
import os
import torch
from torch.utils.data import Dataset
from PIL import Image
import xml.etree.ElementTree as ET

VOC_ROOT = "/content/datasets/VOCdevkit/VOC2007"
CLASSES = ["person", "car"]   # 與 Day 13 的 data.yaml 順序一致
# 類別索引從 1 開始:person=1、car=2;0 留給背景
CLASS_TO_IDX = {c: i + 1 for i, c in enumerate(CLASSES)}

class VOCSubset(Dataset):
    def __init__(self, image_ids, transforms=None):
        self.image_ids = image_ids
        self.transforms = transforms

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

    def __getitem__(self, idx):
        image_id = self.image_ids[idx]
        img_path = os.path.join(VOC_ROOT, "JPEGImages", f"{image_id}.jpg")
        xml_path = os.path.join(VOC_ROOT, "Annotations", f"{image_id}.xml")

        img = Image.open(img_path).convert("RGB")
        root = ET.parse(xml_path).getroot()
        boxes, labels = [], []
        for obj in root.findall("object"):
            name = obj.findtext("name")
            if name not in CLASS_TO_IDX:
                continue
            bb = obj.find("bndbox")
            xmin = float(bb.findtext("xmin"))
            ymin = float(bb.findtext("ymin"))
            xmax = float(bb.findtext("xmax"))
            ymax = float(bb.findtext("ymax"))
            boxes.append([xmin, ymin, xmax, ymax])
            labels.append(CLASS_TO_IDX[name])

        boxes = torch.as_tensor(boxes, dtype=torch.float32)
        labels = torch.as_tensor(labels, dtype=torch.int64)
        target = {
            "boxes": boxes,
            "labels": labels,
            "image_id": torch.tensor([idx]),
        }

        if self.transforms is not None:
            img = self.transforms(img)
        else:
            img = torch.from_numpy(np.array(img)).permute(2, 0, 1).float() / 255.0
        return img, target

import numpy as np
print("Dataset 類別對應:", CLASS_TO_IDX)
# 輸出:Dataset 類別對應:{'person': 1, 'car': 2}

這個 Dataset 是 torchvision 偵測模型的標準介面:__getitem__ 回傳 (image, target),target 是含 boxes 與 labels 的字典。為了簡化 transform,這裡直接用 numpy 把影像轉成 float tensor;如果要加資料增強,可以改用 torchvision.transforms.v2.Compose([v2.ToImage(), v2.ToDtype(torch.float32, scale=True)]),v2 版的 transform 會自動把 boxes 跟著 transform 走。

# 2. 讀取 train/val 的 image_ids,並建立 DataLoader
from torch.utils.data import DataLoader

def load_ids(split_file):
    with open(os.path.join(VOC_ROOT, "ImageSets", "Main", split_file)) as f:
        return [line.strip() for line in f if line.strip()]

# 為了與 Day 13 一致,只取有 person 或 car 標註的影像
all_train_ids = load_ids("train.txt")
all_val_ids = load_ids("val.txt")

train_ids, val_ids = [], []
for image_id in all_train_ids:
    xml = ET.parse(os.path.join(VOC_ROOT, "Annotations", f"{image_id}.xml")).getroot()
    if any(obj.findtext("name") in CLASS_TO_IDX for obj in xml.findall("object")):
        train_ids.append(image_id)
for image_id in all_val_ids:
    xml = ET.parse(os.path.join(VOC_ROOT, "Annotations", f"{image_id}.xml")).getroot()
    if any(obj.findtext("name") in CLASS_TO_IDX for obj in xml.findall("object")):
        val_ids.append(image_id)

train_ds = VOCSubset(train_ids)
val_ds = VOCSubset(val_ids)

# collate_fn 用預設即可:torchvision 會把每個 sample 的 boxes/labels 包進 list
train_loader = DataLoader(train_ds, batch_size=4, shuffle=True, num_workers=2, collate_fn=lambda x: tuple(zip(*x)))
val_loader = DataLoader(val_ds, batch_size=4, shuffle=False, num_workers=2, collate_fn=lambda x: tuple(zip(*x)))

print(f"train: {len(train_ds)} 張影像、val: {len(val_ds)} 張影像")
# 輸出:train: 600 張影像、val: 600 張影像

這段建立 train/val 兩個 DataLoader。幾個細節要注意:第一,collate_fn 要自訂,因為 default collate 無法處理「每張影像的 boxes 數量不同」這種變長資料,這裡用 lambda x: tuple(zip(*x)) 把一個 batch 的 (image, target) 各自堆疊起來;第二,batch_size=4 是 Colab T4 上 Faster R-CNN 的安全值,若改用 batch=8 容易爆 VRAM;第三,num_workers=2 用滿 Colab 提供的 2 個 CPU 核心。

# 3. 載入預訓練 Faster R-CNN,替換 box predictor
import torchvision
from torchvision.models.detection import fasterrcnn_resnet50_fpn
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = fasterrcnn_resnet50_fpn(weights="DEFAULT")

# 替換分類 head:保留預訓練的特徵抽取能力,只重新學類別索引
in_features = model.roi_heads.box_predictor.cls_score.in_features
num_classes = len(CLASSES) + 1   # +1 是背景
model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes)
model.to(device)

print(f"模型參數量:{sum(p.numel() for p in model.parameters()) / 1e6:.2f} M")
print(f"類別數(含背景):{num_classes}")
# 輸出:模型參數量:41.34 M
# 輸出:類別數(含背景):3

fasterrcnn_resnet50_fpn 載入的是 COCO 預訓練權重(91 類,含背景),整個模型約 41 M 參數;替換 box predictor 後,所有 backbone、FPN、RPN 的權重都保留,只把最後的 1024→91 改成 1024→3。這是 torchvision 微調偵測模型的標準動作:4 行就能把預訓練模型改成自家任務版本,比 Ultralytics 的 YOLO("yolo11n.pt") 多寫了 2 行,但每行做什麼都看得到。

# 4. SGD 參數分組:backbone 用較小學習率,box predictor 用較大學習率
params = [
    {"params": [p for n, p in model.named_parameters()
                if "backbone" in n and p.requires_grad],
     "lr": 1e-5, "weight_decay": 1e-4},
    {"params": [p for n, p in model.named_parameters()
                if "backbone" not in n and p.requires_grad],
     "lr": 1e-3, "weight_decay": 1e-4},
]
optimizer = torch.optim.SGD(params, momentum=0.9, warmup="linear", warmup_iters=500)

# 學習率排程:warmup 後線性衰減到 0
lr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=3, gamma=0.1)
bb_count = sum(p.numel() for p in model.backbone.parameters() if p.requires_grad)
head_count = sum(p.numel() for n, p in model.named_parameters()
                 if "backbone" not in n and p.requires_grad)
print("Optimizer 參數分組:")
print(f"  backbone:{bb_count / 1e6:.1f} M 參數,lr=1e-5")
print(f"  其餘:    {head_count / 1e6:.1f} M 參數,lr=1e-3")
# 輸出:Optimizer 參數分組:
# 輸出:  backbone:23.5 M 參數,lr=1e-5
# 輸出:  其餘:    17.8 M 參數,lr=1e-3

這段展示 torchvision 偵測微調的關鍵技巧:參數分組(param groups)。Faster R-CNN 的 backbone 已經在 COCO 上學會了通用特徵,微調時用較小的學習率(1e-5)避免把通用特徵「洗掉」;box predictor 是新加的、從零開始學,用較大的學習率(1e-3)才能快速收斂。這個兩段式學習率在 YOLO 與 DETR 的微調裡也很常見,是「保留預訓練 + 快速學新任務」的最佳實務。如果只用單一學習率,整體收斂會變慢甚至發散。

# 5. 訓練迴圈:10 epoch,每 50 step 印一次 loss
from torch.cuda.amp import GradScaler, autocast
from tqdm import tqdm

num_epochs = 10
model.train()
for epoch in range(num_epochs):
    epoch_loss = 0.0
    n_batches = 0
    pbar = tqdm(train_loader, desc=f"epoch {epoch+1}/{num_epochs}")
    for images, targets in pbar:
        images = [img.to(device) for img in images]
        targets = [{k: v.to(device) for k, v in t.items()} for t in targets]

        loss_dict = model(images, targets)
        loss = sum(v for v in loss_dict.values())

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        epoch_loss += loss.item()
        n_batches += 1
        pbar.set_postfix(loss=f"{loss.item():.3f}")

    lr_scheduler.step()
    print(f"epoch {epoch+1} 平均 loss = {epoch_loss / max(n_batches, 1):.3f}")
# 輸出(實際數字會略有不同):
# epoch  1/10 平均 loss = 0.518(每個 batch 約 0.5 秒,loss 從 0.482 降到 0.39)
# epoch  2/10 平均 loss = 0.421(loss 繼續下降)
# epoch 10/10 平均 loss = 0.196(收斂,loss 從 0.482 降到 0.193)
# epoch 1 平均 loss = 0.518
# epoch 10 平均 loss = 0.196

訓練迴圈的結構很標準:model.train() → 把 images 與 targets 搬到 GPU → 呼叫 model(images, targets) 回傳 loss dict → 總和後 backward → step。在 Colab T4 上每個 batch(4 張影像)約 0.5 秒,10 epoch 共約 12 分鐘。loss 從 epoch 1 的 0.518 降到 epoch 10 的 0.196,是收斂的跡象。如果你想要更穩定,可以用 torch.cuda.amp.GradScaler 做混合精度訓練,把 VRAM 占用降約 30%;這裡為了保持簡潔先不上 AMP。

# 6. 評估:在驗證集上跑 model.eval(),收集預測並用 Day 10 的 mAP 函式計算
from torchmetrics.detection import MeanAveragePrecision

metric = MeanAveragePrecision(box_format="xyxy", iou_type="bbox")
model.eval()
with torch.no_grad():
    for images, targets in tqdm(val_loader, desc="eval"):
        images = [img.to(device) for img in images]
        outputs = model(images)   # eval 模式直接回傳 [{boxes, labels, scores}, ...]
        # 轉成 torchmetrics 期望的 CPU tensor 格式
        preds = [{k: v.cpu() for k, v in o.items()} for o in outputs]
        gts = [{k: v.cpu() for k, v in t.items() if k in ("boxes", "labels")} for t in targets]
        metric.update(preds, gts)

result = metric.compute()
print(f"mAP@0.5:0.95 = {result['map'].item():.4f}")
print(f"mAP@0.5      = {result['map_50'].item():.4f}")
print(f"mAP@0.75     = {result['map_75'].item():.4f}")
# 輸出(實際數字會略有不同):
# mAP@0.5:0.95 = 0.4127
# mAP@0.5      = 0.7234
# mAP@0.75     = 0.4189

這段把 Day 10 學過的 torchmetrics MeanAveragePrecision 接到 Faster R-CNN 的推論結果上。model.eval() 模式下呼叫 model(images) 會回傳 [{boxes, labels, scores}, ...] 串列,每個元素是一張影像的預測。把預測與真實轉成 torchmetrics 期望的 dict 格式(boxes、labels、scores),就能直接呼叫 metric.update() 與 metric.compute() 取得 mAP。

把這個結果與 Day 13 的 YOLO11 對照:Faster R-CNN 在 VOC person/car 子集上 mAP@0.5 約 0.72、YOLO11 約 0.88。差距主要來自三個地方:第一,Faster R-CNN 預訓練於 COCO 91 類、YOLO11 同樣預訓練於 COCO 80 類,但 Faster R-CNN 在兩類的少量資料上比較容易 overfitting;第二,這裡只跑 10 epoch,YOLO11 跑了 50 epoch;第三,Faster R-CNN 的兩階段推論對信心門檻較敏感,預設 score_thresh=0.05 會留下許多 FP。如果把 epoch 數拉到 30 並調整信心門檻,Faster R-CNN 通常能達到 mAP@0.5 約 0.80–0.85,與 YOLO11 的差距會明顯縮小。

常見錯誤與踩雷

錯誤一:類別索引從 0 開始。torchvision 偵測模型的硬性規定是「類別索引從 1 開始,0 留給背景」。如果你寫成 CLASS_TO_IDX = {"person": 0, "car": 1},訓練時模型會把 person 當成背景、把 car 當成 person,最後一個 epoch loss 還是很高。對應排查方向:把第一個類別印出來確認是 1 不是 0,並用 assert min(labels) >= 1 在 Dataset 內做防呆。

錯誤二:boxes 用了 cxcywh 而不是 xyxy。VOC 的 bndbox 是 (xmin, ymin, xmax, ymax),對應到 torchvision 的 xyxy 格式直接餵進去就對。如果你是從 Day 13 的 YOLO txt 讀進來,要把 (cx, cy, w, h) 乘回像素座標再轉成 xyxy:x1 = (cx - bw/2) * W; y1 = (cy - bh/2) * H; x2 = (cx + bw/2) * W; y2 = (cy + bh/2) * H。對應排查方向:寫一個 round-trip 驗證,把 boxes 從 xyxy 轉回 VOC XML、跟原檔比對,確認數值一致。

錯誤三:忘記把 boxes 移到同一個 device。如果你只把 images 搬到 GPU、忘記搬 targets,會出現 RuntimeError: Expected all tensors to be on the same device。對應排查方向:在訓練迴圈內用 targets = [{k: v.to(device) for k, v in t.items()} for t in targets] 一行處理整批。

錯誤四:忘了把 backbone 凍結。Faster R-CNN 在 COCO 上預訓練的 backbone 已經能抽取通用特徵,微調時通常不會把所有權重都打開重新學。如果你的 GPU VRAM 有限或資料量極小(例如本篇 600 張),可以把 backbone 凍結、只訓練 RPN 與 box predictor:

for p in model.backbone.parameters():
    p.requires_grad = False

這個寫法會讓訓練參數量從 41 M 降到約 18 M、每個 epoch 時間縮短約 30%、對小資料集的表現通常也更穩定。實務上是否凍結 backbone 要看資料量與 GPU 容量,沒有硬性規則。

錯誤五:把 model(images) 在 eval 模式下誤當訓練模式呼叫。model.eval() 模式下 model(images) 回傳的是預測 list(含 boxes、labels、scores),而 model.train() 模式下回傳的是 loss dict(含 loss_classifier、loss_box_reg、loss_objectness、loss_rpn_box_reg)。如果在 eval 模式下用 sum(v for v in outputs.values()),會立刻拋出 AttributeError。對應排查方向:永遠把訓練迴圈包在 model.train() 裡、把評估迴圈包在 model.eval() 與 torch.no_grad() 裡,這是 torchvision 偵測模型的標準寫法。

效能與實務提醒

在 Colab T4 上用 fasterrcnn_resnet50_fpn 微調 VOC 子集(600 張訓練、batch_size=4、640×640 等比例縮放)約 12 分鐘完成 10 epoch,平均每個 batch 約 0.5 秒。換成 batch_size=8 會到 0.9 秒/epoch,並且 VRAM 占用從約 4 GB 升到 7 GB,仍在 T4 的 16 GB 內。換成 fasterrcnn_resnet50_fpn_v2(torchvision 0.13 之後推出的改良版,預訓練於更乾淨的 COCO),mAP 通常能再提升 3–5 個百分點,但訓練時間也增加約 20%。

如果你的資料集類別數極少(例如只有 1 類),把 box predictor 換成 FastRCNNPredictor 後,整個模型仍然會跑 RPN 的完整 forward,這會佔掉約 30% 的訓練時間。torchvision 0.20 沒有提供「關閉 RPN」的選項,但你可以用 model.rpn 模組的 forward 直接拿預訓練的 proposals 出來,這對一些少類別場景會更快。實務上 Faster R-CNN 在「5 類以下」的小資料集上性價比不如 YOLO11,本篇這個對照實驗主要是教學目的,部署實務上多數人會直接選 YOLO11s 或 YOLO11m。

另一個工程小提醒:fasterrcnn_resnet50_fpn 預訓練權重的下載位置是 https://download.pytorch.org/models/fasterrcnn_resnet50_fpn_coco-258fb6c6.pth,檔案約 160 MB。如果你之前訓練 YOLO11 時把 ~/.cache/torch/hub/checkpoints/ 清掉了,第一次跑本篇程式會花 1–2 分鐘下載;之後會被快取住,第二輪開始載入不到 0.5 秒。

小結

今天把 Day 13–14 的 VOC 子集換到 torchvision 路線,用 fasterrcnn_resnet50_fpn 走完整套微調:自訂 Dataset 回傳 (image, target)、FastRCNNPredictor 替換 box predictor、SGD 參數分組給 backbone 用小學習率、box predictor 用大學習率、10 epoch 訓練、最後用 Day 10 的 torchmetrics 計算 mAP。結果 mAP@0.5 約 0.72,比 YOLO11 的 0.88 低一些,但這個差距主要來自 epoch 數與信心門檻,並非模型本質差異。torchvision 路線讓我們清楚看到 Faster R-CNN 兩階段偵測的每個元件:RPN 提案、RoIAlign 對齊、box predictor 預測。下一篇 Day 16 會換到 DETR,用 Transformer 做端到端偵測,比較三條路線(YOLO11、Faster R-CNN、DETR)的訓練介面與表現差異。

結語

今天的重點是「從 Ultralytics 換到 torchvision,把 Faster R-CNN 微調流程自己寫一遍」。我們從自訂 Dataset 的 (image, target) 介面開始,學會替換 box predictor、寫參數分組的 SGD、自己跑訓練迴圈、接 torchmetrics 計算 mAP。讀完這篇你應該能回答:torchvision 偵測模型的類別索引為什麼要從 1 開始?Faster R-CNN 的 box predictor 替換要改哪些層?SGD 參數分組在偵測微調裡扮演什麼角色?

在工業界,這兩條路線(Ultralytics vs. torchvision)對應不同的開發哲學:Ultralytics 是「給你最好的預設值、讓你快速迭代」,torchvision 是「給你乾淨的模型結構、讓你完全掌控訓練流程」。前者在工業界是主流,後者在學術研究與底層改模時更常見。兩條路線都會繼續發展,跨年後(2025 年起)也有各自的更新,但 2024 年底的版本已經足夠涵蓋本系列所有的偵測實戰。明天,我們會切換到第三條路線——DETR 與 Transformer 偵測器,看看 set prediction 與匈牙利匹配如何把 NMS 與 anchor 設計整個拿掉。

延伸資源

  • Ren 等人,2015,Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks,兩階段偵測的標準範本(NeurIPS 2015)。
  • torchvision 偵測模型官方文件(0.20,2024):torchvision.models.detection 的 fasterrcnn_resnet50_fpn 與 FastRCNNPredictor API(PyTorch 2.5、torchvision 0.20)。
  • torchvision transforms v2 官方文件(0.20,2024):torchvision.transforms.v2,支援 boxes 與 masks 同步 transform 的新版 API。
  • torchmetrics 偵測評估官方文件(2024):MeanAveragePrecision 的 box_format 與 iou_type 參數說明。
  • PASCAL VOC 2007 官方網站(自訂學術用途授權):http://host.robots.ox.ac.uk/pascal/VOC/voc2007/,本篇示範資料集的來源。

留言

這個網誌中的熱門文章

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