Correction

Transcription

Correction
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
2.1
Méthode des moindres carrés
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 Tx (+1 ou −1) avec la formule
suivante :
wi ← wi + (Tx − F (x))xi
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
On trace le coût en fonction de nombre d’itération :
pyplot.plot(errors)
6
4
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
4.1
Évaluation sur la base USPS
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 =
print("
output =
print("
np.array([ perceptron.predict(weights, x) for x in data ])
Score (train)", accuracy(labels_k, output))
np.array([ perceptron.predict(weights, x) for x in data_test ])
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
0
1
2
3
4
5
6
7
8
9
Apprentissage
0.987
0.992
0.971
0.976
0.976
0.975
0.982
0.965
0.952
0.958
Test
0.971
0.988
0.957
0.951
0.960
0.964
0.982
0.968
0.938
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
0
1
2
3
4
5
6
7
8
9
Apprentissage
0.993
0.992
0.977
0.984
0.976
0.975
0.983
0.992
0.980
0.980
10
Test
0.982
0.986
0.957
0.968
0.962
0.962
0.980
0.989
0.961
0.972

Documents pareils