Real-World AI/ML β’ EduArtha
Industry Problems & Solutions
Don't just read theory β implement it. Every concept you learned must become working code. This guide takes you from building your first project to publishing research and shipping AI products that solve real industry problems.
6 Steps to AI Mastery | 8 Industry Domains | Working Code | Case Studies
Your AI Roadmap
6 steps from zero to AI researcher
Build Small Projects from Scratch
Why This Step Matters
- Don't just read theory β implement it. Every concept must become working code
- This is where real understanding forms
- Building from scratch proves you truly understand gradients, attention, and training loops
1. Implement Backpropagation from Scratch (No PyTorch)
Build a tiny autograd engine like Andrej Karpathy's micrograd. This proves you truly understand gradients β the foundation of all deep learning.
Python
class Value:
"""A scalar value with automatic gradient computation β like micrograd"""
def __init__(self, data, _children=(), _op=''):
self.data = data
self.grad = 0.0
self._backward = lambda: None
self._prev = set(_children)
self._op = _op
def __add__(self, other):
other = other if isinstance(other, Value) else Value(other)
out = Value(self.data + other.data, (self, other), '+')
def _backward():
self.grad += out.grad # d(a+b)/da = 1
other.grad += out.grad # d(a+b)/db = 1
out._backward = _backward
return out
def __mul__(self, other):
other = other if isinstance(other, Value) else Value(other)
out = Value(self.data * other.data, (self, other), '*')
def _backward():
self.grad += other.data * out.grad # d(a*b)/da = b
other.grad += self.data * out.grad # d(a*b)/db = a
out._backward = _backward
return out
def relu(self):
out = Value(0 if self.data < 0 else self.data, (self,), 'ReLU')
def _backward():
self.grad += (out.data > 0) * out.grad
out._backward = _backward
return out
def backward(self):
"""Topological sort + reverse-mode autodiff"""
topo, visited = [], set()
def build_topo(v):
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 v in reversed(topo):
v._backward()
# Test it β this is literally how PyTorch works internally!
a = Value(2.0); b = Value(-3.0); c = Value(10.0)
d = a * b + c # d = 2*(-3) + 10 = 4
d.backward()
print(f"a.grad = {a.grad}") # -3.0 (dd/da = b = -3)
print(f"b.grad = {b.grad}") # 2.0 (dd/db = a = 2)
Project: Build a Neural Network with Your Autograd
Python
import random
class Neuron:
def __init__(self, nin):
self.w = [Value(random.uniform(-1,1)) for _ in range(nin)]
self.b = Value(0)
def __call__(self, x):
act = sum((wi*xi for wi,xi in zip(self.w, x)), self.b)
return act.relu()
def parameters(self): return self.w + [self.b]
class MLP:
def __init__(self, nin, nouts):
sz = [nin] + nouts
self.layers = [[Neuron(sz[i]) for _ in range(sz[i+1])] for i in range(len(nouts))]
def __call__(self, x):
for layer in self.layers:
x = [n(x) for n in layer]
return x[0] if len(x)==1 else x
# Train on XOR β the classic test!
model = MLP(2, [4, 4, 1])
X = [[0,0],[0,1],[1,0],[1,1]]
Y = [0, 1, 1, 0]
for epoch in range(100):
preds = [model(x) for x in X]
loss = sum((p - y)*(p - y) for p, y in zip(preds, Y))
for p in model.parameters(): p.grad = 0.0
loss.backward()
for p in model.parameters(): p.data -= 0.05 * p.grad
2. Train a Character-Level Language Model (GPT-Style) from Scratch
Follow Andrej Karpathy's nanoGPT β build every layer yourself: embedding, attention, MLP, loss.
Python
import torch, torch.nn as nn, torch.nn.functional as F
class Head(nn.Module):
"""Single head of self-attention"""
def __init__(self, head_size, n_embd, block_size):
super().__init__()
self.key = nn.Linear(n_embd, head_size, bias=False)
self.query = nn.Linear(n_embd, head_size, bias=False)
self.value = nn.Linear(n_embd, head_size, bias=False)
self.register_buffer('tril', torch.tril(torch.ones(block_size, block_size)))
def forward(self, x):
B, T, C = x.shape
k, q = self.key(x), self.query(x)
wei = q @ k.transpose(-2,-1) * C**-0.5
wei = wei.masked_fill(self.tril[:T,:T]==0, float('-inf'))
wei = F.softmax(wei, dim=-1)
return wei @ self.value(x)
class GPT(nn.Module):
def __init__(self, vocab_size, n_embd=64, n_head=4, n_layer=4, block_size=256):
super().__init__()
self.tok_emb = nn.Embedding(vocab_size, n_embd)
self.pos_emb = nn.Embedding(block_size, n_embd)
self.blocks = nn.Sequential(*[Block(n_embd, n_head, block_size) for _ in range(n_layer)])
self.ln_f = nn.LayerNorm(n_embd)
self.lm_head = nn.Linear(n_embd, vocab_size)
def forward(self, idx, targets=None):
B, T = idx.shape
x = self.tok_emb(idx) + self.pos_emb(torch.arange(T, device=idx.device))
x = self.ln_f(self.blocks(x))
logits = self.lm_head(x)
loss = F.cross_entropy(logits.view(-1,logits.size(-1)), targets.view(-1)) if targets is not None else None
return logits, loss
3. Fine-Tune an Open-Source LLM on a Custom Dataset
Use LLaMA or Mistral with LoRA on a small domain dataset (e.g., physics Q&A for EduArtha).
4. Build and Deploy a Simple AI-Powered App
A chatbot, document summarizer, or quiz generator using your model via API.
5 Projects You Must Build
Real industry problems solved from scratch β no pre-trained shortcuts
π Project 1: Question Difficulty Classifier from Scratch
No pre-trained models allowed. Raw text β trained neural network β deployed classifier.
The Real Industry Problem
CBSE/NCERT textbooks have thousands of questions with no difficulty labels. An EdTech platform needs to automatically classify each question as L1 (recall), L2 (application), L3 (analysis) using Bloom's Taxonomy β so students get the right level of challenge. Companies like Byju's, Khan Academy, and Toppr pay for this.
What You Must Build from Scratch
- Tokenizer β no NLTK, no spaCy. Write your own BPE (Byte Pair Encoding) tokenizer. Merge vocab pairs from a CBSE corpus. Handle Hinglish tokens.
- Word embedding layer β from scratch. Implement Word2Vec skip-gram with negative sampling. Train on 10,000 CBSE questions. Understand what word vectors actually mean geometrically.
- Text classification neural net β implement backprop manually. Build a 2-layer MLP in pure NumPy. Implement cross entropy loss, softmax output, and gradient descent by hand. No autograd.
- Attention-based classifier β then compare. Now rebuild it in PyTorch with a simple self-attention layer. Compare accuracy. Understand why attention outperforms naive averaging.
- Active learning loop. Your model should identify which unlabeled questions it is most uncertain about and ask a human to label only those. This is how real annotation pipelines work.
Python
# Step 1: BPE Tokenizer from scratch
class BPETokenizer:
def __init__(self, vocab_size=5000):
self.vocab_size = vocab_size
self.merges = {}
self.vocab = {}
def _get_pair_counts(self, words):
pairs = {}
for word, freq in words.items():
symbols = word.split()
for i in range(len(symbols)-1):
pair = (symbols[i], symbols[i+1])
pairs[pair] = pairs.get(pair, 0) + freq
return pairs
def train(self, corpus):
"""Learn BPE merges from CBSE question corpus"""
words = self._init_vocab(corpus) # character-level split
for i in range(self.vocab_size - len(self.vocab)):
pairs = self._get_pair_counts(words)
if not pairs: break
best = max(pairs, key=pairs.get)
words = self._merge_pair(words, best)
self.merges[best] = i
print(f"Learned {len(self.merges)} merges")
# Step 2: Word2Vec skip-gram from scratch
import numpy as np
class Word2Vec:
def __init__(self, vocab_size, embed_dim=100):
self.W_in = np.random.randn(vocab_size, embed_dim) * 0.01
self.W_out = np.random.randn(embed_dim, vocab_size) * 0.01
def forward(self, center_id, context_ids, negative_ids):
# Center word embedding
h = self.W_in[center_id] # (embed_dim,)
# Positive: maximize dot product with context words
pos_score = self._sigmoid(h @ self.W_out[:, context_ids])
# Negative: minimize dot product with random words
neg_score = self._sigmoid(-h @ self.W_out[:, negative_ids])
loss = -np.log(pos_score + 1e-7).sum() - np.log(neg_score + 1e-7).sum()
return loss
# Step 3: MLP classifier with manual backprop (NumPy only)
class ManualMLP:
def __init__(self, input_dim, hidden_dim, num_classes=3):
self.W1 = np.random.randn(input_dim, hidden_dim) * np.sqrt(2/input_dim)
self.b1 = np.zeros(hidden_dim)
self.W2 = np.random.randn(hidden_dim, num_classes) * np.sqrt(2/hidden_dim)
self.b2 = np.zeros(num_classes)
def forward(self, X):
self.z1 = X @ self.W1 + self.b1
self.a1 = np.maximum(0, self.z1) # ReLU
self.z2 = self.a1 @ self.W2 + self.b2
exp_z = np.exp(self.z2 - self.z2.max(axis=1, keepdims=True))
self.probs = exp_z / exp_z.sum(axis=1, keepdims=True) # softmax
return self.probs
def backward(self, X, y_onehot, lr=0.01):
m = X.shape[0]
dz2 = (self.probs - y_onehot) / m # cross-entropy + softmax gradient
dW2 = self.a1.T @ dz2
db2 = dz2.sum(axis=0)
da1 = dz2 @ self.W2.T
dz1 = da1 * (self.z1 > 0) # ReLU gradient
dW1 = X.T @ dz1
db1 = dz1.sum(axis=0)
# Update weights
self.W2 -= lr * dW2; self.b2 -= lr * db2
self.W1 -= lr * dW1; self.b1 -= lr * db1
Tech Stack
Python NumPy (no autograd) PyTorch BPE tokenizer Word2Vec Bloom's Taxonomy labels Active Learning
How Industry Evaluates This
| Metric | Target | Metric | Target |
|---|---|---|---|
| Macro F1 score | >0.82 on held-out set | Annotation efficiency | 95% accuracy with only 30% labeled data |
| Inference speed | <50ms per question on CPU | Confusion matrix | L1 vs L3 misclassification <5% |
π Project 2: Options Volatility Surface Prediction
Predict implied volatility for Nifty options across strikes and expiries.
The Real Industry Problem
Every options desk at Goldman Sachs, NSE, or Zerodha needs to model the volatility surface β the implied volatility for every strike price and expiry date. Traditional models (Black-Scholes, SABR) make assumptions that fail in real markets. ML-based vol surface models are now used by quant desks to find mispriced options, construct zero-loss strategies, and hedge positions.
What You Must Build from Scratch
- Feature engineering for options data. Build features: moneyness (K/S), time to expiry (Ο), VIX, open interest, Put-Call ratio, historical realized vol. Understand why each matters financially.
- Implement a feedforward neural net for regression β backprop by hand. Predict IV as a continuous value. Use MSE loss. Implement gradient descent manually in NumPy first. Then port to PyTorch with Adam optimizer.
- Add arbitrage-free constraints as a custom loss. A vol surface that allows arbitrage is useless. Add a penalty term to your loss function that enforces calendar spread no-arbitrage and butterfly no-arbitrage conditions.
- Temporal model: LSTM over rolling vol windows. Markets have memory. Implement an LSTM from scratch (all 4 gates, manual backprop through time). Feed it 30-day rolling vol windows. Compare against the static MLP.
- Backtest a butterfly spread using model predictions. When your model predicts IV significantly different from market IV, simulate entering a butterfly spread. Measure P&L over 3 months of NSE data.
Python
# Feature engineering for Nifty options
def build_options_features(option_chain, spot_price, vix):
features = {
"moneyness": option_chain["strike"] / spot_price, # K/S
"log_moneyness": np.log(option_chain["strike"] / spot_price),
"time_to_expiry": option_chain["days_to_expiry"] / 365,
"sqrt_tau": np.sqrt(option_chain["days_to_expiry"] / 365),
"vix": vix,
"open_interest": np.log1p(option_chain["oi"]),
"put_call_ratio": option_chain["put_oi"] / option_chain["call_oi"],
"realized_vol_30d": compute_realized_vol(spot_price, window=30),
}
return features
# Arbitrage-free loss constraint
def arbitrage_free_loss(predicted_iv, strikes, expiries, lambda_arb=10.0):
"""Enforce no-arbitrage conditions in the vol surface"""
mse_loss = F.mse_loss(predicted_iv, target_iv)
# Calendar spread: IV must increase with time (roughly)
total_var = predicted_iv**2 * expiries # total variance = ΟΒ²Ο
calendar_violation = F.relu(-torch.diff(total_var, dim=1)).sum()
# Butterfly: convexity in strike β dΒ²C/dKΒ² β₯ 0
d2_dK2 = torch.diff(predicted_iv, n=2, dim=0)
butterfly_violation = F.relu(-d2_dK2).sum()
return mse_loss + lambda_arb * (calendar_violation + butterfly_violation)
# LSTM for temporal vol prediction β from scratch
class LSTMCell:
"""Manual LSTM with all 4 gates"""
def __init__(self, input_dim, hidden_dim):
scale = np.sqrt(1/(input_dim + hidden_dim))
# Forget, Input, Cell, Output gates
self.Wf = np.random.randn(input_dim+hidden_dim, hidden_dim) * scale
self.Wi = np.random.randn(input_dim+hidden_dim, hidden_dim) * scale
self.Wc = np.random.randn(input_dim+hidden_dim, hidden_dim) * scale
self.Wo = np.random.randn(input_dim+hidden_dim, hidden_dim) * scale
self.bf = np.zeros(hidden_dim)
self.bi = np.zeros(hidden_dim)
self.bc = np.zeros(hidden_dim)
self.bo = np.zeros(hidden_dim)
def forward(self, x, h_prev, c_prev):
concat = np.concatenate([h_prev, x])
f = self._sigmoid(concat @ self.Wf + self.bf) # Forget gate
i = self._sigmoid(concat @ self.Wi + self.bi) # Input gate
c_tilde = np.tanh(concat @ self.Wc + self.bc) # Candidate
c = f * c_prev + i * c_tilde # Cell state
o = self._sigmoid(concat @ self.Wo + self.bo) # Output gate
h = o * np.tanh(c) # Hidden state
return h, c
Tech Stack
NumPy (backprop) PyTorch NSE options data LSTM from scratch Custom loss functions Black-Scholes (baseline) Backtesting engine
How Industry Evaluates This
| Metric | Target | Metric | Target |
|---|---|---|---|
| IV prediction RMSE | <0.5 vol points vs market | No-arbitrage violations | Zero butterfly arbitrage in output surface |
| Backtest Sharpe ratio | Strategy Sharpe >1.5 on 6 months | Beat baseline | Beat SABR model RMSE by >20% |
π Project 3: Physics-Informed Neural Network for Nuclear Binding Energy
Replace semi-empirical mass formula with a neural net that respects shell structure.
The Real Industry Problem
The Bethe-WeizsΓ€cker semi-empirical mass formula (SEMF) predicts nuclear binding energies but fails near magic numbers and deformed nuclei. Labs like GSI, CERN, and RIKEN need accurate predictions for nuclei far from stability. A neural network that incorporates known shell-model physics while learning residual patterns from data could outperform SEMF and traditional models β this is publishable research at your level.
What You Must Build from Scratch
- Implement the SEMF as your baseline model. Code Bethe-WeizsΓ€cker formula. Evaluate on AME2020 (Atomic Mass Evaluation) database. Calculate residuals β these are what your neural net must learn.
- Feature engineering with nuclear structure knowledge. Features: Z, N, A, pairing term, shell distance from magic numbers (2,8,20,28,50,82,126), deformation parameter Ξ², isospin asymmetry. Physical domain knowledge goes into features.
- Build a physics-informed neural network (PINN) in PyTorch. Your loss = data_loss + Ξ» Γ physics_loss. The physics constraint: binding energy per nucleon must be concave with A (stability condition). Implement this as a differentiable penalty.
- Implement uncertainty quantification. Use Monte Carlo Dropout or Deep Ensembles to get confidence intervals on each prediction. For nuclei far from stability, uncertainty should be large β your model must know what it doesn't know.
- Predict 50 unknown nuclei and compare to experiment. Mask 50 nuclei from training. Predict their binding energies. Compare to experimental values. This is exactly how a real paper's validation section works.
Python
# Semi-Empirical Mass Formula β your baseline
def semf_binding_energy(Z, N):
"""Bethe-WeizsΓ€cker formula (MeV)"""
A = Z + N
# Volume, Surface, Coulomb, Asymmetry terms
a_v, a_s, a_c, a_a = 15.67, 17.23, 0.714, 23.29
B = (a_v * A - a_s * A**(2/3) - a_c * Z*(Z-1) / A**(1/3)
- a_a * (N-Z)**2 / A)
# Pairing term
if Z % 2 == 0 and N % 2 == 0: B += 12.0 / A**0.5
elif Z % 2 == 1 and N % 2 == 1: B -= 12.0 / A**0.5
return B
# Physics-informed features
def nuclear_features(Z, N):
A = Z + N
magic = [2, 8, 20, 28, 50, 82, 126]
return {
"Z": Z, "N": N, "A": A,
"isospin_asymmetry": (N-Z)/A,
"pairing": (1 if Z%2==0 and N%2==0 else -1 if Z%2==1 and N%2==1 else 0),
"shell_dist_Z": min(abs(Z - m) for m in magic),
"shell_dist_N": min(abs(N - m) for m in magic),
"deformation_beta": estimate_deformation(Z, N),
"semf_residual": experimental_BE(Z,N) - semf_binding_energy(Z,N),
}
# PINN for binding energy
class NuclearPINN(nn.Module):
def __init__(self, n_features=10):
super().__init__()
self.net = nn.Sequential(
nn.Linear(n_features, 128), nn.ReLU(),
nn.Linear(128, 128), nn.ReLU(),
nn.Linear(128, 64), nn.ReLU(),
nn.Linear(64, 1)) # Predict B/A (binding energy per nucleon)
def forward(self, x):
return self.net(x)
def pinn_loss(model, features, targets, A_values, lambda_phys=0.5):
predictions = model(features).squeeze()
data_loss = F.mse_loss(predictions, targets)
# Physics constraint: B/A should be concave with A
# (stability: adding nucleons shouldn't increase B/A indefinitely)
sorted_idx = A_values.argsort()
ba_sorted = predictions[sorted_idx]
d2_ba = torch.diff(ba_sorted, n=2) # Second derivative
physics_loss = F.relu(d2_ba).sum() # Penalize convex regions
return data_loss + lambda_phys * physics_loss
Tech Stack
PyTorch AME2020 database PINN: custom loss MC Dropout Deep Ensembles Shell model features Matplotlib 3D surfaces
How Industry Evaluates This
| Metric | Target | Metric | Target |
|---|---|---|---|
| RMS deviation | <300 keV vs AME2020 (SEMF: ~2.9 MeV) | Magic number behavior | Correct shell closure peaks in predictions |
| Uncertainty calibration | 90% of true values inside 90% CI | Extrapolation test | Accuracy on neutron-rich isotopes not in training set |
π Project 4: Medical Image Segmentation β Built from Scratch
Detect and segment tumors in chest X-rays without using any pretrained weights.
The Real Industry Problem
Radiology departments at hospitals like AIIMS, Medanta, and Apollo process thousands of chest X-rays daily. AI assisted diagnosis can flag critical cases immediately. But medical AI must be interpretable and uncertainty aware β a radiologist needs to know not just what the model predicted, but how confident it is and exactly which pixels drove the decision.
What You Must Build from Scratch
- Implement convolution operation in NumPy β no
torch.nn.Conv2d. Implement forward pass AND backprop (gradient w.r.t. input and kernel). This is the hardest mathematical step. - Build a U-Net architecture in PyTorch. Encoder (downsampling with max-pool), bottleneck, decoder (upsampling with skip connections). Implement each block yourself β no torchvision models. Use CheXpert or NIH Chest X-ray dataset.
- Custom loss: Dice + Focal loss combination. Standard BCE fails on class-imbalanced medical data (tumors are tiny). Implement Dice loss for overlap quality and Focal loss for hard example mining. Combine them with a learnable Ξ».
- Grad-CAM explainability β implement from scratch. Implement Gradient-weighted Class Activation Maps. Given a prediction, compute which spatial regions drove it. Overlay heatmap on the original X-ray. This is what makes medical AI trustworthy.
- Test-time augmentation + confidence calibration. Run inference 20 times with random augmentations. Mean = final prediction. Variance = uncertainty. Implement temperature scaling to calibrate confidence scores against a validation set.
Python
# Conv2D from scratch in NumPy
def conv2d_forward(X, W, stride=1, padding=0):
"""X: (B,C_in,H,W), W: (C_out,C_in,kH,kW)"""
if padding > 0:
X = np.pad(X, ((0,0),(0,0),(padding,padding),(padding,padding)))
B, C_in, H, W_ = X.shape
C_out, _, kH, kW = W.shape
H_out = (H - kH) // stride + 1
W_out = (W_ - kW) // stride + 1
out = np.zeros((B, C_out, H_out, W_out))
for i in range(H_out):
for j in range(W_out):
patch = X[:, :, i*stride:i*stride+kH, j*stride:j*stride+kW]
out[:, :, i, j] = np.tensordot(patch, W, axes=([1,2,3],[1,2,3]))
return out
# U-Net architecture
class UNet(nn.Module):
def __init__(self, in_ch=1, out_ch=1):
super().__init__()
self.enc1 = self._block(1, 64)
self.enc2 = self._block(64, 128)
self.enc3 = self._block(128, 256)
self.bottleneck = self._block(256, 512)
self.dec3 = self._block(512+256, 256) # skip connection!
self.dec2 = self._block(256+128, 128)
self.dec1 = self._block(128+64, 64)
self.final = nn.Conv2d(64, out_ch, 1)
self.pool = nn.MaxPool2d(2)
self.up = nn.Upsample(scale_factor=2)
def forward(self, x):
e1 = self.enc1(x); e2 = self.enc2(self.pool(e1))
e3 = self.enc3(self.pool(e2))
b = self.bottleneck(self.pool(e3))
d3 = self.dec3(torch.cat([self.up(b), e3], 1))
d2 = self.dec2(torch.cat([self.up(d3), e2], 1))
d1 = self.dec1(torch.cat([self.up(d2), e1], 1))
return torch.sigmoid(self.final(d1))
# Dice + Focal loss
def dice_focal_loss(pred, target, alpha=0.5, gamma=2.0):
# Dice loss: penalizes low overlap
smooth = 1e-5
intersection = (pred * target).sum()
dice = 1 - (2*intersection + smooth) / (pred.sum() + target.sum() + smooth)
# Focal loss: focuses on hard examples
bce = F.binary_cross_entropy(pred, target, reduction='none')
focal = ((1-pred)**gamma * target + pred**gamma * (1-target)) * bce
return alpha * dice + (1-alpha) * focal.mean()
# Grad-CAM from scratch
def grad_cam(model, image, target_layer):
"""Compute which regions drove the model's prediction"""
activations, gradients = {}, {}
def save_activation(module, inp, out): activations['value'] = out
def save_gradient(module, inp, out): gradients['value'] = out[0]
target_layer.register_forward_hook(save_activation)
target_layer.register_full_backward_hook(save_gradient)
output = model(image)
output.backward()
weights = gradients['value'].mean(dim=[2,3], keepdim=True) # GAP of gradients
cam = F.relu((weights * activations['value']).sum(dim=1))
cam = F.interpolate(cam.unsqueeze(1), size=image.shape[-2:])
return cam / cam.max() # Normalize [0,1]
Tech Stack
NumPy (conv2d backprop) PyTorch U-Net from scratch CheXpert dataset Dice + Focal loss Grad-CAM Temperature scaling
How Industry Evaluates This
| Metric | Target | Metric | Target |
|---|---|---|---|
| Dice coefficient | >0.85 on held-out test set | Sensitivity | >92% (missing tumors is catastrophic) |
| Calibration ECE | Expected Calibration Error <0.05 | Inference time | <200ms per image on GPU |
π Project 5: Fine-Tune a Small LLM on CBSE Curriculum with RLHF
Train a 125M parameter model that generates pedagogically correct answers for Indian students.
The Real Industry Problem
General-purpose LLMs like GPT-4 answer CBSE questions poorly β they use US curriculum language, ignore NCERT marking schemes, and don't know concepts like "value-based questions" or India's 3-hour board exam format. An Indian-curriculum-specific LLM that generates correct, appropriately-leveled, Hinglish-friendly explanations is a defensible product moat for EduArtha.
What You Must Build from Scratch
- Build a GPT-2 style transformer from scratch in PyTorch. Implement: token embedding, positional encoding, multi-head self-attention (manual QKV matrices), layer norm, feed-forward block, causal masking. 6 layers, 125M params.
- Pre-train on CBSE/NCERT corpus. Scrape NCERT PDFs (Class 6-12), past year papers, CBSE sample papers. Build a domain-specific tokenizer. Pre-train with next-token prediction. Log perplexity on a held-out set.
- Supervised fine-tuning (SFT) on question-answer pairs. Create 5,000 (question, ideal_answer) pairs with teacher annotations. Fine-tune your pre-trained model on these. Implement LoRA from scratch β modify only low-rank weight updates.
- Train a reward model β implement from scratch. Collect human preference data: show teachers two model answers, ask which is better pedagogically. Train a reward model (same architecture + scalar head) on these preferences using Bradley-Terry model.
- RLHF with PPO β implement the training loop. Use the reward model to fine-tune your SFT model using Proximal Policy Optimization. Implement the clipped surrogate objective. This is exactly how ChatGPT was trained β at small scale.
Python
# Step 1: GPT-2 from scratch β 125M params
class TransformerBlock(nn.Module):
def __init__(self, d_model=768, n_head=12, d_ff=3072):
super().__init__()
self.ln1 = nn.LayerNorm(d_model)
self.attn = nn.MultiheadAttention(d_model, n_head, batch_first=True)
self.ln2 = nn.LayerNorm(d_model)
self.ff = nn.Sequential(nn.Linear(d_model, d_ff), nn.GELU(),
nn.Linear(d_ff, d_model))
def forward(self, x, mask=None):
x = x + self.attn(self.ln1(x), self.ln1(x), self.ln1(x), attn_mask=mask)[0]
x = x + self.ff(self.ln2(x))
return x
# Step 4: Reward Model with Bradley-Terry
class RewardModel(nn.Module):
def __init__(self, base_model):
super().__init__()
self.backbone = base_model
self.reward_head = nn.Linear(768, 1) # Scalar reward
def forward(self, input_ids):
hidden = self.backbone(input_ids)
return self.reward_head(hidden[:, -1]) # Last token's reward
def reward_loss(reward_chosen, reward_rejected):
"""Bradley-Terry model: P(chosen > rejected) = Ο(r_c - r_r)"""
return -torch.log(torch.sigmoid(reward_chosen - reward_rejected)).mean()
# Step 5: PPO training loop
def ppo_step(model, ref_model, reward_model, prompts, beta=0.1, clip_eps=0.2):
# Generate responses
responses = model.generate(prompts, max_length=256)
# Get rewards
rewards = reward_model(responses)
# KL divergence penalty (prevent reward hacking)
log_probs = model.log_prob(responses)
ref_log_probs = ref_model.log_prob(responses)
kl_penalty = beta * (log_probs - ref_log_probs)
adjusted_rewards = rewards - kl_penalty
# PPO clipped surrogate objective
ratio = torch.exp(log_probs - old_log_probs)
clipped = torch.clamp(ratio, 1-clip_eps, 1+clip_eps)
loss = -torch.min(ratio * adjusted_rewards, clipped * adjusted_rewards).mean()
return loss
Tech Stack
PyTorch GPT-2 from scratch LoRA implementation NCERT corpus Bradley-Terry reward model PPO from scratch Weights & Biases
How Industry Evaluates This
| Metric | Target | Metric | Target |
|---|---|---|---|
| CBSE answer quality | Teacher rating >4.2/5 on 100 test questions | Curriculum accuracy | NCERT factual correctness >91% |
| RLHF preference rate | RLHF model preferred over SFT model in >70% of comparisons | Perplexity | <45 on CBSE hold-out test set |
Exercises
Exercise 1.1: Extend micrograd to support division, power, and tanh
Division: a/b = a * b^(-1). Power: d(a^n)/da = n * a^(n-1). Tanh: d(tanh(x))/dx = 1 - tanh(x)Β². Implement each as a method on Value class with proper backward functions. Test by comparing gradients with PyTorch's autograd on the same computation.
Exercise 1.2: Pick one industry project and implement it end-to-end
Recommended order: (1) Question Difficulty Classifier (most accessible). (2) Medical Segmentation (requires GPU). (3) Nuclear PINN (requires physics knowledge). (4) Options Volatility (requires finance knowledge). (5) CBSE LLM with RLHF (most ambitious). Spend 2-4 weeks on each. Document everything in a GitHub repo with README.
Chapter Summary
- Build micrograd and nanoGPT to understand the fundamentals from scratch
- 5 Industry Projects: Question classifier, options vol surface, nuclear PINN, medical segmentation, CBSE LLM with RLHF
- Each project includes the real industry problem, step-by-step build guide, tech stack, and evaluation metrics
- No pre-trained shortcuts β build from raw math to deployed model