A complete YOLOv8 object detection pipeline implemented from scratch in PyTorch — fault-tolerant training, evaluation, fine-tuning, HDF5 dataset builds, ONNX export, and inference. Bonus: a Rust binary for fast ONNX inference on static images or live webcam feed.
Table of Contents
- Description
- Features
- Project structure
- Model architecture
- Installation
- Dataset format
- Usage
- Configuration files
- How the training works
- Fault-tolerant training
- Documentation
- To contribute
- Licence
- Acknowledgments
- References
- Contact
This project is a full re-implementation of YOLOv8 in pure PyTorch — no third-party YOLO dependency. It is designed to be readable, hackable, and easy to run on your own dataset. Every component (backbone, neck, head, loss, metrics, augmentations, trainer) is written from scratch and documented.
A companion Rust binary lets you run ONNX inference at native speed, either on a single image or in real-time from a webcam.
- Datasets as folders or zip archives, with a
data.yamlclass list, a pre-flight validation scan and a JSON scan cache (train.cache.json) for instant restarts. - HDF5 dataset builds (
buildh5ds): pre-compute letterboxed (and optionally augmented) samples intotrain.h5/test.h5and train straight from them (use_hdf5: true). - Fault-tolerant training: a checkpoint every
ckpt_stepoptimizer steps (and during val/test passes) captures the model, optimizer, EMA, AMP scaler, dataloader positions, partial meters and RNG states. Training resumes mid-epoch without ever seeing a sample twice. - Resumable DataLoader adapter: the shuffle order is a pure function of (seed + epoch), so the exact epoch order is rebuilt after a crash.
- Structured run folders:
runs/<name>/train,train2, ... andeval,eval2, ... each withweights/,checkpoints/,plotes/,logs/,history.csvandconfig_used.yaml. - Disjoint validation / final test:
val_probtakes a deterministic fraction of the test split for per-epoch validation (model selection, early stopping); the final evaluation runs on the held-out remainder, so the reported test metrics are never computed on samples that drove the model selection. - Rich augmentations: mosaic (with
close_mosaic), HSV jitter, affine transforms, MixUp, Cutout, blur, noise, grayscale. - Cosine and linear LR schedulers with full warm-up (LR, per-group bias LR, momentum); SGD / Adam / AdamW.
- EMA weights for validation and
best.pt; AMP on CUDA; gradient accumulation with end-of-epoch flush; early stopping. - Full COCO-style evaluation: mAP@0.5, mAP@0.5:0.95, macro/micro P/R/F1, PR and F1-confidence curves, confusion matrices, prediction renders.
- Export to ONNX (with optional FP16, graph simplification, and numerical verification) and a fully standalone ONNX inference script.
- Unit tests for the model, loss, metrics, dataset, adapter, schedulers and the trainer (including mid-epoch resume).
.
├── src/yolov8/
│ ├── modules/ # the building blocks of the network
│ │ ├── scaling.py # (depth, width, ratio) for n/s/m/l/x
│ │ ├── conv.py # Conv2d + BatchNorm + SiLU
│ │ ├── c2f.py # CSP block: split, bottlenecks, merge
│ │ ├── sppf.py # fast spatial pyramid pooling
│ │ ├── dfl.py # bin distribution -> one distance
│ │ ├── anchors.py # anchor centers + stride of each anchor
│ │ ├── upsample.py # nearest neighbor upsample
│ │ ├── backbone.py # CSPDarknet, returns P3/P4/P5
│ │ ├── neck.py # PAN-FPN fusion of the three scales
│ │ └── head.py # decoupled anchor-free detection head
│ ├── model.py # YOLO (backbone + neck + head)
│ ├── lossfn.py # TAL assigner + CIoU + DFL + BCE loss
│ ├── dataset/
│ │ ├── sources.py # folder and zip dataset sources
│ │ ├── scanner.py # validation scan + JSON cache
│ │ ├── validation.py # image and label file checks
│ │ ├── names.py # class names from data.yaml
│ │ ├── transforms.py # letterbox, tensor conversion
│ │ ├── augment.py # all augmentations + Augmenter
│ │ ├── yolo_dataset.py # the detection Dataset itself
│ │ ├── hdf5_store.py # HDF5 build + read
│ │ ├── adapter.py # resumable DataLoader adapter
│ │ └── factory.py # dataset construction from the config
│ ├── metrics/
│ │ ├── boxes.py # IoU and box format helpers
│ │ ├── nms.py # non maximum suppression
│ │ ├── ap.py # COCO 101-point AP
│ │ └── evaluation.py # accumulator, mAP, confusion matrix
│ ├── training/
│ │ ├── optimizers.py # param groups, SGD/Adam/AdamW, freezing
│ │ ├── lr_schedulers.py# cosine / linear with full warmup
│ │ ├── ema.py # exponential moving average of weights
│ │ ├── meters.py # resumable loss meters
│ │ ├── checkpoints.py # naming, rotation, RNG capture
│ │ ├── runs.py # runs/<name>/train[i] folders
│ │ └── trainer.py # the fault-tolerant training loop
│ ├── logging.py # loguru setup + torchinfo summary
│ ├── plotting.py # history and evaluation figures
│ ├── onnx_export.py # ONNX graph, FP16, simplify, verify
│ ├── devices.py # device selection
│ ├── config.py # nested dataclass configs + YAML loaders
│ └── entrypoints/
│ ├── buildds.py # build HDF5 datasets
│ ├── train.py # train + val + final test
│ ├── evaluate.py # full evaluation
│ ├── exportmodel.py # ONNX export
│ ├── inference.py # standalone ONNX inference
│ └── finetuning.py # build a fine-tunable checkpoint
├── cpu/configs/ # ready-made configs for CPU
├── gpu/configs/ # ready-made configs for NVIDIA CUDA / AMD ROCm
├── tests/ # pytest unit and integration tests
├── docs/
│ ├── en_concepts.md # beginner-friendly concept guide (English)
│ ├── fr_concepts.md # the same guide in French
│ └── metrics/ # LaTeX note on the evaluation metrics
├── archive/ # first exploratory version, kept as a reference
├── predict.py # standalone ONNX inference on one image
├── live.py # standalone ONNX inference on a webcam
├── yolov8rust/src/main.rs # Rust ONNX inference binary (image)
└── yololivers/src/main.rs # Rust ONNX inference binary (webcam)
The whole network is backbone -> neck -> head, built by
model.py from the blocks in
modules/.
One letter picks the size. scaling.py turns it into three factors:
depth d (how many bottlenecks per block), width w (how many
channels) and ratio r (extra width of the deepest stage).
| Version | d | w | r | Params (nc=80) |
|---|---|---|---|---|
n |
1/3 | 1/4 | 2.0 | 3.16 M |
s |
1/3 | 1/2 | 2.0 | 11.17 M |
m |
2/3 | 3/4 | 1.5 | 25.90 M |
l |
1.0 | 1.0 | 1.0 | 43.69 M |
x |
1.0 | 1.25 | 1.0 | 68.23 M |
These counts match the reference YOLOv8 models. Run
python -m yolov8.model to check them yourself.
A CSPDarknet: five stride-2 Conv blocks (Conv2d + BatchNorm + SiLU)
that halve the image each time, each followed by a C2f block except
the first one. It returns the three feature maps used for detection,
at strides 8, 16 and 32 (P3, P4, P5). The last stage ends with an
SPPF: three chained 5x5 max pools concatenated together, a cheap way
to mix several receptive fields.
C2f is the CSP idea: a 1x1 conv, then split the channels in two. The
first half goes straight to the output, the second half runs through
the bottleneck chain, and every intermediate result is kept. All of
them are concatenated and mixed by a final 1x1 conv. This gives many
gradient paths for few FLOPs.
A PAN-FPN. First a top-down pass: upsample P5, concatenate with P4,
C2f; upsample that, concatenate with P3, C2f. Then a bottom-up
pass with stride-2 convs going back up and concatenating again. Small
objects get the semantic context of the deep layers, and big objects
keep the fine spatial detail.
Anchor-free and decoupled: at each of the three scales, two separate branches (each = two 3x3 Conv blocks + one 1x1 Conv2d) predict boxes and classes. Splitting them matters because localizing and classifying do not want the same features.
The box branch does not regress 4 numbers. It predicts, for each side
of the box, a probability distribution over 16 bins (reg_max).
The DFL layer takes the expected value of that distribution with a
frozen 1x1 conv holding the weights [0, 1, ..., 15]. The model can
therefore say "this edge is probably here, but it is fuzzy", which
regresses ambiguous edges much better than a single number.
Two details that are easy to get wrong:
- Branch widths follow the standard YOLOv8 convention:
box_mid = max(16, ch0/4, 64)andcls_mid = max(ch0, min(nc, 100)). The class branch width must not followncalone: with 10 classes that would squeeze every classification feature through a 10-channel bottleneck and cap the mAP. - Bias initialization (
initialize_biases): the class biases start atlog(5 / nc / (640 / stride)^2), a prior for "few objects per cell". Without it every anchor starts at sigmoid = 0.5, the summed BCE explodes and the gradients saturate the clipping for several epochs.
Strides are not hardcoded: the constructor runs a dummy forward pass and measures them, then sets the biases (which depend on them).
In train() mode the head returns the three raw maps. In eval() it
returns (decoded, raw): the decoded tensor
(B, 4 + nc, n_anchors) in image space for NMS, and the raw maps so
the validation loss can be computed without a second forward pass.
pip install git+https://github.com/cacybernetic/YOLO8
# or, faster:
uv pip install git+https://github.com/cacybernetic/YOLO8This registers the CLI tools (trainyolo8, evalyolo8, runyolo8,
ftyolo8, exportw, buildh5ds). Download the configuration files
from cpu/configs/ or gpu/configs/.
curl -LsSf https://astral.sh/uv/install.sh | sh # install uv
git clone https://github.com/cacybernetic/YOLO8
cd YOLO8
uv venv --python 3.10
source .venv/bin/activate
uv pip install -e .Note — headless server (no display):
sudo apt-get install libgl1-mesa-glx libglib2.0-0
pip install uv
uv venv --python 3.10
.venv\Scripts\activate
uv pip install -e .Only needed for the Rust ONNX inference binary.
- Install Rust: rustup.rs
cargo build --release
The binary is compiled to target/release/yolov8rust.
A dataset split is either a folder or a zip archive with this layout:
train.zip (or train/)
├── images/ # .jpg, .png, .jpeg, ...
├── labels/ # one .txt per image
└── data.yaml # class names
data.yaml must contain at least a names list (nc is optional):
nc: 10
names:
- door
- cabinetDoor
- refrigeratorDoor
- window
- chair
- table
- cabinet
- couch
- openedDoor
- poleEach .txt label file contains one object per line, values normalized
to [0, 1]:
<class_id> <cx> <cy> <w> <h>
Train and test are given separately (train_path, test_path). The
test split is divided into two disjoint parts with val_prob
(default 0.5): a validation part used for the per-epoch metrics and
the model selection, and a held-out part used for the final
evaluation. Sample caps are available via max_train_samples /
max_test_samples.
Before anything runs, the dataset is scanned: corrupt images, missing
labels and malformed lines are dropped with a report, and the result is
cached in train.cache.json / test.cache.json next to the dataset.
Six commands are installed:
| Command | Role |
|---|---|
buildh5ds |
Build HDF5 datasets (train.h5, test.h5) |
trainyolo8 |
Train a model (train + val + final test) |
evalyolo8 |
Evaluate a model on the full test set |
runyolo8 |
Standalone ONNX inference on an image |
ftyolo8 |
Build a fine-tunable checkpoint |
exportw |
Export to ONNX |
Each command takes a single --config argument pointing to its YAML
file. Pick the folder matching your hardware: cpu/configs/ or
gpu/configs/.
buildh5ds --config gpu/configs/hdf5.yamlThen set use_hdf5: true in train.yaml to train from train.h5 /
test.h5. Use augmented_copies: N to also bake N augmented copies of
each train sample into the file.
Edit gpu/configs/train.yaml (dataset paths, run_name), then:
trainyolo8 --config gpu/configs/train.yamlEverything lands in runs/<run_name>/train[i]/:
weights/best.pt(EMA weights of the best epoch) andweights/last.ptcheckpoints/checkpoint_eXXXXcYYYY.pth— fault-tolerance snapshotsplotes/training_history.png,history.csv,logs/,config_used.yamltest_results.csv— final evaluation on the held-out test part
With resume: true (default), restarting the same command reuses the
latest run folder and continues from the newest checkpoint — even in
the middle of an epoch or of a validation pass.
evalyolo8 --config gpu/configs/eval.yamlResults go to runs/<run_name>/eval[i]/: results.csv (global
metrics), per_class.csv, plotes/ (PR curve, F1-confidence curve,
confusion matrices) and renders/ (example predictions).
Step 1 — build the fine-tunable checkpoint:
ftyolo8 --config gpu/configs/finetuning.yamlThis transfers the backbone, neck, box branches and DFL, and re-initializes the classification heads for the new class count.
Step 2 — train as usual with, in train.yaml:
model:
pretrained_weights: weights/finetune_init.pt
freeze_feature_layers: true # recommended for small datasetsexportw --config gpu/configs/export.yamlThe exported .onnx file is numerically verified against the PyTorch
model by default.
runyolo8 is a fully standalone ONNX inference script (numpy +
opencv + onnxruntime only — you can copy
src/yolov8/entrypoints/inference.py anywhere and it keeps working):
runyolo8 --model weights/best.onnx --nc 10 \
--image photo.jpg --output result.jpgOptions: --conf 0.25, --iou 0.45, --size 640, --show,
--names classes.txt (one class name per line).
Two extra helper scripts at the project root run a pre-trained ONNX
model without any dependency on the yolov8 package:
# Single image (numpy + Pillow only)
python predict.py --model weights/best.onnx --nc 80 \
--image samples/photo.jpg --output result.jpg
# Webcam or video file (OpenCV)
python live.py --model weights/best.onnx --nc 80 --source 0./target/release/yolov8rust \
--model weights/best.onnx --image photo.jpg \
--output result.jpg --nc 80 --conf 0.25 --iou 0.45
# Live from webcam
./target/release/yololivers --model weights/best.onnx --source 0 --nc 80All behavior is controlled through the YAML files in cpu/configs/ and
gpu/configs/. The train config is nested:
| Block | Key fields |
|---|---|
| (top level) | run_name, output_dir, device, seed, log_interval |
dataset |
train_path, test_path, use_hdf5, train_h5, test_h5, validate, cache, max_train_samples, max_test_samples, val_prob, image_size, augment.* |
model |
version (n/s/m/l/x), pretrained_weights, freeze_feature_layers |
optimization |
epochs, batch_size, optimizer, max_lr, scheduler, grad_accum, amp, ema, patience |
loss |
box_gain, cls_gain, dfl_gain |
checkpoint |
ckpt_step, max_checkpoint, resume, best_metric |
validation |
interval, conf_threshold, iou_threshold |
Unknown keys are reported with a warning and ignored.
lossfn.py computes three terms, summed with
the gains from the loss: block (defaults box_gain: 7.5,
cls_gain: 0.5, dfl_gain: 1.5):
| Term | What it does |
|---|---|
box |
CIoU between the predicted and the target box. On top of the plain IoU it adds the distance between the centers and a term on the aspect ratio, so a box that does not overlap yet still gets a useful gradient. |
dfl |
Distribution Focal Loss on the 16 bins of each box side: it pushes the probability mass onto the two bins around the true distance. |
cls |
BCE on the class logits, one independent sigmoid per class. |
There are no anchor boxes, so something must decide which of the ~8400
anchor points is responsible for each object. This is the
Task-Aligned Assigner (Assigner, top_k=10, alpha=0.5,
beta=6.0).
For every (anchor, object) pair it computes an alignment score:
score = cls_score^alpha * CIoU^beta
then keeps the top_k best anchors per object among those whose
center falls inside the box. An anchor claimed by several objects is
given to the one it overlaps best. The assignment therefore
follows the model as it learns, instead of relying on a fixed IoU
threshold: an anchor becomes positive because it is good at both
tasks at once, which is what keeps classification and localization
from drifting apart.
The classification target is not a hard 1: it is the normalized alignment score, so a well aligned anchor is asked for a higher confidence than a barely aligned one.
- Three parameter groups (
optimizers.py): weights withdim >= 2get the weight decay, 1-D weights (BatchNorm) and biases get none. Decaying BatchNorm and bias terms hurts for no gain. - Weight decay is scaled by the effective batch:
wd * (batch_size * grad_accum) / nbswithnbs: 64. The default0.0005was tuned for a batch of 64; a batch of 16 would otherwise be over-regularized. - Full warmup (
lr_schedulers.py): the weight LR rises from 0 tomax_lr, the bias LR comes down fromwarmup_bias_lr: 0.1tomax_lr(the biases need to move fast at the start), and the momentum rises fromwarmup_momentum: 0.8tomomentum. The warmup is capped at 30% of the total step budget so a short run keeps a real decay phase, then cosine or linear decay tomin_lr. - EMA (
ema.py): a smoothed copy of the weights, with a decay that grows asdecay * (1 - exp(-updates / tau)). Validation andbest.ptuse the EMA weights, which are much more stable than the raw ones. - AMP on CUDA, but the loss always runs in float32: fp16 targets would quantize the box coordinates.
- Gradient accumulation simulates a large batch, with a flush at
the end of the epoch so a partial group is never dropped, and
gradient clipping at
grad_clip. close_mosaic: mosaic and mixup are turned off for the last N epochs, so the model finishes on real images rather than collages.
Each epoch runs train, then val every validation.interval
epochs. The validation metrics drive best.pt and the early stopping
(patience). When training ends, a final test runs on the part of
the test split that validation never touched (see val_prob), so the
reported numbers are not the ones that selected the model.
The training loop checkpoints every ckpt_step optimizer steps into
checkpoints/checkpoint_e<epoch>c<step>.pth (one file per save point,
rotated with max_checkpoint). Each checkpoint stores:
- model, optimizer, EMA, AMP scaler state;
- the position of the three dataloader adapters (train / val / test);
- the partial loss meters and metric accumulators;
- the RNG states (python, numpy, torch, cuda);
- the training history and the config.
On restart with resume: true, the highest-numbered run folder holding
a checkpoint is reused and training continues exactly where it stopped:
mid-train-epoch, mid-validation or mid-final-test, without seeing any
sample twice in the same epoch. This makes multi-day epochs safe
against power or system failures.
- docs/en_concepts.md — beginner-friendly guide to every concept used here (English).
- docs/fr_concepts.md — le même guide en français.
Run the test suite with:
make test # or: pytest testsContributions are welcome! Please follow these steps:
- Fork the repository and clone it locally.
- Create a new branch for your feature:
git checkout -b feature/my-feature - Commit your changes:
git commit -m 'Add a new feature' - Push to the branch:
git push origin feature/my-feature - Open a Pull Request.
This project is licensed under the MIT License. See the LICENSE file for details.
This project was built while learning the inner workings of YOLOv8. A
huge thank-you to dtdo90 for the
excellent educational repository
dtdo90/yolov8_detection
and the accompanying
YouTube walkthrough,
both of which served as the primary reference for understanding the
architecture (backbone, neck, head). Many implementation choices in
this project — the structure of the Detect head, and the integration
of the DFL into the box regression — are directly inspired by its work.
If you find this project useful, please consider giving the dtdo90 repository a star as a token of appreciation for the educational content that made it possible.
The implementation is based on the following papers and resources:
- TAL — Task-Aligned Assigner — Feng, C., Zhong, Y., Gao, Y., Scott, M. R., & Huang, W. (2021). TOOD: Task-Aligned One-stage Object Detection. ICCV 2021. arXiv:2108.07755
- DFL — Distribution Focal Loss — Li, X., Wang, W., Wu, L., Chen, S., Hu, X., Li, J., Tang, J., & Yang, J. (2020). Generalized Focal Loss: Learning Qualified and Distributed Bounding Boxes for Dense Object Detection. NeurIPS 2020. arXiv:2006.04388
- CIoU Loss — Zheng, Z., Wang, P., Liu, W., Li, J., Ye, R., & Ren, D. (2020). Distance-IoU Loss: Faster and Better Learning for Bounding Box Regression. AAAI 2020. arXiv:1911.08287
- Focal Loss — Lin, T.-Y., Goyal, P., Girshick, R., He, K., & Dollár, P.
(2017). Focal Loss for Dense Object Detection. ICCV 2017. Used as the
reference for the bias initialization of the new classification heads
during fine-tuning (
b = -log((1-π)/π)with π=0.01). arXiv:1708.02002
- CSPNet — Wang, C.-Y., Liao, H.-Y. M., Wu, Y.-H., Chen, P.-Y., Hsieh, J.-W., & Yeh, I.-H. (2020). CSPNet: A New Backbone that can Enhance Learning Capability of CNN. CVPRW 2020. Foundation of the C2f blocks used in the backbone. arXiv:1911.11929
- PAN — Path Aggregation Network — Liu, S., Qi, L., Qin, H., Shi, J., & Jia, J. (2018). Path Aggregation Network for Instance Segmentation. CVPR 2018. Used as the basis for the multi-scale neck. arXiv:1803.01534
- SPPF — He, K., Zhang, X., Ren, S., & Sun, J. (2015). Spatial Pyramid Pooling in Deep Convolutional Networks for Visual Recognition. The Fast variant (SPPF) used in this project is the standard YOLOv5/v8 design. arXiv:1406.4729
- COCO evaluation protocol — Lin, T.-Y., Maire, M., Belongie, S., Hays, J., Perona, P., Ramanan, D., Dollár, P., & Zitnick, C. L. (2014). Microsoft COCO: Common Objects in Context. ECCV 2014. Source of the 101-point AP interpolation and the IoU thresholds 0.5:0.05:0.95. arXiv:1405.0312
- Survey on detection metrics — Padilla, R., Netto, S. L., & da Silva, E. A. B. (2020). A Survey on Performance Metrics for Object-Detection Algorithms. IWSSIP 2020. DOI
- dtdo90/yolov8_detection — DT Do (2024). Implementation of the YOLOv8 detection model with an accompanying YouTube tutorial.
For questions or suggestions:
- Author: DOCTOR MOKIRA — dr.mokira@gmail.com
- Maintainer: CONSOLE ART CYBERNETIC — ca.cybernetic@gmail.com
- GitHub: cacybernetic/YOLO8
