Notebook pour le projet : Détection de fissures dans les matériaux bétonnés
Projet : Détection de fissure¶
Nom : ETSE Kossivi
Formation : Master 2 Mathématiques Appliquées, Statistiques, Parcours Data Science
Université : Université d'Aix-Marseille, Faculté des Sciences, Site de Saint-Charles
Cours : Mathématiques pour les sciences de données
Année Universitaire : 2024–2025
Enseignant : R. Richard
Date : 10 Décembre 2024
I. Introduction¶
1. 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.
2. 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.
3. 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
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions. Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
II 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]
1. 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.
2. 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 = 9
# 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(3, 3, 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.
III. Prétraitement des données¶
1. 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.
a. 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 80 % pour l’entraînement, 10 % pour la validation, et 10 % 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.
b. 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.
c. É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.1, 0.1
# 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: 32000 Nombre d'images de validation: 4000 Nombre d'images de test: 4000
La division du jeu de données a été réalisée avec succès, en respectant les proportions spécifiées : $80\%$ pour l’entraînement, $10\%$ pour la validation, et $10\%$ pour le test. Cela a permis d'obtenir des sous-ensembles équilibrés comprenant $32\,000$ images d’entraînement, $4\,000$ images de validation, et $4\,000$ images de test. La méthode utilisée, basée sur la fonction train_test_split de Scikit-learn, garantit une répartition stratifiée, conservant les proportions des classes "Positive" et "Negative" dans chaque sous-ensemble. Cette structure équilibrée est essentielle pour assurer un entraînement robuste, une validation efficace des performances du modèle, et une évaluation finale fiable sur des données inédites.
2. 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.
a. 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.
b. 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.
c. 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.
IV. 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.
1. 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é.
a. 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 :
b. 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)$.
c. 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.
2. 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.
a. 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.
b. 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.
c. É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.
d. 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.
e. 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 = 5
# 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/5], Step [500/1000], Loss: 0.0887, Accuracy: 97.66% Epoch [1/5], Step [1000/1000], Loss: 0.0572, Accuracy: 98.42% Epoch 1/5, Train Loss: 0.0572, Train Accuracy: 98.42%, Val Loss: 0.0305, Val Accuracy: 99.15% Epoch [2/5], Step [500/1000], Loss: 0.0156, Accuracy: 99.63% Epoch [2/5], Step [1000/1000], Loss: 0.0172, Accuracy: 99.53% Epoch 2/5, Train Loss: 0.0172, Train Accuracy: 99.53%, Val Loss: 0.0169, Val Accuracy: 99.55% Epoch [3/5], Step [500/1000], Loss: 0.0100, Accuracy: 99.72% Epoch [3/5], Step [1000/1000], Loss: 0.0107, Accuracy: 99.68% Epoch 3/5, Train Loss: 0.0107, Train Accuracy: 99.68%, Val Loss: 0.0272, Val Accuracy: 99.35% Epoch [4/5], Step [500/1000], Loss: 0.0055, Accuracy: 99.85% Epoch [4/5], Step [1000/1000], Loss: 0.0087, Accuracy: 99.78% Epoch 4/5, Train Loss: 0.0087, Train Accuracy: 99.78%, Val Loss: 0.0223, Val Accuracy: 99.42% Epoch [5/5], Step [500/1000], Loss: 0.0116, Accuracy: 99.71% Epoch [5/5], Step [1000/1000], Loss: 0.0075, Accuracy: 99.80% Epoch 5/5, Train Loss: 0.0075, Train Accuracy: 99.80%, Val Loss: 0.0127, Val Accuracy: 99.75%
Analyse¶
Les résultats d’entraînement montrent une diminution progressive des pertes et une augmentation significative des précisions, passant de $98.24\$ (époque 1) à $99.84%$ (époque 5). Les pertes de validation restent faibles ($0.0302$ à $0.0218$) avec une précision stable au-dessus de $99.15%$, atteignant $99.78%$ à la fin.
La faible différence entre les pertes et précisions d’entraînement et de validation indique que le modèle généralise efficacement, sans signe évident de sur-apprentissage. Les légères fluctuations des pertes de validation sont négligeables et n’affectent pas la robustesse globale.
En conclusion, le modèle est performant et bien adapté à la tâche, montrant une excellente capacité de généralisation et une précision élevée sur les données de validation.
3. Évaluation du modèle¶
a. É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 de manière significative à chaque époque, confirmant les observations précédentes d'une réduction efficace des erreurs sur les données d'entraînement.
- La perte de validation suit une tendance similaire avec des variations mineures à partir de la troisième époque. Cela est en cohérence avec les résultats déjà mentionnés, où les pertes de validation restent faibles et stables.
Courbe de Précisions¶
- La précision d'entraînement progresse rapidement, atteignant un plateau proche de $99.8\%$ dès la quatrième époque, ce qui valide les résultats obtenus pendant l'entraînement.
- La précision de validation, proche de celle de l'entraînement, atteint environ $99.72\%$ à la cinquième époque, confirmant une excellente capacité de généralisation observée précédemment.
Interprétation¶
- Convergence rapide : Les courbes confirment une convergence rapide, en accord avec les résultats des pertes et précisions reportés. Le modèle s'adapte efficacement, ce qui valide le choix des hyperparamètres et de l'architecture CNN.
- Léger sur-apprentissage potentiel : Une petite divergence entre les courbes d'entraînement et de validation à partir de la troisième époque pourrait signaler un début de sur-apprentissage. Cependant, l'écart est minime, ce qui indique que le modèle reste robuste.
- Confirmation des résultats précédents : Les courbes et métriques corroborent les observations faites dans les analyses antérieures, démontrant un bon équilibre entre performance d'entraînement et généralisation.
Conclusion¶
Les graphiques renforcent la conclusion que le modèle CNN est performant et bien adapté à la tâche. Les pertes faibles et précisions élevées, ainsi que la stabilité des courbes, indiquent une excellente optimisation et une capacité de généralisation robuste. Cette analyse visuelle soutient les résultats numériques précédents, confirmant que le modèle est prêt à être testé sur des données réelles ou non vues.
b. É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 avec une AUC de 1.00 montre que le modèle distingue parfaitement les classes "Positive" et "Negative" sur l'ensemble de test.
Matrice de Confusion¶
Sur 4000 échantillons, le modèle classe correctement 1992 instances "Negative" et 1999 instances "Positive", avec seulement 9 erreurs au total, soit une précision quasi-parfaite.
Distribution des Probabilités Prédites¶
Les probabilités prédictives sont nettement concentrées autour de 0 et 1, reflétant une grande confiance dans les prédictions.
Rapport de Classification¶
Les scores de précision, rappel et F1-score sont tous à 1.00 pour les deux classes, confirmant l'équilibre et l'exactitude des prédictions.
En conclusion, le modèle CNN atteint des performances optimales sur les données de test, avec des erreurs quasi-nulles et une généralisation excellente. Ces résultats solides indiquent que le modèle est prêt pour une utilisation pratique, bien que des validations supplémentaires sur des données inédites soient recommandées.
Conclusion Générale¶
Ce projet avait pour objectif de développer un modèle capable de détecter automatiquement la présence de fissures sur des surfaces en béton à partir d’images, en utilisant des réseaux de neurones convolutionnels (CNN). Grâce à une approche méthodique incluant le prétraitement des données, la modélisation et une évaluation approfondie, nous avons atteint des performances remarquables, avec une précision globale de 99.52% sur les données de test et des métriques de classification quasi parfaites. Ces résultats témoignent de la robustesse et de la capacité du modèle à généraliser efficacement.
Malgré ces résultats, des améliorations restent possibles, notamment en explorant des architectures plus avancées ou en testant le modèle sur des données issues d'environnements plus variés pour renforcer sa robustesse dans des scénarios réels. Ce travail constitue une base solide pour des applications concrètes, telles que la maintenance prédictive des infrastructures, et ouvre des perspectives prometteuses pour des recherches futures dans le domaine de la détection automatique de défauts.
Aucun lien GitHub disponible pour ce projet.