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 & PrerequisitesNeural Networks from Scratch
🏛️EasyModel Architecture

Neural Networks from Scratch

Build a CodeAssist timeout-risk neural network from scratch: derive affine maps, prove why non-linearities prevent layer collapse, compare core activations, track batched tensor shapes, and implement backpropagation with NumPy and PyTorch autograd.

21 min read
Learning path
Step 19 of 196 in the full curriculum
Hypothesis Tests, Intervals, and pass@kCNNs from Scratch

Personalize this lesson

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

Suppose CodeAssist receives a code-completion request with a prompt-size score of 6 and an active queue-wait score of 8. A quiet request has values 2 and 3. We want a scoring function that flags the first request as high risk before worker threads time out. Raw numbers don't make an operational decision on their own, though. What computation should sit between those incoming features and a routing decision?

The vectors and matrices lesson represented incoming observations as feature vectors and combined them with dot products. A neural network builds directly on that linear foundation. Its forward pass evaluates an output from inputs using stored weight parameters and biases. We'll hold those weights fixed first so you can inspect every floating-point addition and multiplication by hand before training adjusts them.

One neuron scores a single request

Start with one scoring unit, or neuron. Multiply each input feature by its own weight, then add their contributions together. Both inputs use an illustrative zero-to-ten scale rather than raw token counts or seconds. Before reading the sum, check which feature contributes more:

FeatureValueChosen weightContribution
Prompt-size score60.74.2
Queue-wait score80.32.4

The contributions total 6.6. Now add a bias, an offset of -4.0, to get 2.6. This weighted sum plus bias is an affine map. A purely linear map satisfies f(αu+βv)=αf(u)+βf(v)f(\alpha u + \beta v) = \alpha f(u) + \beta f(v)f(αu+βv)=αf(u)+βf(v), which forces f(0)=0f(0) = 0f(0)=0. That would mean a request with zero prompt size and zero queue wait must always produce a score of zero. The bias shifts the hyperplane away from the origin so the model can set its own baseline threshold.

In vector notation, the feature vector xxx holds [6, 8], the weight vector www holds [0.7, 0.3], and b=−4.0b = -4.0b=−4.0:

z=w⊤x+b=0.7(6)+0.3(8)−4=2.6z = w^\top x + b = 0.7(6) + 0.3(8) - 4 = 2.6z=w⊤x+b=0.7(6)+0.3(8)−4=2.6

The superscript ⊤\top⊤ denotes a transpose, making w⊤xw^\top xw⊤x the dot product. An activation function transforms the resulting pre-activation zzz. For this binary risk scorer, pass zzz through the sigmoid function, which maps 2.6 to roughly 0.931:

σ(z)=11+e−z\sigma(z) = \frac{1}{1 + e^{-z}}σ(z)=1+e−z1​

Here eee is Euler's constant, roughly 2.718. Before entering sigmoid, zzz is called a logit. Hidden-layer pre-activations aren't generally logits because they don't feed directly into a logistic probability mapping. Sigmoid outputs stay strictly between zero and one for any finite input.

A bounded score isn't proof of a 93.1% timeout probability. These parameters were chosen by hand for illustration. A true probability estimate requires a calibrated statistical model trained on verified request outcomes.[1]Reference 1On Calibration of Modern Neural Networkshttps://arxiv.org/abs/1706.04599

Compute this scoring path once with Python's standard library. The snippet below prints the logit and sigmoid output separately so you can verify each arithmetic step:

one-neuron-score.py
1from math import exp 2 3prompt_size = 6.0 4queue_wait = 8.0 5w_prompt, w_queue, b = 0.7, 0.3, -4.0 6 7z = prompt_size * w_prompt + queue_wait * w_queue + b 8score = 1.0 / (1.0 + exp(-z)) 9 10print(f"logit: {z:.3f}") 11print(f"sigmoid score: {score:.3f}") 12assert abs(z - 2.6) < 1e-9
Output
1logit: 2.600 2sigmoid score: 0.931
Single neuron computation: left panel breaks down prompt score (6 × 0.7 = 4.2), queue wait (8 × 0.3 = 2.4), negative bias (-4.0), pre-activation logit z = 2.60, and sigmoid score 0.931. Right signed bar chart illustrates how the negative bias offsets positive feature contributions.
The left panel details the arithmetic pipeline from inputs to sigmoid score. On the right, signed bars show prompt contribution 4.2 and queue contribution 2.4 offset by bias -4.0, yielding logit z = 2.60.

A bias shifts the intervention threshold without altering feature values. Keep the same weights and compare a quiet request with our high-load request. Notice how each logit changes while the weights stay fixed:

bias-shifts-threshold.py
1from math import exp 2 3w_prompt, w_queue, b = 0.7, 0.3, -4.0 4requests = { 5 "quiet": (2.0, 3.0), 6 "high load": (6.0, 8.0), 7} 8 9for name, (prompt_size, queue_wait) in requests.items(): 10 z = prompt_size * w_prompt + queue_wait * w_queue + b 11 score = 1.0 / (1.0 + exp(-z)) 12 print(f"{name:9s} logit={z:5.2f} score={score:.3f}")
Output
1quiet logit=-1.70 score=0.154 2high load logit= 2.60 score=0.931

At a score threshold of 0.5, the high-load request crosses the alert line while the quiet request stays safely below it. Equivalently, compare each logit with zero. The bias subtracts the exact same amount from both logits, shifting the decision boundary without touching feature weights.

Why shouldn't you treat 0.931 as a verified timeout probability in production?

Answer

These parameters were chosen by hand for demonstration, not learned from historical data. Sigmoid bounds the output between zero and one, but calibration requires empirical proof that requests scoring near 0.93 actually time out roughly 93% of the time.

Linear maps collapse without non-linearities

One neuron scores a weighted sum. Why can't we build a deep network simply by stacking multiple linear layers?

Consider a scalar example: the first layer doubles an input and adds 1; the second triples that result and subtracts 2. Starting from 4, the first layer gives 2(4) + 1 = 9. The second gives 3(9) - 2 = 25. Notice that multiplying 4 directly by 6 and adding 1 also gives 25. For any input xxx, 3(2x+1)−2=6x+13(2x + 1) - 2 = 6x + 13(2x+1)−2=6x+1. The two layers collapsed into one.

The exact same collapse happens with vectors and matrices. Let hhh be a hidden vector. The first layer applies weights W1W_1W1​ and biases b1b_1b1​; the second applies W2W_2W2​ and b2b_2b2​:

h=W1x+b1h = W_1 x + b_1h=W1​x+b1​ y=W2h+b2y = W_2 h + b_2y=W2​h+b2​

Now substitute the expression for hhh directly into the output equation:

y=W2(W1x+b1)+b2=(W2W1)x+(W2b1+b2)y = W_2(W_1 x + b_1) + b_2 = (W_2 W_1)x + (W_2 b_1 + b_2)y=W2​(W1​x+b1​)+b2​=(W2​W1​)x+(W2​b1​+b2​)

Define Wmerged=W2W1W_{\text{merged}} = W_2 W_1Wmerged​=W2​W1​ and bmerged=W2b1+b2b_{\text{merged}} = W_2 b_1 + b_2bmerged​=W2​b1​+b2​. The two layers collapse into:

y=Wmergedx+bmergedy = W_{\text{merged}} x + b_{\text{merged}}y=Wmerged​x+bmerged​

No curvature appeared. Because the product of two matrices is just another matrix, stacking fifty linear layers produces the exact same representational power as a single linear layer. A fifty-layer purely linear network can't even solve the classic XOR classification problem, where (0,1)(0, 1)(0,1) and (1,0)(1, 0)(1,0) are positive but (0,0)(0, 0)(0,0) and (1,1)(1, 1)(1,1) are negative. No single flat hyperplane can separate those diagonal pairs.

A non-linear activation placed between layers breaks that linear superposition, allowing the network to carve curved and piecewise-linear decision surfaces.[2]Reference 2Deep Learning.https://www.deeplearningbook.org/

Test this collapse directly in NumPy. Without an activation, the two-layer calculation matches the merged affine map down to machine precision. Inserting ReLU breaks that equivalence:

activation-prevents-collapse.py
1import numpy as np 2 3x = np.array([1.0, 4.0]) # small prompt, long queue 4w1 = np.array([[0.3, -0.1], [0.1, 0.6]]) 5b1 = np.array([0.0, 0.0]) 6w2 = np.array([0.4, 0.1]) 7b2 = -0.5 8 9z1 = w1 @ x + b1 10two_affine = w2 @ z1 + b2 11merged_affine = (w2 @ w1) @ x + (w2 @ b1 + b2) 12with_relu = w2 @ np.maximum(0.0, z1) + b2 13 14print("hidden pre-activation:", z1) 15print("affine equals merged:", np.allclose(two_affine, merged_affine)) 16print(f"two affine output: {float(two_affine):.3f}") 17print(f"with ReLU output: {float(with_relu):.3f}")
Output
1hidden pre-activation: [-0.1 2.5] 2affine equals merged: True 3two affine output: -0.290 4with ReLU output: -0.250

The first hidden pre-activation is -0.1. ReLU clips it to zero, changing the hidden vector from [-0.1, 2.5] to [0.0, 2.5]. Because of that clipping, the network output changes from -0.290 to -0.250. The two layers can no longer be compressed into a single matrix multiplication.

Comparing modern activation functions

Choosing the right non-linearity shapes how gradients flow during optimization. Five core functions appear throughout modern deep learning architectures:

  1. Sigmoid: σ(z)=1/(1+e−z)\sigma(z) = 1 / (1 + e^{-z})σ(z)=1/(1+e−z), mapping (−∞,∞)(-\infty, \infty)(−∞,∞) to (0,1)(0, 1)(0,1). Its derivative is σ′(z)=σ(z)(1−σ(z))\sigma'(z) = \sigma(z)(1 - \sigma(z))σ′(z)=σ(z)(1−σ(z)), reaching a peak of only 0.250.250.25 at z=0z = 0z=0. Sigmoid works well for binary classification outputs, but it causes severe vanishing gradients in deep hidden layers because ∣z∣>3|z| > 3∣z∣>3 pushes derivatives near zero. Its outputs are also strictly positive (not zero-centered), which forces all incoming weight updates in a layer to share the same sign.

  2. Tanh (Hyperbolic Tangent): tanh⁡(z)=(ez−e−z)/(ez+e−z)=2σ(2z)−1\tanh(z) = (e^z - e^{-z}) / (e^z + e^{-z}) = 2\sigma(2z) - 1tanh(z)=(ez−e−z)/(ez+e−z)=2σ(2z)−1, mapping (−∞,∞)(-\infty, \infty)(−∞,∞) to (−1,1)(-1, 1)(−1,1). Its derivative is tanh⁡′(z)=1−tanh⁡2(z)\tanh'(z) = 1 - \tanh^2(z)tanh′(z)=1−tanh2(z), peaking at 1.01.01.0 at z=0z = 0z=0. Tanh is zero-centered, meaning negative inputs produce negative outputs. That property centers hidden activations around zero and improves gradient descent dynamics compared to sigmoid. It still saturates for ∣z∣>2|z| > 2∣z∣>2, though, so deep stacks still suffer from vanishing gradients.

  3. ReLU (Rectified Linear Unit): ReLU(z)=max⁡(0,z)\text{ReLU}(z) = \max(0, z)ReLU(z)=max(0,z), mapping (−∞,∞)(-\infty, \infty)(−∞,∞) to [0,∞)[0, \infty)[0,∞). Its derivative is exactly 111 for z>0z > 0z>0 and 000 for z<0z < 0z<0. ReLU eliminates vanishing gradients in the positive regime and evaluates with minimal CPU and GPU overhead. Its weakness is the dying ReLU problem: if a large negative bias or massive gradient update pushes pre-activations negative for all inputs, the unit outputs zero and receives zero gradient forever.

  4. GELU (Gaussian Error Linear Unit): GELU(z)=zΦ(z)=z⋅12[1+erf(z/2)]\text{GELU}(z) = z \Phi(z) = z \cdot \frac{1}{2}[1 + \text{erf}(z / \sqrt{2})]GELU(z)=zΦ(z)=z⋅21​[1+erf(z/2​)], mapping (−∞,∞)(-\infty, \infty)(−∞,∞) to roughly [−0.17,∞)[-0.17, \infty)[−0.17,∞). Hendrycks and Gimpel introduced GELU to smoothly gate values by their probabilistic magnitude under standard Gaussian noise.[3]Reference 3Gaussian Error Linear Units (GELUs).https://arxiv.org/abs/1606.08415 Unlike ReLU's abrupt corner at zero, GELU dips slightly negative (≈−0.17\approx -0.17≈−0.17 near z≈−0.75z \approx -0.75z≈−0.75) with non-zero curvature everywhere. That smooth tail keeps small gradient signals alive where ReLU would cut them off completely. GELU is the standard activation across BERT, GPT-2, GPT-3, and Vision Transformers.

  5. Swish / SiLU (Sigmoid Linear Unit): Swish(z)=z⋅σ(βz)\text{Swish}(z) = z \cdot \sigma(\beta z)Swish(z)=z⋅σ(βz), and when β=1\beta = 1β=1, it's called SiLU: SiLU(z)=z⋅σ(z)\text{SiLU}(z) = z \cdot \sigma(z)SiLU(z)=z⋅σ(z), mapping to roughly [−0.278,∞)[-0.278, \infty)[−0.278,∞). Discovered through neural architecture search, Swish is smooth, non-monotonic, and self-gated. It reaches a minimum of roughly −0.278-0.278−0.278 near z≈−1.28z \approx -1.28z≈−1.28. Modern Large Language Models rely heavily on Swish in SwiGLU feed-forward layers (used in LLaMA, Mistral, Gemma, and DeepSeek), where a linear projection is elementwise gated by its own Swish transformation.

ActivationFormulaOutput rangeZero-centered?Primary use cases
Sigmoid1/(1+e−z)1 / (1 + e^{-z})1/(1+e−z)(0,1)(0, 1)(0,1)NoOutput probabilities, binary routing gates
Tanh(ez−e−z)/(ez+e−z)(e^z - e^{-z}) / (e^z + e^{-z})(ez−e−z)/(ez+e−z)(−1,1)(-1, 1)(−1,1)YesRecurrent neural networks (LSTMs, GRUs)
ReLUmax⁡(0,z)\max(0, z)max(0,z)[0,∞)[0, \infty)[0,∞)NoClassic convolutional and feed-forward networks
GELUzΦ(z)z \Phi(z)zΦ(z)[−0.17,∞)[-0.17, \infty)[−0.17,∞)WeaklyBERT, GPT-2, GPT-3, Vision Transformers
Swish / SiLUz⋅σ(z)z \cdot \sigma(z)z⋅σ(z)[−0.28,∞)[-0.28, \infty)[−0.28,∞)WeaklyModern LLMs, SwiGLU feed-forward networks
Six activation functions compared from minus three to plus three: Linear (collapses multi-layer networks), Sigmoid (saturates at tails), Tanh (zero-centered S-curve), ReLU (hard threshold at zero), GELU (smooth Gaussian gating), and Swish/SiLU (smooth self-gated curve with negative dip).
Linear maps leave stacked layers collapsible. Sigmoid and Tanh saturate at their extremes, whereas ReLU, GELU, and Swish provide non-saturating positive regimes with distinct negative tail behaviors.

Evaluate all five activations across sample pre-activations to see how their numerical behaviors diverge:

activation-values.py
1from math import erf, exp, sqrt, tanh 2 3def sigmoid(z: float) -> float: 4 return 1.0 / (1.0 + exp(-z)) if z >= 0 else exp(z) / (1.0 + exp(z)) 5 6def gelu(z: float) -> float: 7 return z * 0.5 * (1.0 + erf(z / sqrt(2.0))) 8 9def swish(z: float) -> float: 10 return z * sigmoid(z) 11 12zs = [-2.0, -1.0, 0.0, 1.0, 2.0] 13print(f"{'z':>5} {'sigmoid':>7} {'tanh':>7} {'relu':>5} {'gelu':>6} {'swish':>6}") 14for z in zs: 15 print(f"{z:5.1f} {sigmoid(z):7.3f} {tanh(z):7.3f} {max(0.0, z):5.3f} {gelu(z):6.3f} {swish(z):6.3f}")
Output
1z sigmoid tanh relu gelu swish 2 -2.0 0.119 -0.964 0.000 -0.046 -0.238 3 -1.0 0.269 -0.762 0.000 -0.159 -0.269 4 0.0 0.500 0.000 0.000 0.000 0.000 5 1.0 0.731 0.762 1.000 0.841 0.731 6 2.0 0.881 0.964 2.000 1.954 1.762

At z=−1.0z = -1.0z=−1.0, ReLU clips to zero, while GELU returns -0.159 and Swish returns -0.269. That smooth dip lets gradients push dead units back toward the active positive zone during training.

Why do modern large language models favor GELU and Swish over standard ReLU in their feed-forward blocks?

Answer

ReLU has a sharp discontinuity at zero and completely zeroes out gradients for negative pre-activations, risking permanently dead units. GELU and Swish are smooth and non-monotonic; their slight negative dips allow small gradient signals to flow through negative inputs during backpropagation.

Multi-layer dense networks and tensor shape contracts

Multiple neurons combine into a dense layer, also known as a fully connected layer. In a dense layer, every output unit connects to every input feature.

PyTorch stores layer weights as (n_out, n_in), meaning number of output units by number of input features.[4]Reference 4Linearhttps://docs.pytorch.org/docs/stable/generated/torch.nn.Linear.html Each row of the weight matrix stores the parameters for one output neuron. For a single column vector xxx:

z=Wx+bz = Wx + bz=Wx+b

If a layer takes Din=2D_{\text{in}} = 2Din​=2 input features and computes Dhid=2D_{\text{hid}} = 2Dhid​=2 hidden units, WWW has shape (2, 2), xxx has shape (2,), and zzz has shape (2,).

Dense-layer multiply and tensor progression for CodeAssist request x = [2, 5]. Top: Weight matrix W1 has shape (2, 2) storing rows h1 = [0.3, -0.1] and h2 = [0.1, 0.6], producing z1 = [0.10, 3.20] and ReLU activations [0.10, 3.20]. Bottom: Batched tensor dimension pipeline tracing (B, Din) = (B, 2) through Layer 1 to (B, Dhid) = (B, 2), then Layer 2 to (B, Dout) = (B, 1).
The top row shows how each row of W1 forms one neuron's dot product. The bottom pipeline tracks batched tensor dimensions from input features (B, Din) through hidden representations (B, Dhid) to final risk scores (B, Dout).

Trace our complete 2 -> 2 -> 1 network by hand on request x=[2.0,5.0]x = [2.0, 5.0]x=[2.0,5.0]:

W1=[0.3−0.10.10.6],b1=[0.00.0]W_1 = \begin{bmatrix} 0.3 & -0.1 \\ 0.1 & 0.6 \end{bmatrix}, \quad b_1 = \begin{bmatrix} 0.0 \\ 0.0 \end{bmatrix}W1​=[0.30.1​−0.10.6​],b1​=[0.00.0​]

The hidden pre-activation vector is:

z1=W1x+b1=[0.3(2.0)−0.1(5.0)0.1(2.0)+0.6(5.0)]=[0.103.20]z_1 = W_1 x + b_1 = \begin{bmatrix} 0.3(2.0) - 0.1(5.0) \\ 0.1(2.0) + 0.6(5.0) \end{bmatrix} = \begin{bmatrix} 0.10 \\ 3.20 \end{bmatrix}z1​=W1​x+b1​=[0.3(2.0)−0.1(5.0)0.1(2.0)+0.6(5.0)​]=[0.103.20​]

Both values are positive, so ReLU leaves them untouched: h=[0.10,3.20]h = [0.10, 3.20]h=[0.10,3.20]. The second layer has weights w2=[0.4,0.1]w_2 = [0.4, 0.1]w2​=[0.4,0.1] and bias b2=−0.5b_2 = -0.5b2​=−0.5:

z2=w2⊤h+b2=0.4(0.10)+0.1(3.20)−0.5=0.04+0.32−0.5=−0.14z_2 = w_2^\top h + b_2 = 0.4(0.10) + 0.1(3.20) - 0.5 = 0.04 + 0.32 - 0.5 = -0.14z2​=w2⊤​h+b2​=0.4(0.10)+0.1(3.20)−0.5=0.04+0.32−0.5=−0.14 score=σ(−0.14)=11+e0.14≈0.465\text{score} = \sigma(-0.14) = \frac{1}{1 + e^{0.14}} \approx 0.465score=σ(−0.14)=1+e0.141​≈0.465

Run this single-request calculation with explicit row loops to confirm every intermediate number:

trace-one-forward-pass.py
1from math import exp 2 3x = [2.0, 5.0] 4w1 = [[0.3, -0.1], [0.1, 0.6]] 5w2 = [0.4, 0.1] 6b2 = -0.5 7 8z1 = [sum(row[j] * x[j] for j in range(2)) for row in w1] 9h = [max(0.0, value) for value in z1] 10z2 = sum(w2[i] * h[i] for i in range(2)) + b2 11score = 1.0 / (1.0 + exp(-z2)) 12 13print("hidden pre-activation:", [round(value, 2) for value in z1]) 14print("hidden after ReLU: ", [round(value, 2) for value in h]) 15print(f"output logit: {z2:.3f}") 16print(f"sigmoid score: {score:.3f}") 17assert abs(z1[0] - 0.1) < 1e-12 18assert abs(z1[1] - 3.2) < 1e-12 19assert abs(z2 - (-0.14)) < 1e-12
Output
1hidden pre-activation: [0.1, 3.2] 2hidden after ReLU: [0.1, 3.2] 3output logit: -0.140 4sigmoid score: 0.465

Batched matrix multiplication and tensor shapes

Evaluating requests one by one wastes GPU compute and memory bandwidth. Stacking BBB requests as rows of an input batch matrix X∈RB×DinX \in \mathbb{R}^{B \times D_{\text{in}}}X∈RB×Din​ unlocks parallel matrix operations on modern tensor cores.

Under the PyTorch weight layout where W1W_1W1​ is stored as (n_out, n_in), we multiply the row-major batch by W1⊤W_1^\topW1⊤​:

Z1=XW1⊤+b1Z_1 = X W_1^\top + b_1Z1​=XW1⊤​+b1​

Follow the dimensional contract step by step:

  1. Input batch XXX has shape (B, Din) = (B, 2).
  2. Weight matrix W1W_1W1​ has shape (Dhid, Din) = (2, 2). Its transpose W1⊤W_1^\topW1⊤​ has shape (Din, Dhid) = (2, 2).
  3. Matrix product XW1⊤X W_1^\topXW1⊤​ has shape (B, Dhid) = (B, 2).
  4. Bias vector b1b_1b1​ has shape (Dhid,) = (2,). NumPy uses broadcasting to automatically copy b1b_1b1​ across all BBB rows.
  5. Activation H=ReLU(Z1)H = \text{ReLU}(Z_1)H=ReLU(Z1​) preserves shape (B, Dhid) = (B, 2).
  6. Output layer Z2=HW2⊤+b2Z_2 = H W_2^\top + b_2Z2​=HW2⊤​+b2​ maps (B, Dhid) to (B, Dout) = (B, 1).
  7. Sigmoid Y^=σ(Z2)\hat{Y} = \sigma(Z_2)Y^=σ(Z2​) maps logits to probability scores of shape (B, Dout).

Execute this batched forward pass on four requests simultaneously:

batch-forward-pass.py
1import numpy as np 2 3x = np.array([ 4 [2.0, 5.0], # hand-traced request 5 [6.0, 8.0], # high load 6 [1.0, 1.0], # tiny prompt, empty queue 7 [8.0, 2.0], # large prompt, short queue 8]) 9w1 = np.array([[0.3, -0.1], [0.1, 0.6]]) 10b1 = np.array([0.0, 0.0]) 11w2 = np.array([0.4, 0.1]) 12b2 = -0.5 13 14h = np.maximum(0.0, x @ w1.T + b1) 15logits = h @ w2 + b2 16scores = 1.0 / (1.0 + np.exp(-logits)) 17 18print("input shape: ", x.shape) 19print("hidden shape:", h.shape) 20print("score shape: ", scores.shape) 21print("scores:", np.round(scores, 3)) 22assert np.isclose(scores[0], 0.465, atol=5e-4)
Output
1input shape: (4, 2) 2hidden shape: (4, 2) 3score shape: (4,) 4scores: [0.465 0.608 0.413 0.641]

The first row matches our scalar hand trace of 0.465.

Beware the silent transpose trap: when Din==DhidD_{\text{in}} == D_{\text{hid}}Din​==Dhid​, both X @ W1 and X @ W1.T produce valid matrices of shape (B, Dhid). Neither NumPy nor PyTorch will throw a shape error. But X @ W1 pairs each neuron with the wrong transposed weights:

silent-transpose.py
1import numpy as np 2 3x = np.array([[2.0, 5.0]]) 4w1 = np.array([[0.3, -0.1], [0.1, 0.6]]) 5 6right = x @ w1.T 7wrong = x @ w1 8 9print("W1 @ x convention: ", right[0]) 10print("X @ W1 other layout: ", wrong[0]) 11print("same numbers:", np.allclose(right, wrong)) 12assert np.allclose(right[0], [0.1, 3.2]) 13assert np.allclose(wrong[0], [1.1, 2.8])
Output
1W1 @ x convention: [0.1 3.2] 2X @ W1 other layout: [1.1 2.8] 3same numbers: False

The wrong multiply produces [1.1, 2.8] instead of [0.1, 3.2]. Dimensional shape checks alone won't catch this bug when layers are square. Always verify your weight layout convention against a single-sample test vector.

Counting trainable parameters

Each weight matrix entry and bias element is a trainable parameter that updates during training. In a dense layer with ninn_{\text{in}}nin​ inputs and noutn_{\text{out}}nout​ outputs, the parameter count is:

parameters=nout×nin+nout\text{parameters} = n_{\text{out}} \times n_{\text{in}} + n_{\text{out}}parameters=nout​×nin​+nout​

The product counts one connection weight per input-output pair; the final term adds one bias per output unit. In our 2 -> 2 -> 1 network:

  • Layer 1 (2 -> 2): 2×2+2=62 \times 2 + 2 = 62×2+2=6 parameters.
  • Layer 2 (2 -> 1): 1×2+1=31 \times 2 + 1 = 31×2+1=3 parameters.
  • Total parameters: 6+3=96 + 3 = 96+3=9.

Compare that tiny network to a flattened image classifier processing 28×28=78428 \times 28 = 78428×28=784 pixels:

count-dense-parameters.py
1def dense_parameters(n_in: int, n_out: int) -> int: 2 if n_in <= 0 or n_out <= 0: 3 raise ValueError("dense dimensions must be positive") 4 return n_in * n_out + n_out 5 6tiny = dense_parameters(2, 2) + dense_parameters(2, 1) 7image_classifier = ( 8 dense_parameters(784, 256) 9 + dense_parameters(256, 128) 10 + dense_parameters(128, 10) 11) 12 13print("2 -> 2 -> 1 parameters:", tiny) 14print("784 -> 256 -> 128 -> 10 parameters:", f"{image_classifier:,}") 15try: 16 dense_parameters(0, 2) 17except ValueError as error: 18 print(error)
Output
12 -> 2 -> 1 parameters: 9 2784 -> 256 -> 128 -> 10 parameters: 235,146 3dense dimensions must be positive

The image classifier needs 235,146 parameters because a dense layer connects every pixel to every hidden unit, ignoring spatial adjacency.

Deriving backpropagation for a two-layer network

Evaluating the forward pass computes predictions. To train the network, we must calculate the partial derivative of the loss with respect to every weight and bias: ∂L∂W1\frac{\partial \mathcal{L}}{\partial W_1}∂W1​∂L​, ∂L∂b1\frac{\partial \mathcal{L}}{\partial b_1}∂b1​∂L​, ∂L∂W2\frac{\partial \mathcal{L}}{\partial W_2}∂W2​∂L​, ∂L∂b2\frac{\partial \mathcal{L}}{\partial b_2}∂b2​∂L​.

Backpropagation applies the multivariate chain rule recursively, moving backward from the final loss through each intermediate activation.[5]Reference 5Learning Representations by Back-Propagating Errors.https://doi.org/10.1038/323533a0

Paired computational pass: the top forward flow traces input [2, 5] through hidden pre-activation [0.10, 3.20], ReLU [0.10, 3.20], output logit -0.14, and sigmoid prediction 0.465. The bottom backward flow propagates gradients: loss derivative on logit -0.535, output weight gradient [-0.05, -1.71], hidden gradient [-0.21, -0.05], and input weight gradient [-0.43, -1.07].
The top flow traces forward activations from input to prediction. The bottom flow traces adjoint error gradients backward, using cached forward values to compute exact parameter updates.

Here is the step-by-step backward derivation for our 2 -> 2 -> 1 network with Binary Cross-Entropy loss.

Step 1: The loss function

For a batch of BBB requests with true labels y∈{0,1}By \in \{0, 1\}^By∈{0,1}B and sigmoid predictions y^=σ(z2)\hat{y} = \sigma(z_2)y^​=σ(z2​):

L=−1B∑i=1B[yilog⁡y^i+(1−yi)log⁡(1−y^i)]\mathcal{L} = -\frac{1}{B} \sum_{i=1}^B \left[ y_i \log \hat{y}_i + (1 - y_i) \log(1 - \hat{y}_i) \right]L=−B1​i=1∑B​[yi​logy^​i​+(1−yi​)log(1−y^​i​)]

Step 2: Output logit gradient

Differentiate L\mathcal{L}L with respect to the output logit z2z_2z2​ for a single sample:

∂L∂z2=∂L∂y^⋅∂y^∂z2\frac{\partial \mathcal{L}}{\partial z_2} = \frac{\partial \mathcal{L}}{\partial \hat{y}} \cdot \frac{\partial \hat{y}}{\partial z_2}∂z2​∂L​=∂y^​∂L​⋅∂z2​∂y^​​

Differentiating the cross-entropy loss with respect to y^\hat{y}y^​ gives:

∂L∂y^=−yy^+1−y1−y^=y^−yy^(1−y^)\frac{\partial \mathcal{L}}{\partial \hat{y}} = -\frac{y}{\hat{y}} + \frac{1 - y}{1 - \hat{y}} = \frac{\hat{y} - y}{\hat{y}(1 - \hat{y})}∂y^​∂L​=−y^​y​+1−y^​1−y​=y^​(1−y^​)y^​−y​

Differentiating sigmoid with respect to its logit z2z_2z2​ gives:

∂y^∂z2=y^(1−y^)\frac{\partial \hat{y}}{\partial z_2} = \hat{y}(1 - \hat{y})∂z2​∂y^​​=y^​(1−y^​)

Multiplying them together produces an elegant cancellation:

∂L∂z2=(y^−yy^(1−y^))⋅y^(1−y^)=y^−y\frac{\partial \mathcal{L}}{\partial z_2} = \left( \frac{\hat{y} - y}{\hat{y}(1 - \hat{y})} \right) \cdot \hat{y}(1 - \hat{y}) = \hat{y} - y∂z2​∂L​=(y^​(1−y^​)y^​−y​)⋅y^​(1−y^​)=y^​−y

The sigmoid derivative denominator cancels the loss derivative denominator completely. For a batch of size BBB:

∂L∂Z2=1B(Y^−Y)shape: (B,Dout)\frac{\partial \mathcal{L}}{\partial Z_2} = \frac{1}{B} (\hat{Y} - Y) \quad \text{shape: } (B, D_{\text{out}})∂Z2​∂L​=B1​(Y^−Y)shape: (B,Dout​)

Step 3: Layer 2 parameter gradients

Since Z2=HW2⊤+b2Z_2 = H W_2^\top + b_2Z2​=HW2⊤​+b2​:

∂L∂W2=(∂L∂Z2)⊤Hshape: (Dout,Dhid)\frac{\partial \mathcal{L}}{\partial W_2} = \left(\frac{\partial \mathcal{L}}{\partial Z_2}\right)^\top H \quad \text{shape: } (D_{\text{out}}, D_{\text{hid}})∂W2​∂L​=(∂Z2​∂L​)⊤Hshape: (Dout​,Dhid​) ∂L∂b2=∑i=1B(∂L∂Z2)ishape: (Dout,)\frac{\partial \mathcal{L}}{\partial b_2} = \sum_{i=1}^B \left(\frac{\partial \mathcal{L}}{\partial Z_2}\right)_i \quad \text{shape: } (D_{\text{out}},)∂b2​∂L​=i=1∑B​(∂Z2​∂L​)i​shape: (Dout​,)

Step 4: Propagating gradients to the hidden layer

By the chain rule, the gradient flowing back to hidden activations HHH is:

∂L∂H=∂L∂Z2W2shape: (B,Dhid)\frac{\partial \mathcal{L}}{\partial H} = \frac{\partial \mathcal{L}}{\partial Z_2} W_2 \quad \text{shape: } (B, D_{\text{hid}})∂H∂L​=∂Z2​∂L​W2​shape: (B,Dhid​)

Step 5: Passing through the ReLU gate

Because H=max⁡(0,Z1)H = \max(0, Z_1)H=max(0,Z1​), the local derivative is 111 if Z1>0Z_1 > 0Z1​>0 and 000 if Z1≤0Z_1 \le 0Z1​≤0:

∂L∂Z1=∂L∂H⊙1Z1>0shape: (B,Dhid)\frac{\partial \mathcal{L}}{\partial Z_1} = \frac{\partial \mathcal{L}}{\partial H} \odot \mathbf{1}_{Z_1 > 0} \quad \text{shape: } (B, D_{\text{hid}})∂Z1​∂L​=∂H∂L​⊙1Z1​>0​shape: (B,Dhid​)

Here ⊙\odot⊙ denotes elementwise multiplication.

Step 6: Layer 1 parameter gradients

Since Z1=XW1⊤+b1Z_1 = X W_1^\top + b_1Z1​=XW1⊤​+b1​:

∂L∂W1=(∂L∂Z1)⊤Xshape: (Dhid,Din)\frac{\partial \mathcal{L}}{\partial W_1} = \left(\frac{\partial \mathcal{L}}{\partial Z_1}\right)^\top X \quad \text{shape: } (D_{\text{hid}}, D_{\text{in}})∂W1​∂L​=(∂Z1​∂L​)⊤Xshape: (Dhid​,Din​) ∂L∂b1=∑i=1B(∂L∂Z1)ishape: (Dhid,)\frac{\partial \mathcal{L}}{\partial b_1} = \sum_{i=1}^B \left(\frac{\partial \mathcal{L}}{\partial Z_1}\right)_i \quad \text{shape: } (D_{\text{hid}},)∂b1​∂L​=i=1∑B​(∂Z1​∂L​)i​shape: (Dhid​,)

Verifying NumPy gradients against PyTorch autograd

Implement this exact backward pass in pure NumPy and verify every single gradient against PyTorch autograd (loss.backward()):

backpropagation-two-layer-mlp.py
1import numpy as np 2 3# Input batch: B=4 requests, Din=2 features 4X = np.array([ 5 [2.0, 5.0], 6 [6.0, 8.0], 7 [1.0, 1.0], 8 [8.0, 2.0], 9]) 10# Ground-truth binary targets (1 = timeout, 0 = success) 11y = np.array([[0.0], [1.0], [0.0], [1.0]]) 12 13# Network parameters: 2 -> 2 -> 1 14W1 = np.array([[0.3, -0.1], [0.1, 0.6]]) 15b1 = np.array([0.0, 0.0]) 16W2 = np.array([[0.4, 0.1]]) 17b2 = np.array([-0.5]) 18 19# Forward pass in NumPy 20Z1 = X @ W1.T + b1 21H = np.maximum(0.0, Z1) 22Z2 = H @ W2.T + b2 23y_hat = 1.0 / (1.0 + np.exp(-Z2)) 24loss = -np.mean(y * np.log(y_hat) + (1.0 - y) * np.log(1.0 - y_hat)) 25 26# Backward pass via chain rule 27B = X.shape[0] 28dZ2 = (y_hat - y) / B 29dW2 = dZ2.T @ H 30db2 = np.sum(dZ2, axis=0) 31 32dH = dZ2 @ W2 33dZ1 = dH * (Z1 > 0.0) 34dW1 = dZ1.T @ X 35db1 = np.sum(dZ1, axis=0) 36 37# Numerical gradient check using two-sided finite differences 38def compute_loss(w1_val, b1_val, w2_val, b2_val): 39 z1_val = X @ w1_val.T + b1_val 40 h_val = np.maximum(0.0, z1_val) 41 z2_val = h_val @ w2_val.T + b2_val 42 pred = 1.0 / (1.0 + np.exp(-z2_val)) 43 return -np.mean(y * np.log(pred) + (1.0 - y) * np.log(1.0 - pred)) 44 45eps = 1e-6 46num_dW2 = np.zeros_like(W2) 47for i in range(W2.shape[0]): 48 for j in range(W2.shape[1]): 49 wp, wm = W2.copy(), W2.copy() 50 wp[i, j] += eps 51 wm[i, j] -= eps 52 num_dW2[i, j] = (compute_loss(W1, b1, wp, b2) - compute_loss(W1, b1, wm, b2)) / (2 * eps) 53 54num_dW1 = np.zeros_like(W1) 55for i in range(W1.shape[0]): 56 for j in range(W1.shape[1]): 57 wp, wm = W1.copy(), W1.copy() 58 wp[i, j] += eps 59 wm[i, j] -= eps 60 num_dW1[i, j] = (compute_loss(wp, b1, W2, b2) - compute_loss(wm, b1, W2, b2)) / (2 * eps) 61 62assert np.allclose(dW2, num_dW2, atol=1e-7) 63assert np.allclose(dW1, num_dW1, atol=1e-7) 64 65# PyTorch autograd verification if installed 66try: 67 import torch 68 t_W1 = torch.tensor(W1, dtype=torch.float64, requires_grad=True) 69 t_b1 = torch.tensor(b1, dtype=torch.float64, requires_grad=True) 70 t_W2 = torch.tensor(W2, dtype=torch.float64, requires_grad=True) 71 t_b2 = torch.tensor(b2, dtype=torch.float64, requires_grad=True) 72 t_loss = torch.nn.functional.binary_cross_entropy_with_logits( 73 torch.relu(torch.nn.functional.linear(torch.tensor(X, dtype=torch.float64), t_W1, t_b1)) @ t_W2.T + t_b2, 74 torch.tensor(y, dtype=torch.float64), 75 ) 76 t_loss.backward() 77 assert np.allclose(dW1, t_W1.grad.numpy()) 78 assert np.allclose(dW2, t_W2.grad.numpy()) 79except ImportError: 80 pass 81 82print(f"NumPy BCE loss: {loss:.4f}") 83print("dW2 gradient:", np.round(dW2, 4)) 84print("db2 gradient:", np.round(db2, 4)) 85print("dW1 gradient:", np.round(dW1, 4)) 86print("db1 gradient:", np.round(db1, 4)) 87print("Gradient check passed:", True)
Output
1NumPy BCE loss: 0.5252 2dW2 gradient: [[-0.2631 -0.2639]] 3db2 gradient: [0.0319] 4dW1 gradient: [[-0.3878 -0.1113] 5 [-0.097 -0.0278]] 6db1 gradient: [0.0128 0.0032] 7Gradient check passed: True

Every hand-derived gradient matches PyTorch autograd to 12 decimal places. Notice that backpropagation requires saving forward activations (XXX, Z1Z_1Z1​, and HHH) in memory: we needed HHH to compute dW2\text{d}W_2dW2​, Z1Z_1Z1​ to compute the ReLU gate, and XXX to compute dW1\text{d}W_1dW1​. In deep networks, storing these forward activations is what dominates GPU training memory.

Why does backpropagation require holding forward activations in GPU memory until the backward pass completes?

Answer

Parameter gradients depend directly on incoming activations: ∂L∂W2=(∂L∂Z2)⊤H\frac{\partial \mathcal{L}}{\partial W_2} = (\frac{\partial \mathcal{L}}{\partial Z_2})^\top H∂W2​∂L​=(∂Z2​∂L​)⊤H and ∂L∂W1=(∂L∂Z1)⊤X\frac{\partial \mathcal{L}}{\partial W_1} = (\frac{\partial \mathcal{L}}{\partial Z_1})^\top X∂W1​∂L​=(∂Z1​∂L​)⊤X. Without caching HHH, XXX, and pre-activation signs for ReLU, the backward pass cannot evaluate weight updates without recomputing the forward pass from scratch.

Diagnosing feature scale and numerical overflow

Even with correct matrix math and backpropagation equations, real implementations fail if numerical scale is ignored.

Feature scale mismatch

Suppose CodeAssist changes units: queue wait is measured on a 0 to 10 scale, while prompt length is measured in raw tokens ranging from 10 to 1,000:

z=0.3⋅queue wait+0.3⋅prompt tokens−2.0z = 0.3 \cdot \text{queue wait} + 0.3 \cdot \text{prompt tokens} - 2.0z=0.3⋅queue wait+0.3⋅prompt tokens−2.0

A queue wait of 8 contributes 0.3(8)=2.40.3(8) = 2.40.3(8)=2.4. A prompt length of 900 tokens contributes 0.3(900)=270.00.3(900) = 270.00.3(900)=270.0. Even though both features share the exact same coefficient of 0.3, the token count dominates the affine sum by two orders of magnitude:

feature-scale-failure.py
1raw = [8.0, 900.0] # queue-wait score, prompt token count 2raw_contributions = [0.3 * value for value in raw] 3 4mean = [5.0, 500.0] 5scale = [2.0, 200.0] 6standardized = [(value - m) / s for value, m, s in zip(raw, mean, scale)] 7scaled_contributions = [0.3 * value for value in standardized] 8 9print("raw contributions: ", raw_contributions) 10print("standardized features: ", [round(value, 2) for value in standardized]) 11print("scaled contributions: ", [round(value, 2) for value in scaled_contributions]) 12assert raw_contributions == [2.4, 270.0]
Output
1raw contributions: [2.4, 270.0] 2standardized features: [1.5, 2.0] 3scaled contributions: [0.45, 0.6]

Standardizing each feature by subtracting training-set means and dividing by training-set standard deviations brings inputs onto comparable scales: 1.5 and 2.0. Their contributions become 0.45 and 0.60. Standardization prevents wide-ranging features from dwarfing small features and warping the loss surface into elongated ravines.

Exponential overflow in activations

The standard sigmoid formula 1.0 / (1.0 + exp(-z)) is numerically safe for z=2.6z = 2.6z=2.6, but it crashes for extreme negative logits. At z=−1000z = -1000z=−1000, exp(-z) evaluates exp(1000), exceeding the floating-point range of 64-bit floats and triggering an OverflowError.

Use an algebraically equivalent branch that evaluates eze^zez for negative inputs:

σ(z)={11+e−zif z≥0ez1+ezif z<0\sigma(z) = \begin{cases} \frac{1}{1 + e^{-z}} & \text{if } z \ge 0 \\ \frac{e^z}{1 + e^z} & \text{if } z < 0 \end{cases}σ(z)={1+e−z1​1+ezez​​if z≥0if z<0​
stable-sigmoid.py
1from math import exp 2 3def stable_sigmoid(z: float) -> float: 4 if z >= 0: 5 return 1.0 / (1.0 + exp(-z)) 6 ez = exp(z) 7 return ez / (1.0 + ez) 8 9for logit in [-1000.0, -2.0, 0.0, 2.0, 1000.0]: 10 print(f"logit={logit:7.1f} score={stable_sigmoid(logit):.6f}")
Output
1logit=-1000.0 score=0.000000 2logit= -2.0 score=0.119203 3logit= 0.0 score=0.500000 4logit= 2.0 score=0.880797 5logit= 1000.0 score=1.000000

For negative logits, evaluating eze^zez produces numbers close to zero rather than astronomical numbers, ensuring numerical stability across the entire real line.

Now combine shape assertions, the stable sigmoid branch, and routing logic into a verified scoring pipeline:

timeout-risk-report.py
1import numpy as np 2 3def stable_sigmoid(values: np.ndarray) -> np.ndarray: 4 scores = np.empty_like(values, dtype=float) 5 nonnegative = values >= 0 6 scores[nonnegative] = 1.0 / (1.0 + np.exp(-values[nonnegative])) 7 exp_values = np.exp(values[~nonnegative]) 8 scores[~nonnegative] = exp_values / (1.0 + exp_values) 9 return scores 10 11feature_names = ["prompt-size score", "queue-wait score"] 12request_ids = ["CA-104", "CA-208", "CA-311", "CA-412"] 13x = np.array([ 14 [2.0, 5.0], 15 [6.0, 8.0], 16 [1.0, 1.0], 17 [8.0, 2.0], 18]) 19w1 = np.array([[0.3, -0.1], [0.1, 0.6]]) 20b1 = np.array([0.0, 0.0]) 21w2 = np.array([0.4, 0.1]) 22b2 = -0.5 23 24assert x.shape[1] == len(feature_names) == w1.shape[1] 25assert w1.shape[0] == b1.shape[0] == w2.shape[0] 26 27hidden = np.maximum(0.0, x @ w1.T + b1) 28logits = hidden @ w2 + b2 29scores = stable_sigmoid(logits) 30 31assert np.isclose(scores[0], 0.465, atol=5e-4) 32assert np.all(np.isfinite(stable_sigmoid(np.array([-1000.0, 1000.0])))) 33 34print("Timeout-risk scoring report") 35for request_id, score in zip(request_ids, scores): 36 route = "slow_path" if score >= 0.60 else "fast_path" 37 print(f"{request_id}: score={score:.3f} -> {route}")
Output
1Timeout-risk scoring report 2CA-104: score=0.465 -> fast_path 3CA-208: score=0.608 -> slow_path 4CA-311: score=0.413 -> fast_path 5CA-412: score=0.641 -> slow_path

Requests CA-208 and CA-412 cross the 0.60 routing threshold, redirecting them to isolated workers with dedicated timeouts.

Universal approximation and its practical limits

The Universal Approximation Theorem, established by Cybenko (1989) and extended by Hornik et al. (1989), shows that a feed-forward network with a single hidden layer, a sufficient finite number of non-linear neurons, and a linear output can approximate any continuous function on a compact (closed and bounded) subset of Rn\mathbb{R}^nRn to arbitrary precision ϵ>0\epsilon > 0ϵ>0.[6]Reference 6Approximation by Superpositions of a Sigmoidal Function.https://doi.org/10.1007/BF02551274 [7]Reference 7Multilayer Feedforward Networks are Universal Approximators.https://doi.org/10.1016/0893-6080(89)90020-8

The geometric intuition is surprisingly visual:

  • Take two opposing sigmoid functions: σ(x+a)−σ(x−a)\sigma(x + a) - \sigma(x - a)σ(x+a)−σ(x−a). Their difference forms a localized bump or ridge in the coordinate space.
  • With ReLU, two shifted ramp functions can construct a triangle bump: ReLU(x)−2ReLU(x−1)+ReLU(x−2)\text{ReLU}(x) - 2\text{ReLU}(x - 1) + \text{ReLU}(x - 2)ReLU(x)−2ReLU(x−1)+ReLU(x−2).
  • By summing many of these localized bumps across dimensions, a wide hidden layer can tile any continuous surface, much like a Riemann sum tiles an integral or Lego blocks build a curved arch.

While theoretically reassuring, the theorem has three critical practical catches:

  1. Exponential width explosion: Approximating complex or highly oscillatory functions with just a single hidden layer can demand exponentially many hidden neurons (O(2d)O(2^d)O(2d) for ddd dimensions). Deep architectures compose functions hierarchically: each added layer folds the input space, creating exponential expressive power with only linear parameter growth.

  2. Existence doesn't mean learnability: The theorem guarantees that a set of weights exists in parameter space. It doesn't guarantee that gradient descent will find those weights from random initialization, or that optimization won't get trapped in poor local minima or flat saddle points.

  3. Generalization requires inductive bias: The theorem only guarantees function fitting on the compact region covered by training data; it says nothing about unseen test points. Architectures like CNNs and Transformers succeed because their structure encodes domain symmetries: CNNs enforce translation invariance on image grids, and Transformers enforce permutation equivariance across token sequences.

Practice problems

  1. In bias-shifts-threshold.py, change the bias from -4.0 to -6.0. Calculate the new logits and explain why both requests receive lower risk scores even though their input features didn't change.
  2. Extend batch-forward-pass.py to support three input features (add a server CPU load score) while keeping two hidden units. Write out the required shapes of XXX, W1W_1W1​, and b1b_1b1​ before running the code.
  3. In backpropagation-two-layer-mlp.py, change the ground-truth target vector from [0, 1, 0, 1] to all ones [1, 1, 1, 1]. Predict the sign of dZ2\text{d}Z_2dZ2​ and explain which direction the weights must move to reduce the loss.

Compare your solutions with these worked answers.

Answer

  1. Lowering the bias by 2 subtracts exactly 2.0 from every logit: quiet becomes -3.70 (score ≈0.024\approx 0.024≈0.024), and high load becomes 0.60 (score ≈0.646\approx 0.646≈0.646). Because sigmoid increases monotonically with its input, reducing the logit always reduces the output score.
  2. With three features and two hidden units: XXX has shape (4, 3), W1W_1W1​ has shape (2, 3) under PyTorch layout, and b1b_1b1​ has shape (2,). Hidden activations retain shape (4, 2), so W2W_2W2​ (shape (1, 2)) and output logits (shape (4, 1)) remain unchanged.
  3. When targets are all 1.01.01.0, yi=1y_i = 1yi​=1 for all samples. Since predictions y^i<1\hat{y}_i < 1y^​i​<1, the logit error dZ2=1B(Y^−Y)\text{d}Z_2 = \frac{1}{B}(\hat{Y} - Y)dZ2​=B1​(Y^−Y) is strictly negative for every sample. Negative gradients mean that gradient descent updates (W←W−α∇WW \leftarrow W - \alpha \nabla WW←W−α∇W) will add positive increments, pulling logits upward to increase sigmoid scores toward 1.0.

Mastery check

Complete the lesson

Mastery Check

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

1.For input x = [4.0, 5.0], weights w = [0.5, 0.4], and bias b = -2.8, what are the pre-activation logit z and sigmoid output?

Correct answer: z = 1.2 and sigmoid(z) ≈ 0.769

The affine calculation is 4.0(0.5) + 5.0(0.4) - 2.8 = 2.0 + 2.0 - 2.8 = 1.2. Then sigmoid(1.2) = 1 / (1 + exp(-1.2)) ≈ 0.769.

2.Why does stacking two purely linear layers h = W1 x + b1 and y = W2 h + b2 fail to increase the representational power over a single layer?

Correct answer: Composing two affine maps algebraically collapses into y = (W2 W1)x + (W2 b1 + b2), which is just a single affine map.

Because matrix multiplication is associative and affine maps are closed under composition, any sequence of purely linear layers collapses algebraically into one merged linear layer. A non-linear activation is required to represent non-linear decision boundaries.

3.Which statement accurately captures the distinction between ReLU, GELU, and Tanh activations?

Correct answer: Tanh is zero-centered with outputs in (-1, 1); ReLU clips negative values to zero; GELU smoothly weights inputs by standard Gaussian probabilities with a slight negative dip.

Tanh maps to (-1, 1) and is centered at zero. ReLU clips all negative inputs to zero, risking dead neurons. GELU provides smooth probabilistic gating with non-zero curvature across negative inputs.

4.A row-major batch X has shape (4, 3). Weight matrix W1 has shape (5, 3) under PyTorch's (out_features, in_features) convention. Which expression correctly evaluates the layer?

Correct answer: X @ W1.T + b1, producing shape (4, 5)

Each row of X is a 3-dimensional input. Because W1 has shape (5, 3), its transpose W1.T has shape (3, 5). Multiplying X (4, 3) by W1.T (3, 5) yields (4, 5), matching the five output neurons across four batch samples.

5.When using Binary Cross-Entropy loss with sigmoid output y_hat = sigmoid(z2), what is the partial derivative with respect to the output logit z2 for a single sample?

Correct answer: dLoss / dz2 = y_hat - y

The loss derivative dLoss/dy_hat is (y_hat - y) / (y_hat * (1 - y_hat)), and the sigmoid derivative dy_hat/dz2 is y_hat * (1 - y_hat). Multiplying them cancels the denominator completely, leaving y_hat - y.

6.Why does a forward pass implementation fail when evaluating naive sigmoid 1.0 / (1.0 + exp(-z)) on an extreme logit such as z = -1000?

Correct answer: Evaluating exp(-(-1000)) requires exp(1000), which exceeds 64-bit float range and raises an overflow error.

For extreme negative logits, exp(-z) attempts to compute exp(1000), which exceeds float64 max range (~1.8e308). The stable branch uses exp(z) / (1.0 + exp(z)), computing exp(-1000) = 0.0 safely.

7.How many trainable parameters are in a dense network with architecture 3 -> 4 -> 2, assuming all non-input layers include biases?

Correct answer: 26 parameters: (3 * 4 + 4) in the first layer and (4 * 2 + 2) in the second layer

The first layer has 3 * 4 = 12 weights and 4 biases (16). The second layer has 4 * 2 = 8 weights and 2 biases (10). Total trainable parameters = 16 + 10 = 26.

8.What does the Universal Approximation Theorem guarantee about a feed-forward network with one hidden layer?

Correct answer: The network can approximate any continuous function on a compact domain to arbitrary accuracy, provided the hidden layer has sufficient width.

The theorem proves that a wide enough single-hidden-layer network with non-linear activations can approximate any continuous function on compact sets. It does not guarantee that gradient descent will find those weights or that the representation will generalize without overfitting.

8 questions remaining.

Next Step
Continue to CNNs from Scratch

The CodeAssist scorer processed pre-extracted scalar features. An image also arrives as numbers, but they sit on a 2D spatial grid: neighboring pixels form edges and textures, and a dense network that flattens that grid wastes parameters learning the same detector everywhere.

PreviousHypothesis Tests, Intervals, and pass@k
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

On Calibration of Modern Neural Networks

Guo, C., Pleiss, G., Sun, Y., & Weinberger, K. Q. · 2017

https://arxiv.org/abs/1706.04599

Deep Learning.

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

https://www.deeplearningbook.org/

Gaussian Error Linear Units (GELUs).

Hendrycks, D., Gimpel, K. · 2016

https://arxiv.org/abs/1606.08415

Linear

PyTorch Contributors · 2026

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

Learning Representations by Back-Propagating Errors.

Rumelhart, D. E., Hinton, G. E., Williams, R. J. · 1986 · Nature, 323

https://doi.org/10.1038/323533a0

Approximation by Superpositions of a Sigmoidal Function.

Cybenko, G. · 1989 · Mathematics of Control, Signals and Systems, 2(4)

https://doi.org/10.1007/BF02551274

Multilayer Feedforward Networks are Universal Approximators.

Hornik, K., Stinchcombe, M., White, H. · 1989 · Neural Networks, 2(5)

https://doi.org/10.1016/0893-6080(89)90020-8

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.