\( \newcommand{\R}{\mathbb{R}} \newcommand{\Rp}{\mathbb{R_+}} \newcommand{\Z}{\mathbb{Z}} \newcommand{\Rn}{\mathbb{R}^n} \newcommand{\N}{\mathbb{N}} \renewcommand{\to}{\rightarrow} \newcommand{\from}{\colon} \renewcommand{\Vec}{\mathsf{vec}} \newcommand{\Ker}{\mathrm{Ker}} \renewcommand{\Im}{\mathrm{Im}} \newcommand{\rang}{\mathrm{rang}} \newcommand{\O}{\mathcal{O}} \newcommand{\C}{\mathcal{C}} \newcommand{\U}{\mathcal{U}} \newcommand{\x}{\mathbf{x}} \newcommand{\z}{\mathbf{z}} \newcommand{\w}{\mathbf{w}} \newcommand{\a}{\mathbf{a}} \newcommand{\dpartial}[2]{{\partial #1 \over \partial #2}} \newcommand{\d}{\mathbf{d}} \)

TP 4 : Séparation Linéaire

Programmation pour l’IA

On va reprendre le dataset de la semaine dernière : iris.csv. On rappelle la visualisation suivante :

  1. Préparer le jeu de données iris.csv. On veut features=["sepal.length", "sepal.width"], data ne doit contenir que les données relatives à ces features, classes doit contenir les classes des exemples dans \(\{-1, 1\}\) : classnames = ["setosa", "autre"] où les éléments de classes Virginica et Versicolor sont envoyés sur la classe "autre".
Correction
df = pd.read_csv("iris.csv")
features = ["sepal.length", "sepal.width"]
classnames = ["setosa", "autre"]

data = df[features].to_numpy()

d = {"Setosa" : 1, "Virginica" : -1, "Versicolor" : -1 }
classes = df["variety"].replace(d).to_numpy(dtype=int)



data_train, data_test, classes_train, classes_test = train_test_split(data, classes, test_size=0.2, random_state=52)

clf = svm.SVC(kernel="linear", C=1000)
clf.fit(data_train, classes_train)

accuracy = metrics.accuracy_score(classes_test, clf.predict(data_test))
print(accuracy)
  1. En utilisant svm de scikit-learn, apprenez un séparateur à vaste marge. On pourra s’inspirer du code suivant :
from sklearn import svm
clf = svm.SVC(kernel="linear", C=1000)
clf.fit(X, y)
Correction

On ajoute cela au code précédent.

from sklearn import svm
clf = svm.SVC(kernel="linear", C=1000)
clf.fit(data_train, classes_train)
  1. En s’inspirant de l’exemple donné dans la documentation de scikit-learn, faire une visualisation du séparateur trouvé et de ses marges.
Correction
# On trace les points du jeu de données
plt.scatter(data[:, 0], data[:, 1], c=classes)

# plot the decision function
ax = plt.gca()

DecisionBoundaryDisplay.from_estimator(
    clf,
    data,
    plot_method="contour",
    colors="k",
    levels=[-1, 0, 1],
    alpha=0.5,
    linestyles=["--", "-", "--"],
    ax=ax,
)

# plot support vectors
ax.scatter(
    clf.support_vectors_[:, 0],
    clf.support_vectors_[:, 1],
    s=100,
    linewidth=1,
    facecolors="none",
    edgecolors="k",
)
plt.show()
  1. Refaire l’exercice en ne prenant que 80% du jeu de données et mesurer la précision de votre modèle (voir TP précédent).
Correction

Le code en entier. On remarquera dans ce cas que des points se trouvent dans la bande. Ce sont des points de test qui n’ont donc pas pris en compte dans le calcul du séparateur. On remarquera la modification du nuage de point (scatter) pour distinguer les données d’entraînement des données test.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn import svm
from sklearn.inspection import DecisionBoundaryDisplay
from sklearn.model_selection import train_test_split
from sklearn import metrics

df = pd.read_csv("iris.csv")
features = ["sepal.length", "sepal.width"]

data = df[features].to_numpy()

d = {"Setosa" : 1, "Virginica" : -1, "Versicolor" : -1 }
classes = df["variety"].replace(d).to_numpy(dtype=int)

data_train, data_test, classes_train, classes_test = train_test_split(data, classes, test_size=0.2, random_state=52)

clf = svm.SVC(kernel="linear", C=1000)
clf.fit(data_train, classes_train)

accuracy = metrics.accuracy_score(classes_test, clf.predict(data_test))
print(f"Précision du modèle : {accuracy}")

plt.scatter(data_train[:, 0], data_train[:, 1], c=classes_train)
plt.scatter(data_test[:, 0], data_test[:, 1], c=classes_test, marker="*")

# plot the decision function
ax = plt.gca()

DecisionBoundaryDisplay.from_estimator(
    clf,
    data,
    plot_method="contour",
    colors="k",
    levels=[-1, 0, 1],
    alpha=0.5,
    linestyles=["--", "-", "--"],
    ax=ax,
)
# plot support vectors
ax.scatter(
    clf.support_vectors_[:, 0],
    clf.support_vectors_[:, 1],
    s=100,
    linewidth=1,
    facecolors="none",
    edgecolors="k",
)
plt.show()
  1. Faire une séparation similaire avec une visualisation pour les classes Versicolor et Virginica en fonction de la longueur/largeur de pétales :

Correction

Il suffit de changer le début du code où les données sont chargées.

df = pd.read_csv("iris.csv")
# on change les features
features = ["petal.length", "petal.width"]
# on ne garde que les variétés Versicolor et Virginica
df = df[df["variety"] != "Setosa"]

# on extrait les données
data = df[features].to_numpy()

# on distingue les classes
d = {"Virginica" : 1, "Versicolor" : -1 }
classes = df["variety"].replace(d).to_numpy(dtype=int)

# le reste est la même chose que dans la question précédente

data_train, data_test, classes_train, classes_test = train_test_split(data, classes, test_size=0.2, random_state=52)

clf = svm.SVC(kernel="linear", C=1000)
clf.fit(data_train, classes_train)

accuracy = metrics.accuracy_score(classes_test, clf.predict(data_test))
print(accuracy)

plt.scatter(data_train[:, 0], data_train[:, 1], c=classes_train)
plt.scatter(data_test[:, 0], data_test[:, 1], c=classes_test, marker="*")

# plot the decision function
ax = plt.gca()

DecisionBoundaryDisplay.from_estimator(
    clf,
    data,
    plot_method="contour",
    colors="k",
    levels=[-1, 0, 1],
    alpha=0.5,
    linestyles=["--", "-", "--"],
    ax=ax,
)
# plot support vectors
ax.scatter(
    clf.support_vectors_[:, 0],
    clf.support_vectors_[:, 1],
    s=100,
    linewidth=1,
    facecolors="none",
    edgecolors="k",
)
plt.show()
  1. Visualisez le jeu de données suivant : mystery.csv.
Correction

Avec le code suivant :

df=pd.read_csv("mystery.csv")
f=["x1","x2"]
data = df[f].to_numpy()
classes = df["class"].to_numpy()
plt.scatter(data[:,0], data[:,1], c=classes)
plt.plot()

On visualise :

  1. Vous semble-t-il possible de le séparer avec un SVM?
Correction

Non, on voit bien qu’une droite ne parviendra pas à séparer l’ensemble des données.

  1. Visualisez \((x_1^2, x_2^2)\).
Correction

Avec le code suivant :

df=pd.read_csv("mystery.csv")
f=["x1","x2"]
data = df[f].to_numpy()
classes = df["class"].to_numpy()
plt.scatter(data[:,0]**2, data[:,1]**2, c=classes)
plt.plot()

On visualise :

qui semble désormais linéairement séparable.

  1. En déduire un classifieur pour le jeu de données précédent.
Correction

On entraîne donc un classifieur sur \((x_1^2, x_2^2)\).

df=pd.read_csv("mystery.csv")
f=["x1","x2"]
data0 = df[f].to_numpy()
data = data**2
classes = df["class"].to_numpy()

# le reste est la même chose que dans l'exercice précédent

data_train, data_test, classes_train, classes_test = train_test_split(data, classes, test_size=0.2, random_state=52)

clf = svm.SVC(kernel="linear", C=1000)
clf.fit(data_train, classes_train)

accuracy = metrics.accuracy_score(classes_test, clf.predict(data_test))
print(accuracy)

plt.scatter(data_train[:, 0], data_train[:, 1], c=classes_train)
plt.scatter(data_test[:, 0], data_test[:, 1], c=classes_test, marker="*")

# plot the decision function
ax = plt.gca()

DecisionBoundaryDisplay.from_estimator(
    clf,
    data,
    plot_method="contour",
    colors="k",
    levels=[-1, 0, 1],
    alpha=0.5,
    linestyles=["--", "-", "--"],
    ax=ax,
)
# plot support vectors
ax.scatter(
    clf.support_vectors_[:, 0],
    clf.support_vectors_[:, 1],
    s=100,
    linewidth=1,
    facecolors="none",
    edgecolors="k",
)
plt.show()

On obtient :

Même exercice avec xor.csv mais c’est à vous de trouver la transformation qui vous permettra de séparer linéairement les données.

Correction

On visualise :

C’est une fonction xor dans le sens suivant : les points violets sont ceux tels que \(s(x_1x_2) = 0 = s(x_1) \oplus s(x_2)\) et les points jaunes sont tels que \(s(x_1x_2) = 1 = s(x_1) \oplus s(x_2)\) où \(s(z) = 0\) si \(z>0\) et \(1\) sinon. On choisit donc la transformation \((x_1,x_2) \mapsto (0, x_1x_2)\). On va donc avoir les points sur une ligne séparés entre les points positifs et les points négatifs.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn import svm
from sklearn.inspection import DecisionBoundaryDisplay
from sklearn.model_selection import train_test_split
from sklearn import metrics

df = pd.read_csv("xor.csv")
features = ["x1", "x2"]
data0 = df[features].to_numpy()
classes = df["class"]

plt.scatter(data0[:,0], data0[:, 1], c=classes)
plt.savefig("xor.svg")
plt.clf()

data = np.stack((np.zeros(len(data0)), data0[:,0]*data0[:,1]), axis=1)

data_train, data_test, classes_train, classes_test = train_test_split(data, classes, test_size=0.3, random_state=52)

clf = svm.SVC(kernel="linear", C=1000)
clf.fit(data_train, classes_train)

accuracy = metrics.accuracy_score(classes_test, clf.predict(data_test))
print(accuracy)

plt.scatter(data_train[:, 0], data_train[:, 1], c=classes_train)
plt.scatter(data_test[:, 0], data_test[:, 1], c=classes_test, marker="*")

# plot the decision function
ax = plt.gca()

DecisionBoundaryDisplay.from_estimator(
    clf,
    data,
    plot_method="contour",
    colors="k",
    levels=[-1, 0, 1],
    alpha=0.5,
    linestyles=["--", "-", "--"],
    ax=ax,
)
# plot support vectors
ax.scatter(
    clf.support_vectors_[:, 0],
    clf.support_vectors_[:, 1],
    s=100,
    linewidth=1,
    facecolors="none",
    edgecolors="k",
)
plt.savefig("xorsep.svg")

Implémentez la régression linéaire par descente de gradient vue en cours.

Correction

En s’inspirant de la méthode vue dans le TP, on peut calculer le gradient en \(W\) ainsi (où \(X,Y\) constitue le jeu de donnée) pour une erreur quadratique (avec numpy, on peut écrire le produit matriciel avec @ et la transposition avec .T):

def grad(X,Y,W):
    return (W@X-Y)@X

Voici un exemple pour le jeu de données iris.csv:

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd

def error(X,Y,W):
    return np.sum((X@W-Y)**2)
    
def grad(X,Y,W):
    return (X@W-Y)@X
    
    
df = pd.read_csv("iris.csv")
features = ["sepal.length", "sepal.width"]

data = df[features].to_numpy()

m = np.max(np.sqrt(np.sum(data*data, axis=1)))
data = data / m
data = np.stack((data[:,0], data[:,1], np.ones(len(data))), axis=1)

d = {"Setosa" : 1, "Virginica" : -1, "Versicolor" : -1 }
classes = df["variety"].replace(d).to_numpy(dtype=int)

delta = 1e-8

W = np.array([0,0,0], dtype=float)

plt.scatter(data[:,0],data[:,1],c=classes)
e = error(data, classes ,W)
g = grad(data,classes,W)

while np.dot(g,g) > 1e-3:
    print(f"Gradient: {g}")
    W -= delta*g
    print(f"New value {W}")
    e=error(data,classes,W)
    g = grad(data,classes,W)
    print(f"Error {e}")
    
a=-W[0]/W[1]

x=np.array(data[:,0])
plt.plot(x, a*x-W[2]/W[1])

plt.show()

On voit ici qu’on a choisi un très petit pas de gradient (\(10^{-8}\)) ce qui peut paraître ridicule. Cependant, l’erreur ici est une somme qui contient 150 termes (un par élément du jeu de données). Le gradient a donc une valeur très importante. En fait, il faudrait normaliser l’erreur (et de ce fait le gradient) par un facteur \(m\) qui est le nombre de point dans le jeu de données. Voir ci-dessous (on peut alors choisir un pas de gradient plus petit) :

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd

def error(X,Y,W):
    return (1/X.shape[0])*np.sum((X@W-Y)**2)
    
def grad(X,Y,W):
    return (2/X.shape[0])*(X@W-Y)@X
    
    
df = pd.read_csv("iris.csv")
features = ["sepal.length", "sepal.width"]

data = df[features].to_numpy()

m = np.max(np.sqrt(np.sum(data*data, axis=1)))
data = data / m
data = np.stack((data[:,0], data[:,1], np.ones(len(data))), axis=1)

d = {"Setosa" : 1, "Virginica" : -1, "Versicolor" : -1 }
classes = df["variety"].replace(d).to_numpy(dtype=int)

delta = 0.4

W = np.array([0,0,0], dtype=float)

plt.scatter(data[:,0],data[:,1],c=classes)
e = error(data, classes ,W)
g = grad(data,classes,W)

while np.dot(g,g) > 1e-3:
    print(f"Gradient: {g}")
    W -= delta*g
    print(f"New value {W}")
    e=error(data,classes,W)
    g = grad(data,classes,W)
    print(f"Error {e}")
    
a=-W[0]/W[1]

x=np.array(data[:,0])
plt.plot(x, a*x-W[2]/W[1])

plt.show()