garmentiq.classification.train_pytorch_nn
Cross-validated training of a classification model from scratch.
1"""Cross-validated training of a classification model from scratch.""" 2import torch 3import torch.nn as nn 4from torch.utils.data import DataLoader, Dataset 5from typing import Callable, Type 6from tqdm.auto import tqdm 7import os 8from sklearn.model_selection import StratifiedKFold 9from garmentiq.utils.device import empty_cache 10from garmentiq.classification.utils import ( 11 CachedDataset, 12 seed_worker, 13 train_epoch, 14 validate_epoch, 15 save_best_model, 16 validate_train_param, 17 validate_test_param, 18) 19 20 21def train_pytorch_nn( 22 model_class: Type[torch.nn.Module], 23 model_args: dict, 24 dataset_class: Callable, 25 dataset_args: dict, 26 param: dict, 27): 28 """ 29 Trains a PyTorch neural network using k-fold cross-validation with early stopping and model checkpointing. 30 31 This function performs training and validation across multiple folds using stratified sampling. 32 It manages model instantiation, training loops, early stopping, and saves the best model based on validation loss. 33 34 Args: 35 model_class (Type[torch.nn.Module]): The class of the PyTorch model to instantiate. 36 Must inherit from `torch.nn.Module`. 37 model_args (dict): Dictionary of arguments used to initialize `model_class`. 38 dataset_class (Callable): A callable class or function that returns a `torch.utils.data.Dataset`-compatible dataset. 39 dataset_args (dict): Dictionary with dataset components: 40 - 'metadata_df' (pandas.DataFrame): Metadata with labels, used for stratification. 41 - 'raw_labels' (array-like): Raw class labels used by StratifiedKFold. 42 - 'cached_images' (torch.Tensor): Preprocessed image tensor. 43 - 'cached_labels' (torch.Tensor): Corresponding labels. 44 param (dict): Dictionary of training hyperparameters and configuration values. 45 Required Keys: 46 - `optimizer_class` (type): PyTorch optimizer class (e.g., `torch.optim.Adam`). 47 - `optimizer_args` (dict): Arguments passed to the optimizer. 48 Optional Keys (with defaults and types): 49 - `device` (Union[str, torch.device]): Training device, e.g. `"cpu"`, 50 `"cuda"`, `"cuda:0"`, or `"mps"`. Hardware acceleration is opt-in; 51 pass it explicitly to use a GPU or Apple Silicon. Default: `"cpu"`. 52 - `n_fold` (int): Number of stratified folds for cross-validation. Default: 5. 53 - `n_epoch` (int): Number of training epochs per fold. Default: 100. 54 - `patience` (int): Epochs to wait before early stopping. Default: 5. 55 - `batch_size` (int): Batch size for training and validation. Default: 64. 56 - `model_save_dir` (str): Directory to save model checkpoints. Default: `"./models"`. 57 - `seed` (int): Random seed for reproducibility. Default: 88. 58 - `seed_worker` (Callable): Function to seed workers in the DataLoader. Default: `seed_worker`. 59 - `max_workers` (int): Number of subprocesses for data loading. Default: `os.cpu_count()`. 60 - `best_model_name` (str): Filename for saving the best model. Default: `"best_model.pt"`. 61 62 Raises: 63 ValueError: If any required key is missing from `param`. 64 TypeError: If any parameter is of the wrong type. 65 FileNotFoundError: If the model directory cannot be created or accessed. 66 67 Returns: 68 None 69 """ 70 # Validate and catch parameters 71 validate_train_param(param) 72 73 # Prepare save directories 74 os.makedirs(param["model_save_dir"], exist_ok=True) 75 overall_best_loss = float("inf") 76 best_model_path = os.path.join(param["model_save_dir"], param["best_model_name"]) 77 78 kfold = StratifiedKFold( 79 n_splits=param["n_fold"], shuffle=True, random_state=param["seed"] 80 ) 81 82 # Loop through each fold 83 for fold, (train_idx, val_idx) in enumerate( 84 kfold.split(dataset_args["metadata_df"], dataset_args["raw_labels"]) 85 ): 86 print(f"\nFold {fold + 1}/{param['n_fold']}") 87 88 # Prepare datasets and dataloaders 89 train_dataset = dataset_class( 90 train_idx, dataset_args["cached_images"], dataset_args["cached_labels"] 91 ) 92 val_dataset = dataset_class( 93 val_idx, dataset_args["cached_images"], dataset_args["cached_labels"] 94 ) 95 96 g = torch.Generator() 97 g.manual_seed(param["seed"]) 98 99 train_loader = DataLoader( 100 train_dataset, 101 batch_size=param["batch_size"], 102 shuffle=True, 103 num_workers=param["max_workers"], 104 worker_init_fn=param["seed_worker"], 105 generator=g, 106 pin_memory=param["pin_memory"], 107 persistent_workers=param["persistent_workers"], 108 ) 109 val_loader = DataLoader( 110 val_dataset, 111 batch_size=param["batch_size"], 112 shuffle=False, 113 num_workers=param["max_workers"], 114 worker_init_fn=param["seed_worker"], 115 generator=g, 116 pin_memory=param["pin_memory"], 117 persistent_workers=param["persistent_workers"], 118 ) 119 120 # Initialize model and optimizer 121 model = model_class(**model_args).to(param["device"]) 122 if param["device"].type == "cuda" and torch.cuda.device_count() > 1: 123 model = torch.nn.DataParallel(model) 124 optimizer = param["optimizer_class"]( 125 model.parameters(), **param["optimizer_args"] 126 ) 127 empty_cache(param["device"]) 128 129 best_fold_loss = float("inf") 130 patience_counter = 0 131 epoch_pbar = tqdm(range(param["n_epoch"]), desc="Total Progress", leave=False) 132 133 # Training and Validation Loop 134 for epoch in epoch_pbar: 135 # Training phase 136 epoch_loss = train_epoch(model, train_loader, optimizer, param) 137 # Validation phase 138 val_loss, f1, acc = validate_epoch(model, val_loader, param) 139 140 # Save the best model and check for early stopping 141 best_fold_loss, patience_counter, overall_best_loss = save_best_model( 142 model, 143 val_loss, 144 best_fold_loss, 145 patience_counter, 146 overall_best_loss, 147 param, 148 fold, 149 best_model_path, 150 ) 151 # Early stopping 152 epoch_pbar.set_postfix( 153 { 154 "train_loss": f"{epoch_loss:.4f}", 155 "val_loss": f"{val_loss:.4f}", 156 "val_acc": f"{acc:.4f}", 157 "val_f1": f"{f1:.4f}", 158 "patience": patience_counter, 159 } 160 ) 161 162 print( 163 f"Fold {fold+1} | Epoch {epoch+1} | Val Loss: {val_loss:.4f} | F1: {f1:.4f} | Acc: {acc:.4f}" 164 ) 165 166 if patience_counter >= param["patience"]: 167 print(f"Early stopping at epoch {epoch+1} (fold {fold + 1})") 168 break 169 170 del model 171 empty_cache(param["device"]) 172 173 print(f"\nTraining completed. Best model saved at: {best_model_path}")
def
train_pytorch_nn( model_class: Type[torch.nn.modules.module.Module], model_args: dict, dataset_class: Callable, dataset_args: dict, param: dict):
22def train_pytorch_nn( 23 model_class: Type[torch.nn.Module], 24 model_args: dict, 25 dataset_class: Callable, 26 dataset_args: dict, 27 param: dict, 28): 29 """ 30 Trains a PyTorch neural network using k-fold cross-validation with early stopping and model checkpointing. 31 32 This function performs training and validation across multiple folds using stratified sampling. 33 It manages model instantiation, training loops, early stopping, and saves the best model based on validation loss. 34 35 Args: 36 model_class (Type[torch.nn.Module]): The class of the PyTorch model to instantiate. 37 Must inherit from `torch.nn.Module`. 38 model_args (dict): Dictionary of arguments used to initialize `model_class`. 39 dataset_class (Callable): A callable class or function that returns a `torch.utils.data.Dataset`-compatible dataset. 40 dataset_args (dict): Dictionary with dataset components: 41 - 'metadata_df' (pandas.DataFrame): Metadata with labels, used for stratification. 42 - 'raw_labels' (array-like): Raw class labels used by StratifiedKFold. 43 - 'cached_images' (torch.Tensor): Preprocessed image tensor. 44 - 'cached_labels' (torch.Tensor): Corresponding labels. 45 param (dict): Dictionary of training hyperparameters and configuration values. 46 Required Keys: 47 - `optimizer_class` (type): PyTorch optimizer class (e.g., `torch.optim.Adam`). 48 - `optimizer_args` (dict): Arguments passed to the optimizer. 49 Optional Keys (with defaults and types): 50 - `device` (Union[str, torch.device]): Training device, e.g. `"cpu"`, 51 `"cuda"`, `"cuda:0"`, or `"mps"`. Hardware acceleration is opt-in; 52 pass it explicitly to use a GPU or Apple Silicon. Default: `"cpu"`. 53 - `n_fold` (int): Number of stratified folds for cross-validation. Default: 5. 54 - `n_epoch` (int): Number of training epochs per fold. Default: 100. 55 - `patience` (int): Epochs to wait before early stopping. Default: 5. 56 - `batch_size` (int): Batch size for training and validation. Default: 64. 57 - `model_save_dir` (str): Directory to save model checkpoints. Default: `"./models"`. 58 - `seed` (int): Random seed for reproducibility. Default: 88. 59 - `seed_worker` (Callable): Function to seed workers in the DataLoader. Default: `seed_worker`. 60 - `max_workers` (int): Number of subprocesses for data loading. Default: `os.cpu_count()`. 61 - `best_model_name` (str): Filename for saving the best model. Default: `"best_model.pt"`. 62 63 Raises: 64 ValueError: If any required key is missing from `param`. 65 TypeError: If any parameter is of the wrong type. 66 FileNotFoundError: If the model directory cannot be created or accessed. 67 68 Returns: 69 None 70 """ 71 # Validate and catch parameters 72 validate_train_param(param) 73 74 # Prepare save directories 75 os.makedirs(param["model_save_dir"], exist_ok=True) 76 overall_best_loss = float("inf") 77 best_model_path = os.path.join(param["model_save_dir"], param["best_model_name"]) 78 79 kfold = StratifiedKFold( 80 n_splits=param["n_fold"], shuffle=True, random_state=param["seed"] 81 ) 82 83 # Loop through each fold 84 for fold, (train_idx, val_idx) in enumerate( 85 kfold.split(dataset_args["metadata_df"], dataset_args["raw_labels"]) 86 ): 87 print(f"\nFold {fold + 1}/{param['n_fold']}") 88 89 # Prepare datasets and dataloaders 90 train_dataset = dataset_class( 91 train_idx, dataset_args["cached_images"], dataset_args["cached_labels"] 92 ) 93 val_dataset = dataset_class( 94 val_idx, dataset_args["cached_images"], dataset_args["cached_labels"] 95 ) 96 97 g = torch.Generator() 98 g.manual_seed(param["seed"]) 99 100 train_loader = DataLoader( 101 train_dataset, 102 batch_size=param["batch_size"], 103 shuffle=True, 104 num_workers=param["max_workers"], 105 worker_init_fn=param["seed_worker"], 106 generator=g, 107 pin_memory=param["pin_memory"], 108 persistent_workers=param["persistent_workers"], 109 ) 110 val_loader = DataLoader( 111 val_dataset, 112 batch_size=param["batch_size"], 113 shuffle=False, 114 num_workers=param["max_workers"], 115 worker_init_fn=param["seed_worker"], 116 generator=g, 117 pin_memory=param["pin_memory"], 118 persistent_workers=param["persistent_workers"], 119 ) 120 121 # Initialize model and optimizer 122 model = model_class(**model_args).to(param["device"]) 123 if param["device"].type == "cuda" and torch.cuda.device_count() > 1: 124 model = torch.nn.DataParallel(model) 125 optimizer = param["optimizer_class"]( 126 model.parameters(), **param["optimizer_args"] 127 ) 128 empty_cache(param["device"]) 129 130 best_fold_loss = float("inf") 131 patience_counter = 0 132 epoch_pbar = tqdm(range(param["n_epoch"]), desc="Total Progress", leave=False) 133 134 # Training and Validation Loop 135 for epoch in epoch_pbar: 136 # Training phase 137 epoch_loss = train_epoch(model, train_loader, optimizer, param) 138 # Validation phase 139 val_loss, f1, acc = validate_epoch(model, val_loader, param) 140 141 # Save the best model and check for early stopping 142 best_fold_loss, patience_counter, overall_best_loss = save_best_model( 143 model, 144 val_loss, 145 best_fold_loss, 146 patience_counter, 147 overall_best_loss, 148 param, 149 fold, 150 best_model_path, 151 ) 152 # Early stopping 153 epoch_pbar.set_postfix( 154 { 155 "train_loss": f"{epoch_loss:.4f}", 156 "val_loss": f"{val_loss:.4f}", 157 "val_acc": f"{acc:.4f}", 158 "val_f1": f"{f1:.4f}", 159 "patience": patience_counter, 160 } 161 ) 162 163 print( 164 f"Fold {fold+1} | Epoch {epoch+1} | Val Loss: {val_loss:.4f} | F1: {f1:.4f} | Acc: {acc:.4f}" 165 ) 166 167 if patience_counter >= param["patience"]: 168 print(f"Early stopping at epoch {epoch+1} (fold {fold + 1})") 169 break 170 171 del model 172 empty_cache(param["device"]) 173 174 print(f"\nTraining completed. Best model saved at: {best_model_path}")
Trains a PyTorch neural network using k-fold cross-validation with early stopping and model checkpointing.
This function performs training and validation across multiple folds using stratified sampling. It manages model instantiation, training loops, early stopping, and saves the best model based on validation loss.
Arguments:
- model_class (Type[torch.nn.Module]): The class of the PyTorch model to instantiate.
Must inherit from
torch.nn.Module. - model_args (dict): Dictionary of arguments used to initialize
model_class. - dataset_class (Callable): A callable class or function that returns a
torch.utils.data.Dataset-compatible dataset. - dataset_args (dict): Dictionary with dataset components:
- 'metadata_df' (pandas.DataFrame): Metadata with labels, used for stratification.
- 'raw_labels' (array-like): Raw class labels used by StratifiedKFold.
- 'cached_images' (torch.Tensor): Preprocessed image tensor.
- 'cached_labels' (torch.Tensor): Corresponding labels.
- param (dict): Dictionary of training hyperparameters and configuration values.
Required Keys:
-
optimizer_class(type): PyTorch optimizer class (e.g.,torch.optim.Adam). -optimizer_args(dict): Arguments passed to the optimizer. Optional Keys (with defaults and types): -device(Union[str, torch.device]): Training device, e.g."cpu","cuda","cuda:0", or"mps". Hardware acceleration is opt-in; pass it explicitly to use a GPU or Apple Silicon. Default:"cpu". -n_fold(int): Number of stratified folds for cross-validation. Default: 5. -n_epoch(int): Number of training epochs per fold. Default: 100. -patience(int): Epochs to wait before early stopping. Default: 5. -batch_size(int): Batch size for training and validation. Default: 64. -model_save_dir(str): Directory to save model checkpoints. Default:"./models". -seed(int): Random seed for reproducibility. Default: 88. -seed_worker(Callable): Function to seed workers in the DataLoader. Default:seed_worker. -max_workers(int): Number of subprocesses for data loading. Default:os.cpu_count(). -best_model_name(str): Filename for saving the best model. Default:"best_model.pt".
Raises:
- ValueError: If any required key is missing from
param. - TypeError: If any parameter is of the wrong type.
- FileNotFoundError: If the model directory cannot be created or accessed.
Returns:
None