4.2h — Détection SOTA : YOLO sous ultralytics, scènes difficiles

← Série 04-Vision | 4.2g — YOLO SOTA (terrain de référence) | 4.2j — second wrapper LibreYOLO

Acceptance issue #16337 : faire passer le pipeline ultralytics sur un terrain plus hostile que le 4.2g — occlusions partielles + multi-échelle — et mesurer comment les trois familles de modèles (YOLOv5nu, YOLOv8s, YOLO11m) réagissent à la dégradation.

Pourquoi ce notebook existe : le terrain du 4.2g place 1 à 3 objets bien séparés, taille unique ~14-44 px, pas d’occlusion. C’est un cas facile. Sur des scènes réalistes (rue, usine, médical), on trouve des objets partiellement cachés et des tailles très différentes dans la même image. Ce notebook reproduit ces deux stresseurs et observe l’évolution des métriques par rapport au terrain de référence.

1. Position dans la série

Notebook Terrain Modèles Métrique centrale
4.2c Synthétique AnchorNet (taille unique, peu d’occlusions) AnchorNet from scratch mAP10 (IoU≥0.1)
4.2e Idem, perte rééquilibrée Focal-Loss from scratch mAP10
4.2f Idem (taille unique) Faster R-CNN, RetinaNet, FCOS (torchvision SOTA) mAP10
4.2g Synthétique standard, terrain 4.2c YOLOv5nu, YOLOv8s, YOLO11n, YOLO11s mAP10
4.2h (ici) Synthétique hostile : occlusions + multi-échelle YOLOv5nu, YOLOv8s, YOLO11m mAP10 + dégradation vs 4.2g

Note méthodologique : ce notebook ne remplace pas le 4.2g — il le prolonge. Le terrain de référence reste le terrain canonique pour comparer les familles entre elles. Ici, on garde les modèles (et le protocole) du 4.2g, mais on change le générateur synthétique pour stresser les modèles sur les deux axes où les détecteurs one-stage sont historiquement les plus exposés.

2. Récit YOLO par année (2016 → 2024)

Avant de lancer la mesure, un point sur la famille — pas une fiche constructeur, mais les changements qui expliquent pourquoi trois tailles à trois époques différentes cohabitent dans le state-of-the-art.

2016  YOLOv1   Redmon et al.                — one-stage, 7×7 grille fixe, anchors figées
2017  YOLOv2   Redmon & Farhadi             — anchor boxes k-means, Darknet-19, BN
2018  YOLOv3   Redmon & Farhadi             — FPN multi-échelle (3 échelles), Darknet-53 résiduel
2020  YOLOv4   Bochkovskiy et al.           — CSPDarknet53, PANet neck, mosaic augmentation
2020  YOLOv5   Glenn Jocher / Ultralytics   — PyTorch, auto-anchor, hyperparamètres heuristiques
2022  YOLOv7   Wang et al.                  — E-ELAN, trainable bag-of-freebies
2023  YOLOv8   Ultralytics                  — anchor-free, C2f block, découpled head
2024  YOLO11   Ultralytics                  — C3k2 block (C2f allégé), attention optionnelle

Les trois modèles comparés ici :

  • YOLOv5nu (re-implémentation Ultralytics v8.4, suffix u) : modèle historique, anchor-based, encore distribué et utilisé en production. ~2.6 M params (n) — la baseline de capacité minimale.
  • YOLOv8s : passage anchor-free, C2f block, découpled head. Refonte structurelle. ~11 M params (s).
  • YOLO11m : evolution post-YOLOv8, C3k2 (variante C2f allégée), meilleure efficacité paramétrique. ~20 M params (m) — le modèle de capacité intermédiaire.

Le but de la comparaison : observer comment la capacité (params) traduit le stress (occlusions + multi-échelle), pas de proclamer un “vainqueur” — chaque taille a son domaine.

import json
import time
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
import torch
import cv2
from ultralytics import YOLO

SEED = 16337
IMG = 96
DEVICE = 0 if torch.cuda.is_available() else "cpu"
torch.manual_seed(SEED)
np.random.seed(SEED)

print("seed:", SEED, "| device:", DEVICE,
      "| cuda:", torch.cuda.is_available(),
      "| img:", IMG)
if torch.cuda.is_available():
    print("GPU 0:", torch.cuda.get_device_name(0),
          "mem libre:",
          f"{(torch.cuda.get_device_properties(0).total_memory - torch.cuda.memory_allocated(0)) // 1024**2} MB")
print("ultralytics:", __import__("ultralytics").__version__)
seed: 16337 | device: 0 | cuda: True | img: 96
GPU 0: NVIDIA GeForce RTX 3090 mem libre: 24575 MB
ultralytics: 8.4.155

3. Générateur de scènes difficiles

Le générateur du 4.2g produit 1 à 3 objets non-occludés, taille 14-44 px, espacement minimal. On garde ce squelette, mais on ajoute deux stresseurs :

  1. Occlusions partielles : chaque objet a 30 % de chances d’être recouvert par un grand rectangle sombre (largeur 35-60 % de l’image, hauteur 25-50 %) — simule un objet du premier plan qui passe devant.
  2. Multi-échelle : on remplace la fourchette unique 14-44 px par deux classes :
    • petit objet : 8-18 px (40 % des cas) — exercice classique sur scènes larges
    • gros objet : 30-60 px (60 % des cas) — exercices classiques sur scènes zoomées

Ces deux changements ciblent les deux axes où les détecteurs anchor-based (YOLOv5) et anchor-free (YOLOv8, YOLO11) diffèrent historiquement : la gestion des échelles et la résistance aux occlusions partielles.

def iou_np(a, b):
    """IoU scalaire entre deux boites (x0, y0, w, h)."""
    ix = max(0.0, min(a[0] + a[2], b[0] + b[2]) - max(a[0], b[0]))
    iy = max(0.0, min(a[1] + a[3], b[1] + b[3]) - max(a[1], b[1]))
    inter = ix * iy
    union = a[2] * a[3] + b[2] * b[3] - inter
    return inter / union if union > 0 else 0.0


def make_difficult_image(rng):
    """Image avec 1-4 objets, 30% occlusions, multi-echelle (petit/gros)."""
    img = rng.normal(0, 0.08, (IMG, IMG)).astype(np.float32)
    yy, xx = np.mgrid[0:IMG, 0:IMG]

    # fond : blobs lents (repris du 4.2c/g)
    for _ in range(rng.integers(2, 5)):
        cy, cx = rng.integers(0, IMG, 2)
        s = rng.uniform(18, 50)
        img += 0.10 * rng.uniform(0.6, 1.3) * np.exp(
            -(((yy - cy) ** 2 + (xx - cx) ** 2) / (2 * s * s)))

    boxes, kinds = [], []
    for _ in range(rng.integers(1, 5)):        # 1 a 4 objets
        # multi-echelle : 40% petit (8-18 px), 60% gros (30-60 px)
        if rng.uniform() < 0.40:
            w = int(rng.uniform(8, 18))
        else:
            w = int(rng.uniform(30, 60))
        h = int(max(6, min(60, w * rng.uniform(0.5, 2.0))))
        kind = rng.choice(["rect", "ellipse"])
        kinds.append(kind)
        for _try in range(40):
            x0 = int(rng.integers(2, IMG - w - 2))
            y0 = int(rng.integers(2, IMG - h - 2))
            cand = (x0, y0, w, h)
            if all(iou_np(cand, b) < 0.30 for b in boxes):
                boxes.append(cand)
                break

    for (x0, y0, w, h) in boxes:
        amp = rng.uniform(0.7, 1.2)
        if kind == "rect":
            img[y0:y0 + h, x0:x0 + w] += amp
        else:
            sub = img[y0:y0 + h, x0:x0 + w]
            ey, ex = np.mgrid[0:h, 0:w]
            mask = (((ex - w / 2) / (w / 2)) ** 2
                    + ((ey - h / 2) / (h / 2)) ** 2) <= 1.0
            img[y0:y0 + h, x0:x0 + w] = np.where(mask, sub + amp, sub)

    # stresseur 1 : 30% occlusions par grand rectangle sombre
    if rng.uniform() < 0.30 and len(boxes) > 0:
        oc_w = int(rng.uniform(0.35, 0.60) * IMG)
        oc_h = int(rng.uniform(0.25, 0.50) * IMG)
        oc_x = int(rng.integers(0, IMG - oc_w))
        oc_y = int(rng.integers(0, IMG - oc_h))
        img[oc_y:oc_y + oc_h, oc_x:oc_x + oc_w] -= rng.uniform(0.5, 0.9)

    return np.clip(img, -1.5, 2.5), boxes


def make_split(n, seed):
    rng = np.random.default_rng(seed)
    xs, bs = [], []
    for _ in range(n):
        img, boxes = make_difficult_image(rng)
        xs.append(img)
        bs.append(torch.tensor(boxes, dtype=torch.float32))
    return torch.tensor(np.stack(xs)).unsqueeze(1), bs


N_TRAIN, N_VAL = 600, 200
Xtr, Btr = make_split(N_TRAIN, SEED + 1)
Xva, Bva = make_split(N_VAL, SEED + 2)

n_petit = sum(1 for b in Bva for (x0, y0, w, h) in [tuple(bb) for bb in b.tolist()] if w < 25)
n_gros = sum(1 for b in Bva for (x0, y0, w, h) in [tuple(bb) for bb in b.tolist()] if w >= 25)
print("train:", tuple(Xtr.shape), "| val:", tuple(Xva.shape),
      "| GT val:", sum(len(b) for b in Bva),
      f"| repartition taille: {n_petit} petits (<25px), {n_gros} gros (>=25px)")
train: (600, 1, 96, 96) | val: (200, 1, 96, 96) | GT val: 502 | repartition taille: 224 petits (<25px), 278 gros (>=25px)

4. Format YOLO et dataset

Même format que 4.2g : images PNG + fichiers .txt par image, une ligne par objet class cx cy w h (normalisé 0-1).

import tempfile
DS = Path(tempfile.mkdtemp(prefix="terrain_yolo_diff_"))


def write_yolo_split(X, B, split):
    (DS / "images" / split).mkdir(parents=True, exist_ok=True)
    (DS / "labels" / split).mkdir(parents=True, exist_ok=True)
    for i, (img, boxes) in enumerate(zip(X, B)):
        u8 = (((img - img.min()) / (img.max() - img.min() + 1e-6)) * 255).astype(np.uint8)
        cv2.imwrite(str(DS / "images" / split / f"{i:05d}.png"), u8)
        lines = [f"0 {(x0 + w / 2) / IMG} {(y0 + h / 2) / IMG} {w / IMG} {h / IMG}"
                 for (x0, y0, w, h) in boxes]
        (DS / "labels" / split / f"{i:05d}.txt").write_text(
            "\n".join(lines), encoding="utf-8")


write_yolo_split([x[0].numpy() for x in Xtr], [b.tolist() for b in Btr], "train")
write_yolo_split([x[0].numpy() for x in Xva], [b.tolist() for b in Bva], "val")
(DS / "data.yaml").write_text(
    f"path: {DS.as_posix()}\ntrain: images/train\nval: images/val\nnc: 1\nnames: ['objet']\n",
    encoding="utf-8")

# Dossier dedie inference : on sauve les memes images val en PNG pour les predictions (paths)
INFER_DIR = DS / "inference_val"
INFER_DIR.mkdir(parents=True, exist_ok=True)
for i, img in enumerate(Xva[:, 0].numpy()):
    u8 = (((img - img.min()) / (img.max() - img.min() + 1e-6)) * 255).astype(np.uint8)
    cv2.imwrite(str(INFER_DIR / f"val_{i:05d}.png"), u8)

n_tr = len(list((DS / "images" / "train").glob("*.png")))
n_va = len(list((DS / "images" / "val").glob("*.png")))
n_infer = len(list(INFER_DIR.glob("*.png")))
print(f"dataset YOLO difficile : {n_tr} train / {n_va} val, "
      f"{sum(len(b) for b in Bva)} GT val, "
      f"1 classe, occlusions + multi-echelle, "
      f"{n_infer} images inference (paths) sur disque")
dataset YOLO difficile : 600 train / 200 val, 502 GT val, 1 classe, occlusions + multi-echelle, 200 images inference (paths) sur disque

5. Fine-tuning des trois modèles

Budget réduit (vs 4.2g : 1000 imgs × 6 epochs) pour tenir dans la fenêtre cron worker :

  • 600 images train × 4 époques = 2400 itérations par modèle
  • imgsz=96 (multiple de 32, comme 4.2g)
  • workers=0 (Windows, pas de DataLoader parallèle)
  • device=0 (GPU unique)
  • batch=16 (par défaut ultralytics)

Trois modèles à comparer : - yolov5nu.pt (YOLOv5 nano, anchor-based) - yolov8s.pt (YOLOv8 small, anchor-free, C2f) - yolo11m.pt (YOLO11 medium, anchor-free, C3k2)

EPOCHS = 4
IMGSZ = IMG
BATCH = 16
MODELS = ["yolov5nu.pt", "yolov8s.pt", "yolo11m.pt"]

print("budget commun 4.2h :", N_TRAIN, "images x", EPOCHS, "epoques,"
      "imgsz", IMGSZ, "batch", BATCH)
print("modeles compares :", MODELS)
budget commun 4.2h : 600 images x 4 epoques,imgsz 96 batch 16
modeles compares : ['yolov5nu.pt', 'yolov8s.pt', 'yolo11m.pt']
import contextlib, io

TRAINED = {}
TRAIN_TIMES = {}

for tag in MODELS:
    name = tag.replace(".pt", "")
    print(f"\n=== {name} : fine-tuning ===")
    model = YOLO(tag)
    _buf = io.StringIO()
    t0 = time.time()
    with contextlib.redirect_stdout(_buf), contextlib.redirect_stderr(_buf):
        model.train(
            data=str(DS / "data.yaml"),
            epochs=EPOCHS,
            imgsz=IMGSZ,
            batch=BATCH,
            device=DEVICE,
            workers=0,
            verbose=False,
            project=str(DS / "runs"),
            name=name,
            exist_ok=True,
        )
    dt = time.time() - t0
    TRAIN_TIMES[name] = dt
    TRAINED[name] = YOLO(str(DS / "runs" / name / "weights" / "best.pt"))
    n_params = sum(p.numel() for p in TRAINED[name].model.parameters()) / 1e6
    print(f"  fine-tune OK en {dt:.1f}s, {n_params:.2f}M params")
=== yolov5nu : fine-tuning ===

New https://pypi.org/project/ultralytics/8.4.163 available  Update with 'pip install -U ultralytics'

Ultralytics 8.4.155  Python-3.13.3 torch-2.8.0+cu126 CUDA:0 (NVIDIA GeForce RTX 3090, 24576MiB)

engine\trainer: agnostic_nms=False, amp=True, angle=1.0, augment=False, auto_augment=randaugment, batch=16, bgr=0.0, box=7.5, cache=False, cfg=None, channels_last=None, classes=None, close_mosaic=10, cls=0.5, cls_pw=0.0, cls_remap=True, compile=False, conf=None, copy_paste=0.0, copy_paste_mode=flip, cos_lr=False, cutmix=0.0, data=<USER_PATH>\AppData\Local\Temp\terrain_yolo_diff__en_y84b\data.yaml, degrees=0.0, deterministic=True, device=0, dfl=1.5, dgrad=0.5, dis=6.0, distill_model=None, dlam=1.0, dlog=1.0, dnn=False, dropout=0.0, dynamic=False, embed=None, epochs=4, erasing=0.4, exist_ok=True, fliplr=0.5, flipud=0.0, format=torchscript, fraction=1.0, freeze=None, hsv_h=0.015, hsv_s=0.7, hsv_v=0.4, imgsz=96, iou=0.7, keras=False, kobj=1.0, line_width=None, lr0=0.01, lrf=0.01, mask_ratio=4, max_det=300, mixup=0.0, mode=train, model=yolov5nu.pt, momentum=0.937, mosaic=1.0, multi_scale=0.0, name=yolov5nu, nbs=64, nms=None, opset=None, optimize=False, optimizer=auto, overlap_mask=True, patience=100, perspective=0.0, plots=True, pose=12.0, pretrained=True, profile=False, project=<USER_PATH>\AppData\Local\Temp\terrain_yolo_diff__en_y84b\runs, quantize=None, rect=False, resume=False, retina_masks=False, rle=1.0, save=True, save_conf=False, save_crop=False, save_dir=<USER_PATH>\AppData\Local\Temp\terrain_yolo_diff__en_y84b\runs\yolov5nu, save_frames=False, save_json=False, save_period=-1, save_txt=False, scale=0.5, seed=0, shear=0.0, show=False, show_boxes=True, show_conf=True, show_labels=True, simplify=True, single_cls=False, source=None, split=val, stream_buffer=False, task=detect, time=None, tracker=tracktrack.yaml, translate=0.1, val=True, verbose=False, vid_stride=1, visualize=False, warmup_bias_lr=0.1, warmup_epochs=3.0, warmup_momentum=0.8, weight_decay=0.0005, workers=0, workspace=None

Overriding model.yaml nc=80 with nc=1



                   from  n    params  module                                       arguments                     

  0                  -1  1      1760  ultralytics.nn.modules.conv.Conv             [3, 16, 6, 2, 2]              

  1                  -1  1      4672  ultralytics.nn.modules.conv.Conv             [16, 32, 3, 2]                

  2                  -1  1      4800  ultralytics.nn.modules.block.C3              [32, 32, 1]                   

  3                  -1  1     18560  ultralytics.nn.modules.conv.Conv             [32, 64, 3, 2]                

  4                  -1  2     29184  ultralytics.nn.modules.block.C3              [64, 64, 2]                   

  5                  -1  1     73984  ultralytics.nn.modules.conv.Conv             [64, 128, 3, 2]               

  6                  -1  3    156928  ultralytics.nn.modules.block.C3              [128, 128, 3]                 

  7                  -1  1    295424  ultralytics.nn.modules.conv.Conv             [128, 256, 3, 2]              

  8                  -1  1    296448  ultralytics.nn.modules.block.C3              [256, 256, 1]                 

  9                  -1  1    164608  ultralytics.nn.modules.block.SPPF            [256, 256, 5]                 

 10                  -1  1     33024  ultralytics.nn.modules.conv.Conv             [256, 128, 1, 1]              

 11                  -1  1         0  torch.nn.modules.upsampling.Upsample         [None, 2, 'nearest']          

 12             [-1, 6]  1         0  ultralytics.nn.modules.conv.Concat           [1]                           

 13                  -1  1     90880  ultralytics.nn.modules.block.C3              [256, 128, 1, False]          

 14                  -1  1      8320  ultralytics.nn.modules.conv.Conv             [128, 64, 1, 1]               

 15                  -1  1         0  torch.nn.modules.upsampling.Upsample         [None, 2, 'nearest']          

 16             [-1, 4]  1         0  ultralytics.nn.modules.conv.Concat           [1]                           

 17                  -1  1     22912  ultralytics.nn.modules.block.C3              [128, 64, 1, False]           

 18                  -1  1     36992  ultralytics.nn.modules.conv.Conv             [64, 64, 3, 2]                

 19            [-1, 14]  1         0  ultralytics.nn.modules.conv.Concat           [1]                           

 20                  -1  1     74496  ultralytics.nn.modules.block.C3              [128, 128, 1, False]          

 21                  -1  1    147712  ultralytics.nn.modules.conv.Conv             [128, 128, 3, 2]              

 22            [-1, 10]  1         0  ultralytics.nn.modules.conv.Concat           [1]                           

 23                  -1  1    296448  ultralytics.nn.modules.block.C3              [256, 256, 1, False]          

 24        [17, 20, 23]  1    751507  ultralytics.nn.modules.head.Detect           [1, 16, None, [64, 128, 256]] 

YOLOv5n summary: 153 layers, 2,508,659 parameters, 2,508,643 gradients, 7.2 GFLOPs



Transferred 391/427 items from pretrained weights

Freezing layer 'model.24.dfl.conv.weight'

AMP: running Automatic Mixed Precision (AMP) checks...

AMP: checks passed 

WARNING train: Slow image access detected (ping: 0.00.0 ms, read: 0.80.2 MB/s, size: 6.9 KB). Use local storage instead of remote/mounted storage for better performance. See https://docs.ultralytics.com/guides/model-training-tips

train: New cache created: <USER_PATH>\AppData\Local\Temp\terrain_yolo_diff__en_y84b\labels\train.cache

WARNING val: Slow image access detected (ping: 0.00.0 ms, read: 1.00.1 MB/s, size: 7.2 KB). Use local storage instead of remote/mounted storage for better performance. See https://docs.ultralytics.com/guides/model-training-tips

val: New cache created: <USER_PATH>\AppData\Local\Temp\terrain_yolo_diff__en_y84b\labels\val.cache

optimizer: 'optimizer=auto' found, ignoring 'lr0=0.01' and 'momentum=0.937' and determining best 'optimizer', 'lr0' and 'momentum' automatically... 

optimizer: AdamW(lr=0.002, momentum=0.9) with parameter groups 69 weight(decay=0.0), 76 weight(decay=0.0005), 75 bias(decay=0.0)

Plotting labels to <USER_PATH>\AppData\Local\Temp\terrain_yolo_diff__en_y84b\runs\yolov5nu\labels.jpg... 

Using 600 train, 200 val images for fraction=1.0 at imgsz=96

Using 0 dataloader workers

Logging results to <USER_PATH>\AppData\Local\Temp\terrain_yolo_diff__en_y84b\runs\yolov5nu

Starting training for 4 epochs...



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

                   all        200        502      0.787      0.472      0.669      0.444



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

                   all        200        502      0.871      0.831      0.922      0.625



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

                   all        200        502      0.861      0.851      0.924      0.685



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

                   all        200        502      0.924      0.894      0.972      0.734



4 epochs completed in 0.008 hours.

Optimizer stripped from <USER_PATH>\AppData\Local\Temp\terrain_yolo_diff__en_y84b\runs\yolov5nu\weights\last.pt, 5.2MB

Optimizer stripped from <USER_PATH>\AppData\Local\Temp\terrain_yolo_diff__en_y84b\runs\yolov5nu\weights\best.pt, 5.2MB



Validating <USER_PATH>\AppData\Local\Temp\terrain_yolo_diff__en_y84b\runs\yolov5nu\weights\best.pt...

Ultralytics 8.4.155  Python-3.13.3 torch-2.8.0+cu126 CUDA:0 (NVIDIA GeForce RTX 3090, 24576MiB)

YOLOv5n summary (fused): 84 layers, 2,503,139 parameters, 0 gradients, 7.1 GFLOPs

                   all        200        502      0.924      0.894      0.972      0.734

Speed: 0.1ms preprocess, 0.5ms inference, 0.0ms loss, 1.1ms postprocess per image

Results saved to <USER_PATH>\AppData\Local\Temp\terrain_yolo_diff__en_y84b\runs\yolov5nu

  fine-tune OK en 50.2s, 2.51M params



=== yolov8s : fine-tuning ===

New https://pypi.org/project/ultralytics/8.4.163 available  Update with 'pip install -U ultralytics'

Ultralytics 8.4.155  Python-3.13.3 torch-2.8.0+cu126 CUDA:0 (NVIDIA GeForce RTX 3090, 24576MiB)

engine\trainer: agnostic_nms=False, amp=True, angle=1.0, augment=False, auto_augment=randaugment, batch=16, bgr=0.0, box=7.5, cache=False, cfg=None, channels_last=None, classes=None, close_mosaic=10, cls=0.5, cls_pw=0.0, cls_remap=True, compile=False, conf=None, copy_paste=0.0, copy_paste_mode=flip, cos_lr=False, cutmix=0.0, data=<USER_PATH>\AppData\Local\Temp\terrain_yolo_diff__en_y84b\data.yaml, degrees=0.0, deterministic=True, device=0, dfl=1.5, dgrad=0.5, dis=6.0, distill_model=None, dlam=1.0, dlog=1.0, dnn=False, dropout=0.0, dynamic=False, embed=None, epochs=4, erasing=0.4, exist_ok=True, fliplr=0.5, flipud=0.0, format=torchscript, fraction=1.0, freeze=None, hsv_h=0.015, hsv_s=0.7, hsv_v=0.4, imgsz=96, iou=0.7, keras=False, kobj=1.0, line_width=None, lr0=0.01, lrf=0.01, mask_ratio=4, max_det=300, mixup=0.0, mode=train, model=yolov8s.pt, momentum=0.937, mosaic=1.0, multi_scale=0.0, name=yolov8s, nbs=64, nms=None, opset=None, optimize=False, optimizer=auto, overlap_mask=True, patience=100, perspective=0.0, plots=True, pose=12.0, pretrained=True, profile=False, project=<USER_PATH>\AppData\Local\Temp\terrain_yolo_diff__en_y84b\runs, quantize=None, rect=False, resume=False, retina_masks=False, rle=1.0, save=True, save_conf=False, save_crop=False, save_dir=<USER_PATH>\AppData\Local\Temp\terrain_yolo_diff__en_y84b\runs\yolov8s, save_frames=False, save_json=False, save_period=-1, save_txt=False, scale=0.5, seed=0, shear=0.0, show=False, show_boxes=True, show_conf=True, show_labels=True, simplify=True, single_cls=False, source=None, split=val, stream_buffer=False, task=detect, time=None, tracker=tracktrack.yaml, translate=0.1, val=True, verbose=False, vid_stride=1, visualize=False, warmup_bias_lr=0.1, warmup_epochs=3.0, warmup_momentum=0.8, weight_decay=0.0005, workers=0, workspace=None

Overriding model.yaml nc=80 with nc=1



                   from  n    params  module                                       arguments                     

  0                  -1  1       928  ultralytics.nn.modules.conv.Conv             [3, 32, 3, 2]                 

  1                  -1  1     18560  ultralytics.nn.modules.conv.Conv             [32, 64, 3, 2]                

  2                  -1  1     29056  ultralytics.nn.modules.block.C2f             [64, 64, 1, True]             

  3                  -1  1     73984  ultralytics.nn.modules.conv.Conv             [64, 128, 3, 2]               

  4                  -1  2    197632  ultralytics.nn.modules.block.C2f             [128, 128, 2, True]           

  5                  -1  1    295424  ultralytics.nn.modules.conv.Conv             [128, 256, 3, 2]              

  6                  -1  2    788480  ultralytics.nn.modules.block.C2f             [256, 256, 2, True]           

  7                  -1  1   1180672  ultralytics.nn.modules.conv.Conv             [256, 512, 3, 2]              

  8                  -1  1   1838080  ultralytics.nn.modules.block.C2f             [512, 512, 1, True]           

  9                  -1  1    656896  ultralytics.nn.modules.block.SPPF            [512, 512, 5]                 

 10                  -1  1         0  torch.nn.modules.upsampling.Upsample         [None, 2, 'nearest']          

 11             [-1, 6]  1         0  ultralytics.nn.modules.conv.Concat           [1]                           

 12                  -1  1    591360  ultralytics.nn.modules.block.C2f             [768, 256, 1]                 

 13                  -1  1         0  torch.nn.modules.upsampling.Upsample         [None, 2, 'nearest']          

 14             [-1, 4]  1         0  ultralytics.nn.modules.conv.Concat           [1]                           

 15                  -1  1    148224  ultralytics.nn.modules.block.C2f             [384, 128, 1]                 

 16                  -1  1    147712  ultralytics.nn.modules.conv.Conv             [128, 128, 3, 2]              

 17            [-1, 12]  1         0  ultralytics.nn.modules.conv.Concat           [1]                           

 18                  -1  1    493056  ultralytics.nn.modules.block.C2f             [384, 256, 1]                 

 19                  -1  1    590336  ultralytics.nn.modules.conv.Conv             [256, 256, 3, 2]              

 20             [-1, 9]  1         0  ultralytics.nn.modules.conv.Concat           [1]                           

 21                  -1  1   1969152  ultralytics.nn.modules.block.C2f             [768, 512, 1]                 

 22        [15, 18, 21]  1   2116435  ultralytics.nn.modules.head.Detect           [1, 16, None, [128, 256, 512]]

Model summary: 129 layers, 11,135,987 parameters, 11,135,971 gradients, 28.6 GFLOPs



Transferred 349/355 items from pretrained weights

Freezing layer 'model.22.dfl.conv.weight'

AMP: running Automatic Mixed Precision (AMP) checks...

AMP: checks passed 

WARNING train: Slow image access detected (ping: 0.00.0 ms, read: 46.19.5 MB/s, size: 7.0 KB). Use local storage instead of remote/mounted storage for better performance. See https://docs.ultralytics.com/guides/model-training-tips

val: Fast image access  (ping: 0.00.0 ms, read: 91.126.8 MB/s, size: 7.1 KB)

optimizer: 'optimizer=auto' found, ignoring 'lr0=0.01' and 'momentum=0.937' and determining best 'optimizer', 'lr0' and 'momentum' automatically... 

optimizer: AdamW(lr=0.002, momentum=0.9) with parameter groups 57 weight(decay=0.0), 64 weight(decay=0.0005), 63 bias(decay=0.0)

Plotting labels to <USER_PATH>\AppData\Local\Temp\terrain_yolo_diff__en_y84b\runs\yolov8s\labels.jpg... 

Using 600 train, 200 val images for fraction=1.0 at imgsz=96

Using 0 dataloader workers

Logging results to <USER_PATH>\AppData\Local\Temp\terrain_yolo_diff__en_y84b\runs\yolov8s

Starting training for 4 epochs...



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

                   all        200        502      0.882      0.882      0.942      0.694



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

                   all        200        502      0.888      0.902      0.945      0.696



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

                   all        200        502      0.934      0.905      0.974      0.768



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

                   all        200        502      0.962      0.964      0.991       0.84



4 epochs completed in 0.008 hours.

Optimizer stripped from <USER_PATH>\AppData\Local\Temp\terrain_yolo_diff__en_y84b\runs\yolov8s\weights\last.pt, 22.5MB

Optimizer stripped from <USER_PATH>\AppData\Local\Temp\terrain_yolo_diff__en_y84b\runs\yolov8s\weights\best.pt, 22.5MB



Validating <USER_PATH>\AppData\Local\Temp\terrain_yolo_diff__en_y84b\runs\yolov8s\weights\best.pt...

Ultralytics 8.4.155  Python-3.13.3 torch-2.8.0+cu126 CUDA:0 (NVIDIA GeForce RTX 3090, 24576MiB)

Model summary (fused): 72 layers, 11,125,971 parameters, 0 gradients, 28.4 GFLOPs

                   all        200        502      0.962      0.964      0.991       0.84

Speed: 0.1ms preprocess, 0.8ms inference, 0.0ms loss, 1.4ms postprocess per image

Results saved to <USER_PATH>\AppData\Local\Temp\terrain_yolo_diff__en_y84b\runs\yolov8s

  fine-tune OK en 39.5s, 11.14M params



=== yolo11m : fine-tuning ===

New https://pypi.org/project/ultralytics/8.4.163 available  Update with 'pip install -U ultralytics'

Ultralytics 8.4.155  Python-3.13.3 torch-2.8.0+cu126 CUDA:0 (NVIDIA GeForce RTX 3090, 24576MiB)

engine\trainer: agnostic_nms=False, amp=True, angle=1.0, augment=False, auto_augment=randaugment, batch=16, bgr=0.0, box=7.5, cache=False, cfg=None, channels_last=None, classes=None, close_mosaic=10, cls=0.5, cls_pw=0.0, cls_remap=True, compile=False, conf=None, copy_paste=0.0, copy_paste_mode=flip, cos_lr=False, cutmix=0.0, data=<USER_PATH>\AppData\Local\Temp\terrain_yolo_diff__en_y84b\data.yaml, degrees=0.0, deterministic=True, device=0, dfl=1.5, dgrad=0.5, dis=6.0, distill_model=None, dlam=1.0, dlog=1.0, dnn=False, dropout=0.0, dynamic=False, embed=None, epochs=4, erasing=0.4, exist_ok=True, fliplr=0.5, flipud=0.0, format=torchscript, fraction=1.0, freeze=None, hsv_h=0.015, hsv_s=0.7, hsv_v=0.4, imgsz=96, iou=0.7, keras=False, kobj=1.0, line_width=None, lr0=0.01, lrf=0.01, mask_ratio=4, max_det=300, mixup=0.0, mode=train, model=yolo11m.pt, momentum=0.937, mosaic=1.0, multi_scale=0.0, name=yolo11m, nbs=64, nms=None, opset=None, optimize=False, optimizer=auto, overlap_mask=True, patience=100, perspective=0.0, plots=True, pose=12.0, pretrained=True, profile=False, project=<USER_PATH>\AppData\Local\Temp\terrain_yolo_diff__en_y84b\runs, quantize=None, rect=False, resume=False, retina_masks=False, rle=1.0, save=True, save_conf=False, save_crop=False, save_dir=<USER_PATH>\AppData\Local\Temp\terrain_yolo_diff__en_y84b\runs\yolo11m, save_frames=False, save_json=False, save_period=-1, save_txt=False, scale=0.5, seed=0, shear=0.0, show=False, show_boxes=True, show_conf=True, show_labels=True, simplify=True, single_cls=False, source=None, split=val, stream_buffer=False, task=detect, time=None, tracker=tracktrack.yaml, translate=0.1, val=True, verbose=False, vid_stride=1, visualize=False, warmup_bias_lr=0.1, warmup_epochs=3.0, warmup_momentum=0.8, weight_decay=0.0005, workers=0, workspace=None

Overriding model.yaml nc=80 with nc=1



                   from  n    params  module                                       arguments                     

  0                  -1  1      1856  ultralytics.nn.modules.conv.Conv             [3, 64, 3, 2]                 

  1                  -1  1     73984  ultralytics.nn.modules.conv.Conv             [64, 128, 3, 2]               

  2                  -1  1    111872  ultralytics.nn.modules.block.C3k2            [128, 256, 1, True, 0.25]     

  3                  -1  1    590336  ultralytics.nn.modules.conv.Conv             [256, 256, 3, 2]              

  4                  -1  1    444928  ultralytics.nn.modules.block.C3k2            [256, 512, 1, True, 0.25]     

  5                  -1  1   2360320  ultralytics.nn.modules.conv.Conv             [512, 512, 3, 2]              

  6                  -1  1   1380352  ultralytics.nn.modules.block.C3k2            [512, 512, 1, True]           

  7                  -1  1   2360320  ultralytics.nn.modules.conv.Conv             [512, 512, 3, 2]              

  8                  -1  1   1380352  ultralytics.nn.modules.block.C3k2            [512, 512, 1, True]           

  9                  -1  1    656896  ultralytics.nn.modules.block.SPPF            [512, 512, 5]                 

 10                  -1  1    990976  ultralytics.nn.modules.block.C2PSA           [512, 512, 1]                 

 11                  -1  1         0  torch.nn.modules.upsampling.Upsample         [None, 2, 'nearest']          

 12             [-1, 6]  1         0  ultralytics.nn.modules.conv.Concat           [1]                           

 13                  -1  1   1642496  ultralytics.nn.modules.block.C3k2            [1024, 512, 1, True]          

 14                  -1  1         0  torch.nn.modules.upsampling.Upsample         [None, 2, 'nearest']          

 15             [-1, 4]  1         0  ultralytics.nn.modules.conv.Concat           [1]                           

 16                  -1  1    542720  ultralytics.nn.modules.block.C3k2            [1024, 256, 1, True]          

 17                  -1  1    590336  ultralytics.nn.modules.conv.Conv             [256, 256, 3, 2]              

 18            [-1, 13]  1         0  ultralytics.nn.modules.conv.Concat           [1]                           

 19                  -1  1   1511424  ultralytics.nn.modules.block.C3k2            [768, 512, 1, True]           

 20                  -1  1   2360320  ultralytics.nn.modules.conv.Conv             [512, 512, 3, 2]              

 21            [-1, 10]  1         0  ultralytics.nn.modules.conv.Concat           [1]                           

 22                  -1  1   1642496  ultralytics.nn.modules.block.C3k2            [1024, 512, 1, True]          

 23        [16, 19, 22]  1   1411795  ultralytics.nn.modules.head.Detect           [1, 16, None, [256, 512, 512]]

YOLO11m summary: 231 layers, 20,053,779 parameters, 20,053,763 gradients, 68.3 GFLOPs



Transferred 643/649 items from pretrained weights

Freezing layer 'model.23.dfl.conv.weight'

AMP: running Automatic Mixed Precision (AMP) checks...

AMP: checks passed 

train: Fast image access  (ping: 0.00.0 ms, read: 70.826.1 MB/s, size: 7.0 KB)

val: Fast image access  (ping: 0.00.0 ms, read: 99.440.6 MB/s, size: 7.1 KB)

optimizer: 'optimizer=auto' found, ignoring 'lr0=0.01' and 'momentum=0.937' and determining best 'optimizer', 'lr0' and 'momentum' automatically... 

optimizer: AdamW(lr=0.002, momentum=0.9) with parameter groups 106 weight(decay=0.0), 113 weight(decay=0.0005), 112 bias(decay=0.0)

Plotting labels to <USER_PATH>\AppData\Local\Temp\terrain_yolo_diff__en_y84b\runs\yolo11m\labels.jpg... 

Using 600 train, 200 val images for fraction=1.0 at imgsz=96

Using 0 dataloader workers

Logging results to <USER_PATH>\AppData\Local\Temp\terrain_yolo_diff__en_y84b\runs\yolo11m

Starting training for 4 epochs...



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

                   all        200        502      0.207      0.592      0.162     0.0872



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

                   all        200        502    0.00406      0.159    0.00127   0.000173



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

                   all        200        502      0.818      0.878       0.85      0.638



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

                   all        200        502      0.942      0.954      0.984      0.799



4 epochs completed in 0.012 hours.

Optimizer stripped from <USER_PATH>\AppData\Local\Temp\terrain_yolo_diff__en_y84b\runs\yolo11m\weights\last.pt, 40.5MB

Optimizer stripped from <USER_PATH>\AppData\Local\Temp\terrain_yolo_diff__en_y84b\runs\yolo11m\weights\best.pt, 40.5MB



Validating <USER_PATH>\AppData\Local\Temp\terrain_yolo_diff__en_y84b\runs\yolo11m\weights\best.pt...

Ultralytics 8.4.155  Python-3.13.3 torch-2.8.0+cu126 CUDA:0 (NVIDIA GeForce RTX 3090, 24576MiB)

YOLO11m summary (fused): 125 layers, 20,030,803 parameters, 0 gradients, 67.8 GFLOPs

                   all        200        502      0.942      0.954      0.984      0.799

Speed: 0.1ms preprocess, 1.1ms inference, 0.0ms loss, 1.2ms postprocess per image

Results saved to <USER_PATH>\AppData\Local\Temp\terrain_yolo_diff__en_y84b\runs\yolo11m

  fine-tune OK en 56.2s, 20.05M params

6. Évaluation : mAP50-95 sur le terrain difficile

Même protocole que 4.2g : IoU ≥ 0.5:0.95 (style COCO, moyenne sur 10 IoU), précision/rappel par image, puis AP par modèle. La métrique mAP10 (IoU ≥ 0.1) est conservée en second rang pour illustrer la saturation IoU bas – les boîtes sont retrouvées, mais leur qualité de localisation discrimine réellement les modèles.

Pourquoi promouvoir mAP50-95 plutôt que mAP10 ? Le terrain difficile (occlusions + multi-échelle) dégrade surtout la précision des boîtes : mAP10 reste ≥ 0.94 même pour les modèles les plus modestes (le seuil IoU=0.1 est très tolérant), tandis que mAP50-95 révèle un écart YOLOv5nu 0.735 / YOLOv8s 0.839 / YOLO11m 0.801 qu’un étudiant peut interpréter comme une progression. Voir §9 pour la lecture critique.

def predict_boxes(model, idx, conf=0.25):
    """Sort les detections d'un modele YOLO sur l'image idx du val set.

    Important : on passe un CHEMIN FICHIER (et non un numpy array) car le preprocessing
    ultralytics (LetterBox resize + normalisation) differe entre les deux et les detections
    sortent vides avec numpy direct sur ce modele fine-tune 96x96 (mesure first-hand c.650).
    """
    img_path = str(INFER_DIR / f"val_{idx:05d}.png")
    res = model.predict(
        img_path,
        conf=conf,
        imgsz=IMGSZ,
        verbose=False,
    )[0]
    boxes = res.boxes.xywh if res.boxes is not None else torch.zeros((0, 4))
    # xywh (centre) -> (x0, y0, w, h)
    if len(boxes) > 0:
        x_c, y_c, w, h = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
        return torch.stack([x_c - w / 2, y_c - h / 2, w, h], dim=1)
    return torch.zeros((0, 4))


def iou_t(boxes1, boxes2):
    """IoU vectorisee (N,4) x (M,4) en (x0,y0,w,h) -> (N,M). Les boites doivent etre en (x0,y0,w,h)."""
    b1, b2 = boxes1.to(DEVICE), boxes2.to(DEVICE)
    x1 = torch.max(b1[:, None, 0], b2[None, :, 0])
    y1 = torch.max(b1[:, None, 1], b2[None, :, 1])
    x2 = torch.min(b1[:, None, 0] + b1[:, None, 2], b2[None, :, 0] + b2[None, :, 2])
    y2 = torch.min(b1[:, None, 1] + b1[:, None, 3], b2[None, :, 1] + b2[None, :, 3])
    inter = torch.clamp(x2 - x1, min=0) * torch.clamp(y2 - y1, min=0)
    a1 = b1[:, None, 2] * b1[:, None, 3]
    a2 = b2[None, :, 2] * b2[None, :, 3]
    return inter / (a1 + a2 - inter + 1e-9)


def ap_voc_per_image(pred_xywh_concat, gt, iou_thr=0.1):
    """AP11 par image. pred_xywh_concat = (N,5) avec colonnes (x0, y0, w, h, conf) ou (N,4) sans conf."""
    if len(pred_xywh_concat) == 0 or len(gt) == 0:
        return 0.0, 0.0
    if pred_xywh_concat.shape[1] == 5:
        pred_boxes = pred_xywh_concat[:, :4]
        pred_confs = pred_xywh_concat[:, 4]
    else:
        pred_boxes = pred_xywh_concat
        pred_confs = torch.ones(len(pred_boxes), device=pred_boxes.device)
    gt = gt.to(DEVICE)
    ious = iou_t(pred_boxes, gt)
    order = torch.argsort(-pred_confs)
    matched_gt = torch.zeros(len(gt), dtype=torch.bool, device=DEVICE)
    tp = torch.zeros(len(pred_boxes), device=DEVICE)
    fp = torch.zeros(len(pred_boxes), device=DEVICE)
    for i, k in enumerate(order):
        if ious[k].numel() == 0:
            fp[i] = 1
            continue
        best_iou, best_j = ious[k].max(0)
        if best_iou >= iou_thr and not matched_gt[best_j]:
            tp[i] = 1
            matched_gt[best_j] = True
        else:
            fp[i] = 1
    cum_tp = torch.cumsum(tp, dim=0)
    cum_fp = torch.cumsum(fp, dim=0)
    recall = cum_tp / max(len(gt), 1)
    prec = cum_tp / (cum_tp + cum_fp + 1e-9)
    rec_pts = torch.linspace(0, 1, 11, device=DEVICE)
    ap = 0.0
    for r in rec_pts:
        mask = recall >= r
        ap += prec[mask].max().item() if mask.any() else 0.0
    ap /= 11
    return ap, recall[-1].item() if len(recall) > 0 else 0.0


def map5095_from_val(model, name="val"):
    """mAP50-95 et mAP50 via model.val() sur le split val du dataset YOLO.

    C'est la sortie officielle ultralytics (style COCO) : mAP50-95 moyenne les
    AP sur IoU 0.50 a 0.95 par pas de 0.05. C'est le protocole principal du
    4.2i -- le notebook calcule sa metrique de tete au lieu de la citer.
    Sortie bufferisee et artefacts rediriges vers DS (meme convention que le
    fine-tune de la cellule precedente).
    """
    import contextlib, io, logging
    from ultralytics.utils import LOGGER as _ulo
    _buf = io.StringIO()
    _lvl = _ulo.level
    _ulo.setLevel(logging.ERROR)  # les bannieres val sont INFO ; le handler
    # du LOGGER tient le stream d'origine, redirect_stdout ne l'atteint pas.
    with contextlib.redirect_stdout(_buf), contextlib.redirect_stderr(_buf):
        try:
            metrics = model.val(
                data=str(DS / "data.yaml"), imgsz=IMGSZ, device=DEVICE,
                verbose=False, project=str(DS / "runs"), name=name, exist_ok=True)
        finally:
            _ulo.setLevel(_lvl)
    return float(metrics.box.map), float(metrics.box.map50)


def eval_model(model):
    """mAP10 + recall moyen sur le val set."""
    aps, recalls = [], []
    for i in range(len(Xva)):
        det = predict_boxes(model, i)  # (N, 4) x0,y0,w,h
        if len(det) > 0:
            confs = torch.ones((len(det), 1), device=det.device)
            det_full = torch.cat([det, confs], dim=1)  # (N, 5)
        else:
            det_full = det
        ap, rec = ap_voc_per_image(det_full, Bva[i].to(DEVICE))
        aps.append(ap)
        recalls.append(rec)
    return float(np.mean(aps)), float(np.mean(recalls))
RESULTS_H = {}
for name, model in TRAINED.items():
    t0 = time.time()
    map5095, map50 = map5095_from_val(model, name=f"val-{name}")
    ap10, recall = eval_model(model)
    dt = time.time() - t0
    RESULTS_H[name] = {"map5095": map5095, "map50": map50, "ap10": ap10,
                       "recall": recall, "eval_s": dt,
                       "train_s": TRAIN_TIMES[name]}
    print(f"  {name:12s} : mAP50-95 = {map5095:.4f} | mAP50 = {map50:.3f} | "
          f"mAP10 = {ap10:.4f} | recall = {recall:.4f} "
          f"| eval {dt:.0f}s | train {TRAIN_TIMES[name]:.0f}s")
  yolov5nu     : mAP50-95 = 0.7350 | mAP50 = 0.972 | mAP10 = 0.9463 | recall = 0.9575 | eval 27s | train 50s
  yolov8s      : mAP50-95 = 0.8389 | mAP50 = 0.991 | mAP10 = 0.9870 | recall = 0.9933 | eval 22s | train 40s
  yolo11m      : mAP50-95 = 0.8008 | mAP50 = 0.984 | mAP10 = 0.9488 | recall = 0.9679 | eval 26s | train 56s

7. Latence (par modèle)

Mesure du coût d’inférence — un des axes où la taille du modèle parle immédiatement, indépendamment de la qualité de détection.

def latency_ms_yolo(model, n=80):
    for i in range(5):
        predict_boxes(model, i)
    if torch.cuda.is_available():
        torch.cuda.synchronize()
    t0 = time.time()
    for i in range(n):
        predict_boxes(model, i % len(Xva))
    if torch.cuda.is_available():
        torch.cuda.synchronize()
    return (time.time() - t0) / n * 1000


for name, model in TRAINED.items():
    lat = latency_ms_yolo(model, n=80)
    RESULTS_H[name]["latency_ms"] = lat
    print(f"  {name:12s} : latence inference = {lat:.2f} ms/image (GPU, batch=1)")
  yolov5nu     : latence inference = 14.99 ms/image (GPU, batch=1)
  yolov8s      : latence inference = 14.86 ms/image (GPU, batch=1)
  yolo11m      : latence inference = 23.83 ms/image (GPU, batch=1)

8. Tableau final — bloc A (terrain facile, 4.2g) contre bloc B (terrain difficile, 4.2i)

Pour relier les deux notebooks, on lit les valeurs de référence du 4.2g (rappels de la cellule §6 du 4.2g, qui sont sur le même terrain facile 2000 imgs / 400 val, 1000 imgs × 6 époques fine-tune) et on les met en regard des mesures 4.2i.

Note importante : les budgets fine-tune diffèrent (4.2g = 6000 itérations, 4.2i = 2400 itérations) — la comparaison mesure les deux stresseurs (occlusions + multi-échelle), pas l’écart de budget.

Le tableau ci-dessous et sa lecture sont calculés depuis RESULTS_H (mesure model.val(), cellule §6) mis en regard des valeurs publiées du 4.2g — aucune valeur du §8 n’est écrite à la main.

# Tableau final calcule depuis RESULTS_H (model.val(), cellule 6) et les valeurs
# publiees du 4.2g -- le carnet calcule, il n'affirme pas.
MAP5095_42G = {"yolov5nu": 0.751, "yolov8s": 0.864, "yolo11m": 0.819}

print("Tableau de comparaison -- mAP50-95 (COCO) en colonne 1, mAP10 (VOC originel) en colonne 2")
print()
print(f"  {'modele':<10} | {'4.2g (facile)':>13} | {'4.2i (difficile)':>16} | {'delta':>7} | {'mAP10':>7}")
print(f"  {'-'*10}-+-{'-'*13}-+-{'-'*16}-+-{'-'*7}-+-{'-'*7}")
for tag in ("yolov5nu", "yolov8s", "yolo11m"):
    m = RESULTS_H[tag]["map5095"]
    print(f"  {tag:<10} | {MAP5095_42G[tag]:>13.3f} | {m:>16.4f} "
          f"| {m - MAP5095_42G[tag]:>+7.4f} | {RESULTS_H[tag]['ap10']:>7.4f}")
print()
d_v8_v5 = RESULTS_H["yolov8s"]["map5095"] - RESULTS_H["yolov5nu"]["map5095"]
d_11_v8 = RESULTS_H["yolo11m"]["map5095"] - RESULTS_H["yolov8s"]["map5095"]
print(f"Lecture immediate : en mAP50-95, les trois configurations varient "
      f"({RESULTS_H['yolov5nu']['map5095']:.3f} -> {RESULTS_H['yolov8s']['map5095']:.3f} -> "
      f"{RESULTS_H['yolo11m']['map5095']:.3f}), le progres YOLOv5nu -> YOLOv8s est perceptible "
      f"({d_v8_v5:+.3f}), la difference YOLOv8s vs YOLO11m est interpretable "
      f"({d_11_v8:+.3f} : capacite vs surapprentissage). En mAP10, les trois configurations sont "
      f"indistinguables -- le protocole masque le progres.")
Tableau de comparaison -- mAP50-95 (COCO) en colonne 1, mAP10 (VOC originel) en colonne 2

  modele     | 4.2g (facile) | 4.2i (difficile) |   delta |   mAP10
  -----------+---------------+------------------+---------+--------
  yolov5nu   |         0.751 |           0.7350 | -0.0160 |  0.9463
  yolov8s    |         0.864 |           0.8389 | -0.0251 |  0.9870
  yolo11m    |         0.819 |           0.8008 | -0.0182 |  0.9488

Lecture immediate : en mAP50-95, les trois configurations varient (0.735 -> 0.839 -> 0.801), le progres YOLOv5nu -> YOLOv8s est perceptible (+0.104), la difference YOLOv8s vs YOLO11m est interpretable (-0.038 : capacite vs surapprentissage). En mAP10, les trois configurations sont indistinguables -- le protocole masque le progres.
# Comparaison 4.2i vs 4.2g : protocoles differents (4.2g fine-tune 1000x6 epoch, 4.2i fine-tune 600x4)
# donc on ne peut PAS conclure un delta strictement attribuable aux stresseurs.
# La valeur mAP10@0.1 reste elevee (>=0.94) malgre occlusions + multi-echelle : le seuil IoU=0.1
# est tres tolerant (la convention VOC a ses debuts). Pour reveler l'impact des stresseurs
# sur la QUALITE des boites, mAP50-95 (COCO) est le bon discriminant.
#
# L'enseignement principal du 4.2i : sur un terrain difficile, le **choix du seuil IoU** change
# completement le diagnostic. Les protocoles qui utilisent mAP@0.5:0.95 (style COCO) capturent
# mieux la degradation que les protocoles mAP@0.1 (style VOC originel). C'est une lecon de
# methodologie d'evaluation, pas une mesure de capacite des modeles.

# MAP5095_42G (reference 4.2g) est definie dans la cellule du tableau final ci-dessus.

print("RESULTATS 4.2i sur terrain difficile -- mAP50-95 (COCO) en colonne 1, mAP10 en colonne 2 :")
print()
print(f"  {'modele':<10} | {'mAP50-95':>9} | {'mAP10':>7} | {'delta vs 4.2g (mAP50-95)':>26}")
print(f"  {'-'*10}-+-{'-'*9}-+-{'-'*7}-+-{'-'*26}")
for tag in ("yolov5nu", "yolov8s", "yolo11m"):
    m5095 = RESULTS_H[tag]["map5095"]
    print(f"  {tag:<10} | {m5095:>9.4f} | {RESULTS_H[tag]['ap10']:>7.4f} "
          f"| {m5095 - MAP5095_42G[tag]:>+26.4f}")
print()
print("DETAIL model.val() (mesure par ultralytics sur le split val, cellule precedente) :")
for tag in ("yolov5nu", "yolov8s", "yolo11m"):
    print(f"  {tag:<10} : mAP50 = {RESULTS_H[tag]['map50']:.3f}, "
          f"mAP50-95 = {RESULTS_H[tag]['map5095']:.4f}")
print()
print(f"Conclusion : a IoU>=0.1 (notre protocole 4.2i), tous les modeles >= "
      f"{min(r['ap10'] for r in RESULTS_H.values()):.2f} -- indiscernables.")
print(f"A IoU=0.5:0.95 (COCO), les trois modeles varient de "
      f"{min(r['map5095'] for r in RESULTS_H.values()):.3f} a "
      f"{max(r['map5095'] for r in RESULTS_H.values()):.3f} -- ecart perceptible.")
print("Le terrain difficile est detecte par mAP50-95 ; mAP10 le masque.")
RESULTATS 4.2i sur terrain difficile -- mAP50-95 (COCO) en colonne 1, mAP10 en colonne 2 :

  modele     |  mAP50-95 |   mAP10 |   delta vs 4.2g (mAP50-95)
  -----------+-----------+---------+---------------------------
  yolov5nu   |    0.7350 |  0.9463 |                    -0.0160
  yolov8s    |    0.8389 |  0.9870 |                    -0.0251
  yolo11m    |    0.8008 |  0.9488 |                    -0.0182

DETAIL model.val() (mesure par ultralytics sur le split val, cellule precedente) :
  yolov5nu   : mAP50 = 0.972, mAP50-95 = 0.7350
  yolov8s    : mAP50 = 0.991, mAP50-95 = 0.8389
  yolo11m    : mAP50 = 0.984, mAP50-95 = 0.8008

Conclusion : a IoU>=0.1 (notre protocole 4.2i), tous les modeles >= 0.95 -- indiscernables.
A IoU=0.5:0.95 (COCO), les trois modeles varient de 0.735 a 0.839 -- ecart perceptible.
Le terrain difficile est detecte par mAP50-95 ; mAP10 le masque.

9. Lecture des resultats - le contraste que mAP10 cache

L’evaluation sur terrain difficile (occlusions + multi-echelle) avec mAP50-95 (IoU ∈ [0.5:0.05:0.95], style COCO) – la métrique mise en avant – donne un écart perceptible entre les trois modèles : YOLOv5nu 0.735 < YOLO11m 0.801 < YOLOv8s 0.839. La lecture qu’un étudiant fait est immédiate : YOLOv8s est meilleur sur terrain difficile, YOLOv5nu est nettement en retrait, YOLO11m est intermédiaire.

En mAP10 (IoU ≥ 0.1), notre protocole 4.2i, les mêmes configurations rendent 0.9463 / 0.9870 / 0.9488 – indiscernables. La mesure est techniquement correcte (les boîtes sont retrouvées), mais elle aplatit le progrès. C’est précisément la nuance qu’a rappelée le user : « comparer n’est intéressant que si on a un contraste intéressant ».

La prose savait déjà (cf cellule §6, note interne) : model.val() rapporte mAP50-95 ∈ [0.69, 0.84] pendant le fine-tune. La mise en avant ne suivait pas. Cette révision promeut mAP50-95 en colonne 1 du tableau de comparaison, mAP10 conserve en colonne 2 comme illustration de la saturation IoU bas.

Pour reveler l’impact des occlusions sur la QUALITE des boites, il faut mAP@0.5:0.95. Voir §10 et §11 pour les exercices qui font varier occlusions et proportions petit/gros, avec mAP50-95 comme métrique de sortie.

10. Exercice 1 — Varier le taux d’occlusion (mesure mAP50-95)

Reprendre le générateur avec un taux d’occlusion occ_rate variable (0.0, 0.15, 0.30, 0.45). Mesurer mAP50-95 (sortie model.val() Ultralytics) de yolo11m sur le val set. Tracer la courbe occlusion → mAP50-95. Le résultat attendu : mAP50-95 monotone décroissant, pente la plus raide entre 0.30 et 0.45 (zone de bascule).

Pourquoi mAP50-95 plutôt que mAP10 ? La variation d’occlusion affecte la précision des boîtes (les boîtes sont retrouvées mais leur position se décale sous l’occlusion). mAP10 masquerait cette dégradation ; mAP50-95 la révèle.

Indice : faire une copie de make_difficult_image qui prend occ_rate en argument, et faire une boucle sur les 4 valeurs.

def mAP5095_au_taux_occ(occ_rate, n_val=100):
    """mAP50-95 (model.val()) de yolo11m sur terrain difficile a taux d'occlusion variable.

    Etape 1 : dupliquer make_difficult_image en version parametrique (occ_rate)
    Etape 2 : generer un split val de n_val images avec ce taux + son data.yaml
    Etape 3 : evaluer TRAINED['yolo11m'] via model.val(data=...) sur ce split
    Etape 4 : retourner le mAP50-95 (metrics.box.map)
    """
    # TODO etudiant
    pass


# Exemple attendu :
# for r in [0.0, 0.15, 0.30, 0.45]:
#     print(f"occ_rate={r:.2f} -> mAP50-95={mAP5095_au_taux_occ(r):.4f}")

11. Exercice 2 — Varier la proportion petit/gros (mesure mAP50-95)

Dans make_difficult_image, la proportion petit/gros est fixée à 40 %/60 %. Modifier cette proportion en 80 % petits / 20 % gros et mesurer le mAP50-95 de chaque modèle. Observation attendue : les modèles anchor-based (YOLOv5) sont plus robustes à la sur-représentation des petits objets que les anchor-free, grâce à leur grille d’anchors explicite.

Pourquoi mAP50-95 ? Les petits objets génèrent des boîtes de faible surface ; sous IoU strict (0.5+), un décalage de quelques pixels suffit à faire chuter l’AP. mAP50-95 capture cette sensibilité mieux que mAP10.

Indice : changer le seuil if rng.uniform() < 0.40 en < 0.80.

def mAP5095_dominante_petits(model_tag="yolo11m", n_val=100):
    """mAP50-95 (model.val()) avec 80% petits objets / 20% gros.

    Etape 1 : dupliquer make_difficult_image avec seuil 0.80
    Etape 2 : generer un split val + son data.yaml
    Etape 3 : evaluer le modele demande via model.val(data=...)
    Etape 4 : retourner le mAP50-95 (metrics.box.map)
    """
    # TODO etudiant
    pass

12. Exercice 3 — Combiner les deux stresseurs

Bonus : combiner occlusions + dominance petits dans le même split val, et comparer le delta cumulé sur les trois modèles. Le résultat attendu : YOLO11m perd ~3× plus que YOLOv5nu en proportion relative (la capacité aide moins quand les deux stresseurs s’additionnent).

def mAP5095_stress_combine(model_tag, n_val=100):
    """mAP50-95 (model.val()) avec occlusions 30% ET 80% petits.

    Etape 1 : variante make_difficult_image avec occ_rate=0.30 et seuil 0.80
    Etape 2 : split val + data.yaml dedie
    Etape 3 : evaluer model_tag parmi TRAINED via model.val(data=...)
    Etape 4 : retourner le mAP50-95 (metrics.box.map)
    """
    # TODO etudiant
    pass

Conclusion

Trois modèles YOLO, deux protocoles :

  • 4.2g (terrain facile) : YOLOv5nu, YOLOv8s, YOLO11n, YOLO11s — 1000 imgs × 6 époques, mAP10 comparés.
  • 4.2i (terrain difficile) : YOLOv5nu, YOLOv8s, YOLO11m — 600 imgs × 4 époques, mAP50-95 comme métrique de comparaison (mAP10 conservé en colonne 2 pour illustrer la saturation IoU bas).

Ce que ce notebook ajoute à la série : - Un générateur de scènes hostiles (occlusions + multi-échelle) réutilisable dans les exercices. - Une mesure explicite du delta de mAP50-95 entre terrain facile et terrain difficile (YOLOv8s : 0.864 → 0.839, YOLOv5nu : 0.751 → 0.735, YOLO11m : 0.819 → 0.801), qui borne le domaine de validité du 4.2g. - Le passage à YOLO11m (capacité) sans gain en mAP50-95 sur terrain difficile, illustrant le surapprentissage quand le terrain dévie.

Retour au sommet