Réduction de dimensionnalité#

´

La simplicité est la sophistication suprème.

Leonard de Vinci

Les chapitres précédents ont traité de l’apprentissage supervisé : régression, classification, évaluation. Nous ouvrons maintenant la Partie III consacrée à l”apprentissage non supervisé, où l’on cherche à extraire de la structure à partir de données non étiquetées. Le premier problème fondamental de cette famille est la réduction de dimensionnalité : comment représenter des données vivant dans un espace de grande dimension par un nombre réduit de variables, tout en préservant au mieux l’information pertinente ?

Ce chapitre présente les méthodes classiques et modernes de réduction de dimensionnalité : l’Analyse en Composantes Principales (ACP), l’ACP noyau, la décomposition en valeurs singulières (SVD), l’analyse discriminante linéaire (LDA), t-SNE et UMAP. Nous terminerons par un exemple complet sur le jeu de données MNIST.

Hide code cell source

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from matplotlib.colors import ListedColormap

from sklearn.datasets import (
    make_swiss_roll, make_circles, make_classification,
    fetch_openml, load_digits
)
from sklearn.decomposition import PCA, KernelPCA, TruncatedSVD
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.manifold import TSNE
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.model_selection import train_test_split

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
np.random.seed(42)

Introduction : la malédiction de la dimensionnalité#

En apprentissage automatique, les données sont souvent représentées par un grand nombre de variables (features). Un jeu d’images de \(28 \times 28\) pixels possède \(d = 784\) dimensions ; un corpus textuel représente par TF-IDF peut atteindre des dizaines de milliers de dimensions. Travailler dans de tels espaces pose des problèmes fondamentaux, regroupés sous le terme de malédiction de la dimensionnalité (curse of dimensionality), introduit par Richard Bellman en 1961.

Concentration des distances#

L’un des phénomènes les plus contre-intuitifs des espaces de grande dimension est la concentration des distances : lorsque \(d\) augmente, les distances entre points tendent à devenir indiscernables.

Proposition 36 (Concentration des distances)

Soient \(\mathbf{x}_1, \ldots, \mathbf{x}_n\) des points tirés uniformément dans l’hypercube \([0, 1]^d\). Notons \(D_{\max} = \max_{i \neq j} \|\mathbf{x}_i - \mathbf{x}_j\|\) et \(D_{\min} = \min_{i \neq j} \|\mathbf{x}_i - \mathbf{x}_j\|\). Alors, sous des conditions générales, lorsque \(d \to \infty\) :

\[\frac{D_{\max} - D_{\min}}{D_{\min}} \xrightarrow{p} 0\]

Autrement dit, le rapport entre la plus grande et la plus petite distance inter-points tend vers 1 : toutes les distances se concentrent autour d’une même valeur.

Remarque 130

Cette concentration a des conséquences directes sur les algorithmes basés sur les distances, comme les \(k\)-plus proches voisins (chapitre 7) ou le clustering (chapitre 13). Si toutes les distances sont similaires, la notion de « voisin le plus proche » perd son sens, et les algorithmes deviennent inefficaces.

Volume des hypersphères#

Un autre phénomène remarquable concerne le volume des hypersphères.

Proposition 37 (Volume de l’hypersphère)

Le volume de l’hypersphère de rayon \(r\) en dimension \(d\) est

\[V_d(r) = \frac{\pi^{d/2}}{\Gamma\left(\frac{d}{2} + 1\right)} \, r^d\]

\(\Gamma\) designe la fonction gamma d’Euler. Lorsque \(d \to \infty\), \(V_d(r) \to 0\) pour tout rayon \(r\) fixe. En d’autres termes, le volume de l’hypersphère devient negligeable devant celui de l’hypercube englobant \([-r, r]^d\).

Illustrons numériquement ce phénomène.

Hide code cell source

from scipy.special import gamma

def volume_hypersphere(d, r=1):
    """Volume de l'hypersphère de rayon r en dimension d."""
    return (np.pi ** (d / 2)) / gamma(d / 2 + 1) * r ** d

dims = np.arange(1, 51)
volumes = [volume_hypersphere(d) for d in dims]
ratio = [volume_hypersphere(d) / (2 ** d) for d in dims]  # ratio sphere/cube

fig, axes = plt.subplots(2, 1, figsize=(9, 9))

axes[0].plot(dims, volumes, 'o-', markersize=4, color='steelblue')
axes[0].set_xlabel("Dimension $d$")
axes[0].set_ylabel("$V_d(1)$")
axes[0].set_title("Volume de l'hypersphère unité")

axes[1].semilogy(dims, ratio, 'o-', markersize=4, color='coral')
axes[1].set_xlabel("Dimension $d$")
axes[1].set_ylabel("$V_d / V_{\\mathrm{cube}}$")
axes[1].set_title("Ratio volume sphère / volume cube")

plt.tight_layout()
plt.show()
_images/d5878d42dab49e9c4519406e96eb47ef037a01b2629e9cc690308964a0170229.png

Hide code cell source

# Illustration de la concentration des distances
from scipy.spatial.distance import pdist

n_points = 500
dims_test = [2, 10, 50, 200, 1000]
fig, axes = plt.subplots(1, len(dims_test), figsize=(16, 3), sharey=True)

for ax, d in zip(axes, dims_test):
    X = np.random.uniform(0, 1, size=(n_points, d))
    distances = pdist(X)
    ax.hist(distances, bins=40, density=True, alpha=0.7, color='steelblue',
            edgecolor='white', linewidth=0.5)
    ax.set_title(f"$d = {d}$", fontsize=11)
    ax.set_xlabel("Distance")
    if d == 2:
        ax.set_ylabel("Densité")

plt.suptitle("Concentration des distances en grande dimension", fontsize=13, y=1.02)
plt.tight_layout()
plt.show()
_images/17982c7c267ca41c9b1d29348b6c73abcdd8dfd6b661a80e8ec669397f512be4.png

Motivations de la reduction de dimensionnalité#

La reduction de dimensionnalité répond à plusieurs besoins pratiques :

  • Visualisation : projeter des données en 2D ou 3D pour les explorer visuellement.

  • Compression : réduire la taille des données tout en conservant l’information essentielle.

  • Réduction du bruit : éliminer les dimensions qui ne portent que du bruit, améliorant ainsi la généralisation des modèles.

  • Colinéarité : lorsque les variables sont fortement corrélées, la réduction permet d’obtenir des features décorrélées, stabilisant l’estimation des modèles.

  • Coût calculatoire : réduire \(d\) accélère l’entrainement et l’inférence des algorithmes.

Définition 138 (Réduction de dimensionnalité)

Soit \(\mathbf{X} \in \mathbb{R}^{n \times d}\) une matrice de données. Une méthode de réduction de dimensionnalité cherche une représentation \(\mathbf{Z} \in \mathbb{R}^{n \times k}\) avec \(k \ll d\), telle qu’une certaine mesure de fidélité entre \(\mathbf{X}\) et \(\mathbf{Z}\) soit maximisée (ou, de manière équivalente, qu’une certaine mesure de perte d’information soit minimisée).

Analyse en Composantes Principales (ACP)#

L”Analyse en Composantes Principales (Principal Component Analysis, PCA) est la méthode de réduction de dimensionnalité la plus classique et la plus utilisée. Son principe est de projeter les données sur les directions de variance maximale.

Formulation#

Définition 139 (ACP — maximisation de la variance)

Soit \(\mathbf{X} \in \mathbb{R}^{n \times d}\) la matrice des données centrées (chaque colonne a une moyenne nulle). L’ACP cherche une direction unitaire \(\mathbf{w}_1 \in \mathbb{R}^d\) telle que la variance de la projection \(\mathbf{X}\mathbf{w}_1\) soit maximale :

\[\mathbf{w}_1 = \arg\max_{\|\mathbf{w}\| = 1} \operatorname{Var}(\mathbf{X}\mathbf{w}) = \arg\max_{\|\mathbf{w}\| = 1} \mathbf{w}^\top \mathbf{S} \mathbf{w}\]

\(\mathbf{S} = \frac{1}{n-1} \mathbf{X}^\top \mathbf{X}\) est la matrice de covariance empirique.

La \(j\)-ième composante principale est obtenue en maximisant la variance sous la contrainte d’orthogonalité avec les composantes précédentes :

\[\begin{split}\mathbf{w}_j = \arg\max_{\substack{\|\mathbf{w}\| = 1 \\ \mathbf{w}^\top \mathbf{w}_i = 0, \; i < j}} \mathbf{w}^\top \mathbf{S} \mathbf{w}\end{split}\]

Proposition 38 (ACP et valeurs propres)

Les directions optimales \(\mathbf{w}_1, \ldots, \mathbf{w}_d\) sont les vecteurs propres de la matrice de covariance \(\mathbf{S}\), ordonnés par valeurs propres décroissantes \(\lambda_1 \geq \lambda_2 \geq \cdots \geq \lambda_d \geq 0\). La variance de la projection sur \(\mathbf{w}_j\) vaut exactement \(\lambda_j\).

Proof. On cherche le maximum de \(\mathbf{w}^\top \mathbf{S} \mathbf{w}\) sous la contrainte \(\mathbf{w}^\top \mathbf{w} = 1\). Par la méthode des multiplicateurs de Lagrange, on forme

\[\mathcal{L}(\mathbf{w}, \lambda) = \mathbf{w}^\top \mathbf{S} \mathbf{w} - \lambda (\mathbf{w}^\top \mathbf{w} - 1)\]

La condition de stationnarité donne \(\nabla_{\mathbf{w}} \mathcal{L} = 2\mathbf{S}\mathbf{w} - 2\lambda \mathbf{w} = 0\), soit \(\mathbf{S}\mathbf{w} = \lambda \mathbf{w}\). Ainsi \(\mathbf{w}\) est un vecteur propre de \(\mathbf{S}\) et la valeur de l’objectif est \(\mathbf{w}^\top \mathbf{S} \mathbf{w} = \lambda \mathbf{w}^\top \mathbf{w} = \lambda\). Pour maximiser, on choisit la plus grande valeur propre \(\lambda_1\). Par récurrence, les composantes suivantes correspondent aux valeurs propres suivantes.

Variance expliquée et choix du nombre de composantes#

Définition 140 (Variance expliquée)

Le ratio de variance expliquee par les \(k\) premières composantes principales est

\[\rho_k = \frac{\sum_{j=1}^k \lambda_j}{\sum_{j=1}^d \lambda_j}\]

Ce ratio mesure la proportion d’information (au sens de la variance) conservée lors de la projection sur les \(k\) premières composantes.

En pratique, on choisit \(k\) tel que \(\rho_k\) dépasse un seuil (typiquement 90 % ou 95 %), ou bien on utilise le scree plot (diagramme des éboulis) pour identifier un « coude » dans la décroissance des valeurs propres.

ACP avec Scikit-learn#

Hide code cell source

# Générer des données en 3D avec une structure 2D
rng = np.random.RandomState(42)
n = 300
t = 1.5 * np.pi * (1 + 2 * rng.rand(n))
X_3d = np.column_stack([
    t * np.cos(t) + 0.5 * rng.randn(n),
    t * np.sin(t) + 0.5 * rng.randn(n),
    3 * rng.randn(n)
])

# Standardiser avant ACP
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X_3d)

# ACP
pca = PCA()
X_pca = pca.fit_transform(X_scaled)

fig, axes = plt.subplots(3, 1, figsize=(9, 12))

# Scree plot
axes[0].bar(range(1, 4), pca.explained_variance_ratio_, color='steelblue',
            edgecolor='white', alpha=0.8)
axes[0].set_xlabel("Composante")
axes[0].set_ylabel("Ratio de variance expliquee")
axes[0].set_title("Scree plot")
axes[0].set_xticks([1, 2, 3])

# Variance cumulee
cum_var = np.cumsum(pca.explained_variance_ratio_)
axes[1].plot(range(1, 4), cum_var, 'o-', color='coral', markersize=8)
axes[1].axhline(y=0.90, color='gray', linestyle='--', alpha=0.7, label='90 %')
axes[1].axhline(y=0.95, color='gray', linestyle=':', alpha=0.7, label='95 %')
axes[1].set_xlabel("Nombre de composantes")
axes[1].set_ylabel("Variance cumulée")
axes[1].set_title("Variance expliquée cumulée")
axes[1].legend()
axes[1].set_xticks([1, 2, 3])

# Projection 2D
scatter = axes[2].scatter(X_pca[:, 0], X_pca[:, 1], c=t, cmap='viridis',
                          s=20, alpha=0.7)
axes[2].set_xlabel("PC1")
axes[2].set_ylabel("PC2")
axes[2].set_title("Projection ACP en 2D")
plt.colorbar(scatter, ax=axes[2], label="$t$")

plt.tight_layout()
plt.show()

print(f"Variance expliquée par composante : {pca.explained_variance_ratio_}")
print(f"Variance cumulée : {cum_var}")
_images/ddaed754dd146ab16b9efe35f370e5f4f1560b69df71614892646fa12b645ce6.png
Variance expliquée par composante : [0.40671892 0.31941195 0.27386913]
Variance cumulée : [0.40671892 0.72613087 1.        ]

Remarque 131

Il est crucial de standardiser les données (centrer et réduire) avant d’appliquer l’ACP. En effet, l’ACP maximise la variance : si les variables ont des échelles différentes, celles à grande variance domineront les premières composantes, indépendamment de leur pertinence. La standardisation met toutes les variables sur un pied d’égalité.

Biplot#

Le biplot est un graphique qui superpose la projection des observations et les directions des variables originales dans l’espace des composantes principales. Il permet de comprendre quelles variables contribuent à chaque composante.

Hide code cell source

# Biplot sur le jeu Iris
from sklearn.datasets import load_iris

iris = load_iris()
X_iris = iris.data
y_iris = iris.target
feature_names = iris.feature_names
target_names = iris.target_names

scaler_iris = StandardScaler()
X_iris_sc = scaler_iris.fit_transform(X_iris)
pca_iris = PCA(n_components=2)
X_iris_pca = pca_iris.fit_transform(X_iris_sc)

fig, ax = plt.subplots(figsize=(9, 7))

# Observations
colors = ['steelblue', 'coral', 'seagreen']
for i, (name, color) in enumerate(zip(target_names, colors)):
    mask = y_iris == i
    ax.scatter(X_iris_pca[mask, 0], X_iris_pca[mask, 1], c=color,
               label=name, s=40, alpha=0.7, edgecolors='k', linewidths=0.3)

# Vecteurs des variables (loadings)
loadings = pca_iris.components_.T
scale = 3
for j, name in enumerate(feature_names):
    ax.arrow(0, 0, loadings[j, 0] * scale, loadings[j, 1] * scale,
             head_width=0.08, head_length=0.05, fc='k', ec='k', alpha=0.8)
    ax.text(loadings[j, 0] * scale * 1.15, loadings[j, 1] * scale * 1.15,
            name, fontsize=9, ha='center', va='center',
            bbox=dict(boxstyle='round,pad=0.2', facecolor='wheat', alpha=0.5))

ax.set_xlabel(f"PC1 ({pca_iris.explained_variance_ratio_[0]:.1%})")
ax.set_ylabel(f"PC2 ({pca_iris.explained_variance_ratio_[1]:.1%})")
ax.set_title("Biplot — Jeu Iris")
ax.legend()
ax.axhline(0, color='gray', linewidth=0.5)
ax.axvline(0, color='gray', linewidth=0.5)

plt.tight_layout()
plt.show()
_images/3e4733c19556f29e226262ed8fa0f8f0b7f73891a08c73d7761e459b078e118e.png

ACP noyau (Kernel PCA)#

Limitations de l’ACP linéaire#

L’ACP classique est une méthode linéaire : elle ne capture que les correlations liéeaires entre les variables. Lorsque la structure sous-jacente des données est non linéaire (par exemple une variété courbe), l’ACP linéaire ne parvient pas à trouver une représentation compacte satisfaisante.

Exemple 12

Considérons des données disposées en cercles concentriques dans \(\mathbb{R}^2\). L’ACP linéaire ne peut pas séparer les deux cercles en projetant sur une seule composante, car aucune direction linéaire ne les distingue.

Le kernel trick appliqué à l’ACP#

L’idée de l”ACP noyau est d’appliquer l’astuce du noyau (kernel trick), vue au chapitre 10 sur les SVM, à l’ACP. On projette implicitement les données dans un espace de grande (voire infinie) dimension via une fonction \(\phi : \mathbb{R}^d \to \mathcal{H}\), puis on applique l’ACP linéaire dans cet espace.

Définition 141 (ACP noyau)

Soit \(\kappa : \mathbb{R}^d \times \mathbb{R}^d \to \mathbb{R}\) une fonction noyau definie positive, c’est-à-dire telle qu’il existe un espace de Hilbert \(\mathcal{H}\) et une application \(\phi\) vérifiant \(\kappa(\mathbf{x}, \mathbf{x}') = \langle \phi(\mathbf{x}), \phi(\mathbf{x}') \rangle_{\mathcal{H}}\). L”ACP noyau consiste à :

  1. Calculer la matrice de Gram \(\mathbf{K} \in \mathbb{R}^{n \times n}\) avec \(K_{ij} = \kappa(\mathbf{x}_i, \mathbf{x}_j)\).

  2. Centrer \(\mathbf{K}\) dans l’espace des features : \(\tilde{\mathbf{K}} = \mathbf{K} - \mathbf{1}_n \mathbf{K} - \mathbf{K} \mathbf{1}_n + \mathbf{1}_n \mathbf{K} \mathbf{1}_n\), ou \(\mathbf{1}_n = \frac{1}{n}\mathbf{11}^\top\).

  3. Calculer les valeurs propres et vecteurs propres de \(\tilde{\mathbf{K}}\).

  4. Les composantes principales noyau sont données par les vecteurs propres normalisés.

Les noyaux les plus courants sont :

  • Noyau RBF (gaussien) : \(\kappa(\mathbf{x}, \mathbf{x}') = \exp\!\left(-\gamma \|\mathbf{x} - \mathbf{x}'\|^2\right)\)

  • Noyau polynomial : \(\kappa(\mathbf{x}, \mathbf{x}') = (\gamma \, \mathbf{x}^\top \mathbf{x}' + c_0)^p\)

Exemple : cercles concentriques#

Hide code cell source

# Données : cercles concentriques
X_circles, y_circles = make_circles(n_samples=500, factor=0.3, noise=0.05,
                                     random_state=42)

# ACP linéaire vs ACP noyau
pca_lin = PCA(n_components=2)
X_pca_lin = pca_lin.fit_transform(X_circles)

kpca_rbf = KernelPCA(n_components=2, kernel='rbf', gamma=15, random_state=42)
X_kpca_rbf = kpca_rbf.fit_transform(X_circles)

kpca_poly = KernelPCA(n_components=2, kernel='poly', degree=2, gamma=1,
                       random_state=42)
X_kpca_poly = kpca_poly.fit_transform(X_circles)

fig, axes = plt.subplots(4, 1, figsize=(8, 14))

# Données originales
axes[0].scatter(X_circles[y_circles == 0, 0], X_circles[y_circles == 0, 1],
                c='steelblue', s=15, alpha=0.7, label='Classe 0')
axes[0].scatter(X_circles[y_circles == 1, 0], X_circles[y_circles == 1, 1],
                c='coral', s=15, alpha=0.7, label='Classe 1')
axes[0].set_title("Données originales")
axes[0].legend(fontsize=8)

# ACP linéaire
axes[1].scatter(X_pca_lin[y_circles == 0, 0], X_pca_lin[y_circles == 0, 1],
                c='steelblue', s=15, alpha=0.7)
axes[1].scatter(X_pca_lin[y_circles == 1, 0], X_pca_lin[y_circles == 1, 1],
                c='coral', s=15, alpha=0.7)
axes[1].set_title("ACP lineaire")
axes[1].set_xlabel("PC1")

# Kernel PCA — RBF
axes[2].scatter(X_kpca_rbf[y_circles == 0, 0], X_kpca_rbf[y_circles == 0, 1],
                c='steelblue', s=15, alpha=0.7)
axes[2].scatter(X_kpca_rbf[y_circles == 1, 0], X_kpca_rbf[y_circles == 1, 1],
                c='coral', s=15, alpha=0.7)
axes[2].set_title("Kernel PCA (RBF, $\\gamma=15$)")
axes[2].set_xlabel("kPC1")

# Kernel PCA — polynomial
axes[3].scatter(X_kpca_poly[y_circles == 0, 0], X_kpca_poly[y_circles == 0, 1],
                c='steelblue', s=15, alpha=0.7)
axes[3].scatter(X_kpca_poly[y_circles == 1, 0], X_kpca_poly[y_circles == 1, 1],
                c='coral', s=15, alpha=0.7)
axes[3].set_title("Kernel PCA (poly, degre 2)")
axes[3].set_xlabel("kPC1")

plt.suptitle("Comparaison ACP linéaire vs ACP noyau", fontsize=13, y=1.02)
plt.tight_layout()
plt.show()
_images/80517b58866fb32576939d8049ea44ed8bdfed300db55fabd34d83a043fd0f63.png

Remarque 132

L’ACP noyau avec un noyau RBF parvient à « dérouler » les cercles concentriques et à séparer linéairement les deux classes dans l’espace projeté. Le choix du paramètre \(\gamma\) est cependant crucial : une valeur trop petite revient à l’ACP linéaire, une valeur trop grande conduit à un sur-ajustement au bruit. En pratique, on peut utiliser la validation croisée pour sélectionner \(\gamma\) dans un pipeline supervisé.

Décomposition en valeurs singulières (SVD)#

Lien avec l’ACP#

La décomposition en valeurs singulières (Singular Value Decomposition, SVD) est une factorisation matricielle fondamentale, étroitement liée a l’ACP.

Définition 142 (SVD)

Toute matrice \(\mathbf{X} \in \mathbb{R}^{n \times d}\) admet une décomposition

\[\mathbf{X} = \mathbf{U} \boldsymbol{\Sigma} \mathbf{V}^\top\]

où :

  • \(\mathbf{U} \in \mathbb{R}^{n \times n}\) est une matrice orthogonale (les colonnes sont les vecteurs singuliers gauches),

  • \(\boldsymbol{\Sigma} \in \mathbb{R}^{n \times d}\) est une matrice diagonale dont les entrées \(\sigma_1 \geq \sigma_2 \geq \cdots \geq 0\) sont les valeurs singulières,

  • \(\mathbf{V} \in \mathbb{R}^{d \times d}\) est une matrice orthogonale (les colonnes sont les vecteurs singuliers droits).

Proposition 39 (Lien SVD-ACP)

Si \(\mathbf{X}\) est centrée, alors la matrice de covariance s’ecrit \(\mathbf{S} = \frac{1}{n-1}\mathbf{X}^\top \mathbf{X} = \frac{1}{n-1} \mathbf{V} \boldsymbol{\Sigma}^\top \boldsymbol{\Sigma} \mathbf{V}^\top\). Les vecteurs propres de \(\mathbf{S}\) sont donc les colonnes de \(\mathbf{V}\) (les vecteurs singuliers droits de \(\mathbf{X}\)), et les valeurs propres sont \(\lambda_j = \frac{\sigma_j^2}{n-1}\).

Autrement dit, l’ACP de \(\mathbf{X}\) s’obtient directement à partir de la SVD de \(\mathbf{X}\), sans calculer explicitement \(\mathbf{S}\), ce qui est plus stable numériquement.

SVD tronquée#

En pratique, on n’a besoin que des \(k\) premières composantes. La SVD tronquée calcule uniquement les \(k\) plus grandes valeurs singulières et les vecteurs associés, ce qui est bien plus efficace que la SVD complète pour des matrices de grande taille.

Définition 143 (SVD tronquée)

La SVD tronquée de rang \(k\) est l’approximation

\[\mathbf{X} \approx \mathbf{U}_k \boldsymbol{\Sigma}_k \mathbf{V}_k^\top\]

\(\mathbf{U}_k \in \mathbb{R}^{n \times k}\), \(\boldsymbol{\Sigma}_k \in \mathbb{R}^{k \times k}\) et \(\mathbf{V}_k \in \mathbb{R}^{d \times k}\). Le théorème d’Eckart-Young garantit que cette approximation est optimale au sens de la norme de Frobenius parmi toutes les matrices de rang \(k\).

Remarque 133

TruncatedSVD de Scikit-learn est particulièrement adapté aux matrices creuses (sparse matrices), car il ne nécessite pas de centrer les données (ce qui détruirait la parcimonie). C’est le choix recommandé pour les représentations TF-IDF en traitement du langage naturel.

Application : compression d’images#

Hide code cell source

# Compression d'image par SVD tronquée
from sklearn.datasets import load_sample_image

# Utiliser une image en niveaux de gris
try:
    china = load_sample_image("china.jpg")
    image = np.mean(china, axis=2)  # conversion en niveaux de gris
except:
    # Fallback : générer une image synthétique
    x = np.linspace(-3, 3, 480)
    y = np.linspace(-3, 3, 640)
    X_grid, Y_grid = np.meshgrid(x, y)
    image = np.sin(X_grid) * np.cos(Y_grid) + 0.5 * np.sin(2 * X_grid * Y_grid)
    image = ((image - image.min()) / (image.max() - image.min()) * 255)

U, s, Vt = np.linalg.svd(image, full_matrices=False)

ranks = [5, 20, 50, 100]
fig, axes = plt.subplots(1, len(ranks) + 1, figsize=(18, 3.5))

axes[0].imshow(image, cmap='gray')
axes[0].set_title(f"Originale\n(rang {min(image.shape)})")
axes[0].axis('off')

for ax, k in zip(axes[1:], ranks):
    img_k = U[:, :k] @ np.diag(s[:k]) @ Vt[:k, :]
    ax.imshow(img_k, cmap='gray')
    compression = k * (image.shape[0] + image.shape[1] + 1) / (image.shape[0] * image.shape[1])
    ax.set_title(f"Rang {k}\n({compression:.1%} des donneés)")
    ax.axis('off')

plt.suptitle("Compression d'image par SVD tronquée", fontsize=13, y=1.02)
plt.tight_layout()
plt.show()

# Décroissance des valeurs singulières
fig, ax = plt.subplots(figsize=(8, 4))
ax.semilogy(s, color='steelblue')
for k in ranks:
    ax.axvline(k, color='coral', linestyle='--', alpha=0.6)
    ax.text(k + 2, s[0] * 0.5, f"$k={k}$", fontsize=9, color='coral')
ax.set_xlabel("Indice")
ax.set_ylabel("Valeur singulière ($\\sigma_i$)")
ax.set_title("Spectre des valeurs singulières")
plt.tight_layout()
plt.show()
_images/4b97e87555a9d110452f7f11fc9f116d49478a7072a7b58f37e4f215206d2b75.png _images/fac31707f8b9ca4eaccd5e945c23b3f327134601fed0d3692e07474c237b738e.png

Analyse discriminante linéaire (LDA)#

Distinction supervisée/non supervisée#

Contrairement à l’ACP qui est une méthode non supervisée (elle ignore les étiquettes), l”Analyse Discriminante Linéaire (Linear Discriminant Analysis, LDA) est une méthode supervisée de réduction de dimensionnalité. Elle cherche les projections qui maximisent la séparabilité entre les classes.

Définition 144 (Matrices de dispersion inter- et intra-classes)

Soit \(\{(\mathbf{x}_i, y_i)\}_{i=1}^n\) un jeu étiqueté avec \(C\) classes. On définit :

  • La moyenne globale : \(\boldsymbol{\mu} = \frac{1}{n} \sum_{i=1}^n \mathbf{x}_i\)

  • La moyenne de la classe \(c\) : \(\boldsymbol{\mu}_c = \frac{1}{n_c} \sum_{i : y_i = c} \mathbf{x}_i\)

  • La matrice de dispersion intra-classe (within-class scatter) :

\[\mathbf{S}_W = \sum_{c=1}^C \sum_{i : y_i = c} (\mathbf{x}_i - \boldsymbol{\mu}_c)(\mathbf{x}_i - \boldsymbol{\mu}_c)^\top\]
  • La matrice de dispersion inter-classes (between-class scatter) :

\[\mathbf{S}_B = \sum_{c=1}^C n_c (\boldsymbol{\mu}_c - \boldsymbol{\mu})(\boldsymbol{\mu}_c - \boldsymbol{\mu})^\top\]

Proposition 40 (Critère de Fisher)

La LDA cherche la direction \(\mathbf{w}\) qui maximise le critère de Fisher :

\[J(\mathbf{w}) = \frac{\mathbf{w}^\top \mathbf{S}_B \mathbf{w}}{\mathbf{w}^\top \mathbf{S}_W \mathbf{w}}\]

La solution est donnée par les vecteurs propres du problème généralisé \(\mathbf{S}_B \mathbf{w} = \lambda \mathbf{S}_W \mathbf{w}\), ou de manière équivalente, par les vecteurs propres de \(\mathbf{S}_W^{-1} \mathbf{S}_B\).

Le nombre maximal de composantes discriminantes est \(\min(d, C-1)\), car \(\operatorname{rang}(\mathbf{S}_B) \leq C - 1\).

Remarque 134

La LDA peut être vue sous deux angles complémentaires :

  • Comme une méthode de réduction de dimensionnalité supervisée (projection sur au plus \(C-1\) axes).

  • Comme un classifieur génératif qui suppose que les classes suivent des lois normales avec la même matrice de covariance.

Dans Scikit-learn, LinearDiscriminantAnalysis joue ces deux rôles.

Hide code cell source

# LDA vs ACP sur le jeu Iris
lda = LinearDiscriminantAnalysis(n_components=2)
X_lda = lda.fit_transform(X_iris_sc, y_iris)

fig, axes = plt.subplots(2, 1, figsize=(9, 9))

# ACP
for i, (name, color) in enumerate(zip(target_names, colors)):
    mask = y_iris == i
    axes[0].scatter(X_iris_pca[mask, 0], X_iris_pca[mask, 1], c=color,
                    label=name, s=40, alpha=0.7, edgecolors='k', linewidths=0.3)
axes[0].set_title("ACP (non supervisée)")
axes[0].set_xlabel("PC1")
axes[0].set_ylabel("PC2")
axes[0].legend(fontsize=9)

# LDA
for i, (name, color) in enumerate(zip(target_names, colors)):
    mask = y_iris == i
    axes[1].scatter(X_lda[mask, 0], X_lda[mask, 1], c=color,
                    label=name, s=40, alpha=0.7, edgecolors='k', linewidths=0.3)
axes[1].set_title("LDA (supervisée)")
axes[1].set_xlabel("LD1")
axes[1].set_ylabel("LD2")
axes[1].legend(fontsize=9)

plt.suptitle("Comparaison ACP vs LDA sur Iris", fontsize=13)
plt.tight_layout()
plt.show()
_images/180188dbc1e4a4680c88b548835d8c6f246c56fc8b6492db1c0073f75eaf0151.png

Remarque 135

Sur le jeu Iris, la LDA sépare nettement les trois classes avec seulement deux composantes discriminantes (le maximum pour \(C = 3\)), tandis que l’ACP, qui ignore les étiquettes, ne garantit pas une telle séparation. Cependant, la LDA nécessite des étiquettes et n’est donc pas utilisable dans un contexte purement non supervisé.

t-SNE#

Les méthodes vues jusqu’ici (ACP, LDA) sont linéaires : elles projettent les données à l’aide de transformations linéaires. Pour la visualisation de données complexes en haute dimension, des méthodes non linéaires sont souvent plus appropriées. La plus célèbre est t-SNE (t-distributed Stochastic Neighbor Embedding), proposée par van der Maaten et Hinton en 2008.

Principe#

Définition 145 (t-SNE)

L’algorithme t-SNE procède en deux étapes :

1. Distributions conditionnelles dans l’espace original. Pour chaque paire de points \((\mathbf{x}_i, \mathbf{x}_j)\), on définit la probabilité conditionnelle

\[p_{j|i} = \frac{\exp\!\left(-\|\mathbf{x}_i - \mathbf{x}_j\|^2 / 2\sigma_i^2\right)}{\sum_{k \neq i} \exp\!\left(-\|\mathbf{x}_i - \mathbf{x}_k\|^2 / 2\sigma_i^2\right)}\]

puis on symétrise : \(p_{ij} = \frac{p_{j|i} + p_{i|j}}{2n}\). Le paramètre \(\sigma_i\) est ajusté pour que la perplexité locale corresponde à une valeur prescrite.

2. Distribution dans l’espace de basse dimension. Dans l’espace de projection \(\mathbb{R}^2\) (ou \(\mathbb{R}^3\)), les similarités sont modelisées par une distribution de Student à un degré de liberté (distribution de Cauchy) :

\[q_{ij} = \frac{(1 + \|\mathbf{z}_i - \mathbf{z}_j\|^2)^{-1}}{\sum_{k \neq l} (1 + \|\mathbf{z}_k - \mathbf{z}_l\|^2)^{-1}}\]

3. Optimisation. On minimise la divergence de Kullback-Leibler \(\mathrm{KL}(P \| Q) = \sum_{i \neq j} p_{ij} \log \frac{p_{ij}}{q_{ij}}\) par descente de gradient.

Définition 146 (Perplexité)

La perplexité est un hyperparamètre de t-SNE qui contrôle le nombre effectif de voisins considérés pour chaque point :

\[\text{Perp}(P_i) = 2^{H(P_i)}\]

\(H(P_i) = -\sum_j p_{j|i} \log_2 p_{j|i}\) est l’entropie de Shannon de la distribution conditionnelle. Une perplexité typique est comprise entre 5 et 50. Des valeurs basses privilégient la structure locale, des valeurs élevées prennent en compte davantage de structure globale.

Remarque 136

t-SNE présente plusieurs limitations importantes à garder en tête :

  • Non paramétrique : t-SNE ne définit pas de transformation applicable à de nouvelles données (pas de methode transform).

  • Non déterministe : le résultat depend de l’initialisation aléatoire (fixer random_state pour la reproductibilité).

  • Structure globale non preserveée : t-SNE excelle à préserver les distances locales, mais les distances entre clusters éloignés ne sont pas nécessairement significatives.

  • Sensible à la perplexité : des valeurs différentes peuvent donner des résultats visuellement très différents.

  • Complexité : \(O(n^2)\) en temps et en mémoire (des approximations comme Barnes-Hut réduisent cela a \(O(n \log n)\)).

Hide code cell source

# t-SNE sur le jeu Digits
digits = load_digits()
X_digits = digits.data
y_digits = digits.target

# Réduire d'abord avec l'ACP pour accélérer t-SNE
pca_50 = PCA(n_components=50, random_state=42)
X_digits_pca = pca_50.fit_transform(X_digits)

# t-SNE avec différentes perplexités
perplexities = [5, 30, 100]
fig, axes = plt.subplots(1, len(perplexities), figsize=(17, 5))

for ax, perp in zip(axes, perplexities):
    tsne = TSNE(n_components=2, perplexity=perp, random_state=42, max_iter=1000)
    X_tsne = tsne.fit_transform(X_digits_pca)
    scatter = ax.scatter(X_tsne[:, 0], X_tsne[:, 1], c=y_digits, cmap='tab10',
                         s=8, alpha=0.7)
    ax.set_title(f"t-SNE (perplexité = {perp})")
    ax.set_xlabel("$z_1$")
    ax.set_ylabel("$z_2$")

plt.suptitle("Effet de la perplexité sur t-SNE (Digits)", fontsize=13, y=1.02)
plt.tight_layout()
plt.show()
_images/22a5d9fc9364f5a01184900def50ed8400dbaef5cdbd0e5c506517692fe4bf5f.png

UMAP#

UMAP (Uniform Manifold Approximation and Projection), proposé par McInnes, Healy et Melville en 2018, est une alternative à t-SNE qui s’est rapidement imposée comme la méthode de référence pour la visualisation en haute dimension.

Intuition topologique#

Définition 147 (UMAP — principe)

UMAP repose sur une approche topologique :

  1. Construction d’un graphe k-NN flou (fuzzy simplicial set). Pour chaque point \(\mathbf{x}_i\), on considère ses \(k\) plus proches voisins. Les distances sont transformees en poids d’aretes via une fonction de decroissance exponentielle, calibrée localement pour que chaque point « voie » son voisinage de manière uniforme. On obtient un graphe pondéré qui encode la topologie locale des données.

  2. Projection en basse dimension. On initialise une représentation \(\mathbf{Z} \in \mathbb{R}^{n \times 2}\) et on construit un graphe similaire dans l’espace de basse dimension.

  3. Optimisation d’une entropie croisée floue (fuzzy cross-entropy). On ajuste \(\mathbf{Z}\) pour minimiser la divergence entre les deux graphes, préservant ainsi la topologie locale.

Remarque 137

UMAP présente plusieurs avantages par rapport à t-SNE :

  • Vitesse : UMAP est significativement plus rapide, avec une complexité typique \(O(n^{1.14})\) en pratique.

  • Structure globale : UMAP preserve mieux les relations inter-clusters que t-SNE.

  • Paramétrique : une extension paramétrique de UMAP existe, permettant de transformer de nouvelles données.

  • Scalabilité : UMAP gère efficacement des jeux de données de plusieurs millions de points.

  • Dimensions : contrairement à t-SNE, UMAP peut projeter en plus de 2-3 dimensions, ce qui permet son utilisation comme pré-traitement pour d’autres algorithmes.

Hide code cell source

# UMAP sur le jeu Digits
try:
    import umap

    reducer = umap.UMAP(n_components=2, n_neighbors=15, min_dist=0.1,
                         random_state=42, n_jobs=1)
    X_umap = reducer.fit_transform(X_digits)

    fig, axes = plt.subplots(2, 1, figsize=(9, 10))

    # t-SNE
    tsne_30 = TSNE(n_components=2, perplexity=30, random_state=42, max_iter=1000)
    X_tsne_30 = tsne_30.fit_transform(X_digits_pca)
    scatter1 = axes[0].scatter(X_tsne_30[:, 0], X_tsne_30[:, 1], c=y_digits,
                                cmap='tab10', s=8, alpha=0.7)
    axes[0].set_title("t-SNE (perplexité = 30)")
    axes[0].set_xlabel("$z_1$")
    axes[0].set_ylabel("$z_2$")
    plt.colorbar(scatter1, ax=axes[0], label="Chiffre")

    # UMAP
    scatter2 = axes[1].scatter(X_umap[:, 0], X_umap[:, 1], c=y_digits,
                                cmap='tab10', s=8, alpha=0.7)
    axes[1].set_title("UMAP ($k=15$, min_dist$=0.1$)")
    axes[1].set_xlabel("$z_1$")
    axes[1].set_ylabel("$z_2$")
    plt.colorbar(scatter2, ax=axes[1], label="Chiffre")

    plt.suptitle("t-SNE vs UMAP sur Digits", fontsize=13)
    plt.tight_layout()
    plt.show()

except ImportError:
    print("La bibliothèque umap-learn n'est pas installée.")
    print("Pour l'installer : pip install umap-learn")
_images/9630aed9279de982c3fdb80ce7f60023f523f367470fed1879a843536339dbed.png

Remarque 138

Les hyperparamètres principaux de UMAP sont :

  • n_neighbors (\(k\)) : nombre de voisins, contrôle l’équilibre local/global (similaire à la perplexité de t-SNE).

  • min_dist : distance minimale entre points dans l’espace projeté. Une valeur faible produit des clusters plus compacts.

  • metric : la métrique de distance utilisée dans l’espace original (euclidienne par défaut, mais d’autres sont possibles).

Comparaison pratique des méthodes#

Apres avoir présenté individuellement chaque méthode, il est utile de les comparer de manière synthétique pour guider le choix en pratique.

Méthode

Linéaire

Supervisée

Paramétrique

Complexité

Usage principal

ACP

Oui

Non

Oui

\(O(nd^2)\)

Compression, pré-traitement

Kernel PCA

Non

Non

Oui

\(O(n^3)\)

Structures non linéaires

SVD tronquée

Oui

Non

Oui

\(O(ndk)\)

Données creuses (NLP)

LDA

Oui

Oui

Oui

\(O(nd^2)\)

Classification, réduction supervisée

t-SNE

Non

Non

Non

\(O(n^2)\)

Visualisation 2D/3D

UMAP

Non

Non

Semi*

\(O(n^{1.14})\)

Visualisation, pré-traitement

* UMAP dispose d’une extension paramétrique optionnelle.

Remarque 139

Recommandations pratiques :

  • Exploration visuelle : UMAP ou t-SNE (UMAP préféré pour les grands jeux de données).

  • Pré-traitement pour un modèle supervisé : ACP (compression, décorrelation) ou LDA (si les étiquettes sont disponibles).

  • Données textuelles creuses : TruncatedSVD (ne détruit pas la parcimonie).

  • Structures non linéaires avec besoin de transform : Kernel PCA.

  • Pipeline ACP \(\to\) t-SNE : réduire à 50 composantes par ACP avant d’appliquer t-SNE accélère considérablement le calcul et réduit le bruit, sans perte notable de qualité.

Exemple complet : MNIST digits#

Pour conclure ce chapitre, nous appliquons un pipeline complet de réduction de dimensionnalité au jeu de données MNIST (chiffres manuscrits), en comparant plusieurs méthodes sur un meme jeu.

Hide code cell source

# Chargement des données MNIST (version réduite via load_digits)
# Pour la version complète (70 000 images 28x28), utiliser fetch_openml('mnist_784')
digits = load_digits()
X_mnist = digits.data.astype(np.float64)
y_mnist = digits.target
n_classes = len(np.unique(y_mnist))

print(f"Dimensions : {X_mnist.shape}")
print(f"Classes : {np.unique(y_mnist)}")
print(f"Nombre d'observations par classe :")
for c in range(n_classes):
    print(f"  Chiffre {c} : {np.sum(y_mnist == c)}")
Dimensions : (1797, 64)
Classes : [0 1 2 3 4 5 6 7 8 9]
Nombre d'observations par classe :
  Chiffre 0 : 178
  Chiffre 1 : 182
  Chiffre 2 : 177
  Chiffre 3 : 183
  Chiffre 4 : 181
  Chiffre 5 : 182
  Chiffre 6 : 181
  Chiffre 7 : 179
  Chiffre 8 : 174
  Chiffre 9 : 180

Hide code cell source

# Visualiser quelques exemples
fig, axes = plt.subplots(2, 10, figsize=(14, 3))
for i in range(10):
    for j in range(2):
        idx = np.where(y_mnist == i)[0][j]
        axes[j, i].imshow(digits.images[idx], cmap='gray_r')
        axes[j, i].axis('off')
        if j == 0:
            axes[j, i].set_title(str(i), fontsize=11)

plt.suptitle("Exemples de chiffres manuscrits (Digits)", fontsize=13, y=1.02)
plt.tight_layout()
plt.show()
_images/ee05a8297b216b4b067d8068265f2fcead80ce6a90e1f01401861bfed8b151f6.png

Hide code cell source

# Etape 1 : Standardisation
scaler_mnist = StandardScaler()
X_mnist_sc = scaler_mnist.fit_transform(X_mnist)

# Etape 2 : ACP — variance expliquée
pca_full = PCA(random_state=42)
pca_full.fit(X_mnist_sc)

fig, axes = plt.subplots(2, 1, figsize=(9, 9))

# Scree plot
axes[0].bar(range(1, 21), pca_full.explained_variance_ratio_[:20],
            color='steelblue', edgecolor='white', alpha=0.8)
axes[0].set_xlabel("Composante")
axes[0].set_ylabel("Ratio de variance expliquée")
axes[0].set_title("Scree plot (20 premieres composantes)")
axes[0].set_xticks(range(1, 21))

# Variance cumulee
cum_var_mnist = np.cumsum(pca_full.explained_variance_ratio_)
axes[1].plot(range(1, len(cum_var_mnist) + 1), cum_var_mnist, color='coral')
axes[1].axhline(y=0.90, color='gray', linestyle='--', alpha=0.7, label='90 %')
axes[1].axhline(y=0.95, color='gray', linestyle=':', alpha=0.7, label='95 %')
n_90 = np.argmax(cum_var_mnist >= 0.90) + 1
n_95 = np.argmax(cum_var_mnist >= 0.95) + 1
axes[1].axvline(n_90, color='steelblue', linestyle='--', alpha=0.5)
axes[1].axvline(n_95, color='seagreen', linestyle='--', alpha=0.5)
axes[1].text(n_90 + 1, 0.85, f"$k={n_90}$ (90%)", fontsize=9, color='steelblue')
axes[1].text(n_95 + 1, 0.80, f"$k={n_95}$ (95%)", fontsize=9, color='seagreen')
axes[1].set_xlabel("Nombre de composantes")
axes[1].set_ylabel("Variance cumulée")
axes[1].set_title("Variance expliquée cumulée")
axes[1].legend()

plt.tight_layout()
plt.show()

print(f"Composantes pour 90% de variance : {n_90}")
print(f"Composantes pour 95% de variance : {n_95}")
_images/51060d433cc545415ff9e05b41042a96a39a41dcb9b2968c136251754f212b2c.png
Composantes pour 90% de variance : 31
Composantes pour 95% de variance : 40

Hide code cell source

# Etape 3 : Comparaison des projections 2D

# ACP 2D
pca_2d = PCA(n_components=2, random_state=42)
X_pca_2d = pca_2d.fit_transform(X_mnist_sc)

# LDA 2D
lda_2d = LinearDiscriminantAnalysis(n_components=2)
X_lda_2d = lda_2d.fit_transform(X_mnist_sc, y_mnist)

# Pipeline ACP(50) -> t-SNE(2)
pca_50_mnist = PCA(n_components=50, random_state=42)
X_pca_50 = pca_50_mnist.fit_transform(X_mnist_sc)

tsne_mnist = TSNE(n_components=2, perplexity=30, random_state=42,
                   max_iter=1000, init='pca', learning_rate='auto')
X_tsne_mnist = tsne_mnist.fit_transform(X_pca_50)

fig, axes = plt.subplots(3, 1, figsize=(9, 15))
cmap = plt.cm.tab10

methods = [
    (X_pca_2d, "ACP", "PC1", "PC2"),
    (X_lda_2d, "LDA", "LD1", "LD2"),
    (X_tsne_mnist, "ACP (50) + t-SNE", "$z_1$", "$z_2$"),
]

for ax, (X_proj, title, xl, yl) in zip(axes, methods):
    scatter = ax.scatter(X_proj[:, 0], X_proj[:, 1], c=y_mnist, cmap=cmap,
                         s=8, alpha=0.6)
    ax.set_title(title, fontsize=12)
    ax.set_xlabel(xl)
    ax.set_ylabel(yl)

# Légende commune
handles = [plt.Line2D([0], [0], marker='o', color='w',
                       markerfacecolor=cmap(i / 9), markersize=8,
                       label=str(i)) for i in range(10)]
fig.legend(handles=handles, title="Chiffre", loc='center right',
           bbox_to_anchor=(1.05, 0.5), fontsize=9)

plt.suptitle("Comparaison des projections 2D sur Digits", fontsize=14, y=1.02)
plt.tight_layout()
plt.show()
_images/b8b6e0525c589676e431b302221a6292eff993ac412eb4019dc65a4ee9c6d208.png

Hide code cell source

# Etape 4 : UMAP (si disponible)
try:
    import umap

    reducer_mnist = umap.UMAP(n_components=2, n_neighbors=15, min_dist=0.1,
                               random_state=42, n_jobs=1)
    X_umap_mnist = reducer_mnist.fit_transform(X_mnist)

    fig, axes = plt.subplots(2, 1, figsize=(9, 10))

    scatter1 = axes[0].scatter(X_tsne_mnist[:, 0], X_tsne_mnist[:, 1],
                                c=y_mnist, cmap='tab10', s=8, alpha=0.6)
    axes[0].set_title("ACP (50) + t-SNE", fontsize=12)
    axes[0].set_xlabel("$z_1$")
    axes[0].set_ylabel("$z_2$")

    scatter2 = axes[1].scatter(X_umap_mnist[:, 0], X_umap_mnist[:, 1],
                                c=y_mnist, cmap='tab10', s=8, alpha=0.6)
    axes[1].set_title("UMAP", fontsize=12)
    axes[1].set_xlabel("$z_1$")
    axes[1].set_ylabel("$z_2$")

    handles = [plt.Line2D([0], [0], marker='o', color='w',
                           markerfacecolor=plt.cm.tab10(i / 9), markersize=8,
                           label=str(i)) for i in range(10)]
    fig.legend(handles=handles, title="Chiffre", loc='center right',
               bbox_to_anchor=(1.05, 0.5), fontsize=9)

    plt.suptitle("t-SNE vs UMAP sur Digits", fontsize=14, y=1.02)
    plt.tight_layout()
    plt.show()

except ImportError:
    print("UMAP non disponible. Installer avec : pip install umap-learn")
_images/6a9773c3db097f7f258a284bbfbb14e5e4dbef63f3835d945dd3a4a3dc6feba2.png

Remarque 140

Quelques observations sur les résultats :

  • L”ACP en 2D ne sépare que grossièrement les classes — elle préserve la variance globale mais pas nécessairement la structure par classe.

  • La LDA sépare mieux les classes, car elle utilise les étiquettes pour maximiser la séparabilité.

  • Le pipeline ACP + t-SNE produit des clusters bien définis, révélant la structure locale des données.

  • UMAP donne des résultats comparables à t-SNE avec un temps de calcul réduit, et tend à mieux preserver la structure globale (les clusters de chiffres similaires, comme 3/5/8 ou 4/9, restent proches).

Hide code cell source

# Résumé : matrice de confusion des distances moyennes entre classes (ACP 50D)
from scipy.spatial.distance import cdist

# Centroïdes par classe dans l'espace ACP 50D
centroids = np.array([X_pca_50[y_mnist == c].mean(axis=0) for c in range(10)])
dist_matrix = cdist(centroids, centroids)

fig, ax = plt.subplots(figsize=(8, 6.5))
mask = np.zeros_like(dist_matrix, dtype=bool)
np.fill_diagonal(mask, True)
sns.heatmap(dist_matrix, annot=True, fmt=".1f", cmap="YlOrRd_r",
            xticklabels=range(10), yticklabels=range(10),
            mask=mask, ax=ax, square=True, linewidths=0.5,
            cbar_kws={"label": "Distance euclidienne"})
ax.set_xlabel("Chiffre")
ax.set_ylabel("Chiffre")
ax.set_title("Distances entre centroides (espace ACP 50D)")

plt.tight_layout()
plt.show()
_images/1f6d09fd4c557660df4e206ae9a66e30d9994508e03dfb32813f28b05e259355.png

Résumé#

Ce chapitre a presenté les principales méthodes de réduction de dimensionnalité, des approches linéaires classiques aux méthodes non linéaires modernes. Retenons les points essentiels :

  1. La malédiction de la dimensionnalité rend indispensable la réduction du nombre de variables pour la visualisation, le débruitage et l’efficacité calculatoire.

  2. L”ACP est la méthode de référence pour la compression linéaire, fondée sur la diagonalisation de la matrice de covariance.

  3. L”ACP noyau étend l’ACP aux structures non linéaires grace à l’astuce du noyau.

  4. La SVD tronquée offre une alternative efficace à l’ACP, particulièrement adaptée aux matrices creuses.

  5. La LDA est la seule méthode supervisée presentée ici ; elle maximise la séparation entre classes.

  6. t-SNE excelle pour la visualisation 2D de données complexes, en préservant les distances locales.

  7. UMAP est une alternative plus rapide et souvent plus fidèle à la structure globale que t-SNE.

Le chapitre suivant abordera le clustering (chapitre 13), qui constitue le deuxième pilier de l’apprentissage non supervisé : au lieu de réduire la dimension, on cherchera à regrouper les observations en clusters homogènes. Les techniques de réduction de dimensionnalité vues ici seront des outils précieux pour visualiser et améliorer les résultats du clustering.