<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Abrar Ahmad Blog</title>
    <link>https://iamabrarahmad.com/blog</link>
    <description>Practical articles on AI engineering, computer vision, RAG and multi-agent systems.</description>
    <language>en</language>
    <atom:link href="https://iamabrarahmad.com/feed.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Train YOLOv8 on a Custom Dataset in Google Colab</title>
      <link>https://iamabrarahmad.com/blog/train-yolov8-custom-dataset-google-colab</link>
      <guid isPermaLink="true">https://iamabrarahmad.com/blog/train-yolov8-custom-dataset-google-colab</guid>
      <pubDate>Tue, 03 Nov 2026 00:00:00 GMT</pubDate>
      <description>Step-by-step guide to training YOLOv8 on your own dataset in Google Colab: dataset format, data.yaml, training, metrics, inference and export.</description>
      <category>YOLOv8</category>
      <category>Computer Vision</category>
      <category>Object Detection</category>
      <category>Google Colab</category>
      <category>Python</category>
      <content:encoded><![CDATA[<p>Pretrained YOLO models know 80 everyday objects: people, cars, dogs, cups. Real projects almost always need something else: number plates, helmets, potholes, cracks in concrete. The fastest way to get there is to fine-tune a pretrained YOLOv8 model on your own labelled images.</p>
<p>This guide walks through the full process in Google Colab, from an empty notebook to a trained model you can run on images and video. As a running example I use my own <a href="https://iamabrarahmad.com/projects/number-plate">car number plate detector</a>: a labelled dataset from Roboflow, a free Colab T4 GPU and the Ultralytics YOLOv8 package. The final model reached <strong>97.9% mAP@50</strong> and <strong>99.1% precision</strong> on its validation set, although, as you&#39;ll see below, it took me three training runs to get there.</p>
<h2 id="what-you-need"><a href="https://iamabrarahmad.com/blog/train-yolov8-custom-dataset-google-colab#what-you-need">What you need</a></h2>
<ul>
<li>A Google account for <a href="https://colab.research.google.com" target="_blank" rel="noopener noreferrer">Google Colab</a>. The free tier is enough to start.</li>
<li>Images of the object you want to detect, with bounding-box labels in YOLO format. I used a number plate dataset from <a href="https://universe.roboflow.com" target="_blank" rel="noopener noreferrer">Roboflow Universe</a>, which exports straight to YOLOv8 format. CVAT and Label Studio work too if you label your own images.</li>
<li>Time. A small dataset trains in under an hour; my final 50-epoch run took about 3.2 hours on a T4.</li>
</ul>
<h2 id="step-1-switch-colab-to-a-gpu"><a href="https://iamabrarahmad.com/blog/train-yolov8-custom-dataset-google-colab#step-1-switch-colab-to-a-gpu">Step 1: Switch Colab to a GPU</a></h2>
<p>Training on a CPU is painfully slow. In Colab, open <strong>Runtime → Change runtime type</strong> and pick a GPU, then check that it&#39;s there:</p>
<pre><code class="language-python">!nvidia-smi
</code></pre>
<p>You should see a table with the GPU name and memory. On the free tier I got an NVIDIA T4, which is plenty for a nano or small YOLO model. If you get an error, the runtime is still on CPU.</p>
<h2 id="step-2-install-ultralytics"><a href="https://iamabrarahmad.com/blog/train-yolov8-custom-dataset-google-colab#step-2-install-ultralytics">Step 2: Install Ultralytics</a></h2>
<p>YOLOv8 is published by Ultralytics as a Python package:</p>
<pre><code class="language-python">!pip install ultralytics

import ultralytics
ultralytics.checks()
</code></pre>
<p><code>checks()</code> prints the installed version, Python, PyTorch and whether CUDA (the GPU) is available.</p>
<h2 id="step-3-prepare-the-dataset"><a href="https://iamabrarahmad.com/blog/train-yolov8-custom-dataset-google-colab#step-3-prepare-the-dataset">Step 3: Prepare the dataset</a></h2>
<p>YOLO expects one image folder and one label folder per split. Each image has a <code>.txt</code> file with the same name in the matching labels folder:</p>
<pre><code class="language-text">dataset/
├── data.yaml
├── train/
│   ├── images/   car_001.jpg, car_002.jpg, ...
│   └── labels/   car_001.txt, car_002.txt, ...
└── valid/
    ├── images/
    └── labels/
</code></pre>
<p>Each line in a label file describes one object:</p>
<pre><code class="language-text">0 0.512 0.634 0.180 0.062
</code></pre>
<p>The five numbers are the class index, then the box centre (x, y), width and height. All four box values are <strong>normalised to 0–1</strong> by the image width and height, not in pixels. An image with no objects can have an empty label file; YOLO treats it as a background example, which helps reduce false positives.</p>
<h3 id="the-datayaml-file"><a href="https://iamabrarahmad.com/blog/train-yolov8-custom-dataset-google-colab#the-datayaml-file">The data.yaml file</a></h3>
<p><code>data.yaml</code> tells YOLO where the splits are and what each class index means. For a single-class detector like mine:</p>
<pre><code class="language-yaml">path: /content/dataset
train: train/images
val: valid/images

names:
  0: number_plate
</code></pre>
<p>For more classes, keep adding lines (<code>1: helmet</code>, <code>2: no_helmet</code>, and so on). The indices must match the first number in your label files.</p>
<h3 id="getting-the-data-into-colab"><a href="https://iamabrarahmad.com/blog/train-yolov8-custom-dataset-google-colab#getting-the-data-into-colab">Getting the data into Colab</a></h3>
<p>If your data is on Roboflow, open the dataset version, click <strong>Export</strong>, choose the <strong>YOLOv8</strong> format and download the zip. It already has the folder layout above and a <code>data.yaml</code>.</p>
<p>The simplest way to get the zip into Colab is through Google Drive: upload it there, then unzip it onto the Colab machine. Reading thousands of small files directly from Drive is slow, so copy it to local disk first:</p>
<pre><code class="language-python">from google.colab import drive
drive.mount(&#39;/content/drive&#39;)

!unzip -q /content/drive/MyDrive/dataset.zip -d /content/
</code></pre>
<p>Check that <code>/content/dataset/data.yaml</code> exists before moving on, and open it once: exported files sometimes contain relative paths like <code>../train/images</code> that don&#39;t match where you unzipped. A wrong path here causes the most common error at the next step: <em>dataset not found</em>.</p>
<h2 id="step-4-train-the-model"><a href="https://iamabrarahmad.com/blog/train-yolov8-custom-dataset-google-colab#step-4-train-the-model">Step 4: Train the model</a></h2>
<p>Start from pretrained weights rather than from scratch. The model already understands edges, shapes and textures, so it only has to learn your object:</p>
<pre><code class="language-python">from ultralytics import YOLO

model = YOLO(&quot;yolov8n.pt&quot;)  # nano: smallest and fastest

results = model.train(
    data=&quot;/content/dataset/data.yaml&quot;,
    epochs=50,
    imgsz=640,
    batch=16,
)
</code></pre>
<p>These are exactly the settings I used for the number plate model. What each one means:</p>
<table>
<thead>
<tr>
<th>Setting</th>
<th>Value</th>
<th>Why</th>
</tr>
</thead>
<tbody><tr>
<td><code>yolov8n.pt</code></td>
<td>nano model, about 3M parameters</td>
<td>Fast and small (my trained weights are 6.2 MB). Try <code>yolov8s.pt</code> or <code>yolov8m.pt</code> if accuracy stalls.</td>
</tr>
<tr>
<td><code>epochs</code></td>
<td>50</td>
<td>Passes over the training set. Most of the improvement comes early; more epochs refine box quality.</td>
</tr>
<tr>
<td><code>imgsz</code></td>
<td>640</td>
<td>Input size in pixels. Keep 640 or higher for small objects like plates.</td>
</tr>
<tr>
<td><code>batch</code></td>
<td>16</td>
<td>Images per step. Lower it if you run out of GPU memory, or use <code>batch=-1</code> to let Ultralytics pick.</td>
</tr>
</tbody></table>
<p>Ultralytics applies sensible augmentation by default: mosaic, horizontal flips, colour jitter, translation and scaling. It also switches mosaic off for the last 10 epochs (<code>close_mosaic=10</code>) so the model finishes on natural-looking images. You rarely need to change these for a first run.</p>
<h3 id="where-the-results-go"><a href="https://iamabrarahmad.com/blog/train-yolov8-custom-dataset-google-colab#where-the-results-go">Where the results go</a></h3>
<p>Everything is saved to <code>runs/detect/train/</code> (the next run goes to <code>train2</code>, and so on):</p>
<ul>
<li><code>weights/best.pt</code>: the checkpoint with the best validation score. This is the one you use.</li>
<li><code>weights/last.pt</code>: the latest checkpoint, for resuming.</li>
<li><code>results.csv</code> and <code>results.png</code>: loss and metrics for every epoch.</li>
<li><code>confusion_matrix.png</code>, precision-recall and F1 curves, and sample predictions on validation images.</li>
</ul>
<h3 id="save-your-work-before-colab-disconnects"><a href="https://iamabrarahmad.com/blog/train-yolov8-custom-dataset-google-colab#save-your-work-before-colab-disconnects">Save your work before Colab disconnects</a></h3>
<p>Colab deletes the machine&#39;s files when the session ends. Copy the run folder to Drive as soon as training finishes:</p>
<pre><code class="language-python">!cp -r /content/runs/detect/train /content/drive/MyDrive/yolo_runs/
</code></pre>
<p>If the session drops in the middle of training, resume from the last checkpoint instead of starting over:</p>
<pre><code class="language-python">model = YOLO(&quot;/content/runs/detect/train/weights/last.pt&quot;)
model.train(resume=True)
</code></pre>
<p>This happened to me: on the free tier, my Colab session disconnected partway through a multi-hour run. Ultralytics saves <code>last.pt</code> after every epoch, so I resumed from it and training continued from the epoch where it stopped, with the same settings, instead of starting again from epoch 1. It only works if <code>last.pt</code> still exists, so on long runs copy the run folder to Drive regularly, not just at the end.</p>
<h2 id="step-5-read-the-metrics"><a href="https://iamabrarahmad.com/blog/train-yolov8-custom-dataset-google-colab#step-5-read-the-metrics">Step 5: Read the metrics</a></h2>
<p>After training, validate the best checkpoint:</p>
<pre><code class="language-python">model = YOLO(&quot;/content/runs/detect/train/weights/best.pt&quot;)
metrics = model.val()

print(f&quot;Precision: {metrics.box.mp:.3f}&quot;)
print(f&quot;Recall:    {metrics.box.mr:.3f}&quot;)
print(f&quot;mAP@50:    {metrics.box.map50:.3f}&quot;)
print(f&quot;mAP@50-95: {metrics.box.map:.3f}&quot;)
</code></pre>
<p>What the four numbers mean:</p>
<ul>
<li><strong>Precision</strong>: of the boxes the model drew, how many were correct. Low precision means false alarms.</li>
<li><strong>Recall</strong>: of the real objects, how many the model found. Low recall means missed objects.</li>
<li><strong>mAP@50</strong>: average precision when a box only needs to overlap the true box by 50%. A good &quot;did it find the object&quot; score.</li>
<li><strong>mAP@50-95</strong>: the same, averaged over stricter overlaps from 50% to 95%. This measures how <em>tight</em> the boxes are.</li>
</ul>
<p>For a deeper explanation of each metric, with training curves from four of my models and what to do when a number is low, see <a href="https://iamabrarahmad.com/blog/yolov8-metrics-explained">YOLOv8 Metrics Explained: Precision, Recall and mAP</a>.</p>
<p>Here is how those numbers developed on my number plate model:</p>
<table>
<thead>
<tr>
<th>Epoch</th>
<th>Precision</th>
<th>Recall</th>
<th>mAP@50</th>
<th>mAP@50-95</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>89.7%</td>
<td>87.2%</td>
<td>88.9%</td>
<td>63.7%</td>
</tr>
<tr>
<td>10</td>
<td>96.7%</td>
<td>93.7%</td>
<td>96.2%</td>
<td>75.2%</td>
</tr>
<tr>
<td>20</td>
<td>98.7%</td>
<td>94.7%</td>
<td>97.0%</td>
<td>77.3%</td>
</tr>
<tr>
<td>42 (best)</td>
<td>99.1%</td>
<td>94.7%</td>
<td>97.9%</td>
<td>79.8%</td>
</tr>
</tbody></table>
<p>Two things stand out. First, transfer learning does the heavy lifting: after a single epoch the model already scored 88.9% mAP@50. Second, after epoch 20, mAP@50 barely moves, but mAP@50-95 keeps climbing. The later epochs are making the boxes tighter, which matters if you crop the detection for another step, like reading the plate with OCR.</p>
<h2 id="step-6-run-the-model-on-images-and-video"><a href="https://iamabrarahmad.com/blog/train-yolov8-custom-dataset-google-colab#step-6-run-the-model-on-images-and-video">Step 6: Run the model on images and video</a></h2>
<p>Use <code>best.pt</code> for predictions. The same call works for a single image, a folder, a video file or a webcam:</p>
<pre><code class="language-python">model = YOLO(&quot;/content/runs/detect/train/weights/best.pt&quot;)

results = model.predict(source=&quot;test_car.jpg&quot;, conf=0.5, save=True)
model.predict(source=&quot;traffic.mp4&quot;, conf=0.5, save=True)
</code></pre>
<p><code>conf=0.5</code> hides detections below 50% confidence. Raise it if you see false positives, lower it if the model misses objects. Annotated images and videos are saved under <code>runs/detect/predict/</code>.</p>
<p>Here is a frame from my model running on a highway test video:</p>
<p><img src="https://iamabrarahmad.com/blog/train-yolov8-custom-dataset-google-colab/detection-result.webp" alt="YOLOv8 number plate detector drawing boxes with 0.79 and 0.77 confidence on the plates of the two nearest cars in highway traffic" loading="lazy" decoding="async" /></p>
<p><em>Output of my trained model on a test clip: the plates of the two nearest vehicles are detected with 0.79 and 0.77 confidence.</em></p>
<p>The frame also shows something the validation numbers don&#39;t. The model reliably finds plates close to the camera, but plates further back are only a few pixels tall in this 852×480 clip, and it doesn&#39;t detect them. Validation images look like the training set; real footage is a different test. For a real ANPR camera, higher resolution or a camera closer to the lane matters as much as the model. You can <a href="https://lnkd.in/p/dYWX6D9F" target="_blank" rel="noopener noreferrer">watch the full demo video on LinkedIn</a>.</p>
<p>To use the detections in your own code, read the boxes from the results:</p>
<pre><code class="language-python">for box in results[0].boxes:
    x1, y1, x2, y2 = box.xyxy[0].tolist()
    print(f&quot;class={int(box.cls)} conf={float(box.conf):.2f} box=({x1:.0f}, {y1:.0f}, {x2:.0f}, {y2:.0f})&quot;)
</code></pre>
<h2 id="step-7-export-for-deployment"><a href="https://iamabrarahmad.com/blog/train-yolov8-custom-dataset-google-colab#step-7-export-for-deployment">Step 7: Export for deployment</a></h2>
<p>For production, export the model to a format your target runtime understands:</p>
<pre><code class="language-python">model.export(format=&quot;onnx&quot;)
</code></pre>
<p>ONNX runs almost anywhere, including CPU servers. Other options include <code>engine</code> (TensorRT, for NVIDIA GPUs and Jetson boards), <code>tflite</code> (mobile) and <code>openvino</code> (Intel CPUs).</p>
<h2 id="what-went-wrong-in-my-training"><a href="https://iamabrarahmad.com/blog/train-yolov8-custom-dataset-google-colab#what-went-wrong-in-my-training">What went wrong in my training</a></h2>
<p>The numbers in this post come from my <strong>third</strong> training run. Ultralytics names each new run folder <code>train</code>, <code>train2</code>, <code>train3</code>, so the history is right there in <code>runs/detect/</code>.</p>
<ul>
<li><strong>Colab disconnected mid-training.</strong> Free Colab sessions don&#39;t last forever, and a 50-epoch run takes hours. Resuming from <code>last.pt</code> (see Step 4) saved me from restarting from scratch.</li>
<li><strong>The first results weren&#39;t good enough.</strong> My first run didn&#39;t reach the accuracy I wanted, so I changed the training settings and trained again. The final run used the settings shown in Step 4.</li>
</ul>
<p>What I&#39;d tell myself before starting: save to Drive from the beginning, and when a run disappoints, change one setting at a time and compare the <code>results.csv</code> files. Otherwise you can&#39;t tell which change actually helped.</p>
<h2 id="common-problems-and-fixes"><a href="https://iamabrarahmad.com/blog/train-yolov8-custom-dataset-google-colab#common-problems-and-fixes">Common problems and fixes</a></h2>
<ul>
<li><strong>&quot;Dataset not found&quot;</strong>: the paths in <code>data.yaml</code> are wrong. Use an absolute <code>path:</code> like <code>/content/dataset</code> and check the folder names match (<code>valid</code> vs <code>val</code> is a classic).</li>
<li><strong>CUDA out of memory</strong>: lower <code>batch</code> (16 → 8) or use <code>batch=-1</code>.</li>
<li><strong>Zero or very low mAP</strong>: usually a labelling problem. Check that label files have the same names as the images, that coordinates are normalised to 0–1, and that class indices match <code>data.yaml</code>.</li>
<li><strong>Good validation scores, poor results on real footage</strong>: the training images don&#39;t look like the real camera. Add images from the actual setting: same angle, lighting and resolution.</li>
<li><strong>Training is slow</strong>: confirm the GPU is active (Step 1), and keep the dataset on Colab&#39;s local disk rather than reading it from Drive.</li>
</ul>
<h2 id="where-to-go-next"><a href="https://iamabrarahmad.com/blog/train-yolov8-custom-dataset-google-colab#where-to-go-next">Where to go next</a></h2>
<ul>
<li>Compare model sizes: train <code>yolov8s.pt</code> with the same data and see whether the accuracy gain is worth the extra size and latency.</li>
<li>The same workflow works for newer Ultralytics models such as YOLO11: change the weights file (for example <code>yolo11n.pt</code>) and keep everything else.</li>
<li>Check the licence before commercial use: Ultralytics models are released under AGPL-3.0, with a separate enterprise licence for closed-source products.</li>
</ul>
<p>The trained weights (<code>best.pt</code>) and detection script for this guide are on <a href="https://github.com/code-by-abrar/Car-number-plate-detection" target="_blank" rel="noopener noreferrer">GitHub</a>, and the full design decisions, training curve and next steps are in the <a href="https://iamabrarahmad.com/projects/number-plate">car number plate detection case study</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>YOLOv8 Metrics Explained: Precision, Recall and mAP</title>
      <link>https://iamabrarahmad.com/blog/yolov8-metrics-explained</link>
      <guid isPermaLink="true">https://iamabrarahmad.com/blog/yolov8-metrics-explained</guid>
      <pubDate>Tue, 03 Nov 2026 00:00:00 GMT</pubDate>
      <description>What precision, recall, mAP50 and mAP50-95 really mean in YOLOv8, explained with real training results from four of my models, and how to act on them.</description>
      <category>YOLOv8</category>
      <category>Computer Vision</category>
      <category>Object Detection</category>
      <category>Model Evaluation</category>
      <category>Machine Learning</category>
      <content:encoded><![CDATA[<p>You finish training a YOLOv8 model and the console prints four numbers: <strong>P</strong>, <strong>R</strong>, <strong>mAP50</strong> and <strong>mAP50-95</strong>. Your model scores 79.6% on one and 54.8% on another. Is that good? Which one should you put in your report? And what should you change if it&#39;s too low?</p>
<p>Most explanations stop at the definitions. In this post I explain each metric with <strong>real training results from four models I trained</strong> with Ultralytics YOLOv8: a number plate detector, an office activity tracker, a plant disease detector and a fire and smoke detector. They are very different tasks, and their numbers tell very different stories.</p>
<p>If you haven&#39;t trained a model yet, start with my step-by-step guide: <a href="https://iamabrarahmad.com/blog/train-yolov8-custom-dataset-google-colab">Train YOLOv8 on a Custom Dataset in Google Colab</a>.</p>
<h2 id="the-four-models-in-this-post"><a href="https://iamabrarahmad.com/blog/yolov8-metrics-explained#the-four-models-in-this-post">The four models in this post</a></h2>
<table>
<thead>
<tr>
<th>Model</th>
<th>Task</th>
<th>Classes</th>
<th>YOLOv8 size</th>
</tr>
</thead>
<tbody><tr>
<td><a href="https://iamabrarahmad.com/projects/number-plate">Car number plate detection</a></td>
<td>Find number plates in traffic video</td>
<td>1</td>
<td>nano</td>
</tr>
<tr>
<td><a href="https://iamabrarahmad.com/projects/office-activity">Office activity tracking</a></td>
<td>Label what each person is doing</td>
<td>6</td>
<td>nano</td>
</tr>
<tr>
<td><a href="https://iamabrarahmad.com/projects/plant-disease">Plant disease detection</a></td>
<td>Identify plant and disease per leaf</td>
<td>29</td>
<td>nano</td>
</tr>
<tr>
<td><a href="https://iamabrarahmad.com/projects/fire-smoke">Fire &amp; smoke detection</a></td>
<td>Find flames and smoke in CCTV</td>
<td>2</td>
<td>small</td>
</tr>
</tbody></table>
<p>And their final validation results, taken from the metrics Ultralytics saves inside each model&#39;s <code>best.pt</code>:</p>
<table>
<thead>
<tr>
<th>Model</th>
<th>Precision</th>
<th>Recall</th>
<th>mAP@50</th>
<th>mAP@50-95</th>
</tr>
</thead>
<tbody><tr>
<td>Number plate</td>
<td>99.1%</td>
<td>94.7%</td>
<td>97.9%</td>
<td>79.8%</td>
</tr>
<tr>
<td>Office activity</td>
<td>83.4%</td>
<td>87.0%</td>
<td>88.3%</td>
<td>74.4%</td>
</tr>
<tr>
<td>Plant disease</td>
<td>79.6%</td>
<td>72.9%</td>
<td>79.6%</td>
<td>54.8%</td>
</tr>
<tr>
<td>Fire &amp; smoke</td>
<td>67.8%</td>
<td>63.8%</td>
<td>71.0%</td>
<td>47.0%</td>
</tr>
</tbody></table>
<p>Keep this table in mind. By the end of the post, you&#39;ll be able to read it like a diagnosis.</p>
<h2 id="where-these-numbers-come-from"><a href="https://iamabrarahmad.com/blog/yolov8-metrics-explained#where-these-numbers-come-from">Where these numbers come from</a></h2>
<p>Every number in this post is measured on the <strong>validation split</strong>: the images listed under <code>val:</code> in your <code>data.yaml</code>, which the model never trains on. After each epoch, Ultralytics runs the model on those images, compares its boxes with the labels and logs the results to <code>runs/detect/train/results.csv</code>.</p>
<p>You&#39;ll see the metrics written as <code>metrics/precision(B)</code> or <code>metrics/mAP50-95(B)</code>. The <strong>(B)</strong> means <em>box</em>. Segmentation models also log a second set marked <strong>(M)</strong> for <em>mask</em>, measured on the pixel masks instead of the boxes.</p>
<h2 id="iou-how-yolo-decides-a-box-is-correct"><a href="https://iamabrarahmad.com/blog/yolov8-metrics-explained#iou-how-yolo-decides-a-box-is-correct">IoU: how YOLO decides a box is &quot;correct&quot;</a></h2>
<p>Every metric depends on one question: did the predicted box land on the object? YOLO answers it with <strong>IoU, Intersection over Union</strong>:</p>
<pre><code class="language-text">IoU = area where the two boxes overlap / area covered by both boxes together
</code></pre>
<ul>
<li>IoU = 1.0: the predicted box matches the labelled box exactly.</li>
<li>IoU = 0.5: they overlap by about half.</li>
<li>IoU = 0: they don&#39;t touch.</li>
</ul>
<p>A prediction counts as a <strong>true positive</strong> if it has the right class <em>and</em> its IoU with a labelled box is above a threshold. Everything else is a <strong>false positive</strong>. A labelled object that no prediction matched is a <strong>false negative</strong>, a miss.</p>
<p>Hold on to the threshold: it&#39;s the whole difference between mAP50 and mAP50-95.</p>
<h2 id="precision-when-the-model-says-plate-is-it-right"><a href="https://iamabrarahmad.com/blog/yolov8-metrics-explained#precision-when-the-model-says-plate-is-it-right">Precision: when the model says &quot;plate&quot;, is it right?</a></h2>
<pre><code class="language-text">Precision = true positives / everything the model detected
</code></pre>
<p>Precision answers: <em>of the boxes the model drew, how many were real objects?</em> Low precision means <strong>false alarms</strong>: boxes on things that aren&#39;t there.</p>
<p>My number plate model has <strong>99.1% precision</strong>: almost every box it draws is a real plate. That matters, because in a number plate recognition system every false box triggers an OCR read and can create a wrong record.</p>
<h2 id="recall-of-the-real-objects-how-many-did-it-find"><a href="https://iamabrarahmad.com/blog/yolov8-metrics-explained#recall-of-the-real-objects-how-many-did-it-find">Recall: of the real objects, how many did it find?</a></h2>
<pre><code class="language-text">Recall = true positives / all objects in the labels
</code></pre>
<p>Recall answers: <em>of everything that was really there, how much did the model find?</em> Low recall means <strong>misses</strong>.</p>
<p>The same number plate model has <strong>94.7% recall</strong>: it finds about 95 of every 100 plates. Very good, but notice it&#39;s lower than its precision. The model is slightly conservative: it would rather skip a plate than draw a wrong box.</p>
<h3 id="precision-and-recall-pull-against-each-other"><a href="https://iamabrarahmad.com/blog/yolov8-metrics-explained#precision-and-recall-pull-against-each-other">Precision and recall pull against each other</a></h3>
<p>Each detection comes with a confidence score, and the threshold you choose moves the balance. A high threshold keeps only confident boxes: precision goes up, recall goes down. A low threshold catches more objects, along with more false alarms.</p>
<p>One detail that confuses many people: <strong>the P and R that Ultralytics prints are not measured at your <code>conf=0.5</code></strong>. They&#39;re reported at the confidence threshold that gives the best F1 score (the balance of precision and recall) on the validation set. In deployment you pick your own threshold, so your real-world precision and recall will be different. Use the printed P and R to compare models, and the precision-recall and F1 curves in the run folder to choose your threshold.</p>
<h2 id="a-real-example-a-model-that-was-too-cautious"><a href="https://iamabrarahmad.com/blog/yolov8-metrics-explained#a-real-example-a-model-that-was-too-cautious">A real example: a model that was too cautious</a></h2>
<p>My fire and smoke detector is the clearest example of precision and recall telling different stories.</p>
<p><img src="https://iamabrarahmad.com/blog/yolov8-metrics-explained/fire-precision-recall.svg" alt="Line chart of validation precision and recall for a YOLOv8s fire and smoke detector over 100 epochs. Precision jumps to around 70% within the first few epochs while recall stays near 30%; recall then climbs steadily to 63.8% by epoch 100 while precision settles in the 60s." loading="lazy" decoding="async" /></p>
<p><em>Precision and recall of my fire and smoke model after every epoch of training.</em></p>
<p>At epoch 10, precision was <strong>73.8%</strong> but recall only <strong>30.7%</strong>. Precision even peaked at <strong>80.0%</strong> at epoch 23, while recall was just 43.3%. If you only looked at precision, you&#39;d think the model was nearly done. It wasn&#39;t: it only fired on the most obvious flames and missed most of the fire and smoke in the images.</p>
<p>From epoch 10 to epoch 100, recall more than doubled, from 30.7% to <strong>63.8%</strong>, while precision settled in the 60s. The final model is <em>less</em> precise than at epoch 23, and far more useful, because it actually finds the fire. A fire alarm that misses most fires is worse than one with a few false alarms you can filter out.</p>
<p><strong>The lesson:</strong> never judge a model on precision alone, or recall alone. Look at both, and decide which mistake costs more in your application.</p>
<h2 id="ap-and-map-one-number-for-the-whole-curve"><a href="https://iamabrarahmad.com/blog/yolov8-metrics-explained#ap-and-map-one-number-for-the-whole-curve">AP and mAP: one number for the whole curve</a></h2>
<p>Since precision and recall depend on the threshold, we want one number that covers every threshold. That&#39;s <strong>Average Precision (AP)</strong>: sort all detections by confidence, trace precision against recall, and measure the area under that curve. A perfect model scores 1.0 (100%).</p>
<p>AP is calculated per class. <strong>mAP</strong>, <em>mean</em> Average Precision, is the average across all classes. For my 29-class plant model, mAP is the average of 29 separate AP scores, so a few weak classes can pull the total down even if most classes are good.</p>
<h2 id="map50-vs-map50-95-the-difference-that-matters-most"><a href="https://iamabrarahmad.com/blog/yolov8-metrics-explained#map50-vs-map50-95-the-difference-that-matters-most">mAP50 vs mAP50-95: the difference that matters most</a></h2>
<ul>
<li><strong>mAP@50</strong> (also written mAP50 or <a href="mailto:mAP@0.5">mAP@0.5</a>) counts a detection as correct if its IoU is at least <strong>0.5</strong>. It asks: <em>did the model find the object?</em></li>
<li><strong>mAP@50-95</strong> calculates mAP at <strong>ten IoU thresholds</strong>, 0.50, 0.55, 0.60 … 0.95, and averages them. At 0.95, the box has to be almost pixel-perfect. It asks: <em>how precisely did the model outline the object?</em></li>
</ul>
<p>That&#39;s why mAP@50-95 is always lower than mAP@50, and why the <strong>gap between them</strong> is so informative. Here is how both developed while training my number plate model:</p>
<p><img src="https://iamabrarahmad.com/blog/yolov8-metrics-explained/number-plate-map.svg" alt="Line chart of validation mAP@50 and mAP@50-95 for a YOLOv8n number plate detector over 50 epochs. mAP@50 starts at 88.9% and levels off near 98% by epoch 20; mAP@50-95 starts at 63.7% and keeps rising slowly to about 80%. A dashed line marks the best checkpoint at epoch 42." loading="lazy" decoding="async" /></p>
<p><em>mAP@50 and mAP@50-95 of my number plate model after every epoch.</em></p>
<p>Two things stand out:</p>
<ol>
<li><strong>mAP@50 was high almost immediately.</strong> Thanks to the pretrained weights, the model hit 88.9% after a single epoch and was close to its final level by epoch 20. Finding plates was easy.</li>
<li><strong>mAP@50-95 kept climbing</strong> long after mAP@50 had flattened, from 77.3% at epoch 20 to 79.8% at its best. The later epochs weren&#39;t teaching the model to find <em>more</em> plates; they were teaching it to draw <em>tighter</em> boxes. For a plate that gets cropped and passed to OCR, that is exactly the improvement you want.</li>
</ol>
<h3 id="the-gap-tells-you-how-well-defined-your-objects-are"><a href="https://iamabrarahmad.com/blog/yolov8-metrics-explained#the-gap-tells-you-how-well-defined-your-objects-are">The gap tells you how well-defined your objects are</a></h3>
<p>Now look at the gap between mAP@50 and mAP@50-95 across all four models:</p>
<table>
<thead>
<tr>
<th>Model</th>
<th>mAP@50</th>
<th>mAP@50-95</th>
<th>Gap</th>
</tr>
</thead>
<tbody><tr>
<td>Office activity</td>
<td>88.3%</td>
<td>74.4%</td>
<td>13.9 points</td>
</tr>
<tr>
<td>Number plate</td>
<td>97.9%</td>
<td>79.8%</td>
<td>18.1 points</td>
</tr>
<tr>
<td>Fire &amp; smoke</td>
<td>71.0%</td>
<td>47.0%</td>
<td>24.0 points</td>
</tr>
<tr>
<td>Plant disease</td>
<td>79.6%</td>
<td>54.8%</td>
<td>24.8 points</td>
</tr>
</tbody></table>
<p>People and number plates have <strong>clear edges</strong>: everyone would draw roughly the same box around them, so the model can match the labels closely and the gap stays small.</p>
<p>Smoke has <strong>no edges at all</strong>. Where does a cloud of smoke end? Two people labelling the same image would draw different boxes, so even a perfect model can&#39;t hit IoU 0.95 consistently. Leaves in the plant dataset overlap, curl and fade into the background, which creates the same problem.</p>
<p>So a low mAP@50-95 is not always the model&#39;s fault. <strong>When the gap is large, check your labels before you blame the model.</strong></p>
<h2 id="why-more-classes-means-lower-numbers"><a href="https://iamabrarahmad.com/blog/yolov8-metrics-explained#why-more-classes-means-lower-numbers">Why more classes means lower numbers</a></h2>
<p>My plant disease model has the lowest mAP@50 of the three nano models, 79.6%, and it&#39;s arguably the most impressive. It has to tell apart <strong>29 classes</strong>, many of which look almost identical: tomato early blight vs tomato late blight vs tomato septoria leaf spot. The number plate model has one class and nothing to confuse it with.</p>
<p>Never compare mAP across different datasets as if it were the same test. 79.6% on 29 look-alike classes and 97.9% on one distinctive class are both good results.</p>
<h2 id="reading-the-training-curve-is-the-model-done"><a href="https://iamabrarahmad.com/blog/yolov8-metrics-explained#reading-the-training-curve-is-the-model-done">Reading the training curve: is the model done?</a></h2>
<p>The curves above answer another important question: <strong>should you train longer?</strong></p>
<ul>
<li><strong>The curve has flattened</strong> (number plate, after about epoch 40): more epochs won&#39;t help much. Improve the data or try a bigger model instead.</li>
<li><strong>The curve is still rising at the last epoch</strong> (plant disease and fire &amp; smoke both had their best mAP@50-95 at their final epoch, 70 and 100): the run ended too early. Train longer.</li>
<li><strong>Training loss keeps falling but validation loss starts rising</strong>: the model is memorising the training images (overfitting). Stop earlier, add more data or augmentation.</li>
</ul>
<h3 id="which-epoch-does-bestpt-come-from"><a href="https://iamabrarahmad.com/blog/yolov8-metrics-explained#which-epoch-does-bestpt-come-from">Which epoch does best.pt come from?</a></h3>
<p>Ultralytics saves the epoch with the best <strong>fitness</strong> score as <code>best.pt</code>. In all four of my runs, the fitness value stored in <code>best.pt</code> is exactly the mAP@50-95 score, so <code>best.pt</code> is the epoch with the highest mAP@50-95. For the number plate model that was epoch 42 of 50, which is why its final <code>best.pt</code> numbers (97.9% mAP@50) are slightly different from the last epoch&#39;s (98.1%).</p>
<p>You can find the best epoch yourself from <code>results.csv</code>:</p>
<pre><code class="language-python">import pandas as pd

df = pd.read_csv(&quot;runs/detect/train/results.csv&quot;)
df.columns = df.columns.str.strip()  # older versions pad the column names with spaces

best = df.loc[df[&quot;metrics/mAP50-95(B)&quot;].idxmax()]
print(best[[&quot;epoch&quot;, &quot;metrics/precision(B)&quot;, &quot;metrics/recall(B)&quot;,
            &quot;metrics/mAP50(B)&quot;, &quot;metrics/mAP50-95(B)&quot;]])
</code></pre>
<h3 id="look-at-every-class-not-just-the-average"><a href="https://iamabrarahmad.com/blog/yolov8-metrics-explained#look-at-every-class-not-just-the-average">Look at every class, not just the average</a></h3>
<p>For multi-class models, the overall mAP hides your weakest classes. Validate <code>best.pt</code> and print the score per class:</p>
<pre><code class="language-python">from ultralytics import YOLO

model = YOLO(&quot;runs/detect/train/weights/best.pt&quot;)
metrics = model.val()

for i, score in enumerate(metrics.box.maps):  # mAP@50-95 per class
    print(f&quot;{model.names[i]:&lt;35} {score:.3f}&quot;)
</code></pre>
<p>The classes at the bottom of that list tell you exactly where to collect more images.</p>
<h2 id="what-to-do-when-a-number-is-low"><a href="https://iamabrarahmad.com/blog/yolov8-metrics-explained#what-to-do-when-a-number-is-low">What to do when a number is low</a></h2>
<table>
<thead>
<tr>
<th>Symptom</th>
<th>What it usually means</th>
<th>What to try</th>
</tr>
</thead>
<tbody><tr>
<td>Low <strong>precision</strong></td>
<td>False alarms on similar-looking things</td>
<td>Add &quot;hard negative&quot; images with no objects (sunsets and red lights for fire, signs for plates); raise the confidence threshold</td>
</tr>
<tr>
<td>Low <strong>recall</strong></td>
<td>The model misses objects</td>
<td>More and more varied training images; larger <code>imgsz</code> for small objects; lower the confidence threshold</td>
</tr>
<tr>
<td>High mAP@50, low mAP@50-95</td>
<td>Finds objects but boxes are loose</td>
<td>Check label consistency; train longer; larger input size</td>
</tr>
<tr>
<td>Low mAP across the board</td>
<td>The model hasn&#39;t learned the task</td>
<td>Check labels and class indices first, then more data, more epochs or a bigger model</td>
</tr>
<tr>
<td>Best epoch = last epoch</td>
<td>Training stopped too early</td>
<td>Increase <code>epochs</code></td>
</tr>
<tr>
<td>One class far below the rest</td>
<td>Too few or confusing examples for it</td>
<td>Collect more images for that class</td>
</tr>
</tbody></table>
<h2 id="which-metric-should-you-report"><a href="https://iamabrarahmad.com/blog/yolov8-metrics-explained#which-metric-should-you-report">Which metric should you report?</a></h2>
<p>There is no single right answer; it depends on what a mistake costs:</p>
<ul>
<li><strong>Number plate recognition:</strong> the detection is cropped for OCR, so tight boxes matter. Report <strong>mAP@50-95</strong> alongside precision.</li>
<li><strong>Fire and safety alarms:</strong> a missed fire is the worst outcome. Report <strong>recall</strong>, and precision at the threshold you deploy with.</li>
<li><strong>Counting and analytics</strong> (office activity, livestock): missing people skews the totals, so <strong>recall</strong> matters, while mAP@50 is a fair summary of detection quality.</li>
<li><strong>Comparing two models on the same dataset:</strong> <strong>mAP@50-95</strong> is the most complete single number, and it&#39;s what COCO benchmarks use.</li>
</ul>
<p>Whatever you report, say which split it&#39;s measured on and at what confidence threshold. &quot;97.9% mAP@50 on the validation set&quot; is a result; &quot;97.9% accuracy&quot; is not.</p>
<h2 id="cheat-sheet"><a href="https://iamabrarahmad.com/blog/yolov8-metrics-explained#cheat-sheet">Cheat sheet</a></h2>
<ul>
<li><strong>IoU</strong>: how much a predicted box overlaps the real one.</li>
<li><strong>Precision</strong>: of the boxes drawn, how many were right. Low = false alarms.</li>
<li><strong>Recall</strong>: of the real objects, how many were found. Low = misses.</li>
<li><strong>mAP@50</strong>: did the model find the objects?</li>
<li><strong>mAP@50-95</strong>: did it outline them precisely?</li>
<li><strong>A big gap between the two</strong>: fuzzy objects or inconsistent labels.</li>
<li><strong>Best epoch is the last epoch</strong>: train longer.</li>
</ul>
<p>The full training details behind each model are in the case studies: <a href="https://iamabrarahmad.com/projects/number-plate">number plate</a>, <a href="https://iamabrarahmad.com/projects/office-activity">office activity</a>, <a href="https://iamabrarahmad.com/projects/plant-disease">plant disease</a> and <a href="https://iamabrarahmad.com/projects/fire-smoke">fire &amp; smoke detection</a>.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
