Index Ai Academy
Phase 3 · Machine learning · Models

Multiclass classification

Softmax and cross-entropy for more than two classes.

About 30 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

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

  1. One score per class
  2. Training: the right answer as a list
  3. Predicting a customer's favorite category
  4. Reading the confusion matrix
  5. Worked example: tallying ten support tickets
  6. What to look for
  7. 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 KK stands for how many classes there are. In the mail room, K=3K = 3.

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:

classweights (orders, discount)starting amountscore
apparel0.5, 0.500.5 × 3 + 0.5 × 1 + 0 = 2
home0.5, −1.00.50.5 × 3 − 1.0 × 1 + 0.5 = 1
accessories−0.5, 1.00.5−0.5 × 3 + 1.0 × 1 + 0.5 = 0

Apparel has the highest score, so apparel is the guess.

Quick check
Where does each class's score come from?
  1. Its own weights and its own starting amount, applied to the same features
  2. The class's name
  3. One shared set of weights
  4. The previous customer's answer
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 kk means "class number kk". The score for class kk is zkz_k:

zk=wk⋅x+bkz_k = \mathbf{w}_k \cdot \mathbf{x} + b_k

The bold x\mathbf{x} is the list of the customer's features. The bold wk\mathbf{w}_k is class kk's list of weights. The dot between them is the dot product: multiply in pairs, then add up. Last, bkb_k is class kk'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 pkp_k for the probability of class kk:

pk=ezk∑jezjp_k = \frac{e^{z_k}}{\sum_j e^{z_j}}

Here ee is a fixed number, about 2.718. The ∑\sum means "add up". The letter jj counts through every class to build the total.

In plain words: raise ee 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))
It runs right here in your browser, with nothing to install.

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, WW. It has one row per feature and one column per class. Then all the scores come out in one step:

z=W⊤x+b\mathbf{z} = W^\top \mathbf{x} + \mathbf{b}

Read it as: "the list of scores equals W transposed times the features, plus the list of biases." W⊤W^\top, said "W transposed", is WW flipped so its columns become rows. Each row is then one class's weights. Multiplying by x\mathbf{x} takes a dot product with each row.

In the example, the columns of WW are [0.5, 0.5], [0.5, −1.0] and [−0.5, 1.0]. The bias list b\mathbf{b} is [0, 0.5, 0.5]. With x\mathbf{x} = [3, 1], the result is [2, 1, 0], the same three scores.

Try it now

One score per class, turned into probabilities that add up to 1. Drag a score and watch every probability move.

Quick check
A message router gives these three scores. What does this print?
scores = {"sales": 0.4, "support": 1.9, "billing": 1.2}
print(max(scores, key=scores.get))
  1. billing
  2. support
  3. sales
  4. 1.9
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 −log⁡pcorrect-\log p_{\text{correct}}: 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:

blankapparelhomeaccessories
predicted0.6650.2450.090
true (one-hot)010
predicted − true0.665−0.7550.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.

Quick check
The model gave apparel 0.665, but apparel isn't the right answer. What does training do to apparel's score?
  1. Pushes it up
  2. Pushes it down
  3. Removes the class
  4. Leaves it alone
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:

∇WL=1nX⊤(P−Y)\nabla_W L = \frac{1}{n} X^\top (P - Y)

There are several symbols here, so take them one at a time:

  • LL is the loss.
  • ∇WL\nabla_W L is the gradient: a table with one slope for every weight. The triangle, ∇\nabla, is said "nabla".
  • nn is the number of examples.
  • XX is the table of features, one row per example.
  • X⊤X^\top, said "X transposed", is XX flipped so rows become columns.
  • PP holds the predicted probabilities, one row per example and one column per class.
  • YY holds the one-hot truths, in the same shape as PP.

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 pk−ykp_k - y_k. Here yky_k is 1 for the true class and 0 for the rest. Each score is zk=wk⋅x+bkz_k = \mathbf{w}_k \cdot \mathbf{x} + b_k. Write xix_i for feature number ii. When class kk's weight for that feature goes up by a little, zkz_k goes up by xix_i times that amount. That's the chain rule from Phase 2. Multiply the two, and the slope for that weight is xi(pk−yk)x_i (p_k - y_k).

The bias bkb_k 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 XX. The formula X⊤(P−Y)X^\top (P - Y) does every one of these multiplications at once, and summed over every example. Dividing by nn turns the sum into an average.

Quick check
The classes are [sales, support, billing]. A ticket is really billing, and the model predicts [0.2, 0.5, 0.3]. What is predicted minus true?
  1. [0.2, 0.5, 0.3]
  2. [-0.8, 0.5, 0.3]
  3. [0.2, 0.5, -0.7]
  4. [0.2, -0.5, 0.7]
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. stratify keeps 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

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))
In the lesson you can change this and run it.

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.

Quick check
In a confusion matrix, the 'true home' row has most of its counts in the 'pred apparel' column. What does that tell you?
  1. Apparel buyers are often misclassified as home buyers
  2. Home buyers are often misclassified as apparel buyers
  3. There are no home buyers in the data
  4. The model is perfect for home buyers
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.

Quick check
Out of 200 incoming messages, 120 are support requests. What accuracy does a 'model' that always guesses support get?
  1. 100%
  2. 60%
  3. 33%
  4. 50%
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 z\mathbf{z}, 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

Create a free account

Try a quiz question

How many weight vectors does softmax regression learn for 4 classes?

  1. 4, one per class
  2. 1
  3. 3
  4. 16

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson