Deep Learning Models: A collection of various deep learning architectures, models, & their implementations (PyTorch or TensorFlow) in Jupyter Notebooks.
- Author: Chizoba Obasi
- Github Repo: https://github.com/chizkidd/deep-learning-models
In [1]:
!pip install watermark
Requirement already satisfied: watermark in /usr/local/lib/python3.11/dist-packages (2.6.0) Requirement already satisfied: ipython>=6.0 in /usr/local/lib/python3.11/dist-packages (from watermark) (7.34.0) Requirement already satisfied: importlib-metadata>=1.4 in /usr/local/lib/python3.11/dist-packages (from watermark) (8.7.0) Requirement already satisfied: setuptools in /usr/local/lib/python3.11/dist-packages (from watermark) (75.2.0) Requirement already satisfied: zipp>=3.20 in /usr/local/lib/python3.11/dist-packages (from importlib-metadata>=1.4->watermark) (3.23.0) Requirement already satisfied: jedi>=0.16 in /usr/local/lib/python3.11/dist-packages (from ipython>=6.0->watermark) (0.19.2) Requirement already satisfied: decorator in /usr/local/lib/python3.11/dist-packages (from ipython>=6.0->watermark) (4.4.2) Requirement already satisfied: pickleshare in /usr/local/lib/python3.11/dist-packages (from ipython>=6.0->watermark) (0.7.5) Requirement already satisfied: traitlets>=4.2 in /usr/local/lib/python3.11/dist-packages (from ipython>=6.0->watermark) (5.7.1) Requirement already satisfied: prompt-toolkit!=3.0.0,!=3.0.1,<3.1.0,>=2.0.0 in /usr/local/lib/python3.11/dist-packages (from ipython>=6.0->watermark) (3.0.51) Requirement already satisfied: pygments in /usr/local/lib/python3.11/dist-packages (from ipython>=6.0->watermark) (2.19.2) Requirement already satisfied: backcall in /usr/local/lib/python3.11/dist-packages (from ipython>=6.0->watermark) (0.2.0) Requirement already satisfied: matplotlib-inline in /usr/local/lib/python3.11/dist-packages (from ipython>=6.0->watermark) (0.1.7) Requirement already satisfied: pexpect>4.3 in /usr/local/lib/python3.11/dist-packages (from ipython>=6.0->watermark) (4.9.0) Requirement already satisfied: parso<0.9.0,>=0.8.4 in /usr/local/lib/python3.11/dist-packages (from jedi>=0.16->ipython>=6.0->watermark) (0.8.4) Requirement already satisfied: ptyprocess>=0.5 in /usr/local/lib/python3.11/dist-packages (from pexpect>4.3->ipython>=6.0->watermark) (0.7.0) Requirement already satisfied: wcwidth in /usr/local/lib/python3.11/dist-packages (from prompt-toolkit!=3.0.0,!=3.0.1,<3.1.0,>=2.0.0->ipython>=6.0->watermark) (0.2.13)
In [2]:
!pip install torchinfo
Requirement already satisfied: torchinfo in /usr/local/lib/python3.11/dist-packages (1.8.0)
In [3]:
%load_ext watermark
# %reload_ext watermark
%watermark -a 'Chizoba Obasi' -v -p torch
Author: Chizoba Obasi Python implementation: CPython Python version : 3.11.13 IPython version : 7.34.0 torch: 2.6.0+cu124
EfficientNet-B0 Classifier on CIFAR-10¶
Network Architecture:
The network in this notebook is an implementation of the EfficientNet-B0 [1] architecture on the CIFAR-10 dataset to train an image classifier.
EfficientNet: Model Architecture¶

REFERENCES:
- Tan, M., & Le, Q. V. (2019). EfficientNet: Rethinking model scaling for convolutional neural networks. International Conference on Machine Learning, 6105–6114
In [4]:
import torch
import torchvision
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
from sklearn.model_selection import train_test_split
from torchinfo import summary
import matplotlib.image as mpimg
import matplotlib.pyplot as plt
%matplotlib inline
import pandas as pd
import numpy as np
import math
import time
import os
import random
if torch.cuda.is_available():
torch.backends.cudnn.deterministic = True
In [5]:
def set_all_seeds(seed):
os.environ["PL_GLOBAL_SEED"] = str(seed)
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
def set_deterministic():
if torch.cuda.is_available():
torch.backends.cudnn.benchmark = False
torch.backends.cudnn.deterministic = True
torch.set_deterministic(True)
In [6]:
# Check if GPU is available and set the device
if torch.cuda.is_available():
num_gpus = torch.cuda.device_count()
print("Num GPUs Available: ", num_gpus)
for i in range(num_gpus):
gpu_name = torch.cuda.get_device_name(i)
print(f"GPU {i}: {gpu_name}")
else:
print("No GPU available.")
Num GPUs Available: 2 GPU 0: Tesla T4 GPU 1: Tesla T4
In [7]:
##########################
### SETTINGS
##########################
# Hyperparameters
RANDOM_SEED = 123
BATCH_SIZE = 128 #256
LEARNING_RATE = 0.001
NUM_EPOCHS = 30 #50
INPUT_SIZE = 32 # CIFAR-10 is 32x32
NUM_CLASSES = 10
# Set device
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(device)
cuda
In [8]:
set_all_seeds(RANDOM_SEED)
In [9]:
# Mean and Std Dev for CIFAR-10 (3 channels)
CIFAR_MEAN = (0.4914, 0.4822, 0.4465)
CIFAR_STD = (0.2471, 0.2435, 0.2616)
# Data Augmentation (Crucial for VGG-16 on CIFAR-10)
train_transforms = transforms.Compose([
transforms.RandomCrop(32, padding=4), # Crop after padding
transforms.RandomHorizontalFlip(), # Flip
transforms.ToTensor(),
transforms.Normalize(CIFAR_MEAN, CIFAR_STD) # Mean Subtraction
])
# Test/Validation Transforms (No Augmentation)
test_transforms = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(CIFAR_MEAN, CIFAR_STD)
])
# Load Datasets
train_dataset = datasets.CIFAR10(root='./data', train=True, download=True, transform=train_transforms)
test_dataset = datasets.CIFAR10(root='./data', train=False, download=True, transform=test_transforms)
# Split training set into training and validation sets
train_size = int(0.9 * len(train_dataset))
val_size = len(train_dataset) - train_size
train_subset, val_subset = torch.utils.data.random_split(train_dataset, [train_size, val_size])
# Create DataLoaders
train_loader = DataLoader(train_subset, batch_size=BATCH_SIZE, shuffle=True, num_workers=2, pin_memory=True)
val_loader = DataLoader(val_subset, batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)
test_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)
In [10]:
# Check that shuffling works properly
# i.e., label indices should be in random order.
# Also, the label order should be different in the second
# epoch.
for images, labels in train_loader:
pass
print(labels[:10])
for images, labels in train_loader:
pass
print(labels[:10])
tensor([1, 5, 5, 4, 2, 8, 5, 2, 4, 2]) tensor([9, 2, 4, 3, 7, 5, 8, 6, 2, 8])
In [11]:
# Check that validation set and test sets are diverse
# i.e., that they contain all classes
for images, labels in val_loader:
pass
print(labels[:10])
for images, labels in test_loader:
pass
print(labels[:10])
tensor([1, 6, 5, 9, 9, 0, 6, 6]) tensor([7, 5, 8, 0, 8, 2, 7, 0, 3, 5])
2. Model Creation¶
Building EfficientNet-B0 from scratch is a fantastic exercise because it introduces you to the MBConv (Mobile Inverted Bottleneck) block and Squeeze-and-Excitation (SE) optimization.
Implementation Tips for "From Scratch":¶
- Squeeze-and-Excitation Block: This is the "secret sauce" that allows EfficientNet to weight channels dynamically.
MBConvBlock: This combines Depthwise-Separable convolutions with the SE block and a skip connection.- Activation Function: EfficientNet uses SiLU (also known as Swish: ). Don't use ReLU, or it won't be a true EfficientNet.
- Weights Initialization: When training from scratch, use Kaiming Initialization for the Conv layers to help convergence.
- Batch Size: You can now use a larger batch size (like 128) because you aren't upscaling images to 224x224.
Note: In this scratch version, I have modified the Strides to 1 in the first few stages. This ensures that a 32x32 CIFAR-10 image doesn't shrink to 1x1 before the network is even half-finished.
In [12]:
class SqueezeExcitation(nn.Module):
def __init__(self, in_channels, reduced_dim):
super().__init__()
self.se = nn.Sequential(
nn.AdaptiveAvgPool2d(1), # Squeeze
nn.Conv2d(in_channels, reduced_dim, 1),
nn.SiLU(),
nn.Conv2d(reduced_dim, in_channels, 1),
nn.Sigmoid() # Excitation
)
def forward(self, x):
return x * self.se(x)
class MBConv(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size, stride, expand_ratio, se_ratio=0.25):
super().__init__()
expand_channels = in_channels * expand_ratio
self.use_residual = (stride == 1 and in_channels == out_channels)
layers = []
# 1. Expansion phase
if expand_ratio != 1:
layers.append(nn.Conv2d(in_channels, expand_channels, 1, bias=False))
layers.append(nn.BatchNorm2d(expand_channels))
layers.append(nn.SiLU())
# 2. Depthwise phase
layers.append(nn.Conv2d(expand_channels, expand_channels, kernel_size,
stride, kernel_size//2, groups=expand_channels, bias=False))
layers.append(nn.BatchNorm2d(expand_channels))
layers.append(nn.SiLU())
# 3. Squeeze and Excitation
se_reduced_dim = int(in_channels * se_ratio)
layers.append(SqueezeExcitation(expand_channels, se_reduced_dim))
# 4. Pointwise phase (Projection)
layers.append(nn.Conv2d(expand_channels, out_channels, 1, bias=False))
layers.append(nn.BatchNorm2d(out_channels))
self.block = nn.Sequential(*layers)
def forward(self, x):
if self.use_residual:
return x + self.block(x)
return self.block(x)
class EfficientNetB0(nn.Module):
def __init__(self, num_classes=10):
super().__init__()
# Initial Conv: Changed stride 2 -> 1 for CIFAR-10
self.stem = nn.Sequential(
nn.Conv2d(3, 32, 3, stride=1, padding=1, bias=False),
nn.BatchNorm2d(32),
nn.SiLU()
)
# Configuration: [expand_ratio, channels, repeats, stride, kernel_size]
self.config = [
[1, 16, 1, 1, 3],
[6, 24, 2, 1, 3], # Stride 1 for CIFAR (Standard is 2)
[6, 40, 2, 2, 5],
[6, 80, 3, 2, 3],
[6, 112, 3, 1, 5],
[6, 192, 4, 2, 5],
[6, 320, 1, 1, 3],
]
layers = []
in_channels = 32
for expand_ratio, out_channels, repeats, stride, kernel_size in self.config:
for i in range(repeats):
layers.append(MBConv(in_channels, out_channels, kernel_size,
stride if i == 0 else 1, expand_ratio))
in_channels = out_channels
self.layers = nn.Sequential(*layers)
# Head
self.head = nn.Sequential(
nn.Conv2d(320, 1280, 1, bias=False),
nn.BatchNorm2d(1280),
nn.SiLU(),
nn.AdaptiveAvgPool2d(1),
nn.Flatten(),
nn.Dropout(0.2),
nn.Linear(1280, num_classes)
)
# Initialize Weights
self._initialize_weights()
def _initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
# Using fan_out for depthwise/pointwise layers
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
if m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Linear):
nn.init.normal_(m.weight, 0, 0.01)
nn.init.constant_(m.bias, 0)
def forward(self, x):
x = self.stem(x)
x = self.layers(x)
x = self.head(x)
return x
# Instantiate
model = EfficientNetB0().to(device)
In [13]:
# Model parameter details
summary(model,
input_size=(BATCH_SIZE, 3, INPUT_SIZE, INPUT_SIZE),
# verbose=1,
col_names=["input_size", "output_size", "num_params", "kernel_size", "mult_adds"]
)
Out[13]:
==================================================================================================================================================================================== Layer (type:depth-idx) Input Shape Output Shape Param # Kernel Shape Mult-Adds ==================================================================================================================================================================================== EfficientNetB0 [128, 3, 32, 32] [128, 10] -- -- -- ├─Sequential: 1-1 [128, 3, 32, 32] [128, 32, 32, 32] -- -- -- │ └─Conv2d: 2-1 [128, 3, 32, 32] [128, 32, 32, 32] 864 [3, 3] 113,246,208 │ └─BatchNorm2d: 2-2 [128, 32, 32, 32] [128, 32, 32, 32] 64 -- 8,192 │ └─SiLU: 2-3 [128, 32, 32, 32] [128, 32, 32, 32] -- -- -- ├─Sequential: 1-2 [128, 32, 32, 32] [128, 320, 4, 4] -- -- -- │ └─MBConv: 2-4 [128, 32, 32, 32] [128, 16, 32, 32] -- -- -- │ │ └─Sequential: 3-1 [128, 32, 32, 32] [128, 16, 32, 32] 1,448 -- 104,940,544 │ └─MBConv: 2-5 [128, 16, 32, 32] [128, 24, 32, 32] -- -- -- │ │ └─Sequential: 3-2 [128, 16, 32, 32] [128, 24, 32, 32] 6,004 -- 616,729,088 │ └─MBConv: 2-6 [128, 24, 32, 32] [128, 24, 32, 32] -- -- -- │ │ └─Sequential: 3-3 [128, 24, 32, 32] [128, 24, 32, 32] 10,710 -- 1,076,159,232 │ └─MBConv: 2-7 [128, 24, 32, 32] [128, 40, 16, 16] -- -- -- │ │ └─Sequential: 3-4 [128, 24, 32, 32] [128, 40, 16, 16] 15,350 -- 760,017,664 │ └─MBConv: 2-8 [128, 40, 16, 16] [128, 40, 16, 16] -- -- -- │ │ └─Sequential: 3-5 [128, 40, 16, 16] [128, 40, 16, 16] 31,290 -- 826,533,120 │ └─MBConv: 2-9 [128, 40, 16, 16] [128, 80, 8, 8] -- -- -- │ │ └─Sequential: 3-6 [128, 40, 16, 16] [128, 80, 8, 8] 37,130 -- 490,343,680 │ └─MBConv: 2-10 [128, 80, 8, 8] [128, 80, 8, 8] -- -- -- │ │ └─Sequential: 3-7 [128, 80, 8, 8] [128, 80, 8, 8] 102,900 -- 667,322,880 │ └─MBConv: 2-11 [128, 80, 8, 8] [128, 80, 8, 8] -- -- -- │ │ └─Sequential: 3-8 [128, 80, 8, 8] [128, 80, 8, 8] 102,900 -- 667,322,880 │ └─MBConv: 2-12 [128, 80, 8, 8] [128, 112, 8, 8] -- -- -- │ │ └─Sequential: 3-9 [128, 80, 8, 8] [128, 112, 8, 8] 126,004 -- 856,074,752 │ └─MBConv: 2-13 [128, 112, 8, 8] [128, 112, 8, 8] -- -- -- │ │ └─Sequential: 3-10 [128, 112, 8, 8] [128, 112, 8, 8] 208,572 -- 1,376,030,208 │ └─MBConv: 2-14 [128, 112, 8, 8] [128, 112, 8, 8] -- -- -- │ │ └─Sequential: 3-11 [128, 112, 8, 8] [128, 112, 8, 8] 208,572 -- 1,376,030,208 │ └─MBConv: 2-15 [128, 112, 8, 8] [128, 192, 4, 4] -- -- -- │ │ └─Sequential: 3-12 [128, 112, 8, 8] [128, 192, 4, 4] 262,492 -- 920,509,952 │ └─MBConv: 2-16 [128, 192, 4, 4] [128, 192, 4, 4] -- -- -- │ │ └─Sequential: 3-13 [128, 192, 4, 4] [128, 192, 4, 4] 587,952 -- 979,900,416 │ └─MBConv: 2-17 [128, 192, 4, 4] [128, 192, 4, 4] -- -- -- │ │ └─Sequential: 3-14 [128, 192, 4, 4] [128, 192, 4, 4] 587,952 -- 979,900,416 │ └─MBConv: 2-18 [128, 192, 4, 4] [128, 192, 4, 4] -- -- -- │ │ └─Sequential: 3-15 [128, 192, 4, 4] [128, 192, 4, 4] 587,952 -- 979,900,416 │ └─MBConv: 2-19 [128, 192, 4, 4] [128, 320, 4, 4] -- -- -- │ │ └─Sequential: 3-16 [128, 192, 4, 4] [128, 320, 4, 4] 717,232 -- 1,244,174,336 ├─Sequential: 1-3 [128, 320, 4, 4] [128, 10] -- -- -- │ └─Conv2d: 2-20 [128, 320, 4, 4] [128, 1280, 4, 4] 409,600 [1, 1] 838,860,800 │ └─BatchNorm2d: 2-21 [128, 1280, 4, 4] [128, 1280, 4, 4] 2,560 -- 327,680 │ └─SiLU: 2-22 [128, 1280, 4, 4] [128, 1280, 4, 4] -- -- -- │ └─AdaptiveAvgPool2d: 2-23 [128, 1280, 4, 4] [128, 1280, 1, 1] -- -- -- │ └─Flatten: 2-24 [128, 1280, 1, 1] [128, 1280] -- -- -- │ └─Dropout: 2-25 [128, 1280] [128, 1280] -- -- -- │ └─Linear: 2-26 [128, 1280] [128, 10] 12,810 -- 1,639,680 ==================================================================================================================================================================================== Total params: 4,020,358 Trainable params: 4,020,358 Non-trainable params: 0 Total mult-adds (Units.GIGABYTES): 14.88 ==================================================================================================================================================================================== Input size (MB): 1.57 Forward/backward pass size (MB): 3408.01 Params size (MB): 16.08 Estimated Total Size (MB): 3425.66 ====================================================================================================================================================================================
In [14]:
criterion = nn.CrossEntropyLoss()
# optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE, weight_decay=5e-4)
optimizer = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE, weight_decay=5e-4)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=NUM_EPOCHS, eta_min=1e-6)
def calculate_accuracy(loader):
model.eval()
correct = 0
total = 0
with torch.no_grad():
for inputs, labels in loader:
inputs, labels = inputs.to(device), labels.to(device)
outputs = model(inputs)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
return 100 * correct / total
# Training Loop
start_time = time.time()
history = {'train_loss': [], 'val_loss': [], 'train_acc': [], 'val_acc': [], 'lrs': []}
print(f"Starting training on {device}...")
for epoch in range(NUM_EPOCHS):
model.train()
train_loss, train_correct, train_total = 0.0, 0, 0
for i, (inputs, labels) in enumerate(train_loader):
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
train_loss += loss.item() * inputs.size(0)
_, predicted = outputs.max(1)
train_total += labels.size(0)
train_correct += predicted.eq(labels).sum().item()
train_loss = train_loss / len(train_subset)
train_acc = 100. * train_correct / train_total
# Calculate validation loss and accuracy
model.eval()
val_loss, val_correct, val_total = 0.0, 0, 0
with torch.no_grad():
for inputs, labels in val_loader:
inputs, labels = inputs.to(device), labels.to(device)
outputs = model(inputs)
loss = criterion(outputs, labels)
val_loss += loss.item() * inputs.size(0)
_, predicted = outputs.max(1)
val_total += labels.size(0)
val_correct += predicted.eq(labels).sum().item()
val_loss = val_loss / len(val_subset)
val_acc = 100. * val_correct / val_total
scheduler.step()
current_lr = optimizer.param_groups[0]['lr']
# Log metrics
history['train_loss'].append(train_loss)
history['val_loss'].append(val_loss)
history['train_acc'].append(train_acc)
history['val_acc'].append(val_acc)
history['lrs'].append(current_lr)
print(f'Epoch {epoch + 1}/{NUM_EPOCHS} | '
f'Train Loss: {train_loss:.5f}, '
f'Train Acc.: {train_acc:.2f}% | '
f'Val Loss: {val_loss:.5f}, '
f'Val Acc.: {val_acc:.2f}% | '
f'LR: {current_lr:.8f}'
)
print('\nTotal Training Time: %.2f min' % ((time.time() - start_time)/60))
# --- Final Evaluation ---
test_accuracy = calculate_accuracy(test_loader)
print(f'\nTest Accuracy: {test_accuracy:.2f}%')
Starting training on cuda... Epoch 1/30 | Train Loss: 1.60032, Train Acc.: 39.72% | Val Loss: 1.33364, Val Acc.: 51.76% | LR: 0.00099726 Epoch 2/30 | Train Loss: 1.08361, Train Acc.: 61.32% | Val Loss: 1.01320, Val Acc.: 64.06% | LR: 0.00098908 Epoch 3/30 | Train Loss: 0.85020, Train Acc.: 70.32% | Val Loss: 0.90244, Val Acc.: 68.76% | LR: 0.00097555 Epoch 4/30 | Train Loss: 0.72896, Train Acc.: 75.08% | Val Loss: 0.76207, Val Acc.: 74.44% | LR: 0.00095682 Epoch 5/30 | Train Loss: 0.64968, Train Acc.: 77.83% | Val Loss: 0.73072, Val Acc.: 74.96% | LR: 0.00093308 Epoch 6/30 | Train Loss: 0.59704, Train Acc.: 79.57% | Val Loss: 0.64726, Val Acc.: 78.36% | LR: 0.00090460 Epoch 7/30 | Train Loss: 0.55662, Train Acc.: 81.15% | Val Loss: 0.64641, Val Acc.: 77.54% | LR: 0.00087170 Epoch 8/30 | Train Loss: 0.51604, Train Acc.: 82.41% | Val Loss: 0.64870, Val Acc.: 78.00% | LR: 0.00083473 Epoch 9/30 | Train Loss: 0.48318, Train Acc.: 83.56% | Val Loss: 0.56256, Val Acc.: 80.92% | LR: 0.00079410 Epoch 10/30 | Train Loss: 0.45845, Train Acc.: 84.54% | Val Loss: 0.52241, Val Acc.: 82.26% | LR: 0.00075025 Epoch 11/30 | Train Loss: 0.43246, Train Acc.: 85.22% | Val Loss: 0.49367, Val Acc.: 83.10% | LR: 0.00070366 Epoch 12/30 | Train Loss: 0.41405, Train Acc.: 85.94% | Val Loss: 0.50176, Val Acc.: 82.34% | LR: 0.00065485 Epoch 13/30 | Train Loss: 0.38834, Train Acc.: 86.86% | Val Loss: 0.45898, Val Acc.: 83.82% | LR: 0.00060435 Epoch 14/30 | Train Loss: 0.36786, Train Acc.: 87.43% | Val Loss: 0.44432, Val Acc.: 84.96% | LR: 0.00055271 Epoch 15/30 | Train Loss: 0.34615, Train Acc.: 88.17% | Val Loss: 0.44099, Val Acc.: 85.02% | LR: 0.00050050 Epoch 16/30 | Train Loss: 0.32895, Train Acc.: 88.79% | Val Loss: 0.43182, Val Acc.: 84.74% | LR: 0.00044829 Epoch 17/30 | Train Loss: 0.30636, Train Acc.: 89.34% | Val Loss: 0.40617, Val Acc.: 85.92% | LR: 0.00039665 Epoch 18/30 | Train Loss: 0.28627, Train Acc.: 90.12% | Val Loss: 0.39009, Val Acc.: 87.10% | LR: 0.00034615 Epoch 19/30 | Train Loss: 0.26854, Train Acc.: 90.80% | Val Loss: 0.37416, Val Acc.: 87.66% | LR: 0.00029734 Epoch 20/30 | Train Loss: 0.24797, Train Acc.: 91.64% | Val Loss: 0.36167, Val Acc.: 88.02% | LR: 0.00025075 Epoch 21/30 | Train Loss: 0.23367, Train Acc.: 92.00% | Val Loss: 0.35960, Val Acc.: 87.66% | LR: 0.00020690 Epoch 22/30 | Train Loss: 0.21770, Train Acc.: 92.38% | Val Loss: 0.34750, Val Acc.: 88.14% | LR: 0.00016627 Epoch 23/30 | Train Loss: 0.20236, Train Acc.: 93.12% | Val Loss: 0.33933, Val Acc.: 88.48% | LR: 0.00012930 Epoch 24/30 | Train Loss: 0.18630, Train Acc.: 93.76% | Val Loss: 0.33489, Val Acc.: 88.66% | LR: 0.00009640 Epoch 25/30 | Train Loss: 0.17299, Train Acc.: 94.14% | Val Loss: 0.32322, Val Acc.: 89.18% | LR: 0.00006792 Epoch 26/30 | Train Loss: 0.16390, Train Acc.: 94.46% | Val Loss: 0.34044, Val Acc.: 88.68% | LR: 0.00004418 Epoch 27/30 | Train Loss: 0.15627, Train Acc.: 94.68% | Val Loss: 0.31890, Val Acc.: 89.86% | LR: 0.00002545 Epoch 28/30 | Train Loss: 0.14808, Train Acc.: 95.14% | Val Loss: 0.32634, Val Acc.: 89.26% | LR: 0.00001192 Epoch 29/30 | Train Loss: 0.14205, Train Acc.: 95.34% | Val Loss: 0.32047, Val Acc.: 89.54% | LR: 0.00000374 Epoch 30/30 | Train Loss: 0.14286, Train Acc.: 95.24% | Val Loss: 0.32164, Val Acc.: 89.18% | LR: 0.00000100 Total Training Time: 36.49 min Test Accuracy: 89.93%
In [15]:
# --- Final Evaluation ---
test_accuracy = calculate_accuracy(test_loader)
print(f'\nTest Accuracy: {test_accuracy:.2f}%')
Test Accuracy: 89.93%
In [16]:
def plot_history(history):
"""
Plots the training and validation loss, accuracy, and learning rate
from a PyTorch history dictionary.
Args:
history (dict): Dictionary containing training metrics.
Expected keys: 'train_loss', 'val_loss', 'train_acc', 'val_acc', 'lrs'.
"""
epochs = range(len(history['train_loss']))
plt.figure(figsize=(18, 5))
# Plot 1: Loss
plt.subplot(1, 3, 1)
plt.plot(epochs, history['train_loss'], label='Training Loss')
plt.plot(epochs, history['val_loss'], label='Validation Loss')
plt.title('Loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
# plt.grid(True)
# Plot 2: Accuracy
plt.subplot(1, 3, 2)
plt.plot(epochs, history['train_acc'], label='Training Accuracy')
plt.plot(epochs, history['val_acc'], label='Validation Accuracy')
plt.title('Accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.legend()
# plt.grid(True)
plt.tight_layout()
plt.show()
# plt.savefig()
plot_history(history)
In [17]:
from sklearn.metrics import confusion_matrix
import seaborn as sns
def _compute_confusion_matrix(model, data_loader, device):
"""
Computes a confusion matrix for the given model and data.
Uses scikit-learn for robustness and speed.
"""
model.eval()
all_targets = []
all_predictions = []
with torch.no_grad():
for features, targets in data_loader:
features = features.to(device)
logits = model(features)
_, predicted_labels = torch.max(logits, 1)
# Move to CPU and convert to list/numpy immediately
all_targets.extend(targets.view(-1).cpu().numpy())
all_predictions.extend(predicted_labels.view(-1).cpu().numpy())
return confusion_matrix(all_targets, all_predictions)
def _plot_confusion_matrix(mat, class_names):
"""
Visualizes the confusion matrix using Seaborn for professional reporting.
"""
plt.figure(figsize=(10, 8))
sns.heatmap(mat, annot=True, fmt='d', cmap='Blues',
xticklabels=class_names, yticklabels=class_names)
plt.xlabel('Predicted Label')
plt.ylabel('True Label')
plt.title('Confusion Matrix: MobileNetV2 on CIFAR-10')
plt.show()
In [18]:
class_dict = {0: 'plane',
1: 'car',
2: 'bird',
3: 'cat',
4: 'deer',
5: 'dog',
6: 'frog',
7: 'horse',
8: 'ship',
9: 'truck'}
_mat = _compute_confusion_matrix(model=model, data_loader=test_loader, device=device)
_plot_confusion_matrix(_mat, class_names=class_dict.values())
In [19]:
%watermark -iv
matplotlib : 3.7.2 numpy : 1.26.4 pandas : 2.2.3 seaborn : 0.12.2 sklearn : 1.2.2 torch : 2.6.0+cu124 torchinfo : 1.8.0 torchvision: 0.21.0+cu124
In [ ]: