Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

15 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

nano-grad

Author: Swayam · Roll No: 24B4229

A reverse-mode autograd engine + tiny neural-network library built from scratch in pure NumPy. No PyTorch / JAX / TensorFlow / autograd for differentiation — just numpy and hand-written calculus.

Trained a 2-layer MLP on MNIST to 97.65% test accuracy and a CNN on CIFAR-10 to 73% — using a from-scratch autograd engine (no PyTorch/JAX for differentiation). MNIST is within 0.3% of a reference PyTorch model on the same architecture, at ~4× the training time.

Every gradient is verified against finite-difference numerical baselines (tests/test_gradcheck.py), so the math is provably correct, not just plausible.

Results at a glance

Task Result vs. reference
MNIST MLP (784→128→10) 97.65% test PyTorch ~97.5% on same arch
CIFAR-10 CNN (from-scratch conv2d) 73.0% test +5.5% after fixing overfitting
Gradient correctness 14/14 finite-diff checks pass analytic == numeric
Speed (MLP fwd+bwd) ~4× slower than PyTorch identical math, pure numpy
Peak training RAM (CNN) ~1.6 GB per-epoch process isolation
Autodiff library imports 0 torch/jax/tf grep proves it

The rule (what makes this defensible)

import torch is banned in the engine. The differentiation code (nanograd/tensor.py, nn.py, optim.py) imports only numpy + the standard library. Even the MNIST loader is pure urllib + gzip + numpy — there is no torch anywhere in the repo.

grep -rn "import torch" nanograd/    # -> no matches

How reverse-mode autodiff works

Every Tensor records the op that produced it and the parent tensors it came from — that graph is the tape. Each op attaches a _backward closure holding its local gradient. Calling .backward() on a scalar loss:

  1. Topological sort the graph so every node comes after its parents.
  2. Seed dL/dL = 1.
  3. Walk the nodes in reverse, calling each _backward once. The chain rule falls out: each closure multiplies the incoming out.grad by its local derivative and accumulates into its parents' .grad.
   x ──┐
       ├─(matmul)─► h ─(relu)─► a ─(matmul)─► logits ─(softmax-CE)─► loss
   W ──┘                                                              │
                                                                      ▼
        grads flow back along the same edges  ◄───────────  dL/dloss = 1

Broadcasting is handled by _unbroadcast, which sums a gradient back down to its operand's shape — so x + b with a broadcast bias gets correct gradients for free.

Ops implemented (all gradient-checked)

Op Forward Local gradient
add, sub x + y 1, -1 (broadcast-aware)
mul, div x * y, x / y y, x / quotient rule
matmul x @ W g @ Wᵀ, xᵀ @ g
pow(n) xⁿ n·xⁿ⁻¹
exp, log , ln x , 1/x
relu max(0,x) 1[x>0]
sigmoid 1/(1+e⁻ˣ) s(1-s)
tanh tanh x 1-tanh²x
sum, mean reduce broadcast 1 (/N)
reshape, transpose view inverse view
softmax_cross_entropy fused, stable (softmax - onehot)/N
conv2d im2col → matmul col2im scatter (dx), dout·colsᵀ (dW)
maxpool2d window max route grad to argmax position
flatten (N, …)→(N, D) reshape back

Results

MNIST — 784 → 128 (ReLU) → 10, Adam, batch 64, 5 epochs

97.65% test accuracy in ~37s (pure numpy, CPU).

MNIST loss curve

CIFAR-10 — CNN, from-scratch conv2d + maxpool

Conv(3→16) → ReLU → MaxPool → Conv(16→32) → ReLU → MaxPool → Dropout → FC(2048→256) → ReLU → Dropout → FC(256→10), Adam, cosine LR, softmax CE, with random-flip + random-crop data augmentation.

73.0% test accuracy (24 epochs, best 73.0%), peak RAM 1.7 GB.

CIFAR-10 training

Diagnosing and fixing the plateau. A first, unregularized CNN plateaued at 68.3% test with 78.3% train — a 10-point gap that diagnosed as clear overfitting. Adding Dropout + data augmentation (flip/crop) + a cosine LR schedule closed the gap to 3.2% and lifted test accuracy to 73.0%:

train test gap
baseline CNN 78.3% 68.3% 9.9%
+ dropout + augmentation 75.9% 72.8% 3.2%

Trained entirely on the nano-grad engine (conv2d via im2col, maxpool via argmax routing — both finite-difference verified). Dropout is implemented purely through the engine's elementwise multiply, so its gradient falls out for free. To keep peak RAM bounded, each epoch runs in its own process (memory resets between epochs) and the autograd graph is torn down every batch — peak RAM stays ~1.6 GB regardless of run length.

Benchmark vs PyTorch (same MLP, 300 fwd+bwd passes, batch 64)

nano-grad is ~4× slower than PyTorch — same math, no fused kernels, no C++. That's the honest cost of writing autodiff by hand in numpy.

Benchmark

Optimizers — SGD+momentum vs Adam (from scratch)

Optimizer comparison

Quickstart

pip install -r requirements.txt

PYTHONPATH=. python3 tests/test_tensor.py       # sanity + grad tests
PYTHONPATH=. python3 tests/test_gradcheck.py    # 11 finite-difference checks
PYTHONPATH=. python3 tests/test_conv.py         # conv2d + maxpool grad checks
PYTHONPATH=. python3 examples/train_xor.py      # learns XOR -> loss 0
PYTHONPATH=. python3 examples/train_mnist.py    # -> 97.65% test acc
PYTHONPATH=. python3 examples/train_cifar.py    # CNN on CIFAR-10
PYTHONPATH=. python3 examples/benchmark.py      # vs PyTorch
PYTHONPATH=. python3 examples/compare_optim.py  # SGD vs Adam

API (PyTorch-like)

from nanograd.tensor import Tensor
from nanograd.nn import Sequential, Linear, ReLU
from nanograd.optim import Adam

net = Sequential(Linear(784, 128), ReLU(), Linear(128, 10))
opt = Adam(net.parameters(), lr=1e-3)

logits = net(Tensor(x_batch, requires_grad=False))
loss = logits.softmax_cross_entropy(y_batch)
net.zero_grad()
loss.backward()
opt.step()

Layout

nanograd/
  tensor.py       autograd engine — Tensor, ops (incl. conv2d/maxpool), backward
  nn.py           Module / Linear / Conv2d / MaxPool2d / Flatten / Dropout / acts
  optim.py        SGD (+momentum), Adam (bias-corrected)
  data/mnist.py   pure-numpy MNIST loader (urllib + gzip + IDX parse)
  data/cifar10.py CIFAR-10 loader (HF-parquet + Pillow, tarball fallback)
examples/         train_xor, train_mnist, train_cifar, benchmark, compare_optim
tests/            test_tensor.py, test_gradcheck.py, test_conv.py

What I'd improve next

  • BatchNorm + a deeper/wider net to push CIFAR-10 from 73% past 80% (dropout + augmentation already closed the overfitting gap; the next gain is capacity).
  • A proper no_grad() context and in-place grad accumulation to cut allocations.
  • Vectorized/batched conv and simple graph fusion to close the speed gap.

Author

Swayam — Roll No. 24B4229

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages