{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","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"},{"sourceId":848739,"sourceType":"datasetVersion","datasetId":251095},{"sourceId":274102283,"sourceType":"kernelVersion"}],"dockerImageVersionId":31154,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"Version of the corresponding [training notebook](https://www.kaggle.com/code/sasaleaf/csiro-pytorch-baseline-train): **Version 10**\n# 🌱 PyTorch CNN Baseline\n\nThis notebook presents a **CNN** baseline built with **PyTorch**,\nbased on **EfficientNet v0**.\nThe final head is modified to output **3** predictions,\nand the model is fine-tuned for this task.\n\n---\n## 🎯 Target Structure\nThe neural network directly predicts 3 out of 5 target variables:\n- `Dry_Clover_g`\n- `Dry_Dead_g`\n- `GDM_g`\n\nThe remaining 2 targets are derived using the following relationships:\n- `Dry_Green_g` = `GDM_g` - `Dry_Clover_g`\n- `Dry_Total_g` = `GDM_g` + `Dry_Dead_g`\n\n---\n💬 Note:\nIf you notice anything unclear or have suggestions for improvement,\nplease feel free to leave a comment!","metadata":{}},{"cell_type":"code","source":"import os\nimport gc\nimport random\nimport numpy as np\nimport pandas as pd\nfrom sklearn.model_selection import KFold\nfrom sklearn.metrics import r2_score\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\n\nimport cv2\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nfrom efficientnet_pytorch import model as enet\nimport matplotlib.pyplot as plt","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-07T03:59:26.804516Z","iopub.execute_input":"2025-11-07T03:59:26.804791Z","iopub.status.idle":"2025-11-07T04:00:10.718276Z","shell.execute_reply.started":"2025-11-07T03:59:26.804770Z","shell.execute_reply":"2025-11-07T04:00:10.717354Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Config","metadata":{}},{"cell_type":"code","source":"# ======== 1. Config ========\n\nclass CFG:\n    # path\n    data_dir = \"/kaggle/input/csiro-biomass/\"\n    train_csv_path = os.path.join(data_dir, \"train.csv\")\n    test_csv_path = os.path.join(data_dir, \"test.csv\")\n    sample_sub_path = os.path.join(data_dir, \"sample_submission.csv\")\n    \n    # CV\n    n_folds = 5\n    seed = 42\n    \n    # model\n    model_name = \"efficientnet-b0\"\n    pretrained = False\n    pretrained_weights_path = \"/kaggle/input/efficientnet-pytorch/efficientnet-b0-08094119.pth\"\n    best_model_dir = \"/kaggle/input/csiro-pytorch-baseline-train/\"\n    \n    # image\n    img_size_h = 256\n    img_size_w = 512\n    in_chans = 3\n    \n    # train\n    device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    epochs = 150\n    batch_size = 32\n    lr = 1e-3\n    eta_min = 1e-5\n    weight_decay = 1e-6\n    \n    # target columns\n    target_cols = [\n        \"Dry_Clover_g\",\n        \"Dry_Dead_g\",\n        \"Dry_Green_g\",\n        \"Dry_Total_g\",\n        \"GDM_g\"\n    ]\n    n_targets = 3 # Dry_Clover_g, Dry_Dead_g, GDM_g","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-07T04:00:10.719278Z","iopub.execute_input":"2025-11-07T04:00:10.719802Z","iopub.status.idle":"2025-11-07T04:00:10.810011Z","shell.execute_reply.started":"2025-11-07T04:00:10.719773Z","shell.execute_reply":"2025-11-07T04:00:10.808961Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Utility","metadata":{}},{"cell_type":"code","source":"# ======== 2. Utility ========\n\ndef seed_everything(seed):\n    random.seed(seed)\n    os.environ[\"PYTHONHASHSEED\"] = str(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n\ndef weighted_r2_score(y_true: np.ndarray, y_pred: np.ndarray):\n    \"\"\"\n    Metric\n    y_true, y_pred: shape (N, 5)\n    \"\"\"\n    weights = np.array([0.1, 0.1, 0.1, 0.2, 0.5])\n    r2_scores = []\n    \n    for i in range(y_true.shape[1]):\n        y_t = y_true[:, i]\n        y_p = y_pred[:, i]\n        ss_res = np.sum((y_t - y_p) ** 2)\n        ss_tot = np.sum((y_t - np.mean(y_t)) ** 2)\n        r2 = 1 - ss_res / ss_tot if ss_tot > 0 else 0.0\n        r2_scores.append(r2)\n        \n    r2_scores = np.array(r2_scores)\n    weighted_r2 = np.sum(r2_scores * weights) / np.sum(weights)\n    return weighted_r2, r2_scores\n\nseed_everything(CFG.seed)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-07T04:00:10.811760Z","iopub.execute_input":"2025-11-07T04:00:10.811973Z","iopub.status.idle":"2025-11-07T04:00:10.836922Z","shell.execute_reply.started":"2025-11-07T04:00:10.811956Z","shell.execute_reply":"2025-11-07T04:00:10.836165Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Preprocessing","metadata":{}},{"cell_type":"code","source":"# ======== 3. Preprocessing ========\n\ndef get_processed_data(cfg):\n    \"\"\"\n    'long' -> 'wide'\n    \"\"\"\n    train_df = pd.read_csv(cfg.train_csv_path)\n    \n    # unique id (ex: ID1011485656)\n    train_df[\"image_id\"] = train_df[\"image_path\"].apply(lambda x: x.split('/')[-1].split('.')[0])\n    \n    # pivot\n    train_pivot = train_df.pivot(\n        index=\"image_id\", \n        columns=\"target_name\", \n        values=\"target\"\n    ).reset_index()\n    \n    meta_df = train_df.drop_duplicates(subset=\"image_id\").drop(\n        columns=[\"sample_id\", \"target_name\", \"target\"]\n    )\n    \n    train_processed_df = meta_df.merge(train_pivot, on=\"image_id\", how=\"left\")\n    \n    # CV fold\n    kf = KFold(n_splits=cfg.n_folds, shuffle=True, random_state=cfg.seed)\n    train_processed_df[\"fold\"] = -1\n    for fold, (train_idx, val_idx) in enumerate(kf.split(train_processed_df)):\n        train_processed_df.loc[val_idx, \"fold\"] = fold\n        \n    return train_processed_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-07T04:00:10.837618Z","iopub.execute_input":"2025-11-07T04:00:10.837824Z","iopub.status.idle":"2025-11-07T04:00:10.843814Z","shell.execute_reply.started":"2025-11-07T04:00:10.837808Z","shell.execute_reply":"2025-11-07T04:00:10.843119Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Dataset","metadata":{}},{"cell_type":"code","source":"# ======== 4. Dataset & Augmentations ========\n\ndef get_transforms(is_train):\n    \"\"\"Augmentation\"\"\"\n    if is_train:\n        return A.Compose([\n            A.Resize(CFG.img_size_h, CFG.img_size_w),\n            A.HorizontalFlip(p=0.5),\n            A.VerticalFlip(p=0.5),\n            A.Rotate(limit=30, p=0.5),\n            A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n            ToTensorV2(),\n        ])\n    else:\n        return A.Compose([\n            A.Resize(CFG.img_size_h, CFG.img_size_w),\n            A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n            ToTensorV2(),\n        ])\n\nclass BiomassDataset(Dataset):\n    def __init__(self, df, transforms=None, is_test=False):\n        self.df = df\n        self.image_paths = df[\"image_path\"].values\n        self.transforms = transforms\n        self.is_test = is_test\n        \n        if not self.is_test:\n            self.targets = df[CFG.target_cols].values\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        image_path = self.image_paths[idx]\n        full_path = os.path.join(CFG.data_dir, image_path)\n        \n        try:\n            image = cv2.imread(full_path)\n            if image is None:\n                raise FileNotFoundError(f\"Image not found at {full_path}\")\n            image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        except Exception as e:\n            print(f\"Error loading image {full_path}: {e}\")\n            # dummy\n            image = np.zeros((CFG.img_size_h, CFG.img_size_w, 3), dtype=np.uint8)\n\n        # transform\n        if self.transforms:\n            image = self.transforms(image=image)[\"image\"]\n        \n        if self.is_test:\n            return image\n        else:\n            target = torch.tensor(self.targets[idx], dtype=torch.float32)\n            return image, target","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-07T04:00:10.844535Z","iopub.execute_input":"2025-11-07T04:00:10.844785Z","iopub.status.idle":"2025-11-07T04:00:10.860174Z","shell.execute_reply.started":"2025-11-07T04:00:10.844768Z","shell.execute_reply":"2025-11-07T04:00:10.859496Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Model","metadata":{}},{"cell_type":"code","source":"# ======== 5. Model ========\n\nclass BiomassModel(nn.Module):\n    def __init__(self, model_name=CFG.model_name, pretrained=CFG.pretrained, n_targets=CFG.n_targets):\n        super().__init__()\n        self.model = enet.EfficientNet.from_pretrained(\n            model_name, \n            weights_path=CFG.pretrained_weights_path,\n            in_channels=CFG.in_chans\n        )\n        \n        # number of output features\n        in_features = self.model._fc.in_features\n        \n        # head\n        self.model._fc = nn.Linear(in_features, n_targets)\n\n    def forward(self, x):\n        output = self.model(x)\n        return output","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-07T04:00:10.861068Z","iopub.execute_input":"2025-11-07T04:00:10.861293Z","iopub.status.idle":"2025-11-07T04:00:10.880636Z","shell.execute_reply.started":"2025-11-07T04:00:10.861268Z","shell.execute_reply":"2025-11-07T04:00:10.879956Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Train / Valdation","metadata":{}},{"cell_type":"code","source":"# ======== 3 <-> 5 target transform ========\n\ndef get_loss_targets(targets_5):\n    \"\"\"\n    Extracts 3 target values (B, 3) for loss computation from the 5 ground truth targets (B, 5).\n\n    Args:\n        targets_5 (torch.Tensor): Tensor of shape (B, 5) containing \n            [Clover, Dead, Green, Total, GDM].\n\n    Returns:\n        torch.Tensor: Tensor of shape (B, 3) containing the selected ground truth targets \n            [Clover_true, Dead_true, GDM_true].\n    \"\"\"\n    \n    # Index 0: Dry_Clover_g\n    # Index 1: Dry_Dead_g\n    # Index 4: GDM_g\n    loss_targets_3 = torch.stack(\n        [\n            targets_5[:, 0], # Clover\n            targets_5[:, 1], # Dead\n            targets_5[:, 4]  # GDM\n        ],\n        dim=1\n    )\n    return loss_targets_3\n\ndef expand_predictions_torch(preds_3):\n    \"\"\"\n    [torch] Expand the 3 NN predictions (N, 3) to 5 predictions (N, 5).\n    \"\"\"\n    # clip\n    P_Clover = torch.clamp(preds_3[:, 0], min=0)\n    P_Dead = torch.clamp(preds_3[:, 1], min=0)\n    P_GDM = torch.clamp(preds_3[:, 2], min=0)\n    \n    # Compute derived targets based on constraints.\n    P_Green = torch.clamp(P_GDM - P_Clover, min=0)\n    P_Total = P_GDM + P_Dead\n    \n    preds_5 = torch.stack(\n        [\n            P_Clover, # Index 0\n            P_Dead,   # Index 1\n            P_Green,  # Index 2\n            P_Total,  # Index 3\n            P_GDM     # Index 4\n        ],\n        dim=1\n    )\n    return preds_5\n\ndef expand_predictions_np(preds_3):\n    \"\"\"\n    [Numpy] Expand the three NN predictions (N, 3) to five predictions (N, 5).\n    \"\"\"\n    P_Clover = np.clip(preds_3[:, 0], a_min=0, a_max=None)\n    P_Dead = np.clip(preds_3[:, 1], a_min=0, a_max=None)\n    P_GDM = np.clip(preds_3[:, 2], a_min=0, a_max=None)\n    \n    P_Green = np.clip(P_GDM - P_Clover, a_min=0, a_max=None)\n    P_Total = P_GDM + P_Dead\n    \n    preds_5 = np.stack(\n        [P_Clover, P_Dead, P_Green, P_Total, P_GDM],\n        axis=1\n    )\n    return preds_5\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-07T04:00:10.881394Z","iopub.execute_input":"2025-11-07T04:00:10.881604Z","iopub.status.idle":"2025-11-07T04:00:10.903870Z","shell.execute_reply.started":"2025-11-07T04:00:10.881587Z","shell.execute_reply":"2025-11-07T04:00:10.902813Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ======== 6. Train / Validation ========\n\ndef train_fn(model, dataloader, optimizer, criterion, device):\n    \"\"\"Training function for 1 epoch.\"\"\"\n    model.train()\n    total_loss = 0\n    \n    for images, targets in dataloader:\n        images = images.to(device)\n        targets_5 = targets.to(device) # (B, 5)\n        \n        optimizer.zero_grad()\n        outputs_3 = model(images) # (B, 3) [Clover_pred, Dead_pred, GDM_pred]\n        targets_3_for_loss = get_loss_targets(targets_5) # (B, 3) [Clover_true, Dead_true, GDM_true]\n        loss = criterion(outputs_3, targets_3_for_loss)\n        \n        loss.backward()\n        optimizer.step()\n        \n        total_loss += loss.item()\n        \n    return total_loss / len(dataloader)\n\ndef val_fn(model, dataloader, criterion, device):\n    \"\"\"Eval function for 1 epoch.\"\"\"\n    model.eval()\n    total_loss = 0\n    all_targets_5_np = []\n    all_preds_5_np = []\n    \n    with torch.no_grad():\n        for images, targets in dataloader:\n            images = images.to(device)\n            targets_5 = targets.to(device) # (B, 5)\n            \n            outputs_3 = model(images) # (B, 3) [Clover_pred, Dead_pred, GDM_pred]\n            targets_3_for_loss = get_loss_targets(targets_5)\n            loss = criterion(outputs_3, targets_3_for_loss)\n            \n            total_loss += loss.item()\n            preds_5 = expand_predictions_torch(outputs_3)\n            \n            all_targets_5_np.append(targets_5.cpu().numpy())\n            all_preds_5_np.append(preds_5.cpu().numpy())\n            \n    val_loss = total_loss / len(dataloader)\n    \n    # NumPy\n    y_true_5 = np.concatenate(all_targets_5_np, axis=0)\n    y_pred_5 = np.concatenate(all_preds_5_np, axis=0)\n    \n    # Metric\n    weighted_r2, _ = weighted_r2_score(y_true_5, y_pred_5)\n    \n    return val_loss, weighted_r2","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-07T04:00:10.905100Z","iopub.execute_input":"2025-11-07T04:00:10.905500Z","iopub.status.idle":"2025-11-07T04:00:10.954101Z","shell.execute_reply.started":"2025-11-07T04:00:10.905467Z","shell.execute_reply":"2025-11-07T04:00:10.953287Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Running","metadata":{}},{"cell_type":"code","source":"# ======== 7. CV Training ========\n\ndef run_training():\n    print(f\"Device: {CFG.device}\")\n    \n    # 1. data\n    train_df = get_processed_data(CFG)\n    oof_predictions = np.zeros((len(train_df), len(CFG.target_cols))) # (N, 5)\n    \n    for fold in range(CFG.n_folds):\n        print(f\"\\n======== FOLD {fold+1} / {CFG.n_folds} ========\")\n        \n        # 2. fold\n        train_fold_df = train_df[train_df[\"fold\"] != fold].reset_index(drop=True)\n        val_fold_df_with_index = train_df[train_df[\"fold\"] == fold]\n        val_indices = val_fold_df_with_index.index\n        val_fold_df = val_fold_df_with_index.reset_index(drop=True)\n        \n        # 3. Dataset & DataLoader\n        train_dataset = BiomassDataset(train_fold_df, transforms=get_transforms(is_train=True))\n        val_dataset = BiomassDataset(val_fold_df, transforms=get_transforms(is_train=False))\n        \n        train_loader = DataLoader(\n            train_dataset, batch_size=CFG.batch_size, shuffle=True, \n            num_workers=os.cpu_count(), pin_memory=True\n        )\n        val_loader = DataLoader(\n            val_dataset, batch_size=CFG.batch_size * 2, shuffle=False, \n            num_workers=os.cpu_count(), pin_memory=True\n        )\n        \n        # 4. model\n        model = BiomassModel().to(CFG.device)\n        criterion = nn.MSELoss() \n        optimizer = optim.AdamW(model.parameters(), lr=CFG.lr, weight_decay=CFG.weight_decay)\n        scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=CFG.epochs, eta_min=CFG.eta_min)\n        \n        # 5. training\n        best_val_score = -np.inf\n        model_path = f\"model_fold_{fold}.pth\"\n\n        history = {\n            'train_loss': [],\n            'val_loss': []\n        }\n        \n        for epoch in range(CFG.epochs):\n            train_loss = train_fn(model, train_loader, optimizer, criterion, CFG.device)\n            val_loss, val_score = val_fn(model, val_loader, criterion, CFG.device)\n\n            history['train_loss'].append(train_loss)\n            history['val_loss'].append(val_loss)\n\n            print(f\"Epoch {epoch+1}/{CFG.epochs} - Train Loss: {train_loss:.4f}, Val Loss: {val_loss:.4f}, Val R2: {val_score:.4f}\")\n            \n            scheduler.step()\n            \n            # best score\n            if val_score > best_val_score:\n                best_val_score = val_score\n                torch.save(model.state_dict(), model_path)\n                print(f\"Best model saved to {model_path} (Score: {best_val_score:.4f})\")\n                \n        # viz\n        plt.figure(figsize=(10, 4))\n        plt.plot(history['train_loss'], label='Train Loss')\n        plt.plot(history['val_loss'], label='Validation Loss')\n        plt.title(f'Fold {fold+1} - Train & Validation Loss')\n        plt.xlabel('Epoch')\n        plt.ylabel('Loss')\n        plt.legend()\n        plt.grid(True)\n        plt.show()\n\n        # save OOF predictions.\n        print(f\"Loading best model for OOF prediction from {model_path}\")\n        model.load_state_dict(torch.load(model_path))\n        \n        val_loader_for_oof = DataLoader(\n            val_dataset, batch_size=CFG.batch_size * 2, shuffle=False, \n            num_workers=os.cpu_count(), pin_memory=True\n        )\n        model.eval()\n        fold_preds_list_3 = []\n        with torch.no_grad():\n             for images, _ in val_loader_for_oof:\n                images = images.to(CFG.device)\n                preds_3 = model(images) # (B, 3)\n                fold_preds_list_3.append(preds_3.cpu().numpy())\n        \n        oof_fold_preds_3 = np.concatenate(fold_preds_list_3, axis=0) # (N_val, 3)\n        oof_fold_preds_5 = expand_predictions_np(oof_fold_preds_3)\n        oof_predictions[val_indices] = oof_fold_preds_5\n\n        del model, train_dataset, val_dataset, train_loader, val_loader\n        gc.collect()\n        torch.cuda.empty_cache()\n\n    # Final OOF score\n    oof_score, oof_scores_by_target = weighted_r2_score(train_df[CFG.target_cols].values, oof_predictions)\n    print(f\"\\n======== CV Finished ========\")\n    print(f\"Overall OOF Weighted R2 Score: {oof_score:.4f}\")\n    print(\"OOF R2 by Target:\")\n    for i, col in enumerate(CFG.target_cols):\n        print(f\"  - {col}: {oof_scores_by_target[i]:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-07T04:00:10.956221Z","iopub.execute_input":"2025-11-07T04:00:10.956437Z","iopub.status.idle":"2025-11-07T04:00:10.979873Z","shell.execute_reply.started":"2025-11-07T04:00:10.956421Z","shell.execute_reply":"2025-11-07T04:00:10.979105Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ======== 8. Inference ========\n\ndef run_inference():\n    print(\"\\n======== Starting Inference ========\")\n    \n    # 1. data\n    test_df = pd.read_csv(CFG.test_csv_path)\n    test_unique_df = test_df.drop_duplicates(subset=\"image_path\").reset_index(drop=True)\n    \n    test_dataset = BiomassDataset(\n        test_unique_df, \n        transforms=get_transforms(is_train=False), \n        is_test=True\n    )\n    test_loader = DataLoader(\n        test_dataset, batch_size=CFG.batch_size * 2, shuffle=False,\n        num_workers=os.cpu_count(), pin_memory=True\n    )\n    \n    # 2. Prediction using 5-fold models\n    all_fold_preds = []\n    for fold in range(CFG.n_folds):\n        print(f\"Predicting with Fold {fold+1}...\")\n        model_path = os.path.join(CFG.best_model_dir, f\"model_fold_{fold}.pth\")\n        \n        model = BiomassModel().to(CFG.device)\n        try:\n            model.load_state_dict(torch.load(model_path))\n        except FileNotFoundError:\n            print(f\"Warning: Model file {model_path} not found. Skipping fold {fold+1}.\")\n            continue\n            \n        model.eval()\n        \n        fold_preds = []\n        with torch.no_grad():\n            for images in test_loader:\n                images = images.to(CFG.device)\n                outputs = model(images)\n                fold_preds.append(outputs.cpu().numpy())\n        \n        all_fold_preds.append(np.concatenate(fold_preds, axis=0))\n    \n    if not all_fold_preds:\n        print(\"Error: No models were loaded. Cannot perform inference.\")\n        return\n\n    # 3. average\n    # (n_folds, n_test_images, n_targets) -> (n_test_images, n_targets)\n    avg_preds_3 = np.mean(all_fold_preds, axis=0) # (n_test_images, 3)\n    avg_preds_5 = expand_predictions_np(avg_preds_3) # (n_test_images, 5)\n    \n    # 4. submission\n    \n    # 'wide'\n    preds_df = pd.DataFrame(avg_preds_5, columns=CFG.target_cols)\n    test_unique_df = pd.concat([test_unique_df, preds_df], axis=1)\n    \n    # -> 'long'\n    test_pred_long_df = test_unique_df.melt(\n        id_vars=[\"image_path\"], \n        value_vars=CFG.target_cols,\n        var_name=\"target_name\",\n        value_name=\"target\"\n    )\n\n    submission_df = test_df[[\"sample_id\", \"image_path\", \"target_name\"]].merge(\n        test_pred_long_df,\n        on=[\"image_path\", \"target_name\"],\n        how=\"left\"\n    )\n    \n    final_submission = submission_df[[\"sample_id\", \"target\"]].copy()\n\n    # sanitize\n    final_submission[\"target\"] = final_submission[\"target\"].fillna(0)\n    final_submission[\"target\"] = final_submission[\"target\"].replace([np.inf, -np.inf], 0)\n    final_submission[\"target\"] = final_submission[\"target\"].clip(lower=0)\n    \n    # submission\n    final_submission.to_csv(\"submission.csv\", index=False)\n    print(\"Inference complete. submission.csv saved.\")\n    print(final_submission.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-07T04:00:10.980669Z","iopub.execute_input":"2025-11-07T04:00:10.980992Z","iopub.status.idle":"2025-11-07T04:00:11.003660Z","shell.execute_reply.started":"2025-11-07T04:00:10.980968Z","shell.execute_reply":"2025-11-07T04:00:11.003057Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Main","metadata":{}},{"cell_type":"code","source":"# run_training()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-07T04:00:11.004465Z","iopub.execute_input":"2025-11-07T04:00:11.004647Z","iopub.status.idle":"2025-11-07T04:00:11.026630Z","shell.execute_reply.started":"2025-11-07T04:00:11.004632Z","shell.execute_reply":"2025-11-07T04:00:11.026090Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"run_inference()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-07T04:00:11.027388Z","iopub.execute_input":"2025-11-07T04:00:11.027655Z","iopub.status.idle":"2025-11-07T04:00:15.330425Z","shell.execute_reply.started":"2025-11-07T04:00:11.027631Z","shell.execute_reply":"2025-11-07T04:00:15.329489Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}