Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • main
1 result

Target

Select target project
  • edelland/mod_4_6-td1
  • tdenis/mod_4_6-td1
2 results
Select Git revision
  • main
1 result
Show changes
Commits on Source (19)
Showing with 445 additions and 0 deletions
# Rapport : Classification d'Images
## Introduction
Ce TP à pour objectif le développement d'un programme complet de classification d'images. On développe deux model comme le sujet le demande : les k plus proches voisins (k-NN) et les réseaux de neurones (NN). Le jeu de données utilisé est CIFAR-10 (60 000 images couleur de taille 32x32 réparties en 10 classes)
## Préparation des Données CIFAR
Les données CIFAR-10 ont été chargé et préparé à l'aide de plusieurs fonctions utilitaires :
- `read_cifar_batch` : Lecture d'un batch unique de données CIFAR.
- `read_cifar` : Lecture de tous les batches (5 batches d'entraînement et 1 batch de test).
- `split_dataset` : Division aléatoire des données en ensembles d'entraînement et de test.
On place dans le main de read_cifar.py un test pour voir si on arrive bien a lire et split , les sortie sont satisfaisante :
data shape: (60000, 3072)
labels shape: (60000,)
Training shape: (54000, 3072)
Testing shape: (6000, 3072)
## Classification k-NN
L'implémentation du classificateur k-NN repose sur plusieurs fonctions demandé dans le README.md :
- `distance_matrix` : Calcul de la matrice des distances euclidiennes L2 entre deux matrices. Cette implémentation utilise uniquement des manipulations matricielles pour optimiser les performances.
- `knn_predict` : Prédiction des étiquettes pour les données de test en fonction des k plus proches voisins.
- `evaluate_knn` : Évaluation de la précision du modèle sur un ensemble de test.
On effectue finalement dans le main de knn.py un test avec 20 valeures de k différente pour juger de l'évolution de l'accuracy par rapport a k. La meilleure performance est obtenue avec k=1, atteignant environ 35.7% de précision. Or les valeurs pour 1 et 2 semble étonnante on se serait attendu a une courbe en cloche, on a éssayé de corrigé le tri mais impossible on a touojours ce saut pour k=1 et 2. Cependant observe ensuite une évolution assez cohérente, avec une max vers k=5 puis une diminution de l'accuracy. Cette diminution de performance avec l'augmentation de k suggère que les classes sont relativement bien séparées localement, mais que l'augmentation du nombre de voisins introduit du bruit dans la classification.
## Réseau de Neurones Artificiel
Le développement du classificateur basé sur un perceptron multicouche (MLP) a nécessité plusieurs étapes, notamment la compréhension théorique de la rétropropagation du gradient.
### Formules de Rétropropagation
On présente ici les formules que l'on nous demand de fournir :
1. La dérivée de la fonction sigmoïde est donnée par : $\sigma' = \sigma \times (1-\sigma)$
2. $\frac{\partial C}{\partial A^{(2)}} = \frac{2}{N_{out}}(A^{(2)} - Y)$
3. $\frac{\partial C}{\partial Z^{(2)}} = \frac{\partial C}{\partial A^{(2)}} \times A^{(2)} \times (1-A^{(2)})$
4. $\frac{\partial C}{\partial W^{(2)}} = [A^{(1)}]^T \times \frac{\partial C}{\partial Z^{(2)}}$
5. $\frac{\partial C}{\partial B^{(2)}} = \frac{\partial C}{\partial Z^{(2)}}$
6. $\frac{\partial C}{\partial A^{(1)}} = \frac{\partial C}{\partial Z^{(2)}} \times [W^{(2)}]^T$
7. $\frac{\partial C}{\partial Z^{(1)}} = \frac{\partial C}{\partial A^{(1)}} \times A^{(1)} \times (1-A^{(1)})$
8. $\frac{\partial C}{\partial W^{(1)}} = [A^{(0)}]^T \times \frac{\partial C}{\partial Z^{(1)}}$
9. $\frac{\partial C}{\partial B^{(1)}} = \frac{\partial C}{\partial Z^{(1)}}$
Ensuite on passe a l'implémentation des fonctions demandé :
- `learn_once_mse` : Apprentissage avec l'erreur quadratique moyenne.
- `learn_once_cross_entropy` : Apprentissage avec l'entropie croisée.
- `train_mlp` : Entraînement complet du réseau.
- `test_mlp` : Évaluation sur l'ensemble de test.
Dans le main de mlp.py on commence par charger les données puis on les normalise, puis split. Ensuite on met en place le training et on finit par afficher nos résultats
Les résultats montrent une amélioration continue de la précision d'entraînement avec les epochs, atteignant environ 45% après 100 époques. Cette progression régulière indique que le modèle apprend efficacement les patterns des données, bien que la performance finale reste modeste en raison de la complexité du jeu de données CIFAR-10.
## Conclusion
Les deux modèles implémentées montrent des performances différentes sur le jeu de données CIFAR-10. Le k-NN offre une performance de base intéressante mais limitée, tandis que le MLP montre une capacité d'apprentissage progressive avec de meilleures performances finales. Ces résultats soulignent l'importance du choix de l'architecture et des hyperparamètres dans les tâches de classification d'images.
\ No newline at end of file
File added
File added
File added
File added
File added
File added
<meta HTTP-EQUIV="REFRESH" content="0; url=http://www.cs.toronto.edu/~kriz/cifar.html">
File added
import numpy as np
import matplotlib.pyplot as plt
from read_cifar import read_cifar, split_dataset
import os
# Question 1: Compute distance matrix
def distance_matrix(X1: np.ndarray, X2: np.ndarray) -> np.ndarray:
"""
Compute L2 Euclidean distance matrix between two matrices.
Using the formula: (a-b)^2 = a^2 + b^2 - 2ab
Args:
X1: First matrix of shape (n_samples_1, n_features)
X2: Second matrix of shape (n_samples_2, n_features)
Returns:
distances: Matrix of shape (n_samples_1, n_samples_2) containing
pairwise L2 distances
"""
# On calcul les norme carrée de nos vecteurs directement avec numpy
X1_norm = np.sum(X1**2, axis=1)
X2_norm = np.sum(X2**2, axis=1)
# On reshape pour pouvoir effectuer nos calculs matriciel directement
X1_norm = X1_norm.reshape(-1, 1) # Vecteur colonne
X2_norm = X2_norm.reshape(1, -1) # Vecteur ligne
# On calcul la disctance en utilisant direct la formule : (a-b)^2 = a^2 + b^2 - 2ab
distances = X1_norm + X2_norm - 2 * np.dot(X1, X2.T)
# On obtenait parfois des valeurs négative (surement a cause d'erreur numérique du calcul python)
distances = np.maximum(distances, 0)
return np.sqrt(distances)
# Question 2: KNN prediction
def knn_predict(dists: np.ndarray, labels_train: np.ndarray, k: int) -> np.ndarray:
num_test = dists.shape[0] # donne le nombre d'echantillons de test
predictions = np.zeros(num_test, dtype=np.int64) # on sotcke ici les predictions qu'on va faire
# On boucle sur les echantillons de test
for i in range(num_test):
# On récupere les k plus proches voisins direct avec argsort qui permet de chopper les indices qui permetterais un orgre croissant
k_nearest_indices = np.argsort(dists[i])[:k]
k_nearest_labels = labels_train[k_nearest_indices]
# grace a bincount on compte le nombre d'element de chaque classe, et on récupere avec argmax le majoritaire
predictions[i] = np.bincount(k_nearest_labels).argmax()
return predictions
# Question 3: Evaluate KNN classifier
def evaluate_knn(data_train: np.ndarray, labels_train: np.ndarray,
data_test: np.ndarray, labels_test: np.ndarray, k: int) -> float:
# On commence par calculer les distances que l'on place dans notre matrice dists
dists = distance_matrix(data_test, data_train)
# On fait ensuite les prédiction avec knn_predict
predictions = knn_predict(dists, labels_train, k)
# Finalement on calcul notre précision en regardant les elts bien classés
correct = 0
total = len(predictions)
for pred, true in zip(predictions, labels_test):
if pred == true:
correct += 1
accuracy = correct / total
return accuracy
# Question 4: On plot nos accuracy en fonction de k
def plot_accuracy_vs_k(data_train: np.ndarray, labels_train: np.ndarray,
data_test: np.ndarray, labels_test: np.ndarray,
k_values: list) -> None:
accuracies = []
# On boucle sur les valeurs de k choisit et on utilise notre fonction evaluate
for k in k_values:
accuracy = evaluate_knn(data_train, labels_train, data_test, labels_test, k)
accuracies.append(accuracy)
print(f"k={k}: accuracy={accuracy:.4f}")
# On creer notre plot et on le sauvegarde
plt.figure(figsize=(10, 6))
plt.plot(k_values, accuracies, 'bo-')
plt.xlabel('k (number of neighbors)')
plt.ylabel('Accuracy')
plt.title('KNN Classification Accuracy vs k')
plt.grid(True)
os.makedirs('results', exist_ok=True)
plt.savefig('results/knn.png')
plt.close()
if __name__ == "__main__":
# On charge les données CIFAR
cifar_dir = "data/cifar-10-batches-py"
all_data, all_labels = read_cifar(cifar_dir)
# On split en train et test
data_train, labels_train, data_test, labels_test = split_dataset(all_data, all_labels, split=0.9)
# On creer le plot pour étudier l impact de k sur la précision (k allant de 1 a 20)
k_values = list(range(1, 21))
plot_accuracy_vs_k(data_train, labels_train, data_test, labels_test, k_values)
import numpy as np
from read_cifar import read_cifar, split_dataset
import matplotlib.pyplot as plt
import os
# Question 10: Implémentation avec MSE
def learn_once_mse(w1, b1, w2, b2, data, targets, learning_rate):
# On code la Forward pass: propagation des données à travers le réseau
a0 = data # Couche d'entrée
z1 = np.matmul(a0, w1) + b1 # Première couche cachée (pré-activation)
a1 = 1 / (1 + np.exp(-z1)) # Activation sigmoid
z2 = np.matmul(a1, w2) + b2 # Couche de sortie (pré-activation)
a2 = 1 / (1 + np.exp(-z2)) # Activation sigmoid finale
predictions = a2
# On calcul l'erreur quadratique moyenne
loss = np.mean(np.square(predictions - targets))
# On code ensuite la Backward pass: calcul des gradients et mise à jour des poids
dC_da2 = 2 * (predictions - targets) / targets.shape[0] # Dérivée par rapport à la sortie
dC_dz2 = dC_da2 * (a2 * (1 - a2)) # Dérivée de la sigmoid
dC_dw2 = np.matmul(a1.T, dC_dz2) # Gradient pour w2
dC_db2 = np.mean(dC_dz2, axis=0) # Gradient pour b2
# On propage l'erreur vers la première couche
dC_da1 = np.matmul(dC_dz2, w2.T)
dC_dz1 = dC_da1 * (a1 * (1 - a1)) # Dérivée de la sigmoid
dC_dw1 = np.matmul(a0.T, dC_dz1) # Gradient pour w1
dC_db1 = np.mean(dC_dz1, axis=0) # Gradient pour b1
# On met à jour les poids et biais avec le gradient descent
w1 = w1 - learning_rate * dC_dw1
b1 = b1 - learning_rate * dC_db1
w2 = w2 - learning_rate * dC_dw2
b2 = b2 - learning_rate * dC_db2
return w1, b1, w2, b2, loss
# Question 11: One-hot encoding
def one_hot(labels):
n_classes = 10 # CIFAR-10 a 10 classes
return np.eye(n_classes)[labels]
# Question 12: Implémentation avec la Cross-Entropy
def learn_once_cross_entropy(w1, b1, w2, b2, data, labels, learning_rate):
batch_size = data.shape[0]
# On implemente la Forward pass
a0 = data
z1 = np.matmul(a0, w1) + b1
a1 = 1 / (1 + np.exp(-z1))
z2 = np.matmul(a1, w2) + b2
# On implemente notre sofmax
z2 = z2 - np.max(z2, axis=1, keepdims=True)
exp_z2 = np.exp(z2)
a2 = exp_z2 / np.sum(exp_z2, axis=1, keepdims=True)
# One-hot encoding
y = one_hot(labels)
# Cross entropy loss avec réduction correcte
loss = -np.sum(y * np.log(a2 + 1e-15)) / batch_size
# Backward pass avec normalisation correcte
dC_dz2 = (a2 - y) / batch_size
dC_dw2 = np.matmul(a1.T, dC_dz2)
dC_db2 = np.mean(dC_dz2, axis=0)
dC_da1 = np.matmul(dC_dz2, w2.T)
dC_dz1 = dC_da1 * (a1 * (1 - a1))
dC_dw1 = np.matmul(a0.T, dC_dz1)
dC_db1 = np.mean(dC_dz1, axis=0)
# Update weights and biases
w1 = w1 - learning_rate * dC_dw1
b1 = b1 - learning_rate * dC_db1
w2 = w2 - learning_rate * dC_dw2
b2 = b2 - learning_rate * dC_db2
return w1, b1, w2, b2, loss
# Question 13: Training function
def train_mlp(w1, b1, w2, b2, data_train, labels_train, learning_rate, num_epoch):
train_accuracies = []
batch_size = 128 # Taille de batch raisonnable
for epoch in range(num_epoch):
# ON mélange aléatoirement les données à chaque époque
indices = np.random.permutation(len(data_train))
data_train = data_train[indices]
labels_train = labels_train[indices]
epoch_losses = []
# On réalise l'entrainement par batch
for i in range(0, len(data_train), batch_size):
# On récupère notre batch de données + les labels
batch_data = data_train[i:i+batch_size]
batch_labels = labels_train[i:i+batch_size]
# On réalise l'entrainement avec la fonction précédement définit sur le batch
w1, b1, w2, b2, loss = learn_once_cross_entropy(
w1, b1, w2, b2, batch_data, batch_labels, learning_rate)
epoch_losses.append(loss)
# On calcul finalement la précision de notre entrainement
predictions = predict_mlp(w1, b1, w2, b2, data_train) # fonction définit en dessous
# Pour l'accuracy on peut directement faire la moyenne du tableau contenant True si
# la prédiction est vrai (si prediction = label) le mean sur [True,False] renvois 0.5
accuracy = np.mean(predictions == labels_train)
train_accuracies.append(accuracy)
# On affiche l'évolution de l'entrainement dans la console
if (epoch + 1) % 10 == 0:
print(f"Epoch {epoch+1}/{num_epoch}, Loss: {np.mean(epoch_losses):.4f}, Accuracy: {accuracy:.4f}")
return w1, b1, w2, b2, train_accuracies
def predict_mlp(w1, b1, w2, b2, data):
# On Forward pass jusqu'à la prédiction (on avait des soucis sans le faire)
z1 = np.matmul(data, w1) + b1
a1 = 1 / (1 + np.exp(-z1))
z2 = np.matmul(a1, w2) + b2
exp_z2 = np.exp(z2 - np.max(z2, axis=1, keepdims=True))
a2 = exp_z2 / np.sum(exp_z2, axis=1, keepdims=True)
return np.argmax(a2, axis=1)
# Question 14: La fonction pour le test (comme a la fin de notre fonction train_mlp)
# On la mise aussi dans la fonction du dessus pour avoir un suivit de la loss
def test_mlp(w1, b1, w2, b2, data_test, labels_test):
predictions = predict_mlp(w1, b1, w2, b2, data_test)
return np.mean(predictions == labels_test)
# Question 15: La fonction pour l'entrainement complet
def run_mlp_training(data_train, labels_train, data_test, labels_test, d_h, learning_rate, num_epoch):
# On initialise tailles, poids ...
d_in = data_train.shape[1]
d_out = 10 # CIFAR-10 classes
w1 = 2 * np.random.rand(d_in, d_h) - 1
b1 = np.zeros((1, d_h))
w2 = 2 * np.random.rand(d_h, d_out) - 1
b2 = np.zeros((1, d_out))
# On passe a l'entrainement
w1, b1, w2, b2, train_accuracies = train_mlp(
w1, b1, w2, b2, data_train, labels_train, learning_rate, num_epoch)
# On passe ensuite au test
test_accuracy = test_mlp(w1, b1, w2, b2, data_test, labels_test)
return train_accuracies, test_accuracy
# Question 16: Fonction pour plot notre courbe d'entrainement
def plot_learning_curve(train_accuracies, test_accuracies):
plt.figure(figsize=(10, 6))
plt.plot(range(1, len(train_accuracies) + 1), train_accuracies, label='Training Accuracy')
plt.plot(range(1, len(test_accuracies) + 1), test_accuracies, label='Test Accuracy', linestyle='--')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.title('MLP Learning Curve')
plt.legend()
plt.grid(True)
# On sauvegarder le graphique
os.makedirs('results', exist_ok=True)
plt.savefig('results/mlp.png')
plt.close()
if __name__ == "__main__":
# On charge les données
cifar_dir = "data/cifar-10-batches-py"
all_data, all_labels = read_cifar(cifar_dir)
# On normalise les données (pixel codé sur 8 bit)
all_data = all_data / 255.0
# On split nos datas
data_train, labels_train, data_test, labels_test = split_dataset(all_data, all_labels, split=0.9)
# On initialise les paramètres, ils sont fournit dans le sujet
d_h = 64
learning_rate = 0.1
num_epoch = 100
# On démarre le training
train_accuracies, test_accuracy = run_mlp_training(
data_train, labels_train, data_test, labels_test,
d_h, learning_rate, num_epoch
)
print(f"\nPrécision finale sur le test : {test_accuracy:.4f}")
plot_learning_curve(train_accuracies, test_accuracy)
import numpy as np
import pickle
import os
from typing import Tuple, List
import matplotlib.pyplot as plt
# Question 2: Fonction pour lire un batch CIFAR
def read_cifar_batch(batch_path: str) -> Tuple[np.ndarray, np.ndarray]:
with open(batch_path, 'rb') as f:
batch = pickle.load(f, encoding='latin1')
data = batch['data'].astype(np.float32)
labels = np.array(batch['labels'], dtype=np.int64)
return data, labels
# Question 3: Fonction pour lire tous les batches CIFAR
def read_cifar(cifar_dir: str) -> Tuple[np.ndarray, np.ndarray]:
# On initialise les listes qui vont stocké nos batchs
data_batches = []
label_batches = []
# On commence par lire les 5 batchs de train qu'on a telecharger et placé dans data/cifar-10-batches-py
for i in range(1, 6): # les noms vont de 1 a 5
batch_path = os.path.join(cifar_dir, f'data_batch_{i}')
# On utilise la fonction du haut pour lire un batch singulier
data, labels = read_cifar_batch(batch_path)
data_batches.append(data)
label_batches.append(labels)
# On lit maintenant le batch de test
test_path = os.path.join(cifar_dir, 'test_batch')
test_data, test_labels = read_cifar_batch(test_path)
data_batches.append(test_data)
label_batches.append(test_labels)
# On finit par concatener tous les batchs, on utilise directement la methode numpy concatenate
all_data = np.concatenate(data_batches)
all_labels = np.concatenate(label_batches)
return all_data, all_labels
# Question 4: Fonction pour split nos datas
def split_dataset(data: np.ndarray, labels: np.ndarray, split: float) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
# On shuffle nos datas
# On commence par creer une liste d'indices aléatoirement placé, avec la méthode permutation
permutation = np.random.permutation(len(data))
# On peut ensuite directement permutter le tous
data = data[permutation]
labels = labels[permutation]
# On calcul l'indice auquel on décide de split les datas entre train et test
split_idx = int(len(data) * split)
# On split les datas
data_train = data[:split_idx]
labels_train = labels[:split_idx]
data_test = data[split_idx:]
labels_test = labels[split_idx:]
return data_train, labels_train, data_test, labels_test
if __name__ == "__main__":
cifar_dir = "data/cifar-10-batches-py"
# On lit cifar
all_data, all_labels = read_cifar(cifar_dir)
print(f"data shape: {all_data.shape}")
print(f"labels shape: {all_labels.shape}")
# On split cifar
data_train, labels_train, data_test, labels_test = split_dataset(all_data, all_labels, split=0.9)
print(f"Training shape: {data_train.shape}")
print(f"Testing shape: {data_test.shape}")
\ No newline at end of file
results/knn.png

39.3 KiB

results/mlp.png

33.1 KiB