{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"},{"sourceId":680375,"sourceType":"modelInstanceVersion","modelInstanceId":516190,"modelId":530846}],"dockerImageVersionId":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-27T13:36:43.273961Z","iopub.execute_input":"2025-12-27T13:36:43.274350Z","iopub.status.idle":"2025-12-27T13:36:43.472919Z","shell.execute_reply.started":"2025-12-27T13:36:43.274309Z","shell.execute_reply":"2025-12-27T13:36:43.472134Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"TRAIN = True\n\nimport os\n\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\n%matplotlib inline\n\n# DEBUG 模式开关\n# 设置为 True 进行快速测试（只跑 2 个 Fold，每个 Fold 5 个 Epoch）\n# 设置为 False 进行全量训练（跑 5 个 Fold，每个 Fold 100 个 Epoch）\nDEBUG =False\n\n\nLOCAL = os.environ.get('KAGGLE_KERNEL_RUN_TYPE', '') == ''\n\n\n\nfrom sklearn.model_selection import KFold, GroupKFold, StratifiedGroupKFold\n\nfrom PIL import Image\nfrom tqdm.auto import tqdm\ntqdm.pandas()\n\nDATA_ROOT = '../input/' if LOCAL else '/kaggle/input/csiro-biomass/'\n\ntrain_df = pd.read_csv(f'{DATA_ROOT}/train.csv')\ntrain_df.head()\n\ntrain_df[['sample_id_prefix', 'sample_id_suffix']] = train_df.sample_id.str.split('__', expand=True)\n\n(train_df.sample_id_suffix == train_df.target_name).all()\n\n\ncols = ['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()\n\nagg_train_df['image_size'] = agg_train_df.image.apply(lambda x: x.size)\nagg_train_df['image_size'].value_counts()\n\nnp.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()\n\nnp.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()\n\nplt.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')\n\nNFOLD = 5\nkfold = KFold(n_splits=NFOLD, shuffle=True, random_state=42)\n\nagg_train_df['fold'] = None\nfor i, (trn_idx, val_idx) in enumerate(kfold.split(agg_train_df.index)):\n    agg_train_df.loc[val_idx, 'fold'] = i\n\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\n\nclass RegressionDataset(Dataset):\n    def __init__(self, data, transform=None):\n        self.data = data\n        self.transform = transform\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        \n        if self.transform:\n            image = self.transform(image)\n            \n        targets = torch.tensor([item['Dry_Green_g'], item['Dry_Clover_g'], item['Dry_Dead_g']], dtype=torch.float32)\n        return image, targets\n\ndef create_dataloader(data, target_image_size=(256, 256), batch_size=32, shuffle=True, aug=True):    \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.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        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\n\n\nimport os\nfrom pathlib import Path\nimport timm\nimport torch\nimport torch.nn as nn\n\nMODEL_NAME = 'efficientnet_b2'\nNUM_CLASSES = 3\nCHECKPOINT = r'/kaggle/input/mo1/pytorch/default/1/model.safetensors'\n\ndef _load_local_checkpoint(m, ckpt_path):\n    if not ckpt_path or not Path(ckpt_path).exists():\n        return\n    try:\n        import safetensors.torch as st\n        sd = st.load_file(ckpt_path)\n    except Exception:\n        sd = torch.load(ckpt_path, map_location='cpu')\n    sd.pop('classifier.weight', None)\n    sd.pop('classifier.bias', None)\n    m.load_state_dict(sd, strict=False)\n\ndef get_model():\n    m = timm.create_model(MODEL_NAME, pretrained=False, num_classes=NUM_CLASSES)\n    _load_local_checkpoint(m, CHECKPOINT)\n    return m\n\n# Get config from a dummy timm model to get input size, mean, std\ndummy_m = timm.create_model(MODEL_NAME, pretrained=False)\ncfg = getattr(dummy_m, 'pretrained_cfg', None) or getattr(dummy_m, 'default_cfg', {})\n# Override input size to 512x512 (4x area of 256x256)\nTARGET_IMAGE_SIZE = (512, 512)\nMEAN = tuple(cfg.get('mean', (0.485, 0.456, 0.406)))\nSTD = tuple(cfg.get('std', (0.229, 0.224, 0.225)))\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.optim import AdamW\n\n# ======== 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\n\ndef 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\n\nclass BiomassLoss(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.base_loss = nn.SmoothL1Loss()\n        \n    def forward(self, outputs, targets):\n        # outputs/targets shape: (Batch, 3) -> [Green, Clover, Dead]\n        \n        # 1. Component Losses\n        loss_green = self.base_loss(outputs[:, 0], targets[:, 0])\n        loss_clover = self.base_loss(outputs[:, 1], targets[:, 1])\n        loss_dead = self.base_loss(outputs[:, 2], targets[:, 2])\n        \n        # 2. Derived Losses (Critical for competition metric)\n        # GDM = Green + Clover\n        pred_gdm = outputs[:, 0] + outputs[:, 1]\n        target_gdm = targets[:, 0] + targets[:, 1]\n        loss_gdm = self.base_loss(pred_gdm, target_gdm)\n        \n        # Total = Green + Clover + Dead\n        pred_total = outputs.sum(dim=1)\n        target_total = targets.sum(dim=1)\n        loss_total = self.base_loss(pred_total, target_total)\n        \n        # Weighted Sum (Weights inspired by competition metric: Total=0.5, GDM=0.2, Others=0.1)\n        # We amplify the importance of Total and GDM\n        final_loss = (\n            1.0 * loss_green + \n            1.0 * loss_clover + \n            1.0 * loss_dead + \n            0.5 * loss_gdm +    # Extra penalty for GDM error\n            1.0 * loss_total    # Strong penalty for Total error\n        )\n        \n        return final_loss\n\ndef train_epoch(model, dataloader, criterion, optimizer, device, accumulation_steps=1):\n    model.train()\n    total_loss = 0\n    optimizer.zero_grad() # Initialize gradients\n\n    for i, (images, targets) in enumerate(dataloader):\n        images = images.to(device)\n        targets = targets.to(device)\n\n        # Forward pass\n        outputs = model(images)\n        loss = criterion(outputs, targets)\n        \n        # Normalize loss for gradient accumulation\n        loss = loss / accumulation_steps\n        loss.backward()\n\n        # Update weights every accumulation_steps\n        if (i + 1) % accumulation_steps == 0:\n            optimizer.step()\n            optimizer.zero_grad()\n\n        # Multiply back to report correct loss value\n        total_loss += loss.item() * accumulation_steps\n\n    # Handle remaining gradients if dataloader length is not divisible by accumulation_steps\n    if len(dataloader) % accumulation_steps != 0:\n        optimizer.step()\n        optimizer.zero_grad()\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 = targets.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    weighted_r2, r2_scores = calc_metric(outputs, targets)\n\n    return total_loss / len(dataloader), weighted_r2, r2_scores\n\ndef train_fold(data, fold):\n    device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')\n    \n    # Hyperparameters\n    # Gradient Accumulation Configuration\n    # Effective Batch Size = real_batch_size * accumulation_steps\n    # Example: 8 * 4 = 32\n    real_batch_size = 8   # Increased to 8 for 16GB VRAM\n    accumulation_steps = 4 \n    \n    lr = 1e-3\n    patience = 10\n    num_epochs = 5 if DEBUG else 100\n\n    # model, loss, optimizer\n    model = get_model()\n    model.to(device)\n\n    criterion = BiomassLoss()  # Custom Compound Loss\n    optimizer = AdamW(model.parameters(), lr=lr)\n    \n    scheduler = optim.lr_scheduler.ReduceLROnPlateau(\n        optimizer, mode='max', factor=0.5, patience=patience//2\n    )\n    \n    # data\n    train_loader = create_dataloader(data[data.fold != fold], TARGET_IMAGE_SIZE, real_batch_size, shuffle=True, aug=True)\n    val_loader = create_dataloader(data[data.fold == fold], TARGET_IMAGE_SIZE, real_batch_size, shuffle=False, aug=False)\n\n    # Training loop\n    history = []\n    best_score = -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, device, accumulation_steps)\n        val_loss, weighted_r2, r2_scores = validate(model, val_loader, criterion, device)\n        \n        scheduler.step(weighted_r2)  # 更新学习率\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            'weighted_r2': weighted_r2,\n            'r2_scores': r2_scores,\n            'lr': optimizer.param_groups[0]['lr'],\n        })\n        \n        # 早停\n        if weighted_r2 > best_score:\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\n\nOUTPUT_DIR = 'trained_models/'\nif not os.path.exists(OUTPUT_DIR):\n    os.mkdir(OUTPUT_DIR)\n\nif 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=(18, 4))\n\n        plt.subplot(1, 3, 1)\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, 2)\n        plt.title('weighted_r2')\n        plt.plot(history.weighted_r2)\n\n        plt.subplot(1, 3, 3)\n        plt.title('Learning Rate')\n        plt.plot(history.lr)\n        plt.show()\n    \n    print('Avg CV:', np.mean(all_best_score))\n\nimport os\nfrom pathlib import Path\n\ndef get_lastest_saved_models():\n    model_root = '/kaggle/input/csiro-simple-output/pytorch/default/'\n    \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/'\n\nSAVED_MODELS = './trained_models/' if TRAIN else get_lastest_saved_models()\n\ndef predict_tta(model, dataloader, device):\n    model.to(device)\n    model.eval()\n\n    all_outputs = []\n    with torch.no_grad():\n        for images, targets in dataloader:\n            images = images.to(device)\n            \n            # TTA: Original\n            pred_1 = model(images)\n            \n            # TTA: Horizontal Flip\n            pred_2 = model(torch.flip(images, dims=[3]))\n            \n            # TTA: Vertical Flip\n            pred_3 = model(torch.flip(images, dims=[2]))\n            \n            # TTA: Both Flips\n            pred_4 = model(torch.flip(images, dims=[2, 3]))\n            \n            # Average predictions\n            outputs = (pred_1 + pred_2 + pred_3 + pred_4) / 4.0\n            \n            all_outputs.append(outputs.detach().cpu())\n    \n    outputs = torch.cat(all_outputs).numpy()\n    return outputs\n\ndef kfold_predict(dataloader):\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        # Recreate the standard model structure\n        model = timm.create_model(MODEL_NAME, pretrained=False, num_classes=3)\n        model.load_state_dict(torch.load(model_file))\n\n        preds = predict_tta(model, dataloader, device)\n        all_preds.append(preds)\n\n    avg_preds = np.mean(all_preds, axis=0)\n    return avg_preds\n\ntest_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)\n\ncols = ['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\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()\n\n# 创建测试集 DataLoader\ntest_loader = create_dataloader(agg_test_df, TARGET_IMAGE_SIZE, 32, shuffle=False, aug=False)\n\n# 进行 K-Fold 集成预测\npreds = kfold_predict(test_loader)\n\n# 将预测结果填回 DataFrame\nagg_test_df[['Dry_Green_g', 'Dry_Clover_g', 'Dry_Dead_g']] = preds\n# 计算衍生指标\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\n\nagg_test_df.head()\n\n# 准备提交格式\ncols = ['Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'Dry_Total_g', 'GDM_g']\n# 将宽格式（一行一个样本）转换为长格式（一行一个目标值）\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\n# 构造提交所需的 sample_id (例如: 1234__Dry_Green_g)\nsub_df['sample_id'] = sub_df.sample_id_prefix + '__' + sub_df.target_name\n\n# 保存为 CSV\ncols = ['sample_id', 'target']\nsub_df[cols].to_csv('submission.csv', index=False)\n\n# 查看前几行\n!head submission.csv","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-27T13:36:43.474608Z","iopub.execute_input":"2025-12-27T13:36:43.474847Z","iopub.status.idle":"2025-12-27T13:59:15.162536Z","shell.execute_reply.started":"2025-12-27T13:36:43.474823Z","shell.execute_reply":"2025-12-27T13:59:15.161786Z"}},"outputs":[],"execution_count":null}]}