Skip to main content
Milestone #04Not StartedPhase 3Intermediate → Advanced14–18 hours · 2 weeks15 required tasks

Automatic Differentiation Engine

Build a micrograd-style reverse-mode autodiff engine and train a neural network in pure Python

PythonComputational Graphs
Build Progress0 / 15 tasks (0%)
0%(0/15 steps)Not Started
Project Specification & Architecture

What You Will Build

Modern deep learning frameworks like PyTorch and JAX are built around one core abstraction: Reverse-Mode Automatic Differentiation (`autograd`). Whenever you execute a forward calculation, the engine dynamically constructs a Directed Acyclic Graph (DAG) of operations and closures. Calling `.backward()` on the final scalar loss traverses that graph in reverse topological order, applying the multivariable chain rule to populate `.grad` on every weight in the network. Inspired by Andrej Karpathy's `micrograd`, you will build a complete scalar autodiff engine in pure Python and use it to train a multilayer perceptron classifier.

Why This Project Matters in the AI/LLM Stack

Once you build `Value` and `.backward()` in ~150 lines of pure Python, backpropagation ceases to be magic. You will understand why gradients accumulate with `+=` when a node is reused across multiple paths, why `.zero_grad()` is required every step, and how PyTorch's `autograd` engine works under the hood.

Learning Objectives

  • Implement a scalar `Value` node that wraps a float, tracks parent operands, and stores its local `.grad` and `_backward` closure.
  • Overload arithmetic operators (`+`, `-`, `*`, `/`, `**`) and nonlinear activations (`tanh`, `relu`, `exp`) with exact local chain-rule updates.
  • Implement DFS topological sorting and reverse-mode `.backward()` with correct `+=` gradient accumulation for multi-use nodes.
  • Build `Neuron`, `Layer`, and `MLP` classes on top of `Value`, verify gradients numerically, and train a neural network to minimize loss.

Prerequisites

  • Phase 1 Multivariable Calculus (derivatives, partial derivatives, chain rule)
  • Object-oriented Python (operator overloading: `__add__`, `__mul__`, `__pow__`, `__radd__`, `__neg__`)

Required Mathematical Concepts

  • Directed Acyclic Graphs (DAGs) of elementary operations
  • Single and multivariable chain rule: ∂L/∂x = ∑_i (∂L/∂z_i)(∂z_i/∂x)
  • Local elementary derivatives: d(a+b)/da = 1, d(a·b)/da = b, d(aⁿ)/da = n·aⁿ⁻¹
  • Nonlinear activation derivatives: d(tanh x)/dx = 1 - tanh²(x), d(ReLU x)/dx = 𝟙(x > 0)
  • Reverse topological ordering of graph nodes
  • Central finite-difference gradient verification
Interactive Engineering Checklist

Step-by-Step Implementation Tasks (15 Steps)

Check off each engineering task as your code passes its acceptance criteria. Progress is saved automatically in your browser.

Stage 1 of 40/6 tasks complete

Scalar Value Node & Elementary Arithmetic Operations

Create the `Value` class, track parent pointers, and overload addition, multiplication, subtraction, division, and power operations.

Core

Every scalar number in the computational graph is wrapped in a `Value` object that tracks both its forward value and its backward sensitivity.

Implementation: Create `class Value` in `autograd/engine.py`. Initialize `self.data = float(data)`, `self.grad = 0.0`, `self._backward = lambda: None`, `self._prev = set(_children)`, and `self._op = _op`.
Expected Outcome: `Value(3.0)` initializes cleanly with `data=3.0` and `grad=0.0`.
Acceptance Check: Includes a readable `__repr__` returning `Value(data=..., grad=...)`.
Core

Expressions often mix `Value` objects with plain Python `int` or `float` literals (e.g., `x + 2.0` or `3.0 * x`).

Implementation: Write a helper `other = other if isinstance(other, Value) else Value(other)` inside binary operators, and implement reflected operators (`__radd__`, `__rmul__`, `__rsub__`, `__rtruediv__`) so `2.0 * Value(3.0)` works seamlessly.
Expected Outcome: Mixed expressions like `2 + Value(3.0) * 4` evaluate without `TypeError`.
Acceptance Check: Both left and right scalar literals are automatically wrapped in `Value` nodes.
Core

To traverse the graph backward from the output loss to the leaf inputs, each output node must remember which child `Value` nodes produced it.

Implementation: Ensure every operation constructs its output `Value` with `_children=(self, other)` and a descriptive `_op` tag (`'+'`, `'*'`, `'**'`, `'relu'`, `'tanh'`).
Expected Outcome: For `c = a + b`, `c._prev` contains `{a, b}` and `c._op == '+'`.
Acceptance Check: Leaf nodes have an empty `_prev` set.
Core

An addition gate has local derivative `1.0` for both inputs, so it routes the upstream gradient `out.grad` unchanged to both parents.

Implementation: Implement `__add__` (and `__neg__`, `__sub__`). In `_backward()`, increment `self.grad += 1.0 * out.grad` and `other.grad += 1.0 * out.grad`.
Expected Outcome: Forward pass sums `.data` and `_backward` adds `out.grad` to both operands.
Acceptance Check: Subtraction `a - b` is implemented cleanly as `a + (-b)`.
Core

By the product rule, for `out = a * b`, `∂out/∂a = b` and `∂out/∂b = a`. A multiplication gate swaps and scales gradients by the opposite operand's value.

Implementation: Implement `__mul__`. In `_backward()`, update `self.grad += other.data * out.grad` and `other.grad += self.data * out.grad`.
Expected Outcome: For `a = Value(2.0)`, `b = Value(-3.0)`, `c = a * b` with `c.grad = 1.0`, calling `c._backward()` sets `a.grad = -3.0` and `b.grad = 2.0`.
Acceptance Check: Uses `+=` rather than `=` so shared operands accumulate gradients.
Core

Supporting `x ** n` (for `int` or `float` exponent `n`) enables division `a / b = a * (b ** -1)` and squared error loss `(y_pred - y) ** 2`.

Implementation: Implement `__pow__(self, other: int | float)` where `out = Value(self.data ** other, (self,), f'**{other}')` and `_backward` updates `self.grad += (other * (self.data ** (other - 1))) * out.grad`. Implement `__truediv__` as `self * (other ** -1)`.
Expected Outcome: `(Value(4.0) / Value(2.0)).data == 2.0` with exact backward gradients.
Acceptance Check: Asserts `isinstance(other, (int, float))` in `__pow__`.
Stage 2 of 40/2 tasks complete

Nonlinear Activations & Computational Graph Construction

Implement `tanh`, `relu`, and `exp` operations and inspect multi-step computational graphs.

Core

Without nonlinear activation functions, a stack of linear layers collapses into a single linear transformation.

Implementation: Implement `.tanh()`, `.relu()`, and `.exp()` methods on `Value`. For `tanh`, local derivative is `1 - tanh²(x)`. For `relu`, local derivative is `1.0 if out.data > 0 else 0.0`.
Expected Outcome: `Value(0.8).tanh()` and `Value(-2.0).relu()` compute exact forward activations and backward gradients.
Acceptance Check: `relu` gradient is `0.0` for negative inputs and `1.0` for positive inputs.
Core

Composing multiple operations builds a multi-layer expression graph ready for reverse traversal.

Implementation: Write a helper function `trace_graph(root: Value) -> tuple[set[Value], set[tuple[Value, Value]]]` that collects all nodes and directed edges in a forward expression such as a 2-input neuron `n = (x1*w1 + x2*w2 + b).tanh()`.
Expected Outcome: Returns the complete set of `Value` nodes and parent-child edges for any compound expression.
Acceptance Check: Accurately enumerates all intermediate operation nodes.
Stage 3 of 40/4 tasks complete

Topological Traversal, Backpropagation & Numerical Validation

Implement topological sort, reverse-mode `.backward()`, `+=` gradient accumulation, and numerical gradient verification.

Intermediate

Before computing `node._backward()`, every downstream node that depends on `node` must have already finished accumulating its gradient into `node.grad`.

Implementation: Implement DFS post-order topological sort starting from the root loss `Value`: maintain a `visited` set, recursively visit all children in `v._prev`, and append `v` to `topo` after all its children have been visited.
Expected Outcome: `reversed(topo)` places the root output first and leaf inputs last, guaranteeing all dependencies are satisfied.
Acceptance Check: Each unique `Value` node appears in `topo` at most once, even in diamond-shaped graphs (`y = (x * x) + x`).
Intermediate

Calling `loss.backward()` seeds `loss.grad = 1.0` (`∂loss/∂loss = 1`) and invokes `_backward()` on every node in reverse topological order.

Implementation: Implement `.backward(self)` on `Value` using your topological sort.
Expected Outcome: A single call to `loss.backward()` populates `.grad` on every node in the graph.
Acceptance Check: Works on arbitrary nested arithmetic and activation expressions.
Core

When a variable `a` is used more than once (e.g., `b = a + a` or `c = a * a + a`), the multivariable chain rule requires summing the gradients from all outgoing branches.

Implementation: Verify that every `_backward()` closure uses `+=` instead of `=`. Test expressions like `a = Value(3.0); b = a + a; b.backward()` and confirm `a.grad == 2.0` (not `1.0`).
Expected Outcome: Multi-use variables accumulate gradients across all paths accurately.
Acceptance Check: Explicit unit test for `f(a) = a * a + a` at `a = 3.0` asserts `a.grad == 7.0`.
Intermediate

Comparing `Value.grad` against finite differences `(f(x+h) - f(x-h)) / (2h)` (and optionally PyTorch `torch.tensor(..., requires_grad=True)`) proves your autodiff engine is exact.

Implementation: Write a numerical gradient check function that evaluates a complex multi-variable expression (`tanh`, `relu`, division, powers), computes `.backward()`, and asserts agreement with central finite differences (`h = 1e-6`).
Expected Outcome: All analytical gradients match finite differences within `1e-6`.
Acceptance Check: Automated test covers all implemented operators (`+`, `-`, `*`, `/`, `**`, `tanh`, `relu`, `exp`).
Stage 4 of 40/3 tasks complete

Multilayer Perceptron, Training Loop & Testing

Construct `Neuron`, `Layer`, and `MLP` modules, train on a dataset, and write unit tests.

Intermediate

With `Value` complete, a neural network is simply a hierarchy of `Module`, `Neuron`, `Layer`, and `MLP` classes that hold `Value` parameters.

Implementation: Create `autograd/nn.py` implementing `Module` (with `.parameters()` and `.zero_grad()`), `Neuron(nin, nonlin=True)`, `Layer(nin, nout)`, and `MLP(nin, nouts)`.
Expected Outcome: `MLP(3, [4, 4, 1])` creates a 3-layer neural network with 41 scalar `Value` parameters.
Acceptance Check: `mlp.zero_grad()` resets `.grad = 0.0` on all parameters.
Core

Training the MLP requires a loop of: forward pass -> scalar loss -> `zero_grad()` -> `loss.backward()` -> parameter update `p.data -= lr * p.grad`.

Implementation: Train an `MLP(3, [4, 4, 1])` on a small binary classification dataset using squared error or hinge loss for 50–100 steps. Print loss and predictions across training.
Expected Outcome: Training loss drops near `0.0` and predictions match target labels.
Acceptance Check: Demonstrates `zero_grad()` before `backward()` every step so gradients do not explode across epochs.
Core

Documenting how computational graphs and reverse-mode autodiff work makes your repository a reference-grade engineering artifact.

Implementation: Create `tests/test_autograd.py` testing operator overloading, diamond graph accumulation, numerical gradient checks, and MLP convergence. Document the architecture in `README.md`.
Expected Outcome: All `pytest` tests pass and `README.md` explains the chain rule and topological sort clearly.
Acceptance Check: Zero external dependencies required for the core `autograd` package.
Reference Architecture & Verification Code

Starter & Verification Code Examples

Topological Sort & Reverse-Mode Backpropagation on `Value`

Illustrates how `backward()` orders the computational DAG topologically and executes local chain-rule closures.

class Value:
    def __init__(self, data: float, _children: tuple["Value", ...] = (), _op: str = "") -> None:
        self.data = float(data)
        self.grad = 0.0
        self._backward = lambda: None
        self._prev = set(_children)
        self._op = _op

    def backward(self) -> None:
        topo: list[Value] = []
        visited: set[Value] = set()

        def build_topo(v: Value) -> None:
            if v not in visited:
                visited.add(v)
                for child in v._prev:
                    build_topo(child)
                topo.append(v)

        build_topo(self)
        self.grad = 1.0
        for node in reversed(topo):
            node._backward()
Verification & Acceptance Suite

How to Test Your Implementation

Verify multi-path gradient accumulation, finite-difference agreement across all operators, and MLP training convergence.

$ pytest tests/test_autograd.py -vTest Runner

Diamond Graph Gradient Accumulation (`+=` Test)

Catch the classic bug where `_backward` overwrites `self.grad = ...` instead of accumulating `self.grad += ...`.

Steps: Compute x = Value(3.0); y = (x * x) + (x + x); y.backward().

Pass Criteria: y.data == 15.0 and x.grad == 8.0 (since dy/dx = 2x + 2 = 8 at x=3).

Central Finite-Difference Verification on Compound DAG

Confirm chain rule accuracy through composed division, power, tanh, and ReLU gates.

Steps: Evaluate f(a, b) = ((a * b + b**2).tanh() / (a - b)).relu() and compare a.grad, b.grad with finite differences (eps=1e-6).

Pass Criteria: Absolute difference < 1e-6 for all leaf inputs.

MLP Convergence & Zero-Grad Check

Verify that an MLP reduces loss over 40 steps of gradient descent.

Steps: Run 40 steps of forward, zero_grad(), backward(), and step(lr=0.05) on a 4-sample dataset.

Pass Criteria: Final loss < 0.05 and all 4 binary targets classified accurately.

Overall Project Acceptance Criteria

  • `Value` class supports `+`, `-`, `*`, `/`, `**`, `tanh`, `relu`, and `exp` with parent tracking.
  • Topological sort and reverse-mode `.backward()` accurately propagate gradients through arbitrary DAGs.
  • Multi-branch gradient accumulation (`+=`) verified on reused variables.
  • `Neuron`, `Layer`, and `MLP` classes built and trained to convergence on a small dataset.
Progressive Spoiler-Free Guidance

Expandable Hints & Debugging Tips

Try implementing each stage on your own first. Expand a hint below if you want to verify tensor shapes, mathematical identities, or common edge cases.

Optional Bonus Extensions

Optional Stretch Challenges

Optional · Does Not Block Completion

Computational Graph Visualizer (DOT / SVG)

Write a utility that exports any `Value` expression graph into Graphviz DOT format showing `.data`, `.grad`, and `_op` gates at every node.

What You Learn: How to visually inspect forward activations and backward gradient flow through a network.

Cross-Entropy Loss & Softmax in Scalar Autodiff

Using `.exp()`, `.log()`, and summation over `Value` logits, implement multi-class Softmax Cross-Entropy loss and verify its gradients against `probs - one_hot`.

What You Learn: How composite losses automatically differentiate through elementary `exp` and `log` primitives.

Expected Deliverable

What Your Completed Repository Should Contain

A working autodiff engine capable of training a small neural network.

Deliverable Artifacts

  • Pure-Python `Value` reverse-mode autodiff engine (`autograd/engine.py`)
  • `Neuron`, `Layer`, and `MLP` neural network modules (`autograd/nn.py`)
  • Numerical gradient verification and MLP training test suite

After Completing This Project, You Should Be Able To...

  • Explain how dynamic computational graphs record forward operations and parent dependencies.
  • Implement reverse-mode automatic differentiation using DFS topological sorting and chain-rule closures.
  • Handle multi-path gradient accumulation (`+=`) and explain why `.zero_grad()` is required between optimization steps.
  • Build and train a multilayer perceptron from scratch on top of scalar autodiff primitives.
Portfolio & Repository Tracking

GitHub Repository Submission

Link your public or private GitHub repository URL to keep a structured portfolio of your from-scratch AI builds. Stored locally in your browser (self-reported; no GitHub tokens or remote code execution).

Status: Not Linked

Suggested Repository: micrograd-autodiff-engine

Recommended Directory Layout:

autograd/engine.py
autograd/nn.py
tests/test_autograd.py
README.md

Recommended README Documentation Checklist

  • Diagram or explanation of forward DAG construction and reverse topological pass
  • Instructions to run finite-difference gradient tests
  • Example training log of the pure-Python MLP
Local Engineering Notebook

Personal Implementation Notes