နားလည်ထားရမယ့် အချက်
Object detection ကို ရိုးရှင်းအောင် reframe လုပ်ရင် regression problem တစ်ခုအဖြစ် ကြည့်လို့ရပါတယ် — image တစ်ပုံစီအတွက် class label တစ်ခုတည်းကို ခန့်မှန်းမယ့်အစား continuous number 4 ခု (bounding box ရဲ့ x1, y1, x2, y2 corner တွေ) ကို ခန့်မှန်းရတယ်။ Backbone ကတော့ ယခင် project တွေကလိုပဲ Conv2d/ReLU stack ကနေ spatial feature တွေ ထုတ်ယူပေမယ့်၊ ဒီနေရာမှာ input resolution မတူသော ပုံတွေအတွက်တောင် fixed-length vector ရအောင် AdaptiveAvgPool2d(1) နဲ့ global average pooling လုပ်ပါတယ်။ Head ကတော့ Linear layer ငယ်တစ်ခုနောက် Sigmoid ကို ဆက်ထားတယ် — output ကို [0, 1] range ထဲ တင်းကျပ်ချုပ်ချယ်ပေးတာက image width/height ရဲ့ fraction အနေနဲ့ box coordinate ကို normalized ပုံစံနဲ့ ကိုယ်စားပြုတဲ့ standard convention နဲ့ ကိုက်ညီစေတယ်။
Training အတွက် SmoothL1Loss (Huber loss ပုံစံ) ကို predicted box နဲ့ ground-truth box ကြားက regression error အဖြစ် သုံးထားပါတယ် — MSE ထက် outlier box coordinate တွေကို ပိုမို robust ဖြစ်စေတယ်။ ဒါပေမယ့် training loss နဲ့ evaluation metric ကြားက mismatch ရှိတယ်ဆိုတာကို သတိပြုရပါမယ် — training မှာသုံးတဲ့ smooth differentiable loss နဲ့ detection quality ကို တကယ်တိုင်းတဲ့ IoU (Intersection over Union) က မတူပါဘူး၊ IoU က box overlap မရှိတဲ့နေရာမှာ gradient မတန်ကြေး ပြတ်တောက်သွားတတ်လို့ တိုက်ရိုက် backprop ရန် မသင့်တော်ပါ။ ရှေ့ပိုင်းသင်ခန်းစာမှာ scratch ကနေ ရေးထားတဲ့ IoU function ကို ပြန်သုံးပြီး loss ကျသွားတာဟာ localization overlap တကယ် တိုးတက်မှုနဲ့ ဆက်စပ်နေလားဆိုတာ sanity check လုပ်ကြည့်ခြင်းက ဒီ project ရဲ့ အဓိကအချက်ပါ။
လက်တွေ့ scenario နဲ့ ချိတ်ကြည့်မယ်
Tutorial Platform ရဲ့ thumbnail auto-crop tool မှာ instructor တင်လိုက်တဲ့ course thumbnail ပုံကြီးတစ်ပုံကို catalog card size အတိုင်းအတာအတွက် ဘယ်လို crop လုပ်ရမလဲဆိုတာ manual ရွေးစရာမလိုအောင်၊ ပုံထဲက အဓိက subject (code window ဒါမှမဟုတ် presenter face) ပတ်လည် bounding box ကို ခန့်မှန်းပေးဖို့ ဒီလို ရိုးရှင်းသော detector တစ်ခု သုံးထားပါတယ် — ခန့်မှန်းထားတဲ့ box ကို crop region အနေနဲ့ တိုက်ရိုက်အသုံးချနိုင်ပါတယ်။
အတူတူ စမ်းရေးကြည့်မယ်
import torch
import torch.nn as nn
import torch.optim as optim
torch.manual_seed(0)
def box_iou(box_a, box_b):
# box_a, box_b: tensors of shape (4,) as [x1, y1, x2, y2] in normalized coords
x1 = torch.max(box_a[0], box_b[0])
y1 = torch.max(box_a[1], box_b[1])
x2 = torch.min(box_a[2], box_b[2])
y2 = torch.min(box_a[3], box_b[3])
inter_w = (x2 - x1).clamp(min=0)
inter_h = (y2 - y1).clamp(min=0)
intersection = inter_w * inter_h
area_a = (box_a[2] - box_a[0]).clamp(min=0) * (box_a[3] - box_a[1]).clamp(min=0)
area_b = (box_b[2] - box_b[0]).clamp(min=0) * (box_b[3] - box_b[1]).clamp(min=0)
union = area_a + area_b - intersection
return (intersection / union).item() if union > 0 else 0.0
class TinyDetector(nn.Module):
def __init__(self):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 16, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2), # 32x32 -> 16x16
nn.Conv2d(16, 32, kernel_size=3, padding=1),
nn.ReLU(),
nn.AdaptiveAvgPool2d(1), # -> 32x1x1, works for any input size
)
self.head = nn.Sequential(
nn.Flatten(),
nn.Linear(32, 4),
nn.Sigmoid(), # squash to [0, 1] like normalized box coords
)
def forward(self, x):
x = self.features(x)
return self.head(x)
num_samples = 32
images = torch.randn(num_samples, 3, 32, 32)
# Synthetic ground-truth boxes: random but valid [x1, y1, x2, y2], x2>x1, y2>y1
x1 = torch.rand(num_samples, 1) * 0.5
y1 = torch.rand(num_samples, 1) * 0.5
x2 = x1 + torch.rand(num_samples, 1) * 0.5 + 0.1
y2 = y1 + torch.rand(num_samples, 1) * 0.5 + 0.1
gt_boxes = torch.cat([x1, y1, x2, y2], dim=1).clamp(max=1.0)
model = TinyDetector()
criterion = nn.SmoothL1Loss()
optimizer = optim.Adam(model.parameters(), lr=1e-2)
model.eval()
with torch.no_grad():
pred_before = model(images)[0]
iou_before = box_iou(pred_before, gt_boxes[0])
print(f"IoU before training (sample 0): {iou_before:.4f}")
model.train()
for step in range(20):
optimizer.zero_grad()
preds = model(images)
loss = criterion(preds, gt_boxes)
loss.backward()
optimizer.step()
if step % 5 == 0 or step == 19:
print(f"Step {step+1}/20 - loss: {loss.item():.4f}")
model.eval()
with torch.no_grad():
pred_after = model(images)[0]
iou_after = box_iou(pred_after, gt_boxes[0])
print(f"IoU after training (sample 0): {iou_after:.4f}")
print("Training complete.")
"IoU before training (sample 0): X.XXXX" ကနေ စတင်ပါမယ် — training မလုပ်ရသေးတဲ့ head ရဲ့ output က sigmoid midpoint (0.5) အနီးက coordinate 4 ခုနားမှာသာ ရှိတတ်လို့ ground-truth box နဲ့ overlap နည်းနည်းသာ ရှိနိုင်တယ် (IoU 0.3 အောက်၊ overlap လုံးဝမရှိရင် 0.0 လည်း ဖြစ်နိုင်တယ်)။ ပြီးရင် "Step 1/20", "Step 6/20", "Step 11/20", "Step 16/20", "Step 20/20" အတွက် loss line 5 ကြောင်း ထွက်ပါမယ် — sample 32 ခုကို regression fit လုပ်ရုံသာ ဖြစ်လို့ step 20 ခုအတွင်းမှာ SmoothL1 loss က ရှင်းရှင်းလင်းလင်း ကျသွားနိုင်ပါတယ်။ နောက်ဆုံးမှာ "IoU after training (sample 0): X.XXXX" က training မလုပ်ခင်ထက် ပိုမြင့်တာ များသောအားဖြင့် တွေ့ရမှာဖြစ်ပြီး "Training complete." နဲ့ ပြီးဆုံးပါမယ်။၅ မိနစ် စမ်းကြည့်
IoU calculation ကို sample 0 တစ်ခုတည်းအစား test set 32 ခုလုံးအတွက် တွက်ပြီး mean IoU (training မလုပ်ခင်/ပြီးနောက်) ကို print ထုတ်အောင် ပြင်ပါ။ Mean IoU improvement ကို criterion = nn.MSELoss() အသုံးပြုတဲ့ version တစ်ခုနဲ့ နှိုင်းယှဉ်ကြည့်ပါ။
သတိလေးတစ်ချက်
Ground-truth box generate လုပ်တဲ့အခါ x2>x1 နဲ့ y2>y1 ဆိုတဲ့ constraint ကို မထိန်းသိမ်းဘဲ x1/y1/x2/y2 ကို လုံးဝ random ဖြစ်အောင် ဆွဲလိုက်ရင် area negative ဖြစ်နိုင်ပြီး box_iou function ထဲက clamp(min=0) logic မှားနိုင်တဲ့ box (width/height 0 ထက်နည်း) တွေ ရောက်လာနိုင်ပါတယ်။
Loss ကျသွားတာကို detection quality တိုးတက်လာတယ်လို့ တိုက်ရိုက်ယူဆလိုက်ရင် မှားနိုင်ပါတယ် — SmoothL1 loss ကျသွားတာနဲ့ IoU တိုးတက်တာက တစ်ခုတည်း မဟုတ်ချေ၊ sample တစ်ခုတည်းကိုပဲ ကြည့်ထားရင် (ဒီ project ရဲ့ sample 0 လိုမျိုး) ကျပန်း fluctuation ကြောင့် IoU ကျသွားနိုင်ပါတယ်။
PyTorch Vision Docs — Models and pre-trained weights — Computer Vision