YOLOv8 deep dive (tiếng Việt): Anchor-free, DFL, và phần Ultralytics ít nói
Ôn bài một chút về YOLOv8 nhỉ ... thực ra code trên git của YOLO luôn chi tiết hơn những gì viết trong paper

FPT vừa mở đăng ký UAV Hackathon, giải thưởng 3 tỉ đồng cho team về nhất. Bài toán xoay quanh perception từ drone, tức object detection cộng vài thứ liên quan. Mình đăng ký xong là biết phải đụng lại vào CV stack một cách nghiêm túc, không thể chỉ pip install rồi gọi API như mấy dự án trước.
Trước khi nhảy vào model mới hơn (YOLOv12, RT-DETR, hoặc whatever team mình chọn cuối cùng), mình muốn ôn lại YOLOv8 cho chắc tay. Lý do đơn giản: nó là baseline mà mọi paper detection 2024-2026 vẫn dùng để so sánh. Không hiểu kỹ baseline thì đọc benchmark của model mới cũng không có chỗ neo.
Bài này là note ôn bài của mình, viết ra để hiểu kỹ hơn chứ không phải để khoe biết. Giả định bạn đã biết deep learning cơ bản, hiểu CNN, biết training và inference là gì, nhưng chưa từng đụng vào object detection cụ thể. Mình cố giải thích vừa đủ chậm để bạn theo được, và đủ chính xác để bạn không bị nhầm như mấy bài explainer hay viết kiểu "có lẽ là CSPDarknet" hay "VFL được dùng cho classification" (cả hai câu này đều sai khi mở source code ra check).
Object detection 101: bốn khái niệm để vào sâu
Object detection khác classification ở một chỗ duy nhất nhưng quan trọng. Classification trả về một label cho cả ảnh, ví dụ "đây là con mèo". Detection trả về một danh sách: "ở vùng này có con mèo, ở vùng kia có con chó, ở vùng nọ có cái cốc", mỗi vùng là một bounding box (bbox) với toạ độ cụ thể. Một ảnh có thể chứa N object thuộc N class khác nhau, và model phải tìm cả vị trí lẫn tên cho từng cái.
Có hai trường phái lớn để giải bài này. Two-stage như Faster R-CNN làm hai bước: trước tiên sinh ra vài nghìn region proposal có thể chứa object, sau đó classify từng proposal. Chính xác nhưng chậm. One-stage như YOLO bỏ bước proposal, chia ảnh thành lưới grid và predict trực tiếp bbox cùng class cho từng cell trong một lần forward duy nhất. Chậm hơn về accuracy nhưng nhanh hơn nhiều về tốc độ. YOLO viết tắt của "You Only Look Once", chính là tinh thần đó.
Để đo chất lượng detection, người ta dùng IoU (Intersection over Union), tỉ lệ giữa diện tích giao và diện tích hợp của bbox dự đoán với bbox ground truth. IoU = 1 nghĩa là khớp hoàn hảo, 0 nghĩa là không chồng tí nào. [email protected] (mean Average Precision) là độ chính xác trung bình khi lấy ngưỡng IoU 0.5 để coi là detect đúng. mAP càng cao càng tốt, COCO dataset có 80 class nên mAP COCO trên 50 đã được coi là khá.

Khái niệm cuối là anchor box. Trong YOLOv2 đến v5, mỗi grid cell có sẵn k box mẫu kích thước cố định (preset bằng k-means trên training data). Model không predict bbox từ con số 0, mà predict offset từ anchor mẫu sang bbox thật. Anchor giúp training ổn định hơn vì model có điểm xuất phát hợp lý, nhưng đòi hỏi bạn phải chọn anchor sizes thủ công cho từng dataset, và xử lý dở với object có aspect ratio bất thường.

Bốn thứ này là nền. Giờ vào YOLOv8.
Bức tranh tổng thể: Backbone, Neck, Head
YOLOv8 và phần lớn detector hiện đại đều có cấu trúc ba tầng. Ảnh đi qua backbone để extract feature, qua neck để trộn feature ở nhiều scale, rồi qua head để predict bbox và class. Mỗi tầng có cải tiến riêng so với v5, và mỗi cải tiến đều mượn ý tưởng từ paper khác.

Sơ đồ này do cộng đồng (RangeKing) vẽ lại từ source code. Bạn không cần đọc kỹ ngay, mình sẽ đi từng phần trong các section sau. Cứ giữ ba từ trong đầu: backbone đi từ dưới lên (P1 → P5), neck đi sang phải tạo ra feature ở 3 scale (P3, P4, P5), head ở cuối predict 3 lần ở 3 scale đó.
Bây giờ đào sâu từng tầng.
Backbone: C2f thay cho C3
Backbone của YOLOv8 vẫn theo trường phái CSPDarknet như v5, nhưng với một thay đổi cốt lõi ở khối module nhỏ. Trước khi giải thích thay đổi đó, cần hiểu CSP làm gì.
CSP viết tắt của Cross Stage Partial. Ý tưởng đơn giản: thay vì cho toàn bộ feature map đi qua một chuỗi N cái Bottleneck convolution sâu (tốn compute, tốn memory), bạn chia feature theo channel làm hai nửa. Một nửa đi qua chuỗi sâu, một nửa shortcut thẳng sang đầu ra. Cuối cùng concat hai nửa lại. Lợi ích là bạn vẫn có depth nhưng giảm gradient redundancy (gradient không phải đi qua hết mọi layer), và parameter-efficient hơn.
Trong YOLOv5 module CSP được implement dưới tên C3 (3 conv layers + n bottleneck). Trong YOLOv8 nó được thay bằng C2f (faster CSP với 2 conv).
Khác biệt quan trọng nằm ở chỗ giữ lại bao nhiêu intermediate feature. C3 chia feature thành 2 phần, một phần đi qua n cái Bottleneck rồi concat với phần shortcut. Tức là bạn chỉ giữ output cuối cùng của chuỗi bottleneck. Tất cả feature trung gian ở giữa chuỗi đều bị bỏ đi.
C2f làm khác. Nó cũng split feature thành 2 chunks, nhưng giữ toàn bộ output trung gian của n bottleneck rồi concat tất cả lại. Nếu bạn có n=3 bottleneck, thì output là concat của (chunk1, chunk2, bottleneck1_out, bottleneck2_out, bottleneck3_out). Tổng cộng 5 feature map ghép lại, không phải 2 như C3.
Tại sao việc này quan trọng? Mỗi bottleneck trung gian thực ra học một feature representation khác nhau, từ thấp đến cao. Bằng cách giữ hết và đưa vào cv2 conv cuối cùng, bạn cho phép layer cuối tự chọn weighted combination tốt nhất, đồng thời tạo nhiều residual path cho gradient backprop. Idea này không mới, nó được mượn từ ELAN (Efficient Layer Aggregation Network) trong YOLOv7. Ultralytics gọi nó là C2f, nhưng tinh thần là ELAN-style block với 2 convolution thay vì 3.

Kết quả thực tế là YOLOv8 train ổn định hơn ở model size lớn, parameter-efficient hơn ở model size nhỏ. Nếu bạn muốn xem code: ultralytics/nn/modules/block.py line 288 cho C2f, line 322 cho C3. Đọc 30 dòng là thấy ngay khác biệt.
Một module nhỏ nữa trong backbone đáng nhắc là SPPF (Spatial Pyramid Pooling Fast), nằm ở cuối backbone trước khi vào neck. SPP gốc dùng 3 cái MaxPool kernel khác nhau (5, 9, 13) chạy song song rồi concat 4 feature map. SPPF làm thông minh hơn: chỉ dùng 1 kernel size 5×5 nhưng chạy 3 lần liên tiếp, output mỗi lần được giữ và concat. Receptive field tương đương SPP nhưng chia sẻ được intermediate computation, nhanh hơn khoảng 2 lần. SPPF cũng được kế thừa nguyên từ YOLOv5.
Neck: PAN+FPN, top-down rồi bottom-up
Sau backbone bạn có một stack feature map ở các độ phân giải khác nhau. Ví dụ ảnh input 640×640 sẽ ra P3 (80×80, stride 8), P4 (40×40, stride 16), P5 (20×20, stride 32). Vấn đề bây giờ là mỗi feature map mạnh ở một chuyện khác nhau. P5 sâu, có semantic tốt (biết "đây là loại cat") nhưng spatial thô. P3 nông, spatial chi tiết nhưng semantic yếu. Object nhỏ cần spatial từ P3, object lớn cần semantic từ P5.
Neck là nơi trộn ba feature map này lại. YOLOv8 dùng kết hợp FPN (Feature Pyramid Network, top-down) và PAN (Path Aggregation Network, bottom-up).
FPN đi top-down. Bắt đầu từ P5 (sâu nhất), upsample bilinear lên kích thước của P4, concat với P4 gốc, qua một C2f. Output này lại upsample lên kích thước P3, concat với P3 gốc, qua một C2f nữa. Như vậy semantic info từ deep layer được "rưới" xuống các shallow layer. Sau pass này, mỗi level đều có cả spatial detail của chính nó lẫn semantic context từ trên.
PAN đi ngược lại, bottom-up. Lấy feature ở P3 (đã được FPN xử lý), downsample bằng Conv stride 2 lên kích thước P4, concat với feature P4 (từ FPN), qua C2f. Tiếp tục downsample lên P5. Pass này pass localization info từ shallow layer ngược lên deep layer. Tại sao cần? Vì spatial detail dễ bị nhoè đi qua nhiều lớp downsample của backbone, PAN giúp khôi phục lại.

Cuối cùng output 3 feature map ở 3 scale (P3, P4, P5), mỗi feature map mang cả semantic lẫn spatial từ mọi tầng khác. Đây là input cho head.
So với v5, neck của v8 đơn giản hơn một chút. v5 có 2 conv 1×1 ở đầu top-down path để giảm channel trước khi concat. v8 bỏ 2 conv này, concat trực tiếp rồi để C2f tự xử lý channel reduction. Ít thành phần, ít hyperparameter.
Head: Decoupled, Anchor-free, và DFL
Đây là phần khác biệt nhất so với v5, và cũng là phần thú vị nhất khi đọc source code. Mình tách ra 3 sub-section.
Decoupled head
YOLOv5 dùng coupled head, tức là một conv 1×1 duy nhất predict cả ba thứ một lúc: bbox coordinates, objectness score (xác suất ô này có chứa object gì đó), và class probabilities. Một conv, một output tensor, mọi thứ chia sẻ cùng feature.
YOLOv8 tách thành decoupled head: hai nhánh song song. Nhánh cv2 predict bbox, nhánh cv3 predict class. Và bỏ luôn objectness branch (lý do sẽ giải thích ở dưới).
Tại sao tách? Classification và localization về bản chất cần feature khác nhau. Classification cần invariant feature, ví dụ "con mèo dù ở góc nào, scale nào cũng là con mèo". Localization cần spatial-sensitive feature, "cạnh trên của bbox đang ở pixel y=247". Hai task này nếu share cùng một conv layer thì conv đó phải compromise giữa hai mục tiêu, không tối ưu cho cái nào.

Idea decoupled head không phải Ultralytics nghĩ ra. Nó được giới thiệu trong YOLOX (Megvii, 2021), và đã chứng minh tăng cả mAP lẫn convergence speed so với coupled head trên benchmark COCO. v8 mượn idea này và giữ nguyên tinh thần.
Anchor-free
YOLOv5 và các phiên bản trước dùng anchor-based: mỗi grid cell có 3 anchor box mẫu (preset từ k-means trên training data), model predict 4 offset từ anchor sang bbox thật. Cách này có vài vấn đề. Bạn phải chạy k-means cho mỗi dataset mới. Số anchor (3 × số cell) lớn, tạo nhiều positive sample dư thừa. Object có aspect ratio cực đoan (như cột điện cao và mảnh) bị anchor chuẩn hoá kéo về aspect quen thuộc, predict không chính xác.
YOLOv8 bỏ hoàn toàn anchor box. Mỗi grid cell có một anchor point ở chính giữa cell (chứ không phải anchor box bao quanh). Model predict 4 distance từ point đó đến 4 cạnh của bbox: left, top, right, bottom. Bốn số scalar này, cộng với toạ độ của anchor point, đủ để dựng lại bbox (xyxy hoặc xywh đều được).

Cách này gọn hơn vì không cần hyperparameter anchor sizes, không cần k-means. Mỗi cell chỉ có 1 prediction (không phải k=3 như anchor-based), giảm số positive sample, training nhanh hơn. Và predict aspect ratio bất kỳ vì không bị anchor mẫu kéo về.
Việc bỏ luôn objectness branch cũng liên quan tới đây. Trong setup anchor-based, objectness có vai trò tách "anchor có object" khỏi "anchor không có object" (vì k×grid cell anchor là rất nhiều, đa số không có object thật). Khi anchor-free chỉ còn 1 prediction per cell, classification score per class là đủ để biết cell nào có object (chỉ cần check max class score). Objectness trở nên dư thừa, bỏ đi.
DFL: Distribution Focal Loss
Đây là innovation khéo nhất, và phần lớn explainer YOLOv8 hoặc skip hẳn hoặc giải thích sai. Mình đào kỹ phần này.
Ý tưởng cơ bản trước. Mình vừa nói model predict 4 distance scalar (left, top, right, bottom) cho mỗi anchor point. Cách "ngây thơ" là output trực tiếp 4 số float bằng một conv layer. Đây là cách mọi paper trước đây làm.
YOLOv8 không output 4 scalar. Nó output 4 phân phối discrete, mỗi phân phối có 16 bins (reg_max=16 mặc định). Tức là cho mỗi distance, model output 16 logits, đi qua softmax để ra một phân phối xác suất trên 16 giá trị nguyên (0, 1, 2, ..., 15). Sau đó scalar cuối cùng được tính bằng expectation:
$$\hat{d} = \sum_{i=0}^{15} i \cdot \text{softmax}(z)_i$$
với $z$ là 16 logits của distance đó. Output cuối là $\hat{d}$, cùng dạng scalar như cách ngây thơ, nhưng đi qua một bước trung gian: phân phối discrete.
Tại sao làm phức tạp lên? Câu trả lời nằm ở việc distance trong ảnh thật không phải Dirac delta. Cạnh của object thường không sắc nét. Có thể bị blur, bị occluded, có thể nằm trong vùng ambiguous (lá cây có hàng nghìn pixel cạnh, đâu mới là "cạnh thật" của bbox?). Khi bạn buộc model output 1 scalar cố định, bạn ép nó pretend về một độ chắc chắn không có.
Phân phối discrete cho phép model thể hiện sự không chắc. Khi cạnh rõ ràng, distribution sẽ peak nhọn ở một bin, expectation ra số chính xác. Khi cạnh mờ, distribution sẽ flat ra trên vài bin, expectation vẫn ra một số trung bình, nhưng huấn luyện có thể tận dụng thông tin uncertainty này. Ngoài ra, vì softmax bounded (mọi probability nằm trong [0, 1] và sum bằng 1), phân phối discrete dễ optimize hơn so với regress trực tiếp scalar không bounded.

Idea này gọi là DFL (Distribution Focal Loss), được giới thiệu trong paper Generalized Focal Loss V2 (Li et al., IEEE TPAMI 2022). YOLOv8 mượn nguyên xi. Code module DFL ở ultralytics/nn/modules/block.py line 58 đến 80, đáng đọc vì chỉ 20 dòng nhưng implement đẹp: nó là một conv layer 1×1 với weights cố định bằng [0, 1, 2, ..., 15], đặt trên softmax. Forward pass thực chất là tính expectation.

Tổng kết head: mỗi cell, mỗi scale, model output 4 × 16 = 64 numbers cho box (qua DFL ra 4 distance, decode ra bbox), cộng nc numbers cho class (qua sigmoid ra probability per class). Không có objectness. Không có anchor. Đó là toàn bộ.
Loss và training tricks
Loss tổng của YOLOv8 là tổng có trọng số của 3 thành phần:
$$\mathcal{L} = 7.5 \cdot \mathcal{L}{\text{CIoU}} + 0.5 \cdot \mathcal{L}{\text{BCE}} + 1.5 \cdot \mathcal{L}_{\text{DFL}}$$
(Default gain ở ultralytics/cfg/default.yaml. Bạn fine-tune được.)
Classification loss dùng nn.BCEWithLogitsLoss, tức binary cross entropy per class. Một lưu ý quan trọng: nhiều bài viết về YOLOv8 (kể cả paper survey mình đọc tối qua) nói model dùng VFL (Varifocal Loss). Mình đã grep source code và thấy class VarifocalLoss có tồn tại ở loss.py line 21, nhưng nó không được instantiate trong v8DetectionLoss. Loss thật sự dùng là BCE đơn giản. Đây là sai lầm hay gặp khi đọc explainer mà không check code.
Box loss chia 2 phần. Phần CIoU loss tính trên decoded bboxes, penalize cả overlap, distance giữa centers, và aspect ratio mismatch. Phần DFL loss là cross-entropy giữa predicted distribution và 2 bins lân cận target distance (linear interpolation). Hai phần này dạy model hai chuyện khác nhau: CIoU dạy bbox decode đúng vị trí và shape, DFL dạy distribution có shape hợp lý (peak gần target, không spread linh tinh).
Label assignment dùng TaskAlignedAssigner (TAL), từ paper TOOD (ICCV 2021). Assignment là bước trước khi tính loss: với mỗi ground truth bbox, bạn phải chọn anchor point nào là positive (sẽ được train để predict bbox đó), anchor nào là negative. Cách ngây thơ là chọn anchor có IoU cao nhất với GT. TAL làm khéo hơn: tính score cls_score^0.5 × iou^6.0 cho mỗi anchor, chọn top-k=10 anchor có score cao nhất làm positive. Ý tưởng là align quality của classification với quality của localization, không chọn anchor có IoU cao nhưng cls confidence thấp.
Ngoài loss và assignment, có vài training trick đáng nhắc:
close_mosaic=10. Mosaic là augmentation ghép 4 ảnh thành 1 (mỗi ảnh chiếm 1 góc), tạo thêm variety về scale, position, context. YOLOv8 bật mosaic suốt training, nhưng tắt 10 epoch cuối cùng. Lý do: mosaic tạo phân phối ảnh không giống test set (test set là ảnh đơn, không phải ghép). Tắt mosaic cuối cùng để model hội tụ trên distribution sạch, tăng vài điểm mAP. Trick này từ YOLOX.
Optimizer auto. Mặc định optimizer=auto. Logic ở trainer.py line 972 đến 997: nếu tổng iteration > 10000 (train COCO full thường vào case này) thì dùng SGD với lr=0.01; ngược lại dùng AdamW. Lý do là SGD generalize tốt hơn ở training run dài, AdamW converge nhanh hơn ở training run ngắn (fine-tune dataset nhỏ).
Mixup và copy_paste mặc định OFF cho detection. Mọi tutorial bạn đọc đều nói "YOLOv8 dùng mixup", nhưng default config là mixup=0.0. Mixup chỉ bật khi train scale rất lớn hoặc segmentation task. Cho detection thường thì mosaic + HSV + flip + scale + translate là đủ.
Năm model size, chọn cái nào?
YOLOv8 có 5 variant, scale theo depth và width. Đây là số liệu thật từ Ultralytics, không phải số sai trong nhiều bài explainer (paper mình đọc tối qua claim YOLOv8x có 90M params, thực tế là 68.2M).
| Model | Params | GFLOPs | mAP COCO 50-95 | Use case |
|---|---|---|---|---|
| YOLOv8n | 3.2M | 8.9 | 37.3 | Edge device, mobile, IoT |
| YOLOv8s | 11.2M | 28.8 | 44.9 | Real-time CPU, server nhẹ |
| YOLOv8m | 25.9M | 79.3 | 50.2 | Real-time GPU, sweet spot cho phần lớn case |
| YOLOv8l | 43.7M | 165.7 | 52.9 | High accuracy GPU |
| YOLOv8x | 68.2M | 258.5 | 53.9 | Max accuracy, không quan tâm latency |
Rule of thumb của mình: bắt đầu bằng YOLOv8m. Nó là sweet spot, train nhanh trên 1 GPU vừa, deploy được trên hầu hết edge GPU. Nếu accuracy đủ cho use case của bạn thì giảm xuống s hoặc n để tiết kiệm compute. Nếu chưa đủ thì mới lên l hoặc x. Đừng default x vì "to là tốt", bạn sẽ phải trả giá gấp 8 lần FLOPs để đổi 4 điểm mAP.
Một lưu ý: số mAP trong bảng là COCO benchmark. Trên dataset của bạn, ranking giữa các model có thể giữ nguyên nhưng absolute number sẽ rất khác. Đừng kỳ vọng 53.9 mAP trên dataset 5000 ảnh logo của bạn chỉ vì YOLOv8x đạt được con số đó trên COCO.
Vài thứ để mang theo
YOLOv8 không phải breakthrough đột ngột. Đọc kỹ thì nó là một composition khéo của ý tưởng từ vài paper trước đó: backbone CSP-style từ v5, C2f mượn ELAN của v7, decoupled head và anchor-free và close_mosaic mượn YOLOX, DFL mượn Generalized Focal Loss, TaskAlignedAssigner mượn TOOD. Ultralytics gọi nó là "anchor-free split Ultralytics head" trong docs như thể tự nghĩ ra, nhưng credit thật sự thuộc về 4-5 paper từ 2020 đến 2022.
Điều này không phải chê. Composition tốt là một loại kỹ thuật riêng, và Ultralytics làm rất giỏi việc gói tất cả thành một Python package mà bạn pip install ultralytics rồi yolo train data=coco.yaml model=yolov8m.pt là chạy được. Phần lớn dev không quan tâm những idea hay đến từ đâu, chỉ cần biết là pip install xong gõ một lệnh là model train xong. Ultralytics phục vụ chính xác nhu cầu đó, và nhờ vậy YOLOv8 trở thành baseline mà cả ngành dùng làm điểm so sánh.
Quay lại UAV Hackathon. Mình ôn YOLOv8 không phải để dùng nó trong cuộc thi (drone perception 2026 chắc chắn cần model mới hơn), mà để có baseline mental model. Khi đọc paper YOLOv12 hay RT-DETR mới ra, mình cần biết "à, chỗ này họ thay DFL bằng gì", "à, họ vẫn giữ TaskAlignedAssigner hay đổi". Không có baseline thì paper mới đọc như liệt kê thuật ngữ.
Bài viết tiếp theo trên blog có thể là so sánh YOLOv12 vs v8 trên dataset drone, hoặc một bài về RT-DETR (transformer detector mà mình đang nghi ngờ liệu có thật sự nhanh hơn YOLO trên edge). Sẽ thấy sau khi mình chạy thử.
Nếu bạn cũng đang chuẩn bị cho hackathon hoặc một dự án CV nào đó, hoặc thấy mình nhầm ở chỗ nào trong bài, mình muốn nghe. YOLOE cũng là một option cho ai cần open-vocabulary detection (nhận diện class mới mà không retrain), mình đã viết riêng một bài.
Bình.
Tham khảo:
- Ultralytics docs YOLOv8: docs.ultralytics.com/models/yolov8
- Source code Ultralytics: github.com/ultralytics/ultralytics
- Yaseen, M. What is YOLOv8: An In-Depth Exploration of the Internal Features of the Next-Generation Object Detector. arXiv:2408.15857 (2024).
- Li, X. et al. Generalized Focal Loss V2: Learning Reliable Localization Quality Estimation for Dense Object Detection. IEEE TPAMI 2022. ieeexplore.ieee.org/document/9792391
- Feng, C. et al. TOOD: Task-aligned One-stage Object Detection. ICCV 2021. arxiv.org/abs/2108.07755
- Ge, Z. et al. YOLOX: Exceeding YOLO Series in 2021. arXiv:2107.08430. arxiv.org/abs/2107.08430
- Architecture diagram: RangeKing (community contribution to Ultralytics docs)