Vision par ordinateur avancée#

L’essentiel est invisible pour les yeux.

— Antoine de Saint-Exupéry, Le Petit Prince

Le chapitre 19 a introduit les réseaux convolutifs (CNN), capables d’extraire automatiquement des hiérarchies de caractéristiques visuelles pour la classification d’images : étant donné une image, le modèle prédit une unique étiquette de classe. Cette tâche, bien que fondamentale, ne représente qu’une fraction des problèmes de vision par ordinateur. Dans de nombreuses applications réelles — conduite autonome, imagerie médicale, robotique — il ne suffit pas de savoir qu’un objet est présent ; il faut savoir il se trouve, quels pixels lui appartiennent, et comment distinguer chaque instance individuelle.

Ce chapitre explore les tâches de prédiction dense (dense prediction) qui vont au-delà de la classification : la détection d’objets, la segmentation sémantique et la segmentation d’instances. Nous étudierons ensuite les Vision Transformers (ViT), qui adaptent l’architecture Transformer (chapitre 23) au domaine visuel, ainsi que les techniques modernes de transfert d’apprentissage et d”augmentation de données. L’objectif est de fournir une vision panoramique de l’état de l’art en vision par ordinateur.

Hide code cell source

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import seaborn as sns

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

import torchvision
import torchvision.transforms as transforms
from torchvision import models

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
np.random.seed(42)
torch.manual_seed(42)

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Périphérique utilisé : {device}")
Périphérique utilisé : cpu

Détection d’objets#

La détection d’objets est la tâche qui consiste à localiser et identifier chaque objet d’intérêt dans une image. Contrairement à la classification qui produit une unique étiquette, la détection produit un ensemble de boîtes englobantes (bounding boxes), chacune associée à une classe et à un score de confiance.

Formulation du problème#

Définition 287 (Détection d’objets)

Soit une image \(\mathbf{I} \in \mathbb{R}^{H \times W \times 3}\). La détection d’objets consiste à prédire un ensemble de \(N\) détections :

\[\{(b_i, c_i, s_i)\}_{i=1}^{N}\]

où :

  • \(b_i = (x_i, y_i, w_i, h_i) \in \mathbb{R}^4\) est la boîte englobante (coordonnées du centre, largeur, hauteur),

  • \(c_i \in \{1, \ldots, K\}\) est la classe de l’objet parmi \(K\) catégories,

  • \(s_i \in [0, 1]\) est le score de confiance de la prédiction.

Le défi central de la détection réside dans le fait que le nombre d’objets \(N\) varie d’une image à l’autre, et que les objets peuvent apparaître à n’importe quelle position et à n’importe quelle échelle. Cela distingue fondamentalement la détection de la classification, où la sortie est de dimension fixe.

Définition 288 (Intersection over Union (IoU))

L”Intersection over Union (IoU), également appelée indice de Jaccard, mesure le recouvrement entre une boîte prédite \(B_p\) et une boîte de vérité terrain \(B_{gt}\) :

\[\text{IoU}(B_p, B_{gt}) = \frac{|B_p \cap B_{gt}|}{|B_p \cup B_{gt}|}\]

\(|\cdot|\) désigne l’aire. L’IoU varie entre 0 (aucun recouvrement) et 1 (recouvrement parfait). Un seuil courant pour considérer une détection comme correcte est \(\text{IoU} \geq 0.5\).

Hide code cell source

# Illustration : calcul de l'IoU entre deux boîtes englobantes
def compute_iou(box1, box2):
    """Calcule l'IoU entre deux boîtes [x1, y1, x2, y2]."""
    x1 = max(box1[0], box2[0])
    y1 = max(box1[1], box2[1])
    x2 = min(box1[2], box2[2])
    y2 = min(box1[3], box2[3])

    intersection = max(0, x2 - x1) * max(0, y2 - y1)
    area1 = (box1[2] - box1[0]) * (box1[3] - box1[1])
    area2 = (box2[2] - box2[0]) * (box2[3] - box2[1])
    union = area1 + area2 - intersection

    return intersection / union if union > 0 else 0.0

# Démonstration visuelle
fig, axes = plt.subplots(1, 3, figsize=(14, 5))
cases = [
    ([1, 1, 4, 4], [3, 3, 6, 6], "Faible recouvrement"),
    ([1, 1, 5, 5], [2, 2, 6, 6], "Recouvrement modéré"),
    ([1, 1, 5, 5], [1.5, 1.5, 5.5, 5.5], "Fort recouvrement"),
]

for ax, (b1, b2, title) in zip(axes, cases):
    iou = compute_iou(b1, b2)
    rect1 = patches.Rectangle((b1[0], b1[1]), b1[2]-b1[0], b1[3]-b1[1],
                                linewidth=2, edgecolor='steelblue',
                                facecolor='steelblue', alpha=0.3)
    rect2 = patches.Rectangle((b2[0], b2[1]), b2[2]-b2[0], b2[3]-b2[1],
                                linewidth=2, edgecolor='coral',
                                facecolor='coral', alpha=0.3)
    ax.add_patch(rect1)
    ax.add_patch(rect2)
    ax.set_xlim(0, 7); ax.set_ylim(0, 7)
    ax.set_aspect('equal')
    ax.set_title(f"{title}\nIoU = {iou:.3f}", fontsize=11)
    ax.legend(["Boîte prédite", "Vérité terrain"], fontsize=8, loc='upper right')

plt.tight_layout()
plt.show()
_images/de3c4f347d86f26ce111efe6fece5ee3ce398964922b204822dcaa50608f0ab5.png

Détecteurs à deux étapes : la famille R-CNN#

Les premiers détecteurs modernes basés sur l’apprentissage profond utilisent une approche en deux étapes : d’abord proposer des régions susceptibles de contenir un objet, puis classifier et affiner chaque proposition.

Définition 289 (Détecteur à deux étapes)

Un détecteur à deux étapes (two-stage detector) décompose la détection en :

  1. Proposition de régions (Region Proposal) : génération d’un ensemble de régions candidates susceptibles de contenir un objet.

  2. Classification et régression : pour chaque région candidate, prédiction de la classe et affinement des coordonnées de la boîte englobante.

R-CNN (Girshick et al., 2014) est le détecteur fondateur de cette famille. L’algorithme utilise la recherche sélective (Selective Search) pour générer environ 2000 propositions de régions, puis extrait les caractéristiques de chaque région à l’aide d’un CNN pré-entraîné (par exemple AlexNet ou VGG). Ces caractéristiques alimentent un SVM linéaire pour la classification et un régresseur pour affiner les boîtes. Le problème majeur est l’inefficacité : le CNN est appliqué indépendamment à chaque région, ce qui rend l’inférence extrêmement lente.

Fast R-CNN (Girshick, 2015) résout cette redondance en appliquant le CNN une seule fois sur l’image entière pour produire une carte de caractéristiques (feature map). Les propositions de régions sont ensuite projetées sur cette carte, et un mécanisme de RoI Pooling (Region of Interest Pooling) extrait un vecteur de taille fixe pour chaque région, permettant la classification et la régression en un seul passage.

Définition 290 (RoI Pooling)

Le RoI Pooling projette une région d’intérêt de taille variable sur la carte de caractéristiques, puis la découpe en une grille de taille fixe \(H_p \times W_p\). Un max-pooling est appliqué à chaque cellule de la grille, produisant un vecteur de dimension \(C \times H_p \times W_p\) indépendant de la taille originale de la région.

Faster R-CNN (Ren et al., 2015) remplace la recherche sélective par un réseau de proposition de régions (Region Proposal Network, RPN) appris de bout en bout. Le RPN opère directement sur la carte de caractéristiques et produit des propositions à l’aide de boîtes d’ancrage (anchor boxes).

Définition 291 (Boîtes d’ancrage)

Les boîtes d’ancrage (anchor boxes) sont un ensemble de boîtes de référence prédéfinies, de différentes tailles et proportions (aspect ratios), centrées sur chaque position de la carte de caractéristiques. Pour chaque ancre, le réseau prédit :

  • un score d”objectness : probabilité que l’ancre contienne un objet (vs. arrière-plan),

  • un décalage \((\delta_x, \delta_y, \delta_w, \delta_h)\) pour affiner les coordonnées.

Si la carte de caractéristiques est de taille \(H' \times W'\) et que l’on utilise \(k\) ancres par position, le RPN génère \(H' \times W' \times k\) propositions candidates.

Hide code cell source

# Illustration : boîtes d'ancrage sur une grille de carte de caractéristiques
fig, ax = plt.subplots(figsize=(7, 7))

grid_size = 4
aspect_ratios = [(1, 1), (1.5, 0.75), (0.75, 1.5)]
colors = ['#4C72B0', '#DD8452', '#55A868']

for i in range(grid_size):
    for j in range(grid_size):
        cx, cy = j + 0.5, i + 0.5
        ax.plot(cx, cy, 'k.', markersize=4)
        if i == 1 and j == 2:
            for (w, h), color in zip(aspect_ratios, colors):
                rect = patches.Rectangle(
                    (cx - w/2, cy - h/2), w, h,
                    linewidth=2, edgecolor=color,
                    facecolor='none', linestyle='--'
                )
                ax.add_patch(rect)

ax.set_xlim(-0.5, grid_size + 0.5)
ax.set_ylim(-0.5, grid_size + 0.5)
ax.set_aspect('equal')
ax.set_title("Boîtes d'ancrage de différentes proportions\n(illustrées pour une position de la grille)")
ax.set_xlabel("Position horizontale")
ax.set_ylabel("Position verticale")
ax.legend(
    [patches.Patch(edgecolor=c, facecolor='none', linestyle='--') for c in colors],
    ["1:1", "2:1", "1:2"], title="Rapport d'aspect", loc='upper left'
)
plt.tight_layout()
plt.show()
_images/4687ab3a27bcc3438c51bcb4e009570293c4834a3026c5c35f6b95d12cf6d65c.png

Remarque 245

L’évolution de R-CNN à Faster R-CNN illustre un principe récurrent en apprentissage profond : remplacer les modules heuristiques par des modules appris. La recherche sélective (heuristique) est remplacée par le RPN (appris), ce qui permet un entraînement de bout en bout et améliore à la fois la vitesse et la précision.

Détecteurs à une étape : YOLO#

Les détecteurs à deux étapes offrent une grande précision mais restent relativement lents. Les détecteurs à une étape (one-stage detectors) éliminent l’étape de proposition de régions et prédisent directement les boîtes englobantes et les classes en un seul passage du réseau.

YOLO (You Only Look Once, Redmon et al., 2016) est le détecteur à une étape le plus emblématique. Son principe est élégant : l’image est divisée en une grille \(S \times S\), et chaque cellule de la grille est responsable de la détection des objets dont le centre tombe dans cette cellule.

Définition 292 (Architecture YOLO)

YOLO divise l’image d’entrée en une grille de \(S \times S\) cellules. Pour chaque cellule, le réseau prédit :

  • \(B\) boîtes englobantes, chacune caractérisée par \((x, y, w, h, \text{conf})\)\((x, y)\) est le centre relatif à la cellule, \((w, h)\) les dimensions relatives à l’image, et \(\text{conf}\) le score de confiance,

  • \(K\) probabilités de classes conditionnelles \(P(c_k \mid \text{objet})\).

La sortie totale est un tenseur de dimension \(S \times S \times (B \times 5 + K)\). La prédiction finale combine le score de confiance et la probabilité de classe :

\[\text{score}(c_k) = P(c_k \mid \text{objet}) \times \text{conf} \times \text{IoU}_{\text{pred}}^{\text{gt}}\]

Hide code cell source

# Illustration : principe de la grille YOLO
fig, axes = plt.subplots(2, 1, figsize=(9, 11))

# Grille YOLO
ax = axes[0]
S = 7
for i in range(S + 1):
    ax.axhline(y=i, color='gray', linewidth=0.5, alpha=0.5)
    ax.axvline(x=i, color='gray', linewidth=0.5, alpha=0.5)

# Objet simulé (voiture)
obj_center = (3.3, 4.2)
obj_w, obj_h = 2.5, 1.8
rect = patches.Rectangle(
    (obj_center[0] - obj_w/2, obj_center[1] - obj_h/2), obj_w, obj_h,
    linewidth=2.5, edgecolor='coral', facecolor='coral', alpha=0.15
)
ax.add_patch(rect)
cell_i, cell_j = int(obj_center[0]), int(obj_center[1])
cell_rect = patches.Rectangle(
    (cell_i, cell_j), 1, 1,
    linewidth=2, edgecolor='steelblue', facecolor='steelblue', alpha=0.2
)
ax.add_patch(cell_rect)
ax.plot(*obj_center, 'ro', markersize=8)
ax.annotate("Centre de l'objet", obj_center,
            textcoords="offset points", xytext=(15, 15),
            fontsize=9, color='red',
            arrowprops=dict(arrowstyle='->', color='red'))

ax.set_xlim(0, S); ax.set_ylim(0, S)
ax.set_aspect('equal')
ax.set_title(f"Grille YOLO ({S}×{S})\nLa cellule bleue prédit l'objet rouge")

# Comparaison vitesse/précision
ax = axes[1]
detectors = ['R-CNN', 'Fast\nR-CNN', 'Faster\nR-CNN', 'YOLOv1', 'SSD', 'YOLOv3']
map_scores = [58.5, 70.0, 73.2, 63.4, 74.3, 57.9]
fps_scores = [0.05, 0.5, 5, 45, 22, 30]

scatter = ax.scatter(fps_scores, map_scores, s=200, c=range(len(detectors)),
                     cmap='viridis', edgecolors='black', linewidth=1, zorder=5)
for i, name in enumerate(detectors):
    ax.annotate(name, (fps_scores[i], map_scores[i]),
                textcoords="offset points", xytext=(10, 5), fontsize=9)
ax.set_xlabel("Images par seconde (FPS)")
ax.set_ylabel("mAP (%)")
ax.set_title("Compromis vitesse / précision")
ax.set_xscale('log')

plt.tight_layout()
plt.show()
_images/081b345fd2b5894fee45833473ceb806b76d0dfb6796f347d36b51d3dc849cb2.png

Suppression des non-maximum (NMS)#

Un détecteur d’objets produit souvent de multiples boîtes chevauchantes pour un même objet. La suppression des non-maximum (Non-Maximum Suppression, NMS) est un post-traitement essentiel qui élimine les détections redondantes.

Définition 293 (Non-Maximum Suppression)

L’algorithme NMS procède comme suit :

  1. Trier toutes les détections par score de confiance décroissant.

  2. Sélectionner la détection de score maximal et la placer dans l’ensemble final.

  3. Supprimer toutes les détections restantes dont l’IoU avec la détection sélectionnée dépasse un seuil \(\tau\) (typiquement \(\tau = 0.5\)).

  4. Répéter les étapes 2–3 jusqu’à épuisement des détections.

Hide code cell source

def nms(boxes, scores, iou_threshold=0.5):
    """Non-Maximum Suppression sur des boîtes [x1, y1, x2, y2]."""
    order = np.argsort(scores)[::-1]
    keep = []

    while len(order) > 0:
        idx = order[0]
        keep.append(idx)
        if len(order) == 1:
            break

        ious = np.array([compute_iou(boxes[idx], boxes[j]) for j in order[1:]])
        remaining = np.where(ious <= iou_threshold)[0]
        order = order[remaining + 1]

    return keep

# Démonstration : NMS sur des détections chevauchantes
boxes_demo = np.array([
    [1.0, 1.0, 4.0, 4.0],
    [1.2, 1.1, 4.2, 4.1],
    [1.1, 0.9, 3.9, 3.8],
    [5.0, 5.0, 7.5, 7.5],
    [5.1, 5.2, 7.6, 7.7],
])
scores_demo = np.array([0.92, 0.88, 0.75, 0.95, 0.80])

kept = nms(boxes_demo, scores_demo, iou_threshold=0.5)

fig, axes = plt.subplots(1, 2, figsize=(13, 6))
for ax, title, indices in zip(
    axes,
    ["Avant NMS (5 détections)", f"Après NMS ({len(kept)} détections)"],
    [range(len(boxes_demo)), kept]
):
    colors = plt.cm.Set2(np.linspace(0, 1, len(boxes_demo)))
    for i in indices:
        b = boxes_demo[i]
        rect = patches.Rectangle(
            (b[0], b[1]), b[2]-b[0], b[3]-b[1],
            linewidth=2, edgecolor=colors[i], facecolor=colors[i], alpha=0.25
        )
        ax.add_patch(rect)
        ax.text(b[0], b[3] + 0.15, f"{scores_demo[i]:.2f}",
                fontsize=10, color=colors[i], fontweight='bold')
    ax.set_xlim(0, 9); ax.set_ylim(0, 9)
    ax.set_aspect('equal')
    ax.set_title(title)

plt.tight_layout()
plt.show()
_images/4c0110fcb8d491bc2194b44ccff7d446855122ff092c99b3c4477db78351c87f.png

Métriques : mAP#

Définition 294 (Mean Average Precision (mAP))

La mean Average Precision (mAP) est la métrique standard pour évaluer les détecteurs d’objets.

  1. Pour chaque classe \(k\), on trie les détections par score décroissant et on calcule la courbe précision-rappel en variant le seuil de confiance. Une détection est un vrai positif si son \(\text{IoU}\) avec une boîte de vérité terrain non encore associée dépasse le seuil (typiquement 0.5), et un faux positif sinon.

  2. L”Average Precision (AP) pour la classe \(k\) est l’aire sous la courbe précision-rappel :

\[\text{AP}_k = \int_0^1 p_k(r) \, dr\]
  1. Le mAP est la moyenne sur toutes les classes :

\[\text{mAP} = \frac{1}{K} \sum_{k=1}^{K} \text{AP}_k\]

Les variantes courantes sont \(\text{mAP}_{50}\) (IoU \(\geq 0.5\)) et \(\text{mAP}_{50:95}\) (moyenne pour les seuils IoU de 0.5 à 0.95 par pas de 0.05).

Détection avec un modèle pré-entraîné#

Torchvision fournit des modèles de détection pré-entraînés sur COCO. Voici comment utiliser Faster R-CNN pour détecter des objets dans une image synthétique.

Hide code cell source

# Démonstration de l'architecture Faster R-CNN (sans téléchargement de poids)
from torchvision.models.detection import fasterrcnn_mobilenet_v3_large_fpn

# Charger le modèle sans poids pré-entraînés (structure légère)
model_det = fasterrcnn_mobilenet_v3_large_fpn(weights=None, num_classes=91)
model_det.eval()

# Catégories COCO (sous-ensemble)
categories_coco = [
    '__background__', 'person', 'bicycle', 'car', 'motorcycle', 'airplane',
    'bus', 'train', 'truck', 'boat', 'traffic light', 'fire hydrant',
    'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog',
]
print(f"Nombre de catégories COCO : 91")
print(f"Premières catégories : {categories_coco[1:11]}")

# Image aléatoire (en production : charger une vraie image)
dummy_image = torch.rand(3, 224, 224)

with torch.no_grad():
    predictions = model_det([dummy_image])

pred = predictions[0]
print(f"\nNombre de détections : {len(pred['boxes'])}")
print(f"Clés de la sortie : {list(pred.keys())}")
print(f"Forme des boîtes : {pred['boxes'].shape}")
print(f"Forme des scores : {pred['scores'].shape}")
Nombre de catégories COCO : 91
Premières catégories : ['person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light']
Nombre de détections : 0
Clés de la sortie : ['boxes', 'labels', 'scores']
Forme des boîtes : torch.Size([0, 4])
Forme des scores : torch.Size([0])

Remarque 246

En pratique, les modèles de détection modernes comme YOLOv8 ou DETR (DEtection TRansformer) atteignent des performances remarquables sur des benchmarks comme COCO. DETR est particulièrement intéressant car il formule la détection comme un problème d’ensemble (set prediction) résolu par un Transformer, éliminant le besoin de NMS et d’ancres prédéfinies.

Segmentation#

La segmentation va plus loin que la détection en attribuant une étiquette à chaque pixel de l’image. Il existe trois variantes principales de cette tâche.

Segmentation sémantique, d’instances et panoptique#

Définition 295 (Types de segmentation)

  • Segmentation sémantique : attribuer une étiquette de classe \(c \in \{1, \ldots, K\}\) à chaque pixel \((i, j)\) de l’image. Deux voitures distinctes reçoivent la même étiquette « voiture ».

  • Segmentation d’instances : identifier chaque instance individuelle d’objet avec un masque distinct. Deux voitures reçoivent des masques séparés, mais les pixels d’arrière-plan ne sont pas classifiés.

  • Segmentation panoptique : combine les deux approches. Chaque pixel reçoit une étiquette de classe et un identifiant d’instance. Les classes « chose » (stuff : ciel, route, herbe) sont traitées en segmentation sémantique, et les classes « objet » (thing : voiture, personne) en segmentation d’instances.

Hide code cell source

# Illustration des trois types de segmentation
fig, axes = plt.subplots(2, 2, figsize=(11, 9))
axes = axes.flatten()

# Image simulée (matrice de classes)
H, W = 8, 10
np.random.seed(42)
image = np.zeros((H, W))
image[0:3, :] = 0       # ciel
image[3:5, :] = 1       # bâtiment
image[5:, :] = 2        # route
image[2:6, 1:3] = 3     # voiture 1
image[3:7, 6:8] = 3     # voiture 2

cmap_sem = plt.colormaps.get_cmap('Set3').resampled(4)

titles = ["Image originale", "Sémantique", "Instances", "Panoptique"]
for ax, title in zip(axes, titles):
    ax.set_title(title, fontsize=11)
    ax.set_xticks([]); ax.set_yticks([])

# Image originale (gris)
axes[0].imshow(np.random.rand(H, W, 3) * 0.3 + 0.3)

# Sémantique
axes[1].imshow(image, cmap=cmap_sem, vmin=0, vmax=3)

# Instances (seuls les objets)
instance_map = np.full((H, W), np.nan)
instance_map[2:6, 1:3] = 1   # voiture instance 1
instance_map[3:7, 6:8] = 2   # voiture instance 2
axes[2].imshow(np.where(np.isnan(instance_map), 0.9, 0.5),
               cmap='gray', vmin=0, vmax=1)
axes[2].imshow(instance_map, cmap='Set1', alpha=0.6)

# Panoptique
axes[3].imshow(image, cmap=cmap_sem, vmin=0, vmax=3, alpha=0.5)
panoptic_overlay = np.zeros((H, W, 4))
panoptic_overlay[2:6, 1:3] = [1, 0.2, 0.2, 0.5]
panoptic_overlay[3:7, 6:8] = [0.2, 0.2, 1, 0.5]
axes[3].imshow(panoptic_overlay)

plt.tight_layout()
plt.show()
_images/469655647848677b7f2b5377f2c35781265c49fbe73efe507b6a8fa8596f8173.png

Architecture U-Net#

L’architecture U-Net (Ronneberger et al., 2015), initialement conçue pour la segmentation d’images médicales, est devenue l’architecture de référence pour la segmentation sémantique. Son principe repose sur une structure encodeur-décodeur enrichie de connexions de saut (skip connections).

Définition 296 (Architecture encodeur-décodeur avec connexions de saut)

L’architecture U-Net se compose de :

  1. Encodeur (chemin contractant) : succession de blocs convolutifs suivis de max-pooling, qui réduisent la résolution spatiale tout en augmentant le nombre de canaux. L’encodeur capture le contexte sémantique.

  2. Décodeur (chemin expansif) : succession de sur-échantillonnages (upsampling) par convolution transposée, suivis de blocs convolutifs, qui restaurent progressivement la résolution spatiale.

  3. Connexions de saut : les cartes de caractéristiques de l’encodeur sont concaténées avec celles du décodeur au même niveau de résolution, permettant au décodeur de combiner l’information sémantique de haut niveau avec la localisation précise de bas niveau.

Si l’encodeur produit des caractéristiques \(\mathbf{e}_l\) au niveau \(l\) et le décodeur produit \(\mathbf{d}_l\), la connexion de saut donne :

\[\mathbf{d}_l' = [\mathbf{d}_l ; \mathbf{e}_l]\]

\([\ ;\ ]\) désigne la concaténation le long de la dimension des canaux.

Hide code cell source

class UNetBlock(nn.Module):
    """Bloc convolutif double pour U-Net."""
    def __init__(self, in_ch, out_ch):
        super().__init__()
        self.conv = nn.Sequential(
            nn.Conv2d(in_ch, out_ch, 3, padding=1),
            nn.BatchNorm2d(out_ch),
            nn.ReLU(inplace=True),
            nn.Conv2d(out_ch, out_ch, 3, padding=1),
            nn.BatchNorm2d(out_ch),
            nn.ReLU(inplace=True),
        )

    def forward(self, x):
        return self.conv(x)


class SimpleUNet(nn.Module):
    """Implémentation simplifiée de U-Net."""
    def __init__(self, in_channels=3, num_classes=2):
        super().__init__()
        # Encodeur
        self.enc1 = UNetBlock(in_channels, 64)
        self.enc2 = UNetBlock(64, 128)
        self.enc3 = UNetBlock(128, 256)
        self.enc4 = UNetBlock(256, 512)
        self.pool = nn.MaxPool2d(2)

        # Pont (bottleneck)
        self.bottleneck = UNetBlock(512, 1024)

        # Décodeur
        self.up4 = nn.ConvTranspose2d(1024, 512, 2, stride=2)
        self.dec4 = UNetBlock(1024, 512)
        self.up3 = nn.ConvTranspose2d(512, 256, 2, stride=2)
        self.dec3 = UNetBlock(512, 256)
        self.up2 = nn.ConvTranspose2d(256, 128, 2, stride=2)
        self.dec2 = UNetBlock(256, 128)
        self.up1 = nn.ConvTranspose2d(128, 64, 2, stride=2)
        self.dec1 = UNetBlock(128, 64)

        # Couche finale
        self.final = nn.Conv2d(64, num_classes, 1)

    def forward(self, x):
        # Encodeur avec sauvegarde pour les skip connections
        e1 = self.enc1(x)
        e2 = self.enc2(self.pool(e1))
        e3 = self.enc3(self.pool(e2))
        e4 = self.enc4(self.pool(e3))

        # Bottleneck
        b = self.bottleneck(self.pool(e4))

        # Décodeur avec skip connections (concaténation)
        d4 = self.dec4(torch.cat([self.up4(b), e4], dim=1))
        d3 = self.dec3(torch.cat([self.up3(d4), e3], dim=1))
        d2 = self.dec2(torch.cat([self.up2(d3), e2], dim=1))
        d1 = self.dec1(torch.cat([self.up1(d2), e1], dim=1))

        return self.final(d1)


# Test de l'architecture
unet = SimpleUNet(in_channels=3, num_classes=5)
x_test = torch.randn(1, 3, 128, 128)
y_test = unet(x_test)
print(f"Entrée  : {x_test.shape}")
print(f"Sortie  : {y_test.shape}")
print(f"Nombre de paramètres : {sum(p.numel() for p in unet.parameters()):,}")
Entrée  : torch.Size([1, 3, 128, 128])
Sortie  : torch.Size([1, 5, 128, 128])
Nombre de paramètres : 31,043,781

Remarque 247

La forme de la sortie U-Net est \((N, K, H, W)\)\(K\) est le nombre de classes. Chaque pixel reçoit un vecteur de logits de dimension \(K\), auquel on applique un softmax pour obtenir les probabilités de classe. La sortie a exactement la même résolution que l’entrée, ce qui est essentiel pour la prédiction dense.

Mask R-CNN#

Mask R-CNN (He et al., 2017) étend Faster R-CNN à la segmentation d’instances en ajoutant une branche de segmentation parallèle aux branches de classification et de régression. Pour chaque région d’intérêt, Mask R-CNN prédit un masque binaire de taille \(m \times m\) pour chaque classe, en utilisant un petit réseau convolutif appliqué aux caractéristiques de la région.

Définition 297 (Mask R-CNN)

Mask R-CNN ajoute à Faster R-CNN une troisième sortie par RoI :

  • Classification : étiquette de classe \(c \in \{1, \ldots, K\}\)

  • Régression : affinement de la boîte \((\delta_x, \delta_y, \delta_w, \delta_h)\)

  • Masque : un masque binaire \(\mathbf{M} \in \{0, 1\}^{m \times m}\) prédit indépendamment pour chaque classe

La perte totale est :

\[\mathcal{L} = \mathcal{L}_{\text{cls}} + \mathcal{L}_{\text{box}} + \mathcal{L}_{\text{mask}}\]

\(\mathcal{L}_{\text{mask}}\) est une entropie croisée binaire pixel par pixel, calculée uniquement sur le masque de la classe prédite (découplage classe/masque).

Un point technique important est le remplacement du RoI Pooling par le RoI Align, qui utilise une interpolation bilinéaire au lieu de quantifications discrètes, améliorant la précision spatiale des masques.

Fonctions de perte pour la segmentation#

Définition 298 (Fonctions de perte pour la segmentation)

Entropie croisée pixel par pixel. Pour une image de \(H \times W\) pixels avec \(K\) classes, la perte est :

\[\mathcal{L}_{\text{CE}} = -\frac{1}{H \cdot W} \sum_{i=1}^{H} \sum_{j=1}^{W} \sum_{k=1}^{K} y_{ijk} \log(\hat{y}_{ijk})\]

\(y_{ijk}\) est l’indicatrice de la classe \(k\) au pixel \((i, j)\) et \(\hat{y}_{ijk}\) la probabilité prédite.

Dice Loss. Le coefficient de Dice mesure le recouvrement entre la prédiction et la vérité terrain, et est particulièrement utile pour les classes déséquilibrées :

\[\mathcal{L}_{\text{Dice}} = 1 - \frac{2 \sum_{i} p_i g_i + \epsilon}{\sum_{i} p_i + \sum_{i} g_i + \epsilon}\]

\(p_i\) est la probabilité prédite et \(g_i\) l’étiquette binaire au pixel \(i\), et \(\epsilon\) est un terme de stabilisation numérique.

Hide code cell source

def dice_loss(pred, target, smooth=1.0):
    """Dice Loss pour segmentation binaire.

    Args:
        pred: prédictions après sigmoid, shape (N, 1, H, W)
        target: masques binaires, shape (N, 1, H, W)
    """
    pred_flat = pred.view(pred.size(0), -1)
    target_flat = target.view(target.size(0), -1)

    intersection = (pred_flat * target_flat).sum(dim=1)
    union = pred_flat.sum(dim=1) + target_flat.sum(dim=1)

    dice = (2.0 * intersection + smooth) / (union + smooth)
    return 1.0 - dice.mean()


# Démonstration : comparaison des pertes
pred_good = torch.sigmoid(torch.randn(1, 1, 32, 32) + 2)  # bonne prédiction
pred_bad = torch.sigmoid(torch.randn(1, 1, 32, 32) - 2)   # mauvaise prédiction
target = torch.ones(1, 1, 32, 32)

bce = nn.BCELoss()
print("=== Bonne prédiction ===")
print(f"  BCE Loss  : {bce(pred_good, target):.4f}")
print(f"  Dice Loss : {dice_loss(pred_good, target):.4f}")
print()
print("=== Mauvaise prédiction ===")
print(f"  BCE Loss  : {bce(pred_bad, target):.4f}")
print(f"  Dice Loss : {dice_loss(pred_bad, target):.4f}")
=== Bonne prédiction ===
  BCE Loss  : 0.1833
  Dice Loss : 0.0840

=== Mauvaise prédiction ===
  BCE Loss  : 2.1660
  Dice Loss : 0.7266

Vision Transformers (ViT)#

Le chapitre 23 a présenté l’architecture Transformer et son mécanisme d’attention multi-tête pour le traitement des séquences. Une question naturelle émerge : peut-on appliquer les Transformers directement aux images, sans convolutions ? La réponse est oui, et c’est l’idée fondatrice du Vision Transformer (ViT) (Dosovitskiy et al., 2020).

Découpage en patches#

La première difficulté est de transformer une image 2D en une séquence exploitable par un Transformer. Le ViT propose une solution élégante : découper l’image en patches de taille fixe et traiter chaque patch comme un « token » visuel, analogue à un mot dans une phrase.

Définition 299 (Patch embedding)

Soit une image \(\mathbf{I} \in \mathbb{R}^{H \times W \times C}\) et une taille de patch \(P \times P\). L’image est découpée en \(N = \frac{H \times W}{P^2}\) patches non chevauchants :

\[\mathbf{x}_p^{(i)} \in \mathbb{R}^{P^2 \cdot C}, \quad i = 1, \ldots, N\]

Chaque patch est « aplati » puis projeté linéairement dans un espace de dimension \(D\) :

\[\mathbf{z}_0^{(i)} = \mathbf{x}_p^{(i)} \mathbf{E} + \mathbf{e}_{\text{pos}}^{(i)}\]

\(\mathbf{E} \in \mathbb{R}^{(P^2 C) \times D}\) est la matrice de projection et \(\mathbf{e}_{\text{pos}}^{(i)} \in \mathbb{R}^D\) est l”embedding positionnel (appris) du patch \(i\).

Un token de classification \([\text{CLS}]\) est ajouté en début de séquence, exactement comme dans BERT. La séquence d’entrée du Transformer est donc :

\[\mathbf{z}_0 = [\mathbf{z}_{\text{cls}} ; \mathbf{z}_0^{(1)} ; \ldots ; \mathbf{z}_0^{(N)}] \in \mathbb{R}^{(N+1) \times D}\]

Hide code cell source

# Illustration : découpage d'une image en patches
fig, axes = plt.subplots(1, 3, figsize=(14, 5))

# Image simulée
H, W = 224, 224
image_sim = np.random.rand(H, W, 3) * 0.3 + 0.4

for ax, P, title in zip(axes, [16, 32, 56],
                          ["P=16 (196 patches)", "P=32 (49 patches)", "P=56 (16 patches)"]):
    ax.imshow(image_sim)
    n_patches_h = H // P
    n_patches_w = W // P
    for i in range(1, n_patches_h):
        ax.axhline(y=i * P, color='white', linewidth=1.5)
    for j in range(1, n_patches_w):
        ax.axvline(x=j * P, color='white', linewidth=1.5)
    ax.set_title(f"{title}\n$N = {n_patches_h} \\times {n_patches_w} = {n_patches_h * n_patches_w}$")
    ax.set_xticks([]); ax.set_yticks([])

plt.suptitle("Découpage d'une image $224 \\times 224$ en patches", fontsize=13, y=1.02)
plt.tight_layout()
plt.show()
_images/97062b90d3fad63af40a39326c5d744212a1a476cc7f19522d6d124ed5afeabc.png

Architecture complète du ViT#

Définition 300 (Vision Transformer (ViT))

L’architecture ViT se compose de :

  1. Patch embedding : projection linéaire des patches + embedding positionnel + token \([\text{CLS}]\).

  2. Encodeur Transformer : \(L\) couches identiques, chacune comprenant :

    • Multi-Head Self-Attention (MHSA) avec connexion résiduelle et normalisation de couche,

    • MLP (deux couches linéaires avec GELU) avec connexion résiduelle et normalisation de couche.

\[\mathbf{z}_l' = \text{MHSA}(\text{LN}(\mathbf{z}_{l-1})) + \mathbf{z}_{l-1}\]
\[\mathbf{z}_l = \text{MLP}(\text{LN}(\mathbf{z}_l')) + \mathbf{z}_l'\]
  1. Tête de classification : le vecteur correspondant au token \([\text{CLS}]\) de la dernière couche est projeté linéairement vers les \(K\) classes :

\[\hat{y} = \text{Linear}(\text{LN}(\mathbf{z}_L^{(0)}))\]

Hide code cell source

class PatchEmbedding(nn.Module):
    """Découpe l'image en patches et les projette linéairement."""
    def __init__(self, img_size=224, patch_size=16, in_channels=3, embed_dim=768):
        super().__init__()
        self.patch_size = patch_size
        self.n_patches = (img_size // patch_size) ** 2

        # Projection linéaire via convolution (noyau = taille du patch, stride = taille du patch)
        self.projection = nn.Conv2d(
            in_channels, embed_dim,
            kernel_size=patch_size, stride=patch_size
        )

    def forward(self, x):
        # x : (N, C, H, W) -> (N, embed_dim, H/P, W/P) -> (N, n_patches, embed_dim)
        x = self.projection(x)          # (N, embed_dim, H/P, W/P)
        x = x.flatten(2).transpose(1, 2)  # (N, n_patches, embed_dim)
        return x


class TransformerBlock(nn.Module):
    """Bloc Transformer : MHSA + MLP avec connexions résiduelles."""
    def __init__(self, embed_dim=768, n_heads=12, mlp_ratio=4.0, dropout=0.1):
        super().__init__()
        self.ln1 = nn.LayerNorm(embed_dim)
        self.attn = nn.MultiheadAttention(embed_dim, n_heads,
                                           dropout=dropout, batch_first=True)
        self.ln2 = nn.LayerNorm(embed_dim)
        self.mlp = nn.Sequential(
            nn.Linear(embed_dim, int(embed_dim * mlp_ratio)),
            nn.GELU(),
            nn.Dropout(dropout),
            nn.Linear(int(embed_dim * mlp_ratio), embed_dim),
            nn.Dropout(dropout),
        )

    def forward(self, x):
        # Multi-Head Self-Attention
        h = self.ln1(x)
        attn_out, _ = self.attn(h, h, h)
        x = x + attn_out
        # MLP
        x = x + self.mlp(self.ln2(x))
        return x


class SimpleViT(nn.Module):
    """Vision Transformer simplifié pour la classification."""
    def __init__(self, img_size=224, patch_size=16, in_channels=3,
                 num_classes=10, embed_dim=768, depth=12,
                 n_heads=12, mlp_ratio=4.0, dropout=0.1):
        super().__init__()
        self.patch_embed = PatchEmbedding(img_size, patch_size,
                                           in_channels, embed_dim)
        n_patches = self.patch_embed.n_patches

        # Token [CLS] et embeddings positionnels
        self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim))
        self.pos_embed = nn.Parameter(torch.zeros(1, n_patches + 1, embed_dim))
        self.pos_drop = nn.Dropout(dropout)

        # Blocs Transformer
        self.blocks = nn.Sequential(*[
            TransformerBlock(embed_dim, n_heads, mlp_ratio, dropout)
            for _ in range(depth)
        ])

        # Tête de classification
        self.norm = nn.LayerNorm(embed_dim)
        self.head = nn.Linear(embed_dim, num_classes)

        # Initialisation
        nn.init.trunc_normal_(self.cls_token, std=0.02)
        nn.init.trunc_normal_(self.pos_embed, std=0.02)

    def forward(self, x):
        N = x.shape[0]

        # Patch embedding
        x = self.patch_embed(x)  # (N, n_patches, embed_dim)

        # Ajouter le token [CLS]
        cls_tokens = self.cls_token.expand(N, -1, -1)
        x = torch.cat([cls_tokens, x], dim=1)  # (N, n_patches+1, embed_dim)

        # Ajouter les embeddings positionnels
        x = self.pos_drop(x + self.pos_embed)

        # Encodeur Transformer
        x = self.blocks(x)

        # Classification via le token [CLS]
        x = self.norm(x[:, 0])  # (N, embed_dim)
        x = self.head(x)        # (N, num_classes)
        return x


# Test de l'architecture ViT
vit = SimpleViT(
    img_size=224, patch_size=16, in_channels=3,
    num_classes=10, embed_dim=384, depth=6,
    n_heads=6, mlp_ratio=4.0
)
x_vit = torch.randn(2, 3, 224, 224)
y_vit = vit(x_vit)

n_params = sum(p.numel() for p in vit.parameters())
print(f"Entrée  : {x_vit.shape}")
print(f"Sortie  : {y_vit.shape}")
print(f"Patches : {vit.patch_embed.n_patches}")
print(f"Nombre de paramètres : {n_params:,}")
Entrée  : torch.Size([2, 3, 224, 224])
Sortie  : torch.Size([2, 10])
Patches : 196
Nombre de paramètres : 11,022,730

Comparaison CNN vs ViT#

Remarque 248

CNN vs Vision Transformer :

Propriété

CNN

ViT

Biais inductif

Fort (localité, invariance par translation)

Faible (attention globale)

Données nécessaires

Efficace avec peu de données

Nécessite beaucoup de données (ou pré-entraînement)

Complexité spatiale

\(O(K^2 \cdot C)\) par couche (\(K\) = taille du noyau)

\(O(N^2 \cdot D)\) par couche (\(N\) = nombre de patches)

Champ réceptif

Croît progressivement avec la profondeur

Global dès la première couche

Scalabilité

Plafonne au-delà d’une certaine taille

S’améliore avec la taille du modèle et des données

Les CNN excellent lorsque les données sont limitées grâce à leurs biais inductifs forts. Les ViT dominent lorsqu’ils sont pré-entraînés sur de très grands jeux de données (ImageNet-21k, JFT-300M). En pratique, les approches hybrides combinent le meilleur des deux mondes.

Approches hybrides CNN + Transformer#

Plusieurs architectures récentes combinent les forces des CNN et des Transformers :

  • Swin Transformer (Liu et al., 2021) : utilise une attention à fenêtre glissante (shifted window) qui limite l’attention à des régions locales tout en permettant l’échange d’information entre fenêtres. La complexité passe de \(O(N^2)\) à \(O(N)\) par rapport au nombre de tokens.

  • ConvNeXt (Liu et al., 2022) : modernise l’architecture ResNet en adoptant les recettes d’entraînement des ViT (augmentation agressive, optimiseur AdamW, plus d’époques), démontrant que les CNN purs peuvent rivaliser avec les Transformers.

  • CoAtNet (Dai et al., 2021) : empile des couches convolutives dans les premières étapes (pour capturer les motifs locaux) et des couches d’attention dans les étapes ultérieures (pour le raisonnement global).

Hide code cell source

# Comparaison des familles d'architectures : performances sur ImageNet
fig, ax = plt.subplots(figsize=(10, 6))

architectures = {
    'CNN classiques': {
        'modèles': ['ResNet-50', 'ResNet-152', 'EfficientNet-B7'],
        'params': [25, 60, 66],
        'top1': [76.1, 78.3, 84.3],
        'color': '#4C72B0'
    },
    'Vision Transformers': {
        'modèles': ['ViT-B/16', 'ViT-L/16', 'ViT-H/14'],
        'params': [86, 307, 632],
        'top1': [77.9, 76.5, 88.6],
        'color': '#DD8452'
    },
    'Hybrides': {
        'modèles': ['Swin-T', 'Swin-B', 'ConvNeXt-B'],
        'params': [28, 88, 89],
        'top1': [81.3, 83.5, 83.8],
        'color': '#55A868'
    }
}

for family, data in architectures.items():
    ax.scatter(data['params'], data['top1'], s=150,
               color=data['color'], edgecolors='black',
               linewidth=1, label=family, zorder=5)
    for model, p, acc in zip(data['modèles'], data['params'], data['top1']):
        ax.annotate(model, (p, acc), textcoords="offset points",
                    xytext=(8, 5), fontsize=8)

ax.set_xlabel("Nombre de paramètres (millions)")
ax.set_ylabel("Top-1 Accuracy ImageNet (%)")
ax.set_title("Comparaison des familles d'architectures sur ImageNet")
ax.legend(fontsize=10)
ax.set_xscale('log')
plt.tight_layout()
plt.show()
_images/4ff9d1aa7de9ebc024a58b41f018bf4818a59d9e9024ae4db3a755723a079c45.png

Remarque 249

Les performances du ViT sur ImageNet-1k sans pré-entraînement supplémentaire sont inférieures à celles des CNN. C’est lorsqu’il est pré-entraîné sur des jeux massifs (ImageNet-21k, JFT-300M) que le ViT surpasse les CNN. Le modèle DINOv2 (Oquab et al., 2023) montre que l’apprentissage auto-supervisé permet au ViT d’apprendre des représentations visuelles remarquables sans aucune étiquette.

Transfer learning en vision#

Le transfert d’apprentissage (transfer learning) est une technique omniprésente en vision par ordinateur. Plutôt que d’entraîner un modèle de zéro, on utilise un modèle pré-entraîné sur un grand jeu de données (typiquement ImageNet) comme point de départ pour une tâche cible.

Extraction de caractéristiques vs fine-tuning#

Définition 301 (Stratégies de transfert d’apprentissage)

Extraction de caractéristiques (feature extraction) :

  • On gèle tous les paramètres du modèle pré-entraîné (le backbone).

  • On remplace la dernière couche (tête de classification) par une nouvelle couche adaptée à la tâche cible.

  • Seuls les paramètres de la nouvelle tête sont entraînés.

  • Avantage : rapide, peu de données nécessaires. Inconvénient : adaptation limitée.

Fine-tuning (affinement) :

  • On remplace également la tête de classification.

  • On dégèle tout ou partie des couches du backbone et on entraîne l’ensemble avec un taux d’apprentissage faible.

  • Avantage : meilleure adaptation. Inconvénient : nécessite plus de données et de calcul.

Fine-tuning progressif (gradual unfreezing) :

  • On commence par entraîner uniquement la tête, puis on dégèle progressivement les couches du backbone, des plus profondes aux plus superficielles.

  • Cela permet une adaptation stable en préservant les caractéristiques de bas niveau (bords, textures).

Hide code cell source

# Exemple : extraction de caractéristiques avec ResNet-50 pré-entraîné
from torchvision.models import resnet50, ResNet50_Weights

# Charger le modèle pré-entraîné
backbone = resnet50(weights=ResNet50_Weights.IMAGENET1K_V2)

# Geler tous les paramètres
for param in backbone.parameters():
    param.requires_grad = False

# Remplacer la tête de classification pour 10 classes
num_features = backbone.fc.in_features
backbone.fc = nn.Linear(num_features, 10)

# Seule la nouvelle tête est entraînable
trainable = sum(p.numel() for p in backbone.parameters() if p.requires_grad)
total = sum(p.numel() for p in backbone.parameters())
print(f"Paramètres totaux    : {total:,}")
print(f"Paramètres entraînables : {trainable:,}")
print(f"Proportion gelée     : {(total - trainable) / total * 100:.1f}%")
Paramètres totaux    : 23,528,522
Paramètres entraînables : 20,490
Proportion gelée     : 99.9%

Backbones pré-entraînés courants#

Remarque 250

Les backbones pré-entraînés les plus utilisés en vision :

Backbone

Année

Paramètres

Top-1 ImageNet

Architecture

ResNet-50

2015

25M

80.9%

CNN résiduel

EfficientNet-B0

2019

5.3M

77.7%

CNN optimisé (NAS)

EfficientNet-B7

2019

66M

84.3%

CNN optimisé (NAS)

ViT-B/16

2020

86M

81.8%

Transformer

Swin-B

2021

88M

83.5%

Transformer hiérarchique

ConvNeXt-B

2022

89M

83.8%

CNN modernisé

Le choix du backbone dépend du compromis entre la taille du jeu de données cible, les contraintes de calcul et la précision requise.

Conseils pratiques pour le transfert#

Exemple 36 (Bonnes pratiques pour le transfer learning)

  1. Taux d’apprentissage : utiliser un taux \(10\times\) à \(100\times\) plus faible que pour un entraînement de zéro (typiquement \(10^{-4}\) à \(10^{-5}\) pour le fine-tuning).

  2. Taux d’apprentissage discriminatif : utiliser un taux plus faible pour les premières couches et plus élevé pour les dernières :

    \[\eta_l = \eta_{\text{base}} \cdot \gamma^{L - l}\]

    \(l\) est l’indice de la couche, \(L\) le nombre total de couches, et \(\gamma < 1\).

  3. Normalisation des entrées : appliquer exactement la même normalisation que celle utilisée lors du pré-entraînement (moyenne et écart-type d’ImageNet).

  4. Taille du jeu de données :

    • \(< 1\,000\) images : extraction de caractéristiques.

    • \(1\,000\)\(10\,000\) images : fine-tuning des dernières couches.

    • \(> 10\,000\) images : fine-tuning complet.

  5. Augmentation de données : toujours appliquer de l’augmentation, particulièrement lorsque les données sont limitées.

Hide code cell source

# Exemple : fine-tuning progressif (illustration du dégel par couches)
from torchvision.models import efficientnet_b0, EfficientNet_B0_Weights

model_ft = efficientnet_b0(weights=EfficientNet_B0_Weights.IMAGENET1K_V1)

# Phase 1 : geler tout, entraîner la tête
for param in model_ft.parameters():
    param.requires_grad = False

model_ft.classifier[1] = nn.Linear(model_ft.classifier[1].in_features, 5)

phase1_trainable = sum(p.numel() for p in model_ft.parameters() if p.requires_grad)

# Phase 2 : dégeler les derniers blocs du backbone
for param in model_ft.features[-2:].parameters():
    param.requires_grad = True

phase2_trainable = sum(p.numel() for p in model_ft.parameters() if p.requires_grad)

# Phase 3 : dégeler tout
for param in model_ft.parameters():
    param.requires_grad = True

phase3_trainable = sum(p.numel() for p in model_ft.parameters() if p.requires_grad)

total_params = sum(p.numel() for p in model_ft.parameters())

print("Fine-tuning progressif (EfficientNet-B0) :")
print(f"  Phase 1 (tête seule)      : {phase1_trainable:>10,} paramètres "
      f"({phase1_trainable/total_params*100:.1f}%)")
print(f"  Phase 2 (derniers blocs)  : {phase2_trainable:>10,} paramètres "
      f"({phase2_trainable/total_params*100:.1f}%)")
print(f"  Phase 3 (tout le modèle)  : {phase3_trainable:>10,} paramètres "
      f"({phase3_trainable/total_params*100:.1f}%)")
Fine-tuning progressif (EfficientNet-B0) :
  Phase 1 (tête seule)      :      6,405 paramètres (0.2%)
  Phase 2 (derniers blocs)  :  1,135,797 paramètres (28.3%)
  Phase 3 (tout le modèle)  :  4,013,953 paramètres (100.0%)

Augmentation de données avancée#

L’augmentation de données est une technique de régularisation qui crée artificiellement de nouvelles données d’entraînement en appliquant des transformations aux images existantes. Au-delà des transformations géométriques classiques (rotation, retournement, recadrage), les techniques modernes d’augmentation mélangent les exemples entre eux ou modifient les images de manière plus agressive.

CutMix et MixUp#

Définition 302 (CutMix et MixUp)

MixUp (Zhang et al., 2018) : mélange linéaire de deux exemples et de leurs étiquettes :

\[\tilde{x} = \lambda x_i + (1 - \lambda) x_j\]
\[\tilde{y} = \lambda y_i + (1 - \lambda) y_j\]

\(\lambda \sim \text{Beta}(\alpha, \alpha)\), typiquement \(\alpha = 0.2\).

CutMix (Yun et al., 2019) : coupe une région rectangulaire d’une image et la remplace par la région correspondante d’une autre image. L’étiquette est pondérée par la proportion de surface :

\[\tilde{x} = \mathbf{M} \odot x_i + (1 - \mathbf{M}) \odot x_j\]
\[\tilde{y} = \lambda y_i + (1 - \lambda) y_j\]

\(\mathbf{M} \in \{0, 1\}^{H \times W}\) est un masque binaire rectangulaire et \(\lambda\) est la proportion de l’image originale conservée.

Hide code cell source

# Illustration : MixUp et CutMix
np.random.seed(42)

img1 = np.random.rand(64, 64, 3) * 0.5 + np.array([0.7, 0.3, 0.2])
img1 = np.clip(img1, 0, 1)
img2 = np.random.rand(64, 64, 3) * 0.5 + np.array([0.2, 0.3, 0.7])
img2 = np.clip(img2, 0, 1)

# MixUp
lam_mixup = 0.6
img_mixup = lam_mixup * img1 + (1 - lam_mixup) * img2

# CutMix
lam_cutmix = 0.6
img_cutmix = img1.copy()
cut_h = int(64 * np.sqrt(1 - lam_cutmix))
cut_w = int(64 * np.sqrt(1 - lam_cutmix))
cx, cy = 32, 32
x1c = max(cx - cut_w // 2, 0)
y1c = max(cy - cut_h // 2, 0)
x2c = min(cx + cut_w // 2, 64)
y2c = min(cy + cut_h // 2, 64)
img_cutmix[y1c:y2c, x1c:x2c] = img2[y1c:y2c, x1c:x2c]

fig, axes = plt.subplots(2, 2, figsize=(10, 9))
for ax, img, title in zip(axes.flatten(),
    [img1, img2, img_mixup, img_cutmix],
    ["Image A (classe : chat)", "Image B (classe : chien)",
     f"MixUp (λ={lam_mixup})\ny = 0.6·chat + 0.4·chien",
     f"CutMix (λ≈{lam_cutmix})\ny ≈ 0.6·chat + 0.4·chien"]):
    ax.imshow(img)
    ax.set_title(title, fontsize=10)
    ax.set_xticks([]); ax.set_yticks([])

plt.tight_layout()
plt.show()
_images/c1026630cef43172eaf049099e82e27cf1a1ffa8c4f97cdafea6aefc732ee1ac.png

RandAugment#

Définition 303 (RandAugment)

RandAugment (Cubuk et al., 2020) simplifie la recherche de politiques d’augmentation en n’utilisant que deux hyperparamètres :

  • \(N\) : le nombre de transformations appliquées séquentiellement,

  • \(M\) : la magnitude (intensité) commune de toutes les transformations.

À chaque image, \(N\) transformations sont tirées uniformément parmi un ensemble prédéfini (rotation, translation, cisaillement, contraste, luminosité, saturation, etc.), chacune avec une magnitude \(M\) sur une échelle de 0 à 30.

Cela réduit l’espace de recherche de \(O(K^N \cdot M^N)\) (pour \(K\) transformations) à seulement deux paramètres scalaires \((N, M)\), rendant l’optimisation par validation croisée triviale.

Hide code cell source

# Illustration de différentes magnitudes de RandAugment
from torchvision.transforms import v2

# Créer une image de test
torch.manual_seed(42)
test_image = torch.rand(3, 128, 128)

fig, axes = plt.subplots(2, 2, figsize=(10, 9))
axes_flat = axes.flatten()

# Image originale
axes_flat[0].imshow(test_image.permute(1, 2, 0).numpy())
axes_flat[0].set_title("Image originale", fontsize=11)
axes_flat[0].set_xticks([]); axes_flat[0].set_yticks([])

# RandAugment avec différentes magnitudes
for idx, (M, ax) in enumerate(zip([5, 15, 25], axes_flat[1:])):
    transform = v2.RandAugment(num_ops=2, magnitude=M)
    augmented = transform(test_image)
    ax.imshow(augmented.permute(1, 2, 0).clamp(0, 1).numpy())
    ax.set_title(f"RandAugment (N=2, M={M})", fontsize=11)
    ax.set_xticks([]); ax.set_yticks([])

plt.tight_layout()
plt.show()
_images/9bce5a94beeda7cc94f37495b8773c99c1c3f051ebc2297b6679a1125d877aac.png

Remarque 251

Les techniques d’augmentation avancées comme CutMix, MixUp et RandAugment sont devenues des composantes standard des recettes d’entraînement modernes. Par exemple, la recette d’entraînement de DeiT (Touvron et al., 2021) utilise simultanément RandAugment, MixUp, CutMix et Random Erasing. Ces techniques sont complémentaires et leur combinaison améliore systématiquement les performances.

Applications#

La vision par ordinateur avancée trouve des applications dans de nombreux domaines à fort impact sociétal et industriel.

Imagerie médicale#

Exemple 37 (Vision par ordinateur en imagerie médicale)

L’imagerie médicale est l’un des domaines où la vision par ordinateur a le plus grand potentiel :

  • Détection de tumeurs : les CNN et U-Net segmentent les tumeurs dans les scanners (IRM, CT, mammographies) avec une précision parfois comparable à celle des radiologues experts.

  • Rétinopathie diabétique : les modèles de classification analysent les images du fond de l’oeil pour détecter les signes précoces de rétinopathie, permettant un dépistage à grande échelle.

  • Pathologie numérique : les modèles analysent les lames histologiques numérisées pour détecter les cellules cancéreuses, avec des résolutions gigapixels traitées par des architectures à attention multi-échelle.

  • Reconstruction d’images : les réseaux profonds améliorent la résolution des images IRM acquises rapidement, réduisant le temps d’examen pour les patients.

Le transfert d’apprentissage est particulièrement crucial dans ce domaine, car les données annotées par des experts sont rares et coûteuses.

Conduite autonome#

Exemple 38 (Vision par ordinateur pour la conduite autonome)

Un véhicule autonome doit percevoir et comprendre son environnement en temps réel :

  • Détection d’objets : identifier les véhicules, piétons, cyclistes, panneaux de signalisation et feux tricolores avec une latence minimale. Les détecteurs à une étape (YOLO) sont préférés pour leur vitesse.

  • Segmentation sémantique : segmenter la scène en route, trottoir, bâtiments, végétation pour comprendre l’espace navigable.

  • Estimation de profondeur : les réseaux monoculaires estiment la profondeur à partir d’une seule image, complétant les données LiDAR.

  • Fusion multi-capteurs : combiner les données des caméras, du LiDAR et du radar à l’aide de réseaux à fusion tardive ou intermédiaire.

La segmentation panoptique est particulièrement pertinente car elle fournit une compréhension complète de la scène : chaque pixel est classifié et chaque instance d’objet est individualisée.

Imagerie satellitaire et télédétection#

Exemple 39 (Imagerie satellitaire)

L’analyse automatique d’images satellitaires ouvre des perspectives considérables :

  • Cartographie : segmentation sémantique des bâtiments, routes, cours d’eau et végétation pour la mise à jour automatique des cartes.

  • Surveillance environnementale : détection de la déforestation, suivi de la fonte des glaces, estimation des surfaces agricoles.

  • Gestion des catastrophes : détection rapide des zones inondées ou des bâtiments endommagés après un séisme ou un ouragan.

  • Agriculture de précision : analyse de l’état des cultures par segmentation des parcelles et détection des zones de stress hydrique.

Les défis spécifiques incluent la très haute résolution des images (souvent plusieurs gigapixels), les variations d’illumination et d’angle de prise de vue, et le déséquilibre des classes.

Contrôle qualité industriel#

Exemple 40 (Contrôle qualité en vision industrielle)

Dans l’industrie manufacturière, la vision par ordinateur permet l’inspection automatique :

  • Détection de défauts : identifier les rayures, fissures, taches ou déformations sur les pièces manufacturées à l’aide de la détection d’anomalies ou de la segmentation.

  • Métrologie : mesurer les dimensions des pièces avec une précision sub-pixel.

  • Tri automatique : classifier les produits par qualité sur les lignes de production en temps réel.

  • Inspection de soudures : vérifier la qualité des soudures par analyse d’images radiographiques.

Les approches par détection d’anomalies sont particulièrement adaptées car les données de défauts sont rares : on entraîne le modèle uniquement sur des exemples conformes et on détecte les anomalies comme des déviations de la distribution normale.

Hide code cell source

# Chronologie des avancées en vision par ordinateur
fig, ax = plt.subplots(figsize=(15, 4))

events = [
    (2012, "AlexNet\n(CNN profond)", '#4C72B0'),
    (2014, "R-CNN / VGG\nGoogLeNet", '#DD8452'),
    (2015, "ResNet\nU-Net\nFaster R-CNN", '#55A868'),
    (2016, "YOLO", '#C44E52'),
    (2017, "Mask R-CNN\nMobileNet", '#8172B3'),
    (2019, "EfficientNet", '#937860'),
    (2020, "ViT\nDETR", '#DA8BC3'),
    (2021, "Swin\nDeiT", '#8C8C8C'),
    (2022, "ConvNeXt\nSAM", '#CCB974'),
    (2023, "DINOv2\nSegment\nAnything", '#64B5CD'),
]

ax.set_xlim(2011, 2024.5)
ax.set_ylim(-1.5, 3)
ax.axis('off')
ax.set_title("Chronologie des avancées majeures en vision par ordinateur", fontsize=13, pad=15)
ax.axhline(y=0, color='gray', linewidth=2, alpha=0.3, xmin=0.02, xmax=0.98)

for year, label, color in events:
    ax.plot(year, 0, 'o', color=color, markersize=12, zorder=5)
    ax.annotate(f"{year}\n{label}", xy=(year, 0), xytext=(year, 0.6),
                fontsize=7.5, ha='center', va='bottom', color=color,
                arrowprops=dict(arrowstyle='-', color=color, alpha=0.5))

plt.tight_layout()
plt.show()
_images/2f3bd6f3f06fe08cc5719ba7c0ebb35c3fda92b436ddd59298534e65d4992a2b.png

Résumé#

Ce chapitre a étendu notre compréhension de la vision par ordinateur au-delà de la classification, en explorant les tâches de prédiction dense et les architectures modernes.

  1. La détection d’objets localise et identifie les objets dans une image. Les détecteurs à deux étapes (Faster R-CNN) offrent une grande précision grâce aux propositions de régions et aux ancres, tandis que les détecteurs à une étape (YOLO) privilégient la vitesse par une prédiction directe sur une grille. L”IoU et le mAP sont les métriques standard, et la NMS élimine les détections redondantes.

  2. La segmentation attribue une étiquette à chaque pixel. L’architecture U-Net (encodeur-décodeur avec connexions de saut) est la référence pour la segmentation sémantique. Mask R-CNN étend la détection à la segmentation d’instances en ajoutant une branche de masque. La Dice loss complète l’entropie croisée pour les classes déséquilibrées.

  3. Les Vision Transformers (ViT) appliquent l’architecture Transformer aux images via un découpage en patches. Ils surpassent les CNN lorsqu’ils sont pré-entraînés sur de grands jeux de données, grâce à leur attention globale. Les architectures hybrides (Swin Transformer, ConvNeXt) combinent les avantages des deux approches.

  4. Le transfert d’apprentissage — par extraction de caractéristiques ou fine-tuning — est essentiel en pratique. Le choix du backbone pré-entraîné et de la stratégie de dégel dépend de la taille du jeu de données cible.

  5. Les techniques d”augmentation avancée (MixUp, CutMix, RandAugment) sont devenues des composantes standard des pipelines d’entraînement modernes, améliorant la robustesse et la généralisation.

  6. Les applications de la vision avancée — imagerie médicale, conduite autonome, télédétection, contrôle qualité — illustrent l’impact considérable de ces techniques dans le monde réel.

Remarque 252

La vision par ordinateur évolue rapidement. Les modèles de fondation comme SAM (Segment Anything Model, Kirillov et al., 2023) et DINOv2 montrent qu’un unique modèle pré-entraîné peut être adapté à une grande variété de tâches visuelles, souvent avec très peu de données annotées. Cette tendance vers des modèles généralistes, combinée à l’intégration de la vision et du langage (vision-language models), dessine un avenir où la compréhension visuelle automatique se rapproche de la polyvalence humaine.