PyTorch Implementation of MNIST Digit Recognition Using Fully Connected and Convolutional Architectures

Constructing a neural network for digit recognition begins with importing the necessary libraries and defining the model architecture. The following implementation demonstrates a progression from a basic linear model to a convolutional network using the PyTorch framework.

Basic Fully Connected Architecture

A simple multi-layer perceptron can be established by subclassing nn.Module. This initial model consists of three linear layers without non-linear activation functions.

import torch
import torch.nn as nn
import numpy as np

class LinearClassifier(nn.Module):
    def __init__(self, input_features, hidden_units_1, hidden_units_2, output_classes):
        super(LinearClassifier, self).__init__()
        self.input_projection = nn.Linear(input_features, hidden_units_1)
        self.hidden_layer = nn.Linear(hidden_units_1, hidden_units_2)
        self.output_projection = nn.Linear(hidden_units_2, output_classes)

    def forward(self, tensor_input):
        out = self.input_projection(tensor_input)
        out = self.hidden_layer(out)
        out = self.output_projection(out)
        return out

Integrating Activation Functions

To introduce non-linearity, Rectified Linear Units (ReLU) are added. Using nn.Sequential simplifies the layer definition by grouping linear transformations with their corresponding activation functions.

class ActivatedMLP(nn.Module):
    def __init__(self, input_features, hidden_units_1, hidden_units_2, output_classes):
        super(ActivatedMLP, self).__init__()
        self.block_1 = nn.Sequential(
            nn.Linear(input_features, hidden_units_1),
            nn.ReLU(inplace=True)
        )
        self.block_2 = nn.Sequential(
            nn.Linear(hidden_units_1, hidden_units_2),
            nn.ReLU(inplace=True)
        )
        self.final_layer = nn.Linear(hidden_units_2, output_classes)

    def forward(self, tensor_input):
        out = self.block_1(tensor_input)
        out = self.block_2(out)
        out = self.final_layer(out)
        return out

Applying Batch Normalization

Batch normalization helps stabilize the learning process. It is typically inserted between the linear transformation and the activation function. The following architecture incorporates normalization layers into the hidden blocks.

class NormalisedMLP(nn.Module):
    def __init__(self, input_features, hidden_units_1, hidden_units_2, output_classes):
        super(NormalisedMLP, self).__init__()
        self.block_1 = nn.Sequential(
            nn.Linear(input_features, hidden_units_1),
            nn.BatchNorm1d(hidden_units_1),
            nn.ReLU(inplace=True)
        )
        self.block_2 = nn.Sequential(
            nn.Linear(hidden_units_1, hidden_units_2),
            nn.BatchNorm1d(hidden_units_2),
            nn.ReLU(inplace=True)
        )
        self.final_layer = nn.Linear(hidden_units_2, output_classes)

    def forward(self, tensor_input):
        out = self.block_1(tensor_input)
        out = self.block_2(out)
        out = self.final_layer(out)
        return out

Data Preparation and Loading

Efficient data handling requires transforming images into tensors and normalizing pixel values. The standard MNIST mean and standard deviation are used for normalization. DataLoaders manage batching and shuffling.

from torchvision import datasets, transforms
from torch.utils.data import DataLoader

mini_batch_size = 64
step_size = 1e-2
training_epochs = 20

pipeline = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize(mean=(0.1307,), std=(0.3081,))
])

train_dataset = datasets.MNIST(
    root='./data',
    train=True,
    transform=pipeline,
    download=True
)

test_dataset = datasets.MNIST(
    root='./data',
    train=False,
    transform=pipeline
)

training_dataloader = DataLoader(train_dataset, batch_size=mini_batch_size, shuffle=True)
testing_dataloader = DataLoader(test_dataset, batch_size=mini_batch_size, shuffle=False)

Convolutional Neural Network Implemantation

For improved accuracy, a convolutional architecture processes spatial hierarchies in the image data. This model includes convolutional layers, max pooling, dropout for regularization, and fully connected layers for classificasion.

import torch.nn.functional as F

class DigitConvNet(nn.Module):
    def __init__(self):
        super(DigitConvNet, self).__init__()
        self.conv_layer_1 = nn.Conv2d(1, 10, kernel_size=5)
        self.conv_layer_2 = nn.Conv2d(10, 20, kernel_size=5)
        self.dropout_2d = nn.Dropout2d()
        self.fc_layer_1 = nn.Linear(320, 50)
        self.fc_layer_2 = nn.Linear(50, 10)

    def forward(self, input_tensor):
        out = F.relu(F.max_pool2d(self.conv_layer_1(input_tensor), 2))
        out = F.relu(F.max_pool2d(self.dropout_2d(self.conv_layer_2(out)), 2))
        out = out.view(-1, 320)
        out = F.relu(self.fc_layer_1(out))
        out = F.dropout(out, training=self.training)
        out = self.fc_layer_2(out)
        return F.log_softmax(out, dim=1)

Training and Evaluation Loop

The training process iterates through epochs, calculating gradients and updating weights. Evaluation is performed using torch.no_grad() to disable gradient computation for efficiency. Metrics such as loss and accuracy are tracked throughout the process.

import torch.optim as optim
import matplotlib.pyplot as plt

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
network = DigitConvNet().to(device)
criterion = nn.NLLLoss()
optimizer = optim.SGD(network.parameters(), lr=step_size, momentum=0.5)

train_losses = []
test_losses = []
train_counter = []
test_counter = [i * len(training_dataloader.dataset) for i in range(training_epochs + 1)]

def execute_training_phase(epoch_idx):
    network.train()
    for batch_idx, (data_batch, target_vector) in enumerate(training_dataloader):
        data_batch, target_vector = data_batch.to(device), target_vector.to(device)
        optimizer.zero_grad()
        output_logits = network(data_batch)
        batch_loss = criterion(output_logits, target_vector)
        batch_loss.backward()
        optimizer.step()
        
        if batch_idx % 10 == 0:
            print(f'Train Epoch: {epoch_idx} [{batch_idx * len(data_batch)}/{len(training_dataloader.dataset)}] Loss: {batch_loss.item():.6f}')
            train_losses.append(batch_loss.item())
            train_counter.append((batch_idx * 64) + ((epoch_idx - 1) * len(training_dataloader.dataset)))

def execute_evaluation():
    network.eval()
    total_loss = 0
    correct_predictions = 0
    with torch.no_grad():
        for data_batch, target_vector in testing_dataloader:
            data_batch, target_vector = data_batch.to(device), target_vector.to(device)
            output_logits = network(data_batch)
            total_loss += criterion(output_logits, target_vector, reduction='sum').item()
            pred_values = output_logits.data.max(1, keepdim=True)[1]
            correct_predictions += pred_values.eq(target_vector.data.view_as(pred_values)).sum()
    
    avg_loss = total_loss / len(testing_dataloader.dataset)
    test_losses.append(avg_loss)
    accuracy = 100. * correct_predictions.item() / len(testing_dataloader.dataset)
    print(f'Test Set: Avg Loss: {avg_loss:.4f}, Accuracy: {correct_predictions.item()}/{len(testing_dataloader.dataset)} ({accuracy:.0f}%)')

for epoch in range(1, training_epochs + 1):
    execute_training_phase(epoch)
    execute_evaluation()

Visualizing Performance Metrics

Tracking loss over training steps provides insight into model convergence. The training loss is plotted as a continuous line, while test loss is represented by scatter points to distinguish evaluation intervals.

plt.figure()
plt.plot(train_counter, train_losses, color='blue')
plt.scatter(test_counter, test_losses, color='red')
plt.legend(['Training Loss', 'Evaluation Loss'], loc='upper right')
plt.xlabel('Number of Training Examples Seen')
plt.ylabel('Negative Log Likelihood Loss')
plt.show()

Tags: pytorch deep-learning MNIST neural-networks computer-vision

Posted on Mon, 31 Aug 2026 16:05:16 +0000 by zoran