Regression and Classification
The two most common jobs for a model: predict a number, or pick a category. Fit a line with a bias, then turn scores into probabilities.
On this page
Two questions land on your desk on the same day. The first: "How much rent should we ask for this 60 m² flat?" The second: "Is this incoming message spam?" Both are prediction problems, and both can be learned from past examples. But the answers have different shapes. The first is a number on a scale. The second is one of two boxes.
Predicting a number is called regression. Predicting a category is called classification. Most practical machine learning is one or the other, and they share almost all their machinery.
Regression: fit a line
In How Models Learn the model was minutes = w × km. That line is forced through zero: 0 km means 0 minutes. Rent does not work like that. Here are three flats:
| Size (m²) | Monthly rent |
|---|---|
| 30 | 800 |
| 50 | 1,100 |
| 70 | 1,400 |
The best line through zero is about rent = 21.33 × size. It predicts 640 for the small flat and 1,493 for the large one, too low and then too high. The fix is a second parameter, the bias b, which lets the line start at any height:
rent = w × size + b
Gradient descent now tunes two dials instead of one. The gradient for w is the same as before: the average of 2 × error × input. For b, nudging it up by a tiny amount raises every prediction by that same amount, so its gradient is just the average of 2 × error. The line with the lowest loss is w = 15 and b = 350, which fits all three flats exactly: 15 × 30 + 350 = 800. The 60 m² flat gets 15 × 60 + 350 = 1,250.
Real models have one weight per feature: size, floor, distance to the station, and so on, plus one bias. Each weight says how much the prediction moves when that feature goes up by one.
Classification: from a score to a probability
Now the spam question. Take one feature, the number of links in a message. You could compute a score the same way, score = w × links + b. But a score can be any number, like −2.3 or 4.4, and you want an answer between "surely not spam" and "surely spam".
So squash the score into the range 0 to 1 with the sigmoid function:
P(spam) = 1 / (1 + e−score)
Here e is a fixed number, about 2.718. You do not need its details, only the shape it produces. A score of 0 gives exactly 0.5. Large positive scores give values close to 1. Large negative scores give values close to 0. The output never quite reaches 0 or 1, so the model never claims total certainty.
Training this model on seven labelled messages, with the code further down, gives w ≈ 1.34 and b ≈ −2.34. The trace below uses these rounded values, so its probabilities can differ from the exact ones by 0.01. Step through what the model says about new messages.
score = 1.34 * links - 2.34p = 1 / (1 + e^(-score))if p >= threshold: spamelse: not spam- links00msg1122334455score0-2.3412345P(spam)00.0912345
- threshold
- 0.5
- flagged
- 0
10 links: score = −2.34. A clearly negative score, so
pis small: 0.09. Not spam. - links0011msg22334455score0-2.341-12345P(spam)00.0910.272345
- threshold
- 0.5
- flagged
- 0
21 link: score = 1.34 − 2.34 = −1.00, so
p= 0.27. Still below 0.5. Not spam. - links001122msg334455score0-2.341-120.34345P(spam)00.0910.2720.58345
- threshold
- 0.5
- flagged
- 1
32 links: score = 0.34, just above 0, so
p= 0.58. Over the threshold, so it is flagged, but only barely. The model is unsure here. - links00112233msg4455score0-2.341-120.3431.6845P(spam)00.0910.2720.5830.8445
- threshold
- 0.5
- flagged
- 2
43 links: score = 1.68,
p= 0.84. Spam. - links0011223344msg55score0-2.341-120.3431.6843.025P(spam)00.0910.2720.5830.8440.955
- threshold
- 0.5
- flagged
- 3
54 links: score = 3.02,
p= 0.95. Each extra link adds the same 1.34 to the score, but the probability grows less and less as it nears 1. - links001122334455msgscore0-2.341-120.3431.6843.0254.36P(spam)00.0910.2720.5830.8440.9550.99
- threshold
- 0.5
- flagged
- 4
65 links: score = 4.36,
p= 0.99. Spam. With a threshold of 0.5, four of the six messages are flagged. - links001122334455score0-2.341-120.3431.6843.0254.36P(spam)00.0910.2720.5830.8440.9550.99
- threshold
- 0.9
- flagged
- 2
7Losing a real message costs more than seeing one spam, so raise the threshold to 0.9. Now only the 4-link and 5-link messages are flagged. Same model, different decision.
The model gives a probability; the threshold turns it into a decision. Those are two separate choices. The model learns the first from data. You choose the second from what each kind of mistake costs. Overfitting and Evaluation shows how to measure that trade-off.
Training a classifier
Squared error works poorly with a sigmoid: when the model is confidently wrong, its gradient is tiny, so learning crawls. Classifiers use a different loss instead, called log loss or cross-entropy. It punishes confident mistakes hard. Saying 0.99 for a message that turns out to be real costs about five times as much as saying 0.6.
The calculus behind log loss is beyond this lesson, but the result is pleasantly simple. The gradient for each weight is the average of (p − label) × input, where the label is 1 for spam and 0 otherwise. That is the same shape you met before: error times input.
function sigmoid(z) {
return 1 / (1 + Math.exp(-z));
}
// Learn w and b for: P(spam) = sigmoid(w * links + b)
function trainSpam(links, spam, rate, steps) {
let w = 0;
let b = 0;
for (let s = 0; s < steps; s++) {
let gradW = 0;
let gradB = 0;
for (let i = 0; i < links.length; i++) {
const p = sigmoid(w * links[i] + b);
const error = p - spam[i]; // spam[i] is 1 or 0
gradW += error * links[i];
gradB += error;
}
w -= rate * gradW / links.length;
b -= rate * gradB / links.length;
}
return { w, b };
}
trainSpam([0, 1, 1, 2, 3, 4, 5], [0, 0, 1, 0, 1, 1, 1], 0.5, 2000);
// { w: 1.34..., b: -2.34... }import math
def sigmoid(z):
return 1 / (1 + math.exp(-z))
def train_spam(links, spam, rate, steps):
"""Learn w and b for: P(spam) = sigmoid(w * links + b)."""
w, b = 0.0, 0.0
for _ in range(steps):
grad_w = grad_b = 0.0
for x, y in zip(links, spam):
p = sigmoid(w * x + b)
error = p - y # y is 1 or 0
grad_w += error * x
grad_b += error
w -= rate * grad_w / len(links)
b -= rate * grad_b / len(links)
return w, b
train_spam([0, 1, 1, 2, 3, 4, 5], [0, 0, 1, 0, 1, 1, 1], 0.5, 2000)
# (1.34..., -2.34...)Look at the training data: one 1-link message is spam, and one 2-link message is not. No threshold on links can get every example right, so the model settles on probabilities that reflect the mix. A probability tells you how sure the model is, which a bare yes or no cannot.
More than two categories
To sort support tickets into "billing", "bug" or "account", the model computes one score per category, each with its own weights. A function called softmax turns those scores into probabilities that add up to 1, and the highest one wins. Language models use the same step to turn their scores for every possible next token into probabilities, as you will see in Sampling and Temperature.
Common mistakes
Try it yourself
Label each prediction task as regression or classification, and say what the output would look like.
Show a sample answer
- Regression. A number such as 18.5 °C.
- Classification, two categories. A probability of disease, then a threshold.
- Classification, five categories. One probability per team from softmax; pick the highest or send it to a person when none is clearly ahead.
- Regression. A number of minutes.
- Either. As classification, each star is its own category, and the model never says 3.5. As regression, it can say 3.5, which keeps the order of the stars (4 is closer to 5 than to 1). Both are used; the choice depends on whether "almost 4" is useful to you.
The question to ask is: does the gap between two answers mean something? If yes, it is usually regression.
Check yourself
A classifier's score for a message is exactly 0. What probability does the sigmoid give?
Why add a bias b to the rent model?
You raise a spam filter's threshold from 0.5 to 0.9 without retraining. What changes?
What to practise next
To code one gradient step for a line with a bias, try Delivery Time Nudge. A model that fits its training examples well is not automatically good. Overfitting and Evaluation shows how to test a model honestly and which numbers to look at beyond "percent correct".