Les SVM sont de manière inhérente des classificateurs binaires. La méthode traditionnelle pour résoudre un problème multi-classes est d'utiliser la politique du "one versus the rest", c'est-à-dire d'attribuer un SVM par classe. Pour entraîner le classifieur pour la classe $y$, tous les exemples du jeu de données appartenant à la classe $k$ sont considérés comme des exemples positifs et tous les autres exemples comme négatifs. Un classifieur $C$ doit donc trouver l'hyperplan qui différencie les exemples de sa classe et les exemples des autres classes. Lors de l'inférence (c'est-à-dire lorsque l'on teste le système une fois entrainé), on attribue la classe $k$ à l'entrée $x$ du classifieur $C$ qui aura présenté la plus vaste marge pour ce même exemple $x$.
Nous pouvons tester un problème multi-classes avec SVM avec le jeu de données LFW (Labeled Faces in the Wild).
import matplotlib.pyplot as plt
from sklearn.multiclass import OneVsRestClassifier
from sklearn.datasets import fetch_lfw_people
faces = fetch_lfw_people(min_faces_per_person=60)
print("Nombre de classes:", faces.target_names.shape)
print(faces.target_names)
print("Dataset shape",faces.images.shape)
fig, ax = plt.subplots(3, 5)
for i, axi in enumerate(ax.flat):
axi.imshow(faces.images[i], cmap='bone')
axi.set(xticks=[], yticks=[],
xlabel=faces.target_names[faces.target[i]])
plt.show()
Pour cet exercice, nous allons faire une expérience en bonne et due forme. Dans la section précédente, nous testions le système directement sur les données utilisées pour l'entraînement : ce n'est pas correct. Pour tester l'efficacité de l'apprentissage d'une machine, lors de l'inférence, il faut donner des exemples qui n'ont jamais été vus par le système. On divise donc le jeu de données en deux parties : train se et test set.
from sklearn.model_selection import train_test_split
Xtrain, Xtest, ytrain, ytest = train_test_split(faces.data, faces.target,
random_state=1)
print(Xtrain.shape)
print(Xtest.shape)
Nous devons aussi soulever une deuxième question : que mettre en entrée de la machine ? Nous voyons dans la sortie précédente qu'un exemple (ou une image) a pour dimension $62 \times 47$ soit un vecteur $x \in \mathbb{R}^{2914}$. Peut-on se permettre de donner la matrice des pixels aplatis (flatten) en un vecteur en entrée ?
Le SVM a besoin en entrée de caractéristiques principales, ou discriminantes qu'il peut moduler pour tracer son hyperplan entre les classes. Une caractéristique, aussi appelée feature, est un élément composant le vecteur $x$. Ainsi, notre vecteur $x$ dispose de 2914 features, ici des pixels. Existe-il une corrélation entre les features que la SVM soit capable de les utiliser pour discriminer les classes ? Essayons :
from sklearn.svm import SVC
from sklearn.decomposition import PCA
from sklearn.pipeline import make_pipeline
import time
model = OneVsRestClassifier(SVC(kernel='rbf', gamma="auto"))
start = time.time()
model.fit(Xtrain, ytrain)
end = time.time()
print(end - start)
yfit = model.predict(Xtest)
fig, ax = plt.subplots(4, 6)
for i, axi in enumerate(ax.flat):
axi.imshow(Xtest[i].reshape(62, 47), cmap='bone')
axi.set(xticks=[], yticks=[])
axi.set_ylabel(faces.target_names[yfit[i]].split()[-1],
color='black' if yfit[i] == ytest[i] else 'red')
fig.suptitle('Predicted Names; Incorrect Labels in Red', size=14)
plt.show()
Non seulement le temps d'exécution est très long, mais les résultats sont relativement mauvais.
Pour palier à ce problème haute dimension (curse of dimensionality), une technique appelée PCA (Analyse en composante principales) est une méthode statistique (dont les principes ont été formulés en 1901 !) qui a trouvé son application en reconnaissance de visages et compression d'image. La PCA permet de trouver des patterns dans des données de hautes dimensions et de les exprimer dans une dimensionnalité réduite afin que les similarités et différences (c'est-à-dire variance et corrélation linéaire) entre les features soient mises en évidence.
Nous choisissons une PCA qui retient 150 features par image :
from sklearn.svm import SVC
from sklearn.decomposition import PCA
from sklearn.pipeline import make_pipeline
import time
pca = PCA(n_components=150, whiten=True, random_state=1) #random is seed
svc = OneVsRestClassifier(SVC(kernel='rbf', gamma="auto"))
model = make_pipeline(pca, svc)
from sklearn.model_selection import train_test_split
Xtrain, Xtest, ytrain, ytest = train_test_split(faces.data, faces.target,
random_state=1)
start = time.time()
model.fit(Xtrain, ytrain)
end = time.time()
print(end - start)
yfit = model.predict(Xtest)
fig, ax = plt.subplots(4, 6)
for i, axi in enumerate(ax.flat):
axi.imshow(Xtest[i].reshape(62, 47), cmap='bone')
axi.set(xticks=[], yticks=[])
axi.set_ylabel(faces.target_names[yfit[i]].split()[-1],
color='black' if yfit[i] == ytest[i] else 'red')
fig.suptitle('Predicted Names; Incorrect Labels in Red', size=14)
plt.show()
La PCA permet de réduire l'erreur à 7/24 sur ces 24 exemples du test-set.
Nous pouvons vérifier le contenu du modèle OneVsRestClassifier, nous avons bien 8 SVM empilés :
print(len(model.named_steps['onevsrestclassifier'].estimators_))
print(model.named_steps['onevsrestclassifier'].estimators_)
Nous allons maintenant réaliser le même exercice avec des réseaux de neurones. Grâce à sa topologie, une suite de transformations
linéaires (via les perceptrons) et de fonctions non-linéaires (sigmoïde, tanh, relu,...), une couche cachée $h_t$ n'est pas
contrainte de moduler les entrées "raw" (comme des pixels) mais bien la sortie de la couche $h_{t-1}$ (sauf la couche $h_0$ qui
prend les pixels).
Un réseau de neurones (NN) apprend donc ses propres features dans ses couches cachées nécessaires à la classification finale. Un NN avec deux couches cachées se représente comme tel :
On pourrait grossièrement attribuer à la couche cachée 1 la fonction de PCA et à la couche cachée 2 la fonction de réduction de dimensionalité.
Il est important d'ajouter que la taille des couches est modulable, on peut donc avoir $k$ sorties pour le multi-classes, il suffit de
changer la taille de la couche de sortie :
import numpy as np
import matplotlib.pyplot as plt
import torch.nn.functional as F
import torch.nn as nn
import torch
from sklearn.datasets import fetch_lfw_people
from sklearn.model_selection import train_test_split
faces = fetch_lfw_people(min_faces_per_person=60)
print("Nombre de classes:", faces.target_names.shape)
print(faces.target_names)
print("Dataset shape",faces.images.shape)
#Les images doivent être en 3D. Ici les images sont en noir et blanc, donc la première dim
# est 1. Dans le cas du RGB, la dimension est de 3
images_reshape = np.expand_dims(faces.images, axis=1) #1348 x 1 x 62 x 47
Xtrain, Xtest, ytrain, ytest = train_test_split(images_reshape, faces.target,
random_state=1)
# transformation des inputs numpy vers torch
X = torch.from_numpy(Xtrain).cuda().float()
y = torch.from_numpy(ytrain).cuda().long()
# network dimensions
n_input_dim = X.shape[1]
n_output = faces.target_names.shape[0] # output = nombre de classes dans le dataset (8)
Pour construire le modèle, nous utilisons ici des convolutions (conv2d). C'est un cas particulier des couches linéaires pleinement connectées comme vu précédemment où certaines connections sont coupées :
Nous verrons plus en détails les propriétés des convolutions dans les prochaines sections.
# Build the network
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(1, 6, 5)
self.pool = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(6, 16, 5)
self.conv3 = nn.Conv2d(16, 32, 5)
self.fc1 = nn.Linear(32 * 4 * 2, 120)
self.fc2 = nn.Linear(120, n_output)
self.dropout = nn.Dropout(p=0.2)
def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = self.pool(F.relu(self.conv3(x)))
x = x.view(-1, 32 * 4 * 2)
x = self.dropout(x)
x = F.relu(self.fc1(x))
x = self.dropout(x)
x = F.relu(self.fc2(x))
return x
net = Net().cuda()
loss_func = nn.CrossEntropyLoss() #fonction de la loss
learning_rate = 0.002 #pas d'apprentissage
optimizer = torch.optim.SGD(net.parameters(), lr=learning_rate, momentum=0.9) #descente de gradient
num_iterations = 5000
batch_size = 128
for i in range(num_iterations):
rand = np.random.choice(X.shape[0], batch_size)
input = X[rand]
target = y[rand]
optimizer.zero_grad()
# forward pass
prob = net(input)
#calcul de la loss
loss = loss_func(prob, target)
#mise à jour du réseau
loss.backward()
optimizer.step()
#print de la loss
if i%200==0:
print("Iter {} - loss: {:.2f}".format(i,loss.item()))
net.eval()
x = torch.from_numpy(Xtest).cuda()
probs = net(x)
#le réseau sort des probabilités pour chaque classe (chaque réponse possible)
#on prend la confiance "max" du réseau comme réponse
answers = torch.argmax(probs, dim=1)
answers = answers.detach().cpu().numpy()
fig, ax = plt.subplots(4, 6)
for i, axi in enumerate(ax.flat):
axi.imshow(Xtest[i].reshape(62, 47), cmap='bone')
axi.set(xticks=[], yticks=[])
axi.set_ylabel(faces.target_names[answers[i]].split()[-1],
color='black' if answers[i] == ytest[i] else 'red')
fig.suptitle('Predicted Names; Incorrect Labels in Red', size=14)
plt.show()
import torch
x = torch.tensor([0.5, 1.8, 0.1])
print(torch.sigmoid(x))
print(torch.sigmoid(x).sum(0))
print(torch.nn.functional.softmax(x, dim=0))
print(torch.nn.functional.softmax(x, dim=0).sum(0))