Multiclass classification
Softmax and cross-entropy for more than two classes.
Create a free account Try part of it here
Why it matters
Many questions have more than two possible answers. A shop wants to know which product category each customer prefers. A help desk needs to send each ticket to the right team. A chatbot must tell which of five things a message is asking for. The tool for these is logistic regression with one change. Every class gets its own score, and softmax turns the scores into probabilities. This is exactly the last layer of a language model. That layer is a classifier that picks among roughly 100,000 tokens. You'll also learn the confusion matrix. It shows what a classifier mixes up, not only how often it's wrong.
What you'll be able to do
- Extend logistic regression to several classes with softmax
- Train a softmax classifier from scratch with one-hot labels
- Read a confusion matrix to see which classes get mixed up
- Compare against a majority baseline and per-class results
What it covers
- One score per class
- Training: the right answer as a list
- Predicting a customer's favorite category
- Reading the confusion matrix
- Worked example: tallying ten support tickets
- What to look for
- Other ways to handle many classes
One score per class
Picture a mail room with three bins: sales, support and billing. Each letter goes in exactly one bin. Logistic regression could only answer yes or no, like "is this billing?" Now you need to pick one answer out of several.
Each possible answer is called a class. The letter stands for how many classes there are. In the mail room, .
Here's the idea. Give every class its own scorecard. Each scorecard is a weighted sum, the same kind you used in linear and logistic regression. A letter gets one score per class. The class with the highest score wins.
Here's a small example. A shop wants to guess which category a customer buys most: apparel (clothes), home goods or accessories. It looks at two features of each customer:
- how many orders they placed last year;
- whether they used a discount (1 for yes, 0 for no).
Each class has its own weight for each feature, plus its own starting amount. Take a customer with 3 orders who used a discount:
| class | weights (orders, discount) | starting amount | score |
|---|---|---|---|
| apparel | 0.5, 0.5 | 0 | 0.5 × 3 + 0.5 × 1 + 0 = 2 |
| home | 0.5, −1.0 | 0.5 | 0.5 × 3 − 1.0 × 1 + 0.5 = 1 |
| accessories | −0.5, 1.0 | 0.5 | −0.5 × 3 + 1.0 × 1 + 0.5 = 0 |
Apparel has the highest score, so apparel is the guess.
Show the answer
Its own weights and its own starting amount, applied to the same features
Every class looks at the same customer through its own weights, which is why the scores differ.
The formula
The letter means "class number ". The score for class is :
The bold is the list of the customer's features. The bold is class 's list of weights. The dot between them is the dot product: multiply in pairs, then add up. Last, is class 's starting amount, called its bias (or intercept).
In plain words: each class multiplies the features by its own weights, adds them up, and adds its own starting amount. That gives one score per class.
From scores to probabilities
Scores like 2, 1 and 0 aren't probabilities. Softmax, from Phase 2, turns them into probabilities. Write for the probability of class :
Here is a fixed number, about 2.718. The means "add up". The letter counts through every class to build the total.
In plain words: raise to the power of each score, then divide each result by the total. Every probability is positive, and together they add up to 1.
For the scores 2, 1 and 0, that gives apparel 0.665, home 0.245 and accessories 0.090. These are the same numbers as the worked example in the softmax lesson. In Python, the sum is a loop:
import math x = [3, 1] # 3 orders last year, used a discount (1 = yes) weights = {"apparel": [0.5, 0.5], "home": [0.5, -1.0], "accessories": [-0.5, 1.0]} bias = {"apparel": 0.0, "home": 0.5, "accessories": 0.5} scores = {k: sum(w * xi for w, xi in zip(weights[k], x)) + bias[k] for k in weights} total = sum(math.exp(z) for z in scores.values()) for k, z in scores.items(): print(k, z, round(math.exp(z) / total, 3))
Each line shows a class, its score and its probability: 2.0 and 0.665, 1.0 and 0.245, then 0.0 and 0.09. Softmax keeps the order, so the highest score always has the highest probability.
This model has two names. It's called softmax regression, or multinomial logistic regression. With only two classes, it does the same job as ordinary logistic regression.
Go deeper All classes at once, as one matrix
You can stack every class's weights side by side in one table, . It has one row per feature and one column per class. Then all the scores come out in one step:
Read it as: "the list of scores equals W transposed times the features, plus the list of biases." , said "W transposed", is flipped so its columns become rows. Each row is then one class's weights. Multiplying by takes a dot product with each row.
In the example, the columns of are [0.5, 0.5], [0.5, −1.0] and [−0.5, 1.0]. The bias list is [0, 0.5, 0.5]. With = [3, 1], the result is [2, 1, 0], the same three scores.
One score per class, turned into probabilities that add up to 1. Drag a score and watch every probability move.
scores = {"sales": 0.4, "support": 1.9, "billing": 1.2}
print(max(scores, key=scores.get))Show the answer
support
max with key=scores.get returns the class with the highest score, which is support (1.9). Softmax keeps the order, so support would also get the highest probability.
Training: the right answer as a list
To train the model, you need past examples where you know the right class. Each right answer is written as a list with a 1 for the true class and 0 everywhere else. A list like that is called a one-hot vector. With the classes in the order apparel, home, accessories:
- a customer who mostly buys apparel is [1, 0, 0];
- one who mostly buys home goods is [0, 1, 0];
- one who mostly buys accessories is [0, 0, 1].
The loss is the cross-entropy from Phase 2. With one right answer, it's : minus the natural log (ln) of the probability the model gave the true class. Say our customer really buys home goods most. The model gave home only 0.245, so the loss is −ln 0.245, about 1.41.
Which way to nudge each weight
Training uses gradient descent, from earlier lessons. It needs a slope for every weight. The slope says how much the loss changes when that weight goes up a little. For softmax with cross-entropy, the slopes have a famously simple form: predicted minus true.
Here it is for our customer, whose true class is home:
| blank | apparel | home | accessories |
|---|---|---|---|
| predicted | 0.665 | 0.245 | 0.090 |
| true (one-hot) | 0 | 1 | 0 |
| predicted − true | 0.665 | −0.755 | 0.090 |
A positive gap means "this class got too much probability", so its score gets pushed down. A negative gap means "too little", so its score gets pushed up. Home gets the big push up.
Show the answer
Pushes it down
Predicted minus true is positive, meaning the class got too much probability.
Each weight's slope is its feature value times its class's gap. The customer had 3 orders. So the slope for apparel's orders weight is 3 × 0.665 = 1.995. For home's orders weight it's 3 × −0.755 = −2.265.
With many customers, you average these slopes over all of them. Written as one formula:
There are several symbols here, so take them one at a time:
- is the loss.
- is the gradient: a table with one slope for every weight. The triangle, , is said "nabla".
- is the number of examples.
- is the table of features, one row per example.
- , said "X transposed", is flipped so rows become columns.
- holds the predicted probabilities, one row per example and one column per class.
- holds the one-hot truths, in the same shape as .
In plain words: for every weight, multiply each example's feature value by that class's "predicted minus true" gap. Then average over all the examples.
Here's the same calculation in Python, for our one customer:
x = [3, 1] # one customer: 3 orders, used a discount p = [0.665, 0.245, 0.090] # predicted: apparel, home, accessories y = [0, 1, 0] # one-hot truth: this customer mostly buys home gap = [pk - yk for pk, yk in zip(p, y)] print("predicted minus true:", [round(g, 3) for g in gap]) for name, xj in zip(["orders", "discount"], x): print(f"slopes for the {name} weights:", [round(xj * g, 3) for g in gap])
The gaps are [0.665, −0.755, 0.09]. The orders weights get slopes of [1.995, −2.265, 0.27]. The discount feature is 1, so its slopes equal the gaps.
A training step moves each weight a little against its slope. With a learning rate of 0.1, apparel's orders weight goes from 0.5 to 0.5 − 0.1 × 1.995 = 0.3005. Home's goes from 0.5 to 0.5 + 0.1 × 2.265 = 0.7265. Next time, this customer's home score will be higher. Repeat that over many steps and many customers, and you have trained the model. The exercise has you build exactly this loop.
Go deeper Why the slope is "predicted minus true"
The cross-entropy lesson showed that each score's slope is predicted minus true. In this lesson's letters, that's . Here is 1 for the true class and 0 for the rest. Each score is . Write for feature number . When class 's weight for that feature goes up by a little, goes up by times that amount. That's the chain rule from Phase 2. Multiply the two, and the slope for that weight is .
The bias acts like a feature that's always 1. So its slope is the gap itself. That's why the exercise adds a column of ones to . The formula does every one of these multiplications at once, and summed over every example. Dividing by turns the sum into an average.
Show the answer
[0.2, 0.5, -0.7]
The one-hot truth is [0, 0, 1]. Subtract it: 0.2 − 0 = 0.2, 0.5 − 0 = 0.5 and 0.3 − 1 = −0.7. Billing's score gets pushed up, and the other two get pushed down.
Predicting a customer's favorite category
Now try it on bigger data. The practice shop has 600 customers. Each one has a top_category: the kind of product they buy most, apparel, home or accessories. Can four facts about how someone shops predict it? The facts are orders last year, average order value, the share of orders that used a discount, and months as a customer.
The code follows the usual steps from earlier lessons:
- It holds back 30% of the customers as a test set.
stratifykeeps the mix of categories the same in both parts. - It puts every feature on the same scale with
StandardScaler. - It fits scikit-learn's
LogisticRegression. Give it three classes, and it fits softmax regression for you. (By default it also adds a small penalty that keeps the weights modest. The regularization lesson covers that.)
Then it prints three things. First come the class names. Next is the test accuracy beside the majority baseline. That's the accuracy you'd get by always guessing the most common class. Here that's apparel, a little under half of the customers. Last is a confusion matrix, which the next section explains.
import numpy as np import pandas as pd import requests from sklearn.linear_model import LogisticRegression from sklearn.metrics import confusion_matrix from sklearn.model_selection import train_test_split from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler df = pd.DataFrame(requests.get("https://api.gradient.test/v1/customers").json()["data"]) features = ["orders_last_year", "avg_order_value", "discount_share", "tenure_months"] X_train, X_test, y_train, y_test = train_test_split(df[features], df["top_category"], test_size=0.3, random_state=1, stratify=df["top_category"]) model = make_pipeline(StandardScaler(), LogisticRegression()).fit(X_train, y_train) print("classes:", list(model.classes_)) print("accuracy:", round(model.score(X_test, y_test), 3), " majority baseline:", round(y_test.value_counts(normalize=True).max(), 3)) print(pd.DataFrame(confusion_matrix(y_test, model.predict(X_test), labels=model.classes_), index=[f"true {c}" for c in model.classes_], columns=[f"pred {c}" for c in model.classes_]))
Reading the confusion matrix
A single accuracy number says how often the model is right. It doesn't say what it gets wrong. A confusion matrix does. It's a tally table, like a mail room keeping count of where letters should have gone and where they actually went.
- Each row is a true class.
- Each column is a predicted class.
- Each cell counts the examples with that true class and that prediction.
The diagonal, from top left to bottom right, holds the correct predictions. Every cell off the diagonal is one specific mix-up, such as "really home, but predicted apparel".
Worked example: tallying ten support tickets
A router sorts 10 tickets into sales, support and billing. Five are really billing, three support and two sales. Build the confusion matrix by counting, then compare the accuracy with always guessing billing.
classes = ["sales", "support", "billing"] true = ["billing"] * 5 + ["support"] * 3 + ["sales"] * 2 pred = ["billing"] * 5 + ["billing", "support", "support"] + ["billing", "sales"] counts = [[0, 0, 0] for _ in classes] # rows: true class, columns: predicted for t, p in zip(true, pred): counts[classes.index(t)][classes.index(p)] += 1 for c, row in zip(classes, counts): print(f"true {c:<8}", row) correct = sum(t == p for t, p in zip(true, pred)) print("accuracy:", correct / len(true)) print("always 'billing':", true.count("billing") / len(true))
The rows read [1, 0, 1], [0, 2, 1] and [0, 0, 5]. The diagonal holds 1, 2 and 5, so 8 of the 10 tickets are right. That's an accuracy of 0.8.
Now look past that number. The billing column holds 7 predictions, but only 5 tickets are really billing. One sales ticket and one support ticket were sent to billing. The router leans toward the biggest class. Always guessing billing would already score 0.5, so the router adds 0.3 on top of doing nothing clever.
Show the answer
Home buyers are often misclassified as apparel buyers
Rows are the truth and columns are the prediction. So the 'true home' row shows what the model guessed for real home buyers, and it mostly guessed apparel.
What to look for
- A class that's almost never predicted. Its column is nearly empty. The model has given up on it. Usually that's because the class is rare, or its features overlap with a bigger class.
- Two classes that swap a lot. Their features look alike to the model. Better features, not a bigger model, usually fix that.
- Accuracy next to the baseline. When classes are unbalanced, always guessing the biggest one is already right a large share of the time. An accuracy only means something next to that.
What the customer model shows
Here the model does worse than always guessing apparel. The confusion matrix shows why. It predicts apparel for almost everyone. Order value, order count and discount use barely tell you what people buy.
That's a realistic and useful result. Not every question can be answered from the data you have. The baseline and the confusion matrix tell you so in a minute, before anyone builds a campaign on the predictions. The fix is better features, such as what customers browse or what they bought first. A fancier model won't help.
Show the answer
60%
It's right on every support message and wrong on every other one: 120 ÷ 200 = 60%. Any real model has to beat that to be worth using.
Other ways to handle many classes
- One-vs-rest: train one yes/no classifier per class, such as "apparel or not?" Then pick the class whose classifier is most confident. This works with any yes/no model.
- Trees and forests handle many classes on their own. They come in the next lessons.
- Language models are softmax classifiers too. Each class is a token in the vocabulary. A huge network computes the scores , one per token, and softmax turns them into probabilities for the next token.
For marketers: routing inbound messages to sales, support or partnerships is multiclass classification. An LLM with structured output (Phase 1) is often the quickest classifier to build. Either way, this lesson's tools, the confusion matrix and the baseline, are how you judge it.
Practice what you just read
- 4 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
How many weight vectors does softmax regression learn for 4 classes?