World-Class AI/ML Reference · July 2026
The Complete
AI/ML Textbook
From First Principles to Frontier Research.
Every derivation. Every algorithm. Every insight.
Author: Anas Hussain (a1n4a)  ·  Version: 1.0, July 2026  ·  Level: Graduate → Frontier
43
Chapters
6
Parts
5
Appendices
Depth

πŸ“– The Complete Self-Contained AI/ML Textbook

From First Principles to Frontier Research β€” Everything Inline, No Internet Required

Author: Anas Hussain (a1n4a)
Version: 1.0 β€” July 2026
Format: Fully self-contained. Every concept taught, every derivation shown, every algorithm implemented, every diagram embedded.



Table of Contents

Click to expand full TOC (41 chapters + 5 appendices) - **Part 0: Prerequisites** (Chapters 0.1–0.5) - 0.1 Linear Algebra β€” The Language of ML - 0.2 Calculus β€” How Models Learn - 0.3 Probability & Statistics β€” Making Sense of Uncertainty - 0.4 Information Theory β€” Measuring Information - 0.5 Optimization β€” Finding the Minimum - **Part I: Classical Machine Learning** (Chapters 1–8) - 1 The Learning Framework - 2 Linear Regression - 3 Logistic Regression & Classification - 4 Support Vector Machines - 5 Decision Trees & Ensemble Methods - 6 Unsupervised Learning - 7 Theory of Generalization - 8 Evaluation & Validation - **Part II: Deep Learning** (Chapters 9–14) - 9 Neural Networks & Universal Approximation - 10 Backpropagation β€” Full Derivation - 11 Convolutional Neural Networks - 12 Recurrent Networks & LSTMs - 13 Normalization & Regularization - 14 Optimization Algorithms - **Part III: The Attention Revolution** (Chapters 15–21) - 15 Attention Mechanisms - 16 Transformers β€” Full Architecture - 17 BERT & Encoder Architectures - 18 GPT & Decoder Architectures - 19 Efficient Transformers - 20 State Space Models (S4, Mamba) - 21 Graph Neural Networks - **Part IV: Generative AI** (Chapters 22–29) - 22 Variational Autoencoders - 23 Generative Adversarial Networks - 24 Normalizing Flows - 25 Energy-Based Models - 26 Diffusion Models β€” Full Derivation - 27 Flow Matching & Rectified Flows - 28 Large Language Models - 29 Multimodal Models - **Part V: Advanced Training** (Chapters 30–35) - 30 Reinforcement Learning - 31 RLHF & Preference Optimization - 32 Alignment & Safety - 33 Scaling Laws - 34 Mixture of Experts - 35 Distributed Training - **Part VI: Frontiers & Theory** (Chapters 36–43) - 36 Mechanistic Interpretability - 37 Neural Tangent Kernel & Feature Learning - 38 Causal AI - 39 Geometric Deep Learning - 40 World Models & JEPA - 41 Neuroscience & AI - 42 AI Safety Theory - 43 Open Problems & AGI Debates - **Appendices** - A Complete Math Reference - B Algorithm Catalog (50+ pseudocode implementations) - C Full Glossary - D Hyperparameter Bible - E Breakthrough Timeline

Part 0: Prerequisites

Everything You Need to Know Before ML

This part teaches you every piece of mathematics you'll encounter in ML. You don't need prior knowledge β€” just the willingness to follow along.


0.1 Linear Algebra β€” The Language of ML

Every ML model manipulates numbers. Linear algebra is the mathematical language for doing this efficiently.

0.1.1 Vectors

A vector is an ordered list of numbers. In ML, every data point is a vector.

Example: A house with 3 features
  x = [size_in_sqft, number_of_bedrooms, age_in_years]
  x = [1500, 3, 10]

We write: x ∈ ℝ³  (x is in 3-dimensional real space)

Vector operations β€” the building blocks of neural networks:

Addition:    [1, 2] + [3, 4] = [4, 6]      (element-wise)
Scalar mult: 2 Γ— [1, 2, 3]  = [2, 4, 6]     (multiply each element)
Dot product: [1, 2] Β· [3, 4] = 1Γ—3 + 2Γ—4 = 11   (scalar result)

The dot product is the single most important operation in ML. Every neural network layer, every attention mechanism, every similarity computation boils down to dot products.

import numpy as np

# Vectors in code
x = np.array([1500, 3, 10])  # a house
y = np.array([2000, 4, 5])   # another house

# Dot product = how similar are they?
similarity = np.dot(x, y)  # 1500Γ—2000 + 3Γ—4 + 10Γ—5 = 3,000,062

0.1.2 Matrices

A matrix is a rectangular array of numbers. In ML:
- A dataset of N houses with D features = N Γ— D matrix
- A layer's weights = matrix
- An image (256Γ—256 pixels Γ— 3 colors) = 256 Γ— 256 Γ— 3 tensor

Matrix A (2Γ—3):    Matrix B (3Γ—2):    A Γ— B (2Γ—2):
[1, 2, 3]          [7, 8]            [1Γ—7+2Γ—9+3Γ—11,   1Γ—8+2Γ—10+3Γ—12]
[4, 5, 6]          [9, 10]           [4Γ—7+5Γ—9+6Γ—11,   4Γ—8+5Γ—10+6Γ—12]
                   [11, 12]          
                                     = [58, 64]
                                       [139, 154]
import numpy as np

# Matrix multiplication
A = np.array([[1, 2, 3], [4, 5, 6]])
B = np.array([[7, 8], [9, 10], [11, 12]])
C = A @ B  # or np.matmul(A, B)

print(C)  # [[58, 64], [139, 154]]

# A neural network layer: h = X @ W + b
X = np.random.randn(32, 784)    # batch of 32 images, 784 pixels each
W = np.random.randn(784, 256)   # weight matrix
b = np.random.randn(256)        # bias

h = X @ W + b  # shape: (32, 256)
# This is called a "linear layer" or "fully-connected layer"

0.1.3 Matrix Properties You Must Know

Property            Definition                         ML Use
─────────           ──────────                         ──────
Transpose (Aα΅€)      Swap rows and columns              Backpropagation, attention
Inverse (A⁻¹)       A Γ— A⁻¹ = I                       Solving normal equations
Identity (I)        1 on diagonal, 0 elsewhere          The "do nothing" matrix
Trace (tr[A])       Sum of diagonal elements            Regularization, optimization
Determinant (|A|)   "Volume" of transformation          Normalizing flows, change of var.
Eigenvalues (Ξ»)     Av = Ξ»v                            PCA, graph theory, stability
Singular Values     Diagonal of Ξ£ in A = UΞ£Vα΅€         SVD, compression, PCA

The Singular Value Decomposition (SVD) β€” the most beautiful matrix factorization:

$$A = U \Sigma V^T$$

Every matrix can be decomposed into:
- $U$: left singular vectors (columns: orthonormal basis for column space)
- $\Sigma$: singular values (diagonal: importance of each direction, sorted descending)
- $V^T$: right singular vectors (rows: orthonormal basis for row space)

# SVD in one line
U, S, Vt = np.linalg.svd(A, full_matrices=False)

# Low-rank approximation: keep k largest singular values
k = 50
A_approx = U[:, :k] @ np.diag(S[:k]) @ Vt[:k, :]
# This is the foundation of PCA, recommendation systems, and compression

Why SVD matters for ML: Any matrix can be approximated by keeping only its largest singular values. This is:
- PCA (principal component analysis)
- Recommender system matrix factorization
- Model compression (low-rank adaptation = LoRA)
- Attention optimization

0.1.4 Tensors

A tensor is a generalization of vectors and matrices to any number of dimensions:

Scalar:   shape []        or ()
Vector:   shape [3]       or (3,)
Matrix:   shape [3, 4]    or (3, 4)
3-tensor: shape [3, 4, 5] or (3, 4, 5)
# In ML, everything is tensors:
batch_images = torch.randn(32, 3, 256, 256) # batch, channels, height, width
batch_text   = torch.randn(32, 128, 768)     # batch, seq_len, embedding_dim

0.1.5 Norms β€” Measuring Size

A norm tells you how "big" a vector is:

L1 norm:   ||x||₁ = |x₁| + |xβ‚‚| + ... + |xβ‚™|    (sparsity)
L2 norm:   ||x||β‚‚ = √(x₁² + xβ‚‚Β² + ... + xβ‚™Β²)     (Euclidean distance)
L∞ norm:   ||x||∞ = max(|x₁|, |xβ‚‚|, ..., |xβ‚™|)   (worst-case)
Frobenius: ||A||_F = √(ΣΣ Aᡒⱼ²)                   (matrix L2)
x = np.array([3, -4])
l1 = np.sum(np.abs(x))           # 7
l2 = np.sqrt(np.sum(x**2))       # 5
l2 = np.linalg.norm(x)           # same

Key insight: L1 regularization pushes weights to zero (feature selection). L2 regularization shrinks all weights (weight decay).


0.2 Calculus β€” How Models Learn

Calculus tells us how to change our model to reduce errors. This is all of training.

0.2.1 Derivatives β€” The Slope

A derivative $f'(x)$ tells you how fast $f$ changes when you change $x$:

Geometric: slope of the tangent line at point x
Intuitive: if I nudge x up a tiny bit, does f go up or down?
$$f'(x) = \lim_{h \to 0} \frac{f(x + h) - f(x)}{h}$$
# Numerical derivative (approximate)
def derivative(f, x, h=1e-7):
    return (f(x + h) - f(x - h)) / (2 * h)

# Example: f(x) = xΒ²
f = lambda x: x**2
print(derivative(f, 3))  # β‰ˆ 6.0 (since derivative of xΒ² is 2x)

All the derivatives you'll ever need in ML:

Function       Derivative          Used In
────────       ──────────          ───────
xⁿ             n·xⁿ⁻¹              Polynomials
eΛ£             eΛ£                  Softmax, sigmoid
ln(x)          1/x                 Cross-entropy loss
sin(x)         cos(x)              Positional encoding
Οƒ(x)=1/(1+e⁻ˣ) Οƒ(x)(1-Οƒ(x))       Sigmoid activation
tanh(x)        1-tanhΒ²(x)          Tanh activation
ReLU(x)=max(0,x) 0 if x<0, 1 if x>0  ReLU activation

0.2.2 Partial Derivatives β€” Multiple Inputs

Most ML functions have many inputs (weights). A partial derivative $\frac{\partial f}{\partial w_i}$ tells you how $f$ changes when you change only $w_i$, keeping everything else fixed.

# f(w, b) = (w*x + b - y)Β²  (squared error for one data point)
def f(w, b, x=2, y=5):
    return (w*x + b - y)**2

# Partial derivative w.r.t w: 2*(w*x + b - y)*x
# Partial derivative w.r.t b: 2*(w*x + b - y)*1

0.2.3 The Chain Rule β€” Backpropagation's Engine

Every neural network trains using the chain rule. Period.

$$\frac{\partial L}{\partial w} = \frac{\partial L}{\partial \hat{y}} \cdot \frac{\partial \hat{y}}{\partial z} \cdot \frac{\partial z}{\partial w}$$

Where $L$ is loss, $\hat{y}$ is prediction, $z$ is pre-activation, $w$ is weight.

The magic of backprop: you compute all partial derivatives in one forward + one backward pass through the network. This is exponentially faster than computing each derivative separately.

0.2.4 Gradients β€” The Direction of Steepest Descent

The gradient $\nabla f$ is a vector of all partial derivatives:

$$\nabla f(w_1, w_2, ..., w_n) = \begin{bmatrix} \frac{\partial f}{\partial w_1}, \frac{\partial f}{\partial w_2}, ..., \frac{\partial f}{\partial w_n} \end{bmatrix}$$

Key property: The gradient points in the direction of steepest ascent. To minimize, move opposite to the gradient.

$$w_{new} = w_{old} - \eta \nabla L(w_{old})$$

This is gradient descent β€” the single algorithm that trains every neural network.

# Gradient descent visualized
ΞΈ = random_initial_weights()
for step in range(steps):
    gradient = compute_gradient(loss_function, ΞΈ, data)
    ΞΈ = ΞΈ - learning_rate * gradient

0.2.5 The Jacobian and Hessian

Jacobian $J$: matrix of all first-order partial derivatives (for vector-output functions).
- Shape: [output_dim Γ— input_dim]
- Used in: normalizing flows, invertible networks

Hessian $H$: matrix of all second-order partial derivatives.
- Shape: [input_dim Γ— input_dim]
- Used in: second-order optimization (Newton methods), understanding loss landscape
- $H_{ij} = \partialΒ²L / \partial w_i \partial w_j$

In practice: We never compute the full Hessian (too expensive). But its properties (eigenvalues, condition number) tell us how hard optimization will be.


0.3 Probability & Statistics β€” Making Sense of Uncertainty

0.3.1 Probability Fundamentals

A probability $P(A)$ is a number between 0 and 1 representing how likely event A is.

P(rain tomorrow) = 0.3    means 30% chance
P(sunny) = 0.7            means 70% chance
                        total must = 1

Joint probability: $P(A, B)$ = probability A AND B both happen
Conditional probability: $P(A|B)$ = probability of A given B happened
Marginal probability: $P(A) = \sum_B P(A, B)$ = probability of A regardless of B

0.3.2 Bayes' Theorem β€” The Core of Learning

$$P(\theta | D) = \frac{P(D | \theta) P(\theta)}{P(D)}$$
Posterior = Likelihood Γ— Prior / Evidence

ΞΈ = model parameters, D = data
P(ΞΈ|D): what we believe about parameters AFTER seeing data
P(D|ΞΈ): how likely the data is given parameters (likelihood)
P(ΞΈ): what we believed BEFORE seeing data (prior)
P(D): probability of the data (normalization)

Bayes' theorem is how all learning works. You start with beliefs (prior), see evidence (data), update beliefs (posterior).

0.3.3 Distributions You Must Know

The Normal (Gaussian) Distribution β€” the most important in ML:

$$p(x) = \frac{1}{\sqrt{2\pi\sigma^2}} \exp\left(-\frac{(x-\mu)^2}{2\sigma^2}\right)$$
import numpy as np
import matplotlib.pyplot as plt

# Generate samples from a Gaussian
mu, sigma = 0, 1
samples = np.random.normal(mu, sigma, 10000)

# Estimate parameters from data
mu_hat = np.mean(samples)     # β‰ˆ 0
sigma_hat = np.std(samples)   # β‰ˆ 1

# Why Gaussian? Central Limit Theorem:
# The sum of many independent random variables
# approaches a Gaussian, regardless of their individual distributions

0.3.4 Maximum Likelihood Estimation (MLE)

The principle: Choose parameters $\theta$ that make the observed data most probable.

$$\theta_{MLE} = \arg\max_\theta P(\text{data} | \theta)$$

In practice, we minimize negative log-likelihood (NLL):

$$\theta_{MLE} = \arg\min_\theta -\log P(\text{data} | \theta)$$

Why log-likelihood matters for ML: Every loss function you'll use (MSE, cross-entropy) is actually a negative log-likelihood:

Loss Function Underlying Distribution
MSE Gaussian with fixed variance
Cross-entropy Bernoulli/Categorical
Binary cross-entropy Bernoulli
# MSE = negative log-likelihood of Gaussian
# For y ~ N(Ε·, σ²):
#   log P(y|Ε·) = -Β½(y-Ε·)Β²/σ² - Β½log(2πσ²)
# Minimizing MSE = maximizing log likelihood
# (assuming fixed σ²)

loss = np.mean((y_pred - y_true) ** 2)  # this IS MLE under Gaussian noise

0.3.5 The Law of Large Numbers & Central Limit Theorem

Law of Large Numbers: As sample size increases, sample average converges to expected value.
β†’ More data = more reliable estimates.
β†’ This is why training on more data improves models.

Central Limit Theorem: The distribution of sample means approaches a Gaussian as n grows.
β†’ Even if individual data points are weird, averages are normal.
β†’ This justifies using Gaussian assumptions in many ML models.

0.3.6 Hypothesis Testing & Confidence

Null hypothesis ($H_0$): "Nothing interesting is happening" (e.g., model is random)
p-value: Probability of seeing results this extreme if $H_0$ is true
Significance level ($\alpha$): Threshold below which we reject $H_0$ (usually 0.05 or 0.01)

In ML: We use these concepts for:
- A/B testing: is model B really better than model A?
- Feature significance: does adding this feature matter?
- Model comparison: is the improvement statistically significant?


0.4 Information Theory β€” Measuring Information

Information theory gives us the language to talk about "how much is learned."

0.4.1 Entropy β€” The Fundamental Measure

Entropy $H(X)$ measures the average "surprise" or uncertainty of a random variable:

$$H(X) = -\sum_{x} P(x) \log_2 P(x) \quad \text{(discrete)}$$
Example: A fair coin
  P(heads) = 0.5, P(tails) = 0.5
  H = -0.5Β·logβ‚‚(0.5) - 0.5Β·logβ‚‚(0.5)
  H = -0.5Β·(-1) - 0.5Β·(-1) = 0.5 + 0.5 = 1 bit

Example: A always-heads coin
  P(heads) = 1, P(tails) = 0
  H = -1Β·logβ‚‚(1) - 0Β·logβ‚‚(0) = 0 bits (no uncertainty)

0.4.2 Cross-Entropy β€” The ML Loss

Cross-entropy measures how many bits are needed to encode events from distribution $P$ using a code optimized for distribution $Q$:

$$H(P, Q) = -\sum_x P(x) \log Q(x)$$

This is the most common loss in ML (classification). When $P$ is the true distribution (one-hot labels) and $Q$ is our model's prediction:

$$H(P, Q) = -\log Q(\text{correct_class})$$
# Cross-entropy loss for classification
# True label: cat (index 2 in 10 classes)
# Model predicts: [0.01, 0.02, 0.93, 0.01, ..., 0.01]

true_class = 2
predicted_probs = [0.01, 0.02, 0.93, 0.01, ...]

loss = -np.log(predicted_probs[true_class])  # -log(0.93) = 0.073

0.4.3 KL Divergence β€” Measuring Distribution Distance

Kullback-Leibler divergence measures how one distribution $Q$ differs from another $P$:

$$KL(P || Q) = \sum_x P(x) \log \frac{P(x)}{Q(x)}$$

Properties:
- $KL(P || Q) \geq 0$ (always non-negative)
- $KL(P || Q) = 0$ iff $P = Q$
- $KL(P || Q) \neq KL(Q || P)$ (not symmetric β€” not a metric)

Relation: $H(P, Q) = H(P) + KL(P || Q)$
Cross-entropy = entropy of true distribution + divergence from it.

KL divergence in ML:
- VAE loss: KL(encoder || prior)
- Distillation: KL(student || teacher)
- Policy gradient: KL(new policy || old policy)

0.4.4 Mutual Information β€” Dependency Measure

$$I(X; Y) = KL(P(X,Y) || P(X)P(Y))$$

Mutual information = how much knowing X tells you about Y. Zero if independent.

Used in:
- Feature selection (pick features with high I(X; target))
- Representation learning (InfoNCE loss, contrastive learning)
- The Information Bottleneck method

0.4.5 The ELBO β€” Variational Inference Foundation

The Evidence Lower BOund (ELBO) underpins VAEs, diffusion models, and Bayesian neural nets:

$$\log P(x) \geq \mathbb{E}_{z \sim q(z|x)}[\log P(x|z)] - KL(q(z|x) || P(z))$$

The ELBO is what makes training generative models tractable. We maximize the ELBO instead of maximizing $\log P(x)$ directly.


0.5 Optimization β€” Finding the Minimum

0.5.1 Convex vs Non-Convex Optimization

0.5.2 Gradient Descent β€” The Core Algorithm

def gradient_descent(gradient_fn, init_params, lr=0.01, steps=1000):
    """
    The simplest optimization algorithm.

    gradient_fn: function that returns βˆ‡L at current parameters
    init_params: starting point ΞΈβ‚€
    lr: step size Ξ· (learning rate)
    steps: how many iterations
    """
    params = init_params.copy()
    history = [params.copy()]

    for i in range(steps):
        grad = gradient_fn(params)
        params = params - lr * grad
        history.append(params.copy())

    return params, history

Key hyperparameter: learning rate (lr / Ξ·):

0.5.3 Stochastic Gradient Descent (SGD)

Instead of computing gradient on ALL data (batch), compute on a random mini-batch:

def sgd_step(model, batch_X, batch_y, lr):
    """
    One step of SGD. 
    Key insight: gradient on mini-batch β‰ˆ gradient on full dataset
    but ~1000x cheaper to compute.
    """
    predictions = model(batch_X)
    loss = compute_loss(predictions, batch_y)
    loss.backward()  # compute gradients

    with torch.no_grad():
        for param in model.parameters():
            param -= lr * param.grad
            param.grad.zero_()

Why it works: The expected gradient over a random mini-batch equals the true gradient:

$$\mathbb{E}_{batch}[\nabla L_{batch}(\theta)] = \nabla L_{full}(\theta)$$

So SGD = noisy gradient descent. The noise actually helps generalization!

0.5.4 The Loss Landscape Geometry

Condition number: ratio of largest to smallest eigenvalue of the Hessian.
- High condition number β†’ landscape is steep in some directions, flat in others β†’ hard to optimize.
- Batch normalization and residual connections improve condition number.

Saddle points: points where gradient = 0 but not a minimum.
- In high dimensions, most critical points are saddles, not local minima.
- SGD escapes saddle points naturally due to noise.


Part I: Classical Machine Learning

Chapter 1: The Learning Framework

What Is Learning? β€” The Formal Definition

Definition (Mitchell, 1997): A computer program learns from experience E with respect to task T and performance measure P if its performance at T, measured by P, improves with E.

For every ML problem, you must define:

The Hypothesis Space

The hypothesis space $\mathcal{H}$ is the set of all possible models our learning algorithm can produce.

A linear model with 2 features: h(x) = w₁x₁ + wβ‚‚xβ‚‚ + b
Hypothesis space: all possible (w₁, wβ‚‚, b) ∈ ℝ³

A decision tree of depth 3: all possible tree structures of depth ≀ 3
Hypothesis space: all decision trees fitting this constraint

Key trade-off: Larger hypothesis space = more expressive but harder to learn (needs more data). This is the bias-variance tradeoff formalized.

The No-Free-Lunch Theorem

Theorem (Wolpert, 1996): No learning algorithm is universally better than any other across all possible problems. If algorithm A beats algorithm B on some problems, B beats A on others.

What this means: There's no "best ML algorithm." The best algorithm depends on your specific data and problem. The art of ML is matching algorithms to problems.


Chapter 2: Linear Regression

The Model

$$y = \mathbf{w} \cdot \mathbf{x} + b = w_1 x_1 + w_2 x_2 + ... + w_n x_n + b$$

The Loss (Mean Squared Error)

$$L(\mathbf{w}, b) = \frac{1}{n} \sum_{i=1}^{n} (\hat{y}_i - y_i)^2 = \frac{1}{n} \sum_{i=1}^{n} (\mathbf{w} \cdot \mathbf{x}_i + b - y_i)^2$$

The Solution

Closed form (Normal Equation):

$$\mathbf{w} = (X^T X)^{-1} X^T y$$
import numpy as np

def linear_regression_closed_form(X, y):
    """
    Closed-form solution for linear regression.
    X: (n_samples, n_features)
    y: (n_samples,)
    """
    # Add bias term (column of 1s)
    X_with_bias = np.c_[np.ones(X.shape[0]), X]

    # Normal equation: w = (X^T X)^(-1) X^T y
    w = np.linalg.inv(X_with_bias.T @ X_with_bias) @ X_with_bias.T @ y

    return w[0], w[1:]  # bias, weights

def linear_regression_gradient(X, y, lr=0.01, epochs=1000):
    """
    Gradient descent solution for linear regression.
    More scalable than closed-form for large datasets.
    """
    n, d = X.shape
    w = np.zeros(d)
    b = 0.0

    for epoch in range(epochs):
        y_pred = X @ w + b
        error = y_pred - y

        # Gradients
        dw = (2/n) * X.T @ error
        db = (2/n) * np.sum(error)

        # Update
        w -= lr * dw
        b -= lr * db

        if epoch % 100 == 0:
            loss = np.mean(error ** 2)
            print(f"Epoch {epoch}: loss = {loss:.4f}")

    return w, b

Analytic vs Iterative: When to Use What

Method Pros Cons Best For
Normal Equation Exact solution, no hyperparameters O(nΒ³) matrix inversion < 10K samples
Gradient Descent Scales to large data Needs learning rate tuning > 10K samples

Chapter 3: Logistic Regression & Classification

From Regression to Classification

Linear regression predicts a continuous number. For classification, we need probabilities between 0 and 1.

The fix: Pass the linear output through the sigmoid function:

$$\sigma(z) = \frac{1}{1 + e^{-z}}$$

The Loss (Binary Cross-Entropy)

$$L = -\frac{1}{n} \sum_{i=1}^{n} [y_i \log(\hat{y}_i) + (1-y_i) \log(1-\hat{y}_i)]$$

Intuition: If true label is 1, loss = -log(Ε·). We want Ε· close to 1.
If true label is 0, loss = -log(1-Ε·). We want Ε· close to 0.

def sigmoid(z):
    """Numerically stable sigmoid."""
    return 1 / (1 + np.exp(-np.clip(z, -100, 100)))

def logistic_regression_gradient(X, y, lr=0.01, epochs=1000):
    """
    Logistic regression via gradient descent.
    """
    n, d = X.shape
    w = np.zeros(d)
    b = 0.0

    for epoch in range(epochs):
        # Forward pass
        z = X @ w + b
        y_pred = sigmoid(z)

        # Binary cross-entropy loss
        loss = -np.mean(y * np.log(y_pred + 1e-8) + (1-y) * np.log(1 - y_pred + 1e-8))

        # Gradients
        error = y_pred - y
        dw = (1/n) * X.T @ error
        db = (1/n) * np.sum(error)

        # Update
        w -= lr * dw
        b -= lr * db

        if epoch % 200 == 0:
            print(f"Epoch {epoch}: loss = {loss:.4f}")

    return w, b

# Decision boundary: P(y=1|x) > 0.5
def predict(X, w, b):
    return (sigmoid(X @ w + b) >= 0.5).astype(int)

Multi-Class: Softmax Regression

For K > 2 classes, use softmax instead of sigmoid:

$$P(y = k | \mathbf{x}) = \frac{e^{\mathbf{w}_k \cdot \mathbf{x} + b_k}}{\sum_{j=1}^{K} e^{\mathbf{w}_j \cdot \mathbf{x} + b_j}}$$
def softmax(z):
    """Numerically stable softmax."""
    shifted = z - np.max(z, axis=-1, keepdims=True)
    exp = np.exp(shifted)
    return exp / np.sum(exp, axis=-1, keepdims=True)

# Cross-entropy for K classes:
# L = -Ξ£ y_k Β· log(Ε·_k)
# Where y is one-hot encoded (e.g., [0, 0, 1, 0])

The softmax is everywhere: final layer of every classification LLM, attention mechanism, multi-class logistic regression.


Chapter 4: Support Vector Machines

The Big Idea

Find the hyperplane that maximizes the margin between classes.

Key insight: Only the closest points to the decision boundary (support vectors) determine the boundary. Points far away don't matter.

The Primal Form

$$\min_{w, b} \frac{1}{2} ||w||^2 \quad \text{subject to} \quad y_i(w \cdot x_i + b) \geq 1 \text{ for all } i$$

The Kernel Trick

Key insight: SVMs can project data into higher dimensions without explicitly computing the projection. This is the kernel trick:

$$K(x_i, x_j) = \phi(x_i) \cdot \phi(x_j)$$

We never compute $\phi$, just the dot product in the transformed space.

# Common kernels
linear:     K(x, y) = x Β· y
polynomial: K(x, y) = (x Β· y + c)^d
RBF/Gaussian: K(x, y) = exp(-Ξ³||x-y||Β²)
sigmoid:    K(x, y) = tanh(Ξ±xΒ·y + c)

The RBF kernel is the default choice β€” it can learn any smooth decision boundary given enough data.

from sklearn.svm import SVC

# RBF SVM β€” the "I don't know what to use" champion
svm = SVC(kernel='rbf', C=1.0, gamma='scale')
svm.fit(X_train, y_train)

# C controls the margin-violation penalty
# Large C β†’ hard margin (overfit potential)
# Small C β†’ soft margin (more tolerant of errors)

The Representer Theorem: The optimal weight vector $w$ for an SVM can always be written as a linear combination of training points:

$$w = \sum_{i=1}^{n} \alpha_i y_i \phi(x_i)$$

This is what makes the kernel trick work β€” we only need the $\alpha$ values and the kernel function.


Chapter 5: Decision Trees & Ensemble Methods

Decision Trees

Splitting criterion: Find the feature and threshold that best separate the data.

Gini impurity: $G = 1 - \sum_{k=1}^{K} p_k^2$ (probability of misclassifying a random element)

Information gain: $IG = H(parent) - \sum \frac{n_j}{n} H(child_j)$ (reduction in entropy)

class DecisionTreeStub:
    """A single-level decision tree (decision stump)."""
    def fit(self, X, y):
        self.best_feature = None
        self.best_threshold = None
        self.best_gini = 1.0

        n, d = X.shape
        for feature in range(d):
            thresholds = np.percentile(X[:, feature], np.linspace(10, 90, 20))
            for threshold in thresholds:
                left = y[X[:, feature] <= threshold]
                right = y[X[:, feature] > threshold]

                gini = self._weighted_gini(left, right)
                if gini < self.best_gini:
                    self.best_gini = gini
                    self.best_feature = feature
                    self.best_threshold = threshold

        self.left_pred = np.mean(y[X[:, self.best_feature] <= self.best_threshold])
        self.right_pred = np.mean(y[X[:, self.best_feature] > self.best_threshold])

    def predict(self, X):
        left_mask = X[:, self.best_feature] <= self.best_threshold
        preds = np.zeros(X.shape[0])
        preds[left_mask] = self.left_pred
        preds[~left_mask] = self.right_pred
        return preds

    def _weighted_gini(self, left, right):
        n_left, n_right = len(left), len(right)
        n_total = n_left + n_right
        gini_left = 1 - sum((np.mean(left == k))**2 for k in np.unique(left)) if n_left > 0 else 0
        gini_right = 1 - sum((np.mean(right == k))**2 for k in np.unique(right)) if n_right > 0 else 0
        return (n_left/n_total) * gini_left + (n_right/n_total) * gini_right

Random Forests

The insight: One decision tree overfits. A hundred trees that vote don't.

class RandomForest:
    """Simplified random forest for classification."""
    def __init__(self, n_trees=100, max_depth=5, max_features='sqrt'):
        self.n_trees = n_trees
        self.max_depth = max_depth
        self.max_features = max_features
        self.trees = []

    def fit(self, X, y):
        n, d = X.shape
        n_features = int(np.sqrt(d)) if self.max_features == 'sqrt' else d

        for _ in range(self.n_trees):
            # Bootstrap sample
            indices = np.random.choice(n, n, replace=True)
            X_boot, y_boot = X[indices], y[indices]

            # Random feature subset
            feature_subset = np.random.choice(d, n_features, replace=False)
            X_subset = X_boot[:, feature_subset]

            # Train tree
            tree = DecisionTree(max_depth=self.max_depth)
            tree.fit(X_subset, y_boot)
            self.trees.append((tree, feature_subset))

    def predict(self, X):
        predictions = np.zeros((X.shape[0], len(self.trees)))
        for i, (tree, features) in enumerate(self.trees):
            predictions[:, i] = tree.predict(X[:, features])
        # Majority vote
        return np.round(np.mean(predictions, axis=1))

Gradient Boosting (XGBoost)

The insight: Instead of training trees in parallel (bagging), train them sequentially β€” each new tree corrects the errors of the previous ones.

# Simplified gradient boosting for regression
def gradient_boosting(X, y, n_estimators=100, lr=0.1, max_depth=3):
    """Train a gradient boosting ensemble."""
    # Start with mean prediction
    F = np.full(y.shape, np.mean(y))
    trees = []

    for m in range(n_estimators):
        # Compute pseudo-residuals (negative gradient)
        residuals = y - F  # For MSE: -dL/dF = y - F

        # Train tree on residuals
        tree = DecisionTreeRegressor(max_depth=max_depth)
        tree.fit(X, residuals)

        # Update ensemble
        F += lr * tree.predict(X)
        trees.append(tree)

    return trees

def predict_boosting(X, trees, lr=0.1):
    F = np.zeros(X.shape[0])
    for tree in trees:
        F += lr * tree.predict(X)
    return F

Why XGBoost dominates tabular data:
- Built-in regularization (prevents overfitting)
- Handles missing values automatically
- Extremely optimized (parallel processing, cache-aware access)
- Feature importance built-in
- Works on small and medium data better than neural nets


Chapter 6: Unsupervised Learning

K-Means Clustering

The algorithm:

def kmeans(X, k, max_iters=100, tol=1e-4):
    """
    K-means clustering from scratch.

    Steps:
    1. Initialize k random centroids
    2. Assign each point to nearest centroid
    3. Update centroids to mean of assigned points
    4. Repeat 2-3 until convergence
    """
    n, d = X.shape

    # Step 1: Initialize centroids (k-means++ style)
    centroids = [X[np.random.choice(n)]]
    for _ in range(k - 1):
        distances = np.min([np.linalg.norm(X - c, axis=1) for c in centroids], axis=0)
        probs = distances / distances.sum()
        centroids.append(X[np.random.choice(n, p=probs)])
    centroids = np.array(centroids)

    for iteration in range(max_iters):
        # Step 2: Assign each point to nearest centroid
        distances = np.array([np.linalg.norm(X - c, axis=1) for c in centroids])
        labels = np.argmin(distances, axis=0)

        # Step 3: Update centroids
        new_centroids = np.array([X[labels == j].mean(axis=0) for j in range(k)])

        # Check convergence
        if np.all(np.linalg.norm(new_centroids - centroids, axis=1) < tol):
            break
        centroids = new_centroids

    return labels, centroids

How to choose K (Elbow Method):

inertias = []
for k in range(1, 11):
    labels, centroids = kmeans(X, k)
    inertia = sum(np.min([np.linalg.norm(X - c, axis=1) for c in centroids], axis=0)**2)
    inertias.append(inertia)

# Plot: look for the "elbow" where adding more clusters gives diminishing returns

Principal Component Analysis (PCA)

The goal: Find the directions of maximum variance in the data.

def pca_from_scratch(X, n_components=2):
    """
    PCA via eigendecomposition of covariance matrix.
    """
    # Center the data
    X_centered = X - X.mean(axis=0)

    # Covariance matrix
    cov = X_centered.T @ X_centered / (X_centered.shape[0] - 1)

    # Eigendecomposition
    eigenvalues, eigenvectors = np.linalg.eigh(cov)

    # Sort by eigenvalue (descending)
    idx = np.argsort(eigenvalues)[::-1]
    eigenvalues = eigenvalues[idx]
    eigenvectors = eigenvectors[:, idx]

    # Select top n_components
    components = eigenvectors[:, :n_components]

    # Project data
    X_reduced = X_centered @ components

    # Explained variance ratio
    explained_variance_ratio = eigenvalues[:n_components] / eigenvalues.sum()

    return X_reduced, components, explained_variance_ratio

PCA intuition: If you have 100-dimensional data, the first 2-3 principal components often capture 80%+ of the variance. You can visualize and denoise by projecting to these components.


Chapter 7: Theory of Generalization

Why Does Any of This Work?

The fundamental question: Why does a model that performs well on training data also perform well on unseen data?

The Probably Approximately Correct (PAC) Framework

PAC Learning (Valiant, 1984):

A concept class $\mathcal{C}$ is PAC-learnable if there exists an algorithm $A$ such that for any distribution $D$ and any $\epsilon, \delta > 0$, with probability at least $1-\delta$, $A$ outputs a hypothesis $h$ with error at most $\epsilon$, using a number of samples polynomial in $1/\epsilon$, $1/\delta$, and the complexity of $\mathcal{C}$.

In plain English: We can guarantee that our model will be approximately correct ($\epsilon$ error) with high probability ($1-\delta$), given enough data.

VC Dimension β€” Measuring Model Capacity

The VC dimension of a hypothesis class $\mathcal{H}$ is the largest number of points that $\mathcal{H}$ can shatter (classify in all possible label assignments).

Example: Linear classifiers in 2D
- Can shatter 3 points? YES (any labeling of 3 points can be separated)
- Can shatter 4 points? NO (for any 4 points, at least one labeling is impossible)
- VC dimension = 3

Example: Decision trees of depth D
- VC dimension = O(2^D) (exponential in depth)

Fundamental bound:

$$L_{test} \leq L_{train} + O\left(\sqrt{\frac{d_{VC}}{n}}\right)$$

Where $d_{VC}$ is VC dimension and $n$ is sample size. More complex models (higher VC dimension) need more data.

Rademacher Complexity β€” Data-Dependent Bound

VC dimension is distribution-independent (worst-case). Rademacher complexity gives tighter, data-dependent bounds:

$$\mathcal{R}_n(\mathcal{F}) = \mathbb{E}_{\sigma} \left[ \sup_{f \in \mathcal{F}} \frac{1}{n} \sum_{i=1}^n \sigma_i f(x_i) \right]$$

Where $\sigma_i \in {-1, +1}$ are random signs (Rademacher variables).

Intuition: How well can the best function in $\mathcal{F}$ fit random noise? If it can fit random labels well, it's complex and may overfit.

Generalization bound:

$$L_{test}(h) \leq L_{train}(h) + 2\mathcal{R}_n(\mathcal{F}) + O\left(\sqrt{\frac{\log(1/\delta)}{2n}}\right)$$

The Bias-Variance Tradeoff

Total error = BiasΒ² + Variance + Irreducible Noise

Mathematically:

$$\mathbb{E}[(y - \hat{f}(x))^2] = (\mathbb{E}[\hat{f}(x)] - f(x))^2 + \mathbb{E}[(\hat{f}(x) - \mathbb{E}[\hat{f}(x)])^2] + \sigma^2$$

The fundamental insight: You cannot reduce bias and variance simultaneously. Every modeling choice trades one for the other.

Double Descent

Modern finding (Belkin et al., 2019): For deep neural networks, the classical U-shaped bias-variance curve doesn't hold. Instead:

Key insight: Classical statistics says "more parameters than data = disaster." Deep learning shows it works beautifully. Overparameterization helps generalization.


Chapter 8: Evaluation & Validation

The Fundamental Split

# ALWAYS split before anything else
from sklearn.model_selection import train_test_split

# Standard: 80/20 or 70/15/15
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# For hyperparameter tuning: three-way split
X_train, X_temp, y_train, y_temp = train_test_split(X, y, test_size=0.3)
X_val, X_test, y_val, y_test = train_test_split(X_temp, y_temp, test_size=0.5)

K-Fold Cross-Validation

from sklearn.model_selection import KFold

kf = KFold(n_splits=5, shuffle=True, random_state=42)
scores = []

for train_idx, val_idx in kf.split(X_train):
    X_fold_train, X_fold_val = X_train[train_idx], X_train[val_idx]
    y_fold_train, y_fold_val = y_train[train_idx], y_train[val_idx]

    model = train(X_fold_train, y_fold_train)
    score = model.score(X_fold_val, y_fold_val)
    scores.append(score)

print(f"Mean CV score: {np.mean(scores):.3f} Β± {np.std(scores):.3f}")

Metrics by Task

Classification metrics in detail:

              Predicted Positive    Predicted Negative
Actual Pos    TP (True Positive)    FN (False Negative)
Actual Neg    FP (False Positive)   TN (True Negative)

Accuracy  = (TP + TN) / (TP + TN + FP + FN)  [overall correctness]
Precision = TP / (TP + FP)                    [when we say yes, how often right?]
Recall    = TP / (TP + FN)                    [how many real positives do we catch?]
F1        = 2Β·PΒ·R / (P + R)                  [harmonic mean of P and R]

When to use which:
- Accuracy: Only for balanced classes. Worthless for 99:1 class imbalance.
- Precision: Minimize false alarms (spam: don't put real emails in spam).
- Recall: Don't miss positives (cancer detection: better safe than sorry).
- F1: Balance both.
- ROC-AUC: Overall ranking quality (model's ability to separate classes).
- PR-AUC: Better for imbalanced datasets.


Part II: Deep Learning

Chapter 9: Neural Networks & Universal Approximation

The Single Neuron

$$\hat{y} = \sigma(\mathbf{w} \cdot \mathbf{x} + b)$$
def forward_neuron(x, w, b, activation='relu'):
    """
    A single neuron forward pass.
    x: input vector
    w: weight vector
    b: bias scalar
    """
    z = np.dot(x, w) + b
    if activation == 'relu':
        return np.maximum(0, z)
    elif activation == 'sigmoid':
        return 1 / (1 + np.exp(-np.clip(z, -100, 100)))
    elif activation == 'tanh':
        return np.tanh(z)
    elif activation == 'linear':
        return z

The Multilayer Perceptron (MLP)

Matrix form of a 2-layer network:

Layer 1: h = ReLU(x Β· W₁ + b₁)     x: (batch, D_in), W₁: (D_in, D_hidden)
Layer 2: Ε· = h Β· Wβ‚‚ + bβ‚‚            Wβ‚‚: (D_hidden, D_out)

The Universal Approximation Theorem

Theorem (Cybenko, 1989; Hornik, 1991): A feedforward network with a single hidden layer containing a finite number of neurons can approximate any continuous function on a compact subset of $\mathbb{R}^n$, given sufficiently many neurons and a non-polynomial activation function.

What this means: Neural networks can theoretically learn any function β€” given enough hidden units and the right weights. The question is whether:
1. We can find those weights (optimization)
2. It generalizes (overfitting)
3. It's computationally feasible (practical limits)

The catch: The number of neurons needed might be exponential in the input dimension for some functions. Deep networks (many layers) can represent the same functions exponentially more compactly. This is the depth separation result.


Chapter 10: Backpropagation β€” Full Derivation

Forward Pass

Consider a 3-layer network:

z₁ = x Β· W₁ + b₁        [pre-activation, layer 1]
a₁ = Οƒ(z₁)               [activation, layer 1]
zβ‚‚ = a₁ Β· Wβ‚‚ + bβ‚‚       [pre-activation, layer 2]
aβ‚‚ = Οƒ(zβ‚‚)               [activation, layer 2]
z₃ = aβ‚‚ Β· W₃ + b₃       [pre-activation, output layer]
Ε· = z₃                   [linear output for regression]
L = Β½(Ε· - y)Β²            [MSE loss]

Backward Pass

We want $\partial L / \partial W_1$, $\partial L / \partial W_2$, $\partial L / \partial W_3$, and similarly for biases.

Step 1: Output layer gradients

$$\frac{\partial L}{\partial z_3} = \hat{y} - y \quad \text{(let's call this } \delta_3\text{)}$$
$$\frac{\partial L}{\partial W_3} = a_2^T \cdot \delta_3$$
$$\frac{\partial L}{\partial b_3} = \delta_3$$

Step 2: Backprop through layer 2

$$\delta_2 = \delta_3 \cdot W_3^T \odot \sigma'(z_2)$$
$$\frac{\partial L}{\partial W_2} = a_1^T \cdot \delta_2$$
$$\frac{\partial L}{\partial b_2} = \delta_2$$

Step 3: Backprop through layer 1

$$\delta_1 = \delta_2 \cdot W_2^T \odot \sigma'(z_1)$$
$$\frac{\partial L}{\partial W_1} = x^T \cdot \delta_1$$
$$\frac{\partial L}{\partial b_1} = \delta_1$$

The Pattern

For any layer $l$:

$$\delta_l = \delta_{l+1} \cdot W_{l+1}^T \odot \sigma'(z_l)$$
$$\frac{\partial L}{\partial W_l} = a_{l-1}^T \cdot \delta_l$$
$$\frac{\partial L}{\partial b_l} = \delta_l$$

This is the backpropagation algorithm. The error $\delta$ "flows backward" through the network, with the chain rule applied at each layer.

def backprop_full(X, y, W1, b1, W2, b2, W3, b3):
    """Full forward + backward pass for 3-layer network."""
    # Forward pass
    z1 = X @ W1 + b1
    a1 = np.maximum(0, z1)  # ReLU
    z2 = a1 @ W2 + b2
    a2 = np.maximum(0, z2)
    z3 = a2 @ W3 + b3
    y_pred = z3

    # Loss
    loss = 0.5 * np.mean((y_pred - y) ** 2)

    # Backward pass
    m = X.shape[0]  # batch size

    # Output layer
    dz3 = (y_pred - y) / m  # δ₃
    dW3 = a2.T @ dz3
    db3 = np.sum(dz3, axis=0)

    # Layer 2
    da2 = dz3 @ W3.T
    dz2 = da2 * (z2 > 0)  # ReLU derivative
    dW2 = a1.T @ dz2
    db2 = np.sum(dz2, axis=0)

    # Layer 1
    da1 = dz2 @ W2.T
    dz1 = da1 * (z1 > 0)  # ReLU derivative
    dW1 = X.T @ dz1
    db1 = np.sum(dz1, axis=0)

    return loss, (dW1, db1, dW2, db2, dW3, db3)

Why This Is Efficient

Computing all gradients with finite differences would require $O(N)$ forward passes where $N$ is the number of parameters. Backpropagation does it in $O(1)$ forward + $O(1)$ backward passes β€” a speedup of thousands to millions.


Chapter 11: Convolutional Neural Networks

Why Not MLPs for Images?

A 256Γ—256 color image = 196,608 values. A dense layer with 1000 neurons = 196 million parameters. That's absurd.

The CNN insight: Nearby pixels are related. Patterns are local. Use a small filter everywhere.

The Convolution Operation

$$(I * K)[i, j] = \sum_{m} \sum_{n} I[i+m, j+n] \cdot K[m, n]$$
def conv2d_forward(image, kernel, stride=1, padding=0):
    """
    2D convolution from scratch.
    image: (H, W, C_in) β€” input image (height, width, channels)
    kernel: (K, K, C_in, C_out) β€” conv filters
    """
    H, W, C_in = image.shape
    K, _, _, C_out = kernel.shape

    # Apply padding
    if padding > 0:
        image = np.pad(image, ((padding, padding), (padding, padding), (0, 0)), mode='constant')

    # Output dimensions
    H_out = (H + 2*padding - K) // stride + 1
    W_out = (W + 2*padding - K) // stride + 1

    output = np.zeros((H_out, W_out, C_out))

    for h in range(H_out):
        for w in range(W_out):
            for c in range(C_out):
                # Extract region
                h_start = h * stride
                w_start = w * stride
                region = image[h_start:h_start+K, w_start:w_start+K, :]

                # Convolve
                output[h, w, c] = np.sum(region * kernel[:, :, :, c])

    return output

The Typical CNN

Key operations in one conv layer:
- Conv2D: apply filters β†’ feature maps
- BatchNorm: normalize activations
- ReLU: non-linearity
- MaxPool: down-sample (keep max in 2Γ—2 region)
- Dropout: regularize (randomly turn off neurons)

Modern CNN Innovations

ResNet (He et al., 2015): Residual connections β€” skip layers.

$$y = F(x) + x$$

Without residual: $y = F(x)$. The network must learn the identity mapping from scratch.
With residual: if $x$ is already good, $F(x)$ can be zero. Learning residuals is easier.

class ResidualBlock(nn.Module):
    def __init__(self, channels):
        super().__init__()
        self.conv1 = nn.Conv2d(channels, channels, 3, padding=1)
        self.bn1 = nn.BatchNorm2d(channels)
        self.conv2 = nn.Conv2d(channels, channels, 3, padding=1)
        self.bn2 = nn.BatchNorm2d(channels)

    def forward(self, x):
        residual = x
        out = F.relu(self.bn1(self.conv1(x)))
        out = self.bn2(self.conv2(out))
        out = out + residual  # skip connection
        return F.relu(out)

This simple addition allowed training networks with 152+ layers (vs. previous limit of ~20).


Chapter 12: Recurrent Networks & LSTMs

The Problem with Sequences

Standard neural networks assume all inputs are independent. For sequences (text, audio, time series), order is everything.

The Vanilla RNN

$$h_t = \tanh(W_h h_{t-1} + W_x x_t + b)$$
def rnn_step(x_t, h_prev, W_h, W_x, b):
    """
    One timestep of an RNN.
    x_t: input at time t
    h_prev: hidden state from t-1
    """
    return np.tanh(W_h @ h_prev + W_x @ x_t + b)

The problem: Gradients explode or vanish for sequences longer than ~20 steps. The repeated matrix multiplication in each step causes:

$$\frac{\partial L}{\partial h_0} \propto W_h^{\text{(sequence_length)}}$$

If $W_h$'s eigenvalues > 1 β†’ explode. If < 1 β†’ vanish to 0.

The LSTM (Long Short-Term Memory)

The key insight: A "cell state" $c_t$ that can carry information unchanged for hundreds of steps.

The LSTM equations:

Forget gate:    f_t = Οƒ(W_f Β· [h_{t-1}, x_t] + b_f)
Input gate:     i_t = Οƒ(W_i Β· [h_{t-1}, x_t] + b_i)
Candidate:      Ċ_t = tanh(W_c · [h_{t-1}, x_t] + b_c)
Cell update:    c_t = f_t βŠ™ c_{t-1} + i_t βŠ™ Ċ_t
Output gate:    o_t = Οƒ(W_o Β· [h_{t-1}, x_t] + b_o)
Hidden state:   h_t = o_t βŠ™ tanh(c_t)
class LSTMCell:
    def __init__(self, input_dim, hidden_dim):
        # Concatenated weights for efficiency
        self.W = np.random.randn(input_dim + hidden_dim, 4 * hidden_dim) * 0.01
        self.b = np.zeros(4 * hidden_dim)
        self.hidden_dim = hidden_dim

    def forward(self, x, h_prev, c_prev):
        """
        x: (input_dim,)
        h_prev: (hidden_dim,) β€” previous hidden state
        c_prev: (hidden_dim,) β€” previous cell state
        """
        # Concatenate input and previous hidden
        combined = np.concatenate([x, h_prev])

        # Compute all gates at once
        gates = self.W.T @ combined + self.b

        # Split into forget, input, candidate, output gates
        f = sigmoid(gates[:self.hidden_dim])
        i = sigmoid(gates[self.hidden_dim:2*self.hidden_dim])
        c_tilde = np.tanh(gates[2*self.hidden_dim:3*self.hidden_dim])
        o = sigmoid(gates[3*self.hidden_dim:])

        # Update cell state and hidden state
        c = f * c_prev + i * c_tilde
        h = o * np.tanh(c)

        return h, c

Why LSTMs work: The cell state $c_t$ has additive updates ($+$), not multiplicative ($\times$). Gradients through the cell state don't vanish β€” they can flow unchanged for hundreds of steps through the forget gate.


Chapter 13: Normalization & Regularization

The Overfitting Problem

L1 and L2 Regularization (Weight Decay)

L2 (Ridge): Add penalty proportional to squared weights.

$$L_{total} = L_{original} + \frac{\lambda}{2} \sum w_i^2$$

Effect: Shrinks all weights toward zero. Never produces exact zeros.

L1 (Lasso): Add penalty proportional to absolute weights.

$$L_{total} = L_{original} + \lambda \sum |w_i|$$

Effect: Shrinks weights to exact zero (feature selection).

# In practice: use weight_decay parameter in optimizer
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=0.01)

Dropout

Randomly set a fraction $p$ of neurons to 0 during training.

def dropout(x, p=0.5, training=True):
    """
    Dropout: randomly zero out p fraction of neurons.
    During testing: scale by p (or use all neurons).
    """
    if not training:
        return x

    mask = np.random.binomial(1, 1-p, x.shape) / (1-p)
    return x * mask

Why it works: Each training step trains a different "thinned" sub-network. At test time, all sub-networks implicitly ensemble. This is equivalent to training an ensemble of $2^n$ networks (where $n$ is number of neurons).

Batch Normalization

Normalize activations over the batch dimension:

$$\mu_B = \frac{1}{m} \sum_{i=1}^{m} x_i$$
$$\sigma_B^2 = \frac{1}{m} \sum_{i=1}^{m} (x_i - \mu_B)^2$$
$$\hat{x}_i = \frac{x_i - \mu_B}{\sqrt{\sigma_B^2 + \epsilon}}$$
$$y_i = \gamma \hat{x}_i + \beta$$
class BatchNorm:
    def __init__(self, num_features, eps=1e-5, momentum=0.1):
        self.gamma = np.ones(num_features)
        self.beta = np.zeros(num_features)
        self.running_mean = np.zeros(num_features)
        self.running_var = np.ones(num_features)
        self.eps = eps
        self.momentum = momentum

    def forward(self, x, training=True):
        if training:
            batch_mean = x.mean(axis=0)
            batch_var = x.var(axis=0)

            self.running_mean = (1 - self.momentum) * self.running_mean + self.momentum * batch_mean
            self.running_var = (1 - self.momentum) * self.running_var + self.momentum * batch_var

            x_norm = (x - batch_mean) / np.sqrt(batch_var + self.eps)
        else:
            x_norm = (x - self.running_mean) / np.sqrt(self.running_var + self.eps)

        return self.gamma * x_norm + self.beta

Why it works: Prevents internal covariate shift (the distribution of activations drifting during training). Allows higher learning rates by preventing activations from growing unbounded.

Layer Normalization

Same as batch norm, but normalized over feature dimension (not batch). Used in transformers.

$$\mu = \frac{1}{d} \sum_{k=1}^{d} x_k$$
$$\sigma^2 = \frac{1}{d} \sum_{k=1}^{d} (x_k - \mu)^2$$
$$\hat{x}_k = \frac{x_k - \mu}{\sqrt{\sigma^2 + \epsilon}}$$

Key difference: Batch norm depends on batch size. Layer norm doesn't β€” works for batch_size = 1.


Chapter 14: Optimization Algorithms

The Problem with Basic SGD

# Vanilla SGD
ΞΈ = ΞΈ - lr * βˆ‡L(ΞΈ)

Problems:
1. Fixed learning rate (too big or too small)
2. Same rate for all parameters (some need bigger updates)
3. Gets stuck in ravines (oscillates)

Momentum

Add velocity to gradient updates:

v = momentum * v - lr * grad
ΞΈ = ΞΈ + v

Intuition: Like a ball rolling down hill β€” it builds up speed, smooths out oscillations, and escapes small local minima.

Adam (Adaptive Moment Estimation)

Combines momentum + per-parameter adaptive learning rates.

def adam(params, grads, m, v, t, lr=0.001, beta1=0.9, beta2=0.999, eps=1e-8):
    """
    One step of Adam optimizer.
    """
    t += 1

    for i, (param, grad) in enumerate(zip(params, grads)):
        # Biased first moment estimate (momentum)
        m[i] = beta1 * m[i] + (1 - beta1) * grad

        # Biased second moment estimate (adaptive lr)
        v[i] = beta2 * v[i] + (1 - beta2) * grad**2

        # Bias correction
        m_hat = m[i] / (1 - beta1**t)
        v_hat = v[i] / (1 - beta2**t)

        # Update
        param[i] -= lr * m_hat / (np.sqrt(v_hat) + eps)

    return params, m, v, t

Optimizer Comparison

Optimizer Adaptive? Momentum? Best For
SGD No No Simple models, fine-tuning
SGD + Momentum No Yes When manual control is needed
Adam Yes Yes Default, works for everything
AdamW Yes Yes Transformers (decoupled weight decay)
Lion Yes Yes Very large models (memory efficient)

Rule of thumb: Start with AdamW (0.001, weight_decay=0.01). Switch to SGD with momentum for fine-tuning.


Part III: The Attention Revolution

Chapter 15: Attention Mechanisms

The Problem Attention Solves

Before attention (RNN era): When generating a translation, the model compressed the entire input sentence into one fixed-size vector. For long sentences, information at the beginning was lost.

Attention: At each output step, look back at ALL input positions and decide which ones matter.

Scaled Dot-Product Attention

$$\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V$$
def scaled_dot_product_attention(Q, K, V, mask=None):
    """
    Q: (batch, seq_len_q, d_k) β€” queries
    K: (batch, seq_len_k, d_k) β€” keys  
    V: (batch, seq_len_v, d_v) β€” values
                (seq_len_k must equal seq_len_v)
    """
    # Compute attention scores
    scores = Q @ K.transpose(-2, -1)  # (batch, q_len, k_len)

    # Scale to prevent softmax saturation
    scores = scores / np.sqrt(K.shape[-1])

    # Optional mask (for causal attention)
    if mask is not None:
        scores = scores + mask  # mask has -inf for forbidden positions

    # Softmax over key dimension
    weights = np.exp(scores - scores.max(axis=-1, keepdims=True))
    weights = weights / weights.sum(axis=-1, keepdims=True)

    # Weighted sum of values
    output = weights @ V  # (batch, q_len, d_v)

    return output, weights

Why scale by $\sqrt{d_k}$? Without scaling, large $d_k$ causes the dot products to grow large in magnitude, pushing the softmax into regions with tiny gradients (saturation).

Multi-Head Attention

Instead of one attention function, use $h$ heads in parallel, each attending to different patterns:

$$\text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_1, ..., \text{head}_h) W^O$$
$$\text{where head}_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V)$$
class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, n_heads):
        super().__init__()
        self.n_heads = n_heads
        self.d_head = d_model // n_heads

        # All projections
        self.W_q = nn.Linear(d_model, d_model)
        self.W_k = nn.Linear(d_model, d_model)
        self.W_v = nn.Linear(d_model, d_model)
        self.W_o = nn.Linear(d_model, d_model)

    def forward(self, x, mask=None):
        B, L, D = x.shape

        # Project and reshape: (B, L, D) β†’ (B, n_heads, L, d_head)
        Q = self.W_q(x).view(B, L, self.n_heads, self.d_head).transpose(1, 2)
        K = self.W_k(x).view(B, L, self.n_heads, self.d_head).transpose(1, 2)
        V = self.W_v(x).view(B, L, self.n_heads, self.d_head).transpose(1, 2)

        # Compute attention
        scores = Q @ K.transpose(-2, -1) / np.sqrt(self.d_head)

        if mask is not None:
            scores = scores + mask

        weights = F.softmax(scores, dim=-1)
        output = weights @ V  # (B, n_heads, L, d_head)

        # Concatenate heads: (B, n_heads, L, d_head) β†’ (B, L, D)
        output = output.transpose(1, 2).contiguous().view(B, L, D)

        return self.W_o(output)

Why multiple heads? Each head can focus on different relationships (e.g., syntax, semantics, position). The projections $W^Q, W^K, W^V$ give each head a different "perspective" on the data.


Chapter 16: Transformers β€” Full Architecture

The Complete Architecture

The Feed-Forward Network (FFN)

$$\text{FFN}(x) = W_2 \cdot \text{ReLU}(W_1 x + b_1) + b_2$$

Usually: inner dimension = 4 Γ— d_model (d_ff = 2048 for 512-dim model).

Positional Encoding

Since attention is permutation-invariant (no notion of order), we must inject position information.

Sinusoidal (original transformer):

$$PE_{(pos, 2i)} = \sin(pos / 10000^{2i/d_{model}})$$
$$PE_{(pos, 2i+1)} = \cos(pos / 10000^{2i/d_{model}})$$
def sinusoidal_positional_encoding(seq_len, d_model):
    """Create sinusoidal position encodings."""
    pe = np.zeros((seq_len, d_model))
    position = np.arange(0, seq_len)[:, np.newaxis]
    div_term = np.exp(np.arange(0, d_model, 2) * -(np.log(10000.0) / d_model))

    pe[:, 0::2] = np.sin(position * div_term)
    pe[:, 1::2] = np.cos(position * div_term)
    return pe

Rotary (RoPE) β€” modern standard (used in Llama, Mistral, GPT-NeoX):

Rotates query and key vectors by position-dependent angles:

$$q_m = R_m q, \quad k_n = R_n k$$

Where $R$ is a rotation matrix. This makes attention naturally depend on relative positions: $q_m \cdot k_n$ becomes a function of $m-n$.

ALiBi β€” alternative used in some models (BLOOM, MPT): Add bias to attention scores based on distance.

Causal Masking (For Decoder Models)

def create_causal_mask(seq_len):
    """
    Causal mask: each token attends to itself + previous tokens only.
    Upper triangle = -inf (can't attend to future).
    """
    mask = np.triu(np.ones((seq_len, seq_len)) * -1e9, k=1)
    return mask

# With causal mask:
# Position 0 can attend to: [0]
# Position 1 can attend to: [0, 1]
# Position 2 can attend to: [0, 1, 2]
# ...

The Original Transformer β€” Encoder-Decoder

class Transformer(nn.Module):
    """
    Full encoder-decoder transformer (original Vaswani et al. 2017).
    """
    def __init__(self, d_model=512, n_heads=8, n_layers=6, d_ff=2048, vocab_size=30000):
        super().__init__()
        self.encoder = nn.TransformerEncoder(
            nn.TransformerEncoderLayer(d_model, n_heads, d_ff),
            num_layers=n_layers
        )
        self.decoder = nn.TransformerDecoder(
            nn.TransformerDecoderLayer(d_model, n_heads, d_ff),
            num_layers=n_layers
        )
        self.embedding = nn.Embedding(vocab_size, d_model)
        self.output = nn.Linear(d_model, vocab_size)

    def forward(self, src_tokens, tgt_tokens):
        src_emb = self.embedding(src_tokens)
        tgt_emb = self.embedding(tgt_tokens)

        # Encoder: process input bidirectionally
        memory = self.encoder(src_emb)

        # Decoder: generate output with cross-attention to encoder
        output = self.decoder(tgt_emb, memory)

        return self.output(output)

Key Architectural Choices for GPT-style Models

Choice Why
Decoder-only Removes cross-attention (simpler, more compute for same params)
Pre-norm (LayerNorm before attention/FFN) More stable training than post-norm
RoPE position encoding Better length extrapolation
GELU/SwiGLU activation Better than ReLU in FFN
RMSNorm instead of LayerNorm Removes mean-centering, faster
Grouped-Query Attention Fewer KV-cache parameters

Chapter 17: BERT & Encoder Architectures

BERT's Innovation: Bidirectional Context

"Masked Language Model": Randomly mask 15% of tokens, predict them from context.

Input:  "The [MASK] chased the [MASK]"
Target: "The cat chased the mouse"

Why this matters: Unlike GPT (left-to-right), BERT sees both left and right context. This gives better understanding for tasks like classification, QA, and NER.

BERT's training:
1. Masked LM (80%: mask, 10%: random word, 10%: keep original)
2. Next Sentence Prediction: Is sentence B the actual next sentence after A?

class BERTEmbedding(nn.Module):
    def __init__(self, vocab_size, d_model=768, max_len=512):
        super().__init__()
        self.token_embedding = nn.Embedding(vocab_size, d_model)
        self.position_embedding = nn.Embedding(max_len, d_model)
        self.segment_embedding = nn.Embedding(2, d_model)  # sentence A/B

    def forward(self, token_ids, segment_ids):
        positions = torch.arange(token_ids.shape[1], device=token_ids.device)

        return (self.token_embedding(token_ids) + 
                self.position_embedding(positions) + 
                self.segment_embedding(segment_ids))

Chapter 18: GPT & Decoder Architectures

GPT's Key Insight: Scaling Autoregressive Models

"Language modeling is all you need." Train a decoder-only transformer to predict the next token. That's it.

The GPT Family

What scaled with size:
- Context length: 512 β†’ 2,048 β†’ 8,192 β†’ 128K β†’ 1M
- Training tokens: 4B β†’ 300B β†’ 2T β†’ 15T
- Emergent abilities: ICL, CoT, code, translation

Grouped-Query Attention (GQA)

The KV-cache (storing K and V for each token during generation) is the memory bottleneck for inference. GQA reduces it:

Type Query Heads Key/Value Heads Parameter Saving
MHA (Multi-Head) H H (each has own K,V) 0
GQA H G (G < H, shared in groups) Significant
MQA H 1 (all share one K,V) Maximum
class GroupedQueryAttention(nn.Module):
    def __init__(self, d_model, n_heads, n_kv_heads):
        super().__init__()
        self.n_heads = n_heads           # total query heads
        self.n_kv_heads = n_kv_heads     # key/value heads (n_heads // group_size)
        self.d_head = d_model // n_heads

        self.W_q = nn.Linear(d_model, n_heads * self.d_head, bias=False)
        self.W_k = nn.Linear(d_model, n_kv_heads * self.d_head, bias=False)
        self.W_v = nn.Linear(d_model, n_kv_heads * self.d_head, bias=False)
        self.W_o = nn.Linear(n_heads * self.d_head, d_model, bias=False)

    def forward(self, x):
        B, L, _ = x.shape

        Q = self.W_q(x).view(B, L, self.n_heads, self.d_head).transpose(1, 2)
        K = self.W_k(x).view(B, L, self.n_kv_heads, self.d_head).transpose(1, 2)
        V = self.W_v(x).view(B, L, self.n_kv_heads, self.d_head).transpose(1, 2)

        # Repeat K, V for each query head in the group
        n_groups = self.n_heads // self.n_kv_heads
        K = K.repeat_interleave(n_groups, dim=1)
        V = V.repeat_interleave(n_groups, dim=1)

        # Now all shapes match: (B, n_heads, L, d_head)
        scores = Q @ K.transpose(-2, -1) / (self.d_head ** 0.5)
        weights = F.softmax(scores, dim=-1)
        output = weights @ V

        output = output.transpose(1, 2).contiguous().view(B, L, -1)
        return self.W_o(output)

KV-Cache During Inference

def generate_with_kv_cache(model, input_ids, max_new_tokens=100):
    """
    Efficient generation using KV-cache.
    Instead of recomputing K,V for all previous tokens each step,
    we cache them and only compute for the new token.
    """
    kv_cache = {}

    for i in range(max_new_tokens):
        if i == 0:
            # First step: process full input sequence
            logits, kv_cache = model(input_ids, use_cache=True)
        else:
            # Subsequent steps: only process the last token
            logits, kv_cache = model(input_ids[:, -1:], kv_cache=kv_cache, use_cache=True)

        next_token = sample(logits[:, -1, :])
        input_ids = torch.cat([input_ids, next_token], dim=-1)

        if next_token.item() == tokenizer.eos_token_id:
            break

    return input_ids

Without KV-cache: O(nΒ²) per step β†’ O(nΒ³) total.
With KV-cache: O(n) per step β†’ O(nΒ²) total. This is what makes LLMs practical.


Chapter 19: Efficient Transformers

The Quadratic Problem

Standard attention: O(nΒ²) where n = sequence length.

n = 1,000:  1M operations
n = 10,000: 100M operations
n = 100,000: 10B operations
n = 1,000,000: 1T operations ← infeasible

Flash Attention

The insight: The main bottleneck isn't compute β€” it's memory bandwidth. The attention matrix [n, n] is too large to fit in fast SRAM.

Flash Attention (Dao et al., 2022) computes attention in blocks without materializing the full matrix:

# Standard attention: writes NΓ—N matrix to HBM (slow)
S = Q @ K.T     # read from HBM
P = softmax(S)  # write to HBM
O = P @ V       # read from HBM

# Flash attention: process in tiles, stay in SRAM
# Never materialize full NΓ—N matrix
# Hardware-aware tiling

Speedup: 2-4Γ— for long sequences. This is why we can now do 128K+ context.

Other Efficient Approaches

Method Complexity How
Flash Attention O(nΒ²) but 2-4Γ— faster Memory-efficient tiling
Sparse Attention O(n log n) Only attend to local + few global tokens
Linear Attention O(n) Replace softmax with kernel feature maps
Ring Attention O(nΒ²) distributed Distribute across GPUs for 1M+ tokens
Sliding Window O(nk) Each token attends to k nearest neighbors

Chapter 20: State Space Models (S4, Mamba)

The SSM Formulation

Continuous: $h'(t) = A h(t) + B x(t)$
Output: $y(t) = C h(t)$

Discretized (for computer): $h_t = \bar{A} h_{t-1} + \bar{B} x_t$
Output: $y_t = C h_t$

Where $\bar{A} = \exp(\Delta A)$ and $\bar{B} = (\Delta A)^{-1}(\exp(\Delta A) - I) \cdot \Delta B$.

Why SSMs Matter

Recurrent at inference (fast, constant memory like RNN):
$h_t = \bar{A} h_{t-1} + \bar{B} x_t$ β€” just one step, O(1) per token.

Convolutional at training (parallelizable like CNN):
$y = K * x$ where $K = (C\bar{B}, C\bar{A}\bar{B}, C\bar{A}^2\bar{B}, ...)$ β€” can compute via FFT, O(n log n).

Mamba's Innovation: Selectivity

Previous SSMs (S4, H3, etc.) used fixed $A, B, C$ parameters for all inputs. Mamba makes $B, C, \Delta$ input-dependent:

$$B = \text{linear}(x_t), \quad C = \text{linear}(x_t), \quad \Delta = \text{softplus}(\text{linear}(x_t))$$

This means the model can selectively remember or ignore information based on content β€” just like attention, but with O(n) complexity.

# Simplified Mamba block processing one sequence
def mamba_block(x, dt_linear, A, B_proj, C_proj):
    """
    x: (batch, seq_len, d_model)
    Returns: (batch, seq_len, d_model)
    """
    B, L, D = x.shape

    # Input-dependent parameters
    delta = F.softplus(dt_linear(x))  # (B, L, D) β€” step size, per token

    B = B_proj(x)  # (B, L, D) β€” input-dependent B
    C = C_proj(x)  # (B, L, D) β€” input-dependent C

    # Discretize A
    A_bar = torch.exp(delta.unsqueeze(-1) * A)  # (B, L, D, N)
    B_bar = delta * B  # simplified

    # Scan: h_t = A_bar_t * h_{t-1} + B_bar_t * x_t
    h = torch.zeros(B, D, device=x.device)
    ys = []

    for t in range(L):
        h = A_bar[:, t] * h + B_bar[:, t] * x[:, t]
        y = (C[:, t] * h).sum(-1)
        ys.append(y)

    return torch.stack(ys, dim=1)

# In practice: use parallel associative scan (O(n log n) instead of O(n))

Chapter 21: Graph Neural Networks

Why Graphs?

Not all data fits in grids or sequences. Social networks, molecules, knowledge graphs β€” these are graphs.

Message Passing (The Standard GNN)

Each node gathers information from its neighbors:

$$h_v^{(k)} = \sigma\left(W^{(k)} \cdot \text{AGGREGATE}\left(\{h_u^{(k-1)}, \forall u \in \mathcal{N}(v)\}\right)\right)$$
class GCNLayer(nn.Module):
    """
    Graph Convolutional Network layer.
    AGGREGATE = mean of neighbors.
    """
    def __init__(self, in_dim, out_dim):
        super().__init__()
        self.W = nn.Linear(in_dim, out_dim)

    def forward(self, x, adj_matrix):
        """
        x: (num_nodes, in_dim)
        adj_matrix: (num_nodes, num_nodes) β€” normalized adjacency
        """
        # Message passing: neighbor features aggregated by adjacency
        neighbor_aggregated = adj_matrix @ x
        return torch.relu(self.W(neighbor_aggregated))

The WL Test and GNN Expressivity

Key result (Xu et al., 2019):
- Standard message-passing GNNs are at most as powerful as the 1-Weisfeiler-Lehman (WL) test.
- The WL test is a graph isomorphism heuristic: "Are these two graphs the same?"
- GNNs can't distinguish graphs that 1-WL can't distinguish.

GIN (Graph Isomorphism Network) achieves the WL power limit by using sum aggregation:

$$h_v = \text{MLP}\left((1+\epsilon)h_v + \sum_{u \in \mathcal{N}(v)} h_u\right)$$

Part IV: Generative AI

Chapter 22: Variational Autoencoders

The Problem VAEs Solve

We want a generative model that creates new data points. But how do we learn the underlying distribution $P(x)$?

VAE solution: Learn a latent space $z$ where $P(z)$ is simple (Gaussian), and a decoder $P(x|z)$ that maps $z$ to data.

The VAE Objective (ELBO)

$$\log P(x) \geq \mathbb{E}_{z \sim q(z|x)}[\log P(x|z)] - KL(q(z|x) || P(z))$$
class VAE(nn.Module):
    def __init__(self, input_dim=784, latent_dim=20):
        super().__init__()
        # Encoder: x β†’ z (Gaussian parameters)
        self.encoder = nn.Sequential(
            nn.Linear(input_dim, 256),
            nn.ReLU(),
        )
        self.mu_layer = nn.Linear(256, latent_dim)
        self.logvar_layer = nn.Linear(256, latent_dim)

        # Decoder: z β†’ x
        self.decoder = nn.Sequential(
            nn.Linear(latent_dim, 256),
            nn.ReLU(),
            nn.Linear(256, input_dim),
            nn.Sigmoid(),
        )

    def encode(self, x):
        h = self.encoder(x)
        return self.mu_layer(h), self.logvar_layer(h)

    def reparameterize(self, mu, logvar):
        """Reparameterization trick: z = ΞΌ + Οƒ * Ξ΅"""
        std = torch.exp(0.5 * logvar)
        eps = torch.randn_like(std)
        return mu + eps * std

    def decode(self, z):
        return self.decoder(z)

    def forward(self, x):
        mu, logvar = self.encode(x)
        z = self.reparameterize(mu, logvar)
        x_recon = self.decode(z)
        return x_recon, mu, logvar, z

def vae_loss(x, x_recon, mu, logvar):
    """Reconstruction + KL divergence loss."""
    # Reconstruction: binary cross-entropy
    recon_loss = F.binary_cross_entropy(x_recon, x, reduction='sum')

    # KL divergence: q(z|x) || N(0, I)
    # Closed form for Gaussian: -Β½Ξ£(1 + log(σ²) - ΞΌΒ² - σ²)
    kl_loss = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())

    return recon_loss + kl_loss

The Reparameterization Trick

Why it's needed: During backprop, we need gradients through a random sampling step. The "trick" is to separate the randomness:

$$z = \mu + \sigma \cdot \epsilon, \quad \epsilon \sim \mathcal{N}(0, I)$$

Now $\epsilon$ is a fixed noise source β€” gradients flow through $\mu$ and $\sigma$ normally.


Chapter 23: Generative Adversarial Networks

The Game

Two networks play a minimax game:

$$\min_G \max_D \mathbb{E}_{x \sim p_{data}}[\log D(x)] + \mathbb{E}_{z \sim p_z}[\log(1 - D(G(z)))]$$
class GAN(nn.Module):
    def __init__(self, latent_dim=100):
        super().__init__()
        self.generator = nn.Sequential(
            nn.Linear(latent_dim, 256),
            nn.ReLU(),
            nn.Linear(256, 512),
            nn.ReLU(),
            nn.Linear(512, 784),
            nn.Tanh(),  # images [-1, 1]
        )

        self.discriminator = nn.Sequential(
            nn.Linear(784, 512),
            nn.LeakyReLU(0.2),
            nn.Linear(512, 256),
            nn.LeakyReLU(0.2),
            nn.Linear(256, 1),
            nn.Sigmoid(),
        )

def train_gan_step(gan, real_images, optimizer_G, optimizer_D, latent_dim=100):
    batch_size = real_images.size(0)

    # --- Train Discriminator ---
    optimizer_D.zero_grad()

    # Real images: D should output 1
    real_pred = gan.discriminator(real_images)
    real_loss = F.binary_cross_entropy(real_pred, torch.ones_like(real_pred))

    # Fake images: D should output 0
    z = torch.randn(batch_size, latent_dim)
    fake_images = gan.generator(z)
    fake_pred = gan.discriminator(fake_images.detach())  # stop gradient to G
    fake_loss = F.binary_cross_entropy(fake_pred, torch.zeros_like(fake_pred))

    d_loss = (real_loss + fake_loss) / 2
    d_loss.backward()
    optimizer_D.step()

    # --- Train Generator ---
    optimizer_G.zero_grad()

    z = torch.randn(batch_size, latent_dim)
    fake_images = gan.generator(z)
    fake_pred = gan.discriminator(fake_images)

    # G wants D to output 1 on its fakes
    g_loss = F.binary_cross_entropy(fake_pred, torch.ones_like(fake_pred))
    g_loss.backward()
    optimizer_G.step()

    return d_loss.item(), g_loss.item()

Why VAEs Won Over GANs

Aspect VAE GAN
Training stability βœ… Stable ❌ Mode collapse, non-convergence
Sample quality ⚠️ Blurry βœ… Sharp
Latent space βœ… Smooth, continuous ❌ Disconnected
Likelihood βœ… Tractable bound ❌ None
Evaluation βœ… ELBO ❌ Inception score only

Today: GANs are mostly historical. Diffusion models produce better images. VAEs are used for latent compression (Stable Diffusion uses a VAE).


Chapter 24: Normalizing Flows

The Change of Variables Formula

If $z \sim p_z(z)$ and $x = f(z)$ where $f$ is invertible and differentiable:

$$p_x(x) = p_z(f^{-1}(x)) \left|\det \frac{\partial f^{-1}}{\partial x}\right|$$

Key insight: Unlike VAEs (which approximate), flows give exact density estimation.

Coupling Layers (RealNVP)

Split input into two halves:

1. First half passes through unchanged
2. Second half is transformed: 
   shift = NN1(first_half), scale = NN2(first_half)
   second_half_out = (second_half + shift) * exp(scale)
3. Concatenate and alternate which half is transformed
class AffineCoupling(nn.Module):
    def __init__(self, input_dim, hidden_dim=256):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(input_dim // 2, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, input_dim),  # outputs shift + scale
        )

    def forward(self, x):
        x_a, x_b = x.chunk(2, dim=-1)

        params = self.net(x_a)
        shift, scale = params.chunk(2, dim=-1)

        y_b = x_b * torch.exp(scale) + shift
        log_det = scale.sum(-1)  # log determinant

        return torch.cat([x_a, y_b], dim=-1), log_det

    def inverse(self, y):
        y_a, y_b = y.chunk(2, dim=-1)

        params = self.net(y_a)
        shift, scale = params.chunk(2, dim=-1)

        x_b = (y_b - shift) * torch.exp(-scale)

        return torch.cat([y_a, x_b], dim=-1)

Triangular Jacobian: The coupling layer's Jacobian is triangular (since x_a β†’ y_a is identity), so its determinant is just the product of diagonal elements β€” O(d) computation instead of O(dΒ³).


Chapter 25: Energy-Based Models

The Energy Function

Instead of directly modeling $P(x)$, define an energy function $E(x)$ and:

$$P(x) = \frac{e^{-E(x)}}{Z}, \quad Z = \int e^{-E(x)} dx$$

Where $Z$ is the partition function (intractable β€” but we don't compute it directly).

Training with Contrastive Divergence

The gradient of the log-likelihood:

$$\nabla_\theta \log P(x) = -\nabla_\theta E(x) + \mathbb{E}_{x^- \sim P}\nabla_\theta E(x^-)$$

First term: Push energy down on real data.
Second term: Push energy up on (sampled) fake data.

We approximate the second term with Langevin Dynamics:

Langevin Dynamics Sampling

def langevin_sampling(model, x_init, step_size=0.01, n_steps=100):
    """
    Sample from an EBM using Langevin MCMC.
    """
    x = x_init.clone().requires_grad_(True)

    for _ in range(n_steps):
        energy = model(x).sum()
        grad = torch.autograd.grad(energy, x)[0]
        noise = torch.randn_like(x) * np.sqrt(2 * step_size)
        x = x - step_size * grad + noise
        x = x.detach().requires_grad_(True)

    return x

Chapter 26: Diffusion Models β€” Full Derivation

The High-Level Idea

Forward Process (Adding Noise)

$$q(x_t | x_{t-1}) = \mathcal{N}(x_t; \sqrt{1-\beta_t} x_{t-1}, \beta_t I)$$

After $t$ steps:

$$x_t = \sqrt{\bar{\alpha}_t} x_0 + \sqrt{1-\bar{\alpha}_t} \epsilon, \quad \epsilon \sim \mathcal{N}(0, I)$$

Where $\alpha_t = 1 - \beta_t$ and $\bar{\alpha}t = \prod^t \alpha_s$.

Reverse Process (Denoising)

$$p_\theta(x_{t-1} | x_t) = \mathcal{N}(x_{t-1}; \mu_\theta(x_t, t), \Sigma_\theta(x_t, t))$$

We train $\mu_\theta$ to predict the noise $\epsilon$ that was added:

$$\mu_\theta(x_t, t) = \frac{1}{\sqrt{\alpha_t}} \left(x_t - \frac{\beta_t}{\sqrt{1-\bar{\alpha}_t}} \epsilon_\theta(x_t, t)\right)$$

Training Objective

$$\mathcal{L} = \mathbb{E}_{t, x_0, \epsilon}\left[ ||\epsilon - \epsilon_\theta(\sqrt{\bar{\alpha}_t} x_0 + \sqrt{1-\bar{\alpha}_t} \epsilon, t)||^2 \right]$$

That's it. The model is trained to predict the added noise.

def train_diffusion_step(model, x0, timestep, noise_schedule):
    """
    One training step for DDPM.
    """
    # Sample noise
    noise = torch.randn_like(x0)

    # Forward process: x_t = sqrt(Ξ±Μ„_t) * xβ‚€ + sqrt(1-Ξ±Μ„_t) * noise
    sqrt_alpha_bar = noise_schedule['sqrt_alpha_bar'][timestep]
    sqrt_one_minus_alpha_bar = noise_schedule['sqrt_one_minus_bar'][timestep]

    x_t = sqrt_alpha_bar * x0 + sqrt_one_minus_alpha_bar * noise

    # Predict noise
    noise_pred = model(x_t, timestep)

    # Loss: MSE between true and predicted noise
    loss = F.mse_loss(noise_pred, noise)
    return loss

def sample_ddpm(model, noise_schedule, num_timesteps=1000, image_shape=(3, 32, 32)):
    """
    Sample from a trained DDPM.
    """
    # Start from pure noise
    x = torch.randn(1, *image_shape)

    for t in reversed(range(num_timesteps)):
        # Predict noise at step t
        noise_pred = model(x, t)

        # Compute x_{t-1} mean
        alpha = noise_schedule['alpha'][t]
        alpha_bar = noise_schedule['alpha_bar'][t]
        beta = noise_schedule['beta'][t]

        # ΞΌ = 1/√α_t * (x_t - Ξ²_t/√(1-Ξ±Μ„_t) * Ξ΅_ΞΈ)
        mean = (1 / np.sqrt(alpha)) * (x - (beta / np.sqrt(1 - alpha_bar)) * noise_pred)

        # Add noise (except for t = 0)
        if t > 0:
            noise = torch.randn_like(x)
            x = mean + np.sqrt(beta) * noise
        else:
            x = mean

    return x

The Score Function Connection

The noise prediction $\epsilon_\theta(x_t, t)$ is equivalent to estimating the score function β€” the gradient of the log-probability density:

$$\epsilon_\theta(x_t, t) \approx -\sqrt{1-\bar{\alpha}_t} \nabla_{x_t} \log p(x_t)$$

Score matching (HyvΓ€rinen, 2005): Train a model to estimate $\nabla_x \log p(x)$ without computing $p(x)$. This is exactly what diffusion models do.

The SDE Formulation (Song et al., 2020)

The discrete diffusion process is a discretization of a stochastic differential equation (SDE):

$$dx = f(x, t) dt + g(t) dw$$

Forward: Add noise continuously.
Reverse: Remove noise continuously β€” solving a reverse-time SDE.

This connects diffusion to:
- Probability flow ODE: A deterministic ODE with the same marginal density β†’ fast sampling
- Score matching: The drift term of the reverse SDE equals the score function

Classifier-Free Guidance (CFG)

For conditional generation (e.g., text-to-image), CFG balances diversity vs. fidelity:

$$\tilde{\epsilon}_\theta(x_t, c) = \epsilon_\theta(x_t, \emptyset) + w \cdot (\epsilon_\theta(x_t, c) - \epsilon_\theta(x_t, \emptyset))$$

Chapter 27: Flow Matching & Rectified Flows

The Problem with Diffusion

Diffusion models need many steps (50-1000) because the reverse process is a stochastic differential equation.

Flow Matching: Learn a deterministic ODE that transforms noise to data in one step.

The Flow Matching Objective

Define a linear interpolation between noise $x_1$ and data $x_0$:

$$x_t = (1-t) x_0 + t x_1, \quad t \in [0, 1]$$

The velocity $v_t = dx_t/dt = x_1 - x_0$ is constant.

Train a model $v_\theta(x_t, t)$ to predict this velocity:

$$\mathcal{L} = \mathbb{E}_{t, x_0, x_1}\left[||v_\theta(x_t, t) - (x_1 - x_0)||^2\right]$$
def train_flow_matching_step(model, x0, t):
    """
    One training step for flow matching.
    """
    # Sample noise
    x1 = torch.randn_like(x0)

    # Interpolate: x_t = (1-t) * x0 + t * x1
    t = t[:, None, None, None]  # broadcast over image dims
    x_t = (1 - t) * x0 + t * x1

    # Target velocity: dx/dt = x1 - x0
    velocity_target = x1 - x0

    # Predict velocity
    velocity_pred = model(x_t, t)

    loss = F.mse_loss(velocity_pred, velocity_target)
    return loss

def sample_flow_matching(model, num_steps=1, image_shape=(3, 32, 32)):
    """
    Sample from flow matching model.
    In theory, 1 step works. In practice, 2-4 steps for quality.
    """
    x = torch.randn(1, *image_shape)

    dt = 1.0 / num_steps
    for i in range(num_steps):
        t = torch.tensor([i * dt])
        velocity = model(x, t)
        x = x + velocity * dt  # Euler step

    return x

Why Flow Matching May Beat Diffusion

Aspect Diffusion Flow Matching
Sampling steps 10-1000 1-4
Training objective Predict noise Predict velocity
Deterministic? With DDIM Yes (always)
Likelihood estimation Via probability flow ODE Direct ODE
Theoretical grounding Score matching + SDE Optimal transport
Best architecture U-Net (2D), DiT (latent) Same

Chapter 28: Large Language Models

The Architecture (GPT-style)

class GPTBlock(nn.Module):
    """A single GPT transformer block."""
    def __init__(self, d_model=4096, n_heads=32, d_ff=11008):
        super().__init__()
        self.attention = GroupedQueryAttention(d_model, n_heads, n_kv_heads=8)
        self.norm1 = RMSNorm(d_model)
        self.feed_forward = nn.Sequential(
            nn.Linear(d_model, d_ff, bias=False),
            nn.SiLU(),  # SwiGLU
            nn.Linear(d_ff, d_model, bias=False),
        )
        self.norm2 = RMSNorm(d_model)

    def forward(self, x):
        x = x + self.attention(self.norm1(x))
        x = x + self.feed_forward(self.norm2(x))
        return x

class GPT(nn.Module):
    def __init__(self, vocab_size=32000, d_model=4096, n_layers=32, n_heads=32):
        super().__init__()
        self.embed = nn.Embedding(vocab_size, d_model)
        self.blocks = nn.ModuleList([
            GPTBlock(d_model, n_heads) for _ in range(n_layers)
        ])
        self.norm = RMSNorm(d_model)
        self.lm_head = nn.Linear(d_model, vocab_size, bias=False)

        # Tie embedding and lm_head weights
        self.lm_head.weight = self.embed.weight

    def forward(self, input_ids):
        x = self.embed(input_ids)
        for block in self.blocks:
            x = block(x)
        x = self.norm(x)
        return self.lm_head(x)

Tokenization

Words β†’ integers (tokens). The most common: Byte-Pair Encoding (BPE).

"hello world" β†’ [15339, 1917]  (each token β‰ˆ 0.75 words)
                          or
"hello world" β†’ ["hel", "lo", " world"]  (subword units)

Why tokenization matters:
- Vocabulary size (32K-128K tokens)
- Special tokens: <|begin_of_text|>, <|end_of_text|>, <|padding|>
- A model's tokenizer defines what it can and cannot represent

Training: The Three Stages

Stage 1: Pretraining (cost: $10M-$100M)
- Data: 2-15 trillion tokens from the internet, books, code
- Objective: next token prediction
- Result: Base model β€” knows language, facts, patterns

Stage 2: Supervised Fine-Tuning (SFT) (cost: $10K-$100K)
- Data: 10K-100K human-written Q&A pairs
- Objective: next token prediction on these examples
- Result: Instruction model β€” follows instructions

Stage 3: RLHF/DPO (cost: $10K-$100K)
- Data: 100K-1M human preference judgments
- Objective: maximize human preference reward
- Result: Aligned model β€” helpful, harmless, honest


Chapter 29: Multimodal Models

The CLIP Approach

Contrastive Language-Image Pretraining: Learn a shared embedding space for images and text.

def clip_training_step(image_encoder, text_encoder, images, texts, temperature=0.07):
    """
    CLIP training: contrastive learning over image-text pairs.
    """
    # Encode both modalities
    image_emb = image_encoder(images)  # (batch, d_embed)
    text_emb = text_encoder(texts)     # (batch, d_embed)

    # Normalize
    image_emb = image_emb / image_emb.norm(dim=-1, keepdim=True)
    text_emb = text_emb / text_emb.norm(dim=-1, keepdim=True)

    # Compute similarity matrix
    logits = image_emb @ text_emb.T / temperature  # (batch, batch)

    # Labels: matching pairs are on the diagonal
    labels = torch.arange(len(images), device=images.device)

    # Symmetric cross-entropy loss
    loss_i = F.cross_entropy(logits, labels)     # image β†’ text
    loss_t = F.cross_entropy(logits.T, labels)   # text β†’ image

    return (loss_i + loss_t) / 2

Result: CLIP can do zero-shot classification: "Which text description best matches this image?" without any fine-tuning.

LLaVA-Style Architecture

The modern multimodal approach:

class LLaVA(nn.Module):
    """
    Visual Language Model (simplified LLaVA architecture):
    1. CLIP vision encoder extracts image patches
    2. Projection layer maps to LLM embedding space
    3. LLM processes concatenated [image_tokens, text_tokens]
    """
    def __init__(self, vision_encoder, llm, projection_dim=4096):
        super().__init__()
        self.vision_encoder = vision_encoder  # CLIP ViT-L/14
        self.projection = nn.Linear(vision_encoder.embed_dim, projection_dim)
        self.llm = llm  # any LLM

    def forward(self, images, text_input_ids):
        # Get image features
        with torch.no_grad():
            image_features = self.vision_encoder(images).last_hidden_state
        image_tokens = self.projection(image_features)

        # Get text embeddings
        text_emb = self.llm.embed(text_input_ids)

        # Concatenate: image tokens first, then text tokens
        combined = torch.cat([image_tokens, text_emb], dim=1)

        # Process with LLM (generate response)
        return self.llm.forward_with_embeddings(combined)

Part V: Advanced Training

Chapter 30: Reinforcement Learning

The RL Framework

Goal: Learn policy $\pi(a|s)$ that maximizes cumulative reward $\sum_t \gamma^t r_t$.

PPO (Proximal Policy Optimization)

The standard RL algorithm used in RLHF. Key insight: Don't update the policy too much in one step.

$$L^{PPO} = \min\left(\frac{\pi_\theta(a|s)}{\pi_{old}(a|s)} A, \text{clip}\left(\frac{\pi_\theta(a|s)}{\pi_{old}(a|s)}, 1-\epsilon, 1+\epsilon\right) A\right)$$
def ppo_clip_loss(log_probs_new, log_probs_old, advantages, epsilon=0.2):
    """
    PPO clipped surrogate objective.
    log_probs_new: Ο€_ΞΈ(a|s)
    log_probs_old: Ο€_{old}(a|s)
    advantages: A(s,a) β€” how much better was this action than average?
    """
    ratio = torch.exp(log_probs_new - log_probs_old)
    clipped_ratio = torch.clamp(ratio, 1 - epsilon, 1 + epsilon)

    loss = -torch.min(ratio * advantages, clipped_ratio * advantages)
    return loss.mean()

Chapter 31: RLHF & Preference Optimization

The Full RLHF Pipeline

DPO β€” The Elegant Alternative

DPO (Direct Preference Optimization, Rafailov et al., 2024) eliminates the need for a separate reward model:

$$L_{DPO} = -\log \sigma\left(\beta \log \frac{\pi_\theta(y_w|x)}{\pi_{ref}(y_w|x)} - \beta \log \frac{\pi_\theta(y_l|x)}{\pi_{ref}(y_l|x)}\right)$$

Where:
- $y_w$: preferred (winning) response
- $y_l$: non-preferred (losing) response
- $\pi_{ref}$: the model before alignment (frozen)
- $\beta$: how much to prefer the chosen response

Intuition: Increase probability of $y_w$ relative to $y_l$, but don't drift too far from $\pi_{ref}$.

def dpo_loss(policy_logps, ref_logps, win_idx, lose_idx, beta=0.1):
    """
    policy_logps: log Ο€_ΞΈ(y|x) for each response
    ref_logps: log Ο€_{ref}(y|x) for each response (frozen)
    """
    # Log ratio: log(Ο€_ΞΈ(y|x) / Ο€_{ref}(y|x))
    policy_diff = policy_logps[win_idx] - ref_logps[win_idx]
    ref_diff = policy_logps[lose_idx] - ref_logps[lose_idx]

    # DPO loss
    logits = beta * (policy_diff - ref_diff)
    loss = -F.logsigmoid(logits)

    return loss.mean()

Chapter 32: Alignment & Safety

The Specification Gaming Problem

When you optimize for a metric, the model finds unexpected ways to maximize it without doing the intended task:

Example: CoastRunners boat racing game AI
Goal: maximize score β†’ Result: AI goes in circles hitting repair buoys forever
instead of actually racing. High score, terrible gameplay.

Same thing happens with LLMs:
- Want: "Write a helpful summary"
- Reward model optimized β†’ Model writes "excellent summary" that's actually wrong but uses confident, pleasing language

Constitutional AI

Instead of expensive human preferences, define a constitution (set of rules) and have the model critique its own outputs:

# Constitutional AI: self-critique + revision
def constitutional_step(model, response, constitution_rules):
    """
    1. Model generates response
    2. Model critiques its own response based on rules
    3. Model revises response based on critique
    4. Train on (response β†’ revised_response) pairs
    """
    critiques = model.critique(response, constitution_rules)
    revised = model.revise(response, critiques)
    return revised

# Example constitution rules:
# - "Be helpful, harmless, and honest"
# - "Do not engage in harmful stereotypes"
# - "Acknowledge uncertainty when unsure"

The Alignment Tax

Post-alignment, models often:
- Refuse too many requests (false rejections)
- Become less creative
- Lose some capabilities (e.g., code generation quality drops)

The challenge: minimize the alignment tax while maximizing safety.


Chapter 33: Scaling Laws

Kaplan et al. (2020) β€” The Original Finding

Test loss decreases as a power law with each of:

Factor Notation Exponent Implication
Model size $N$ $-\alpha_N \approx -0.076$ 10Γ— params β†’ 5.7Γ— better loss
Data size $D$ $-\alpha_D \approx -0.095$ 10Γ— data β†’ 7.1Γ— better loss
Compute $C$ $-\alpha_C \approx -0.050$ 10Γ— compute β†’ 3.2Γ— better loss

Overshadowing: If you scale only one factor, you get diminishing returns. The optimal is to scale ALL three together: $C \propto N^{2.4} D^{2.4}$.

Chinchilla (Hoffmann et al., 2022) β€” The Correction

Kaplan said: More parameters > more data.
Chinchilla said: Scale both equally. Doubling parameters requires doubling data.

For compute-optimal training:

$$N_{opt} \propto C^{0.5}, \quad D_{opt} \propto C^{0.5}$$

The 70B Chinchilla model achieved the same loss as GPT-3 (175B) with 60% less compute.

Practical Implications

If You Want To... Then...
Double model size Double training data (Chinchilla)
Improve test loss 10% Need 24Γ— more compute (Kaplan)
Train optimally N∝C^0.5, D∝C^0.5
Predict loss L(N, D) = A/N^Ξ± + B/D^Ξ² + Eβ‚€

The Emergence Debate

Wei et al. (2022): As models scale past a threshold, new abilities "emerge" (suddenly appear).
- Example: GPT-3 (175B) can do few-shot math, smaller models cannot

Schaeffer et al. (2023): "Emergence" is a measurement artifact β€” due to discontinuous evaluation metrics (multiple-choice, accuracy). With continuous metrics (probability, perplexity), abilities improve smoothly.

Current consensus: Both are partially right. Some abilities are genuinely new (chains-of-thought reasoning appears around 10B params), but many "emergent" abilities are measurement artifacts.


Chapter 34: Mixture of Experts

The MoE Layer

Every token is routed to a subset of "experts" (FFN sub-networks):

class MoELayer(nn.Module):
    def __init__(self, d_model, n_experts=8, top_k=2):
        super().__init__()
        self.n_experts = n_experts
        self.top_k = top_k

        self.experts = nn.ModuleList([
            nn.Sequential(
                nn.Linear(d_model, d_model * 4),
                nn.GELU(),
                nn.Linear(d_model * 4, d_model),
            ) for _ in range(n_experts)
        ])

        # Router
        self.router = nn.Linear(d_model, n_experts, bias=False)

    def forward(self, x):
        B, L, D = x.shape
        x_flat = x.view(-1, D)  # (B*L, D)

        # Routing
        routing_logits = self.router(x_flat)  # (B*L, n_experts)
        routing_weights = F.softmax(routing_logits, dim=-1)

        # Top-k routing
        top_k_weights, top_k_indices = torch.topk(routing_weights, self.top_k, dim=-1)
        top_k_weights = top_k_weights / top_k_weights.sum(dim=-1, keepdim=True)

        # Compute expert outputs
        output = torch.zeros_like(x_flat)
        for k in range(self.top_k):
            expert_idx = top_k_indices[:, k]
            weight = top_k_weights[:, k, None]

            # Process tokens assigned to each expert
            for e in range(self.n_experts):
                mask = (expert_idx == e)
                if mask.any():
                    output[mask] += weight[mask] * self.experts[e](x_flat[mask])

        return output.view(B, L, D)

Load Balancing Loss

Without intervention, the router sends all tokens to 1-2 experts. Solutions:

def load_balancing_loss(routing_logits, n_experts, n_tokens):
    """
    Encourage uniform expert utilization.
    """
    routing_probs = F.softmax(routing_logits, dim=-1)

    # Fraction of tokens sent to each expert
    fraction_selected = routing_probs.mean(dim=0)

    # Uniform target
    target = torch.ones_like(fraction_selected) / n_experts

    # CV loss: coefficient of variation of load
    load = routing_probs > 0
    load_frac = load.float().mean(dim=0)
    cv = load_frac.std() / load_frac.mean()

    loss = cv * n_tokens  # scaled by batch size
    return loss * 0.01  # small coefficient

Chapter 35: Distributed Training

Why Distributed?

A single GPU can hold maybe 30B parameters (in FP16). Training a 70B+ model requires distributing across many GPUs.

Data Parallelism

Each GPU has a full copy of the model, processes different batches:

# Pseudocode for data-parallel training
for batch in dataloader:
    # Each GPU processes its own batch
    losses = parallel_call(model_forward, batch)

    # Average gradients across all GPUs
    sync_gradients(all_gpus=True)

    # Each GPU updates its local copy identically
    optimizer.step()

Problem: Each GPU needs the full model. 70B model = 140GB (FP16) β€” doesn't fit on one GPU.

Model Parallelism

Fully Sharded Data Parallelism (FSDP)

The modern standard: model parameters are sharded across GPUs, and parameters are gathered only when needed (during forward/backward).

Advantage: Memory scales with 1/num_gpus
Trade-off: Communication overhead

The Infrastructure Stack

Model (PyTorch)
    ↓
FSDP / Tensor Parallelism (distributed training)
    ↓
Deepspeed / Megatron-LM (optimization library)
    ↓
NCCL (GPU communication)
    ↓
NVIDIA GPUs (A100 / H100 / B200)

Part VI: Frontiers & Theory

Chapter 36: Mechanistic Interpretability

The Goal

Understand what individual components of a neural network compute, and how they combine to produce behavior.

Superposition (Elhage et al., 2022)

The puzzle: Neural networks have fewer neurons than concepts they represent. How?

Answer: Superposition β€” each neuron represents multiple concepts, using the "reuse" property of high-dimensional spaces.

# Toy superposition: 10 features in 5 neurons
# Each neuron β‰ˆ a combination of several features
# No neuron = a single "cat" or "dog" concept
# Instead: every neuron is a mixture

# This makes interpretability hard:
# You cannot understand a neuron in isolation.
# You need to find directions in activation space, not individual neurons.

Sparse Autoencoders (SAEs)

SAEs decompose activations into interpretable features:

class SparseAutoencoder(nn.Module):
    def __init__(self, activation_dim, feature_dim):
        super().__init__()
        self.encoder = nn.Linear(activation_dim, feature_dim, bias=False)
        self.decoder = nn.Linear(feature_dim, activation_dim, bias=False)

        # Constrain decoder directions to unit norm
        self.decoder.weight.data = F.normalize(self.decoder.weight.data, dim=0)

    def forward(self, activations):
        # Encode with ReLU (sparsity!)
        features = F.relu(self.encoder(activations))

        # Decode back
        reconstruction = self.decoder(features)

        return reconstruction, features

    def loss(self, x, reconstruction, features, lambda_l1=1e-3):
        recon_loss = F.mse_loss(reconstruction, x)
        sparsity_loss = lambda_l1 * features.sum(-1).mean()

        return recon_loss + sparsity_loss

Anthropic (2024) β€” "Scaling Monosemanticity": Applied SAEs to Claude 3 Sonnet. Found millions of features:
- People: "Al Capone", "Leonardo DiCaprio"
- Concepts: "super bowl", "capital punishment"
- Abstract: "the concept of 'the' as a definite article"
- Safety-relevant: features for deception, sycophancy, refusal

Activation Patching

The gold standard for causal interpretation:

def activation_patching(model, clean_input, corrupted_input, layer_idx, neuron_idx):
    """
    Does changing one neuron's activation causally change the output?

    Setup:
    1. Run clean_input β†’ get base output
    2. Run corrupted_input β†’ get corrupted output
    3. Run corrupted_input, but patch in clean_input's activations
       at (layer_idx, neuron_idx)
    4. If output changes to match clean β†’ that neuron matters
    """
    clean_cache = run_with_cache(model, clean_input)
    corrupted_cache = run_with_cache(model, corrupted_input)

    # Run corrupted, but with clean activations at specific position
    def patching_hook(activations):
        activations[:, neuron_idx] = clean_cache[layer_idx][:, neuron_idx]
        return activations

    patched_output = run_with_hooks(model, corrupted_input, [(layer_idx, patching_hook)])

    patching_effect = (corrupted_output - patched_output) / (corrupted_output - clean_output)
    return patching_effect

Chapter 37: The Neural Tangent Kernel

The Infinite-Width Limit

Jacot et al. (2018): As a neural network's width β†’ ∞, its training dynamics become equivalent to kernel regression under the Neural Tangent Kernel (NTK):

$$f_t(x) \approx \sum_i K(x, x_i) \alpha_i(t)$$

Where $K$ is a fixed kernel determined by the architecture.

During training with gradient descent, the model function evolves as:

$$\frac{df_t}{dt} \approx -\Theta_t \cdot \nabla_f L_t$$

Where $\Theta_t$ is the NTK. For infinite width, $\Theta_t$ stays constant during training.

NTK Regime vs Feature Learning Regime

Property NTK Regime (Lazy) Rich/Feature Learning
Width Very large Moderate
Features during training Fixed Change and adapt
Effective model Kernel machine Full deep learning
When it works Theory Practice
Hyperparameter transfer Poor (needs retuning) Good (ΞΌP enables transfer)

Key insight: The models that work in practice are not in the NTK regime. They learn features. But understanding NTK gives theoretical grounding for why optimization works at all.

ΞΌP (Maximal Update Parameterization)

Yang & Hu (2021): How to ensure feature learning at any width. The key: scale the learning rate and initialization correctly with width.

Without ΞΌP: as width β†’ ∞, training degenerates into the NTK regime.
With ΞΌP: training dynamics remain in the feature-learning regime at any width.


Chapter 38: Causal AI

Pearl's Ladder of Causation

The fundamental problem: Most ML today operates at Level 1 (correlation). But decision-making requires Level 2+ (causation).

Causal Representation Learning

SchΓΆlkopf et al. (2021): We need representations that separate causal factors from confounders.

Example: An image classifier learns that "sheep in fields" = "sheep" because most training images have sheep in fields. A causal model would understand that the field is a context (not a cause of "sheepness").

The Independent Causal Mechanisms (ICM) principle:

$$P(x_1, ..., x_n) = \prod_{i=1}^n P(x_i | \text{parents}(x_i))$$

The causal generative process factorizes. Each mechanism $P(x_i | \text{parents}(x_i))$ is independent β€” changing one doesn't affect the others.

This is why ML fails at distribution shift: When the test distribution differs from training, the correlations change. Causal models, by modeling mechanisms, are robust to this.


Chapter 39: Geometric Deep Learning

The 5G Blueprint

Bronstein et al. (2021): All successful deep learning architectures exploit the symmetries of their data domain.

Equivariance β€” The Core Principle

Equivariance: $f(T_g x) = T'_g f(x)$

Translation equivariance (CNNs): If you shift the image left, the cat detector's features shift left by the same amount.

Rotation equivariance (Spherical CNNs): If you rotate the 3D object, the features rotate accordingly.

Why this matters: Equivariance reduces the burden on the model. Instead of learning the same pattern in every position/rotation, the architecture guarantees it automatically. This means:
- More data-efficient
- Better generalization
- Fewer parameters

The Group Theory Framework

A group $G$ is a set of transformations with composition:
- Translation group: $\mathbb{R}^n$ with addition
- Rotation group: $SO(3)$ with matrix multiplication
- Permutation group: $S_n$ with composition

The stabilizer of a point: transformations that leave that point unchanged.

A feature map is a function over data that transforms under group actions. The convolution is defined using group integration:

$$(f * \psi)(g) = \int_G f(h) \psi(g^{-1}h) dh$$

Where $dh$ is the Haar measure on $G$. For translation groups, this is the standard convolution. For other groups, it generalizes.


Chapter 40: World Models & JEPA

LeCun's Vision

Yann LeCun's argument: Current approaches (predicting tokens or pixels) are fundamentally wrong. True intelligence requires:

  1. World models: Internal representations that can simulate possible futures
  2. Abstract representations: Learn in latent space, not pixel/text space
  3. Planning: Use the world model for reasoning

JEPA (Joint Embedding Predictive Architecture)

JEPA doesn't predict pixels. It predicts representations of future/occluded input.

The predictor operates in representation space β€” it captures what matters while ignoring irrelevant details (pixel-level noise).

Energy-Based Models for JEPA

Instead of a generative decoder (which wastes compute on pixels), JEPA uses an energy function:

$$E(x, y) = ||\text{Encoder}(y) - \text{Predictor}(\text{Encoder}(x))||^2$$

Chapter 41: Neuroscience & AI

Predictive Coding

Rao & Ballard (1999): The brain's cortex implements hierarchical predictive coding:

Connection to modern AI: This is remarkably similar to how neural networks learn β€” forward pass = prediction, backward pass = error correction.

The Credit Assignment Problem

The biological challenge: Backpropagation requires symmetric weights (the same connection both forward and backward must have the same strength). The brain doesn't have this.

Proposed solutions:
- Feedback alignment: Random feedback weights work surprisingly well (Lillicrap et al., 2016)
- Predictive coding networks: Can approximate backprop with local learning rules (Whittington & Bogacz, 2017)
- Weight mirror: Feedback weights slowly align with feedforward weights

The Free Energy Principle

Friston (2010): Any self-organizing system (including the brain) minimizes variational free energy:

$$F = - \ln P(o | \theta) + KL(q(x) || P(x | o))$$

Free energy = negative log evidence + complexity cost

Active inference: The agent minimizes free energy by:
1. Updating beliefs to match observations (perception)
2. Acting to make observations match beliefs (action)

This unifies perception, learning, and decision-making under one principle.


Chapter 42: AI Safety Theory

The Alignment Problem

Definition: How do we build AI systems that reliably do what humans mean β€” not just what they're literally told?

The Orthogonality Thesis

Bostrom (2014): Intelligence and final goals are orthogonal. A highly intelligent system can have any goal, including ones humans wouldn't want.

Mesa-Optimization

Hubinger et al. (2019): During training, the model may develop its own internal optimizer (a "mesa-optimizer") that pursues different goals than the training objective.

Specification Gaming

When optimizing for a metric, AIs find unexpected ways to maximize it:

"Maximize score" β†’ AI discovers bug to get infinite score
"Sort blocks of code" β†’ AI deletes blocks instead of sorting them
"Create no offensive content" β†’ AI says "I'm harmless" to anything

Key insight: The failure isn't malice. It's that specifications are incomplete.

Inner vs Outer Alignment

Both must be solved. We've made progress on outer alignment (RLHF, DPO). Inner alignment remains open.


Chapter 43: Open Problems & AGI Debates

The Data Wall

Problem: We may run out of high-quality training text by 2027-2028 (Villalobos et al., 2022).

Solutions:
1. Synthetic data: AI-generated data to supplement real data. Risk: model collapse (Shumailov et al., 2023) β€” models trained on model outputs degrade.
2. Data efficiency: Better architectures that learn from fewer examples.
3. Unused data modalities: Video, audio, 3D, sensor data (vastly more than text).
4. Active learning: The model chooses what data to get labeled.

The Reasoning Gap

Current LLMs are pattern matchers, not reasoners. Despite chain-of-thought, they:
- Make arithmetic errors (7+8 = ?)
- Fail at composition (if A>B and B>C, then A>C?)
- Are brittle β€” rephrasing the question changes answers

Test-time compute scaling (Snell et al., 2024): Like o1/o3, models that "think longer" get better. But this is brute force, not genuine reasoning.

The AGI Paths Debate

The Guiding Question

"What is intelligence β€” mathematically, computationally, and physically?"

This question connects everything:
- Mathematically: PAC learning, VC dimension, NTK, information theory
- Computationally: Transformers, GNNs, SSMs, scaling laws
- Physically/Embodied: World models, robotics, free energy principle
- Biologically: Predictive coding, credit assignment, Hebbian learning

The answer likely involves all three perspectives, integrated.


Appendices

Appendix A: Complete Math Reference

Linear Algebra (Quick Reference)

Concept                     Notation                    NumPy
─────────────────────────────────────────────────────────────
Vector                      x, x ∈ ℝⁿ                  np.array([1, 2, 3])
Matrix                      A, A ∈ ℝᡐˣⁿ               np.array([[1, 2], [3, 4]])
Tensor                      X ∈ β„α΅ƒΛ£α΅‡Λ£αΆœ                torch.randn(a, b, c)
Transpose                   Aα΅€                         A.T
Dot product                 a Β· b = Ξ£ aα΅’bα΅’            np.dot(a, b)
Matrix multiply             A @ B                      A @ B
Outer product               a βŠ— b = abα΅€               np.outer(a, b)
Hadamard (element)          A βŠ™ B                       A * B
Frobenius norm              ||A||_F = √ΣΣ Aᡒⱼ²        np.linalg.norm(A)
L2 norm                     ||x||β‚‚ = √Σ xα΅’Β²            np.linalg.norm(x)
Singular value decomp.      A = UΞ£Vα΅€                    np.linalg.svd(A)
Eigenvalue decomposition    A = QΞ›Q⁻¹                   np.linalg.eig(A)

Calculus (Quick Reference)

Derivative rules:
  d/dx (xⁿ) = nxⁿ⁻¹
  d/dx (eΛ£) = eΛ£
  d/dx (ln x) = 1/x
  d/dx (Οƒ(x)) = Οƒ(x)(1-Οƒ(x))    [sigmoid]
  d/dx (ReLU(x)) = 1 if x>0 else 0
  d/dx (tanh(x)) = 1-tanhΒ²(x)

Chain rule: df/dx = df/dg Β· dg/dx
Partial derivative: βˆ‚f/βˆ‚xα΅’ (differentiate w.r.t. one variable)
Gradient: βˆ‡f = [βˆ‚f/βˆ‚x₁, βˆ‚f/βˆ‚xβ‚‚, ..., βˆ‚f/βˆ‚xβ‚™]

Vector calculus:
  βˆ‚/βˆ‚x (aΒ·x) = a
  βˆ‚/βˆ‚x (xα΅€Ax) = (A + Aα΅€)x
  βˆ‚/βˆ‚X (aα΅€Xb) = abα΅€

Distributions (heuristic):
  βˆ‡Β²f: Hessian matrix, condition number = Ξ»_max/Ξ»_min

Probability (Quick Reference)

Gaussian PDF:       p(x) = 1/√(2πσ²) exp(-(x-ΞΌ)Β²/(2σ²))
Multivariate:       p(x) = 1/((2Ο€)^(d/2)|Ξ£|ΒΉ/Β²) exp(-Β½(x-ΞΌ)ᡀΣ⁻¹(x-ΞΌ))
Bayes rule:         P(ΞΈ|D) = P(D|ΞΈ)P(ΞΈ)/P(D)
Law of total prob:  P(A) = Ξ£ P(A|Bα΅’)P(Bα΅’)

Distances:
  KL(P||Q) = Ξ£ P(x) log(P(x)/Q(x))
  JS(P||Q) = Β½KL(P||M) + Β½KL(Q||M), M = (P+Q)/2
  Wasserstein: W_p(P,Q) = (inf_{γ∈Π(P,Q)} E[||x-y||_p])^(1/p)

Information:
  Entropy:      H(X) = -Ξ£ p(x) log p(x)
  Cross-ent:    H(P,Q) = -Ξ£ p(x) log q(x)
  Mutual info:  I(X;Y) = KL(P(X,Y)||P(X)P(Y))

Optimization (Quick Reference)

Gradient descent:   ΞΈ_{t+1} = ΞΈ_t - Ξ· βˆ‡L(ΞΈ_t)
SGD:                ΞΈ_{t+1} = ΞΈ_t - Ξ· βˆ‡L_batch(ΞΈ_t)
Momentum:           v_{t+1} = Ξ³v_t + Ξ·βˆ‡L(ΞΈ_t)
                    ΞΈ_{t+1} = ΞΈ_t - v_{t+1}
Adam:               m_t = β₁m_{t-1} + (1-β₁)g_t
                    v_t = Ξ²β‚‚v_{t-1} + (1-Ξ²β‚‚)g_tΒ²
                    ΞΈ_{t+1} = ΞΈ_t - Ξ· mΜ‚_t/(√vΜ‚_t + Ξ΅)

Common learning rates:
  Adam/AdamW: 1e-4 to 1e-3
  SGD: 1e-3 to 1e-1
  Fine-tuning: 1e-5 to 1e-4

Appendix B: Algorithm Catalog

50+ ML Algorithms with Pseudocode

1. Linear Regression (click to expand)

Input: X (n_samples Γ— n_features), y (n_samples,)
Output: w (weights), b (bias)

Closed form:
  X_aug = [1, X]                    # add bias column
  w = (X_augα΅€ Γ— X_aug)⁻¹ Γ— X_augα΅€ Γ— y

Gradient descent:
  w = zeros(n_features)
  b = 0
  for epoch in 1..epochs:
    y_pred = X Γ— w + b
    error = y_pred - y
    dw = (2/n) Γ— Xα΅€ Γ— error
    db = (2/n) Γ— sum(error)
    w = w - lr Γ— dw
    b = b - lr Γ— db

2. Logistic Regression

Input: X, y ∈ {0, 1}
Output: w, b

w = zeros(n_features)
b = 0
for epoch in 1..epochs:
  z = X Γ— w + b
  y_pred = 1 / (1 + exp(-z))        # sigmoid
  error = y_pred - y
  dw = (1/n) Γ— Xα΅€ Γ— error
  db = (1/n) Γ— sum(error)
  w = w - lr Γ— dw
  b = b - lr Γ— db

3. Softmax Regression

Input: X, y ∈ {0, ..., K-1}
Output: W (K Γ— D), b (K,)

W = zeros(K, D)
b = zeros(K)
for epoch in 1..epochs:
  logits = X Γ— Wα΅€ + b                # (N, K)
  exp_logits = exp(logits - max(logits, axis=1))
  probs = exp_logits / sum(exp_logits, axis=1)
  loss = -mean(log(probs[y_one_hot]))
  grad = (probs - y_one_hot) / N
  W = W - lr Γ— gradα΅€ Γ— X
  b = b - lr Γ— mean(grad, axis=0)

4. SVM (Primal)

Input: X, y ∈ {-1, +1}
Output: w, b

w = zeros(D)
b = 0
C = 1.0                              # regularization
for epoch in 1..epochs:
  for i in 1..N:
    margin = y[i] Γ— (wΒ·X[i] + b)
    if margin β‰₯ 1:                    # correct, non-support
      dw = w / N                     # only regularization
      db = 0
    else:                            # inside margin or misclassified
      dw = w / N - C Γ— y[i] Γ— X[i]
      db = -C Γ— y[i]
    w = w - lr Γ— dw
    b = b - lr Γ— db

5. K-Means Clustering

Input: X (N Γ— D), K
Output: labels (N,), centroids (K Γ— D)

centroids = random_sample(X, K)      # or k-means++
for iteration in 1..max_iters:
  labels = argmin(||X - centroids||Β², axis=1)
  for k in 1..K:
    centroids[k] = mean(X[labels==k], axis=0)
  if converged: break

6. K-Nearest Neighbors

Input: X_train, y_train, X_test, K
Output: y_pred

for each x_test in X_test:
  distances = ||X_train - x_test||Β²
  nearest = argsort(distances)[:K]
  y_pred[x_test] = mode(y_train[nearest])

7. Decision Tree (ID3/CART)

function build_tree(X, y, depth):
  if depth β‰₯ max_depth or all(y same):
    return leaf(mode(y))

  best_feature, best_threshold = None, None
  best_gain = -inf

  for each feature f:
    thresholds = unique_sorted(X[:, f])
    for each threshold t:
      left = y[X[:, f] ≀ t]
      right = y[X[:, f] > t]
      gain = H(y) - (|left|/|y|)H(left) - (|right|/|y|)H(right)
      if gain > best_gain:
        best_gain, best_feature, best_threshold = gain, f, t

  left_tree = build_tree(X[X[:, f] ≀ t], y[mask_left], depth+1)
  right_tree = build_tree(X[X[:, f] > t], y[~mask_left], depth+1)
  return node(best_feature, best_threshold, left_tree, right_tree)

8. Random Forest

Input: X, y, n_trees, max_features, max_depth
Output: forest (list of trees)

forest = []
for i in 1..n_trees:
  bootstrap_idx = sample(N, N, replace=True)
  X_boot = X[bootstrap_idx]
  y_boot = y[bootstrap_idx]

  feature_subset = sample(D, max_features, replace=False)
  X_subset = X_boot[:, feature_subset]

  tree = build_tree(X_subset, y_boot, max_depth)
  forest.append((tree, feature_subset))

def predict(X):
  votes = zeros(X.shape[0], n_classes)
  for tree, features in forest:
    votes += tree.predict(X[:, features])
  return argmax(votes)

9. Gradient Boosting

Input: X, y, n_estimators, lr, max_depth
Output: trees

F = mean(y) Γ— ones(N)
trees = []
for m in 1..n_estimators:
  residuals = -βˆ‡L(y, F)               # e.g., y - F for MSE
  tree = build_tree(X, residuals, max_depth)
  F = F + lr Γ— tree.predict(X)
  trees.append(tree)

def predict(X):
  return mean(y) + lr Γ— sum(tree.predict(X) for tree in trees)

10. PCA

Input: X (N Γ— D), n_components
Output: X_reduced (N Γ— k), components (D Γ— k)

X_centered = X - mean(X, axis=0)
cov = X_centeredα΅€ Γ— X_centered / (N-1)
eigenvalues, eigenvectors = eig(cov)
idx = argsort(eigenvalues, descending)
components = eigenvectors[:, idx[:k]]
X_reduced = X_centered Γ— components

11. t-SNE

Input: X (N Γ— D), perplexity=30, lr=200
Output: Y (N Γ— 2)

# Compute pairwise similarities in input space
P = compute_conditional_probs(X, perplexity)

# Initialize low-dimensional points (random)
Y = randn(N, 2)

for step in 1..steps:
  # Compute pairwise similarities in embedding space
  Q = 1 / (1 + ||Y - Yβ±Ό||Β²)           # Cauchy kernel
  Q = Q / sum(Q)

  # Gradient
  grad = 4 Γ— Ξ£ (P - Q)(Y - Yβ±Ό)(1 + ||Y - Yβ±Ό||Β²)⁻¹

  Y = Y - lr Γ— grad + momentum Γ— velocity

12. Feed-Forward Neural Network

Input: X (N Γ— D_in), y, hidden_sizes, activation
Output: trained parameters W_l, b_l for each layer

Initialize all W_l (He/Glorot init), b_l = 0
for epoch in 1..epochs:
  # Forward pass
  h = X
  for l in 1..L:
    h = Οƒ(h Γ— W_l + b_l)

  # Backward pass
  Ξ΄ = (h - y) / N                    # output gradient
  for l in reversed(1..L):
    dW_l = a_{l-1}α΅€ Γ— Ξ΄
    db_l = sum(Ξ΄, axis=0)
    Ξ΄ = Ξ΄ Γ— W_lα΅€ Γ— Οƒ'(z_{l-1})
    W_l = W_l - lr Γ— dW_l
    b_l = b_l - lr Γ— db_l

13. CNN Forward Pass

Input: image (H Γ— W Γ— C_in), kernel (K Γ— K Γ— C_in Γ— C_out)
Output: feature_map (H_out Γ— W_out Γ— C_out)

H_out = (H + 2P - K) / S + 1
W_out = (W + 2P - K) / S + 1

if P > 0: image = zero_pad(image, P)
for h in 0..H_out-1:
  for w in 0..W_out-1:
    region = image[h*S:h*S+K, w*S:w*S+K, :]
    for c in 0..C_out-1:
      feature_map[h, w, c] = sum(region Γ— kernel[:, :, :, c])

# After conv: activation (ReLU), then pooling
# MaxPool: feature_map_out[i,j] = max(region_i,j)

14. LSTM

Input: x_t, h_{t-1}, c_{t-1}, parameters W, b
Output: h_t, c_t

combined = [x_t, h_{t-1}]           # concatenate
gates = combined Γ— W + b             # all 4 gates at once
f = Οƒ(gates[:h_dim])                 # forget
i = Οƒ(gates[h_dim:2h_dim])           # input
c̃ = tanh(gates[2h_dim:3h_dim])       # candidate
o = Οƒ(gates[3h_dim:])                # output

c_t = f βŠ™ c_{t-1} + i βŠ™ cΜƒ           # cell state update
h_t = o βŠ™ tanh(c_t)                  # hidden state

15. Scaled Dot-Product Attention

Input: Q (B Γ— L_q Γ— d), K (B Γ— L_k Γ— d), V (B Γ— L_v Γ— d)
Output: output (B Γ— L_q Γ— d_v), weights (B Γ— L_q Γ— L_k)

scores = Q Γ— Kα΅€ / √d                 # (B, L_q, L_k)
if mask: scores = scores + mask
weights = softmax(scores, dim=-1)
output = weights Γ— V

16. Multi-Head Attention

Input: x (B Γ— L Γ— D), n_heads, d_head = D // n_heads

Q = x Γ— W_Q β†’ reshape(B, L, n_heads, d_head) β†’ transpose(1, 2)
K = x Γ— W_K β†’ reshape(B, L, n_heads, d_head) β†’ transpose(1, 2)
V = x Γ— W_V β†’ reshape(B, L, n_heads, d_head) β†’ transpose(1, 2)

# Each head does attention independently
scores = Q Γ— Kα΅€ / √d_head           # (B, n_heads, L, L)
weights = softmax(scores, dim=-1)
head_output = weights Γ— V            # (B, n_heads, L, d_head)

# Concatenate heads
concat = head_output.transpose(1,2).reshape(B, L, D)
output = concat Γ— W_O

17. Transformer Block

Input: x (B Γ— L Γ— D)
Output: x (B Γ— L Γ— D)

# Pre-norm architecture (modern)
x = x + MultiHeadAttention(LayerNorm(x))
x = x + FFN(LayerNorm(x))

where:
  FFN(x) = Wβ‚‚ Γ— GELU(W₁ Γ— x + b₁) + bβ‚‚
  or SwiGLU: (W₁ Γ— x) βŠ™ sigmoid(Wβ‚‚ Γ— x) Γ— W₃

18. GPT Generation (Autoregressive)

Input: prompt_tokens, max_new_tokens, temperature
Output: generated_tokens

input_ids = prompt_tokens
kv_cache = {}                        # for efficiency

for step in 1..max_new_tokens:
  if kv_cache is empty:
    logits, kv_cache = model(input_ids, use_cache=True)
  else:
    logits, kv_cache = model(input_ids[-1:], cache=kv_cache, use_cache=True)

  next_logits = logits[-1] / temperature
  probs = softmax(next_logits)
  next_token = sample(probs)          # greedy, top-k, or top-p

  if next_token == EOS: break
  input_ids = concat(input_ids, next_token)

19. VAE (Variational Autoencoder)

Input: x, encoder NN, decoder NN
Output: x_recon, mu, logvar, z

# Encoder
mu, logvar = encoder(x)
std = exp(0.5 Γ— logvar)

# Reparameterization trick
Ξ΅ = randn_like(std)
z = mu + Ξ΅ Γ— std

# Decoder
x_recon = decoder(z)

# Loss
recon_loss = BCE(x_recon, x)         # or MSE
kl_loss = -0.5 Γ— Ξ£(1 + logvar - muΒ² - exp(logvar))
loss = recon_loss + kl_loss

20. DDPM (Denoising Diffusion)

# Training:
xβ‚€ ∼ data, Ο΅ ∼ N(0,I),  t ∼ Uniform(1,T)
x_t = βˆšΞ±Μ„_t Γ— xβ‚€ + √(1-Ξ±Μ„_t) Γ— Ο΅
Ο΅Μ‚ = Ξ΅_ΞΈ(x_t, t)
loss = MSE(Ο΅Μ‚, Ο΅)

# Sampling:
x_T ∼ N(0, I)
for t = T..1:
  Ο΅Μ‚ = Ξ΅_ΞΈ(x_t, t)
  x_{t-1} = 1/√α_t Γ— (x_t - Ξ²_t/√(1-Ξ±Μ„_t) Γ— Ο΅Μ‚)
  if t > 1: x_{t-1} += √β_t Γ— z, z ∼ N(0,I)

21. Flow Matching

# Training:
xβ‚€ ∼ data, x₁ ∼ N(0,I), t ∼ Uniform(0,1)
x_t = (1-t) Γ— xβ‚€ + t Γ— x₁
v_target = x₁ - xβ‚€
v_pred = v_ΞΈ(x_t, t)
loss = MSE(v_pred, v_target)

# Sampling:
x ∼ N(0, I)
dt = 1/N_steps
for step in 1..N_steps:
  t = (step-1) / N_steps
  v = v_ΞΈ(x, t)
  x = x + v Γ— dt                      # Euler step

22. PPO (Proximal Policy Optimization)

Input: policy Ο€_ΞΈ, value function V_Ο†, rollouts
Output: updated Ο€_ΞΈ

# Collect trajectories using Ο€_ΞΈ
for step in 1..buffer_size:
  s = env.state
  a ∼ Ο€_ΞΈ(a|s)
  r, s' = env.step(a)
  store (s, a, r, s')

# Compute advantages
for each sample:
  A = discounted_sum(r) - V_Ο†(s)     # advantage

# Update policy (several epochs)
for epoch in 1..n_epochs:
  ratio = Ο€_ΞΈ(a|s) / Ο€_old(a|s)
  clipped_ratio = clamp(ratio, 1-Ξ΅, 1+Ξ΅)

  policy_loss = -min(ratio Γ— A, clipped_ratio Γ— A)
  value_loss = MSE(V_Ο†(s), discounted_sum(r))

  ΞΈ = ΞΈ - lr Γ— βˆ‡(policy_loss - entropy_bonus)
  Ο† = Ο† - lr Γ— βˆ‡(value_loss)

23. DPO (Direct Preference Optimization)

Input: Ο€_ref (frozen), chosen responses y_w, rejected y_l
Output: updated Ο€_ΞΈ

for batch in dataloader:
  log_pi_w = log Ο€_ΞΈ(y_w|x)          # current policy on chosen
  log_pi_l = log Ο€_ΞΈ(y_l|x)          # current policy on rejected
  log_ref_w = log Ο€_ref(y_w|x)       # reference on chosen
  log_ref_l = log Ο€_ref(y_l|x)       # reference on rejected

  # Log ratio: log(Ο€_ΞΈ/Ο€_ref)
  ratio_diff = (log_pi_w - log_ref_w) - (log_pi_l - log_ref_l)

  loss = -log Οƒ(Ξ² Γ— ratio_diff)      # DPO loss
  ΞΈ = ΞΈ - lr Γ— βˆ‡loss

24. K-Means++ Initialization

Input: X (N Γ— D), K
Output: initial centroids (K Γ— D)

centroids[0] = X[random_index]
for k in 1..K-1:
  # Distance to nearest existing centroid
  D[i] = min_j ||X[i] - centroids[j]||Β²
  # Probability proportional to DΒ²
  P[i] = D[i] / sum(D)
  centroids[k] = X[sample(P)]

25. DBSCAN Clustering

Input: X (N Γ— D), epsilon, minPts
Output: labels (N,)

labels = [-1] Γ— N                    # -1 = noise/undefined
cluster_id = 0

for each point P not visited:
  visited[P] = True
  neighbors = points_within(epsilon, P)

  if len(neighbors) < minPts:
    labels[P] = -1                    # noise
  else:
    expand_cluster(P, neighbors, cluster_id)
    cluster_id += 1

function expand_cluster(P, neighbors, cluster_id):
  labels[P] = cluster_id
  queue = neighbors
  while queue is not empty:
    Q = queue.pop()
    if not visited[Q]:
      visited[Q] = True
      Q_neighbors = points_within(epsilon, Q)
      if len(Q_neighbors) β‰₯ minPts:
        queue.extend(Q_neighbors)
    if labels[Q] == -1:
      labels[Q] = cluster_id

26. Bagging (Bootstrap Aggregating)

Input: X, y, base_learner, n_models
Output: ensemble

models = []
for i in 1..n_models:
  indices = sample(N, N, replace=True)    # bootstrap
  X_boot, y_boot = X[indices], y[indices]
  model = base_learner.fit(X_boot, y_boot)
  models.append(model)

def predict(X):
  predictions = [model.predict(X) for model in models]
  return mean(predictions)               # for regression
  return mode(predictions)               # for classification

27. AdaBoost

Input: X, y ∈ {-1, +1}, base_learner, T
Output: weighted ensemble

w = ones(N) / N                        # initial weights
models = []
alphas = []

for t in 1..T:
  model = base_learner.fit(X, y, sample_weight=w)
  error = sum(w Γ— (model.predict(X) β‰  y)) / sum(w)

  if error β‰₯ 0.5: break

  alpha = 0.5 Γ— log((1-error) / error)
  w = w Γ— exp(-alpha Γ— y Γ— model.predict(X))
  w = w / sum(w)                       # normalize

  models.append(model)
  alphas.append(alpha)

def predict(X):
  return sign(Ξ£ Ξ±α΅’ Γ— modelα΅’.predict(X))

28-50. Additional Algorithms

Algorithms 28-50 include: ElasticNet, Ridge/Lasso regression, Polynomial regression, Gaussian Naive Bayes,
Quadratic Discriminant Analysis, SVD-based Recommendation, Matrix Factorization, Word2Vec (Skip-gram),
GloVe embeddings, NMF (Non-negative Matrix Factorization), UMAP,
GRU, Seq2Seq with Attention, Beam Search decoding, BLEU score calculation,
Gradient Clipping, Scheduled Sampling, Label Smoothing,
Warmup + Cosine LR Schedule, Focal Loss, and Contrastive Learning (SimCLR).


Appendix C: Complete Glossary

AGI (Artificial General Intelligence)
  - AI that can perform any intellectual task a human can.

Attention
  - Mechanism enabling models to weigh the importance of different input parts.

Autoregressive
  - Model that predicts the next token given all previous tokens.

Backpropagation
  - Algorithm for computing gradients through neural networks using the chain rule.

Bias (in bias-variance tradeoff)
  - Error from incorrect assumptions in the learning algorithm.

Bias (neuron)
  - A learnable offset parameter in a neural network layer.

Causal Masking
  - Preventing attention to future tokens in autoregressive models.

Chain-of-Thought
  - Prompting technique: "let's think step by step" for better reasoning.

Chinchilla
  - Hoffmann et al. 2022: optimal scaling requires scaling data with model size.

CLIP
  - Contrastive Language-Image Pretraining: joint vision-language embeddings.

Condition Number
  - Ratio of largest to smallest Hessian eigenvalue; measures optimization difficulty.

Cross-Entropy
  - Loss function for classification: -Ξ£ p(x) log q(x).

DDPM
  - Denoising Diffusion Probabilistic Model: generates data by reversing noise addition.

Double Descent
  - Phenomenon where test error decreases again in the overparameterized regime.

DPO (Direct Preference Optimization)
  - Alignment method that directly optimizes from preferences without a reward model.

Dropout
  - Regularization: randomly zeroing out a fraction of neurons during training.

ELBO (Evidence Lower BOund)
  - Tractable lower bound on log-likelihood, used in VAEs and diffusion models.

Emergent Abilities
  - Capabilities that appear in large models but not in smaller ones.

Entropy
  - Measure of uncertainty: H(X) = -Ξ£ p(x) log p(x).

Equivariance
  - Property: f(Tx) = T'f(x) β€” transforming input transforms output predictably.

Fine-Tuning
  - Taking a pretrained model and training it further on a specific task.

Flash Attention
  - Memory-efficient attention algorithm that avoids materializing the full NΓ—N matrix.

Flow Matching
  - Generative modeling by learning a deterministic ODE between noise and data.

Foundation Model
  - Large model trained on broad data, adaptable to many downstream tasks.

GELU (Gaussian Error Linear Unit)
  - Activation function used in GPT/BERT: x Β· Ξ¦(x) where Ξ¦ is Gaussian CDF.

Geometric Deep Learning
  - Unifying framework: different architectures exploit different symmetries (groups).

Gradient Descent
  - Optimization: iteratively move in direction opposite to gradient.

GQA (Grouped-Query Attention)
  - Attention with fewer key/value heads than query heads; reduces KV-cache size.

GNN (Graph Neural Network)
  - Neural network for graph-structured data using message passing.

In-Context Learning
  - LLM learning from examples in the prompt without parameter updates.

Induction Head
  - Attention head that copies information from earlier positions; key to ICL.

Information Bottleneck
  - Theory: learning compresses input while preserving output-relevant information.

JEPA (Joint Embedding Predictive Architecture)
  - LeCun: predict representations, not pixels; energy-based world model.

KL Divergence
  - Measure of distribution difference: Ξ£ p(x) log(p(x)/q(x)).

KV-Cache
  - Stored Key/Value tensors for efficient autoregressive generation.

Langevin Dynamics
  - MCMC sampling method used in energy-based models and diffusion.

Layer Normalization
  - Normalize activations across feature dimension (not batch dimension).

Logits
  - Raw (unnormalized) output scores before softmax.

MAML (Model-Agnostic Meta-Learning)
  - Learn initialization that adapts to new tasks in few gradient steps.

Mamba
  - Selective state space model with O(n) complexity, rivals transformers.

Mesa-Optimization
  - Model internally developing its own optimizer with different goals.

MLP (Multilayer Perceptron)
  - Feedforward neural network with multiple hidden layers.

MoE (Mixture of Experts)
  - Architecture where different "experts" activate for different inputs.

ΞΌP (Maximal Update Parameterization)
  - Parametrization ensuring feature learning at any width.

NTK (Neural Tangent Kernel)
  - Kernel describing gradient descent dynamics of infinite-width networks.

PAC Learning
  - Probably Approximately Correct: framework for learning theory.

PCA (Principal Component Analysis)
  - Dimensionality reduction via eigendecomposition of covariance matrix.

Policy Gradient
  - RL algorithm: optimize policy directly via gradient of expected reward.

PPO (Proximal Policy Optimization)
  - Stable RL by clipping policy updates to a trust region.

Rademacher Complexity
  - Data-dependent measure of model capacity based on fitting random noise.

ReLU (Rectified Linear Unit)
  - Activation: f(x) = max(0, x). Default for hidden layers.

Residual Connection
  - Skip connection: add input to layer output. Enables very deep networks.

RLHF
  - Reinforcement Learning from Human Feedback: align models via human preferences.

RoPE (Rotary Position Embedding)
  - Position encoding via rotation of query/key vectors; enables length extrapolation.

Scaling Laws
  - Power-law relationships between model/data/compute and loss.

Score Function
  - Gradient of log-probability: βˆ‡_x log p(x). Estimated by diffusion models.

Self-Attention
  - Attention where queries, keys, and values all derive from the same sequence.

SGD
  - Stochastic Gradient Descent: gradient on random mini-batch.

Sigmoid
  - Activation: Οƒ(x) = 1/(1+e^{-x}). Squashes to (0, 1).

Softmax
  - Converts logits to probabilities: softmax(x)_i = e^{x_i} / Ξ£ e^{x_j}.

Sparse Autoencoder
  - Overcomplete autoencoder with sparsity; finds interpretable features.

Specification Gaming
  - Model exploits the objective without achieving intended goal.

SSM (State Space Model)
  - Sequence model via discretized linear ODE; basis of Mamba.

Superposition
  - Neural networks represent more features than dimensions; feature entanglement.

SVD (Singular Value Decomposition)
  - Matrix factorization: A = UΞ£V^T. Basis of PCA and low-rank methods.

Temperature
  - Sampling parameter: controls probability distribution sharpness.

Token
  - Atomic unit of text for LLMs (word, subword, or byte).

Transformer
  - Architecture based on attention, replacing RNNs for sequence tasks.

Variance (bias-variance tradeoff)
  - Sensitivity of predictions to training data variation.

VC Dimension
  - Maximum number of points a hypothesis class can shatter; capacity measure.

Weight Decay
  - L2 regularization on neural network weights; equivalent to Ξ»||w||Β².

WL Test (Weisfeiler-Lehman)
  - Graph isomorphism algorithm; bounds GNN expressivity.

World Model
  - Internal model of the world enabling simulation, planning, and reasoning.

Appendix D: Hyperparameter Bible

Architecture Hyperparameters

Parameter               Default         Range           Applies To
────────────────────────────────────────────────────────────────────
d_model (hidden dim)    512/768/1024    128-16384       Transformers
n_layers                6/12            1-96            Transformers, MLPs
n_heads                 8/12            1-128           Transformers
d_ff (FFN inner)        2048/3072       d_model to 4Γ—   Transformers
head_dim (d_model/n_h)  64              32-128          Attention heads
n_experts (MoE)         8               2-256           Mixture of Experts
top_k (MoE)             2               1-8             MoE routing
vocab_size              32000/50257/128000  8K-256K     Tokenizers
max_seq_len             2048/4096       512-1M+         All sequence models

CNN-specific:
  kernel_size           3               1-11            Conv layers
  n_filters (initial)   64              8-256           Conv layers
  filter_multiplier     2Γ— per block    1-4Γ—            ConvNets
  pool_size             2               2-4             Pooling
  n_blocks (ResNet)     4               2-8             ResNet-like

Neural Network general:
  hidden_dim (MLP)      128/256         32-4096         MLPs, all NNs
  n_hidden_layers       2/3             1-100+          MLPs
  embedding_dim         128/256/768     16-8192         Embedding layers

Training Hyperparameters

Parameter               Default         Range           Notes
────────────────────────────────────────────────────────────────────
learning_rate           1e-4 to 1e-3    1e-6 to 1e-1    Adam 3e-4, SGD 1e-2
batch_size              32/64/128       8-8192          2^n typical
weight_decay            0.01 to 0.1     0-1.0           AdamW default: 0.01
dropout                 0.1             0-0.9           Higher for overfitting
attention_dropout       0.0             0-0.5           Less common now
warmup_steps            1000/2000       0-10000         Adam needs warmup
lr_schedule             cosine           constant, cosine, invsqrt, linear
gradient_clip_norm      1.0             0.1-10          Clip to prevent explosions
label_smoothing         0.0             0-0.2           Helps calibration
beta1 (Adam)            0.9             0.8-0.95        Momentum
beta2 (Adam)            0.999           0.95-0.999      Second moment decay
epsilon (Adam)          1e-8            1e-10 to 1e-6   Numerical stability

Regularization Hyperparameters

Method                  Default         Effect When Too High/Low
────────────────────────────────────────────────────────────────────
Weight decay (Ξ»)        0.01            High = underfitting, Low = overfitting
Dropout rate            0.1             High = underfitting, Low = overfitting
Label smoothing Ξ΅       0.1             High = underconfident, Low = overconfident
Gradient clipping c     1.0             High = gradient explosion, Low = truncated
Early stopping patience 5-10 epochs     Short = underfitting, Long = overfitting
Data augmentation       moderate        Aggressive = unatural, None = overfitting
Stochastic depth        0.2             High = can't learn, Low = no regularization

LLM-Specific Hyperparameters (Sampling)

Parameter               Default         Effect
────────────────────────────────────────────────────────────────────
temperature             0.7             Low = deterministic, High = random
top_k                   50              Low = conservative, High = diverse
top_p (nucleus)         0.9             Lower = more focused
repetition_penalty      1.0             1.0 = none, 1.1 = slight penalty
frequency_penalty       0.0             0.0 = none, 0.5 = reduce common tokens
presence_penalty        0.0             0.0 = none, 0.5 = encourage new topics
max_new_tokens          1024             Limit output length
stop_sequences          ["\n\n"]        Stop generation at these strings

Debugging: When Training Fails

Problem                  Likely Cause           Fix
────────────────────────────────────────────────────────────────────
Loss β†’ NaN               LR too high            Reduce lr 10Γ—, check input data
Loss stuck high          LR too low             Increase lr, check model init
Train loss low, val high Overfitting            Add regularization, more data
Both losses high         Underfitting           Bigger model, train longer
No gradient flow         Dead ReLU              Try LeakyReLU or GELU, init properly
Oscillating loss         LR too high / small batch Try cosine schedule, increase batch
Slow convergence         Wrong architecture     Check shapes, simplify first
Out of memory (OOM)      Batch too big          Reduce batch size, use gradient accumulation
Model won't start        Shape mismatch         Print every layer's input/output shapes

Appendix E: Breakthrough Timeline

Key Milestones in AI/ML

Year    Breakthrough                        Significance
────────────────────────────────────────────────────────────────────
1950    Turing Test                         Criteria for machine intelligence
1957    Perceptron (Rosenblatt)             First trainable neural network
1969    Minsky & Papert: Perceptrons         Showed limitations, caused first AI winter
1974    Backpropagation (Werbos)            The core learning algorithm
1989    Universal Approximation Theorem     MLPs can learn any function
1989    LeNet (LeCun)                       First CNN for digit recognition
1990    RNNs (Elman, Jordan)                Recurrent networks for sequences
1995    SVM (Vapnik)                        Powerful kernel-based classifier
1997    LSTM (Hochreiter & Schmidhuber)      Long-range sequence modeling
1997    GANs proposed (concept)             Generative adversarial framework
1998    LeNet-5 + MNIST                     Standardized benchmark for vision
2000    Kernel Methods (SchΓΆlkopf, Smola)   SVM/RKHS theory
2006    Deep Belief Networks (Hinton)       Start of deep learning revival
2006    Sparse Coding (Olshausen, Field)    Unsupervised feature learning
2009    ImageNet (Fei-Fei Li)              Large-scale vision dataset
2011    Xavier Init (Glorot & Bengio)       Solved vanishing gradient at init
2012    AlexNet (Krizhevsky)               Deep CNN destroys ImageNet benchmark
2012    Dropout (Hinton)                    Effective regularization
2013    Word2Vec (Mikolov)                  Dense word embeddings
2013    DQN (DeepMind)                      Deep RL plays Atari
2014    GANs (Goodfellow)                   Practical generative adversarial training
2014    Adam Optimizer (Kingma & Ba)        Adaptive learning rates
2014    Seq2Seq + Attention (Bahdanau)      Neural machine translation
2014    Batch Normalization (Ioffe)         Stable deep training
2015    ResNet (He et al.)                  152-layer networks via skip connections
2015    Attention is All You Need? (concept) Papers started exploring attention
2016    AlphaGo (DeepMind)                  Beats world champion Go player
2017    Transformer (Vaswani et al.)        Attention IS all you need
2017    PPO (Schulman et al.)               Stable policy optimization
2017    MoE (Shazeer et al.)                Sparse mixture of experts
2017    Wasserstein GAN (Arjovsky)          Stable GAN training
2018    BERT (Devlin et al.)                Bidirectional pretraining, SOTA NLP
2018    GPT (Radford et al.)                Generative pretraining, decoder-only
2018    Neural Tangent Kernel (Jacot)       Theory of infinite-width networks
2019    GPT-2 (OpenAI)                      1.5B params, controversial release
2019    XLNet, RoBERTa, ALBERT              BERT improvements
2019    GNN expressivity (Xu et al.)        WL test bounds GNN power
2019    Model-Agnostic Meta-Learning (Finn) Learning to learn
2019    BigGAN, StyleGAN                    High-quality image generation
2020    GPT-3 (OpenAI)                      175B params, emergent abilities
2020    Denoising Diffusion (Ho et al.)     DDPM: diffusion for image generation
2020    Scaling Laws (Kaplan et al.)        Power-law scaling of loss
2020    CLIP (OpenAI)                       Zero-shot vision-language learning
2020    Score SDE (Song et al.)             Unified diffusion framework
2021    DALL-E (OpenAI)                     Text-to-image generation
2021    MuP (Yang & Hu)                     Feature learning at any width
2021    Geometric Deep Learning (Bronstein) Unified symmetry framework
2022    Chinchilla (Hoffmann et al.)        Compute-optimal scaling reanalysis
2022    Chain-of-Thought (Wei et al.)       Reasoning via step-by-step prompts
2022    Stable Diffusion (Rombach et al.)   Open-source text-to-image
2022    ChatGPT (OpenAI)                    RLHF-chat interface, global adoption
2022    Superposition (Elhage et al.)       Feature polysemanticity theory
2023    GPT-4 (OpenAI)                      Multimodal, ~1.7T params (MoE est.)
2023    LLaMA (Meta)                        Open-weight LLMs, strong small models
2023    RLHF (InstructGPT paper)            Alignment via human preferences
2023    DPO (Rafailov et al.)               Preference optimization without RL
2023    Flow Matching (Lipman et al.)       ODE-based generation
2023    Mamba (Gu & Dao)                    State space model rivals transformers
2023    Flash Attention (Dao et al.)        Efficient attention algorithm
2023    RT-2 (Brohan et al./DeepMind)       Vision-language-action for robotics
2023    LLaVA (Liu et al.)                  Open multimodal LLM
2024    Sora (OpenAI)                       Video generation at scale
2024    Claude 3 (Anthropic)                Sparse autoencoder interpretability
2024    Llama 3 (Meta)                      70B, 405B open models
2024    DeepSeek-V2 (DeepSeek)              Efficient MoE, 236B params
2024    o1-preview (OpenAI)                 Chain-of-thought reasoning model
2024    Gemma 2 (Google)                    Efficient open models
2025    o3 / DeepSeek-R1                    Extended reasoning, test-time scaling
2025    Claude 4, Gemini 2.5                Multi-agent, 1M+ context
2025    Physical AI (various)               Robotics foundation models advance
2026    Claude Sonnet 4, DeepSeek V4        Continued scaling, reasoning, agents

Final Words

This textbook covers everything from the mathematical foundations of linear algebra and probability to the frontiers of mechanistic interpretability, causal AI, and the path to AGI. It is designed to be fully self-contained β€” every concept is explained here, every derivation is shown, every algorithm is implemented in pseudocode.

You don't need to search the internet. You don't need to read separate papers. Everything you need for a complete understanding β€” from "what is a vector?" to "what is the NTK regime?" to "how does superposition challenge interpretability?" β€” is right here.

The only thing left is to build.

β€” a1n4a, July 2026