Introduction¶
Présentation du problème¶
Dans le domaine de l’ingénierie des structures, la détection de fissures sur les surfaces en béton représente un enjeu majeur. En effet, une fissure non repérée peut progressivement s’élargir, affaiblir la structure concernée et, à terme, menacer sa stabilité et sa sécurité. Or, l’inspection traditionnelle, réalisée par des opérateurs humains, reste coûteuse, lente, et souvent sujette à des erreurs dues à la fatigue, aux conditions environnementales ou au manque de visibilité. Cette situation nous incite à rechercher des approches automatisées, plus rapides et plus fiables, pour assurer une maintenance préventive efficace et garantir la longévité des infrastructures.
C’est dans ce contexte que nous nous intéressons à la mise en place d’un système de détection automatique des fissures dans des images de surfaces en béton. Nous visons ainsi à pallier les limites de l’inspection humaine, tout en offrant une précision et une robustesse accrues. Notre approche s’intègre dans une logique de gestion intelligente des infrastructures, susceptible d’améliorer la sécurité et de réduire les coûts de maintenance.
Objectif du projet¶
Notre objectif est donc de développer un système automatique, basé sur les réseaux de neurones convolutionnels (CNN), pour détecter de manière fiable et robuste la présence de fissures dans des images de surfaces en béton. Nous souhaitons, d’une part, atteindre une performance prédictive élevée, et, d’autre part, mettre au point un processus reproductible et généralisable. Idéalement, notre modèle saura identifier la présence ou l’absence de fissures, même dans des conditions visuelles variées (éclairage, texture, environnement).
Pour ce faire, nous prévoyons de suivre un flux de travail structuré. Nous commencerons par le prétraitement des données (normalisation, augmentation, etc.), puis nous passerons à l’entraînement d’un ou plusieurs modèles CNN. Enfin, nous évaluerons les performances obtenues à l’aide de métriques standards telles que l’exactitude, la précision, le rappel ou encore le score F1, afin de mesurer de manière rigoureuse la qualité du modèle.
Importantion des packages nécessaires¶
import numpy as np
import os
from matplotlib import pyplot as plt
from PIL import Image
from torch.utils.data import Dataset, DataLoader
from sklearn.model_selection import train_test_split
import torch
from torchvision import transforms
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from sklearn.metrics import classification_report, confusion_matrix, roc_curve, auc
import seaborn as sns
import pandas as pd
Description du Jeu de Données¶
Pour mener à bien nos travaux, nous nous appuyons sur le jeu de données Surface Crack Detection disponible sur la plateforme Kaggle.
# Répertoire des données
data_dir = "archive"
# Liste des classes (noms des sous-dossiers)
class_names = [class_name for class_name in os.listdir(data_dir) if os.path.isdir(os.path.join(data_dir, class_name))]
num_class = len(class_names)
# Création d'une liste des fichiers d'images et des labels associés
image_files = [
[os.path.join(data_dir, class_name, x)
for x in os.listdir(os.path.join(data_dir, class_name)) if x != ".DS_Store"] # Ignore .DS_Store
for class_name in class_names
]
image_file_list = []
image_label_list = []
for i, class_name in enumerate(class_names):
image_file_list.extend(image_files[i])
image_label_list.extend([i] * len(image_files[i]))
num_total = len(image_label_list)
# Obtention des dimensions de l'image (en supposant que toutes les images ont les mêmes dimensions)
image_width, image_height = Image.open(image_file_list[0]).size
# Comptage du nombre d'images pour chaque classe
label_counts = [len(image_files[i]) for i in range(num_class)]
# Affichage des informations
print(f"Total image count: {num_total}")
print(f"Image dimensions: {image_width} x {image_height}")
print(f"Label names: {class_names}")
print(f"Label counts: {label_counts}")
Total image count: 40000 Image dimensions: 227 x 227 Label names: ['Positive', 'Negative'] Label counts: [20000, 20000]
Structure du dataset¶
Il ressort que la base de données contient un total de 40 000 images, chacune de taille 227 x 227 pixels. On constate qu’il existe deux classes distinctes : “Positive” et “Negative”, avec un effectif équilibré de 20 000 images dans chaque catégorie. La répartition équilibrée entre les deux catégories garantit une base de données propice à l'entrainement et à l'évaluation des modèles.
Visualisation de quelques images¶
Avant d’aller plus loin, nous allons visualiser quelques exemples du jeu de données afin de mieux comprendre sa nature.
# Nombre d'images à afficher
num_images_to_show = 15
# Affichage des images
plt.figure(figsize=(8, 8))
for i, idx in enumerate(np.random.randint(num_total, size=num_images_to_show)):
# Chargement de l'image et sa taille
img_path = image_file_list[idx]
img = Image.open(img_path)
arr = np.array(img)
# Affichage de l'image dans une grille 3x3
plt.subplot(5, 5, i + 1)
plt.imshow(arr, cmap='gray')
# Ajout de label sous l'image
plt.xlabel(class_names[image_label_list[idx]])
plt.xticks([])
plt.yticks([])
# Amélioration de la disposition et de l'affichage
plt.tight_layout()
plt.show()
Analyse des sorties
Après visualisation de quelques échantillons de notre jeu de données, il ressort une distinction nette entre les deux classes, “Positive” et “Negative”. Les images positives présentent des fissures bien marquées, aux contours distincts et avec des textures variées, tandis que les images négatives apparaissent homogènes, sans aucune anomalie visible. Cette différence claire entre les deux catégories suggère que le modèle pourra apprendre à distinguer efficacement ces caractéristiques visuelles lors de l’entraînement.
Les images sont fournies dans un format standard, souvent JPEG ou PNG, ce qui les rend compatibles avec les outils de traitement d'images. Cette étape de visualisation a également permis de confirmer la qualité des données et leur aptitude à servir de base pour une extraction des caractéristiques pertinente via des réseaux de neurones convolutionnels (CNN).
Ainsi, cette analyse préliminaire nous offre une meilleure compréhension des données, tout en validant leur adéquation pour les étapes ultérieures du processus de classification.
Prétraitement des données¶
Méthodes de prétraitement envisagées¶
Le prétraitement des données est une étape essentielle pour garantir des performances optimales du modèle de classification. Dans cette section, nous décrivons les étapes que nous allons mettre en œuvre pour préparer le jeu de données, en particulier la division en sous-ensembles adaptés à l’entraînement, à la validation et au test.
Division des données¶
Pour entraîner et évaluer le modèle de manière rigoureuse, nous allons diviser le jeu de données en trois sous-ensembles distincts :
- Entraînement : cet ensemble sera utilisé pour ajuster les paramètres du modèle.
- Validation : il permettra d’évaluer les performances intermédiaires et d’ajuster les hyperparamètres afin d’éviter le surapprentissage.
- Test : cet ensemble sera réservé exclusivement à la mesure des performances finales du modèle sur des données inédites.
Nous choisirons une répartition de $60\%$ pour l’entraînement, $15\%$ pour la validation, et $25\%$ pour le test. Cette division sera réalisée de manière stratifiée grâce à la fonction train_test_split de Scikit-learn. Cela garantira que les proportions des classes "Positive" et "Negative" sont respectées dans chaque sous-ensemble, assurant ainsi un équilibre des données.
Conversion des données¶
Avant de diviser les données, nous convertirons les chemins des images et leurs étiquettes en tableaux NumPy. Cette conversion facilitera leur manipulation et leur traitement dans les étapes ultérieures.
Étapes futures¶
Une fois cette division effectuée, ces ensembles serviront de base pour les autres étapes de prétraitement, comme l'application de transformations (redimensionnement, normalisation) et la création de structures adaptées pour l'entraînement des réseaux de neurones convolutifs (CNN).
Le code suivant permet de réaliser la séparation de notre jeu de données comme décrit ci-dessus.
# Conversion en tableaux NumPy
image_file_list = np.array(image_file_list)
image_label_list = np.array(image_label_list)
def train_val_test_split(image_files, image_labels, valid_frac, test_frac):
"""
Divise les données en ensembles d'entraînement, de validation et de test.
Parameters
----------
image_files : np.ndarray
Tableau des chemins des images.
image_labels : np.ndarray
Tableau des labels associés aux images.
valid_frac : float
Fraction des données à utiliser pour la validation.
test_frac : float
Fraction des données à utiliser pour le test.
Returns
-------
trainX : np.ndarray
Chemins des images d'entraînement.
valX : np.ndarray
Chemins des images de validation.
testX : np.ndarray
Chemins des images de test.
trainY : np.ndarray
Labels d'entraînement.
valY : np.ndarray
Labels de validation.
testY : np.ndarray
Labels de test.
"""
# Division en entraînement et (validation+test)
trainX, remainingX, trainY, remainingY = train_test_split(
image_files,
image_labels,
test_size=valid_frac + test_frac,
stratify=image_labels,
random_state=42
)
# Division du 'remaining' en validation et test
valX, testX, valY, testY = train_test_split(
remainingX,
remainingY,
test_size=test_frac / (valid_frac + test_frac),
stratify=remainingY,
random_state=42
)
return trainX, valX, testX, trainY, valY, testY
# Paramètres pour la division
valid_frac, test_frac = 0.15, 0.25
# Application de la fonction de séparation
trainX, valX, testX, trainY, valY, testY = train_val_test_split(
image_file_list,
image_label_list,
valid_frac,
test_frac
)
# Affichage des tailles
print(f"Nombre d'images d'entraînement: {len(trainX)}")
print(f"Nombre d'images de validation: {len(valX)}")
print(f"Nombre d'images de test: {len(testX)}")
Nombre d'images d'entraînement: 24000 Nombre d'images de validation: 6000 Nombre d'images de test: 10000
La division du jeu de données a été effectuée avec succès selon les proportions spécifiées : $60\%$ pour l’entraînement, $15\%$ pour la validation, et $25\%$ pour le test. Cette répartition a permis de constituer des sous-ensembles équilibrés comprenant $24\,000$ images pour l’entraînement, $6\,000$ images pour la validation, et $10\,000$ images pour le test. La méthode utilisée, basée sur la fonction train_test_split de Scikit-learn, garantit une répartition stratifiée, maintenant les proportions des classes "Positive" et "Negative" dans chaque sous-ensemble. Cette nouvelle structure équilibrée favorise un entraînement robuste, une validation approfondie des performances du modèle, et une évaluation finale encore plus complète sur des données inédites.
Création des datasets et DataLoaders¶
Pour traiter efficacement les données avec PyTorch, nous avons créé une classe personnalisée appelée SurfaceCrackDataset. Cette classe gère :
- Le chargement des chemins des images et de leurs étiquettes.
- L’application des transformations définies, telles que le redimensionnement et la normalisation.
Nous utilisons ensuite cette classe pour créer des datasets adaptés aux ensembles d’entraînement, de validation et de test. Ces datasets sont ensuite intégrés dans des DataLoaders, qui permettent de charger les données par lots (batchs) pendant l'entraînement et l'évaluation.
Définition des transformations¶
Les transformations appliquées aux images incluent :
- Redimensionnement des images à une taille standard de 128 x 128 pixels.
- Conversion en tenseurs pour rendre les données compatibles avec PyTorch.
- Normalisation avec une moyenne et un écart type ajustés (moyenne :
[0.5, 0.5, 0.5], écart type :[0.5, 0.5, 0.5]) pour accélérer l'entraînement et assurer une meilleure convergence.
Ces transformations sont définies à l'aide de torchvision.transforms et appliquées de manière cohérente sur l'ensemble des données.
Création des datasets¶
Nous utilisons la classe SurfaceCrackDataset pour charger les images et leurs étiquettes dans des datasets. Ces datasets sont créés séparément pour les ensembles d'entraînement, de validation et de test.
Initialisation des DataLoaders¶
Les DataLoaders permettent de charger les données par lots (batchs) tout en gérant l'aléatoire (shuffle) pour l'entraînement. Nous définissons un batch size de 32 images et configurons les DataLoaders pour chaque ensemble :
- Le DataLoader d'entraînement charge les données de manière aléatoire (shuffle=True).
- Les DataLoaders de validation et de test chargent les données de manière séquentielle (shuffle=False).
le code suivant permet de réaliser ces taches :
class SurfaceCrackDataset(Dataset):
"""
Dataset personnalisé pour charger les images de fissures et leurs étiquettes.
Parameters
----------
image_paths : array-like
Liste ou tableau des chemins vers les images.
labels : array-like
Liste ou tableau des labels associés aux images (0 ou 1).
transform : torchvision.transforms.Compose, optional
Transformations à appliquer aux images.
"""
def __init__(self, image_paths, labels, transform=None):
self.image_paths = image_paths
self.labels = labels
self.transform = transform
def __len__(self):
"""
Retourne la taille du dataset.
"""
return len(self.image_paths)
def __getitem__(self, idx):
"""
Récupère l'image et le label à l'indice idx.
Parameters
----------
idx : int
Indice de l'exemple.
Returns
-------
image : torch.Tensor
L'image transformée en tenseur.
label : int
Le label associé à l'image.
"""
img_path = self.image_paths[idx]
label = self.labels[idx]
image = Image.open(img_path).convert('RGB')
if self.transform:
image = self.transform(image)
return image, label
# Définition des transformations
transformations = transforms.Compose([
transforms.Resize((128, 128)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])
])
# Création des datasets
train_dataset = SurfaceCrackDataset(trainX, trainY, transform=transformations)
val_dataset = SurfaceCrackDataset(valX, valY, transform=transformations)
test_dataset = SurfaceCrackDataset(testX, testY, transform=transformations)
# Création des DataLoaders
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False)
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)
print("Les DataLoaders pour l'entraînement, la validation et le test sont prêts.")
Les DataLoaders pour l'entraînement, la validation et le test sont prêts.
Modélisation¶
La modélisation constitue une étape clé dans le développement de notre système de détection automatique des fissures. Dans cette section, nous concevons et entraînons un modèle basé sur un réseau de neurones convolutifs (CNN), une architecture particulièrement adaptée aux tâches de classification d’images. Notre approche inclut la définition de l’architecture du modèle, le choix des hyperparamètres, ainsi que les stratégies d’entraînement et d’évaluation. Ce chapitre détaille également les mécanismes d’apprentissage mis en œuvre pour optimiser les performances du modèle, en s'appuyant sur des ensembles de données prétraitées et un pipeline de calcul conçu pour maximiser la robustesse et la précision du système.
Modèle CNN (Convolutional Neural Network)¶
Dans cette section, nous allons concevoir un modèle basé sur un réseau de neurones convolutifs (CNN) pour la tâche de détection automatique des fissures. Cette architecture est particulièrement adaptée à l’analyse d’images, grâce à sa capacité à extraire des caractéristiques visuelles pertinentes, telles que les textures et les contours, à différents niveaux de complexité.
Architecture prévue¶
Le modèle que nous allons implémenter sera structuré autour des éléments suivants :
Couches convolutionnelles :
- Nous utiliserons trois couches convolutionnelles successives, chacune équipée de filtres de taille $3 \times 3$, avec une stride de $1$ et un padding de $1$. Ces couches auront respectivement $32$, $64$, et $128$ filtres, ce qui permettra d’extraire progressivement des caractéristiques visuelles de plus en plus abstraites.
Fonctions d’activation :
- Chaque couche convolutionnelle sera suivie d’une fonction d’activation ReLU (Rectified Linear Unit), qui introduira de la non-linéarité et permettra au modèle d’apprendre des relations complexes dans les données.
MaxPooling :
- Une opération de pooling de type MaxPooling avec une fenêtre de taille $2 \times 2$ sera appliquée après chaque couche convolutionnelle. Cela réduira la résolution spatiale des cartes de caractéristiques tout en conservant les informations essentielles.
Couches entièrement connectées :
- Après l’extraction des caractéristiques, nous aplatirons les cartes pour produire un vecteur unidimensionnel. Ce vecteur sera ensuite passé à travers deux couches entièrement connectées :
- Une première couche avec $128$ neurones pour réduire la dimension.
- Une seconde couche, finale, qui produira une sortie de taille $2$, correspondant aux deux classes ("Positive" et "Negative").
- Après l’extraction des caractéristiques, nous aplatirons les cartes pour produire un vecteur unidimensionnel. Ce vecteur sera ensuite passé à travers deux couches entièrement connectées :
Flux prévu des données¶
Les données suivront les étapes suivantes dans le modèle :
- Les images en entrée auront une dimension $(\text{batch size}, 3, 128, 128)$.
- Ces images passeront à travers les trois couches convolutionnelles et les opérations de pooling, réduisant progressivement leurs dimensions spatiales.
- La sortie finale des couches convolutionnelles sera aplatie pour former un vecteur de taille $(\text{batch size}, 128 \times 16 \times 16)$.
- Ce vecteur sera transformé par les couches entièrement connectées pour produire une sortie de logits de dimension $(\text{batch size}, 2)$.
Objectifs et avantages¶
L’objectif principal de cette architecture sera de capturer les motifs complexes des fissures dans les images, tout en maintenant une structure simple pour garantir une convergence rapide et efficace. Les avantages attendus de ce modèle incluent :
- Une extraction hiérarchique des caractéristiques visuelles grâce aux couches convolutionnelles.
- Une réduction de la complexité spatiale par le pooling, permettant un traitement efficace.
- Une capacité de classification précise grâce aux couches entièrement connectées.
Le code suivant permet d'implémenter cette architecture en utilisant la bibliothèque PyTorch.
class CNN_Model(nn.Module):
"""
Modèle CNN simple pour la détection de fissures.
Architecture :
- 3 couches convolutionnelles suivies de ReLU et MaxPooling
- 2 couches fully-connected pour la classification finale
Input : (batch_size, 3, 128, 128)
Output : logits de taille (batch_size, 2)
"""
def __init__(self):
super(CNN_Model, self).__init__()
self.conv1 = nn.Conv2d(in_channels=3, out_channels=32, kernel_size=3, stride=1, padding=1)
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
self.conv2 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, stride=1, padding=1)
self.conv3 = nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, stride=1, padding=1)
# Après 3 max-poolings successifs, la taille diminue de 128 -> 64 -> 32 -> 16 (2^3 = 8x réduction)
# Donc la dernière carte de caractéristiques est de taille (128, 16, 16)
self.fc1 = nn.Linear(128 * 16 * 16, 128)
self.fc2 = nn.Linear(128, 2)
def forward(self, x):
"""
Décrit le passage des données à travers le réseau.
"""
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = self.pool(F.relu(self.conv3(x)))
# Aplatir avant la partie fully-connected
x = x.view(-1, 128 * 16 * 16)
x = F.relu(self.fc1(x))
x = self.fc2(x)
return x
# Initialisation du modèle
model = CNN_Model()
print(model)
CNN_Model( (conv1): Conv2d(3, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (pool): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) (conv2): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (conv3): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (fc1): Linear(in_features=32768, out_features=128, bias=True) (fc2): Linear(in_features=128, out_features=2, bias=True) )
Le modèle CNN_Model implémente un réseau de neurones convolutifs conçu pour la détection des fissures dans des images de dimensions $(3, 128, 128)$. Il se compose de trois couches convolutionnelles, chacune suivie d'une activation ReLU et d'un pooling de type MaxPooling avec une fenêtre de taille $2 \times 2$, réduisant progressivement les dimensions spatiales des cartes de caractéristiques de $128 \times 128$ à $16 \times 16$. Les nombres de filtres augmentent de $32$ à $128$, permettant une extraction hiérarchique des caractéristiques. La sortie des couches convolutionnelles est aplatie en un vecteur de taille $32\,768$ (correspondant à $128 \times 16 \times 16$), qui est ensuite passé à travers deux couches entièrement connectées : une première avec $128$ neurones, et une dernière produisant une sortie de dimension $2$ pour la classification binaire. Ce modèle, bien que relativement simple, est structuré pour capturer efficacement les motifs visuels des fissures tout en maintenant un équilibre entre complexité et efficacité computationnelle.
Entraînement du modèle¶
Dans cette étape, nous allons entraîner notre modèle CNN pour détecter les fissures dans les images. L'entraînement consistera à ajuster les paramètres du modèle afin qu'il puisse apprendre à classer correctement les images en fonction des caractéristiques visuelles pertinentes. Nous mettrons en place un pipeline structuré pour entraîner le modèle sur l'ensemble d'entraînement et évaluer ses performances sur l'ensemble de validation.
Paramètres d'entraînement¶
Pour l'entraînement, nous définirons les paramètres suivants :
- Nombre d'époques : Le modèle sera entraîné sur un nombre fixe d'époques, fixé à 10, afin de garantir une convergence adéquate.
- Taux d'apprentissage : Nous utiliserons un taux d'apprentissage initial de 0.001 pour contrôler la vitesse d'optimisation.
- Fonction de perte : La fonction de perte choisie sera la cross-entropy (nn.CrossEntropyLoss), adaptée aux tâches de classification binaire.
- Optimiseur : Nous utiliserons l'optimiseur Adam (optim.Adam) pour sa capacité à adapter dynamiquement les taux d'apprentissage et accélérer la convergence.
- Device : L'entraînement sera effectué sur un GPU si disponible, sinon sur un CPU.
Méthode d'entraînement¶
L'entraînement sera organisé en plusieurs passes (epochs) sur l'ensemble d'entraînement. Chaque passe inclura :
- Propagation avant (forward pass) : Les images seront passées à travers le modèle pour produire des prédictions.
- Calcul de la perte : La différence entre les prédictions et les étiquettes réelles sera mesurée à l'aide de la fonction de perte.
- Rétropropagation (backward pass) : Le gradient de la perte par rapport aux paramètres du modèle sera calculé.
- Mise à jour des paramètres : Les paramètres du modèle seront ajustés à l'aide de l'optimiseur pour minimiser la perte.
Nous afficherons régulièrement les métriques intermédiaires (perte et accuracy) pendant l'entraînement pour suivre la progression du modèle.
Évaluation sur l'ensemble de validation¶
À la fin de chaque époque, nous évaluerons les performances du modèle sur l'ensemble de validation. Cette étape nous permettra de mesurer la capacité du modèle à généraliser sur des données inédites. Pendant cette évaluation, le modèle sera configuré en mode évaluation (eval mode), ce qui désactivera les mécanismes tels que le dropout. Les métriques calculées incluront :
- Perte moyenne : Une indication de la précision des prédictions.
- Accuracy moyenne : Le pourcentage d'exemples correctement classés.
Affichage des métriques¶
Nous afficherons à chaque époque les performances globales, incluant :
- La perte et l'accuracy sur l'ensemble d'entraînement.
- La perte et l'accuracy sur l'ensemble de validation.
Ces résultats permettront d'identifier d'éventuels problèmes, comme le surapprentissage ou une stagnation des performances, et d'apporter les ajustements nécessaires.
Objectifs de l'entraînement¶
À l'issue de cette étape, nous espérons obtenir un modèle capable d'extraire efficacement les caractéristiques visuelles des images et de distinguer avec précision les fissures des surfaces intactes. Les performances observées sur l'ensemble de validation fourniront une indication préliminaire de la capacité de généralisation du modèle, avant une évaluation finale sur l'ensemble de test.
Le code suivant permet de réaliser cette tâche.
def calculate_accuracy(predictions, labels):
"""
Calcule l'accuracy entre les prédictions et les étiquettes réelles.
Parameters
----------
predictions : torch.Tensor
Sorties du modèle de taille (batch_size, num_classes).
labels : torch.Tensor
Labels réels de taille (batch_size).
Returns
-------
accuracy : float
Pourcentage d'exemples correctement classés dans ce batch.
"""
# Obtenir les prédictions avec la probabilité maximale
_, predicted = torch.max(predictions, 1)
correct = (predicted == labels).sum().item()
# Calculer la précision
accuracy = 100.0 * correct / labels.size(0)
return accuracy
def train_one_epoch(model, train_loader, criterion, optimizer, device, epoch, total_epochs, print_freq=100):
"""
Effectue une passe d'entraînement (une époque) sur l'ensemble des données d'entraînement.
Parameters
----------
model : nn.Module
Le modèle à entraîner.
train_loader : DataLoader
DataLoader pour les données d'entraînement.
criterion : nn.Module
Fonction de perte (ex: nn.CrossEntropyLoss).
optimizer : torch.optim.Optimizer
Optimiseur pour mettre à jour les paramètres du modèle.
device : torch.device
Dispositif (CPU ou GPU) sur lequel exécuter le modèle.
epoch : int
Numéro de l'époque en cours.
total_epochs : int
Nombre total d'époques prévues.
print_freq : int, optional
Fréquence (en nombre de batchs) à laquelle afficher les informations pendant l'entraînement.
Returns
-------
avg_loss : float
Perte moyenne sur l'époque.
avg_acc : float
Accuracy moyenne sur l'époque.
"""
# Mode d'entraînement
model.train()
# Variables pour suivre les métriques
running_loss = 0.0
running_correct = 0
total_samples = 0
for batch_idx, (images, labels) in enumerate(train_loader, start=1):
# Chargement des données sur le device (CPU/GPU)
images = images.to(device)
labels = labels.to(device)
# Étape forward
outputs = model(images)
loss = criterion(outputs, labels)
# Étape backward
optimizer.zero_grad()
loss.backward()
optimizer.step()
# Calcul de la précision pour ce batch
_, predicted = torch.max(outputs, 1)
correct = (predicted == labels).sum().item()
batch_acc = 100.0 * correct / labels.size(0)
# Mise à jour des métriques globales
running_loss += loss.item() * labels.size(0)
running_correct += correct
total_samples += labels.size(0)
# Affichage des informations périodiquement
if batch_idx % print_freq == 0:
avg_batch_loss = running_loss / total_samples
avg_batch_acc = 100.0 * running_correct / total_samples
print(f"Epoch [{epoch}/{total_epochs}], Step [{batch_idx}/{len(train_loader)}], "
f"Loss: {avg_batch_loss:.4f}, Accuracy: {avg_batch_acc:.2f}%")
# Calcul des moyennes pour l'époque
avg_loss = running_loss / total_samples
avg_acc = 100.0 * running_correct / total_samples
return avg_loss, avg_acc
def evaluate(model, val_loader, criterion, device):
"""
Évalue le modèle sur l'ensemble de validation.
Parameters
----------
model : nn.Module
Le modèle à évaluer.
val_loader : DataLoader
DataLoader pour les données de validation.
criterion : nn.Module
Fonction de perte.
device : torch.device
Dispositif (CPU ou GPU).
Returns
-------
avg_loss : float
Perte moyenne sur l'ensemble de validation.
avg_acc : float
Accuracy moyenne sur l'ensemble de validation.
"""
# Mode évaluation (désactive le calcul des gradients)
model.eval()
# Variables pour suivre les métriques
running_loss = 0.0
running_correct = 0
total_samples = 0
with torch.no_grad(): # Pas de backpropagation
for images, labels in val_loader:
# Chargement des données sur le device (CPU/GPU)
images = images.to(device)
labels = labels.to(device)
# Étape forward
outputs = model(images)
loss = criterion(outputs, labels)
# Calcul de la précision pour ce batch
_, predicted = torch.max(outputs, 1)
correct = (predicted == labels).sum().item()
# Mise à jour des métriques globales
running_loss += loss.item() * labels.size(0)
running_correct += correct
total_samples += labels.size(0)
# Calcul des moyennes pour l'ensemble de validation
avg_loss = running_loss / total_samples
avg_acc = 100.0 * running_correct / total_samples
return avg_loss, avg_acc
# Paramètres d'entraînement
learning_rate = 0.001
num_epochs = 10
# Définition du device (GPU si disponible, sinon CPU)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model.to(device)
# Définition de la fonction de perte et de l'optimiseur
criterion = torch.nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
# Listes pour stocker les métriques pour les graphiques
train_losses = []
val_losses = []
train_accuracies = []
val_accuracies = []
print("Début de l'entraînement...")
# Boucle d'entraînement sur plusieurs époques
for epoch in range(1, num_epochs + 1):
# Entraînement du modèle sur l'ensemble d'entraînement
train_loss, train_acc = train_one_epoch(model, train_loader, criterion, optimizer, device, epoch, num_epochs, print_freq=500)
train_losses.append(train_loss)
train_accuracies.append(train_acc)
# Évaluation du modèle sur l'ensemble de validation
val_loss, val_acc = evaluate(model, val_loader, criterion, device)
val_losses.append(val_loss)
val_accuracies.append(val_acc)
# Affichage des métriques globales pour chaque époque
print(f"Epoch {epoch}/{num_epochs}, "
f"Train Loss: {train_loss:.4f}, Train Accuracy: {train_acc:.2f}%, "
f"Val Loss: {val_loss:.4f}, Val Accuracy: {val_acc:.2f}%\n")
Début de l'entraînement... Epoch [1/10], Step [500/750], Loss: 0.0759, Accuracy: 97.72% Epoch 1/10, Train Loss: 0.0591, Train Accuracy: 98.23%, Val Loss: 0.0257, Val Accuracy: 99.15% Epoch [2/10], Step [500/750], Loss: 0.0222, Accuracy: 99.32% Epoch 2/10, Train Loss: 0.0228, Train Accuracy: 99.30%, Val Loss: 0.0200, Val Accuracy: 99.38% Epoch [3/10], Step [500/750], Loss: 0.0133, Accuracy: 99.58% Epoch 3/10, Train Loss: 0.0132, Train Accuracy: 99.59%, Val Loss: 0.0141, Val Accuracy: 99.62% Epoch [4/10], Step [500/750], Loss: 0.0112, Accuracy: 99.71% Epoch 4/10, Train Loss: 0.0105, Train Accuracy: 99.72%, Val Loss: 0.0153, Val Accuracy: 99.45% Epoch [5/10], Step [500/750], Loss: 0.0071, Accuracy: 99.81% Epoch 5/10, Train Loss: 0.0088, Train Accuracy: 99.74%, Val Loss: 0.0120, Val Accuracy: 99.60% Epoch [6/10], Step [500/750], Loss: 0.0086, Accuracy: 99.76% Epoch 6/10, Train Loss: 0.0069, Train Accuracy: 99.80%, Val Loss: 0.0137, Val Accuracy: 99.70% Epoch [7/10], Step [500/750], Loss: 0.0018, Accuracy: 99.94% Epoch 7/10, Train Loss: 0.0036, Train Accuracy: 99.90%, Val Loss: 0.0147, Val Accuracy: 99.70% Epoch [8/10], Step [500/750], Loss: 0.0056, Accuracy: 99.86% Epoch 8/10, Train Loss: 0.0078, Train Accuracy: 99.81%, Val Loss: 0.0181, Val Accuracy: 99.65% Epoch [9/10], Step [500/750], Loss: 0.0036, Accuracy: 99.91% Epoch 9/10, Train Loss: 0.0030, Train Accuracy: 99.92%, Val Loss: 0.0248, Val Accuracy: 99.63% Epoch [10/10], Step [500/750], Loss: 0.0018, Accuracy: 99.95% Epoch 10/10, Train Loss: 0.0022, Train Accuracy: 99.95%, Val Loss: 0.0174, Val Accuracy: 99.75%
Analyse
Les résultats obtenus tout au long des 10 époques montrent une amélioration continue des performances du modèle en termes de pertes et de précision :
Évolution des performances d'entraînement :
Les pertes d'entraînement diminuent progressivement, passant de $5.91\%$ à $0.22\%$, tandis que la précision d'entraînement augmente régulièrement de $98.23\%$ (époque 1) à $99.95\%$ (époque 10). Cette tendance traduit une optimisation efficace des paramètres du modèle.Performances de validation :
Les pertes de validation restent globalement faibles, oscillant entre $2.57\%$ (époque 1) et $1.74\%$ (époque 10), avec une précision de validation qui reste stable et élevée, allant de $99.15\%$ à $99.75\%$. Bien que des fluctuations légères soient observées (par exemple, une hausse des pertes à l'époque 9 avec $2.48\%$), ces variations n'ont pas d'impact significatif sur la robustesse globale du modèle.Généralisation et sur-apprentissage :
La faible différence entre les pertes et précisions d'entraînement et de validation suggère que le modèle généralise bien sur les données de validation. Les fluctuations des pertes de validation vers la fin (par exemple, $1.81\%$ à l'époque 8 ou $2.48\%$ à l'époque 9) ne semblent pas indiquer de sur-apprentissage notable, étant donné que les performances globales restent excellentes.Performances globales :
À la fin de l'entraînement, le modèle atteint une précision de validation de $99.75\%$ et une perte de validation de $1.74\%$. Ces résultats témoignent d'une performance remarquable, avec une capacité de généralisation robuste et des prédictions fiables sur les données non vues.
En conclusion, le modèle démontre une forte aptitude à la tâche grâce à une optimisation stable, une excellente capacité de généralisation et des précisions élevées sur les données de validation, le rendant particulièrement adapté à l'application cible.
Évaluation du modèle¶
Évaluation sur les données de validation¶
Après l'entraînement du modèle, il est crucial d'évaluer ses performances sur des ensembles de données de validation et de test. Cette évaluation nous permettra de mesurer la capacité du modèle à généraliser sur des données inédites et de vérifier son aptitude à classifier correctement les images en fonction de la présence ou non de fissures.
Courbes d'entraînement¶
Pour mieux comprendre la progression de l'entraînement, nous allons tracer des courbes qui illustrent :
- L'évolution des pertes (loss) sur les ensembles d'entraînement et de validation.
- L'évolution des précisions (accuracy) pour ces mêmes ensembles.
Ces graphiques permettent d’identifier des phénomènes tels que la convergence, le surapprentissage (overfitting) ou le sous-apprentissage (underfitting) en fonction des époques.
# Fonction pour afficher les courbes de pertes et de précisions
def plot_training_curves(epochs, train_losses, val_losses, train_accuracies, val_accuracies):
"""
Génère les graphiques des courbes de perte et de précision pour l'entraînement et la validation.
Parameters
----------
epochs : int
Nombre total d'époques.
train_losses : list
Liste des pertes moyennes par époque pour l'entraînement.
val_losses : list
Liste des pertes moyennes par époque pour la validation.
train_accuracies : list
Liste des précisions moyennes par époque pour l'entraînement.
val_accuracies : list
Liste des précisions moyennes par époque pour la validation.
"""
plt.figure(figsize=(12, 6))
# Courbe des pertes
plt.subplot(1, 2, 1)
plt.plot(range(1, epochs + 1), train_losses, label='Pertes Entraînement', marker='o')
plt.plot(range(1, epochs + 1), val_losses, label='Pertes Validation', marker='o')
plt.title("Courbe des Pertes")
plt.xlabel("Époques")
plt.ylabel("Perte")
plt.legend()
plt.grid()
# Courbe des précisions
plt.subplot(1, 2, 2)
plt.plot(range(1, epochs + 1), train_accuracies, label='Précision Entraînement', marker='o')
plt.plot(range(1, epochs + 1), val_accuracies, label='Précision Validation', marker='o')
plt.title("Courbe des Précisions")
plt.xlabel("Époques")
plt.ylabel("Précision (%)")
plt.legend()
plt.grid()
plt.tight_layout()
plt.show()
# Appel des fonctions pour afficher les graphiques
print("Affichage des courbes d'entraînement...")
plot_training_curves(num_epochs, train_losses, val_losses, train_accuracies, val_accuracies)
Affichage des courbes d'entraînement...
Analyse des graphiques
Courbe de Pertes¶
- La perte d'entraînement diminue régulièrement et atteint des valeurs très faibles, reflétant une réduction efficace des erreurs sur les données d'entraînement.
- La perte de validation, bien qu'initialement plus élevée, reste faible avec des oscillations légères à partir de la cinquième époque. Ces fluctuations pourraient refléter la complexité croissante dans l'apprentissage des données de validation.
Courbe de Précisions¶
- La précision d'entraînement augmente rapidement et atteint presque $100\%$ dès les dernières époques, confirmant que le modèle s'adapte parfaitement aux données d'entraînement.
- La précision de validation est légèrement inférieure mais reste très proche, culminant autour de $99.75\%$, ce qui démontre une bonne généralisation.
Interprétation¶
- Convergence rapide : Les courbes montrent que le modèle atteint rapidement une convergence, validant les choix d'hyperparamètres et d'architecture.
- Stabilité : Les faibles pertes et les précisions élevées des deux phases indiquent une optimisation efficace et un bon équilibre entre sur-apprentissage et généralisation.
- Robustesse : Les résultats confirment que le modèle est bien adapté à la tâche de classification, avec une performance constante sur l'entraînement et la validation.
Conclusion¶
Le modèle continue de démontrer une excellente performance, avec des pertes faibles et des précisions élevées pour les phases d'entraînement et de validation. Ces résultats corroborent les observations précédentes et montrent que le modèle est bien optimisé pour généraliser efficacement sur des données inédites.
Évaluation sur les données de test¶
Nous allons également générer un rapport complet des performances du modèle sur les données de test. Ce rapport inclura :
- Matrice de confusion : Une visualisation qui détaille les classifications correctes et incorrectes pour chaque classe (positive et négative).
- Courbe ROC et AUC : Une représentation de la capacité du modèle à séparer les classes avec une mesure quantitative de la discrimination via l'aire sous la courbe (AUC).
- Distribution des probabilités : Une analyse des probabilités prédites par le modèle, mettant en évidence la confiance des prédictions.
- Rapport de classification : Un résumé des métriques clés, telles que la précision (precision), le rappel (recall) et le F1-score, pour chaque classe ainsi que pour l'ensemble global.
Objectifs de l'évaluation
L'évaluation a pour objectif de :
- Vérifier si le modèle atteint des performances cohérentes entre les ensembles d'entraînement, de validation et de test.
- Identifier les forces et les faiblesses du modèle, comme des déséquilibres ou des erreurs récurrentes.
- Confirmer que le modèle est prêt pour une utilisation dans un environnement réel ou pour une mise à l'échelle.
Ces étapes nous fourniront une analyse approfondie des performances et guideront d’éventuelles améliorations pour des itérations futures. Le code suivant permet de réaliser cette évalution sur les données test.
def generate_report(model, test_loader, device):
"""
Génère un rapport complet des performances du modèle avec :
- Matrice de confusion
- Courbe ROC et AUC
- Rapport de classification (heatmap)
- Distribution des probabilités prédites
Parameters
----------
model : nn.Module
Le modèle à tester.
test_loader : DataLoader
DataLoader pour les données de test.
device : torch.device
Dispositif sur lequel exécuter l'évaluation.
"""
model.eval()
all_labels = []
all_predictions = []
all_probs = []
with torch.no_grad():
for images, labels in test_loader:
images, labels = images.to(device), labels.to(device)
# Prédictions
outputs = model(images)
probabilities = F.softmax(outputs, dim=1) # Convertir en probabilités
_, predicted = torch.max(outputs, 1)
# Sauvegarde des étiquettes et les prédictions
all_labels.extend(labels.cpu().numpy())
all_predictions.extend(predicted.cpu().numpy())
all_probs.extend(probabilities.cpu().numpy())
# Convertsion des résultats en numpy array
all_labels = np.array(all_labels)
all_predictions = np.array(all_predictions)
all_probs = np.array(all_probs)
# Initialisation des graphiques
fig, axs = plt.subplots(2, 2, figsize=(15, 12))
# Matrice de confusion
conf_matrix = confusion_matrix(all_labels, all_predictions)
sns.heatmap(conf_matrix, annot=True, fmt='d', cmap='Blues', xticklabels=['Negative', 'Positive'], yticklabels=['Negative', 'Positive'], ax=axs[0, 1])
axs[0, 1].set_title("Matrice de Confusion")
axs[0, 1].set_xlabel("Prédictions")
axs[0, 1].set_ylabel("Vérités")
# Courbe ROC et AUC
fpr, tpr, _ = roc_curve(all_labels, all_probs[:, 1]) # Utiliser les probabilités de la classe Positive
roc_auc = auc(fpr, tpr)
axs[0, 0].plot(fpr, tpr, color='blue', label=f'ROC Curve (AUC = {roc_auc:.2f})')
axs[0, 0].plot([0, 1], [0, 1], color='gray', linestyle='--') # Diagonale
axs[0, 0].set_title("Courbe ROC")
axs[0, 0].set_xlabel("False Positive Rate")
axs[0, 0].set_ylabel("True Positive Rate")
axs[0, 0].legend(loc="lower right")
# Distribution des probabilités prédites
sns.histplot(all_probs[:, 1], kde=True, color='blue', bins=30, ax=axs[1, 0])
axs[1, 0].set_title("Distribution des probabilités prédites")
axs[1, 0].set_xlabel("Probabilité prédite pour la classe 1")
axs[1, 0].set_ylabel("Fréquence")
# Rapport de classification
report = classification_report(all_labels, all_predictions, target_names=['Negative', 'Positive'], output_dict=True)
report_df = pd.DataFrame(report).iloc[:-1, :].T # Exclure l'accuracy globale
sns.heatmap(report_df, annot=True, cmap='coolwarm', cbar=False, ax=axs[1, 1])
axs[1, 1].set_title("Rapport de Classification")
axs[1, 1].set_xlabel("Métriques")
axs[1, 1].set_ylabel("Classes")
# Ajustement et affichage
plt.tight_layout()
plt.show()
# Appel de la fonction pour générer le rapport
print("Génération du rapport complet...")
generate_report(model, test_loader, device)
Génération du rapport complet...
Analyse des Résultats
Courbe ROC et AUC La courbe ROC affiche une AUC de $1.00$, démontrant une capacité parfaite du modèle à distinguer entre les classes "Positive" et "Negative" sur l'ensemble de test.
Matrice de Confusion Parmi $10,000$ échantillons, le modèle classifie correctement $4,974$ instances "Negative" et $4,993$ instances "Positive", avec seulement $33$ erreurs ($26$ faux positifs et $7$ faux négatifs). Ces résultats illustrent une précision quasi-parfaite.
Distribution des Probabilités Prédites La distribution des probabilités montre que les prédictions sont principalement concentrées autour de $0$ et $1$, indiquant une forte confiance du modèle dans ses décisions.
Rapport de Classification Les métriques de classification, y compris la précision, le rappel et le F1-score, atteignent $1.00$ pour les deux classes. Cela confirme un excellent équilibre et une grande exactitude dans les prédictions.
Conclusion : Le modèle CNN démontre une performance exceptionnelle sur les données de test, avec une généralisation solide et des erreurs négligeables. Ces résultats confirment la robustesse et la fiabilité du modèle pour une utilisation pratique. Toutefois, il serait judicieux de valider ces performances sur des données entièrement nouvelles pour renforcer la confiance dans son efficacité.
Conclusion¶
Ce projet a exploré la classification automatique d'images pour détecter les fissures dans des matériaux en béton, en s'appuyant sur le dataset Kaggle Surface Crack Detection. À travers des techniques avancées de vision par ordinateur et de deep learning, notamment des réseaux de neurones convolutionnels (CNN), nous avons pu atteindre des performances remarquables, avec des précisions élevées dépassant $99\%$ sur les ensembles de validation et de test.
La division stratégique du dataset, avec $24\,000$ images pour l’entraînement, $6\,000$ pour la validation, et $10\,000$ pour le test, a permis de garantir une répartition équilibrée des classes "Positive" et "Negative". Cette structure a assuré un entraînement robuste et une évaluation fiable des performances du modèle.
Les résultats obtenus, comme des courbes ROC proches de la perfection ($AUC = 1.00$), des matrices de confusion avec un nombre d’erreurs négligeable, et des métriques de classification parfaites (précision, rappel, F1-score), démontrent une excellente généralisation du modèle. Ces performances confirment que l'approche développée est bien adaptée à cette tâche spécifique.
En conclusion, ce projet a non seulement validé l'efficacité des CNN pour la classification d'images de fissures, mais a également mis en évidence l'importance d'une préparation minutieuse des données et d'un choix rigoureux des hyperparamètres. Les résultats obtenus sont prometteurs pour des applications industrielles concrètes, bien que des validations sur des données réelles supplémentaires soient recommandées avant leur mise en production.