| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495 |
- import pathlib as pl
- import time
- from threading import Event
- from typing import Callable, Tuple, cast
- import numpy as np
- import pandas as pd
- import torch
- import torch.nn as nn
- from torch.utils.data import DataLoader
- from model.dataset import ADNIDataset
- from util.progress import ProgressTracker
- from util.ui_logger import PipelineLogger
- type TrainMetrics = Tuple[
- float, float, float, float
- ] # (train_loss, val_loss, train_acc, val_acc)
- type TestMetrics = Tuple[float, float] # (test_loss, test_acc)
- type KLLossFn = Callable[[nn.Module], torch.Tensor | None]
- def _model_device(model: nn.Module) -> torch.device:
- first_param = next(model.parameters(), None)
- if first_param is None:
- return torch.device("cpu")
- return first_param.device
- def _move_batch_to_model_device(
- model: nn.Module,
- mri: torch.Tensor,
- xls: torch.Tensor,
- targets: torch.Tensor,
- ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
- device = _model_device(model)
- mri = mri.to(device, non_blocking=True)
- xls = xls.to(device, non_blocking=True)
- targets = targets.to(device, non_blocking=True)
- return mri, xls, targets
- def _batch_correct_and_total(
- outputs: torch.Tensor,
- targets: torch.Tensor,
- ) -> Tuple[int, int]:
- if outputs.ndim > 1 and outputs.size(-1) > 1:
- predicted_classes = outputs.argmax(dim=1)
- if targets.ndim > 1 and targets.size(-1) > 1:
- target_classes = targets.argmax(dim=1)
- else:
- target_classes = targets.long().view(-1)
- correct = int((predicted_classes == target_classes).sum().item())
- total = int(target_classes.numel())
- return correct, total
- predicted = (outputs > 0.5).to(dtype=targets.dtype)
- correct = int((predicted == targets).sum().item())
- total = int(targets.numel())
- return correct, total
- def _check_control_events(
- stop_event: Event | None,
- pause_event: Event | None,
- ) -> None:
- if stop_event is not None and stop_event.is_set():
- raise InterruptedError("Pipeline execution stopped by user.")
- while pause_event is not None and pause_event.is_set():
- time.sleep(0.5)
- if stop_event is not None and stop_event.is_set():
- raise InterruptedError("Pipeline execution stopped by user while paused.")
- def test_model(
- model: nn.Module,
- test_loader: DataLoader[ADNIDataset],
- criterion: nn.Module,
- progress: ProgressTracker,
- log: PipelineLogger,
- stop_event: Event | None = None,
- pause_event: Event | None = None,
- ) -> TestMetrics:
- """
- Tests the model on the test dataset.
- Args:
- model (nn.Module): The model to test.
- test_loader (DataLoader[ADNIDataset]): DataLoader for the test dataset.
- criterion (nn.Module): Loss function to compute the loss.
- Returns:
- TrainMetrics: A tuple containing the test loss and test accuracy.
- """
- model.eval()
- test_loss = 0.0
- correct = 0
- total = 0
- total_samples = 0
- test_progress = progress.get_sub_tracker("Testing Batches")
- test_progress.update(total=len(test_loader), advance=0)
- with torch.no_grad():
- for _, (mri, xls, targets, _) in enumerate(test_loader):
- _check_control_events(stop_event=stop_event, pause_event=pause_event)
- mri, xls, targets = _move_batch_to_model_device(model, mri, xls, targets)
- outputs = model((mri, xls))
- loss = criterion(outputs, targets)
- batch_size = mri.size(0)
- test_loss += loss.item() * batch_size
- total_samples += batch_size
- # Calculate accuracy
- batch_correct, batch_total = _batch_correct_and_total(outputs, targets)
- correct += batch_correct
- total += batch_total
- test_progress.update(total=len(test_loader), advance=1)
- test_loss = test_loss / total_samples if total_samples > 0 else 0.0
- test_acc = correct / total if total > 0 else 0.0
- return test_loss, test_acc
- def train_epoch(
- model: nn.Module,
- train_loader: DataLoader[ADNIDataset],
- val_loader: DataLoader[ADNIDataset],
- optimizer: torch.optim.Optimizer,
- criterion: nn.Module,
- log: PipelineLogger,
- progress: ProgressTracker,
- stop_event: Event | None = None,
- pause_event: Event | None = None,
- ) -> Tuple[float, float, float, float]:
- """
- Trains the model for one epoch and evaluates it on the validation set.
- Args:
- model (nn.Module): The model to train.
- train_loader (DataLoader[ADNIDataset]): DataLoader for the training dataset.
- val_loader (DataLoader[ADNIDataset]): DataLoader for the validation dataset.
- optimizer (torch.optim.Optimizer): Optimizer for updating model parameters.
- criterion (nn.Module): Loss function to compute the loss.
- Returns:
- Tuple[float, float, float, float]: A tuple containing the training loss, validation loss, training accuracy, and validation accuracy.
- """
- model.train()
- train_loss = 0.0
- train_correct = 0
- train_total = 0
- train_total_samples = 0
- # Training loop - with progress bar for the training batches
- train_progress = progress.get_sub_tracker("Training Batches")
- train_progress.reset()
- train_progress.set_title("Training Batches")
- train_progress.update(total=len(train_loader), advance=0)
- for _, (mri, xls, targets, _) in enumerate(train_loader):
- _check_control_events(stop_event=stop_event, pause_event=pause_event)
- mri, xls, targets = _move_batch_to_model_device(model, mri, xls, targets)
- optimizer.zero_grad()
- outputs = model((mri, xls))
- loss = criterion(outputs, targets)
- loss.backward() # type: ignore[reportUnknownMemberType]
- optimizer.step()
- batch_size = mri.size(0)
- train_loss += loss.item() * batch_size
- train_total_samples += batch_size
- batch_correct, batch_total = _batch_correct_and_total(outputs, targets)
- train_correct += batch_correct
- train_total += batch_total
- train_progress.update(total=len(train_loader), advance=1)
- train_loss = train_loss / train_total_samples if train_total_samples > 0 else 0.0
- model.eval()
- val_loss = 0.0
- correct = 0
- total = 0
- val_total_samples = 0
- test_progress = progress.get_sub_tracker("Validation Batches")
- test_progress.reset()
- test_progress.set_title("Validation Batches")
- test_progress.update(total=len(val_loader), advance=0)
- with torch.no_grad():
- for _, (mri, xls, targets, _) in enumerate(val_loader):
- _check_control_events(stop_event=stop_event, pause_event=pause_event)
- mri, xls, targets = _move_batch_to_model_device(model, mri, xls, targets)
- outputs = model((mri, xls))
- loss = criterion(outputs, targets)
- batch_size = mri.size(0)
- val_loss += loss.item() * batch_size
- val_total_samples += batch_size
- # Calculate accuracy
- batch_correct, batch_total = _batch_correct_and_total(outputs, targets)
- correct += batch_correct
- total += batch_total
- test_progress.update(total=len(val_loader), advance=1)
- val_loss = val_loss / val_total_samples if val_total_samples > 0 else 0.0
- val_acc = correct / total if total > 0 else 0.0
- train_acc = train_correct / train_total if train_total > 0 else 0.0
- return train_loss, val_loss, train_acc, val_acc
- def train_model(
- model: nn.Module,
- train_loader: DataLoader[ADNIDataset],
- val_loader: DataLoader[ADNIDataset],
- optimizer: torch.optim.Optimizer,
- criterion: nn.Module,
- num_epochs: int,
- output_path: pl.Path,
- progress: ProgressTracker,
- log: PipelineLogger,
- stop_event: Event | None = None,
- pause_event: Event | None = None,
- ) -> Tuple[nn.Module, pd.DataFrame]:
- """
- Trains the model using the provided training and validation data loaders.
- Args:
- model (nn.Module): The model to train.
- train_loader (DataLoader[ADNIDataset]): DataLoader for the training dataset.
- val_loader (DataLoader[ADNIDataset]): DataLoader for the validation dataset.
- num_epochs (int): Number of epochs to train the model.
- learning_rate (float): Learning rate for the optimizer.
- Returns:
- Result[nn.Module, str]: A Result object containing the trained model or an error message.
- """
- # Record the training history
- # We record the Epoch, Training Loss, Validation Loss, Training Accuracy, and Validation Accuracy
- # use a (num_epochs, 4) shape ndarray to store the history before creating the DataArray
- nhist = np.zeros((num_epochs, 4), dtype=np.float32)
- # Get new progress bar for epochs
- epoch_progress = progress.get_sub_tracker("Epochs")
- epoch_progress.update(total=num_epochs, advance=0)
- for epoch in range(num_epochs):
- _check_control_events(stop_event=stop_event, pause_event=pause_event)
- train_loss, val_loss, train_acc, val_acc = train_epoch(
- model,
- train_loader,
- val_loader,
- optimizer,
- criterion,
- log=log,
- progress=epoch_progress,
- stop_event=stop_event,
- pause_event=pause_event,
- )
- # Update the history
- nhist[epoch, 0] = train_loss
- nhist[epoch, 1] = val_loss
- nhist[epoch, 2] = train_acc
- nhist[epoch, 3] = val_acc
- log.info(
- f"Epoch [{epoch + 1}/{num_epochs}], "
- f"Train Loss: {train_loss:.4f}, Val Loss: {val_loss:.4f}, "
- f"Train Acc: {train_acc:.4f}, Val Acc: {val_acc:.4f}"
- )
- # If we are at 25, 50, or 75% of the epochs, save the model
- if num_epochs > 4:
- if (epoch + 1) % (num_epochs // 4) == 0:
- model_save_path = (
- output_path / "intermediate_models" / f"model_epoch_{epoch + 1}.pt"
- )
- torch.save(model.state_dict(), model_save_path)
- log.info(f"Model saved at epoch {epoch + 1}")
- epoch_progress.update(total=num_epochs, advance=1)
- # return the trained model and the traning history
- history = pd.DataFrame(
- data=nhist.astype(np.float32),
- columns=["train_loss", "val_loss", "train_acc", "val_acc"],
- index=np.arange(1, num_epochs + 1),
- )
- return model, history
- def test_model_bayesian(
- model: nn.Module,
- test_loader: DataLoader[ADNIDataset],
- criterion: nn.Module,
- get_kl_loss: KLLossFn,
- progress: ProgressTracker,
- log: PipelineLogger,
- stop_event: Event | None = None,
- pause_event: Event | None = None,
- ) -> TestMetrics:
- """
- Tests a Bayesian model on the test dataset with KL-augmented loss.
- """
- model.eval()
- test_loss = 0.0
- correct = 0
- total = 0
- total_samples = 0
- test_progress = progress.get_sub_tracker("Testing Batches")
- test_progress.update(total=len(test_loader), advance=0)
- with torch.no_grad():
- for _, (mri, xls, targets, _) in enumerate(test_loader):
- _check_control_events(stop_event=stop_event, pause_event=pause_event)
- mri, xls, targets = _move_batch_to_model_device(model, mri, xls, targets)
- outputs = model((mri, xls))
- data_loss = cast(torch.Tensor, criterion(outputs, targets))
- batch_size = mri.size(0)
- kl_term = get_kl_loss(model)
- kl_loss = (
- kl_term / batch_size
- if kl_term is not None
- else torch.tensor(0.0, device=outputs.device)
- )
- loss: torch.Tensor = data_loss + kl_loss
- test_loss += loss.item() * batch_size
- total_samples += batch_size
- batch_correct, batch_total = _batch_correct_and_total(outputs, targets)
- correct += batch_correct
- total += batch_total
- test_progress.update(total=len(test_loader), advance=1)
- test_loss = test_loss / total_samples if total_samples > 0 else 0.0
- test_acc = correct / total if total > 0 else 0.0
- return test_loss, test_acc
- def train_epoch_bayesian(
- model: nn.Module,
- train_loader: DataLoader[ADNIDataset],
- val_loader: DataLoader[ADNIDataset],
- optimizer: torch.optim.Optimizer,
- criterion: nn.Module,
- get_kl_loss: KLLossFn,
- log: PipelineLogger,
- progress: ProgressTracker,
- stop_event: Event | None = None,
- pause_event: Event | None = None,
- ) -> TrainMetrics:
- """
- Trains a Bayesian model for one epoch and evaluates on validation data.
- """
- model.train()
- train_loss = 0.0
- train_correct = 0
- train_total = 0
- train_total_samples = 0
- train_progress = progress.get_sub_tracker("Training Batches")
- train_progress.reset()
- train_progress.set_title("Training Batches")
- train_progress.update(total=len(train_loader), advance=0)
- for _, (mri, xls, targets, _) in enumerate(train_loader):
- _check_control_events(stop_event=stop_event, pause_event=pause_event)
- mri, xls, targets = _move_batch_to_model_device(model, mri, xls, targets)
- optimizer.zero_grad()
- outputs = model((mri, xls))
- data_loss = cast(torch.Tensor, criterion(outputs, targets))
- batch_size = mri.size(0)
- kl_term = get_kl_loss(model)
- kl_loss = (
- kl_term / batch_size
- if kl_term is not None
- else torch.tensor(0.0, device=outputs.device)
- )
- loss: torch.Tensor = data_loss + kl_loss
- loss.backward() # type: ignore[reportUnknownMemberType]
- optimizer.step()
- train_loss += loss.item() * batch_size
- train_total_samples += batch_size
- batch_correct, batch_total = _batch_correct_and_total(outputs, targets)
- train_correct += batch_correct
- train_total += batch_total
- train_progress.update(total=len(train_loader), advance=1)
- train_loss = train_loss / train_total_samples if train_total_samples > 0 else 0.0
- model.eval()
- val_loss = 0.0
- correct = 0
- total = 0
- val_total_samples = 0
- test_progress = progress.get_sub_tracker("Validation Batches")
- test_progress.reset()
- test_progress.set_title("Validation Batches")
- test_progress.update(total=len(val_loader), advance=0)
- with torch.no_grad():
- for _, (mri, xls, targets, _) in enumerate(val_loader):
- _check_control_events(stop_event=stop_event, pause_event=pause_event)
- mri, xls, targets = _move_batch_to_model_device(model, mri, xls, targets)
- outputs = model((mri, xls))
- data_loss = cast(torch.Tensor, criterion(outputs, targets))
- batch_size = mri.size(0)
- kl_term = get_kl_loss(model)
- kl_loss = (
- kl_term / batch_size
- if kl_term is not None
- else torch.tensor(0.0, device=outputs.device)
- )
- vloss: torch.Tensor = data_loss + kl_loss
- val_loss += vloss.item() * batch_size
- val_total_samples += batch_size
- batch_correct, batch_total = _batch_correct_and_total(outputs, targets)
- correct += batch_correct
- total += batch_total
- test_progress.update(total=len(val_loader), advance=1)
- val_loss = val_loss / val_total_samples if val_total_samples > 0 else 0.0
- val_acc = correct / total if total > 0 else 0.0
- train_acc = train_correct / train_total if train_total > 0 else 0.0
- return train_loss, val_loss, train_acc, val_acc
- def train_model_bayesian(
- model: nn.Module,
- train_loader: DataLoader[ADNIDataset],
- val_loader: DataLoader[ADNIDataset],
- optimizer: torch.optim.Optimizer,
- criterion: nn.Module,
- num_epochs: int,
- output_path: pl.Path,
- get_kl_loss: KLLossFn,
- progress: ProgressTracker,
- log: PipelineLogger,
- stop_event: Event | None = None,
- pause_event: Event | None = None,
- ) -> Tuple[nn.Module, pd.DataFrame]:
- """
- Trains a Bayesian model with KL-augmented objective.
- """
- nhist = np.zeros((num_epochs, 4), dtype=np.float32)
- epoch_progress = progress.get_sub_tracker("Epochs")
- epoch_progress.update(total=num_epochs, advance=0)
- for epoch in range(num_epochs):
- _check_control_events(stop_event=stop_event, pause_event=pause_event)
- train_loss, val_loss, train_acc, val_acc = train_epoch_bayesian(
- model,
- train_loader,
- val_loader,
- optimizer,
- criterion,
- get_kl_loss,
- log=log,
- progress=epoch_progress,
- stop_event=stop_event,
- pause_event=pause_event,
- )
- nhist[epoch, 0] = train_loss
- nhist[epoch, 1] = val_loss
- nhist[epoch, 2] = train_acc
- nhist[epoch, 3] = val_acc
- log.info(
- f"Epoch [{epoch + 1}/{num_epochs}], "
- f"Train Loss: {train_loss:.4f}, Val Loss: {val_loss:.4f}, "
- f"Train Acc: {train_acc:.4f}, Val Acc: {val_acc:.4f}"
- )
- if num_epochs > 4:
- if (epoch + 1) % (num_epochs // 4) == 0:
- model_save_path = (
- output_path / "intermediate_models" / f"model_epoch_{epoch + 1}.pt"
- )
- torch.save(model.state_dict(), model_save_path)
- log.info(f"Model saved at epoch {epoch + 1}")
- epoch_progress.update(total=num_epochs, advance=1)
- history = pd.DataFrame(
- data=nhist.astype(np.float32),
- columns=["train_loss", "val_loss", "train_acc", "val_acc"],
- index=np.arange(1, num_epochs + 1),
- )
- return model, history
|