M11 · Constraints and regularization, geometrically
Lagrange multipliers, KKT conditions and the geometry of norms: why L1 regularization zeroes weights, why L2 shrinks them, and how to use that to choose your features.
Mastering AI · Required Maths · Week 4 (optimization) · Part 11
Goal of this step
In M10, gradient descent was free to go anywhere. In practice we often hold it back: we want small weights, or few of them. This article shows that regularizing means optimizing under a constraint, and that the shape of the constraint decides everything. It’s the proof promised since M1: why the corners of the diamond set weights exactly to zero.
By the end of this article you’ll be able to:
- solve a constrained optimization problem with a Lagrange multiplier, and interpret that multiplier as the price of the constraint;
- write the KKT conditions for an inequality constraint and recognize an active constraint;
- explain why penalizing by is the same as imposing ;
- explain geometrically, then with calculus, why zeroes weights and shrinks them;
- read a regularization path and use it to select features on the running project;
- place SVMs and AdamW in this framework.
Key concepts
1. Constrained optimization: Lagrange multipliers
Intuition. You’re walking on a hill, but you must stay on a trail. The lowest point of the trail isn’t the bottom of the valley: it’s the spot where the trail grazes a level set without crossing it. There, the level set and the trail are tangent, so their normals are aligned. And the normal to a level set is the gradient (M4).
To minimize subject to , the condition reads:
The number is the Lagrange multiplier. You get both equations by setting to zero the gradient of the Lagrangian:
with respect to and : the constraint is no longer an obstacle, just one more variable.
The price of the constraint. has a concrete meaning: if you loosen the constraint by , the optimal value changes by about . A large means the constraint is expensive; means it doesn’t get in the way.
Example: minimize subject to . The two equations give and , and the constraint forces , hence the optimum and . We check it in code below.
PCA was a Lagrange problem. In M3, the first component is the direction that maximizes the variance , with . Setting the gradient of the Lagrangian to zero gives:
The critical points are the eigenvectors of the covariance, and the variance reached is : the multiplier is the eigenvalue.
2. Inequality constraints: the KKT conditions
Intuition. With a constraint , there are only two cases. Either the free minimum is already inside the allowed region: the constraint is useless, it’s inactive. Or it’s outside: the optimum then lies on the boundary, the constraint is active, and we’re back to the Lagrange case.
The Karush-Kuhn-Tucker (KKT) conditions sum up both cases:
The last line, complementary slackness, says one of the two factors is zero: either (inactive constraint, it costs nothing), or (active constraint). The sign says the constraint can only hurt, never help. For a convex problem (M6), these conditions are necessary and sufficient.
3. Penalty or constraint: it’s the same thing
Intuition. There are two ways to say “I want small weights”: forbid the weights from leaving a ball of radius , or make them pay a fine proportional to their size. The KKT conditions show these are two ways of writing the same problem:
For a convex problem, every radius has a matching penalty that gives the same solution, and vice versa: is exactly the KKT multiplier of the constraint. The larger , the smaller the ball. We check it numerically below.
That’s also what M8 said from another angle: the penalty is a Gaussian prior on the weights, the penalty a Laplace prior. Three readings, one object: a ball, a fine, a belief.
4. The geometry: an ellipse touching a ball
Intuition. Near its minimum, the loss looks like a parabola (Taylor, M6): its level sets are ellipses centered on the unconstrained minimum. Inflate the ellipse until it touches the allowed ball. The contact point is the regularized solution.
- The ball is round: the ellipse can touch it anywhere, and there’s no reason for that to be exactly on an axis. The weights are pulled toward 0 but stay non-zero.
- The ball has corners on the axes, i.e. where one coordinate is exactly 0. A growing ellipse is very likely to hit a corner before an edge. In the figure, the free minimum is at : isn’t zero without the constraint, yet the solution sets it exactly to 0.
In high dimension, the effect gets stronger: the diamond becomes a polytope whose vertices and edges live on subspaces where many coordinates are zero. Hence sparse models: few non-zero weights.
5. The same result with calculus: soft thresholding versus division
Intuition. Look at the slope of each penalty near 0. The slope of is : it fades as approaches 0, so the slightest signal in the data is enough to keep a small weight. The slope of is : it pushes toward 0 with a constant force. If the data pull less hard than , the weight stays exactly at 0.
In one dimension, with the least squares solution, we minimize plus the penalty:
For , the absolute value isn’t differentiable at 0: we use its subgradient, the interval . The optimality condition at becomes , i.e. . That’s soft thresholding: everything below the threshold drops to zero, the rest is trimmed by . divides everything by the same factor and zeroes nothing.
This formula is exact in several dimensions when the features are orthogonal ( ). In general, it’s the basic step of the algorithms that solve the Lasso (coordinate descent, proximal gradient).
A practical consequence: standardize. The penalty treats every weight the same way. A feature in milliseconds has a tiny weight, barely penalized; the same feature in seconds would have a weight 1,000 times larger, crushed by the penalty. Without standardization, regularization depends on units: always standardize first (M1, part 14).
6. Overview: SVMs and support vectors
A linear SVM looks for the hyperplane that separates the classes with the largest margin. It’s a constrained problem: minimize subject to for every example , with . Each constraint has its multiplier , and KKT complementary slackness says:
- examples far from the boundary have an inactive constraint, so : they don’t influence the solution;
- only examples on the margin have . These are the support vectors, and depends on them alone.
The KKT conditions therefore explain why an SVM boils down to a few examples. Exercise 4 checks it.
7. Weight decay and AdamW
With the penalty , the gradient gains a term and M10’s update becomes:
At every step, the weights are multiplied by a factor slightly below 1: that’s weight decay. With SGD, the penalty and weight decay are identical. With Adam, they aren’t: if you add to the gradient, it goes through the division by , and weights with large gradients barely shrink. AdamW (Loshchilov and Hutter, 2019) applies the factor separately, after the Adam step: it’s the default optimizer for training the networks of part 22, the thesis LSTM included.
Code example
Lagrange in practice: the price of a constraint, and PCA
import numpy as np
from scipy.optimize import minimize
# 1. minimize f(x, y) = (x - 2)² + (y - 2)² subject to g(x, y) = x + 2y = c
f = lambda p: (p[0] - 2) ** 2 + (p[1] - 2) ** 2
grad_f = lambda p: np.array([2 * (p[0] - 2), 2 * (p[1] - 2)])
grad_g = np.array([1.0, 2.0])
def optimum(c):
constraint = {"type": "eq", "fun": lambda p: p[0] + 2 * p[1] - c}
return minimize(f, x0=np.zeros(2), constraints=[constraint], method="SLSQP", tol=1e-12).x
p = optimum(2.0)
lam = -(grad_f(p) @ grad_g) / (grad_g @ grad_g) # ∇f + λ∇g = 0 => λ
print(f"optimum = {p.round(4)} | f = {f(p):.4f} | ∇f = {grad_f(p).round(4)} | λ = {lam:.4f}")
print(f"residual ∇f + λ∇g = {(grad_f(p) + lam * grad_g).round(8)}")
for c in (2.01, 2.1): # λ = price of the constraint
print(f"c = {c}: f* = {f(optimum(c)):.4f} | change {f(optimum(c)) - f(p):+.4f} "
f"| prediction -λ·Δc = {-lam * (c - 2):+.4f}")
# 2. PCA is a Lagrange problem: max wᵀCw subject to wᵀw = 1
rng = np.random.default_rng(0)
z = rng.normal(size=(500, 2)) * [3.0, 0.5] # M3's cloud
R = np.array([[np.cos(0.6), -np.sin(0.6)], [np.sin(0.6), np.cos(0.6)]])
X = z @ R.T + [5.0, 2.0]
C = np.cov(X, rowvar=False)
unit_norm = {"type": "eq", "fun": lambda w: w @ w - 1}
w = minimize(lambda w: -w @ C @ w, x0=np.array([1.0, 0.0]), constraints=[unit_norm],
method="SLSQP", tol=1e-12).x
print(f"w* = {w.round(4)} | multiplier wᵀCw = {w @ C @ w:.4f} | ‖Cw − λw‖ = {np.linalg.norm(C @ w - (w @ C @ w) * w):.1e}")
vals, vecs = np.linalg.eigh(C)
print(f"eigh: largest eigenvalue {vals[-1]:.4f}, vector {vecs[:, -1].round(4)}")
- The solver finds the optimum with and , exactly : , as by hand.
- The price of the constraint: going from to , drops by , exactly . For , the drop is versus predicted: the prediction is first-order, it drifts a little as grows.
- PCA: the constrained maximum is , with a multiplier of . That’s the largest eigenvalue of the covariance (M3’s ) and its eigenvector, up to sign.
The geometry, in numbers
This script computes the two contact points of the figure in section 4: a quadratic loss with minimum , constrained to a ball of radius 1.
import numpy as np
from scipy.optimize import minimize
A = np.array([[1.0, 0.3], [0.3, 1.5]])
w0 = np.array([2.5, 0.3]) # unconstrained minimum
loss = lambda w: (w - w0) @ A @ (w - w0) # level sets = ellipses centered on w0
t = 1.0 # radius of the ball
balls = {
"L1 (diamond)": {"type": "ineq", "fun": lambda w: t - np.abs(w).sum()},
"L2 (circle) ": {"type": "ineq", "fun": lambda w: t - np.sqrt(w @ w)},
}
for name, constraint in balls.items():
best = min((minimize(loss, x0=x0, constraints=[constraint], method="SLSQP", tol=1e-12)
for x0 in ([0.1, 0.1], [0.5, -0.4], [-0.3, 0.6])), key=lambda r: r.fun)
w = best.x
print(f"{name:<13} w* = ({w[0]:.4f}, {w[1]:.4f}) | loss = {best.fun:.4f} | gradient = {(2 * A @ (w - w0)).round(3)}")
| Ball | Solution | Loss | Loss gradient at |
|---|---|---|---|
| (diamond) | 2.6550 | ||
| (circle) | 2.3901 |
The solution lands exactly on the corner . The solution keeps , and its gradient is indeed parallel to : . That’s the Lagrange condition, since the gradient of points in the direction of . For , the gradient isn’t parallel to the normal of an edge: at a corner, the ball has no unique normal, and that’s precisely what lets the solution stop there.
Soft thresholding, KKT conditions and penalty/constraint equivalence
We build 5 orthogonal features ( ) so that the formulas of section 5 are exact, with true weights .
import numpy as np
from scipy.optimize import minimize
from sklearn.linear_model import Lasso, Ridge
rng = np.random.default_rng(0)
n = 200
Q, _ = np.linalg.qr(rng.normal(size=(n, 5)))
X = Q * np.sqrt(n) # orthogonal columns: XᵀX / n = I
w_true = np.array([3.0, -2.0, 1.0, 0.5, 0.0])
y = X @ w_true + rng.normal(0, 1, n)
z = X.T @ y / n # least squares (since XᵀX / n = I)
lam = 0.8
soft = np.sign(z) * np.maximum(np.abs(z) - lam, 0) # L1 solution: soft thresholding
shrunk = z / (1 + lam) # L2 solution: plain division
lasso = Lasso(alpha=lam, fit_intercept=False).fit(X, y).coef_
ridge = Ridge(alpha=n * lam, fit_intercept=False).fit(X, y).coef_ # Ridge doesn't divide by n
print("least squares :", z.round(3))
print("L1 (formula) :", soft.round(3), "| sklearn Lasso :", lasso.round(3))
print("L2 (formula) :", shrunk.round(3), "| sklearn Ridge :", ridge.round(3))
# Lasso KKT conditions: |Xᵀ(y − Xw)/n| = λ on non-zero weights, ≤ λ on zero weights
corr = X.T @ (y - X @ lasso) / n
print("|Xᵀr/n| :", np.abs(corr).round(3), "| zero weights:", np.isclose(lasso, 0))
# penalty <=> constraint: solve min (1/2n)‖y − Xw‖² subject to ‖w‖₁ ≤ t, with t = ‖w_lasso‖₁
t = np.abs(lasso).sum()
loss = lambda uv: np.sum((y - X @ (uv[:5] - uv[5:])) ** 2) / (2 * n) # w = u − v, u, v ≥ 0
res = minimize(loss, x0=np.zeros(10), method="SLSQP", bounds=[(0, None)] * 10,
constraints=[{"type": "ineq", "fun": lambda uv: t - uv.sum()}], tol=1e-12)
w_constrained = res.x[:5] - res.x[5:]
print(f"t = {t:.3f} | constrained solution: {w_constrained.round(3)} | max gap with the Lasso {np.abs(w_constrained - lasso).max():.1e}")
| True weights | 3 | −2 | 1 | 0.5 | 0 |
| Least squares | 2.946 | −1.923 | 0.971 | 0.455 | −0.106 |
| , (formula = scikit-learn) | 2.146 | −1.123 | 0.171 | 0 | 0 |
| , (formula = scikit-learn) | 1.637 | −1.069 | 0.539 | 0.253 | −0.059 |
What we read:
- the formulas of section 5 match scikit-learn to the thousandth. Watch the conventions:
Lassodivides the loss by ,Ridgedoesn’t, hencealpha=n * lam; - trims every weight by exactly 0.8 and zeroes those that were below the threshold ( and ). divides everything by 1.8 and zeroes nothing;
- the flip side: also removed , whose true weight is 0.5. A real but weak effect looks like noise. And the kept weights are biased toward 0: in practice, you often refit an unregularized model on the selected features;
- the KKT conditions show up in the correlation between the features and the residual: it’s exactly on the three non-zero weights (an “active” constraint for each), and stays below ( and ) on the two zero weights;
- penalty = constraint: imposing with (the norm of the Lasso solution), we recover the same solution to within . The Lasso’s is the multiplier of that constraint.
Running project: a regularization path to choose features
We reuse the synthetic memory regions from M9 and M10, with the 4 domain features, and add 6 pure-noise features. A good model should ignore them. We vary the regularization from very strong to almost none and track every weight: that’s the regularization path. In scikit-learn, the parameter is .
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import f1_score, log_loss
from sklearn.model_selection import train_test_split
def make_regions(n, rng):
"""Synthetic memory regions (same generator as M9 and M10): label 1 = hot in the next window."""
rate = rng.gamma(0.6, 4, n)
recent_accesses = rng.poisson(rate)
since_last = np.round(rng.exponential(30 / (1 + recent_accesses)), 1)
periodic = rng.binomial(1, 0.25, n)
before_peak = rng.integers(0, 12, n)
logit = (0.9 * np.log1p(recent_accesses) - 0.05 * since_last
+ 3.0 * periodic * (before_peak < 3) - 1.5 + rng.normal(0, 0.8, n))
X = np.column_stack([recent_accesses, since_last, periodic, before_peak])
return X, (logit > 0).astype(int)
def domain_features(X):
return np.column_stack([np.log1p(X[:, 0]), X[:, 1], X[:, 2], X[:, 2] * (X[:, 3] < 3)])
rng = np.random.default_rng(0)
X, y = make_regions(5000, rng)
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.3, random_state=0, stratify=y)
# 4 useful features + 6 pure-noise features the model should ignore
names = ["log_accesses", "since_last", "periodic", "peak_imminent"] + [f"noise_{k}" for k in range(1, 7)]
Ftr = np.column_stack([domain_features(Xtr), rng.normal(size=(len(Xtr), 6))])
Fte = np.column_stack([domain_features(Xte), rng.normal(size=(len(Xte), 6))])
mu, sd = Ftr.mean(axis=0), Ftr.std(axis=0)
Ztr, Zte = (Ftr - mu) / sd, (Fte - mu) / sd # standardize: the penalty must treat every weight alike
Cs = np.logspace(-3, 1, 41) # C = 1/λ: small C = strong regularization
paths = {}
for penalty, l1_ratio in (("l1", 1.0), ("l2", 0.0)):
coefs = []
for C in Cs:
m = LogisticRegression(C=C, l1_ratio=l1_ratio, solver="saga", max_iter=20_000, tol=1e-8).fit(Ztr, ytr)
coefs.append(m.coef_[0])
if C in Cs[[0, 8, 16, 24, 40]]:
p = m.predict_proba(Zte)[:, 1]
print(f"{penalty} C = {C:<7.3g} non-zero weights = {np.sum(np.abs(m.coef_[0]) > 1e-6):>2} | "
f"‖w‖₁ = {np.abs(m.coef_[0]).sum():6.3f} | test log loss = {log_loss(yte, p):.4f} | "
f"test F1 = {f1_score(yte, (p > 0.5).astype(int)):.3f}")
paths[penalty] = np.array(coefs)
# order in which features enter the L1 path (smallest C where the weight becomes non-zero)
entry = [Cs[np.argmax(np.abs(paths["l1"][:, j]) > 1e-6)] if (np.abs(paths["l1"][:, j]) > 1e-6).any()
else np.inf for j in range(len(names))]
for j in np.argsort(entry):
print(f" {names[j]:<15} enters at C = {entry[j]:.3g} | weight at C = 10: L1 {paths['l1'][-1, j]:+.3f}, L2 {paths['l2'][-1, j]:+.3f}")
print("smallest |L2 weight| along the whole path:", f"{np.abs(paths['l2']).min():.1e}")
| Penalty | Non-zero weights (out of 10) | Test log loss | Test F1 | ||
|---|---|---|---|---|---|
| 0.001 | 0 | 0.000 | 0.5278 | 0.000 | |
| 0.0063 | 2 | 1.816 | 0.3309 | 0.575 | |
| 0.040 | 3 | 4.058 | 0.2921 | 0.681 | |
| 0.25 | 9 | 5.522 | 0.2884 | 0.680 | |
| 10 | 10 | 5.967 | 0.2884 | 0.685 | |
| 0.001 | 10 | 1.110 | 0.3921 | 0.289 | |
| 0.040 | 10 | 4.070 | 0.2933 | 0.679 | |
| 10 | 10 | 5.957 | 0.2884 | 0.685 |
Order of entry into the
path: log_accesses (
), peak_imminent (
), since_last (
), then a first noise feature at
, and periodic only at
.
What the path shows:
- under very strong regularization, zeroes everything. The model then predicts the base rate (log loss 0.528, F1 zero). , at the same , keeps all 10 weights non-zero: along the whole path, the smallest weight is , never exactly 0;
- at
,
keeps only 3 features out of 10, and they’re the right ones:
log_accesses,peak_imminentandsince_last. With them alone, the test F1 is 0.681, versus 0.685 for the full model. The 6 noise features brought nothing; periodicenters after the first noise feature, and its final weight stays tiny (+0.03). That’s correct: in the generator, being periodic only matters as the peak approaches, and that information is already inpeak_imminent. The path recovered the structure of the problem without being told;- without regularization ( ), and give the same model: when the ball is large enough to contain the free minimum, the constraint is inactive ( ), whatever its shape.
To choose
for real, you don’t look at the test set: you cross-validate along the path (LogisticRegressionCV, part 18). The path itself is for understanding which features carry the signal.
Hands-on exercise
- By hand. Maximize subject to with a Lagrange multiplier. Then check the interpretation of as the price of the constraint: how much does the maximum increase when you move to ?
- Soft thresholding. Derive the formula by handling the cases , and separately. Then, on the example’s orthogonal features, count the Lasso’s zero weights for , predicting the result before running the code.
- Correlated features. In the running project, add a noisy copy of
log_accesses(the same column plus noise with standard deviation 0.05). Compare the weights of the two copies with and with at . Which one spreads the weight, which one picks a copy? Then try the elastic net withl1_ratio=0.5. - SVM and KKT. Train
SVC(kernel="linear", C=1.0)on the running project’s 4 standardized domain features (2,000 examples are enough). How many support vectors (support_)? Check that examples with (computed withdecision_functionand ) are never support vectors. - Adam versus AdamW. In M10’s Adam trainer, on the raw features, add regularization in two ways:
added to the gradient before Adam, then the
factor applied after the step (AdamW), with
. Which weight shrinks the most in each case? Relate the answer to the scale of
since_last. - Running project. On the 9 features of part 14, plot the path. Which features enter first? Which ones only enter under weak regularization? Compare with the feature importances of part 19.
Hint for question 1: the optimum is with . For , the maximum is : it goes from 25 to 25.5025, i.e. about .
Further resources
- James, Witten, Hastie and Tibshirani, An Introduction to Statistical Learning (ISLR), chapter 6, section 6.2: Ridge, Lasso and the balls figure, free online.
- Hastie, Tibshirani and Friedman, The Elements of Statistical Learning, section 3.4: shrinkage methods and regularization paths.
- Tibshirani, Regression Shrinkage and Selection via the Lasso (Journal of the Royal Statistical Society B, 1996): the paper that introduced the Lasso.
- Boyd and Vandenberghe, Convex Optimization, chapter 5 (duality and KKT conditions), free online.
- Bishop, Pattern Recognition and Machine Learning, appendix E (Lagrange multipliers) and chapter 7 (SVMs).
- Loshchilov and Hutter, Decoupled Weight Decay Regularization (ICLR 2019): AdamW.
What you should be able to do before moving on
- Solve a simple problem with a Lagrange multiplier and interpret as the price of the constraint
- Write the KKT conditions and say whether a constraint is active or inactive
- Explain why a penalty is equivalent to a constraint
- Explain geometrically why zeroes weights and shrinks them
- Derive soft thresholding and the factor
- Read a regularization path and explain why you standardize before regularizing
- Say what AdamW changes compared with an penalty inside Adam
Next part: M12 · Wrap-up: rereading everything with the maths. We go back over the whole maths programme and tie it to the models of the technical phase, from the dot product to the thesis LSTM.