Régression Elastic Net : Guide Complet — Principes, Exemples et Implémentation Python

Régression Elastic Net : Guide Complet — Principes, Exemples et Implémentation Python

Régression Elastic Net : Guide Complet — Principes, Exemples et Implémentation Python

Résumé introductif

La régression elastic net est une méthode de régression linéaire régularisée qui combine simultanément les pénalisations L1 (Lasso) et L2 (Ridge), offrant le meilleur des deux mondes : la parcimonie du Lasso et la stabilité du Ridge face aux variables corrélées.

La régression elastic net a été introduite par Zou et Hastie en 2005 pour répondre aux limitations du Lasso dans des contextes de haute dimension où les features sont fortement corrélées. Le Lasso a tendance à sélectionner une seule variable parmi un groupe de variables corrélées, tandis que le Ridge les conserve toutes sans réaliser de sélection franche. L’Elastic Net, grâce à son mélange contrôlé des deux pénalisations, parvient à sélectionner des groupes entiers de variables corrélées tout en maintenant un modèle parcimonieux. C’est aujourd’hui l’outil de choix en génomique, en finance quantitative et dans tous les domaines où le nombre de variables dépasse largement le nombre d’observations.


Principe mathématique

La régression Elastic Net résout le problème d’optimisation suivant :

$$
\min_{w} \; |y – Xw|_2^2 + \alpha\left[\rho|w|_1 + \frac{1 – \rho}{2}|w|_2^2\right]
$$

où :

  • $y \in \mathbb{R}^n$ est le vecteur des variables cibles (valeurs à prédire),
  • $X \in \mathbb{R}^{n \times p}$ est la matrice des features (n observations, p variables),
  • $w \in \mathbb{R}^p$ est le vecteur des coefficients du modèle,
  • $|y – Xw|_2^2$ est le terme d’erreur quadratique (comme en régression linéaire classique),
  • $|w|1 = \sum |w_j|$}^{p est la norme L1 (pénalisation Lasso),
  • $|w|2^2 = \sum w_j^2$}^{p est la norme L2 au carré (pénalisation Ridge),
  • $\alpha \geq 0$ contrôle l’intensité globale de la régularisation,
  • $\rho \in [0, 1]$ (appelé l1_ratio dans scikit-learn) détermine le mélange entre L1 et L2.

Cas limites importants

Valeur de $\rho$ Modèle résultant Comportement
$\rho = 1$ Lasso pur Sélection de variables franche, coefficients nuls possibles
$\rho = 0$ Ridge pur Tous les coefficients réduits mais jamais exactement nuls
$0 < \rho < 1$ Elastic Net Combinaison des deux effets

L’effet de groupe (grouping effect)

La propriété la plus remarquable de la régression elastic net est l’effet de groupe : lorsque plusieurs variables sont fortement corrélées, le terme L2 tend à leur attribuer des coefficients similaires, tandis que le terme L1 tend à les sélectionner ou les éliminer collectivement. Mathématiquement, si deux variables $x_j$ et $x_k$ sont corrélées, alors leurs coefficients $w_j$ et $w_k$ convergent vers des valeurs proches.

Ce comportement résout directement la faiblesse du Lasso qui, face à deux variables corrélées, en sélectionne une seule de manière arbitraire. L’Elastic Net conserve les deux (ou les écarte toutes les deux), préservant ainsi l’information portée par le groupe de variables corrélées.


Intuition — Comment le comprendre ?

Imaginez deux boutons sur un tableau de bord :

  1. Le bouton $\alpha$ (alpha) — il contrôle combien on pénalise. Plus $\alpha$ est grand, plus les coefficients sont tirés vers zéro. À $\alpha = 0$, on retrouve la régression linéaire ordinaire (OLS). À $\alpha$ très élevé, tous les coefficients tendent vers zéro.
  2. Le bouton $\rho$ (l1_ratio) — il contrôle comment on pénalise. Plus $\rho$ est proche de 1, plus le modèle se comporte comme un Lasso (coefficients nuls, sélection de variables). Plus $\rho$ est proche de 0, plus il se comporte comme un Ridge (coefficients réduits mais non nuls, stabilité).

Pourquoi combiner les deux ?

Considérons un jeu de données avec 200 variables, dont plusieurs groupes de variables fortement corrélées entre elles (par exemple, différentes mesures biologiques liées à un même phénomène physiologique).

  • Le Lasso seul sélectionne une ou deux variables par groupe, ignorant le reste. L’interprétation est trompeuse.
  • Le Ridge seul attribue un petit coefficient à chacune des 200 variables. Le modèle est stable mais impossible à interpréter (aucune variable n’est écartée).
  • L’Elastic Net sélectionne des groupes entiers de variables pertinentes, attribuant des coefficients comparables aux variables corrélées et mettant les coefficients des variables non informatives exactement à zéro. C’est à la fois parcimonieux et fidèle à la structure de corrélation des données.

La géométrie de la contrainte

En régularisation L1 (Lasso), la région admissible des coefficients est un polyèdre (un losange en 2D), dont les sommets se trouvent sur les axes. L’intersection de la courbe d’erreur avec cette zone se fait donc souvent sur un axe, ce qui produit des coefficients nuls.

En régularisation L2 (Ridge), la région admissible est une boule (un cercle en 2D). L’intersection se fait en un point quelconque, aucun coefficient n’est forcé à zéro.

L’Elastic Net correspond à une interpolation entre ces deux formes : c’est un polyèdre aux arêtes arrondies. Les arêtes permettent la parcimonie (coefficients nuls), tandis que les arrondis permettent aux coefficients corrélés de partager l’effort de façon équilibrée.


Implémentation Python — Exemple complet

Installation

pip install scikit-learn numpy matplotlib

Code complet et commenté

# ============================================================
# Régression Elastic Net — Exemple complet et commenté
# ============================================================

import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import (
    ElasticNet, ElasticNetCV, Lasso, Ridge, LinearRegression
)
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error, r2_score
from sklearn.preprocessing import StandardScaler

# -------------------------------------------------------------------
# Étape 1 : Génération de données synthétiques
# On crée un jeu avec des groupes de variables corrélées
# -------------------------------------------------------------------

np.random.seed(42)
n_samples = 200
n_features = 50

# On crée 5 groupes de 10 variables corrélées
X = np.zeros((n_samples, n_features))
for groupe in range(5):
    # Variable latente commune à chaque groupe
    latent = np.random.randn(n_samples, 1)
    debut = groupe * 10
    fin = (groupe + 1) * 10
    # Chaque variable du groupe = latente + bruit individuel
    X[:, debut:fin] = 0.8 * latent + 0.2 * np.random.randn(n_samples, 10)

# Coefficients véritables : seuls 2 groupes sont vraiment importants
w_true = np.zeros(n_features)
w_true[0:10] = 3.0      # Groupe 0 : fortement contributif
w_true[10:20] = -2.0    # Groupe 1 : contribution négative
w_true[20:30] = 0.0     # Groupe 2 : non informatif
w_true[30:40] = 0.0     # Groupe 3 : non informatif
w_true[40:50] = 0.5     # Groupe 4 : faible contribution

# Variable cible avec bruit
y = X @ w_true + np.random.randn(n_samples) * 2.0

# -------------------------------------------------------------------
# Étape 2 : Séparation train/test et standardisation
# -------------------------------------------------------------------

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# Standardisation indispensable pour la régularisation
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# -------------------------------------------------------------------
# Étape 3 : Ajustement avec ElasticNetCV (recherche alpha automatique)
# -------------------------------------------------------------------

# ElasticNetCV teste automatiquement plusieurs valeurs de alpha
# pour différents l1_ratio, avec validation croisée interne
en_cv = ElasticNetCV(
    l1_ratio=[0.1, 0.3, 0.5, 0.7, 0.9, 0.95, 1.0],
    alphas=np.logspace(-3, 2, 100),
    cv=5,
    max_iter=10000,
    random_state=42
)
en_cv.fit(X_train_scaled, y_train)

print(f"Meilleur alpha : {en_cv.alpha_:.4f}")
print(f"Meilleur l1_ratio : {en_cv.l1_ratio_:.2f}")
print(f"R2 sur le test  : {en_cv.score(X_test_scaled, y_test):.4f}")

# -------------------------------------------------------------------
# Étape 4 : Comparaison ElasticNet vs Lasso vs Ridge vs OLS
# -------------------------------------------------------------------

# Configuration commune des hyperparamètres
alpha_opt = en_cv.alpha_
l1_ratio_opt = en_cv.l1_ratio_

# Entraînement des différents modèles
models = {
    "OLS": LinearRegression(),
    "Ridge": Ridge(alpha=alpha_opt),
    "Lasso": Lasso(alpha=alpha_opt, max_iter=10000),
    f"Elastic Net (l1={l1_ratio_opt:.2f})": ElasticNet(
        alpha=alpha_opt, l1_ratio=l1_ratio_opt, max_iter=10000
    ),
    "Elastic Net (l1=0.5)": ElasticNet(
        alpha=alpha_opt, l1_ratio=0.5, max_iter=10000
    ),
}

# Évaluation comparative
print("\n" + "=" * 70)
print(f"{'Modele':<40} {'R2 Test':>10} {'RMSE Test':>10} {'Coefficients non nuls':>20}")
print("-" * 70)

for nom, model in models.items():
    model.fit(X_train_scaled, y_train)
    y_pred = model.predict(X_test_scaled)
    r2 = r2_score(y_test, y_pred)
    rmse = np.sqrt(mean_squared_error(y_test, y_pred))
    n_nonzero = np.sum(np.abs(model.coef_) > 1e-6)
    print(f"{nom:<40} {r2:>10.4f} {rmse:>10.4f} {n_nonzero:>20}")

# -------------------------------------------------------------------
# Étape 5 : Visualisation des coefficients
# -------------------------------------------------------------------

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

# Graphique 1 : Coefficients véritables
axes[0, 0].bar(range(n_features), w_true, color='steelblue')
axes[0, 0].set_title('Coefficients veritables (generation des donnees)', fontsize=11)
axes[0, 0].set_xlabel('Index de la variable')
axes[0, 0].set_ylabel('Coefficient')
axes[0, 0].axhline(y=0, color='k', linewidth=0.5)
axes[0, 0].axvline(x=10, color='red', linestyle='--', linewidth=0.5)
axes[0, 0].axvline(x=20, color='red', linestyle='--', linewidth=0.5)
axes[0, 0].axvline(x=30, color='red', linestyle='--', linewidth=0.5)
axes[0, 0].axvline(x=40, color='red', linestyle='--', linewidth=0.5)

# Graphique 2 : OLS vs Elastic Net vs Lasso
x_pos = np.arange(n_features)
width = 0.25
axes[0, 1].bar(x_pos - width, models["OLS"].coef_, width, label="OLS", alpha=0.7)
en_key = [k for k in models if k.startswith("Elastic Net (l1=") and "]" not in k][0]
axes[0, 1].bar(x_pos, models[en_key].coef_, width,
               label=f"Elastic Net", alpha=0.7)
axes[0, 1].bar(x_pos + width, models["Lasso"].coef_, width, label="Lasso", alpha=0.7)
axes[0, 1].set_title('Comparaison des coefficients', fontsize=11)
axes[0, 1].set_xlabel('Index de la variable')
axes[0, 1].set_ylabel('Coefficient')
axes[0, 1].legend(fontsize=8)
axes[0, 1].axhline(y=0, color='k', linewidth=0.5)

# Graphique 3 : Parcours de régularisation (coefficients en fonction de alpha)
alphas_path = np.logspace(-3, 3, 50)
coefs_path = []
for a in alphas_path:
    en = ElasticNet(alpha=a, l1_ratio=l1_ratio_opt, max_iter=10000)
    en.fit(X_train_scaled, y_train)
    coefs_path.append(en.coef_)
coefs_path = np.array(coefs_path)

axes[1, 0].semilogx(alphas_path, coefs_path, linewidth=0.8, alpha=0.7)
axes[1, 0].set_title('Parcours des coefficients (variations de alpha)', fontsize=11)
axes[1, 0].set_xlabel('alpha (regularisation)')
axes[1, 0].set_ylabel('Coefficient')

axes[1, 1].axis('off')
resume = (
    "Résultats de la comparaison :\n\n"
    "L'Elastic Net sélectionne des groupes cohérents\n"
    "de variables corrélées, contrairement au Lasso\n"
    "qui en retient souvent une seule arbitrairement.\n\n"
    "Le meilleur R2 apparaît généralement avec un\n"
    "mélange équilibré de pénalités L1 et L2."
)
axes[1, 1].text(
    0.05,
    0.95,
    resume,
    fontsize=10,
    va='top',
    fontfamily='monospace',
    transform=axes[1, 1].transAxes,
    wrap=False,
)

plt.tight_layout()
plt.savefig('elastic_net_coefficients.png', dpi=150)
plt.close()

# -------------------------------------------------------------------
# Étape 6 : Graphique de l'impact du l1_ratio
# -------------------------------------------------------------------

fig2, ax = plt.subplots(figsize=(12, 7))
ratios = [0.1, 0.3, 0.5, 0.7, 0.9, 1.0]

for ratio in ratios:
    en = ElasticNet(alpha=alpha_opt, l1_ratio=ratio, max_iter=10000)
    en.fit(X_train_scaled, y_train)
    n_nz = np.sum(np.abs(en.coef_) > 1e-6)
    r2 = en.score(X_test_scaled, y_test)
    ax.plot(range(n_features), en.coef_, linewidth=1.5, alpha=0.8,
            label=f'l1_ratio={ratio:.1f} (R2={r2:.3f}, nz={n_nz})')

ax.set_title('Coefficients Elastic Net pour differentes valeurs de l1_ratio', fontsize=12)
ax.set_xlabel('Index de la variable')
ax.set_ylabel('Coefficient')
ax.legend(fontsize=9, loc='upper right')
ax.axhline(y=0, color='k', linewidth=0.5)

plt.tight_layout()
plt.savefig('elastic_net_l1_ratio.png', dpi=150)
plt.close()

# -------------------------------------------------------------------
# Étape 7 : Validation croisée manuelle de l1_ratio
# -------------------------------------------------------------------

from sklearn.model_selection import cross_val_score

print("\nValidation croisee de l1_ratio (alpha fixe) :")
for ratio in [0.1, 0.3, 0.5, 0.7, 0.9, 1.0]:
    en = ElasticNet(alpha=alpha_opt, l1_ratio=ratio, max_iter=10000)
    scores = cross_val_score(en, X_train_scaled, y_train, cv=5, scoring='r2')
    print(f"  l1_ratio={ratio:.1f} -> R2 moyen = {scores.mean():.4f} (+/- {scores.std():.4f})")

Points clés de l’implémentation

  1. Standardisation obligatoire : la régularisation est sensible à l’échelle des variables. Il faut toujours utiliser StandardScaler avant d’appliquer un ElasticNet.
  2. ElasticNetCV : cette classe effectue automatiquement une validation croisée sur une grille de valeurs pour alpha et l1_ratio. C’est le point de départ recommandé pour tout projet.
  3. Interprétation du parcours de coefficients : en observant comment les coefficients évoluent quand on fait varier $\alpha$, on identifie les variables les plus robustes (celles dont le coefficient reste non nul le plus longtemps).
  4. Deux graphiques produits : le script génère deux images PNG — elastic_net_coefficients.png (comparaison visuelle des modèles) et elastic_net_l1_ratio.png (impact du ratio L1/L2 sur les coefficients).

Hyperparamètres

Nom Rôle Valeurs typiques Impact
alpha Intensité globale de la régularisation 0.001 à 100 (recherche logarithmique) Plus alpha est grand, plus les coefficients sont réduits vers zéro. C’est l’hyperparamètre principal à tuner.
l1_ratio Proportion de la pénalisation L1 (le reste est L2) 0.0 à 1.0 l1_ratio=1 donne un Lasso pur. l1_ratio=0 donne un Ridge pur. En pratique, on explore souvent 0.1, 0.3, 0.5, 0.7, 0.9.
fit_intercept Calculer ou non un terme constant (intercept) True (défaut) ou False Généralement True, surtout si les données ne sont pas centrées manuellement.
max_iter Nombre maximum d’itérations de l’algorithme 1000 à 100000 Si le modèle ne converge pas, augmenter cette valeur. Les avertissements de convergence sont fréquents avec des alpha très petits.
tol Tolérance pour le critère de convergence 1e-4 (défaut) à 1e-6 Plus tol est petit, plus la solution est précise mais plus le calcul est long.
selection Stratégie de mise à jour des coefficients ('cyclic' ou 'random') 'cyclic' (défaut) ou 'random' 'random' peut converger plus vite sur certains jeux de données, surtout avec un tolérance élevée.
positive Forcer les coefficients à être positifs False (défaut) ou True Utile lorsque le domaine métier exige des coefficients positifs (ex : comptage, probabilités).

Avantages

  • Sélection de variables avec stabilité : contrairement au Lasso, l’Elastic Net sélectionne des groupes entiers de variables corrélées plutôt qu’une seule arbitrairement.
  • Performant en haute dimension : fonctionne bien même lorsque $p \gg n$ (plus de variables que d’observations), une situation classique en génomique ou en traitement du langage.
  • Flexibilité : en ajustant l1_ratio, on navigue continuement entre les comportements Lasso et Ridge, permettant de trouver le compromis optimal pour chaque problème.
  • Convexité garantie : la fonction objectif est strictement convexe (dès que $\rho < 1$), garantissant l’existence d’un minimum global unique.
  • Interprétabilité : comme le Lasso, il produit des modèles parcimonieux avec des coefficients exactement nuls, facilitant l’interprétation.
  • Implémentation robuste : disponible dans toutes les bibliothèques majeures (scikit-learn, glmnet, statsmodels).

Limites

  • Deux hyperparamètres à tuner : contrairement à Ridge ou Lasso qui n’ont qu’un seul paramètre $\alpha$, l’Elastic Net nécessite de rechercher à la fois $\alpha$ et l1_ratio, ce qui augmente le coût computationnel.
  • Sensibilité à l’échelle : comme toute méthode régularisée par norme, il requiert une standardisation préalable des variables.
  • Non-invariant aux transformations non linéaires : comme tout modèle linéaire, il ne capture pas les interactions ou les effets non linéaires sans feature engineering explicite.
  • Difficulté avec les corrélations non linéaires : l’effet de groupe fonctionne pour les corrélations linéaires, mais pas pour les dépendances non linéaires entre variables.
  • Moins performant que les méthodes non linéaires : sur des problèmes complexes avec de fortes non-linéarités, les forêts aléatoires ou les réseaux de neurones peuvent surpasser l’Elastic Net.

Cas d’usage

1. Génomique et bio-informatique

Dans les études d’association pangénomique (GWAS), on analyse des millions de SNPs (polymorphismes nucléotidiques) sur quelques milliers de patients. Les SNPs situés à proximité sur un chromosome sont fortement corrélés (déséquilibre de liaison). L’Elastic Net permet d’identifier des régions chromosomiques entières associées à une maladie, là où le Lasso ne retiendrait qu’un seul SNP représentatif. C’est une application emblématique de la régression elastic net en biologie computationnelle.

2. Finance quantitative

En construction de portefeuilles, les rendements d’actions d’un même secteur sont naturellement corrélés. L’Elastic Net permet de sélectionner les facteurs prédictifs pertinents (momentum, volatilité, ratios financiers) tout en maintenant un groupe cohérent de variables liées à un même risque systématique. Les modèles restent stables même en période de crise où les corrélations augmentent — un avantage crucial pour la gestion des risques.

3. Traitement du texte (NLP)

Les modèles bag-of-words ou TF-IDF produisent des milliers de features (mots, n-grammes) qui sont intrinsèquement corrélées : « machine », « learning » et « algorithme » co-occurrent souvent dans les mêmes documents. L’Elastic Net conserve ces groupes sémantiques ensemble, améliorant à la fois la prédiction et l’interprétation thématique du modèle. La régression elastic net est particulièrement adaptée aux problèmes de classification textuelle avec vocabulaire étendu.

4. Chimie médicinale et QSAR

En modélisation Quantitative Structure-Activité (QSAR), on prédit l’activité biologique d’une molécule à partir de centaines de descripteurs moléculaires (masse, polarité, surface, etc.). Ces descripteurs sont hautement corrélés par nature. La régression elastic net identifie les familles de descripteurs pertinents pour l’activité, guidant ainsi la conception de nouvelles molécules.


Voir aussi

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur la façon dont les données de vos commentaires sont traitées.