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
LearnML Algorithms & EvaluationLinear Regression from Scratch
🏛️MediumModel Architecture

Linear Regression from Scratch

Fit assistant latency with a line, derive OLS and gradient descent from first principles, and expose hidden non-linearities through residual diagnostics.

28 min read
Learning path
Step 32 of 196 in the full curriculum
The LLM LifecycleLogistic Regression and Metrics

Personalize this lesson

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

An assistant reads retrieved context before responding. In a small replay table, latency climbs from 118 to 198 milliseconds as context grows from 100 to 500 tokens. A straight line suggests about 220 milliseconds for 600 tokens. How do we find that line, when does an analytical matrix inversion beat iterative steps, and what could a training score of 0.9965 be hiding?

The linear-algebra lesson introduced least-squares projections, and Training & Backpropagation demonstrated iterative parameter updates. Here we'll connect both worlds: formulate the hypothesis function and Mean Squared Error loss, solve the weights with the closed-form Normal Equation, examine its computational complexity against gradient descent, inspect the geometry of ill-conditioned loss bowls, compare Ridge and Lasso regularization, and evaluate the core Gauss-Markov assumptions that tell us when ordinary least squares can be trusted.

Predicting latency from retrieved evidence

For each request replay, the input feature is retrieved text length measured in hundreds of tokens. The target is end-to-end latency in milliseconds. Keeping units visible keeps physical intuition intact: x = 1 means 100 tokens, while y = 118 means 118 ms. These five rows represent synthetic teaching measurements:

Evidence length x (hundreds of tokens)Observed latency y (ms)
1118
2141
3162
4181
5198

Observed latency climbs by roughly 20 ms for each additional 100 tokens. Before setting up a formal solver, we can express our hypothesis with two parameters:

  • The intercept, b, is the line's prediction at x = 0 (baseline latency overhead).
  • The slope, w (or m), is the change in predicted latency for each additional 100 tokens.

For a single observation with feature vector x∈RDx \in \mathbb{R}^Dx∈RD, the linear regression hypothesis function is:

y^=w⊤x+b\hat{y} = w^\top x + by^​=w⊤x+b

With one feature, this simplifies to y^=wx+b\hat{y} = w x + by^​=wx+b. A sensible first candidate line is:

y^=100+20x\hat{y} = 100 + 20xy^​=100+20x

Read this as an operational baseline: start around 100 ms of fixed overhead, then add 20 ms for each 100 tokens of retrieved context. For x = 6 (600 tokens), the candidate line predicts 100+20×6=220100 + 20 \times 6 = 220100+20×6=220 ms.

This is single-feature linear regression. We choose b and w to minimize squared prediction errors across our training dataset, a criterion called ordinary least squares (OLS). The model captures an empirical association; it doesn't prove that retrieved text alone causes this latency response.

An intercept can approximate fixed overhead, but we haven't measured x = 0. Calling the fitted 100 ms a measured server boot cost would extrapolate beyond our evidence. Likewise, predicting latency at x = 6 extends past the observed range, making it an extrapolation.

Before running the inspection script, predict five requests, an evidence range of 1 to 5, and a latency range of 118 to 198 ms.

inspect-the-request-replay.py
1import numpy as np 2 3x = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) 4y = np.array([118.0, 141.0, 162.0, 181.0, 198.0]) 5 6print("requests", len(x)) 7print("evidence_range", (int(x.min()), int(x.max())), "hundreds_of_tokens") 8print("latency_range", (int(y.min()), int(y.max())), "ms")
Output
1requests 5 2evidence_range (1, 5) hundreds_of_tokens 3latency_range (118, 198) ms

Residuals show what the line missed

At x = 1, the candidate line predicts 120 ms while the replay measured 118. At x = 3, it predicts 160 ms while the replay measured 162. A model's predictions become informative when you name and inspect that signed gap. The residual is:

ei=yi−y^ie_i = y_i - \hat{y}_iei​=yi​−y^​i​

On the raw latency scale, small 1-2 ms errors almost disappear against a 100-200 ms span. Isolating residuals on their own dedicated scale reveals both their sign and their structural pattern.

Side-by-side comparison of linear fit and residual analysis for five latency observations. Left panel shows the fitted OLS line y = 100 + 20x against observations with vertical error stems and R-squared 0.9965. Right panel isolates residuals on an enlarged scale: negative at endpoints and positive in the middle, revealing a systematic quadratic bend.
The fitted line looks close to every observation on the latency scale. The residual scale strips away the dominant slope and reveals a non-random parabolic bend: negative at both ends and positive in the middle.

Using the candidate line y^=100+20x\hat{y} = 100 + 20xy^​=100+20x, write out each prediction before combining errors into an aggregate metric:

xActual yPredicted y-hatResidual y - y-hatResidual squared
1118120-24
214114011
316216024
418118011
5198200-24

Signs carry direct diagnostic information:

  • Negative residual: the model predicted too high (the request ran faster than predicted).
  • Positive residual: the model predicted too low (the request ran slower than predicted).
  • Residual near zero: the observation sat directly on the fitted line.

Residual signs show how predictions miss, but the sequence of signs exposes model structure. The residuals run [−2,+1,+2,+1,−2][-2, +1, +2, +1, -2][−2,+1,+2,+1,−2]: negative at both endpoints and positive in the center. That low-high-low arc indicates that the true latency curve bends, even though every individual miss is small.

To summarize performance in a single number, square every residual and compute the mean:

MSE=1N∑i=1N(yi−y^i)2=4+1+4+1+45=2.8 ms2\text{MSE} = \frac{1}{N} \sum_{i=1}^N (y_i - \hat{y}_i)^2 = \frac{4 + 1 + 4 + 1 + 4}{5} = 2.8\,\text{ms}^2MSE=N1​i=1∑N​(yi​−y^​i​)2=54+1+4+1+4​=2.8ms2

That quantity is mean squared error. Squaring stops positive and negative misses from canceling each other out, while penalizing large blunders much more heavily than small discrepancies.

MSE is measured in squared units: milliseconds squared. Taking its square root gives the root mean squared error: RMSE=2.8≈1.67\text{RMSE} = \sqrt{2.8} \approx 1.67RMSE=2.8​≈1.67 milliseconds. That returns the error scale back to the original units of observed latency.

Before interpreting a model's error, compare it with the simplest plausible fallback: predicting the target mean yˉ=160\bar{y} = 160yˉ​=160 ms for every request. That mean baseline produces an MSE of 802.8, whereas the candidate line produces 2.8. This comparison verifies that the evidence feature supplies real predictive signal beyond target averaging.

The coefficient of determination, R-squared, measures the proportion of target variance explained by the model:[1]Reference 1r2_scorehttps://scikit-learn.org/stable/modules/generated/sklearn.metrics.r2_score.html

R2=1−∑(yi−y^i)2∑(yi−yˉ)2=1−144014=1−2.8802.8≈0.9965R^2 = 1 - \frac{\sum (y_i - \hat{y}_i)^2}{\sum (y_i - \bar{y})^2} = 1 - \frac{14}{4014} = 1 - \frac{2.8}{802.8} \approx 0.9965R2=1−∑(yi​−yˉ​)2∑(yi​−y^​i​)2​=1−401414​=1−802.82.8​≈0.9965

An R2R^2R2 of 0 indicates performance matching the constant mean baseline. An R2R^2R2 of 1 indicates zero residual error. Negative values indicate predictions worse than the target mean.

Dividing the numerator and denominator by N=5N = 5N=5 reproduces the ratio 1 - MSE_line / MSE_baseline. Here, 0.99650.99650.9965 describes training fit quality across these five points. It doesn't make the underlying residual curve vanish, nor does it guarantee accuracy on future requests outside this sample.

Why square residuals before averaging them instead of taking their raw sum?

Answer

Taking raw sums lets positive and negative residuals cancel out, making large over-predictions and under-predictions look like zero error. Squaring ensures all errors contribute positively, penalizing large blunders disproportionately.

Before running the evaluation cell, predict residuals [-2, 1, 2, 1, -2], MSE 2.8, baseline MSE 802.8, and R2≈0.9965R^2 \approx 0.9965R2≈0.9965.

score-candidate-line-against-baseline.py
1import numpy as np 2 3x = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) 4y = np.array([118.0, 141.0, 162.0, 181.0, 198.0]) 5pred = 100 + 20 * x 6residual = y - pred 7mse = np.mean(residual ** 2) 8baseline_mse = np.mean((y - y.mean()) ** 2) 9r2 = 1 - mse / baseline_mse 10 11print("predictions", pred.astype(int)) 12print("residuals", residual.astype(int)) 13print("mse", round(float(mse), 1)) 14print("baseline_mse", round(float(baseline_mse), 1)) 15print("r2", round(float(r2), 4))
Output
1predictions [120 140 160 180 200] 2residuals [-2 1 2 1 -2] 3mse 2.8 4baseline_mse 802.8 5r2 0.9965

Why the math becomes matrix multiplication

For a single feature, writing y^=100+20x\hat{y} = 100 + 20xy^​=100+20x is straightforward. When adding cache hit ratios, prompt complexity, and system load, code needs a vectorized representation that scales cleanly.

We construct a design matrix X∈RN×(D+1)X \in \mathbb{R}^{N \times (D+1)}X∈RN×(D+1): each row represents one observation, and each column supplies one model term. An all-ones leading column supplies the intercept term, while subsequent columns hold feature values:

X=[1112131415]X = \begin{bmatrix} 1 & 1 \\ 1 & 2 \\ 1 & 3 \\ 1 & 4 \\ 1 & 5 \end{bmatrix}X=​11111​12345​​

Packing the parameters into a single vector w=[b,m]⊤∈R2w = [b, m]^\top \in \mathbb{R}^2w=[b,m]⊤∈R2, the prediction rule across all NNN requests becomes a matrix-vector product:

y^=Xw\hat{y} = X wy^​=Xw

For this replay table, the optimal weights are w=[100,20]⊤w = [100, 20]^\topw=[100,20]⊤. Vectorization doesn't alter the math; it lets linear algebra libraries execute dot products simultaneously across rows and features.

Check the third request (x=3x = 3x=3), where the design row is [1,3][1, 3][1,3]:

[13][10020]=1×100+3×20=160\begin{bmatrix} 1 & 3 \end{bmatrix} \begin{bmatrix} 100 \\ 20 \end{bmatrix} = 1 \times 100 + 3 \times 20 = 160[1​3​][10020​]=1×100+3×20=160

The leading 1 multiplies the intercept 100, while 3 multiplies the slope 20. Matrix multiplication repeats that calculation across every row simultaneously.

Geometrically, the vector of predictions y^=Xw\hat{y} = X wy^​=Xw is a linear combination of XXX's columns. At the ordinary least squares minimum, Xw∗X w^*Xw∗ is the orthogonal projection of target vector yyy onto the column space col(X)\text{col}(X)col(X). The resulting residual vector e=y−Xw∗e = y - X w^*e=y−Xw∗ is orthogonal to every column of XXX:[2]Reference 2Numerical Linear Algebra.https://people.maths.ox.ac.uk/trefethen/books.html

X⊤(y−Xw∗)=0X^\top (y - X w^*) = 0X⊤(y−Xw∗)=0

Arbitrary candidate weights don't satisfy this orthogonality condition. We'll verify it in code after solving for the weights.

What happens if you remove the all-ones column from the design matrix?

Answer

The model loses its intercept parameter b and can only predict w * x, forcing the regression line through the origin (0, 0). If the true process has a non-zero baseline, this constraint severely degrades fit quality.

Before running the comparison script, predict the result of dropping the intercept: the line will be forced through zero and its MSE will climb past 802.8.

design-matrix-keeps-an-intercept.py
1import numpy as np 2 3x = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) 4y = np.array([118.0, 141.0, 162.0, 181.0, 198.0]) 5 6with_intercept = np.c_[np.ones_like(x), x] 7without_intercept = x[:, None] 8 9w_full, *_ = np.linalg.lstsq(with_intercept, y, rcond=None) 10w_forced, *_ = np.linalg.lstsq(without_intercept, y, rcond=None) 11mse_full = np.mean((with_intercept @ w_full - y) ** 2) 12mse_forced = np.mean((without_intercept @ w_forced - y) ** 2) 13baseline_mse = np.mean((y - y.mean()) ** 2) 14 15print("with_intercept", w_full.round(2), "mse", round(float(mse_full), 2)) 16print("forced_through_zero", w_forced.round(2), "mse", round(float(mse_forced), 2)) 17print("mean_baseline_mse", round(float(baseline_mse), 2)) 18print("forced_worse_than_mean", mse_forced > baseline_mse)
Output
1with_intercept [100. 20.] mse 2.8 2forced_through_zero [47.27] mse 1820.98 3mean_baseline_mse 802.8 4forced_worse_than_mean True

Forcing the line through zero yields an MSE of 1820.98, more than double the mean baseline's error of 802.8. Allowing a fitted offset is essential for these data, even when the offset doesn't isolate an isolated physical source of delay.

The Normal Equation: closed-form ordinary least squares

How do we solve for w∗w^*w∗ without guessing combinations of bbb and mmm? We express the total Mean Squared Error loss in matrix notation and apply matrix calculus:

J(w)=1N∥Xw−y∥22=1N(Xw−y)⊤(Xw−y)J(w) = \frac{1}{N} \|X w - y\|_2^2 = \frac{1}{N} (X w - y)^\top (X w - y)J(w)=N1​∥Xw−y∥22​=N1​(Xw−y)⊤(Xw−y)

Expanding the inner product gives:

J(w)=1N(w⊤X⊤Xw−2y⊤Xw+y⊤y)J(w) = \frac{1}{N} \left( w^\top X^\top X w - 2 y^\top X w + y^\top y \right)J(w)=N1​(w⊤X⊤Xw−2y⊤Xw+y⊤y)

Taking the gradient with respect to the parameter vector www:

∇wJ(w)=2N(X⊤Xw−X⊤y)\nabla_w J(w) = \frac{2}{N} \left( X^\top X w - X^\top y \right)∇w​J(w)=N2​(X⊤Xw−X⊤y)

Some textbooks define the loss with a leading 12N\frac{1}{2N}2N1​ factor to cancel the 2 in the derivative. Both formulations share identical stationary points. Setting the gradient to zero produces the Normal Equation:

X⊤Xw=X⊤yX^\top X w = X^\top yX⊤Xw=X⊤y

When XXX has full column rank, the Gram matrix X⊤X∈R(D+1)×(D+1)X^\top X \in \mathbb{R}^{(D+1) \times (D+1)}X⊤X∈R(D+1)×(D+1) is symmetric positive definite and invertible. Solving yields the closed-form ordinary least squares estimator:[3]Reference 3The Elements of Statistical Learning.https://hastie.su.domains/ElemStatLearn/

w∗=(X⊤X)−1X⊤yw^* = (X^\top X)^{-1} X^\top yw∗=(X⊤X)−1X⊤y

For our latency replay, the normal system is:

[5151555][bm]=[8002600]\begin{bmatrix} 5 & 15 \\ 15 & 55 \end{bmatrix} \begin{bmatrix} b \\ m \end{bmatrix} = \begin{bmatrix} 800 \\ 2600 \end{bmatrix}[515​1555​][bm​]=[8002600​]

Row-reducing gives 55m−3(15m)=10m=2600−2400=20055m - 3(15m) = 10m = 2600 - 2400 = 20055m−3(15m)=10m=2600−2400=200, so m=20m = 20m=20. Back-substitution yields 5b+15(20)=800  ⟹  b=1005b + 15(20) = 800 \implies b = 1005b+15(20)=800⟹b=100. Because the loss is a convex quadratic, this stationary point is the global minimum.

Evaluating the computational complexity reveals important scaling limits:

  1. Forming X⊤XX^\top XX⊤X requires multiplying a (D+1)×N(D+1) \times N(D+1)×N matrix by an N×(D+1)N \times (D+1)N×(D+1) matrix, costing O(ND2)O(N D^2)O(ND2) arithmetic operations.
  2. Inverting or solving the (D+1)×(D+1)(D+1) \times (D+1)(D+1)×(D+1) linear system via Cholesky decomposition (X⊤X=LL⊤X^\top X = L L^\topX⊤X=LL⊤) costs O(D3)O(D^3)O(D3) operations.
  3. Storing X⊤XX^\top XX⊤X requires O(D2)O(D^2)O(D2) memory.

Total time complexity for the Normal Equation is O(ND2+D3)O(N D^2 + D^3)O(ND2+D3). When feature count DDD is small (for example, D≤10,000D \le 10{,}000D≤10,000) and data fits in memory, the closed-form solve provides the exact optimal weights in one analytical step without tuning learning rates or step counts. When DDD reaches hundreds of thousands (such as wide bag-of-words or embedding tables), O(D3)O(D^3)O(D3) inversion and O(D2)O(D^2)O(D2) memory become intractable.

Forming X⊤XX^\top XX⊤X explicitly also squares the condition number of the system: κ(X⊤X)=κ(X)2\kappa(X^\top X) = \kappa(X)^2κ(X⊤X)=κ(X)2. For production code, np.linalg.lstsq solves (X,y)(X, y)(X,y) directly using singular value decomposition (SVD) or QR factorization, avoiding explicit Gram matrix construction while handling rank deficiency cleanly.[2]Reference 2Numerical Linear Algebra.https://people.maths.ox.ac.uk/trefethen/books.html[4]Reference 4numpy.linalg.lstsqhttps://numpy.org/doc/stable/reference/generated/numpy.linalg.lstsq.html

Before running the closed-form script, predict weights [100, 20], MSE 2.8, and an orthogonality check of True.

numpy-implementation-with-the-closed-form.py
1import numpy as np 2 3x = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) 4y = np.array([118.0, 141.0, 162.0, 181.0, 198.0]) 5 6X = np.c_[np.ones_like(x), x] 7 8w_closed = np.linalg.solve(X.T @ X, X.T @ y) 9pred_closed = X @ w_closed 10residuals = y - pred_closed 11 12baseline = np.full_like(y, y.mean()) 13line_mse = np.mean((pred_closed - y) ** 2) 14baseline_mse = np.mean((baseline - y) ** 2) 15r2 = 1 - line_mse / baseline_mse 16 17print("closed_form_weights", w_closed.round(2)) 18print("residuals", residuals.round(2)) 19print("line_mse", line_mse.round(2)) 20print("baseline_mse", baseline_mse.round(2)) 21print("r2", round(r2, 4)) 22print("residual_orthogonal", bool(np.allclose(X.T @ residuals, 0)))
Output
1closed_form_weights [100. 20.] 2residuals [-2. 1. 2. 1. -2.] 3line_mse 2.8 4baseline_mse 802.8 5r2 0.9965 6residual_orthogonal True

The check X.T @ residuals confirms geometric projection: the residuals are orthogonal to the column span of XXX. No linear adjustment of the weights can reduce squared error further.

Next, inspect the conditioning and numerical stability of XXX with np.linalg.lstsq. Expect rank 2, condition number cond_X near 8.37, and cond_XTX near 69.99.

least-squares-with-rank-check.py
1import numpy as np 2 3x = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) 4y = np.array([118.0, 141.0, 162.0, 181.0, 198.0]) 5X = np.c_[np.ones_like(x), x] 6 7w, residual_sum, rank, singular_values = np.linalg.lstsq(X, y, rcond=None) 8pred = X @ w 9 10print("weights", w.round(2)) 11print("rank", rank, "columns", X.shape[1]) 12print("mse", round(float(np.mean((pred - y) ** 2)), 2)) 13print("singular_values", singular_values.round(2)) 14print("cond_X", round(float(np.linalg.cond(X)), 2)) 15print("cond_XTX", round(float(np.linalg.cond(X.T @ X)), 2))
Output
1weights [100. 20.] 2rank 2 columns 2 3mse 2.8 4singular_values [7.69 0.92] 5cond_X 8.37 6cond_XTX 69.99

The 2-norm condition number of XXX is σmax⁡/σmin⁡≈7.69/0.92≈8.37\sigma_{\max} / \sigma_{\min} \approx 7.69 / 0.92 \approx 8.37σmax​/σmin​≈7.69/0.92≈8.37. Squaring that condition number when forming X⊤XX^\top XX⊤X produces 69.9969.9969.99. On five well-behaved rows that solves cleanly, but on ill-conditioned data that squaring can wipe out numerical precision.

What happens when feature columns contain redundant information? Create an artificially flawed feature matrix by appending double_tokens = 2 * tokens. The third column provides zero new information. Predict a singular matrix error from np.linalg.solve, rank 2 for 3 columns from lstsq, and an unchanged MSE of 2.8.

redundant-features-break-normal-equation.py
1import numpy as np 2 3x = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) 4y = np.array([118.0, 141.0, 162.0, 181.0, 198.0]) 5X_bad = np.c_[np.ones_like(x), x, 2 * x] 6 7try: 8 np.linalg.solve(X_bad.T @ X_bad, X_bad.T @ y) 9except np.linalg.LinAlgError: 10 print("normal_equation", "singular") 11else: 12 print("normal_equation", "returned weights; rank still needs checking") 13 14w, _, rank, _ = np.linalg.lstsq(X_bad, y, rcond=None) 15print("lstsq_rank", rank, "columns", X_bad.shape[1]) 16print("lstsq_weights", w.round(2)) 17print("weight_norm", round(float(np.linalg.norm(w)), 2)) 18print("alt_norm_100_20_0", round(float(np.linalg.norm([100.0, 20.0, 0.0])), 2)) 19print("mse", round(float(np.mean((X_bad @ w - y) ** 2)), 2))
Output
1normal_equation singular 2lstsq_rank 2 columns 3 3lstsq_weights [100. 4. 8.] 4weight_norm 100.4 5alt_norm_100_20_0 101.98 6mse 2.8

The prediction remains identical because 4x+8(2x)=20x4x + 8(2x) = 20x4x+8(2x)=20x. However, the division of weight between the collinear columns isn't unique. Among the infinite set of solutions that achieve minimal MSE, np.linalg.lstsq returns the solution with the smallest Euclidean norm ∥w∥2\|w\|_2∥w∥2​.[4]Reference 4numpy.linalg.lstsqhttps://numpy.org/doc/stable/reference/generated/numpy.linalg.lstsq.html Here that produces [100,4,8][100, 4, 8][100,4,8] with norm 100.4, compared to 101.98 for [100,20,0][100, 20, 0][100,20,0]. SVD picks a stable minimum-norm vector, but it can't separate the physical contributions of duplicate features.

Gradient descent finds the same line by walking downhill

When datasets contain millions of observations or thousands of features, constructing and inverting X⊤XX^\top XX⊤X is impractical. Iterative optimization sidesteps matrix inversion by taking successive steps downhill along the negative gradient.

We walk the gradient descent update loop: initialize weights, evaluate predictions, compute derivatives, and adjust weights by a step size called the learning rate (α\alphaα):

w←w−α∇wMSEw \leftarrow w - \alpha \nabla_w \text{MSE}w←w−α∇w​MSE

For Mean Squared Error, the parameter gradients derive from the chain rule:

∇wMSE=2NX⊤(Xw−y)\nabla_w \text{MSE} = \frac{2}{N} X^\top (X w - y)∇w​MSE=N2​X⊤(Xw−y)

Breaking this into separate scalar derivatives for the intercept and feature weights clarifies the arithmetic:

∂MSE∂b=2N∑i=1N(y^i−yi),∂MSE∂wj=2N∑i=1N(y^i−yi)xi,j\frac{\partial \text{MSE}}{\partial b} = \frac{2}{N} \sum_{i=1}^N (\hat{y}_i - y_i), \qquad \frac{\partial \text{MSE}}{\partial w_j} = \frac{2}{N} \sum_{i=1}^N (\hat{y}_i - y_i) x_{i,j}∂b∂MSE​=N2​i=1∑N​(y^​i​−yi​),∂wj​∂MSE​=N2​i=1∑N​(y^​i​−yi​)xi,j​

Starting with initial weights w0=[0,0]⊤w_0 = [0, 0]^\topw0​=[0,0]⊤, every prediction is 0. Because zero sits below every observed latency value, each prediction error y^i−yi\hat{y}_i - y_iy^​i​−yi​ is negative:

Calculation stepIntercept coordinateSlope coordinate
Predictions y^\hat{y}y^​[0, 0, 0, 0, 0][0, 0, 0, 0, 0]
Errors y^−y\hat{y} - yy^​−y[-118, -141, -162, -181, -198][-118, -141, -162, -181, -198]
Feature weighting∑1×(y^i−yi)=−800\sum 1 \times (\hat{y}_i - y_i) = -800∑1×(y^​i​−yi​)=−800∑xi×(y^i−yi)=−2600\sum x_i \times (\hat{y}_i - y_i) = -2600∑xi​×(y^​i​−yi​)=−2600
Multiply by 2/N=2/52 / N = 2 / 52/N=2/5gradb=−320\text{grad}_b = -320gradb​=−320gradm=−1040\text{grad}_m = -1040gradm​=−1040

The slope gradient magnitude is over three times larger than the intercept gradient because higher evidence values pull more aggressively on the slope derivative.

With learning rate α=0.05\alpha = 0.05α=0.05, the first parameter update is:

w1=[00]−0.05[−320−1040]=[1652]w_1 = \begin{bmatrix} 0 \\ 0 \end{bmatrix} - 0.05 \begin{bmatrix} -320 \\ -1040 \end{bmatrix} = \begin{bmatrix} 16 \\ 52 \end{bmatrix}w1​=[00​]−0.05[−320−1040​]=[1652​]

The slope immediately overshoots its target of 20, landing at 52. Yet total MSE drops from 26402.8 to 2194.8. Optimization progress reflects joint loss reduction across the parameter space, not whether every individual coordinate moves monotonically toward its final value.

Each iteration of full-batch gradient descent requires computing XwX wXw (O(ND)O(N D)O(ND) operations) and X⊤(y^−y)X^\top (\hat{y} - y)X⊤(y^​−y) (O(ND)O(N D)O(ND) operations). For KKK iterations, total runtime is O(KND)O(K N D)O(KND) while requiring only O(D)O(D)O(D) additional storage for the gradient vector. No D×DD \times DD×D matrix is ever constructed or inverted.

Before running the single-step verification, predict gradient [-320, -1040], next weights [16, 52], and an MSE drop from 26402.8 to 2194.8.

verify-the-first-gradient-step.py
1import numpy as np 2 3x = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) 4y = np.array([118.0, 141.0, 162.0, 181.0, 198.0]) 5X = np.c_[np.ones_like(x), x] 6w = np.zeros(2) 7 8error = X @ w - y 9grad = (2 / len(X)) * X.T @ error 10w_next = w - 0.05 * grad 11 12print("gradient", grad) 13print("next_weights", w_next) 14print("old_mse", round(float(np.mean(error ** 2)), 1)) 15print("new_mse", round(float(np.mean((X @ w_next - y) ** 2)), 1))
Output
1gradient [ -320. -1040.] 2next_weights [16. 52.] 3old_mse 26402.8 4new_mse 2194.8

Now run 1000 steps of gradient descent and verify convergence toward the OLS solution [100,20][100, 20][100,20].

gradient-descent-line-fit.py
1import numpy as np 2 3x = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) 4y = np.array([118.0, 141.0, 162.0, 181.0, 198.0]) 5X = np.c_[np.ones_like(x), x] 6 7w = np.zeros(2) 8lr = 0.05 9 10for step in range(1000): 11 pred = X @ w 12 error = pred - y 13 grad = (2 / len(X)) * (X.T @ error) 14 w -= lr * grad 15 16 if step in {0, 99, 999}: 17 loss = np.mean((X @ w - y) ** 2) 18 print(f"after_step={step + 1} loss={loss:.2f} w={w.round(2)}") 19 20w_direct, *_ = np.linalg.lstsq(X, y, rcond=None) 21print("matches_direct", bool(np.allclose(w, w_direct, atol=1e-4, rtol=0)))
Output
1after_step=1 loss=2194.80 w=[16. 52.] 2after_step=100 loss=49.09 w=[84.05 24.42] 3after_step=1000 loss=2.80 w=[100. 20.] 4matches_direct True

After 1000 iterations, the iterative weights match the closed-form solution to within 10−410^{-4}10−4, confirming that both solvers converge to the identical least-squares minimum.

Convex loss bowl geometry and step size selection

Why did the first gradient step jump past the optimal slope of 20 to 52, and why did later iterations take dozens of steps to crawl from [84.05,24.42][84.05, 24.42][84.05,24.42] to [100,20][100, 20][100,20]? The answer lies in the quadratic geometry of the loss surface.

The second-derivative matrix (Hessian) of the Mean Squared Error loss is constant:

H=∇w2MSE=2NX⊤XH = \nabla_w^2 \text{MSE} = \frac{2}{N} X^\top XH=∇w2​MSE=N2​X⊤X

Because X⊤XX^\top XX⊤X is symmetric and positive definite when XXX has full rank, all eigenvalues of HHH are strictly positive: λi>0\lambda_i > 0λi​>0. This confirms that the MSE loss surface is a strictly convex paraboloid bowl with a single global minimum. Local minima and saddle points don't exist.

MSE contours in intercept-slope space alongside a mechanism comparison. Left panel shows the convex quadratic bowl with condition number 70: gradient descent starts at [0, 0], overshoots to [16, 52] on step 1, then follows the curved valley to the minimum at [100, 20]. Right panel compares the Normal Equation one-step jump with iterative Gradient Descent across compute complexity, memory, and scaling regimes.
The loss surface forms an eccentric quadratic bowl. High curvature along the steep axis causes the initial step to overshoot to slope 52, while low curvature along the valley floor requires hundreds of iterations to traverse.

The eigenvalues of HHH, λmax⁡\lambda_{\max}λmax​ and λmin⁡\lambda_{\min}λmin​, govern the surface curvature along its principal axes:

  • The condition number of the Hessian is κ=λmax⁡λmin⁡\kappa = \frac{\lambda_{\max}}{\lambda_{\min}}κ=λmin​λmax​​.
  • For our replay data, the Hessian matrix is [26622]\begin{bmatrix} 2 & 6 \\ 6 & 22 \end{bmatrix}[26​622​], yielding λmax⁡≈23.66\lambda_{\max} \approx 23.66λmax​≈23.66 and λmin⁡≈0.34\lambda_{\min} \approx 0.34λmin​≈0.34.
  • The condition number is κ≈23.660.34≈70\kappa \approx \frac{23.66}{0.34} \approx 70κ≈0.3423.66​≈70.

A condition number of 70 means the loss bowl is 70 times steeper along its sharpest axis than along its flattest floor. The level contours are elongated ellipses rather than circles. Gradients point almost perpendicular to the long valley floor, forcing gradient descent to oscillate across the steep walls while creeping slowly along the base.

This geometry determines the mathematical bound for learning rate stability. For gradient descent on a quadratic surface to converge without diverging, the step size must satisfy:

0<α<2λmax⁡0 < \alpha < \frac{2}{\lambda_{\max}}0<α<λmax​2​

For our replay, 2λmax⁡≈223.66≈0.0845\frac{2}{\lambda_{\max}} \approx \frac{2}{23.66} \approx 0.0845λmax​2​≈23.662​≈0.0845. Our chosen learning rate of 0.050.050.05 satisfies this stability bound (0.05<0.08450.05 \lt 0.08450.05<0.0845), ensuring eventual convergence. If we increase α\alphaα to 0.20.20.2, the update step exceeds the valley diameter, causing updates to bounce outward with exponentially growing loss.

The optimal theoretical fixed step size for pure quadratic descent is α∗=2λmax⁡+λmin⁡\alpha^* = \frac{2}{\lambda_{\max} + \lambda_{\min}}α∗=λmax​+λmin​2​, which yields an asymptotic convergence rate of κ−1κ+1\frac{\kappa - 1}{\kappa + 1}κ+1κ−1​. When features are standardized (zero mean, unit variance), the columns of XXX become nearly orthogonal, reducing κ→1\kappa \to 1κ→1 and transforming narrow ravines into circular bowls where gradient descent reaches the minimum rapidly.

Test what happens when the stability bound is violated by setting α=0.2\alpha = 0.2α=0.2. Predict an exploding loss sequence where each step overshoots further away from the minimum.

detect-an-unstable-learning-rate.py
1import numpy as np 2 3x = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) 4y = np.array([118.0, 141.0, 162.0, 181.0, 198.0]) 5X = np.c_[np.ones_like(x), x] 6w = np.zeros(2) 7losses = [] 8 9for _ in range(5): 10 error = X @ w - y 11 losses.append(float(np.mean(error ** 2))) 12 grad = (2 / len(X)) * X.T @ error 13 w -= 0.2 * grad 14 15print("losses", [round(loss, 1) for loss in losses]) 16print("loss_increased", losses[-1] > losses[0])
Output
1losses [26402.8, 349474.8, 4852473.8, 67584328.5, 941482657.6] 2loss_increased True

Within five iterations, loss explodes from 2.6×1042.6 \times 10^42.6×104 to 9.4×1089.4 \times 10^89.4×108. Numerical divergence is an optimization step-size failure, not evidence that linear regression is unsuited for the problem.

Stabilizing ill-conditioned fits with Ridge and Lasso

When feature columns are strongly correlated, sample sizes are limited, or feature counts exceed observation counts (D>ND > ND>N), ordinary least squares suffers from severe variance. Small shifts in training observations produce wild swings in estimated coefficients. Regularization counters this by introducing a parameter penalty into the optimization objective.[3]Reference 3The Elements of Statistical Learning.https://hastie.su.domains/ElemStatLearn/

Ridge Regression (L2 regularization) adds the sum of squared feature weights to the loss:

Jridge(w)=1N∥Xw−y∥22+λ∥w1:D∥22J_{\text{ridge}}(w) = \frac{1}{N} \|X w - y\|_2^2 + \lambda \|w_{1:D}\|_2^2Jridge​(w)=N1​∥Xw−y∥22​+λ∥w1:D​∥22​

Setting the gradient to zero yields an analytical closed-form solution:

wridge∗=(X⊤X+λNI)−1X⊤yw^*_{\text{ridge}} = \left( X^\top X + \lambda N I \right)^{-1} X^\top ywridge∗​=(X⊤X+λNI)−1X⊤y

Adding λNI\lambda N IλNI shifts every eigenvalue of X⊤XX^\top XX⊤X upward by +λN>0+\lambda N > 0+λN>0. Even when X⊤XX^\top XX⊤X is strictly rank-deficient or singular, X⊤X+λNIX^\top X + \lambda N IX⊤X+λNI is guaranteed to be strictly positive definite and invertible. Ridge regression smoothly shrinks coefficients toward zero, stabilizing correlated parameters and reducing prediction variance at the expense of introducing slight bias.

Lasso Regression (L1 regularization) penalizes the sum of absolute feature weights:

Jlasso(w)=1N∥Xw−y∥22+λ∥w1:D∥1=1N∥Xw−y∥22+λ∑j=1D∣wj∣J_{\text{lasso}}(w) = \frac{1}{N} \|X w - y\|_2^2 + \lambda \|w_{1:D}\|_1 = \frac{1}{N} \|X w - y\|_2^2 + \lambda \sum_{j=1}^D |w_j|Jlasso​(w)=N1​∥Xw−y∥22​+λ∥w1:D​∥1​=N1​∥Xw−y∥22​+λj=1∑D​∣wj​∣

Because the absolute value function has a non-differentiable corner at zero, Lasso can't be solved with standard matrix inversion. Instead, it's solved via subgradient optimization or coordinate descent using the soft-thresholding operator:

Sλ(z)=sign(z)max⁡(∣z∣−λ,0)S_{\lambda}(z) = \text{sign}(z) \max(|z| - \lambda, 0)Sλ​(z)=sign(z)max(∣z∣−λ,0)

The geometry of the L1 penalty explains its distinctive behavior. In parameter space, the L1 constraint boundary forms a diamond (cross-polytope) with sharp vertices positioned along the coordinate axes. The elliptical contours of the MSE loss bowl typically contact these sharp corners first. When an elliptical contour touches a corner, the corresponding parameter is driven exactly to zero. Lasso induces sparsity, performing automated feature selection by eliminating uninformative inputs.

PropertyOrdinary least squaresRidge (L2 penalty)Lasso (L1 penalty)
Objective penaltyNoneλ∥w∥22\lambda \|w\|_2^2λ∥w∥22​λ∥w∥1\lambda \|w\|_1λ∥w∥1​
Closed-form solution(X⊤X)−1X⊤y(X^\top X)^{-1} X^\top y(X⊤X)−1X⊤y(X⊤X+λNI)−1X⊤y(X^\top X + \lambda N I)^{-1} X^\top y(X⊤X+λNI)−1X⊤yNone (coordinate descent)
Invertibility guaranteeFails if X⊤XX^\top XX⊤X singularGuaranteed invertibleSolved iteratively
Coefficient behaviorUnconstrainedSmooth shrinkageSparse (exact zeros)
Primary use caseFull-rank, low DDD dataMulticollinear featuresFeature selection, high DDD

Gauss-Markov foundations and residual diagnostic checks

Why has ordinary least squares remained the default baseline for continuous regression for two centuries? The theoretical foundation rests on the Gauss-Markov Theorem.[3]Reference 3The Elements of Statistical Learning.https://hastie.su.domains/ElemStatLearn/

The theorem states that under five core assumptions, the OLS estimator w^OLS\hat{w}_{\text{OLS}}w^OLS​ is BLUE: the Best Linear Unbiased Estimator. Among all linear unbiased estimators, OLS achieves the minimum sampling variance.

The five Gauss-Markov assumptions establish that guarantee:

  1. Linearity in parameters: The true data-generating process takes the form y=Xw+ϵy = X w + \epsilony=Xw+ϵ. Features can undergo non-linear transformations (such as x2x^2x2 or log⁡x\log xlogx), but parameters must enter linearly.
  2. Strict exogeneity (Zero conditional mean): E[ϵ∣X]=0\mathbb{E}[\epsilon \mid X] = 0E[ϵ∣X]=0. The noise carries zero expected value regardless of feature values. This assumption rules out omitted variable bias and measurement errors in XXX.
  3. No perfect multicollinearity (Full rank): rank(X)=D+1≤N\text{rank}(X) = D+1 \le Nrank(X)=D+1≤N. No feature is an exact linear combination of other features, ensuring X⊤XX^\top XX⊤X is invertible.
  4. Spherical error variance:
    • Homoscedasticity: Var(ϵi∣X)=σ2\text{Var}(\epsilon_i \mid X) = \sigma^2Var(ϵi​∣X)=σ2 for all iii. Noise variance remains constant across all feature levels. If latency variance spreads out for longer prompts, errors are heteroscedastic.
    • No autocorrelation: Cov(ϵi,ϵj∣X)=0\text{Cov}(\epsilon_i, \epsilon_j \mid X) = 0Cov(ϵi​,ϵj​∣X)=0 for i≠ji \neq ji=j. Individual observations must be independent. If replays come from sequential cache warming, residuals correlate across rows.
  5. Residual normality: ϵ∣X∼N(0,σ2I)\epsilon \mid X \sim \mathcal{N}(0, \sigma^2 I)ϵ∣X∼N(0,σ2I).

A common misconception is that OLS requires features or errors to follow a normal distribution. Normality is not required for the Gauss-Markov BLUE guarantee, nor is it needed for OLS consistency. By the Central Limit Theorem, parameter estimates become asymptotically normal in large samples regardless of error distributions. Normality is required only for exact small-sample hypothesis testing (ttt-tests, FFF-tests) and finite-sample prediction intervals.

Our five-row latency replay highlights where linear assumptions begin to strain:

  • The residuals [−2,+1,+2,+1,−2][-2, +1, +2, +1, -2][−2,+1,+2,+1,−2] form an inverted parabola. This non-random structure violates the zero conditional mean assumption across local feature slices (E[ϵ∣x=3]≈+2\mathbb{E}[\epsilon \mid x=3] \approx +2E[ϵ∣x=3]≈+2, while E[ϵ∣x=1]≈−2\mathbb{E}[\epsilon \mid x=1] \approx -2E[ϵ∣x=1]≈−2).
  • The macro fit looks stellar (R2=0.9965R^2 = 0.9965R2=0.9965), but the residual diagnostic confirms that a straight line misses systematic curvature.

Evaluation starts on unseen requests

Training metrics measure descriptive fit on data the solver has already inspected. They don't certify how well a model generalizes to fresh traffic. A production latency service requires an explicit train, validation, and test split containing requests from evidence lengths, prompt topics, and traffic volumes the model never saw during fitting.

To demonstrate data separation mechanics, refit using only the first four evidence lengths (x∈{1,2,3,4}x \in \{1, 2, 3, 4\}x∈{1,2,3,4}) and score the held-out fifth request (x=5x = 5x=5). Because we have already inspected all five points, this retrospective split provides an illustrative partition rather than untouched test evidence. In real projects, hold out test partitions before exploratory analysis.

Predict weights [98, 21], a prediction of 203 ms for x = 5, and an error of -5 ms.

evaluate-a-held-out-request.py
1import numpy as np 2 3x_train = np.array([1.0, 2.0, 3.0, 4.0]) 4y_train = np.array([118.0, 141.0, 162.0, 181.0]) 5x_test = np.array([5.0]) 6y_test = np.array([198.0]) 7 8X_train = np.c_[np.ones_like(x_train), x_train] 9X_test = np.c_[np.ones_like(x_test), x_test] 10w, *_ = np.linalg.lstsq(X_train, y_train, rcond=None) 11prediction = X_test @ w 12baseline = np.full_like(y_test, y_train.mean()) 13 14print("train_weights", w.round(2)) 15print("test_prediction_ms", prediction.round(2)) 16print("test_error_ms", (y_test - prediction).round(2)) 17print("line_squared_error", round(float(np.mean((prediction - y_test) ** 2)), 2)) 18print("baseline_squared_error", round(float(np.mean((baseline - y_test) ** 2)), 2))
Output
1train_weights [98. 21.] 2test_prediction_ms [203.] 3test_error_ms [-5.] 4line_squared_error 25.0 5baseline_squared_error 2256.25

The baseline predicts the training mean (150.5150.5150.5 ms) without inspecting the test observation. One scored point can't establish typical future variance or compute a meaningful test R2R^2R2, but it demonstrates data array separation: the training split determines parameters, while held-out data evaluates predictive accuracy.

Match a trusted implementation

Writing linear regression from scratch illuminates mathematical mechanics. Once from-scratch implementations work, verify numerical output against a maintained production library. scikit-learn's LinearRegression solves ordinary least squares, learning an intercept by default when fed a 2D feature matrix of shape (5, 1) and a target vector of shape (5,).[5]Reference 5LinearRegressionhttps://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html

Predict exact numerical agreement before running the verification: intercept 100.0, slope 20.0, MSE 2.8, and R2≈0.9965R^2 \approx 0.9965R2≈0.9965.

compare-with-scikit-learn.py
1import numpy as np 2from sklearn.linear_model import LinearRegression 3from sklearn.metrics import mean_squared_error, r2_score 4 5x = np.array([1.0, 2.0, 3.0, 4.0, 5.0]).reshape(-1, 1) 6y = np.array([118.0, 141.0, 162.0, 181.0, 198.0]) 7 8model = LinearRegression().fit(x, y) 9pred = model.predict(x) 10 11print("intercept", round(float(model.intercept_), 2)) 12print("slope", np.round(model.coef_, 2)) 13print("mse", round(float(mean_squared_error(y, pred)), 2)) 14print("r2", round(float(r2_score(y, pred)), 4))
Output
1intercept 100.0 2slope [20.] 3mse 2.8 4r2 0.9965

Matching intercept, slope, MSE, and R2R^2R2 confirms that our custom NumPy implementation executes the exact mathematical operations of standard OLS libraries.

Operating contracts and extrapolation boundaries

A fitted line predicts 220 ms for x = 6, but a single scalar point estimate provides zero indication of variability. A prediction interval specifies the expected range for an individual future request, incorporating both model parameter uncertainty and observation noise variance σ2\sigma^2σ2. A confidence interval captures uncertainty around the estimated mean response. Neither transforms five training replays into a formal service-level agreement.

Deploying regression models requires explicit operating boundaries:

  1. Record feature units, allowable training ranges (1≤x≤51 \le x \le 51≤x≤5), condition numbers, and held-out error distributions alongside saved weights.
  2. If an incoming request arrives with x = 50 after training on 1 through 5, route it to an explicit fallback path rather than blindly trusting linear extrapolation. Beyond the training domain, physical dynamics often transition into different regimes (such as memory swapping or context truncation).

Common failure patterns

Low training error doesn't mean an operating model is sound. Inspect these characteristic failure patterns and diagnostic remedies:

SymptomRoot causeDiagnostic fix
Residuals display a systematic curve (e.g. low-high-low)True relationship is non-linearAdd polynomial or log features, or transition to spline/tree models
Loss bounces violently or explodes during gradient descentLearning rate α>2/λmax⁡\alpha > 2/\lambda_{\max}α>2/λmax​, or features have mismatched scalesLower learning rate below stability bound; standardize features to zero mean and unit variance
Normal Equation raises singular matrix error, or weights swing wildlyMulticollinear features cause rank deficiency in X⊤XX^\top XX⊤XInspect condition number and rank; eliminate duplicate features or apply Ridge regularization (X⊤X+λNIX^\top X + \lambda N IX⊤X+λNI)
Training R2R^2R2 is near 1.0, but test set errors are massiveOverfitting or target leakage into feature pipelinesEnforce strict train-test splits before feature engineering; verify cross-validation performance
A single high-value observation drags the fitted line toward itselfSquared loss penalizes large outliers quadraticallyInspect outlier validity; consider Huber loss or median-based robust regression

Try it yourself

Run these focused experiments using the NumPy code above. Turn each question into a measurable output by printing updated weights, losses, and residual signatures:

ExperimentTarget observation
Predict latency for x = 6 before running code220 ms=100+20×6220\text{ ms} = 100 + 20 \times 6220 ms=100+20×6. This is an extrapolation past training support, not a measured latency guarantee.
Modify the final target from 198 to 240 and refitWeights shift to [83.2,28.4][83.2, 28.4][83.2,28.4], and the x=6x = 6x=6 prediction jumps to 253.6253.6253.6 ms. One extreme observation alters slope across the entire input domain.
Set lr = 0.2 in the gradient loopLoss explodes rapidly because α>2/λmax⁡\alpha > 2/\lambda_{\max}α>2/λmax​, confirming step-size instability.
Drop the intercept column and fit using x aloneForcing the line through zero inflates MSE from 2.82.82.8 to 1820.981820.981820.98, performing worse than the simple target mean baseline.
Add collinear column 2 * x and call np.linalg.solveThe system raises a singular matrix error, while lstsq returns the minimum-norm solution vector [100,4,8][100, 4, 8][100,4,8].
Standardize x to zero mean and unit varianceThe Hessian condition number drops to κ≈1\kappa \approx 1κ≈1, allowing gradient descent to converge without transverse oscillation.
Complete the lesson

Mastery Check

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

1.For a dataset with N observations and D features, what is the computational time complexity of computing the Normal Equation solution w* = (X^T X)^(-1) X^T y?

Correct answer: O(N D^2 + D^3), dominated by forming the Gram matrix and inverting the D x D system.

Forming X^T X takes O(N D^2) operations. Inverting the D x D matrix takes O(D^3). Matrix-vector products take O(N D + D^2). Total time: O(N D^2 + D^3).

2.A model achieves a training R-squared of 0.9965 on five observations, but its residuals are [-2, +1, +2, +1, -2]. What diagnostic conclusion follows?

Correct answer: The straight-line model misses systematic curvature, violating the zero conditional mean assumption across local feature slices.

High R-squared can occur in non-linear data with small sample size. An inverted-U pattern in residuals indicates severe underfitting of quadratic curvature, violating E[epsilon|x] = 0.

3.Why does Ridge regression (L2 regularization) guarantee an invertible system (X^T X + lambda * N * I) even when X has collinear columns?

Correct answer: Adding lambda * N * I shifts all eigenvalues of X^T X upward by lambda * N > 0, ensuring the matrix is strictly positive definite.

X^T X is positive semi-definite (eigenvalues >= 0). Adding lambda * N * I shifts all eigenvalues up by lambda * N > 0, making the smallest eigenvalue strictly positive and the matrix invertible.

4.How does Lasso regression (L1 regularization) induce sparse weight vectors where uninformative feature weights become exactly zero?

Correct answer: The L1 constraint boundary has sharp corners on the coordinate axes, which elliptical MSE loss contours typically contact first.

The L1 ball is a diamond/octahedron with corners where coordinates are zero. Elliptical level sets of MSE loss expand from the unconstrained minimum and almost always contact a sharp corner first.

5.For gradient descent on a quadratic MSE loss surface with Hessian maximum eigenvalue lambda_max, what is the stability requirement for learning rate alpha?

Correct answer: 0 < alpha < 2 / lambda_max; exceeding this bound causes updates to overshoot and diverge.

In 1D, f(w) = 0.5 * lambda * w^2. Update: w_{k+1} = (1 - alpha * lambda) * w_k. Stability requires |1 - alpha * lambda| < 1, which means 0 < alpha < 2 / lambda_max.

6.What role does the condition number kappa = lambda_max / lambda_min of the Hessian matrix play during gradient descent optimization?

Correct answer: It determines the eccentricity of the loss contours; when kappa >> 1, gradients oscillate across steep walls while crawling slowly along the floor.

High condition number creates elongated elliptical contours. Gradient descent bounces back and forth across the steep dimension while making minimal progress along the flat dimension.

7.According to the Gauss-Markov theorem, what does it mean that ordinary least squares is 'BLUE'?

Correct answer: It is the Best Linear Unbiased Estimator: it achieves the minimum sampling variance among all linear unbiased estimators.

BLUE stands for Best Linear Unbiased Estimator. Among all estimators that are linear combinations of y and unbiased, OLS has the lowest covariance matrix (minimum variance).

8.Is normally distributed noise required for the Gauss-Markov theorem or for OLS consistency?

Correct answer: No; normality is required only for exact small-sample hypothesis testing and confidence intervals, not for BLUE or consistency.

Gauss-Markov requires only E[e]=0, Homoscedasticity, and No correlation. Consistency requires E[x*e]=0 and full rank. Normality is needed only for exact t-tests and F-tests in small samples.

9.You construct a design matrix with features x and 2*x. Calling np.linalg.solve raises a LinAlgError, but np.linalg.lstsq returns weights [100, 4, 8]. What is happening?

Correct answer: The duplicate column creates exact multicollinearity, making X^T X singular; lstsq uses SVD to return the unique minimum-norm solution.

Collinear columns make X^T X non-invertible (determinant 0). solve() fails, but lstsq() computes the Moore-Penrose pseudoinverse via SVD, picking the solution with minimal L2 norm.

10.Why is predicting latency at x = 50 tokens using a model trained on x in [1, 5] an operational hazard?

Correct answer: It represents extreme extrapolation; unobserved physical effects like memory paging or context limits can invalidate the linear assumption.

Linear models extrapolate without uncertainty bounds. Real hardware systems have non-linearities (KV cache allocations, OOMs, context limits) that do not appear in small inputs.

10 questions remaining.

Next Step
Continue to Logistic Regression and Metrics

Linear regression fits continuous numbers like latency. Classification takes those linear scores, maps them through a logistic sigmoid into probabilities, and optimizes cross-entropy loss to decide whether a request succeeds or fails.

PreviousThe LLM Lifecycle
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

r2_score

scikit-learn Developers · 2026

https://scikit-learn.org/stable/modules/generated/sklearn.metrics.r2_score.html

Numerical Linear Algebra.

Trefethen, L. N., & Bau, D. · 1997 · SIAM

https://people.maths.ox.ac.uk/trefethen/books.html

The Elements of Statistical Learning.

Hastie, T., Tibshirani, R., Friedman, J. · 2009

https://hastie.su.domains/ElemStatLearn/

numpy.linalg.lstsq

NumPy Developers · 2026

https://numpy.org/doc/stable/reference/generated/numpy.linalg.lstsq.html

LinearRegression

scikit-learn Developers · 2026

https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.