⭐ Vision Engineer · Chặng 12Bài 3/8Bài học~105 phút

Từ R-CNN đến Faster R-CNN: anchor, RoI Pooling, RPN

Họ R-CNN giảm từ 50 giây xuống 0,2 giây mỗi ảnh: bounding box regression, RoI Pooling, anchor, RPN, 4 hàm loss và fine-tune bằng torchvision.

Từ R-CNN đến Faster R-CNN: anchor, RoI Pooling, RPN

Học xong bài này, bạn sẽ

  • Hiểu bounding box regression và RoI Pooling
  • Hiểu anchor và Region Proposal Network
  • Fine-tune Faster R-CNN với torchvision

Nên học trước: IoU, mAP: đánh giá Object Detection

Nội dung bài viết
  1. Trước R-CNN: từ một đối tượng đến nhiều đối tượng
  2. R-CNN (2014): Regions with CNN features
  3. Bounding box regression
  4. Fast R-CNN (2015): chạy CNN một lần cho cả ảnh
  5. RoI Pooling
  6. Faster R-CNN (2015): để mạng tự đề xuất vùng
  7. So sánh ba thế hệ
  8. Thực hành: fine-tune Faster R-CNN với torchvision
  9. Sau Faster R-CNN: detector một giai đoạn
  10. Tóm tắt
  11. Tài liệu tham khảo

Họ R-CNN là một trong những câu chuyện tiến hoá đẹp nhất của thị giác máy tính: trong vòng hai năm (2014–2015), thời gian phát hiện đối tượng trên một ảnh giảm từ khoảng 50 giây xuống 0,2 giây, đồng thời độ chính xác tăng lên. Mỗi bước tiến loại bỏ đúng một "nút thắt" của phiên bản trước. Hiểu họ R-CNN là hiểu những khái niệm nền tảng còn dùng đến hôm nay: bounding box regression, RoI pooling, anchor box, Region Proposal Network.

Số trang (ví dụ "trang 81") trong bài chỉ tới slide của bài giảng Deep Learning – Computer Vision – NLP (Thang Nguyen, Viet Nguyen); bài này giải thích chi tiết, kiểm chứng và đính chính các slide đó.

Bài này cần kiến thức về IoU, NMS và mAP ở bài trước, IoU, mAP: đánh giá Object Detection (đọc thêm: sliding window, selective search, NMS và cách tính AP).

Trước R-CNN: từ một đối tượng đến nhiều đối tượng

Phân loại trả lời "ảnh này là gì"; phát hiện đối tượng trả lời "có những vật gì, ở đâu", và số vật thay đổi theo từng ảnh (trang 142–143). Đây chính là chỗ khó: mạng nơ-ron thường có đầu ra kích thước cố định.

Định vị một đối tượng: hai "đầu" trên cùng một mạng

Nếu biết chắc ảnh có đúng một vật (trang 144), bài toán khá đơn giản: một CNN trích ra vector đặc trưng (ví dụ 4096 chiều), rồi gắn hai đầu ra. Đầu "What?" là lớp Linear ra điểm từng lớp, huấn luyện bằng cross-entropy. Đầu "Where?" là lớp Linear ra 4 số (x, y, w, h), huấn luyện bằng L2 loss so với hộp thật. Hai loss cộng lại thành multi-task loss L = L_cls + λ·L_box.

import torch.nn as nn
from torchvision import models


class DinhViMotVat(nn.Module):
    def __init__(self, so_lop):
        super().__init__()
        backbone = models.resnet18(weights=None)       # dùng weights=ResNet18_Weights.DEFAULT khi có mạng
        backbone.fc = nn.Identity()                    # lấy vector đặc trưng 512 chiều
        self.backbone = backbone
        self.dau_lop = nn.Linear(512, so_lop)          # What?
        self.dau_hop = nn.Linear(512, 4)               # Where? tọa độ chuẩn hóa về [0, 1]

    def forward(self, x):
        f = self.backbone(x)
        return self.dau_lop(f), self.dau_hop(f).sigmoid()

Nhiều đối tượng: sliding window quá đắt

Cách ngây thơ (trang 146): trượt một cửa sổ khắp ảnh, mỗi vị trí hỏi bộ phân loại "đây là vật gì hay chỉ là nền?". Nhưng vật có thể ở mọi vị trí, mọi kích thước, mọi tỉ lệ khung. Số hình chữ nhật trong ảnh W×H là W(W+1)/2 · H(H+1)/2: với ảnh 800×600 là khoảng 57,8 tỉ cửa sổ.

Giải pháp là chỉ xét những vùng "có vẻ chứa vật" (trang 147). Selective Search chia ảnh thành rất nhiều vùng nhỏ rồi gộp dần các vùng giống nhau về màu, kết cấu, kích thước, sinh khoảng 2.000 vùng đề xuất mỗi ảnh trong vài giây trên CPU mà vẫn bao được hầu hết vật thật. R-CNN bắt đầu từ đây.

R-CNN (2014): Regions with CNN features

Ross Girshick và cộng sự ghép hai ý tưởng: region proposals (Selective Search) và đặc trưng CNN mạnh mẽ vừa được AlexNet chứng minh. Quy trình bốn bước (trang 148):

  1. Ảnh đầu vào.
  2. Trích khoảng 2.000 vùng đề xuất bằng Selective Search.
  3. Tính đặc trưng CNN: mỗi vùng được bóp méo (warp) về kích thước cố định 227×227, đưa qua CNN (AlexNet) lấy vector đặc trưng 4096 chiều.
  4. Phân loại vùng: mỗi lớp có một SVM tuyến tính riêng chấm điểm vector đặc trưng (trang 149: SVM[Bird] 0,17, SVM[Dog] 0,87, SVM[Cat] 0,42). Song song, một bộ bounding box regressor tinh chỉnh toạ độ vùng (trang 150).

Kết quả: mAP 53,3% trên Pascal VOC 2012, cải thiện hơn 30% (tương đối) so với kỹ thuật tốt nhất trước đó. Nhưng R-CNN có ba điểm yếu lớn:

  • Rất chậm: 2.000 lần chạy CNN cho mỗi ảnh, khoảng 47–50 giây/ảnh khi suy luận trên GPU.
  • Huấn luyện nhiều giai đoạn rời rạc: fine-tune CNN, rồi huấn luyện SVM, rồi huấn luyện regressor; các phần không tối ưu cùng nhau.
  • Tốn bộ nhớ: đặc trưng của mọi vùng phải ghi ra đĩa (hàng trăm GB cho VOC).

Bounding box regression

Vùng đề xuất \(p\) hiếm khi khít đối tượng. Thay vì dự đoán toạ độ tuyệt đối, mô hình học các độ dịch tương đối từ \(p\) sang box thật \(g\) (trang 151). Box biểu diễn bằng tâm và kích thước \((x, y, w, h)\).

Nhãn huấn luyện (mục tiêu \(t\)) được tính từ box đề xuất \(p\) và box thật \(g\):

\[ t_x = \frac{g_x - p_x}{p_w}, \quad t_y = \frac{g_y - p_y}{p_h}, \quad t_w = \log\frac{g_w}{p_w}, \quad t_h = \log\frac{g_h}{p_h} \]

Dự đoán: mạng xuất ra \(d(p) = (d_x, d_y, d_w, d_h)\), giải mã thành box dự đoán \(\hat g\):

\[ \hat g_x = p_w d_x(p) + p_x, \quad \hat g_y = p_h d_y(p) + p_y, \quad \hat g_w = p_w e^{d_w(p)}, \quad \hat g_h = p_h e^{d_h(p)} \]

Loss: hồi quy bình phương có điều chuẩn (R-CNN); Fast/Faster R-CNN thay bằng smooth L1 để bền với ngoại lai.

\[ \mathcal L_{reg} = \sum_{i\in\{x,y,w,h\}} \big(t_i - d_i(p)\big)^2 + \lambda\|w\|^2 \]

Vì sao thiết kế như vậy? Chia cho \(p_w, p_h\) làm mục tiêu không phụ thuộc kích thước: lệch 10 pixel với box nhỏ là nhiều, với box lớn là ít. Dùng \(\log\) cho chiều rộng, cao đảm bảo \(\hat g_w = p_w e^{d_w}\) luôn dương.

Ví dụ: \(p = (100, 100, 50, 80)\), \(g = (110, 90, 60, 100)\) cho \(t = (0{,}2;\ -0{,}125;\ \log 1{,}2 \approx 0{,}182;\ \log 1{,}25 \approx 0{,}223)\). Giải mã ngược \(t\) cho lại đúng \(g\).

import math


def encode(p, g):
    """p: anchor/proposal, g: box thật, đều dạng (cx, cy, w, h). Trả về mục tiêu t."""
    px, py, pw, ph = p
    gx, gy, gw, gh = g
    return ((gx - px) / pw, (gy - py) / ph, math.log(gw / pw), math.log(gh / ph))


def decode(p, d):
    """Từ độ dịch d mạng dự đoán, tính box dự đoán."""
    px, py, pw, ph = p
    dx, dy, dw, dh = d
    return (pw * dx + px, ph * dy + py, pw * math.exp(dw), ph * math.exp(dh))


t = encode((100, 100, 50, 80), (110, 90, 60, 100))
print([round(v, 3) for v in t])               # [0.2, -0.125, 0.182, 0.223]
print(decode((100, 100, 50, 80), t))          # (110.0, 90.0, 60.0, 100.0)

Fast R-CNN (2015): chạy CNN một lần cho cả ảnh

Nhận xét then chốt: 2.000 vùng đề xuất chồng lấn nhau rất nhiều, chạy CNN riêng cho từng vùng là lãng phí. Fast R-CNN (Girshick, 2015) đảo ngược thứ tự (trang 155, 157):

  1. Đưa toàn bộ ảnh qua ConvNet một lần, được feature map (ví dụ "conv5").
  2. Chiếu các vùng đề xuất (Regions of Interest, RoI) từ Selective Search lên feature map.
  3. RoI Pooling cắt phần feature tương ứng mỗi RoI và đưa về kích thước cố định.
  4. Qua vài lớp fully connected, rồi tách hai nhánh: softmax phân loại \(K + 1\) lớp (thêm lớp "background") và bounding-box regressor cho từng lớp.

Toàn bộ phần sau region proposal được huấn luyện end-to-end với một multi-task loss: \(L = L_{cls} + \lambda\,[u \ge 1]\,L_{loc}\), trong đó \([u \ge 1]\) nghĩa là chỉ tính loss định vị cho vùng không phải background. SVM bị bỏ, không cần ghi đặc trưng ra đĩa. Thời gian suy luận giảm từ khoảng 50 giây xuống 2 giây mỗi ảnh (nhanh hơn ~25 lần), và phần lớn 2 giây đó giờ là của Selective Search.

RoI Pooling

Các RoI có kích thước khác nhau, nhưng lớp FC phía sau cần đầu vào cố định. RoI Pooling giải quyết điều đó (trang 156):

  1. Chiếu proposal lên feature map: chia toạ độ cho tổng stride của backbone (ví dụ ảnh 640×480 thành feature 20×15, stride 32).
  2. "Snap" (làm tròn) toạ độ về các ô lưới nguyên.
  3. Chia vùng thành lưới \(H\times W\) ô con xấp xỉ bằng nhau (ví dụ 2×2; thực tế thường 7×7).
  4. Max-pool trong từng ô con.

Kết quả luôn là \(C\times H\times W\) (ví dụ 512×7×7) bất kể RoI lớn hay nhỏ.

Ví dụ với feature map 8×8 ở trang 156: RoI phủ hàng 3–7, cột 0–6 (vùng 5×7). Chia 2×2: chiều cao 5 tách thành 2 + 3 hàng, chiều rộng 7 tách thành 3 + 4 cột.

Vùng RoI 5×7 trên feature map
0.850.240.840.760.290.750.62
0.320.740.390.310.340.030.33
0.200.690.130.160.730.650.96
0.190.140.860.090.880.070.01
0.830.240.040.970.340.350.50
→
Kết quả 2×2
0.850.76
0.860.97
RoI AlignViệc làm tròn toạ độ hai lần (khi snap và khi chia ô) gây lệch vài pixel trên feature map, tương đương hàng chục pixel trên ảnh gốc. Với phân loại thì chấp nhận được, nhưng với phân vùng thì không. Mask R-CNN (2017) thay bằng RoI Align: không làm tròn, lấy giá trị tại các điểm thực bằng nội suy song tuyến tính. torchvision có cả torchvision.ops.roi_pool và roi_align.

Faster R-CNN (2015): để mạng tự đề xuất vùng

Khi CNN đã nhanh, Selective Search (chạy trên CPU, khoảng 2 giây) trở thành nút thắt. Ren, He, Girshick và Sun đặt câu hỏi: feature map của CNN đã chứa đủ thông tin về "chỗ nào có vật", sao không dùng chính nó để đề xuất vùng? Faster R-CNN thêm một mạng nhỏ là Region Proposal Network (RPN) chạy trên cùng feature map (trang 158):

Ảnh Backboneconv layers Feature map RPNanchor → RoI RoI pooling FC → lớp + boxclassifier
Faster R-CNN = RPN + Fast R-CNN, dùng chung một backbone

Region proposal giờ gần như "miễn phí" (khoảng 10 ms) vì dùng chung phần tính toán nặng nhất. Toàn hệ thống đạt khoảng 0,2 giây/ảnh (5 FPS với VGG-16 trên GPU thời đó), đồng thời đề xuất học được còn chất lượng hơn Selective Search.

Anchor boxes

Làm sao một lớp conv dự đoán được số lượng box thay đổi? Ý tưởng là anchor (trang 159): tại mỗi vị trí trên feature map, đặt sẵn \(k\) box mẫu với nhiều kích thước và tỉ lệ khung. Bài báo gốc dùng 3 kích thước (128², 256², 512² pixel) × 3 tỉ lệ (1:2, 1:1, 2:1) = 9 anchor mỗi vị trí. Feature map khoảng 60×40 cho khoảng 60·40·9 = 21.600 anchor phủ khắp ảnh. Mạng không dự đoán box từ hư không mà trả lời hai câu hỏi cho từng anchor: "anchor này có chứa vật không?" và "cần dịch, co giãn anchor bao nhiêu để khít vật?" (đúng công thức bounding box regression ở trên).

Region Proposal Network

Theo trang 160, RPN là một mạng tích chập nhỏ:

  1. Một conv 3×3 trượt trên feature map (sliding window), tạo vector trung gian 256 chiều (ZF-Net) hoặc 512 chiều (VGG) tại mỗi vị trí.
  2. Hai lớp conv 1×1 song song:
    • cls layer: \(2k\) điểm (có vật / không có vật cho \(k\) anchor).
    • reg layer: \(4k\) toạ độ (độ dịch \(d_x, d_y, d_w, d_h\) cho \(k\) anchor).

Gán nhãn anchor khi huấn luyện: anchor có IoU > 0,7 với một box thật (hoặc là anchor có IoU cao nhất với box thật đó) là positive; IoU < 0,3 với mọi box thật là negative; còn lại bị bỏ qua. Mỗi ảnh lấy mẫu 256 anchor với tỉ lệ positive:negative tối đa 1:1 để tránh mất cân bằng (background chiếm áp đảo).

Sinh proposal: giải mã độ dịch thành box, cắt theo biên ảnh, chạy NMS (IoU 0,7), giữ khoảng 2.000 proposal (khi huấn luyện) hoặc 300 (khi suy luận) để đưa sang nhánh Fast R-CNN.

Bốn hàm loss

Faster R-CNN huấn luyện đồng thời 4 loss (trang 161):

#LossNhiệm vụTên trong torchvision
1RPN classificationAnchor có phải vật thể hay khôngloss_objectness
2RPN regressionDịch anchor thành proposalloss_rpn_box_reg
3Object classificationPhân loại proposal thành background hoặc một lớp cụ thểloss_classifier
4Object regressionTinh chỉnh proposal thành box cuối cùngloss_box_reg

Loss của RPN viết gọn (với \(p_i^*\) = 1 nếu anchor là positive):

\[ L = \frac{1}{N_{cls}}\sum_i L_{cls}(p_i, p_i^*) + \lambda\,\frac{1}{N_{reg}}\sum_i p_i^*\, L_{reg}(t_i, t_i^*) \]

Chỉ anchor positive mới đóng góp vào loss hồi quy; \(L_{reg}\) là smooth L1.

So sánh ba thế hệ

R-CNNFast R-CNNFaster R-CNN
Region proposalSelective Search (ngoài mạng)Selective Search (ngoài mạng)RPN (trong mạng, học được)
Chạy CNN~2.000 lần/ảnh1 lần/ảnh1 lần/ảnh
Bộ phân loạiSVMSoftmaxSoftmax
Huấn luyệnNhiều giai đoạn rời rạcEnd-to-end (trừ proposal)End-to-end
Thời gian/ảnh~50 giây~2 giây~0,2 giây
Tăng tốc1×25×250×

(Số liệu thời gian theo trang 162, đo trên GPU thời điểm 2015.)

Thực hành: fine-tune Faster R-CNN với torchvision

torchvision cung cấp Faster R-CNN với backbone ResNet-50 + FPN (Feature Pyramid Network: dùng feature map ở nhiều tầng để phát hiện tốt cả vật nhỏ lẫn vật lớn), đã huấn luyện trên COCO. Để dùng cho dữ liệu riêng, chỉ cần thay lớp dự đoán cuối:

import torch
from torchvision.models.detection import fasterrcnn_resnet50_fpn_v2, FasterRCNN_ResNet50_FPN_V2_Weights
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor

num_classes = 3                      # 2 loại lỗi + 1 lớp background (bắt buộc tính cả background)
model = fasterrcnn_resnet50_fpn_v2(weights=FasterRCNN_ResNet50_FPN_V2_Weights.DEFAULT)
in_features = model.roi_heads.box_predictor.cls_score.in_features      # 1024
model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes)

# Dataset phải trả về (image, target):
#   image : tensor float (3, H, W), giá trị [0, 1] (model tự chuẩn hoá và resize)
#   target: {"boxes": FloatTensor (N, 4) dạng x1y1x2y2 pixel, "labels": Int64Tensor (N,) bắt đầu từ 1}
def collate_fn(batch):
    return tuple(zip(*batch))        # ảnh có kích thước khác nhau nên không stack được

# loader = DataLoader(dataset, batch_size=4, shuffle=True, collate_fn=collate_fn)
params = [p for p in model.parameters() if p.requires_grad]
optimizer = torch.optim.SGD(params, lr=0.005, momentum=0.9, weight_decay=5e-4)

model.train()
for images, targets in loader:
    loss_dict = model(list(images), list(targets))   # 4 loss: loss_classifier, loss_box_reg,
    loss = sum(loss_dict.values())                   #         loss_objectness, loss_rpn_box_reg
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

# Suy luận: trả về list dict {"boxes", "labels", "scores"} cho từng ảnh, đã qua NMS
model.eval()
with torch.no_grad():
    pred = model([image])[0]
keep = pred["scores"] > 0.5
print(pred["boxes"][keep], pred["labels"][keep])

Chi tiết đầy đủ có trong hướng dẫn chính thức TorchVision Object Detection Finetuning Tutorial của PyTorch.

Sau Faster R-CNN: detector một giai đoạn

Faster R-CNN vẫn là chuẩn mực về độ chính xác và là nền của Mask R-CNN (xem Image Segmentation). Nhưng cho ứng dụng thời gian thực, cộng đồng chuyển sang các detector một giai đoạn (one-stage), dự đoán lớp và box trực tiếp trên lưới feature map không qua bước proposal:

  • YOLO (2016–nay): chia ảnh thành lưới, mỗi ô dự đoán box và lớp; liên tục cải tiến qua nhiều phiên bản. Phiên bản Ultralytics mới nhất là YOLO26 (ra mắt tháng 1/2026), suy luận end-to-end không cần NMS, tối ưu cho thiết bị biên.
  • SSD (2016): dự đoán trên nhiều feature map ở các độ phân giải khác nhau.
  • RetinaNet (2017): focal loss giải quyết mất cân bằng giữa hàng chục nghìn anchor background và vài anchor chứa vật, giúp one-stage đạt độ chính xác ngang two-stage.
  • DETR / RT-DETR: dùng Transformer, không cần anchor hay NMS.
# pip install ultralytics
from ultralytics import YOLO

model = YOLO("yolo26n.pt")                                   # bản nano, pretrained trên COCO
model.train(data="data.yaml", epochs=100, imgsz=640)          # data.yaml: đường dẫn train/val + tên lớp
results = model("anh_san_pham.jpg")                           # dự đoán
results[0].show()

Trong công việc thực tế, YOLO thường là lựa chọn đầu tiên nhờ tốc độ và công cụ hoàn chỉnh. Hiểu R-CNN giúp bạn biết bên trong chúng có gì: anchor (hoặc anchor-free), bounding box regression, IoU, NMS và mAP đều là những khái niệm dùng chung.

Tóm tắt

  • R-CNN: Selective Search + CNN cho từng vùng + SVM + bbox regression; chính xác nhưng rất chậm.
  • Bbox regression học độ dịch tương đối \((t_x, t_y, t_w, t_h)\), không phụ thuộc kích thước.
  • Fast R-CNN: một lần CNN cho cả ảnh, RoI Pooling, multi-task loss, end-to-end.
  • Faster R-CNN: RPN với 9 anchor mỗi vị trí thay Selective Search; 4 loss; ~0,2 giây/ảnh.
  • Thực tế: torchvision Faster R-CNN để học và khi cần độ chính xác; YOLO cho thời gian thực.

Bài tiếp theo trong chặng đi từ "khung bao" xuống mức từng pixel: Phân đoạn ảnh: semantic, instance, panoptic. Đọc thêm về FCN, DeepLab và Mask R-CNN: Image Segmentation.

Tài liệu tham khảo

  • Girshick et al. (2014). Rich feature hierarchies for accurate object detection and semantic segmentation (R-CNN).
  • Girshick (2015). Fast R-CNN.
  • Ren, He, Girshick, Sun (2015). Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks.
  • Lin et al. (2017). Feature Pyramid Networks for Object Detection; Focal Loss for Dense Object Detection.
  • Lilian Weng (2017). Object Detection for Dummies Part 3: R-CNN Family (lilianweng.github.io).
  • PyTorch: TorchVision Object Detection Finetuning Tutorial; Ultralytics docs: docs.ultralytics.com.

Bài tập thực hành

  1. Tự tính mục tiêu (tx, ty, tw, th) cho 3 cặp anchor – box thật, rồi giải mã ngược để kiểm tra bằng code trong bài.

Làm xong bài tập mới chuyển sang bài tiếp theo: tự viết code là cách duy nhất để nhớ lâu.

Bài tiếp theo
Ban biên tập HọcFree

Chúng tôi viết những bài hướng dẫn dễ hiểu, thực tế để ai cũng có thể tự học lập trình và ngoại ngữ, hoàn toàn miễn phí.