AI Engineering
PyTorch, Explained: Tensors to Training Loops
If you're building anything in agentic AI, fine-tuning, or applied ML, PyTorch is usually the layer right under the tools you already use — Hugging Face transformers, LangChain's local model backends, most research code you'll find on GitHub. This post is a from-scratch walkthrough: what PyTorch actually is, the four ideas that make it click (tensors, autograd, nn.Module, the training loop), and a working example you can run end to end.
What PyTorch actually is
PyTorch is an open-source deep learning library, originally built at Meta and now developed under the PyTorch Foundation (part of the Linux Foundation). Two things make it different from plain NumPy:
- Tensors run on GPUs. Same API as a NumPy array, but operations can execute on CUDA, ROCm, or Apple's MPS backend instead of the CPU.
- Autograd. Every operation on a tensor is tracked, so PyTorch can compute gradients automatically — which is what makes training a neural network with backpropagation practical.
It uses eager execution by default: code runs line by line, like normal Python, rather than requiring you to build a static graph first. That's the main reason it became the default for research and, increasingly, production.
1. Installing it
Head to the official installer picker rather than copying a command from a blog post (mine included) — it changes with your OS, package manager, and CUDA version:
👉 pytorch.org/get-started/locally
As of mid-2026 the current stable line is the 2.12.x series, supporting Python 3.10–3.14. A typical CPU-only install:
pip install torch torchvision torchaudioVerify it:
import torch
print(torch.__version__)
print(torch.cuda.is_available()) # True if you have a CUDA GPU set up2. Tensors — the only data structure you need
A tensor is an n-dimensional array with a dtype and a device (CPU or GPU).
import torch
x = torch.tensor([[1.0, 2.0], [3.0, 4.0]])
y = torch.ones_like(x)
z = x + y # elementwise
m = x @ y # matrix multiply
r = x.reshape(4) # reshape, no copy if possible
print(x.shape, x.dtype, x.device)Move it to a GPU (if available) with one call — this is the whole story of why PyTorch scales:
device = "cuda" if torch.cuda.is_available() else "cpu"
x = x.to(device)Full tensor operation reference: docs.pytorch.org/docs/stable/torch.html
3. Autograd — gradients without doing calculus by hand
Set requires_grad=True on a tensor and PyTorch tracks every operation applied to it, building a computation graph on the fly. Call .backward() and it walks that graph in reverse, filling in .grad for every tensor that needed one.
w = torch.tensor(2.0, requires_grad=True)
b = torch.tensor(1.0, requires_grad=True)
x = torch.tensor(3.0)
y = w * x + b # y = 7.0
y.backward() # computes dy/dw and dy/db
print(w.grad) # tensor(3.) -> dy/dw = x
print(b.grad) # tensor(1.) -> dy/db = 1That's the entire mechanism behind training every neural network you'll ever write in PyTorch. Deeper dive: docs.pytorch.org/docs/stable/notes/autograd.html
4. Defining a model with nn.Module
Every PyTorch model is a class with two parts: what layers it has (__init__), and how data flows through them (forward).
import torch.nn as nn
class MLP(nn.Module):
def __init__(self, in_features, hidden, out_features):
super().__init__()
self.net = nn.Sequential(
nn.Linear(in_features, hidden),
nn.ReLU(),
nn.Linear(hidden, out_features),
)
def forward(self, x):
return self.net(x)
model = MLP(in_features=20, hidden=64, out_features=2).to(device)Layer reference: docs.pytorch.org/docs/stable/nn.html
5. Data — Dataset and DataLoader
Dataset defines how to fetch one sample; DataLoader handles batching, shuffling, and parallel loading around it.
from torch.utils.data import Dataset, DataLoader
class ToyDataset(Dataset):
def __init__(self, X, y):
self.X, self.y = X, y
def __len__(self):
return len(self.X)
def __getitem__(self, idx):
return self.X[idx], self.y[idx]
loader = DataLoader(ToyDataset(X, y), batch_size=32, shuffle=True)6. The training loop
This five-step pattern — zero gradients, forward pass, compute loss, backward pass, step the optimizer — is identical whether you're training a two-layer MLP or fine-tuning a transformer.
import torch.optim as optim
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=1e-3)
for epoch in range(10):
for batch_X, batch_y in loader:
batch_X, batch_y = batch_X.to(device), batch_y.to(device)
optimizer.zero_grad()
outputs = model(batch_X)
loss = criterion(outputs, batch_y)
loss.backward()
optimizer.step()
print(f"epoch {epoch}: loss {loss.item():.4f}")Optimizer reference: docs.pytorch.org/docs/stable/optim.html
7. Saving and loading
Save the state_dict (a plain dictionary of tensor weights), not the whole object — it's portable across code versions.
torch.save(model.state_dict(), "model.pt")
model = MLP(20, 64, 2)
model.load_state_dict(torch.load("model.pt"))
model.eval() # turns off dropout/batchnorm training behaviorWhere this goes next
Once the above is second nature, the ecosystem around core PyTorch is where the leverage is:
- TorchVision — pretrained image models, datasets, transforms
- TorchText / Hugging Face
transformers— NLP and LLMs on top of PyTorch tensors - TorchServe — model serving in production
- torch.compile — JIT-compiles your model for faster training/inference with almost no code changes
- PyTorch Lightning — removes training-loop boilerplate for larger projects
Reference links
- Official site: pytorch.org
- Install picker: pytorch.org/get-started/locally
- Full docs: docs.pytorch.org/docs/stable
- Official tutorials: docs.pytorch.org/tutorials
- Source code: github.com/pytorch/pytorch
- PyPI package: pypi.org/project/torch
- PyTorch Foundation: pytorch.org (Foundation)
Next in this series: wiring a PyTorch model behind an MCP server so an agent can call it directly — closer to the agentic AI work I do day to day.
Related articles
AI Engineering
Self-Attention Explained: The Mechanism Behind Every Transformer
How self-attention lets a model decide which words matter to each other — the query/key/value mechanics, scaled dot-product attention, multi-head attention, and causal masking, with a from-scratch PyTorch implementation.
- #machine-learning
- #transformers
- #self-attention
- #deep-learning
- #python
AI Engineering
Embeddings, Explained: From a Word Corpus to Vectors That Mean Something
Start from a plain sentence, tokenize it with NLTK, and build up through every major embedding type — one-hot, TF-IDF, Word2Vec, GloVe, and modern contextual embeddings — with working code for each.
- #nlp
- #embeddings
- #nltk
- #word2vec
- #ai-engineering
AI Engineering
Reweighting, Rescaling, Smoothing, and Bagging: The Data Techniques Behind Every Model
Five terms that show up in almost every ML pipeline, explained simply and with code: word/sample reweighting, time scaling, data rescaling, smoothing, and bagging.
- #machine-learning
- #data-preprocessing
- #python
- #fundamentals