Skip to content
intermediate Phase 16 · Hyperparameter Tuning and Regularization

Regularization with L1, L2, and Dropout

Constrain model capacity so trained networks generalize beyond training data.

55m
0 problems
Topic Progress 0%

Why Adding 0.0001 × Σ|w| to Your Loss Turns 200 Features Into 37

A fraud team gives you 200 candidate features and a mandate: the scoring service must explain every decision by naming the features it used. A plain logistic regression fits all 200 and its coefficients wobble wildly between cross-validation folds — correlated features trade weight back and forth, and last week's top feature is this week's noise. Adding a penalty to the loss fixes both problems at once: the fit gets stable, and most coefficients collapse to exactly zero.

The two penalties differ in one algebraic step, and everything else follows from it.

L2 (ridge): minimize L(w) + λ Σ w_j². The gradient of the penalty is 2λw, so each SGD step becomes:

w ← w − η(∇L + 2λw) = (1 − 2ηλ)·w − η∇L

That factor (1 − 2ηλ) < 1 multiplies the weight down every single step — multiplicative shrinkage. Over N steps of a fixed rate the cumulative multiplier is approximately (1 − 2ηλ)^N ≈ e^(−2ηλN): weights decay exponentially toward zero but, in exact arithmetic, never reach it. L2 makes weights small; it never makes them zero.

L1 (lasso): minimize L(w) + λ Σ |w_j|. The derivative of |w| is sign(w), a constant-magnitude pull of λ regardless of how tiny the weight already is:

w ← w − η·∇L − ηλ·sign(w)

A small weight gets pulled by the same force as a large one, so it crosses zero and is clamped there — the proximal update is soft-thresholding, w_j ← sign(w_j)·max(0, |w_j| − ηλ). L1 produces exact zeros, which is why it doubles as feature selection: 200 features in, 37 non-zero coefficients out, and the fraud team gets its explanation list.

Geometry explains the sparsity without any calculus: level sets of the loss are ellipses, and the feasible region |w₁|+|w₂| ≤ t is a diamond with corners on the axes. The constrained optimum sits where the ellipse first touches the diamond, and because a corner is a sharp point, contact happens at an axis with probability 1 — one weight is exactly zero. L2's circle has no corners, so contact lands anywhere.

L1 L2 ElasticNet
Penalty λ Σ|w_j| λ Σ w_j² λ₁ Σ|w_j| + λ₂ Σ w_j²
Effect on weights soft-threshold to exact 0 uniform shrink, never 0 both
Feature selection yes, built in no yes, grouped
Correlated features picks one arbitrarily spreads weight across all groups them
Gradient λ·sign(w) (subgradient) 2λw mixed

In scikit-learn the penalty strength is alpha (for Ridge/Lasso) or C (for LogisticRegression, where C is inverse regularization — large C means weak penalty). That quantity acts multiplicatively, so it belongs on a log scale in any sweep, exactly as in Grid and Random Search:

from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_validate, StratifiedKFold
from scipy.stats import loguniform

for C in [1e-3, 1e-1, 1e1, 1e3]:           # decades, not 0.1/0.2/0.3
    clf = LogisticRegression(C=C, penalty='l1', solver='liblinear',
                             max_iter=5000)   # liblinear/saga support L1
    cv = cross_validate(clf, X_tr, y_tr, cv=StratifiedKFold(5, shuffle=True,
                        random_state=42), scoring='roc_auc')
    print(f'C={C:>6}  AUC={cv["test_score"].mean():.4f}  '
          f'nonzero={sum((clf.fit(X_tr, y_tr).coef_ != 0).ravel())}')

The loop reports the two axes you actually trade off: validation AUC and the number of surviving features. Pick the smallest model within one standard error of the best AUC.

One distinction interviewers like to probe: L2 and weight decay are the same thing for plain SGD, but not for Adam. With SGD, weight_decay=λ adds λw to the gradient, identical to L2. Adam then divides that gradient by √v (a running second moment), so the effective shrinkage depends on each weight's gradient history — often far weaker or stranger than intended. AdamW applies the decay directly to the weights, decoupled from the adaptive scaling: torch.optim.AdamW(params, lr=3e-4, weight_decay=0.01) versus torch.optim.Adam(params, weight_decay=0.01). In deep learning you almost always want the decoupled form; L1 rarely appears directly in neural nets because exact zeros fight with optimizers' momentum, which is where dropout takes over — the next chapter.

Dropout as an Ensemble of 2ⁿ Thinned Networks You Never Trained Separately

A 512-unit hidden layer trained on 30,000 examples learns a comfortable shortcut: unit 217 and unit 91 fire together on every training batch, and the classifier leans almost entirely on that one path. Training accuracy hits 1.0 in fifteen epochs; validation accuracy stalls at 0.79. Dropout attacks the shortcut directly: during training, each unit is independently zeroed with probability p before it reaches the next layer.

import torch.nn as nn

class Net(nn.Module):
    def __init__(self, d_in=64, d_out=2):
        super().__init__()
        self.fc1 = nn.Linear(d_in, 256)
        self.drop = nn.Dropout(p=0.3)      # p = 0.3 → each unit survives with prob 0.7
        self.fc2 = nn.Linear(256, d_out)

    def forward(self, x):
        x = torch.relu(self.fc1(x))
        x = self.drop(x)                   # active only while model.train()
        return self.fc2(x)

Every training forward pass therefore evaluates a different thinned subnetwork — with n units there are 2^n possible masks, and gradient updates accumulate across all of them. The weights can never rely on unit 217 and unit 91 co-firing, because in roughly half the batches one of them is absent. Robustness to a missing partner is learned instead.

Two mechanics questions come up every time, and both have crisp answers.

Why scale at training time? PyTorch and Keras both use inverted dropout: activations are multiplied by 1/(1 − p) during training, so the expected activation each unit sends downstream is unchanged, and inference runs the full network with raw weights — no adjustment. The legacy Theano-style alternative (scale by 1/(1−p) at test time instead) is what breaks models ported between frameworks: a Keras model evaluated inside a hand-written Theano-style loop comes out 1/(1−p) too small. If your predictions change run to run in production, the single most likely cause is that you forgot model.eval():

model.train()   # dropout active, BatchNorm updates running stats
model.eval()    # dropout is the identity, predictions are deterministic
with torch.no_grad():
    preds = model(X_test)

model.eval() does not mean "evaluate the metric"; it means "switch off train-time behavior". Skipping it gives nondeterministic serving predictions that vary by several accuracy points — a bug that survives code review because training code usually calls train() for you.

Why is it an ensemble? With masks m ∈ {0,1}ⁿ drawn with E[m_j] = (1 − p), the dropout output at expectation equals the output of the geometric mean over subnetworks, so training optimizes an average over 2^n models that share weights — Hinton's description is "an efficient approximation to averaging over all thinned networks." Gal and Ghahramani proved the train-time mask corresponds to a variational approximation over the model's posterior, which is why Monte Carlo dropout at inference works: run K=10 forward passes with dropout enabled under torch.no_grad(), average them for the prediction, and take the variance as epistemic uncertainty — high variance means the ensemble disagrees, i.e. the input is unlike anything it saw.

Setting Typical p Notes
Input layer 0.0–0.2 often 0; too much destroys signal
Hidden MLP layers 0.2–0.5 0.5 is the classic default for fully-connected nets
Recurrent/attention 0.1–0.3 higher p destabilizes sequence training
Small CNNs 0 (use augmentation + weight decay) dropout fights BatchNorm's statistics

Where dropout is the wrong tool: convolutional networks with BatchNorm, where zeroed activations corrupt batch statistics and augmentation already supplies most of the regularization; and tiny models where the mask adds variance to an already-noisy gradient. Where it is the right tool: wide fully-connected towers trained on modest data — precisely the model that memorizes through one pathway.

Do not stack every lever at once. Dropout 0.5 + weight decay 1e-2 + aggressive augmentation + short patience on a 20,000-row dataset produces a model whose training loss never gets below 0.9: that is underfitting, and the fix is removing regularization. Knowing which lever to reach for, and in which direction, is the diagnosis you will practice in Diagnosing the Generalization Gap; the next chapter builds the decision table and adds the regularizer that costs nothing — more training examples.

The Free Regularizer: Augmentation, Label Smoothing, and Choosing Which Lever to Pull

Every training image in your chest-X-ray dataset arrives already centered, and the model scores 0.94 on validation — then a hospital sends a film shifted two centimeters left and accuracy falls to 0.71. The dataset did not shrink; the model simply never saw that variation. Data augmentation regularizes by changing what the model is allowed to memorize: if every epoch presents a different plausible rendering of the same label, the only stable thing left to learn is the mapping itself.

The arithmetic of augmentation is why it beats most hand-built tricks. A 32×32 CIFAR image has 32 valid horizontal crops × 2 flip states = 64 distinct training views per stored image; 50,000 images become 3.2 million views a year of training without a single byte of new data. The standard torchvision recipe:

import torch, torchvision.transforms as T

train_tf = T.Compose([
    T.RandomCrop(32, padding=4),
    T.RandomHorizontalFlip(),
    T.ColorJitter(brightness=0.2, contrast=0.2),
    T.ToTensor(),
    T.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)),
])
val_tf = T.Compose([T.ToTensor(), T.Normalize(...)])   # NEVER augment at evaluation
criterion = torch.nn.CrossEntropyLoss(label_smoothing=0.1)
opt = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=0.05)

Two lines in that block deserve attention. Label smoothing replaces the one-hot target with (1 − ε)·y + ε/K — with ε = 0.1 and K = 10, the target is 0.91 for the correct class. Its effect is mathematical: a plain cross-entropy keeps pushing the correct logit toward +∞ even after the prediction is right, driving confidence and weight norms upward forever; the smoothed target caps the optimum at a finite logit of roughly log((1−ε)/(ε/K)), so the model stops inflating its confidence. Calibrated probabilities and a smaller weight norm come out of the same change. Second, weight_decay=0.05 belongs on AdamW, because (as covered above) decoupled decay is what actually shrinks the weights — and standard practice excludes biases and normalization parameters from decay while applying it to the 2-D weight tensors.

More data variants that act as regularizers:

Lever Mechanism Cost
Flip / crop / jitter forces invariance to nuisance variation zero (on the fly)
Mixup: x = λx_i + (1−λ)x_j, y likewise softens decision boundaries between neighbors λ ~ Beta(0.2, 0.2) to tune
Cutout / CutMask destroys whole regions so no single patch carries the label free
Label smoothing ε=0.05–0.1 caps logit growth, calibrates confidence free
Extra real labeled data moves the data frontier itself labeling budget

Now the decision problem you will actually face in an interview: the generalization gap is open — training loss 0.12, validation 0.48 — and you have a drawer full of regularizers. Which one, and in which direction? Read the train metric first, because the wrong direction actively harms:

Train loss Val loss Diagnosis Action
high high, close together underfitting remove dropout/decay, add capacity or features
low high, gap opens after epoch k overfitting weight decay, dropout, augmentation, stop at k
low high, gap from epoch 1 capacity ≫ data signal smaller model or far more data/augmentation
low high, noisy spikes LR or split problems lower LR, check split hygiene before regularizing

The stop-at-k row in that table is not a manual decision either — the patience rule from Early Stopping and Learning Rate Schedules automates it, and the rest of the levers above change what the curve looks like before the stopper fires.

Stacking is a search, not a checklist: λ (weight decay), p (dropout), ε (smoothing), and augmentation strength are four hyperparameters interacting on one axis of "how constrained is the model". Tune them jointly on a log scale with the budget arithmetic from Grid and Random Search, or let a surrogate spend your budget more cleverly — that is exactly what Bayesian Hyperparameter Optimization does with Optuna.

One training-loop hygiene point that produces phantom over-regularization reports: excludes bias and BatchNorm parameters from weight decay, or at least know that you are not doing it. In PyTorch the default AdamW(model.parameters(), weight_decay=0.05) decays everything, including the shift and scale parameters whose whole job is to be free:

decay = [p for n, p in model.named_parameters() if p.ndim >= 2]
no_decay = [p for n, p in model.named_parameters() if p.ndim < 2]
opt = torch.optim.AdamW([
    {'params': decay, 'weight_decay': 0.05},
    {'params': no_decay, 'weight_decay': 0.0},
], lr=3e-4)

A quick sanity check before shipping any regularized model: train and validation accuracy must both be plotted. A model with train 0.86 and val 0.85 is healthy; a model with train 0.99 and val 0.83 is not, no matter how good its validation number looks in isolation — and reading exactly that evidence is the next topic.

Quiz

1. With plain SGD and learning rate eta, what is the per-step effect of an L2 penalty term lambda*sum(w^2) on a single weight w_j?

Question 1 options

2. You train a network with torch.optim.Adam(..., weight_decay=0.05) and notice weights barely shrink. What is the best explanation?

Question 2 options

3. Predictions from a PyTorch model with nn.Dropout vary between two identical inference calls on the same input. What is the cause?

Question 3 options

4. Training loss is 0.88 and validation loss is 0.90 after both curves flattened early. What is the correct first move?

Question 4 options

Flashcards

Question

Why does L1 produce exact zeros while L2 only makes weights small?

Answer

L2's gradient 2*lambda*w is proportional to the weight itself, so shrinkage is multiplicative and asymptotically approaches zero without reaching it. L1's subgradient lambda*sign(w) is constant in magnitude, so a small weight is pulled across zero by the same force as a large one and the proximal step (soft-thresholding) clamps it at exactly zero — which is why L1 doubles as feature selection.

Question

When are L2 and weight decay the same, and when are they different?

Answer

With plain SGD they are the same operation: weight_decay adds lambda*w to the gradient, identical to an L2 term. With Adam they diverge, because the adaptive 1/sqrt(v) scaling rescales the lambda*w term per weight. AdamW decouples the decay (w <- w - eta*lambda*w after the Adam update), so the shrinkage is exactly lambda regardless of gradient history — the right default for neural nets.

Question

What is inverted dropout, and what goes wrong if a model ported from another framework uses the other convention?

Answer

Scale activations by 1/(1-p) at train time and leave inference untouched, so the full network runs with raw weights. The alternative convention scales at test time instead. A model written under one convention and evaluated under the other comes out 1/(1-p) too large or too small — silently — which is why a framework's dropout layer must be used with that framework's evaluation path rather than a hand-rolled loop.

Question

What does Monte Carlo dropout do at inference and what does its output variance mean?

Answer

Keep dropout enabled for K forward passes under no_grad, average the K softmax outputs as the prediction, and use their variance as an epistemic uncertainty estimate. High variance means the weight-sampling ensemble disagrees — the input is unlike training data. The trick is justified by Gal and Ghahramani's result that train-time masking approximates variational inference over the posterior.

Question

Your generalization gap is open (train 0.12, val 0.48). Name the four lever families and the one case where the fix is removing regularization.

Answer

Levers: more constraint (weight decay, dropout, augmentation, label smoothing, early stopping) and more data (real examples or synthetic variants) to shrink variance; smaller capacity if the gap opens immediately. The exception: when training loss is also high and close to validation loss, the model underfits — there the fix is to reduce regularization or add capacity/features, because extra constraint only makes both curves worse.

Revision Notes

Key Takeaways

  • 1. L2 shrinks multiplicatively ((1 - 2*eta*lambda) per step) and never zeros a weight; L1 pulls with constant force lambda*sign(w), crosses zero, and stays — that is feature selection
  • 2. L2 and weight decay coincide only for SGD; under Adam the 1/sqrt(v) scaling distorts decay, so AdamW's decoupled update is the correct default
  • 3. Dropout trains an implicit ensemble over 2^n thinned subnetworks with tied weights; inverted scaling means eval must run the full, unscaled network
  • 4. forgetting model.eval() is the most common cause of nondeterministic production predictions — dropout, not the input pipeline, is almost always the culprit
  • 5. Regularization direction matters: high train and high validation loss means remove constraint; low train and high validation means add it — the lever set is decay, dropout, augmentation, more data, early stopping

Interview Tips

  • • "Explain the difference between L1 and L2" — give the gradient (2*lambda*w vs lambda*sign(w)), then the consequence (shrink vs exact zeros), then the use case (stability vs sparsity/feature selection); mention the diamond-versus-circle geometry if you have time
  • • "L2 or weight decay — same thing?" — yes for SGD, no for Adam: name the 1/sqrt(v) distortion and cite AdamW's decoupled update. This distinction separates people who read the papers from people who only ran the tutorials
  • • "What does dropout actually do?" — masks units to train a weight-tied ensemble over thinned subnetworks, then mention the variational interpretation and MC dropout for uncertainty to show the Bayesian connection
  • • "Your model overfits — what do you try first?" — answer with a sequence and a measurement: confirm the gap on curves, then cheap levers (early stopping, augmentation, label smoothing) before expensive ones (more labeling), and say you'd re-measure train loss to avoid crossing into underfitting

Cheat Sheet

L1, L2, and Dropout

Penalty math (one step):

Penalty Update on one weight Reaches 0?
L2 / SGD weight decay w <- (1 - 2*eta*lambda)*w - eta*grad never (≈ e^(-2etalambda*N) over N steps)
L1 w <- w - eta*grad - eta*lambda*sign(w) yes — soft-threshold clamps it
Adam vs AdamW Adam: decay enters grad, rescaled by 1/sqrt(v); AdamW: w <- w - eta*lambda*w after the step prefer AdamW

Choose: L1 = sparse/explainable (logistic with liblinear/saga); L2 = stable coefficients, correlated features shared; ElasticNet = both (grouped selection).

Dropout: mask with prob p at train, scale by 1/(1-p) (inverted), identity at eval. model.train() vs model.eval() is the determinism switch. p: input 0-0.2, hidden 0.2-0.5, CNNs usually 0 (augment instead). MC dropout: K passes with dropout on → mean = prediction, variance = epistemic uncertainty.

Augmentation/smoothing: 32 crops x 2 flips = 64 views per image; CrossEntropyLoss(label_smoothing=0.1) caps the optimal logit at log((1-eps)/(eps/K)) so confidence stops inflating.

Diagnose before dosing (train/val loss):

Train Val Meaning Direction
high high, close bias / underfit remove regularization
low high, diverging variance / overfit add decay, dropout, augmentation, stop early
low high, from epoch 1 capacity >> data shrink model or grow data
low high, spiky LR or split issue fix LR/split first

PyTorch decay groups: decay only p.ndim >= 2; biases and norm params get weight_decay=0.

Continue Learning