Skip to main content
Problem Statement
  • Develop a neural network using PyTorch to recognize handwritten digits (0–9) from MNIST images.
  • Train the model using labeled images so it can learn patterns and classify unseen handwritten digits correctly.
  • Evaluate the model using test data and measure its performance using classification accuracy.

1. Importing PyTorch

import torch

PyTorch is the main deep-learning library we’re using. It provides:
  • Tensors
  • GPU/CPU computation
  • Automatic differentiation
  • Neural-network operations
  • Model training utilities
Think of a tensor as a multidimensional array. For example:
is a 1-dimensional tensor. You can also have:
which is a 2D tensor.

torch.nn

torch.nn contains tools for creating neural networks. For example:
Your code uses:
to create fully connected layers. We use nn as a shorter name for torch.nn.

torch.optim

This contains optimization algorithms. Your code uses:
Adam is responsible for updating the neural-network weights after calculating the error.

DataLoader

DataLoader takes a dataset and gives it to the neural network in batches. Instead of giving 60,000 images to the network at once:
we can give:
This is called mini-batch training.

datasets and transforms

torchvision contains datasets and computer-vision utilities. You’re using:
to download/load the MNIST handwritten-digit dataset. And:
to convert images into PyTorch tensors.

2. Selecting CPU or GPU

This determines where your neural network will run. The logic is:
So:
checks whether PyTorch can use an NVIDIA CUDA GPU. If yes:
Otherwise:

Why GPU?

Neural networks perform lots of mathematical operations. A GPU can perform many operations in parallel and is generally much faster for deep learning. For MNIST, however, a CPU is usually sufficient because the network is very small.
This might print:
or:

3. Image transformation

MNIST images are originally image data. ToTensor() converts them into PyTorch tensors. It also scales pixel values. Originally, a pixel is generally represented from:
After ToTensor():
For example:
This makes the data easier for a neural network to work with.

4. Loading the training dataset

This loads the training portion of MNIST. Let’s understand every argument.

root

This tells PyTorch where to store the dataset. Your project might look like:

train=True

MNIST contains two main portions:
train=True means:
Give me the training data.

download=True

If MNIST isn’t already present, PyTorch downloads it. If it’s already downloaded, it normally won’t download it again.

transform=transform

This applies:
to every image when it is retrieved.

5. Loading the test dataset

This is almost identical. The important difference:
means:
Give me the testing dataset.
So now you have:
The model learns using the training data. The test data is kept separate to evaluate whether the model can recognize images it wasn’t trained on.

6. Creating DataLoaders

Training DataLoader

This creates batches of training images.

batch_size=64

Instead of processing one image:
the model processes:
at a time.

shuffle=True

Before every training epoch, the training data is shuffled. For example, imagine your dataset contains:
Shuffling might make it:
This generally helps training because the model doesn’t repeatedly see the data in the same order.

7. Test DataLoader

Again, we use batches of 64. But:
because we don’t need random ordering during testing. The model is simply being evaluated.

8. Checking dataset sizes

Expected output:

9. Looking at one image

This retrieves the first training example. There are two things:
For example:

MNIST images are:
Because ToTensor() adds a channel dimension, you’ll typically get:
The three dimensions mean:
MNIST is grayscale, so it has only one channel. For RGB images you’d usually have:
because RGB has:

Could output:

10. Building the neural network

Now comes the important part.
You’re defining your own neural-network class. nn.Module is the base class for PyTorch neural networks. Your network inherits from it. Think:

11. Constructor

__init__() runs when you create:

Why super().__init__()?

Because SimpleNN inherits functionality from nn.Module. Calling:
initializes the parent nn.Module properly. This allows PyTorch to track things like:
  • model parameters
  • weights
  • gradients
  • layers

12. First neural-network layer

This is a fully connected layer. 28*28 is:
because:
An MNIST image contains:
So the first layer takes:
and produces:
Therefore:

What does Linear actually do?

Mathematically:
More formally:
The layer learns:
during training. Initially, the weights are not useful. Training gradually changes them.

13. Second layer

This takes the 128 values from the first layer and produces 10 outputs. So:
Why 10? Because MNIST has 10 classes:
Therefore, the final output contains 10 numbers.

14. Understanding the complete network

Your network is:

15. The forward() function

This defines how data flows through the network. When you write:
PyTorch effectively calls:

16. Flattening the image

This is extremely important. Your images initially have shape:
For example:
But nn.Linear expects something like:
So we flatten each image.

x.size(0)

returns the batch size. For example:
might be:

-1

The -1 means:
PyTorch, calculate this dimension automatically.
Since:
the result becomes:
So:

17. First layer

Now:
goes through:
Result:

18. ReLU activation

ReLU means: Rectified Linear Unit Mathematically:
Examples:
So negative values become zero.

Why do we need ReLU?

Without activation functions, stacking linear layers doesn’t give the network much additional expressive power. ReLU introduces non-linearity. This allows the network to learn more complicated patterns.

19. Final layer

Now:
becomes:
Each image now has 10 output values. For example, the network might output:
The largest value is:
which corresponds to digit:
So the model predicts:

20. Creating the model

First:
creates the neural network. Then:
moves it to the selected device. If:
the model goes to GPU. If:
it stays on CPU.
You’ll see something similar to:

21. Loss function

The loss function tells us:
How wrong is the model?
Your model predicts 10 scores. Suppose the correct answer is:
but the model gives a high score to:
The loss will be relatively high. If the model strongly predicts:
the loss will be lower.

Why CrossEntropyLoss?

CrossEntropyLoss is commonly used for multi-class classification. MNIST is a multi-class classification problem because there are 10 possible classes:
An important detail: Your final layer should not have softmax when using CrossEntropyLoss. CrossEntropyLoss internally handles the necessary log-softmax operation.

22. Optimizer

The optimizer updates the network’s weights.

model.parameters()

This gives Adam access to the parameters that need to be learned. Your network contains parameters such as:

Learning rate

Learning rate controls how large the updates are. Conceptually:
A very large learning rate can make training unstable. A very small learning rate can make training slow. 0.001 is a common starting point for Adam.

23. Number of epochs

An epoch means:
The model has processed the entire training dataset once.
You have:
So:
You’re doing:
Therefore the network gets 10 passes through the training dataset.

24. Starting the training loop

Since:
this runs:

25. Training mode

This tells PyTorch:
The model is currently being trained.
This matters particularly for models containing layers such as:
Your current network doesn’t use them, but it’s still good practice to explicitly call:
before training.

26. Tracking loss

This starts a counter. During the epoch, you’ll accumulate all batch losses:

27. Getting batches

train_loader gives you batches. With:
you might get:
So each iteration processes 64 images.

28. Moving images to GPU/CPU

Suppose you’re using GPU. The model is on GPU:
Therefore the input data must also be on GPU. You can’t normally do:
and expect the operation to work. Both need to be on the same device.

29. Clearing old gradients

This is very important. PyTorch accumulates gradients by default. Imagine:
If you don’t clear them, they accumulate. So before calculating the gradients for the current batch:
clears the previous gradients.

30. Forward pass

This is called the forward pass. Data flows through:
For a batch of 64 images:

31. Calculate loss

Now we compare:
The loss tells us how poorly the model performed on that batch. For example:
could be early in training. Later:
might indicate the model has learned much better.

32. Backpropagation

This is where PyTorch calculates the gradients. The basic process is:
A gradient tells us approximately:
If I change this parameter, how will the loss change?
PyTorch’s autograd system automatically calculates these gradients. You don’t have to manually derive all the calculus.

33. Updating the weights

Now Adam uses the calculated gradients to update the model parameters. Conceptually:
The model is therefore learning.

34. Accumulating the loss

loss is a PyTorch tensor.
extracts the ordinary Python number from it. For example:
might return:
Then:
keeps adding the losses from all batches.

35. Printing average loss

Suppose:
and:
Then:
gives the average batch loss.

Why epoch+1?

Python starts counting from zero:
produces:
But humans usually want:
Therefore:
is used.

:.4f

means:
Display the number with 4 digits after the decimal point.
For example:
becomes:

36. Testing the model

After training:
This puts the model into evaluation mode. Again, this is particularly important for layers such as Dropout and BatchNorm.

37. Initialize counters

We need to calculate:
So we start with zero.

38. Disable gradient calculation

During testing, we’re not training. We don’t need gradients. So:
tells PyTorch:
Don’t calculate/store gradients here.
This saves memory and computation.

39. Loop through test batches

Again, we’re receiving batches of 64 images.

40. Move test data to device

Same reason as during training. The data needs to be on the same device as the model.

41. Get predictions

The model produces:
For each of the 64 images, there are 10 output scores. For example:

42. Find predicted class

This finds the largest score in each row. Suppose:
The maximum values are:
Their indices are:
Therefore:

What is _?

torch.max() returns two things:
You only need the indices. So:
means:

43. Count total images

labels.size(0) gives the number of labels in the current batch. Usually:
So:
Eventually:

44. Count correct predictions

Let’s break this down. Suppose:
Comparison:
gives:
Then:
counts the True values:
So:

45. Calculate accuracy

Suppose:
Then:

46. There’s a small bug in your final print

You wrote:
This will literally print:
because you forgot the f before the string. You need:
Now if:
you’ll get:

47. Code


48. The entire process in one picture

The most important thing is to understand the flow of data. Your program does this:

49. The most important concepts to remember

If you’re learning PyTorch, focus on these concepts first:

50. The training cycle you should memorize

The core of virtually every PyTorch training loop is:
Think of it as:
Or even more simply: