LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 196 articles completed

🛠️Computing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
📊Math & Statistics0/9
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningBayesian Inference from EvidenceStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
📚Preparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
🧮ML Algorithms & Evaluation0/15
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageStatistical Learning and GeneralizationConformal Prediction and UncertaintyClustering and PCAProbabilistic Generative ModelsCore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingCausal Inference and CounterfactualsPyTorch Training LoopsDataset Pipelines and Data Quality
📦Production ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
🧪Core LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
🧰Applied LLM Engineering0/25
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernancePrivacy-Preserving Machine LearningData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BPrompt Optimization with DSPyModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
🎓Portfolio Capstones0/9
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned ClassifierCapstone: Reproducible ML Study
🧠Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
🧬Advanced Training & Adaptation0/17
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabJAX for PyTorch ResearchersContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineMixed Precision TrainingDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningTraining Run OperationsReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight Interpolation
🤖Advanced Agents & Retrieval0/16
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryRecursive Language Models (RLM)Multi-Agent OrchestrationCapstone: Production Agent
⚡Inference & Production Scale0/31
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceAccelerator Architecture Field GuideCorrect GPU Profiling and BenchmarkingGPU Kernel Performance Engineering LabGPU Parallel Primitives LabGPU Compilers and Kernel LanguagesGEMM From Naive CUDA to Tensor CoresLow-Precision GPU ProgrammingGPU Runtime Execution LabAI-Generated Kernel EngineeringModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingGPU Failure OperationsDistributed Inference Data PlaneServing Benchmark EngineeringA/B Testing for LLMs
🏗️System Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning Agent System Design
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
🔬Project Deep Dives0/17
Deep Dive - vLLMDeep Dive - SkyRLDeep Dive - FlashAttentionDeep Dive - FlashInferDeep Dive - DeepGEMMDeep Dive - NCCLDeep Dive - MegatronDeep Dive - DeepSpeedDeep Dive - RayDeep Dive - MLflowDeep Dive - PyTorchDeep Dive - TransformersDeep Dive - SGLangDeep Dive - slimeDeep Dive - DeepEPDeep Dive - TinkerDeep Dive - Light-PEFT
Back to Topics
LearnPreparation & PrerequisitesSoftmax, Cross-Entropy & Optimization
📝EasyNLP Fundamentals

Softmax, Cross-Entropy & Optimization

Turn raw class scores into stable probabilities and a useful learning signal, then apply the same loss to next-token predictions.

22 min read
Learning path
Step 22 of 196 in the full curriculum
Training & BackpropagationRNNs, LSTMs, GRUs, and Sequence Modeling

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

The previous lesson predicted one continuous scalar: extra decode latency in milliseconds. Half squared error could say "raise the prediction by four units." A review triage bot faces a different challenge: pick exactly one category among bug, docs, and security.

Suppose the incident note reads, "This docs page is stale," while the model emits raw scores bug=3.0, docs=1.0, security=0.0. It picks bug, missing the correct label. What training signal should backpropagation send back through the network?

A binary right-or-wrong check can't distinguish a narrow near-miss from an arrogant blunder. Assigning docs an 11% probability should cost significantly more than assigning it 49%, even if bug wins the top spot in both runs. We need a mathematical bridge that turns unbounded classification scores into honest probability shares, converts missed confidence into an exact penalty, and sends back a gradient that pulls the correct score upward while pushing wrong favorites down.

A decision begins as three raw scores

The final linear layer of a neural network outputs one real number per possible class. These numbers are logits: unbounded scores produced by taking dot products between internal representations and class weight vectors. They don't represent probabilities yet, and they aren't constrained between zero and one.

LabelCurrent logitWhat the incident note says
bug3.0Model's current favorite, but incorrect
docs1.0Correct supervised target
security0.0Plausible alternative, incorrect here

A larger logit means the network prefers that label over competitors. A score of 3.0 doesn't mean 300% confidence. Read the table as an uncalibrated ranking: bug leads docs by two score points, but we haven't assigned any percentage share to any option.

Logits can be negative, zero, or wildly positive without breaking mathematics. Probabilities, by contrast, must satisfy two strict Kolmogorov axioms: every probability pip_ipi​ must lie in [0,1][0, 1][0,1], and the entire set of mutually exclusive choices must sum to exactly 111.

Predict the three checks in inspect-raw-logits.py before running it. bug takes the highest score, the raw sum is 4.04.04.0, and the list fails the probability test. We've got a ranking, but not a normalized distribution that an information-theoretic loss can evaluate.

inspect-raw-logits.py
1labels = ["bug", "docs", "security"] 2logits = [3.0, 1.0, 0.0] 3 4best_index = max(range(len(logits)), key=logits.__getitem__) 5print("largest logit:", labels[best_index]) 6print("raw score sum:", sum(logits)) 7print("valid probability distribution:", all(0 <= z <= 1 for z in logits) and sum(logits) == 1)
Output
1largest logit: bug 2raw score sum: 4.0 3valid probability distribution: False

The raw scores identify a winner, but they don't yet say how much probability the correct label received.

Softmax turns score gaps into probability mass

Softmax maps an unconstrained vector of KKK real numbers into a valid categorical probability distribution. It operates in two deliberate stages: exponentiation and normalization.

First, it raises Euler's constant e≈2.71828e \approx 2.71828e≈2.71828 to each logit. Because ez>0e^z > 0ez>0 for every real number z∈(−∞,+∞)z \in (-\infty, +\infty)z∈(−∞,+∞), exponentiation eliminates negative values and guarantees positivity. At the same time, exponentiation magnifies differences: a constant score lead of +2.0+2.0+2.0 expands into an exponential multiplier of e2.0≈7.389e^{2.0} \approx 7.389e2.0≈7.389.

Second, it sums those positive weights into a single shared denominator, called the partition function:

∑j=1Kezj\sum_{j=1}^K e^{z_j}j=1∑K​ezj​

Dividing each exponential weight by this sum forces the output shares to sum to 1.01.01.0.

For our three triage scores, walk through the arithmetic: e3.0≈20.086e^{3.0} \approx 20.086e3.0≈20.086, e1.0≈2.718e^{1.0} \approx 2.718e1.0≈2.718, and e0.0=1.000e^{0.0} = 1.000e0.0=1.000. The shared sum is 23.80423.80423.804.

LabelLogit ziz_izi​Unnormalized weight ezie^{z_i}ezi​Probability share pip_ipi​
bug3.020.090.844
docs1.02.720.114
security0.01.000.042
Total23.811.000

The general equation applies this two-step transformation to any vector z∈RKz \in \mathbb{R}^Kz∈RK:

pi=ezi∑j=1Kezjp_i = \frac{e^{z_i}}{\sum_{j=1}^{K} e^{z_j}}pi​=∑j=1K​ezj​ezi​​

Here pip_ipi​ is the probability assigned to class iii, ziz_izi​ is its logit, and KKK is the number of competing classes. Notice the distribution: the model assigns 0.844 to the incorrect bug label, leaving only 0.114 for the correct docs label. That value is the handoff to the loss function.

Visual progression of softmax and log-sum-exp stabilization: raw logits [3.0, 1.0, 0.0] are shifted by max logit 3.0 to [0.0, -2.0, -3.0], exponentiated to bounded weights [1.000, 0.135, 0.050] summing to 1.185, and normalized into probability mass 84.4 percent bug, 11.4 percent docs, and 4.2 percent security.
Exponentiation stretches score gaps into positive weights. Subtracting the peak logit 3.0 caps every exponential at 1.0 to prevent floating-point overflow, while normalizing by the shared sum 1.185 preserves the exact probability shares.

Before running stable-softmax.py, confirm the invariants: the class ranking stays preserved, every entry is strictly positive, and the values sum to 1.0.

stable-softmax.py
1import math 2 3def stable_softmax(logits: list[float]) -> list[float]: 4 peak = max(logits) 5 weights = [math.exp(z - peak) for z in logits] 6 total = sum(weights) 7 return [weight / total for weight in weights] 8 9labels = ["bug", "docs", "security"] 10logits = [3.0, 1.0, 0.0] 11probabilities = stable_softmax(logits) 12 13for label, probability in zip(labels, probabilities): 14 print(f"{label:8s} {probability:.3f}") 15print("sum ", round(sum(probabilities), 3))
Output
1bug 0.844 2docs 0.114 3security 0.042 4sum 1.0

The correct label is docs, but its probability is only 0.114. Why can't the model fix this by simply tweaking the post-softmax numbers directly?

Answer

Probabilities are consequences of logits and compete for a fixed budget of 1.0. Learning updates parameters, which changes the logits. Pushing the docs logit upward relative to bug is what increases its probability on the next forward pass.

Stable arithmetic prevents floating-point overflow

Our gentle logits [3.0, 1.0, 0.0] fit easily inside standard floating-point variables. What happens when a deep network produces raw scores like [1000.0, 998.0, 997.0]?

In IEEE 754 single-precision floating-point (float32), numbers overflow once they exceed approximately 3.4×10383.4 \times 10^{38}3.4×1038. Because e88.72≈3.4×1038e^{88.72} \approx 3.4 \times 10^{38}e88.72≈3.4×1038, any logit greater than 88.7288.7288.72 overflows to +inf. In half-precision formats (float16 and bfloat16), the dynamic range is even tighter: float16 overflows at 655046550465504, which corresponds to e11.09e^{11.09}e11.09. Logits as modest as 12.012.012.0 trigger +inf.

Once an exponential overflows to +inf, the denominator evaluates to +inf. Dividing inf / inf yields NaN (Not a Number), poisoning every downstream weight tensor in your model.

Softmax possesses a key algebraic property: shift invariance.[1]Reference 1Deep Learning.https://www.deeplearningbook.org/ Adding or subtracting any arbitrary scalar constant ccc from every logit leaves the resulting probabilities completely unchanged:

ezi−c∑j=1Kezj−c=ezie−ce−c∑j=1Kezj=ezi∑j=1Kezj\frac{e^{z_i - c}}{\sum_{j=1}^K e^{z_j - c}} = \frac{e^{z_i} e^{-c}}{e^{-c} \sum_{j=1}^K e^{z_j}} = \frac{e^{z_i}}{\sum_{j=1}^K e^{z_j}}∑j=1K​ezj​−cezi​−c​=e−c∑j=1K​ezj​ezi​e−c​=∑j=1K​ezj​ezi​​

The common factor e−ce^{-c}e−c factors out of the sum and cancels between numerator and denominator. This identity unlocks the classic numerical stabilization trick: choose c=m=max⁡j(zj)c = m = \max_j(z_j)c=m=maxj​(zj​), the maximum logit across the vector.

Subtracting mmm shifts the entire logit vector so that the largest value becomes exactly zero (zmax−m=0z_{\text{max}} - m = 0zmax​−m=0). Every other shifted logit is strictly negative (zj−m≤0z_j - m \le 0zj​−m≤0). Exponentiating non-positive numbers guarantees:

  1. The largest exponential is e0=1.0e^0 = 1.0e0=1.0. Overflow is mathematically impossible.
  2. Every other exponential lies in (0,1](0, 1](0,1].
  3. The denominator sum is at least 1.01.01.0, preventing division by zero.

Very negative shifted logits (such as −1000-1000−1000) will underflow to floating-point 0.0. That underflow is benign during the forward pass: a weight of 0.0 simply means that candidate receives zero probability share.

overflow-then-stable.py
1import math 2 3def stable_softmax(logits: list[float]) -> list[float]: 4 peak = max(logits) 5 weights = [math.exp(z - peak) for z in logits] 6 total = sum(weights) 7 return [weight / total for weight in weights] 8 9large_logits = [1000.0, 998.0, 997.0] 10try: 11 _ = [math.exp(z) for z in large_logits] 12 naive_ok = True 13except OverflowError: 14 naive_ok = False 15 16stable = stable_softmax(large_logits) 17print("naive overflowed:", not naive_ok) 18print("stable:", [round(probability, 3) for probability in stable]) 19print("stable sum:", round(sum(stable), 3))
Output
1naive overflowed: True 2stable: [0.844, 0.114, 0.042] 3stable sum: 1.0

NumPy's naive exponential returns inf, followed by NaN when dividing by the infinite sum. Shifting by the maximum value first eliminates overflow while returning the exact same probability distribution.

Cross-entropy prices missed predictions through surprise

Softmax gave us a probability distribution where docs received pdocs≈0.114p_{\text{docs}} \approx 0.114pdocs​≈0.114. How do we score that prediction?

Cross-entropy measures the quality of a predicted distribution against ground truth. It's grounded in Maximum Likelihood Estimation (MLE) and information theory.

In information theory, observing an event with probability ppp yields an information content, or surprise, of:

I(p)=−log⁡(p)I(p) = -\log(p)I(p)=−log(p)

We use natural logarithms throughout, measuring surprise in nats (using base-two logarithms would measure surprise in bits). If an event is certain (p=1.0p = 1.0p=1.0), observing it brings zero surprise: −ln⁡(1.0)=0-\ln(1.0) = 0−ln(1.0)=0. If an event is extremely rare (p→0p \to 0p→0), observing it brings near-infinite surprise: −ln⁡(0.001)≈6.91-\ln(0.001) \approx 6.91−ln(0.001)≈6.91 nats.

When training a supervised model, the ground truth for an input is a target distribution yyy. For single-label classification, yyy is a one-hot vector where the correct class index ccc gets weight 1.01.01.0 and all competing classes get 0.00.00.0. For our triage note:

y=[0, 1, 0]y = [0,\ 1,\ 0]y=[0, 1, 0]

Cross-entropy calculates the expected surprise across all classes under the true distribution:

LCE=−∑k=1Kyklog⁡(pk)L_{\text{CE}} = -\sum_{k=1}^K y_k \log(p_k)LCE​=−k=1∑K​yk​log(pk​)

Because yk=0y_k = 0yk​=0 for all non-target classes and yc=1y_c = 1yc​=1 for the target, every term except the correct class multiplies by zero. Cross-entropy collapses to the Negative Log-Likelihood (NLL) of the true target class:

L=−log⁡(pc)L = -\log(p_c)L=−log(pc​)

For our incident note where c=docsc = \text{docs}c=docs:

L=−log⁡(0.114195…)≈2.170 natsL = -\log(0.114195\ldots) \approx 2.170\text{ nats}L=−log(0.114195…)≈2.170 nats

If the network had assigned 0.844 to docs, the loss would be only −log⁡(0.844)≈0.170-\log(0.844) \approx 0.170−log(0.844)≈0.170 nats. If it had placed 0.999 on docs, the loss would plummet to 0.0010.0010.001 nats.

The log-sum-exp formulation prevents underflow crashes

Computing p = softmax(z) and then calling math.log(p[c]) introduces a subtle failure mode. If a logit is very negative (for example, logits = [1000.0, 0.0, -1.0] with target docs), zdocs−m=0.0−1000.0=−1000.0z_{\text{docs}} - m = 0.0 - 1000.0 = -1000.0zdocs​−m=0.0−1000.0=−1000.0. In floating-point arithmetic, e−1000e^{-1000}e−1000 underflows to 0.0. Calling log(0.0) produces -inf, crashing training.

The loss can be expressed directly in terms of raw logits, bypassing intermediate probabilities entirely.[1]Reference 1Deep Learning.https://www.deeplearningbook.org/ Expanding pcp_cpc​:

L=−log⁡(ezc∑jezj)=log⁡(∑j=1Kezj)−zcL = -\log\left(\frac{e^{z_c}}{\sum_j e^{z_j}}\right) = \log\left(\sum_{j=1}^K e^{z_j}\right) - z_cL=−log(∑j​ezj​ezc​​)=log(j=1∑K​ezj​)−zc​

The first term is log-sum-exp. We stabilize it using the same maximum shift m=max⁡j(zj)m = \max_j(z_j)m=maxj​(zj​):

log⁡(∑j=1Kezj)=log⁡(em∑j=1Kezj−m)=m+log⁡(∑j=1Kezj−m)\log\left(\sum_{j=1}^K e^{z_j}\right) = \log\left(e^m \sum_{j=1}^K e^{z_j - m}\right) = m + \log\left(\sum_{j=1}^K e^{z_j - m}\right)log(j=1∑K​ezj​)=log(emj=1∑K​ezj​−m)=m+log(j=1∑K​ezj​−m)

Substituting this back into the loss expression:

L=(m−zc)+log⁡(∑j=1Kezj−m)L = (m - z_c) + \log\left(\sum_{j=1}^K e^{z_j - m}\right)L=(m−zc​)+log(j=1∑K​ezj​−m)

Grouping (m−zc)(m - z_c)(m−zc​) first prevents catastrophic cancellation. When zc=0.0z_c = 0.0zc​=0.0 and m=1000.0m = 1000.0m=1000.0, the first term evaluates cleanly to 1000.01000.01000.0, while the log-sum evaluates to log⁡(1.0+… )≈0.0\log(1.0 + \dots) \approx 0.0log(1.0+…)≈0.0. The loss returns 1000.01000.01000.0 without ever taking the logarithm of zero.

cross-entropy-from-logits.py
1import math 2 3def cross_entropy_from_logits(logits: list[float], target_index: int) -> float: 4 peak = max(logits) 5 log_total = math.log(sum(math.exp(z - peak) for z in logits)) 6 return (peak - logits[target_index]) + log_total 7 8ordinary = [3.0, 1.0, 0.0] 9shifted_high = [z + 1000.0 for z in ordinary] 10 11print("ordinary docs loss:", round(cross_entropy_from_logits(ordinary, 1), 3)) 12print("large-offset loss: ", round(cross_entropy_from_logits(shifted_high, 1), 3)) 13print("underflowed target weight:", math.exp(-1000.0)) 14print("finite target loss:", cross_entropy_from_logits([1000.0, 0.0, -1.0], 1)) 15print("equal huge scores:", round(cross_entropy_from_logits([1e20, 1e20, 1e20], 1), 3)) 16assert math.isclose(cross_entropy_from_logits([1e20] * 3, 1), math.log(3))
Output
1ordinary docs loss: 2.17 2large-offset loss: 2.17 3underflowed target weight: 0.0 4finite target loss: 1000.0 5equal huge scores: 1.099

Another sample gets 0.80 probability on its correct class. Which sample contributes more loss: that sample or our docs sample at 0.114?

Answer

Our docs sample contributes far more loss. With its unrounded probability, −log⁡(0.114)-\log(0.114)−log(0.114) is roughly 2.1702.1702.170 nats, while −log⁡(0.80)-\log(0.80)−log(0.80) is only 0.2230.2230.223 nats. Lower target probabilities create exponentially harsher penalties.

Entropy is reference uncertainty and KL divergence is prediction error

Deepen the distinction between entropy, cross-entropy, and Kullback-Leibler (KL) divergence.

Entropy H(P)H(P)H(P) measures the inherent uncertainty already present inside a reference probability distribution PPP:

H(P)=−∑kP(k)log⁡P(k)H(P) = -\sum_k P(k) \log P(k)H(P)=−k∑​P(k)logP(k)

Cross-entropy H(P,Q)H(P, Q)H(P,Q) measures the total cost of representing distribution PPP using model distribution QQQ. Kullback-Leibler divergence DKL(P∥Q)D_{\text{KL}}(P \parallel Q)DKL​(P∥Q) measures the extra penalty caused specifically by the mismatch between QQQ and PPP:

H(P,Q)=H(P)+DKL(P∥Q)H(P, Q) = H(P) + D_{\text{KL}}(P \parallel Q)H(P,Q)=H(P)+DKL​(P∥Q)

For our one-hot docs target, P=[0,1,0]P = [0, 1, 0]P=[0,1,0] has zero entropy (H(P)=0H(P) = 0H(P)=0): the supervised answer is completely deterministic. Every fraction of a nat in our 2.1702.1702.170 loss comes from model error (DKL=2.170D_{\text{KL}} = 2.170DKL​=2.170).

When targets are soft (such as in distillation or human annotations with disagreement, like P=[0.5,0.5,0]P = [0.5, 0.5, 0]P=[0.5,0.5,0]), the reference distribution has inherent entropy H(P)=ln⁡(2)≈0.693H(P) = \ln(2) \approx 0.693H(P)=ln(2)≈0.693 nats. Even an ideal model (Q=PQ = PQ=P) will report a loss of 0.6930.6930.693 nats, with DKL=0D_{\text{KL}} = 0DKL​=0. Don't mistake inherent target uncertainty for model failure.

entropy-cross-entropy-kl.py
1from math import exp, log 2 3def entropy(probabilities: list[float]) -> float: 4 return sum(-p * log(p) for p in probabilities if p > 0) 5 6def cross_entropy(target: list[float], predicted: list[float]) -> float: 7 return -sum(p * log(q) for p, q in zip(target, predicted) if p > 0) 8 9weights = [exp(value) for value in [3.0, 1.0, 0.0]] 10predicted = [weight / sum(weights) for weight in weights] 11one_hot_target = [0.0, 1.0, 0.0] 12uncertain_target = [0.5, 0.5, 0.0] 13 14one_hot_loss = cross_entropy(one_hot_target, predicted) 15matched_loss = cross_entropy(uncertain_target, uncertain_target) 16 17print(f"one-hot target entropy: {entropy(one_hot_target):.3f}") 18print(f"one-hot cross-entropy: {one_hot_loss:.3f}") 19print(f"one-hot KL mismatch: {one_hot_loss - entropy(one_hot_target):.3f}") 20print(f"soft target entropy: {entropy(uncertain_target):.3f}") 21print(f"matching cross-entropy: {matched_loss:.3f}") 22print(f"matching KL mismatch: {matched_loss - entropy(uncertain_target):.3f}")
Output
1one-hot target entropy: 0.000 2one-hot cross-entropy: 2.170 3one-hot KL mismatch: 2.170 4soft target entropy: 0.693 5matching cross-entropy: 0.693 6matching KL mismatch: 0.000

A non-zero loss doesn't mean the model is broken if the labels themselves carry irreducible noise. The decomposition separates target entropy from model prediction error.

The combined gradient reveals a clean error signal

Loss tells us how poorly the model performed. To update weights, backpropagation needs the gradient: the partial derivative of the loss with respect to each logit, ∂L∂zk\frac{\partial L}{\partial z_k}∂zk​∂L​.

Softmax and cross-entropy create one of the cleanest gradient simplifications in machine learning.[1]Reference 1Deep Learning.https://www.deeplearningbook.org/

Recall the loss in terms of raw logits:

L=log⁡(∑j=1Kezj)−zcL = \log\left(\sum_{j=1}^K e^{z_j}\right) - z_cL=log(j=1∑K​ezj​)−zc​

Differentiate with respect to any logit zkz_kzk​:

∂L∂zk=∂∂zklog⁡(∑j=1Kezj)−∂zc∂zk\frac{\partial L}{\partial z_k} = \frac{\partial}{\partial z_k} \log\left(\sum_{j=1}^K e^{z_j}\right) - \frac{\partial z_c}{\partial z_k}∂zk​∂L​=∂zk​∂​log(j=1∑K​ezj​)−∂zk​∂zc​​

By the chain rule, the derivative of log⁡(u)\log(u)log(u) is 1u∂u∂zk\frac{1}{u} \frac{\partial u}{\partial z_k}u1​∂zk​∂u​. With u=∑jezju = \sum_j e^{z_j}u=∑j​ezj​:

∂∂zklog⁡(∑j=1Kezj)=1∑j=1Kezj⋅∂∂zkezk=ezk∑j=1Kezj=pk\frac{\partial}{\partial z_k} \log\left(\sum_{j=1}^K e^{z_j}\right) = \frac{1}{\sum_{j=1}^K e^{z_j}} \cdot \frac{\partial}{\partial z_k} e^{z_k} = \frac{e^{z_k}}{\sum_{j=1}^K e^{z_j}} = p_k∂zk​∂​log(j=1∑K​ezj​)=∑j=1K​ezj​1​⋅∂zk​∂​ezk​=∑j=1K​ezj​ezk​​=pk​

The derivative of the log-sum-exp term is softmax itself.

Meanwhile, the second term ∂zc∂zk\frac{\partial z_c}{\partial z_k}∂zk​∂zc​​ equals 111 if k=ck = ck=c (the target class) and 000 otherwise. That is exactly the target weight yky_kyk​:

∂L∂zk=pk−yk\frac{\partial L}{\partial z_k} = p_k - y_k∂zk​∂L​=pk​−yk​

Vectorized across all classes:

∇zL=p−y\nabla_z L = p - y∇z​L=p−y

Predicted probability minus true target. Look at the numbers for our triage example:

LabelPredicted probability pppTarget weight yyyLogit gradient p−yp - yp−yGradient descent update −η(p−y)-\eta(p - y)−η(p−y)
bug0.8440+0.844Lowers wrong favorite
docs0.1141-0.886Raises correct target
security0.0420+0.042Lowers minor competitor

The zero-sum conservation property

Sum all elements of the gradient vector:

∑k=1K∂L∂zk=∑k=1K(pk−yk)=∑k=1Kpk−∑k=1Kyk=1.0−1.0=0\sum_{k=1}^K \frac{\partial L}{\partial z_k} = \sum_{k=1}^K (p_k - y_k) = \sum_{k=1}^K p_k - \sum_{k=1}^K y_k = 1.0 - 1.0 = 0k=1∑K​∂zk​∂L​=k=1∑K​(pk​−yk​)=k=1∑K​pk​−k=1∑K​yk​=1.0−1.0=0

The gradients sum to zero. The upward pull on the correct class (−0.886-0.886−0.886) is exactly balanced by the downward push on competing classes (+0.844+0.042=+0.886+0.844 + 0.042 = +0.886+0.844+0.042=+0.886). Softmax distributes a fixed unit of probability mass; learning to favor one class inevitably pulls mass away from others.

Three probability number lines join the one-hot target to the prediction, labeling p minus y: bug plus 0.844, docs minus 0.886, and security plus 0.042. Direct gradient descent lowers bug and security logits while raising docs.
Each segment joins target y to prediction p; the labeled difference is the gradient p minus y. A direct gradient-descent step subtracts η times the gradient, raising docs and lowering its competitors in an exact zero-sum balance.

In a complete neural network, backpropagation channels this p−yp - yp−y residual back to shared weights via the chain rule. Testing on logits directly makes the local mechanic transparent.

one-logit-signal-step.py
1import math 2 3def probabilities_and_loss(logits: list[float], target_index: int) -> tuple[list[float], float]: 4 peak = max(logits) 5 shifted = [z - peak for z in logits] 6 log_total = math.log(sum(math.exp(value) for value in shifted)) 7 log_probs = [value - log_total for value in shifted] 8 probabilities = [math.exp(value) for value in log_probs] 9 return probabilities, -log_probs[target_index] 10 11labels = ["bug", "docs", "security"] 12logits = [3.0, 1.0, 0.0] 13target_index = labels.index("docs") 14target = [1.0 if index == target_index else 0.0 for index in range(3)] 15 16before, before_loss = probabilities_and_loss(logits, target_index) 17gradient = [probability - label for probability, label in zip(before, target)] 18after_logits = [z - 0.5 * slope for z, slope in zip(logits, gradient)] 19after, after_loss = probabilities_and_loss(after_logits, target_index) 20 21print("gradient:", [round(value, 3) for value in gradient]) 22print("docs probability:", round(before[1], 3), "->", round(after[1], 3)) 23print("loss:", round(before_loss, 3), "->", round(after_loss, 3))
Output
1gradient: [0.844, -0.886, 0.042] 2docs probability: 0.114 -> 0.23 3loss: 2.17 -> 1.469

Confirm the mathematical slope against numerical finite differences: nudge each logit by ϵ=10−5\epsilon = 10^{-5}ϵ=10−5 and check the symmetric difference quotient.

verify-cross-entropy-gradient.py
1import math 2 3def loss(logits: list[float], target_index: int) -> float: 4 peak = max(logits) 5 shifted = [z - peak for z in logits] 6 return math.log(sum(math.exp(value) for value in shifted)) - shifted[target_index] 7 8logits = [3.0, 1.0, 0.0] 9target_index = 1 10peak = max(logits) 11shifted = [z - peak for z in logits] 12total = sum(math.exp(value) for value in shifted) 13probabilities = [math.exp(value) / total for value in shifted] 14analytic = [ 15 probability - (1.0 if index == target_index else 0.0) 16 for index, probability in enumerate(probabilities) 17] 18 19epsilon = 1e-5 20numeric = [] 21for index in range(3): 22 plus = logits.copy() 23 minus = logits.copy() 24 plus[index] += epsilon 25 minus[index] -= epsilon 26 slope = (loss(plus, target_index) - loss(minus, target_index)) / (2 * epsilon) 27 numeric.append(slope) 28 29print("analytic:", [round(value, 6) for value in analytic]) 30print("numeric: ", [round(value, 6) for value in numeric]) 31print("match:", all(math.isclose(left, right, abs_tol=1e-6) for left, right in zip(analytic, numeric)))
Output
1analytic: [0.843795, -0.885805, 0.04201] 2numeric: [0.843795, -0.885805, 0.04201] 3match: True

Why squared probability error saturates and stalls

Why not use Mean Squared Error (MSE) on predicted probabilities: LMSE=12∑k(pk−yk)2L_{\text{MSE}} = \frac{1}{2} \sum_k (p_k - y_k)^2LMSE​=21​∑k​(pk​−yk​)2?

To see why MSE fails for classification, examine how gradients flow through the softmax Jacobian matrix:

∂pi∂zj=pi(δij−pj)\frac{\partial p_i}{\partial z_j} = p_i (\delta_{ij} - p_j)∂zj​∂pi​​=pi​(δij​−pj​)

When differentiating cross-entropy LCE=−∑iyilog⁡(pi)L_{\text{CE}} = -\sum_i y_i \log(p_i)LCE​=−∑i​yi​log(pi​) via the multivariate chain rule:

∂LCE∂zj=∑i∂L∂pi∂pi∂zj=∑i(−yipi)⋅pi(δij−pj)=−∑iyi(δij−pj)=pj−yj\frac{\partial L_{\text{CE}}}{\partial z_j} = \sum_i \frac{\partial L}{\partial p_i} \frac{\partial p_i}{\partial z_j} = \sum_i \left(-\frac{y_i}{p_i}\right) \cdot p_i (\delta_{ij} - p_j) = -\sum_i y_i (\delta_{ij} - p_j) = p_j - y_j∂zj​∂LCE​​=i∑​∂pi​∂L​∂zj​∂pi​​=i∑​(−pi​yi​​)⋅pi​(δij​−pj​)=−i∑​yi​(δij​−pj​)=pj​−yj​

The factor −yipi-\frac{y_i}{p_i}−pi​yi​​ has pip_ipi​ in the denominator, which cancels the pip_ipi​ in the softmax Jacobian numerator.

Under MSE, the derivative with respect to probability is ∂LMSE∂pi=(pi−yi)\frac{\partial L_{\text{MSE}}}{\partial p_i} = (p_i - y_i)∂pi​∂LMSE​​=(pi​−yi​). There is no pip_ipi​ denominator to cancel the softmax Jacobian:

∂LMSE∂zj=∑i(pi−yi)pi(δij−pj)\frac{\partial L_{\text{MSE}}}{\partial z_j} = \sum_i (p_i - y_i) p_i (\delta_{ij} - p_j)∂zj​∂LMSE​​=i∑​(pi​−yi​)pi​(δij​−pj​)

When the model is confidently wrong (for instance, pbug=0.9993p_{\text{bug}} = 0.9993pbug​=0.9993 and pdocs=0.0003p_{\text{docs}} = 0.0003pdocs​=0.0003), the softmax derivative term pi(1−pi)≈0.0003p_i(1 - p_i) \approx 0.0003pi​(1−pi​)≈0.0003 vanishes. The gradient flattens out, shrinking the correction by three orders of magnitude. The network enters a saturated plateau and stops learning.

With cross-entropy, the gradient on the target logit is pdocs−1=0.0003−1.0=−0.9997p_{\text{docs}} - 1 = 0.0003 - 1.0 = -0.9997pdocs​−1=0.0003−1.0=−0.9997, delivering a massive, full-strength corrective kick.

confidently-wrong-gradients.py
1import math 2 3def softmax(logits: list[float]) -> list[float]: 4 peak = max(logits) 5 weights = [math.exp(z - peak) for z in logits] 6 total = sum(weights) 7 return [weight / total for weight in weights] 8 9def squared_probability_loss(logits: list[float], target: list[float]) -> float: 10 error = [probability - label for probability, label in zip(softmax(logits), target)] 11 return 0.5 * sum(value * value for value in error) 12 13logits = [8.0, 0.0, 0.0] 14target = [0.0, 1.0, 0.0] 15probabilities = softmax(logits) 16ce_gradient = [probability - label for probability, label in zip(probabilities, target)] 17 18epsilon = 1e-5 19mse_gradient = [] 20for index in range(3): 21 plus = logits.copy() 22 minus = logits.copy() 23 plus[index] += epsilon 24 minus[index] -= epsilon 25 slope = ( 26 squared_probability_loss(plus, target) 27 - squared_probability_loss(minus, target) 28 ) / (2 * epsilon) 29 mse_gradient.append(slope) 30 31print("probabilities:", [round(value, 4) for value in probabilities]) 32print("cross-entropy gradient:", [round(value, 4) for value in ce_gradient]) 33print("squared-probability gradient:", [round(value, 4) for value in mse_gradient])
Output
1probabilities: [0.9993, 0.0003, 0.0003] 2cross-entropy gradient: [0.9993, -0.9997, 0.0003] 3squared-probability gradient: [0.001, -0.0007, -0.0003]

The numerical gap is striking: cross-entropy provides an aggressive gradient around −1.0-1.0−1.0, while squared probability error stalls with a feeble signal around −0.0007-0.0007−0.0007.

PyTorch expects unnormalized logits directly

In PyTorch, nn.CrossEntropyLoss expects unnormalized logits, not probabilities.[2]Reference 2CrossEntropyLosshttps://docs.pytorch.org/docs/stable/generated/torch.nn.CrossEntropyLoss.html

Internally, PyTorch fuses log_softmax with negative log-likelihood (NLLLoss). Fusing them performs the stable log-sum-exp trick in a single optimized GPU kernel and avoids materializing intermediate probabilities in memory.

If you mistakenly insert an nn.Softmax() activation before nn.CrossEntropyLoss, PyTorch treats those already-normalized probabilities as logits. It applies log-softmax a second time: log⁡(softmax(p))−pc\log(\text{softmax}(p)) - p_clog(softmax(p))−pc​. The code won't crash because tensor shapes match, but the objective function is corrupted and gradients shrink drastically.

pytorch-cross-entropy.py
1import torch 2from torch import nn 3 4logits = torch.tensor([[3.0, 1.0, 0.0]], requires_grad=True) 5target = torch.tensor([1]) # docs 6loss_fn = nn.CrossEntropyLoss() 7 8loss = loss_fn(logits, target) 9loss.backward() 10probabilities = torch.softmax(logits.detach(), dim=1) 11 12print("probabilities:", [round(value, 3) for value in probabilities[0].tolist()]) 13print("loss:", round(loss.item(), 3)) 14print("gradient:", [round(value, 3) for value in logits.grad[0].tolist()])
Output
1probabilities: [0.844, 0.114, 0.042] 2loss: 2.17 3gradient: [0.844, -0.886, 0.042]

Now reproduce the common bug: softmax first, then CrossEntropyLoss. The shapes match, so Python raises no warning. Compare the target gradients.

do-not-softmax-before-cross-entropy.py
1import torch 2from torch import nn 3 4target = torch.tensor([1]) # docs 5loss_fn = nn.CrossEntropyLoss() 6 7correct_input = torch.tensor([[3.0, 1.0, 0.0]], requires_grad=True) 8correct_loss = loss_fn(correct_input, target) 9correct_loss.backward() 10 11wrong_input = torch.tensor([[3.0, 1.0, 0.0]], requires_grad=True) 12wrong_loss = loss_fn(torch.softmax(wrong_input, dim=1), target) 13wrong_loss.backward() 14 15print("raw logits loss:", round(correct_loss.item(), 3)) 16print("probabilities passed as logits:", round(wrong_loss.item(), 3)) 17print("correct docs gradient:", round(correct_input.grad[0, 1].item(), 3)) 18print("distorted docs gradient:", round(wrong_input.grad[0, 1].item(), 3))
Output
1raw logits loss: 2.17 2probabilities passed as logits: 1.387 3correct docs gradient: -0.886 4distorted docs gradient: -0.127

The distorted gradient reveals the defect: the corrective gradient for docs shrank from −0.886-0.886−0.886 to −0.127-0.127−0.127. Leave your final classification layer linear during training, and feed raw logits directly into nn.CrossEntropyLoss. Apply softmax only during inference when a client or caller explicitly requests probabilities.

Temperature scaling shapes inference entropy

During training, we optimize the network at temperature T=1.0T = 1.0T=1.0. During inference or knowledge distillation, scaling logits by a positive temperature parameter T>0T > 0T>0 alters distribution entropy:

pi(T)=ezi/T∑j=1Kezj/Tp_i(T) = \frac{e^{z_i / T}}{\sum_{j=1}^K e^{z_j / T}}pi​(T)=∑j=1K​ezj​/Tezi​/T​

Dividing logits by TTT changes how sharply probabilities concentrate:

  • When T<1.0T < 1.0T<1.0, logit differences expand. The leading class absorbs more probability mass. As T→0T \to 0T→0, softmax approaches an argmax one-hot distribution (greedy selection).
  • When T>1.0T > 1.0T>1.0, logit differences shrink. Mass spreads more evenly across competitors. As T→∞T \to \inftyT→∞, the distribution approaches uniform uncertainty (1/K1/K1/K).

Temperature doesn't alter class ranking: the highest logit before scaling remains the highest logit after scaling. Temperature is a decoding and calibration control, not a parameter update.

In knowledge distillation, Hinton et al. use high temperature (T=3.0T = 3.0T=3.0 to 5.05.05.0) to reveal soft relationships between non-target classes (so-called "dark knowledge").[3]Reference 3Distilling the Knowledge in a Neural Network.https://arxiv.org/abs/1503.02531 In large language model serving, temperature modulates sampling diversity.

Temperaturebug (z=3z=3z=3)docs (z=1z=1z=1)security (z=0z=0z=0)Interpretation
0.50.9800.0180.002Sharpened distribution; wrong winner reinforced
1.00.8440.1140.042Original learned model distribution
2.00.6290.2310.140Flattened distribution; alternatives gain mass
temperature-scales-logits.py
1import math 2 3def softmax_at_temperature(logits: list[float], temperature: float) -> list[float]: 4 if not math.isfinite(temperature) or temperature <= 0: 5 raise ValueError("temperature must be finite and positive") 6 scaled = [z / temperature for z in logits] 7 peak = max(scaled) 8 weights = [math.exp(z - peak) for z in scaled] 9 total = sum(weights) 10 return [weight / total for weight in weights] 11 12logits = [3.0, 1.0, 0.0] 13for temperature in (0.5, 1.0, 2.0): 14 probabilities = softmax_at_temperature(logits, temperature) 15 print(f"T={temperature:.1f}", [round(probability, 3) for probability in probabilities])
Output
1T=0.5 [0.98, 0.018, 0.002] 2T=1.0 [0.844, 0.114, 0.042] 3T=2.0 [0.629, 0.231, 0.14]

If docs is correct but bug is currently the highest logit, does lowering temperature repair the model?

Answer

No. Lowering temperature sharpens the current ranking, strengthening the mistaken bug preference to 98%. Training changes logits by following loss gradients; temperature only rescales probabilities generated from current logits.

Label smoothing curbs overconfident logits

Training models on hard one-hot targets introduces a structural hazard: overconfidence.

With a hard target ydocs=1.0y_{\text{docs}} = 1.0ydocs​=1.0, minimizing cross-entropy loss L=−log⁡(pdocs)L = -\log(p_{\text{docs}})L=−log(pdocs​) requires driving pdocs→1.0p_{\text{docs}} \to 1.0pdocs​→1.0. In softmax, pdocs→1.0p_{\text{docs}} \to 1.0pdocs​→1.0 only occurs when:

zdocs−zk→+∞for all k≠docsz_{\text{docs}} - z_k \to +\infty \quad \text{for all } k \ne \text{docs}zdocs​−zk​→+∞for all k=docs

Because logits are linear combinations of weights and activations, the optimizer continuously drives weight norms to grow larger and larger. The network becomes dogmatically overconfident, degrades its calibration, and memorizes noise in the training labels.

Label smoothing resolves this by softening the hard target distribution.[4]Reference 4Attention Is All You Need.https://arxiv.org/abs/1706.03762 Given smoothing parameter α∈(0,1)\alpha \in (0, 1)α∈(0,1) (typically α=0.1\alpha = 0.1α=0.1) and KKK classes:

y~k=(1−α)yk+αK\tilde{y}_k = (1 - \alpha) y_k + \frac{\alpha}{K}y~​k​=(1−α)yk​+Kα​

For our 3-class triage problem with target docs (c=1c=1c=1), α=0.1\alpha = 0.1α=0.1, and K=3K = 3K=3:

  • Target class: y~docs=(1−0.1)⋅1.0+0.13=0.900+0.0333=0.9333\tilde{y}_{\text{docs}} = (1 - 0.1) \cdot 1.0 + \frac{0.1}{3} = 0.900 + 0.0333 = 0.9333y~​docs​=(1−0.1)⋅1.0+30.1​=0.900+0.0333=0.9333
  • Competitor classes: y~bug=(1−0.1)⋅0.0+0.13=0.0333\tilde{y}_{\text{bug}} = (1 - 0.1) \cdot 0.0 + \frac{0.1}{3} = 0.0333y~​bug​=(1−0.1)⋅0.0+30.1​=0.0333
  • Competitor classes: y~security=0.0333\tilde{y}_{\text{security}} = 0.0333y~​security​=0.0333

What logit gap does the model need now to achieve zero gradient? Instead of an infinite score difference, the target probability reaches equilibrium when:

pdocspcompetitor=0.93330.0333=28  ⟹  zdocs−zcompetitor=ln⁡(28)≈3.332\frac{p_{\text{docs}}}{p_{\text{competitor}}} = \frac{0.9333}{0.0333} = 28 \implies z_{\text{docs}} - z_{\text{competitor}} = \ln(28) \approx 3.332pcompetitor​pdocs​​=0.03330.9333​=28⟹zdocs​−zcompetitor​=ln(28)≈3.332

The model only needs a modest, finite gap of ≈3.33\approx 3.33≈3.33 logits. If weights push zdocsz_{\text{docs}}zdocs​ higher so that pdocs>0.933p_{\text{docs}} > 0.933pdocs​>0.933, the gradient pk−y~kp_k - \tilde{y}_kpk​−y~​k​ becomes positive, pushing the target logit back down. Label smoothing acts as an automatic brake on runaway weights.

PyTorch provides built-in label smoothing via nn.CrossEntropyLoss(label_smoothing=0.1).

label-smoothing-cross-entropy.py
1import torch 2from torch import nn 3 4logits = torch.tensor([[3.0, 1.0, 0.0]], requires_grad=True) 5target = torch.tensor([1]) # docs 6alpha = 0.1 7num_classes = 3 8 9loss_fn = nn.CrossEntropyLoss(label_smoothing=alpha) 10loss = loss_fn(logits, target) 11loss.backward() 12 13probs = torch.softmax(logits.detach(), dim=1)[0] 14smoothed_target = torch.tensor([ 15 alpha / num_classes, 16 (1.0 - alpha) + (alpha / num_classes), 17 alpha / num_classes, 18]) 19manual_loss = -torch.sum(smoothed_target * torch.log(probs)) 20grad_manual = probs - smoothed_target 21 22print("probabilities: ", [round(x, 3) for x in probs.tolist()]) 23print("smoothed target: ", [round(x, 3) for x in smoothed_target.tolist()]) 24print("pytorch loss: ", round(loss.item(), 3)) 25print("manual loss: ", round(manual_loss.item(), 3)) 26print("logit gradient: ", [round(x, 3) for x in logits.grad[0].tolist()]) 27print("manual gradient: ", [round(x, 3) for x in grad_manual.tolist()])
Output
1probabilities: [0.844, 0.114, 0.042] 2smoothed target: [0.033, 0.933, 0.033] 3pytorch loss: 2.137 4manual loss: 2.137 5logit gradient: [0.81, -0.819, 0.009] 6manual gradient: [0.81, -0.819, 0.009]

The output verifies the mathematical correspondence: PyTorch's label-smoothed loss matches manual NLL against y~\tilde{y}y~​ exactly, and the gradient equals p−y~p - \tilde{y}p−y~​.

Next-token prediction extends the loss across sequences

Our triage classifier evaluated one decision. Autoregressive language models evaluate the exact same categorical decision at every sequence position.

Given input context x1,x2,…,xtx_1, x_2, \dots, x_tx1​,x2​,…,xt​, a language model emits hidden states ht∈Rdh_t \in \mathbb{R}^dht​∈Rd. The language model head projects hth_tht​ against an embedding matrix to produce vocabulary logits zt∈R∣V∣z_t \in \mathbb{R}^{|V|}zt​∈R∣V∣, where ∣V∣|V|∣V∣ is vocabulary size. The observed next token xt+1x_{t+1}xt+1​ provides the supervised target.

For a sequence of length SSS, the sequence loss is the mean negative log-likelihood across all predicted positions:

Lseq=−1S∑t=1Slog⁡P(xt+1∣x1,…,xt)L_{\text{seq}} = -\frac{1}{S} \sum_{t=1}^S \log P(x_{t+1} \mid x_1, \dots, x_t)Lseq​=−S1​t=1∑S​logP(xt+1​∣x1​,…,xt​)

Reuse our running numbers with a tiny vocabulary: ["bug", "docs", "today"].

  • Position 1 predicts docs (c=1c=1c=1) from logits [3.0, 1.0, 0.0]. Target probability is 0.1140.1140.114, producing L1=2.170L_1 = 2.170L1​=2.170 nats.
  • Position 2 predicts today (c=2c=2c=2) from logits [0.5, 0.0, 2.0]. Target probability is 0.7360.7360.736, producing L2=0.306L_2 = 0.306L2​=0.306 nats.
  • Sequence mean loss: Lseq=2.170+0.3062=1.238L_{\text{seq}} = \frac{2.170 + 0.306}{2} = 1.238Lseq​=22.170+0.306​=1.238 nats.
Cross-entropy loss curve and sequence loss reduction: the negative log curve shows steep penalty L=2.170 for missed target docs (p=0.114) versus mild penalty L=0.306 for correct today (p=0.736), resulting in docs dominating 87.6 percent of the sequence loss and a mean loss of 1.238 nats.
The negative log penalty escalates rapidly when probability on the target drops toward zero. The missed docs token contributes 2.170 of the 2.476 total loss (about 87.6 percent), while correct today contributes only 0.306. Averaging over sequence positions reports a mean loss of 1.238 nats.

Notice the proportion: position 1 accounts for 2.170/2.476≈87.6%2.170 / 2.476 \approx 87.6\%2.170/2.476≈87.6% of the sequence loss.

Tensor contracts and vocabulary memory scaling

In PyTorch, a language model batch emits logits shaped [B, S, V] (batch size, sequence length, vocabulary size) and targets shaped [B, S]. PyTorch's multi-dimensional cross-entropy expects classes on dimension 1: [B, V, S]. Alternatively, reshape both into 2D tensors: [B * S, V] with targets [B * S].

In modern language models with vocabularies between 32k32\text{k}32k and 128k+128\text{k}+128k+ tokens (such as Llama 3 or GPT-4o), materializing full 3D logit tensors [B, S, V] creates severe VRAM bottlenecks. At batch size B=16B=16B=16, sequence length S=4096S=4096S=4096, and ∣V∣=128256|V|=128256∣V∣=128256, a float32 logit tensor requires over 33 GB of GPU memory just for the scores. Production pipelines use chunked cross-entropy or fused GPU kernels that stream hidden states in small tiles, computing log-sum-exp and gradients in SRAM without ever writing the full vocabulary matrix to global GPU memory.

sequence-loss-shapes.py
1import torch 2from torch import nn 3 4logits = torch.tensor([[[3.0, 1.0, 0.0], [0.5, 0.0, 2.0]]]) 5targets = torch.tensor([[1, 2]]) # docs, today 6loss_fn = nn.CrossEntropyLoss() 7 8class_axis_loss = loss_fn(logits.transpose(1, 2), targets) 9flattened_loss = loss_fn( 10 logits.reshape(-1, logits.size(-1)), 11 targets.reshape(-1), 12) 13 14print("logits shape:", tuple(logits.shape)) 15print("targets shape:", tuple(targets.shape)) 16print("class-axis loss:", round(class_axis_loss.item(), 3)) 17print("flattened loss:", round(flattened_loss.item(), 3))
Output
1logits shape: (1, 2, 3) 2targets shape: (1, 2) 3class-axis loss: 1.238 4flattened loss: 1.238

The same reduction can be implemented without PyTorch using standard library loops:

average-position-losses.py
1import math 2 3def per_position_cross_entropy(logits: list[list[float]], targets: list[int]) -> list[float]: 4 losses = [] 5 for row, target_index in zip(logits, targets): 6 peak = max(row) 7 log_total = math.log(sum(math.exp(z - peak) for z in row)) 8 losses.append((peak - row[target_index]) + log_total) 9 return losses 10 11logits = [[3.0, 1.0, 0.0], [0.5, 0.0, 2.0]] 12targets = [1, 2] 13losses = per_position_cross_entropy(logits, targets) 14 15print("position losses:", [round(value, 3) for value in losses]) 16print("mean loss:", round(sum(losses) / len(losses), 3))
Output
1position losses: [2.17, 0.306] 2mean loss: 1.238

Diagnostic failure modes in production pipelines

When classification or language modeling runs misbehave, trace errors systematically from arithmetic through interface contracts:

SymptomProbable causeDiagnostic check and remediation
Loss evaluates to NaN or OverflowError occursDirect logit exponentiation without max-subtractionSubtract m=max⁡(z)m = \max(z)m=max(z) before exponentiating; verify inputs don't already contain NaN
Loss outputs +inf on initial stepsCalling log(softmax(z)) when a target probability underflows to zeroUse log-sum-exp directly on logits: (m - z_c) + log(sum(exp(z - m)))
Plausible loss but slow or stalled convergenceManual softmax layer placed before nn.CrossEntropyLossPass unnormalized logits directly into nn.CrossEntropyLoss
Confidently wrong predictions don't correctLoss function switched to MSE on probabilitiesUse cross-entropy; verify logit gradients scale as p−yp - yp−y rather than saturated values
Runaway weights and extreme overconfidenceTraining with hard one-hot labels on noisy dataAdd label smoothing (label_smoothing=0.1) to cap logit divergence
Sampling generates garbled or uniform textInference temperature set too high (T≫1T \gg 1T≫1)Lower temperature (T∈[0.2,0.8]T \in [0.2, 0.8]T∈[0.2,0.8]) or use top-p nucleus sampling
Sequence loss hides severe per-token failuresMean reduction washes out individual token spikesLog unreduced per-token losses alongside sequence means

Diagnose issues in strict dependency order: numerical stability first, interface contract second, gradient sign third, decoding controls fourth, and sequence reduction last.

Try a second target

Experiment with the running example: switch the correct label to security (c=2c = 2c=2) while keeping logits [3.0, 1.0, 0.0].

Before running the code, calculate the expected loss and gradient by hand:

  • Target probability: psecurity≈0.042p_{\text{security}} \approx 0.042psecurity​≈0.042.
  • Loss: −log⁡(0.0420)≈3.170-\log(0.0420) \approx 3.170−log(0.0420)≈3.170 nats.
  • Gradient p−yp - yp−y:
    • bug: 0.844−0=+0.8440.844 - 0 = +0.8440.844−0=+0.844 (pushed down)
    • docs: 0.114−0=+0.1140.114 - 0 = +0.1140.114−0=+0.114 (pushed down)
    • security: 0.042−1.0=−0.9580.042 - 1.0 = -0.9580.042−1.0=−0.958 (pulled up strongly) Notice the zero-sum balance: +0.844+0.114−0.958=0.000+0.844 + 0.114 - 0.958 = 0.000+0.844+0.114−0.958=0.000. Subtracting η(p−y)\eta(p - y)η(p−y) raises the security logit while suppressing both competitors.

What values should the security-target experiment produce before the update?

Answer

The probabilities remain [0.844, 0.114, 0.042], because changing labels doesn't alter the forward pass. The loss jumps to approximately 3.170 nats. The gradient becomes [0.844, 0.114, -0.958]. Subtracting that gradient raises security rather than docs.

Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.A softmax receives logits [1000, 998, 997]. Why does subtracting the maximum before exponentiating preserve the mathematical distribution while preventing exponential overflow?

Correct answer: Subtracting one constant gives each exponential the same positive factor, which cancels between numerator and denominator while keeping the largest exponent at zero.

For any constant c, exp(z_i-c)=exp(z_i)exp(-c) and the common factor cancels in softmax. Choosing the maximum keeps all shifted values nonpositive, making exponentials safe.

2.For labels [bug, docs, security], probabilities are [0.844, 0.114, 0.042] and the target is docs. If logits are updated directly by subtracting a positive step times p-y, how do they move?

Correct answer: The high wrong bug logit moves down, the correct docs logit moves up, and security moves down slightly.

The gradient is positive for non-target classes and negative for the target. Subtracting it lowers wrong logits and raises the target, with the largest upward correction applied to the correct target.

3.Why should a PyTorch classifier pass raw logits, rather than probabilities after softmax, into nn.CrossEntropyLoss?

Correct answer: The loss combines a stable log-softmax with negative log likelihood; passing probabilities makes it normalize already transformed scores and changes gradients.

CrossEntropyLoss is designed for unnormalized class scores. Applying softmax first does not merely duplicate a harmless step: the loss treats the probability values as logits, changing the objective and often weakening learning.

4.Two next-token positions assign target probabilities 0.114 for docs and 0.736 for today. With mean cross-entropy, what is reported and which position matters more?

Correct answer: Mean loss is about 1.238; the docs position contributes more because -ln(0.114) is much larger than -ln(0.736).

The position losses are about 2.170 and 0.306; their mean is 1.238. Cross-entropy averages negative logs, so a low target probability can dominate the mean rather than being diluted by probability averaging.

5.A decoder emits logits [B,S,V] and integer targets [B,S], with one vocabulary distribution per position. Which layouts satisfy PyTorch's class-dimension contract?

Correct answer: Transpose logits to [B,V,S] with targets [B,S], or flatten to [B*S,V] with targets [B*S].

For K-dimensional CrossEntropyLoss input, classes occupy dimension one. The two-dimensional flattening form places one [V] vector beside each target and gives the same mean over positions.

6.With logits [3,1,0], the correct label is docs, and temperature changes from 1.0 to 0.5. What happens to the probability distribution?

Correct answer: The current ranking sharpens, raising the already leading bug probability from about 0.844 to about 0.980; temperature does not fix the label.

Temperature divides logits before softmax. A value below one magnifies existing differences, so the confident wrong class receives even more probability; it is a distribution control, not a learning correction.

7.A model exactly matches the soft target [0.5,0.5,0]. What are its cross-entropy and KL divergence, using natural logarithms?

Correct answer: Cross-entropy is about 0.693 and KL is zero, because the target retains uncertainty without prediction mismatch.

The target entropy is -0.5 ln(0.5) - 0.5 ln(0.5) = ln(2). Matching it leaves no KL mismatch, so cross-entropy equals this entropy. Classes with zero target weight contribute zero.

8.A target probability underflows when logits have very large gaps. Which expression is numerically stable and preserves the intended loss?

Correct answer: Use (m-z_y) + log(sum(exp(z_j-m))), where m = max(z), keeping exponentials bounded and cancelling the offset first.

The shifted expression is algebraically equal to log(sum exp(z)) - z_y but avoids large positive exponentials and avoids taking the log of an underflowed target probability. Replacing log-sum-exp with max changes the loss.

9.A classifier outputs [0.9993, 0.0003, 0.0003], but class two is correct. How do softmax cross-entropy and squared probability error differ here?

Correct answer: Cross-entropy supplies near-unit logit corrections through p-y, while squared probability error can have tiny gradients after the saturated softmax derivative matrix.

For cross-entropy, the wrong favorite's logit gradient is about +.9993 and the target's about -.9997. Squared error differentiates through a nearly saturated softmax, which can shrink its corrective signal.

10.Why does label smoothing with parameter alpha prevent logit weights from growing toward positive and negative infinity?

Correct answer: It replaces one-hot targets with a smoothed distribution, meaning zero loss gradient is attained at a finite logit difference rather than requiring infinite separation.

Hard targets require p_target -> 1.0, which needs infinite logit differences and drives weight growth. Label smoothing sets a target probability below 1.0, so the gradient p - y_tilde reaches zero at finite logit differences.

10 questions remaining.

Next Step
Continue to RNNs, LSTMs, GRUs, and Sequence Modeling

You can now turn competing logits into a stable loss and a correction signal at each supervised position. Next you'll build the first model in this path that carries ordered history forward, so those next-token logits can depend on what came before.

PreviousTraining & Backpropagation
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

Deep Learning.

Goodfellow, I., Bengio, Y., Courville, A. · 2016

https://www.deeplearningbook.org/

CrossEntropyLoss

PyTorch Contributors · 2026

https://docs.pytorch.org/docs/stable/generated/torch.nn.CrossEntropyLoss.html

Distilling the Knowledge in a Neural Network.

Hinton, G., Vinyals, O., & Dean, J. · 2015

https://arxiv.org/abs/1503.02531

Attention Is All You Need.

Vaswani, A., et al. · 2017

https://arxiv.org/abs/1706.03762

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.