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 177 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/8
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningStatistics 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/11
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageClustering and PCACore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingPyTorch 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/24
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernanceData 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/16
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 TuningReward 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/19
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceModel 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 & AutoscalingA/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.

13 min read
Learning path
Step 21 of 177 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.

In the previous lesson, a model predicted one number: next-token latency in milliseconds. Its loss could say "increase the prediction." A code-review classifier now faces a different job. Given a short review note, should it label the issue bug, docs, or security?

Suppose the evidence says docs is correct, but the model currently favors bug. We need a score for how bad that choice is, and a gradient that says which competing scores to raise or lower. Softmax turns raw scores into probability shares. Cross-entropy measures how little probability reached the correct choice. Together they form the standard categorical output-and-loss pair taught for neural classifiers and language models.[1]Reference 1Deep Learning.https://www.deeplearningbook.org/

A decision begins as three raw scores

The final layer of a classifier emits one number per possible label. These numbers are logits: unconstrained scores, not probabilities.

LabelCurrent logitWhat the review note says
bug3.0Model's current favorite, but wrong
docs1.0Correct supervised label
security0.0Possible, but not correct here

A larger logit means the model prefers a label relative to its competitors. It doesn't mean 3.0 is 300%, and a logit can be negative without creating a negative probability.

This first snippet makes the mistake visible: the scores select a favorite, but don't satisfy probability rules.

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
Two bar charts show logits 3, 1, and 0 becoming exponential weights 20.09, 2.72, and 1.00, then a stacked probability bar splits one unit into 84.4%, 11.4%, and 4.2%.
Exponentiation expands the score gaps into positive weights. Dividing every weight by the same `23.81` total preserves their proportions and packs them into one unit of probability mass.

Softmax assigns probability shares

Softmax takes every logit ziz_izi​, exponentiates it, and divides by the sum of all exponentiated logits:

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 label iii, ziz_izi​ is its raw logit, and KKK is the number of choices. Exponentiation makes each share positive. Dividing by their total makes the shares sum to one.

For our three class scores, the arithmetic is small enough to do by hand:

ActionLogit ziz_izi​ezie^{z_i}ezi​Probability pip_ipi​
bug3.020.090.844
docs1.02.720.114
security0.01.000.042
Total23.811.000

The model places only 0.114 probability on the correct docs label. That's the quantity the loss must expose.

Use NumPy to reproduce the table. This version is already stable because it subtracts the largest logit before taking exponentials.

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

The correct label is docs, but its probability is 0.114. Why can't the model fix this by raising only the displayed probability number after softmax?

Answer

The probabilities are consequences of the logits and compete for one unit of total mass. Learning changes parameters, which changes logits. Raising the docs logit relative to bug is what makes its probability rise on the next forward pass.

Stable arithmetic matters before scale does

Softmax depends on differences between logits, not on their absolute offset. Adding or subtracting the same constant from every score leaves every probability unchanged:

ezi−c∑jezj−c=ezie−ce−c∑jezj=ezi∑jezj\frac{e^{z_i-c}}{\sum_j e^{z_j-c}} = \frac{e^{z_i}e^{-c}}{e^{-c}\sum_j e^{z_j}} = \frac{e^{z_i}}{\sum_j e^{z_j}}∑j​ezj​−cezi​−c​=e−c∑j​ezj​ezi​e−c​=∑j​ezj​ezi​​

That cancellation gives us a safety rule: subtract the largest logit before exponentiating. The largest exponent becomes e0=1e^0 = 1e0=1, while the relative differences stay intact.

This failure case uses the same score gaps shifted upward by 997. Direct exponentiation overflows; max-shifting produces the original distribution.

overflow-then-stable.py
1import numpy as np 2 3def stable_softmax(logits: np.ndarray) -> np.ndarray: 4 shifted = logits - np.max(logits) 5 exp_shifted = np.exp(shifted) 6 return exp_shifted / exp_shifted.sum() 7 8large_logits = np.array([1000.0, 998.0, 997.0]) 9with np.errstate(over="ignore", invalid="ignore"): 10 naive = np.exp(large_logits) / np.exp(large_logits).sum() 11 12stable = stable_softmax(large_logits) 13print("naive finite:", np.isfinite(naive).all()) 14print("stable:", np.round(stable, 3)) 15print("stable sum:", round(float(stable.sum()), 3))
Output
1naive finite: False 2stable: [0.844 0.114 0.042] 3stable sum: 1.0

Cross-entropy grades the correct label

The supervised label for this example is docs. Written as a one-hot target vector in the same label order, it's:

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

One-hot means that exactly one class receives target weight one. Cross-entropy compares this target with the predicted probability vector:

L=−∑iyilog⁡(pi)L = -\sum_i y_i \log(p_i)L=−i∑​yi​log(pi​)

Every term except docs is multiplied by zero, so the loss reduces to:

L=−log⁡(pdocs)=−log⁡(0.114)≈2.170L = -\log(p_{\text{docs}}) = -\log(0.114) \approx 2.170L=−log(pdocs​)=−log(0.114)≈2.170

Had the label been the model's preferred bug, the loss would have been only −log⁡(0.844)≈0.170-\log(0.844) \approx 0.170−log(0.844)≈0.170. The loss is high because the supervised answer received little probability, not because the top choice merely happened to be wrong.

For numerical stability, compute that same cross-entropy straight from logits instead of first materializing a tiny probability. For a correct class index yyy:

L=log⁡(∑jezj)−zyL = \log\left(\sum_j e^{z_j}\right) - z_yL=log(j∑​ezj​)−zy​

The first term is called log-sum-exp. Its stable implementation uses the same maximum m=max⁡jzjm = \max_j z_jm=maxj​zj​ introduced for stable softmax:

log⁡(∑jezj)=m+log⁡(∑jezj−m)\log\left(\sum_j e^{z_j}\right) = m + \log\left(\sum_j e^{z_j-m}\right)log(j∑​ezj​)=m+log(j∑​ezj​−m)

This code computes the loss for the correct docs label directly from logits. It works for ordinary scores and extremely large offsets.

cross-entropy-from-logits.py
1import numpy as np 2 3def cross_entropy_from_logits(logits: np.ndarray, target_index: int) -> float: 4 maximum = np.max(logits) 5 logsumexp = maximum + np.log(np.exp(logits - maximum).sum()) 6 return float(logsumexp - logits[target_index]) 7 8ordinary = np.array([3.0, 1.0, 0.0]) 9shifted_high = ordinary + 1000.0 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))
Output
1ordinary docs loss: 2.17 2large-offset loss: 2.17

For a shorter arithmetic exercise, consider a second example with logits [1.0, 0.0, -2.0] in the same label order. bug still leads, but by a smaller margin. Compare the loss under two possible labels.

Now compute it:

compare-label-losses.py
1import numpy as np 2 3def stable_softmax(logits): 4 shifted = logits - np.max(logits) 5 exp_logits = np.exp(shifted) 6 return exp_logits / exp_logits.sum() 7 8logits = np.array([1.0, 0.0, -2.0]) # bug, docs, security 9probabilities = stable_softmax(logits) 10 11print("docs loss", round(float(-np.log(probabilities[1])), 3)) 12print("bug loss", round(float(-np.log(probabilities[0])), 3))
Output
1docs loss 1.349 2bug loss 0.349

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

Answer

Our docs example contributes more loss. Its loss is -log(0.114) = 2.170, while -log(0.80) = 0.223. A correct label assigned low probability should demand a larger correction.

Why squared probability error is a weak fit here

The earlier latency lesson used half squared error for a continuous target in milliseconds. We could attach softmax to a classifier and square its probability error, but the softmax derivative then shrinks the signal when a wrong class is already saturated near probability one. Cross-entropy paired with softmax avoids that extra shrinkage: its logit gradient will be p−yp-yp−y.[1]Reference 1Deep Learning.https://www.deeplearningbook.org/

This example makes the difference visible. The logits are extremely sure about the wrong bug label while the target is docs.

confidently-wrong-gradients.py
1import numpy as np 2 3def softmax(logits: np.ndarray) -> np.ndarray: 4 shifted = logits - np.max(logits) 5 exps = np.exp(shifted) 6 return exps / exps.sum() 7 8def squared_probability_loss(logits: np.ndarray, target: np.ndarray) -> float: 9 error = softmax(logits) - target 10 return float(0.5 * np.sum(error ** 2)) 11 12logits = np.array([8.0, 0.0, 0.0]) 13target = np.array([0.0, 1.0, 0.0]) 14probabilities = softmax(logits) 15ce_gradient = probabilities - target 16 17epsilon = 1e-5 18mse_gradient = np.array([ 19 (squared_probability_loss(logits + np.eye(3)[i] * epsilon, target) 20 - squared_probability_loss(logits - np.eye(3)[i] * epsilon, target)) 21 / (2 * epsilon) 22 for i in range(3) 23]) 24 25print("probabilities:", np.round(probabilities, 4)) 26print("cross-entropy gradient:", np.round(ce_gradient, 4)) 27print("squared-probability gradient:", np.round(mse_gradient, 4))
Output
1probabilities: [9.993e-01 3.000e-04 3.000e-04] 2cross-entropy gradient: [ 9.993e-01 -9.997e-01 3.000e-04] 3squared-probability gradient: [ 0.001 -0.0007 -0.0003]

Cross-entropy isn't the only possible classification objective, but it gives a direct categorical likelihood objective and a useful correction even when a wrong option dominates.

The gradient says exactly what to correct

For one labeled example, combine stable softmax with cross-entropy:

L=log⁡(∑jezj)−zyL = \log\left(\sum_j e^{z_j}\right) - z_yL=log(j∑​ezj​)−zy​

Differentiate with respect to any logit zkz_kzk​:

∂L∂zk=ezk∑jezj−1[k=y]=pk−yk\frac{\partial L}{\partial z_k} = \frac{e^{z_k}}{\sum_j e^{z_j}} - \mathbf{1}[k=y] = p_k - y_k∂zk​∂L​=∑j​ezj​ezk​​−1[k=y]=pk​−yk​

This derivative is commonly summarized component-wise as pi−yip_i - y_ipi​−yi​: predicted probability share minus target share.

The indicator 1[k=y]\mathbf{1}[k=y]1[k=y] equals one only for the correct class. For our docs example:

LabelProbability pppTarget yyyGradient p−yp-yp−yGradient descent effect
bug0.8440+0.844Lowers wrong favorite
docs0.1141-0.886Raises correct label
security0.0420+0.042Lowers competitor slightly
Three probability number lines show p minus y as an arrow from one-hot target to prediction: bug plus 0.844, docs minus 0.886, and security plus 0.042. An action column shows that gradient descent subtracts this signal, lowering bug and security logits while raising the correct docs logit.
Arrow direction shows the signed gradient `p − y`; SGD moves in the opposite direction by subtracting it. That lowers the high wrong `bug` logit, raises the low correct `docs` logit, and gives `security` only a small downward correction.

Logits themselves are outputs, not normally the parameters an optimizer stores. Updating them below is a diagnostic shortcut: it shows the direction that upstream weight updates are trying to produce on the next forward pass.

one-logit-signal-step.py
1import numpy as np 2 3def probabilities_and_loss(logits: np.ndarray, target_index: int): 4 shifted = logits - np.max(logits) 5 log_probs = shifted - np.log(np.exp(shifted).sum()) 6 return np.exp(log_probs), float(-log_probs[target_index]) 7 8labels = ["bug", "docs", "security"] 9logits = np.array([3.0, 1.0, 0.0]) 10target_index = labels.index("docs") 11target = np.eye(3)[target_index] 12 13before, before_loss = probabilities_and_loss(logits, target_index) 14gradient = before - target 15after_logits = logits - 0.5 * gradient 16after, after_loss = probabilities_and_loss(after_logits, target_index) 17 18print("gradient:", np.round(gradient, 3)) 19print("docs probability:", round(float(before[1]), 3), "->", round(float(after[1]), 3)) 20print("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

As in the previous lesson, a finite-difference test catches sign mistakes in a new loss implementation. Nudging each logit by a tiny amount should agree with p−yp-yp−y.

verify-cross-entropy-gradient.py
1import numpy as np 2 3def loss(logits: np.ndarray, target_index: int) -> float: 4 shifted = logits - np.max(logits) 5 return float(np.log(np.exp(shifted).sum()) - shifted[target_index]) 6 7logits = np.array([3.0, 1.0, 0.0]) 8target_index = 1 9shifted = logits - np.max(logits) 10probabilities = np.exp(shifted) / np.exp(shifted).sum() 11analytic = probabilities - np.eye(3)[target_index] 12 13epsilon = 1e-5 14numeric = np.array([ 15 (loss(logits + np.eye(3)[i] * epsilon, target_index) 16 - loss(logits - np.eye(3)[i] * epsilon, target_index)) 17 / (2 * epsilon) 18 for i in range(3) 19]) 20 21print("analytic:", np.round(analytic, 6)) 22print("numeric: ", np.round(numeric, 6)) 23print("match:", np.allclose(analytic, numeric, atol=1e-6))
Output
1analytic: [ 0.843795 -0.885805 0.04201 ] 2numeric: [ 0.843795 -0.885805 0.04201 ] 3match: True

PyTorch receives logits directly

Training code shouldn't implement this loss from scratch unless you're testing your understanding or building a custom variant. In PyTorch, nn.CrossEntropyLoss receives raw logits for class-index targets and computes the equivalent of log-softmax followed by negative log-likelihood internally. Passing already-softmaxed probabilities changes the function being optimized.[2]Reference 2Optimizing Model Parameters.https://docs.pytorch.org/tutorials/beginner/basics/optimization_tutorial.html

First, confirm that PyTorch returns the same loss and p−yp-yp−y gradient as our hand calculation:

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:", probabilities.numpy().round(3)) 13print("loss:", round(loss.item(), 3)) 14print("gradient:", logits.grad.numpy().round(3))
Output
1probabilities: [[0.844 0.114 0.042]] 2loss: 2.17 3gradient: [[ 0.844 -0.886 0.042]]

Now reproduce a common bug. The incorrect call passes probabilities where the loss expects logits, effectively normalizing an already normalized output again.

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

Shapes won't warn you about this bug. Keep the model's final classification layer linear during training and feed those raw logits into cross-entropy.

Temperature changes a distribution, not a label

Softmax can be made sharper or flatter by dividing logits by a positive temperature TTT:

pi(T)=ezi/T∑jezj/Tp_i(T) = \frac{e^{z_i/T}}{\sum_j e^{z_j/T}}pi​(T)=∑j​ezj​/Tezi​/T​

When TTT is below one, logit differences grow and the leading option takes more probability. When TTT is above one, differences shrink and more probability remains on alternatives. Hinton, Vinyals, and Dean use this operation to reveal softer class relationships during knowledge distillation.[3]Reference 3Distilling the Knowledge in a Neural Network.https://arxiv.org/abs/1503.02531 In text generation, the same mathematical scaling can shape the distribution a decoding policy samples from. Softmax still doesn't choose a token by itself.

TemperaturebugdocssecurityInterpretation
0.50.9800.0180.002Wrong favorite becomes harder to escape
1.00.8440.1140.042Original model distribution
2.00.6290.2310.140Alternatives receive more mass

Use the same stable function to check each distribution:

temperature-scales-logits.py
1import numpy as np 2 3def softmax_at_temperature(logits: np.ndarray, temperature: float) -> np.ndarray: 4 scaled = logits / temperature 5 shifted = scaled - np.max(scaled) 6 exponentials = np.exp(shifted) 7 return exponentials / exponentials.sum() 8 9logits = np.array([3.0, 1.0, 0.0]) 10for temperature in (0.5, 1.0, 2.0): 11 probabilities = softmax_at_temperature(logits, temperature) 12 print(f"T={temperature:.1f}", np.round(probabilities, 3))
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. Lower temperature sharpens the current ranking, so it makes the wrong bug preference even stronger. Training changes logits by following loss gradients; temperature only rescales the distribution produced from the current logits.

A sequence supplies many supervised positions

Our classifier example produced one loss. A language model trains on the same categorical idea many times at once: after each context position, it produces logits for the next token, and the observed next token supplies the class label. A sequence model still needs a mechanism for history, which is the question the next lesson will address.

To make the language-model version literal, use a tiny vocabulary: bug, docs, and today. Suppose a two-token reply should be docs today. At each supervised position, the model produces a new logit vector over that same vocabulary:

Supervised positionCorrect tokenProbability on tokenLoss
Reply token 1docs0.1142.170
Reply token 2today0.7360.306
Mean loss1.238
Bar chart comparing per-token cross-entropy losses 2.170 for missed docs and 0.306 for correct today, with an 88 to 12 contribution split and mean loss 1.238 feeding one backward pass.
The missed docs position contributes 2.170 of the 2.476 total loss, about 88%, while today contributes 0.306. Averaging gives 1.238, but the larger miss still dominates the shared gradient.

This final NumPy example applies the same stable loss across two supervised reply positions. Each row holds logits over the same tiny vocabulary; averaging yields the one scalar sent backward.

average-position-losses.py
1import numpy as np 2 3def per_position_cross_entropy(logits: np.ndarray, targets: np.ndarray) -> np.ndarray: 4 shifted = logits - np.max(logits, axis=1, keepdims=True) 5 log_probs = shifted - np.log(np.exp(shifted).sum(axis=1, keepdims=True)) 6 return -log_probs[np.arange(len(targets)), targets] 7 8# Vocabulary order: bug, docs, today. 9# Row 1 target is docs. Row 2 target is today. 10logits = np.array([[3.0, 1.0, 0.0], [0.5, 0.0, 2.0]]) 11targets = np.array([1, 2]) 12losses = per_position_cross_entropy(logits, targets) 13 14print("position losses:", np.round(losses, 3)) 15print("mean loss:", round(float(losses.mean()), 3))
Output
1position losses: [2.17 0.306] 2mean loss: 1.238

Debugging checklist

SymptomLikely causeCheck or fix
Loss becomes NaN in custom NumPy codeExponentiating large logits directlySubtract row maximum, then compute log-sum-exp
Training loss is plausible but gradients are weak or wrongProbabilities were passed into CrossEntropyLossPass raw logits into the loss
Model is confidently wrong on a classCorrect label has tiny softmax massInspect p−yp-yp−y and confirm correct logit receives negative gradient
Generation samples unwanted alternativesSampling distribution is too flatInspect temperature and decoding policy separately from training
Mean sequence loss hides one severe missMean reduction combines easy and hard positionsPrint per-position losses before averaging

Mastery check

Key concepts

  • Logits are unconstrained scores whose differences determine probability shares.
  • Stable softmax subtracts the maximum logit before exponentiation.
  • Cross-entropy with a class label is the negative log probability of that label.
  • Softmax plus cross-entropy produces the logit gradient p−yp-yp−y.
  • PyTorch categorical cross-entropy receives raw logits, not probabilities.
  • Temperature rescales logits; decoding decides how a token is selected.
  • Sequence training averages this same categorical loss across supervised positions.

Evaluation rubric

  • Foundational: Compute the stable softmax probabilities and cross-entropy loss for the docs example by hand.
  • Intermediate: Explain why max shifting leaves probabilities unchanged and diagnose an overflowing implementation.
  • Intermediate: Derive p−yp-yp−y and explain the sign of each label gradient when bug is wrongly preferred.
  • Advanced: Verify the gradient numerically, use the PyTorch interface correctly, and connect per-position loss to next-token sequence training.

Common pitfalls

  • Symptom: A logit is reported as a confidence percentage. Cause: Raw output scores were confused with normalized probabilities. Fix: Apply softmax for interpretation and treat calibration as a separate evaluation question.
  • Symptom: Custom loss returns NaN on large scores. Cause: Direct exponentiation overflowed. Fix: Compute max-shifted softmax or stable log-sum-exp.
  • Symptom: nn.CrossEntropyLoss runs but learns poorly. Cause: Model probabilities were passed as if they were logits. Fix: Pass the final linear layer's raw scores.
  • Symptom: Lower temperature reinforces an incorrect option. Cause: Temperature sharpened existing score order; it doesn't learn a correction. Fix: Use training gradients to repair logits and tune decoding separately.
  • Symptom: Mean sequence loss looks acceptable despite a serious wrong token. Cause: Easy token positions dilute one high-loss event. Fix: Inspect unreduced position losses during debugging.

Follow-up questions

Why does max-shifted softmax return the same probabilities as ordinary softmax?

Answer

Subtracting one constant from every logit multiplies every exponential by the same positive factor. That factor appears in both numerator and denominator, so it cancels while keeping the exponentials numerically safe.

For probabilities [0.844, 0.114, 0.042] and target docs, why is the gradient [0.844, -0.886, 0.042] useful?

Answer

Gradient descent subtracts the vector. It therefore lowers the high wrong bug logit, raises the low correct docs logit, and slightly lowers security. The largest corrections occur where the model is most mistaken.

Why should a PyTorch model omit softmax immediately before nn.CrossEntropyLoss?

Answer

The loss accepts raw logits and internally performs the stable log-softmax calculation needed for categorical loss. Feeding probabilities instead makes it normalize transformed scores again and changes the loss and gradients.

What new question remains after every next-token prediction has a score?

Answer

We still need a model whose next-token logits depend on earlier tokens in order. The next lesson introduces recurrent hidden state as the first sequence-memory mechanism before later transformer attention.

Complete the lesson

Mastery Check

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

1.A custom softmax over logits [1000.0, 998.0, 997.0] overflows, so you subtract the maximum before exponentiating and use [0.0, -2.0, -3.0]. Why should this produce the same probabilities as the original logits would in exact arithmetic?

Correct answer: Subtracting the same constant from every logit multiplies every exponential by the same positive factor, and that common factor cancels in the softmax ratio.

Softmax depends on logit differences, not the absolute offset. Replacing each z_i with z_i - c changes each exponential to e^{z_i}e^{-c}; the shared e^{-c} appears in both numerator and denominator. Choosing c as the maximum keeps the largest exponent at e^0 = 1 and avoids overflow without changing the distribution.

2.For label order [bug, docs, security], softmax gives probabilities [0.844, 0.114, 0.042] and the correct label is docs. What does the cross-entropy logit gradient [0.844, -0.886, 0.042] cause under gradient descent?

Correct answer: It lowers the high wrong bug logit, raises the low correct docs logit, and slightly lowers security.

For softmax plus cross-entropy, the derivative with respect to each logit is p_i - y_i. Gradient descent subtracts this derivative. Positive entries for non-target classes push those logits down, while subtracting the negative target entry pushes the docs logit up.

3.Why should a PyTorch classifier pass raw logits, not softmax probabilities, into nn.CrossEntropyLoss during training?

Correct answer: The loss internally performs the stable log-softmax calculation for class-index targets. Feeding probabilities instead normalizes transformed scores again and changes the loss and gradients.

nn.CrossEntropyLoss combines log-softmax with negative log-likelihood for raw scores. If the model applies softmax first, the loss treats those probabilities as logits and normalizes them again. This changes the objective and can substantially weaken the gradient rather than merely repeating an equivalent operation.

4.A tiny language-model training example has two supervised next-token positions. The target token probabilities are 0.114 for docs at position 1 and 0.736 for today at position 2. If the training loss is the mean cross-entropy over positions, what is reported and which position contributes more?

Correct answer: Mean loss is about 1.238, and the missed docs position contributes more because -log(0.114) is much larger than -log(0.736).

Each supervised position contributes the negative log probability of its target. The losses are approximately 2.170 and 0.306, so their mean is (2.170 + 0.306) / 2 = 1.238. The low-probability docs target dominates the average even though the reported scalar combines both positions.

5.With logits [3.0, 1.0, 0.0] in label order [bug, docs, security], bug is highest but the correct label is docs. If you lower the softmax temperature from 1.0 to 0.5, what happens?

Correct answer: It sharpens the current ranking, raising bug from about 0.844 to about 0.980; it doesn't learn a correction for docs.

Temperature divides logits before softmax. A temperature below one enlarges their differences, so the already leading bug option receives more probability. Temperature changes the distribution available to a decoding policy, but it doesn't change the label or train the model to raise docs.

6.For logits [3.0, 1.0, 0.0], the exponentials are approximately [20.09, 2.72, 1.00]. If docs is the second class, what are its softmax probability and cross-entropy loss?

Correct answer: 0.114 and about 2.170

The exponential total is 23.81, so the docs share is 2.72 / 23.81, or about 0.114. For a one-hot docs label, all other target terms are zero and cross-entropy reduces to -log(0.114), which is about 2.170.

7.A target probability underflows when cross-entropy is computed from very large logits. Which calculation preserves the intended loss most reliably?

Correct answer: Use max-shifted log-sum-exp: L = m + log(sum(exp(z_j-m))) - z_y.

Cross-entropy from logits is log(sum(exp(z_j))) - z_y. Taking m as the maximum and rewriting the first term as m + log(sum(exp(z_j-m))) keeps every exponential at most one while preserving the exact expression. Rounding, clipping, or replacing the log-sum with a maximum changes the objective.

8.A classifier gives probabilities [0.9993, 0.0003, 0.0003], but the second class is correct. How do cross-entropy and squared probability error differ in this confidently wrong case?

Correct answer: Cross-entropy gives near +1 and -1 gradients; squared error gives tiny gradients.

For softmax cross-entropy, the logit gradient is p-y, so the wrong favorite receives a gradient near +1 and the correct class receives one near -1. Squared probability error must also pass through the softmax derivative, which is very small near saturation, so its correction can shrink even when the prediction is badly wrong.

8 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 predictions 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/

Optimizing Model Parameters.

PyTorch Contributors · 2026 · Official tutorial

https://docs.pytorch.org/tutorials/beginner/basics/optimization_tutorial.html

Distilling the Knowledge in a Neural Network.

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

https://arxiv.org/abs/1503.02531

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.