Regularization
Penalize complexity so models generalize.
Create a free account Try part of it here
Why it matters
Overfitting happens when a model is free to bend around the noise in its training data. Regularization takes some of that freedom away. It charges the model a small price for being complicated, usually for having large weights. You'll find it in nearly every model you train, under different names. In this phase it's the C setting of logistic regression in scikit-learn, which is the Python library most of this phase uses. In Phase 4, where the models are neural networks, it's weight decay and dropout. Once you know what this knob does, you can turn it on purpose.
What you'll be able to do
- Explain regularization as a penalty on model complexity added to the loss
- Compare ridge (L2) and lasso (L1) penalties, and why lasso zeroes out features
- Choose the penalty strength with validation data
- Recognize regularization in deep learning: weight decay, dropout, early stopping
What it covers
- A price for complexity
- The formula, one piece at a time
- Worked example: two models, one penalty
- Seeing it: a wiggly curve tamed
- Ridge (L2) and lasso (L1)
- Why lasso lands on exactly zero
- The knobs in scikit-learn and deep learning
- Choosing λ
A price for complexity
Think of a phone plan that charges for every gigabyte. You still use data. You stop wasting it on things that aren't worth the cost.
Regularization puts a model on a plan like that. In the last lesson, an overfit model learned the noise in its training data, not only the pattern. It often does this with large weights. A weight is a number the model multiplies a feature by. Big weights let predictions swing sharply, so the model can chase every odd point. Small weights keep predictions calmer and smoother.
So you change what training aims for. Ordinary training makes the error on the training data as small as it can. Regularized training makes this total as small as it can:
error + penalty
The penalty grows as the weights grow. A large weight is still allowed. It has to earn its place by cutting the error by more than it adds to the penalty.
Show the answer
Cut the error by more than it adds to the penalty
Training now minimizes error + penalty. A large weight adds to the penalty, so it survives only if it reduces the error by more than that.
The formula, one piece at a time
Here are the symbols first:
- is the number of training examples.
- is the true answer for example number .
- , said "y-hat", is the model's prediction for that example.
- is weight number .
- , a capital Greek sigma, means "add these up".
- , said "lambda", is the penalty strength. You choose it.
In plain words: loss = average squared error + lambda × (every weight squared, added up). The first part rewards fitting the data. The second part rewards small weights. Lambda decides how much the second part counts.
The first part is the mean squared error from the loss functions lesson. The second part is new.
At the penalty vanishes. You're back to ordinary training, called plain least squares. As grows, the model prefers smaller, smoother weights. It accepts a slightly worse fit on the training data in return. Up to a point, that trade pays off. Past that point the penalty is too strong: training error and validation error are both high, because the model is over-regularized and underfitting. The fix is to lower lambda. Large weights are what let a model swing to hit single noisy points, so shrinking them reduces overfitting.
This penalty, the sum of squared weights, is called ridge. You'll meet a second kind below.
Worked example: two models, one penalty
Two models fit the same training data. Each has two weights.
- Model A has weights 2 and 1. Its average squared error is 0.30.
- Model B has weights 6 and −5. It fits a little better, with an error of 0.25.
Use and work out each total.
- Model A: penalty = 0.01 × (2² + 1²) = 0.01 × 5 = 0.05. Total = 0.30 + 0.05 = 0.35.
- Model B: penalty = 0.01 × (6² + (−5)²) = 0.01 × 61 = 0.61. Total = 0.25 + 0.61 = 0.86.
Model B fits slightly better, but its big weights cost far more. Regularized training picks Model A. The code does the same sums.
errors = {"A": 0.30, "B": 0.25}
weights = {"A": [2, 1], "B": [6, -5]}
lam = 0.01
for name in ["A", "B"]:
penalty = lam * sum(w ** 2 for w in weights[name])
print(name, "error", errors[name], " penalty", round(penalty, 2), " total", round(errors[name] + penalty, 2))Model A's total is 0.35 and Model B's is 0.86, so A wins. The for loop inside sum is the sign. Squaring wipes out the minus sign on −5, so a large negative weight costs as much as a large positive one.
Show the answer
Model B, because its error (0.25) is lower
With λ = 0 the penalty is zero, so only the error counts. B's 0.25 beats A's 0.30. That's ordinary training, and it happily picks the model with the wilder weights.
Seeing it: a wiggly curve tamed
Here's the effect in a picture. The code makes 15 points that follow a smooth wave, plus random noise. Then it fits a degree-12 polynomial. That's a curve built from x, x², x³ and so on up to x¹², each with its own weight. Twelve weights give the curve a lot of freedom to bend.
It fits the curve twice: once with no penalty, and once with a ridge penalty. The gray dashed line is the true wave the points came from.
import numpy as np import matplotlib.pyplot as plt from sklearn.linear_model import LinearRegression, Ridge from sklearn.pipeline import make_pipeline from sklearn.preprocessing import PolynomialFeatures, StandardScaler rng = np.random.default_rng(3) x = np.sort(rng.uniform(0, 1, 15)) y = np.sin(2 * np.pi * x) + rng.normal(0, 0.25, 15) grid = np.linspace(0, 1, 300).reshape(-1, 1) for name, reg in [("no penalty", LinearRegression()), ("ridge, λ = 0.1", Ridge(alpha=0.1))]: model = make_pipeline(PolynomialFeatures(12), StandardScaler(), reg).fit(x.reshape(-1, 1), y) plt.plot(grid, model.predict(grid), label=name) plt.scatter(x, y, color="gray", s=20, zorder=3) plt.plot(grid, np.sin(2 * np.pi * grid), "--", color="gray", label="true pattern") plt.ylim(-2, 2) plt.legend()
scikit-learn calls the penalty strength alpha. It's the λ above.
Without a penalty, the curve threads through the points with wild swings between them. With a ridge penalty, it follows the trend. The pipeline also rescales the columns first with StandardScaler. You'll see why that matters below.
Slide the penalty and watch the error on new points fall, then rise again when the penalty is too strong.
Ridge (L2) and lasso (L1)
There are two common ways to charge for weights.
- Ridge charges for each weight squared. It's the penalty you've used so far.
- Lasso charges for each weight's size, ignoring its sign. That size is called the absolute value, written . It's Python's
abs():abs(-5)is 5.
| blank | Penalty | Effect |
|---|---|---|
| Ridge (L2) | shrinks all weights toward 0, rarely exactly 0 | |
| Lasso (L1) | pushes many weights to exactly 0: automatic feature selection | |
| Elastic net | a mix of both | lasso's selection with ridge's stability |
In plain words: ridge squares every weight and adds them up. Lasso drops every minus sign and adds them up. Both then multiply the total by lambda. "L2" and "L1" are the technical names for these two ways of measuring size.
Go back to the worked example with . Lasso charges Model A 0.01 × (2 + 1) = 0.03. It charges Model B 0.01 × (6 + 5) = 0.11.
lam = 0.01 for name, weights in [("A", [2, 1]), ("B", [6, -5])]: ridge = lam * sum(w ** 2 for w in weights) lasso = lam * sum(abs(w) for w in weights) print(name, "ridge", round(ridge, 2), " lasso", round(lasso, 2))
w = [0.5, -1.5] lam = 0.2 print(round(lam * sum(abs(x) for x in w), 2))
Show the answer
0.4
It's the lasso penalty. Drop the minus sign and add: 0.5 + 1.5 = 2. Then 0.2 × 2 = 0.4. (The ridge penalty would be 0.2 × (0.25 + 2.25) = 0.5.)
Why lasso lands on exactly zero
Ridge cares a lot about big weights and hardly at all about small ones. A weight of 0.1 costs only 0.1² = 0.01 under ridge. So ridge barely bothers to push it any further. Lasso charges the same rate all the way down to zero.
The code shrinks three weights by 0.1 each. It prints how much penalty each method saves, before multiplying by λ.
for w in [1.0, 0.5, 0.1]: new = w - 0.1 print(w, "->", round(new, 1), " ridge saves", round(w ** 2 - new ** 2, 2), " lasso saves", round(abs(w) - abs(new), 2))
Ridge saves 0.19 on the big weight but only 0.01 on the small one. Lasso saves 0.1 every time. So for big weights, ridge pushes harder. For small weights, lasso does.
Now picture a feature that barely helps. Under lasso, its small weight still pays the full rate. That fee is bigger than the little the weight adds to the fit. So lasso pushes it all the way to zero, and the feature drops out of the model. That's why lasso does automatic feature selection.
Go deeper The same idea with slopes
The pull a penalty puts on a weight is its slope. The slope of is . So ridge's pull fades as the weight nears zero: at 0.1 it's only 0.2. Weights get small but rarely reach exactly 0.
The slope of is 1 for any positive weight and −1 for any negative one. So lasso's pull never fades. If the data pulls on a weight less strongly than lasso does, the weight goes to exactly 0 and stays there.
Lasso on real data
The next code uses the churn data from earlier lessons: will a customer leave? It takes five real features, such as days since the last order and support tickets. Then it adds 15 columns of pure random noise. Those columns carry no information, so a good model should ignore them.
It fits two logistic regressions. Here C sets the penalty strength backwards: a big C means a weak penalty (more on that below). So C=100 is nearly unpenalized. The second model uses a lasso penalty, set by l1_ratio=1.
import numpy as np import pandas as pd import requests from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler df = pd.DataFrame(requests.get("https://api.gradient.test/v1/customers").json()["data"]) real = ["days_since_last_order", "orders_last_year", "support_tickets", "trail_club", "tenure_months"] rng = np.random.default_rng(0) noise = pd.DataFrame(rng.normal(size=(len(df), 15)), columns=[f"noise_{i}" for i in range(15)]) X = StandardScaler().fit_transform(pd.concat([df[real], noise], axis=1)) y = df["churned"] for name, model in [("no strong penalty", LogisticRegression(C=100, max_iter=2000)), ("lasso (L1)", LogisticRegression(l1_ratio=1, C=0.05, solver="liblinear"))]: coefs = model.fit(X, y).coef_[0] print(f"{name:<18} real features kept: {np.sum(coefs[:5] != 0)}/5 noise features kept: {np.sum(np.abs(coefs[5:]) > 1e-9)}/15")
The nearly unpenalized model gives every noise column some weight. Lasso keeps the real signals and zeros out the noise.
Show the answer
Lasso (L1)
Lasso pushes the weights of unhelpful features to exactly 0, so those features drop out. Ridge shrinks weights but rarely to exactly 0, so every feature stays in.
The knobs in scikit-learn and deep learning
Ridge(alpha=...)andLasso(alpha=...)do regression with a ridge or lasso penalty. A largeralphameans a stronger penalty.LogisticRegression(C=...): C is the inverse of the penalty strength. So a smaller C means a stronger penalty. The default penalty is ridge (L2). That means every logistic regression you've fit in this phase was lightly regularized.- Decision trees:
max_depthandmin_samples_leafare regularization too. They limit how complicated a tree can get. - Neural networks (Phase 4): weight decay is the ridge idea. It shrinks every weight a little on each step, and it's the "W" in AdamW, which is the optimizer most large models are trained with. Dropout randomly switches off parts of the network during training. Early stopping ends training once the validation score stops improving.
Scale your features first
A weight's size depends on the units of its feature. Say a price in dollars gets a weight of 2. Measure the same price in cents and the weight becomes 0.02, because the numbers are 100 times bigger. The model predicts exactly the same either way. But ridge charges 2² = 4 for the dollars version and only 0.02² = 0.0004 for the cents version.
So a penalty is only fair when features share a scale. Standardize them first, as you did in the data-prep lesson. Both code examples above do this with StandardScaler.
Show the answer
Regularization gets much stronger, so weights shrink
C is the inverse of the penalty strength. A smaller C means a stronger penalty, so the weights shrink.
Choosing λ
Think of a dog on a leash. Too loose, and the dog chases every squirrel. Too tight, and it can barely walk. The penalty works the same way. Too little, and the model still overfits the noise. Too much, and the model is forced so flat that it underfits. It then misses the real pattern too, and both the training and validation errors are high.
You can't know the right λ in advance, so you test. Try values spread on a log scale. Each value is a fixed multiple of the last, often ten times: 0.001, 0.01, 0.1, 1, 10. Fit a model on the training data with each one. Keep the one with the lowest validation error. (Cross-validation, which is a sturdier version of this test, has its own lesson.) It's the same routine you used to choose a tree's depth in the overfitting lesson. In the exercise, you'll do it yourself for ridge.
Show the answer
0.1
Keep the λ with the lowest validation error: 0.31, at λ = 0.1. Smaller values left the model too free, and larger ones flattened it.
Practice what you just read
- 5 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 4 review cards for spaced repetition
Try a quiz question
Which penalty tends to set some weights to exactly zero?