Search Tech Journey

Find topics, journeys and posts

6-month learning plan87 / 130
back to blog
mlintermediate 55m read

S087 · Regularization — L1, L2, Elastic Net

Add a penalty to the loss and watch overfitting die — ridge shrinks, lasso selects, elastic net compromises. The single most useful technique in classical ML.

🤖Machine LearningM11 · Classical ML· Session 087 of 130 90 min

🎯 Add L1, L2, and Elastic Net penalties to your loss — understand why one shrinks, the other selects, and how to tune λ properly with cross-validation.

Why this session exists

Almost every real dataset has more features than you have signal for. Fit a plain linear or logistic regression on 200 features and 500 rows and you'll get glorious training accuracy that collapses on new data. Regularization is the one trick that lets you throw features at your model without paying the overfitting tax — and choosing between L1, L2, and Elastic Net is a design decision that recurs in every model family (deep learning included).

You will be able to
  • Write the loss functions for ridge, lasso, and elastic net from memory.
  • Explain geometrically why L1 produces sparse solutions and L2 shrinks smoothly.
  • Tune the regularization strength λ using k-fold cross-validation.
  • Choose between L1 and L2 based on whether you want feature selection or shrinkage.
  • Recognise regularization in disguise — weight decay, dropout, early stopping.

Prerequisites

  • S085 · Linear regression from scratch — you can implement OLS + GD.
  • S086 · Logistic regression — same gradient shape, extended loss.
  • S040 · Linear algebra — vector norms.


(a) Intuition · 5 min

A budget that stops you buying too many features
🌍 Real world

You're moving to a new apartment. Without a rule, you'll stuff every closet and end up with junk you never use. So you set a rule: total weight of stuff you bring must be under X kilos. Now you're forced to decide what's really worth carrying.

Two ways to enforce the rule: (1) charge $1 per kilo (soft budget — you can bring more, but it costs) — that's L1. (2) charge $1 per kilo squared (harder as you get bigger) — that's L2. Both stop you from bringing 500 kilos of stuff; they penalise differently.

💻 Code world

Regularization adds a penalty on the size of θ to the loss: L(θ) = data_loss(θ) + λ · penalty(θ). The optimiser now has to balance ‘fit the data’ against ‘keep θ small.’ Small θ = simpler model = less overfitting.

L2 penalty = λ·||θ||² = shrinks every coefficient smoothly toward zero. L1 penalty = λ·||θ||₁ = pushes small coefficients to exactly zero (feature selection). Elastic Net = combine both, tuned by a mixing ratio.

The three penalties in one comparison

Ridge, lasso, elastic net — one sentence each
  • Ridge (L2): L = data_loss + λ·Σθⱼ². Shrinks all coefficients smoothly. Never zeros them out. Handles correlated features gracefully.
  • Lasso (L1): L = data_loss + λ·Σ|θⱼ|. Zeros out small coefficients → feature selection built in. Struggles with highly correlated features (picks one arbitrarily).
  • Elastic Net: L = data_loss + λ·(α·Σ|θⱼ| + (1-α)·Σθⱼ²). Combines both. α controls the mix. Default sklearn α = 0.5.

A brief history

  1. 1943
    Tikhonov regularization
    Andrey Tikhonov invents it in the USSR to solve ill-posed inverse problems. Ridge is renamed western import.
  2. 1970
    Ridge regression · Hoerl & Kennard
    Formal introduction in statistics literature. Still a fringe technique for decades.
  3. 1996
    Lasso · Robert Tibshirani
    ‘Regression Shrinkage and Selection via the Lasso’ — one of the most-cited stats papers ever. Solved the ‘too many features’ problem.
  4. 2005
    Elastic Net · Zou & Hastie
    Combines L1 + L2. Handles correlated features better than pure lasso.
  5. 2012
    Dropout · Hinton et al
    Deep learning gets its own regularizer. Randomly zero out neurons during training — an implicit ensemble.
  6. 2019
    Double descent · OpenAI/Belkin
    The classical bias-variance curve gets a second dip in the overparameterised regime. Regularization reshapes it.

(b) Visual walkthrough · 15 min

The geometry — why L1 gives zeros and L2 doesn't

The loss minimum lies where the elliptical loss contour first touches the constraint region. For a diamond (L1), the touching point is almost always at a corner where one coordinate is zero — that's the sparsity effect. For a circle (L2), the touching point is anywhere on the boundary — no reason to prefer zero over any other value.

Ridge vs Lasso vs Elastic Net — full comparison

Ridge (L2)

Shrink everything · closed-form solvable

  • Loss: MSE + λ·Σθⱼ². Convex + smooth.
  • Closed-form: θ = (XᵀX + λI)⁻¹ Xᵀy — always invertible.
  • All coefficients get smaller, none zero.
  • Handles correlated features well (spreads weight).
  • Default in most sklearn classifiers.
Lasso (L1)

Feature selection built in

  • Loss: MSE + λ·Σ|θⱼ|. Convex but non-smooth at 0.
  • No closed form — solve with coordinate descent or subgradient.
  • Small coefficients pushed to EXACTLY zero.
  • With correlated features, picks one arbitrarily.
  • Great for high-dimensional data (d ≫ n).
Elastic Net

Best of both

  • Loss: MSE + λ(α·L1 + (1-α)·L2).
  • Sparsity from L1 + stability from L2.
  • Handles correlated features better than pure lasso.
  • Two hyperparameters (λ, α) — more tuning.
  • Default in DeepMind's tabnet, glmnet.

The bias-variance trade-off, one dial

1
λ = 0

No regularization. High variance, low bias. Overfits — perfect train, terrible test.

2🟡
λ small

Gentle shrinkage. Slight bias, big variance reduction. Usually helpful.

3
λ optimal

The sweet spot on the bias-variance trade-off. Found via cross-validation.

4
λ too large

All coefficients → 0. High bias, low variance. Model predicts the mean.

The regularization path — every coefficient as λ shrinks


Common misconception
✗ What most people think

"Regularization shrinks coefficients toward zero, which makes the model simpler and therefore less accurate on the training data but more accurate on new data. So it's a fixed tax I pay for generalisation."

✓ What is actually true

Regularization is not a tax with a fixed benefit — it is a deliberate bias-for-variance trade whose value depends entirely on where you currently sit. On an already-underfit model, adding L2 makes both training and test error worse. The penalty only pays when the variance it removes exceeds the bias it introduces, which is why λ is a hyperparameter you must tune per dataset rather than a good practice you always apply.

Why the myth is so sticky

The myth is sticky because in practice you almost always meet regularization in the regime where it helps — the model was overfitting, you turned on L2, test error dropped, story confirmed. You never see the counterfactual where the model was already too rigid. It is also reinforced by defaults: sklearn's LogisticRegression applies L2 with C=1.0 unless told otherwise, so many people have never observed an unregularised fit at all and assume the penalty is part of the model rather than an addition to it.

Prove it to yourself

Sweep λ and watch both curves. The point is that train error rises monotonically while test error is U-shaped — and that the minimum is not at zero and not at infinity:

from sklearn.linear_model import Ridge
import numpy as np

for lam in [0, 1e-3, 1e-1, 1, 10, 100, 1e4]:
    m = Ridge(alpha=lam).fit(Xtr, ytr)
    tr = ((m.predict(Xtr) - ytr)**2).mean()
    te = ((m.predict(Xte) - yte)**2).mean()
    print(lam, round(tr, 4), round(te, 4), round(np.abs(m.coef_).sum(), 2))

# train MSE only ever increases with lam
# test MSE falls then rises -- the U is the whole story
# coefficient magnitude collapses toward 0 as lam -> inf
From first principles
Start with the question

Why does L1 produce exactly-zero coefficients while L2 only shrinks them toward zero but never reaches it? Both penalise size. The difference in outcome is qualitative, not just a matter of degree — and it is forced by geometry.

  1. 1
    Both penalties can be written as constrained optimisation: minimise the squared error subject to the coefficient vector lying inside a budget region. L2 constrains ‖β‖₂ ≤ t (a ball); L1 constrains ‖β‖₁ ≤ t (a diamond/cross-polytope).
    forced by · every penalised objective has an equivalent constrained form by Lagrangian duality
  2. 2
    The squared-error surface forms concentric ellipses centred on the unconstrained least-squares solution. The regularised answer is the first point where an expanding ellipse touches the constraint region.
    forced by · you want the lowest-error point that is still inside the budget, which is the tangency point
  3. 3
    The L2 ball is perfectly smooth — no corners, no flat faces. An ellipse touching a sphere generically touches at a point with all coordinates non-zero, and the tangency location moves continuously as you shrink the budget.
    forced by · a smooth boundary has a unique tangent plane everywhere, so nothing distinguishes the axes
  4. 4
    The L1 diamond has vertices lying exactly on the axes, and at a vertex a coordinate is precisely zero. Vertices are corners, so a whole cone of ellipse orientations touches there first — the vertices capture tangency with disproportionate probability.
    forced by · non-differentiable corners attract the optimum; a range of gradients is compatible with a single corner
  5. 5
    Equivalently in gradient terms: the L2 gradient is 2β, which shrinks proportionally and therefore vanishes as β approaches zero — an asymptote it never crosses. The L1 subgradient is a constant ±λ regardless of magnitude, so it keeps pushing with undiminished force right up to zero and then holds the coefficient pinned there.
    forced by · a penalty gradient that does not vanish at the origin can drive a coefficient exactly to it and keep it there
⇒ Therefore

Therefore sparsity is a consequence of the L1 ball's corners — equivalently, of its constant-magnitude gradient. L2's smoothness makes exact zeros a measure-zero accident; L1's corners make them the common case.

And note what this predicts: with a group of highly correlated features, L1 must behave unstably — the corner it lands on is nearly arbitrary among the group, so tiny data perturbations flip which feature survives. L2, having no corners, spreads weight smoothly across all of them instead. That is exactly the observed behaviour, and exactly why Elastic Net exists: the L2 term restores the grouping effect while the L1 term keeps the sparsity.

Mental modelA leash on the coefficients

The loss function pulls coefficients outward toward whatever fits the training data best, including its noise. The penalty is a leash pulling them back toward the origin. The fitted model sits where the two forces balance, and λ is the leash's tension.

Short leash (large λ) ⇒ coefficients pinned near zero, model too rigid, high bias. Long leash (λ → 0) ⇒ coefficients chase noise, high variance. The shape of the leash determines the character of the answer: L2 pulls proportionally to size and so mostly disciplines the big coefficients, while L1 pulls with constant force and so eliminates the small ones outright.

  • You must standardise features first. The penalty is applied to coefficient magnitude, and magnitude depends on the feature's units — an unscaled feature is penalised by an accident of measurement.
  • Never penalise the intercept. It is not a slope; shrinking it biases the model's overall level toward zero for no reason.
  • L1 selects (zeros out), L2 shares (spreads across correlated features), Elastic Net does both. Choose by whether you need a shorter feature list or a stabler one.
  • Early stopping, dropout, data augmentation, and adding data are all regularisers too. Any constraint that limits how thoroughly the model can fit the training noise belongs to this family.
🔔 Fires when you see

Fire this the moment you see: a large gap between train and validation scores · coefficients with enormous magnitudes or wild sign flips across CV folds · more features than rows · perfectly separable classification data · a penalty applied without standardising · λ chosen once and never re-tuned after the feature set changed.

The tradeoff

You have several hundred features, many correlated, and you are overfitting. Ridge, Lasso, or Elastic Net?

Ridge (L2)
+ you gain keeps every feature but shrinks them, which handles multicollinearity gracefully by splitting weight across correlated features rather than picking one arbitrarily; the objective stays smooth so it has a closed form and optimises fast and stably; predictions are usually the most accurate of the three when most features carry some signal
− you pay no feature selection at all — you still compute, store, and serve every feature, so the inference pipeline and the data dependencies stay as heavy as before; and the model remains hard to read, since "small but non-zero" across 300 features tells a human nothing
pick when you believe most features contribute a little (dense true signal), features are correlated, and prediction accuracy matters more than a short feature list
Lasso (L1)
+ you gain produces an actually sparse model — many coefficients exactly zero — which shrinks the serving pipeline, removes upstream data dependencies, and yields something a human can read and a stakeholder can question
− you pay unstable under correlated features: it picks one of a correlated group essentially at random and the choice flips across bootstrap samples, so "the selected features" is not a reliable finding. It also saturates — it cannot select more than n features when p exceeds n — and generally predicts slightly worse than ridge when the truth is dense.
pick when you genuinely believe only a small subset matters, or the operational cost of each feature (latency, an upstream service, a data contract) is high enough that dropping features has real value
Elastic Net (L1 + L2)
+ you gain the L2 component restores the grouping effect, so correlated features enter or leave together and selection stops flip-flopping, while the L1 component still delivers zeros; it also escapes Lasso's n-feature ceiling
− you pay two hyperparameters instead of one, so the CV grid is a two-dimensional search and tuning cost roughly squares; and the result is neither maximally sparse nor maximally accurate — a compromise on both axes
pick when p is large with genuinely correlated blocks of features and you want sparsity you can trust — the common situation in real feature stores
What a senior engineer actually does

Start with Ridge as the default, because unless you have a real reason to believe the signal is sparse, dense-and-shrunk is the safer prior and it is the most numerically well-behaved. Reach for L1 when a shorter feature list has concrete operational value — every feature you delete is a pipeline you no longer maintain and a service that can no longer take you down.

The failure mode worth naming: treating Lasso's selected features as a scientific claim about which variables matter. With correlated inputs that list is an artefact of the sample, and it will change if you refit next month. If you need stable selection, use Elastic Net or check selection stability across bootstrap resamples — and either way tune λ by cross-validation on your actual data, since the optimum has no default and moves whenever the feature set does.


(c) Hands-on · 25 min

We're going to fit OLS, ridge, lasso, and elastic net on a dataset with lots of noise features, then cross-validate the regularization strength.

# regularization.py — L1/L2/EN comparison on noisy data, ~130 lines.
import numpy as np
import pandas as pd
from sklearn.datasets import make_regression
from sklearn.linear_model import LinearRegression, Ridge, Lasso, ElasticNet, RidgeCV, LassoCV, ElasticNetCV
from sklearn.model_selection import train_test_split, KFold
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.metrics import mean_squared_error
 
np.random.seed(42)
 
# --- 1. Data: 200 rows, 50 features, but only 5 are actually informative ---
# The other 45 are pure noise — a great stress test for lasso.
X, y, true_coef = make_regression(
    n_samples=200, n_features=50, n_informative=5,
    noise=15.0, coef=True, random_state=42
)
print(f"True nonzero coefficients: {np.sum(true_coef != 0)} of {len(true_coef)}")
print(f"True nonzero features: {np.where(true_coef != 0)[0].tolist()}")
 
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)
 
# --- 2. Baseline: plain OLS ---
pipe_ols = Pipeline([("scaler", StandardScaler()), ("model", LinearRegression())])
pipe_ols.fit(X_train, y_train)
rmse_ols = mean_squared_error(y_test, pipe_ols.predict(X_test), squared=False)
nonzero_ols = np.sum(np.abs(pipe_ols.named_steps["model"].coef_) > 1e-4)
print(f"\n[OLS · λ=0]    test RMSE = {rmse_ols:6.2f}   nonzero coefs = {nonzero_ols}/50")
 
# --- 3. Ridge (L2) — cross-validated over λ ---
pipe_ridge = Pipeline([
    ("scaler", StandardScaler()),
    ("model", RidgeCV(alphas=np.logspace(-3, 3, 50), cv=5)),
])
pipe_ridge.fit(X_train, y_train)
best_alpha_ridge = pipe_ridge.named_steps["model"].alpha_
rmse_ridge = mean_squared_error(y_test, pipe_ridge.predict(X_test), squared=False)
nonzero_ridge = np.sum(np.abs(pipe_ridge.named_steps["model"].coef_) > 1e-4)
print(f"[Ridge · CV]   test RMSE = {rmse_ridge:6.2f}   nonzero = {nonzero_ridge}/50   λ = {best_alpha_ridge:.4f}")
 
# --- 4. Lasso (L1) — cross-validated over λ ---
pipe_lasso = Pipeline([
    ("scaler", StandardScaler()),
    ("model", LassoCV(alphas=np.logspace(-3, 2, 50), cv=5, max_iter=10000)),
])
pipe_lasso.fit(X_train, y_train)
best_alpha_lasso = pipe_lasso.named_steps["model"].alpha_
rmse_lasso = mean_squared_error(y_test, pipe_lasso.predict(X_test), squared=False)
lasso_coefs = pipe_lasso.named_steps["model"].coef_
nonzero_lasso = np.sum(np.abs(lasso_coefs) > 1e-4)
print(f"[Lasso · CV]   test RMSE = {rmse_lasso:6.2f}   nonzero = {nonzero_lasso}/50   λ = {best_alpha_lasso:.4f}")
 
# --- 5. Elastic Net — CV over both α (mix) and λ ---
pipe_en = Pipeline([
    ("scaler", StandardScaler()),
    ("model", ElasticNetCV(l1_ratio=[.1, .3, .5, .7, .9], alphas=np.logspace(-3, 2, 50), cv=5, max_iter=10000)),
])
pipe_en.fit(X_train, y_train)
best_l1 = pipe_en.named_steps["model"].l1_ratio_
best_alpha_en = pipe_en.named_steps["model"].alpha_
rmse_en = mean_squared_error(y_test, pipe_en.predict(X_test), squared=False)
nonzero_en = np.sum(np.abs(pipe_en.named_steps["model"].coef_) > 1e-4)
print(f"[ElasticNet]   test RMSE = {rmse_en:6.2f}   nonzero = {nonzero_en}/50   λ = {best_alpha_en:.4f}, α = {best_l1}")
 
# --- 6. Compare recovered nonzero features to truth ---
true_nonzero = set(np.where(true_coef != 0)[0].tolist())
lasso_nonzero = set(np.where(np.abs(lasso_coefs) > 1e-4)[0].tolist())
print(f"\nTrue nonzero features: {sorted(true_nonzero)}")
print(f"Lasso-recovered     : {sorted(lasso_nonzero)}")
print(f"Correctly recovered : {sorted(true_nonzero & lasso_nonzero)}")
print(f"False positives     : {sorted(lasso_nonzero - true_nonzero)}")
print(f"False negatives     : {sorted(true_nonzero - lasso_nonzero)}")
 
# --- 7. Regularization path for lasso — watch coefs shrink to zero ---
print("\n=== Lasso regularization path ===")
for alpha in [100, 30, 10, 3, 1, 0.3, 0.1, 0.01]:
    m = Lasso(alpha=alpha, max_iter=10000)
    m.fit(StandardScaler().fit_transform(X_train), y_train)
    active = np.sum(np.abs(m.coef_) > 1e-4)
    print(f"  λ = {alpha:>6}: {active:2d} active features")

What each block does

Anatomy of the script

make_regression with informative=5
Creates ground truth where only 5 of the 50 features have nonzero true coefficients. A perfect stress test for lasso's feature selection claim.
data
Pipeline everywhere
Regularization is scale-sensitive. Pipeline ensures scaler.fit() runs only on train inside each CV fold — no leakage.
hygiene
RidgeCV / LassoCV / ElasticNetCV
sklearn's built-in cross-validated variants. Avoid hand-rolling CV loops — these have efficient warm-starts across the alpha grid.
cv
np.logspace(-3, 3, 50)
Log-spaced alpha grid from 0.001 to 1000. Regularization strength should always be searched on a log scale — it's the order-of-magnitude that matters.
hyperparam
Compare recovered features
For lasso specifically, we can check whether it found the same 5 features that generated the data. This is where L1 shines.
select
Regularization path
The number of active (nonzero) features as λ shrinks. Watch it grow from 0 at λ=100 to ~40 at λ=0.01.
path
Try itAdd correlated features and watch lasso stumble

Add three copies of an existing informative column (X[:, 0] + tiny noise) to your feature matrix. Refit lasso and elastic net. You'll see:

  • Lasso: keeps one of the four correlated columns, zeros the other three — arbitrarily.
  • Ridge: spreads weight across all four correlated columns.
  • Elastic Net: spreads across all four but with smaller coefs on the copies.

For prediction accuracy, elastic net beats lasso here. For interpretability, lasso is misleading.

💡 Hint · With correlated features, elastic net becomes clearly better than pure lasso. This is the exact motivation for combining L1 + L2.

(d) Production reality · 15 min

War story Netflix Prize · 2006-2009· 2009$1M prize
🔥 What broke

Early teams naively fit high-dimensional matrix factorisations without regularization. Training RMSE looked amazing; test (private LB) RMSE was catastrophic. The models had memorised the training ratings.

🧯 The fix
Every serious team added L2 regularization to the latent factors and the biases. The formula L = ||R - PQᵀ||² + λ(||P||² + ||Q||²) became standard. It's still the default in every collaborative filtering library today.
🎓 Lesson to steal
Any model with lots of parameters and finite data needs regularization. In collaborative filtering, without L2 you're just memorising a rating matrix.
Post-mortem
War story Every kaggle competition · 2010-2020thousands of teams
🔥 What broke

Common winning pattern: gradient-boosted trees + a linear model with heavy regularization, then blend them. Teams that skipped regularization on the linear model always did worse — the blend requires each ingredient to generalise.

Beginners routinely reported ‘but my lasso model has RMSE 100!’ Root cause: forgot to scale features. The regularization was penalising the wrong things.

🧯 The fix
Learn the pipeline pattern. Always scale + regularize + CV-tune the alpha. Never one without the others.
🎓 Lesson to steal
Regularization is not magic. It requires scaling, CV, and a log-scale search over λ. Skip any of the three and you get worse-than-OLS results.
War story OpenAI · GPT training · 2019-present~billions of parameters
🔥 What broke

Transformer training has always used weight decay (L2 regularization on the weights). Without it, the models overfit small subsets of the training data. With classic Adam + weight decay, the effective regularization was subtly wrong (AdamW paper, 2019).

🧯 The fix
AdamW decoupled weight decay from the gradient update, making it a true L2 regularizer. Every modern LLM (GPT, LLaMA, Claude, Gemini) uses AdamW with weight decay ~0.1.
🎓 Lesson to steal
Regularization scales all the way up. The λ in your 50-parameter linear model is the same λ (weight decay) in your billion-parameter LLM. Different scale, same math.
Post-mortem

Where this shows up in the rest of the plan

Regularization is a fundamental primitive across all of ML
S088 · Bias–variance
The formal explanation of what λ is trading off.
S089 · Decision trees
Max depth, min samples per leaf = regularization for trees.
S091 · Gradient boosting
XGBoost has separate L1 + L2 penalties on leaf weights + tree structure.
S095 · Neural network fundamentals
‘Weight decay’ = L2 on network weights. Dropout = implicit ensemble regularization.
S097 · Bayesian regression
Ridge = MAP with Gaussian prior on θ. Lasso = MAP with Laplace prior.
S123 · System design · rec systems
Every embedding model uses L2 (‘reg loss’) on the embedding tables.

(e) Recall + stretch · 10 min

Recall — click each to reveal · click to reveal
★ = stretch question

Explain-out-loud test

If you can't teach these three without notes, redo the session:

  1. What does regularization do in one sentence? (limits θ magnitude to prevent overfitting)
  2. Difference between ridge and lasso in behaviour? (shrink vs select)
  3. What breaks if you skip scaling before regularization? (penalty applies unequally across features)

What comes next

Hub: The 6-Month Learning Plan


Part of a 130-session evergreen learning series. Session structure: intuition → visual → hands-on → production war stories → recall. Duration: 90 minutes.