{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"04bbea9b-ed9c-4da4-b1b1-9d66b72b3850","cell_type":"markdown","source":"# Assignment 1: Image Classification with a Simple CNN\n### Course Event: Phase 3 - Convolutional Neural Networks\n**Dataset**: CIFAR-10  \n**GPU**: Kaggle P100 (Free)  \n**Framework**: PyTorch\n\nThis notebook implements a small Convolutional Neural Network (CNN) from scratch to classify 10 object categories in the CIFAR-10 dataset. It covers data visualization, model construction, learning rate scheduling, model evaluation, and visual interpretability.\n\n---\n## Notebook Modules\n1. **Exploratory Data Analysis (EDA)**: Class balance check and sample image visualization.\n2. **Data Preprocessing & Augmentation**: DataLoader implementation with normalization and random transformations.\n3. **CNN Model Architecture**: 2-layer CNN implementation and param summary.\n4. **Model Training & Loss Curves**: Training loop for 10 epochs tracking train/validation metrics.\n5. **Performance Evaluation & Error Analysis**: Confusion matrix, classification report, and prediction grid.\n6. **Model Interpretability (Occlusion Mapping)**: Pixel-importance analysis.\n","metadata":{}},{"id":"2296ded2-5a5a-48b0-9e9d-580cd46c7a74","cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\nimport torch.nn.functional as F\nimport torchvision\nimport torchvision.transforms as transforms\nfrom torch.utils.data import DataLoader\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom sklearn.metrics import classification_report, confusion_matrix\n\n# Set seeds for reproducibility\nSEED = 42\ntorch.manual_seed(SEED)\nnp.random.seed(SEED)\nif torch.cuda.is_available():\n    torch.cuda.manual_seed_all(SEED)\n\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f'Execution Device: {device}')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-24T14:20:32.155875Z","iopub.execute_input":"2026-06-24T14:20:32.156182Z","iopub.status.idle":"2026-06-24T14:20:42.268539Z","shell.execute_reply.started":"2026-06-24T14:20:32.156145Z","shell.execute_reply":"2026-06-24T14:20:42.267588Z"}},"outputs":[],"execution_count":null},{"id":"f09bdbf6-f46f-4526-b619-702127eab3bf","cell_type":"markdown","source":"## Module 2: Data Preprocessing & Augmentation\nWe normalize images with CIFAR-10 dataset stats (mean and standard deviation per channel). We also apply random cropping and flipping to improve generalization.\n","metadata":{}},{"id":"4491f81f-0d00-495e-8ec4-4eb29d0017c3","cell_type":"code","source":"transform_train = transforms.Compose([\n    transforms.RandomCrop(32, padding=4),\n    transforms.RandomHorizontalFlip(),\n    transforms.ToTensor(),\n    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616))\n])\n\ntransform_test = transforms.Compose([\n    transforms.ToTensor(),\n    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616))\n])\n\ntrain_dataset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform_train)\ntest_dataset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform_test)\n\n# Loaders optimized with pin_memory and multi-processing workers\ntrain_loader = DataLoader(train_dataset, batch_size=128, shuffle=True, num_workers=2, pin_memory=True)\ntest_loader = DataLoader(test_dataset, batch_size=128, shuffle=False, num_workers=2, pin_memory=True)\n\nclasses = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')\nprint('Datasets loaded successfully.')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-24T14:20:42.270174Z","iopub.execute_input":"2026-06-24T14:20:42.270882Z","iopub.status.idle":"2026-06-24T14:20:54.215744Z","shell.execute_reply.started":"2026-06-24T14:20:42.270837Z","shell.execute_reply":"2026-06-24T14:20:54.214900Z"}},"outputs":[],"execution_count":null},{"id":"caf86179-94cb-4c1d-b6dc-56b7d3a20045","cell_type":"markdown","source":"## Module 1: Exploratory Data Analysis (EDA)\nWe verify that the dataset classes are perfectly balanced and display sample images from each category.\n","metadata":{}},{"id":"ed75dda8-a4a1-40e3-8e0c-d760310fefbd","cell_type":"code","source":"# Class distribution bar chart\nlabels = [label for _, label in train_dataset]\nclass_counts = np.bincount(labels)\n\nplt.figure(figsize=(10, 4))\nsns.barplot(x=list(classes), y=class_counts, palette='viridis')\nplt.title('CIFAR-10 Class Distribution (Training Dataset)')\nplt.xlabel('Class Label')\nplt.ylabel('Image Count')\nplt.grid(axis='y', linestyle='--', alpha=0.7)\nplt.show()\n\n# Plotting a grid of 10 sample images (1 per class)\nplt.figure(figsize=(15, 6))\nfor i in range(10):\n    idx = labels.index(i)\n    img, label = train_dataset[idx]\n    # Unnormalize image for plotting\n    img = img * torch.tensor([0.2470, 0.2435, 0.2616]).view(3, 1, 1) + torch.tensor([0.4914, 0.4822, 0.4465]).view(3, 1, 1)\n    vis_img = np.transpose(img.numpy(), (1, 2, 0))\n    vis_img = np.clip(vis_img, 0, 1)\n    plt.subplot(2, 5, i+1)\n    plt.imshow(vis_img)\n    plt.title(classes[label].capitalize())\n    plt.axis('off')\nplt.suptitle('CIFAR-10 Representative Sample Images', fontsize=16)\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-24T14:20:54.216857Z","iopub.execute_input":"2026-06-24T14:20:54.217145Z","iopub.status.idle":"2026-06-24T14:21:11.434892Z","shell.execute_reply.started":"2026-06-24T14:20:54.217121Z","shell.execute_reply":"2026-06-24T14:21:11.434085Z"}},"outputs":[],"execution_count":null},{"id":"5e48ed54-98ef-4cd5-a1c0-bc320f652467","cell_type":"markdown","source":"## Module 3: CNN Model Architecture\n### Architectural Justification:\n- **2-Layer CNN Structure**: Employs two sequential convolutional blocks to extract local spatial features (edges, contours).\n- **Batch Normalization**: Applied after each convolutional layer to stabilize learning and accelerate training convergence.\n- **Max Pooling**: Reduces spatial dimensions ($32 \\times 32 \\to 16 \\to 8$), providing translation invariance and reducing memory consumption.\n- **Dropout**: Standard regularization (50% drop rate) in the fully-connected head to prevent model overfitting.\n","metadata":{}},{"id":"f0ea9090-3816-4c64-b183-8659ed4cd910","cell_type":"code","source":"class SimpleCNN(nn.Module):\n    def __init__(self):\n        super(SimpleCNN, self).__init__()\n        self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1)\n        self.bn1 = nn.BatchNorm2d(32)\n        self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)\n        self.bn2 = nn.BatchNorm2d(64)\n        self.pool = nn.MaxPool2d(2, 2)\n        \n        # After two MaxPool2d (32x32 -> 16x16 -> 8x8), feature map is 64x8x8 = 4096 dimensions\n        self.fc1 = nn.Linear(64 * 8 * 8, 128)\n        self.dropout = nn.Dropout(0.5)\n        self.fc2 = nn.Linear(128, 10)\n        \n    def forward(self, x):\n        x = self.pool(F.relu(self.bn1(self.conv1(x))))\n        x = self.pool(F.relu(self.bn2(self.conv2(x))))\n        x = x.view(-1, 64 * 8 * 8)\n        x = F.relu(self.fc1(x))\n        x = self.dropout(x)\n        x = self.fc2(x)\n        return x\n\nmodel = SimpleCNN().to(device)\n\ntry:\n    from torchinfo import summary\n    print(summary(model, input_size=(1, 3, 32, 32)))\nexcept ImportError:\n    print(model)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-24T14:21:11.436457Z","iopub.execute_input":"2026-06-24T14:21:11.436749Z","iopub.status.idle":"2026-06-24T14:21:12.323573Z","shell.execute_reply.started":"2026-06-24T14:21:11.436725Z","shell.execute_reply":"2026-06-24T14:21:12.322720Z"}},"outputs":[],"execution_count":null},{"id":"57b57e45-0b50-477d-a83e-d2715c7f0914","cell_type":"markdown","source":"## Module 4: Model Training & Loss Curves\nWe train the model for 10 epochs using the CrossEntropyLoss criterion, Adam optimizer, and a learning rate scheduler.\n","metadata":{}},{"id":"599ad2eb-d5a4-45e1-bb2c-d649061608dc","cell_type":"code","source":"criterion = nn.CrossEntropyLoss()\noptimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-4)\nscheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.1)\n\nepochs = 10\nhistory = {'train_loss': [], 'train_acc': [], 'val_loss': [], 'val_acc': []}\n\nfor epoch in range(epochs):\n    model.train()\n    running_loss = 0.0\n    correct = 0\n    total = 0\n    \n    for images, labels in train_loader:\n        images, labels = images.to(device), labels.to(device)\n        \n        optimizer.zero_grad()\n        outputs = model(images)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n        \n        running_loss += loss.item() * images.size(0)\n        _, predicted = outputs.max(1)\n        total += labels.size(0)\n        correct += predicted.eq(labels).sum().item()\n        \n    train_loss = running_loss / len(train_loader.dataset)\n    train_acc = correct / total\n    \n    # Validation phase\n    model.eval()\n    val_loss = 0.0\n    val_correct = 0\n    val_total = 0\n    with torch.no_grad():\n        for images, labels in test_loader:\n            images, labels = images.to(device), labels.to(device)\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n            val_loss += loss.item() * images.size(0)\n            _, predicted = outputs.max(1)\n            val_total += labels.size(0)\n            val_correct += predicted.eq(labels).sum().item()\n            \n    val_loss /= len(test_loader.dataset)\n    val_acc = val_correct / val_total\n    \n    scheduler.step()\n    \n    history['train_loss'].append(train_loss)\n    history['train_acc'].append(train_acc)\n    history['val_loss'].append(val_loss)\n    history['val_acc'].append(val_acc)\n    \n    print(f'Epoch {epoch+1:02d}/{epochs:02d} | Train Loss: {train_loss:.4f}, Train Acc: {train_acc:.4f} | Val Loss: {val_loss:.4f}, Val Acc: {val_acc:.4f}')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-24T14:21:12.324397Z","iopub.execute_input":"2026-06-24T14:21:12.324665Z","iopub.status.idle":"2026-06-24T14:23:08.587933Z","shell.execute_reply.started":"2026-06-24T14:21:12.324642Z","shell.execute_reply":"2026-06-24T14:23:08.587275Z"}},"outputs":[],"execution_count":null},{"id":"d0598217-606b-47d4-a2bc-694b32d7818b","cell_type":"markdown","source":"### Training and Validation Loss/Accuracy Curves\n","metadata":{}},{"id":"93f09f41-3cc5-4241-930d-11afddf5cdbf","cell_type":"code","source":"plt.figure(figsize=(14, 5))\nplt.subplot(1, 2, 1)\nplt.plot(range(1, epochs+1), history['train_loss'], label='Train Loss', color='mediumblue', marker='o')\nplt.plot(range(1, epochs+1), history['val_loss'], label='Val Loss', color='crimson', linestyle='--', marker='s')\nplt.title('Training vs. Validation Loss Trend')\nplt.xlabel('Epoch')\nplt.ylabel('Loss Value')\nplt.legend()\nplt.grid(True)\n\nplt.subplot(1, 2, 2)\nplt.plot(range(1, epochs+1), history['train_acc'], label='Train Accuracy', color='mediumblue', marker='o')\nplt.plot(range(1, epochs+1), history['val_acc'], label='Val Accuracy', color='crimson', linestyle='--', marker='s')\nplt.title('Training vs. Validation Accuracy Trend')\nplt.xlabel('Epoch')\nplt.ylabel('Accuracy')\nplt.legend()\nplt.grid(True)\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-24T14:23:08.589308Z","iopub.execute_input":"2026-06-24T14:23:08.589611Z","iopub.status.idle":"2026-06-24T14:23:08.860414Z","shell.execute_reply.started":"2026-06-24T14:23:08.589580Z","shell.execute_reply":"2026-06-24T14:23:08.859784Z"}},"outputs":[],"execution_count":null},{"id":"b0ac416e-c07d-45a2-b38e-11d24bc44679","cell_type":"markdown","source":"## Module 5: Performance Evaluation & Error Analysis\nHere we evaluate the model on the test dataset, showing the final accuracy, classification metrics, and confusion matrix.\n","metadata":{}},{"id":"98ae6c80-8d29-4c85-8a46-d31f2a6e727e","cell_type":"code","source":"model.eval()\nall_preds = []\nall_labels = []\nwith torch.no_grad():\n    for images, labels in test_loader:\n        images = images.to(device)\n        outputs = model(images)\n        _, preds = outputs.max(1)\n        all_preds.extend(preds.cpu().numpy())\n        all_labels.extend(labels.numpy())\n        \nprint('Classification Report:')\nprint(classification_report(all_labels, all_preds, target_names=classes))\n\ncm = confusion_matrix(all_labels, all_preds)\nplt.figure(figsize=(10, 8))\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=classes, yticklabels=classes)\nplt.title('Confusion Matrix on Test Dataset')\nplt.ylabel('True Label')\nplt.xlabel('Predicted Label')\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-24T14:23:08.861527Z","iopub.execute_input":"2026-06-24T14:23:08.861830Z","iopub.status.idle":"2026-06-24T14:23:10.675640Z","shell.execute_reply.started":"2026-06-24T14:23:08.861794Z","shell.execute_reply":"2026-06-24T14:23:10.674692Z"}},"outputs":[],"execution_count":null},{"id":"f333327e-7f11-4b20-b63a-7f83e2ac85bb","cell_type":"markdown","source":"## Module 6: Model Interpretability (Occlusion Mapping)\nTo understand what features our CNN relies on, we implement a simple occlusion sensitivity analyzer. This slides a black patch over the image and measures the change in classification confidence, highlighting spatial regions critical to the prediction.\n","metadata":{}},{"id":"1a2b3849-03d7-4319-bcc5-c888b65799f7","cell_type":"code","source":"def compute_occlusion_map(model, img, target_class, patch_size=8, stride=4):\n    model.eval()\n    _, h, w = img.shape\n    heatmap = np.zeros((h, w))\n    counts = np.zeros((h, w))\n    \n    with torch.no_grad():\n        base_out = F.softmax(model(img.unsqueeze(0).to(device)), dim=1)[0, target_class].item()\n        \n    for y in range(0, h - patch_size + 1, stride):\n        for x in range(0, w - patch_size + 1, stride):\n            occluded_img = img.clone()\n            occluded_img[:, y:y+patch_size, x:x+patch_size] = 0.0\n            \n            with torch.no_grad():\n                out = F.softmax(model(occluded_img.unsqueeze(0).to(device)), dim=1)[0, target_class].item()\n            \n            importance = base_out - out\n            heatmap[y:y+patch_size, x:x+patch_size] += importance\n            counts[y:y+patch_size, x:x+patch_size] += 1\n            \n    heatmap /= (counts + 1e-8)\n    heatmap = (heatmap - heatmap.min()) / (heatmap.max() - heatmap.min() + 1e-8)\n    return heatmap\n\n# Plot maps for 3 random test images\nplt.figure(figsize=(12, 8))\nfor idx in range(3):\n    img, label = test_dataset[idx]\n    heatmap = compute_occlusion_map(model, img, label)\n    \n    # Unnormalize image for display\n    vis_img = img * torch.tensor([0.2470, 0.2435, 0.2616]).view(3, 1, 1) + torch.tensor([0.4914, 0.4822, 0.4465]).view(3, 1, 1)\n    vis_img = np.clip(np.transpose(vis_img.numpy(), (1, 2, 0)), 0, 1)\n    \n    plt.subplot(2, 3, idx+1)\n    plt.imshow(vis_img)\n    plt.title(f'Original: {classes[label].capitalize()}')\n    plt.axis('off')\n    \n    plt.subplot(2, 3, idx+4)\n    plt.imshow(vis_img)\n    plt.imshow(heatmap, cmap='jet', alpha=0.5)\n    plt.title('Occlusion Sensitivity Map')\n    plt.axis('off')\nplt.suptitle('CNN Feature Localization via Occlusion Saliency', fontsize=15)\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-24T14:23:10.677154Z","iopub.execute_input":"2026-06-24T14:23:10.677569Z","iopub.status.idle":"2026-06-24T14:23:11.140674Z","shell.execute_reply.started":"2026-06-24T14:23:10.677537Z","shell.execute_reply":"2026-06-24T14:23:11.139973Z"}},"outputs":[],"execution_count":null},{"id":"f3619c29-5834-4c93-9622-1672ca97abe4","cell_type":"markdown","source":"## Module 7: Discussion & Predictions Grid\nWe visualize 10 sample predictions below.\n","metadata":{}},{"id":"571328ee-2c6c-45fe-807d-68f2f6dd08aa","cell_type":"code","source":"plt.figure(figsize=(15, 6))\nfor i in range(10):\n    img, label = test_dataset[i]\n    with torch.no_grad():\n        output = model(img.unsqueeze(0).to(device))\n        _, pred = output.max(1)\n        pred = pred.item()\n        \n    vis_img = img * torch.tensor([0.2470, 0.2435, 0.2616]).view(3, 1, 1) + torch.tensor([0.4914, 0.4822, 0.4465]).view(3, 1, 1)\n    vis_img = np.clip(np.transpose(vis_img.numpy(), (1, 2, 0)), 0, 1)\n    \n    plt.subplot(2, 5, i+1)\n    plt.imshow(vis_img)\n    color = 'green' if pred == label else 'red'\n    plt.title(f'True: {classes[label]}\\nPred: {classes[pred]}', color=color)\n    plt.axis('off')\nplt.suptitle('Model Predictions on Unseen Test Dataset', fontsize=16)\nplt.tight_layout()\nplt.show()\n\nprint('=== ERROR ANALYSIS & MODEL DISCUSSION ===')\nprint('The simple 2-layer CNN successfully learns low-level and mid-level feature representations.')\nprint('Class confusion primarily manifests in visually ambiguous pairs (e.g. cat/dog and car/truck).')\nprint('To address these issues and surpass 80% accuracy, future implementations should incorporate')\nprint('deeper CNN architectures (e.g. ResNet) and advanced regularization strategies like Cosine Annealing.')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-24T14:23:11.141593Z","iopub.execute_input":"2026-06-24T14:23:11.141981Z","iopub.status.idle":"2026-06-24T14:23:11.596531Z","shell.execute_reply.started":"2026-06-24T14:23:11.141955Z","shell.execute_reply":"2026-06-24T14:23:11.595816Z"}},"outputs":[],"execution_count":null}]}