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()