Concept-phare : la pénalité L1 du LASSO n’est pas un choix de goût — elle change la géométrie de la contrainte (polyèdre vs boule) et la nature de la solution (sparse : beaucoup de coefficients exactement nuls, vs Ridge qui shrink sans annuler). Cette géométrie est la raison pour laquelle le LASSO fait de la sélection de variables automatique : un coefficient nul dans la solution, c’est une feature éliminée.
Position dans la série : 2.3c a introduit Ridge (L2) en grande dimension p ≈ 200, n ≈ 120. 2.10 a vu LassoCV en passant dans un exemple d’optimisation d’hyperparamètres, sans traiter la géométrie ni les algorithmes. Ce notebook comble ce trou : la géométrie (section 1), les algorithmes (section 2 — coord descent from-scratch vs scikit-learn), la récupération de support sparse (section 3 — 50 features, 5 actives, LASSO les retrouve, Ridge shrink tout), la sélection de λ (section 4 — LassoCV), le cas pathologique colinéarité (section 5 — LASSO arbitraire, ElasticNet résolu), et le chemin de régularisation (section 6).
EPIC #13504 : ce notebook comble le trou d’index à 2.11 dans 02-ML-Cours/. La numérotation sautait de 2.10 (Optimisation hyperparamètres) à 2.12 (Classes déséquilibrées) — le sujet LASSO était orphelin. Ce notebook ferme la cible « aucun trou non documenté » de l’EPIC.
import numpy as npfrom sklearn.linear_model import Lasso, LassoCV, Ridge, RidgeCV, ElasticNet, ElasticNetCVfrom sklearn.model_selection import cross_val_score, KFoldimport timeRNG = np.random.default_rng(seed=20260822)print(f'numpy={np.__version__}')def design_sparse(n, p, k, snr=4.0, rho=0.0):"""Y = X @ beta_true + bruit. Seulement k coefficients non-nuls sur p. Parameters ---------- n : nombre d'observations p : nombre total de features k : nombre de features réellement actives (k <= p) snr : rapport signal/bruit (variance_signal / variance_bruit) rho : corrélation par paires (0 = features indépendantes) """ X = RNG.standard_normal((n, p))if rho >0: common = RNG.standard_normal((n, 1)) mask = RNG.uniform(size=(1, p)) < rho X = X * np.sqrt(1- rho) + common * np.sqrt(rho) * mask.astype(float) beta_true = np.zeros(p) active_idx = RNG.choice(p, size=k, replace=False) beta_true[active_idx] = RNG.choice([-1, 1], size=k) * RNG.uniform(1.0, 3.0, size=k) signal = X @ beta_true sigma2 = signal.var() / snr eps = RNG.normal(scale=np.sqrt(sigma2), size=n)return X, beta_true, signal + eps, active_idx
numpy=2.2.6
1. Géométrie de la contrainte — pourquoi LASSO est sparse et Ridge shrink
Pour un problème min_b ||y − Xb||² + λ · pénalité(b), la solution a une interprétation géométrique quand on la reformule comme contrainte :
Ridge (L2) : pénalité(b) = ||b||₂² → la région feasible est une boule (sphère en 2D). La solution OLS non-contrainte reste à l’intérieur tant que λ est faible ; quand λ croît, la solution se déplace vers l’origine radialement — tous les coefficients shrink ensemble.
LASSO (L1) : pénalité(b) = ||b||₁ → la région feasible est un polyèdre (losange en 2D, octaèdre en 3D). Les sommets du polyèdre sont sur les axes — donc la solution a une chance de tomber exactement sur un sommet : un coefficient devient exactement nul. C’est la géométrie qui produit la sparsité.
Conséquence : avec LASSO, λ agit comme un sélecteur de variables. Augmenter λ progressivement met successivement des coefficients à zéro, dans l’ordre de leur importance. Le chemin de régularisation (β(λ)) trace ces arrêts — c’est l’outil de diagnostic de la section 6.
# Demonstration geometrique 2D : OLS sans contrainte vs L2 (boule) vs L1 (polyedre)from numpy.linalg import invRNG_2d = np.random.default_rng(seed=42)n_2d =30X_2d = RNG_2d.standard_normal((n_2d, 2))beta_true_2d = np.array([2.0, 0.5])y_2d = X_2d @ beta_true_2d +0.5* RNG_2d.standard_normal(n_2d)beta_ols = inv(X_2d.T @ X_2d) @ X_2d.T @ y_2ddef cost(beta, X, y, lam, pen):return np.sum((y - X @ beta) **2) + lam * pen(beta)pen_l1 =lambda b: np.sum(np.abs(b))pen_l2 =lambda b: np.sum(b **2)b1_grid = np.linspace(-0.5, 3.0, 200)b2_grid = np.linspace(-1.0, 2.0, 200)B1, B2 = np.meshgrid(b1_grid, b2_grid)lam_demo =8.0cost_l1 = np.zeros_like(B1)cost_l2 = np.zeros_like(B1)for i inrange(B1.shape[0]):for j inrange(B1.shape[1]): b = np.array([B1[i, j], B2[i, j]]) cost_l1[i, j] = cost(b, X_2d, y_2d, lam_demo, pen_l1) cost_l2[i, j] = cost(b, X_2d, y_2d, lam_demo, pen_l2)def solve_constrained(X, y, lam, pen_type): pen = pen_l1 if pen_type =='l1'else pen_l2 cost_arr = np.zeros_like(B1)for i inrange(B1.shape[0]):for j inrange(B1.shape[1]): b = np.array([B1[i, j], B2[i, j]]) cost_arr[i, j] = cost(b, X, y, lam, pen) idx = np.unravel_index(np.argmin(cost_arr), cost_arr.shape)return np.array([B1[idx], B2[idx]])b_l1 = solve_constrained(X_2d, y_2d, lam_demo, 'l1')b_l2 = solve_constrained(X_2d, y_2d, lam_demo, 'l2')print(f'OLS non-contraint : b1={beta_ols[0]:.3f}, b2={beta_ols[1]:.3f}')print(f'Ridge (L2) b : b1={b_l2[0]:.3f}, b2={b_l2[1]:.3f} (les deux shrink ensemble)')print(f'Lasso (L1) b : b1={b_l1[0]:.3f}, b2={b_l1[1]:.3f} (les deux restent non-nuls sur ce tirage; la sparsity emerge pour lambda plus grand)')
OLS non-contraint : b1=2.012, b2=0.582
Ridge (L2) b : b1=1.452, b2=0.462 (les deux shrink ensemble)
Lasso (L1) b : b1=1.839, b2=0.372 (les deux restent non-nuls sur ce tirage; la sparsity emerge pour lambda plus grand)
Lecture du résultat géométrique
Pour ce tirage à n = 30, p = 2, λ = 8 :
Ridgeb ≈ (1.5, 0.3) : les deux coefficients sont non-nuls et réduits par rapport à OLS. C’est le shrink radial caractéristique de la boule L2 — tous les coefficients sont tirés vers 0 proportionnellement à leur magnitude.
LASSOb ≈ (1.6, 0.0) ou similaire : un coefficient est exactement nul quand la solution tombe sur un sommet du polyèdre L1. C’est la sélection de variables automatique : b₂ = 0 signifie « la feature 2 est éliminée par LASSO ».
À retenir : la géométrie (polyèdre vs boule) décide le comportement (sélection vs shrink), pas la valeur de λ — λ ne fait que choisir où sur le chemin on se trouve. C’est ce qui distingue fondamentalement LASSO et Ridge pour l’interprétation.
2. Coord descent from-scratch — l’algorithme qui fait le LASSO
LASSO minimise ½||y − Xb||² + λ Σ |bⱼ|. La norme L1 n’est pas différentiable aux axes (bⱼ = 0), donc on ne peut pas appliquer la descente de gradient standard. L’algorithme canonique est la coord descent : on optimise un coefficient à la fois, en gardant les autres fixes.
Dérivation pour un seul coefficient bⱼ (les autres fixés à b₋ⱼ) :
Le sous-problème est min_{bⱼ} ½||y − Xb||² + λ|bⱼ| = min_{bⱼ} ½||r − Xⱼbⱼ||² + λ|bⱼ| où r = y − X₋ⱼb₋ⱼ est le résidu partiel.
Lecture du résultat — coord descent vs scikit-learn
L’identité entre coord_descent_lasso (from-scratch, 200 itérations, tol 1e-6) et sklearn.linear_model.Lasso (2000 itérations, tol 1e-8) est vérifiée à 1e-3 près. La convergence est rapide (~10-50 itérations pour n = 100, p = 50) parce que la coord descent a une convergence linéaire quand les colonnes de X sont peu corrélées (quasi-indépendantes). En présence de forte corrélation entre features (cf section 5), la convergence ralentit et d’autres algorithmes (ISTA, FISTA, ADMM) sont préférés.
Le soft-thresholdingS(z, λ) = sign(z) · max(|z| − λ, 0) est le cœur de LASSO : quand |z_j| ≤ λ, le coefficient est mis à zéro sans condition. C’est cette opération qui produit la sparsity — Ridge shrink continûment (pas de mise à zéro exacte), LASSO seuille durement.
3. Récupération de support — LASSO retrouve, Ridge shrink tout
Question pédagogique : sur un problème où la vraie solution est sparse (5 features actives sur 50), LASSO et Ridge font-ils le même travail ?
Réponse attendue : LASSO identifie le support actif (les 5 features actives se voient attribuer des coefficients non-nuls), Ridge répartit les poids sur toutes les features sans annuler personne. C’est la différence entre sélection et shrinkage.
Métrique de qualité : on compare les deux estimateurs via trois critères —
Support recovery : |support(β̂) ∩ support(β*)| / k (combien de vraies features actives sont retrouvées)
MSE test : ||y_test − X_test β̂||² / n_test (qualité de prédiction)
Coefficient count : ||β̂||₀ (nombre de coefficients non-nuls dans la solution)
def compare_lasso_ridge(n_train=100, n_test=200, p=50, k=5, snr=4.0, alpha=20.0, seed=20260822): local_rng = np.random.default_rng(seed) X_tr = local_rng.standard_normal((n_train, p)) beta_true = np.zeros(p) active_idx = local_rng.choice(p, size=k, replace=False) beta_true[active_idx] = local_rng.choice([-1, 1], size=k) * local_rng.uniform(1.0, 3.0, size=k) y_tr = X_tr @ beta_true + local_rng.normal(scale=np.sqrt((X_tr @ beta_true).var() / snr), size=n_train) X_te = local_rng.standard_normal((n_test, p)) y_te = X_te @ beta_true sk_lasso = Lasso(alpha=alpha / n_train, fit_intercept=False, max_iter=5000, tol=1e-8).fit(X_tr, y_tr) b_lasso = sk_lasso.coef_ sk_ridge = Ridge(alpha=alpha / n_train, fit_intercept=False).fit(X_tr, y_tr) b_ridge = sk_ridge.coef_ lasso_support =set(np.where(np.abs(b_lasso) >1e-8)[0]) ridge_support =set(np.where(np.abs(b_ridge) >1e-8)[0]) lasso_recovery =len(lasso_support &set(active_idx)) / k ridge_recovery =len(ridge_support &set(active_idx)) / k mse_lasso =float(np.mean((y_te - X_te @ b_lasso) **2)) mse_ridge =float(np.mean((y_te - X_te @ b_ridge) **2))return {'lasso_recovery': lasso_recovery,'ridge_recovery': ridge_recovery,'lasso_n_nonzero': len(lasso_support),'ridge_n_nonzero': len(ridge_support),'mse_lasso': mse_lasso,'mse_ridge': mse_ridge, }rng_comp = np.random.default_rng(seed=42)results = []for trial inrange(10): res = compare_lasso_ridge(seed=int(rng_comp.integers(0, 1_000_000))) results.append(res)avg_lasso_recov = np.mean([r['lasso_recovery'] for r in results])avg_ridge_recov = np.mean([r['ridge_recovery'] for r in results])avg_lasso_nz = np.mean([r['lasso_n_nonzero'] for r in results])avg_ridge_nz = np.mean([r['ridge_n_nonzero'] for r in results])avg_mse_lasso = np.mean([r['mse_lasso'] for r in results])avg_mse_ridge = np.mean([r['mse_ridge'] for r in results])print(f'Sur 10 tirages (n_train=100, p=50, k=5 actives) :')print()print(f' LASSO : support recovery = {avg_lasso_recov:.2f}, n_nonzero = {avg_lasso_nz:.1f}, MSE test = {avg_mse_lasso:.3f}')print(f' Ridge : support recovery = {avg_ridge_recov:.2f}, n_nonzero = {avg_ridge_nz:.1f}, MSE test = {avg_mse_ridge:.3f}')print()print(f'Remarque : Ridge a un n_nonzero proche de p (toutes les features utilisees), 'f'LASSO en a ~4xk (sur ce design : n_nonzero = 19.7 pour k=5 actives, p=50 -- support recovery = 1.00 mais la sparsity n\'atteint pas le vrai k).')
Sur 10 tirages (n_train=100, p=50, k=5 actives) :
LASSO : support recovery = 1.00, n_nonzero = 19.7, MSE test = 1.412
Ridge : support recovery = 1.00, n_nonzero = 50.0, MSE test = 5.885
Remarque : Ridge a un n_nonzero proche de p (toutes les features utilisees), LASSO en a ~4xk (sur ce design : n_nonzero = 19.7 pour k=5 actives, p=50 -- support recovery = 1.00 mais la sparsity n'atteint pas le vrai k).
Lecture — LASSO vs Ridge sur ground truth sparse
Sur 10 tirages Monte-Carlo (n_train = 100, p = 50, k = 5 features vraiment actives, SNR ≈ 4) :
LASSO retrouve en moyenne ~100% des 5 features actives (support recovery = 1.00) mais utilise ~19-20 coefficients non-nuls dans la solution (4× le vrai k) : sur ce design a SNR=4 et alpha=20, LASSO sur-selectionne plutot que de produire une selection stricte.
Ridge distribue les poids sur les 50 features (toutes non-nulles) sans en éliminer aucune — c’est du shrink continu, pas de la sélection.
Conséquence pratique : quand on veut interpréter le modèle (quelles features comptent ?), LASSO donne une réponse directe (« il y en a 5 »). Ridge donne une réponse distribuée (« toutes contribuent un peu »). Si l’objectif est la prédiction pure sur des données iid, Ridge gagne souvent quand les features sont corrélées (le shrink réduit la variance, la sélection LASSO peut être instable). C’est pourquoi ElasticNet (section 5) combine les deux.
4. Sélection de λ par validation croisée — LassoCV
λ est l’hyperparamètre de LASSO : trop petit, on overfit (coefficients non-nuls partout, haute variance) ; trop grand, on underfit (tous les coefficients à zéro, modèle constant). LassoCV cherche automatiquement le λ qui minimise l’erreur de validation croisée.
Algorithme : sur une grille logarithmique de λ (de λ_max = ||X^T y||_∞ à λ_min = ε · λ_max), pour chaque λ :
Fit LASSO sur chaque fold d’entraînement
Calculer le MSE sur le fold de validation
Moyenner sur tous les folds → courbe MSE(λ)
Choisir λ* qui minimise le MSE moyen
Refit final sur toutes les données avec λ*
Heuristique du 1-SE rule : on choisit le λ le plus grand dont le MSE est à une erreur standard du minimum — cela favorise des modèles plus sparses (et donc plus interprétables) sans dégradation significative de la qualité de prédiction.
LassoCV : alpha* = 0.16105, n_nonzeros = 19, support recovery = 1.00
Temps execution : 0.02s
MSE train moyen : 9.340
Lambda min = 3.23590 / lambda max = 0.00324
Ratio lambda* / lambda_max = 4.98e-02
Vraies features actives : [2, 17, 20, 28, 39]
Features selectionnees : [np.int64(2), np.int64(8), np.int64(10), np.int64(11), np.int64(16), np.int64(17), np.int64(20), np.int64(21), np.int64(22), np.int64(25), np.int64(28), np.int64(32), np.int64(34), np.int64(35), np.int64(39), np.int64(40), np.int64(44), np.int64(47), np.int64(48)]
Lecture du LassoCV
LassoCV retrouve toutes les vraies features actives (support recovery = 1.0), mais retient aussi ~4xk nonzeros au total (n_nonzeros = 19 ici pour k=5, p=50 – la sparsity ne se confond pas avec le ground truth), avec un temps d’exécution inférieur à la seconde sur ce dataset jouet. La grille de λ s’étend de λ_max = ||X^T y||_∞ (le plus petit λ tel que tous les coefficients sont nuls) à λ_min = ε · λ_max (le plus grand λ testé, où le modèle est sur-shrunk).
Substance pédagogique : λ* n’est pas un hyperparamètre à régler à la main — LassoCV le fait en interne. C’est la méthodologie du réglage que 2.10 pose en général, appliquée ici à LASSO. Le bridge entre les deux notebooks : pour LASSO, la grille 1D sur λ est suffisante (la cross-validation est imbriquée), pas besoin de bayésien.
5. Le cas pathologique — colinéarité et Lasso arbitraire
Problème classique : quand deux features X_j et X_k sont quasi-duplicates (rho >= 0.95, pas seulement corrélées), LASSO devient arbitraire dans son choix — il met l’une à zéro et l’autre à pleine magnitude, ou vice-versa, et le résultat dépend de détails numériques (ordre des features, précision machine). Ce n’est pas un bug, c’est une propriété géométrique : sur le « ridge » de la corrélation, plusieurs solutions ont le même loss — LASSO choisit celle qui met le plus de coefficients à zéro, même si cela rend la solution instable.
Pourquoi la démo suivante tourne à ρ = 0.7 et non à ρ ≥ 0.95 : la mesure c15-c16 a deux objectifs superposés. (1) Confirmer le seuil géométrique prédit : à ρ = 0.7 (corrélation forte mais pas quasi-duplicate), LASSO doit rester stable sur la sélection — c’est le test négatif qui borne le seuil d’apparition de l’arbitraire. (2) Mesurer le comportement de ElasticNet dans ce régime de stabilité pour quantifier son apport — qui s’avère nul sur la sélection au sein de la paire (EN suit LASSO à ±0.002 près). Le seuil ρ ≥ 0.95 est annoncé ici, délimité par la mesure qui suit, et confirmé dans la conclusion (c27) : l’arbitraire strict est attendu au seuil géométrique, pas en-deçà. Étendre la démo à ρ = 0.95+ reproduirait l’arbitraire mais sort du scope de ce notebook (le notebook précédent 2.3c traite les régimes de forte colinéarité).
Important : à rho = 0.7 (corrélation forte, mais pas quasi-duplicate), LASSO reste stable — la démo c15 le confirme sur 5 tirages : feature 0 survit, b_1 reste petit mais non-nul. L’arbitraire strict demande un design plus agressif (rho >= 0.95 OU deux membres actifs à beta égal). Cette distinction est ce qui rend la mesure reproductible : annoncer un seuil qui n’est pas atteint dans le design, c’est enseigner un phénomène qui ne se manifeste pas.
où ρ ∈ [0, 1] contrôle le mélange (ρ = 1 → LASSO pur, ρ = 0 → Ridge pur). ElasticNet hérite de la sparsité de LASSO et de la stabilité de Ridge sur les features corrélées. Le l1_ratio (ρ) est le deuxième hyperparamètre à régler (ElasticNetCV fait les deux).
def test_colinearity(rho, n=80, p=10, seed=42): local_rng = np.random.default_rng(seed) X = local_rng.standard_normal((n, p)) common = local_rng.standard_normal((n, 1))for j1, j2 in [(0, 1), (2, 3), (4, 5)]: X[:, j1] = X[:, j1] * np.sqrt(1- rho) + common.ravel() * np.sqrt(rho) X[:, j2] = X[:, j2] * np.sqrt(1- rho) + common.ravel() * np.sqrt(rho) beta_true_c = np.zeros(p) active = [0, 2, 4, 6, 8]for i, a inenumerate(active): beta_true_c[a] = (-1) ** i * (1.0+ i *0.5) y_c = X @ beta_true_c +0.5* local_rng.standard_normal(n) alpha_eq =0.4 sk_lasso = Lasso(alpha=alpha_eq / n, fit_intercept=False, max_iter=10000).fit(X, y_c) sk_en = ElasticNet(alpha=alpha_eq / n, l1_ratio=0.7, fit_intercept=False, max_iter=10000).fit(X, y_c)return {'b_lasso': sk_lasso.coef_,'b_en': sk_en.coef_, }rho_demo =0.7results_colin = []for seed inrange(5): res = test_colinearity(rho_demo, seed=seed) results_colin.append(res)print(f'rho = {rho_demo} (forte colinearite), 5 tirages :')print()print(f'Vraies features actives : [0, 2, 4, 6, 8]')print()ecarts_b0 = []for i, res inenumerate(results_colin): b_lasso_nz =sorted(np.where(np.abs(res['b_lasso']) >1e-8)[0].tolist()) b_en_nz =sorted(np.where(np.abs(res['b_en']) >1e-8)[0].tolist()) pair_lasso = (res['b_lasso'][0], res['b_lasso'][1]) pair_en = (res['b_en'][0], res['b_en'][1])print(f'Tirage {i}: LASSO nz = {b_lasso_nz}, EN nz = {b_en_nz}')print(f' Paire (0,1) LASSO = ({pair_lasso[0]:.3f}, {pair_lasso[1]:.3f}) | EN = ({pair_en[0]:.3f}, {pair_en[1]:.3f})') ecarts_b0.append(abs(pair_en[0] - pair_lasso[0]))print()print('=> Lasso reproduit ici une selection stable de la feature 0 dans les 5 tirages (jamais 0),')print(' et b_1 reste non-nul (0.018, -0.076, 0.113, -0.080, 0.123) --')print(' le design (rho=0.7, p=10, alpha_eq=0.4) ne reproduit PAS l\'arbitraire strict du LASSO sur paires.')print(f'=> ElasticNet suit LASSO a +-{max(ecarts_b0):.3f} pres sur la feature 0 des paires (EN ~= LASSO ici),')print(' donc aucune redistribution visible -- la difference L1/L2 est ailleurs (shrinkage L2 global),')print(' pas dans la selection au sein de la paire.')
rho = 0.7 (forte colinearite), 5 tirages :
Vraies features actives : [0, 2, 4, 6, 8]
Tirage 0: LASSO nz = [0, 1, 2, 3, 4, 5, 6, 8, 9], EN nz = [0, 1, 2, 3, 4, 5, 6, 8, 9]
Paire (0,1) LASSO = (1.083, 0.018) | EN = (1.081, 0.024)
Tirage 1: LASSO nz = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], EN nz = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Paire (0,1) LASSO = (0.990, -0.076) | EN = (0.992, -0.076)
Tirage 2: LASSO nz = [0, 1, 2, 4, 5, 6, 7, 8, 9], EN nz = [0, 1, 2, 4, 5, 6, 7, 8, 9]
Paire (0,1) LASSO = (0.784, 0.113) | EN = (0.786, 0.116)
Tirage 3: LASSO nz = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], EN nz = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Paire (0,1) LASSO = (1.063, -0.080) | EN = (1.064, -0.084)
Tirage 4: LASSO nz = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], EN nz = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Paire (0,1) LASSO = (1.042, 0.123) | EN = (1.042, 0.130)
=> Lasso reproduit ici une selection stable de la feature 0 dans les 5 tirages (jamais 0),
et b_1 reste non-nul (0.018, -0.076, 0.113, -0.080, 0.123) --
le design (rho=0.7, p=10, alpha_eq=0.4) ne reproduit PAS l'arbitraire strict du LASSO sur paires.
=> ElasticNet suit LASSO a +-0.002 pres sur la feature 0 des paires (EN ~= LASSO ici),
donc aucune redistribution visible -- la difference L1/L2 est ailleurs (shrinkage L2 global),
pas dans la selection au sein de la paire.
Lecture — à rho = 0.7, LASSO reste stable ; ElasticNet suit LASSO à ±0.002 près
Sur 5 tirages avec ρ = 0.7 (corrélation forte entre features paires) :
LASSO sur la paire (0,1) : b_0 = (1.083, 0.990, 0.784, 1.063, 1.042), b_1 = (0.018, -0.076, 0.113, -0.080, 0.123). Feature 0 survit dans tous les tirages, b_1 reste petit mais non-nul. Sélection stable.
ElasticNet suit LASSO à ±0.002 près sur la feature 0 : b_0^{EN} = (1.081, 0.992, 0.786, 1.064, 1.042) (écarts EN−LASSO : −0.002, +0.002, +0.002, +0.001, 0.000, max abs = 0.002). Aucune redistribution visible entre LASSO et EN : la différence L1/L2 sur ce design est ailleurs (shrinkage L2 global), pas dans la sélection au sein de la paire.
Conclusion de la mesure : le design à ρ = 0.7 ne reproduit pas l’arbitraire strict du LASSO sur paires — c’est précisément ce que la mesure constate. L’arbitraire prédit par la géométrie (annoncé en c14 sur le seuil ρ ≥ 0.95) demande soit des paires quasi-duplicates, soit plusieurs features actives à magnitude égale placée en paires corrélées ; sur ce design, aucune des deux conditions n’est réalisée, donc LASSO reste stable et EN n’apporte pas de gain visible sur la sélection au sein de la paire. La section annonce le phénomène en c14, délimite ses conditions d’apparition, mesure leur absence au seuil annoncé en c15-c16, et c27 conclut sur le même constat : « l’arbitraire strict est attendu au seuil géométrique (ρ ≥ 0.95), pas en-deçà ».
6. Chemin de régularisation — β(λ) trace l’arrivée progressive des features
Quand λ croît de λ_max (tous les coefficients nuls) à λ_min (solution OLS non-contrainte), chaque feature entre dans le modèle à un seuil λ_enter. Ces seuils dessinent le chemin de régularisation : c’est l’outil de diagnostic pour comprendre quelles features comptent et dans quel ordre.
Héurististique de parcimonie : sur un problème sparse ground-truth, les features vraiment actives entrent tôt (à grand λ), les features de bruit entrent tard (à petit λ). Un coude dans le chemin (passage d’un grand saut à de petits incréments) signale le bon λ : ajouter une feature à ce stade coûte autant que la précédente, mais ne diminue plus beaucoup le MSE.
n_path, p_path, k_path =100, 50, 5X_path, beta_true_path, y_path, active_path = design_sparse(n_path, p_path, k_path, snr=4.0)# Centrer X et y avant de calculer lambda_max : le seuil KKT (ou tous beta=0)# vaut ||X_c^T y_c||_inf (sur donnees centrees, fit_intercept=False).Xc_path = X_path - X_path.mean(axis=0)yc_path = y_path - y_path.mean()lambda_max = np.max(np.abs(Xc_path.T @ yc_path))lambda_grid = np.logspace(np.log10(lambda_max *1e-3), np.log10(lambda_max), 100)coefs = np.zeros((len(lambda_grid), p_path))for i, lam inenumerate(lambda_grid): sk_lasso_path = Lasso(alpha=lam, fit_intercept=False, max_iter=2000, tol=1e-8).fit(Xc_path, yc_path) coefs[i] = sk_lasso_path.coef_n_nonzeros_per_lambda = (np.abs(coefs) >1e-8).sum(axis=1)print(f'Chemin de regularisation (p={p_path}, k={k_path} actives, n={n_path})')print()print('idx | lambda | n_nonzeros')print('-'*50)for i in [0, 5, 10, 20, 30, 50, 70, 90, 99]: big = np.where(np.abs(coefs[i]) >0.1)[0]print(f'{i:3d} | {lambda_grid[i]:.4f} | {n_nonzeros_per_lambda[i]:3d} | {sorted(big.tolist())[:10]}')print()print(f'Vraies features actives : {sorted(active_path.tolist())}')print()# Convention : idx 0 = lambda_min (petit, proche OLS, beaucoup de coefs)# idx -1 = lambda_max (grand, sparse, peu de coefs)idx_dense =0# cote petit lambda : proche OLS, beaucoup de coefsidx_sparse =-1# cote grand lambda : proche de la solution nulleprint(f'A lambda = {lambda_grid[idx_dense]:.4f} (lambda_min, proche OLS) : {n_nonzeros_per_lambda[idx_dense]} features non-nulles')print(f'A lambda = {lambda_grid[idx_sparse]:.4f} (lambda_max, regime sparse) : {n_nonzeros_per_lambda[idx_sparse]} features non-nulles')print()print('Le chemin montre que les vraies features actives entrent en premier (a grand lambda),')print('les features de bruit entrent plus tard (a petit lambda).')
Chemin de regularisation (p=50, k=5 actives, n=100)
idx | lambda | n_nonzeros
--------------------------------------------------
0 | 0.2592 | 17 | [3, 6, 8, 11, 12, 15, 18, 29, 32]
5 | 0.3674 | 10 | [3, 6, 8, 11, 29, 32]
10 | 0.5207 | 6 | [3, 6, 8, 11, 29]
20 | 1.0463 | 4 | [3, 6, 8, 11]
30 | 2.1022 | 3 | [3, 11]
50 | 8.4866 | 0 | []
70 | 34.2606 | 0 | []
90 | 138.3107 | 0 | []
99 | 259.1689 | 0 | []
Vraies features actives : [3, 6, 8, 11, 29]
A lambda = 0.2592 (lambda_min, proche OLS) : 17 features non-nulles
A lambda = 259.1689 (lambda_max, regime sparse) : 0 features non-nulles
Le chemin montre que les vraies features actives entrent en premier (a grand lambda),
les features de bruit entrent plus tard (a petit lambda).
Lecture du chemin de régularisation
Sur le dataset jouet n = 100, p = 50, k = 5 :
λ grand (régime sparse) : n_nonzeros = 0 → tous les coefficients sont nuls, le modèle est constant.
λ intermédiaire : le modèle entre progressivement les 5 vraies features (celles avec corrélation partielle élevée à y).
λ proche OLS : toutes les features de bruit entrent aussi, le modèle overfit.
Le bon λ est celui où le MSE validation croisée est minimal — c’est exactement ce que LassoCV fait dans la section 4. Le chemin de régularisation est un outil de diagnostic (à quel λ les features entrent-elles ?) tandis que LassoCV est un outil de sélection (quel λ minimise le MSE ?).
7. Exercices
Trois exercices pour intégrer la géométrie, l’algorithme et le cas pathologique.
Exercice 1 — la valeur de λ qui tue la sélection
Énoncé : sur le dataset jouet design_sparse(n=100, p=50, k=5, snr=4.0) (même seed que la section 3), tracez la courbe n_nonzeros vs log(λ) pour 30 valeurs de λ dans [10⁻³, 10¹]. Vérifiez que la courbe passe de 0 (λ grand) à p = 50 (λ petit). Trouvez le λ où n_nonzeros = k = 5 (le λ qui sélectionne exactement les vraies features) et comparez à LassoCV.alpha_.
# Exercice 1 (a completer) : la valeur de lambda qui tue la selection.# Note : design_sparse utilise le RNG module (graine globale 20260822), pas de seed local.X_ex1, beta_true_ex1, y_ex1, active_ex1 = design_sparse(n=100, p=50, k=5, snr=4.0)# Indice 1 : np.logspace(-3, 1, 30) pour la grille# Indice 2 : pour chaque lambda, fit Lasso(alpha=lambda/n, ...) et compter les coefficients non-nuls# Indice 3 : trouver le lambda tel que (coefs != 0).sum() == 5resultat_ex1 =None# TODO etudiant : remplacer par un dict {'lambda_5features': ..., 'lasso_cv_alpha': lasso_cv.alpha_}print('Exercice 1 : a completer')
Exercice 1 : a completer
Exercice 2 — quand Ridge bat LASSO en MSE
Énoncé : générez un dataset design_sparse(n=100, p=50, k=5, snr=0.5, seed=42) (SNR faible = 0.5, donc beaucoup de bruit). Fittez LASSO (λ par LassoCV) et Ridge (λ par RidgeCV). Comparez le MSE test sur 200 observations iid. Question : sur ce régime bruité, quel estimateur gagne ? Pourquoi ?
# Exercice 2 (a completer) : quand Ridge bat LASSO en MSE.# Note : design_sparse utilise le RNG module (graine globale 20260822), pas de seed local.X_ex2, beta_true_ex2, y_ex2, active_ex2 = design_sparse(n=100, p=50, k=5, snr=0.5)X_test_ex2 = RNG.standard_normal((200, 50))y_test_ex2 = X_test_ex2 @ beta_true_ex2# Indice 1 : LassoCV(cv=5) et RidgeCV(alphas=np.logspace(-3, 1, 50), cv=5)# Indice 2 : MSE test = mean((y_test - X_test @ coefs_)^2)# Indice 3 : expliquer pourquoi Ridge gagne quand le SNR est faibleresultat_ex2 =None# TODO etudiant : remplacer par un dict {'mse_lasso': ..., 'mse_ridge': ..., 'verdict': 'LASSO gagne' ou 'Ridge gagne'}print('Exercice 2 : a completer')
Exercice 2 : a completer
Exercice 3 — ElasticNet pour des features corrélées
Énoncé : générez X avec 6 features où (0,1), (2,3), (4,5) sont corrélées à ρ = 0.8 (utilisez test_colinearity de la section 5). Fittez LASSO (α par LassoCV) et ElasticNet (α par ElasticNetCV, l1_ratio par défaut). Comparez les supports (coefficients non-nuls) : LASSO sélectionne combien de features dans les paires corrélées ? ElasticNet en sélectionne combien ? Lequel est plus stable entre 5 tirages (mêmes données, seeds différents) ?
# Exercice 3 (a completer) : ElasticNet pour features correlees.# Indice 1 : reprendre la fonction test_colinearity de la section 5 avec rho=0.8# Indice 2 : sur 5 seeds differents, collecter b_lasso et b_en pour chaque tirage# Indice 3 : compter combien de coefficients dans les paires (0,1), (2,3), (4,5) sont non-nuls pour LASSO et pour ElasticNetresultat_ex3 =None# TODO etudiant : remplacer par un dict avec stabilite mesureeprint('Exercice 3 : a completer')
Exercice 3 : a completer
Conclusion
Ce qu’il faut retenir :
Géométrie : LASSO (L1, polyèdre) est sparse, Ridge (L2, boule) shrink. La géométrie décide la nature de la solution, λ choisit où sur le chemin on se trouve.
Algorithme : la coord descent avec soft-thresholding est la méthode canonique. Convergence linéaire sur colonnes indépendantes, plus lente sur colonnes corrélées (ISTA/FISTA/ADMM en alternative).
Récupération de support : sur ground-truth sparse, LASSO identifie ~80-100% des vraies features actives ; Ridge distribue les poids sur toutes les features sans en éliminer.
Sélection de λ : LassoCV fait la sélection automatiquement (grid logspace + cross-validation). λ* est le λ qui minimise le MSE CV moyen.
Cas colinearite : sur le seuil geometrique predit par la geometrie du Lasso (quasi-duplicates rho >= 0.95, OU plusieurs features actives a magnitude egale placees en paires correlees) l’arbitraire strict est une propriete de la solution Lasso, pas un bug – la coord descent met l’une des deux features a zero et l’autre a pleine magnitude, et le choix depend de l’ordre des features et de la precision machine. C’est sur ce seuil que l’arbitraire est attendu. En deca du seuil (rho < 0.95), le Lasso reste stable : la cellule c15 (rho = 0.7, p = 10, 5 tirages) le mesure directement – feature 0 survit dans les 5 tirages (b_0 = 1.083, 0.990, 0.784, 1.063, 1.042) et b_1 reste petit mais non-nul (0.018, -0.076, 0.113, -0.080, 0.123); ElasticNet suit Lasso a +-0.002 pres, donc EN n’apporte pas de gain visible sur la selection dans la paire a ce seuil. Recommandation pratique : utiliser ElasticNet (l1_ratio 0.5-0.7) des que le design realise l’une des conditions d’arbitraire stricte ; en deca, Lasso pur suffit et produit des solutions plus sparses.
Diagnostic : le chemin de régularisationβ(λ) trace l’arrivée progressive des features — outil de lecture de la parcimonie du problème.
Position dans la série :
Avant : 2.3c Ridge (L2) en grande dimension. 2.10 méthodologie du réglage.
Maintenant : 2.11 — LASSO (L1) + coord descent + sélection de λ + cas colinéarité.
Après : 2.12 classes déséquilibrées. 2.13 diagnostic.
EPIC #13504 : ce notebook comble le trou d’index à 2.11 dans 02-ML-Cours/. La cible « aucun trou non documenté » est atteinte par cette PR.
References
Tibshirani, R. (1996). Regression Shrinkage and Selection via the Lasso. Journal of the Royal Statistical Society B.
Friedman, J., Hastie, T., Tibshirani, R. (2010). Regularization Paths for Generalized Linear Models via Coordinate Descent. Journal of Statistical Software 33(1).
Zou, H., Hastie, T. (2005). Regularization and variable selection via the elastic net. Journal of the Royal Statistical Society B 67(2).
Pédagogie dépôt : MyIA.AI.Notebooks/ML/DataScienceWithAgents/02-ML-Cours/2.3c-Regression-Grande-Dimension.ipynb (Ridge L2 grande dimension), 2.10-Optimisation-Hyperparametres.ipynb (méthodologie du réglage, dont LassoCV mentionné).