Skip to content

First Example: Classifying Newswires (Reuters, Multiclass)

Going from 2 classes to 46 changes three lines of code — the output width, the activation, the loss — and it changes everything about how you should read the result. The IMDB dataset was exactly 50/50 balanced. Reuters has one topic holding 35% of the data and another holding 10 examples, and that single fact makes accuracy a misleading number.

  • Softmax derived from scratch, verified against Keras to 3.27e−08, plus why the max is subtracted before exponentiating.
  • The two baselines: majority class 0.3620, uniform guessing 0.0217.
  • Sparse integer labels versus one-hot: identical loss, 23× less memory.
  • What a 4-unit bottleneck costs, measured: 0.7703 → 0.6207 test accuracy.
  • Why 0.7703 accuracy sits alongside a macro-average recall of 0.3279 — and that 13 of 46 classes were never predicted once.
  • How to trade coverage for reliability: abstain below 0.90 confidence and accuracy rises to 0.9389 on the 55% of newswires you keep.
Loading the newswires
from tensorflow import keras
import numpy as np
 
(raw_train, y_train), (raw_test, y_test) = keras.datasets.reuters.load_data(
    num_words=10000)
 
print(len(raw_train), len(raw_test))     # 8982 2246
print(int(y_train.max()) + 1)            # 46
print(np.bincount(y_train).max())        # 3159

The label distribution is the defining feature of this dataset:

RankClass idTraining examplesShare
133,1590.3517
241,9490.2170
3195490.0611
4164440.0494
514320.0481
4635100.0011
figure Every class in the training set, largest to smallest matplotlib
Bar chart of the 46 topic classes ordered from largest to smallest on a logarithmic vertical axis. The first bar towers above the rest at over three thousand examples, the second is close to two thousand, and the remaining forty-four decline smoothly to a final bar of ten examples. Bar chart of the 46 topic classes ordered from largest to smallest on a logarithmic vertical axis. The first bar towers above the rest at over three thousand examples, the second is close to two thousand, and the remaining forty-four decline smoothly to a final bar of ten examples.
The vertical axis is logarithmic — on a linear axis the last thirty bars would be invisible. The largest topic has 3,159 examples and the smallest has 10, a ratio of 316x, and the top three topics together account for 63.0% of all 8,982 newswires.

Two baselines follow directly:

BaselineTest accuracy
always predict class 30.3620
uniform random over 46 classes0.0217

Any accuracy under 0.3620 is worse than a model that ignores the input entirely. That is the number to keep in mind for the rest of this page.

Softmax: turning scores into a distribution

Section titled “Softmax: turning scores into a distribution”

The output layer produces 46 raw scores. Softmax converts them into probabilities that sum to 1:

softmax(z)i=ezij=1Kezj\text{softmax}(\mathbf{z})_i = \frac{e^{z_i}}{\sum_{j=1}^{K} e^{z_j}}

Exponentiating makes everything positive; dividing by the sum makes it total 1. Because exe^x grows so fast, softmax exaggerates differences — a score gap of 1 becomes a probability ratio of e2.72e \approx 2.72.

Worked on three scores by hand:

zzeze^zprobability
2.07.3890560.659001
1.02.7182820.242433
0.11.1051710.098566

Sum of probabilities: 1.000000. tf.nn.softmax returns the same three values to a maximum difference of 3.27e−08 (float32 rounding, not disagreement).

Softmax is unchanged if you shift every score by the same constant cc:

ezicjezjc=eceziecjezj=ezijezj\frac{e^{z_i - c}}{\sum_j e^{z_j - c}} = \frac{e^{-c} e^{z_i}}{e^{-c} \sum_j e^{z_j}} = \frac{e^{z_i}}{\sum_j e^{z_j}}

That invariance is what makes the standard implementation safe. Computed naïvely on z=[1000,1001,1002]\mathbf{z} = [1000, 1001, 1002], e1000e^{1000} overflows float64 and every probability comes back nan. Subtracting the max first gives [0.090031,0.244728,0.665241][0.090031, 0.244728, 0.665241] — the mathematically identical answer, with the largest exponent now e0=1e^0 = 1.

sketch Drag the scores, watch the distribution p5.js
Three raw output scores and their softmax. Drag any bar to change its score. The button adds 5 to all three at once — the probabilities do not move, because softmax is shift-invariant.

Two encodings, two matching losses, identical results:

The same loss, twice
integers = np.array([1, 0])
one_hot = np.eye(3, dtype="float32")[integers]
predictions = np.array([[0.1, 0.7, 0.2], [0.6, 0.3, 0.1]], dtype="float32")
 
keras.losses.sparse_categorical_crossentropy(integers, predictions)  # 0.433750
keras.losses.categorical_crossentropy(one_hot, predictions)          # 0.433750

The difference is 5.96e−08 — float32 rounding. The real difference is memory: one-hot labels for 8,982 rows and 46 classes cost 1,652,688 bytes against 71,856 for int64 integers, a factor of 23. At 46 classes that is trivial; at 50,000 vocabulary classes in a language model it is not, which is why sparse_categorical_crossentropy is the default choice.

64-64-46, softmax out
model = keras.Sequential([
    keras.layers.Input((10000,)),
    keras.layers.Dense(64, activation="relu"),
    keras.layers.Dense(64, activation="relu"),
    keras.layers.Dense(46, activation="softmax"),
])
model.compile("rmsprop", "sparse_categorical_crossentropy",
              metrics=["accuracy"])
LayerParameters
Dense(64) on 10,000 inputs10,000×64+64=640,06410{,}000 \times 64 + 64 = 640{,}064
Dense(64) on 64 inputs64×64+64=4,16064 \times 64 + 64 = 4{,}160
Dense(46) on 64 inputs64×46+46=2,99064 \times 46 + 46 = 2{,}990
total647,214

Now shrink the second hidden layer to 4 units and change nothing else:

figure Same input, same output, same everything but four numbers matplotlib
Two panels comparing a network with a 64-unit second hidden layer against one with 4 units. Left: validation loss, where the 64-unit curve bottoms out near 0.98 at epoch 10 and the 4-unit curve plateaus far above it around 1.42. Right: validation accuracy, where the 64-unit curve settles near 0.79 and the 4-unit curve near 0.64. Two panels comparing a network with a 64-unit second hidden layer against one with 4 units. Left: validation loss, where the 64-unit curve bottoms out near 0.98 at epoch 10 and the 4-unit curve plateaus far above it around 1.42. Right: validation accuracy, where the 64-unit curve settles near 0.79 and the 4-unit curve near 0.64.
The 4-unit model reached 0.6269 test accuracy against the 64-unit model's 0.7738 — a loss of 0.1469 from one layer's width. It also took 17 epochs to reach its best validation loss instead of 10, and that best was 1.4224 against 0.9790.
Second hidden layerBest val lossAt epochVal accuracyTest accuracy
64 units0.9790100.78560.7738
4 units1.4224170.64080.6269

This is usually called an information bottleneck, and the phrase deserves care. Four real-valued numbers could in principle encode 46 labels — four float32s carry far more than the log2465.5\log_2 46 \approx 5.5 bits required. The measured failure is not an information-theoretic impossibility; it is that gradient descent on a 4-dimensional ReLU representation does not find such a code. The layer’s output is non-negative and piecewise linear, the 46-way separation has to be linear in those four coordinates, and training lands in a much worse solution.

The lesson survives the correction: every layer’s width caps what the layers after it can distinguish, and no downstream capacity recovers what a narrow layer discarded. The practical rule is that intermediate layers should be comfortably wider than the number of output classes.

The 64-unit model reaches 0.7703 test accuracy (9 epochs, trained on 7,000 rows). Against the 0.3620 majority baseline that looks like a solid result. Then look per class:

Class idTest examplesRecall
38130.9668
44740.8713
11050.8000
191330.7594
the 22 classes with ≤10 test examples (123 newswires)1230.1301

And the summary statistic that makes it unambiguous:

MetricValue
accuracy (every newswire weighted equally)0.7703
macro-average recall (every class weighted equally)0.3279
classes never predicted even once13 of 46

Accuracy 0.7703, macro recall 0.3279. Both are correct measurements of the same model. Accuracy is dominated by classes 3 and 4, which together are 57% of the test set and where the model scores 0.96 and 0.87. Averaged over classes instead of over rows, the model is barely better than a coin flip on the tail — and for 13 topics it has effectively learned that the safest move is to never guess them at all.

figure Where the errors actually go matplotlib
A ten by ten confusion matrix over the largest topics, row-normalised. The diagonal is dark for the top classes and lighter further down. The row for class 20 shows only 0.44 on the diagonal with 0.21 leaking to class 3 and 0.16 to class 19. A ten by ten confusion matrix over the largest topics, row-normalised. The diagonal is dark for the top classes and lighter further down. The row for class 20 shows only 0.44 on the diagonal with 0.21 leaking to class 3 and 0.16 to class 19.
Row-normalised, so each row sums to 1 across all 46 predictions and the visible cells are the ten largest topics. Class 20 is the weakest of the ten at 0.44, and its errors are not random: 0.21 of them go to class 3 and 0.16 to class 19, the two largest topics available. When a model is unsure it drifts toward the classes it has seen most.

If your problem cares about the rare classes — fraud, rare diseases, uncommon faults — accuracy will tell you everything is fine while the model ignores exactly the cases you built it for. Report macro recall, per-class recall, or a confusion matrix alongside it.

Softmax gives a confidence with every prediction. Grouping the test set by that confidence:

Max softmax probabilityNewswiresAccuracy in the band
[0.0, 0.4)3360.3661
[0.4, 0.7)3410.5718
[0.7, 0.9)3260.7515
[0.9, 1.0]1,2430.9389

Confidence is informative — it separates a 0.37 band from a 0.94 band — but it is not calibrated: mean confidence is 0.8567 when the model is right and still 0.5139 when it is wrong. Turn that into a policy by refusing to answer below a threshold:

Abstain belowCoverageNewswires keptAccuracy on kept
1.00002,2460.7703
0.400.85041,9100.8414
0.700.69861,5690.8999
0.900.55341,2430.9389
0.990.22355020.9622

Answering only the 55% it is most sure about takes the model from 0.7703 to 0.9389 — without touching a single weight. In any workflow where a human can handle the rest, that trade is usually the whole product.

sketch Trade coverage for accuracy p5.js
Drag the threshold across the five measured cutoffs. Coverage is the share of the 2,246 test newswires the model still answers; accuracy is measured on that kept subset.
diagram Diagram mermaid
  • Reading accuracy alone on imbalanced data. 0.7703 accuracy and 0.3279 macro recall describe the same model. Only one of them notices that 13 classes are never predicted.
  • Forgetting the majority-class baseline. Predicting class 3 every time scores 0.3620 here. Any result must be compared against that, not against 1/46.
  • Making an intermediate layer narrower than the output. 4 units cost 0.1469 test accuracy against 64.
  • Mismatching label encoding and loss. Integer labels with categorical_crossentropy (or one-hot with the sparse_ version) produces a shape error at best and a silently wrong loss at worst.
  • Computing softmax without subtracting the max. e1000e^{1000} overflows to inf and every probability becomes nan.
  • Treating softmax output as calibrated probability. Mean confidence was 0.5139 on the predictions that were wrong.
  • Using softmax on the output layer and from_logits=True in the loss. The loss then exponentiates already-normalised probabilities. Pick one.
  • Reuters has 46 topics, 8,982 training newswires, and a 316× ratio between the largest and smallest class.
  • Softmax exponentiates and normalises; it is shift-invariant, which is exactly why implementations subtract the max before exponentiating.
  • Sparse integer labels and one-hot labels give identical losses; integers cost 23× less memory here and far less on large label spaces.
  • Narrowing the second hidden layer from 64 to 4 units cost 0.1469 test accuracy. Keep intermediate widths above the number of classes.
  • Accuracy 0.7703 versus macro recall 0.3279 is the central lesson: on imbalanced data, always report a per-class view.
  • Abstaining below 0.90 confidence raises accuracy to 0.9389 on the 55.3% of newswires the model still answers.

The same three-step recipe once more, with a continuous target instead of a class — which changes the loss, the last layer, and how you have to validate when there are only 404 training rows: Predicting House Prices.

pch.quizTag pch.quizDefaultTitle
  1. The model scores 0.7703 accuracy and 0.3279 macro-average recall. What is the difference between those two numbers measuring?

    pch.quizShowAnswer

    B — Accuracy weights every newswire equally, so the two largest topics (57% of the test set) dominate it; macro recall weights every class equally, so the 44 small topics count as much as the big two — The model gets 0.9668 recall on class 3 and 0.1301 on the 22 rarest classes. Row-weighted and class-weighted averages therefore tell opposite stories.

  2. Why is 1/46 = 0.0217 the wrong baseline to compare a Reuters model against?

    pch.quizShowAnswer

    B — Because the classes are heavily imbalanced: always predicting the largest class scores 0.3620, so that is the real bar to beat — Uniform guessing only makes sense as a baseline when the classes are uniform. Here the majority-class baseline is 17x higher.

  3. Softmax implementations subtract the maximum score before exponentiating. What does that change?

    pch.quizShowAnswer

    B — Nothing mathematically — softmax is shift-invariant — but it prevents e^z from overflowing to infinity, which turns every probability into nan — For z = [1000, 1001, 1002] the naive form returns nan; subtracting the max gives [0.090031, 0.244728, 0.665241].

  4. Narrowing the second hidden layer from 64 to 4 units dropped test accuracy from 0.7738 to 0.6269. What is the precise reason?

    pch.quizShowAnswer

    B — Four numbers could encode 46 labels in principle, but gradient descent on a non-negative 4-dimensional ReLU representation does not find such a code, and no later layer can restore what that layer dropped — The failure is about what training finds, not about bits. The practical rule still holds: keep intermediate layers wider than the class count.

  5. Refusing to predict when confidence is below 0.90 raised accuracy from 0.7703 to 0.9389. What was given up?

    pch.quizShowAnswer

    B — Coverage: 44.7% of newswires now get no answer at all, so the gain is only useful if something else (a human, a fallback rule) handles them — Selective prediction trades coverage for reliability. Always report both numbers — an accuracy figure without its coverage is meaningless.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading