IA de vision par ordinateur
De l'information que les humains acquièrent, environ 80% provient de la vision.
Quand vous voyez une photo, vous pouvez immédiatement reconnaître combien de personnes s'y trouvent, ce qu'elles font, et si l'arrière-plan est à l'intérieur ou à l'extérieur.
Mais pour un ordinateur, cette photo n'est qu'un tas de nombres — chaque pixel est représenté par trois valeurs pour le rouge, le vert et le bleu, et c'est tout.
La vision par ordinateur (CV) est la technologie qui permet aux ordinateurs de comprendre les images.
Du déverrouillage facial sur les téléphones à la reconnaissance de l'état de la route en conduite autonome, et à l'analyse des lésions en imagerie médicale, la vision par ordinateur est devenue omniprésente.
Ce module vous fera passer des réseaux de neurones convolutifs de base jusqu'aux derniers modèles vision-langage.
Parcours d'apprentissage : Réseaux de neurones convolutifs → Vision Transformer → Détection d'objets → Segmentation d'images → Modèles de diffusion → CLIP → Modèles vision-langage. Chaque étape est accompagnée d'exemples de code exécutables.
Réseaux de neurones convolutifs (CNN)
Le CNN est une technologie fondamentale en vision par ordinateur. Il imite le fonctionnement du cortex visuel humain, en extrayant les caractéristiques des images via des champs récepteurs locaux.
Principes de l'opération de convolution
L'idée centrale de la convolution est :Utiliser une petite fenêtre glissante (noyau de convolution) pour parcourir l'image et extraire des caractéristiques locales.。
Par exemple, un noyau 3×3 examine 9 pixels à la fois, calcule leur somme pondérée et produit une valeur de sortie.
Ce processus est répété ; le noyau glisse sur l'image de gauche à droite et de haut en bas, générant finalement une « carte de caractéristiques ».
Exemple
# Implémenter l'opération de convolution la plus simple avec NumPy
# Démontrer comment la convolution extrait les caractéristiques de contour
# ============================================
import numpy as np
def simple_convolution(image: np.ndarray, kernel: np.ndarray) -> np.ndarray:
"""
Implémentez l'opération de convolution 2D la plus basique (sans padding ni stride)
Paramètres :
image : image d'entrée (H, W), niveaux de gris monocanal
kernel : noyau de convolution (kH, kW)
Retourne :
Carte de caractéristiques après convolution
"""
# Obtenir les dimensions de l'image et du noyau
img_h, img_w = image.shape
kernel_h, kernel_w = kernel.shape
# Calculer la taille de la carte de caractéristiques de sortie
# Taille de sortie = taille d'entrée - taille du noyau + 1
out_h = img_h - kernel_h + 1
out_w = img_w - kernel_w + 1
# Initialiser la carte de caractéristiques de sortie
output = np.zeros((out_h, out_w))
# Faire glisser le noyau de convolution pour calculer
for i in range(out_h):
for j in range(out_w):
# Extraire la région locale de l'image correspondant au noyau
region = image[i:i+kernel_h, j:j+kernel_w]
# Multiplier les éléments correspondants et faire la somme (c'est l'opération de convolution)
output[i, j] = np.sum(region * kernel)
return output
# Créer une image de test simple : un carré blanc au milieu, du noir autour
# Forme : image en niveaux de gris 8×8
test_image = np.array([
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
])
print("Image originale :")
print(test_image)
# Définir un noyau de détection de bords (opérateur de Sobel simplifié)
# Ce noyau peut détecter les bords verticaux
edge_kernel = np.array([
[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1],
])
# Effectuer la convolution
feature_map = simple_convolution(test_image, edge_kernel)
print("\nCarte de caractéristiques après convolution (bords verticaux détectés) :")
print(np.round(feature_map, 2))
La clé de l'opération de convolution est :Les paramètres du noyau de convolution sont appris, et non conçus manuellement.。
Pendant l'entraînement, le modèle ajuste automatiquement les valeurs du noyau, ce qui lui permet d'extraire des caractéristiques utiles pour la tâche.
Agrégation
L'agrégation compresse la taille de la carte de caractéristiques, réduit le calcul, tout en préservant les caractéristiques importantes.
Le plus couramment utilisé est l'agrégation maximale : divisez la carte de caractéristiques en plusieurs petits blocs, et chaque bloc ne conserve que la valeur maximale.
Exemple
# Implémenter l'opération d'agrégation maximale
# ============================================
def max_pooling(feature_map: np.ndarray, pool_size: int = 2) -> np.ndarray:
"""
Implémenter l'opération d'agrégation maximale
Paramètres :
feature_map: carte de caractéristiques d'entrée (H, W)
pool_size: taille de la fenêtre d'agrégation, par défaut 2×2
Retourne :
La carte de caractéristiques après agrégation
"""
h, w = feature_map.shape
# Calculer la taille de sortie
out_h = h // pool_size
out_w = w // pool_size
output = np.zeros((out_h, out_w))
for i in range(out_h):
for j in range(out_w):
# Extraire la région de la fenêtre de pooling
region = feature_map[
i*pool_size:(i+1)*pool_size,
j*pool_size:(j+1)*pool_size
]
# Prendre la valeur maximale
output[i, j] = np.max(region)
return output
# Effectuer le pooling en utilisant la carte de caractéristiques précédente
print("Carte de caractéristiques avant le pooling :")
print(feature_map)
pooled = max_pooling(feature_map, pool_size=2)
print("\n"Carte de caractéristiques après max pooling 2×2 :")
print(pooled)
Comparaison des architectures classiques
Dans l'histoire du développement des CNN, il existe plusieurs architectures phares, chacune représentant des idées de conception de différentes périodes.
| Architecture | Année | Innovation clé | Caractéristiques |
|---|---|---|---|
| AlexNet | 2012 | Activation ReLU, Dropout, augmentation de données | Le pionnier de l'ère de l'apprentissage profond, premier à surpasser significativement les méthodes traditionnelles sur ImageNet. |
| VGG | 2014 | Utilisation uniforme de petits noyaux de convolution 3×3 | Structure simple et élégante, facile à comprendre et à implémenter |
| ResNet | 2015 | Connexion résiduelle (Connexion de saut) | Résout le problème de la disparition du gradient dans les réseaux profonds, permettant aux réseaux d'avoir des centaines de couches. |
| EfficientNet | 2019 | Méthode de mise à l'échelle composée | Met à l'échelle la profondeur, la largeur et la résolution simultanément, avec une efficacité paramétrique extrêmement élevée. |
Connexion résiduelle (Connexion de saut)
L'innovation centrale de ResNet est la connexion résiduelle, qui résout le problème selon lequel « plus le réseau est profond, plus il est difficile à entraîner ».
L'objectif d'apprentissage d'une couche de réseau traditionnelle est d'apprendre directement l'application qui associe l'entrée x à la sortie y.
L'objectif d'apprentissage d'un réseau résiduel est :Apprendre la différence (le résidu) entre la sortie y et l'entrée x.。
Formule : y = F(x) + x, où F(x) est le résidu à apprendre.
Intuition de la connexion résiduelle : laisser le réseau apprendre « combien changer en fonction de l'état existant » est bien plus facile que de le laisser « apprendre l'application complète à partir de zéro ».
Exemple
# Implémenter un classificateur d'images CNN complet avec PyTorch
# Inclut la convolution, le pooling, les connexions résiduelles
# ============================================
import torch
import torch.nn as nn
import torch.nn.functional as F
class ResidualBlock(nn.Module):
"""Un bloc résiduel simple"""
def __init__(self, in_channels: int, out_channels: int, stride: int = 1):
super().__init__()
# Première couche de convolution
self.conv1 = nn.Conv2d(
in_channels, out_channels,
kernel_size=3, stride=stride, padding=1, bias=False
)
self.bn1 = nn.BatchNorm2d(out_channels)
# Deuxième couche convolutionnelle
self.conv2 = nn.Conv2d(
out_channels, out_channels,
kernel_size=3, stride=1, padding=1, bias=False
)
self.bn2 = nn.BatchNorm2d(out_channels)
# Connexion de raccourci (pour correspondre aux changements de dimension)
self.shortcut = nn.Sequential()
if stride != 1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(
in_channels, out_channels,
kernel_size=1, stride=stride, bias=False
),
nn.BatchNorm2d(out_channels)
)
def forward(self, x: torch.Tensor) -> torch.Tenseur:
# Chemin principal : conv → BN → ReLU → conv → BN
out = F.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
# Connexion résiduelle : ajouter l'entrée (ou l'entrée après transformation de dimension)
out += self.shortcut(x)
# ReLU final
out = F.relu(out)
return out
class SimpleCNN(nn.Module):
"""CNN simple pour la classification d'images (avec connexions résiduelles)"""
def __init__(self, num_classes: int = 10):
super().__init__()
# Couche convolutionnelle initiale
self.conv1 = nn.Conv2d(3, 32, kernel_size=3, stride=1, padding=1)
self.bn1 = nn.BatchNorm2d(32)
# Couche résiduelle
self.layer1 = ResidualBlock(32, 32)
self.layer2 = ResidualBlock(32, 64, stride=2)
self.layer3 = ResidualBlock(64, 64)
# Pooling moyen global
self.global_pool = nn.AdaptiveAvgPool2d((1, 1))
# Tête de classification
self.fc = nn.Linear(64, num_classes)
def forward(self, x: torch.Tensor) -> torch.Tenseur:
# forme de x: (batch_size, 3, 32, 32)
x = F.relu(self.bn1(self.conv1(x)))
x = self.layer1(x) # (batch_size, 32, 32, 32)
x = self.layer2(x) # (batch_size, 64, 16, 16)
x = self.layer3(x) # (batch_size, 64, 16, 16)
x = self.global_pool(x) # (batch_size, 64, 1, 1)
x = x.view(x.size(0), -1) # (batch_size, 64)
x = self.fc(x) # (batch_size, num_classes)
return x
# Créer le modèle et tester
model = SimpleCNN(num_classes=10)
print("Structure du modèle:")
print(model)
# Test de propagation avant
# Créer un lot factice : 4 images, chacune 3×32×32 (RVB 32×32)
test_input = torch.randn(4, 3, 32, 32)
test_output = model(test_input)
print(f"\nForme de l'entrée : {test_input.shape}")
print(f"Forme de sortie : {test_output.shape} (batch_size, num_classes)")
Vision Transformer(ViT)
En 2020, Google a publié l'article "An Image is Worth 16x16 Words", faisant passer l'architecture Transformer du domaine du langage à celui de la vision.
Découpage d'image (Patch Embedding)
Les Transformers traitent des données séquentielles, mais les images sont des données de grille 2D.
La solution de ViT est simple :Découper l'image en petits patchs et traiter chaque patch comme un mot。
Par exemple, une image de 224×224 découpée en patchs de 16×16 donne 14×14 = 196 patchs.
Chaque patch est aplati en un vecteur 1D et projeté dans un plongement via une couche linéaire, qui peut ensuite être fourni au Transformer.
Exemple
# Implémenter le plongement de patchs (découpage d'image)
# ============================================
class PatchEmbedding(nn.Module):
"""
Segmenter l'image en patchs et les intégrer
Paramètres :
img_size: taille de l'image d'entrée (carrée)
patch_size: taille de chaque patch (carré)
in_channels: nombre de canaux d'entrée (3 pour RVB)
embed_dim: dimension du plongement de sortie
"""
def __init__(
self,
img_size: int = 224,
patch_size: int = 16,
in_channels: int = 3,
embed_dim: int = 768
):
super().__init__()
self.img_size = img_size
self.patch_size = patch_size
# Calculer le nombre de patchs : (224/16)^2 = 14^2 = 196
self.num_patches = (img_size // patch_size) ** 2
# Utiliser une convolution pour implémenter le partitionnement et l'embedding des patchs
# Ceci équivaut à : découper les patchs → aplatir → projection linéaire
self.proj = nn.Conv2d(
in_channels, embed_dim,
kernel_size=patch_size, stride=patch_size
)
def forward(self, x: torch.Tensor) -> torch.Tenseur:
# forme de x : (batch_size, in_channels, img_size, img_size)
x = self.proj(x) # (batch_size, embed_dim, num_patches^0.5, num_patches^0.5)
x = x.flatten(2) # (batch_size, embed_dim, num_patches)
x = x.transpose(1, 2) # (batch_size, num_patches, embed_dim)
return x
# Test de l'embedding de patchs
patch_embed = PatchEmbedding(img_size=224, patch_size=16, embed_dim=768)
test_image = torch.randn(1, 3, 224, 224) # 1 image RGB de 224×224
patches = patch_embed(test_image)
print(f"Forme de l'image d'entrée : {test_image.shape}")
print(f"Forme de la séquence de patchs : {patches.shape} (batch_size, num_patches, embed_dim)")
Architecture complète du ViT
L'architecture complète du ViT nécessite également l'ajout d'un token de classe et d'un encodage positionnel.
Le token de classe est un vecteur apprenable qui sert à "agréger" les informations de tous les patchs.
L'encodage positionnel est également apprenable ; il indique au modèle « où chaque patch est situé dans l'image ».
Exemple
# Implémenter un Vision Transformer simplifié
# ============================================
class MultiHeadAttention(nn.Module):
"""Auto-attention multi-têtes"""
def __init__(self, dim: int, num_heads: int):
super().__init__()
self.num_heads = num_heads
self.head_dim = dim // num_heads
assert self.head_dim * num_heads == dim, "La dimension doit être divisible par le nombre de têtes"
# Matrices de projection Q, K, V
self.qkv = nn.Linear(dim, dim * 3)
# Projection de sortie
self.proj = nn.Linear(dim, dim)
def forward(self, x: torch.Tensor) -> torch.Tenseur:
batch_size, seq_len, dim = x.shape
# Calculer Q, K, V
qkv = self.qkv(x) # (batch_size, seq_len, dim*3)
qkv = qkv.reshape(
batch_size, seq_len, 3, self.num_heads, self.head_dim
) # (batch_size, seq_len, 3, num_heads, head_dim)
q, k, v = qkv.unbind(2) # Chaque forme : (batch_size, seq_len, num_heads, head_dim)
# Réorganiser les dimensions pour calculer l'attention
q = q.transpose(1, 2) # (batch_size, num_heads, seq_len, head_dim)
k = k.transpose(1, 2)
v = v.transpose(1, 2)
# Calculer les scores d'attention
attention = q @ k.transpose(-2, -1) / (self.head_dim ** 0.5)
attention = attention.softmax(dim=-1)
# Agréger les valeurs
out = attention @ v # (batch_size, num_heads, seq_len, head_dim)
out = out.transpose(1, 2) # (batch_size, seq_len, num_heads, head_dim)
out = out.flatten(2) # (batch_size, seq_len, dim)
# Projection de sortie
out = self.proj(out)
return out
class TransformerBlock(nn.Module):
"""Un bloc Transformer"""
def __init__(self, dim: int, num_heads: int, mlp_ratio: float = 4.0):
super().__init__()
self.norm1 = nn.LayerNorm(dim)
self.attn = MultiHeadAttention(dim, num_heads)
self.norm2 = nn.LayerNorm(dim)
# MLP
mlp_hidden = int(dim * mlp_ratio)
self.mlp = nn.Sequential(
nn.Linear(dim, mlp_hidden),
nn.GELU(),
nn.Linear(mlp_hidden, dim)
)
def forward(self, x: torch.Tensor) -> torch.Tenseur:
# Connexion résiduelle 1
x = x + self.attn(self.norm1(x))
# Connexion résiduelle 2
x = x + self.mlp(self.norm2(x))
return x
class SimpleViT(nn.Module):
"""Vision Transformer simplifié"""
def __init__(
self,
img_size: int = 224,
patch_size: int = 16,
in_channels: int = 3,
num_classes: int = 10,
embed_dim: int = 192,
depth: int = 6,
num_heads: int = 6
):
super().__init__()
# Intégration des patchs
self.patch_embed = PatchEmbedding(
img_size, patch_size, in_channels, embed_dim
)
num_patches = self.patch_embed.num_patches
# Jeton de classe
self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim))
# Encodage positionnel
self.pos_embed = nn.Parameter(
torch.zeros(1, num_patches + 1, embed_dim)
)
# Couches Transformer
self.blocks = nn.ModuleList([
TransformerBlock(embed_dim, num_heads)
for _ in range(depth)
])
# LayerNorm final et tête de classification
self.norm = nn.LayerNorm(embed_dim)
self.head = nn.Linear(embed_dim, num_classes)
def forward(self, x: torch.Tensor) -> torch.Tenseur:
batch_size = x.shape[0]
# Intégration des patchs
x = self.patch_embed(x) # (batch_size, num_patches, embed_dim)
# Ajouter le token de classe
cls_tokens = self.cls_token.expand(batch_size, -1, -1)
x = torch.cat((cls, x), dim=1) # (batch_size, 1+num_patches, embed_dim)
# Ajouter un encodage positionnel
x = x + self.pos_embed
# Passer par les couches Transformer
for block in self.blocks:
x = block(x)
# Utiliser uniquement le token de classe pour la classification
x = self.norm(x)
cls_output = x[:, 0] # Prendre la première position (token de classe)
x = self.head(cls_output)
return x
# Créer le modèle ViT et tester
vit = SimpleViT(
img_size=32, # Utiliser des images plus petites pour faciliter les tests
patch_size=8, # patch 8×8
embed_dim=128,
depth=4,
num_heads=4,
num_classes=10
)
print("Architecture ViT simplifiée :")
print(vit)
test_input = torch.randn(2, 3, 32, 32) # 2 images 32×32
test_output = vit(test_input)
print(f"\nForme de l'entrée : {test_input.shape}")
print(f"Forme de sortie : {test_output.shape}")
Comparaison CNN vs ViT
Deux architectures ont chacune leurs avantages et inconvénients ; le choix dépend de votre scénario spécifique.
| Caractéristiques | CNN | ViT |
|---|---|---|
| Biais inductif | Fort (localité, invariance par translation) | Faible (apprend à partir des données) |
| Besoins en données | Fonctionne avec de petites quantités de données | Nécessite de grandes quantités de données pour montrer ses avantages |
| Efficacité de calcul | Généralement plus rapide, optimisation matérielle mature | Nombre élevé de paramètres, gourmand en calcul |
| Interprétabilité | Noyaux de convolution faciles à visualiser | Les cartes d'attention sont interprétables, mais le modèle global est plus difficile à interpréter |
| Dépendances à longue portée | Nécessitent plusieurs couches pour être établies | Prend naturellement en charge le champ récepteur global |
| Scénarios applicables | Données limitées, exigences de temps réel élevées | Grands ensembles de données, tâches visuelles complexes |
Règle empirique : utilisez CNN (ResNet, EfficientNet) lorsque les données sont limitées ; envisagez ViT lorsque les données atteignent des millions ou plus. Il existe également des architectures hybrides (telles que ConvNeXt) qui combinent les avantages des deux.
Détection d'objets
La classification d'images répond à « qu'y a-t-il dans cette image », tandis que la détection d'objets répond à « qu'y a-t-il dans cette image et où se trouvent les objets ».
La sortie de la détection d'objets est une série de boîtes englobantes, chacune correspondant à une catégorie d'objet.
Détection en deux étapes : série R-CNN
L'idée de la détection en deux étapes est :Trouver d'abord des régions susceptibles de contenir des objets, puis classer ces régions。
Première étape : générer des propositions de régions, éventuellement des centaines de boîtes indiquant « quelque chose pourrait se trouver ici. »
Deuxième étape : extraire les caractéristiques de chaque région candidate, déterminer de quel objet il s'agit et ajuster la position de la boîte.
Évolution de la famille R-CNN :
| Modèle | Améliorations clés | Vitesse |
|---|---|---|
| R-CNN | Remplace les caractéristiques traditionnelles par CNN | Lent (dizaines de secondes par image) |
| Fast R-CNN | ROI Pooling, calcul partagé des caractéristiques | Moyen (quelques secondes par image) |
| Faster R-CNN | RPN(Region Proposal Network) | Rapide (des centaines de millisecondes par image) |
| Mask R-CNN | Ajoute une branche de segmentation | Moyenne |
Détection en une étape : série YOLO
L'idée de la détection en une étape est :Effectuer directement une prédiction dense sur l'image sans générer de régions candidates séparément。
YOLO (You Only Look Once) est un représentant de la détection en une étape, extrêmement rapide et adapté aux applications en temps réel.
YOLO divise l'image en une grille S×S, chaque cellule de la grille étant responsable de la prédiction des objets dont le point central tombe dans cette cellule.
Chaque cellule de la grille prédit : B boîtes englobantes (avec confiance) et les probabilités pour C classes.
Détection de bout en bout : DETR
DETR (DEtection TRansformer) est un nouveau paradigme proposé par Facebook AI, utilisant Transformer pour transformer la détection d'objets en un problème de prédiction directe de séquence.
Sa plus grande caractéristique est : pas besoin de suppression non maximale (NMS), pas besoin de boîtes d'ancrage (Anchor), la sortie est le résultat final.
Pratique : Détection d'objets YOLOv11
Nous utilisons maintenant Ultralytics YOLOv11 pour créer un exemple complet de détection d'objets.
Exemple
# Détection d'objets YOLOv11 en pratique
# Inclut le chargement du modèle, la détection, la visualisation
# ============================================
import torch
import numpy as np
from PIL import Image, ImageDraw, ImageFont
import matplotlib.pyplot as plt
class SimpleYOLO:
"""Démo YOLO simplifiée (avec modèle pré-entraîné)"""
def __init__(self, model_name: str = "yolo11n.pt"):
"""
Initialise le modèle YOLO
Paramètres :
model_name: nom du modèle, yolo11n.pt (nano, small), yolo11s.pt (small), etc.
"""
self.model_name = model_name
self.model = None
# 80 catégories du jeu de données COCO
self.coco_names = [
'personne', 'vélo', 'voiture', 'moto', 'avion', 'bus',
'train', 'camion', 'bateau', 'feu de circulation', 'borne d'incendie',
'panneau stop', 'parcmètre', 'banc', 'oiseau', 'chat', 'chien',
'cheval', 'mouton', 'vache', 'éléphant', 'ours', 'zèbre', 'girafe',
'sac à dos', 'parapluie', 'sac à main', 'cravate', 'valise', 'frisbee',
'skis', 'snowboard', 'ballon de sport', 'cerf-volant', 'batte de baseball',
'gant de baseball', 'skateboard', 'planche de surf', 'raquette de tennis',
'bouteille', 'verre à vin', 'tasse', 'fourchette', 'couteau', 'cuillère', 'bol',
'banane', 'pomme', 'sandwich', 'orange', 'brocoli', 'carotte',
'hot-dog', 'pizza', 'donut', 'gâteau', 'chaise', 'canapé',
'plante en pot', 'lit', 'table à manger', 'toilettes', 'télé', 'ordinateur portable',
'souris', 'télécommande', 'clavier', 'téléphone portable', 'micro-ondes', 'four',
'grille-pain', 'évier', 'réfrigérateur', 'livre', 'horloge', 'vase',
'ciseaux', 'ours en peluche', 'sèche-cheveux', 'brosse à dents'
]
# Couleur pour chaque catégorie
self.colors = np.random.randint(0, 255, (80, 3))
def load_model(self):
"""Charger le modèle pré-entraîné (ici en utilisant une implémentation simulée pour démontrer le concept)"""
print(f"Chargement du modèle {self.model_name}...")
print("Remarque : En utilisation réelle, veuillez installer ultralytics et appeler :")
print(" from ultralytics import YOLO")
print(" model = YOLO('yolo11n.pt')")
self.model = "pretrained_model_loaded"
print("Modèle chargé avec succès !")
return self
def predict_dummy(self, image: np.ndarray) -> list:
"""
Résultats de détection simulés (pour démonstration)
En utilisation réelle, appelez model(image)
Retourne :
Liste des résultats de détection, chaque élément est un dictionnaire :
{'bbox': [x1, y1, x2, y2], 'class_id': int, 'score': float, 'class_name': str}
"""
# Ici, nous simulons la détection de plusieurs objets
h, w = image.shape[:2]
dummy_results = [
{'bbox': [w*0.1, h*0.2, w*0.3, h*0.6], 'class_id': 0, 'score': 0.92, 'class_name': 'person'},
{'bbox': [w*0.35, h*0.4, w*0.7, h*0.8], 'class_id': 2, 'score': 0.85, 'class_name': 'car'},
{'bbox': [w*0.75, h*0.3, w*0.9, h*0.55], 'class_id': 16, 'score': 0.78, 'class_name': 'dog'},
]
return dummy_results
def visualize(self, image: np.ndarray, results: list) -> np.ndarray:
"""
Dessiner des boîtes de détection et des étiquettes sur l'image
Paramètres :
image: image originale (H, W, 3)
results: liste des résultats de détection
Retourne :
Image annotée
"""
if isinstance(image, np.ndarray):
image = Image.fromarray(image)
draw = ImageDraw.Draw(image)
for result in results:
bbox = result['bbox']
class_id = result['class_id']
score = result['score']
class_name = result['class_name']
color = tuple(self.colors[class_id % 80].tolist())
# Dessiner la boîte
draw.rectangle(bbox, outline=color, width=3)
# Fond de l'étiquette
label = f"{class_name} {score:.2f}"
try:
font = ImageFont.truetype("arial.ttf", 16)
except:
font = ImageFont.load_default()
# Calculer la taille du texte
bbox_label = draw.textbbox((0, 0), label, font=font)
text_w = bbox_label[2] - bbox_label[0]
text_h = bbox_label[3] - bbox_label[1]
# Dessiner l'arrière-plan de l'étiquette
label_bg = [bbox[0], bbox[1]-text_h-5, bbox[0]+text_w+5, bbox[1]]
draw.rectangle(label_bg, fill=color)
# Dessiner le texte
draw.text((bbox[0]+2, bbox[1]-text_h-3), label, fill=(255,255,255), font=font)
return np.array(image)
# Créer une image simulée (pour démonstration)
def create_test_image() -> np.ndarray:
"""Créer une image de test"""
h, w = 480, 640
image = np.zeros((h, w, 3), dtype=np.uint8)
# Fond dégradé
image[:, :, 0] = np.linspace(100, 200, w)[np.newaxis, :]
image[:, :, 1] = np.linspace(150, 100, h)[:, np.newaxis]
image[:, :, 2] = 180
return image
# Démontrer le processus de détection YOLO
print("=" * 60)
print("EXEMPLE YOLOv11 Démo de détection d'objets")
print("=" * 60)
# Initialiser
detector = SimpleYOLO().load_model()
# Créer une image de test
test_image = create_test_image()
print(f"\nTaille de l'image d'entrée : {test_image.shape}")
# Détection simulée
results = detector.predict_dummy(test_image)
print(f"\n"Détecté {len(results)} objets :")
for i, r in enumerate(results, 1):
print(f" {i}. {r['class_name']} (Confiance : {r['score']:.2f})")
# Visualisation
visualized = detector.visualize(test_image, results)
print(f"\nVisualisation terminée, taille de l'image de sortie : {visualized.shape}")
# ============================================
# Code réel pour utiliser YOLOv11 (nécessite l'installation d'ultralytics)
# ============================================
print("\n" + "=" * 60)
print("Exemple de code pour utiliser réellement YOLOv11 :")
print("=" * 60)
print("""
# Installation : pip install ultralytics
from ultralytics import YOLO
# 1. Charger le modèle
model = YOLO('yolo11n.pt') # Vous pouvez aussi utiliser 'yolo11s.pt', 'yolo11m.pt', etc.
# 2. Prédire
results = model('test_image.jpg') # Peut être un chemin d'image, une vidéo ou 0 (webcam)
# 3. Traiter les résultats
for result in results:
# Boîtes de détection
boxes = result.boxes
# Masques de segmentation (si vous utilisez un modèle de segmentation)
masks = result.masks
# Afficher les résultats
result.show()
# Enregistrer les résultats
result.save('result.jpg')
# 4. Entraînement (facultatif)
# model.train(data='coco128.yaml', epochs=100, imgsz=640)
""")
Segmentation d'image
La détection d'objets marque les objets avec des boîtes rectangulaires, tandis que la segmentation d'image descend au niveau du pixel, en indiquant à quel objet appartient chaque pixel.
Segmentation sémantique vs Segmentation d'instances
La segmentation d'image comporte deux branches principales : la segmentation sémantique et la segmentation d'instances.
| Tâche | Objectif | Exemple | Modèles représentatifs |
|---|---|---|---|
| Segmentation sémantique | Classer chaque pixel (indépendamment de l'instance) | Marquer toutes les personnes en rouge, toutes les voitures en bleu | FCN、U-Net、DeepLab |
| Segmentation d'instances | Classer chaque pixel et distinguer les différentes instances | Marquer la personne 1 en rouge, la personne 2 en orange | Mask R-CNN、YOLOv8-Seg |
| Segmentation panoptique | Sémantique + instance, représentation unifiée | Étiqueter simultanément toutes les « choses » et l’« arrière-plan » | Panoptic FPN、MaskFormer |
Un exemple classique : supposons qu’il y ait deux personnes et deux chiens.
La segmentation sémantique dit : ici il y a des « personnes » et des « chiens » (seulement les catégories, pas les nombres).
La segmentation d’instances dit : ici il y a « personne 1 », « personne 2 », « chien 1 », « chien 2 » (chaque individu est distinct).
Segment Anything Model(SAM)
En 2023, Meta AI a publié SAM (Segment Anything Model), ce qui a transformé la segmentation en une tâche interactive guidée par des prompts.
L'entrée de SAM peut être : un point, une boîte, un extrait de texte ou un masque approximatif.
La sortie de SAM est : le masque de segmentation précis de l'objet correspondant.
La philosophie de conception de SAM est :Pilotable par prompt, généralisable, zero-shot。
Exemple
# Démo du concept de segmentation SAM
# Montrer comment segmenter à l'aide de prompts tels que des points, des boîtes, etc.
# ============================================
class SimpleSAM:
"""Démo simplifiée du concept SAM"""
def __init__(self):
self.image_embedding = None
print("Initialisation de la démo du concept SAM")
def set_image(self, image: np.ndarray):
"""Encoder l'image (une seule fois)"""
print("Encodage de l'image en embedding...")
self.image_embedding = "image_encoded"
return self
def predict_from_point(self, point: tuple, point_label: int = 1):
"""
Segmenter à partir d'un point
Paramètres :
point : coordonnées (x, y)
point_label : 1 pour le premier plan, 0 pour l'arrière-plan
"""
print(f"Segment à partir du point {point} ({'premier plan' if point_label == 1 else 'arrière-plan'})")
return "mask_from_point"
def predict_from_box(self, box: tuple):
"""Segmenter à partir d'une boîte englobante"""
x1, y1, x2, y2 = box
print(f"Segmenter à partir de la boîte englobante [{x1}, {y1}, {x2}, {y2}]")
return "mask_from_box"
def predict_from_points(self, points: list, labels: liste):
"""Segmentation à partir de plusieurs points"""
print(f"Segmentation à partir de {len(points)} points")
return "mask_from_points"
# Démontrer le flux de travail d'utilisation de SAM
print("=" * 60)
print("EXEMPLE de démo du concept de segmentation SAM")
print("=" * 60)
sam = SimpleSAM()
# 1. Configurer l'image (encoder une fois)
test_img = create_test_image()
sam.set_image(test_img)
# 2. Segmenter avec différents prompts
print()
mask1 = sam.predict_from_point((100, 200), point_label=1)
print()
mask2 = sam.predict_from_box((50, 50, 200, 300))
print()
mask3 = sam.predict_from_points([(100, 100), (150, 150)], [1, 1])
# ============================================
# Exemple de code pour l'utilisation réelle de SAM
# ============================================
print("\n" + "=" * 60)
print("Exemple de code pour l'utilisation réelle de SAM:")
print("=" * 60)
print("""
# Installation: pip install segment-anything torch torchvision opencv-python
import cv2
import numpy as np
from segment_anything import sam_model_registry, SamPredictor
# 1. Charger le modèle
# Télécharger le checkpoint: https://dl.fbaipublicfiles.com/segment_anything/sam_vit_h_4b8939.pth
sam = sam_model_registry["vit_h"](checkpoint="sam_vit_h_4b8939.pth")
sam.to(device="cuda")
predictor = SamPredictor(sam)
# 2. Configurer l'image
image = cv2.imread("test_image.jpg")
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
predictor.set_image(image)
# 3. Segmenter avec des invites de points
input_point = np.array([[300, 250]]) # coordonnées du point d'invite
input_label = np.array([1]) # 1 = premier plan, 0 = arrière-plan
masks, scores, logits = predictor.predict(
point_coords=input_point,
point_labels=input_label,
multimask_output=True
)
# 4. Visualiser les résultats
for i, mask in enumerate(masks):
color = np.array([30, 144, 255])
h, w = mask.shape
mask_image = mask.reshape(h, w, 1) * color.reshape(1, 1, -1)
cv2.imwrite(f"mask_{i}.jpg", mask_image)
""")
Applications de la segmentation dans l'industrie
La segmentation d'images est une technologie essentielle dans de nombreuses applications industrielles :
| Champ | Scénario d'application | Type de tâche |
|---|---|---|
| Imagerie médicale | Segmentation de tumeurs, mesure d'organes | Segmentation sémantique |
| Conduite autonome | Zone de conduite, détection des lignes de voie | Segmentation sémantique |
| Imagerie de télédétection | Classification de l'occupation des sols, extraction de bâtiments | Segmentation sémantique |
| Fabrication intelligente | Détection de défauts, mesure de pièces | Segmentation d'instances |
| Montage vidéo | Détourage de personnes, remplacement d'arrière-plan | Segmentation d'instances |
Modèles de diffusion
Les modèles de diffusion sont la technologie centrale de la génération d'images par IA ; Stable Diffusion, DALL-E 3 et Midjourney sont tous basés sur elle.
Processus de diffusion avant (ajout de bruit)
La diffusion avant est un processus simple :Ajoutez progressivement du bruit gaussien à l'image jusqu'à ce qu'elle devienne du bruit pur.。
Il s'agit d'un processus déterministe ; chaque étape n'ajoute qu'un peu de bruit, et nous pouvons utiliser des formules mathématiques pour calculer précisément la quantité de bruit à tout moment.
Exemple
# Démontrer le processus direct du modèle de diffusion (ajout de bruit)
# ============================================
import numpy as np
import matplotlib.pyplot as plt
class ForwardDiffusion:
"""Démo du processus de diffusion direct"""
def __init__(self, num_timesteps: int = 1000):
self.num_timesteps = num_timesteps
# Définir la planification de beta (augmentation linéaire de beta_start à beta_end)
self.beta_start = 0.0001
self.beta_end = 0.02
self.betas = np.linspace(self.beta_start, self.beta_end, num_timesteps)
# Calculer alpha et alpha_bar
self.alphas = 1.0 - self.betas
self.alphas_bar = np.cumprod(self.alphas)
def q_sample(self, x0: np.ndarray, t: int) -> tuple:
"""
Calculer xt au temps t directement à partir de x0
Paramètres :
x0: image originale (H, W, 3)
t: pas de temps (0 <= t < num_timesteps)
Retourne :
xt: image au temps t
noise: le bruit ajouté
"""
sqrt_alpha_bar_t = np.sqrt(self.alphas_bar[t])
sqrt_one_minus_alpha_bar_t = np.sqrt(1 - self.alphas_bar[t])
# Générer un bruit aléatoire de la même forme
noise = np.random.randn(*x0.shape)
# Calculer xt en une étape
xt = sqrt_alpha_bar_t * x0 + sqrt_one_minus_alpha_bar_t * noise
return xt, noise
# Créer une image de test simple : un cercle lumineux au milieu
def create_diffusion_test_image():
h, w = 64, 64
x = np.linspace(-1, 1, w)
y = np.linspace(-1, 1, h)
xx, yy = np.meshgrid(x, y)
r = np.sqrt(xx**2 + yy**2)
image = np.exp(-r**2 / 0.1) # Un cercle lumineux avec une distribution gaussienne
image = (image - image.min()) / (image.max() - image.min()) # Normaliser dans [0, 1]
image = np.stack([image, image*0.8, image*0.5], axis=-1) # Convertir en 3 canaux
return image
print("=" * 60)
print("EXEMPLE Démonstration du processus de diffusion avant")
print("=" * 60)
# Initialiser le processus de diffusion
diffusion = ForwardDiffusion(num_timesteps=1000)
# Créer une image de test
x0 = create_diffusion_test_image()
print(f"Forme de l'image originale: {x0.shape}")
# Afficher les résultats d'ajout de bruit à différents pas de temps
timesteps_to_show = [0, 100, 200, 300, 500, 1000]
print("\nNiveau de bruit à différents pas de temps: ")
for t in timesteps_to_show:
if t < 1000:
xt, noise = diffusion.q_sample(x0, t)
alpha_bar = diffusion.alphas_bar[t]
print(f" t={t:4d}: alpha_bar={alpha_bar:.4f}, SNR={alpha_bar/(1-alpha_bar):.4f}")
else:
print(f" t={t:4d}: bruit pur")
print("\nLa formule centrale de la diffusion directe : ")
print(" q(x_t | x_0) = N(x_t; sqrt(alpha_bar_t) * x0, (1 - alpha_bar_t) * I)")
print("\nCette formule nous permet de calculer x_t à n'importe quel instant t à partir de x0 en une seule étape, sans itération pas à pas !)
Processus inverse de débruitage
Le processus inverse est l'inverse du processus direct :En partant de bruit pur, prédire et éliminer progressivement le bruit pour finalement obtenir une image。
C'est ce que notre modèle doit apprendre : étant donné xt et t, prédire quel est le bruit ajouté.
Objectif d'entraînement : minimiser l'erreur quadratique moyenne entre le bruit prédit par le modèle et le bruit réel.
Architecture U-Net
Le cœur d'un modèle de diffusion est un réseau U-Net, dont la forme ressemble à la lettre "U" : sous-échantillonnage et compression à gauche, sur-échantillonnage et récupération à droite.
L'élément clé de U-Net est la connexion de saut, qui transmet directement les caractéristiques des couches de sous-échantillonnage aux couches de sur-échantillonnage correspondantes, en préservant les informations détaillées.
Échantillonnage DDPM vs DDIM
Après avoir entraîné le modèle, nous devons échantillonner à partir du bruit pour générer des images. Les différents échantillonneurs varient en vitesse et en qualité.
| Échantillonneur | Nom complet | Étapes | Caractéristiques |
|---|---|---|---|
| DDPM | Modèles probabilistes de diffusion par débruitage | 1000 | Méthode originale, bonne qualité mais lente |
| DDIM | Modèles implicites de diffusion par débruitage | 50-100 | Échantillonnage déterministe, rapide |
| LMS | Méthode linéaire multipas | 30-50 | Rapide, bonne qualité |
| DPM-Solver | DPM-Solver | 10-20 | Ultra-rapide, obtient de bons résultats en seulement quelques étapes |
Stable Diffusion utilise actuellement DPM-Solver++ par défaut, obtenant généralement des résultats de haute qualité en 20-30 étapes, des dizaines de fois plus rapide que le DDPM original.
Architecture de Stable Diffusion
Stable Diffusion ne fonctionne pas directement dans l'espace des pixels, mais dans l'espace latent (Latent Space), ce qui le rend plus rapide et plus efficace.
La version complète de Stable Diffusion se compose de trois parties :
1. VAE (auto-encodeur variationnel) : compresse les images en vecteurs latents, ou reconstruit les images à partir des vecteurs latents.
2. UNet : le réseau de débruitage central, prend des vecteurs latents, des pas de temps et des embeddings de prompt, et prédit le bruit.
3. Encodeur de texte (CLIP) : convertit les prompts textuels en embeddings, indiquant au modèle quoi générer.
Exemple
# Démonstration du concept de Stable Diffusion
# Afficher le pipeline de génération complet
# ============================================
class SimpleStableDiffusion:
"""Démonstration simplifiée du concept de Stable Diffusion"""
def __init__(self):
print("Initialisation de la démonstration du concept de Stable Diffusion")
def encode_text(self, prompt: str):
"""Encoder le prompt textuel"""
print(f"Encoder le prompt: '{prompt}'")
return "text_embedding"
def encode_image(self, image: np.ndarray):
"""Le VAE encode l'image dans l'espace latent"""
print(Encoder l'image dans l'espace latent (encodeur VAE))
return "latent_code"
def decode_latent(self, latent: str):
Le VAE décode l'image à partir de l'espace latent
print(Décoder l'image à partir de l'espace latent (décodeur VAE))
return "decoded_image"
def denoise_step(self, latent: str, text_embedding: str, t: int):
Débruitage en une étape
print(fÉtape de débruitage t={t})
return "denoised_latent"
def generate(self, prompt: str, num_inference_steps: int = 50):
Pipeline complet de génération
print("=" * 60)
print(fDébut de la génération : '{prompt}')
print("=" * 60)
# 1. Encoder le texte
text_emb = self.encode_text(prompt)
# 2. Partir d'un bruit aléatoire
print("\nInitialisation du bruit aléatoire...)
latent = "random_noise"
# 3. Débruiter étape par étape
print(f"\nDémarrage de la boucle de débruitage ({num_inference_steps} étapes) :)
for t in reversed(range(num_inference_steps)):
latent = self.denoise_step(latent, text_emb, t)
# 4. Décoder pour obtenir l'image
print("\nDécodage du vecteur latent en image...)
image = self.decode_latent(latent)
print("\nGénération terminée !)
return image
# Démontrer le pipeline de génération
sd = SimpleStableDiffusion()
image = sd.generate(
prompt=Un mignon chat orange dans le jardin,
num_inference_steps=20
)
# ============================================
# Exemple de code réel utilisant Stable Diffusion
# ============================================
print("\n" + "=" * 60)
print(Exemple de code réel utilisant Stable Diffusion :)
print("=" * 60)
print("""
# Installation : pip install diffusers transformers accelerate torch
from diffusers import StableDiffusionPipeline
import torch
# 1. Charger le modèle
model_id = "runwayml/stable-diffusion-v1-5"
pipe = StableDiffusionPipeline.from_pretrained(
model_id,
torch_dtype=torch.float16
)
pipe = pipe.to("cuda")
# 2. Texte-vers-image
prompt = "un mignon chat orange dans un jardin"
image = pipe(prompt).images[0]
# 3. Enregistrer le résultat
image.save("cat_garden.png")
# 4. Image-vers-image
from diffusers import StableDiffusionImg2ImgPipeline
from PIL import Image
img_pipe = StableDiffusionImg2ImgPipeline.from_pretrained(
model_id,
torch_dtype=torch.float16
).to("cuda")
init_image = Image.open("input.jpg").convert("RGB")
init_image = init_image.resize((512, 512))
image = img_pipe(
prompt="donne-lui l'air d'une peinture",
image=init_image,
strength=0.75 # degré de variation
).images[0]
image.save("painted.png")
# 5. Inpainting (modification locale)
from diffusers import StableDiffusionInpaintPipeline
inpaint_pipe = StableDiffusionInpaintPipeline.from_pretrained(
model_id,
torch_dtype=torch.float16
).to("cuda")
image = inpaint_pipe(
prompt="mettre un chapeau au chat",
image=init_image,
mask_image=mask_image # image de masque, les zones blanches sont les parties à modifier
).images[0]
""")
CLIP : Fondements de l'alignement multimodal
CLIP (Contrastive Language-Image Pre-training) est un travail publié par OpenAI en 2021, qui a pour la première fois véritablement aligné le texte et les images dans le même espace vectoriel.
Principes de l'apprentissage contrastif
La méthode d'entraînement de CLIP est l'apprentissage contrastif :Faire en sorte que les paires image-texte correctes aient une similarité élevée, et que les paires incorrectes aient une faible similarité.。
Pendant l'entraînement, un lot contient N images et N descriptions textuelles (en correspondance biunivoque), formant N×N paires image-texte possibles.
La diagonale contient les échantillons positifs appariés, et le reste correspond à des échantillons négatifs non appariés.
L'objectif d'entraînement est : maximiser la similarité des échantillons positifs et minimiser la similarité des échantillons négatifs.
Entraînement à l'alignement image-texte
CLIP se compose de deux encodeurs :
1. Encodeur d'images : encode une image en un vecteur (peut être ResNet ou ViT).
-
2. Encodeur de texte : encode le texte en vecteurs (Transformer).
Après projection des deux vecteurs dans le même espace, le degré de correspondance image-texte est calculé par similarité cosinus.
Exemple
# Démo du concept CLIP : apprentissage contrastif et alignement image-texte
# ============================================
import torch
import torch.nn.functional as F
class SimpleCLIP:
"""Démo simplifiée du concept CLIP"""
def __init__(self, embed_dim: int = 512):
self.embed_dim = embed_dim
# Simulation simple : pas un véritable encodage, juste une projection aléatoire
print("Initialisation de la démo du concept CLIP")
print(f"Dimension d'embedding : {embed_dim}")
def encode_image(self, image: str) -> torch.Tenseur:
"""Simuler l'encodage d'image"""
# En pratique, ce serait un Vision Transformer ou ResNet
return torch.randn(1, self.embed_dim)
def encode_text(self, text: str) -> torch.Tenseur:
"""Simuler l'encodage de texte"""
# En pratique, ce serait un Text Transformer
return torch.randn(1, self.embed_dim)
def compute_similarity(self, image_emb: torch.Tensor, text_emb: torch.Tensor) -> float:
"""Calculer la similarité cosinus"""
image_emb = F.normalize(image_emb, dim=-1)
text_emb = F.normalize(text_emb, dim=-1)
return (image_emb @ text_emb.T).item()
# Démontrer le flux d'inférence CLIP
print("=" * 60)
print("EXEMPLE de démonstration de classification zero-shot CLIP")
print("=" * 60)
clip = SimpleCLIP(embed_dim=512)
# Supposons qu'il y a une image
print("\n1. Encodage de l'image...")
image_emb = clip.encode_image("Une photo d'un chat")
# Supposons qu'il y a plusieurs catégories candidates
candidates = ["Une photo d'un chien", "Une photo d'un chat", "Une voiture", "Une personne"]
print(f"\n2. Encodage des textes candidats : {candidates}")
# Calculer les similarités
print("\n3. Calcul de la similarité image-texte :")
for text in candidates:
text_emb = clip.encode_text(text)
sim = clip.compute_similarity(image_emb, text_emb)
print(f" '{text}' → similarité: {sim:.3f}")
print("\n(En pratique, 'une photo d'un chat' aura la similarité la plus élevée)")
# ============================================
# Exemple concret d'utilisation de CLIP
# ============================================
print("\n" + "=" * 60)
print("Exemple concret d'utilisation de CLIP:")
print("=" * 60)
print("""
# Installation : pip install clip torch torchvision
import clip
import torch
from PIL import Image
# 1. Charger le modèle
device = "cuda" if torch.cuda.is_available() else "cpu"
model, preprocess = clip.load("ViT-B/32", device=device)
# 2. Préparer l'image
image = preprocess(Image.open("cat.jpg")).unsqueeze(0).to(device)
# 3. Préparer le texte
text = clip.tokenize([
"une photo d'un chien",
"une photo d'un chat",
"une photo d'une voiture",
"une photo d'une personne"
]).to(device)
# 4. Inférence
with torch.no_grad():
image_features = model.encode_image(image)
text_features = model.encode_text(text)
logits_per_image, logits_per_text = model(image, text)
probs = logits_per_image.softmax(dim=-1).cpu().numpy()
# 5. Afficher les résultats
print("Probabilités de prédiction :")
for i, p in enumerate(probs[0]):
print(f" {i+1}. {p:.2%}")
""")
Capacité de classification zero-shot
La capacité la plus fascinante de CLIP est la classification zero-shot :Elle peut classer directement sans entraînement sur le jeu de données cible。
Les modèles entraînés sur l'ImageNet traditionnel ne peuvent classer que les 1000 classes d'ImageNet ; l'ajout de nouvelles classes nécessite un réentraînement.
CLIP peut utiliser un texte arbitraire pour décrire des catégories, par exemple : « une photo d'un chat », « un chat orange assis sur l'herbe », « un croquis d'un chat ».
L'importance de CLIP ne réside pas seulement dans la classification zero-shot, mais aussi dans le fait qu'il établit une interface générale « vision-langage » ; de nombreux modèles ultérieurs (comme Stable Diffusion, les modèles vision-langage) sont construits sur ses fondations.
Modèles vision-langage (VLM)
Un modèle vision-langage (VLM) combine la compréhension des images et la génération de langage, permettant au modèle de « regarder des images et parler ».
Architecture de LLaVA
LLaVA (Grand assistant de langage et de vision) est un VLM open source. Sa conception est simple et claire, ce qui le rend très adapté aux débutants.
L'architecture de LLaVA se divise en trois parties :
-
1. Encodeur visuel : l'encodeur visuel de CLIP, qui convertit les images en embeddings.
-
2. Couche de projection : projette les embeddings visuels dans la même dimension que l'entrée du LLM.
-
3. LLM : un grand modèle de langage (par exemple, Vicuna, LLaMA) qui reçoit l'entrée image-texte concaténée et génère des réponses.
L'entraînement se divise en deux étapes :
-
1. Phase de pré-entraînement : seule la couche de projection est entraînée, en utilisant un grand nombre de paires image-texte pour l'alignement.
-
2. Phase d'affinage par instruction : le modèle entier est entraîné avec des jeux de données d'instructions visuelles (par exemple, LLaVA-Instruct).
Connexion entre l'encodeur visuel et le LLM
La clé pour connecter la vision et le langage réside dans :Représenter les images sous forme de séquences de tokens que les modèles de langage peuvent comprendre。
Il existe plusieurs méthodes de connexion courantes :
| Méthode | Caractéristiques | Modèles représentatifs |
|---|---|---|
| Jeton spécial | Utiliser un seul jeton [IMG] pour représenter toute l'image | Premiers modèles |
| Concaténation de séquences | Concaténer directement les embeddings de patchs d'image avant le texte | LLaVA, GPT-4V |
| Attention croisée | Ajouter une attention croisée aux caractéristiques visuelles dans le LLM | BLIP-2, Flamingo |
Exemple
# Démo du concept de modèle vision-langage (VLM)
# Montrer comment concaténer les entrées visuelles et linguistiques
# ============================================
class SimpleVLM:
"""Modèle vision-langage simplifié"""
def __init__(self, vocab_size: int = 50000, hidden_size: int = 768):
self.vocab_size = vocab_size
self.hidden_size = hidden_size
print("Initialisation du VLM simplifié")
def encode_vision(self, image: str) -> torch.Tenseur:
"""Encoder l'image en séquence"""
# Supposons qu'il y ait 256 jetons visuels
return torch.randn(1, 256, self.hidden_size)
def encode_text(self, text: str) -> torch.Tenseur:
"""Encoder le texte en séquence"""
# Supposons que le texte comporte 16 tokens
return torch.randn(1, 16, self.hidden_size)
def generate(self, image: str, question: str) -> str:
"""La concaténation image-texte génère la réponse"""
print("=" * 60)
print(f"Question : {question}")
print("=" * 60)
# 1. Encoder l'image et le texte
print("\n1. Encodage de l'image...)
vision_emb = self.encode_vision(image)
print("2. Encodage du texte...")
text_emb = self.encode_text(question)
# 3. Concaténer : [séquence vision] + [séquence texte]
print("\n3. Concaténation des séquences vision et texte...)
print(f" Forme de l'embedding visuel : {vision_emb.shape}")
print(f" Forme de l'embedding textuel : {text_emb.shape}")
combined = torch.cat([vision_emb, text_emb], dim=1)
print(f" Forme après concaténation : {combined.shape}")
# 4. Le LLM génère la réponse
print("\n4. Le LLM génère la réponse...)
return "C'est un mignon chat orange, il a l'air très heureux."
# Démonstration de questions-réponses VLM
vlm = SimpleVLM()
answer = vlm.generate(
image="Une photo d'un chat orange",
question="Qu'y a-t-il sur cette image ? Veuillez la décrire."
)
print(f"\nRéponse : {answer}")
# ============================================
# Exemple de code pour utiliser LLaVA en pratique
# ============================================
print("\n" + "=" * 60)
print("Exemple de code pour utiliser LLaVA en pratique :")
print("=" * 60)
print("""
# LLaVA nécessite beaucoup de mémoire GPU, il est recommandé d'utiliser la bibliothèque transformers ou llava
from transformers import AutoProcessor, AutoModelForVisionAndLanguageGeneration
import torch
from PIL import Image
# 1. Charger le modèle et le processeur
model_id = "llava-hf/llava-1.5-7b-hf"
processor = AutoProcessor.from_pretrained(model_id)
model = AutoModelForVisionAndLanguageGeneration.from_pretrained(
model_id,
torch_dtype=torch.float16,
device_map="auto"
)
# 2. Préparer l'entrée
image = Image.open("cat.jpg")
prompt = "USER: <image>\\nQue contient cette image ? Veuillez la décrire. ASSISTANT:"
inputs = processor(images=image, text=prompt, return_tensors="pt").to("cuda")
# 3. Générer
with torch.no_grad():
output = model.generate(**inputs, max_new_tokens=500)
answer = processor.decode(output[0], skip_special_tokens=True)
print(answer)
""")
Atelier pratique : projet complet de classification d'images + détection d'objets
Maintenant, rassemblons les connaissances que nous avons apprises pour construire un projet pratique complet.
Exemple
# Projet pratique : pipeline complet de vision par IA
# Comprend la classification d'images, la détection d'objets et la visualisation des résultats
# ============================================
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
import numpy as np
from PIL import Image
import json
from pathlib import Path
class VisionPipeline:
"""
Pipeline complet de vision IA
Fonctionnalités :
1. Classification d'images
2. Détection d'objets
3. Résumé des résultats et visualisation
"""
def __init__(self, device: str = "cpu"):
self.device = device
self.classifier = None
self.detector = None
print(f"EXEMPLE Pipeline de vision initialisé (device: {device})")
def build_classifier(self, num_classes: int = 10):
"""Construire un classifieur d'images"""
print("\nConstruction du classifieur d'images...)
# Utiliser le SimpleCNN défini précédemment
self.classifier = SimpleCNN(num_classes=num_classes).to(self.device)
print("Classifieur d'images construit avec succès !")
return self
def build_detector(self):
"""Construire un détecteur d'objets"""
print("\nConstruction du détecteur d'objets...)
self.detector = SimpleYOLO()
print("Détecteur construit avec succès !")
return self
def load_pretrained_weights(self, classifier_path: str = None):
"""Charger les poids pré-entraînés"""
if classifier_path and self.classifier:
print(f"\nCharger les poids du classifieur: {classifier_path}")
# En pratique: self.classifier.load_state_dict(torch.load(classifier_path))
return self
def classify_image(self, image: np.ndarray) -> tuple:
"""
Classification d'images
Retourne :
(pred_class_id, pred_class_name, confidence)
"""
if self.classifier is None:
raise ValueError("Veuillez d'abord construire le classifieur: build_classifier()")
# Prétraitement
if isinstance(image, np.ndarray):
if image.ndim == 2: # Niveaux de gris
image = np.stack([image]*3, axis=-1)
image = torch.from_numpy(image).permute(2, 0, 1).float() / 255.0
image = F.interpolate(image.unsqueeze(0), size=(32, 32), mode='bilinear')
image = image.to(self.device)
# Inférence
self.classifier.eval()
with torch.no_grad():
logits = self.classifier(image)
probs = F.softmax(logits, dim=-1)
confidence, pred_idx = probs.max(dim=-1)
# Renvoyer le résultat
class_names = [
'airplane', 'automobile', 'bird', 'cat', 'deer',
'dog', 'frog', 'horse', 'ship', 'truck'
]
pred_class = class_names[pred_idx.item()]
return pred_idx.item(), pred_class, confidence.item()
def detect_objects(self, image: np.ndarray) -> list:
"""Détection d'objets"""
if self.detector is None:
raise ValueError("Veuillez d'abord construire le détecteur : build_detector()")
# Utiliser une détection simulée
return self.detector.predict_dummy(image)
def process_image(self, image: np.ndarray) -> dict:
"""
Traiter une image entière
Retourne :
Dictionnaire contenant tous les résultats
"""
print("\n" + "=" * 60)
print("Début du traitement de l'image")
print("=" * 60)
results = {}
# 1. Classification d'image
if self.classifier is not None:
print("\n[1/3] Exécution de la classification d'image...")
class_id, class_name, conf = self.classify_image(image)
results['classification'] = {
'class_id': class_id,
'class_name': class_name,
'confidence': conf
}
print(f" → Résultat de classification : {class_name} (Confiance : {conf:.2%})")
# 2. Détection d'objets
if self.detector is not None:
print("\n[2/3] Exécution de la détection d'objets...")
detections = self.detect_objects(image)
results['detections'] = detections
print(f" → {len(detections)} objets détectés :")
for det in detections:
print(f" - {det['class_name']} ({det['score']:.2f})")
# 3. Visualisation
print("\n[3/3] Génération de la visualisation...")
if self.detector is not None:
vis_image = self.detector.visualize(image, detections)
results['visualization'] = vis_image
print(" → Visualisation terminée")
print("\n" + "=" * 60)
print("Traitement d'image terminé !")
print("=" * 60)
return results
def save_results(self, results: dict, output_dir: str = "output"):
"""Enregistrer les résultats dans un fichier"""
output_path = Path(output_dir)
output_path.mkdir(exist_ok=True)
# Enregistrer les résultats JSON
json_result = {
'classification': results.get('classification'),
'detections': [
{k: v for k, v in det.items() if k != 'bbox'}
for det in results.get('detections', [])
]
}
with open(output_path / "results.json", "w") as f:
json.dump(json_result, f, indent=2, ensure_ascii=False)
# Enregistrer l'image de visualisation
if 'visualization' in results:
vis = Image.fromarray(results['visualization'])
vis.save(output_path / "visualization.jpg")
print(f"\nRésultats enregistrés dans : {output_path}/")
# Exemple complet d'entraînement
class SimpleDataset(Dataset):
"""Jeu de données simulé simple"""
def __init__(self, num_samples: int = 100):
self.num_samples = num_samples
# Générer des données factices
self.images = torch.randn(num_samples, 3, 32, 32)
self.labels = torch.randint(0, 10, (num_samples,))
def __len__(self):
return self.num_samples
def __getitem__(self, idx):
return self.images[idx], self.labels[idx]
def train_classifier(pipeline: VisionPipeline, num_epochs: int = 5):
"""Flux de travail complet pour l'entraînement du classifieur"""
print("\n" + "=" * 60)
print("Démarrer l'entraînement du classifieur")
print("=" * 60)
# 1. Préparer les données
print("\nPréparation du jeu de données...")
train_dataset = SimpleDataset(1000)
val_dataset = SimpleDataset(200)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=32)
print(f"Ensemble d'entraînement : {len(train_dataset)} échantillons")
print(f"Ensemble de validation : {len(val_dataset)} échantillons")
# 2. Optimiseur et fonction de perte
optimizer = torch.optim.Adam(pipeline.classifier.parameters(), lr=1e-3)
criterion = nn.CrossEntropyLoss()
# 3. Boucle d'entraînement
print("\nDébut de l'entraînement...")
for epoch in range(num_epochs):
pipeline.classifier.train()
total_loss = 0.0
correct = 0
total = 0
for images, labels in train_loader:
images, labels = images.to(pipeline.device), labels.to(pipeline.device)
optimizer.zero_grad()
outputs = pipeline.classifier(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
total_loss += loss.item()
_, predicted = outputs.max(1)
total += labels.size(0)
correct += predicted.eq(labels).sum().item()
train_acc = correct / total
avg_loss = total_loss / len(train_loader)
# Validation
pipeline.classifier.eval()
correct = 0
total = 0
with torch.no_grad():
for images, labels in val_loader:
images, labels = images.to(pipeline.device), labels.to(pipeline.device)
outputs = pipeline.classifier(images)
_, predicted = outputs.max(1)
total += labels.size(0)
correct += predicted.eq(labels).sum().item()
val_acc = correct / total
print(f"Époque {epoch+1}/{num_epochs} - "
f"Perte : {avg_loss:.4f}, "
f"Précision d'entraînement : {train_acc:.2%}, "
fPrécision de validation : {val_acc:.2%})
print("\nEntraînement terminé !)
# ============================================
Exécutez le projet pratique complet.
# ============================================
print("=" * 60)
print(EXEMPLE Projet pratique complet d'IA visuelle)
print("=" * 60)
# 1. Créer le pipeline
pipeline = VisionPipeline(device="cuda" if torch.cuda.is_available() else "cpu")
# 2. Construire le modèle
pipeline.build_classifier(num_classes=10)
pipeline.build_detector()
3. Entraînement du classifieur
train_classifier(pipeline, num_epochs=3)
# 4. Traitement des images
test_image = create_test_image()
results = pipeline.process_image(test_image)
5. Enregistrer le résultat
pipeline.save_results(results, output_dir="example_output")
print("\n" + "=" * 60)
print(Exécution du projet terminée !)
print("=" * 60)
print("\nSuggestions d'apprentissage pour la prochaine étape :)
print(1. Essayez d'entraîner sur un jeu de données réel (par exemple, CIFAR-10).)
print(2. Réglage fin à l'aide d'un modèle pré-entraîné)
print(3. Essayez des architectures de modèles plus complexes.)
print(4. Déployez dans des applications pratiques.)