Bootcamp
A 60-minute PyTorch bootcamp that takes one tabular task from raw CSV to a trained, evaluated, saved model — the smallest reproducible loop most projects need.
A 60-minute PyTorch bootcamp
EXAMPLE
# ===== Objectives =====
# 1. Load a tabular dataset into a Dataset / DataLoader
# 2. Build a small MLP
# 3. Train with mixed precision + early stopping
# 4. Evaluate on the test split
# 5. Save and reload the model
# ===== 0-5 min: scope + data =====
# Pick a small CSV (titanic, churn, MNIST). Decide the target and the metric
# (accuracy, F1, AUC) BEFORE training.
# ===== 5-15 min: dataset + loader =====
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader, random_split
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
df = pd.read_csv('churn.csv')
y = df.pop('churned').values
X = df.values.astype('float32')
# Scale here — easier than fitting inside the loop
sc = StandardScaler(); X = sc.fit_transform(X)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, stratify=y, test_size=0.2, random_state=42)
class TabDataset(Dataset):
def __init__(self, X, y):
self.X = torch.from_numpy(X).float()
self.y = torch.from_numpy(y).long()
def __len__(self): return len(self.X)
def __getitem__(self, i): return self.X[i], self.y[i]
train_loader = DataLoader(TabDataset(X_tr, y_tr), batch_size=128, shuffle=True,
num_workers=2, pin_memory=True, persistent_workers=True)
test_loader = DataLoader(TabDataset(X_te, y_te), batch_size=256, shuffle=False,
num_workers=2, pin_memory=True, persistent_workers=True)
# ===== 15-25 min: model =====
class MLP(nn.Module):
def __init__(self, in_dim, hidden=64, out_dim=2):
super().__init__()
self.net = nn.Sequential(
nn.Linear(in_dim, hidden), nn.ReLU(), nn.Dropout(0.2),
nn.Linear(hidden, hidden), nn.ReLU(), nn.Dropout(0.2),
nn.Linear(hidden, out_dim),
)
def forward(self, x): return self.net(x)
device = 'cuda' if torch.cuda.is_available() else 'cpu'
torch.manual_seed(42)
model = MLP(X.shape[1]).to(device)
opt = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=1e-2)
sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=20)
scaler = torch.amp.GradScaler('cuda', enabled=device == 'cuda')
# ===== 25-45 min: train loop with mixed precision + early stop =====
best_val = -1.0
patience = 5
no_improve = 0
for epoch in range(50):
# train
model.train()
for xb, yb in train_loader:
xb, yb = xb.to(device, non_blocking=True), yb.to(device, non_blocking=True)
opt.zero_grad(set_to_none=True)
with torch.amp.autocast(device_type=device, dtype=torch.float16, enabled=device == 'cuda'):
logits = model(xb)
loss = F.cross_entropy(logits, yb)
scaler.scale(loss).backward()
scaler.unscale_(opt)
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
scaler.step(opt)
scaler.update()
sched.step()
# eval
model.eval()
correct = 0; total = 0
with torch.inference_mode():
for xb, yb in test_loader:
xb, yb = xb.to(device), yb.to(device)
logits = model(xb)
correct += (logits.argmax(-1) == yb).sum().item()
total += yb.size(0)
acc = correct / total
print(f'epoch {epoch:02d} test acc {acc:.4f}')
if acc > best_val:
best_val = acc
torch.save(model.state_dict(), 'best.pt')
no_improve = 0
else:
no_improve += 1
if no_improve >= patience: break
# ===== 45-55 min: load best + final eval =====
model.load_state_dict(torch.load('best.pt', map_location=device))
model.eval()
with torch.inference_mode():
preds, labels = [], []
for xb, yb in test_loader:
xb, yb = xb.to(device), yb.to(device)
preds.append(model(xb).argmax(-1).cpu()); labels.append(yb.cpu())
preds = torch.cat(preds); labels = torch.cat(labels)
print('best test acc:', (preds == labels).float().mean().item())
# ===== 55-60 min: save + serve sketch =====
torch.save({'model': model.state_dict(), 'scaler': sc}, 'churn.pt')
# Inference service:
# ck = torch.load('churn.pt', map_location='cpu')
# model.load_state_dict(ck['model']); sc = ck['scaler']
# ...
# ===== Post-bootcamp checklist =====
# - Loss + accuracy reported on a held-out split
# - Best checkpoint saved (not just the last epoch)
# - Mixed precision on if GPU supports it
# - Gradient clipping + cosine schedule
# - Reproducibility seed set
# - Inference path runs at < 50ms latency on CPU
# ===== Pitfalls =====
# - .item() inside the train loop (forces CPU↔GPU sync, kills throughput)
# - missing model.eval() / model.train() at the right moments
# - calling backward twice without zero_grad
# - learning rate too high; convergence diverges -> NaN
# - skipping the gradient clip on transformers / RNNs -> exploding gradients
Why it matters
Mixed precision + GradScaler + grad clipping + cosine schedule is the four-line "make training fast and stable" combo. Apply it once on a small MLP and it scales unchanged to transformers — only the model code changes. The training loop is a template you copy from project to project.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Discussion
Loading…