WeHelp
PyTorch 可以應用在迴歸預測模型、分類預測模型、影像辨識、物件偵測、生成式模型等等領域。
  1. 房屋價格預測
  2. 乳癌良性 / 惡性分類
  3. 紅酒產地分類
  4. CNN 影像辨識
  5. R-CNN 物件偵測
  6. GAN 生成對抗網路
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 人工智慧
建議完成「Python 資料工程」教程後,繼續學習以下課程。