Back to Home
    PyTorchPyTorch 2.xIntermediate

    PyTorch

    Essential PyTorch patterns for tensor ops, autograd, model building, training loops, and deployment.

    pythondeep-learningmlaitensorsneural-networks

    Tensors

    3 topics

    Creating Tensors

    Tensors are the core data structure in PyTorch — multi-dimensional arrays that can live on CPU or GPU.

    python
    import torch
    
    # From Python list
    t = torch.tensor([[1, 2], [3, 4]], dtype=torch.float32)
    
    # Filled tensors
    zeros = torch.zeros(3, 4)       # all zeros
    ones  = torch.ones(3, 4)        # all ones
    rand  = torch.rand(3, 4)        # uniform [0, 1)
    randn = torch.randn(3, 4)       # standard normal
    
    # Like another tensor (same shape/dtype)
    like = torch.zeros_like(t)
    
    # From NumPy (shares memory on CPU)
    import numpy as np
    arr = np.array([1.0, 2.0])
    t2  = torch.from_numpy(arr)

    Use dtype=torch.float32 explicitly — PyTorch defaults to float32 for most ops.

    torch.from_numpy shares memory; modifying one changes the other.

    Tensor Operations

    Element-wise math, matrix multiplication, reshaping, and indexing.

    python
    a = torch.tensor([[1., 2.], [3., 4.]])
    b = torch.tensor([[5., 6.], [7., 8.]])
    
    # Element-wise
    a + b;  a * b;  a ** 2
    
    # Matrix multiply
    c = a @ b           # or torch.matmul(a, b)
    
    # Reshape / view
    flat = a.view(-1)       # [1, 2, 3, 4]  — shares storage
    reshaped = a.reshape(1, 4)  # safe even on non-contiguous
    
    # Indexing & slicing (same as NumPy)
    a[0]        # first row
    a[:, 1]     # second column
    a[a > 2]    # boolean mask
    
    # Reduce
    a.sum();  a.mean();  a.max()

    view() requires contiguous memory; reshape() handles non-contiguous tensors safely.

    In-place ops end with _: a.add_(b) modifies a directly — breaks autograd graphs.

    Moving to GPU

    Tensors and models must be on the same device. Always check availability before assuming CUDA.

    python
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    
    t = torch.randn(3, 3).to(device)
    
    # Or pass device at creation
    t2 = torch.ones(3, 3, device=device)
    
    # Move back to CPU for NumPy conversion
    arr = t.cpu().numpy()

    Apple Silicon: use device='mps' for GPU acceleration on Mac.

    Call .detach() before .numpy() if the tensor has requires_grad=True.

    Autograd

    1 topic

    Gradients & backward()

    PyTorch tracks operations on tensors with requires_grad=True to build a computation graph, then computes gradients via backward().

    python
    x = torch.tensor(3.0, requires_grad=True)
    y = x ** 2 + 2 * x + 1   # y = (x+1)^2
    
    y.backward()              # compute dy/dx
    print(x.grad)             # tensor(8.) — dy/dx at x=3 is 2x+2=8
    
    # Accumulates! Zero before each step
    x.grad.zero_()
    
    # Stop tracking (inference / data preprocessing)
    with torch.no_grad():
        val = x ** 2           # no graph built

    Gradients accumulate by default — always call optimizer.zero_grad() before backward().

    Use torch.no_grad() during validation to save memory and speed up inference.

    Building Models

    2 topics

    nn.Module

    Subclass nn.Module to define any model. Layers are declared in init and the forward pass in forward().

    python
    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(784, 256, 10)
    print(model)                  # shows layer tree
    print(sum(p.numel() for p in model.parameters()))  # param count

    Never call forward() directly — call model(x), which runs hooks + forward.

    nn.Sequential is fine for simple stacks; use a custom forward for skip connections or branching.

    Common Layers

    Reference for the most-used layer types.

    python
    nn.Linear(in, out)              # fully-connected
    nn.Conv2d(in_ch, out_ch, kernel) # 2D convolution
    nn.MaxPool2d(kernel_size)        # spatial downsampling
    nn.BatchNorm2d(num_features)     # batch normalization
    nn.Dropout(p=0.5)               # regularization
    nn.Embedding(vocab, dim)         # lookup table for NLP
    nn.LSTM(input, hidden, layers)   # recurrent
    nn.MultiheadAttention(embed, heads)  # transformer block

    Training Loop

    3 topics

    Standard Training Loop

    The canonical PyTorch training loop: forward pass → loss → backward → optimizer step.

    python
    import torch.optim as optim
    
    model = MLP(784, 256, 10).to(device)
    optimizer = optim.Adam(model.parameters(), lr=1e-3)
    criterion = nn.CrossEntropyLoss()
    
    for epoch in range(num_epochs):
        model.train()                    # enables dropout/batchnorm
        for X, y in train_loader:
            X, y = X.to(device), y.to(device)
    
            optimizer.zero_grad()        # clear accumulated grads
            logits = model(X)            # forward
            loss = criterion(logits, y)  # compute loss
            loss.backward()              # compute gradients
            optimizer.step()             # update weights
    
        print(f'Epoch {epoch}: loss={loss.item():.4f}')

    model.train() and model.eval() toggle dropout/BatchNorm behavior — always switch modes.

    loss.item() extracts a Python float; avoids accumulating the whole graph in memory.

    Validation Loop

    Run on held-out data after each epoch with gradients disabled.

    python
    model.eval()
    correct = total = 0
    
    with torch.no_grad():
        for X, y in val_loader:
            X, y = X.to(device), y.to(device)
            logits = model(X)
            preds = logits.argmax(dim=1)
            correct += (preds == y).sum().item()
            total   += y.size(0)
    
    acc = correct / total
    print(f'Val accuracy: {acc:.4f}')

    torch.no_grad() cuts memory roughly in half during inference — always use it in eval.

    DataLoader & Dataset

    Wrap any dataset in DataLoader for automatic batching, shuffling, and parallel loading.

    python
    from torch.utils.data import Dataset, DataLoader
    
    class MyDataset(Dataset):
        def __init__(self, X, y):
            self.X = torch.tensor(X, dtype=torch.float32)
            self.y = torch.tensor(y, dtype=torch.long)
    
        def __len__(self):
            return len(self.X)
    
        def __getitem__(self, idx):
            return self.X[idx], self.y[idx]
    
    train_loader = DataLoader(
        MyDataset(X_train, y_train),
        batch_size=64,
        shuffle=True,
        num_workers=4,     # parallel loading
        pin_memory=True,   # faster CPU→GPU transfer
    )

    pin_memory=True + non_blocking=True on .to(device) overlaps data transfer with compute.

    num_workers > 0 can cause issues on Windows; set to 0 if you see multiprocessing errors.

    Save & Load

    1 topic

    Saving & Loading Checkpoints

    Save model weights (state_dict) rather than the full object — more portable across code changes.

    python
    # Save
    torch.save({
        'epoch': epoch,
        'model_state': model.state_dict(),
        'optimizer_state': optimizer.state_dict(),
        'loss': loss,
    }, 'checkpoint.pt')
    
    # Load
    checkpoint = torch.load('checkpoint.pt', map_location=device)
    model.load_state_dict(checkpoint['model_state'])
    optimizer.load_state_dict(checkpoint['optimizer_state'])
    start_epoch = checkpoint['epoch']

    Always pass map_location=device when loading — avoids CUDA errors on CPU-only machines.

    For inference-only: torch.save(model.state_dict(), 'weights.pt') is enough.

    Inference & Export

    1 topic

    TorchScript & ONNX Export

    Export models for production deployment without a Python runtime.

    python
    # TorchScript — serialize the model
    scripted = torch.jit.script(model)
    scripted.save('model_scripted.pt')
    loaded = torch.jit.load('model_scripted.pt')
    
    # ONNX — interoperable with TensorRT, ONNX Runtime, CoreML
    dummy = torch.randn(1, 784, device=device)
    torch.onnx.export(
        model, dummy, 'model.onnx',
        input_names=['input'],
        output_names=['output'],
        dynamic_axes={'input': {0: 'batch'}},  # variable batch size
        opset_version=17,
    )

    Prefer TorchScript for PyTorch-native serving (TorchServe, C++ LibTorch).

    ONNX is better for cross-framework deployment (e.g. running in ONNX Runtime on edge devices).

    Related Articles

    Background reading and deeper explanations for this sheet.

    Cost Optimization in Agentic Systems: The Most Underrated Lever for Reliable AI at Scale

    Most teams building agentic systems obsess over accuracy and latency, but ignore the fastest path to sustainable scale: cost optimization. In this guide, you’ll learn practical cost models, real architecture patterns, and implementation tactics to cut spend without sacrificing quality or autonomy.

    keyword overlap

    AI Product Pricing Strategies: How to Monetize for Growth, Margin, and Customer Trust

    Pricing an AI product is not just a finance decision—it shapes adoption, retention, margins, and user trust. In this practical guide, you’ll learn how to choose the right pricing model, align price with value and costs, and avoid common mistakes using real-world examples and implementation frameworks.

    keyword overlap

    FastAPI Interview Questions: Practical Cheat Sheet for Developers

    Preparing for FastAPI interviews can feel overwhelming if you only memorize definitions. This practical cheat sheet helps you answer common FastAPI interview questions with confidence using real-world examples, production-ready patterns, and beginner-friendly explanations with depth.

    keyword overlap

    GenAI SaaS Architecture: A Practical Blueprint for Building, Scaling, and Securing AI Products

    Designing a SaaS product on top of GenAI is more than calling an LLM API—it requires thoughtful architecture across product, data, safety, and operations. This practical guide walks you through a beginner-friendly yet deep system blueprint, with real-world patterns, code snippets, and deployment strategies you can use immediately.

    keyword overlap