Finetuning Classification Accuracy: Training Loop & Confusion Matrix Audit
How are unnormalized scalar logits converted into discrete binary or multi-class decisions, and why is softmax mathematically redundant for argmax?
How this lesson unfolds.
Having attached our classification head and frozen the backbone, we now execute the training process. In this chapter, we formulate the fused loss function, run the multi-epoch training loop, audit confusion matrices, and deploy a production-ready spam classifier.
How are unnormalized scalar logits converted into discrete binary or multi-class decisions, and why is softmax mathematically redundant for argmax?
Imagine a high-jump competition where athletes are awarded raw height scores. If jumper A jumped 2.4 meters and jumper B jumped 1.8 meters, you already know jumper A won! You don't need to convert their heights into percentages of the total height jumped to announce the gold medalist. Argmax picks the winner directly; softmax is only needed if you want to bet on the odds.
- $$\arg\max_i \left(\frac{e^{z_i}}{\sum e^{z_j}}\right) \equiv \arg\max_i (z_i)$$
- Learning goalProve the mathematical equivalence of argmax on logits versus probabilities and implement calibrated thresholding.
- Helpful foundationsForward logit extraction from causal LLMs · Softmax mathematical definition · Deep Learning (Goodfellow, Bengio, & Courville, MIT Press 2016)
- Common trapCalling torch.softmax before torch.argmax during inference; softmax does not change the rank order and adds unnecessary floating-point operations.
- Next connectionAssemble the multi-batch classification evaluation loop in Screen 2.
More context & mechanics
Given raw logit vector z = [z₁, ṡ, z_C] ∈ R^C:
- Decision via Direct Argmax:
ŷ = argmax_(i ∈ [1, C]) zᵢ
- Softmax Probability Calibration:
P(y = i | x) = (e^(zᵢ)) / (∑_(j=1)^C e^(zⱼ))
Because e^z is strictly monotonically increasing (z_a > z_b ⟺ e^(z_a) > e^(z_b)), the maximum coordinate index is invariant:
argmax_(i) P(y=i) ≡ argmax_(i) zᵢ
Softmax is required during training to compute differentiable loss, and during inference when risk thresholds require probability confidence (e.g. flagging spam only if P(spam) > 0.95).
Consider output logits from our classifier for a test SMS message:
z = [2.80, -1.20] (Index 0 = Ham, Index 1 = Spam)
- Fast Decision (Argmax): argmax([2.80, -1.20]) = 0 (Ham). Latency: 0.2 μs.
- Calibrated Probabilities (Softmax):
e^(2.80) ≈ 16.4446, e^(−1.20) ≈ 0.3012, ∑ = 16.7458
P(Ham) = (16.4446) / (16.7458) = 98.2\%, P(Spam) = (0.3012) / (16.7458) = 1.8\%
The system routes the email to the primary inbox with 98.2\% verified confidence.
Production classification APIs return both predicted label and confidence score for automated routing and human-in-the-loop review.
Derived from Luce's choice axiom (1959) and standardized in modern neural decision systems.
Argmax crowns the winner; softmax calculates the odds: skip softmax when speed is all you need.
How is classification accuracy accumulated across variable-length DataLoader batches in PyTorch without memory leakage?
Imagine counting votes in a national election. Each polling station (mini-batch) doesn't calculate its own final percentage; they count exact integer ballots: '320 yes, 80 no'. The central headquarters adds up all raw ballots and divides by the national total. Averaging percentages of different-sized precincts introduces mathematical distortion.
- $$\text{Accuracy} = \frac{\sum_{b=1}^{N_{batches}} \sum_{i=1}^{B_b} \mathbb{I}(\hat{y}_{b, i} = y_{b, i})}{\sum_{b=1}^{N_{batches}} B_b}$$
- $$Evaluating accuracy requires wrapping the validation loop in torch.no_grad() and model.eval(), accumulating integer correct count sum(preds == targets).item() and dividing by total sample count N at the end.$$
- Learning goalImplement an unbiased, leak-free classification accuracy evaluation loop in PyTorch.
- Helpful foundationsLogits to decisions argmax mechanics · PyTorch DataLoader iteration · PyTorch Evaluation Loop & Accuracy Optimization (PyTorch Documentation)
- Common trapAveraging the mean accuracy of each batch; because the final batch in an epoch is often smaller (remainder batch), averaging batch accuracies introduces statistical distortion.
- Next connectionExecute this evaluation before training to establish the Pre-Finetuning Baseline in Screen 3.
More context & mechanics
An evaluation function must avoid batch-size averaging distortions:
```python
@torch.no_grad()
def calc_accuracy(model, data_loader, device):
model.eval()
correct, total = 0, 0
for batch in data_loader:
input_ids = batch["input_ids"].to(device)
targets = batch["labels"].to(device)
logits = model(input_ids) # shape (B, C)
preds = torch.argmax(logits, dim=-1)
correct += (preds == targets).sum().item()
total += targets.numel()
return correct / total
```
Key requirements:
- `torch.no_grad()`: Disables computation graph tracking, saving gigabytes of VRAM.
- `(preds == targets).sum().item()`: Extracts raw Python integer, preventing tensor memory accumulation.
Evaluating on a validation split of 1,200 examples across 3 batches:
- Batch 1 (B=512): 492 correct ⟹ 492 / 512 = 96.09\%
- Batch 2 (B=512): 486 correct ⟹ 486 / 512 = 94.92\%
- Batch 3 (B=176, partial remainder batch): 168 correct ⟹ 168 / 176 = 95.45\%
- Flawed Batch Average: (96.09 + 94.92 + 95.45) / (3) = 95.486\%.
- Exact Ballots Sum:
Total Correct = 492 + 486 + 168 = 1,146
Exact Accuracy = 1146/1200 = 95.500\%
Exact accumulation preserves decimal precision across uneven batch sizes.
Standard evaluation loop pattern across all PyTorch, torchvision, and Hugging Face benchmarks.
Standard numerical engineering practice to avoid Simpson's Paradox and batch weighting distortions.
Sum the integer ballots, divide by total votes: never average percentages across uneven batches.
What is the measured performance of the pretrained GPT-2 backbone on SMS spam classification before fine-tuning, and why is this diagnostic recorded?
Imagine testing the effectiveness of a new running shoe on an athlete's marathon time. Before giving them the new shoes, you must measure their current baseline time with normal sneakers. If you don't know their baseline, you can't tell whether their 3-hour marathon was caused by the shoes or by their existing athletic conditioning.
- $$\text{Baseline Accuracy}_{balanced} = \frac{1}{C} \times 100\%$$
- Learning goalRecord and analyze pre-finetuning baseline accuracy, loss, and class prediction distributions on balanced and imbalanced splits.
- Helpful foundationsPyTorch classification accuracy loop implementation · Untrained model baseline expectations · Language Models are Few-Shot Learners (Brown et al., NeurIPS 2020)
- Common trapEvaluating accuracy on an imbalanced dataset without checking the majority class baseline; on an 87% ham dataset, 87% accuracy is trivial random guessing.
- Next connectionFormulate the cross-entropy loss function for classification logits in Screen 4.
More context & mechanics
The pre-finetuning audit measures four baseline metrics:
- Training Set Accuracy: Acc_(train)^((0)) ≈ 1/C (50.0\% for binary).
- Validation Set Accuracy: Acc_(val)^((0)) ≈ 1/C (50.0\% for binary).
- Cross-Entropy Loss: L₀ ≈ ln(C) = ln(2) ≈ 0.6931 nats.
- Prediction Balance: Checks whether predictions are evenly distributed or collapsed to a single constant class (z₀ ≫ z₁).
If the freshly initialized head outputs Acc = 85\%, the dataset is either heavily imbalanced (e.g. 85% ham) or labels leaked into the prompt.
Audit on SMS Spam Collection Dataset (1,074 test messages, class ratio 87% ham, 13% spam):
- Raw Majority Baseline: A dummy model predicting 'Ham' every time achieves 87.0\%.
- Balanced Audit Subset (200 Ham, 200 Spam):
- Predictions: 198 predicted Ham, 202 predicted Spam.
- Correct Predictions: 201 / 400.
- Initial Accuracy: 50.25\%.
- Initial Cross-Entropy Loss: 0.6942 nats.
The model demonstrates zero class bias and optimal initial entropy.
Standard scientific reporting requirement in machine learning papers and engineering design documents.
Emphasized in statistical design of experiments (Fisher, 1935) and deep learning diagnostics (Karpathy, 2019).
Record the starting line before running the race: baseline metrics prove that fine-tuning genuinely created value.
How does PyTorch torch.nn.CrossEntropyLoss fuse log-softmax with negative log-likelihood for numerical stability, and what is its gradient?
Imagine trying to calculate a tiny decimal like 0.0000000000001 on an old pocket calculator. The calculator rounds it to 0, and then when you try to take the logarithm, it screams 'ERROR: CANNOT DIVIDE BY ZERO'. Fusing log and softmax calculates the logarithm directly in exponent space, completely bypassing the dangerous tiny numbers.
- $$\mathcal{L} = -z_y + \text{LogSumExp}(\mathbf{z}), \quad \nabla_\mathbf{z} \mathcal{L} = \mathbf{p} - \mathbf{y}$$
- $$Instead of computing softmax and log separately, PyTorch CrossEntropyLoss fuses both using the LogSumExp trick, eliminating floating-point underflow and producing the stable analytic gradient dL/dz = p - y.$$
- Learning goalDerive the fused LogSumExp formulation of cross-entropy loss and compute its analytic gradient vector.
- Helpful foundationsSoftmax and cross-entropy derivative collapse · LogSumExp stabilization calculus
- Common trapApplying torch.nn.NLLLoss after torch.nn.Softmax manually; always use torch.nn.CrossEntropyLoss directly on raw logits to avoid numerical underflow.
- Next connectionIntegrate this loss into the complete Classification Training Loop in Screen 5.
More context & mechanics
For input logits z ∈ R^C and ground-truth class y ∈ 0, ṡ, C-1:
L = -ln((e^(z_y)) / (∑_(j=1)^C e^(zⱼ))) = -z_y + ln(∑_(j=1)^C e^(zⱼ))
- The LogSumExp Trick:
To prevent overflow when zⱼ is large, subtract M = maxⱼ zⱼ:
ln(∑_(j=1)^C e^(zⱼ)) = M + ln(∑_(j=1)^C e^(zⱼ - M))
- Backward Gradient:
(∂ L) / (∂ zᵢ) = pᵢ - I(i = y) ⟹ ∇_z L = p - y_(one\_hot)
The gradient is purely the difference between the predicted probability distribution and the true one-hot vector.
Suppose true label is Class 1 (Spam, y = [0, 1]).
Model outputs logits z = [1.5, -0.5]:
- M = max(1.5, -0.5) = 1.5.
- Shifted exponents: e^(1.5 - 1.5) = e⁰ = 1.0, e^(−0.5 - 1.5) = e^(−2.0) ≈ 0.1353.
- LogSumExp = 1.5 + ln(1.0 + 0.1353) = 1.5 + ln(1.1353) = 1.5 + 0.1269 = 1.6269.
- Loss: L = -z₁ + 1.6269 = -(-0.5) + 1.6269 = 2.1269 nats.
- Probabilities: p₀ = (1.0) / (1.1353) = 0.8808, p₁ = (0.1353) / (1.1353) = 0.1192.
- Gradient:
∇_z L = [0.8808 - 0, \ 0.1192 - 1] = [+0.8808, \ -0.8808]
Positive gradient drives logit z₀ down; negative gradient pushes true class logit z₁ up!
The universal loss function for neural classification across PyTorch, TensorFlow, JAX, and TensorRT.
Developed in numerical optimization in the 1980s and formalized in modern deep learning frameworks.
Subtract max before exponentiating: LogSumExp eliminates NaN, leaving the pristine p - y gradient.
How does the complete classification training loop coordinate forward propagation, backward autograd, AdamW optimization, and periodic validation?
Imagine coaching an apprentice chef who already knows knife skills. In classification training, you show them 1,000 dishes and test them after every 50 dishes. Because they already know how to cook, they don't need years of school; within 4 practice sessions (epochs), their accuracy leaps from guessing to Michelin-star precision.
- $$\mathbf{w}_{t+1} = \mathbf{w}_t - \alpha \cdot \text{AdamW}(\nabla_\mathbf{w} \mathcal{L}_{cls})$$
- $$Training for 4-5 epochs with AdamW (lr=5e-5, weight_decay=0.1) on tail-unfrozen layers drives validation accuracy from 50% to >96% in under 5 minutes on consumer hardware.$$
- Learning goalBuild and execute the complete multi-epoch classification training loop in PyTorch, tracking loss and accuracy curves.
- Helpful foundationsCross-entropy loss on logits · Selective tail unfreezing · PyTorch classification accuracy loop
- Common trapForgetting optimizer.zero_grad(); in PyTorch, gradients accumulate by default across iterations, leading to exploding gradient updates if not explicitly zeroed.
- Next connectionPerform comprehensive post-finetuning performance auditing in Screen 6.
More context & mechanics
The multi-epoch classification training algorithm follows five structured steps per iteration:
- Batch Fetch: Extract token inputs X ∈ Z^(B × T) and class labels Y ∈ Z^B.
- Forward Step: Compute Z = model(X) ∈ R^(B × C).
- Loss Computation: L = criterion(Z, Y).
- Backward Step:
optimizer.zero\_grad(), L.backward(), optimizer.step()
- Periodic Evaluation: Every epoch, evaluate accuracy on both training and validation loaders to track convergence and prevent overfitting.
Training GPT-2 Small on SMS Spam dataset (1,174 training samples, B=8, 146 steps per epoch):
- Epoch 1: Loss = 0.482, Train Acc = 84.2\%, Val Acc = 86.5\%.
- Epoch 2: Loss = 0.185, Train Acc = 94.6\%, Val Acc = 93.8\%.
- Epoch 3: Loss = 0.076, Train Acc = 98.1\%, Val Acc = 96.9\% (Peak generalization!).
- Epoch 4: Loss = 0.031, Train Acc = 99.5\%, Val Acc = 96.5\%.
- Epoch 5: Loss = 0.012, Train Acc = 100.0\%, Val Acc = 96.2\%.
Total training time: 3 minutes and 42 seconds on Apple Silicon M-series GPU!
Standard recipe for building enterprise document taggers, customer support intent routers, and safety guardrails.
Standardized in transfer learning workflows popularized by Hugging Face Transformers and fast.ai (2018–2020).
Zero grad, backward, step, evaluate: four epochs transform raw pretraining into 96% precision.
How do Confusion Matrices, Precision, Recall, and F1-Score provide a complete diagnostic audit of model performance beyond raw accuracy?
Imagine a fire alarm in a hotel. If the alarm never rings, it is 99.9% accurate on normal days because there is almost never a fire. But on the day of an actual fire, its accuracy is 0%! Accuracy is deceptive on rare events. Recall measures: 'Did you sound the alarm when there was a real fire?' Precision measures: 'When the alarm rang, was there an actual fire?'
- $$\text{F1} = \frac{2 \cdot \text{Precision} \cdot \text{Recall}}{\text{Precision} + \text{Recall}}$$
- $$On imbalanced datasets, raw accuracy hides false negatives; auditing Precision = TP / (TP + FP) and Recall = TP / (TP + FN) ensures the spam filter catches unwanted messages without quarantining legitimate communications.$$
- Learning goalConstruct and analyze a confusion matrix, calculating Precision, Recall, and F1-Score for fine-tuned LLM classifiers.
- Helpful foundationsClassification training loop convergence · Binary classification statistical metrics
- Common trapRelying on raw accuracy alone on imbalanced datasets; a model that predicts the majority class 100% of the time achieves high accuracy while possessing exactly zero recall for the target class.
- Next connectionDeploy the trained model into real-world inference in Production Spam Classifier (Screen 7).
More context & mechanics
On real-world classification tasks, class distributions are heavily imbalanced. We evaluate via a 2 × 2 Confusion Matrix:
- True Positives (TP): Spam correctly identified as Spam.
- False Positives (FP): Ham falsely flagged as Spam (Severe failure!).
- False Negatives (FN): Spam missed and delivered to Inbox.
- True Negatives (TN): Ham correctly identified as Ham.
Statistical metrics:
Precision = (TP) / (TP + FP), Recall = (TP) / (TP + FN)
F1-Score = 2 × (Precision × Recall) / (Precision + Recall)
Audit on 1,074 unseen test messages (934 Ham, 140 Spam):
- Confusion Matrix:
- TN = 928, FP = 6 (Only 6 legitimate emails misclassified!).
- FN = 7, TP = 133 (133 out of 140 spam caught!).
- Accuracy: (928 + 133) / (1074) = 1061/1074 = 98.79\%.
- Precision: (133) / (133 + 6) = 133/139 = 95.68\%.
- Recall: (133) / (133 + 7) = 133/140 = 95.00\%.
- F1-Score: 2 × (0.9568 × 0.9500) / (0.9568 + 0.9500) = 95.34\%.
The model demonstrates institutional-grade classification precision.
Mandatory reporting standards for medical diagnostics, spam filtering, fraud detection, and credit scoring.
Formalized in statistical detection theory (Radar engineering, 1940s) and popularized in machine learning evaluation.
Precision is trustworthiness; recall is thoroughness: F1 harmonizes both into a single truthful metric.
How is the fine-tuned model encapsulated into a production inference function that accepts raw text strings and outputs calibrated predictions in milliseconds?
Imagine walking up to an airport security scanner. You don't hand the guard raw electromagnetic wavelength charts; you step through the arch, and the machine immediately flashes a clear green light ('Clear') or red light ('Search Required') along with a risk score. Production inference wraps complex tensor math into a clean, instantaneous green-or-red light function.
- $$\text{Text} \xrightarrow{\text{Encode}} \mathbf{x}_{1:T} \xrightarrow{\text{Forward}} \mathbf{z} \in \mathbb{R}^2 \xrightarrow{\text{Softmax}} \text{Label}$$
- Learning goalEncapsulate a fine-tuned transformer classifier into a robust, sub-15ms production Python inference function.
- Helpful foundationsPost-finetuning performance audit · Logits to decisions softmax argmax mechanics
- Common trapBelieving production inference requires token-by-token autoregressive generation loops; classification requires only a single forward pass, operating 50x faster than text generation.
- Next connectionTransitions directly to Chapter 37 to explore full Instruction Finetuning with the Stanford Alpaca dataset.
More context & mechanics
The production inference pipeline implements end-to-end string-to-decision execution:
```python
@torch.no_grad()
def classify_message(text, model, tokenizer, device, max_length=128):
model.eval()
encoded = tokenizer.encode(text)
if len(encoded) > max_length:
encoded = encoded[:max_length]
input_ids = torch.tensor(encoded, device=device).unsqueeze(0) # (1, T)
logits = model(input_ids) # shape (1, 2)
probs = torch.softmax(logits, dim=-1).squeeze(0)
pred_idx = torch.argmax(probs).item()
confidence = probs[pred_idx].item()
label = "Spam" if pred_idx == 1 else "Ham"
return "label": label, "confidence": confidence, "probs": probs.tolist()
```
Testing the production wrapper with real-world inputs:
- Test Message 1: 'Hey, are we still meeting for lunch at 12:30?'
- Logits: [+3.85, -2.10]
- Prediction: Ham (Legitimate) | Confidence: 99.74\% | Latency: 11.8 ms.
- Test Message 2: 'WINNER! You have won a 1000 dollar gift card! Click here to claim your prize now!'
- Logits: [-2.95, +4.10]
- Prediction: Spam | Confidence: 99.91\% | Latency: 12.1 ms.
The pipeline operates deterministically with zero memory accumulation.
Deployed inside email servers, chat applications (Discord, Slack bots), and SMS gateways for automated real-time filtering.
Represents the modern realization of text classification, evolving from naive Bayes filters (Sahami et al., 1998) to deep contextual transformer inference.
Tokenize, forward, softmax, decide: a 10-line wrapper turns deep linear algebra into a real-time decision engine.