Régression logistique

CSI 4506 - automne 2025

Marcel Turcotte

Version: sept. 17, 2025 15h24

Préambule

Message 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.
  • 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.

Régression logistique

Données et problème

  • Jeu de données : Palmer Penguins
  • Tâche : Classification binaire pour distinguer les manchots Gentoo des espèces non-Gentoo
  • Attribut : Longueur des nageoires

Histogramme

Code
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

try:
    from palmerpenguins import load_penguins
except ImportError:
    ! pip install palmerpenguins
    from palmerpenguins import load_penguins

# Charger le jeu de données Palmer Penguins
df = load_penguins()

# Ne garder que 'flipper_length_mm' et 'species'
df = df[['flipper_length_mm', 'species']]

# Supprimer les lignes avec des valeurs manquantes (NaNs)
df.dropna(inplace=True)

# Créer une étiquette binaire : 1 si Gentoo, 0 sinon
df['is_gentoo'] = (df['species'] == 'Gentoo').astype(int)

# Séparer les attributs (X) et les étiquettes (y)
X = df[['flipper_length_mm']]
y = df['is_gentoo']

# Tracer la distribution des longueurs de nageoires par étiquette d'espèce binaire
plt.figure(figsize=(10, 6))
sns.histplot(data=df, x='flipper_length_mm', hue='is_gentoo', kde=True, bins=30, palette='Set1')
plt.title('Distribution de la longueur des nageoires (Gentoo vs. Autres)')
plt.xlabel('Longueur des nageoires (mm)')
plt.ylabel('Fréquence')
plt.legend(title='Espèce', labels=['Gentoo', 'Non Gentoo'])
plt.show()

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\).

Modèle

  • Cas Général : \(P(y = k | x, \theta)\), où \(k\) est une étiquette de classe.
  • Cas Binaire : \(y \in \{0,1\}\)
    • Prédire \(P(y = 1 | x, \theta)\)

Visualiser nos données

Code
# Diagramme de dispersion de la longueur des nageoires vs. étiquette binaire (Gentoo ou Non Gentoo)
plt.figure(figsize=(10, 6))

# Points étiquetés comme Gentoo (is_gentoo = 1)
plt.scatter(
    df.loc[df['is_gentoo'] == 1, 'flipper_length_mm'],
    df.loc[df['is_gentoo'] == 1, 'is_gentoo'],
    color='blue',
    label='Gentoo'
)

# Points étiquetés comme Non Gentoo (is_gentoo = 0)
plt.scatter(
    df.loc[df['is_gentoo'] == 0, 'flipper_length_mm'],
    df.loc[df['is_gentoo'] == 0, 'is_gentoo'],
    color='red',
    label='Non Gentoo'
)

plt.title('Longueur des Nageoires vs. Indicateur Gentoo')
plt.xlabel('Longueur des Nageoires (mm)')
plt.ylabel('Étiquette Binaire (1 = Gentoo, 0 = Non Gentoo)')
plt.legend(loc='best')
plt.grid(True)
plt.show()

Intuition

Ajuster une régression linéaire n’est pas la solution, mais \(\ldots\)

Code
from sklearn.linear_model import LinearRegression
import pandas as pd

lin_reg = LinearRegression()
lin_reg.fit(X, y)

X_new = pd.DataFrame([X.min(), X.max()], columns=X.columns)

y_pred = lin_reg.predict(X_new)

# Tracer le nuage de points
plt.figure(figsize=(5, 3))
plt.scatter(X, y, c=y, cmap='bwr', edgecolor='k')
plt.plot(X_new, y_pred, "r-")
plt.xlabel('Longueur des nageoires (mm)')
plt.ylabel('Étiquette binaire (Gentoo ou Non Gentoo)')
plt.title('Longueur des nageoires vs. Étiquette binaire (Gentoo ou Non Gentoo)')
plt.yticks([0, 1], ['Non Gentoo', 'Gentoo'])
plt.grid(True)
plt.show()

Intuition (suite)

  • Une haute flipper_length_mm aboutit généralement à une sortie du modèle proche de 1.

  • Inversement, une faible flipper_length_mm produit généralement une sortie du modèle proche de 0.

  • Notamment, les sorties du modèle ne sont pas confinées à l’intervalle [0, 1] et peuvent occasionnellement être inférieures à 0 ou dépasser 1.

Sure, here’s the translation of the selected text into French:

Intuition (suite)

  • Pour un seul attribut, la frontière de décision est un point spécifique.
  • Dans ce cas, la frontière de décision est d’environ 205.

Intuition (suite)

  • À mesure que longueur_nageoire_mm augmente de 205 à 230, la confiance dans la classification de l’exemple en tant que Gentoo augmente.
  • Inversement, à mesure que longueur_nageoire_mm diminue de 205 à 170, la confiance dans la classification de l’exemple en tant que non-Gentoo augmente.

Intuition (suite)

  • Pour les valeurs proches de la frontière de décision, 205, certains exemples sont classés comme Gentoo tandis que d’autres ne le sont pas, ce qui conduit à une incertitude de classification comparable à un lancer de pièce (probabilité de 0,5).

Fonction logistique

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}} \]

Code
# Fonction Sigmoïde
def sigmoid(t):
    return 1 / (1 + np.exp(-t))

# Générer des valeurs de t
t = np.linspace(-6, 6, 1000)

# Calculer les valeurs de y pour la fonction sigmoïde
sigma = sigmoid(t)

# Créer une figure
fig, ax = plt.subplots()
ax.plot(t, sigma, color='blue', linewidth=2)  # Garder la courbe opaque

# Tracer l'axe vertical à x = 0
ax.axvline(x=0, color='black', linewidth=1)

# Ajouter des étiquettes sur l'axe vertical
ax.set_yticks([0, 0.5, 1.0])

# Ajouter des étiquettes aux axes
ax.set_xlabel('t')
ax.set_ylabel(r'$\sigma(t)$')

plt.grid(True)
plt.show()

Régression logistique (intuition)

  • Lorsque la distance à la frontière de décision est zéro, 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 hautes 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)}) \]

Notation

  • Equation de la régression logistique: \[ \sigma(\theta_0 + \theta_1 x_i^{(1)} + \theta_2 x_i^{(2)} + \ldots + \theta_D x_i^{(D)}) \]

  • Multipling \(\theta_0\) (intercept/bias) by 1: \[ \sigma(\theta_0 \times 1 + \theta_1 x_i^{(1)} + \theta_2 x_i^{(2)} + \ldots + \theta_D x_i^{(D)}) \]

  • Multipling \(\theta_0\) by \(x_i^{(0)} = 1\): \[ \sigma(\theta_0 x_i^{(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) \]

  • En régression logistique, la probabilité de classer correctement un exemple augmente à mesure que sa distance par rapport à la frontière de décision augmente.
  • Ce principe est valable pour les classes positives et 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.

Chiffres manuscrits

1989 Yann LeCun

Reconnaissance de chiffres manuscrits

Objectifs :

  • Développer un modèle de régression logistique pour la reconnaissance de chiffres manuscrits.

  • Visualiser les informations et les motifs acquis par le modèle.

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

Code
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}')

  • 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, random_state=42)

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
from sklearn.multiclass import OneVsRestClassifier

clf = OneVsRestClassifier(LogisticRegression())
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        17
           1       1.00      1.00      1.00        11
           2       1.00      1.00      1.00        17
           3       1.00      0.94      0.97        17
           4       1.00      1.00      1.00        25
           5       0.96      1.00      0.98        22
           6       1.00      1.00      1.00        19
           7       1.00      0.95      0.97        19
           8       0.80      1.00      0.89         8
           9       0.96      0.92      0.94        25

    accuracy                           0.98       180
   macro avg       0.97      0.98      0.97       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.estimators_[0].coef_.shape, clf.estimators_[0].intercept_.shape)
((1, 64), (1,))

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

Visualisation

clf.estimators_[0].coef_[0].round(2).reshape(width, height)
array([[ 0.  , -0.25,  0.04,  0.21,  0.02, -0.58, -0.36, -0.05],
       [ 0.  , -0.39,  0.04,  0.43,  0.53,  0.72, -0.03, -0.12],
       [-0.03,  0.19,  0.28, -0.04, -0.94,  0.84, -0.01, -0.06],
       [-0.04,  0.27,  0.26, -0.57, -1.75,  0.24,  0.07, -0.03],
       [ 0.  ,  0.32,  0.57, -0.58, -1.62, -0.15,  0.17,  0.  ],
       [-0.08, -0.13,  0.92, -0.85, -0.83, -0.01,  0.22, -0.  ],
       [-0.04, -0.36,  0.61, -0.14,  0.28,  0.01, -0.37, -0.41],
       [ 0.01, -0.31, -0.31,  0.54, -0.32, -0.18, -0.32, -0.21]])

Visualisation

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

Visualisation

Code
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,width), 
    plt.imshow(clf.estimators_[index].coef_.reshape(width,width), 
               cmap=plt.cm.RdBu)

Visualisation

Code
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,width), 
    plt.imshow(clf.estimators_[index].coef_.reshape(width,width), 
               cmap=plt.cm.RdBu,
               interpolation='bilinear')

Prologue

Références

Alharbi, Fadi, et Aleksandar Vakanski. 2023. « Machine Learning Methods for Cancer Classification Using Gene Expression Data: A Review ». Bioengineering 10 (2): 173. https://doi.org/10.3390/bioengineering10020173.
Russell, Stuart, et Peter Norvig. 2020. Artificial Intelligence: A Modern Approach. 4ᵉ éd. Pearson. http://aima.cs.berkeley.edu/.
Wu, Qianfan, Adel Boueiz, Alican Bozkurt, Arya Masoomi, Allan Wang, Dawn L DeMeo, Scott T Weiss, et Weiliang Qiu. 2018. « Deep Learning Methods for Predicting Disease Status Using Genomic Data ». Journal of biometrics & biostatistics 9 (5).

Ressource

Prochain cours

  • Évaluation croisée et mesures de performance

Annexe

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:
 [np.int64(0), np.int64(1), np.int64(2), np.int64(0), np.int64(1), np.int64(2), np.int64(1), np.int64(0)]

Marcel Turcotte

Marcel.Turcotte@uOttawa.ca

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

Université d’Ottawa