{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"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":"gpu","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":13900620,"sourceType":"datasetVersion","datasetId":8856212},{"sourceId":663314,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":471723,"modelId":487624},{"sourceId":686541,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":507446,"modelId":522154},{"sourceId":687311,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":507446,"modelId":522154}],"dockerImageVersionId":31193,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# [csiro simple V29](https://www.kaggle.com/code/jiazhuang/csiro-simple?scriptVersionId=286378326)","metadata":{}},{"cell_type":"markdown","source":"## Config","metadata":{}},{"cell_type":"code","source":"import os","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:19:40.477153Z","iopub.execute_input":"2025-12-17T02:19:40.477606Z","iopub.status.idle":"2025-12-17T02:19:40.484883Z","shell.execute_reply.started":"2025-12-17T02:19:40.477582Z","shell.execute_reply":"2025-12-17T02:19:40.484083Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 环境设置\n\nTRAIN = False  # submission时只用跑推理，设为 False\n\nDEBUG = os.environ.get('KAGGLE_KERNEL_RUN_TYPE', '') == 'Interactive'  # 交互式环境下会少跑一些epoch，用于快速跑通流程，方便调试\nLOCAL = os.environ.get('KAGGLE_KERNEL_RUN_TYPE', '') == ''  # 用于在本地开发，觉得代码ok后通过 `kaggle k push` 命令提交到 kaggle 平台\n\nif LOCAL:\n    os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:19:40.486277Z","iopub.execute_input":"2025-12-17T02:19:40.486777Z","iopub.status.idle":"2025-12-17T02:19:40.499320Z","shell.execute_reply.started":"2025-12-17T02:19:40.486750Z","shell.execute_reply":"2025-12-17T02:19:40.498678Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 训练设置\n\n## CV\nCV_STRATEGY = 'random'  # groupby_Sampling_Date\nNFOLD = 5\nKFOLD_SEED = 42\n\n## Model\nMODEL_NAME = 'vit_large_patch16_dinov3_qkvb'  # convnext_base\n\n## Training Hyper Params\nLR = 1e-3","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:19:40.499948Z","iopub.execute_input":"2025-12-17T02:19:40.500871Z","iopub.status.idle":"2025-12-17T02:19:40.510837Z","shell.execute_reply.started":"2025-12-17T02:19:40.500845Z","shell.execute_reply":"2025-12-17T02:19:40.510136Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Load Data","metadata":{"execution":{"iopub.execute_input":"2025-11-21T05:07:31.762158Z","iopub.status.busy":"2025-11-21T05:07:31.760963Z","iopub.status.idle":"2025-11-21T05:07:31.766349Z","shell.execute_reply":"2025-11-21T05:07:31.765388Z","shell.execute_reply.started":"2025-11-21T05:07:31.762125Z"}}},{"cell_type":"markdown","source":"### 数据解读\n\n#### 干物质（Dry Matter）\n牧草含有大量水分，其含水量会因天气、时间、生长阶段等因素而剧烈变化。为了得到一个稳定、可比较的衡量标准来评估牧草的真实营养价值和数量，农学家和农民会先将牧草样本烘干，去除所有水分，然后称量剩余部分的重量。这个重量就是“干物质”或“干燥生物量”。比赛中的所有指标都是基于这个“干”重来计算的。\n\n#### 1. Dry green vegetation (excluding clover) / 干燥的绿色植被（不包括三叶草）\n\n*   **这是什么？**\n    这主要是指牧场中正在生长的、绿色的禾本科牧草（如黑麦草、羊茅等）的干重。它代表了牧场中主要的、有活力的非豆科植物部分。\n*   **为什么重要？**\n    这是牲畜（如牛、羊）**最主要的能量来源**。这些绿色的草富含碳水化合物，为动物提供了日常活动和生长所需的基本能量。它的数量直接决定了牧场能养活多少动物。将其与三叶草分开测量，是因为它们的营养成分（特别是蛋白质含量）有显著差异。\n\n#### 2. Dry dead material / 干燥的枯死物质\n\n*   **这是什么？**\n    这是指牧场中已经枯黄、死亡的植物部分的干重。它可能是前一个季节留下的老草，或是因干旱、过度成熟而死亡的植物。\n*   **为什么重要？**\n    这部分是**低质量的饲料**。它的营养价值非常低（蛋白质和能量含量都很低），消化率差，而且口感不好，牲畜通常会尽量避免采食。\n    *   **指示作用**：如果这部分占比较高，说明牧场管理可能存在问题（例如，放牧不及时导致牧草长老、枯死），或者牧场健康状况不佳。它会“稀释”优质饲料的比例，降低牲畜的采食效率。\n    *   **生态影响**：过多的枯死物质会覆盖在地面，阻碍阳光照射，抑制新草的生长。\n\n#### 3. Dry clover biomass / 干燥的三叶草生物量\n\n*   **这是什么？**\n    这是指牧场中所有三叶草（Clover）或其他豆科植物（如苜蓿）的干重。\n*   **为什么重要？**\n    三叶草是牧场中的“**超级食物**”。\n    *   **高蛋白质**：与禾本科牧草相比，三叶草的蛋白质含量要高得多。蛋白质是动物增重、产奶和维持健康的关键。因此，三叶草的含量直接关系到饲料的“质量”。\n    *   **天然肥料**：三叶草具有“固氮”能力，能将空气中的氮气转化为土壤中的氮肥，为周围的禾本科牧草提供天然养分，从而提高整个牧场的生产力并减少对化肥的依赖。\n\n#### 4. Green dry matter (GDM) / 绿色干物质\n\n*   **这是什么？**\n    这是 **(干燥的绿色植被) + (干燥的三叶草生物量)** 的总和。简单来说，它代表了牧场中所有**有生命的、绿色的植物**的总干重。\n*   **为什么重要？**\n    这是评估牧场**当前可用优质饲料总量**的核心指标。当农民决定一个牧区（paddock）可以放养多少头牛、能放养多少天时，他们最关心的就是GDM。它直接反映了牧场的“承载能力”（Carrying Capacity）。这个数值越高，意味着可供牲畜采食的优质饲料越多。\n\n#### 5. Total dry biomass / 总干燥生物量\n\n*   **这是什么？**\n    这是牧场中所有地上部分生物量的总和，即 **(绿色植被) + (三叶草) + (枯死物质)** 的总干重。\n*   **为什么重要？**\n    这个指标反映了牧场上**所有植物物质的总量**。通过比较“总干燥生物量”和“绿色干物质（GDM）”，农民可以快速了解牧场的健康状况。\n    *   **健康指标**：如果“总干燥生物量”很高，但“绿色干物质”占比很低，说明牧场里堆积了大量无用的枯死物质，需要进行管理（如通过短期重度放牧清理，或用机械割除）。\n    *   **长期规划**：这个数据有助于了解牧场的季节性生长周期和整体生产力。","metadata":{}},{"cell_type":"code","source":"from PIL import Image\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\n%matplotlib inline\n\nfrom sklearn.model_selection import KFold, GroupKFold, StratifiedGroupKFold\nfrom tqdm.auto import tqdm\ntqdm.pandas()","metadata":{"execution":{"iopub.status.busy":"2025-12-17T02:19:40.512888Z","iopub.execute_input":"2025-12-17T02:19:40.513085Z","iopub.status.idle":"2025-12-17T02:19:41.538795Z","shell.execute_reply.started":"2025-12-17T02:19:40.513069Z","shell.execute_reply":"2025-12-17T02:19:41.538004Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"DATA_ROOT = '../input/' if LOCAL else '/kaggle/input/csiro-biomass/'","metadata":{"execution":{"iopub.status.busy":"2025-12-17T02:19:41.539614Z","iopub.execute_input":"2025-12-17T02:19:41.539934Z","iopub.status.idle":"2025-12-17T02:19:41.543641Z","shell.execute_reply.started":"2025-12-17T02:19:41.539915Z","shell.execute_reply":"2025-12-17T02:19:41.543050Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df = pd.read_csv(f'{DATA_ROOT}/train.csv')\ntrain_df.head()","metadata":{"execution":{"iopub.status.busy":"2025-12-17T02:19:41.544657Z","iopub.execute_input":"2025-12-17T02:19:41.544899Z","iopub.status.idle":"2025-12-17T02:19:41.593188Z","shell.execute_reply.started":"2025-12-17T02:19:41.544877Z","shell.execute_reply":"2025-12-17T02:19:41.592599Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df[['sample_id_prefix', 'sample_id_suffix']] = train_df.sample_id.str.split('__', expand=True)","metadata":{"execution":{"iopub.status.busy":"2025-12-17T02:19:41.593855Z","iopub.execute_input":"2025-12-17T02:19:41.594041Z","iopub.status.idle":"2025-12-17T02:19:41.604333Z","shell.execute_reply.started":"2025-12-17T02:19:41.594025Z","shell.execute_reply":"2025-12-17T02:19:41.603626Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"(train_df.sample_id_suffix == train_df.target_name).all()","metadata":{"execution":{"iopub.status.busy":"2025-12-17T02:19:41.604977Z","iopub.execute_input":"2025-12-17T02:19:41.605192Z","iopub.status.idle":"2025-12-17T02:19:41.620243Z","shell.execute_reply.started":"2025-12-17T02:19:41.605171Z","shell.execute_reply":"2025-12-17T02:19:41.619531Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cols = ['sample_id_prefix', 'image_path', 'Sampling_Date', 'State', 'Species', 'Pre_GSHH_NDVI', 'Height_Ave_cm']\nagg_train_df = train_df.groupby(cols).apply(lambda df: df.set_index('target_name').target)\nagg_train_df.reset_index(inplace=True)\nagg_train_df.columns.name = None\n\nagg_train_df['image'] = agg_train_df.image_path.progress_apply(\n    lambda path: Image.open(DATA_ROOT + path).convert('RGB')\n)\n\nagg_train_df.head()","metadata":{"execution":{"iopub.status.busy":"2025-12-17T02:19:41.621044Z","iopub.execute_input":"2025-12-17T02:19:41.621291Z","iopub.status.idle":"2025-12-17T02:20:02.237210Z","shell.execute_reply.started":"2025-12-17T02:19:41.621269Z","shell.execute_reply":"2025-12-17T02:20:02.236502Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"agg_train_df['image_size'] = agg_train_df.image.apply(lambda x: x.size)\nagg_train_df['image_size'].value_counts()","metadata":{"execution":{"iopub.status.busy":"2025-12-17T02:20:02.239997Z","iopub.execute_input":"2025-12-17T02:20:02.240276Z","iopub.status.idle":"2025-12-17T02:20:02.249186Z","shell.execute_reply.started":"2025-12-17T02:20:02.240258Z","shell.execute_reply":"2025-12-17T02:20:02.248443Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"np.isclose(\n    agg_train_df[['Dry_Green_g', 'Dry_Clover_g']].sum(axis=1),\n    agg_train_df['GDM_g'],\n    atol=1e-04\n).mean()","metadata":{"execution":{"iopub.status.busy":"2025-12-17T02:20:02.249911Z","iopub.execute_input":"2025-12-17T02:20:02.250139Z","iopub.status.idle":"2025-12-17T02:20:02.265841Z","shell.execute_reply.started":"2025-12-17T02:20:02.250109Z","shell.execute_reply":"2025-12-17T02:20:02.265214Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"np.isclose(\n    agg_train_df[['GDM_g', 'Dry_Dead_g']].sum(axis=1),\n    agg_train_df['Dry_Total_g'],\n    atol=1e-04\n).mean()","metadata":{"execution":{"iopub.status.busy":"2025-12-17T02:20:02.266656Z","iopub.execute_input":"2025-12-17T02:20:02.266924Z","iopub.status.idle":"2025-12-17T02:20:02.281379Z","shell.execute_reply.started":"2025-12-17T02:20:02.266897Z","shell.execute_reply":"2025-12-17T02:20:02.280732Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(16, 4))\nplt.subplot(1, 3, 1)\nagg_train_df.Dry_Green_g.plot(kind='hist')\n_ = plt.title('Dry_Green_g')\n\nplt.subplot(1, 3, 2)\nagg_train_df.Dry_Clover_g.plot(kind='hist')\n_ = plt.title('Dry_Clover_g')\n\nplt.subplot(1, 3, 3)\nagg_train_df.Dry_Dead_g.plot(kind='hist')\n_ = plt.title('Dry_Dead_g')","metadata":{"execution":{"iopub.status.busy":"2025-12-17T02:20:02.282178Z","iopub.execute_input":"2025-12-17T02:20:02.282448Z","iopub.status.idle":"2025-12-17T02:20:02.869030Z","shell.execute_reply.started":"2025-12-17T02:20:02.282425Z","shell.execute_reply":"2025-12-17T02:20:02.868281Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## CV Strategy","metadata":{}},{"cell_type":"markdown","source":"It appears there is a significant gap between the local CV score and the public LB score. We need to figure out the distributional differences between the test set and training set in order to select an appropriate CV strategy. Here are some discussions regarding the distribution of the test set.\n\n1. [Is there data drift between the training data and the test data?](https://www.kaggle.com/competitions/csiro-biomass/discussion/613724)\n\n    From **Competition Host**:\n> The data split between the public and private sets is not completely random.\n> \n> We used data from various seasons and states throughout the year as the training set. For validation and testing, we included some data from overlapping time periods and locations, **while also incorporating data from non-overlapping time periods to evaluate the model's generalization ability**.\n\n2. [State and Species](https://www.kaggle.com/competitions/csiro-biomass/discussion/615003)\n\n    From **Competition Host**:\n> Hi there, images in the testing set contain the same State and Species\n\n3. [Are There Any New Species in the Test Dataset?](https://www.kaggle.com/competitions/csiro-biomass/discussion/614083)\n    From **Competition Host**:\n> Great question. The direct answer is that all the dominant species in the test set are the same as those in the training set.\n> \n> Please note that the \"Species\" column refers to the visually annotated dominant species in the images and does not include every species present (which would be practically impossible). Additionally, the \"Species\" column is only provided for the training set and is not available for the testing set (in case you didn't know it).\n\nIt appears the distribution differences between the test and training sets are **primarily reflected in the date**. Therefore, we could attempt using `Sampling_Date` as groups for K-fold cross-validation.","metadata":{}},{"cell_type":"code","source":"agg_train_df['Sampling_Date_Month'] = agg_train_df.Sampling_Date.apply(lambda x: x.split('/')[1].strip())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:20:02.870061Z","iopub.execute_input":"2025-12-17T02:20:02.870359Z","iopub.status.idle":"2025-12-17T02:20:02.875190Z","shell.execute_reply.started":"2025-12-17T02:20:02.870335Z","shell.execute_reply":"2025-12-17T02:20:02.874520Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"KFoldClass = StratifiedGroupKFold if CV_STRATEGY == 'groupby_Sampling_Date' else KFold\nkfold = KFoldClass(n_splits=NFOLD, shuffle=True, random_state=KFOLD_SEED)\n\nagg_train_df['fold'] = None\nfor i, (trn_idx, val_idx) in enumerate(kfold.split(agg_train_df.index, y=agg_train_df.State, groups=agg_train_df.Sampling_Date)):\n    agg_train_df.loc[val_idx, 'fold'] = i\n    \n    trn_df = agg_train_df[agg_train_df.fold != i]\n    val_df = agg_train_df[agg_train_df.fold == i]\n    \n    trn_months = sorted(int(x) for x in trn_df.Sampling_Date_Month.unique())\n    val_months = sorted(int(x) for x in val_df.Sampling_Date_Month.unique())\n    print(f'trn({trn_df.shape[0]}) -> val({val_df.shape[0]}): {trn_months} -> {val_months}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:20:02.875927Z","iopub.execute_input":"2025-12-17T02:20:02.876108Z","iopub.status.idle":"2025-12-17T02:20:02.901934Z","shell.execute_reply.started":"2025-12-17T02:20:02.876094Z","shell.execute_reply":"2025-12-17T02:20:02.901163Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## DataLoader","metadata":{}},{"cell_type":"code","source":"from torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms","metadata":{"execution":{"iopub.status.busy":"2025-12-17T02:20:02.902761Z","iopub.execute_input":"2025-12-17T02:20:02.903067Z","iopub.status.idle":"2025-12-17T02:20:09.203106Z","shell.execute_reply.started":"2025-12-17T02:20:02.903044Z","shell.execute_reply":"2025-12-17T02:20:09.202345Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class RegressionDataset(Dataset):\n    def __init__(self, data, transform=None, vertical_split=True):\n        self.data = data\n        self.transform = transform\n        self.vertical_split = vertical_split\n\n    def __len__(self):\n        return self.data.shape[0]\n\n    def __getitem__(self, idx):\n        item = self.data.iloc[idx]\n        image = item.image\n        targets = [item['Dry_Green_g'], item['Dry_Clover_g'], item['Dry_Dead_g']]\n        \n        if self.vertical_split:\n            # 垂直均分成左右两张图片\n            width, height = image.size\n            mid_point = width // 2\n            left_image = image.crop((0, 0, mid_point, height))\n            right_image = image.crop((mid_point, 0, width, height))\n            \n            if self.transform:\n                left_image = self.transform(left_image)\n                right_image = self.transform(right_image)\n            \n            return left_image, right_image, targets\n        \n        else:\n            if self.transform:\n                image = self.transform(image)\n\n            return image, targets\n\n\ndef create_dataloader(data, target_image_size=(256, 256), batch_size=32, shuffle=True, aug=True, tta_transform=None):    \n    if aug:\n        transform = transforms.Compose([\n            transforms.Resize(target_image_size),\n            transforms.RandomHorizontalFlip(p=0.5),\n            transforms.RandomVerticalFlip(p=0.5),\n            transforms.RandomApply([transforms.RandomRotation([90, 90])], p=0.5),\n            transforms.ColorJitter(brightness=0.1, contrast=0.1, saturation=0.1, hue=0.05),\n            transforms.ToTensor(),\n            transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n        ])\n    \n    else:\n        if tta_transform:\n            transform = transforms.Compose([\n                transforms.Resize(target_image_size),\n                tta_transform,\n                transforms.ToTensor(),\n                transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n            ])\n        else:\n            transform = transforms.Compose([\n                transforms.Resize(target_image_size),\n\n                transforms.ToTensor(),\n                transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n            ])\n\n    dataset = RegressionDataset(data, transform=transform)\n    print('dataset size:', len(dataset))\n    dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=shuffle, num_workers=4)\n    return dataloader","metadata":{"execution":{"iopub.status.busy":"2025-12-17T02:20:09.203946Z","iopub.execute_input":"2025-12-17T02:20:09.204875Z","iopub.status.idle":"2025-12-17T02:20:09.214065Z","shell.execute_reply.started":"2025-12-17T02:20:09.204848Z","shell.execute_reply":"2025-12-17T02:20:09.213468Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_tta_dataloaders(data, target_image_size, batch_size):\n    res = []\n    for transform in [None, transforms.RandomHorizontalFlip(p=1.0), transforms.RandomVerticalFlip(p=1.0), transforms.RandomRotation([90, 90])]:\n        res.append(\n            create_dataloader(data, target_image_size, batch_size, shuffle=False, aug=False, tta_transform=transform)\n        )\n    return res","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:20:09.214782Z","iopub.execute_input":"2025-12-17T02:20:09.215036Z","iopub.status.idle":"2025-12-17T02:20:09.235593Z","shell.execute_reply.started":"2025-12-17T02:20:09.215012Z","shell.execute_reply":"2025-12-17T02:20:09.234809Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Model","metadata":{}},{"cell_type":"code","source":"import timm\nimport torch\nimport torch.nn as nn","metadata":{"execution":{"iopub.status.busy":"2025-12-17T02:20:09.236468Z","iopub.execute_input":"2025-12-17T02:20:09.236728Z","iopub.status.idle":"2025-12-17T02:20:12.177393Z","shell.execute_reply.started":"2025-12-17T02:20:09.236706Z","shell.execute_reply":"2025-12-17T02:20:12.176735Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = timm.create_model(MODEL_NAME, pretrained=False, num_classes=3)","metadata":{"execution":{"iopub.status.busy":"2025-12-17T02:20:12.178094Z","iopub.execute_input":"2025-12-17T02:20:12.178330Z","iopub.status.idle":"2025-12-17T02:20:16.805612Z","shell.execute_reply.started":"2025-12-17T02:20:12.178313Z","shell.execute_reply":"2025-12-17T02:20:16.804809Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.pretrained_cfg","metadata":{"execution":{"iopub.status.busy":"2025-12-17T02:20:16.806582Z","iopub.execute_input":"2025-12-17T02:20:16.806840Z","iopub.status.idle":"2025-12-17T02:20:16.812446Z","shell.execute_reply.started":"2025-12-17T02:20:16.806813Z","shell.execute_reply":"2025-12-17T02:20:16.811755Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"TARGET_IMAGE_SIZE = model.pretrained_cfg['input_size'][1:]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:20:16.813148Z","iopub.execute_input":"2025-12-17T02:20:16.813405Z","iopub.status.idle":"2025-12-17T02:20:16.830360Z","shell.execute_reply.started":"2025-12-17T02:20:16.813382Z","shell.execute_reply":"2025-12-17T02:20:16.829662Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class MultiTargetRegressor(nn.Module):\n    def __init__(self, model_name, pretrained=True, num_classes=3, dropout=0.0, freeze_backbone=False):\n        super().__init__()\n        self.backbone = timm.create_model(model_name, pretrained=pretrained, num_classes=0, global_pool='avg')\n        self.dropout = nn.Dropout(dropout)\n        \n        def make_head():\n            return nn.Sequential(\n                nn.Linear(self.backbone.num_features * 2, 8),\n                nn.ReLU(inplace=True),\n                nn.Dropout(dropout),\n                nn.Linear(8, 1),\n            )\n        \n        self.head_green = make_head()\n        self.head_clover = make_head()\n        self.head_dead = make_head()\n        \n        self.softplus = nn.Softplus(beta=1.0)\n    \n        if freeze_backbone:\n            for p in self.backbone.parameters():\n                p.requires_grad = False\n    \n    def forward(self, left_img, right_img):\n        left_feat = self.backbone(left_img)\n        right_feat = self.backbone(right_img)\n        combined = torch.cat([left_feat, right_feat], dim=1)\n        \n        green = self.softplus(self.head_green(combined))    # Bx1\n        clover = self.softplus(self.head_clover(combined))  # Bx1\n        dead = self.softplus(self.head_dead(combined))   # Bx1\n    \n        logits = torch.cat([green, clover, dead], dim=1)  # Bx3\n        return logits","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:20:16.831063Z","iopub.execute_input":"2025-12-17T02:20:16.831291Z","iopub.status.idle":"2025-12-17T02:20:16.843544Z","shell.execute_reply.started":"2025-12-17T02:20:16.831270Z","shell.execute_reply":"2025-12-17T02:20:16.842902Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Train","metadata":{}},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.optim import AdamW\nfrom transformers import get_cosine_schedule_with_warmup","metadata":{"execution":{"iopub.status.busy":"2025-12-17T02:20:16.844161Z","iopub.execute_input":"2025-12-17T02:20:16.844425Z","iopub.status.idle":"2025-12-17T02:20:18.310892Z","shell.execute_reply.started":"2025-12-17T02:20:16.844408Z","shell.execute_reply":"2025-12-17T02:20:18.309931Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ======== Weighted R² ========\ndef weighted_r2_score(y_true: np.ndarray, y_pred: np.ndarray):\n    \"\"\"\n    y_true, y_pred: shape (N, 5): Green/Clover/Dead/GDM/Total\n    \"\"\"\n    weights = np.array([0.1, 0.1, 0.1, 0.2, 0.5])\n    r2_scores = []\n    for i in range(5):\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    r2_scores = np.array(r2_scores)\n    weighted_r2 = np.sum(r2_scores * weights) / np.sum(weights)\n    return weighted_r2, r2_scores","metadata":{"execution":{"iopub.status.busy":"2025-12-17T02:20:18.311769Z","iopub.execute_input":"2025-12-17T02:20:18.312144Z","iopub.status.idle":"2025-12-17T02:20:18.317581Z","shell.execute_reply.started":"2025-12-17T02:20:18.312126Z","shell.execute_reply":"2025-12-17T02:20:18.316979Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def calc_metric(outputs, targets):\n    '''\n        outputs/targets: shape (N, 3): Green/Clover/Dead\n    '''\n    y_true = np.column_stack((\n        targets,\n        targets[:, :2].sum(axis=1),\n        targets.sum(axis=1),\n    ))\n    \n    y_pred = np.column_stack((\n        outputs,\n        outputs[:, :2].sum(axis=1),\n        outputs.sum(axis=1),\n    ))\n    \n    weighted_r2, r2_scores = weighted_r2_score(y_true, y_pred)\n    return weighted_r2, r2_scores","metadata":{"execution":{"iopub.status.busy":"2025-12-17T02:20:18.318481Z","iopub.execute_input":"2025-12-17T02:20:18.318692Z","iopub.status.idle":"2025-12-17T02:20:18.332167Z","shell.execute_reply.started":"2025-12-17T02:20:18.318674Z","shell.execute_reply":"2025-12-17T02:20:18.331402Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_epoch(model, dataloader, criterion, optimizer, scheduler, device):\n    model.train()\n    total_loss = 0\n\n    for images, targets in dataloader:\n        images = images.to(device)\n        targets = torch.stack(targets).T.float().to(device)\n\n        optimizer.zero_grad()\n        outputs = model(images)\n        loss = criterion(outputs, targets)\n        loss.backward()\n        optimizer.step()\n        scheduler.step()\n\n        total_loss += loss.item()\n\n    return total_loss / len(dataloader)\n\n\ndef validate(model, dataloader, criterion, device):\n    model.eval()\n    total_loss = 0\n    all_outputs = []\n    all_targets = []\n\n    with torch.no_grad():\n        for images, targets in dataloader:\n            images = images.to(device)\n            targets = torch.stack(targets).T.float().to(device)\n\n            outputs = model(images)\n            loss = criterion(outputs, targets)\n            total_loss += loss.item()\n\n            all_outputs.append(outputs.detach().cpu())\n            all_targets.append(targets.detach().cpu())\n\n    outputs = torch.cat(all_outputs).numpy()\n    targets = torch.cat(all_targets).numpy()\n    \n    return total_loss / len(dataloader), outputs, targets\n    \n    # weighted_r2, r2_scores = calc_metric(outputs, targets)\n    # return total_loss / len(dataloader), weighted_r2, r2_scores\n\n\ndef tta_validate(model, dataloaders, criterion, device):\n    if not isinstance(dataloaders, list):\n        dataloaders = [dataloaders]\n    \n    all_loss = []\n    all_outputs = []\n    all_targets = []\n    for dataloader in dataloaders:\n        loss, outputs, targets = validate(model, dataloader, criterion, device)\n        all_loss.append(loss)\n        all_outputs.append(outputs)\n        all_targets.append(targets)\n    \n    avg_loss = np.mean(all_loss)\n    avg_outputs = np.mean(all_outputs, axis=0)\n    avg_targets = np.mean(all_targets, axis=0)\n    \n    weighted_r2, r2_scores = calc_metric(avg_outputs, avg_targets)\n    \n    return avg_loss, weighted_r2, r2_scores","metadata":{"execution":{"iopub.status.busy":"2025-12-17T02:20:18.332921Z","iopub.execute_input":"2025-12-17T02:20:18.333240Z","iopub.status.idle":"2025-12-17T02:20:18.346028Z","shell.execute_reply.started":"2025-12-17T02:20:18.333222Z","shell.execute_reply":"2025-12-17T02:20:18.345255Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_fold(data, fold):\n    device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')\n    \n    # Hyperparameters\n    batch_size = 64\n    lr = LR\n    patience = 10\n    num_epochs = 5 if DEBUG else 100\n    warmup_ratio = 0.05\n\n    # data\n    train_loader = create_dataloader(data[data.fold != fold], TARGET_IMAGE_SIZE, batch_size, shuffle=True, aug=True)\n    # val_loader = create_dataloader(data[data.fold == fold], TARGET_IMAGE_SIZE, batch_size, shuffle=False, aug=False)\n    val_loaders = get_tta_dataloaders(data[data.fold == fold], TARGET_IMAGE_SIZE, batch_size)\n\n    # model, loss, optimizer\n    model = timm.create_model(MODEL_NAME, pretrained=True, num_classes=3)\n    model.to(device)\n\n    criterion = nn.SmoothL1Loss()  # nn.MSELoss()\n    optimizer = AdamW(model.parameters(), lr=lr)\n\n    num_training_steps = num_epochs * len(train_loader)\n    warmup_steps = int(warmup_ratio * num_training_steps)\n    \n    # scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='max', factor=0.5, patience=patience//2)\n    scheduler = get_cosine_schedule_with_warmup(optimizer, num_warmup_steps=warmup_steps, num_training_steps=num_training_steps)\n\n    # Training loop\n    history = []\n    best_score = -float('inf')\n    # best_loss = float('inf')\n    epochs_without_improvement = 0\n    \n    for epoch in range(num_epochs):\n        train_loss = train_epoch(model, train_loader, criterion, optimizer, scheduler, device)\n        # val_loss, weighted_r2, r2_scores = validate(model, val_loader, criterion, device)\n        val_loss, weighted_r2, r2_scores = tta_validate(model, val_loaders, criterion, device)\n\n        if epoch % 10 == 0:\n            print(f\"Epoch [{epoch}/{num_epochs}]: Train Loss: {train_loss:.4f}, Val Loss: {val_loss:.4f}, weighted_r2: {weighted_r2:.4f}, lr: {optimizer.param_groups[0]['lr']}\")\n        \n        history.append({\n            'train_loss': train_loss,\n            'val_loss': val_loss,\n            'lr': optimizer.param_groups[0]['lr'],\n            'weighted_r2': weighted_r2,\n            'r2_scores': r2_scores,\n        })\n        \n        # 早停\n        if weighted_r2 > best_score:\n        # if val_loss < best_loss:\n            best_loss = val_loss\n            best_score = weighted_r2\n            epochs_without_improvement = 0\n            # 保存最佳模型\n            torch.save(model.state_dict(), f'{OUTPUT_DIR}/best_mode_fold{fold}.pth')\n        \n        else:\n            epochs_without_improvement += 1\n        \n        if epochs_without_improvement >= patience:\n            print(f\"早停: epoch={epoch}, {patience} 个 epoch 无改善\")\n            break\n\n    print(f\"\\nTraining completed. Best weighted_r2: {best_score:.4f}\")\n    \n    return history, best_score","metadata":{"execution":{"iopub.status.busy":"2025-12-17T02:20:18.350022Z","iopub.execute_input":"2025-12-17T02:20:18.350270Z","iopub.status.idle":"2025-12-17T02:20:18.363527Z","shell.execute_reply.started":"2025-12-17T02:20:18.350253Z","shell.execute_reply":"2025-12-17T02:20:18.362901Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"OUTPUT_DIR = 'trained_models/'\nif not os.path.exists(OUTPUT_DIR):\n    os.mkdir(OUTPUT_DIR)","metadata":{"execution":{"iopub.status.busy":"2025-12-17T02:20:18.364260Z","iopub.execute_input":"2025-12-17T02:20:18.365086Z","iopub.status.idle":"2025-12-17T02:20:18.377582Z","shell.execute_reply.started":"2025-12-17T02:20:18.365063Z","shell.execute_reply":"2025-12-17T02:20:18.376880Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if TRAIN:\n    all_best_score = []\n\n    for i in range(2 if DEBUG else NFOLD):\n        print(f'### fold={i}')\n        history, best_score = train_fold(agg_train_df, fold=i)\n        all_best_score.append(best_score)\n        history = pd.DataFrame(history)\n\n        history.to_json(\n            f'{OUTPUT_DIR}/history_fold{i}.jsonl',\n            orient='records',\n            lines=True,\n            force_ascii=False,\n        )\n\n        # plot\n        plt.figure(figsize=(16, 4))\n\n        plt.subplot(1, 3, 1)\n        plt.title('LR')\n        plt.plot(history.lr)\n\n        plt.subplot(1, 3, 2)\n        plt.title('Loss')\n        plt.plot(history.train_loss, label='train')\n        plt.plot(history.val_loss, label='val')\n        plt.legend()\n\n        plt.subplot(1, 3, 3)\n        plt.title('weighted_r2')\n        plt.plot(history.weighted_r2)\n        plt.show()\n    \n    print('Avg CV:', np.mean(all_best_score))","metadata":{"execution":{"iopub.status.busy":"2025-12-17T02:20:18.378365Z","iopub.execute_input":"2025-12-17T02:20:18.378691Z","iopub.status.idle":"2025-12-17T02:20:18.389438Z","shell.execute_reply.started":"2025-12-17T02:20:18.378669Z","shell.execute_reply":"2025-12-17T02:20:18.388725Z"},"scrolled":true,"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Inference","metadata":{}},{"cell_type":"code","source":"import os\nfrom pathlib import Path","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:20:18.390834Z","iopub.execute_input":"2025-12-17T02:20:18.391073Z","iopub.status.idle":"2025-12-17T02:20:18.400591Z","shell.execute_reply.started":"2025-12-17T02:20:18.391056Z","shell.execute_reply":"2025-12-17T02:20:18.400007Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_lastest_saved_models(model_root='/kaggle/input/csiro-simple-output/pytorch/default/'):    \n    latest = 1\n    for version in os.listdir(model_root):\n        try:\n            version = int(version)\n        except:\n            continue\n\n        if version > latest:\n            latest = version\n\n    return f'{model_root}/{latest}/trained_models/'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:20:18.401264Z","iopub.execute_input":"2025-12-17T02:20:18.401600Z","iopub.status.idle":"2025-12-17T02:20:18.413434Z","shell.execute_reply.started":"2025-12-17T02:20:18.401578Z","shell.execute_reply":"2025-12-17T02:20:18.412789Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# SAVED_MODELS = './trained_models/' if TRAIN else get_lastest_saved_models('/kaggle/input/csiro-local-trained/pytorch/default/')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:20:18.414168Z","iopub.execute_input":"2025-12-17T02:20:18.414686Z","iopub.status.idle":"2025-12-17T02:20:18.428266Z","shell.execute_reply.started":"2025-12-17T02:20:18.414663Z","shell.execute_reply":"2025-12-17T02:20:18.427561Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"SAVED_MODELS = '/kaggle/input/csiro-local-trained/pytorch/default//10/trained_models/'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:20:18.429076Z","iopub.execute_input":"2025-12-17T02:20:18.429325Z","iopub.status.idle":"2025-12-17T02:20:18.440624Z","shell.execute_reply.started":"2025-12-17T02:20:18.429287Z","shell.execute_reply":"2025-12-17T02:20:18.439821Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict(model, dataloader, device):\n    model.to(device)\n    model.eval()\n\n    all_outputs = []\n    with torch.no_grad():\n        for left_images, right_images, targets in dataloader:\n            left_images = left_images.to(device)\n            right_images = right_images.to(device)\n    \n            outputs = model(left_images, right_images)\n            all_outputs.append(outputs.detach().cpu())\n    \n    outputs = torch.cat(all_outputs).numpy()\n    return outputs","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:20:18.441430Z","iopub.execute_input":"2025-12-17T02:20:18.441796Z","iopub.status.idle":"2025-12-17T02:20:18.453753Z","shell.execute_reply.started":"2025-12-17T02:20:18.441771Z","shell.execute_reply":"2025-12-17T02:20:18.452960Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def tta_predict(model, dataloaders, device):\n    all_outputs = []\n    for dataloader in dataloaders:\n        outputs = predict(model, dataloader, device)\n        all_outputs.append(outputs)\n    avg_outputs = np.mean(all_outputs, axis=0)\n    return avg_outputs","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:20:18.454556Z","iopub.execute_input":"2025-12-17T02:20:18.454869Z","iopub.status.idle":"2025-12-17T02:20:18.465696Z","shell.execute_reply.started":"2025-12-17T02:20:18.454850Z","shell.execute_reply":"2025-12-17T02:20:18.465035Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def kfold_predict(dataloaders):\n    device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')\n    \n    all_preds = []\n    for model_file in Path(SAVED_MODELS).glob('*.pth'):\n        # model = timm.create_model(MODEL_NAME, pretrained=False, num_classes=3)\n        fold_idx = int(model_file.name.split('.')[0].split('fold')[1])\n        if fold_idx >= NFOLD: continue\n        print(model_file.name)\n        model = MultiTargetRegressor(MODEL_NAME, pretrained=False, num_classes=3)\n        model.load_state_dict(torch.load(model_file))\n\n        preds = tta_predict(model, dataloaders, device)\n        all_preds.append(preds)\n\n    avg_preds = np.mean(all_preds, axis=0)\n    return avg_preds","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:20:18.466444Z","iopub.execute_input":"2025-12-17T02:20:18.466654Z","iopub.status.idle":"2025-12-17T02:20:18.479580Z","shell.execute_reply.started":"2025-12-17T02:20:18.466631Z","shell.execute_reply":"2025-12-17T02:20:18.478829Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_df = pd.read_csv(DATA_ROOT + 'test.csv')\n\ntest_df['target'] = 0.0\ntest_df[['sample_id_prefix', 'sample_id_suffix']] = test_df.sample_id.str.split('__', expand=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:20:18.480262Z","iopub.execute_input":"2025-12-17T02:20:18.480535Z","iopub.status.idle":"2025-12-17T02:20:18.499151Z","shell.execute_reply.started":"2025-12-17T02:20:18.480516Z","shell.execute_reply":"2025-12-17T02:20:18.498582Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cols = ['sample_id_prefix', 'image_path']\nagg_test_df = test_df.groupby(cols).apply(lambda df: df.set_index('target_name').target)\nagg_test_df.reset_index(inplace=True)\nagg_test_df.columns.name = None\n\nagg_test_df['image'] = agg_test_df.image_path.progress_apply(\n    lambda path: Image.open(DATA_ROOT + path).convert('RGB')\n)\n\nagg_test_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:20:18.499915Z","iopub.execute_input":"2025-12-17T02:20:18.500118Z","iopub.status.idle":"2025-12-17T02:20:18.644634Z","shell.execute_reply.started":"2025-12-17T02:20:18.500103Z","shell.execute_reply":"2025-12-17T02:20:18.643951Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_loader = get_tta_dataloaders(agg_test_df, TARGET_IMAGE_SIZE, 64)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:20:18.645514Z","iopub.execute_input":"2025-12-17T02:20:18.645829Z","iopub.status.idle":"2025-12-17T02:20:18.651112Z","shell.execute_reply.started":"2025-12-17T02:20:18.645803Z","shell.execute_reply":"2025-12-17T02:20:18.650274Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"preds = kfold_predict(test_loader)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:20:18.652005Z","iopub.execute_input":"2025-12-17T02:20:18.652292Z","iopub.status.idle":"2025-12-17T02:21:37.306380Z","shell.execute_reply.started":"2025-12-17T02:20:18.652272Z","shell.execute_reply":"2025-12-17T02:21:37.305547Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"agg_test_df[['Dry_Green_g', 'Dry_Clover_g', 'Dry_Dead_g']] = preds\nagg_test_df['GDM_g'] = agg_test_df.Dry_Green_g + agg_test_df.Dry_Clover_g\nagg_test_df['Dry_Total_g'] = agg_test_df.GDM_g + agg_test_df.Dry_Dead_g","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:21:37.307393Z","iopub.execute_input":"2025-12-17T02:21:37.307827Z","iopub.status.idle":"2025-12-17T02:21:37.314541Z","shell.execute_reply.started":"2025-12-17T02:21:37.307802Z","shell.execute_reply":"2025-12-17T02:21:37.313817Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"agg_test_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:21:37.315416Z","iopub.execute_input":"2025-12-17T02:21:37.315722Z","iopub.status.idle":"2025-12-17T02:21:37.338160Z","shell.execute_reply.started":"2025-12-17T02:21:37.315699Z","shell.execute_reply":"2025-12-17T02:21:37.337360Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cols = ['Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'Dry_Total_g', 'GDM_g']\nsub_df = agg_test_df.set_index('sample_id_prefix')[cols].stack()\nsub_df = sub_df.reset_index()\nsub_df.columns = ['sample_id_prefix', 'target_name', 'target']\n\nsub_df['sample_id'] = sub_df.sample_id_prefix + '__' + sub_df.target_name","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:21:37.339126Z","iopub.execute_input":"2025-12-17T02:21:37.339710Z","iopub.status.idle":"2025-12-17T02:21:37.363896Z","shell.execute_reply.started":"2025-12-17T02:21:37.339686Z","shell.execute_reply":"2025-12-17T02:21:37.363350Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cols = ['sample_id', 'target']\nsub_df[cols].to_csv('submission_p1.csv', index=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:21:37.364616Z","iopub.execute_input":"2025-12-17T02:21:37.365068Z","iopub.status.idle":"2025-12-17T02:21:37.373503Z","shell.execute_reply.started":"2025-12-17T02:21:37.365044Z","shell.execute_reply":"2025-12-17T02:21:37.372799Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!head submission_p1.csv","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:21:37.374307Z","iopub.execute_input":"2025-12-17T02:21:37.374547Z","iopub.status.idle":"2025-12-17T02:21:37.628078Z","shell.execute_reply.started":"2025-12-17T02:21:37.374532Z","shell.execute_reply":"2025-12-17T02:21:37.627161Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# [csiro simple V30](https://www.kaggle.com/code/jiazhuang/csiro-simple?scriptVersionId=286468599)","metadata":{}},{"cell_type":"markdown","source":"### Model","metadata":{}},{"cell_type":"code","source":"import timm\nimport torch\nimport torch.nn as nn","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:21:37.629132Z","iopub.execute_input":"2025-12-17T02:21:37.629431Z","iopub.status.idle":"2025-12-17T02:21:37.633350Z","shell.execute_reply.started":"2025-12-17T02:21:37.629407Z","shell.execute_reply":"2025-12-17T02:21:37.632615Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class FiLM(nn.Module):\n    def __init__(self, feat_dim):\n        super().__init__()\n        hidden = max(32, feat_dim // 2)\n        self.mlp = nn.Sequential(\n            nn.Linear(feat_dim, hidden), nn.ReLU(inplace=True), nn.Linear(hidden, feat_dim * 2)\n        )\n\n    def forward(self, context):\n        gamma_beta = self.mlp(context)\n        return torch.chunk(gamma_beta, 2, dim=1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:21:37.634136Z","iopub.execute_input":"2025-12-17T02:21:37.634457Z","iopub.status.idle":"2025-12-17T02:21:37.647725Z","shell.execute_reply.started":"2025-12-17T02:21:37.634441Z","shell.execute_reply":"2025-12-17T02:21:37.646965Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class MultiTargetRegressor(nn.Module):\n    def __init__(self, model_name, pretrained=True, num_classes=3, dropout=0.0, freeze_backbone=False):\n        super().__init__()\n        self.backbone = timm.create_model(model_name, pretrained=pretrained, num_classes=0, global_pool='avg')\n        \n        self.film = FiLM(self.backbone.num_features)\n        \n        self.dropout = nn.Dropout(dropout)\n        self.head = nn.Linear(self.backbone.num_features * 2, num_classes)\n    \n        if freeze_backbone:\n            for p in self.backbone.parameters():\n                p.requires_grad = False\n    \n    def forward(self, left_img, right_img):\n        left_feat = self.backbone(left_img)\n        right_feat = self.backbone(right_img)\n        \n        context = (left_feat + right_feat) / 2\n        gamma, beta = self.film(context)\n        \n        left_feat_modulated = left_feat * (1 + gamma) + beta\n        right_feat_modulated = right_feat * (1 + gamma) + beta\n        \n        combined = torch.cat([left_feat_modulated, right_feat_modulated], dim=1)\n        \n        features = self.dropout(combined)\n        logits = self.head(features)\n        return logits","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:21:37.648506Z","iopub.execute_input":"2025-12-17T02:21:37.648767Z","iopub.status.idle":"2025-12-17T02:21:37.662067Z","shell.execute_reply.started":"2025-12-17T02:21:37.648745Z","shell.execute_reply":"2025-12-17T02:21:37.661348Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"SAVED_MODELS = '/kaggle/input/csiro-local-trained/pytorch/default//11/trained_models/'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:21:37.662794Z","iopub.execute_input":"2025-12-17T02:21:37.662997Z","iopub.status.idle":"2025-12-17T02:21:37.679007Z","shell.execute_reply.started":"2025-12-17T02:21:37.662977Z","shell.execute_reply":"2025-12-17T02:21:37.678178Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_loader = get_tta_dataloaders(agg_test_df, TARGET_IMAGE_SIZE, 64)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:21:37.679815Z","iopub.execute_input":"2025-12-17T02:21:37.680050Z","iopub.status.idle":"2025-12-17T02:21:37.693385Z","shell.execute_reply.started":"2025-12-17T02:21:37.680025Z","shell.execute_reply":"2025-12-17T02:21:37.692700Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"preds = kfold_predict(test_loader)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:21:37.694185Z","iopub.execute_input":"2025-12-17T02:21:37.694477Z","iopub.status.idle":"2025-12-17T02:22:58.038153Z","shell.execute_reply.started":"2025-12-17T02:21:37.694460Z","shell.execute_reply":"2025-12-17T02:22:58.037384Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"agg_test_df[['Dry_Green_g', 'Dry_Clover_g', 'Dry_Dead_g']] = preds\nagg_test_df['GDM_g'] = agg_test_df.Dry_Green_g + agg_test_df.Dry_Clover_g\nagg_test_df['Dry_Total_g'] = agg_test_df.GDM_g + agg_test_df.Dry_Dead_g","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:22:58.039179Z","iopub.execute_input":"2025-12-17T02:22:58.039444Z","iopub.status.idle":"2025-12-17T02:22:58.046073Z","shell.execute_reply.started":"2025-12-17T02:22:58.039421Z","shell.execute_reply":"2025-12-17T02:22:58.045337Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"agg_test_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:22:58.046986Z","iopub.execute_input":"2025-12-17T02:22:58.047461Z","iopub.status.idle":"2025-12-17T02:22:58.067154Z","shell.execute_reply.started":"2025-12-17T02:22:58.047444Z","shell.execute_reply":"2025-12-17T02:22:58.066508Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cols = ['Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'Dry_Total_g', 'GDM_g']\nsub_df = agg_test_df.set_index('sample_id_prefix')[cols].stack()\nsub_df = sub_df.reset_index()\nsub_df.columns = ['sample_id_prefix', 'target_name', 'target']\n\nsub_df['sample_id'] = sub_df.sample_id_prefix + '__' + sub_df.target_name","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:22:58.068057Z","iopub.execute_input":"2025-12-17T02:22:58.068636Z","iopub.status.idle":"2025-12-17T02:22:58.082881Z","shell.execute_reply.started":"2025-12-17T02:22:58.068612Z","shell.execute_reply":"2025-12-17T02:22:58.082306Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cols = ['sample_id', 'target']\nsub_df[cols].to_csv('submission_p2.csv', index=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:22:58.083684Z","iopub.execute_input":"2025-12-17T02:22:58.083889Z","iopub.status.idle":"2025-12-17T02:22:58.185631Z","shell.execute_reply.started":"2025-12-17T02:22:58.083868Z","shell.execute_reply":"2025-12-17T02:22:58.185032Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!head submission_p2.csv","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:22:58.186382Z","iopub.execute_input":"2025-12-17T02:22:58.186673Z","iopub.status.idle":"2025-12-17T02:22:58.433795Z","shell.execute_reply.started":"2025-12-17T02:22:58.186645Z","shell.execute_reply":"2025-12-17T02:22:58.432781Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# [0.68 score CSIRO ensemble of 2](https://www.kaggle.com/code/antonoof/0-68-score-csiro-ensemble-of-2?scriptVersionId=286100968)","metadata":{}},{"cell_type":"code","source":"import os\nimport math\nimport argparse\nimport logging\nfrom dataclasses import dataclass, field\nfrom typing import List, Tuple, Dict, Optional\n\nimport cv2\nimport timm\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\nfrom tqdm import tqdm\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\n\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\n\n@dataclass\nclass Config:\n    dropout: float = 0.1\n    hidden_ratio: float = 0.35\n    \n    dino_candidates: Tuple[str, ...] = (\n        \"vit_base_patch14_dinov2\",\n        \"vit_base_patch14_reg4_dinov2\",\n        \"vit_small_patch14_dinov2\",\n    )\n    small_grid: Tuple[int, int] = (4, 4)\n    big_grid: Tuple[int, int] = (2, 2)\n    t2t_depth: int = 2\n    cross_layers: int = 2\n    cross_heads: int = 6\n    \n    pyramid_dims: Tuple[int, int, int] = (384, 512, 640)\n    mobilevit_heads: int = 4\n    mobilevit_depth: int = 2\n    sra_heads: int = 8\n    sra_ratio: int = 2\n    mamba_depth: int = 3\n    mamba_kernel: int = 5\n    aux_head: bool = True\n    aux_loss_weight: float = 0.4\n    \n    base_path: str = \"/kaggle/input/csiro-biomass\"\n    test_csv: str = \"/kaggle/input/csiro-biomass/test.csv\"\n    test_image_dir: str = \"/kaggle/input/csiro-biomass/test\"\n    \n    experiment_dir_a: str = \"/kaggle/input/csiro/pytorch/default/12\"\n    ckpt_pattern_fold_x_a: str = \"/kaggle/input/csiro/pytorch/default/12/fold_{fold}/checkpoints/best_wr2.pt\"\n    ckpt_pattern_foldx_a: str = \"/kaggle/input/csiro/pytorch/default/12/fold{fold}/checkpoints/best_wr2.pt\"\n    n_folds_a: int = 5\n    \n    model_dir_b: str = \"/kaggle/input/csiro-mvp-models\"\n    model_paths_b: List[str] = field(default_factory=lambda: [\n        f\"/kaggle/input/csiro-mvp-models/model{i}.pth\" for i in range(1, 11)\n    ])\n    weight_a: float = 0.95\n    weight_b: float = 0.075\n    \n    batch_size: int = 1\n    num_workers: int = 0\n    mixed_precision: bool = True\n    use_tta: bool = True\n    \n    submission_file: str = \"submission.csv\"\n    \n    all_target_cols: Tuple[str, ...] = (\n        \"Dry_Green_g\",\n        \"Dry_Dead_g\",\n        \"Dry_Clover_g\",\n        \"GDM_g\",\n        \"Dry_Total_g\",\n    )\n    \n    @property\n    def device(self):\n        return torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    \n    @property\n    def ckpts_a(self):\n        return self.model_paths_b[:5]\n    \n    @property\n    def ckpts_b(self):\n        return self.model_paths_b[5:]\n\n\nCFG = Config()\n\n\nclass FeedForward(nn.Module):\n    def __init__(self, dim, mlp_ratio=4.0, dropout=0.0):\n        super().__init__()\n        hid = int(dim * mlp_ratio)\n        self.net = nn.Sequential(\n            nn.Linear(dim, hid),\n            nn.GELU(),\n            nn.Dropout(dropout),\n            nn.Linear(hid, dim),\n            nn.Dropout(dropout),\n        )\n\n    def forward(self, x):\n        return self.net(x)\n\n\nclass AttentionBlock(nn.Module):\n    def __init__(self, dim, heads=8, dropout=0.0, mlp_ratio=4.0):\n        super().__init__()\n        self.norm1 = nn.LayerNorm(dim)\n        self.attn = nn.MultiheadAttention(dim, heads, dropout=dropout, batch_first=True)\n        self.norm2 = nn.LayerNorm(dim)\n        self.ff = FeedForward(dim, mlp_ratio=mlp_ratio, dropout=dropout)\n\n    def forward(self, x):\n        h = self.norm1(x)\n        attn_out, _ = self.attn(h, h, h, need_weights=False)\n        x = x + attn_out\n        x = x + self.ff(self.norm2(x))\n        return x\n\n\nclass MobileViTBlock(nn.Module):\n    def __init__(self, dim, heads=4, depth=2, patch=(2, 2), dropout=0.0):\n        super().__init__()\n        self.local = nn.Sequential(\n            nn.Conv2d(dim, dim, 3, padding=1, groups=dim),\n            nn.Conv2d(dim, dim, 1),\n            nn.GELU(),\n        )\n        self.patch = patch\n        self.transformer = nn.ModuleList(\n            [AttentionBlock(dim, heads=heads, dropout=dropout, mlp_ratio=2.0) for _ in range(depth)]\n        )\n        self.fuse = nn.Conv2d(dim * 2, dim, kernel_size=1)\n\n    def forward(self, x: torch.Tensor):\n        local_feat = self.local(x)\n        B, C, H, W = local_feat.shape\n        ph, pw = self.patch\n        new_h = math.ceil(H / ph) * ph\n        new_w = math.ceil(W / pw) * pw\n        if new_h != H or new_w != W:\n            local_feat = F.interpolate(local_feat, size=(new_h, new_w), mode=\"bilinear\", align_corners=False)\n            H, W = new_h, new_w\n\n        tokens = local_feat.unfold(2, ph, ph).unfold(3, pw, pw)\n        tokens = tokens.contiguous().view(B, C, -1, ph, pw)\n        tokens = tokens.permute(0, 2, 3, 4, 1).reshape(B, -1, C)\n\n        for blk in self.transformer:\n            tokens = blk(tokens)\n\n        feat = tokens.view(B, -1, ph * pw, C).permute(0, 3, 1, 2)\n        nh = H // ph\n        nw = W // pw\n        feat = feat.view(B, C, nh, nw, ph, pw).permute(0, 1, 2, 4, 3, 5)\n        feat = feat.reshape(B, C, H, W)\n\n        if feat.shape[-2:] != x.shape[-2:]:\n            feat = F.interpolate(feat, size=x.shape[-2:], mode=\"bilinear\", align_corners=False)\n\n        out = self.fuse(torch.cat([x, feat], dim=1))\n        return out\n\n\nclass SpatialReductionAttention(nn.Module):\n    def __init__(self, dim, heads=8, sr_ratio=2, dropout=0.0):\n        super().__init__()\n        self.heads = heads\n        self.scale = (dim // heads) ** -0.5\n        self.q = nn.Linear(dim, dim)\n        self.kv = nn.Linear(dim, dim * 2)\n        self.sr_ratio = sr_ratio\n        if sr_ratio > 1:\n            self.sr = nn.Conv2d(dim, dim, kernel_size=sr_ratio, stride=sr_ratio)\n            self.norm = nn.LayerNorm(dim)\n        else:\n            self.sr = None\n        self.proj = nn.Linear(dim, dim)\n        self.drop = nn.Dropout(dropout)\n\n    def forward(self, x, hw: Tuple[int, int]):\n        B, N, C = x.shape\n        q = self.q(x).reshape(B, N, self.heads, C // self.heads).permute(0, 2, 1, 3)\n\n        if self.sr is not None:\n            H, W = hw\n            feat = x.transpose(1, 2).reshape(B, C, H, W)\n            feat = self.sr(feat)\n            feat = feat.reshape(B, C, -1).transpose(1, 2)\n            feat = self.norm(feat)\n        else:\n            feat = x\n\n        kv = self.kv(feat)\n        k, v = kv.chunk(2, dim=-1)\n        k = k.reshape(B, -1, self.heads, C // self.heads).permute(0, 2, 3, 1)\n        v = v.reshape(B, -1, self.heads, C // self.heads).permute(0, 2, 1, 3)\n\n        attn = torch.matmul(q, k) * self.scale\n        attn = attn.softmax(dim=-1)\n        attn = self.drop(attn)\n        out = torch.matmul(attn, v).permute(0, 2, 1, 3).reshape(B, N, C)\n        out = self.proj(out)\n        return out\n\n\nclass PVTBlock(nn.Module):\n    def __init__(self, dim, heads=8, sr_ratio=2, dropout=0.0, mlp_ratio=4.0):\n        super().__init__()\n        self.norm1 = nn.LayerNorm(dim)\n        self.sra = SpatialReductionAttention(dim, heads=heads, sr_ratio=sr_ratio, dropout=dropout)\n        self.norm2 = nn.LayerNorm(dim)\n        self.ff = FeedForward(dim, mlp_ratio=mlp_ratio, dropout=dropout)\n\n    def forward(self, x, hw: Tuple[int, int]):\n        x = x + self.sra(self.norm1(x), hw)\n        x = x + self.ff(self.norm2(x))\n        return x\n\n\nclass LocalMambaBlock(nn.Module):\n    def __init__(self, dim, kernel_size=5, dropout=0.0):\n        super().__init__()\n        self.norm = nn.LayerNorm(dim)\n        self.dwconv = nn.Conv1d(dim, dim, kernel_size=kernel_size, padding=kernel_size//2, groups=dim)\n        self.gate = nn.Linear(dim, dim)\n        self.proj = nn.Linear(dim, dim)\n        self.drop = nn.Dropout(dropout)\n\n    def forward(self, x):\n        shortcut = x\n        x = self.norm(x)\n        g = torch.sigmoid(self.gate(x))\n        x = (x * g).transpose(1, 2)\n        x = self.dwconv(x).transpose(1, 2)\n        x = self.proj(x)\n        x = self.drop(x)\n        return shortcut + x\n\n\nclass T2TRetokenizer(nn.Module):\n    def __init__(self, dim, depth=2, heads=4, dropout=0.0):\n        super().__init__()\n        self.blocks = nn.ModuleList(\n            [AttentionBlock(dim, heads=heads, dropout=dropout, mlp_ratio=2.0) for _ in range(depth)]\n        )\n\n    def forward(self, tokens: torch.Tensor, grid_hw: Tuple[int, int]):\n        B, T, C = tokens.shape\n        H, W = grid_hw\n        feat_map = tokens.transpose(1, 2).reshape(B, C, H, W)\n        seq = feat_map.flatten(2).transpose(1, 2)\n        for blk in self.blocks:\n            seq = blk(seq)\n        seq_map = seq.transpose(1, 2).reshape(B, C, H, W)\n        pooled = F.adaptive_avg_pool2d(seq_map, (2, 2))\n        retokens = pooled.flatten(2).transpose(1, 2)\n        return retokens, seq_map\n\n\nclass CrossScaleFusion(nn.Module):\n    def __init__(self, dim, heads=6, dropout=0.0, layers=2):\n        super().__init__()\n        self.layers_s = nn.ModuleList(\n            [AttentionBlock(dim, heads=heads, dropout=dropout, mlp_ratio=2.0) for _ in range(layers)]\n        )\n        self.layers_b = nn.ModuleList(\n            [AttentionBlock(dim, heads=heads, dropout=dropout, mlp_ratio=2.0) for _ in range(layers)]\n        )\n        self.cross_s = nn.ModuleList(\n            [\n                nn.MultiheadAttention(dim, heads, dropout=dropout, batch_first=True, kdim=dim, vdim=dim)\n                for _ in range(layers)\n            ]\n        )\n        self.cross_b = nn.ModuleList(\n            [\n                nn.MultiheadAttention(dim, heads, dropout=dropout, batch_first=True, kdim=dim, vdim=dim)\n                for _ in range(layers)\n            ]\n        )\n        self.norm_s = nn.LayerNorm(dim)\n        self.norm_b = nn.LayerNorm(dim)\n\n    def forward(self, tok_s: torch.Tensor, tok_b: torch.Tensor):\n        B, Ts, C = tok_s.shape\n        Tb = tok_b.shape[1]\n        cls_s = tok_s.new_zeros(B, 1, C)\n        cls_b = tok_b.new_zeros(B, 1, C)\n        tok_s = torch.cat([cls_s, tok_s], dim=1)\n        tok_b = torch.cat([cls_b, tok_b], dim=1)\n\n        for ls, lb, cs, cb in zip(self.layers_s, self.layers_b, self.cross_s, self.cross_b):\n            tok_s = ls(tok_s)\n            tok_b = lb(tok_b)\n            q_s = self.norm_s(tok_s[:, :1])\n            q_b = self.norm_b(tok_b[:, :1])\n            cls_s_upd, _ = cs(\n                q_s,\n                torch.cat([tok_b, q_b], dim=1),\n                torch.cat([tok_b, q_b], dim=1),\n                need_weights=False,\n            )\n            cls_b_upd, _ = cb(\n                q_b,\n                torch.cat([tok_s, q_s], dim=1),\n                torch.cat([tok_s, q_s], dim=1),\n                need_weights=False,\n            )\n            tok_s = torch.cat([tok_s[:, :1] + cls_s_upd, tok_s[:, 1:]], dim=1)\n            tok_b = torch.cat([tok_b[:, :1] + cls_b_upd, tok_b[:, 1:]], dim=1)\n\n        tokens = torch.cat([tok_s[:, :1], tok_b[:, :1], tok_s[:, 1:], tok_b[:, 1:]], dim=1)\n        return tokens\n\n\nclass TileEncoder(nn.Module):\n    def __init__(self, backbone: nn.Module, input_res: int):\n        super().__init__()\n        self.backbone = backbone\n        self.input_res = input_res\n\n    def forward(self, x: torch.Tensor, grid: Tuple[int, int]):\n        B, C, H, W = x.shape\n        r, c = grid\n        hs = torch.linspace(0, H, steps=r + 1, device=x.device).round().long()\n        ws = torch.linspace(0, W, steps=c + 1, device=x.device).round().long()\n        tiles = []\n        for i in range(r):\n            for j in range(c):\n                rs, re = hs[i].item(), hs[i + 1].item()\n                cs, ce = ws[j].item(), ws[j + 1].item()\n                xt = x[:, :, rs:re, cs:ce]\n                if xt.shape[-2:] != (self.input_res, self.input_res):\n                    xt = F.interpolate(xt, size=(self.input_res, self.input_res), mode=\"bilinear\", align_corners=False)\n                tiles.append(xt)\n        tiles = torch.stack(tiles, dim=1)\n        flat = tiles.view(-1, C, self.input_res, self.input_res)\n        feats = self.backbone(flat)\n        feats = feats.view(B, -1, feats.shape[-1])\n        return feats\n\n\nclass PyramidMixer(nn.Module):\n    def __init__(\n        self,\n        dim_in: int,\n        dims: Tuple[int, int, int],\n        mobilevit_heads: int = 4,\n        mobilevit_depth: int = 2,\n        sra_heads: int = 6,\n        sra_ratio: int = 2,\n        mamba_depth: int = 3,\n        mamba_kernel: int = 5,\n        dropout: float = 0.0,\n    ):\n        super().__init__()\n        c1, c2, c3 = dims\n        self.proj1 = nn.Linear(dim_in, c1)\n        self.mobilevit = MobileViTBlock(c1, heads=mobilevit_heads, depth=mobilevit_depth, dropout=dropout)\n        self.proj2 = nn.Linear(c1, c2)\n        self.pvt = PVTBlock(c2, heads=sra_heads, sr_ratio=sra_ratio, dropout=dropout, mlp_ratio=3.0)\n        self.mamba_local = LocalMambaBlock(c2, kernel_size=mamba_kernel, dropout=dropout)\n        self.proj3 = nn.Linear(c2, c3)\n        self.mamba_global = nn.ModuleList(\n            [LocalMambaBlock(c3, kernel_size=mamba_kernel, dropout=dropout) for _ in range(mamba_depth)]\n        )\n        self.final_attn = AttentionBlock(c3, heads=min(8, c3 // 64 + 1), dropout=dropout, mlp_ratio=2.0)\n\n    def _tokens_to_map(self, tokens: torch.Tensor, target_hw: Tuple[int, int]):\n        B, N, C = tokens.shape\n        H, W = target_hw\n        need = H * W\n        if N < need:\n            pad = tokens.new_zeros(B, need - N, C)\n            tokens = torch.cat([tokens, pad], dim=1)\n        tokens = tokens[:, :need, :]\n        feat_map = tokens.transpose(1, 2).reshape(B, C, H, W)\n        return feat_map\n\n    @staticmethod\n    def _fit_hw(n_tokens: int) -> Tuple[int, int]:\n        h = int(math.sqrt(n_tokens))\n        w = h\n        while h * w < n_tokens:\n            w += 1\n            if h * w < n_tokens:\n                h += 1\n        return h, w\n\n    def forward(self, tokens: torch.Tensor):\n        B, N, C = tokens.shape\n        map_hw = (3, 4)\n        feat_map = self._tokens_to_map(tokens, map_hw)\n\n        t1 = self.proj1(tokens)\n        m1 = self._tokens_to_map(t1, map_hw)\n        m1 = self.mobilevit(m1)\n        t1_out = m1.flatten(2).transpose(1, 2)[:, :N]\n\n        t2 = self.proj2(t1_out)\n        new_len = max(4, N // 2)\n        t2 = t2[:, :new_len] + F.adaptive_avg_pool1d(t2.transpose(1, 2), new_len).transpose(1, 2)\n        hw2 = self._fit_hw(t2.size(1))\n        if t2.size(1) < hw2[0] * hw2[1]:\n            pad = t2.new_zeros(B, hw2[0] * hw2[1] - t2.size(1), t2.size(2))\n            t2 = torch.cat([t2, pad], dim=1)\n        t2 = self.pvt(t2, hw2)\n        t2 = self.mamba_local(t2)\n\n        t3 = self.proj3(t2)\n        pooled = torch.stack([t3.mean(dim=1), t3.max(dim=1).values], dim=1)\n        t3 = pooled\n        for blk in self.mamba_global:\n            t3 = blk(t3)\n        t3 = self.final_attn(t3)\n        global_feat = t3.mean(dim=1)\n        return global_feat, {\"stage1_map\": m1.detach(), \"stage2_tokens\": t2.detach(), \"stage3_tokens\": t3.detach()}\n\n\nclass CrossPVT_T2T_MambaDINO(nn.Module):\n    def __init__(self, dropout: float = 0.1, hidden_ratio: float = 0.35):\n        super().__init__()\n        self.backbone, self.feat_dim, self.backbone_name, self.input_res = self._build_dino_backbone()\n        self.tile_encoder = TileEncoder(self.backbone, self.input_res)\n        self.t2t = T2TRetokenizer(self.feat_dim, depth=CFG.t2t_depth, heads=CFG.cross_heads, dropout=dropout)\n        self.cross = CrossScaleFusion(\n            self.feat_dim, heads=CFG.cross_heads, dropout=dropout, layers=CFG.cross_layers\n        )\n        self.pyramid = PyramidMixer(\n            dim_in=self.feat_dim,\n            dims=CFG.pyramid_dims,\n            mobilevit_heads=CFG.mobilevit_heads,\n            mobilevit_depth=CFG.mobilevit_depth,\n            sra_heads=CFG.sra_heads,\n            sra_ratio=CFG.sra_ratio,\n            mamba_depth=CFG.mamba_depth,\n            mamba_kernel=CFG.mamba_kernel,\n            dropout=dropout,\n        )\n\n        combined = CFG.pyramid_dims[-1] * 2\n        self.combined_dim = combined\n        hidden = max(32, int(combined * hidden_ratio))\n\n        def head():\n            return nn.Sequential(\n                nn.Linear(combined, hidden),\n                nn.GELU(),\n                nn.Dropout(dropout),\n                nn.Linear(hidden, 1),\n            )\n\n        self.head_green = head()\n        self.head_clover = head()\n        self.head_dead = head()\n        self.score_head = nn.Sequential(nn.LayerNorm(combined), nn.Linear(combined, 1))\n        self.aux_head = (\n            nn.Sequential(nn.LayerNorm(CFG.pyramid_dims[1]), nn.Linear(CFG.pyramid_dims[1], 5))\n            if CFG.aux_head\n            else None\n        )\n        self.softplus = nn.Softplus(beta=1.0)\n\n        self.cross_gate_left = nn.Linear(CFG.pyramid_dims[-1], CFG.pyramid_dims[-1])\n        self.cross_gate_right = nn.Linear(CFG.pyramid_dims[-1], CFG.pyramid_dims[-1])\n\n    def _build_dino_backbone(self):\n        last_err = None\n        for name in CFG.dino_candidates:\n            for gp in [\"token\", \"avg\", \"__default__\"]:\n                try:\n                    if gp == \"__default__\":\n                        m = timm.create_model(name, pretrained=False, num_classes=0)\n                        gp_str = \"default\"\n                    else:\n                        m = timm.create_model(name, pretrained=False, num_classes=0, global_pool=gp)\n                        gp_str = gp\n                    feat = m.num_features\n                    input_res = self._infer_input_res(m)\n                    if hasattr(m, \"set_grad_checkpointing\"):\n                        m.set_grad_checkpointing(True)\n                    return m, feat, name, int(input_res)\n                except Exception as e:\n                    last_err = e\n                    continue\n        raise RuntimeError(f\"Cannot create DINO backbone. Last error: {last_err}\")\n\n    @staticmethod\n    def _infer_input_res(m) -> int:\n        if hasattr(m, \"patch_embed\") and hasattr(m.patch_embed, \"img_size\"):\n            isz = m.patch_embed.img_size\n            return int(isz if isinstance(isz, (int, float)) else isz[0])\n        if hasattr(m, \"img_size\"):\n            isz = m.img_size\n            return int(isz if isinstance(isz, (int, float)) else isz[0])\n        dc = getattr(m, \"default_cfg\", {}) or {}\n        ins = dc.get(\"input_size\", None)\n        if ins:\n            if isinstance(ins, (tuple, list)) and len(ins) >= 2:\n                return int(ins[1])\n            return int(ins if isinstance(ins, (int, float)) else 224)\n        return 518\n\n    def _half_forward(self, x_half: torch.Tensor):\n        tiles_small = self.tile_encoder(x_half, CFG.small_grid)\n        tiles_big = self.tile_encoder(x_half, CFG.big_grid)\n        t2, stage1_map = self.t2t(tiles_small, CFG.small_grid)\n        fused = self.cross(t2, tiles_big)\n        feat, feat_maps = self.pyramid(fused)\n        feat_maps[\"stage1_map\"] = stage1_map\n        return feat, feat_maps\n\n    def _merge_heads(self, f_l: torch.Tensor, f_r: torch.Tensor):\n        g_l = torch.sigmoid(self.cross_gate_left(f_r))\n        g_r = torch.sigmoid(self.cross_gate_right(f_l))\n        f_l = f_l * g_l\n        f_r = f_r * g_r\n        f = torch.cat([f_l, f_r], dim=1)\n        green_pos = self.softplus(self.head_green(f))\n        clover_pos = self.softplus(self.head_clover(f))\n        dead_pos = self.softplus(self.head_dead(f))\n        gdm = green_pos + clover_pos\n        total = gdm + dead_pos\n        return total, gdm, green_pos, f\n\n    def forward(self, *inputs, x_left=None, x_right=None, return_features: bool = False):\n        if inputs:\n            if len(inputs) == 1:\n                first = inputs[0]\n                if isinstance(first, (tuple, list)):\n                    if len(first) >= 1:\n                        x_left = first[0]\n                    if len(first) >= 2:\n                        x_right = first[1]\n                else:\n                    x_left = first\n            else:\n                x_left = inputs[0]\n                x_right = inputs[1]\n\n        if x_left is None:\n            return {}\n\n        if x_right is None:\n            if isinstance(x_left, torch.Tensor):\n                if x_left.shape[1] % 2 != 0:\n                    raise ValueError(\"Cannot infer left/right branches from single tensor.\")\n                x_left, x_right = torch.chunk(x_left, 2, dim=1)\n            else:\n                raise ValueError(\"Missing x_right input.\")\n\n        feat_l, feats_l = self._half_forward(x_left)\n        feat_r, feats_r = self._half_forward(x_right)\n        total, gdm, green, f_concat = self._merge_heads(feat_l, feat_r)\n\n        out = {\n            \"total\": total,\n            \"gdm\": gdm,\n            \"green\": green,\n            \"score_feat\": f_concat,\n        }\n        if self.aux_head is not None:\n            aux_tokens = torch.cat([feats_l[\"stage2_tokens\"], feats_r[\"stage2_tokens\"]], dim=1)\n            aux_pred = self.softplus(self.aux_head(aux_tokens.mean(dim=1)))\n            out[\"aux\"] = aux_pred\n        if return_features:\n            out[\"feature_maps\"] = {\n                \"stage1_left\": feats_l.get(\"stage1_map\"),\n                \"stage1_right\": feats_r.get(\"stage1_map\"),\n                \"stage3_left\": feats_l.get(\"stage3_tokens\"),\n                \"stage3_right\": feats_r.get(\"stage3_tokens\"),\n            }\n        return out\n\n\n\nclass FiLM(nn.Module):\n    def __init__(self, feat_dim):\n        super().__init__()\n        hidden = max(32, feat_dim // 2)\n        self.mlp = nn.Sequential(\n            nn.Linear(feat_dim, hidden), nn.ReLU(inplace=True), nn.Linear(hidden, feat_dim * 2)\n        )\n\n    def forward(self, context):\n        gamma_beta = self.mlp(context)\n        return torch.chunk(gamma_beta, 2, dim=1)\n\n\nclass BaseDINO(nn.Module):\n    def __init__(self, backbone_name):\n        super().__init__()\n        self.dropout = 0.30\n        self.hidden_ratio = 0.25\n        self.grid = (2, 2)\n        self.backbone_name = backbone_name\n        \n        self.backbone = timm.create_model(backbone_name, pretrained=False, num_classes=0)\n        self.feat_dim = self.backbone.num_features\n        self.input_size = self._get_input_size(self.backbone)\n        self.combined_dim = self.feat_dim * 2\n        hidden_size = max(8, int(self.combined_dim * self.hidden_ratio))\n\n        def make_head():\n            return nn.Sequential(\n                nn.Linear(self.combined_dim, hidden_size),\n                nn.ReLU(inplace=True),\n                nn.Dropout(self.dropout),\n                nn.Linear(hidden_size, 1),\n            )\n\n        self.head_green = make_head()\n        self.head_clover = make_head()\n        self.head_dead = make_head()\n        self.softplus = nn.Softplus(beta=1.0)\n\n    def _get_input_size(self, model):\n        if hasattr(model, \"patch_embed\") and hasattr(model.patch_embed, \"img_size\"):\n            size = model.patch_embed.img_size\n            return int(size if isinstance(size, (int, float)) else size[0])\n        \n        if hasattr(model, \"img_size\"):\n            size = model.img_size\n            return int(size if isinstance(size, (int, float)) else size[0])\n        \n        cfg = getattr(model, \"default_cfg\", {}) or {}\n        input_size = cfg.get(\"input_size\", None)\n        \n        if input_size:\n            if isinstance(input_size, (tuple, list)) and len(input_size) >= 2:\n                return int(input_size[1])\n            return int(input_size if isinstance(input_size, (int, float)) else 224)\n        \n        arch = cfg.get(\"architecture\", \"\") or str(type(model))\n        return 518 if \"dinov2\" in arch.lower() or \"dinov3\" in arch.lower() else 224\n\n    def merge_features(self, left_feat, right_feat):\n        combined = torch.cat([left_feat, right_feat], dim=1)\n        green = self.softplus(self.head_green(combined))\n        clover = self.softplus(self.head_clover(combined))\n        dead = self.softplus(self.head_dead(combined))\n        gdm = green + clover\n        total = gdm + dead\n        return total, gdm, green\n\n\nclass TiledFiLMDINO(BaseDINO):\n    def __init__(self, backbone_name):\n        super().__init__(backbone_name)\n        self.film_left = FiLM(self.feat_dim)\n        self.film_right = FiLM(self.feat_dim)\n\n    def _split_dimension(self, length, parts):\n        step = length // parts\n        segments = []\n        start = 0\n        \n        for _ in range(parts - 1):\n            segments.append((start, start + step))\n            start += step\n        \n        segments.append((start, length))\n        return segments\n\n    def _extract_tile_features(self, x):\n        B, C, H, W = x.shape\n        rows, cols = self.grid\n        row_segments = self._split_dimension(H, rows)\n        col_segments = self._split_dimension(W, cols)\n        features = []\n        \n        for (rs, re) in row_segments:\n            for (cs, ce) in col_segments:\n                tile = x[:, :, rs:re, cs:ce]\n                if tile.shape[-2:] != (self.input_size, self.input_size):\n                    tile = F.interpolate(tile, size=(self.input_size, self.input_size), mode=\"bilinear\")\n                feat = self.backbone(tile)\n                features.append(feat)\n        \n        return torch.stack(features, dim=0).permute(1, 0, 2)\n\n    def _process_stream(self, x, film_layer):\n        tiles = self._extract_tile_features(x)\n        context = tiles.mean(dim=1)\n        gamma, beta = film_layer(context)\n        modulated = tiles * (1 + gamma.unsqueeze(1)) + beta.unsqueeze(1)\n        return modulated.mean(dim=1)\n\n    def forward(self, left_img, right_img):\n        left_feat = self._process_stream(left_img, self.film_left)\n        right_feat = self._process_stream(right_img, self.film_right)\n        return self.merge_features(left_feat, right_feat)\n\n\nclass BiomassDataset(Dataset):\n    def __init__(self, df, transform, img_dir):\n        self.df = df.reset_index(drop=True)\n        self.transform = transform\n        self.img_dir = img_dir\n        self.paths = self.df[\"image_path\"].values\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        filename = os.path.basename(self.paths[idx])\n        full_path = os.path.join(self.img_dir, filename)\n        \n        img = cv2.imread(full_path)\n        if img is None:\n            img = np.zeros((1000, 2000, 3), dtype=np.uint8)\n        else:\n            img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n\n        h, w, _ = img.shape\n        mid = w // 2\n        left_img = img[:, :mid]\n        right_img = img[:, mid:]\n\n        left_tensor = self.transform(image=left_img)[\"image\"]\n        right_tensor = self.transform(image=right_img)[\"image\"]\n        \n        return left_tensor, right_tensor\n\n\ndef get_tta_transforms_v4(img_size: int):\n    base = [\n        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ToTensorV2(),\n    ]\n\n    transforms = []\n    transforms.append(\n        A.Compose([\n            A.Resize(img_size, img_size, interpolation=cv2.INTER_AREA),\n            *base,\n        ])\n    )\n\n    transforms.append(\n        A.Compose([\n            A.HorizontalFlip(p=1.0),\n            A.Resize(img_size, img_size, interpolation=cv2.INTER_AREA),\n            *base,\n        ])\n    )\n\n    transforms.append(\n        A.Compose([\n            A.VerticalFlip(p=1.0),\n            A.Resize(img_size, img_size, interpolation=cv2.INTER_AREA),\n            *base,\n        ])\n    )\n\n    return transforms\n\n\ndef get_tta_transforms_mvp(img_size):\n    norm = [A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ToTensorV2()]\n    return [\n        A.Compose([A.Resize(img_size, img_size), *norm]),\n        A.Compose([A.HorizontalFlip(p=1.0), A.Resize(img_size, img_size), *norm]),\n        A.Compose([A.VerticalFlip(p=1.0), A.Resize(img_size, img_size), *norm]),\n        A.Compose([A.RandomRotate90(p=1.0), A.Resize(img_size, img_size), *norm]),\n    ]\n\n\ndef strip_module_prefix(state_dict: dict) -> dict:\n    if not state_dict:\n        return state_dict\n\n    keys = list(state_dict.keys())\n    if all(k.startswith(\"module.\") for k in keys):\n        return {k[len(\"module.\"):]: v for k, v in state_dict.items()}\n    return state_dict\n\n\ndef clean_state_dict_mvp(state_dict):\n    if not state_dict:\n        return state_dict\n    \n    cleaned_dict = {}\n    \n    for k, v in state_dict.items():\n        if k.startswith(\"module.\"):\n            k = k[7:]\n        \n        if k.startswith(\"student.\"):\n            k = k[8:]\n        \n        skip_prefixes = (\"txt_enc.\", \"img_proj.\", \"txt_film\", \"teacher.\", \"momentum_teacher.\")\n        if any(k.startswith(prefix) for prefix in skip_prefixes):\n            continue\n            \n        cleaned_dict[k] = v\n    \n    return cleaned_dict\n\n\ndef load_checkpoint_v4(path: str) -> nn.Module:\n    state = torch.load(path, map_location=\"cpu\", weights_only=False)\n    cfg_dict = state.get(\"cfg\", {})\n    dropout = cfg_dict.get(\"dropout\", CFG.dropout)\n    hidden_ratio = cfg_dict.get(\"hidden_ratio\", CFG.hidden_ratio)\n\n    model = CrossPVT_T2T_MambaDINO(dropout=dropout, hidden_ratio=hidden_ratio)\n\n    model_state = state.get(\"model_state\")\n    if model_state is None:\n        model_state = state\n\n    model_state = strip_module_prefix(model_state)\n    model.load_state_dict(model_state, strict=False)\n    model.to(CFG.device)\n    model.eval()\n\n    return model\n\n\ndef load_checkpoint_mvp(checkpoint_path):\n    if not os.path.exists(checkpoint_path):\n        return None\n    \n    try:\n        raw_state = torch.load(checkpoint_path, map_location=CFG.device, weights_only=False)\n    except Exception:\n        return None\n    \n    if isinstance(raw_state, dict):\n        if 'state_dict' in raw_state:\n            state_dict = raw_state['state_dict']\n        elif 'model' in raw_state:\n            state_dict = raw_state['model']\n        else:\n            state_dict = raw_state\n    else:\n        state_dict = raw_state\n    \n    state_dict = clean_state_dict_mvp(state_dict)\n    \n    if not state_dict:\n        return None\n    \n    # Используем те же backbone, что и в исходном рабочем коде\n    backbones = [\n        \"vit_base_patch14_reg4_dinov2\",\n        \"vit_base_patch14_reg4_dinov3\",\n        \"vit_base_patch14_dinov3\",\n    ]\n    \n    for backbone in backbones:\n        try:\n            model = TiledFiLMDINO(backbone)\n            result = model.load_state_dict(state_dict, strict=False)\n            \n            missing = [k for k in result.missing_keys if not k.startswith('backbone.pos_embed')]\n            \n            if len(missing) == 0:\n                model.to(CFG.device)\n                model.eval()\n                return model\n                \n        except Exception:\n            continue\n    \n    return None\n\n\n@torch.no_grad()\ndef predict_one_view_v4(models: List[nn.Module], loader: DataLoader) -> np.ndarray:\n    preds_list = []\n    amp_dtype = \"cuda\" if CFG.device.type == \"cuda\" else \"cpu\"\n    \n    for xl, xr in tqdm(loader, leave=False):\n        xl = xl.to(CFG.device, non_blocking=True)\n        xr = xr.to(CFG.device, non_blocking=True)\n        x_cat = torch.cat([xl, xr], dim=1)\n        \n        per_model_preds = []\n        \n        with torch.amp.autocast(amp_dtype, enabled=CFG.mixed_precision):\n            for model in models:\n                out = model(x_cat, return_features=False)\n                \n                total = out[\"total\"]\n                gdm = out[\"gdm\"]\n                green = out[\"green\"]\n                \n                dead = total - gdm\n                clover = gdm - green\n                five = torch.cat([green, dead, clover, gdm, total], dim=1)\n                \n                per_model_preds.append(five.float().cpu())\n        \n        stacked = torch.mean(torch.stack(per_model_preds, dim=0), dim=0)\n        preds_list.append(stacked.numpy())\n    \n    return np.concatenate(preds_list, axis=0)\n\n\n@torch.no_grad()\ndef predict_one_view_mvp(models: List[nn.Module], loader: DataLoader) -> np.ndarray:\n    preds = []\n    use_amp = CFG.device.type == \"cuda\"\n    \n    for left_imgs, right_imgs in tqdm(loader, leave=False):\n        left_imgs = left_imgs.to(CFG.device, non_blocking=True)\n        right_imgs = right_imgs.to(CFG.device, non_blocking=True)\n        batch_preds = []\n        \n        with torch.amp.autocast(\"cuda\", enabled=use_amp):\n            for model in models:\n                total, gdm, green = model(left_imgs, right_imgs)\n                dead = torch.clamp(total - gdm, min=0.0)\n                clover = torch.clamp(gdm - green, min=0.0)\n                pred = torch.cat([green, dead, clover, gdm, total], dim=1)\n                batch_preds.append(pred.clamp(0.05, 400.0).cpu())\n        \n        preds.append(torch.stack(batch_preds).mean(dim=0).numpy())\n    \n    return np.concatenate(preds, axis=0)\n\n\ndef run_inference_v4(test_df: pd.DataFrame, image_dir: str) -> np.ndarray:\n    models = []\n    \n    for fold in range(CFG.n_folds_a):\n        ckpt_path = CFG.ckpt_pattern_fold_x_a.format(fold=fold)\n        if not os.path.exists(ckpt_path):\n            ckpt_path = CFG.ckpt_pattern_foldx_a.format(fold=fold)\n        \n        if not os.path.exists(ckpt_path):\n            continue\n        \n        model = load_checkpoint_v4(ckpt_path)\n        models.append(model)\n    \n    if not models:\n        raise RuntimeError(\"No V4 models loaded!\")\n    \n    input_size = getattr(models[0], \"input_res\", 518)\n    \n    if CFG.use_tta:\n        tta_transforms = get_tta_transforms_v4(input_size)\n        per_view_preds = []\n        \n        for transform in tta_transforms:\n            ds = BiomassDataset(test_df, transform, image_dir)\n            dl = DataLoader(\n                ds,\n                batch_size=CFG.batch_size,\n                shuffle=False,\n                num_workers=CFG.num_workers,\n                pin_memory=True,\n            )\n            view_pred = predict_one_view_v4(models, dl)\n            per_view_preds.append(view_pred)\n        \n        final_pred = np.mean(per_view_preds, axis=0)\n    else:\n        transform = get_tta_transforms_v4(input_size)[0]\n        ds = BiomassDataset(test_df, transform, image_dir)\n        dl = DataLoader(\n            ds,\n            batch_size=CFG.batch_size,\n            shuffle=False,\n            num_workers=CFG.num_workers,\n            pin_memory=True,\n        )\n        final_pred = predict_one_view_v4(models, dl)\n    \n    return final_pred\n\n\ndef run_inference_mvp(checkpoint_paths, df, img_dir) -> np.ndarray:\n    models = []\n    \n    for ckpt_path in checkpoint_paths:\n        model = load_checkpoint_mvp(ckpt_path)\n        if model is not None:\n            models.append(model)\n    \n    if not models:\n        raise ValueError(\"No MVP models loaded!\")\n    \n    input_size = models[0].input_size\n    \n    tta_preds = []\n    for transform in get_tta_transforms_mvp(input_size):\n        ds = BiomassDataset(df, transform, img_dir)\n        dl = DataLoader(ds, batch_size=CFG.batch_size, shuffle=False, num_workers=0, pin_memory=True)\n        tta_preds.append(predict_one_view_mvp(models, dl))\n    \n    return np.mean(tta_preds, axis=0)\n\n\ndef create_submission(final_pred: np.ndarray, test_long: pd.DataFrame, test_unique: pd.DataFrame) -> pd.DataFrame:\n    green = final_pred[:, 0]\n    dead = final_pred[:, 1]\n    clover = final_pred[:, 2]\n    gdm = final_pred[:, 3]\n    total = final_pred[:, 4]\n\n    def clean(x):\n        x = np.nan_to_num(x, nan=0.0, posinf=0.0, neginf=0.0)\n        return np.maximum(0, x)\n\n    green, dead, clover, gdm, total = map(clean, [green, dead, clover, gdm, total])\n\n    wide = pd.DataFrame(\n        {\n            \"image_path\": test_unique[\"image_path\"],\n            \"Dry_Green_g\": green,\n            \"Dry_Dead_g\": dead,\n            \"Dry_Clover_g\": clover,\n            \"GDM_g\": gdm,\n            \"Dry_Total_g\": total,\n        }\n    )\n\n    long_preds = wide.melt(\n        id_vars=[\"image_path\"],\n        value_vars=CFG.all_target_cols,\n        var_name=\"target_name\",\n        value_name=\"target\",\n    )\n\n    sub = pd.merge(\n        test_long[[\"sample_id\", \"image_path\", \"target_name\"]],\n        long_preds,\n        on=[\"image_path\", \"target_name\"],\n        how=\"left\",\n    )[[\"sample_id\", \"target\"]]\n\n    sub[\"target\"] = np.nan_to_num(sub[\"target\"], nan=0.0, posinf=0.0, neginf=0.0)\n    sub.to_csv(CFG.submission_file, index=False)\n    \n    return sub\n\n\ntest_df = pd.read_csv(CFG.test_csv)\nunique_df = test_df.drop_duplicates(subset=[\"image_path\"]).reset_index(drop=True)\n\ntry:\n    pred_v4 = run_inference_v4(unique_df, CFG.test_image_dir)\nexcept Exception as e:\n    print(f\"V4 inference failed: {e}\")\n    pred_v4 = None\n\ntry:\n    pred_mvp_a = run_inference_mvp(CFG.ckpts_a, unique_df, CFG.test_image_dir)\n    pred_mvp_b = run_inference_mvp(CFG.ckpts_b, unique_df, CFG.test_image_dir)\n    pred_mvp = CFG.weight_a * pred_mvp_a + CFG.weight_b * pred_mvp_b\nexcept Exception as e:\n    print(f\"MVP inference failed: {e}\")\n    pred_mvp = None\n    \nfinal_pred = (pred_v4 + pred_mvp) / 2.0\n    \nsubmission = create_submission(final_pred, test_df, unique_df)\nsubmission.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:22:58.435569Z","iopub.execute_input":"2025-12-17T02:22:58.435820Z","iopub.status.idle":"2025-12-17T02:25:18.613601Z","shell.execute_reply.started":"2025-12-17T02:22:58.435796Z","shell.execute_reply":"2025-12-17T02:25:18.612883Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Ensemble","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:25:18.614416Z","iopub.execute_input":"2025-12-17T02:25:18.614887Z","iopub.status.idle":"2025-12-17T02:25:18.618290Z","shell.execute_reply.started":"2025-12-17T02:25:18.614869Z","shell.execute_reply":"2025-12-17T02:25:18.617719Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"ls","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:25:18.618952Z","iopub.execute_input":"2025-12-17T02:25:18.619111Z","iopub.status.idle":"2025-12-17T02:25:18.934875Z","shell.execute_reply.started":"2025-12-17T02:25:18.619098Z","shell.execute_reply":"2025-12-17T02:25:18.934065Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df1 = pd.read_csv('submission_p1.csv')\ndf2 = pd.read_csv('submission_p2.csv')\ndf3 = pd.read_csv('submission.csv')\n\ndf1['pred1'] = df1['target']","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:36:25.072810Z","iopub.execute_input":"2025-12-17T02:36:25.073512Z","iopub.status.idle":"2025-12-17T02:36:25.084771Z","shell.execute_reply.started":"2025-12-17T02:36:25.073480Z","shell.execute_reply":"2025-12-17T02:36:25.084122Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df1.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:36:26.830992Z","iopub.execute_input":"2025-12-17T02:36:26.831324Z","iopub.status.idle":"2025-12-17T02:36:26.840583Z","shell.execute_reply.started":"2025-12-17T02:36:26.831275Z","shell.execute_reply":"2025-12-17T02:36:26.839920Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df2.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:36:27.138963Z","iopub.execute_input":"2025-12-17T02:36:27.139714Z","iopub.status.idle":"2025-12-17T02:36:27.146812Z","shell.execute_reply.started":"2025-12-17T02:36:27.139690Z","shell.execute_reply":"2025-12-17T02:36:27.146155Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df3.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:36:28.563909Z","iopub.execute_input":"2025-12-17T02:36:28.564170Z","iopub.status.idle":"2025-12-17T02:36:28.571666Z","shell.execute_reply.started":"2025-12-17T02:36:28.564141Z","shell.execute_reply":"2025-12-17T02:36:28.570964Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## simple avg","metadata":{}},{"cell_type":"code","source":"df1['pred2'] = df1.sample_id.map(df2.set_index('sample_id').target)\ndf1['pred3'] = df1.sample_id.map(df3.set_index('sample_id').target)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:36:48.625155Z","iopub.execute_input":"2025-12-17T02:36:48.625696Z","iopub.status.idle":"2025-12-17T02:36:48.632767Z","shell.execute_reply.started":"2025-12-17T02:36:48.625671Z","shell.execute_reply":"2025-12-17T02:36:48.631873Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df1['target'] = 0.25 * df1.pred1 + 0.25 * df1.pred2 + 0.5 * df1.pred3","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:36:56.178780Z","iopub.execute_input":"2025-12-17T02:36:56.179556Z","iopub.status.idle":"2025-12-17T02:36:56.183928Z","shell.execute_reply.started":"2025-12-17T02:36:56.179532Z","shell.execute_reply":"2025-12-17T02:36:56.183269Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df1.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:36:56.454475Z","iopub.execute_input":"2025-12-17T02:36:56.454762Z","iopub.status.idle":"2025-12-17T02:36:56.463984Z","shell.execute_reply.started":"2025-12-17T02:36:56.454741Z","shell.execute_reply":"2025-12-17T02:36:56.463187Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cols = ['sample_id', 'target']\ndf1[cols].to_csv('submission.csv', index=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:37:10.555834Z","iopub.execute_input":"2025-12-17T02:37:10.556419Z","iopub.status.idle":"2025-12-17T02:37:10.562706Z","shell.execute_reply.started":"2025-12-17T02:37:10.556394Z","shell.execute_reply":"2025-12-17T02:37:10.561873Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!head submission.csv","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T02:37:10.725625Z","iopub.execute_input":"2025-12-17T02:37:10.725906Z","iopub.status.idle":"2025-12-17T02:37:11.019441Z","shell.execute_reply.started":"2025-12-17T02:37:10.725884Z","shell.execute_reply":"2025-12-17T02:37:11.018490Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}