Certificat Big Data Apprentissage TP2: Séparateurs linéaires et perceptron Correction détaillée Olivier Schwander Ce document s accompagne d une archive contenant les codes sources, décomposé en plusieurs modules Python. Pour des raisons de concisions, la plupart des directives import sont omises : elles peuvent se déduire du code ou être retrouvées dans les fichiers sources. 1 Chargement des données Les données USPS sont réparties dans deux fichiers : la base d apprentissage zip.train.gz, la base de test zip.test.gz. On commence par écrire une fonction générique capable de charger l un ou l autre de ces fichiers : def load_file(path): raw = np.loadtxt(path) labels = raw[:, 0] # Première colonne: chiffre data = raw[:, 1:] # Deuxième colonne: pixels de l image return data, labels Puis deux fonctions pour charger une base ou l autre : def load_train(): return load_file("usps/zip.train.gz") def load_test(): return load_file("usps/zip.test.gz") Une fonction d affichage d une image est utile pour les tests : def display(img): img = img.reshape(16, 16) # Mise sous forme matricielle de l image pyplot.imshow(img, cmap=pyplot.gray()) # Affichage en niveau de gris pyplot.show() 1
2 Méthode des moindres carrés 2.1 Outils Deux variables vont être nécessaire dans la suite : max_iter pour limiter le nombre d itérations de la descente de gradient, epsilon qui représente le coefficient de la descente de gradient. On définit ces variables avec deux valeurs qui pourront éventuellement être adaptées par la suite : max_iter = 100 epsilon = 1e-3 La fonction F calcule la sortie pour une entrée x : def F(weights, x): return np.dot(weights, x) La fonction suivante permet de transformer un problème multi-classe en un problème de classification binaire. On attribue l étiquette +1 au point de la classe donnée en argument, et l étiquette 1 à tous les autres. def two_classes(labels, label_of_interest): n = labels.shape[0] ones = np.ones(n) minus_ones = -np.ones(n) return np.select([labels == label_of_interest, labels!= label_of_interest], [ones, minus_ones]) 2.2 Mise à jour La mise à jour des poids s effectue pour une entrée x et sa classe T x (+1 ou 1) avec la formule suivante : w i w i + ɛ(t x F (x))x i La fonction qui réalise cette mise à jour s écrit : def update(weights, x, t): error = (t - F(weights, x)) weights += epsilon * error * x Une version moins compacte et équivalente (bien que plus lente) pourrait s écrire : def update(weights, x, t): error = (t - F(weights, x)) for i in range(len(weights): weights[i] += epsilon * error * x[i] 2
2.3 Boucle La phase d apprentissage proprement dite est une méthode itérative qui applique la règle de mise à jour jusqu à la convergence de la fonction de coût. Une première version considère que la convergence est réalisée au bout d un certain nombre d itérations et s arrête donc au bout d un nombre fixe d étapes : def train1(data, labels): n = data.shape[0] # Nombre d observations d = data.shape[1] # Dimension des observations (et donc taille du vecteur de poids) iter_count = 0 weights = np.zeros(d) while iter_count < max_iter: for i in range(n): # Boucle sur toutes les observations update(weights, data[i], labels[i]) iter_count += 1 return weights Remarque : on initialise le vecteur de poids au vecteur nul. Une version plus évoluée calcule l erreur globale à chaque itération et s arrête quand celle-ci ne varie plus beaucoup (moins qu un certain seuil) : def train2(data, labels): n = data.shape[0] d = data.shape[1] iter_count = 0 weights = np.zeros(d) error_old = 0.0 error = 1.0 while iter_count < max_iter and abs(error - error_old) < 1e-3: error_old = error error = 0.0 # Erreur globale for i in range(n): update(weights, data[i], labels[i]) error += (labels[i] - F(weights, data[i]))**2 # Erreur sur une observation iter_count += 1 return weights 3
La troisième fonction sauvegarde les erreurs au fur et à mesure de façon à pouvoir tracer la courbe de l évolution de la fonction de coût : def train3(data, labels): n = data.shape[0] d = data.shape[1] iter_count = 0 weights = np.zeros(d) errors = [] # Liste des valeurs de l erreur globale error_old = 0.0 error = 1.0 while iter_count < max_iter and abs(error - error_old) > 1e-3: error_old = error error = 0.0 for i in range(n): update(weights, data[i], labels[i]) error += (labels[i] - F(weights, data[i]))**2 iter_count += 1 errors.append(error) # Sauvegarde de l erreur globale return weights, errors Attention : cette fonction renvoie maintenant un couple avec les poids et les erreurs, au lieu de simplement renvoyer les poids. 2.4 Interface Il reste encore à gérer le biais. On écrit pour cela une nouvelle fonction qui va se charger de rajouter une colonne aux données : def append_bias(data): n = data.shape[0] d = data.shape[1] ones = np.ones((n, 1)) # Colonne de 1 à rajouter data2 = np.concatenate([data, ones], axis=1) # Grâce à axis=1, c est une colonne # que l on rajoute return data2 Pour simplifier l utilisation des différentes fonctions, on va définir une dernière fonction avec un paramètre optionnel pour choisir si on veut utiliser une version avec ou sans la liste des erreurs en 4
sortie et qui va se charger d ajouter le biais : def train(data, labels, with_errors=false): data2 = append_bias(data) if with_errors: return train3(data2, labels) # weights, errors else: return train2(data2, labels) # weights 2.5 Prédiction On définit la fonction de Heaviside : def h(x): if x > 0: return 1 else: return -1 Deux fonctions permettent de prédire la classe d une entrée : la première calcule F et applique le seuillage, la seconde rajoute le biais et appelle la première fonction. def predict2(weights, x): return h(f(weights, x)) def predict(weights, x): ones = np.ones(1) x2 = np.concatenate([x, ones], axis=0) return predict2(weights, x2) 3 Évaluation sur des données aléatoires et visualisation On génère 20 points en dimension 2, à partir de deux lois normales : n = 10 d = 2 data0 = np.random.randn(n, d) + 3 data1 = np.random.randn(n, d) - 3 data = np.concatenate([data0, data1]) labels = np.concatenate([np.zeros(n), np.ones(n)]) # Deux classes étiquettées 0 et 1 labels = perceptron.two_classes(labels, 0) # Deux classes étiquettées -1 et 1 On apprend le perceptron sur ces données, en récupérant la liste des erreurs : 5
weights, errors = perceptron.train(data, labels, with_errors=true) print(weights) On affiche les classes prédites sur notre modèle pour les données d apprentissage : for i in range(data.shape[0]): print(i, labels[i], perceptron.predict(weights, data[i]), data[i]) Une visualisation des points et de l hyperplan : pyplot.scatter(data0[:,0], data0[:,1], marker="x", color="r", s=100) pyplot.scatter(data1[:,0], data1[:,1], marker="*", color="b", s=100) x0 = 0 y0 = -weights[2]/weights[1] x1 = -weights[2]/weights[0] y1 = 0 a = (y1 - y0) / (x1 - x0) b = y0 pyplot.plot([-10, +10], [-10 * a + b, +10 * a + b], color="g") pyplot.xlim(-6, 6) pyplot.ylim(-6, 6) pyplot.show() 6 4 2 0 2 4 6 6 4 2 0 2 4 6 On trace le coût en fonction de nombre d itération : pyplot.plot(errors) 6
pyplot.show() 16 14 12 10 8 6 4 2 0 0 10 20 30 40 50 60 On pourrait faire une véritable évaluation des performances, en générant de nouveaux points pour servir de base de tests. 4 Évaluation sur la base USPS 4.1 Précision Une mesure de la qualité de la classification est la précision. On calcule le taux de points dont la classe a été prédite correctement : def accuracy(truth, output): n = truth.shape[0] return (truth == output).sum() / n On pourrait écrire cette fonction de façon plus claire (mais plus lente) : def accuracy(truth, output): n = truth.shape[0] accur = 0. for i in range(n): if truth[i] == output[i]: accur += 1 return accur / n 7
4.2 Évaluation pour un chiffre On charge les données : data, labels = usps.load_train() data_test, labels_test = usps.load_test() Dans un premier temps, on se concentre sur le chiffre 1. On transforme les 10 classes en seulement deux classes +1 pour 1 et -1 pour les autres chiffres : labels_1 = perceptron.two_classes(labels, 1) On apprend le modèle : weights, errors = perceptron.train(data, labels_1, with_errors=true) On mesure les performances avec la précision, sur la base d apprentissage et la base de test : output = np.array([ perceptron.predict(weights, x) for x in data ]) print("score (train)", accuracy(labels_1, output)) output = np.array([ perceptron.predict(weights, x) for x in data_test ]) print("score (test)", accuracy(perceptron.two_classes(labels_test, 1), output)) 4.3 Évaluation sur tous les chiffres On peut faire une boucle pour calculer la précision pour chaque chiffre, en sauvegardant pour chaque chiffre une image pour la matrice de poids et la courbe de l évolution de l erreur : for k in range(10): labels_k = perceptron.two_classes(labels, k) weights, errors = perceptron.train(data, labels_k, with_errors=true) print(k) output = np.array([ perceptron.predict(weights, x) for x in data ]) print(" Score (train)", accuracy(labels_k, output)) output = np.array([ perceptron.predict(weights, x) for x in data_test ]) print(" Score (test)", accuracy(perceptron.two_classes(labels_test, k), output)) pyplot.clf() pyplot.imshow(weights[:-1].reshape((16, 16)), cmap=pyplot.gray()) pyplot.colorbar() pyplot.savefig("usps_" + str(k) + "-weights.png") pyplot.plot(errors) pyplot.savefig("usps_" + str(k) + "-errors.png") On obtient les précisions suivantes : 8
Chiffre Apprentissage Test 0 0.987 0.971 1 0.992 0.988 2 0.971 0.957 3 0.976 0.951 4 0.976 0.960 5 0.975 0.964 6 0.982 0.982 7 0.965 0.968 8 0.952 0.938 9 0.958 0.956 On peut visualiser la matrice de poids pour le chiffre 1 : 5 Utilisation de la bibliothèque sklearn Dans la suite du cours, on utilisera essentiellement la bibliothèque sklearn qui contient de nombreux algorithmes d apprentissage, ainsi que de fonctions utiles pour l évaluation de performances. L expérience précédente sur la base USPS se résume à : import numpy as np from matplotlib import pyplot 9
from sklearn.linear_model.perceptron import Perceptron from sklearn.metrics import accuracy_score import usps import perceptron # Pour la fonction two_classes data, labels = usps.load_train() data_test, labels_test = usps.load_test() for k in range(10): labels_k = perceptron.two_classes(labels, k) net = Perceptron() net.fit(data, labels_k) output_train = net.predict(data) output_test = net.predict(data_test) print(k) print(" Score (train)", accuracy_score(labels_k, output_train)) labels_k_test = perceptron.two_classes(labels_test, k) print(" Score (test)", accuracy_score(labels_k_test, output_test)) Les scores de classification obtenus sont très proches (les différences proviennent de différents choix d implémentation) : Chiffre Apprentissage Test 0 0.993 0.982 1 0.992 0.986 2 0.977 0.957 3 0.984 0.968 4 0.976 0.962 5 0.975 0.962 6 0.983 0.980 7 0.992 0.989 8 0.980 0.961 9 0.980 0.972 10