Anchor Boxes
Anchor Boxes - Hộp neo
Object Detection cần dự đoán bounding box (x, y, w, h) cho mỗi đối tượng. Dự đoán 4 số từ đầu (from scratch) hay điều chỉnh từ 1 hộp mẫu có sẵn — cái nào dễ hơn?
Hình minh họa
9 anchor box tại 1 vị trí (3 tỷ lệ × 3 kích thước)
Di chuột lên từng hộp để xem IoU với ground truth và 4 giá trị — chính là thứ mô hình học.
Anchor box giống bản mẫu may quần áo: thay vì may từ vải trắng (dự đoán toạ độ từ 0), bạn chọn bản mẫu size gần nhất rồi chỉnh sửa nhẹ. Mô hình chỉ cần dự đoán 4 offset nhỏ thay vì 4 toạ độ tuyệt đối — và chính vì offset gần 0, loss ổn định hơn, mô hình hội tụ nhanh hơn nhiều.
Faster R-CNN dùng 9 anchors/vị trí (3 tỷ lệ × 3 kích thước). Trên feature map 40×40, tổng cộng bao nhiêu anchor box?
Một anchor có IoU = 0.55 với ground truth. Theo chuẩn Faster R-CNN, anchor này được dùng như thế nào trong training?
Giải thích
Anchor Boxes là tập hợp bounding box mẫu với tỷ lệ khung hình (aspect ratio) và kích thước (scale) xác định trước, đặt đều đặn trên lưới feature map. Chúng đóng vai trò điểm khởi đầu cho regression — thay vì đoán (x, y, w, h) từ số 0, mô hình chỉ học dịch chuyển tương đối từ anchor gần nhất.
Công thức regression target (Faster R-CNN, R. Girshick et al., 2015):
Với là anchor và là ground truth. Khi inference, giải ngược công thức: và .
Gán nhãn (Anchor Assignment) — quyết định anchor nào được dùng cho training và dùng làm gì:
- Positive: IoU ≥ 0.7 với 1 ground truth bất kỳ, HOẶC là anchor có IoU lớn nhất với 1 GT (tránh trường hợp GT nhỏ không có anchor nào đạt 0.7).
- Negative: IoU < 0.3 với mọi ground truth → gán class = background, không tham gia regression loss.
- Bỏ qua: 0.3 ≤ IoU < 0.7 — không dùng cho training để tránh nhiễu loss ở vùng ranh giới.
Anchor-based (Faster R-CNN, SSD, RetinaNet, YOLOv3–v5): Đặt anchor với ratio/scale preset trước, dự đoán offset. Cần tune kích thước anchor cho từng dataset — lớn với ảnh vệ tinh, nhỏ với face detection.
Anchor-free (FCOS, CenterNet, YOLOv8+): Dự đoán trực tiếp từ mỗi điểm trên feature map — khoảng cách đến 4 cạnh object hoặc center + size. Đơn giản hơn, ít hyperparameter hơn. Xu hướng hiện đại, kết quả cạnh tranh với anchor-based trên COCO.
Công thức chia cho chiều rộng anchor, không phải stride. Lợi ích: offset normalize — anchor lớn dịch nhiều pixel vẫn cho t_x nhỏ, anchor nhỏ dịch ít pixel vẫn cho t_x đủ lớn. Điều này giúp loss có cùng thang đo cho mọi kích thước anchor.
YOLO v2/v3 dùng biến thể: — sigmoid ép tâm anchor ở trong ô grid → tránh dịch quá xa.
Với 14,400 anchor trên 1 ảnh, nhiều anchor cạnh nhau sẽ cùng hội tụ về 1 object thực → nhiều bounding box gần trùng nhau cho cùng 1 object. Non-Maximum Suppression (NMS) loại bỏ các hộp trùng lặp: giữ hộp có confidence cao nhất, loại hộp có IoU ≥ 0.5 với nó. Liên kết: Non-Maximum Suppression.
Lịch sử ngắn: ý tưởng "hộp tham chiếu" xuất hiện trong Selective Search (Uijlings, 2013) rồi được chuẩn hoá trong R-CNN (Girshick, 2014). Tới Faster R-CNN (2015), anchor trở thành thành phần end-to-end — RPN tự sinh proposals mà không cần bước region proposal rời rạc. Sau đó SSD (2016) và YOLOv2 (2017) phổ biến anchor ra cộng đồng one-stage detector. Từ 2019, anchor-free (FCOS, CenterNet) bắt đầu thách thức và dần chiếm ưu thế trên nhiều benchmark.
import numpy as np
def generate_base_anchors(
scales=(64, 128, 256), # kích thước anchor (px)
ratios=(0.5, 1.0, 2.0), # tỷ lệ w/h
):
"""Sinh 9 anchor tại gốc (0, 0) — dạng (x1, y1, x2, y2)."""
anchors = []
for s in scales:
for r in ratios:
w = s * np.sqrt(r)
h = s / np.sqrt(r)
anchors.append([-w / 2, -h / 2, w / 2, h / 2])
return np.array(anchors) # shape (9, 4)
def tile_anchors(base, feat_size=(40, 40), stride=16):
"""Rải 9 anchor xuống toàn bộ feature map."""
H, W = feat_size
# Tâm của mỗi ô grid trên ảnh gốc
shifts_x = (np.arange(W) + 0.5) * stride
shifts_y = (np.arange(H) + 0.5) * stride
gx, gy = np.meshgrid(shifts_x, shifts_y)
shifts = np.stack([gx.ravel(), gy.ravel(),
gx.ravel(), gy.ravel()], axis=1) # (H*W, 4)
# Broadcast: (1, 9, 4) + (H*W, 1, 4) → (H*W, 9, 4)
anchors = base[None, :, :] + shifts[:, None, :]
return anchors.reshape(-1, 4) # (H*W*9, 4)
def bbox_regression_target(anchor, gt):
"""Tính (t_x, t_y, t_w, t_h) — mục tiêu học của RPN."""
ax = (anchor[0] + anchor[2]) / 2
ay = (anchor[1] + anchor[3]) / 2
aw = anchor[2] - anchor[0]
ah = anchor[3] - anchor[1]
gx = (gt[0] + gt[2]) / 2
gy = (gt[1] + gt[3]) / 2
gw = gt[2] - gt[0]
gh = gt[3] - gt[1]
tx = (gx - ax) / aw
ty = (gy - ay) / ah
tw = np.log(gw / aw)
th = np.log(gh / ah)
return np.array([tx, ty, tw, th])
base = generate_base_anchors()
all_anchors = tile_anchors(base, feat_size=(40, 40), stride=16)
print(f"Total anchors: {len(all_anchors)}") # 14400
print(f"Shape: {all_anchors.shape}") # (14400, 4)
# Demo: target cho 1 cặp (anchor, GT) cụ thể
anchor = all_anchors[7200]
gt = np.array([300, 200, 420, 380]) # x1, y1, x2, y2
print("Regression target:", bbox_regression_target(anchor, gt))Đoạn code trên làm 3 việc: (1) sinh 9 anchor tại gốc toạ độ, (2) "rải" chúng lên toàn bộ feature map bằng meshgrid + broadcast, (3) tính regression target cho 1 cặp (anchor, GT) cụ thể. Đây là 3 thao tác cốt lõi của bước Anchor Generator trong mọi Faster R-CNN implementation.
import torch
import torch.nn as nn
import torch.nn.functional as F
def decode_boxes(anchors, deltas):
"""Ngược công thức: anchor + offset → box dự đoán."""
ax = (anchors[:, 0] + anchors[:, 2]) * 0.5
ay = (anchors[:, 1] + anchors[:, 3]) * 0.5
aw = anchors[:, 2] - anchors[:, 0]
ah = anchors[:, 3] - anchors[:, 1]
tx, ty, tw, th = deltas.unbind(dim=1)
# Clamp để tránh exp overflow khi mạng chưa hội tụ
tw = torch.clamp(tw, max=4.135)
th = torch.clamp(th, max=4.135)
cx = ax + aw * tx
cy = ay + ah * ty
w = aw * torch.exp(tw)
h = ah * torch.exp(th)
return torch.stack([cx - w / 2, cy - h / 2,
cx + w / 2, cy + h / 2], dim=1)
class RPNLoss(nn.Module):
"""Loss của Region Proposal Network — 2 thành phần:
1. Classification: anchor có object hay không (BCE).
2. Regression: chỉ tính trên positive anchor (Smooth L1).
"""
def __init__(self, lambda_reg: float = 10.0):
super().__init__()
self.lambda_reg = lambda_reg
def forward(self, cls_logits, bbox_deltas,
cls_targets, bbox_targets, bbox_weights):
# cls_targets: {1: positive, 0: negative, -1: ignore}
valid = cls_targets >= 0
cls_loss = F.binary_cross_entropy_with_logits(
cls_logits[valid], cls_targets[valid].float()
)
# Regression chỉ trên positive — nhân với bbox_weights (1 hoặc 0)
reg_loss = F.smooth_l1_loss(
bbox_deltas * bbox_weights,
bbox_targets * bbox_weights,
reduction="sum",
) / bbox_weights.sum().clamp(min=1.0)
return cls_loss + self.lambda_reg * reg_loss
# ─── Huấn luyện RPN trên 1 batch ảnh ──────────────────
def train_step(model, images, gt_boxes_per_image, optimizer, loss_fn):
"""1 bước huấn luyện Region Proposal Network.
Tham số:
images: tensor (B, 3, H, W)
gt_boxes_per_image: list các tensor (N_i, 4) — N object mỗi ảnh
"""
model.train()
optimizer.zero_grad()
# 1. Forward — RPN đưa ra 2 output:
# cls_logits: (B, A*H'*W') — điểm objectness
# bbox_deltas: (B, A*H'*W'*4) — 4 offset cho mỗi anchor
cls_logits, bbox_deltas, anchors = model(images)
# 2. Matching anchor ↔ GT, tạo target
cls_targets, bbox_targets, bbox_weights = [], [], []
for i, gt in enumerate(gt_boxes_per_image):
ious = box_iou(anchors, gt) # (A_total, N_i)
max_iou, argmax = ious.max(dim=1)
cls_t = torch.full((len(anchors),), -1,
dtype=torch.long, device=anchors.device)
cls_t[max_iou >= 0.7] = 1 # positive
cls_t[max_iou < 0.3] = 0 # negative
# Anchor có IoU cao nhất với mỗi GT cũng là positive
best_per_gt = ious.argmax(dim=0)
cls_t[best_per_gt] = 1
matched_gt = gt[argmax]
bbox_t = bbox_regression_target_batch(anchors, matched_gt)
bbox_w = (cls_t == 1).float().unsqueeze(1).expand_as(bbox_t)
cls_targets.append(cls_t)
bbox_targets.append(bbox_t)
bbox_weights.append(bbox_w)
cls_targets = torch.stack(cls_targets)
bbox_targets = torch.stack(bbox_targets)
bbox_weights = torch.stack(bbox_weights)
# 3. Loss + backward
loss = loss_fn(cls_logits, bbox_deltas,
cls_targets, bbox_targets, bbox_weights)
loss.backward()
optimizer.step()
return loss.item()Lưu ý: trong thực tế ta còn sample 256 anchor (128 positive, 128 negative) mỗi ảnh để tránh imbalance nặng — có thể có hàng chục ngàn negative nhưng chỉ vài chục positive. Sampling đúng cách là chìa khoá để RPN hội tụ nhanh.
1 feature map duy nhất không đủ cho object đa kích thước. FPN kết hợp feature ở nhiều tầng (P3, P4, P5, P6, P7) với stride tăng dần (8, 16, 32, 64, 128). Tầng càng thấp (stride nhỏ) dùng anchor nhỏ — bắt object nhỏ. Tầng cao dùng anchor lớn.
Kết quả: RetinaNet với FPN đạt AP cao hơn 5 điểm so với Faster R-CNN cùng backbone, đặc biệt trên object nhỏ. FPN là chuẩn mực cho detection hiện đại.
So sánh parameterization giữa các họ detector
| Detector | Công thức tâm | Công thức size | Ghi chú |
|---|---|---|---|
| Faster R-CNN | Two-stage, offset không bị chặn | ||
| SSD | σ₁=0.1, σ₂=0.2 — chuẩn hoá loss | ||
| YOLOv3 | Sigmoid ép tâm trong grid cell | ||
| FCOS | không có | Anchor-free: 4 khoảng cách đến cạnh | |
| YOLOv8 | center từ grid | DFL (distribution) | Anchor-free + Distribution Focal Loss |
Dù công thức khác nhau, điểm chung xuyên suốt là: luôn dự đoán đại lượng tương đối thay vì tuyệt đối. Đó là bài học chính rút ra từ anchor box — áp dụng được cho cả pose estimation, 3D detection, và segmentation bounding.
Xem thêm: IoU — chỉ số đo trùng khớp, Non-Maximum Suppression, Object Detection tổng quan.
- Anchor box = hộp mẫu đặt sẵn tại mỗi vị trí trên feature map, mỗi vị trí có 9 anchor (3 ratio × 3 scale).
- Mô hình KHÔNG dự đoán toạ độ tuyệt đối — nó học 4 offset (tx, ty, tw, th) từ anchor gần nhất.
- Gán nhãn: IoU ≥ 0.7 → positive (học offset), IoU < 0.3 → negative (background), khoảng giữa → bỏ qua.
- tw/th dùng log để giữ w > 0 sau decode và để phân phối target đối xứng quanh 0 — loss ổn định hơn.
- Số anchor cực lớn (40×40×9 = 14,400 cho 1 ảnh). Cần sampling cân bằng + NMS để xử lý kết quả.
- Xu hướng hiện đại: anchor-free (FCOS, YOLOv8) — dự đoán trực tiếp từ tâm, ít hyperparameter, kết quả ngang ngửa.
Kiểm tra hiểu biết
Tại sao cần nhiều anchor box với tỷ lệ khác nhau?
Anchor box là chiếc cầu đưa regression không ràng buộc (đoán 4 số từ 0) về regression có neo (đoán offset nhỏ). Đây là một trong những ý tưởng nền tảng trong object detection hiện đại — hiểu anchor giúp bạn đọc Faster R-CNN, SSD, YOLO, và cả RetinaNet dễ dàng.
- Hiểu được 9 anchor sinh ra ở đâu và tại sao chia 3×3.
- Đọc được công thức và ý nghĩa log.
- Biết phân loại positive/negative/ignore qua IoU.
- Nắm được giới hạn anchor-based và lý do anchor-free nổi lên.
- Biết đọc K-Means anchor + FPN multi-scale để tuning cho dataset riêng.
Gợi ý bài tập: lấy 100 ảnh COCO bất kỳ, vẽ histogram 2D của (log-w, log-h) bounding box ground truth, rồi chạy K-Means với k=9. So sánh 9 centroid bạn thu được với anchor mặc định của Faster R-CNN. Chênh lệch bao nhiêu? Trong dataset đó, anchor mặc định có tối ưu không?
Bước tiếp theo: tìm hiểu IoU chi tiết, rồi xem NMS lọc 14,400 hộp còn lại sao cho mỗi object chỉ còn 1 bounding box.