PyTorch & Deep Learning

Complete guide to building and deploying deep learning models with PyTorch

Table of Contents

PyTorch Basics

Setup & Installation
# Installation
# CPU only
pip install torch torchvision torchaudio

# CUDA 11.8
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118

# CUDA 12.1
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121

# macOS (MPS backend)
pip install torch torchvision torchaudio

# Verify installation
import torch

print(f"PyTorch version: {torch.__version__}")
print(f"CUDA available: {torch.cuda.is_available()}")
print(f"CUDA version: {torch.version.cuda}")
print(f"Device count: {torch.cuda.device_count()}")
print(f"Current device: {torch.cuda.current_device()}")
print(f"Device name: {torch.cuda.get_device_name(0)}")

# Check MPS (Apple Silicon)
print(f"MPS available: {torch.backends.mps.is_available()}")

# Set device
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Using device: {device}")

# Or with MPS support
if torch.cuda.is_available():
    device = torch.device('cuda')
elif torch.backends.mps.is_available():
    device = torch.device('mps')
else:
    device = torch.device('cpu')

# Basic imports
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import DataLoader, Dataset
import torchvision
import torchvision.transforms as transforms
import numpy as np
import matplotlib.pyplot as plt

# Set random seed for reproducibility
def set_seed(seed=42):
    torch.manual_seed(seed)
    torch.cuda.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)
    np.random.seed(seed)
    torch.backends.cudnn.deterministic = True
    torch.backends.cudnn.benchmark = False

set_seed(42)

# Memory management
# Clear GPU cache
torch.cuda.empty_cache()

# Get memory stats
print(f"Allocated: {torch.cuda.memory_allocated() / 1e9:.2f} GB")
print(f"Reserved: {torch.cuda.memory_reserved() / 1e9:.2f} GB")

# Set memory growth (like TensorFlow)
torch.cuda.set_per_process_memory_fraction(0.8, 0)
PyTorch Workflow
┌─────────────────────────────────────────────────────────┐
│              PyTorch Deep Learning Workflow              │
└─────────────────────────────────────────────────────────┘

1. Data Preparation
   ↓
   ┌─────────────────────────────────────────────┐
   │ • Load data                                  │
   │ • Create Dataset & DataLoader               │
   │ • Apply transforms                          │
   │ • Split train/val/test                      │
   └─────────────────────────────────────────────┘
   ↓
2. Model Definition
   ↓
   ┌─────────────────────────────────────────────┐
   │ • Define nn.Module                          │
   │ • Implement __init__ and forward()          │
   │ • Move model to device                      │
   └─────────────────────────────────────────────┘
   ↓
3. Loss & Optimizer
   ↓
   ┌─────────────────────────────────────────────┐
   │ • Choose loss function                      │
   │ • Choose optimizer (Adam, SGD, etc.)        │
   │ • Set learning rate & hyperparameters       │
   └─────────────────────────────────────────────┘
   ↓
4. Training Loop
   ↓
   ┌─────────────────────────────────────────────┐
   │ For each epoch:                             │
   │   For each batch:                           │
   │     1. Forward pass                         │
   │     2. Compute loss                         │
   │     3. Backward pass (compute gradients)    │
   │     4. Optimizer step (update weights)      │
   │     5. Zero gradients                       │
   └─────────────────────────────────────────────┘
   ↓
5. Validation
   ↓
   ┌─────────────────────────────────────────────┐
   │ • Set model to eval mode                    │
   │ • Disable gradient computation              │
   │ • Compute validation metrics                │
   └─────────────────────────────────────────────┘
   ↓
6. Testing & Deployment
   ↓
   ┌─────────────────────────────────────────────┐
   │ • Evaluate on test set                      │
   │ • Save model                                │
   │ • Export for deployment                     │
   └─────────────────────────────────────────────┘

Complete Example:

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset

# 1. Data
X_train = torch.randn(1000, 10)
y_train = torch.randint(0, 2, (1000,))
train_dataset = TensorDataset(X_train, y_train)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)

# 2. Model
class SimpleNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(10, 64)
        self.fc2 = nn.Linear(64, 32)
        self.fc3 = nn.Linear(32, 2)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

model = SimpleNN().to(device)

# 3. Loss & Optimizer
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# 4. Training
num_epochs = 10
for epoch in range(num_epochs):
    model.train()
    for batch_X, batch_y in train_loader:
        batch_X, batch_y = batch_X.to(device), batch_y.to(device)

        # Forward
        outputs = model(batch_X)
        loss = criterion(outputs, batch_y)

        # Backward
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

    print(f"Epoch {epoch+1}/{num_epochs}, Loss: {loss.item():.4f}")

# 5. Save model
torch.save(model.state_dict(), 'model.pth')

Tensor Operations

Tensor Creation & Manipulation
# Create tensors
# From data
x = torch.tensor([1, 2, 3, 4, 5])
x = torch.tensor([[1, 2], [3, 4]])

# From numpy
import numpy as np
arr = np.array([1, 2, 3])
x = torch.from_numpy(arr)

# Back to numpy
arr = x.numpy()

# Zeros and ones
x = torch.zeros(3, 4)
x = torch.ones(2, 3)
x = torch.zeros_like(existing_tensor)
x = torch.ones_like(existing_tensor)

# Random tensors
x = torch.rand(3, 4)  # Uniform [0, 1)
x = torch.randn(3, 4)  # Normal distribution
x = torch.randint(0, 10, (3, 4))  # Random integers

# Range
x = torch.arange(0, 10, 2)  # [0, 2, 4, 6, 8]
x = torch.linspace(0, 10, 5)  # [0, 2.5, 5, 7.5, 10]

# Identity matrix
x = torch.eye(3)

# Tensor properties
x = torch.randn(3, 4)
print(x.shape)  # torch.Size([3, 4])
print(x.size())  # torch.Size([3, 4])
print(x.dtype)  # torch.float32
print(x.device)  # cpu or cuda
print(x.requires_grad)  # False
print(x.ndim)  # 2

# Change device
x = x.to('cuda')
x = x.to(device)
x = x.cpu()
x = x.cuda()

# Change dtype
x = x.float()  # float32
x = x.double()  # float64
x = x.int()  # int32
x = x.long()  # int64
x = x.to(torch.float16)

# Reshaping
x = torch.randn(3, 4)

# Reshape
x = x.view(2, 6)
x = x.reshape(2, 6)  # More flexible

# Flatten
x = x.view(-1)  # 1D tensor
x = x.flatten()

# Squeeze/unsqueeze (remove/add dimension)
x = torch.randn(1, 3, 1, 4)
x = x.squeeze()  # Shape: [3, 4]
x = x.unsqueeze(0)  # Shape: [1, 3, 4]
x = x.unsqueeze(-1)  # Shape: [1, 3, 4, 1]

# Transpose
x = torch.randn(3, 4)
x = x.t()  # 2D transpose
x = x.transpose(0, 1)  # Swap dimensions
x = x.permute(1, 0)  # Rearrange dimensions

# Indexing & slicing
x = torch.randn(3, 4)

# Basic indexing
x[0]  # First row
x[:, 0]  # First column
x[0, 1]  # Element at (0, 1)
x[1:3]  # Rows 1 and 2

# Advanced indexing
indices = torch.tensor([0, 2])
x[indices]  # Select rows 0 and 2

# Boolean indexing
mask = x > 0
x[mask]  # Elements > 0

# Concatenation
x1 = torch.randn(2, 3)
x2 = torch.randn(2, 3)

# Concatenate along dimension
x = torch.cat([x1, x2], dim=0)  # Shape: [4, 3]
x = torch.cat([x1, x2], dim=1)  # Shape: [2, 6]

# Stack (adds new dimension)
x = torch.stack([x1, x2], dim=0)  # Shape: [2, 2, 3]

# Splitting
x = torch.randn(6, 4)

# Split into chunks
chunks = torch.chunk(x, 3, dim=0)  # 3 chunks

# Split with sizes
chunks = torch.split(x, [2, 2, 2], dim=0)

# Mathematical operations
x = torch.randn(3, 4)
y = torch.randn(3, 4)

# Element-wise
z = x + y
z = x - y
z = x * y
z = x / y
z = x ** 2

# In-place operations (end with _)
x.add_(y)
x.mul_(2)

# Matrix multiplication
a = torch.randn(3, 4)
b = torch.randn(4, 5)
c = torch.mm(a, b)  # [3, 5]
c = a @ b  # Same as mm

# Batch matrix multiplication
a = torch.randn(10, 3, 4)
b = torch.randn(10, 4, 5)
c = torch.bmm(a, b)  # [10, 3, 5]

# Dot product
a = torch.tensor([1., 2., 3.])
b = torch.tensor([4., 5., 6.])
result = torch.dot(a, b)

# Reduction operations
x = torch.randn(3, 4)

# Sum
total = x.sum()
col_sum = x.sum(dim=0)  # Sum columns
row_sum = x.sum(dim=1)  # Sum rows

# Mean
mean = x.mean()
col_mean = x.mean(dim=0)

# Max/Min
max_val = x.max()
max_val, max_idx = x.max(dim=0)
min_val = x.min()

# Argmax/Argmin
idx = x.argmax()
idx = x.argmax(dim=0)

# Standard deviation
std = x.std()

# Comparison operations
x = torch.randn(3, 4)
y = torch.randn(3, 4)

# Element-wise comparison
result = x > y
result = x == y
result = torch.eq(x, y)

# All/Any
all_positive = (x > 0).all()
any_positive = (x > 0).any()

# Advanced operations
# Clamp (clip values)
x = torch.randn(3, 4)
x = x.clamp(min=-1, max=1)

# Where (conditional)
x = torch.randn(3, 4)
y = torch.zeros_like(x)
result = torch.where(x > 0, x, y)  # Keep positive, zero negative

# Gather
x = torch.randn(3, 4)
indices = torch.tensor([[0, 1], [2, 3]])
result = torch.gather(x, 1, indices)

Autograd & Gradients

Automatic Differentiation
# Enable gradient tracking
x = torch.tensor([2.0], requires_grad=True)
y = x ** 2

# Compute gradients
y.backward()

# Access gradients
print(x.grad)  # dy/dx = 2x = 4

# Multi-variable gradients
x = torch.tensor([2.0], requires_grad=True)
y = torch.tensor([3.0], requires_grad=True)

z = x ** 2 + y ** 3
z.backward()

print(x.grad)  # dz/dx = 2x = 4
print(y.grad)  # dz/dy = 3y² = 27

# Gradient accumulation
x = torch.tensor([2.0], requires_grad=True)

for i in range(3):
    y = x ** 2
    y.backward()
    print(x.grad)  # Accumulates: 4, 8, 12

# Zero gradients
x.grad.zero_()

# No gradient context
x = torch.randn(3, 4, requires_grad=True)

# Temporarily disable gradient
with torch.no_grad():
    y = x ** 2  # No gradient tracking

# Evaluation mode (no gradient)
model.eval()
with torch.no_grad():
    predictions = model(inputs)

# Detach from computation graph
x = torch.randn(3, 4, requires_grad=True)
y = x ** 2

# Detach (stop gradient flow)
y_detached = y.detach()

# Gradient for non-scalar outputs
x = torch.randn(3, requires_grad=True)
y = x ** 2

# Need gradient argument
gradient = torch.ones_like(y)
y.backward(gradient)

# Custom backward pass
class MyFunction(torch.autograd.Function):
    @staticmethod
    def forward(ctx, x):
        ctx.save_for_backward(x)
        return x ** 2

    @staticmethod
    def backward(ctx, grad_output):
        x, = ctx.saved_tensors
        return grad_output * 2 * x

# Use custom function
x = torch.tensor([2.0], requires_grad=True)
y = MyFunction.apply(x)
y.backward()
print(x.grad)  # 4

# Gradient clipping
# Clip by value
torch.nn.utils.clip_grad_value_(model.parameters(), clip_value=1.0)

# Clip by norm
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

# Higher-order gradients
x = torch.tensor([2.0], requires_grad=True)
y = x ** 3

# First derivative
grad_y = torch.autograd.grad(y, x, create_graph=True)[0]
print(grad_y)  # 3x² = 12

# Second derivative
grad2_y = torch.autograd.grad(grad_y, x)[0]
print(grad2_y)  # 6x = 12

# Jacobian
def f(x):
    return torch.stack([x[0]**2, x[1]**3, x[0]*x[1]])

x = torch.tensor([2.0, 3.0], requires_grad=True)
y = f(x)

jacobian = torch.autograd.functional.jacobian(f, x)
print(jacobian)

# Hessian
def f(x):
    return (x ** 2).sum()

x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
hessian = torch.autograd.functional.hessian(f, x)
print(hessian)

Neural Networks

Building Neural Networks
# Basic neural network
import torch.nn as nn
import torch.nn.functional as F

class SimpleNet(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(SimpleNet, self).__init__()
        self.fc1 = nn.Linear(input_size, hidden_size)
        self.fc2 = nn.Linear(hidden_size, hidden_size)
        self.fc3 = nn.Linear(hidden_size, output_size)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

# Create model
model = SimpleNet(784, 128, 10)
print(model)

# Move to device
model = model.to(device)

# Count parameters
total_params = sum(p.numel() for p in model.parameters())
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"Total parameters: {total_params}")

# Sequential model
model = nn.Sequential(
    nn.Linear(784, 256),
    nn.ReLU(),
    nn.Dropout(0.2),
    nn.Linear(256, 128),
    nn.ReLU(),
    nn.Dropout(0.2),
    nn.Linear(128, 10)
)

# ModuleList & ModuleDict
class FlexibleNet(nn.Module):
    def __init__(self, layer_sizes):
        super().__init__()
        # ModuleList for dynamic layers
        self.layers = nn.ModuleList([
            nn.Linear(layer_sizes[i], layer_sizes[i+1])
            for i in range(len(layer_sizes) - 1)
        ])

        # ModuleDict for named layers
        self.branches = nn.ModuleDict({
            'branch1': nn.Linear(10, 20),
            'branch2': nn.Linear(10, 30)
        })

    def forward(self, x):
        for layer in self.layers:
            x = F.relu(layer(x))
        return x

# Common layers
# Linear (fully connected)
fc = nn.Linear(in_features=100, out_features=50, bias=True)

# Dropout
dropout = nn.Dropout(p=0.5)

# Batch normalization
bn = nn.BatchNorm1d(num_features=100)
bn2d = nn.BatchNorm2d(num_features=64)

# Layer normalization
ln = nn.LayerNorm(normalized_shape=100)

# Embedding
embedding = nn.Embedding(num_embeddings=10000, embedding_dim=300)

# Activation functions
# ReLU
x = F.relu(x)
relu = nn.ReLU()

# LeakyReLU
x = F.leaky_relu(x, negative_slope=0.01)

# GELU
x = F.gelu(x)

# Sigmoid
x = torch.sigmoid(x)

# Tanh
x = torch.tanh(x)

# Softmax
x = F.softmax(x, dim=1)

# LogSoftmax (more numerically stable)
x = F.log_softmax(x, dim=1)

# Custom layer
class CustomLayer(nn.Module):
    def __init__(self, in_features, out_features):
        super().__init__()
        self.weight = nn.Parameter(torch.randn(out_features, in_features))
        self.bias = nn.Parameter(torch.zeros(out_features))

    def forward(self, x):
        return F.linear(x, self.weight, self.bias)

# Residual connections
class ResidualBlock(nn.Module):
    def __init__(self, channels):
        super().__init__()
        self.conv1 = nn.Conv2d(channels, channels, 3, padding=1)
        self.bn1 = nn.BatchNorm2d(channels)
        self.conv2 = nn.Conv2d(channels, channels, 3, padding=1)
        self.bn2 = nn.BatchNorm2d(channels)

    def forward(self, x):
        residual = x
        out = F.relu(self.bn1(self.conv1(x)))
        out = self.bn2(self.conv2(out))
        out += residual  # Skip connection
        out = F.relu(out)
        return out

# Multi-input/output network
class MultiIONet(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(10, 64)
        self.fc2 = nn.Linear(20, 64)
        self.fc_out1 = nn.Linear(128, 5)
        self.fc_out2 = nn.Linear(128, 3)

    def forward(self, x1, x2):
        x1 = F.relu(self.fc1(x1))
        x2 = F.relu(self.fc2(x2))
        combined = torch.cat([x1, x2], dim=1)

        out1 = self.fc_out1(combined)
        out2 = self.fc_out2(combined)

        return out1, out2

# Initialize weights
def init_weights(m):
    if isinstance(m, nn.Linear):
        nn.init.xavier_uniform_(m.weight)
        nn.init.zeros_(m.bias)
    elif isinstance(m, nn.Conv2d):
        nn.init.kaiming_normal_(m.weight, mode='fan_out')

model.apply(init_weights)

# Freeze/unfreeze layers
# Freeze all parameters
for param in model.parameters():
    param.requires_grad = False

# Unfreeze specific layer
for param in model.fc3.parameters():
    param.requires_grad = True

# Check which parameters are trainable
for name, param in model.named_parameters():
    print(f"{name}: {param.requires_grad}")

Training Loops

Complete Training Pipeline
# Basic training loop
def train_epoch(model, dataloader, criterion, optimizer, device):
    model.train()
    running_loss = 0.0
    correct = 0
    total = 0

    for inputs, labels in dataloader:
        inputs, labels = inputs.to(device), labels.to(device)

        # Zero gradients
        optimizer.zero_grad()

        # Forward pass
        outputs = model(inputs)
        loss = criterion(outputs, labels)

        # Backward pass
        loss.backward()

        # Update weights
        optimizer.step()

        # Statistics
        running_loss += loss.item() * inputs.size(0)
        _, predicted = outputs.max(1)
        total += labels.size(0)
        correct += predicted.eq(labels).sum().item()

    epoch_loss = running_loss / total
    epoch_acc = 100. * correct / total

    return epoch_loss, epoch_acc

# Validation loop
def validate(model, dataloader, criterion, device):
    model.eval()
    running_loss = 0.0
    correct = 0
    total = 0

    with torch.no_grad():
        for inputs, labels in dataloader:
            inputs, labels = inputs.to(device), labels.to(device)

            outputs = model(inputs)
            loss = criterion(outputs, labels)

            running_loss += loss.item() * inputs.size(0)
            _, predicted = outputs.max(1)
            total += labels.size(0)
            correct += predicted.eq(labels).sum().item()

    val_loss = running_loss / total
    val_acc = 100. * correct / total

    return val_loss, val_acc

# Complete training function
def train_model(model, train_loader, val_loader, criterion, optimizer,
                num_epochs, device, scheduler=None):

    best_val_acc = 0.0
    history = {'train_loss': [], 'train_acc': [],
               'val_loss': [], 'val_acc': []}

    for epoch in range(num_epochs):
        print(f'Epoch {epoch+1}/{num_epochs}')
        print('-' * 60)

        # Train
        train_loss, train_acc = train_epoch(
            model, train_loader, criterion, optimizer, device
        )

        # Validate
        val_loss, val_acc = validate(
            model, val_loader, criterion, device
        )

        # Update learning rate
        if scheduler:
            scheduler.step(val_loss)

        # Save history
        history['train_loss'].append(train_loss)
        history['train_acc'].append(train_acc)
        history['val_loss'].append(val_loss)
        history['val_acc'].append(val_acc)

        print(f'Train Loss: {train_loss:.4f} Acc: {train_acc:.2f}%')
        print(f'Val Loss: {val_loss:.4f} Acc: {val_acc:.2f}%')

        # Save best model
        if val_acc > best_val_acc:
            best_val_acc = val_acc
            torch.save({
                'epoch': epoch,
                'model_state_dict': model.state_dict(),
                'optimizer_state_dict': optimizer.state_dict(),
                'val_acc': val_acc,
            }, 'best_model.pth')
            print(f'Saved best model with val_acc: {val_acc:.2f}%')

        print()

    return history

# Training with mixed precision
from torch.cuda.amp import autocast, GradScaler

scaler = GradScaler()

def train_with_amp(model, dataloader, criterion, optimizer, device):
    model.train()
    running_loss = 0.0

    for inputs, labels in dataloader:
        inputs, labels = inputs.to(device), labels.to(device)

        optimizer.zero_grad()

        # Mixed precision forward pass
        with autocast():
            outputs = model(inputs)
            loss = criterion(outputs, labels)

        # Scaled backward pass
        scaler.scale(loss).backward()
        scaler.step(optimizer)
        scaler.update()

        running_loss += loss.item()

    return running_loss / len(dataloader)

# Gradient accumulation
accumulation_steps = 4

optimizer.zero_grad()
for i, (inputs, labels) in enumerate(dataloader):
    inputs, labels = inputs.to(device), labels.to(device)

    outputs = model(inputs)
    loss = criterion(outputs, labels)

    # Scale loss
    loss = loss / accumulation_steps
    loss.backward()

    # Update every accumulation_steps
    if (i + 1) % accumulation_steps == 0:
        optimizer.step()
        optimizer.zero_grad()

# Early stopping
class EarlyStopping:
    def __init__(self, patience=7, min_delta=0):
        self.patience = patience
        self.min_delta = min_delta
        self.counter = 0
        self.best_loss = None
        self.early_stop = False

    def __call__(self, val_loss):
        if self.best_loss is None:
            self.best_loss = val_loss
        elif val_loss > self.best_loss - self.min_delta:
            self.counter += 1
            if self.counter >= self.patience:
                self.early_stop = True
        else:
            self.best_loss = val_loss
            self.counter = 0

# Usage
early_stopping = EarlyStopping(patience=5)

for epoch in range(num_epochs):
    train_loss = train_epoch(...)
    val_loss = validate(...)

    early_stopping(val_loss)
    if early_stopping.early_stop:
        print("Early stopping triggered")
        break

# Learning rate finder
def find_lr(model, dataloader, criterion, optimizer, device,
            start_lr=1e-7, end_lr=10, num_iter=100):

    model.train()
    lr_scheduler = torch.optim.lr_scheduler.ExponentialLR(
        optimizer, gamma=(end_lr / start_lr) ** (1 / num_iter)
    )

    losses = []
    lrs = []

    for i, (inputs, labels) in enumerate(dataloader):
        if i >= num_iter:
            break

        inputs, labels = inputs.to(device), labels.to(device)

        optimizer.zero_grad()
        outputs = model(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        lrs.append(optimizer.param_groups[0]['lr'])
        losses.append(loss.item())

        lr_scheduler.step()

    # Plot
    plt.plot(lrs, losses)
    plt.xscale('log')
    plt.xlabel('Learning Rate')
    plt.ylabel('Loss')
    plt.show()

    return lrs, losses

Resources & Learning Path

Learning Progression
Phase 1: PyTorch Fundamentals (2-3 weeks)
□ Tensor operations
□ Autograd and gradients
□ Basic neural networks
□ Training loops
□ Data loading

Phase 2: Deep Learning Basics (3-4 weeks)
□ CNNs for computer vision
□ RNNs and LSTMs
□ Optimization techniques
□ Regularization
□ Transfer learning

Phase 3: Advanced Architectures (4-6 weeks)
□ ResNets and modern CNNs
□ Transformers and attention
□ GANs
□ Advanced optimization
□ Model ensembling

Phase 4: Production ML (Ongoing)
□ Model deployment
□ Distributed training
□ MLOps practices
□ Performance optimization
□ Monitoring and maintenance
Related Comprehensive Sheets
Python Ecosystem
→ Python Advanced (performance, async)
→ NumPy & Scientific Computing
→ Data Science with Pandas
→ Visualization with Matplotlib

Machine Learning
→ Scikit-Learn ML
→ TensorFlow & Keras
→ Model Deployment
→ MLOps Practices

Specialized Topics
→ Computer Vision
→ Natural Language Processing
→ Reinforcement Learning
→ Time Series Forecasting

Infrastructure
→ GPU Computing
→ Distributed Training
→ Docker & Kubernetes
→ Cloud ML Platforms
Pro Tips Summary
Performance
✓ Use GPU when available
✓ Enable mixed precision training
✓ Use DataLoader with num_workers
✓ Profile your code
✓ Batch operations when possible
✓ Use torch.compile() (PyTorch 2.0+)

Training
✓ Monitor gradients
✓ Use learning rate scheduling
✓ Implement early stopping
✓ Save checkpoints regularly
✓ Validate frequently
✓ Track metrics with TensorBoard

Debugging
✓ Check tensor shapes
✓ Verify gradient flow
✓ Test on small dataset first
✓ Use torch.autograd.detect_anomaly()
✓ Check for NaN/Inf values
✓ Visualize intermediate outputs

Best Practices
✓ Set random seeds
✓ Use version control
✓ Document hyperparameters
✓ Split data properly
✓ Normalize inputs
✓ Use proper evaluation metrics
✓ Test on held-out data