{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"},{"sourceId":278594283,"sourceType":"kernelVersion"}],"dockerImageVersionId":31192,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"CSIRO Pasture Biomass Prediction\n1. コア戦略：重要成分の予測\n本コンペでは5つのバイオマス量を予測する必要がある。\nEDA の結果、以下の線形依存関係が確認された：\nDry_Total_g ≈ Dry_Green_g + Dry_Dead_g + Dry_Clover_g\nGDM_g ≈ Dry_Green_g + Dry_Clover_g\n冗長性を避け、かつスコア重みの高い 3 つのターゲットのみを学習する：\nDry_Total_g（スコア 50%）\nGDM_g（20%）\nDry_Green_g（10%）\n残りの2つは推論時に算出：\npred_Clover = max(0, pred_GDM - pred_Green)\npred_Dead   = max(0, pred_Total - pred_GDM)\n2. データ処理と K-Fold 戦略\n画像構造\n元画像は 2000 × 1000 px の横長画像。\nTwo-Stream（左右ストリーム）処理\n解像度を下げると細部（クローバー葉など）が失われるため：\n画像を左右に 1000×1000 で分割\nそれぞれを 768×768 にリサイズ\nLeft / Right の2ストリームとしてモデルに入力\n5-Fold Cross-Validation\nデータ数（357枚）が少ないため 5-Fold を採用。\nGroupKFold（Sampling_Date 単位で分割）\nデータリーク防止のため：\n同じ撮影日の画像はすべて同一 Fold に属させる\nValidation は「見たことのない撮影日」で評価\n3. モデルアーキテクチャ：Two-Stream × Multi-Head\n共有バックボーン\ntimm の ConvNeXt（例：convnext_tiny）\nImageNet 事前学習済み\nnum_classes=0 とし、特徴抽出器として利用\n2 つのストリーム\nimg_left  →  backbone → features_left\nimg_right →  backbone → features_right\n特徴量の融合\ncombined = torch.cat([features_left, features_right], dim=1)\n3つのヘッド（Multi-Head）で出力\nhead_total\nhead_gdm\nhead_green\n各ターゲットに特化した MLP。\n4. データ拡張\n左右の画像パッチに 独立に Augmentation を適用：\nHorizontalFlip (0.5)\nVerticalFlip (0.5)\nRandomRotate90 (0.5)\nColorJitter\nNormalize\nResize(768, 768)\n→ 小さなデータセットでも多様性を大幅に増やせる。\n5. 損失関数：Weighted SmoothL1Loss\nSmoothL1（Huber）を使用し、安定した学習を実現。\nコンペの評価重みに合わせた損失：\nLoss=0.5⋅LossTotal+0.2⋅LossGDM+0.1⋅LossGreen\n6. 学習戦略：2段階ファインチューニング\nStage 1：バックボーン凍結\nEpoch 1〜5\nバックボーンは凍結\nヘッドのみ学習\n学習率：1e-4\nStage 2：バックボーン解凍（全体学習）\nEpoch 6〜20\nバックボーンを解凍\n全層を低い Learning Rate（1e-5）で微調整\nモデル保存\nLoss ではなく R² で最良を判定\nValidation R² が最高になったタイミングを保存\n過学習で R² が下がる後半 epoch を無視できる","metadata":{}},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nimport timm\nimport cv2\nfrom tqdm import tqdm\nimport gc\n\n#-----------------------------------------------------------\n# 設定（configuration）\n#-----------------------------------------------------------\nclass CFG:\n    base_path      = '/kaggle/input/csiro-biomass'\n    test_csv       = os.path.join(base_path, 'test.csv')\n    test_image_dir = os.path.join(base_path, 'test')\n\n    model_dir = '/kaggle/input/pasture-biomass-prediction-with-two-stream-convnex'\n\n    submission_file = 'submission.csv'\n\n    model_name = 'convnext_tiny'\n    image_size = 768\n\n    device = torch.device('cuda' if torch.cuda.is_available()\n                                 else 'cpu')\n    batch_size  = 1\n    num_workers = 1\n    n_folds     = 5\n\n    target_cols = ['Dry_Total_g', \n                   'GDM_g', \n                   'Dry_Green_g']\n\n    all_target_cols = ['Dry_Green_g', \n                       'Dry_Dead_g', \n                       'Dry_Clover_g',\n                       'GDM_g',\n                       'Dry_Total_g']\n\nprint(f\"使用するデバイス: {CFG.device}\")\nprint(f\"モデルのバックボーン: {CFG.model_name}\")\nprint(f\"推論時の画像サイズ: {CFG.image_size}x{CFG.image_size}\")\n\n\n#-----------------------------------------------------------\n# Augmentations（検証用にのみ使用）\n#-----------------------------------------------------------\nfrom albumentations import(\n    Compose,\n    Resize,\n    Normalize,\n    HorizontalFlip,\n    VerticalFlip\n)\n\ndef generate_tta_transforms():\n    \"\"\"\n    TTA（Test-Time Augmentation）のための 複数の変換パイプラインのリスト を返す。\n    各パイプラインは、画像の異なる “ビュー（見え方）” を生成する。\n    \"\"\"\n    base_transforms = [\n        Normalize(\n            mean = [0.485, 0.456, 0.406],\n            std  = [0.229, 0.224, 0.225]\n        ),\n        ToTensorV2()\n    ]\n\n    # -----------------\n    # View 1: 元画像（Resize + Normalize のみ）\n    # -----------------\n    original_view = Compose([\n        Resize(CFG.image_size, CFG.image_size),\n        *base_transforms\n    ])\n\n    # -----------------\n    # View 2: 水平反転（HFlip）\n    # -----------------\n    hflip_view = Compose([\n        HorizontalFlip(p=1.0),\n        Resize(CFG.image_size, CFG.image_size),\n        *base_transforms\n    ])\n\n    # -----------------\n    # View 3: 垂直反転（VFlip）\n    # -----------------\n    vflip_view = Compose([\n        VerticalFlip(p=1.0),\n        Resize(CFG.image_size, CFG.image_size),\n        *base_transforms\n    ])\n\n    return [original_view, hflip_view, vflip_view]\n\nprint(\"generate_tta_transforms() 関数を定義しました。\")\n\n\nclass TestBiomassDataset(Dataset):\n    \"\"\"\n    テスト画像用のカスタム Dataset（“二つのストリーム”戦略）。\n    TTA のために、特定の transform パイプラインを受け取れるように改修したもの。\n    \"\"\"\n    def __init__(self, df, transform_pipeline, image_dir):\n        self.df = df\n        self.transforms  = transform_pipeline\n        self.image_dir   = image_dir\n        self.image_paths = df['image_path'].values\n\n    def __len__(self):\n        return len(self.df)\n    \n    def __getitem__(self, idx):\n        image_path_suffix = self.image_paths[idx]\n\n        filename  = os.path.basename(image_path_suffix)\n        full_path = os.path.join(self.image_dir, filename)\n\n        image = cv2.imread(full_path)\n        if image is None:\n            print(f\"警告: 画像を読み込めませんでした: {full_path}。黒画像を返します。\")\n            image = np.zeros((1000, 2000, 3), dtype=np.unit8)\n\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n\n        height, width, _ = image.shape\n        mid_point = width // 2\n        image_left = image[:, :mid_point]\n        image_right = image[:, mid_point:]\n\n        image_left_tensor = self.transforms(image=image_left)['image']\n        image_right_tensor = self.transforms(image=image_right)['image']\n\n        return image_left_tensor, image_right_tensor\n\n#-----------------------------------------------------------\n# 4. モデルアーキテクチャ（trainファイルからコピー）\n#-----------------------------------------------------------\nclass BiomassModel(nn.Module):\n    \"\"\"\n    モデルアーキテクチャ（2ストリーム・3出力）\n    ※ トレーニング用ファイルと完全に同一である必要があります。\n    \"\"\"\n    def __init__(self, model_name, pretrained, n_targets=3):\n        super(BiomassModel, self).__init__()\n\n        self.backbone = timm.create_model(\n            model_name,\n            pretrained = pretrained,\n            num_classes = 0,\n            global_pool = 'avg'\n        )\n\n        self.n_features = self.backbone.num_features\n        self.n_combined_features = self.n_features * 2\n\n        self.head_total = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(self.n_combined_features // 2, 1)\n        )\n        self.head_gdm = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(self.n_combined_features // 2, 1)\n        )        \n        self.head_green = nn.Sequential(\n            nn.Linear(self.n_combined_features, self.n_combined_features // 2),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(self.n_combined_features // 2, 1)\n        )\n\n    def forward(self, image_left, image_right):\n        features_left  = self.backbone(image_left)\n        features_right = self.backbone(image_right)\n        combined = torch.cat([features_left, features_right], dim=1)\n\n        out_total = self.head_total(combined)\n        out_dgm   = self.head_gdm(combined)\n        out_green = self.head_green(combined)\n\n        return out_total, out_dgm, out_green\n\ndef predict_one_view(models_list, test_loader, device):\n    view_preds_3 = {'total':[],'gdm':[], 'green':[]}\n\n    with torch.no_grad():\n        for (image_left, image_right) in tqdm(test_loader, desc='PredictingView', leave=False):\n            image_left  = image_left.to(device)\n            image_right = image_right.to(device)\n\n            batch_preds_3_folds = {'total':[], 'gdm':[], 'green':[]}\n\n            for model in models_list:\n                pred_total, pred_gdm, pred_green = model(image_left, image_right)\n                batch_preds_3_folds['total'].append(pred_total.cpu())\n                batch_preds_3_folds['gdm'].append(pred_gdm.cpu())\n                batch_preds_3_folds['green'].append(pred_green.cpu())\n\n            avg_pred_total = torch.mean(torch.stack(batch_preds_3_folds['total']), dim=0)\n            avg_pred_gdm   = torch.mean(torch.stack(batch_preds_3_folds['gdm']), dim=0)\n            avg_pred_green = torch.mean(torch.stack(batch_preds_3_folds['green']), dim=0)\n\n            view_preds_3['total'].append(avg_pred_total.numpy())\n            view_preds_3['gdm'].append(avg_pred_gdm.numpy())\n            view_preds_3['green'].append(avg_pred_green.numpy())\n\n    preds_np = {\n        'total':np.concatenate(view_preds_3['total']).flatten(),\n        'gdm':np.concatenate(view_preds_3['gdm']).flatten(),\n        'green':np.concatenate(view_preds_3['green']).flatten()\n     }\n    return preds_np\n\ndef run_inference_with_tta():\n    print(f\"\\n{'='*50}\")\n    print(f\"🚀 TTA を使った推論を開始します 🚀\")\n    print(f\"{'='*50}\")\n\n    print(f\"{CFG.test_csv} を読み込んでいます...\")\n    try:\n        test_df_long = pd.read_csv(CFG.test_csv)\n        test_df_unique = test_df_long.drop_duplicates(subset=['image_path']).reset_index(drop=True)\n        print(f\"{len(test_df_unique)} 枚のユニークなテスト画像を見つけました。\")\n    except FileNotFoundError:\n        print(f\"エラー: {CFG.test_csv} が見つかりません\")\n        return None, None, None\n\n    print(\"\\n学習済みの 5 つのモデルを読み込んでいます…\")\n\n    models_list = []\n    for fold in range(CFG.n_folds):\n        model_path = os.path.join(CFG.model_dir, f'best_model_fold{fold}.pth')\n        if not os.path.exists(model_path):\n            print(f\"エラー: モデルファイルが見つかりません: {model_path}\")\n            return None, None, None\n        model = BiomassModel(CFG.model_name, pretrained=False)\n        try:\n            model.load_state_dict(torch.load(model_path, map_location=CFG.device))\n        except RuntimeError:\n            state_dict = torch.load(model_path, map_location=CFG.device)\n            from collections import orderddict\n            new_state_dict = ordereddict()\n            for k, v in state_dict.items():\n                name = k.replace('module.', '')\n                new_state_dict[name] = v\n            model.load_state_dict(new_state_dict)\n        model.eval()\n        model.to(CFG.device)\n        models_list.append(model)\n    print(f\"✓ {len(models_list)} 個のモデルを正常に読み込みました。\")\n\n    tta_transforms = generate_tta_transforms()\n    print(f\"\\n{len(tta_transforms)} 個の tta ビューで予測を開始します...\")\n\n    all_tta_view_preds = []\n\n    for i, tta_transform in enumerate(tta_transforms):\n        print(f\"--- tta ビュー {i+1}/{len(tta_transforms)} を実行中 ---\")\n\n        test_dataset = TestBiomassDataset(\n            df = test_df_unique,\n            transform_pipeline = tta_transform,\n\n            image_dir = CFG.test_image_dir\n        )\n        test_loader = DataLoader(\n            test_dataset,\n            batch_size = CFG.batch_size,\n            shuffle = False,\n            num_workers = CFG.num_workers,\n            pin_memory = True\n        )\n\n        view_preds_np = predict_one_view(models_list, test_loader, CFG.device)\n        all_tta_view_preds.append(view_preds_np)\n        print(f\"✓ tta ビュー {i+1} を完了しました\")\n\n    print(\"\\n各 tta ビューの結果をアンサンブルしています...\")\n    final_ensembled_preds = {\n        'total':np.mean([d['total'] for d in all_tta_view_preds], axis=0),\n        'gdm':np.mean([d['gdm'] for d in all_tta_view_preds], axis=0),\n        'green':np.mean([d['green'] for d in all_tta_view_preds], axis=0),\n    }\n    print(\"✓ 予測が完了しました。\")\n\n    del models_list, test_loader, test_dataset\n    gc.collect()\n    torch.cuda.empty_cache()\n\n    return final_ensembled_preds, test_df_long, test_df_unique\n\ndef create_submission(preds_np, test_df_long, test_df_unique):\n    if preds_np is None:\n        print(\"上のエラーにより、submission の作成をスキップします。\")\n        return\n    print(\"\\n後処理を行い、submission ファイルを作成しています...\")\n\n    pred_total_final = preds_np['total']\n    pred_gdm_final   = preds_np['gdm']\n    pred_green_final = preds_np['green']\n\n    pred_clover_final = np.maximum(0, pred_gdm_final - pred_green_final)\n    pred_dead_final   = np.maximum(0, pred_total_final - pred_gdm_final)\n\n    pred_wide_df = pd.DataFrame({\n        'image_path':test_df_unique['image_path'],\n        'Dry_Green_g':pred_green_final,\n        'Dry_Dead_g':pred_dead_final,\n        'Dry_Clover_g':pred_clover_final,\n        'GDM_g':pred_gdm_final,\n        'Dry_Total_g':pred_total_final\n    })\n    preds_long_df = pred_wide_df.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    submission_df = pd.merge(\n        test_df_long[['sample_id', 'image_path', 'target_name']],\n        preds_long_df,\n        on = ['image_path', 'target_name'],\n        how = 'left'\n    )\n\n    submission_df = submission_df[['sample_id', 'target']]\n\n    submission_df.to_csv(CFG.submission_file, index=False)\n\n    print(f\"\\n🎉 完了！ submission ファイルを保存しました: {CFG.submission_file}\")\n    print(\"--- submission ファイルの先頭 5 行 ---\")\n    print(submission_df.head())\n\n    print(\"\\n--- submission ファイルの末尾 5 行 ---\")\n    print(submission_df.tail())\n\nif __name__ == \"__main__\":\n    all_preds_np, df_long, df_unique = run_inference_with_tta()\n\n    create_submission(all_preds_np, df_long, df_unique)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-17T11:41:03.573985Z","iopub.execute_input":"2025-11-17T11:41:03.574779Z","iopub.status.idle":"2025-11-17T11:41:09.121049Z","shell.execute_reply.started":"2025-11-17T11:41:03.574755Z","shell.execute_reply":"2025-11-17T11:41:09.120371Z"}},"outputs":[],"execution_count":null}]}