Linear Discriminant Analysis : Guide Complet d’Analyse Discriminante Linéaire
Résumé
L’Analyse Discriminante Linéaire (Linear Discriminant Analysis, ou LDA) est un algorithme fondamental de classification supervisée et de réduction de dimensionnalité. Contrairement à l’ACP qui cherche simplement les directions de plus grande variance, la LDA cherche activement à maximiser la séparation entre les classes tout en minimisant la dispersion au sein de chaque classe. Développée par Ronald Fisher en 1936 sous le nom d’« analyse discriminante », cette méthode mathématiquement élégante reste l’un des classificateurs les plus utilisés en pratique, notamment dans les domaines de la reconnaissance faciale, de la bio-informatique et du traitement du signal.
Dans ce guide, nous explorerons en profondeur le principe mathématique derrière la LDA, son intuition géométrique, une implémentation complète en Python from scratch, ainsi que ses avantages, limites et cas d’usage concrets.
Principe Mathématique
La LDA repose sur une idée centrale : projeter les données dans un espace de plus faible dimension où les classes sont le plus séparées possible. Pour formaliser cette intuition, Fisher a introduit deux matrices de dispersion fondamentales.
Matrice de dispersion inter-classe (S_B)
La matrice de dispersion inter-classe mesure l’éloignement des centres de chaque classe par rapport au centre global des données. Elle se calcule ainsi :
S_B = Σ_k n_k · (μ_k − μ)(μ_k − μ)^T
où :
– n_k est le nombre d’échantillons dans la classe C_k
– μ_k est le vecteur moyenne de la classe C_k
– μ est le vecteur moyenne globale de toutes les données
– T désigne la transposée
Plus cette matrice a de grandes valeurs propres, plus les centres des classes sont éloignés les uns des autres.
Matrice de dispersion intra-classe (S_W)
La matrice de dispersion intra-classe mesure la dispersion des points au sein de chaque classe. Elle se calcule ainsi :
S_W = Σk Σ{i ∈ C_k} (x_i − μ_k)(x_i − μ_k)^T
où :
– x_i est le i-ième échantillon appartenant à la classe C_k
– μ_k est la moyenne de la classe C_k
Une petite matrice S_W indique que les points de chaque classe sont regroupés de manière compacte autour de leur centre.
Le critère de Fisher
L’objectif de la LDA est de trouver un vecteur de projection w qui maximise le ratio de Fisher :
J(w) = (w^T · S_B · w) / (w^T · S_W · w)
Ce ratio mesure précisément le compromis recherché : un numérateur grand signifie que les classes sont bien séparées après projection, tandis qu’un dénominateur petit signifie que les points de chaque classe restent groupés.
Maximisation du critère : pour trouver le w optimal, on doit résoudre le problème d’optimisation sous contrainte. En utilisant le multiplicateur de Lagrange, on dérive que le vecteur optimal vérifie l’équation aux valeurs propres généralisées :
S_B · w = λ · S_W · w
ce qui équivaut à :
S_W^{-1} · S_B · w = λ · w
Solution pour deux classes
Dans le cas particulier de deux classes (classification binaire), la solution admet une forme analytique remarquablement simple :
w = S_W^{-1} · (μ_1 − μ_2)
Ce vecteur donne directement la direction optimale de projection. Les données sont ensuite projetées sur cet axe, et un seuil (généralement le milieu entre les moyennes projetées) permet de classer les nouveaux points.
Cas multiclasse
Pour K classes, la LDA peut extraire au plus K − 1 composantes discriminantes. On diagonalise la matrice S_W^{-1} · S_B et on retient les K − 1 vecteurs propres associés aux plus grandes valeurs propres. Ces vecteurs forment la base de l’espace discriminant optimal.
Intuition Géométrique
Imaginez que vous avez deux foules de personnes dans une salle — disons, les supporters de deux équipes de football différentes. Vous êtes debout sur un balcon et vous regardez la scène d’en haut (projection 2D). Les deux groupes se mélangent un peu, il est difficile de les distinguer clairement.
Maintenant, imaginez que vous puissiez changer votre angle de vue. Si vous regardez sous un certain angle particulier, les deux groupes apparaissent nettement séparés : l’équipe A à gauche, l’équipe B à droite, avec un espace vide au milieu. C’est exactement ce que fait la LDA — elle trouve l’axe de projection optimal, celui qui écarte au maximum les centres des classes tout en resserrant les points de chaque classe.
Pourquoi cette approche est-elle si efficace ? Parce qu’elle exploite l’information des étiquettes de classe, contrairement à l’ACP qui ignore complètement ces informations. L’ACP cherche simplement la direction où les données varient le plus, mais cette direction n’est pas nécessairement celle qui sépare le mieux les classes. La LDA, elle, est guidée par la supervision : elle sait quelles données appartiennent à quelle classe et en profite.
Hypothèse importante : la LDA suppose que chaque classe suit une distribution gaussienne (normale) multivariée et que toutes les classes partagent la même matrice de covariance. C’est une hypothèse forte qui fonctionne bien en pratique quand les classes ont des formes et des tailles similaires dans l’espace des features.
Implémentation Python
1. LDA From Scratch avec calcul de S_B et S_W
import numpy as np
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
import matplotlib.pyplot as plt
class LDAClassifier:
"""Analyse Discriminante Linéaire implémentée from scratch."""
def __init__(self):
self.w = None # Vecteur(s) de projection
self.means = None # Moyennes par classe
self.classes = None # Labels uniques
self.threshold = None # Seuil de décision (cas binaire)
def _compute_S_W(self, X, y):
"""Calcule la matrice de dispersion intra-classe."""
n_features = X.shape[1]
S_W = np.zeros((n_features, n_features))
for c in self.classes:
X_c = X[y == c]
mean_c = np.mean(X_c, axis=0)
diff = X_c - mean_c
S_W += diff.T @ diff
return S_W
def _compute_S_B(self, X, y):
"""Calcule la matrice de dispersion inter-classe."""
n_features = X.shape[1]
overall_mean = np.mean(X, axis=0)
S_B = np.zeros((n_features, n_features))
for c in self.classes:
X_c = X[y == c]
n_c = X_c.shape[0]
mean_c = np.mean(X_c, axis=0)
diff = (mean_c - overall_mean).reshape(-1, 1)
S_B += n_c * (diff @ diff.T)
return S_B
def fit(self, X, y):
"""Apprentissage de la LDA."""
self.classes = np.unique(y)
self.means = {}
for c in self.classes:
self.means = np.mean(X[y == c], axis=0)
S_W = self._compute_S_W(X, y)
S_B = self._compute_S_B(X, y)
# Régularisation de S_W pour garantir l'inversibilité
S_W += np.eye(S_W.shape[0]) * 1e-6
# Résolution du problème aux valeurs propres généralisées
S_W_inv = np.linalg.inv(S_W)
A = S_W_inv @ S_B
eigenvalues, eigenvectors = np.linalg.eigh(A)
# Tri décroissant des valeurs propres
idx = np.argsort(eigenvalues)[::-1]
eigenvalues = eigenvalues[idx]
eigenvectors = eigenvectors[:, idx]
# Pour deux classes : on prend le premier vecteur propre
n_components = min(len(self.classes) - 1, X.shape[1])
self.w = eigenvectors[:, :n_components]
# Calcul du seuil (cas binaire)
if len(self.classes) == 2:
proj0 = X[y == self.classes[0]] @ self.w
proj1 = X[y == self.classes[1]] @ self.w
self.threshold = (np.mean(proj0) + np.mean(proj1)) / 2
return self
def transform(self, X):
"""Projection des données dans l'espace discriminant."""
return X @ self.w
def predict(self, X):
"""Prédiction des classes."""
projected = self.transform(X)
if len(self.classes) == 2:
predictions = np.where(projected >= self.threshold,
self.classes[1], self.classes[0])
else:
# Cas multiclasse : affectation au centre le plus proche
predictions = np.zeros(len(X), dtype=int)
for i in range(len(X)):
dists = [np.linalg.norm(projected[i] - self.means @ self.w)
for c in self.classes]
predictions[i] = self.classes[np.argmin(dists)]
return predictions
def score(self, X, y):
"""Précision de classification."""
return np.mean(self.predict(X) == y)
# --- Test sur des données synthétiques ---
X, y = make_classification(n_samples=600, n_features=2, n_informative=2,
n_redundant=0, n_clusters_per_class=1,
class_sep=1.5, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, random_state=42
)
lda = LDAClassifier()
lda.fit(X_train, y_train)
accuracy = lda.score(X_test, y_test)
print(f"LDA From Scratch — Précision : {accuracy:.4f}")
2. LDA avec scikit-learn
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.metrics import accuracy_score, classification_report
# Entraînement avec sklearn
lda_sklearn = LinearDiscriminantAnalysis()
lda_sklearn.fit(X_train, y_train)
predictions = lda_sklearn.predict(X_test)
precision = accuracy_score(y_test, predictions)
print(f"LDA sklearn — Précision : {precision:.4f}")
print("\nRapport de classification :")
print(classification_report(y_test, predictions))
# Coefficients et seuil
print(f"\nVecteurs de projection : {lda_sklearn.coef_}")
print(f"Moyennes par classe : {lda_sklearn.means_}")
3. Visualisation 2D → 1D
def visualize_lda_projection(X, y, lda_model, title="Projection LDA"):
"""Visualise la projection LDA de 2D vers 1D."""
X_proj = lda_model.transform(X)
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Espace original 2D
ax1 = axes[0]
for c in np.unique(y):
mask = y == c
ax1.scatter(X[mask, 0], X[mask, 1],
label=f"Classe {c}", alpha=0.7, s=40)
# Vecteur directeur LDA
w = lda_model.w.ravel()
w_norm = w / np.linalg.norm(w)
ax1.quiver(np.mean(X[:, 0]), np.mean(X[:, 1]),
w_norm[0] * 3, w_norm[1] * 3,
color='red', scale=5, width=0.01, label='Direction LDA')
ax1.set_title("Espace original (2D)")
ax1.set_xlabel("Feature 1")
ax1.set_ylabel("Feature 2")
ax1.legend()
ax1.grid(True, alpha=0.3)
# Projection 1D
ax2 = axes[1]
for c in np.unique(y):
mask = y == c
ax2.scatter(X_proj[mask], np.zeros_like(X_proj[mask]),
label=f"Classe {c}", alpha=0.5, s=40)
# Ajout d'un léger bruit vertical pour la lisibilité
for c in np.unique(y):
mask = y == c
jitter = np.random.normal(0, 0.1, size=X_proj[mask].shape)
ax2.scatter(X_proj[mask], jitter,
label=f"Classe {c}", alpha=0.3, s=20)
ax2.set_title("Projection LDA (1D)")
ax2.set_xlabel("Axe discriminant")
ax2.set_yticks([])
ax2.legend()
ax2.grid(True, alpha=0.3)
plt.suptitle(title, fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()
visualize_lda_projection(X_test, y_test, lda, "LDA : De 2D à 1D")
4. Comparaison LDA vs PCA
from sklearn.decomposition import PCA
def compare_lda_pca(X, y):
"""Compare visuellement les projections LDA et PCA."""
lda = LDAClassifier()
lda.fit(X, y)
X_lda = lda.transform(X)
pca = PCA(n_components=1)
X_pca = pca.fit_transform(X)
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
ax1 = axes[0]
for c in np.unique(y):
mask = y == c
jitter = np.random.normal(0, 0.1, size=X_lda[mask].shape)
ax1.scatter(X_lda[mask] + np.random.normal(0, 0.05, size=jitter.shape),
jitter, label=f"Classe {c}", alpha=0.4, s=15)
ax1.set_title("Projection LDA (supervisée)")
ax1.set_xlabel("Axe discriminant")
ax1.set_yticks([])
ax1.legend()
ax1.grid(True, alpha=0.3)
ax2 = axes[1]
for c in np.unique(y):
mask = y == c
jitter = np.random.normal(0, 0.1, size=X_pca[mask].shape)
ax2.scatter(X_pca[mask], jitter,
label=f"Classe {c}", alpha=0.4, s=15)
ax2.set_title("Projection PCA (non supervisée)")
ax2.set_xlabel("Composante principale")
ax2.set_yticks([])
ax2.legend()
ax2.grid(True, alpha=0.3)
plt.suptitle("LDA vs PCA — Qualité de séparation des classes",
fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()
compare_lda_pca(X, y)
Remarque clé : dans la visualisation LDA vs PCA, on observe typiquement que la LDA sépare beaucoup mieux les classes que la PCA, car elle utilise l’information des labels lors de la recherche de l’axe de projection. La PCA, elle, maximise uniquement la variance totale, ce qui n’est pas synonyme de bonne séparation entre classes.
Hyperparamètres
L’implémentation de scikit-learn expose plusieurs hyperparamètres importants qu’il est essentiel de comprendre pour optimiser les performances.
n_components
Nombre de composantes discriminantes à extraire. La valeur maximale est K − 1 où K est le nombre de classes. Pour un problème binaire, on ne peut extraire qu’une seule composante. Pour un problème à 4 classes, on peut en extraire jusqu’à 3. Réduire ce paramètre permet de faire de la réduction de dimensionnalité avant d’appliquer un autre classificateur.
lda = LinearDiscriminantAnalysis(n_components=2) # Pour 3+ classes
solver
Algorithme de résolution utilisé pour calculer les vecteurs discriminants :
- « svd » (par défaut) : utilise la décomposition en valeurs singulières. Rapide, ne calcule pas la matrice de covariance. Incompatible avec le shrinkage.
- « lsqr » : utilise la méthode des moindres carrés. Plus efficace sur les grands jeux de données. Supporte le shrinkage.
- « eigen » : résout directement le problème aux valeurs propres généralisées. Supporte à la fois le shrinkage et l’optimisation de la séparation entre classes.
lda = LinearDiscriminantAnalysis(solver='lsqr', shrinkage='auto')
shrinkage
Technique de régularisation qui mélange la matrice de covariance empirique avec une matrice identité. Particulièrement utile quand le nombre de features est proche du nombre d’échantillons (problème de petite taille d’échantillon).
- None : pas de shrinkage (par défaut)
- « auto » : shrinkage automatique selon le lemme de Ledoit-Wolf
- Float entre 0 et 1 : coefficient de shrinkage manuel (0 = aucun, 1 = identité pure)
lda = LinearDiscriminantAnalysis(solver='lsqr', shrinkage=0.3)
priors
Probabilités a priori des classes. Par défaut, elles sont estimées à partir des fréquences dans les données d’entraînement. On peut les spécifier manuellement pour corriger un déséquilibre de classes ou intégrer une connaissance métier.
lda = LinearDiscriminantAnalysis(priors=[0.3, 0.7]) # Classe 0 : 30%, Classe 1 : 70%
tol
Tolérance de convergence pour les solveurs itératifs (lsqr, eigen). Une valeur plus stricte (plus petite) garantit une précision supérieure mais augmente le temps de calcul.
lda = LinearDiscriminantAnalysis(solver='eigen', tol=1e-6)
Avantages et Limites
Avantages
- Efficacité computationnelle : très rapide à l’entraînement et à l’inférence, car la solution est analytique (pas d’optimisation itérative comme pour les réseaux de neurones).
- Pas d’hyperparamètres sensibles : contrairement aux forêts aléatoires ou aux SVM, la LDA fonctionne bien avec ses paramètres par défaut.
- Réduction de dimensionnalité intégrée : la LDA projette naturellement les données dans un espace de dimension K−1, ce qui facilite la visualisation et accélère les pipelines.
- Interprétabilité : les coefficients w sont directement interprétables — ils indiquent l’importance de chaque feature pour la discrimination entre classes.
- Probabilités calibrées : la LDA fournit des probabilités a posteriori bien calibrées (grâce à l’hypothèse gaussienne), contrairement à d’autres classifieurs qui nécessitent un recalibrage (Platt scaling, isotonic regression).
Limites
- Hypothèse de covariance égale : la LDA suppose que toutes les classes partagent la même matrice de covariance. Si cette hypothèse est fortement violée, les performances chutent. Dans ce cas, la QDA (Quadratic Discriminant Analysis) est plus appropriée.
- Hypothèse gaussienne : chaque classe est supposée suivre une distribution normale multivariée. Pour des données très asymétriques ou multimodales, la performance peut être médiocre.
- Linéarité : les frontières de décision sont linéaires. Si les classes ne sont pas linéairement séparables (ex : XOR), la LDA ne pourra pas les distinguer.
- Sensibilité aux outliers : les matrices S_B et S_W utilisent des moyennes, très sensibles aux valeurs aberrantes. Un prétraitement robuste (robust scaling, trimming) est souvent nécessaire.
- Nombre de composantes limité : on ne peut extraire au plus que K − 1 composantes. Pour un problème binaire, une seule dimension discriminante est disponible.
4 Cas d’Usage Concrets
1. Reconnaissance Faciale (Eigenfaces & Fisherfaces)
La LDA est utilisée dans le célèbre algorithme des Fisherfaces pour la reconnaissance faciale. Après une étape préliminaire de PCA pour réduire la dimensionnalité, la LDA est appliquée pour maximiser la séparation entre les visages de différentes personnes. Chaque individu forme une classe, et la projection discriminante permet d’obtenir des signatures faciales très discriminantes.
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.decomposition import PCA
# Pipeline typique Fisherfaces
pipeline = make_pipeline(PCA(n_components=50), LinearDiscriminantAnalysis(n_components=9))
pipeline.fit(visages_entrainement, identites)
2. Diagnostic Médical
En bio-informatique, la LDA est couramment employée pour la classification de tumeurs à partir de données d’expression génique. Les milliers de gènes (features) sont réduits à quelques axes discriminants qui séparent efficacement les tissus sains des tissus cancéreux. Son interprétabilité est un atout majeur : les coefficients w permettent d’identifier les gènes les plus discriminants, offrant ainsi des pistes biologiques concrètes.
3. Filtrage Anti-Spam
Un classifieur LDA peut être utilisé comme filtre anti-spam de base. Chaque email est représenté par un vecteur de caractéristiques (fréquence de mots, présence de liens, longueur, etc.) et la LDA apprend à séparer les spam des emails légitimes. Bien que moins puissant que les méthodes modernes, il reste rapide, interprétable et efficace sur des jeux de données bien structurés.
4. Analyse de Sentiment
La LDA sert de classifieur de référence pour l’analyse de sentiment (positif/négatif) sur des textes vectorisés (TF-IDF, embeddings). Sa rapidité d’entraînement en fait un excellent point de comparaison : si une méthode complexe (BERT, réseaux de neurones) ne bat pas significativement la LDA, le problème est peut-être déjà suffisamment résolu par une approche simple.
Voir Aussi
- Résoudre le Problème d’Entretien Maximum Subarray avec Python
- Implémenter l’Algorithme de Dekker en Python : Synchronisation des Threads Efficace

