From ed7c63f70b3a44d31069c6c88ed470fca30838e1 Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Thu, 2 Oct 2025 12:07:22 +1000 Subject: [PATCH 01/31] Added project folder Alzheimers_Classifier_s4693608 for Task 8 with initial README --- recognition/Alzheimers_Classifier_s4693608/README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 recognition/Alzheimers_Classifier_s4693608/README.md diff --git a/recognition/Alzheimers_Classifier_s4693608/README.md b/recognition/Alzheimers_Classifier_s4693608/README.md new file mode 100644 index 000000000..019517b3d --- /dev/null +++ b/recognition/Alzheimers_Classifier_s4693608/README.md @@ -0,0 +1 @@ +"# Alzheimer's Classifier (Task 8)" From 2eb58b6b8d9c7735f9c3196a546b90fd3a09eae8 Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Fri, 3 Oct 2025 13:31:58 +1000 Subject: [PATCH 02/31] Added initial Python files (modules.py, dataset.py, train.py, predict.py) --- recognition/Alzheimers_Classifier_s4693608/dataset.py | 0 recognition/Alzheimers_Classifier_s4693608/modules.py | 0 recognition/Alzheimers_Classifier_s4693608/predict.py | 0 recognition/Alzheimers_Classifier_s4693608/train.py | 0 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 recognition/Alzheimers_Classifier_s4693608/dataset.py create mode 100644 recognition/Alzheimers_Classifier_s4693608/modules.py create mode 100644 recognition/Alzheimers_Classifier_s4693608/predict.py create mode 100644 recognition/Alzheimers_Classifier_s4693608/train.py diff --git a/recognition/Alzheimers_Classifier_s4693608/dataset.py b/recognition/Alzheimers_Classifier_s4693608/dataset.py new file mode 100644 index 000000000..e69de29bb diff --git a/recognition/Alzheimers_Classifier_s4693608/modules.py b/recognition/Alzheimers_Classifier_s4693608/modules.py new file mode 100644 index 000000000..e69de29bb diff --git a/recognition/Alzheimers_Classifier_s4693608/predict.py b/recognition/Alzheimers_Classifier_s4693608/predict.py new file mode 100644 index 000000000..e69de29bb diff --git a/recognition/Alzheimers_Classifier_s4693608/train.py b/recognition/Alzheimers_Classifier_s4693608/train.py new file mode 100644 index 000000000..e69de29bb From b4d4f9883fb204b4e213993991a64b7dd4c673b5 Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Fri, 3 Oct 2025 14:27:37 +1000 Subject: [PATCH 03/31] Added dataset.py skeleton --- .../Alzheimers_Classifier_s4693608/dataset.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/recognition/Alzheimers_Classifier_s4693608/dataset.py b/recognition/Alzheimers_Classifier_s4693608/dataset.py index e69de29bb..a27eda849 100644 --- a/recognition/Alzheimers_Classifier_s4693608/dataset.py +++ b/recognition/Alzheimers_Classifier_s4693608/dataset.py @@ -0,0 +1,51 @@ +import os +import torch +from torch.utils.data import Dataset, DataLoader +import nibabel as nib +import numpy as np +import torchvision.transforms as transforms + +class ADNIDataset(Dataset): + """ + PyTorch Dataset class for loading and preprocessing ADNI brain MRI scans. + + This dataset takes a list of file paths (NIfTI format) and corresponding labels, + loads each scan, extracts a representative 2D slice, normalizes the image, and + optionally applies transforms before returning a tensor with its label. + + Args: + file_paths (list of str): List of paths to NIfTI image files (.nii or .nii.gz). + labels (list of int): List of integer labels corresponding to each file. + (e.g., 0 = Normal, 1 = Alzheimer's). + transform (callable, optional): A torchvision transform or custom function + applied to the image tensor. + + Returns: + tuple: (image_tensor, label), where + - image_tensor (torch.FloatTensor): Preprocessed 2D brain slice, shape [1, H, W]. + - label (torch.LongTensor): Class label for the image. + """ + +def get_dataloaders(train_files, val_files, test_files, + train_labels, val_labels, test_labels, + batch_size=16): + """ + Creates PyTorch DataLoader objects for training, validation, and testing. + + Given lists of file paths and labels for each split, this function constructs + ADNIDataset objects, applies preprocessing/transforms, and wraps them in + DataLoader objects for efficient batching and iteration. + + Args: + train_files (list of str): File paths for training images. + val_files (list of str): File paths for validation images. + test_files (list of str): File paths for testing images. + train_labels (list of int): Class labels for training images. + val_labels (list of int): Class labels for validation images. + test_labels (list of int): Class labels for testing images. + batch_size (int, optional): Number of samples per batch. Defaults to 16. + + Returns: + tuple: (train_loader, val_loader, test_loader), where each is a + torch.utils.data.DataLoader ready for use in training loops. + """ \ No newline at end of file From b745cd80b23232910b4d4d834093a6e4aa5d6846 Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Fri, 3 Oct 2025 20:32:59 +1000 Subject: [PATCH 04/31] Implemented image collection and DataLoader helper function --- .../Alzheimers_Classifier_s4693608/dataset.py | 91 ++++++++++++------- 1 file changed, 58 insertions(+), 33 deletions(-) diff --git a/recognition/Alzheimers_Classifier_s4693608/dataset.py b/recognition/Alzheimers_Classifier_s4693608/dataset.py index a27eda849..ff0d54ba3 100644 --- a/recognition/Alzheimers_Classifier_s4693608/dataset.py +++ b/recognition/Alzheimers_Classifier_s4693608/dataset.py @@ -1,51 +1,76 @@ import os import torch from torch.utils.data import Dataset, DataLoader -import nibabel as nib -import numpy as np +from PIL import Image import torchvision.transforms as transforms class ADNIDataset(Dataset): """ - PyTorch Dataset class for loading and preprocessing ADNI brain MRI scans. - - This dataset takes a list of file paths (NIfTI format) and corresponding labels, - loads each scan, extracts a representative 2D slice, normalizes the image, and - optionally applies transforms before returning a tensor with its label. + A PyTorch Dataset for loading 2D MRI image slices from the ADNI dataset, + organized into Alzheimer's disease (AD) and normal control (NC) categories. Args: - file_paths (list of str): List of paths to NIfTI image files (.nii or .nii.gz). - labels (list of int): List of integer labels corresponding to each file. - (e.g., 0 = Normal, 1 = Alzheimer's). - transform (callable, optional): A torchvision transform or custom function - applied to the image tensor. + root_dir (str): Path to the AD_NC directory containing "train" and "test". + split (str): Which dataset split to use, "train" or "test". + transform (callable, optional): Optional transform to be applied on an image. - Returns: - tuple: (image_tensor, label), where - - image_tensor (torch.FloatTensor): Preprocessed 2D brain slice, shape [1, H, W]. - - label (torch.LongTensor): Class label for the image. + Attributes: + samples (list): List of tuples (image_path, label) for all samples + in the specified split. """ -def get_dataloaders(train_files, val_files, test_files, - train_labels, val_labels, test_labels, - batch_size=16): + def __init__(self, root_dir, split="train", transform=None): + self.root_dir = os.path.join(root_dir, split) # path automatically goes to train folder + self.transform = transform + self.samples = [] # (image_path, label) + + # get all (image_path, label) pairs + for label_name, label in [("AD", 1), ("NC", 0)]: + class_dir = os.path.join(self.root_dir, label_name) + for fname in os.listdir(class_dir): + self.samples.append((os.path.join(class_dir, fname), label)) + + def __len__(self): + return len(self.samples) + + def __getitem__(self, idx): + img_path, label = self.samples[idx] + image = Image.open(img_path) + + if self.transform: + image = self.transform(image) + + return image, torch.tensor(label, dtype=torch.long) + +def get_dataloaders(root_dir, batch_size=16): """ - Creates PyTorch DataLoader objects for training, validation, and testing. + Create PyTorch DataLoaders for the ADNI dataset. - Given lists of file paths and labels for each split, this function constructs - ADNIDataset objects, applies preprocessing/transforms, and wraps them in - DataLoader objects for efficient batching and iteration. + This function initializes ADNIDataset instances for the training and + testing splits, applies preprocessing transforms (resize, tensor conversion, + normalisation), and returns DataLoaders for batched access. Args: - train_files (list of str): File paths for training images. - val_files (list of str): File paths for validation images. - test_files (list of str): File paths for testing images. - train_labels (list of int): Class labels for training images. - val_labels (list of int): Class labels for validation images. - test_labels (list of int): Class labels for testing images. - batch_size (int, optional): Number of samples per batch. Defaults to 16. + root_dir (str): Path to the AD_NC directory containing 'train' and 'test'. + batch_size (int, optional): Number of samples per batch. Default is 16. Returns: - tuple: (train_loader, val_loader, test_loader), where each is a - torch.utils.data.DataLoader ready for use in training loops. - """ \ No newline at end of file + tuple: + - train_loader (DataLoader): DataLoader for the training set, + with shuffling enabled. + - test_loader (DataLoader): DataLoader for the test set, + with shuffling disabled. + """ + transform = transforms.Compose([ + transforms.Resize((224, 224)), + transform.ToTensor(), + transform.Normalize(mean=[0.5], std=[0.5]) + ]) + + train_dataset = ADNIDataset(os.path.join(root_dir, "AD_NC"), split="train", transform=transform) + test_dataset = ADNIDataset(os.path.join(root_dir, "AD_NC"), split="test", transform=transform) + + train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) + test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False) + + return train_loader \ No newline at end of file From 04727ad83957f4d62912dcca36531b600138851d Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Sat, 4 Oct 2025 12:10:40 +1000 Subject: [PATCH 05/31] Tested dataset loader and fixed errors --- recognition/Alzheimers_Classifier_s4693608/dataset.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/recognition/Alzheimers_Classifier_s4693608/dataset.py b/recognition/Alzheimers_Classifier_s4693608/dataset.py index ff0d54ba3..a36148a88 100644 --- a/recognition/Alzheimers_Classifier_s4693608/dataset.py +++ b/recognition/Alzheimers_Classifier_s4693608/dataset.py @@ -63,8 +63,8 @@ def get_dataloaders(root_dir, batch_size=16): """ transform = transforms.Compose([ transforms.Resize((224, 224)), - transform.ToTensor(), - transform.Normalize(mean=[0.5], std=[0.5]) + transforms.ToTensor(), + transforms.Normalize(mean=[0.5], std=[0.5]) ]) train_dataset = ADNIDataset(os.path.join(root_dir, "AD_NC"), split="train", transform=transform) @@ -73,4 +73,4 @@ def get_dataloaders(root_dir, batch_size=16): train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False) - return train_loader \ No newline at end of file + return train_loader, test_loader \ No newline at end of file From 9b06cc59943d21dbdecc485f6acccd92e94b008d Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Sat, 4 Oct 2025 12:20:45 +1000 Subject: [PATCH 06/31] Added ConvNeXt classifier skeleton --- .../Alzheimers_Classifier_s4693608/modules.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/recognition/Alzheimers_Classifier_s4693608/modules.py b/recognition/Alzheimers_Classifier_s4693608/modules.py index e69de29bb..d6b638d66 100644 --- a/recognition/Alzheimers_Classifier_s4693608/modules.py +++ b/recognition/Alzheimers_Classifier_s4693608/modules.py @@ -0,0 +1,24 @@ +import torch +import torch.nn as nn +import timm + +class AlzheimersClassifier(nn.Module): + """ + ConvNeXt-based classifier for Alzheimer's (AD) vs Normal Control (NC). + + This class wraps a pretrained ConvNeXt model from the `timm` library and modifies it to: + - Accept grayscale input images (1 channel) by duplicating them into 3 channels, + since ConvNeXt expects RGB input. + - Replace the final classification head with a linear layer outputting 2 classes + (Alzheimer's disease = 1, Normal Control = 0). + + Attributes: + model : timm.models.ConvNeXt + The ConvNeXt backbone model with a modified classifier head. + + Methods: + forward(x: torch.Tensor) -> torch.Tensor + Performs a forward pass through the network. + Takes grayscale input of shape [B, 1, H, W], duplicates channels, + and outputs logits of shape [B, 2]. + """ \ No newline at end of file From 77d501a4d6359ccdb94f9afcb71bb05736a1adac Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Sat, 4 Oct 2025 12:26:11 +1000 Subject: [PATCH 07/31] Implemented ConvNeXt model loading with pretrained weights --- .../Alzheimers_Classifier_s4693608/modules.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/recognition/Alzheimers_Classifier_s4693608/modules.py b/recognition/Alzheimers_Classifier_s4693608/modules.py index d6b638d66..29971e713 100644 --- a/recognition/Alzheimers_Classifier_s4693608/modules.py +++ b/recognition/Alzheimers_Classifier_s4693608/modules.py @@ -21,4 +21,20 @@ class AlzheimersClassifier(nn.Module): Performs a forward pass through the network. Takes grayscale input of shape [B, 1, H, W], duplicates channels, and outputs logits of shape [B, 2]. - """ \ No newline at end of file + """ + def __init__(self, model_name="convnext_tiny", num_classes=2, pretrained=True): + super().__init__() + # Load pretrained ConvNeXt backbone + self.model = timm.create_model(model_name, pretrained=pretrained) + + # Replace classifier head (for 2 classes instead of 1000) + in_features = self.model.get_classifier().in_features + self.model.reset_classifier(num_classes) + + # Overwrite with new classifier layer + self.model.head = nn.Linear(in_features, num_classes) + + def forward(self, x): + # duplicate x channels ([B, 1, H, W] -> [B, 3, H, W]) + x = x.repeat(1, 3, 1, 1) + return self.model(x) \ No newline at end of file From c1b6d0275e2227fc2ffbff994b53fba15d6575b5 Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Sat, 4 Oct 2025 12:38:25 +1000 Subject: [PATCH 08/31] Tested forward pass with dummy data and fixed errors --- recognition/Alzheimers_Classifier_s4693608/modules.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/recognition/Alzheimers_Classifier_s4693608/modules.py b/recognition/Alzheimers_Classifier_s4693608/modules.py index 29971e713..78f607e83 100644 --- a/recognition/Alzheimers_Classifier_s4693608/modules.py +++ b/recognition/Alzheimers_Classifier_s4693608/modules.py @@ -28,11 +28,7 @@ def __init__(self, model_name="convnext_tiny", num_classes=2, pretrained=True): self.model = timm.create_model(model_name, pretrained=pretrained) # Replace classifier head (for 2 classes instead of 1000) - in_features = self.model.get_classifier().in_features - self.model.reset_classifier(num_classes) - - # Overwrite with new classifier layer - self.model.head = nn.Linear(in_features, num_classes) + self.model.reset_classifier(num_classes=num_classes) def forward(self, x): # duplicate x channels ([B, 1, H, W] -> [B, 3, H, W]) From defb86095575d890453738ccc20d4a31ed5d07d5 Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Sat, 4 Oct 2025 13:23:19 +1000 Subject: [PATCH 09/31] Added train.py skeleton --- .../Alzheimers_Classifier_s4693608/train.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/recognition/Alzheimers_Classifier_s4693608/train.py b/recognition/Alzheimers_Classifier_s4693608/train.py index e69de29bb..824f4da63 100644 --- a/recognition/Alzheimers_Classifier_s4693608/train.py +++ b/recognition/Alzheimers_Classifier_s4693608/train.py @@ -0,0 +1,32 @@ +import os +import torch +import torch.nn as nn +import torch.optim as optim +from dataset import get_dataloaders +from modules import AlzheimersClassifier +import matplotlib.pyplot as plt + +def train_model(root_dir, epochs=10, batch_szie=16, lr=1e-4, device='cuda'): + """ + Train and evaluate a deep learning model for Alzheimer's disease classification. + + This function performs supervised training of a model using the given training + data, evaluates it on the test data at each epoch, and reports progress. + + Args: + root_dir (str): Path to the ADNI dataset root directory. + epochs (int, optional): Number of training epochs. Default is 10. + batch_size (int, optional): Number of samples per training batch. Default is 16. + lr (float, optional): Learning rate for the optimiser. Default is 1e-4. + device (str, optional): Device to run training on ('cuda' or 'cpu'). Default is "cuda". + + Returns: + None + + Attributes: + - Saves the best model's state_dict to "best_model.pth" in the current directory. + - Generates and saves two plots: + - "loss_curve.png" for training and validation loss + - "val_acc_curve.png" for validation accuracy + - Prints training/validation progress and best validation accuracy to the console. + """ \ No newline at end of file From fd25c4eae6940367a94507ea645e0b6382004da4 Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Sat, 4 Oct 2025 13:32:25 +1000 Subject: [PATCH 10/31] Implemented training loop --- .../Alzheimers_Classifier_s4693608/train.py | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/recognition/Alzheimers_Classifier_s4693608/train.py b/recognition/Alzheimers_Classifier_s4693608/train.py index 824f4da63..286d5c26e 100644 --- a/recognition/Alzheimers_Classifier_s4693608/train.py +++ b/recognition/Alzheimers_Classifier_s4693608/train.py @@ -6,7 +6,7 @@ from modules import AlzheimersClassifier import matplotlib.pyplot as plt -def train_model(root_dir, epochs=10, batch_szie=16, lr=1e-4, device='cuda'): +def train_model(root_dir, epochs=10, batch_size=16, lr=1e-4, device='cuda'): """ Train and evaluate a deep learning model for Alzheimer's disease classification. @@ -29,4 +29,31 @@ def train_model(root_dir, epochs=10, batch_szie=16, lr=1e-4, device='cuda'): - "loss_curve.png" for training and validation loss - "val_acc_curve.png" for validation accuracy - Prints training/validation progress and best validation accuracy to the console. - """ \ No newline at end of file + """ + train_loader, test_loader = get_dataloaders(root_dir, batch_size=batch_size) + + # Define model, loss, optimiser + model = AlzheimersClassifier().to(device) + criterion = nn.CrossEntropyLoss() + optimiser = optim.AdamW(model.parameters(), lr=lr) + + train_losses, val_losses = val_accs = [], [], [] + best_acc = 0.0 + + for epoch in range(epochs): + # Training + model.train() + running_loss = 0.0 + for images, labels in train_loader: + images, labels = images.to(device), labels.to(device) + + optimiser.zero_grad() + outputs = model(images) + loss = criterion(outputs, labels) + loss.backward() + optimiser.step() + + running_loss += loss.item() + + train_loss = running_loss / len(train_loader) + train_losses.append(train_loss) \ No newline at end of file From c9b8ffee87ebf9927e610c64ce14ef8963f23385 Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Mon, 6 Oct 2025 11:02:02 +1000 Subject: [PATCH 11/31] Added validation loop and accuracy calculation --- .../Alzheimers_Classifier_s4693608/train.py | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/recognition/Alzheimers_Classifier_s4693608/train.py b/recognition/Alzheimers_Classifier_s4693608/train.py index 286d5c26e..9a551940e 100644 --- a/recognition/Alzheimers_Classifier_s4693608/train.py +++ b/recognition/Alzheimers_Classifier_s4693608/train.py @@ -56,4 +56,49 @@ def train_model(root_dir, epochs=10, batch_size=16, lr=1e-4, device='cuda'): running_loss += loss.item() train_loss = running_loss / len(train_loader) - train_losses.append(train_loss) \ No newline at end of file + train_losses.append(train_loss) + + # Validation + model.eval() + correct, total, val_loss = 0, 0, 0.0 + with torch.no_grad(): + for images, labels in test_loader: + images, labels = images.to(device), labels.to(device) + outputs = model(images) + loss = criterion(outputs, labels) + val_loss += loss.item() + + _, predicted = torch.max(outputs, 1) + total += labels.size(0) + correct += (predicted == labels).sum().item() + + val_acc = correct / total + val_loss /= len(test_loader) + + val_losses.append(val_loss) + val_accs.append(val_acc) + + print(f"Epoch {epoch+1}/{epochs} | " + f"Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f} | Val Acc: {val_acc:.4f}") + + # Save the best model + if val_acc > best_acc: + best_acc = val_acc + torch.save(model.state_dict(), "best_model.pth") + print("New best model saved!") + + # Plot training curves + plt.figure() + plt.plot(train_losses, label="Train Loss") + plt.plot(val_losses, label="Val Loss") + plt.legend() + plt.title("Training and Validation Loss") + plt.savefig("loss_curve.png") + + plt.figure() + plt.plot(val_accs, label="Val Accuracy") + plt.legend() + plt.title("Validation Accuracy") + plt.savefig("val_acc_curve.png") + + print(f"Best Validation Accuracy: {best_acc:.4f}") \ No newline at end of file From caefe5f0a1bdfd3e889a23f23e46e457b9daac8d Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Mon, 6 Oct 2025 11:12:43 +1000 Subject: [PATCH 12/31] Added main loop --- recognition/Alzheimers_Classifier_s4693608/train.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/recognition/Alzheimers_Classifier_s4693608/train.py b/recognition/Alzheimers_Classifier_s4693608/train.py index 9a551940e..36caed57c 100644 --- a/recognition/Alzheimers_Classifier_s4693608/train.py +++ b/recognition/Alzheimers_Classifier_s4693608/train.py @@ -101,4 +101,8 @@ def train_model(root_dir, epochs=10, batch_size=16, lr=1e-4, device='cuda'): plt.title("Validation Accuracy") plt.savefig("val_acc_curve.png") - print(f"Best Validation Accuracy: {best_acc:.4f}") \ No newline at end of file + print(f"Best Validation Accuracy: {best_acc:.4f}") + +if __name__ == "__main__": + root_dir = "/home/groups/comp3710/ADNI" + train_model(root_dir, epochs=10, batch_size=16, lr=1e-4) \ No newline at end of file From 4ac5cddcfdaab8cebab3237e9fdf6376cad8ce78 Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Mon, 6 Oct 2025 14:45:34 +1000 Subject: [PATCH 13/31] Tested and fixed errors --- recognition/Alzheimers_Classifier_s4693608/train.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recognition/Alzheimers_Classifier_s4693608/train.py b/recognition/Alzheimers_Classifier_s4693608/train.py index 36caed57c..f2a998436 100644 --- a/recognition/Alzheimers_Classifier_s4693608/train.py +++ b/recognition/Alzheimers_Classifier_s4693608/train.py @@ -37,7 +37,7 @@ def train_model(root_dir, epochs=10, batch_size=16, lr=1e-4, device='cuda'): criterion = nn.CrossEntropyLoss() optimiser = optim.AdamW(model.parameters(), lr=lr) - train_losses, val_losses = val_accs = [], [], [] + train_losses, val_losses, val_accs = [], [], [] best_acc = 0.0 for epoch in range(epochs): From ebfc444ff3d5c0d79d7eb845ae92f9fcef9c6440 Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Mon, 6 Oct 2025 15:32:42 +1000 Subject: [PATCH 14/31] Added predict.py skeleton --- .../Alzheimers_Classifier_s4693608/predict.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/recognition/Alzheimers_Classifier_s4693608/predict.py b/recognition/Alzheimers_Classifier_s4693608/predict.py index e69de29bb..cdc9723d7 100644 --- a/recognition/Alzheimers_Classifier_s4693608/predict.py +++ b/recognition/Alzheimers_Classifier_s4693608/predict.py @@ -0,0 +1,39 @@ +import torch +from torchvision import transforms +from PIL import Image +from modules import AlzheimersClassifier + +MODEL_PATH = "best_model.pth" + +def load_model(model_path=MODEL_PATH): + """ + This function initializes an instance of the AlzheimersClassifier model, + loads the trained weights from the specified checkpoint file, and sets + the model to evaluation mode on the appropriate device (CPU or GPU). + + Args: + model_path (str, optional): Path to the saved model checkpoint (.pth file). + Defaults to the value of MODEL_PATH. + + Returns: + AlzheimersClassifier: + A ConvNeXt-based PyTorch model ready for inference. + """ + +def predict_image(image_path, model): + """ + The function loads a grayscale MRI slice, applies the same preprocessing + transformations used during training (resize, tensor conversion, normalization), + and performs a forward pass through the trained model to obtain the predicted + class and associated confidence score. + + Args: + image_path (str): Path to the input image (e.g., .jpeg slice). + model (torch.nn.Module): Trained AlzheimersClassifier model instance. + + Returns: + tuple: + - label (str): Predicted class label ("AD" or "NC"). + - confidence (float): Model confidence for the predicted class, + between 0.0 and 1.0. + """ \ No newline at end of file From 4000188112fc58bca165600da8cc271991772bd9 Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Mon, 6 Oct 2025 19:46:45 +1000 Subject: [PATCH 15/31] Implemented single image processing for testing --- .../Alzheimers_Classifier_s4693608/predict.py | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/recognition/Alzheimers_Classifier_s4693608/predict.py b/recognition/Alzheimers_Classifier_s4693608/predict.py index cdc9723d7..5bdf77d6f 100644 --- a/recognition/Alzheimers_Classifier_s4693608/predict.py +++ b/recognition/Alzheimers_Classifier_s4693608/predict.py @@ -3,9 +3,7 @@ from PIL import Image from modules import AlzheimersClassifier -MODEL_PATH = "best_model.pth" - -def load_model(model_path=MODEL_PATH): +def load_model(model_path="best_model.pth"): """ This function initializes an instance of the AlzheimersClassifier model, loads the trained weights from the specified checkpoint file, and sets @@ -13,12 +11,17 @@ def load_model(model_path=MODEL_PATH): Args: model_path (str, optional): Path to the saved model checkpoint (.pth file). - Defaults to the value of MODEL_PATH. + Defaults to the value of "best_model.pth". Returns: AlzheimersClassifier: A ConvNeXt-based PyTorch model ready for inference. """ + model = AlzheimersClassifier() + model.load_state_dict(torch.load(model_path, map_location="cuda")) + model.to("cuda") + model.eval() + return model def predict_image(image_path, model): """ @@ -36,4 +39,28 @@ class and associated confidence score. - label (str): Predicted class label ("AD" or "NC"). - confidence (float): Model confidence for the predicted class, between 0.0 and 1.0. - """ \ No newline at end of file + """ + image = Image.open(image_path) + + # Apply transform to image + transform = transforms.Compose([ + transforms.Resize((224, 224)), + transforms.ToTensor(), + transforms.Normalize(mean=[0.5], std=[0.5]) + ]) + image = transform(image).unsqueeze(0).to("cuda") + + with torch.no_grad(): + outputs = model(image) + probs = torch.softmax(outputs, dim=1) + conf, pred = torch.max(probs, dim=1) + + class_names = ["NC", "AD"] + label = class_names[pred.item()] + print(f"Prediction: {label} ({conf.item() * 100:.2f}% confidence)") + return label, conf.item() + +if __name__ == "__main__": + model = load_model() + test_image_path = "/home/groups/comp3710/ADNI/AD_NC/test/AD/388206_78.jpeg" + predict_image(test_image_path, model) \ No newline at end of file From c01b060f02c91d1231f608132b108d7564d01e21 Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Tue, 7 Oct 2025 15:47:20 +1000 Subject: [PATCH 16/31] Implemented evaluation of whole folders of AD/NC images --- .../Alzheimers_Classifier_s4693608/predict.py | 52 +++++++++++++++++-- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/recognition/Alzheimers_Classifier_s4693608/predict.py b/recognition/Alzheimers_Classifier_s4693608/predict.py index 5bdf77d6f..730b5ca6b 100644 --- a/recognition/Alzheimers_Classifier_s4693608/predict.py +++ b/recognition/Alzheimers_Classifier_s4693608/predict.py @@ -1,3 +1,4 @@ +import os import torch from torchvision import transforms from PIL import Image @@ -36,7 +37,7 @@ class and associated confidence score. Returns: tuple: - - label (str): Predicted class label ("AD" or "NC"). + - prediction (int): Predicted class label value (1 for "AD" or 0 for "NC"). - confidence (float): Model confidence for the predicted class, between 0.0 and 1.0. """ @@ -58,9 +59,52 @@ class and associated confidence score. class_names = ["NC", "AD"] label = class_names[pred.item()] print(f"Prediction: {label} ({conf.item() * 100:.2f}% confidence)") - return label, conf.item() + return pred.item(), conf.item() + +def evaluate_folder(model, folder_path, label): + """ + This function iterates through all `.jpeg` files in the specified folder, + performs inference using the provided model, and counts how many predictions + match the expected label. + + Args: + model : The trained PyTorch model used for prediction. + folder_path (str): Path to the folder containing `.jpeg` images to evaluate. + label (int): The ground truth label for all images in this folder + + Returns: + correct (int): Number of correctly classified images. + total (int): Total number of images evaluated. + conf_sum (float): Current sum of confidence of classifcation of images in the folder. + """ + # label = 1 for AD, label = 0 for NC + correct = total = conf_sum = 0 + for fname in os.listdir(folder_path): + fpath = os.path.join(folder_path, fname) + pred, conf = predict_image(fpath, model) + total += 1 + correct += int(pred == label) + conf_sum += conf.item() + return correct, total, conf_sum if __name__ == "__main__": model = load_model() - test_image_path = "/home/groups/comp3710/ADNI/AD_NC/test/AD/388206_78.jpeg" - predict_image(test_image_path, model) \ No newline at end of file + + ad_path = "/home/groups/comp3710/ADNI/AD_NC/test/AD" + nc_path = ad_path = "/home/groups/comp3710/ADNI/AD_NC/test/NC" + + ad_correct, ad_total, ad_conf_sum = evaluate_folder(model, ad_path, label = 1) + nc_correct, nc_total, nc_conf_sum = evaluate_folder(model, nc_path, label = 0) + + total_correct = ad_correct + nc_correct + total_images = ad_total + nc_total + accuracy = total_correct / total_images + + print(f"\n ----- Evaluation Results -----") + print(f"AD: {ad_correct}/{ad_total} correct ({ad_correct / ad_total * 100:.2f}%)") + print(f"NC: {nc_correct}/{nc_total} correct ({nc_correct / nc_total * 100:.2f}%)") + print(f"Overall Accuracy: {accuracy*100:.2f}%") + print(f"\n ----- Average Confidence Results -----") + print(f"AD: {ad_conf_sum / ad_total * 100:.2f}%") + print(f"NC: {nc_conf_sum / nc_total * 100:.2f}%") + print(f"Total: {(ad_conf_sum + nc_conf_sum) / total_images * 100:.2f}") \ No newline at end of file From 44feed418f0e5a042b6a48b18b99a13d98be0fc1 Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Wed, 8 Oct 2025 12:23:23 +1000 Subject: [PATCH 17/31] Added data augmentation transforms to training set and updated dataset normalisation to use true mean/std values --- .../Alzheimers_Classifier_s4693608/dataset.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/recognition/Alzheimers_Classifier_s4693608/dataset.py b/recognition/Alzheimers_Classifier_s4693608/dataset.py index a36148a88..f68a6256a 100644 --- a/recognition/Alzheimers_Classifier_s4693608/dataset.py +++ b/recognition/Alzheimers_Classifier_s4693608/dataset.py @@ -61,14 +61,23 @@ def get_dataloaders(root_dir, batch_size=16): - test_loader (DataLoader): DataLoader for the test set, with shuffling disabled. """ - transform = transforms.Compose([ + train_transform = transforms.Compose([ transforms.Resize((224, 224)), + transforms.RandomHorizontalFlip(p=0.5), + transforms.RandomRotation(degrees=10), + transforms.RandomResizedCrop(224, scale=(0.8, 1.0)), transforms.ToTensor(), - transforms.Normalize(mean=[0.5], std=[0.5]) + transforms.Normalize(mean=[0.1159], std=[0.2199]) ]) - train_dataset = ADNIDataset(os.path.join(root_dir, "AD_NC"), split="train", transform=transform) - test_dataset = ADNIDataset(os.path.join(root_dir, "AD_NC"), split="test", transform=transform) + test_transform = transforms.Compose([ + transforms.Resize((224, 224)), + transforms.ToTensor(), + transforms.Normalize(mean=[0.1159], std=[0.2199]) + ]) + + train_dataset = ADNIDataset(os.path.join(root_dir, "AD_NC"), split="train", transform=train_transform) + test_dataset = ADNIDataset(os.path.join(root_dir, "AD_NC"), split="test", transform=test_transform) train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False) From 5194892f9868aff670fc1df028abe152ee4cd926 Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Wed, 8 Oct 2025 12:28:42 +1000 Subject: [PATCH 18/31] Added learning rate scheduler --- recognition/Alzheimers_Classifier_s4693608/train.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/recognition/Alzheimers_Classifier_s4693608/train.py b/recognition/Alzheimers_Classifier_s4693608/train.py index f2a998436..648f0ce86 100644 --- a/recognition/Alzheimers_Classifier_s4693608/train.py +++ b/recognition/Alzheimers_Classifier_s4693608/train.py @@ -2,6 +2,7 @@ import torch import torch.nn as nn import torch.optim as optim +from torch.optim.lr_scheduler import ReduceLROnPlateau from dataset import get_dataloaders from modules import AlzheimersClassifier import matplotlib.pyplot as plt @@ -36,6 +37,7 @@ def train_model(root_dir, epochs=10, batch_size=16, lr=1e-4, device='cuda'): model = AlzheimersClassifier().to(device) criterion = nn.CrossEntropyLoss() optimiser = optim.AdamW(model.parameters(), lr=lr) + scheduler = ReduceLROnPlateau(optimiser, mode='max', factor=0.5, patience=2, verbose=True) train_losses, val_losses, val_accs = [], [], [] best_acc = 0.0 @@ -81,6 +83,9 @@ def train_model(root_dir, epochs=10, batch_size=16, lr=1e-4, device='cuda'): print(f"Epoch {epoch+1}/{epochs} | " f"Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f} | Val Acc: {val_acc:.4f}") + # Update scheduler + scheduler.step(val_acc) + # Save the best model if val_acc > best_acc: best_acc = val_acc From 9f0f202f4984fbb3b17be5175cc4f9d733cf30d4 Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Wed, 8 Oct 2025 12:34:22 +1000 Subject: [PATCH 19/31] Added early stopping mechanism --- recognition/Alzheimers_Classifier_s4693608/train.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/recognition/Alzheimers_Classifier_s4693608/train.py b/recognition/Alzheimers_Classifier_s4693608/train.py index 648f0ce86..c77fdfdb1 100644 --- a/recognition/Alzheimers_Classifier_s4693608/train.py +++ b/recognition/Alzheimers_Classifier_s4693608/train.py @@ -41,6 +41,8 @@ def train_model(root_dir, epochs=10, batch_size=16, lr=1e-4, device='cuda'): train_losses, val_losses, val_accs = [], [], [] best_acc = 0.0 + epochs_no_improve = 0 + patience = 3 for epoch in range(epochs): # Training @@ -85,12 +87,22 @@ def train_model(root_dir, epochs=10, batch_size=16, lr=1e-4, device='cuda'): # Update scheduler scheduler.step(val_acc) + current_lr = optimiser.param_groups[0]['lr'] + print(f"Learning rate: {current_lr:.6f}") # Save the best model if val_acc > best_acc: best_acc = val_acc + epochs_no_improve = 0 torch.save(model.state_dict(), "best_model.pth") print("New best model saved!") + else: + epochs_no_improve += 1 + print(f"No improvement for {epochs_no_improve} epoch(s).") + + if epochs_no_improve >= patience: + print("\n Early stopping triggered!") + break # Plot training curves plt.figure() From 3b4ab7f226fc27404d84f2eb24b5f685ad690ef5 Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Wed, 8 Oct 2025 12:49:45 +1000 Subject: [PATCH 20/31] Added droput layer to ConvNeXt head and switched to base ConvNeXt model --- .../Alzheimers_Classifier_s4693608/modules.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/recognition/Alzheimers_Classifier_s4693608/modules.py b/recognition/Alzheimers_Classifier_s4693608/modules.py index 78f607e83..c44dcadbe 100644 --- a/recognition/Alzheimers_Classifier_s4693608/modules.py +++ b/recognition/Alzheimers_Classifier_s4693608/modules.py @@ -9,7 +9,7 @@ class AlzheimersClassifier(nn.Module): This class wraps a pretrained ConvNeXt model from the `timm` library and modifies it to: - Accept grayscale input images (1 channel) by duplicating them into 3 channels, since ConvNeXt expects RGB input. - - Replace the final classification head with a linear layer outputting 2 classes + - Replace the final classification head with a dropout rate and linear layer outputting 2 classes (Alzheimer's disease = 1, Normal Control = 0). Attributes: @@ -22,13 +22,19 @@ class AlzheimersClassifier(nn.Module): Takes grayscale input of shape [B, 1, H, W], duplicates channels, and outputs logits of shape [B, 2]. """ - def __init__(self, model_name="convnext_tiny", num_classes=2, pretrained=True): + def __init__(self, model_name="convnext_base", num_classes=2, pretrained=True, dropout=0.4): super().__init__() # Load pretrained ConvNeXt backbone self.model = timm.create_model(model_name, pretrained=pretrained) - # Replace classifier head (for 2 classes instead of 1000) - self.model.reset_classifier(num_classes=num_classes) + # Get number of features in the original head + in_features = self.model.num_features + + # Replace classifier with dropout + linear + self.model.head = nn.Sequential( + nn.Dropout(dropout), + nn.Linear(in_features, num_classes) + ) def forward(self, x): # duplicate x channels ([B, 1, H, W] -> [B, 3, H, W]) From 8a3c32acc0b27978d774367d0c500bedeca7c88d Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Wed, 8 Oct 2025 16:34:19 +1000 Subject: [PATCH 21/31] Tested and fixed errors --- .../Alzheimers_Classifier_s4693608/modules.py | 22 +++++++++++-------- .../Alzheimers_Classifier_s4693608/predict.py | 7 +++--- .../Alzheimers_Classifier_s4693608/train.py | 6 ++--- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/recognition/Alzheimers_Classifier_s4693608/modules.py b/recognition/Alzheimers_Classifier_s4693608/modules.py index c44dcadbe..c0496d972 100644 --- a/recognition/Alzheimers_Classifier_s4693608/modules.py +++ b/recognition/Alzheimers_Classifier_s4693608/modules.py @@ -25,18 +25,22 @@ class AlzheimersClassifier(nn.Module): def __init__(self, model_name="convnext_base", num_classes=2, pretrained=True, dropout=0.4): super().__init__() # Load pretrained ConvNeXt backbone - self.model = timm.create_model(model_name, pretrained=pretrained) - - # Get number of features in the original head - in_features = self.model.num_features + self.model = timm.create_model(model_name, pretrained=pretrained, num_classes=0) + + # Ensure fixed size feature vector + self.pool = nn.AdaptiveAvgPool2d(1) # Replace classifier with dropout + linear - self.model.head = nn.Sequential( - nn.Dropout(dropout), - nn.Linear(in_features, num_classes) - ) + self.dropout = nn.Dropout(dropout) + self.fc = nn.Linear(self.model.num_features, num_classes) def forward(self, x): # duplicate x channels ([B, 1, H, W] -> [B, 3, H, W]) x = x.repeat(1, 3, 1, 1) - return self.model(x) \ No newline at end of file + x = self.model.forward_features(x) + x = self.pool(x) + x = torch.flatten(x, 1) + x = self.dropout(x) + x = self.fc(x) + return x + \ No newline at end of file diff --git a/recognition/Alzheimers_Classifier_s4693608/predict.py b/recognition/Alzheimers_Classifier_s4693608/predict.py index 730b5ca6b..2e7e725d1 100644 --- a/recognition/Alzheimers_Classifier_s4693608/predict.py +++ b/recognition/Alzheimers_Classifier_s4693608/predict.py @@ -77,21 +77,20 @@ def evaluate_folder(model, folder_path, label): total (int): Total number of images evaluated. conf_sum (float): Current sum of confidence of classifcation of images in the folder. """ - # label = 1 for AD, label = 0 for NC correct = total = conf_sum = 0 for fname in os.listdir(folder_path): fpath = os.path.join(folder_path, fname) pred, conf = predict_image(fpath, model) total += 1 correct += int(pred == label) - conf_sum += conf.item() + conf_sum += conf return correct, total, conf_sum if __name__ == "__main__": model = load_model() ad_path = "/home/groups/comp3710/ADNI/AD_NC/test/AD" - nc_path = ad_path = "/home/groups/comp3710/ADNI/AD_NC/test/NC" + nc_path = "/home/groups/comp3710/ADNI/AD_NC/test/NC" ad_correct, ad_total, ad_conf_sum = evaluate_folder(model, ad_path, label = 1) nc_correct, nc_total, nc_conf_sum = evaluate_folder(model, nc_path, label = 0) @@ -107,4 +106,4 @@ def evaluate_folder(model, folder_path, label): print(f"\n ----- Average Confidence Results -----") print(f"AD: {ad_conf_sum / ad_total * 100:.2f}%") print(f"NC: {nc_conf_sum / nc_total * 100:.2f}%") - print(f"Total: {(ad_conf_sum + nc_conf_sum) / total_images * 100:.2f}") \ No newline at end of file + print(f"Total: {(ad_conf_sum + nc_conf_sum) / total_images * 100:.2f}%") \ No newline at end of file diff --git a/recognition/Alzheimers_Classifier_s4693608/train.py b/recognition/Alzheimers_Classifier_s4693608/train.py index c77fdfdb1..716de7f83 100644 --- a/recognition/Alzheimers_Classifier_s4693608/train.py +++ b/recognition/Alzheimers_Classifier_s4693608/train.py @@ -37,12 +37,12 @@ def train_model(root_dir, epochs=10, batch_size=16, lr=1e-4, device='cuda'): model = AlzheimersClassifier().to(device) criterion = nn.CrossEntropyLoss() optimiser = optim.AdamW(model.parameters(), lr=lr) - scheduler = ReduceLROnPlateau(optimiser, mode='max', factor=0.5, patience=2, verbose=True) + scheduler = ReduceLROnPlateau(optimiser, mode='max', factor=0.5, patience=2) train_losses, val_losses, val_accs = [], [], [] best_acc = 0.0 epochs_no_improve = 0 - patience = 3 + patience = 5 for epoch in range(epochs): # Training @@ -122,4 +122,4 @@ def train_model(root_dir, epochs=10, batch_size=16, lr=1e-4, device='cuda'): if __name__ == "__main__": root_dir = "/home/groups/comp3710/ADNI" - train_model(root_dir, epochs=10, batch_size=16, lr=1e-4) \ No newline at end of file + train_model(root_dir, epochs=20, batch_size=32, lr=1e-4) \ No newline at end of file From 014bae4816138460732d9266cf5e287895f330ad Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Fri, 10 Oct 2025 13:21:30 +1000 Subject: [PATCH 22/31] Optimised training --- .../Alzheimers_Classifier_s4693608/modules.py | 5 +-- .../Alzheimers_Classifier_s4693608/train.py | 45 ++++++++----------- 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/recognition/Alzheimers_Classifier_s4693608/modules.py b/recognition/Alzheimers_Classifier_s4693608/modules.py index c0496d972..017a90055 100644 --- a/recognition/Alzheimers_Classifier_s4693608/modules.py +++ b/recognition/Alzheimers_Classifier_s4693608/modules.py @@ -22,7 +22,7 @@ class AlzheimersClassifier(nn.Module): Takes grayscale input of shape [B, 1, H, W], duplicates channels, and outputs logits of shape [B, 2]. """ - def __init__(self, model_name="convnext_base", num_classes=2, pretrained=True, dropout=0.4): + def __init__(self, model_name="convnext_base", num_classes=2, pretrained=True, dropout=0.5): super().__init__() # Load pretrained ConvNeXt backbone self.model = timm.create_model(model_name, pretrained=pretrained, num_classes=0) @@ -42,5 +42,4 @@ def forward(self, x): x = torch.flatten(x, 1) x = self.dropout(x) x = self.fc(x) - return x - \ No newline at end of file + return x \ No newline at end of file diff --git a/recognition/Alzheimers_Classifier_s4693608/train.py b/recognition/Alzheimers_Classifier_s4693608/train.py index 716de7f83..6799d5496 100644 --- a/recognition/Alzheimers_Classifier_s4693608/train.py +++ b/recognition/Alzheimers_Classifier_s4693608/train.py @@ -2,7 +2,7 @@ import torch import torch.nn as nn import torch.optim as optim -from torch.optim.lr_scheduler import ReduceLROnPlateau +from torch.optim.lr_scheduler import OneCycleLR from dataset import get_dataloaders from modules import AlzheimersClassifier import matplotlib.pyplot as plt @@ -37,12 +37,21 @@ def train_model(root_dir, epochs=10, batch_size=16, lr=1e-4, device='cuda'): model = AlzheimersClassifier().to(device) criterion = nn.CrossEntropyLoss() optimiser = optim.AdamW(model.parameters(), lr=lr) - scheduler = ReduceLROnPlateau(optimiser, mode='max', factor=0.5, patience=2) + + # scheduler + scheduler = OneCycleLR( + optimiser, + max_lr=lr * 10, + steps_per_epoch=len(train_loader), + epochs=epochs, + pct_start=0.3, + anneal_strategy='cos', + div_factor=10, + final_div_factor=1e4 + ) train_losses, val_losses, val_accs = [], [], [] best_acc = 0.0 - epochs_no_improve = 0 - patience = 5 for epoch in range(epochs): # Training @@ -56,6 +65,7 @@ def train_model(root_dir, epochs=10, batch_size=16, lr=1e-4, device='cuda'): loss = criterion(outputs, labels) loss.backward() optimiser.step() + scheduler.step() running_loss += loss.item() @@ -83,7 +93,10 @@ def train_model(root_dir, epochs=10, batch_size=16, lr=1e-4, device='cuda'): val_accs.append(val_acc) print(f"Epoch {epoch+1}/{epochs} | " - f"Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f} | Val Acc: {val_acc:.4f}") + f"Train Loss: {train_loss:.4f} | " + f"Val Loss: {val_loss:.4f} | " + f"Val Acc: {val_acc:.4f} | " + f"LR: {current_lr:.6f}") # Update scheduler scheduler.step(val_acc) @@ -93,30 +106,8 @@ def train_model(root_dir, epochs=10, batch_size=16, lr=1e-4, device='cuda'): # Save the best model if val_acc > best_acc: best_acc = val_acc - epochs_no_improve = 0 torch.save(model.state_dict(), "best_model.pth") print("New best model saved!") - else: - epochs_no_improve += 1 - print(f"No improvement for {epochs_no_improve} epoch(s).") - - if epochs_no_improve >= patience: - print("\n Early stopping triggered!") - break - - # Plot training curves - plt.figure() - plt.plot(train_losses, label="Train Loss") - plt.plot(val_losses, label="Val Loss") - plt.legend() - plt.title("Training and Validation Loss") - plt.savefig("loss_curve.png") - - plt.figure() - plt.plot(val_accs, label="Val Accuracy") - plt.legend() - plt.title("Validation Accuracy") - plt.savefig("val_acc_curve.png") print(f"Best Validation Accuracy: {best_acc:.4f}") From 941d09d69b30d48361c9c238b5a90749451010e2 Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Sun, 19 Oct 2025 13:50:21 +1000 Subject: [PATCH 23/31] Improved training accuracy --- recognition/Alzheimers_Classifier_s4693608/train.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/recognition/Alzheimers_Classifier_s4693608/train.py b/recognition/Alzheimers_Classifier_s4693608/train.py index 6799d5496..08e53f657 100644 --- a/recognition/Alzheimers_Classifier_s4693608/train.py +++ b/recognition/Alzheimers_Classifier_s4693608/train.py @@ -35,13 +35,13 @@ def train_model(root_dir, epochs=10, batch_size=16, lr=1e-4, device='cuda'): # Define model, loss, optimiser model = AlzheimersClassifier().to(device) - criterion = nn.CrossEntropyLoss() + criterion = nn.CrossEntropyLoss(weight=torch.tensor([2.0, 1.0]).to(device)) optimiser = optim.AdamW(model.parameters(), lr=lr) # scheduler scheduler = OneCycleLR( optimiser, - max_lr=lr * 10, + max_lr=lr * 5, steps_per_epoch=len(train_loader), epochs=epochs, pct_start=0.3, @@ -113,4 +113,4 @@ def train_model(root_dir, epochs=10, batch_size=16, lr=1e-4, device='cuda'): if __name__ == "__main__": root_dir = "/home/groups/comp3710/ADNI" - train_model(root_dir, epochs=20, batch_size=32, lr=1e-4) \ No newline at end of file + train_model(root_dir, epochs=30, batch_size=32, lr=1e-4) \ No newline at end of file From 66976045b5a4ae861259b362655ce23249c900c9 Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Sun, 19 Oct 2025 13:57:00 +1000 Subject: [PATCH 24/31] Changed to predict overall patient case instead of each individual MRI slice --- .../Alzheimers_Classifier_s4693608/predict.py | 95 ++++++++++--------- 1 file changed, 50 insertions(+), 45 deletions(-) diff --git a/recognition/Alzheimers_Classifier_s4693608/predict.py b/recognition/Alzheimers_Classifier_s4693608/predict.py index 2e7e725d1..7cf535c5b 100644 --- a/recognition/Alzheimers_Classifier_s4693608/predict.py +++ b/recognition/Alzheimers_Classifier_s4693608/predict.py @@ -3,6 +3,8 @@ from torchvision import transforms from PIL import Image from modules import AlzheimersClassifier +from collections import defaultdict +import numpy as np def load_model(model_path="best_model.pth"): """ @@ -24,7 +26,7 @@ def load_model(model_path="best_model.pth"): model.eval() return model -def predict_image(image_path, model): +def predict_slice(image_path, model): """ The function loads a grayscale MRI slice, applies the same preprocessing transformations used during training (resize, tensor conversion, normalization), @@ -39,7 +41,7 @@ class and associated confidence score. tuple: - prediction (int): Predicted class label value (1 for "AD" or 0 for "NC"). - confidence (float): Model confidence for the predicted class, - between 0.0 and 1.0. + between 0.0 and 1.0. """ image = Image.open(image_path) @@ -47,63 +49,66 @@ class and associated confidence score. transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), - transforms.Normalize(mean=[0.5], std=[0.5]) + transforms.Normalize(mean=[0.1159], std=[0.2199]) ]) image = transform(image).unsqueeze(0).to("cuda") with torch.no_grad(): outputs = model(image) - probs = torch.softmax(outputs, dim=1) - conf, pred = torch.max(probs, dim=1) + probs = torch.softmax(outputs, dim=1)[0].cpu().numpy() - class_names = ["NC", "AD"] - label = class_names[pred.item()] - print(f"Prediction: {label} ({conf.item() * 100:.2f}% confidence)") - return pred.item(), conf.item() + return probs -def evaluate_folder(model, folder_path, label): +def aggregate_patient_predictions(patient_probs): """ - This function iterates through all `.jpeg` files in the specified folder, - performs inference using the provided model, and counts how many predictions - match the expected label. + Given a list of [NC_prob, AD_prob] arrays for one patient, + average them and return predicted label + confidence. Args: - model : The trained PyTorch model used for prediction. - folder_path (str): Path to the folder containing `.jpeg` images to evaluate. - label (int): The ground truth label for all images in this folder + patient_probs (dict): probabilities of AD case for all slices of one patient. Returns: - correct (int): Number of correctly classified images. - total (int): Total number of images evaluated. - conf_sum (float): Current sum of confidence of classifcation of images in the folder. + tuple: + - label (str): Predicted class label value (1 for "AD" or 0 for "NC"). + - confidence (float): Model confidence for the predicted class, + between 0.0 and 1.0. + - mean_probs (int): mean probability of AD case across aggregated slices + for a patient. """ - correct = total = conf_sum = 0 - for fname in os.listdir(folder_path): - fpath = os.path.join(folder_path, fname) - pred, conf = predict_image(fpath, model) - total += 1 - correct += int(pred == label) - conf_sum += conf - return correct, total, conf_sum + mean_probs = np.mean(patient_probs, axis=0) + label = np.argmax(mean_probs) + confidence = mean_probs[label] + + return label, confidence, mean_probs if __name__ == "__main__": model = load_model() + root_dir = "/home/groups/comp3710/ADNI/AD_NC/test" + class_names = ["NC", "AD"] + + # Group all slices by patient id + patient_slices = defaultdict(list) + for cls in ["AD", "NC"]: + folder = os.path.join(root_dir, cls) + for fname in os.listdir(folder): + patient_id = fname.split('_')[0] + patient_slices[patient_id].append(os.path.join(folder, fname)) + + # Predict each slice and aggregate + patient_results = {} + for pid, slice_paths in patient_slices.items(): + slice_probs = [predict_slice(p, model) for p in slice_paths] + label, conf, mean_probs = aggregate_patient_predictions(slice_probs) + patient_results[pid] = (label, conf, mean_probs) + + # Evaluate patient accuracy + correct, total = 0, 0 + for pid, (label, conf, probs) in patient_results.item(): + true_label = 1 if any("AD/" in p for p in patient_slices[pid]) else 0 + total += 1 + correct += int(label == true_label) + + print(f"Patient {pid}: Predicted {class_names[label]} ({conf * 100:.2f}%)" + f" | True: {class_names[true_label]}") - ad_path = "/home/groups/comp3710/ADNI/AD_NC/test/AD" - nc_path = "/home/groups/comp3710/ADNI/AD_NC/test/NC" - - ad_correct, ad_total, ad_conf_sum = evaluate_folder(model, ad_path, label = 1) - nc_correct, nc_total, nc_conf_sum = evaluate_folder(model, nc_path, label = 0) - - total_correct = ad_correct + nc_correct - total_images = ad_total + nc_total - accuracy = total_correct / total_images - - print(f"\n ----- Evaluation Results -----") - print(f"AD: {ad_correct}/{ad_total} correct ({ad_correct / ad_total * 100:.2f}%)") - print(f"NC: {nc_correct}/{nc_total} correct ({nc_correct / nc_total * 100:.2f}%)") - print(f"Overall Accuracy: {accuracy*100:.2f}%") - print(f"\n ----- Average Confidence Results -----") - print(f"AD: {ad_conf_sum / ad_total * 100:.2f}%") - print(f"NC: {nc_conf_sum / nc_total * 100:.2f}%") - print(f"Total: {(ad_conf_sum + nc_conf_sum) / total_images * 100:.2f}%") \ No newline at end of file + print(f"\nPatient Prediction Accuracy: {100 * correct / total:.2f}%") \ No newline at end of file From ed85eee89a8b0b1d95bdb1f3ed4bd4e6299a798d Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Sun, 19 Oct 2025 13:59:31 +1000 Subject: [PATCH 25/31] Fixed typo error --- recognition/Alzheimers_Classifier_s4693608/predict.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recognition/Alzheimers_Classifier_s4693608/predict.py b/recognition/Alzheimers_Classifier_s4693608/predict.py index 7cf535c5b..b98217c00 100644 --- a/recognition/Alzheimers_Classifier_s4693608/predict.py +++ b/recognition/Alzheimers_Classifier_s4693608/predict.py @@ -103,7 +103,7 @@ def aggregate_patient_predictions(patient_probs): # Evaluate patient accuracy correct, total = 0, 0 - for pid, (label, conf, probs) in patient_results.item(): + for pid, (label, conf, probs) in patient_results.items(): true_label = 1 if any("AD/" in p for p in patient_slices[pid]) else 0 total += 1 correct += int(label == true_label) From 85ab6ceca73a5d1692f6a5bcb409c23226c05eb6 Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Sun, 19 Oct 2025 14:08:53 +1000 Subject: [PATCH 26/31] Fixed up docstrings --- recognition/Alzheimers_Classifier_s4693608/predict.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recognition/Alzheimers_Classifier_s4693608/predict.py b/recognition/Alzheimers_Classifier_s4693608/predict.py index b98217c00..ac629bb2d 100644 --- a/recognition/Alzheimers_Classifier_s4693608/predict.py +++ b/recognition/Alzheimers_Classifier_s4693608/predict.py @@ -65,7 +65,7 @@ def aggregate_patient_predictions(patient_probs): average them and return predicted label + confidence. Args: - patient_probs (dict): probabilities of AD case for all slices of one patient. + patient_probs (list): probabilities of AD case for all slices of one patient. Returns: tuple: From ca45e4b2bfb29a4f43b9ac5a3cec9a148ee12152 Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Sun, 19 Oct 2025 14:38:46 +1000 Subject: [PATCH 27/31] Removed unused import --- recognition/Alzheimers_Classifier_s4693608/train.py | 1 - 1 file changed, 1 deletion(-) diff --git a/recognition/Alzheimers_Classifier_s4693608/train.py b/recognition/Alzheimers_Classifier_s4693608/train.py index 08e53f657..5a1162252 100644 --- a/recognition/Alzheimers_Classifier_s4693608/train.py +++ b/recognition/Alzheimers_Classifier_s4693608/train.py @@ -5,7 +5,6 @@ from torch.optim.lr_scheduler import OneCycleLR from dataset import get_dataloaders from modules import AlzheimersClassifier -import matplotlib.pyplot as plt def train_model(root_dir, epochs=10, batch_size=16, lr=1e-4, device='cuda'): """ From f44519bd4234e481c827b7e059383a6f43564674 Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Sun, 19 Oct 2025 15:08:48 +1000 Subject: [PATCH 28/31] Changed output display formatting --- recognition/Alzheimers_Classifier_s4693608/predict.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recognition/Alzheimers_Classifier_s4693608/predict.py b/recognition/Alzheimers_Classifier_s4693608/predict.py index ac629bb2d..345d4223d 100644 --- a/recognition/Alzheimers_Classifier_s4693608/predict.py +++ b/recognition/Alzheimers_Classifier_s4693608/predict.py @@ -111,4 +111,4 @@ def aggregate_patient_predictions(patient_probs): print(f"Patient {pid}: Predicted {class_names[label]} ({conf * 100:.2f}%)" f" | True: {class_names[true_label]}") - print(f"\nPatient Prediction Accuracy: {100 * correct / total:.2f}%") \ No newline at end of file + print(f"Patient Prediction Accuracy: {100 * correct / total:.2f}%\n") \ No newline at end of file From a9d2bbbbcd7ac02fb503948a960a1c35ae68ad85 Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Sun, 19 Oct 2025 15:16:19 +1000 Subject: [PATCH 29/31] Fixed up docstrings --- recognition/Alzheimers_Classifier_s4693608/predict.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/recognition/Alzheimers_Classifier_s4693608/predict.py b/recognition/Alzheimers_Classifier_s4693608/predict.py index 345d4223d..41f5bd371 100644 --- a/recognition/Alzheimers_Classifier_s4693608/predict.py +++ b/recognition/Alzheimers_Classifier_s4693608/predict.py @@ -31,17 +31,14 @@ def predict_slice(image_path, model): The function loads a grayscale MRI slice, applies the same preprocessing transformations used during training (resize, tensor conversion, normalization), and performs a forward pass through the trained model to obtain the predicted - class and associated confidence score. + probability of AD/NC case. Args: image_path (str): Path to the input image (e.g., .jpeg slice). model (torch.nn.Module): Trained AlzheimersClassifier model instance. Returns: - tuple: - - prediction (int): Predicted class label value (1 for "AD" or 0 for "NC"). - - confidence (float): Model confidence for the predicted class, - between 0.0 and 1.0. + probs (float): probability of AD/NC case. """ image = Image.open(image_path) @@ -72,7 +69,7 @@ def aggregate_patient_predictions(patient_probs): - label (str): Predicted class label value (1 for "AD" or 0 for "NC"). - confidence (float): Model confidence for the predicted class, between 0.0 and 1.0. - - mean_probs (int): mean probability of AD case across aggregated slices + - mean_probs (float): mean probability of AD case across aggregated slices for a patient. """ mean_probs = np.mean(patient_probs, axis=0) From 01c8fc700a3f28047d891212569ce690701789c6 Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Sun, 19 Oct 2025 15:26:17 +1000 Subject: [PATCH 30/31] Changed output display formatting --- recognition/Alzheimers_Classifier_s4693608/predict.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recognition/Alzheimers_Classifier_s4693608/predict.py b/recognition/Alzheimers_Classifier_s4693608/predict.py index 41f5bd371..957cc2c5b 100644 --- a/recognition/Alzheimers_Classifier_s4693608/predict.py +++ b/recognition/Alzheimers_Classifier_s4693608/predict.py @@ -108,4 +108,4 @@ def aggregate_patient_predictions(patient_probs): print(f"Patient {pid}: Predicted {class_names[label]} ({conf * 100:.2f}%)" f" | True: {class_names[true_label]}") - print(f"Patient Prediction Accuracy: {100 * correct / total:.2f}%\n") \ No newline at end of file + print(f"\nPatient Prediction Accuracy: {100 * correct / total:.2f}%") \ No newline at end of file From a7e99ef810d411b639019dca68de0b1f67d16351 Mon Sep 17 00:00:00 2001 From: CHAR1VAR1 Date: Sun, 19 Oct 2025 15:37:38 +1000 Subject: [PATCH 31/31] Filled out details of project in README --- .../Alzheimers_Classifier_s4693608/README.md | 112 +++++++++++++++++- 1 file changed, 111 insertions(+), 1 deletion(-) diff --git a/recognition/Alzheimers_Classifier_s4693608/README.md b/recognition/Alzheimers_Classifier_s4693608/README.md index 019517b3d..83fcdc64f 100644 --- a/recognition/Alzheimers_Classifier_s4693608/README.md +++ b/recognition/Alzheimers_Classifier_s4693608/README.md @@ -1 +1,111 @@ -"# Alzheimer's Classifier (Task 8)" +# Alzheimer’s Disease MRI Classifier — COMP3710 Task 8 + +This project implements a deep learning classifier to distinguish between Alzheimer’s Disease (AD) and Normal Control (NC) using 2D MRI brain slices from the ADNI dataset. +The model is based on ConvNeXt, trained using PyTorch, and makes predictions on the slice-level and then aggregates through these predictions and averages them to make patient-level predictions. +The model was trained and tested on UQ's rangpur, achieving a final patient prediction accuracy of 80.22%. + +Developed for COMP3710 — Pattern Recognition and Analysis, University of Queensland by Christian Vever - s4693608. + +## Dataset Structure + +/home/groups/comp3710/ADNI/ +├── AD_NC/ +│ ├── train/ +│ │ ├── AD/ +│ │ │ ├── `_.jpeg` +│ │ └── NC/ +│ │ ├── `_.jpeg` +│ ├── test/ +│ │ ├── AD/ +│ │ └── NC/ +└── meta_data_with_label.json + +All images are greyscale JPEGs of size (256 x 240), and are 2D slices of MRI brain scans form the ADNI dataset. +Filenames follow `_.jpeg` naming convention. + +## Requirements + +### System Requirments + +* Python 3.10 + +* Parallel processing using GPU (optional but recommended) + +### Dependency Requirements + +* torch +* torchvision +* timm +* pillow +* numpy + +## Training Model Parameters + +* Model: ConvNeXt (pretrained on ImageNet) +* Epochs: 30 +* Batch Size: 32 (optimal tested number of batches) +* Learning Rate: 1e-4 +* Scheduler: OneCycleLR (allows for increased learnin rate over first few epochs before decreasing) +* Dropout: Enabled in classifier head (set to 0.5; prevents overfitting) +* Loss: Weighted CrossEntropyLoss (weigth = [2.0, 1.0] to focus more on AD cases) +* Augmentation: Random rotation +/- 10 degrees and horizontal flip (to force the classifier to focus more on general features) +* Normalisation: mean = 0.1159, std = 0.2199 (the calculated mean and std of the dataset) + +The script saves the best checkpoint to `best_model.pth`. + +## Running Scripts and Outputs + +Scripts were run on rangpur using sbatcb. +sbatch runner for train.py: +``` +#!/bin/bash +#SBATCH --nodes=1 +#SBATCH --ntasks-per-node=1 +#SBATCH --cpus-per-task=1 +#SBATCH --gres=gpu:1 +#SBATCH --partition=a100 +#SBATCH --job-name=train +#SBATCH --time=04:00:00 +#SBATCH -o train.out +#SBATCH -e train.err + +conda activate torch +python train.py +``` + +The train.py script outputs (per epoch): +* The current epoch number (out of total epochs) +* The training loss for this epoch +* The validation loss for this epoch +* The validation accuracy for this epoch +* The current learning rate +Example output: +``` +Epoch 7/30 | Train Loss: 0.0914 | Val Loss: 0.6345 | Val Acc: 0.7551 | LR: 0.000447 +``` +Once all epochs have been run, the script outputs the best validation accuracy. + +sbatch runner for predict.py: +``` +#!/bin/bash +#SBATCH --nodes=1 +#SBATCH --ntasks-per-node=1 +#SBATCH --cpus-per-task=1 +#SBATCH --gres=gpu:a100 +#SBATCH --job-name=predict +#SBATCH -o predict.out +#SBATCH -e predict.err + +conda activate torch +python predict.py +``` + +The predict.py script outputs: +* The patiend id +* The predicted case (AD or NC) +* The true case (AD or NC) +* The patient prediction accuracy across all slices +Example output: +``` +Patient 389298: Predicted AD (99.99%) | True: AD +``` +Once all patient id's have been tested, the script outputs the overall patient prediction accuracy. \ No newline at end of file