R-CNN 物件偵測
以下以「行人偵測」為案例,使用 **Penn-Fudan Pedestrian Dataset**,並以 PyTorch/torchvision 建立 **Faster R-CNN**。Faster R-CNN 是 R-CNN 系列的改良版本,適合實務使用,並且 torchvision 已提供完整模型與訓練介面。
---
## 1. 實務案例與資料集
### 案例
輸入街景影像,偵測影像中的行人:
- 類別 0:背景
- 類別 1:person
Penn-Fudan 資料集包含約 170 張行人影像,每張影像附有像素級遮罩。可以從遮罩計算每個行人的 bounding box。
資料夾結構:
```text
PennFudanPed/
├── PNGImages/
│ ├── FudanPed00001.png
│ └── ...
└── PedMasks/
├── FudanPed00001_mask.png
└── ...
```
安裝套件:
```bash
pip install torch torchvision torchmetrics pycocotools
```
---
## 2. Dataset 實作
```python
import os
import glob
import numpy as np
import torch
from PIL import Image
from torch.utils.data import Dataset
class PennFudanDataset(Dataset):
def __init__(self, root, train=True, indices=None):
self.root = root
self.image_paths = sorted(
glob.glob(os.path.join(root, "PNGImages", "*.png"))
)
if indices is not None:
self.image_paths = [self.image_paths[i] for i in indices]
self.train = train
def __len__(self):
return len(self.image_paths)
def __getitem__(self, idx):
image_path = self.image_paths[idx]
image = Image.open(image_path).convert("RGB")
file_name = os.path.basename(image_path)
mask_name = file_name.replace(".png", "_mask.png")
mask_path = os.path.join(self.root, "PedMasks", mask_name)
mask = np.array(Image.open(mask_path))
# mask 中 0 是背景,其他數值代表不同物件
object_ids = np.unique(mask)
object_ids = object_ids[object_ids != 0]
boxes = []
for object_id in object_ids:
ys, xs = np.where(mask == object_id)
xmin = xs.min()
xmax = xs.max()
ymin = ys.min()
ymax = ys.max()
boxes.append([xmin, ymin, xmax + 1, ymax + 1])
boxes = torch.as_tensor(boxes, dtype=torch.float32)
# 此案例只有一種前景類別:person
labels = torch.ones((len(boxes),), dtype=torch.int64)
image_id = torch.tensor([idx])
area = (boxes[:, 3] - boxes[:, 1]) * \
(boxes[:, 2] - boxes[:, 0])
iscrowd = torch.zeros((len(boxes),), dtype=torch.int64)
target = {
"boxes": boxes,
"labels": labels,
"image_id": image_id,
"area": area,
"iscrowd": iscrowd
}
# 將 PIL Image 轉成 [C, H, W]、值域為 [0, 1] 的 Tensor
image = torch.from_numpy(
np.array(image)
).permute(2, 0, 1).float() / 255.0
# 只在訓練資料使用水平翻轉
if self.train and torch.rand(1).item() < 0.5:
_, _, width = image.shape
image = torch.flip(image, dims=[2])
old_boxes = boxes.clone()
boxes[:, 0] = width - old_boxes[:, 2]
boxes[:, 2] = width - old_boxes[:, 0]
target["boxes"] = boxes
return image, target
```
### 為什麼需要 `collate_fn`?
不同影像中的物件數量不同,因此每張影像的 bounding box 數量不一定相同,不能直接堆疊成固定大小的 Tensor。
```python
def collate_fn(batch):
return tuple(zip(*batch))
```
---
## 3. 建立訓練集與驗證集
```python
from torch.utils.data import DataLoader
import torch
data_root = "./PennFudanPed"
# 先建立完整資料集以取得資料數量
full_dataset = PennFudanDataset(
root=data_root,
train=True
)
num_samples = len(full_dataset)
indices = torch.randperm(num_samples).tolist()
split = int(0.8 * num_samples)
train_indices = indices[:split]
val_indices = indices[split:]
train_dataset = PennFudanDataset(
root=data_root,
train=True,
indices=train_indices
)
val_dataset = PennFudanDataset(
root=data_root,
train=False,
indices=val_indices
)
train_loader = DataLoader(
train_dataset,
batch_size=2,
shuffle=True,
num_workers=2,
collate_fn=collate_fn
)
val_loader = DataLoader(
val_dataset,
batch_size=1,
shuffle=False,
num_workers=2,
collate_fn=collate_fn
)
```
實際專案中,資料量較小時也可以使用:
- 70% 訓練、15% 驗證、15% 測試
- 或使用 5-fold cross-validation
- 避免同一個場景或同一個人同時出現在訓練與測試資料中
---
## 4. 建立 Faster R-CNN 模型
Faster R-CNN 的主要結構如下:
```text
輸入影像
↓
Backbone,例如 ResNet-50 + FPN
↓
RPN,產生候選區域
↓
RoI Align
↓
分類器與 bounding box regression head
↓
類別、信心分數、預測框
```
使用 COCO 預訓練權重,可以加速收斂。
```python
import torchvision
from torchvision.models.detection import (
fasterrcnn_resnet50_fpn,
FasterRCNN_ResNet50_FPN_Weights
)
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor
device = torch.device(
"cuda" if torch.cuda.is_available() else "cpu"
)
weights = FasterRCNN_ResNet50_FPN_Weights.DEFAULT
model = fasterrcnn_resnet50_fpn(
weights=weights
)
# COCO 預訓練模型的分類 head 類別數量不同,
# 必須替換成符合本案例的類別數
num_classes = 2 # background + person
in_features = model.roi_heads.box_predictor.cls_score.in_features
model.roi_heads.box_predictor = FastRCNNPredictor(
in_features,
num_classes
)
model.to(device)
```
### 類別數設定
如果有 `N` 個實際物件類別,則:
```python
num_classes = N + 1
```
因為 Faster R-CNN 需要額外保留一個 background 類別。
---
## 5. 設定 Optimizer 與 Learning Rate Scheduler
```python
import torch.optim as optim
params = [
p for p in model.parameters()
if p.requires_grad
]
optimizer = optim.SGD(
params,
lr=0.005,
momentum=0.9,
weight_decay=0.0005
)
lr_scheduler = optim.lr_scheduler.StepLR(
optimizer,
step_size=3,
gamma=0.1
)
```
常見設定:
| 設定 | 說明 |
|---|---|
| `lr=0.005` | 小型資料集的起始 learning rate |
| `momentum=0.9` | 穩定 SGD 更新 |
| `weight_decay=0.0005` | 降低 overfitting |
| `step_size=3` | 每 3 個 epoch 降低 learning rate |
| `gamma=0.1` | learning rate 乘以 0.1 |
---
## 6. 模型訓練
torchvision 的 Faster R-CNN 在訓練模式下,輸入 image 與 target 後會回傳多個 loss,例如:
- `loss_classifier`
- `loss_box_reg`
- `loss_objectness`
- `loss_rpn_box_reg`
```python
num_epochs = 10
for epoch in range(num_epochs):
model.train()
epoch_loss = 0.0
for images, targets in train_loader:
images = [
image.to(device)
for image in images
]
targets = [
{
key: value.to(device)
for key, value in target.items()
}
for target in targets
]
# Faster R-CNN 回傳 loss dictionary
loss_dict = model(images, targets)
losses = sum(
loss for loss in loss_dict.values()
)
optimizer.zero_grad()
losses.backward()
# 可選:避免梯度過大
torch.nn.utils.clip_grad_norm_(
model.parameters(),
max_norm=5.0
)
optimizer.step()
epoch_loss += losses.item()
lr_scheduler.step()
average_loss = epoch_loss / len(train_loader)
print(
f"Epoch [{epoch + 1}/{num_epochs}], "
f"Loss: {average_loss:.4f}"
)
```
### 訓練時的重要事項
1. `model.train()` 會讓模型回傳 loss。
2. `images` 必須是影像 Tensor 的 list。
3. `targets` 也必須是 dictionary 的 list。
4. 每個 target 至少需要:
- `boxes`
- `labels`
5. bounding box 格式為:
```text
[x_min, y_min, x_max, y_max]
```
6. `x_max` 必須大於 `x_min`,`y_max` 必須大於 `y_min`。
---
## 7. 使用 mAP 評估模型
物件偵測通常使用 mAP,而不是單純 accuracy。
常見指標:
- `mAP`:IoU 0.5 到 0.95 的平均結果
- `mAP@0.5`:IoU 閾值為 0.5
- `Recall`
- `Precision`
使用 `torchmetrics`:
```python
from torchmetrics.detection.mean_ap import MeanAveragePrecision
def evaluate(model, data_loader, device):
model.eval()
metric = MeanAveragePrecision(
box_format="xyxy",
iou_type="bbox"
)
with torch.no_grad():
for images, targets in data_loader:
images = [
image.to(device)
for image in images
]
predictions = model(images)
predictions_cpu = [
{
key: value.cpu()
for key, value in prediction.items()
}
for prediction in predictions
]
targets_cpu = [
{
key: value.cpu()
for key, value in target.items()
}
for target in targets
]
metric.update(
predictions_cpu,
targets_cpu
)
results = metric.compute()
return results
```
執行評估:
```python
results = evaluate(
model,
val_loader,
device
)
print("mAP:", results["map"].item())
print("mAP@0.5:", results["map_50"].item())
print("mAP@0.75:", results["map_75"].item())
```
### IoU 定義
對預測框與真實框:
```text
IoU = 預測框與真實框的交集面積
/ 預測框與真實框的聯集面積
```
例如:
- IoU ≥ 0.5,通常視為偵測成功
- IoU 越高,代表框的位置越準確
---
## 8. 儲存最佳模型
可以根據驗證集的 `mAP@0.5` 儲存最佳模型。
```python
best_map = 0.0
for epoch in range(num_epochs):
model.train()
for images, targets in train_loader:
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)
losses = sum(loss_dict.values())
optimizer.zero_grad()
losses.backward()
optimizer.step()
lr_scheduler.step()
results = evaluate(
model,
val_loader,
device
)
current_map = results["map_50"].item()
print(
f"Epoch {epoch + 1}: "
f"mAP@0.5 = {current_map:.4f}"
)
if current_map > best_map:
best_map = current_map
torch.save(
{
"epoch": epoch,
"model_state_dict": model.state_dict(),
"optimizer_state_dict": optimizer.state_dict(),
"map_50": current_map
},
"best_fasterrcnn_pedestrian.pth"
)
print("已儲存最佳模型")
```
---
## 9. 推論與信心分數過濾
載入模型後,可以對單張影像進行偵測:
```python
model.eval()
image, target = val_dataset[0]
with torch.no_grad():
prediction = model([
image.to(device)
])[0]
scores = prediction["scores"].cpu()
boxes = prediction["boxes"].cpu()
labels = prediction["labels"].cpu()
# 只保留信心分數大於 0.5 的結果
keep = scores >= 0.5
boxes = boxes[keep]
scores = scores[keep]
labels = labels[keep]
print("預測框:", boxes)
print("信心分數:", scores)
print("類別:", labels)
```
實務上通常會設定:
```python
confidence_threshold = 0.5
```
如果希望減少誤報,可以提高到 0.7;如果希望提高召回率,可以降低到 0.3 或 0.4。
---
## 10. 關鍵步驟總結
### 1. 準備標註資料
Faster R-CNN 不直接使用 mask 作為訓練標註,而是需要:
```python
target = {
"boxes": Tensor[N, 4],
"labels": Tensor[N],
"image_id": Tensor[1],
"area": Tensor[N],
"iscrowd": Tensor[N]
}
```
### 2. 影像與 bounding box 同步增強
如果對影像進行水平翻轉,bounding box 也必須同步更新,否則標註會錯位。
### 3. 替換分類 head
預訓練模型原本針對 COCO 類別,必須替換:
```python
model.roi_heads.box_predictor
```
### 4. 使用預訓練權重
小型資料集建議使用 COCO 預訓練權重,可以:
- 加快收斂
- 提升準確率
- 降低從零訓練的需求
### 5. 以 mAP 評估
物件偵測應使用:
- mAP
- mAP@0.5
- Precision
- Recall
而不是分類問題常用的 accuracy。
---
## 11. 實務上的改善方向
如果模型表現不佳,可以考慮:
1. 增加訓練 epoch,例如 20~50。
2. 使用更完整的資料增強:
- RandomHorizontalFlip
- RandomCrop
- ColorJitter
- RandomResizedCrop
3. 調整 confidence threshold。
4. 使用更大的 batch size,或使用 gradient accumulation。
5. 針對小物件調整影像解析度與 RPN anchor。
6. 使用 ResNet-50-FPN、MobileNet-FPN 等不同 backbone。
7. 確保訓練、驗證、測試資料沒有資料洩漏。
8. 觀察不同 loss:
- 分類 loss
- box regression loss
- objectness loss
- RPN box loss
這個案例展示了從資料載入、標註格式轉換、模型建立、預訓練權重微調、訓練到 mAP 評估的完整 Faster R-CNN 物件偵測流程。
相關學習地圖、教學課程
Python 人工智慧