04 · Regularization Math (L1/L2)¶
Regularization adds a penalty term to the loss so the optimizer is pulled toward simpler models, not just ones that fit the training data. The two workhorses — L1 (lasso) and L2 (ridge/weight decay) — come from adding a norm of the weights to the loss.
The regularized objective¶
where \(L\) is the data loss (e.g. MSE or cross-entropy), \(\lambda \ge 0\) controls penalty strength, and \(R(w)\) is:
Gradients¶
L2 (ridge):
The gradient step becomes \(w \leftarrow w - \eta(\nabla_w L + 2\lambda w) = (1-2\eta\lambda)w - \eta\nabla_w L\) — every step shrinks \(w\) toward zero multiplicatively before applying the data gradient. This is why L2 is called "weight decay."
L1 (lasso):
(\(|w|\) is non-differentiable at \(w=0\); in practice use the subgradient \(\text{sign}(0)=0\).) The penalty subtracts a constant amount from each weight regardless of magnitude, which is why L1 pushes many weights exactly to zero (sparsity) while L2 shrinks everything smoothly but rarely to exactly zero.
Why L1 gives sparsity, geometrically¶
The L2 penalty contour is a circle (smooth, no corners), so the loss contour typically touches it at a point where no coordinate is exactly zero. The L1 penalty contour is a diamond with corners on the axes — the loss contour is likely to first touch a corner, where one or more coordinates are exactly zero.
Worked numeric example¶
Take a single weight \(w=3\) under MSE loss \(L(w) = (w-1)^2\), so \(\nabla_w L = 2(w-1) = 4\), with \(\lambda=0.5,\ \eta=0.1\).
L2 step:
L1 step:
Numeric verification¶
import numpy as np
def loss(w):
return (w - 1) ** 2
def grad_l2(w, lam):
return 2 * (w - 1) + 2 * lam * w
def grad_l1(w, lam):
return 2 * (w - 1) + lam * np.sign(w)
w, lam, eta = 3.0, 0.5, 0.1
w_l2 = w - eta * grad_l2(w, lam)
w_l1 = w - eta * grad_l1(w, lam)
print(f"L2 step: grad={grad_l2(w, lam):.4f} w_new={w_l2:.4f}")
print(f"L1 step: grad={grad_l1(w, lam):.4f} w_new={w_l1:.4f}")
# Finite-difference check of the L2 penalty gradient alone
h = 1e-6
r = lambda w: w ** 2
num_grad = (r(w + h) - r(w - h)) / (2 * h)
print(f"L2 penalty grad analytic={2*w:.4f} numeric={num_grad:.4f}")
L2 step: grad=7.0000 w_new=2.3000
L1 step: grad=4.5000 w_new=2.5500
L2 penalty grad analytic=6.0000 numeric=6.0000
How It Actually Works¶
L2 regularization (\(+\frac{\lambda}{2}\|\theta\|^2\) added to the loss) has a clean gradient, \(\lambda\theta\), and is straightforward to implement as an addition to the analytically or autodiff-computed gradient before the optimizer step. But there's a real implementation subtlety exposed once Adam enters the picture: adding \(\lambda\theta\) to the loss before Adam's moment estimates are computed means the weight decay term gets divided by \(\sqrt{v_t}+\epsilon\) along with the "real" gradient — which couples the effective decay strength to each parameter's gradient history, an interaction not present in plain SGD. AdamW (the modern default in most deep learning frameworks) fixes this by implementing weight decay as a separate arithmetic step, applied directly to the parameters (\(\theta \leftarrow \theta(1-\alpha\lambda)\)) outside of the Adam moment computation entirely — same regularization intent, mechanically different (and empirically better) computation.
L1 regularization (\(\lambda\|\theta\|_1\)) has a genuine computational
wrinkle this module's math glosses over: \(|\theta|\) is not differentiable
at \(\theta=0\). Autodiff frameworks handle this by defining a subgradient
convention at that single point — typically sign(0) = 0 — so
torch.abs(x).backward() returns a valid (if not unique) gradient rather
than raising an error or returning NaN; this is a deliberate engineering
choice about how to numerically handle a mathematically non-smooth point,
not a limitation of the calculus itself.
Exercise¶
- Run gradient descent (20 steps, \(\eta=0.1\)) on \(L(w)=(w-1)^2\) starting at \(w=3\) with L2 penalty \(\lambda=0.5\), and separately with L1 penalty \(\lambda=0.5\). Plot or print \(w\) at each step.
- Which one reaches exactly \(w=0\) for some larger \(\lambda\)? Explain using the sign-vs-magnitude gradient behavior above.
- Derive the gradient of "elastic net" \(R = \alpha\|w\|_1 + (1-\alpha)\|w\|_2^2\) and verify it numerically at \(w=3,\alpha=0.3\).