#!/usr/bin/env python import sys import os # Get the current script's directory script_dir = os.path.dirname(os.path.realpath(__file__)) # Get the root repository directory by going up one level from the script directory repo_root = os.path.dirname(script_dir) # Add the repository root to the Python path sys.path.append(repo_root) import pandas as pd import pytorch_lightning as pl import wandb from argparse import ArgumentParser from pytorch_lightning.callbacks import LearningRateMonitor from pytorch_lightning.loggers import WandbLogger from torchvision.transforms import Compose from src.datasets import load_dataset_SN7, load_dataset_JIF, load_dataset_ProbaV from src.lightning_modules import LitModel, ImagePredictionLogger from src.modules import SRCNN as srcnn, HighResNet as highresnet from src.transforms import FilterData from pytorch_lightning.callbacks.early_stopping import EarlyStopping from pytorch_lightning.callbacks import ModelCheckpoint # from pl_bolts.callbacks import ModuleDataMonitor def cli_main(): """ CLI entrypoint for training. - Parses the CLI arguments - Intialises WandB - Loads the dataset - Generates the model - Runs the training - Finishes WandB logging """ args = parse_arguments() set_random_seed(args) initialise_wandb(args) dataloaders = load_dataset(args) generate_model_backbone(args, dataloaders) add_gpu_augmentations(args) model = generate_model(args) add_callbacks(args, dataloaders) generate_and_run_trainer(args, dataloaders, model) finish_wandb_logging(args) def generate_model(args): """ Generates a Lightning model from the arguments. Parameters ---------- args : argparse.Namespace Arguments parsed from the CLI and model and project specific arguments. Returns ------- LightningModule The Lightning model. """ model = LitModel(**vars(args)) # Lightning model return model def set_random_seed(args): """ Sets the random seed for reproducibility. Parameters ---------- args : argparse.Namespace Parsed arguments containing the random seed (args.seed). """ pl.seed_everything(args.seed) def finish_wandb_logging(args): """ Finishes logging in WandB. Uploads the model to WandB if the upload_checkpoint flag is set. Parameters ---------- args : argparse.Namespace Parsed arguments containing the random seed (args.seed). """ if args.upload_checkpoint: wandb.save(f"checkpoints/{wandb.run.id}-checkpoint.ckpt") wandb.finish() def generate_and_run_trainer(args, dataloaders, model): """ Generates and runs the Lightning trainer. Parameters ---------- args : argparse.Namespace Arguments parsed from the CLI and model and project specific arguments. dataloaders : dict Dictionary containing the dataloaders for the training, validation and test set. model : LightningModule The Lightning model. """ trainer = pl.Trainer.from_argparse_args(args) trainer.fit(model, dataloaders["train"], dataloaders["val"]) if not args.fast_dev_run: trainer.test(model=model, dataloaders=dataloaders["test"]) def add_callbacks(args, dataloaders): """ Adds logging and early stopping callbacks to the Lightning trainer. WandbLogger, ImagePredictionLogger and LearningRateMonitor are added. ModelCheckpoint is used to save the best model during training. EarlyStopping is used to stop training if the validation loss does not improve for 4 epochs. Parameters ---------- args : argparse.Namespace Arguments parsed from the CLI and model and project specific arguments. dataloaders : dict Dictionary containing the dataloaders for the training, validation and test set. """ vars(args)["logger"] = WandbLogger( project="esasuperres", entity="whyhowltd", config=args ) vars(args)["callbacks"] = [ ImagePredictionLogger( train_dataloader=dataloaders["train"], val_dataloader=dataloaders["val"], test_dataloader=dataloaders["test"], log_every_n_epochs=1, window_size=tuple(args.chip_size), ), LearningRateMonitor(logging_interval="step"), # EarlyStopping(monitor="val/loss", patience=4), ModelCheckpoint( dirpath="checkpoints", filename=f"{wandb.run.id}-checkpoint", save_top_k=1, verbose=True, monitor="val/loss", mode="min", ), ] def add_gpu_augmentations(args): """ Adds GPU augmentations to the Lightning model. Parameters ---------- args : argparse.Namespace Arguments parsed from the CLI and model and project specific arguments. """ vars(args)["transform"] = Compose( [ # GPU augmentations # FilterData(values=(0,), thres=0.4, fill="zero", batched=True), # RandomRotateFlipDict(angles=[0, 90, 180, 270], batched=True), ] ) def generate_model_backbone(args, dataloaders): """ Generates the model backbone from the arguments and the dataset. Parameters ---------- args : argparse.Namespace Arguments parsed from the CLI and model and project specific arguments. dataloaders : dict Dictionary containing the dataloaders for the training, validation and """ in_channels = dataloaders["train"].dataset[0]["lr"].shape[1] out_channels = dataloaders["train"].dataset[0]["hr"].shape[1] mask_channels = dataloaders["train"].dataset[0]["lrc"].shape[1] if args.use_masks: in_channels += mask_channels crop_ratio = args.chip_size[0] / args.input_size[0] output_height, output_width = args.output_size cropped_output_size = ( round(output_height * crop_ratio), round(output_width * crop_ratio), ) # TODO: pass hparams as kwargs model_cls = eval(args.model) # Backbone vars(args)["backbone"] = model_cls( in_channels=in_channels, mask_channels=mask_channels, revisits=args.revisits, hidden_channels=args.hidden_channels, homography_fc_size=args.homography_fc_size, out_channels=out_channels, kernel_size=args.kernel_size, residual_layers=args.residual_layers, output_size=cropped_output_size, zoom_factor=args.zoom_factor, sr_kernel_size=args.sr_kernel_size, skip_paddings=True, registration_kind=args.registration_kind, use_reference_frame=args.use_reference_frame, ) def load_dataset(args): """ Loads the appropriate dataset into dataloaders based on the passed dataset argument. Parameters ---------- args : argparse.Namespace Arguments parsed from the CLI and model and project specific arguments. Returns ------- dict Dictionary containing the dataloaders for the training, validation and test set. Raises ------ Exception If the passed dataset name doesn't exist. Exception If the dataset root is missing. """ if args.dataset == "JIF": dataloaders = load_dataset_JIF(**vars(args)) elif args.dataset == "SN7": dataloaders = load_dataset_SN7(**vars(args)) elif args.dataset == "PROBAV": dataloaders = load_dataset_ProbaV(**vars(args)) else: raise Exception("Undefined dataset. Can't create dataloaders.") if args.root is None: raise Exception("The root path of the dataset needs to be set.") return dataloaders def initialise_wandb(args): """ Initialises WandB for logging. Parameters ---------- args : argparse.Namespace Arguments parsed from the CLI and model and project specific arguments. """ tags = [] if not args.upload_checkpoint else ["inference"] wandb.init(project="esasuperres", tags=tags) wandb.run.log_code("./src/") def parse_arguments(): """ Parses the arguments passed from the CLI using ArgumentParser and adds the model and project specific arguments. If a list of aois is passed, it is loaded and added to the args namespace. Returns ------- argparse.Namespace Namespace containing the parsed arguments. """ parser = ArgumentParser() parser = pl.Trainer.add_argparse_args(parser) parser = LitModel.add_model_specific_args(parser) parser = add_project_specific_arguments(parser) args = parser.parse_args() args = add_list_of_aois_to_args(args) return args def add_list_of_aois_to_args(args): """ Adds the list of aois to the args namespace. Parameters ---------- args : argparse.Namespace Namespace containing the parsed arguments. Returns ------- argparse.Namespace Namespace containing the parsed arguments. """ if args.list_of_aois is not None: args.list_of_aois = list(pd.read_csv(args.list_of_aois, index_col=1).index) return args def add_project_specific_arguments(parser): """ Adds the project specific arguments to the parser. Parameters ---------- parser : argparse.ArgumentParser Argument parser to add the project specific arguments to. Returns ------- argparse.ArgumentParser Argument parser with the project specific arguments added. """ # Dataset arguments parser.add_argument( "--dataset", default="JIF", type=str, help="JIF or SN7 dataset." ) parser.add_argument( "--root", default=None, type=str, help="Root folder of the dataset." ) parser.add_argument( "--train_split", default=None, type=int, help="Number of scenes in the train set.", ) parser.add_argument( "--val_split", default=None, type=int, help="Number of scenes in the val set." ) parser.add_argument( "--test_split", default=None, type=int, help="Number of scenes in the test set." ) parser.add_argument( "--list_of_aois", default=None, type=str, help="Pandas DataFrame containing a list of AOIs from the dataset to use.", ) parser.add_argument( "--lr_bands_to_use", default="all", type=str, help="Low resolution bands to use: all or true_color. Default: all.", ) parser.add_argument( "--radiometry_depth", default=12, type=int, help="Radiometry depth to use: 12 or 8 bits. Default: 12 (full).", ) parser.add_argument( "--data_split_seed", default=42, type=int, help="Separate seed to ensure the train/val/test split remains the same.", ) parser.add_argument( "--pansharpen_hr", default=False, type=bool, help="Pansharpen the highres RGB using the panchromatic channel.", ) parser.add_argument( "--compute_median_std", default=False, type=bool, help="Calculates the mean and std of the input dataset.", ) # Training arguments parser.add_argument( "--seed", default=1337, type=int, help="Randomization seed." ) # random seed parser.add_argument("--num_workers", default=8, type=int) # CPU cores parser.add_argument("--batch_size", default=1, type=int) parser.add_argument( "--subset_train", default=1.0, type=float, help="Fraction of the training dataset.", ) # Logging arguments parser.add_argument( "--upload_checkpoint", default=False, type=bool, help="Uploads the model checkpoint to WandB.", ) return parser # TODO # - encode mask at input and skip-connect right before SR layer and loss # - UNet-type encoding (multi-scale bottlenecks) # - cleanest as reference frame # - filter revisits based on timestamps # - log shifts histogram # - notebook for finding the most similar LRs to an HR # - ignore clouded parts at loss: proportional to average mask # - whole-image model # - SCL categorical masks to one-hot # - loss: multi-class CE loss # - differentiable phase correlation for registered loss? grid search doesn't scale well # - tiny regularization on TV loss # - Lanczos resizing if __name__ == "__main__": r""" # Usage src/train.py # Model arguments --batch_size 48 --gpus -1 --precision 16 --model highresnet / --residual_layers 1 --hidden_channels 128 / --shift_px 2 --shift_mode lanczos --shift_step 0.5 / # Training arguments --w_mse 0.3 --w_mae 0.4 --w_ssim 0.3 --learning_rate 1e-4 / # Dataset arguments --dataset JIF --root ./data/ --revisits 8 / --input_size 160 160 --output_size 500 500 --chip_size 50 50 / --radiometry_depth 8 --lr_bands all / --train_split 2700 --val_split 350 --test_split 1 / # Reproducibility arguments --list_of_aois full.csv / --data_split_seed 43 --seed 43 / --upload_checkpoint True / # With SCL masks / --use_masks / --gpus -1 / 0,1 / 0 or skip # GPUs all / specific / none --fast_dev_run # Debugging --stochastic_weight_avg # Stochastic Weight Averaging --help # Help """ cli_main()