VivaPrep
← Jaber Notes

Jaber Notes · 3 of 16

Linear Models

OLS, Ridge, Lasso, Elastic Net, logistic & softmax regression, GLMs.

The workhorses, derived properly: normal equations and Gauss-Markov, Ridge via SVD, Lasso soft-thresholding, the logistic cross-entropy gradient, and the convexity proof that makes it all trainable.

Visual reference

Sigmoid function

10x
Any real number in, a value between 0 and 1 out. Far from zero, the curve flattens — that flat region is where gradients vanish.

L1 vs L2 constraint regions

L1 (diamond)L2 (circle)
The loss contours (ellipses) expand outward from the unregularized optimum until they touch the constraint region. L1's diamond has corners on the axes — the touch point often lands exactly on a corner, zeroing a weight. L2's circle has no corners — the touch point shrinks weights smoothly instead.
Full derivations from loss function through closed-form solution, gradient, and probabilistic interpretation.

Table of contents

  1. Linear regression (OLS)
  2. Ridge regression
  3. Lasso regression
  4. Elastic Net
  5. Logistic regression
  6. Multiclass: softmax regression
  7. Generalized linear models (overview)
  8. Code examples

1. Linear regression (OLS)

1.1 Model

Given features xRp\mathbf{x} \in \mathbb{R}^p (we prepend 1 for intercept so xRp+1\mathbf{x} \in \mathbb{R}^{p+1}) and scalar target yRy \in \mathbb{R}:

y^=wx=w0+w1x1++wpxp.\hat{y} = \mathbf{w}^\top \mathbf{x} = w_0 + w_1 x_1 + \cdots + w_p x_p.

Stacked over nn samples: y^=Xw\hat{\mathbf{y}} = \mathbf{X}\mathbf{w}, where XRn×(p+1)\mathbf{X} \in \mathbb{R}^{n \times (p+1)}.

1.2 Objective: residual sum of squares

RSS(w)=yXw22=(yXw)(yXw).\text{RSS}(\mathbf{w}) = \|\mathbf{y} - \mathbf{X}\mathbf{w}\|_2^2 = (\mathbf{y}-\mathbf{X}\mathbf{w})^\top(\mathbf{y}-\mathbf{X}\mathbf{w}).

Expanded:

RSS=yy2wXy+wXXw.\text{RSS} = \mathbf{y}^\top\mathbf{y} - 2\mathbf{w}^\top\mathbf{X}^\top\mathbf{y} + \mathbf{w}^\top\mathbf{X}^\top\mathbf{X}\mathbf{w}.

1.3 Normal equations (closed-form solution)

Gradient w.r.t. w\mathbf{w} (using matrix calculus identities):

wRSS=2Xy+2XXw.\nabla_\mathbf{w} \text{RSS} = -2\mathbf{X}^\top\mathbf{y} + 2\mathbf{X}^\top\mathbf{X}\mathbf{w}.

Setting to zero:

XXw=Xy.\mathbf{X}^\top\mathbf{X}\mathbf{w} = \mathbf{X}^\top\mathbf{y}.

These are the normal equations. If XX\mathbf{X}^\top\mathbf{X} is invertible (columns of X\mathbf{X} linearly independent):

w=(XX)1Xy.\boxed{\mathbf{w}^\star = (\mathbf{X}^\top\mathbf{X})^{-1}\mathbf{X}^\top\mathbf{y}.}

The Hessian 2RSS=2XX0\nabla^2 \text{RSS} = 2\mathbf{X}^\top\mathbf{X} \succeq 0 (PSD) confirms this is a global minimum. Strictly so if X\mathbf{X} has full column rank.

1.4 Geometric interpretation

y^=Xw=X(XX)1Xy=:Hy\hat{\mathbf{y}} = \mathbf{X}\mathbf{w}^\star = \mathbf{X}(\mathbf{X}^\top\mathbf{X})^{-1}\mathbf{X}^\top\mathbf{y} =: \mathbf{H}\mathbf{y}.

H=X(XX)1X\mathbf{H} = \mathbf{X}(\mathbf{X}^\top\mathbf{X})^{-1}\mathbf{X}^\top is the hat/projection matrix. It is idempotent (H2=H\mathbf{H}^2=\mathbf{H}) and orthogonal projection onto column space of X\mathbf{X}.

Residual vector: r=yy^=(IH)y\mathbf{r} = \mathbf{y}-\hat{\mathbf{y}} = (\mathbf{I}-\mathbf{H})\mathbf{y}, which is orthogonal to column space of X\mathbf{X}: Xr=0\mathbf{X}^\top\mathbf{r} = \mathbf{0}.

1.5 MLE derivation

Assume yi=xiw+εiy_i = \mathbf{x}_i^\top\mathbf{w} + \varepsilon_i, εiN(0,σ2)\varepsilon_i \sim \mathcal{N}(0,\sigma^2) i.i.d.

Log-likelihood:

(w)=n2log(2πσ2)12σ2i=1n(yixiw)2.\ell(\mathbf{w}) = -\frac{n}{2}\log(2\pi\sigma^2) - \frac{1}{2\sigma^2}\sum_{i=1}^n(y_i - \mathbf{x}_i^\top\mathbf{w})^2.

Maximizing over w\mathbf{w}: same as minimizing (yixiw)2\sum(y_i-\mathbf{x}_i^\top\mathbf{w})^2 = OLS. So OLS = MLE under Gaussian noise assumption.

1.6 Gauss-Markov theorem

OLS estimator is the Best Linear Unbiased Estimator (BLUE): among all linear estimators that are unbiased, OLS has minimum variance.

Conditions: E[ε]=0\mathbb{E}[\boldsymbol{\varepsilon}]=\mathbf{0}, Var(ε)=σ2I\text{Var}(\boldsymbol{\varepsilon})=\sigma^2\mathbf{I} (homoscedastic, uncorrelated errors).

1.7 When OLS fails

ProblemCauseFix
XX\mathbf{X}^\top\mathbf{X} singularMulticollinear featuresRidge, Lasso, drop features
Non-linear relationshipModel misspecificationFeature engineering, kernel methods
Heteroscedastic errorsVariance changes with x\mathbf{x}Weighted LS, transform yy
OutliersHeavy-tailed noiseRobust regression (Huber loss)

1.8 Evaluating regression models

Mean Squared Error (MSE): 1n(yiy^i)2\frac{1}{n}\sum(y_i-\hat{y}_i)^2. Same units as y2y^2. Root MSE (RMSE): MSE\sqrt{\text{MSE}}. Same units as yy. Mean Absolute Error (MAE): 1nyiy^i\frac{1}{n}\sum|y_i-\hat{y}_i|. More robust to outliers. R² (coefficient of determination):

R2=1RSSTSS=1(yiy^i)2(yiyˉ)2(,1].R^2 = 1 - \frac{\text{RSS}}{\text{TSS}} = 1 - \frac{\sum(y_i-\hat{y}_i)^2}{\sum(y_i-\bar{y})^2} \in (-\infty, 1].

Fraction of variance explained. R2=1R^2=1 is perfect fit. Adding irrelevant features can only increase (or keep equal) R2R^2 — use adjusted R2R^2 to account for model complexity.


2. Ridge regression

2.1 Objective and motivation

Adding L2 penalty to OLS:

JRidge(w)=yXw2+λw02,J_\text{Ridge}(\mathbf{w}) = \|\mathbf{y}-\mathbf{X}\mathbf{w}\|^2 + \lambda\|\mathbf{w}_{-0}\|^2,

where w0\mathbf{w}_{-0} excludes the intercept (common convention; center features to avoid penalizing intercept).

2.2 Closed-form solution

Gradient:

wJ=2Xy+2XXw+2λDw,\nabla_\mathbf{w} J = -2\mathbf{X}^\top\mathbf{y} + 2\mathbf{X}^\top\mathbf{X}\mathbf{w} + 2\lambda\mathbf{D}\mathbf{w},

where D=diag(0,1,,1)\mathbf{D} = \operatorname{diag}(0, 1, \ldots, 1) (no penalty on intercept).

Setting to zero:

(XX+λD)w=Xy,(\mathbf{X}^\top\mathbf{X} + \lambda\mathbf{D})\mathbf{w} = \mathbf{X}^\top\mathbf{y},
wRidge=(XX+λD)1Xy.\boxed{\mathbf{w}_\text{Ridge}^\star = (\mathbf{X}^\top\mathbf{X} + \lambda\mathbf{D})^{-1}\mathbf{X}^\top\mathbf{y}.}

λD\lambda\mathbf{D} adds to the diagonal → always invertible for λ>0\lambda > 0. Solves multicollinearity.

2.3 SVD analysis (why Ridge shrinks)

Let X=UΣV\mathbf{X} = \mathbf{U}\boldsymbol{\Sigma}\mathbf{V}^\top (thin SVD). Then:

wRidge=j=1pσj2σj2+λvjXyσjvj.\mathbf{w}_\text{Ridge}^\star = \sum_{j=1}^p \frac{\sigma_j^2}{\sigma_j^2+\lambda} \frac{\mathbf{v}_j^\top \mathbf{X}^\top\mathbf{y}}{\sigma_j} \mathbf{v}_j.

Compare OLS: factor σj2σj2+λ\frac{\sigma_j^2}{\sigma_j^2+\lambda} vs. 1. Ridge shrinks each component by σj2σj2+λ<1\frac{\sigma_j^2}{\sigma_j^2+\lambda} < 1, with more shrinkage for small σj\sigma_j (low-variance directions = unstable OLS directions).

2.4 Bayesian view

Ridge is MAP with Gaussian prior wN(0,σ2λI)\mathbf{w} \sim \mathcal{N}(\mathbf{0}, \frac{\sigma^2}{\lambda}\mathbf{I}):

wMAP=argmaxw[12σ2yXw2λ2σ2w2]=wRidge.\mathbf{w}_\text{MAP} = \arg\max_\mathbf{w} \left[-\frac{1}{2\sigma^2}\|\mathbf{y}-\mathbf{X}\mathbf{w}\|^2 - \frac{\lambda}{2\sigma^2}\|\mathbf{w}\|^2\right] = \mathbf{w}_\text{Ridge}^\star.

2.5 Choosing λ\lambda

Cross-validation is the standard approach. sklearn.linear_model.RidgeCV does LOO-CV efficiently via the hat matrix.


3. Lasso regression

3.1 Objective

JLasso(w)=yXw2+λw1.J_\text{Lasso}(\mathbf{w}) = \|\mathbf{y}-\mathbf{X}\mathbf{w}\|^2 + \lambda\|\mathbf{w}\|_1.

No closed form (L1 is non-differentiable at zero). Solved by coordinate descent or proximal gradient.

3.2 Why Lasso induces sparsity (geometric argument)

Consider contours of RSS (ellipses in 2D) intersecting the constraint region:

  • L2 constraint: w2t\|\mathbf{w}\|^2 \leq t — a smooth ball. Contours typically touch the ball interior → non-sparse.
  • L1 constraint: w1t\|\mathbf{w}\|_1 \leq t — a diamond with sharp corners at axes. Contours often touch a corner → one coordinate exactly zero = sparsity.

3.3 Coordinate descent for Lasso

Update one coordinate at a time, holding others fixed. Let rj(i)=yikjwkxikr_j^{(i)} = y_i - \sum_{k \neq j} w_k x_{ik} be the partial residual. Then:

wjnew=ST ⁣(1nixijrj(i),  λ2n),w_j^\text{new} = \text{ST}\!\left(\frac{1}{n}\sum_i x_{ij} r_j^{(i)},\; \frac{\lambda}{2n}\right),

where soft-thresholding (ST):

ST(z,γ)=sign(z)max(zγ,0).\text{ST}(z, \gamma) = \operatorname{sign}(z)\max(|z|-\gamma, 0).

Intuition: if the correlation of feature jj with partial residual is small (below threshold), set wj=0w_j = 0.

3.4 Lasso path (LARS algorithm)

As λ\lambda decreases from 1nXy\|\frac{1}{n}\mathbf{X}^\top\mathbf{y}\|_\infty to 0:

  • Start: w=0\mathbf{w} = \mathbf{0}.
  • First variable enters the model when its correlation with current residual exceeds threshold.
  • LARS (Least Angle Regression) computes the entire regularization path at cost of one OLS fit.

3.5 Bayesian view

Lasso is MAP with Laplace (double-exponential) prior:

p(wj)=λ2σ2exp ⁣(λσ2wj),p(w_j) = \frac{\lambda}{2\sigma^2} \exp\!\left(-\frac{\lambda}{\sigma^2}|w_j|\right),

which has heavier tails than Gaussian → accommodates more extreme values in some components and zeros in others.


4. Elastic Net

4.1 Objective

JEN(w)=yXw2+λ1w1+λ2w22.J_\text{EN}(\mathbf{w}) = \|\mathbf{y}-\mathbf{X}\mathbf{w}\|^2 + \lambda_1\|\mathbf{w}\|_1 + \lambda_2\|\mathbf{w}\|_2^2.

Or parameterized with α[0,1]\alpha \in [0,1] and overall penalty λ\lambda:

λ[1α2w2+αw1].\lambda\left[\frac{1-\alpha}{2}\|\mathbf{w}\|^2 + \alpha\|\mathbf{w}\|_1\right].

α=1\alpha=1 = Lasso, α=0\alpha=0 = Ridge. sklearn uses this parameterization.

4.2 Advantages over Lasso

  • Handles groups of correlated features: Lasso picks one arbitrarily; ElasticNet can retain all.
  • The L2 component ensures uniqueness of solution.
  • Sparsity still encouraged by L1.

5. Logistic regression

5.1 From regression to classification

Linear regression for binary labels y{0,1}y \in \{0,1\} is poor: predictions can go outside [0,1][0,1], squared loss is not ideal for probabilities.

Logistic regression models the log-odds (logit) as a linear function:

logP(y=1x)P(y=0x)=wx.\log\frac{P(y=1|\mathbf{x})}{P(y=0|\mathbf{x})} = \mathbf{w}^\top\mathbf{x}.

Solving for the probability:

P(y=1x)=σ(wx),σ(z)=11+ez.\boxed{P(y=1|\mathbf{x}) = \sigma(\mathbf{w}^\top\mathbf{x}), \quad \sigma(z) = \frac{1}{1+e^{-z}}.}

5.2 Sigmoid function properties

σ(z)=11+ez=ez1+ez.\sigma(z) = \frac{1}{1+e^{-z}} = \frac{e^z}{1+e^z}.
  • σ()=0\sigma(-\infty)=0, σ(0)=0.5\sigma(0)=0.5, σ(+)=1\sigma(+\infty)=1.
  • Derivative: σ(z)=σ(z)(1σ(z))\sigma'(z) = \sigma(z)(1-\sigma(z)). (Proof: apply quotient rule.)
  • Symmetric: 1σ(z)=σ(z)1-\sigma(z) = \sigma(-z).

5.3 Negative log-likelihood (cross-entropy loss)

For binary labels, the Bernoulli likelihood of one sample:

p(yx;w)=σ(wx)y(1σ(wx))1y.p(y|\mathbf{x};\mathbf{w}) = \sigma(\mathbf{w}^\top\mathbf{x})^y \cdot (1-\sigma(\mathbf{w}^\top\mathbf{x}))^{1-y}.

Log-likelihood over all nn samples:

(w)=i=1n[yilogpi+(1yi)log(1pi)],pi=σ(xiw).\ell(\mathbf{w}) = \sum_{i=1}^n \left[y_i \log p_i + (1-y_i)\log(1-p_i)\right], \quad p_i = \sigma(\mathbf{x}_i^\top\mathbf{w}).

Cross-entropy loss = negative log-likelihood:

L(w)=1n(w)=1ni=1n[yilogpi+(1yi)log(1pi)].\mathcal{L}(\mathbf{w}) = -\frac{1}{n}\ell(\mathbf{w}) = -\frac{1}{n}\sum_{i=1}^n\left[y_i \log p_i + (1-y_i)\log(1-p_i)\right].

5.4 Gradient derivation

Gradient of loss for one sample ii:

w[yilogpi(1yi)log(1pi)].\frac{\partial}{\partial \mathbf{w}}\left[- y_i \log p_i - (1-y_i)\log(1-p_i)\right].

Step 1 (chain rule through sigmoid):

Lizi=piyi,zi=xiw.\frac{\partial \mathcal{L}_i}{\partial z_i} = p_i - y_i, \quad z_i = \mathbf{x}_i^\top\mathbf{w}.

Proof:

zi[yilogσ(zi)(1yi)log(1σ(zi))]=yiσ(zi)σ(zi)+1yi1σ(zi)σ(zi)=σ(zi)yi.\frac{\partial}{\partial z_i}\left[-y_i\log\sigma(z_i) - (1-y_i)\log(1-\sigma(z_i))\right] = -\frac{y_i}{\sigma(z_i)}\sigma'(z_i) + \frac{1-y_i}{1-\sigma(z_i)}\sigma'(z_i) = \sigma(z_i) - y_i.

(Uses σ=σ(1σ)\sigma'=\sigma(1-\sigma).)

Step 2: ziw=xi\frac{\partial z_i}{\partial \mathbf{w}} = \mathbf{x}_i.

Combined (averaged):

wL=1ni=1n(piyi)xi=1nX(py).\boxed{\nabla_\mathbf{w}\mathcal{L} = \frac{1}{n}\sum_{i=1}^n(p_i - y_i)\mathbf{x}_i = \frac{1}{n}\mathbf{X}^\top(\mathbf{p}-\mathbf{y}).}

Elegant: same form as linear regression gradient but with p=σ(Xw)\mathbf{p} = \sigma(\mathbf{X}\mathbf{w}) instead of Xw\mathbf{X}\mathbf{w}.

5.5 Hessian (confirms convexity)

2L=1nXWX,W=diag(pi(1pi)).\nabla^2 \mathcal{L} = \frac{1}{n}\mathbf{X}^\top\mathbf{W}\mathbf{X}, \quad \mathbf{W} = \operatorname{diag}(p_i(1-p_i)).

Since pi(1pi)>0p_i(1-p_i) > 0, W0\mathbf{W}\succ 0, and XWX0\mathbf{X}^\top\mathbf{W}\mathbf{X} \succeq 0 (PSD). So logistic regression loss is convex (strictly, if X\mathbf{X} full column rank).

5.6 Optimization

No closed-form. Use:

  • Gradient descent / mini-batch SGD (scale to huge datasets).
  • Newton-Raphson / IRLS: wt+1=wt(2L)1L\mathbf{w}_{t+1} = \mathbf{w}_t - (\nabla^2\mathcal{L})^{-1}\nabla\mathcal{L}. Equivalent to iteratively reweighted least squares. Converges quadratically near optimum.
  • L-BFGS: quasi-Newton, efficient for medium-sized data.

5.7 Decision boundary

Predict class 1 if P(y=1x)0.5P(y=1|\mathbf{x}) \geq 0.5, i.e., if wx0\mathbf{w}^\top\mathbf{x} \geq 0.

Decision boundary: the hyperplane wx=0\mathbf{w}^\top\mathbf{x} = 0. Logistic regression is a linear classifier.

The threshold 0.5 is not always optimal: in imbalanced problems or asymmetric costs, choose threshold via ROC analysis.

5.8 Regularized logistic regression

Add L2: LRidge=L+λ2nw2\mathcal{L}_\text{Ridge} = \mathcal{L} + \frac{\lambda}{2n}\|\mathbf{w}\|^2. sklearn uses C=1/λC = 1/\lambda (larger CC = less regularization).


6. Multiclass: softmax regression

6.1 Model

For KK classes, assign one weight vector wkRp\mathbf{w}_k \in \mathbb{R}^p per class. Let WRK×p\mathbf{W} \in \mathbb{R}^{K\times p}.

Softmax function turns logits z=Wx\mathbf{z} = \mathbf{W}\mathbf{x} into probabilities:

softmax(z)k=ezkj=1Kezj,P(y=kx)=softmax(Wx)k.\text{softmax}(\mathbf{z})_k = \frac{e^{z_k}}{\sum_{j=1}^K e^{z_j}}, \quad P(y=k|\mathbf{x}) = \text{softmax}(\mathbf{W}\mathbf{x})_k.

Numerical stability: subtract maxjzj\max_j z_j before exponentiating (doesn't change softmax, avoids overflow).

6.2 Categorical cross-entropy loss

L(W)=1ni=1nk=1K1[yi=k]logP(y=kxi).\mathcal{L}(\mathbf{W}) = -\frac{1}{n}\sum_{i=1}^n\sum_{k=1}^K \mathbf{1}[y_i=k]\log P(y=k|\mathbf{x}_i).

With one-hot encoding ek\mathbf{e}_k for class kk, this simplifies to 1nilogP(y=yixi)-\frac{1}{n}\sum_i \log P(y=y_i|\mathbf{x}_i).

6.3 Gradient

wkL=1ni=1n(P(y=kxi)1[yi=k])xi.\nabla_{\mathbf{w}_k} \mathcal{L} = \frac{1}{n}\sum_{i=1}^n (P(y=k|\mathbf{x}_i) - \mathbf{1}[y_i=k])\mathbf{x}_i.

Same "prediction minus target" structure as binary logistic.

6.4 Binary vs multiclass approaches

StrategyDescription
One-vs-Rest (OvR)Train KK binary classifiers; predict class with highest score
One-vs-One (OvO)Train (K2)\binom{K}{2} classifiers; majority vote
Softmax (Multinomial LR)Train jointly; theoretically preferred

7. Generalized linear models (overview)

GLMs unify many models: linear regression, logistic regression, Poisson regression, etc.

A GLM has three components:

  1. Random component: yy \sim exponential family with mean μ\mu.
  2. Systematic component: η=wx\eta = \mathbf{w}^\top\mathbf{x} (linear predictor).
  3. Link function gg: g(μ)=ηg(\mu) = \eta.
ModelDistributionLink ggMean μ\mu
Linear regressionGaussianIdentitywx\mathbf{w}^\top\mathbf{x}
Logistic regressionBernoulliLogitσ(wx)\sigma(\mathbf{w}^\top\mathbf{x})
Poisson regressionPoissonLogewxe^{\mathbf{w}^\top\mathbf{x}}
Softmax regressionMultinoulliLog-ratioSoftmax

8. Code examples

See the classical_examples/ directory (created with ML_NOTES.md) for:

FileWhat it shows
linear_regression.pyOLS via normal equations vs LinearRegression
regularization_ridge.pyRidge closed form vs Ridge
logistic_regression.pyBatch GD on binary cross-entropy vs LogisticRegression

Sklearn cheatsheet for linear models

from sklearn.linear_model import (
    LinearRegression,
    Ridge, RidgeCV,
    Lasso, LassoCV,
    ElasticNet, ElasticNetCV,
    LogisticRegression,
)

# Ridge with CV-selected alpha
model = RidgeCV(alphas=[0.1, 1.0, 10.0], cv=5)
model.fit(X_train, y_train)
print(model.alpha_)  # best alpha

# Lasso (coordinate descent)
lasso = LassoCV(cv=5, max_iter=10_000)
lasso.fit(X_train, y_train)
print(lasso.alpha_)
print(np.sum(lasso.coef_ != 0))  # number of nonzero (selected) features

# Logistic regression (C = 1/lambda, larger = less regularization)
clf = LogisticRegression(C=1.0, penalty='l2', solver='lbfgs', max_iter=1000)
clf.fit(X_train, y_train)
clf.predict_proba(X_test)  # calibrated probabilities

*File: notes/03_linear_models.md — next: notes/04_optimization.md*

From-scratch code

Runnable implementations, each checked against its scikit-learn equivalent.

Linear regression — OLS normal equations vs sklearnclassical_examples/linear_regression.py
"""
Ordinary Least Squares (OLS) linear regression.

From scratch: normal equations  w = (X^T X)^{-1} X^T y
Library: sklearn.linear_model.LinearRegression
"""

from __future__ import annotations

import numpy as np
from sklearn.datasets import make_regression
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score


def add_intercept(X: np.ndarray) -> np.ndarray:
    """Prepend column of ones for bias term."""
    return np.column_stack([np.ones(len(X)), X])


def ols_normal_equations(X: np.ndarray, y: np.ndarray) -> np.ndarray:
    """
    Solve min_w ||y - X w||_2^2  via  X^T X w = X^T y.

    X must include intercept column if desired (use add_intercept).
    """
    XtX = X.T @ X
    Xty = X.T @ y
    return np.linalg.solve(XtX, Xty)


def main() -> None:
    rng = np.random.default_rng(0)
    X_raw, y, coef = make_regression(
        n_samples=200,
        n_features=3,
        noise=15.0,
        coef=True,
        random_state=0,
    )

    X = add_intercept(X_raw)
    w_scratch = ols_normal_equations(X, y)

    model = LinearRegression(fit_intercept=False)
    # sklearn expects raw X; we pass X with manual intercept to compare coefficients directly
    model.fit(X, y)
    w_sklearn = model.coef_

    y_pred_s = X @ w_scratch
    y_pred_k = model.predict(X)

    print("OLS coefficients (scratch):", w_scratch)
    print("OLS coefficients (sklearn): ", w_sklearn)
    print("Max abs diff:", np.max(np.abs(w_scratch - w_sklearn)))
    print("MSE scratch:", mean_squared_error(y, y_pred_s))
    print("R2 scratch :", r2_score(y, y_pred_s))


if __name__ == "__main__":
    main()
Logistic regression — batch GD on cross-entropy vs sklearnclassical_examples/logistic_regression.py
"""
Binary logistic regression via gradient descent (from scratch) vs sklearn.

Model: P(y=1|x) = sigma(w^T x), sigma(z) = 1/(1+exp(-z))
Loss: average cross-entropy (negative log-likelihood).
"""

from __future__ import annotations

import numpy as np
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler


def sigmoid(z: np.ndarray) -> np.ndarray:
    return 1.0 / (1.0 + np.exp(-np.clip(z, -500, 500)))


def add_intercept(X: np.ndarray) -> np.ndarray:
    return np.column_stack([np.ones(len(X)), X])


def logistic_gradient(X: np.ndarray, y: np.ndarray, w: np.ndarray) -> np.ndarray:
    """Gradient of mean cross-entropy: (1/n) X^T (sigma(Xw) - y)."""
    p = sigmoid(X @ w)
    n = X.shape[0]
    return (X.T @ (p - y)) / n


def fit_logistic_gd(
    X: np.ndarray,
    y: np.ndarray,
    lr: float = 0.5,
    n_iter: int = 5000,
    tol: float = 1e-7,
) -> np.ndarray:
    w = np.zeros(X.shape[1])
    for _ in range(n_iter):
        g = logistic_gradient(X, y, w)
        w_new = w - lr * g
        if np.linalg.norm(w_new - w) < tol:
            break
        w = w_new
    return w


def main() -> None:
    iris = load_iris()
    # Binary: class 0 vs rest (classes 1,2)
    X = iris.data[iris.target != 2]
    y = (iris.target[iris.target != 2]).astype(float)

    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.25, random_state=0, stratify=y
    )
    scaler = StandardScaler()
    X_train = scaler.fit_transform(X_train)
    X_test = scaler.transform(X_test)

    X_tr = add_intercept(X_train)
    X_te = add_intercept(X_test)

    w = fit_logistic_gd(X_tr, y_train, lr=0.3, n_iter=8000)

    prob_te = sigmoid(X_te @ w)
    pred_scratch = (prob_te >= 0.5).astype(int)

    clf = LogisticRegression(max_iter=2000, solver="lbfgs")
    clf.fit(X_train, y_train)
    pred_sklearn = clf.predict(X_test)

    print("Scratch weights (intercept first):", w)
    print("Sklearn coef + intercept:", np.r_[clf.intercept_, clf.coef_.ravel()])
    print("Accuracy scratch:", accuracy_score(y_test, pred_scratch))
    print("Accuracy sklearn :", accuracy_score(y_test, pred_sklearn))


if __name__ == "__main__":
    main()
Ridge — closed form vs sklearnclassical_examples/regularization_ridge.py
"""
Ridge regression (L2): closed-form solution vs sklearn.

Ridge: minimize ||y - Xw||^2 + lambda ||w'||^2 where w' are non-intercept weights.
We do not penalize intercept (common convention).
"""

from __future__ import annotations

import numpy as np
from sklearn.datasets import make_regression
from sklearn.linear_model import Ridge
from sklearn.metrics import mean_squared_error


def add_intercept(X: np.ndarray) -> np.ndarray:
    return np.column_stack([np.ones(len(X)), X])


def ridge_closed_form(X: np.ndarray, y: np.ndarray, alpha: float) -> np.ndarray:
    """
    w = (X^T X + R)^{-1} X^T y with R = diag(0, alpha, alpha, ...) penalizing all but intercept.
    """
    p = X.shape[1]
    R = np.eye(p)
    R[0, 0] = 0.0
    XtX = X.T @ X + alpha * R
    Xty = X.T @ y
    return np.linalg.solve(XtX, Xty)


def main() -> None:
    X_raw, y = make_regression(n_samples=150, n_features=8, noise=5.0, random_state=1)
    X_raw = np.asarray(X_raw, dtype=np.float64)
    y = np.asarray(y, dtype=np.float64)
    X = add_intercept(X_raw)
    alpha = 10.0

    # Closed form: intercept not penalized (first column of ones).
    w = ridge_closed_form(X, y, alpha)
    # Sklearn: same convention — intercept unpenalized, features penalized.
    model = Ridge(alpha=alpha, fit_intercept=True)
    model.fit(X_raw, y)
    w_sk = np.r_[model.intercept_, model.coef_]

    print("Ridge alpha:", alpha)
    print("Closed-form w:", w)
    print("Sklearn w    :", w_sk)
    print("Max abs diff :", np.max(np.abs(w - w_sk)))
    print("MSE closed   :", mean_squared_error(y, X @ w))
    print("MSE sklearn  :", mean_squared_error(y, model.predict(X_raw)))


if __name__ == "__main__":
    main()