In [ ]:
 

ACCA7025 Application of Machine Learning¶

Image Classification Using a Stacked LSTM Recurrent Neural Network on CIFAR-10¶

Items Details
Module ACCA7025 - Application of Machine Learning
Dataset CIFAR-10 (60,000 images, 10 classes, 32×32 RGB)
Model Stacked LSTM - image rows treated as time steps
Experiments Exp-1: Baseline (DR=0.3) · Exp-2: Regularised (DR=0.5 + BN)
University University of Wales Trinity Saint David
Deadline 18th May 2026

1. Imports and Reproducibility¶

All required libraries are imported and random seeds are fixed to ensure reproducible results across runs.

In [ ]:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import seaborn as sns
import pandas as pd
import os, time

import tensorflow as tf
from tensorflow.keras import layers, models, callbacks
from tensorflow.keras.datasets import cifar10

from sklearn.metrics import (classification_report, confusion_matrix,
                             accuracy_score)
import warnings
warnings.filterwarnings("ignore")

# Reproducibility 
np.random.seed(42)
tf.random.set_seed(42)

print("=" * 65)
print("  ACCA7025 — CIFAR-10 Classification via Stacked LSTM (RNN)")
print("=" * 65)
print(f"  TensorFlow version : {tf.__version__}")
print(f"  GPU available      : {bool(tf.config.list_physical_devices('GPU'))}")
print()
=================================================================
  ACCA7025 — CIFAR-10 Classification via Stacked LSTM (RNN)
=================================================================
  TensorFlow version : 2.21.0
WARNING:tensorflow:TensorFlow GPU support is not available on native Windows for TensorFlow >= 2.11. Even if CUDA/cuDNN are installed, GPU will not be used. Please use WSL2 or the TensorFlow-DirectML plugin.
  GPU available      : False

2. Data Loading and Preprocessing¶

CIFAR-10 is loaded via the Keras API; pixel values are normalised to [0, 1] and labels are one-hot encoded for multi-class classification.

In [2]:
CLASS_NAMES = ['airplane','automobile','bird','cat','deer',
               'dog','frog','horse','ship','truck']
NUM_CLASSES = 10

(x_train, y_train), (x_test, y_test) = cifar10.load_data()

print(f"[Dataset]")
print(f"  Train : {x_train.shape}  |  Test : {x_test.shape}")
print(f"  Classes : {NUM_CLASSES}  |  Image size : 32x32 RGB\n")

# Normalise pixel values to [0, 1]
x_train = x_train.astype("float32") / 255.0
x_test  = x_test.astype("float32")  / 255.0

# One-hot encode labels
y_train_ohe = tf.keras.utils.to_categorical(y_train, NUM_CLASSES)
y_test_ohe  = tf.keras.utils.to_categorical(y_test,  NUM_CLASSES)
y_train_flat = y_train.flatten()
y_test_flat  = y_test.flatten()
Downloading data from https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz
170498071/170498071 ━━━━━━━━━━━━━━━━━━━━ 10s 0us/step
[Dataset]
  Train : (50000, 32, 32, 3)  |  Test : (10000, 32, 32, 3)
  Classes : 10  |  Image size : 32x32 RGB

3. Reshape for RNN Input¶

Each image is reshaped from (32, 32, 3) to (32 time steps, 96 features) so that each pixel row becomes one time step in the LSTM sequence.

In [ ]:
# Reshape for RNN input
# CIFAR-10: (N, 32, 32, 3) - (N, 32 time_steps, 96 features)
# Each of 32 pixel rows becomes one time step.
# Each time step contains 32 pixels × 3 RGB channels = 96 features.
TIME_STEPS = 32   # one per image row
FEATURES   = 96   # 32 pixels × 3 channels per row

x_train_rnn = x_train.reshape(-1, TIME_STEPS, FEATURES)
x_test_rnn  = x_test.reshape(-1,  TIME_STEPS, FEATURES)

print(f"[RNN Reshaping]")
print(f"  Original : {x_train.shape}")
print(f"  RNN input: {x_train_rnn.shape}  "
      f"({TIME_STEPS} time steps × {FEATURES} features)\n")
[RNN Reshaping]
  Original : (50000, 32, 32, 3)
  RNN input: (50000, 32, 96)  (32 time steps × 96 features)

4. Sample Image Visualisation¶

Ten sample images from the training set are displayed to confirm correct loading and class label alignment.

In [4]:
fig, axes = plt.subplots(2, 5, figsize=(12, 5))
fig.suptitle("Figure 1: Sample Images from the CIFAR-10 Dataset",
             fontsize=13, fontweight='bold')
for i, ax in enumerate(axes.flat):
    ax.imshow(x_train[i])
    ax.set_title(CLASS_NAMES[y_train_flat[i]], fontsize=9)
    ax.axis("off")
plt.tight_layout()
plt.savefig("fig1_sample_images.png", dpi=150, bbox_inches="tight")
plt.show()
print("[Saved] fig1_sample_images.png")
No description has been provided for this image
[Saved] fig1_sample_images.png

5. LSTM Model Architecture¶

A configurable stacked LSTM function is defined, comprising two LSTM layers, optional batch normalisation, dropout regularisation, and a softmax output layer.

In [ ]:
def build_lstm(units_1=128, units_2=64, dropout_rate=0.3,
               use_batchnorm=False, learning_rate=0.001, name="LSTM_Model"):
    model = models.Sequential(name=name)
    model.add(layers.Input(shape=(TIME_STEPS, FEATURES)))

    # LSTM Block 1 
    model.add(layers.LSTM(units_1, return_sequences=True))
    if use_batchnorm:
        model.add(layers.BatchNormalization())
    model.add(layers.Dropout(dropout_rate))

    # LSTM Block 2
    model.add(layers.LSTM(units_2))
    if use_batchnorm:
        model.add(layers.BatchNormalization())
    model.add(layers.Dropout(dropout_rate))

    # Classifier Head 
    model.add(layers.Dense(128, activation="relu"))
    model.add(layers.Dropout(dropout_rate))
    model.add(layers.Dense(NUM_CLASSES, activation="softmax"))

    model.compile(
        optimizer=tf.keras.optimizers.Adam(learning_rate=learning_rate),
        loss="categorical_crossentropy",
        metrics=["accuracy"]
    )
    return model

6. Training Callbacks¶

EarlyStopping halts training when validation accuracy ceases to improve, and ReduceLROnPlateau adaptively lowers the learning rate to refine convergence.

In [6]:
def get_callbacks():
    return [
        callbacks.EarlyStopping(
            monitor="val_accuracy",
            patience=8,
            restore_best_weights=True,
            verbose=1
        ),
        callbacks.ReduceLROnPlateau(
            monitor="val_loss",
            factor=0.5,
            patience=4,
            min_lr=1e-6,
            verbose=1
        )
    ]

7. Experiment Configurations¶

Two controlled configurations are defined: Exp-1 is the baseline with dropout 0.3 and no batch normalisation; Exp-2 increases dropout to 0.5 and adds batch normalisation to assess the effect of stronger regularisation.

In [7]:
EXPERIMENTS = {
    "Exp-1: Baseline LSTM (DR=0.3, no BN)": {
        "units_1": 128, "units_2": 64,
        "dropout_rate": 0.3, "use_batchnorm": False,
        "learning_rate": 0.001
    },
    "Exp-2: Regularised LSTM (DR=0.5, BN)": {
        "units_1": 128, "units_2": 64,
        "dropout_rate": 0.5, "use_batchnorm": True,
        "learning_rate": 0.001
    },
}

EPOCHS     = 20
BATCH_SIZE = 64

histories    = {}
trained_models = {}
results_rows = []

8. Model Training¶

Each experiment is trained sequentially under identical conditions; performance metrics and training time are recorded for comparison.

In [10]:
for exp_name, cfg in EXPERIMENTS.items():
    line = "\u2500"

    print(f"\n{line*65}")
    print(f"  {exp_name}")
    print(f"{line*65}")

    model = build_lstm(**cfg, name=exp_name.split(":")[0].replace(" ", "_"))
    model.summary()

    t0 = time.time()
    history = model.fit(
        x_train_rnn, y_train_ohe,
        epochs=EPOCHS,
        batch_size=BATCH_SIZE,
        validation_data=(x_test_rnn, y_test_ohe),
        callbacks=get_callbacks(),
        verbose=1
    )
    elapsed = time.time() - t0

    # Evaluate
    test_loss, test_acc = model.evaluate(x_test_rnn, y_test_ohe, verbose=0)
    best_val = max(history.history["val_accuracy"])
    epochs_run = len(history.history["loss"])
    final_lr   = float(history.history.get("lr", [cfg["learning_rate"]])[-1])

    print(f"\n  Test Accuracy : {test_acc:.4f} ({test_acc*100:.2f}%)")
    print(f"  Best Val Acc  : {best_val:.4f}")
    print(f"  Epochs run    : {epochs_run} / {EPOCHS}")
    print(f"  Training time : {elapsed:.1f}s")

    histories[exp_name]     = history
    trained_models[exp_name] = model

    results_rows.append({
        "Experiment"       : exp_name,
        "LSTM Units"       : f"{cfg['units_1']}/{cfg['units_2']}",
        "Dropout"          : cfg["dropout_rate"],
        "Batch Norm"       : "Yes" if cfg["use_batchnorm"] else "No",
        "Epochs Run"       : epochs_run,
        "Best Val Acc"     : round(best_val,  4),
        "Test Accuracy"    : round(test_acc,  4),
        "Test Loss"        : round(test_loss, 4),
        "Train Time (s)"   : round(elapsed,   1),
    })
─────────────────────────────────────────────────────────────────
  Exp-1: Baseline LSTM (DR=0.3, no BN)
─────────────────────────────────────────────────────────────────
Model: "Exp-1"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                    ┃ Output Shape           ┃       Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ lstm (LSTM)                     │ (None, 32, 128)        │       115,200 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dropout (Dropout)               │ (None, 32, 128)        │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ lstm_1 (LSTM)                   │ (None, 64)             │        49,408 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dropout_1 (Dropout)             │ (None, 64)             │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense (Dense)                   │ (None, 128)            │         8,320 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dropout_2 (Dropout)             │ (None, 128)            │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_1 (Dense)                 │ (None, 10)             │         1,290 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 174,218 (680.54 KB)
 Trainable params: 174,218 (680.54 KB)
 Non-trainable params: 0 (0.00 B)
Epoch 1/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 61s 70ms/step - accuracy: 0.3026 - loss: 1.8860 - val_accuracy: 0.3998 - val_loss: 1.6365 - learning_rate: 0.0010
Epoch 2/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 50s 64ms/step - accuracy: 0.3975 - loss: 1.6424 - val_accuracy: 0.4500 - val_loss: 1.4986 - learning_rate: 0.0010
Epoch 3/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 54s 69ms/step - accuracy: 0.4436 - loss: 1.5308 - val_accuracy: 0.4789 - val_loss: 1.4371 - learning_rate: 0.0010
Epoch 4/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 52s 67ms/step - accuracy: 0.4805 - loss: 1.4412 - val_accuracy: 0.4969 - val_loss: 1.3734 - learning_rate: 0.0010
Epoch 5/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 47s 60ms/step - accuracy: 0.5065 - loss: 1.3784 - val_accuracy: 0.5076 - val_loss: 1.3517 - learning_rate: 0.0010
Epoch 6/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 54s 69ms/step - accuracy: 0.5246 - loss: 1.3276 - val_accuracy: 0.5224 - val_loss: 1.3229 - learning_rate: 0.0010
Epoch 7/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 53s 68ms/step - accuracy: 0.5452 - loss: 1.2767 - val_accuracy: 0.5269 - val_loss: 1.3185 - learning_rate: 0.0010
Epoch 8/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 57s 72ms/step - accuracy: 0.5624 - loss: 1.2355 - val_accuracy: 0.5435 - val_loss: 1.2702 - learning_rate: 0.0010
Epoch 9/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 51s 66ms/step - accuracy: 0.5751 - loss: 1.1977 - val_accuracy: 0.5357 - val_loss: 1.3045 - learning_rate: 0.0010
Epoch 10/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 53s 67ms/step - accuracy: 0.5877 - loss: 1.1673 - val_accuracy: 0.5622 - val_loss: 1.2295 - learning_rate: 0.0010
Epoch 11/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 51s 66ms/step - accuracy: 0.6009 - loss: 1.1345 - val_accuracy: 0.5583 - val_loss: 1.2433 - learning_rate: 0.0010
Epoch 12/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 57s 73ms/step - accuracy: 0.6080 - loss: 1.1097 - val_accuracy: 0.5666 - val_loss: 1.2258 - learning_rate: 0.0010
Epoch 13/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 54s 69ms/step - accuracy: 0.6172 - loss: 1.0815 - val_accuracy: 0.5747 - val_loss: 1.2219 - learning_rate: 0.0010
Epoch 14/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 68s 87ms/step - accuracy: 0.6273 - loss: 1.0579 - val_accuracy: 0.5649 - val_loss: 1.2506 - learning_rate: 0.0010
Epoch 15/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 69s 88ms/step - accuracy: 0.6383 - loss: 1.0339 - val_accuracy: 0.5750 - val_loss: 1.2174 - learning_rate: 0.0010
Epoch 16/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 64s 81ms/step - accuracy: 0.6458 - loss: 1.0093 - val_accuracy: 0.5765 - val_loss: 1.2292 - learning_rate: 0.0010
Epoch 17/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 68s 87ms/step - accuracy: 0.6504 - loss: 0.9981 - val_accuracy: 0.5803 - val_loss: 1.2168 - learning_rate: 0.0010
Epoch 18/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 47s 60ms/step - accuracy: 0.6602 - loss: 0.9669 - val_accuracy: 0.5666 - val_loss: 1.2586 - learning_rate: 0.0010
Epoch 19/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 57s 73ms/step - accuracy: 0.6653 - loss: 0.9570 - val_accuracy: 0.5661 - val_loss: 1.2975 - learning_rate: 0.0010
Epoch 20/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 90s 83ms/step - accuracy: 0.6718 - loss: 0.9396 - val_accuracy: 0.5728 - val_loss: 1.2598 - learning_rate: 0.0010
Restoring model weights from the end of the best epoch: 17.

  Test Accuracy : 0.5803 (58.03%)
  Best Val Acc  : 0.5803
  Epochs run    : 20 / 20
  Training time : 1159.3s

─────────────────────────────────────────────────────────────────
  Exp-2: Regularised LSTM (DR=0.5, BN)
─────────────────────────────────────────────────────────────────
Model: "Exp-2"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                    ┃ Output Shape           ┃       Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ lstm_2 (LSTM)                   │ (None, 32, 128)        │       115,200 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ batch_normalization             │ (None, 32, 128)        │           512 │
│ (BatchNormalization)            │                        │               │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dropout_3 (Dropout)             │ (None, 32, 128)        │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ lstm_3 (LSTM)                   │ (None, 64)             │        49,408 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ batch_normalization_1           │ (None, 64)             │           256 │
│ (BatchNormalization)            │                        │               │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dropout_4 (Dropout)             │ (None, 64)             │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_2 (Dense)                 │ (None, 128)            │         8,320 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dropout_5 (Dropout)             │ (None, 128)            │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_3 (Dense)                 │ (None, 10)             │         1,290 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 174,986 (683.54 KB)
 Trainable params: 174,602 (682.04 KB)
 Non-trainable params: 384 (1.50 KB)
Epoch 1/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 74s 86ms/step - accuracy: 0.2421 - loss: 2.1173 - val_accuracy: 0.3047 - val_loss: 1.8896 - learning_rate: 0.0010
Epoch 2/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 57s 73ms/step - accuracy: 0.3392 - loss: 1.7892 - val_accuracy: 0.3559 - val_loss: 1.7525 - learning_rate: 0.0010
Epoch 3/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 52s 67ms/step - accuracy: 0.3894 - loss: 1.6758 - val_accuracy: 0.3849 - val_loss: 1.7191 - learning_rate: 0.0010
Epoch 4/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 48s 61ms/step - accuracy: 0.4187 - loss: 1.5978 - val_accuracy: 0.4238 - val_loss: 1.5978 - learning_rate: 0.0010
Epoch 5/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 47s 60ms/step - accuracy: 0.4432 - loss: 1.5406 - val_accuracy: 0.4307 - val_loss: 1.5737 - learning_rate: 0.0010
Epoch 6/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 46s 59ms/step - accuracy: 0.4665 - loss: 1.4848 - val_accuracy: 0.4743 - val_loss: 1.4338 - learning_rate: 0.0010
Epoch 7/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 68s 86ms/step - accuracy: 0.4844 - loss: 1.4458 - val_accuracy: 0.4925 - val_loss: 1.3721 - learning_rate: 0.0010
Epoch 8/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 63s 80ms/step - accuracy: 0.4965 - loss: 1.4085 - val_accuracy: 0.4968 - val_loss: 1.3686 - learning_rate: 0.0010
Epoch 9/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 69s 88ms/step - accuracy: 0.5092 - loss: 1.3775 - val_accuracy: 0.4986 - val_loss: 1.4000 - learning_rate: 0.0010
Epoch 10/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 59s 76ms/step - accuracy: 0.5223 - loss: 1.3460 - val_accuracy: 0.5059 - val_loss: 1.3504 - learning_rate: 0.0010
Epoch 11/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 45s 58ms/step - accuracy: 0.5345 - loss: 1.3193 - val_accuracy: 0.5190 - val_loss: 1.3130 - learning_rate: 0.0010
Epoch 12/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 45s 58ms/step - accuracy: 0.5430 - loss: 1.2926 - val_accuracy: 0.5382 - val_loss: 1.3005 - learning_rate: 0.0010
Epoch 13/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 47s 60ms/step - accuracy: 0.5502 - loss: 1.2747 - val_accuracy: 0.5582 - val_loss: 1.2256 - learning_rate: 0.0010
Epoch 14/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 48s 62ms/step - accuracy: 0.5600 - loss: 1.2501 - val_accuracy: 0.5604 - val_loss: 1.2255 - learning_rate: 0.0010
Epoch 15/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 49s 63ms/step - accuracy: 0.5653 - loss: 1.2334 - val_accuracy: 0.5609 - val_loss: 1.2281 - learning_rate: 0.0010
Epoch 16/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 49s 62ms/step - accuracy: 0.5755 - loss: 1.2091 - val_accuracy: 0.5728 - val_loss: 1.1956 - learning_rate: 0.0010
Epoch 17/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 48s 61ms/step - accuracy: 0.5815 - loss: 1.1974 - val_accuracy: 0.5652 - val_loss: 1.2190 - learning_rate: 0.0010
Epoch 18/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 49s 63ms/step - accuracy: 0.5895 - loss: 1.1765 - val_accuracy: 0.5798 - val_loss: 1.1862 - learning_rate: 0.0010
Epoch 19/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 64s 82ms/step - accuracy: 0.5946 - loss: 1.1541 - val_accuracy: 0.5785 - val_loss: 1.1776 - learning_rate: 0.0010
Epoch 20/20
782/782 ━━━━━━━━━━━━━━━━━━━━ 54s 69ms/step - accuracy: 0.6042 - loss: 1.1393 - val_accuracy: 0.5602 - val_loss: 1.2394 - learning_rate: 0.0010
Restoring model weights from the end of the best epoch: 18.

  Test Accuracy : 0.5798 (57.98%)
  Best Val Acc  : 0.5798
  Epochs run    : 20 / 20
  Training time : 1085.1s

9. Results Summary Table¶

All experimental outcomes are consolidated into a summary table for direct comparison of accuracy, loss, and training duration.

In [11]:
df = pd.DataFrame(results_rows)
print("\n\n" + "=" * 65)
print("  RESULTS SUMMARY")
print("=" * 65)
print(df.to_string(index=False))

=================================================================
  RESULTS SUMMARY
=================================================================
                          Experiment LSTM Units  Dropout Batch Norm  Epochs Run  Best Val Acc  Test Accuracy  Test Loss  Train Time (s)
Exp-1: Baseline LSTM (DR=0.3, no BN)     128/64      0.3         No          20        0.5803         0.5803     1.2168          1159.3
Exp-2: Regularised LSTM (DR=0.5, BN)     128/64      0.5        Yes          20        0.5798         0.5798     1.1862          1085.1

10. Training Curves - Experiment 1 (Baseline)¶

Accuracy and loss curves for the baseline LSTM are plotted per epoch to assess convergence and the presence of overfitting.

In [12]:
exp1_name = list(EXPERIMENTS.keys())[0]
h1 = histories[exp1_name]

fig, axes = plt.subplots(1, 2, figsize=(13, 5))
fig.suptitle("Figure 2: Exp-1 Baseline LSTM \u2014 Training Curves", fontsize=12)

axes[0].plot(h1.history["accuracy"],     label="Train",      linewidth=2)
axes[0].plot(h1.history["val_accuracy"], label="Validation", linewidth=2, linestyle="--")
axes[0].set_title("Accuracy per Epoch")
axes[0].set_xlabel("Epoch"); axes[0].set_ylabel("Accuracy")
axes[0].legend(); axes[0].grid(alpha=0.3)
axes[0].set_ylim(0, 1)

axes[1].plot(h1.history["loss"],     label="Train",      linewidth=2)
axes[1].plot(h1.history["val_loss"], label="Validation", linewidth=2, linestyle="--")
axes[1].set_title("Loss per Epoch")
axes[1].set_xlabel("Epoch"); axes[1].set_ylabel("Loss")
axes[1].legend(); axes[1].grid(alpha=0.3)

plt.tight_layout()
plt.savefig("fig2_exp1_curves.png", dpi=150, bbox_inches="tight")
plt.show()
print("\n[Saved] fig2_exp1_curves.png")
No description has been provided for this image
[Saved] fig2_exp1_curves.png

11. Training Curves - Experiment 2 (Regularised)¶

Training curves for the regularised LSTM are plotted to compare the effect of increased dropout and batch normalisation on the train-validation gap.

In [13]:
exp2_name = list(EXPERIMENTS.keys())[1]
h2 = histories[exp2_name]

fig, axes = plt.subplots(1, 2, figsize=(13, 5))
fig.suptitle("Figure 3: Exp-2 Regularised LSTM \u2014 Training Curves", fontsize=12)

axes[0].plot(h2.history["accuracy"],     label="Train",      linewidth=2, color="#2ca02c")
axes[0].plot(h2.history["val_accuracy"], label="Validation", linewidth=2, linestyle="--", color="#2ca02c")
axes[0].set_title("Accuracy per Epoch")
axes[0].set_xlabel("Epoch"); axes[0].set_ylabel("Accuracy")
axes[0].legend(); axes[0].grid(alpha=0.3)
axes[0].set_ylim(0, 1)

axes[1].plot(h2.history["loss"],     label="Train",      linewidth=2, color="#2ca02c")
axes[1].plot(h2.history["val_loss"], label="Validation", linewidth=2, linestyle="--", color="#2ca02c")
axes[1].set_title("Loss per Epoch")
axes[1].set_xlabel("Epoch"); axes[1].set_ylabel("Loss")
axes[1].legend(); axes[1].grid(alpha=0.3)

plt.tight_layout()
plt.savefig("fig3_exp2_curves.png", dpi=150, bbox_inches="tight")
plt.show()
print("[Saved] fig3_exp2_curves.png")
No description has been provided for this image
[Saved] fig3_exp2_curves.png

12. Accuracy Comparison - Experiment 1 vs Experiment 2¶

A bar chart provides a direct visual comparison of test accuracy between both experimental configurations.

In [14]:
labels  = ["Exp-1\nBaseline LSTM", "Exp-2\nRegularised LSTM"]
accs    = df["Test Accuracy"].tolist()
colours = ["#d62728", "#2ca02c"] if accs[1] >= accs[0] else ["#2ca02c", "#d62728"]

fig, ax = plt.subplots(figsize=(7, 5))
bars = ax.bar(labels, accs, color=colours, width=0.45, edgecolor="white")
ax.set_ylim(0, 1)
ax.set_ylabel("Test Accuracy")
ax.set_title("Figure 4: Test Accuracy Comparison \u2014 Exp-1 vs Exp-2", fontweight='bold')
ax.grid(axis="y", alpha=0.3)
for bar, val in zip(bars, accs):
    ax.text(bar.get_x() + bar.get_width() / 2,
            bar.get_height() + 0.01,
            f"{val:.4f}\n({val*100:.2f}%)",
            ha="center", va="bottom", fontsize=10, fontweight='bold')
plt.tight_layout()
plt.savefig("fig4_accuracy_comparison.png", dpi=150, bbox_inches="tight")
plt.show()
print("[Saved] fig4_accuracy_comparison.png")
No description has been provided for this image
[Saved] fig4_accuracy_comparison.png

13. Confusion Matrix - Best Model¶

Both raw count and row-normalised confusion matrices are generated for the best-performing model to identify systematic misclassification patterns across all ten classes.

In [15]:
best_exp  = df.loc[df["Test Accuracy"].idxmax(), "Experiment"]
best_model = trained_models[best_exp]

y_pred = np.argmax(best_model.predict(x_test_rnn, verbose=0), axis=1)
cm     = confusion_matrix(y_test_flat, y_pred)
cm_pct = cm.astype("float") / cm.sum(axis=1)[:, np.newaxis]

fig, axes = plt.subplots(1, 2, figsize=(18, 7))
fig.suptitle(f"Figure 5: Confusion Matrices \u2014 Best Model ({best_exp.split(':')[0]})",
             fontsize=12, fontweight='bold')

sns.heatmap(cm, annot=True, fmt="d", cmap="Blues",
            xticklabels=CLASS_NAMES, yticklabels=CLASS_NAMES,
            ax=axes[0], annot_kws={"size": 8})
axes[0].set_title("Count")
axes[0].set_ylabel("True Label"); axes[0].set_xlabel("Predicted Label")
axes[0].tick_params(axis="x", rotation=45)

sns.heatmap(cm_pct, annot=True, fmt=".2f", cmap="Blues",
            xticklabels=CLASS_NAMES, yticklabels=CLASS_NAMES,
            ax=axes[1], annot_kws={"size": 8})
axes[1].set_title("Normalised (row %)")
axes[1].set_ylabel("True Label"); axes[1].set_xlabel("Predicted Label")
axes[1].tick_params(axis="x", rotation=45)

plt.tight_layout()
plt.savefig("fig5_confusion_matrix.png", dpi=150, bbox_inches="tight")
plt.show()
print("[Saved] fig5_confusion_matrix.png")
No description has been provided for this image
[Saved] fig5_confusion_matrix.png

14. Per-Class Classification Report¶

Precision, recall, and F1-score are computed per class using Scikit-Learn and visualised as a heatmap to highlight class-level performance differences.

In [16]:
report_str  = classification_report(y_test_flat, y_pred, target_names=CLASS_NAMES)
report_dict = classification_report(y_test_flat, y_pred,
                                    target_names=CLASS_NAMES, output_dict=True)
print("\n[Best Model Classification Report]")
print(report_str)

# Plot classification report as heatmap
report_df = pd.DataFrame(report_dict).T
metrics_df = report_df.iloc[:-3][["precision", "recall", "f1-score"]].astype(float)

fig, ax = plt.subplots(figsize=(9, 7))
sns.heatmap(metrics_df, annot=True, fmt=".3f", cmap="YlGnBu",
            linewidths=0.5, vmin=0, vmax=1, ax=ax,
            annot_kws={"size": 10})
ax.set_title(f"Figure 6: Per-Class Metrics \u2014 Best Model ({best_exp.split(':')[0]})",
             fontsize=11, fontweight='bold')
ax.set_ylabel("Class"); ax.set_xlabel("Metric")
ax.tick_params(axis="y", rotation=0)
plt.tight_layout()
plt.savefig("fig6_classification_report.png", dpi=150, bbox_inches="tight")
plt.show()
print("[Saved] fig6_classification_report.png")
[Best Model Classification Report]
              precision    recall  f1-score   support

    airplane       0.62      0.70      0.65      1000
  automobile       0.65      0.81      0.72      1000
        bird       0.46      0.39      0.43      1000
         cat       0.42      0.37      0.39      1000
        deer       0.49      0.55      0.52      1000
         dog       0.46      0.49      0.48      1000
        frog       0.60      0.62      0.61      1000
       horse       0.63      0.64      0.63      1000
        ship       0.77      0.68      0.72      1000
       truck       0.69      0.57      0.63      1000

    accuracy                           0.58     10000
   macro avg       0.58      0.58      0.58     10000
weighted avg       0.58      0.58      0.58     10000

No description has been provided for this image
[Saved] fig6_classification_report.png

15. Save Results Summary¶

All numerical results, the classification report, and the normalised confusion matrix are written to a plain-text file for reference during report writing.

In [17]:
with open("results_summary.txt", "w") as f:
    f.write("=" * 65 + "\n")
    f.write("ACCA7025 \u2014 CIFAR-10 LSTM RESULTS SUMMARY\n")
    f.write("=" * 65 + "\n\n")

    f.write("EXPERIMENT RESULTS\n")
    f.write("-" * 65 + "\n")
    f.write(df.to_string(index=False))
    f.write("\n\n")

    f.write("BEST MODEL\n")
    f.write("-" * 65 + "\n")
    f.write(f"  Name          : {best_exp}\n")
    f.write(f"  Test Accuracy : {df.loc[df['Test Accuracy'].idxmax(),'Test Accuracy']:.4f}\n")
    f.write(f"  Test Loss     : {df.loc[df['Test Accuracy'].idxmax(),'Test Loss']:.4f}\n")
    f.write(f"  Epochs Run    : {df.loc[df['Test Accuracy'].idxmax(),'Epochs Run']}\n\n")

    f.write("PER-CLASS CLASSIFICATION REPORT (Best Model)\n")
    f.write("-" * 65 + "\n")
    f.write(report_str)
    f.write("\n")

    f.write("CONFUSION MATRIX (Best Model \u2014 row-normalised)\n")
    f.write("-" * 65 + "\n")
    cm_df = pd.DataFrame(cm_pct.round(3), index=CLASS_NAMES, columns=CLASS_NAMES)
    f.write(cm_df.to_string())
    f.write("\n")

print("\n[Saved] results_summary.txt")

print("\n" + "=" * 65)
print("  ALL DONE \u2014 FILES SAVED")
print("=" * 65)
print("  fig1_sample_images.png")
print("  fig2_exp1_curves.png")
print("  fig3_exp2_curves.png")
print("  fig4_accuracy_comparison.png")
print("  fig5_confusion_matrix.png")
print("  fig6_classification_report.png")
print("  results_summary.txt")
print("=" * 65)
[Saved] results_summary.txt

=================================================================
  ALL DONE — FILES SAVED
=================================================================
  fig1_sample_images.png
  fig2_exp1_curves.png
  fig3_exp2_curves.png
  fig4_accuracy_comparison.png
  fig5_confusion_matrix.png
  fig6_classification_report.png
  results_summary.txt
=================================================================
In [ ]: