PyTorch 101

Shan-Hung Wu & DataLab
Fall 2026

Why PyTorch?

PyTorch is an open source framework for machine learning. Its tensor operations, automatic differentiation, and Python-first model definitions support both research and practical applications. PyTorch has several benefits:

  1. Easy model building with torch.nn
  2. CPU and GPU execution using the same model code
  3. Flexible experimentation with eager execution and autograd

We are going to use PyTorch as our framework in the following lectures. In this lab, you will learn how to install PyTorch and implement a classical deep learning algorithm.

Environment setup

Software requirements

This lab requires a 64-bit Python environment with PyTorch, torchvision, NumPy, Matplotlib, and a Jupyter interface. An NVIDIA GPU is optional; the examples can also run on CPU. The first MNIST download requires internet access.

GPU support

To use an NVIDIA GPU, install a compatible NVIDIA driver and a CUDA-enabled PyTorch build. Prebuilt PyTorch wheels provide the CUDA runtime and cuDNN libraries needed for this lab; separate CUDA Toolkit or cuDNN installations are not required.

Select your operating system and compute platform on the official installation page. After installation, use torch.cuda.is_available() to check GPU availability, as shown in the PyTorch installation check below.

In Jupyter, prefix a shell command with ! to execute it from a notebook cell. The following commands provide additional GPU diagnostics.

Check the NVIDIA driver and GPU status

Use !nvidia-smi to check the NVIDIA driver, visible GPUs, memory usage, and GPU utilization. This diagnostic requires an NVIDIA driver; on a CPU-only machine, the command may be unavailable and can be skipped.

Check the CUDA Toolkit compiler

Use !nvcc --version to check the installed CUDA Toolkit compiler version. This can differ from the CUDA runtime bundled with PyTorch. If the CUDA Toolkit is not installed, the command may be unavailable. A missing nvcc does not mean that PyTorch cannot use the GPU; prebuilt PyTorch wheels do not require it for this lab.

Tested environment

This notebook was successfully executed in the following 64-bit Conda environment:

These are the tested versions, not a list of every supported version.

Install PyTorch

Install PyTorch and torchvision by following the instructions on the official PyTorch installation page. Select the appropriate operating system and compute platform (CPU or CUDA), then run the installation command provided there inside your Python environment.

Install the other main lab packages with:

python -m pip install numpy matplotlib jupyterlab

The tested versions are listed in the Tested environment section above. If your lab environment is already prepared, skip installation.

Using a virtual environment is recommended. In Jupyter or your IDE, select the Python kernel for the environment where PyTorch is installed.

Run the following code to check the PyTorch installation and CUDA availability:

Google Colab

Google Colab provides a hosted Jupyter environment. Upload this notebook and select a GPU runtime if one is available. Check torch.__version__ and torchvision.__version__ before running, and restart the runtime after changing packages.

GPU availability, hardware, usage limits, and subscription features vary over time; do not assume a particular GPU or a fixed session duration. Save your notebook and outputs before a session ends. Local execution gives you control over the environment.

See Colab and the PyTorch beginner tutorial.

PyTorch 2 quickstart

PyTorch supplies tensor operations, automatic differentiation, neural network layers, and optimizers through a Python API. Models execute eagerly by default;

In this lab we will:

GPU selection and memory allocation

PyTorch allocates CUDA memory on demand and keeps unused allocations in a cache; there is no memory-growth switch to enable. We use only cuda:0 when available, otherwise CPU. To hide other GPUs from the process entirely, set CUDA_VISIBLE_DEVICES=0 before starting the Python kernel.

An optional torch.cuda.set_per_process_memory_fraction limit applies to the caching allocator, rather than guaranteeing a cap on every allocation in the process. torch.cuda.empty_cache() releases unused cached memory, not live tensors.

Load datasets via torchvision.datasets

torchvision.datasets contains image datasets such as MNIST, CIFAR10, CIFAR100, and FashionMNIST. Text and tabular datasets can instead be wrapped in a custom torch.utils.data.Dataset; torchvision is specifically for computer vision.

We use MNIST: 60,000 training and 10,000 test grayscale digit images, each $28\times28$ pixels, initially stored as integers from 0 to 255.

Load and prepare the MNIST dataset. Convert the samples from integers to floating-point numbers:

Build model via Sequential API

When to use?

nn.Sequential fits a plain stack of layers in which each layer passes its output to the next. We retain the original architecture: Flatten → Linear(128) → ReLU → Dropout(0.2) → Linear(10). The final layer returns logits.

For each example, the model returns a vector of unnormalized scores called logits, one for each class. Softmax converts these scores into class probabilities. In multiclass classification, an individual logit is not the log-odds of that class versus all other classes; the difference between two logits equals the log ratio of their softmax probabilities: $z_i-z_j=\log(p_i/p_j)$.

The torch.softmax function converts logits to class probabilities along the last dimension:

Keep the final layer as logits when using nn.CrossEntropyLoss. This loss combines log-softmax with negative log likelihood in a numerically stable way. Adding softmax before this loss would change the calculation.

The loss takes logits first, integer labels second. Labels use torch.long; the default reduction averages over the batch (reduction="none" gives per-example losses).

This loss is the negative log probability of the true class. For a model that assigns roughly $1/10$ to every class, the loss is near $-\log(1/10)\approx2.3$.

print(model) displays the registered layers. model.parameters() provides the parameters used by the optimizer and lets us count trainable parameters:

Training with model.train() and autograd

model.train() selects training behavior, including enabling Dropout. It does not train the model by itself. For each batch, clear previous gradients, compute logits and loss, call loss.backward(), then update parameters with optimizer.step().

Train for five epochs with batch size 32. TensorDataset pairs images with labels; DataLoader shuffles and batches those pairs. Accumulate loss and accuracy for each epoch, and keep a history of the results.

Evaluation with model.eval() and torch.no_grad()

model.eval() selects evaluation behavior, disabling Dropout in this model. It does not calculate scores or disable gradient recording by itself. torch.no_grad() disables gradient recording while we calculate test logits, loss, and accuracy. Weight each batch's mean loss by its size so a smaller final batch contributes correctly to the overall mean.

This architecture typically reaches roughly 97–98% MNIST test accuracy. The exact result depends on initialization, shuffling, hardware, and training.

Build model via Model Subclassing

When to use?

Subclassing nn.Module gives full control over the forward pass. This is useful for custom architectures, dynamic control flow, and research experiments. Unlike nn.Sequential, which connects layers in order, a custom module defines its own forward method and can express branches, shared layers, or multiple inputs and outputs.

We keep the same network architecture as the Sequential example and organize the training/evaluation operations learned above into reusable train_step and test_step functions with metric accumulation.

Use TensorDataset and DataLoader to batch and shuffle the dataset:

Build the PyTorch model by subclassing nn.Module and defining forward:

Choose an optimizer and loss function for training.

Track loss, the number of correct predictions, and the number of samples using ordinary Python numbers. Add each batch's loss multiplied by its size, then divide by the total sample count at the end of the epoch. Accuracy is the total number of correct predictions divided by the total sample count. This also handles a smaller final batch correctly; no custom metric class is needed.

The earlier training loop used PyTorch autograd to compute gradients with loss.backward(), then updated parameters with optimizer.step(). Each parameter's .grad stores its derivative:

$$\frac{\partial L}{\partial W_{i,j}}$$

Now organize those same operations into a reusable train_step function, and evaluation into a test_step function. These are teaching helpers we define here, not built-in model methods. Each step returns the batch's summed loss, number of correct predictions, and sample count; the epoch loop accumulates those values.

See Autograd basics.

The next epoch loop resets the scalar totals, calls these reusable steps, and adds up their results. Divide the loss and correct-prediction totals by the sample count to report the epoch's mean loss and accuracy.

These steps execute eagerly. The next optional section introduces the concept of model compilation; it does not add another training implementation.

Sequential API and Model Subclassing

Style Composition Best fit
nn.Sequential Ordered modules with an automatic forward pass Simple stacks of layers
Subclassing nn.Module Layers registered in __init__, operations defined in forward Custom architectures, branches, and dynamic behavior

These approaches interoperate: a custom module can contain a Sequential module. Both models above have the same layer sizes, dropout rate, and 101,770 trainable parameters. Their training and evaluation use the same PyTorch operations.

torch.nn.functional supplies operations such as F.relu that can be used inside forward; it is not a separate model-building API.

Optional: Introduction to torch.compile

PyTorch normally executes tensor operations eagerly. torch.compile can capture the operations in a model and pass its computation graph to an optimizing backend, which may combine operations and reduce execution overhead. This can improve training or inference speed without requiring a different model architecture.

The first execution has compilation overhead. Actual performance depends on the model, batch size, hardware, and backend, so compilation does not guarantee a speedup. Small models can be slower, and backend/compiler support must be checked for the execution environment.

torch.compile is an optional performance tool, not a requirement for model training. This introductory lab covers the concept only; implementation and fair performance measurement are left to a later, advanced lesson.

See torch.compile and the end-to-end model compilation tutorial.

Customize gradient flow with torch.autograd.Function

Subclass torch.autograd.Function and define static forward and backward methods to provide a custom derivative. Invoke the operation using .apply. This provides control over the backward computation, for example to improve numerical stability.

Consider the softplus function used in likelihood calculations: $$y=\log_e(1+e^x)$$

The derivative is: $$\frac{dy}{dx}=\frac{e^x}{1+e^x}=1-\frac{1}{1+e^x}$$

At $x=100$ it should be approximately 1. With float32, the naive exponential overflows: the forward value is infinity and its automatically computed gradient is NaN. We explicitly use float32 to demonstrate this behavior.

The derivative is sigmoid(x), which can be evaluated stably. We also stabilize the forward calculation with logaddexp(0, x) so neither the output nor the gradient overflows. For ordinary applications, F.softplus already supplies a stable implementation.

Reference