Modèles linéaires: régression logistique

CSI 4506 - automne 2024

Marcel Turcotte

Version: sept. 30, 2024 08h54

Préambule

Citation du jour

Objectifs d’apprentissage

  • Différencier entre les paradigmes de classification binaire et de classification multi-classes.
  • Décrire une méthodologie pour convertir des problèmes de classification multi-classes en tâches de classification binaire.
  • Articuler le concept de frontière de décision et son importance dans les tâches de classification.
  • Implémenter un algorithme de régression logistique, en se concentrant sur son application aux problèmes de classification.

Tâches de classification

Définitions

  • La classification binaire est une tâche d’apprentissage supervisé dont l’objectif est de catégoriser des instances (exemples) en deux classes distinctes.

  • Une tâche de classification multi-classes est un type de problème d’apprentissage supervisé où l’objectif est de catégoriser des instances en trois classes distinctes ou plus.

Classification binaire

  • Certains algorithmes de machine learning sont spécifiquement conçus pour résoudre des problèmes de classification binaire.
    • La régression logistique et les machines à vecteurs de support (SVM) en sont des exemples.

Classification multi-classes

  • Tout problème de classification multi-classes peut être transformé en un problème de classification binaire.
  • Un-contre-tous (One-vs-All – OvA)
    • Un classificateur binaire distinct est entraîné pour chaque classe.
    • Pour chaque classificateur, une classe est traitée comme la classe positive, et toutes les autres classes sont traitées comme les classes négatives.
    • L’attribution finale d’une étiquette de classe est effectuée en fonction du classificateur qui produit le score de confiance le plus élevé pour une entrée donnée.

Discussion

Pour introduire le concept de frontière de décision (decision boundary), réexaminons le jeu de données Iris.

Chargement du jeu de données Iris

from sklearn.datasets import load_iris

# Load the Iris dataset

iris = load_iris()

import pandas as pd

# Create a DataFrame

df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['species'] = iris.target

Graphiques de dispersion

import seaborn as sns
import matplotlib.pyplot as plt

# Using string labels to ease visualization

df['species'] = df['species'].map({0: 'setosa', 1: 'versicolor', 2: 'virginica'})

# Display all pairwise scatter plots

sns.pairplot(df, hue='species', markers=["o", "s", "D"])
plt.suptitle("Pairwise Scatter Plots of Iris Features", y=1.02)
plt.show()

Graphiques de dispersion

Un-contre-tous: jeu de données Iris

import numpy as np

# Transform the target variable into binary classification
# 'setosa' (class 0) vs. 'not setosa' (classes 1 and 2)

y_binary = np.where(iris.target == 0, 0, 1)

# Create a DataFrame for easier plotting with Seaborn

df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['is_setosa'] = y_binary

print(y_binary)

Un-contre-tous: jeu de données Iris

[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
 1 1]

Un-contre-tous: jeu de données Iris

# Using string labels for visualization

df['is_setosa'] = df['is_setosa'].map({0: 'setosa', 1: 'not_setosa'})

# Pairwise scatter plots

sns.pairplot(df, hue='is_setosa', markers=["o", "s"])
plt.suptitle('Pairwise Scatter Plots of Iris Attributes \n(Setosa vs. Not Setosa)', y=1.02)
plt.show()

Un-contre-tous: jeu de données Iris

Setosa vs Non Setosa

  • Clairement, nous avons simplifié le problème.
  • Dans la majorité des graphiques de dispersion, les exemples de Setosa sont regroupés ensemble.

Frontières de décision

Définition

Une frontière de décision est une limite qui partitionne l’espace des attributs en régions correspondant à des étiquettes de classe différentes.

Frontière de décision

Considérons deux attributs, par exemple longueur des pétales et largeur des sépales, la frontière de décision peut être une ligne.

Frontière de décision

Considérons deux attributs, par exemple longueur des pétales et largeur des sépales, la frontière de décision peut être une ligne.

Définition

On dit que les données sont linéairement séparables lorsque deux classes de données peuvent être parfaitement séparées par une unique frontière linéaire, telle qu’une ligne dans un espace bidimensionnel ou un hyperplan dans des dimensions supérieures.

Frontière de décision simple

(a) données d’entraînement, (b) courbe quadratique, et (c) fonction linéaire.

Frontière de décision complexe

Les arbres de décision sont capables de générer des frontières de décision irrégulières et non linéaires.

Attribution: ibidem.

Frontière de décision

Digression

Frontière de décision

  • 2 attributs, la frontière de décision linéaire serait une ligne.
  • 2 attributs, la frontière de décision non-linéaire serait une courbe non-linéaire.
  • 3 attributs, la frontière de décision linéaire serait un plan.
  • 3 attributs, la frontière de décision non-linéaire serait une surface non-linéaire.
  • \(\gt\) 3 attributs, la frontière de décision linéaire serait un hyperplan.
  • \(\gt\) 3 attributs, la frontière de décision non-linéaire serait une hypersurface.

Définition (révisée)

Une frontière de décision est une hypersurface qui partitionne l’espace des attributs en régions correspondant à différentes étiquettes de classe.

Régression logistique

Régression logistique (Logit)

  • Malgré son nom, la régression logistique sert de technique de classification plutôt que de méthode de régression.

  • Les étiquettes en régression logistique sont des valeurs binaires, notées \(y_i \in \{0,1\}\), ce qui en fait une tâche de classification binaire.

  • L’objectif principal de la régression logistique est de déterminer la probabilité qu’un exemple donné \(x_i\) appartienne à la classe positive, c’est-à-dire \(y_i = 1\).

Régression logistique

Considérons un seul attribut, par exemple longueur des pétales, et la valeur de l’étiquette (0, 1).

Ajuster une régression linéaire

\(\ldots\) n’est pas la solution

La ligne résultante s’étend à l’infini dans les deux directions, mais notre objectif est de contraindre les valeurs entre 0 et 1. Ici, 1 indique une forte probabilité que \(x_i\) appartienne à la classe positive, tandis qu’une valeur proche de 0 indique une faible probabilité.

Fonction logistique

La fonction logistique standard transforme une entrée à valeur réelle de \(\mathbb{R}\) en l’intervalle ouvert \((0,1).\) La fonction est définie comme suit :

\[ \sigma(t) = \frac{1}{1+e^{-t}} \]

Régression logistique (intuition)

  • Lorsque la distance à la frontière de décision est nulle, l’incertitude est élevée, rendant une probabilité de 0,5 appropriée.
  • À mesure que nous nous éloignons de la frontière de décision, la confiance augmente, justifiant des probabilités plus élevées ou plus basses en conséquence.

Fonction logistique

Une courbe en forme de S, telle que la fonction logistique standard (également appelée sigmoïde), est qualifiée de fonction d’écrasement (squashing function) car elle transforme un large domaine d’entrée en une plage de sortie restreinte.

\[ \sigma(t) = \frac{1}{1+e^{-t}} \]

Régression Logistique (Logit)

  • De manière analogue à la régression linéaire, la régression logistique calcule une somme pondérée des attributs d’entrée, exprimée comme suit : \[ \theta_0 + \theta_1 x_i^{(1)} + \theta_2 x_i^{(2)} + \ldots + \theta_D x_i^{(D)} \]

  • Cependant, l’utilisation de la fonction sigmoïde limite sa sortie à l’intervalle \((0,1)\) : \[ \sigma(\theta_0 + \theta_1 x_i^{(1)} + \theta_2 x_i^{(2)} + \ldots + \theta_D x_i^{(D)}) \]

Régression logistique

Le modèle de Régression Logistique, sous sa forme vectorisée, est défini comme suit : \[ h_\theta(x_i) = \sigma(\theta x_i) = \frac{1}{1+e^{- \theta x_i}} \]

Régression logistique (deux attributs)

\[ h_\theta(x_i) = \sigma(\theta x_i) \]

  • La probabilité de classer correctement un exemple augmente à mesure que sa distance par rapport à la frontière de décision augmente.
  • Ce principe s’applique aussi bien aux classes positives qu’aux classes négatives.
  • Un exemple situé sur la frontière de décision a une probabilité de 50% d’appartenir à l’une ou l’autre des classes.

Régression logistique

  • Le modèle de Régression Logistique, sous sa forme vectorisée, est défini comme suit : \[ h_\theta(x_i) = \sigma(\theta x_i) = \frac{1}{1+e^{- \theta x_i}} \]

  • Les prédictions sont faites comme suit :

    • \(y_i = 0\), si \(h_\theta(x_i) < 0.5\)
    • \(y_i = 1\), si \(h_\theta(x_i) \geq 0.5\)
  • Les valeurs de \(\theta\) sont apprises en utilisant la descente de gradient.

Un-contre-Tous

Un-contre-Tous (complet)

from sklearn.model_selection import train_test_split
from sklearn.preprocessing import label_binarize

# Load the Iris dataset
iris = load_iris()
X, y = iris.data, iris.target

# Binarize the output
y_bin = label_binarize(y, classes=[0, 1, 2])

# Split the dataset into training and testing sets
X_train, X_test, y_train, y_test = train_test_split(X, y_bin, test_size=0.2, random_state=42)

Un-contre-Tous (complet)

# Train a One-vs-All classifier for each class

classifiers = []
for i in range(3):
    clf = LogisticRegression()
    clf.fit(X_train, y_train[:, i])
    classifiers.append(clf)

Un-contre-Tous (complet)

# Predict on a new sample
new_sample = X_test[0].reshape(1, -1)

confidences = [clf.decision_function(new_sample) for clf in classifiers]

# Final assignment
final_class = np.argmax(confidences)

# Printing the result
print(f"Final class assigned: {iris.target_names[final_class]}")
print(f"True class: {iris.target_names[np.argmax(y_test[0])]}")
Final class assigned: versicolor
True class: versicolor

label_binarized

from sklearn.preprocessing import label_binarize
from pprint import pprint

# Original class labels
y_train = np.array([0, 1, 2, 0, 1, 2, 1, 0])

# Binarize the labels
y_train_binarized = label_binarize(y_train, classes=[0, 1, 2])

# Assume y_train_binarized contains the binarized labels
print("Binarized labels:\n", y_train_binarized)

# Convert binarized labels back to the original numerical values
original_labels = [np.argmax(b) for b in y_train_binarized]
print("Original labels:\n", original_labels)
Binarized labels:
 [[1 0 0]
 [0 1 0]
 [0 0 1]
 [1 0 0]
 [0 1 0]
 [0 0 1]
 [0 1 0]
 [1 0 0]]
Original labels:
 [0, 1, 2, 0, 1, 2, 1, 0]

Chiffres manuscrits

Chiffres manuscrits

Chargement du jeu de données

from sklearn.datasets import load_digits

digits = load_digits()

Quel est le type de digits.data

type(digits.data)
numpy.ndarray

Chiffres manuscrits

Combien d’exemples (N) et combien d’attributs (D)?

digits.data.shape
(1797, 64)

Définition des valeurs de N et D

N, D = digits.data.shape

target a-t-il le même nombre d’entrées (exemples) que data ?

digits.target.shape
(1797,)

Chiffres manuscrits

Quelles sont la largeur et la hauteur de ces images ?

digits.images.shape
(1797, 8, 8)

Définition des valeurs de width et height

_, width, height = digits.images.shape

Chiffres manuscrits

Définition des valeurs de X et y

X = digits.data
y = digits.target

Chiffres manuscrits

X[0] est un vecteur de taille width * height = D (\(8 \times 8 = 64\)).

X[0]
array([ 0.,  0.,  5., 13.,  9.,  1.,  0.,  0.,  0.,  0., 13., 15., 10.,
       15.,  5.,  0.,  0.,  3., 15.,  2.,  0., 11.,  8.,  0.,  0.,  4.,
       12.,  0.,  0.,  8.,  8.,  0.,  0.,  5.,  8.,  0.,  0.,  9.,  8.,
        0.,  0.,  4., 11.,  0.,  1., 12.,  7.,  0.,  0.,  2., 14.,  5.,
       10., 12.,  0.,  0.,  0.,  0.,  6., 13., 10.,  0.,  0.,  0.])

Il correspond à une image de \(8 \times 8 = 64\).

X[0].reshape(width, height)
array([[ 0.,  0.,  5., 13.,  9.,  1.,  0.,  0.],
       [ 0.,  0., 13., 15., 10., 15.,  5.,  0.],
       [ 0.,  3., 15.,  2.,  0., 11.,  8.,  0.],
       [ 0.,  4., 12.,  0.,  0.,  8.,  8.,  0.],
       [ 0.,  5.,  8.,  0.,  0.,  9.,  8.,  0.],
       [ 0.,  4., 11.,  0.,  1., 12.,  7.,  0.],
       [ 0.,  2., 14.,  5., 10., 12.,  0.,  0.],
       [ 0.,  0.,  6., 13., 10.,  0.,  0.,  0.]])

Chiffres manuscrits

Afficher les n=5 premiers exemples.

import matplotlib.pyplot as plt

plt.figure(figsize=(10,2))
n = 5

for index, (image, label) in enumerate(zip(X[0:n], y[0:n])):
    plt.subplot(1, n, index + 1)
    plt.imshow(np.reshape(image, (width,width)), cmap=plt.cm.gray)
    plt.title(f'y = {label}')

Chiffres manuscrits

  • Dans notre ensemble de données, chaque \(x_i\) est un vecteur d’attributs de taille \(D = 64\).

  • Ce vecteur est formé en concaténant les lignes d’une image de \(8 \times 8\).

  • La fonction reshape est utilisée pour convertir ce vecteur de 64 dimensions en son format original d’image \(8 \times 8\).

Chiffres manuscrits

  • Nous allons entraîner 10 classificateurs, chacun correspondant à un chiffre spécifique dans une approche Un-contre-Tous (OvA).

  • Chaque classificateur déterminera les valeurs optimales des \(\theta_j\) (associées aux attributs pixels), lui permettant de distinguer un chiffre de tous les autres.

Chiffres manuscrits

Préparation de notre expérience en apprentissage automatique

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1)

Chiffres manuscrits

Les algorithmes d’optimisation fonctionnent généralement mieux lorsque les attributs ont des plages de valeurs similaires.

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)

Chiffres manuscrits

from sklearn.linear_model import LogisticRegression

clf = LogisticRegression(multi_class='ovr')
clf = clf.fit(X_train, y_train)

Chiffres manuscrits

Application du classificateur à notre ensemble de test

from sklearn.metrics import classification_report

y_pred = clf.predict(X_test)

print(classification_report(y_test, y_pred))
              precision    recall  f1-score   support

           0       1.00      1.00      1.00        21
           1       0.95      1.00      0.97        19
           2       1.00      1.00      1.00        20
           3       1.00      1.00      1.00        26
           4       1.00      1.00      1.00        20
           5       0.93      0.88      0.90        16
           6       1.00      1.00      1.00        21
           7       1.00      1.00      1.00        11
           8       1.00      0.94      0.97        18
           9       0.89      1.00      0.94         8

    accuracy                           0.98       180
   macro avg       0.98      0.98      0.98       180
weighted avg       0.98      0.98      0.98       180

Visualisation

Combien de classes ?

clf.classes_
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

Les coefficients et les intercepts sont dans des tableaux distincts.

(clf.coef_.shape, clf.intercept_.shape)
((10, 64), (10,))

Les intercepts sont \(\theta_0\), tandis que les coefficients sont \(\theta_j, j \in [1,64]\).

Visualisation

clf.coef_[0].round(2).reshape(width, height)
array([[ 0.  , -0.19, -0.08,  0.28, -0.03, -0.69, -0.52, -0.06],
       [ 0.  , -0.26, -0.03,  0.45,  0.55,  0.82,  0.03, -0.18],
       [-0.04,  0.33,  0.36, -0.05, -0.91,  0.91,  0.08, -0.12],
       [-0.04,  0.2 ,  0.07, -0.72, -1.71,  0.08,  0.18, -0.02],
       [ 0.  ,  0.38,  0.46, -0.54, -1.7 , -0.11,  0.1 ,  0.  ],
       [-0.09, -0.14,  0.84, -0.99, -0.78,  0.08,  0.23, -0.  ],
       [-0.04, -0.27,  0.41,  0.12,  0.32, -0.02, -0.34, -0.5 ],
       [ 0.01, -0.26, -0.39,  0.45, -0.6 , -0.1 , -0.34, -0.31]])

Visualisation

coef = clf.coef_
plt.imshow(coef[0].reshape(width, height))

Visualisation

plt.figure(figsize=(10,5))

for index in range(len(clf.classes_)):
    plt.subplot(2, 5, index + 1)
    plt.title(f'y = {clf.classes_[index]}')
    plt.imshow(clf.coef_[index].reshape(width, height),
               cmap=plt.cm.RdBu,
               interpolation='bilinear')

Prologue

Références

Russell, Stuart, et Peter Norvig. 2020. Artificial Intelligence: A Modern Approach. 4ᵉ éd. Pearson. http://aima.cs.berkeley.edu/.

Ressource

Prochain cours

  • Évaluation croisée et mesures de performance

Graphique 3D avec des points en dessous et au-dessus du plan

from mpl_toolkits.mplot3d import Axes3D

# Function to generate points
def generate_points_above_below_plane(num_points=100):
    # Define the plane z = ax + by + c
    a, b, c = 1, 1, 0  # Plane coefficients

    # Generate random points
    x1 = np.random.uniform(-10, 10, num_points)
    x2 = np.random.uniform(-10, 10, num_points)

    y1 = np.random.uniform(-10, 10, num_points)
    y2 = np.random.uniform(-10, 10, num_points)

    # Points above the plane
    z_above = a * x1 + b * y1 + c + np.random.normal(20, 2, num_points)

    # Points below the plane
    z_below = a * x2 + b * y2 + c - np.random.normal(20, 2, num_points)

    # Stack the points into arrays
    points_above = np.vstack((x1, y1, z_above)).T
    points_below = np.vstack((x2, y2, z_below)).T

    return points_above, points_below

# Generate points
points_above, points_below = generate_points_above_below_plane()

# Visualization
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')

# Plot points above the plane
ax.scatter(points_above[:, 0], points_above[:, 1], points_above[:, 2], c='r', label='Above the plane')

# Plot points below the plane
ax.scatter(points_below[:, 0], points_below[:, 1], points_below[:, 2], c='b', label='Below the plane')

# Plot the plane itself for reference
xx, yy = np.meshgrid(range(-10, 11), range(-10, 11))
zz = 1 * xx + 1 * yy + 0
ax.plot_surface(xx, yy, zz, alpha=0.2, color='gray')

# Set labels
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')

# Set title and legend
ax.set_title('3D Points Above and Below a Plane')
ax.legend()

# Show plot
plt.show()

Marcel Turcotte

Marcel.Turcotte@uOttawa.ca

École de science informatique et de génie électrique (SIGE)

Université d’Ottawa