6.5.2Research Frontiers & Practice

Implementing models from scratch

3,575 words16 min readdifficulty · medium

Core Motivation

When you import torch.nn.Linear, you're using a computational abstraction that hides:

  • How gradients actually propagate (backprop mechanics)
  • Memory layout and computation order
  • Numerical stability trade-offs
  • Why certain design choices exist

Building from scratch forces you to answer: What is this layer actually computing, and why does this update rule work?

The Three Pillars of From-Scratch Implementation

1. Forward Pass: Computing Outputs

Derivation for a Simple Dense Layer:

Start with the fundamental question: How do we transform an input vector into an output space?

Linear transformation (the simplest learnable operation): z=Wx+b\mathbf{z} = \mathbf{W}\mathbf{x} + \mathbf{b}

Why this form?

  • W\mathbf{W} (weight matrix): Rotates and scales input space
  • b\mathbf{b} (bias vector): Translates the decision boundary
  • This is the most general affine transformation

For input xRn\mathbf{x} \in \mathbb{R}^{n}, output zRm\mathbf{z} \in \mathbb{R}^{m}:

  • WRm×n\mathbf{W} \in \mathbb{R}^{m \times n} (each row is a separate linear function)
  • bRm\mathbf{b} \in \mathbb{R}^{m}

Then apply activation function σ\sigma: a=σ(z)\mathbf{a} = \sigma(\mathbf{z})

Why activation? Without it, stacking layers just gives another linear function: W2(W1x+b1)+b2=Wx+b\mathbf{W}_2(\mathbf{W}_1\mathbf{x} + \mathbf{b}_1) + \mathbf{b}_2 = \mathbf{W}'\mathbf{x} + \mathbf{b}'. Activation injects non-linearity, enabling universal function approximation.

2. Backward Pass: Computing Gradients

Derivation from Chain Rule:

Given loss LL, we want LW\frac{\partial L}{\partial \mathbf{W}} and Lb\frac{\partial L}{\partial \mathbf{b}}.

Assume we receive LZ\frac{\partial L}{\partial \mathbf{Z}} from the next layer (call it δ\delta).

For weights, using the chain rule: LW=LZZW\frac{\partial L}{\partial \mathbf{W}} = \frac{\partial L}{\partial \mathbf{Z}} \frac{\partial \mathbf{Z}}{\partial \mathbf{W}}

Computing ZW\frac{\partial \mathbf{Z}}{\partial \mathbf{W}}:

From Z=WX+b\mathbf{Z} = \mathbf{W}\mathbf{X} + \mathbf{b}, for element Zi=jWijXj+biZ_i = \sum_j W_{ij}X_j + b_i:

ZiWij=Xj\frac{\partial Z_i}{\partial W_{ij}} = X_j

In matrix form: LW=δXT\frac{\partial L}{\partial \mathbf{W}} = \delta \mathbf{X}^T

Why transpose? Dimensionality check:

  • δ\delta: (m×nbatch)(m \times n_{batch})
  • XT\mathbf{X}^T: (nbatch×n)(n_{batch} \times n)
  • Result: (m×n)(m \times n) ✓ (same shape as W\mathbf{W})

For bias: Lb=δ1\frac{\partial L}{\partial \mathbf{b}} = \delta \mathbf{1} where 1\mathbf{1} is a vector of ones (suming across batch dimension).

Why sum? Bias is shared across all samples—we accumulate their individual gradient contributions.

To pass gradient to previous layer: LX=WTδ\frac{\partial L}{\partial \mathbf{X}} = \mathbf{W}^T \delta

Why transpose here? We're "undoing" the forward multiplication—W\mathbf{W} transformed XZ\mathbf{X} \to \mathbf{Z}, so WT\mathbf{W}^T transforms gradients backwards ZX\mathbf{Z} \to \mathbf{X}.

3. Parameter Update: Learning

Why does this work? Taylor expansion around θt\boldsymbol{\theta}_t:

L(θtαL)L(θt)αL2L(\boldsymbol{\theta}_t - \alpha \nabla L) \approx L(\boldsymbol{\theta}_t) - \alpha \|\nabla L\|^2

The second term is negative, so loss decreases (for small enough α\alpha).

Stochastic Gradient Descent (SGD) Derivation:

True gradient over dataset D\mathcal{D}: θL=1D(x,y)DθL(x,y)\nabla_{\boldsymbol{\theta}} L = \frac{1}{|\mathcal{D}|} \sum_{(\mathbf{x}, y) \in \mathcal{D}} \nabla_{\boldsymbol{\theta}} L(\mathbf{x}, y)

Problem: Computing this requires full pass over dataset (expensive!).

Solution: Estimate using a mini-batch BD\mathcal{B} \subset \mathcal{D}: θL1B(x,y)BθL(x,y)\nabla_{\boldsymbol{\theta}} L \approx \frac{1}{|\mathcal{B}|} \sum_{(\mathbf{x}, y) \in \mathcal{B}} \nabla_{\boldsymbol{\theta}} L(\mathbf{x}, y)

Why this works: By the law of large numbers, the sample average converges to the true expectation. Even small batches (32-256) give good estimates.

Common Pitfalls

Complete End-to-End Example

Key Implementation Patterns

Initialization Strategy

Why it matters: Bad initialization → vanishing/exploding gradients → network won't train.

Xavier/Glorot Initialization: WijN(0,1nin)W_{ij} \sim \mathcal{N}\left(0, \frac{1}{n_{in}}\right)

Derivation: For linear layer with nn inputs, if have variance1: Var(z)=Var(iWixi)=iVar(Wi)Var(xi)=nVar(W)\text{Var}(z) = \text{Var}\left(\sum_i Wi x_i\right) = \sum_i \text{Var}(W_i) \text{Var}(x_i) = n \cdot \text{Var}(W)

To keep Var(z)=1\text{Var}(z) = 1, we need Var(W)=1/n\text{Var}(W) = 1/n.

He Initialization (for ReLU): WijN(0,2nin)W_{ij} \sim \mathcal{N}\left(0, \frac{2}{n_{in}}\right)

Why different? ReLU kills half the neurons, so we need 2× the variance to compensate.

Numerical Stability Tricks

# BAD: Direct softmax (can overflow)
def softmax_bad(z):
    return np.exp(z) / np.sum(np.exp(z))
 
# GOOD: Subtract max for stability
def softmax_stable(z):
    # WHY subtract max? exp(z - max) is bounded by exp(0) = 1
    # This prevents overflow while keeping ratios identical
    exp_z = np.exp(z - np.max(z, axis=0, keepdims=True))
    return exp_z / np.sum(exp_z, axis=0, keepdims=True)

Why this works: ezijezj=ezicjezjc\frac{e^{z_i}}{\sum_j e^{z_j}} = \frac{e^{z_i - c}}{\sum_j e^{z_j - c}} for any constant cc. Choosing c=max(z)c = \max(z) prevents overflow.

Recall Explain to a 12-Year-Old

Imagine you're building a robot that learns to recognize cats. When someone shows you how to build a fancy robot kit from the store, you can use it easily—but you don't understand why it works.

Building a neural network from scratch is like building the robot from individual parts: motors, sensors, wires. You have to:

  1. Wire it correctly (forward pass): Connect sensors → brain → wheels so information flows
  2. Learn from mistakes (backward pass): When the robot messes up, figure out which wires sent bad signals and adjust them
  3. Get better gradually (update): Make tiny adjustments each time so you don't overcorrect

The "from scratch" part means you're using basic tools (like simple math operations) instead of pre-built kits. It's harder, but now you understand exactly how information flows through the robot's brain, how it learns from errors, and how to fix it when something breaks.

The key insight: Learning is just finding patterns by trying things, measuring how wrong you are, and adjusting the "knobs" (weights) a tiny bit in the direction that reduces wrongness. Do this thousands of times, and magically, it learns!

Connections

#flashcards/ai-ml

What is the primary purpose of implementing models from scratch? :: To understand the computational machinery—gradient flow, memory layout, numerical stability—that frameworks abstract away, enabling better debugging, optimization, and innovation.

What are the three core components of from-scratch implementation?
1) Forward pass (computing outputs from inputs), 2) Backward pass (computing gradients via backpropagation), 3) Parameter update (applying gradients to learn).
Why must we cache forward pass values?
Because backward pass requires intermediate activations and pre-activation values to compute gradients via the chain rule; recomputing them is inefficient and can be numerically unstable.
What is the formula for a dense layer's forward pass?
z=Wx+b\mathbf{z} = \mathbf{W}\mathbf{x} + \mathbf{b}, where W\mathbf{W} rotates/scales input space and b\mathbf{b} translates the decision boundary.
Why do we need activation functions?
Without non-linear activations, stacking layers only produces another linear function: W2(W1x)=Wx\mathbf{W}_2(\mathbf{W}_1\mathbf{x}) = \mathbf{W}'\mathbf{x}. Non-linearity enables universal function approximation.
Derive the gradient of a dense layer with respect to its weights.
From Z=WX+b\mathbf{Z} = \mathbf{W}\mathbf{X} + \mathbf{b} and chain rule: LW=LZZW=δXT\frac{\partial L}{\partial \mathbf{W}} = \frac{\partial L}{\partial \mathbf{Z}} \frac{\partial \mathbf{Z}}{\partial \mathbf{W}} = \delta \mathbf{X}^T, where δ=LZ\delta = \frac{\partial L}{\partial \mathbf{Z}}.
Why do we transpose X when computing weight gradients?
Dimensionality matching: δ\delta is (m×nbatch)(m \times n_{batch}), XT\mathbf{X}^T is (nbatch×n)(n_{batch} \times n), producing (m×n)(m \times n) which matches W\mathbf{W}'s shape.
What is the gradient of a dense layer with respect to its input?
LX=WTδ\frac{\partial L}{\partial \mathbf{X}} = \mathbf{W}^T \delta, where WT\mathbf{W}^T "reverses" the forward transformation to flow gradients backward.
Why do we sum gradients across the batch for bias?
Bias is shared across all samples in a batch, so we accumulate gradient contributions from each sample to update the single shared bias parameter.
What is the derivative of ReLU activation?
zReLU(z)={1if z>00if z0\frac{\partial}{\partial z}\text{ReLU}(z) = \begin{cases} 1 & \text{if } z > 0 \\ 0 & \text{if } z \leq 0 \end{cases} — gradient flows through only where input was positive.
Derive the gradient descent update rule.
From Taylor expansion L(θαL)L(θ)αL2L(\theta - \alpha \nabla L) \approx L(\theta) - \alpha \|\nabla L\|^2, the second term is negative, so moving in direction L-\nabla L decreases loss. Update: θt+1=θtαθL\theta_{t+1} = \theta_t - \alpha \nabla_\theta L.
What is mini-batch SGD and why does it work?
Approximating the full gradient L=1DLi\nabla L = \frac{1}{|\mathcal{D}|}\sum \nabla L_i with a small batch L1BiBLi\nabla L \approx \frac{1}{|\mathcal{B}|}\sum_{i \in \mathcal{B}} \nabla L_i. Works by law of large numbers: sample average converges to true expectation.
What is momentum in optimization and how does it help?
Momentum accumulates exponential moving average of gradients: vt=βvt1+(1β)Lv_t = \beta v_{t-1} + (1-\beta)\nabla L. Smooths noisy gradients and accelerates in consistent directions by adding inertia.
What is Xavier/Glorot initialization and why?
WN(0,1/nin)W \sim \mathcal{N}(0, 1/n_{in}). Keeps variance stable: Var(Wixi)=nVar(W)\text{Var}(\sum W_i x_i) = n \cdot \text{Var}(W), so setting Var(W)=1/n\text{Var}(W) = 1/n maintains Var(z)=1\text{Var}(z) = 1.
Why is He initialization different from Xavier?
He initialization uses N(0,2/nin)\mathcal{N}(0, 2/n_{in}) because ReLU kills half the neurons (sets them to 0), so we need 2× the variance to compensate for the reduced activations.
Why subtract max in softmax computation?
Numerical stability: eziezj=ezicezjc\frac{e^{z_i}}{\sum e^{z_j}} = \frac{e^{z_i-c}}{\sum e^{z_j-c}} for any cc. Choosing c=max(z)c=\max(z) bounds ezc1e^{z-c} \leq 1, preventing overflow while keeping ratios identical.
What is the complete backpropagation equation through a layer with activation?
δ1=(W())Tδσ(Z(1))\delta_{\ell-1} = (\mathbf{W}^{(\ell)})^T \delta_{\ell} \odot \sigma'(\mathbf{Z}^{(\ell-1)}), where \odot is element-wise multiplication and σ\sigma' is activation derivative.
Why can't we just transpose randomly until shapes match in backprop?
Correct shape doesn't guarantee correct gradient. Must derive from chain rule symbolically first: wrong transposes compute mathematically invalid gradients that seem to work dimensionally.
What happens if we forget to multiply by activation derivatives?
Gradients don't account for activation's effect on information flow. Example: saturated sigmoid (z=10z=10, σ(10)0\sigma'(10) \approx 0) causes vanishing gradients—ignoring σ\sigma' mises this critical signal death.
Why does XOR require a hidden layer?
XOR is not linearly separable—no single line can separate the classes. A hidden layer with non-linear activation creates a new feature space where the classes become separable.

Concept Map

removes

enables

hides

hides

rests on

uses

W

b

feeds

injects

allows

caches X for

Build from scratch

Computational abstraction

Debug and optimize

Backprop mechanics

Numerical stability

Forward pass

Linear transform Wx+b

Rotates and scales

Translates boundary

Activation sigma

Non-linearity

Universal approximation

Hinglish (regional understanding)

Intuition Hinglish mein samjho

Chalo isko simple tareeke se samajhte hain. Jab aap PyTorch ya TensorFlow mein torch.nn.Linear import karke model banate ho, toh sab kuch ek black box ki tarah kaam karta hai — andar kya ho raha hai, gradients kaise flow kar rahe hain, ye sab hidden rehta hai. From-scratch implementation ka core intuition yahi hai ki aap khud se, numpy jaise basic tools se, ek layer ko banao taaki aap samajh sako ki wo layer actually kya compute kar rahi hai aur kyun kaam karti hai. Jaise cooking sikhna — sirf recipe follow karna alag baat hai, par ye samajhna ki heat se protein kaise change hote hain, wahi asli mastery hai.

Ismein do main cheezein hain: forward pass aur backward pass. Forward pass mein aap input x\mathbf{x} ko ek linear transformation z=Wx+b\mathbf{z} = \mathbf{W}\mathbf{x} + \mathbf{b} se guzarte ho — jahan W\mathbf{W} input space ko rotate aur scale karta hai, aur b\mathbf{b} decision boundary ko shift karta hai. Phir ek activation function σ\sigma lagate ho jo non-linearity add karta hai. Ye non-linearity bahut zaroori hai, kyunki iske bina chahe aap kitni bhi layers stack kar lo, wo sab milke bas ek single linear function ban jaayengi — matlab network kuch complex seekh hi nahi paayega. Backward pass mein aap chain rule use karke nikaalte ho ki har parameter loss ko kitna affect karta hai, aur ye output se input ki taraf ulta chalta hai.

Ye cheez matter kyun karti hai? Kyunki jab aapka model fail hota hai ya galat predictions deta hai, toh debugging tabhi possible hai jab aapko andar ki machinery samajh aati ho. Real research aur practice mein aksar aapko naye ideas try karne padte hain jo ready-made libraries mein hote hi nahi — tab ye foundational knowledge aapko innovate karne ki power deti hai. Ek chhoti si detail bhi note karo: implementation mein hum input X\mathbf{X} ko cache karte hain forward pass mein, kyunki backward pass mein gradients compute karne ke liye wahi input chahiye hota hai. Ye chhoti-chhoti practical insights aapko sirf tabhi milti hain jab aap khud haath ganda karke code likhte ho.

Go deeper — visual, from zero

Test yourself — Research Frontiers & Practice

Connections