Assignment 1: Handwritten Number Classification with PyTorch


EC332 Machine Learning
Department of Computer Science
University of the Punjab
Author: Nazar Khan


Assignment Objective:

Your task is to build and train a deep learning model or models using PyTorch to classify handwritten numbers from an imbalanced dataset available at this link. Due to the imbalanced nature of the dataset, you are encouraged to use appropriate data augmentation techniques and other strategies to improve model performance. You may also use additional training data from some other dataset of handwritten digits, if that helps.


Dataset Description:


Your Tasks:

  1. Data Loading and Preprocessing (15 marks):

  2. Data Handling (10 marks):

  3. Model Development (25 marks):

  4. Training the Model (25 marks):

  5. Evaluation (15 marks):

  6. Analysis and Reflection (10 marks):


Submission Instructions:

  1. Submit a Jupyter Notebook file (Assignment.ipynb) containing:
  2. Include a requirements.txt file with all dependencies used.
  3. Save the model and submit the model weights (model.pth).

Bonus Tasks (Optional, +10 marks):

  1. Use a pre-trained model and fine-tune it for this problem.
  2. Implement an advanced technique for handling class imbalance (e.g., focal loss or SMOTE).

Starter Code Snippets:

Custom Dataset Class:

from torchvision import transforms
from torch.utils.data import Dataset
from PIL import Image
import os

class HandwrittenDataset(Dataset):
    def __init__(self, root_dir, transform=None):
        self.root_dir = root_dir
        self.transform = transform
        self.image_paths = []
        self.labels = []

        for label in os.listdir(root_dir):
            label_path = os.path.join(root_dir, label)
            if os.path.isdir(label_path):
                for img in os.listdir(label_path):
                    self.image_paths.append(os.path.join(label_path, img))
                    self.labels.append(float(label))  # Convert labels to float

    def __len__(self):
        return len(self.image_paths)

    def __getitem__(self, idx):
        image_path = self.image_paths[idx]
        label = self.labels[idx]
        image = Image.open(image_path).convert("RGB")

        if self.transform:
            image = self.transform(image)

        return image, label

Data Augmentation:

# Define the RandomAffine transformation
transform = transforms.Compose([
    transforms.RandomAffine(
        degrees=15,          # Random rotation between -15 to +15 degrees
        translate=(0.1, 0.1),  # Randomly translate up to 10% of width and height
        scale=(0.9, 1.1),     # Randomly scale between 90% to 110%
        shear=10              # Randomly shear up to 10 degrees
    ),
    transforms.ToTensor(),  # Convert image to PyTorch tensor
    transforms.Normalize(mean=[0.5], std=[0.5])  # Normalize to [-1, 1] range
])

Model Training Loop:

import torch
from torch import nn, optim

def train_model(model, dataloaders, criterion, optimizer, num_epochs=10):
    for epoch in range(num_epochs):
        print(f"Epoch {epoch+1}/{num_epochs}")
        print("-" * 10)

        for phase in ['train', 'val']:
            if phase == 'train':
                model.train()
            else:
                model.eval()

            running_loss = 0.0
            corrects = 0

            for inputs, labels in dataloaders[phase]:
                inputs, labels = inputs.to(device), labels.to(device)
                optimizer.zero_grad()

                with torch.set_grad_enabled(phase == 'train'):
                    outputs = model(inputs)
                    loss = criterion(outputs, labels)
                    _, preds = torch.max(outputs, 1)

                    if phase == 'train':
                        loss.backward()
                        optimizer.step()

                running_loss += loss.item() * inputs.size(0)
                corrects += torch.sum(preds == labels.data)

            epoch_loss = running_loss / len(dataloaders[phase].dataset)
            epoch_acc = corrects.double() / len(dataloaders[phase].dataset)

            print(f"{phase} Loss: {epoch_loss:.4f} Acc: {epoch_acc:.4f}")

Grading Rubric:


This assignment combines foundational concepts in PyTorch with practical problem-solving, emphasizing the handling of class imbalance and the application of data augmentation techniques.