{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"}],"dockerImageVersionId":31234,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import polars as pl\nfrom PIL import Image\nimport matplotlib.pyplot as plt\n\nimport time\nfrom pathlib import Path\nfrom collections import OrderedDict\n\nimport torch\nfrom torchvision.transforms import v2\nfrom torchvision.models import (\n    resnet34, ResNet34_Weights,  \n    vit_b_16, ViT_B_16_Weights,\n)\n\n\n# Global Paths\nDATASET_PATH = Path(\"/kaggle/input/csiro-biomass\")\nCHECKPOINT_PATH = Path(\"/kaggle/working/models\")\nASSET_PATH = Path(\"/kaggle/working/assets\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-25T21:32:28.963561Z","iopub.execute_input":"2025-12-25T21:32:28.963833Z","iopub.status.idle":"2025-12-25T21:32:36.196570Z","shell.execute_reply.started":"2025-12-25T21:32:28.963802Z","shell.execute_reply":"2025-12-25T21:32:36.195954Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Loading the Dataset","metadata":{"execution":{"iopub.status.busy":"2025-12-24T07:38:31.226327Z","iopub.execute_input":"2025-12-24T07:38:31.226613Z","iopub.status.idle":"2025-12-24T07:38:31.233444Z","shell.execute_reply.started":"2025-12-24T07:38:31.226596Z","shell.execute_reply":"2025-12-24T07:38:31.231970Z"}}},{"cell_type":"code","source":"# Loading the Dataset.\ntrain_df = pl.read_csv(DATASET_PATH / \"train.csv\")\ntrain_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-25T21:32:36.197866Z","iopub.execute_input":"2025-12-25T21:32:36.198221Z","iopub.status.idle":"2025-12-25T21:32:36.278203Z","shell.execute_reply.started":"2025-12-25T21:32:36.198167Z","shell.execute_reply":"2025-12-25T21:32:36.277642Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Data Preprocessing**","metadata":{}},{"cell_type":"code","source":"# Converting the Sampling Date Series\ntrain_df_dt = train_df.with_columns(\n    pl.col(\"Sampling_Date\").str.to_date().alias(\"Sampling_Date\")\n)\n\n# Extracting the Date, Month and Year\ntrain_df_dt = train_df_dt.with_columns(\n    pl.col(\"Sampling_Date\").dt.day().alias(\"Sample_Day\"),\n    pl.col(\"Sampling_Date\").dt.month().alias(\"Sample_Month\"),\n    pl.col(\"Sampling_Date\").dt.year().alias(\"Sample_Year\")\n)\n\n# Extracting the Seasons as per Australia\ntrain_df_seasons = train_df_dt.with_columns(\n    pl.when(pl.col(\"Sample_Month\").is_in([12, 1, 2])).then(pl.lit(\"Summer\"))\n    .when(pl.col(\"Sample_Month\").is_in([3, 4, 5])).then(pl.lit(\"Autumn\"))\n    .when(pl.col(\"Sample_Month\").is_in([6, 7, 8])).then(pl.lit(\"Winter\"))\n    .when(pl.col(\"Sample_Month\").is_in([9, 10, 11])).then(pl.lit(\"Spring\"))\n    .otherwise(pl.lit(\"unknown\")).alias(\"Sample_Season\")\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-25T21:32:36.278968Z","iopub.execute_input":"2025-12-25T21:32:36.279255Z","iopub.status.idle":"2025-12-25T21:32:36.370918Z","shell.execute_reply.started":"2025-12-25T21:32:36.279233Z","shell.execute_reply":"2025-12-25T21:32:36.370239Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Creating a uniform image_id\nprocessed_df = train_df_seasons.with_columns(\n    pl.col(\"sample_id\").str.split(\"__\").list.get(0).alias(\"image_id\")\n)\n\n# Dropping the unnecessary columns\nprocessed_df = processed_df.drop(\"sample_id\", \"Sampling_Date\")\nprocessed_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-25T21:32:36.371808Z","iopub.execute_input":"2025-12-25T21:32:36.372068Z","iopub.status.idle":"2025-12-25T21:32:36.388513Z","shell.execute_reply.started":"2025-12-25T21:32:36.372047Z","shell.execute_reply":"2025-12-25T21:32:36.387975Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Pivoting the Table for Unifying the Rows of each Image.\nindexes = [\n    \"image_id\", \"image_path\", \"State\", \"Species\", \"Pre_GSHH_NDVI\", \"Height_Ave_cm\", \n    \"Sample_Day\", \"Sample_Month\", \"Sample_Year\", \"Sample_Season\"\n]\n\nwide_df = processed_df.pivot(on=\"target_name\", values=\"target\", index=indexes)\nwide_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-25T21:32:36.390168Z","iopub.execute_input":"2025-12-25T21:32:36.390550Z","iopub.status.idle":"2025-12-25T21:32:36.410431Z","shell.execute_reply.started":"2025-12-25T21:32:36.390528Z","shell.execute_reply":"2025-12-25T21:32:36.409926Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Data Preparation","metadata":{}},{"cell_type":"code","source":"# Default Transforms for batching before model-spec transforms.\nDEFAULT_IMAGE_TRANSFORMS = v2.Compose([\n    v2.ToImage(),\n    v2.ToDtype(torch.float32, scale=True)\n])\n\n# Data Augmentation\nAUGMENTATION_TRANSFORMS = v2.Compose([\n    v2.RandomRotation(20),\n    v2.ColorJitter(brightness=0.2, contrast=0.2),\n    v2.RandomAffine(degrees=0, translate=(0.1, 0.1))\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-25T21:32:36.411222Z","iopub.execute_input":"2025-12-25T21:32:36.411566Z","iopub.status.idle":"2025-12-25T21:32:36.415769Z","shell.execute_reply.started":"2025-12-25T21:32:36.411536Z","shell.execute_reply":"2025-12-25T21:32:36.415038Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CSIRODataset(torch.utils.data.Dataset):\n    \"\"\"Custom Dataset for building the PyTorch friendly data pipeline.\"\"\"\n    def __init__(self, dataframe: pl.DataFrame, transforms: v2.Compose = DEFAULT_IMAGE_TRANSFORMS) -> None:\n        \"\"\"\n        args:\n            dataframe[pl.DataFrame]: The wide-format dataframe with image_id as the unique identifier\n            and 5 targets biomass values for each image.\n            idx[torch.Tensor]: Sample of indexes for the dataset split.\n        \"\"\"\n\n        # Indexes: Can be used for reconstruction of the long-form dataframe for submission\n        self.indexes = dataframe.select(pl.col(\"image_id\"))\n\n        # Image Paths\n        self.image_paths = dataframe.select(pl.col(\"image_path\"))\n\n        # Target Cols\n        target_cols = [\"Dry_Clover_g\", \"Dry_Dead_g\", \"Dry_Green_g\", \"Dry_Total_g\", \"GDM_g\"]\n        self.targets = dataframe.select(pl.col(*target_cols))\n\n        # Transforms\n        self.transforms = transforms\n\n    def __len__(self) -> int:\n        return len(self.image_paths)\n    \n    def __getitem__(self, idx: int) -> dict:\n        # Access the relative_path by index and provide the complete_path\n        relative_path: str = self.image_paths[idx].item()\n        complete_path = DATASET_PATH / relative_path\n\n        # Load the image\n        image = Image.open(complete_path).convert(\"RGB\")\n\n        # Access the targets\n        targets = self.targets.row(idx)\n        targets_tensor = torch.tensor(targets, dtype=torch.float32)\n\n        # Creating the Sample\n        sample = {\"image\": image, \"targets\": targets_tensor}\n\n        # Applying the transforms for the image\n        if self.transforms:\n            sample[\"image\"] = self.transforms(sample[\"image\"])\n\n        return sample\n\n    def show_sample(self, size=6) -> None:\n        \"\"\"Constructs a matplotlib subplot of a sample of images to show the working of the Custom Dataset.\"\"\"\n\n        # Creating the Subplot Canvas for Matplotlib\n        fig, ax = plt.subplots(3, 2, figsize=(15, 13))\n        fig.suptitle(\"A sample of the PyTorch Custom Dataset\")\n\n        # Flatten the indexes for axes\n        axes = ax.flatten()\n\n        # Plotting the Subplots\n        for i in range(size):\n            # Accessing a Random Sample\n            sample_idx = torch.randint(low=0, high=len(self), size=(1,))\n            sample_row = self[int(sample_idx)]\n\n            # Formatting the Custom Subplot Title\n            title = (\n                f\"\\nDry_Clover_g: {sample_row['targets'][0] :.3f}\\nDry_Dead_g: {sample_row['targets'][1] :.3f}\\n\"\n                f\"Dry_Green_g: {sample_row['targets'][2] :.3f}\\nDry_Total_g: {sample_row['targets'][3] :.3f}\\n\"\n                f\"GDM_g: {sample_row['targets'][4] :.3f}\\n\"\n            )\n\n            # Visualing the Sample\n            axes[i].imshow(sample_row[\"image\"].permute(1, 2, 0))\n            axes[i].set_title(title, fontsize=10)\n            axes[i].axis(\"off\")\n\n        plt.tight_layout()\n        plt.show()\n\n\n# Testing the Dataset\ntest_torch_dataset = CSIRODataset(wide_df)\ntest_torch_dataset.show_sample()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-25T21:32:36.416580Z","iopub.execute_input":"2025-12-25T21:32:36.416831Z","iopub.status.idle":"2025-12-25T21:32:38.580821Z","shell.execute_reply.started":"2025-12-25T21:32:36.416802Z","shell.execute_reply":"2025-12-25T21:32:38.580023Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class DataHandler:\n    \"\"\"Constructs the Loaded Dataset based on the split percentage.\"\"\"\n\n    def __init__(self, full_dataset: CSIRODataset, split_percent: float = 0.1) -> None:\n        \"\"\"\n        args:\n            full_dataset[CSIRODataset]: The Custom PyTorch CSIROData which provides a dict of Image and Targets for each sample.\n            split_percent[float]: The percentage of the dataset to be used as validation data.\n        \"\"\"\n\n        # The constructed full dataset.\n        self.full_dataset = full_dataset\n\n        # The length of the Validation Set.\n        valid_len = int(split_percent * len(self.full_dataset))\n\n        # Creating a random split of indices for the training and validation sets.\n        full_idx = torch.randperm(n=len(self.full_dataset))\n        self.train_idx = full_idx[:-valid_len].tolist()\n        self.valid_idx = full_idx[-valid_len:].tolist()\n\n    def construct_train_and_valid(self, batch_size: int = 64, shuffle: bool = True) -> tuple[torch.utils.data.DataLoader, torch.utils.data.DataLoader]:\n        train_set = torch.utils.data.Subset(self.full_dataset, self.train_idx)\n        valid_set = torch.utils.data.Subset(self.full_dataset, self.valid_idx)\n\n        loaded_train = torch.utils.data.DataLoader(\n            train_set,\n            batch_size=batch_size,\n            shuffle=True,\n        )\n        loaded_valid = torch.utils.data.DataLoader(\n            valid_set,\n            batch_size=batch_size,\n            shuffle=True,\n        )\n\n        return loaded_train, loaded_valid\n\n        \n# Testing the Dataloader Creation\ntest_handle = DataHandler(full_dataset=test_torch_dataset)\nloaded_train, loaded_valid = test_handle.construct_train_and_valid()\nprint(f\"Length of Train: {len(loaded_train)}\")\nprint(f\"Length of Valid: {len(loaded_valid)}\")\nprint(f\"Length of Full Dataset: {len(test_torch_dataset)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-25T21:32:38.582601Z","iopub.execute_input":"2025-12-25T21:32:38.582977Z","iopub.status.idle":"2025-12-25T21:32:38.601384Z","shell.execute_reply.started":"2025-12-25T21:32:38.582936Z","shell.execute_reply":"2025-12-25T21:32:38.600713Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Training Loop","metadata":{}},{"cell_type":"code","source":"class TrainingLoop:\n    \"\"\"This class handles the training loop for the models.\"\"\"\n    def __init__(\n            self,\n            device: str,\n            learning_rate: float,\n            model: torch.nn.Module,\n            image_preprocessor: v2.Compose,\n            lr_schedule_patience: int = 3,\n            early_stopping_patience: int = 5\n        ):\n        \n        # Loading the Model Instance for the Training Loop\n        self.model = model\n\n        # Accelerator Device\n        self.device = device\n\n        # Early Stopping Patience\n        self.early_stopping_patience = early_stopping_patience\n\n        # Optimizer\n        self.optim = torch.optim.AdamW(\n            params=self.model.parameters(),\n            lr=learning_rate\n        )\n\n        # Learning Rate Scheduler\n        self.lr_schedule = torch.optim.lr_scheduler.ReduceLROnPlateau(\n            optimizer=self.optim, mode=\"min\", patience=lr_schedule_patience, min_lr=1e-6\n        )\n\n        # Image Preprocessor\n        self.image_preprocessor = image_preprocessor\n\n        # Creating the Checkpoint Storage Directory\n        self.model_dir = Path(CHECKPOINT_PATH)\n        if not self.model_dir.exists():\n            self.model_dir.mkdir()\n    \n    def train_model(\n            self, \n            epochs: int,\n            train_set: torch.utils.data.DataLoader, \n            valid_set: torch.utils.data.DataLoader,\n            model_name: str,\n            augmentation_transforms: v2.Compose = AUGMENTATION_TRANSFORMS\n        ) -> tuple[list[float], list[float]]:\n        \"\"\"Trains the model for the given number of epochs.\"\"\"\n\n        # Cache Losses\n        train_losses = []\n        valid_losses = []\n\n        # Mean Training Variables\n        mean_loss_train = 0\n        mean_loss_valid = 0\n\n        # Training Checkpoint Handlers => Early Stopping\n        best_valid_loss = torch.inf\n        early_stopping_patience_counter = 0\n\n        # Loss Function\n        loss_fn = torch.nn.L1Loss()\n\n        # Training Loop\n        print(\"\\nThe training process has started:\")\n        for i in range(epochs):\n\n            # Average Epoch Time tracking\n            start = time.time()\n\n            # ==== Training Step ====\n\n            # Completing a single epoch\n            for batch in train_set:\n                \n                # Preprocessing the Image Batch and Moving it to the GPU device\n                image_batch = batch[\"image\"]\n                image_batch_processed = self.image_preprocessor(image_batch)\n                image_batch_augmented = augmentation_transforms(image_batch_processed).to(device=self.device)\n                target_batch = batch[\"targets\"].to(device=self.device)\n            \n                # Training Step\n                logits = self.model(image_batch_augmented)\n\n                # Loss Calculation\n                train_loss = loss_fn(logits, target_batch)\n                mean_loss_train += train_loss.item()\n\n                # Backpropagation\n                self.optim.zero_grad()\n                train_loss.backward()\n                self.optim.step()\n\n            # Completion of Epoch\n            end = time.time()\n\n            # ==== Validation Step ====\n\n            # Turning on the Eval mode on the model for the BN-Layers\n            self.model.eval()\n            with torch.no_grad():\n                for batch in valid_set:\n\n                    # Moving the batches to GPU\n                    image_batch = batch[\"image\"]\n                    image_batch_processed = self.image_preprocessor(image_batch).to(device=self.device)\n                    target_batch = batch[\"targets\"].to(device=self.device)\n\n                    # Validation Calculation\n                    logits = self.model(image_batch_processed)\n\n                    # Loss Calculation\n                    valid_loss = loss_fn(logits, target_batch)\n                    mean_loss_valid += valid_loss.item()\n            \n            # Switching the model back to training mode\n            self.model.train()\n\n            # ==== End of Epoch Metrics & Model Checkpointing ====\n            mean_loss_train /= len(train_set)\n            mean_loss_valid /= len(valid_set)\n            time_epoch = end - start\n\n            # Update the LR-Schedule\n            self.lr_schedule.step(mean_loss_valid)\n\n            # Updating the Caches\n            train_losses.append(mean_loss_train)\n            valid_losses.append(mean_loss_valid)\n\n            print(f\"Epoch {i + 1}/{epochs}: Train Loss -> {mean_loss_train :.4f} | Valid Loss -> {mean_loss_valid :.4f} | Time Epoch -> {time_epoch :.4f}\")\n\n            # Updating the best validation loss so far\n            if mean_loss_valid < best_valid_loss:\n                best_valid_loss = mean_loss_valid\n\n                # Saving the Model by weights\n                torch.save(obj=self.model.state_dict(), f=self.model_dir / f\"baseline_{model_name}.pth\")\n                print(\"New best model was saved\")\n                early_stopping_patience_counter = 0\n            else:\n                early_stopping_patience_counter += 1\n                print(f\"No improvement: {early_stopping_patience_counter} / {self.early_stopping_patience}\")\n                if early_stopping_patience_counter >= self.early_stopping_patience:\n                    print(\"Early Stopping\")\n                    break\n\n            # ==== Reset the Training Loop Metrics ====\n            mean_loss_train, mean_loss_valid = 0, 0\n        \n        return train_losses, valid_losses","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-25T21:32:38.602317Z","iopub.execute_input":"2025-12-25T21:32:38.602589Z","iopub.status.idle":"2025-12-25T21:32:38.618418Z","shell.execute_reply.started":"2025-12-25T21:32:38.602567Z","shell.execute_reply":"2025-12-25T21:32:38.617685Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Modelling","metadata":{}},{"cell_type":"code","source":"# Hyperparams for Training\nBATCH_SIZE = 8\nEPOCHS = 50\nLEARNING_RATE = 1e-5\nSPLIT_PERCENT = 0.15\nLR_PATIENCE = 4\nEARLY_STOPPING_PATIENCE = 7","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-25T21:32:38.619329Z","iopub.execute_input":"2025-12-25T21:32:38.619619Z","iopub.status.idle":"2025-12-25T21:32:38.631871Z","shell.execute_reply.started":"2025-12-25T21:32:38.619590Z","shell.execute_reply":"2025-12-25T21:32:38.631130Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Accelerator Device\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"Accelerator Available: {DEVICE}\")\nprint(f\"Units: {torch.cuda.device_count()}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-25T21:32:38.632933Z","iopub.execute_input":"2025-12-25T21:32:38.633169Z","iopub.status.idle":"2025-12-25T21:32:38.732960Z","shell.execute_reply.started":"2025-12-25T21:32:38.633139Z","shell.execute_reply":"2025-12-25T21:32:38.732244Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"BEST_MODEL_LOSS = torch.inf\nBEST_MODEL = None\n\ndef main(model_name: str, print_model_arch: bool = False):\n    # Loading the Custom Torch Dataset using the processed Polars DataFrame\n    csiro_torch_dataset = CSIRODataset(wide_df)\n    \n    # Initialising the DataHandler to create the DataLoaders\n    custom_data_handle = DataHandler(csiro_torch_dataset, split_percent=SPLIT_PERCENT)\n    loaded_train_set, loaded_valid_set = custom_data_handle.construct_train_and_valid(batch_size=BATCH_SIZE)\n    print(\"Loaded the Datasets:\")\n    print(f\"No of batches in Train: {len(loaded_train_set)}\")\n    print(f\"No of batches in Valid: {len(loaded_valid_set)}\")\n\n    # No of prediction heads\n    biomass_heads = 5\n    \n    # Model Initialisation based on Model Name\n    if model_name == \"resnet_small\":\n        model_base = resnet34(weights=ResNet34_Weights.DEFAULT)\n        model_preprocessor = ResNet34_Weights.DEFAULT.transforms()\n    elif model_name == \"resnet_medium\":\n        model_base = resnet50(weights=ResNet50_Weights.DEFAULT)\n        model_preprocessor = ResNet50_Weights.DEFAULT.transforms()\n    elif model_name == \"vit_b_16\":\n        model_base = vit_b_16(weights=ViT_B_16_Weights.DEFAULT)\n        model_preprocessor = ViT_B_16_Weights.DEFAULT.transforms()\n    elif model_name == \"vit_b_32\":\n        model_base = vit_b_32(weights=ViT_B_32_Weights.DEFAULT)\n        model_preprocessor = ViT_B_32_Weights.DEFAULT.transforms()\n    else:\n        raise ValueError(\"Model Name provided was invalid or left unfilled\")\n    \n    # Updating the final layer\n    if \"resnet\" in model_name:\n        final_fan_in = model_base.fc.in_features\n        final_layer = torch.nn.Linear(in_features=final_fan_in, out_features=biomass_heads)\n        model_base._modules[\"fc\"] = final_layer\n    elif \"vit\" in model_name:\n        final_fan_in = model_base.heads.head.in_features\n        final_layer = torch.nn.Linear(in_features=final_fan_in, out_features=biomass_heads)\n        model_base._modules[\"heads\"].head = final_layer\n\n    # Viewing the Updated Model Architecture\n    if print_model_arch:\n        print(\"Updated Model Architecture:\\n\\n\", model_base, end=\"\\n\\n---------------------------------\\n\\n\")\n\n    # Moving the Model to GPU with Distributed Training Enabled\n    model_base.to(device=DEVICE)\n    loaded_model = torch.nn.DataParallel(model_base)\n    \n    # Initialising the Training Handler\n    learning_handler = TrainingLoop(\n        device=DEVICE,\n        learning_rate=LEARNING_RATE,\n        model=loaded_model,\n        image_preprocessor=model_preprocessor,\n        lr_schedule_patience=LR_PATIENCE,\n        early_stopping_patience=EARLY_STOPPING_PATIENCE\n    )\n    \n    # Training the Model\n    train_losses, valid_losses = learning_handler.train_model(\n        epochs=EPOCHS, \n        train_set=loaded_train_set,\n        valid_set=loaded_valid_set,\n        model_name=model_name\n    )\n\n    plt.figure(figsize=(10, 8))\n    plt.title(f\"Learning Curve {model_name}\")\n    plt.plot(range(1, len(train_losses) + 1), train_losses, c=\"b\", ls=\"-\", label=\"Train Loss\")\n    plt.plot(range(1, len(valid_losses) + 1), valid_losses, c=\"g\", ls=\"-.\", label=\"Valid Loss\")\n    plt.legend(loc=\"upper right\")\n    plt.xlabel(\"Epochs\")\n    plt.ylabel(\"Loss\")\n    plt.grid()\n\n    # Updating the Best Model for Inference\n    if valid_losses[-1] <= BEST_MODEL_LOSS:\n        global BEST_MODEL\n        BEST_MODEL = model_name\n\n    # Storing the Learning Curve\n    if not ASSET_PATH.exists():\n        ASSET_PATH.mkdir()\n    plt.savefig(ASSET_PATH / f\"learning_curve_{model_name}.png\")\n\n    # Rendering the plot\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-25T21:32:38.733899Z","iopub.execute_input":"2025-12-25T21:32:38.734142Z","iopub.status.idle":"2025-12-25T21:32:38.744271Z","shell.execute_reply.started":"2025-12-25T21:32:38.734120Z","shell.execute_reply":"2025-12-25T21:32:38.743639Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"main(\"resnet_small\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-25T21:32:38.745113Z","iopub.execute_input":"2025-12-25T21:32:38.745391Z","iopub.status.idle":"2025-12-25T21:50:11.778330Z","shell.execute_reply.started":"2025-12-25T21:32:38.745360Z","shell.execute_reply":"2025-12-25T21:50:11.777792Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"main(\"vit_b_16\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-25T21:50:11.780388Z","iopub.execute_input":"2025-12-25T21:50:11.780643Z","iopub.status.idle":"2025-12-25T22:12:05.017563Z","shell.execute_reply.started":"2025-12-25T21:50:11.780620Z","shell.execute_reply":"2025-12-25T22:12:05.016989Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Making the Submission","metadata":{}},{"cell_type":"code","source":"print(f\"Best Model by training: {BEST_MODEL}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-25T22:12:05.018494Z","iopub.execute_input":"2025-12-25T22:12:05.018732Z","iopub.status.idle":"2025-12-25T22:12:05.022746Z","shell.execute_reply.started":"2025-12-25T22:12:05.018710Z","shell.execute_reply":"2025-12-25T22:12:05.022131Z"}},"outputs":[],"execution_count":null}]}