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與FastRCNNPredictorAPI(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/,本篇示範資料集的來源。
留言
張貼留言