diff --git a/configs/default.yaml b/configs/default.yaml index 5ffa1f9..67e68ad 100644 --- a/configs/default.yaml +++ b/configs/default.yaml @@ -33,6 +33,12 @@ model: temporal_pooling: mean num_classes: 2 +dataloader: + batch_size: 8 + num_workers: 4 + pin_memory: true + drop_last: false + evaluation: metrics: - accuracy diff --git a/src/training/checkpoint.py b/src/training/checkpoint.py new file mode 100644 index 0000000..17f00e3 --- /dev/null +++ b/src/training/checkpoint.py @@ -0,0 +1,74 @@ +""" +DeepVision AI + +Checkpoint Manager +""" + +from pathlib import Path +from datetime import datetime +import json +import shutil + +import torch +import yaml + + +class CheckpointManager: + """ + Handles experiment tracking and checkpoint saving. + """ + + def __init__(self, root_dir="outputs/runs"): + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + + self.run_dir = Path(root_dir) / f"run_{timestamp}" + + self.run_dir.mkdir(parents=True, exist_ok=True) + + @property + def path(self): + return self.run_dir + + def save_best_model(self, model): + + torch.save( + model.state_dict(), + self.run_dir / "best_model.pth", + ) + + def save_last_model(self, model): + + torch.save( + model.state_dict(), + self.run_dir / "last_model.pth", + ) + + def save_metrics(self, metrics): + + with open( + self.run_dir / "metrics.json", + "w", + ) as f: + + json.dump(metrics, f, indent=4) + + def save_config(self, config): + + with open( + self.run_dir / "config.yaml", + "w", + ) as f: + + yaml.dump(config, f) + + def copy_log(self, log_path): + + log_path = Path(log_path) + + if log_path.exists(): + + shutil.copy( + log_path, + self.run_dir / log_path.name, + ) \ No newline at end of file diff --git a/src/training/early_stopping.py b/src/training/early_stopping.py new file mode 100644 index 0000000..3125167 --- /dev/null +++ b/src/training/early_stopping.py @@ -0,0 +1,47 @@ +""" +DeepVision AI + +Early Stopping Utility +""" + +import numpy as np + + +class EarlyStopping: + """ + Stops training when validation loss + stops improving. + """ + + def __init__( + self, + patience=5, + min_delta=0.0, + ): + + self.patience = patience + self.min_delta = min_delta + + self.best_loss = np.inf + + self.counter = 0 + + self.should_stop = False + + def __call__(self, val_loss): + + if val_loss < self.best_loss - self.min_delta: + + self.best_loss = val_loss + + self.counter = 0 + + else: + + self.counter += 1 + + if self.counter >= self.patience: + + self.should_stop = True + + return self.should_stop \ No newline at end of file diff --git a/src/training/trainer.py b/src/training/trainer.py new file mode 100644 index 0000000..0141f7e --- /dev/null +++ b/src/training/trainer.py @@ -0,0 +1,176 @@ +""" +DeepVision AI + +Generic Trainer +""" + +from pathlib import Path + +import torch + + +class Trainer: + + def __init__( + self, + model, + optimizer, + criterion, + train_loader, + val_loader, + device, + logger, + checkpoint_manager, + early_stopping, + scheduler=None, + ): + + self.model = model.to(device) + + self.optimizer = optimizer + + self.criterion = criterion + + self.train_loader = train_loader + + self.val_loader = val_loader + + self.device = device + + self.logger = logger + + self.checkpoint = checkpoint_manager + + self.early_stopping = early_stopping + + self.scheduler = scheduler + + def train_one_epoch(self): + + self.model.train() + + running_loss = 0.0 + + correct = 0 + + total = 0 + + for batch in self.train_loader: + + images = batch["sequence"].to(self.device) + + labels = batch["label"].to(self.device) + + self.optimizer.zero_grad() + + outputs = self.model(images) + + loss = self.criterion(outputs, labels) + + loss.backward() + + self.optimizer.step() + + running_loss += loss.item() + + _, predicted = outputs.max(1) + + total += labels.size(0) + + correct += predicted.eq(labels).sum().item() + + epoch_loss = running_loss / len(self.train_loader) + + epoch_acc = 100 * correct / total + + return epoch_loss, epoch_acc + + def validate(self): + + self.model.eval() + + running_loss = 0.0 + + correct = 0 + + total = 0 + + with torch.no_grad(): + + for batch in self.val_loader: + + images = batch["sequence"].to(self.device) + + labels = batch["label"].to(self.device) + + outputs = self.model(images) + + loss = self.criterion(outputs, labels) + + running_loss += loss.item() + + _, predicted = outputs.max(1) + + total += labels.size(0) + + correct += predicted.eq(labels).sum().item() + + epoch_loss = running_loss / len(self.val_loader) + + epoch_acc = 100 * correct / total + + return epoch_loss, epoch_acc + + def train(self, epochs): + + best_loss = float("inf") + + history = [] + + self.logger.info("Training Started") + + for epoch in range(epochs): + + train_loss, train_acc = self.train_one_epoch() + + val_loss, val_acc = self.validate() + + if self.scheduler: + + self.scheduler.step() + + self.logger.info( + f"Epoch {epoch+1}/{epochs} | " + f"Train Loss={train_loss:.4f} | " + f"Train Acc={train_acc:.2f}% | " + f"Val Loss={val_loss:.4f} | " + f"Val Acc={val_acc:.2f}%" + ) + + history.append( + { + "epoch": epoch + 1, + "train_loss": train_loss, + "train_acc": train_acc, + "val_loss": val_loss, + "val_acc": val_acc, + } + ) + + if val_loss < best_loss: + + best_loss = val_loss + + self.checkpoint.save_best_model(self.model) + + self.checkpoint.save_last_model(self.model) + + if self.early_stopping(val_loss): + + self.logger.info("Early stopping triggered.") + + break + + self.checkpoint.save_metrics(history) + + return history \ No newline at end of file diff --git a/src/utils/logger.py b/src/utils/logger.py new file mode 100644 index 0000000..56d550f --- /dev/null +++ b/src/utils/logger.py @@ -0,0 +1,50 @@ +""" +DeepVision AI + +Logger Utility +""" + +from pathlib import Path +import logging + + +def create_logger( + log_dir="outputs/logs", + log_name="train.log", +): + """ + Create a reusable logger. + """ + + log_dir = Path(log_dir) + + log_dir.mkdir( + parents=True, + exist_ok=True, + ) + + logger = logging.getLogger("DeepVision") + + logger.setLevel(logging.INFO) + + logger.handlers.clear() + + formatter = logging.Formatter( + "%(asctime)s | %(levelname)s | %(message)s" + ) + + file_handler = logging.FileHandler( + log_dir / log_name + ) + + file_handler.setFormatter(formatter) + + console_handler = logging.StreamHandler() + + console_handler.setFormatter(formatter) + + logger.addHandler(file_handler) + + logger.addHandler(console_handler) + + return logger \ No newline at end of file diff --git a/tests/test_checkpoint.py b/tests/test_checkpoint.py new file mode 100644 index 0000000..ab57291 --- /dev/null +++ b/tests/test_checkpoint.py @@ -0,0 +1,36 @@ +import torch.nn as nn + +from src.training.checkpoint import CheckpointManager +from src.utils.config import load_config + + +def main(): + + manager = CheckpointManager() + + print("Run Directory") + + print(manager.path) + + model = nn.Linear(10, 2) + + manager.save_best_model(model) + + manager.save_last_model(model) + + manager.save_metrics( + { + "accuracy": 0.95, + "loss": 0.12, + } + ) + + config = load_config() + + manager.save_config(config) + + print("\nCheckpoint Test Passed") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/test_dataloader.py b/tests/test_dataloader.py new file mode 100644 index 0000000..d0cfc2d --- /dev/null +++ b/tests/test_dataloader.py @@ -0,0 +1,26 @@ +from src.utils.config import load_config +from src.datasets.dataloader import create_dataloaders + + +def main(): + + config = load_config() + + train_loader, val_loader, test_loader = create_dataloaders(config) + + print(f"Train batches : {len(train_loader)}") + print(f"Validation batches : {len(val_loader)}") + print(f"Test batches : {len(test_loader)}") + + batch = next(iter(train_loader)) + + print("\nBatch Information") + + print("Sequence:", batch["sequence"].shape) + print("Labels:", batch["label"].shape) + print("Datasets:", batch["dataset"][:5]) + print("Videos:", batch["video_name"][:5]) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/test_early_stopping.py b/tests/test_early_stopping.py new file mode 100644 index 0000000..131672a --- /dev/null +++ b/tests/test_early_stopping.py @@ -0,0 +1,41 @@ +from src.training.early_stopping import EarlyStopping + + +def main(): + + early_stop = EarlyStopping( + patience=3, + min_delta=0.001, + ) + + losses = [ + 0.70, + 0.55, + 0.40, + 0.35, + 0.33, + 0.331, + 0.332, + 0.335, + 0.340, + ] + + print("Validation Loss Progress\n") + + for epoch, loss in enumerate(losses, start=1): + + stop = early_stop(loss) + + print( + f"Epoch {epoch:02d} | Loss = {loss:.4f}" + ) + + if stop: + + print("\nEarly stopping triggered.") + + break + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/test_logger.py b/tests/test_logger.py new file mode 100644 index 0000000..461dd53 --- /dev/null +++ b/tests/test_logger.py @@ -0,0 +1,20 @@ +from src.utils.logger import create_logger + + +def main(): + + logger = create_logger() + + logger.info("DeepVision AI Logger Initialized") + + logger.info("Training Started") + + logger.warning("Sample Warning") + + logger.error("Sample Error") + + print("\nLogger Test Complete") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/test_sampler.py b/tests/test_sampler.py new file mode 100644 index 0000000..63c2fa7 --- /dev/null +++ b/tests/test_sampler.py @@ -0,0 +1,15 @@ +from src.datasets.sequence_dataset import SequenceDataset +from src.datasets.balanced_sampler import create_balanced_sampler + +dataset = SequenceDataset( + csv_file="outputs/dataset_index.csv", + sequence_root="data/intermediate/sequences" +) + +sampler = create_balanced_sampler(dataset) + +print("Dataset Size :", len(dataset)) + +print("Sampler Size :", len(sampler)) + +print(type(sampler)) \ No newline at end of file diff --git a/tests/test_split_generator.py b/tests/test_split_generator.py new file mode 100644 index 0000000..f1d1055 --- /dev/null +++ b/tests/test_split_generator.py @@ -0,0 +1,12 @@ +from src.datasets.split_generator import DatasetSplitGenerator + + +def main(): + + generator = DatasetSplitGenerator() + + generator.generate() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/test_trainer.py b/tests/test_trainer.py new file mode 100644 index 0000000..33e09b8 --- /dev/null +++ b/tests/test_trainer.py @@ -0,0 +1,80 @@ +import torch +import torch.nn as nn +from torch.utils.data import DataLoader + +from src.training.trainer import Trainer +from src.training.early_stopping import EarlyStopping +from src.training.checkpoint import CheckpointManager +from src.utils.logger import create_logger + + +class DummyDataset(torch.utils.data.Dataset): + + def __len__(self): + return 64 + + def __getitem__(self, idx): + + return { + "sequence": torch.randn(3, 32), + "label": torch.randint(0, 2, (1,)).squeeze() + } + + +class DummyModel(nn.Module): + + def __init__(self): + super().__init__() + + self.net = nn.Sequential( + nn.Flatten(), + nn.Linear(96, 32), + nn.ReLU(), + nn.Linear(32, 2) + ) + + def forward(self, x): + return self.net(x) + + +def main(): + + train_loader = DataLoader( + DummyDataset(), + batch_size=8, + shuffle=True + ) + + val_loader = DataLoader( + DummyDataset(), + batch_size=8 + ) + + model = DummyModel() + + optimizer = torch.optim.Adam( + model.parameters(), + lr=1e-3 + ) + + criterion = nn.CrossEntropyLoss() + + trainer = Trainer( + model=model, + optimizer=optimizer, + criterion=criterion, + train_loader=train_loader, + val_loader=val_loader, + device="cpu", + logger=create_logger(), + checkpoint_manager=CheckpointManager(), + early_stopping=EarlyStopping(patience=2) + ) + + trainer.train(epochs=3) + + print("\nTrainer Test Passed") + + +if __name__ == "__main__": + main() \ No newline at end of file