{"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,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":268942,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":230141,"modelId":251887}],"dockerImageVersionId":31193,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## 使用するデータ\n\n- **トレーニングデータ**: `/kaggle/input/csiro-biomass/train.csv`\n    - 画像パス、サンプリング日、州、種、NDVI、高さ、5つのターゲット値\n- **テストデータ**: `/kaggle/input/csiro-biomass/test.csv`\n    - 画像パス、予測対象のターゲット名\n- **画像データ**:\n    - トレーニング画像: `train/`ディレクトリ\n    - テスト画像: `test/`ディレクトリ\n- **事前学習モデル**: Google SigLIP (so400m-patch14-384)\n\n## 前処理\n\n1. **データ整形**: Long形式からWide形式への変換（5つのターゲットを列に展開）\n2. **グループ作成**: `Sampling_Date`と`State`を結合してGroupKFold用のグループを生成\n3. **特徴抽出**: SigLIPモデルで画像から埋め込みベクトルを抽出（バッチサイズ20）\n4. **特徴保存**: NDJSON形式で特徴量を保存・ロード\n\n## モデルの定義\n\n- **ベースライン**: DummyRegressor（平均値予測）\n- **線形モデル**: Ridge回帰、Lasso回帰\n- **アンサンブル**: GradientBoostingRegressor、CatBoostRegressor\n- **次元削減**: PCA（3次元）を用いた予測値の制約\n\n## 学習の設定\n\n- **クロスバリデーション**: GroupKFold（5分割）\n- **評価指標**: 重み付きR²スコア（Dry_Total_g=0.5、GDM_g=0.2、他=0.1）\n- **PCAトリック**: 5つのターゲット間の相関を利用して予測値を調整\n- **アンサンブル**: GBMとCatBoostの平均\n\n## その他\n\n- **GPU利用**: CUDAが利用可能な場合は自動的に使用\n- **非負制約**: 予測値をclip(0)で非負に制約\n- **サブミッション形式**: Long形式（sample_id, target）","metadata":{}},{"cell_type":"code","source":"\"\"\"\n牧草バイオマス予測パイプライン\n\n画像から特徴量を抽出し、5つのバイオマス成分を予測する統合パイプライン。\nSigLIPモデルによる特徴量抽出 → GroupKFold交差検証 → アンサンブル予測の流れを実装。\n\"\"\"\n\nimport json\nfrom copy import deepcopy\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom typing import Any\n\nimport catboost\nimport numpy as np\nimport polars as pl\nimport torch\nfrom PIL import Image\nfrom sklearn.decomposition import PCA\nfrom sklearn.ensemble import GradientBoostingRegressor\nfrom sklearn.model_selection import GroupKFold\nfrom tqdm.auto import tqdm\nfrom transformers import AutoImageProcessor, AutoModel\n\n\n@dataclass\nclass Config:\n    \"\"\"パイプライン全体の設定を管理するクラス\"\"\"\n    \n    # パス設定\n    data_path: Path = Path('/kaggle/input/csiro-biomass')\n    model_path: str = \"/kaggle/input/google-siglip-so400m-patch14-384/transformers/default/1/\"\n    \n    # モデル設定\n    device: str = 'cuda' if torch.cuda.is_available() else 'cpu'\n    batch_size: int = 20\n    \n    # 交差検証設定\n    n_splits: int = 5\n    random_state: int = 42\n    \n    # ターゲット変数とその重み\n    labels: list[str] = None\n    weights: dict[str, float] = None\n    \n    # PCAの次元数\n    pca_n_components: int = 3\n    \n    def __post_init__(self) -> None:\n        \"\"\"初期化後の処理でデフォルト値を設定\"\"\"\n        if self.labels is None:\n            self.labels = [\n                \"Dry_Clover_g\",\n                \"Dry_Dead_g\", \n                \"Dry_Green_g\",\n                \"Dry_Total_g\",\n                \"GDM_g\"\n            ]\n        \n        if self.weights is None:\n            self.weights = {\n                'Dry_Green_g': 0.1,\n                'Dry_Dead_g': 0.1,\n                'Dry_Clover_g': 0.1,\n                'GDM_g': 0.2,\n                'Dry_Total_g': 0.5,\n            }\n\n\nclass FeatureExtractor:\n    \"\"\"SigLIPモデルを使用して画像から特徴量を抽出するクラス\"\"\"\n    \n    def __init__(self, config: Config) -> None:\n        \"\"\"\n        特徴量抽出器の初期化\n        \n        Args:\n            config: パイプライン設定\n        \"\"\"\n        self.config = config\n        self.model = self._load_model()  # モデル読み込み\n        self.processor = AutoImageProcessor.from_pretrained(config.model_path)  # プロセッサ読み込み\n    \n    def _load_model(self) -> Any:\n        \"\"\"\n        事前学習済みSigLIPモデルを読み込む\n        \n        Returns:\n            評価モードに設定されたモデル\n        \"\"\"\n        model = AutoModel.from_pretrained(self.config.model_path)\n        model = model.to(self.config.device)\n        model.eval()  # 推論モードに設定\n        return model\n    \n    def extract_features(self, image_paths: list[str], save_path: str) -> None:\n        \"\"\"\n        画像リストから特徴量を抽出してファイルに保存\n        \n        Args:\n            image_paths: 画像パスのリスト\n            save_path: 特徴量を保存するndjsonファイルのパス\n        \"\"\"\n        batch_size = self.config.batch_size\n        \n        with torch.no_grad(), open(save_path, 'w') as f:\n            for i in tqdm(range(0, len(image_paths), batch_size), desc=\"特徴量抽出中\"):\n                batch_paths = image_paths[i:i + batch_size]\n                \n                # 画像を読み込み\n                batch_images = [\n                    Image.open(self.config.data_path / path) \n                    for path in batch_paths\n                ]\n                \n                # モデルで特徴量を抽出\n                inputs = self.processor(\n                    images=batch_images, \n                    return_tensors=\"pt\"\n                ).to(self.model.device)\n                features = self.model.get_image_features(**inputs)\n                \n                # 特徴量をJSON Lines形式で保存\n                for feature_vector in features:\n                    feature_dict = {\n                        f'x_{j}': feature_vector[j].item() \n                        for j in range(len(feature_vector))\n                    }\n                    f.write(json.dumps(feature_dict) + '\\n')\n\n\nclass BiomassPredictor:\n    \"\"\"バイオマス予測モデルの訓練と予測を管理するクラス\"\"\"\n    \n    def __init__(self, config: Config) -> None:\n        \"\"\"\n        予測器の初期化\n        \n        Args:\n            config: パイプライン設定\n        \"\"\"\n        self.config = config\n    \n    def _compute_metric(self, y_true: np.ndarray, y_pred: np.ndarray) -> float:\n        \"\"\"\n        コンペティション評価指標(重み付きR²)を計算\n        \n        Args:\n            y_true: 真のターゲット値 (n_samples, n_labels)\n            y_pred: 予測値 (n_samples, n_labels)\n            \n        Returns:\n            重み付きR²スコア\n        \"\"\"\n        labels = self.config.labels\n        weights = self.config.weights\n        \n        # 重み付き平均を計算\n        y_weighted = sum(\n            y_true[:, l].mean() * weights[label]\n            for l, label in enumerate(labels)\n        )\n        \n        # 重み付き残差平方和と全平方和を計算\n        ss_res = sum(\n            ((y_true[:, l] - y_pred[:, l])**2).mean() * weights[label]\n            for l, label in enumerate(labels)\n        )\n        ss_tot = sum(\n            ((y_true[:, l] - y_weighted)**2).mean() * weights[label]\n            for l, label in enumerate(labels)\n        )\n        \n        return 1 - ss_res / ss_tot\n    \n    def cross_validate(\n        self,\n        model: Any,\n        train_data: pl.DataFrame,\n        test_data: pl.DataFrame,\n        feature_columns: list[str]\n    ) -> tuple[np.ndarray, np.ndarray]:\n        \"\"\"\n        GroupKFoldによる交差検証を実行\n        \n        Args:\n            model: 使用する機械学習モデル\n            train_data: 訓練データ\n            test_data: テストデータ\n            feature_columns: 特徴量のカラム名リスト\n            \n        Returns:\n            (訓練データに対する予測値, テストデータに対する予測値)\n        \"\"\"\n        labels = self.config.labels\n        \n        # ラベルが特徴量に含まれていないことを確認\n        # Why not: ラベルリークを防ぐため\n        assert not any((col in labels for col in feature_columns))\n        \n        # データを numpy 配列に変換\n        X = train_data.select(feature_columns).to_numpy()\n        X_test = test_data.select(feature_columns).to_numpy()\n        y_true = train_data.select(labels).to_numpy()\n        \n        # 予測値を格納する配列を初期化\n        y_pred = np.zeros([len(X), len(labels)])\n        y_pred_test = np.zeros([len(X_test), len(labels)])\n        \n        # GroupKFoldの設定\n        n_splits = self.config.n_splits\n        kf = GroupKFold(n_splits=n_splits)\n        groups = train_data.select('group')\n        \n        # 各Foldで訓練と予測\n        for fold_idx, (train_index, valid_index) in enumerate(kf.split(X, groups=groups)):\n            # PCAを訓練データでフィッティング\n            # Why: ターゲット間の相関を活用して予測精度を向上\n            pca = PCA(self.config.pca_n_components).fit(y_true[train_index])\n            \n            # 各ターゲットごとにモデルを訓練\n            for label_idx in range(len(labels)):\n                m = deepcopy(model)  # モデルをコピー\n                m.fit(X[train_index], y_true[train_index, label_idx])\n                \n                # 予測値を計算(負の値は0にクリップ)\n                y_pred[valid_index, label_idx] = m.predict(X[valid_index]).clip(0)\n                y_pred_test[:, label_idx] += m.predict(X_test).clip(0) / n_splits\n            \n            # PCAトリックを適用して予測値を補正\n            # Why: ターゲット間の物理的な制約を満たすため\n            y_pred[valid_index] = pca.inverse_transform(\n                pca.transform(y_pred[valid_index])\n            ).clip(0)\n            y_pred_test = pca.inverse_transform(\n                pca.transform(y_pred_test)\n            ).clip(0)\n            \n            # Fold毎のスコアを表示\n            fold_score = self._compute_metric(y_true[valid_index], y_pred[valid_index])\n            print(f'Fold {fold_idx}: {fold_score:.6f}')\n        \n        # 全体のCVスコアを表示\n        cv_score = self._compute_metric(y_true, y_pred)\n        print(f'Full CV: {cv_score:.6f}')\n        \n        return y_pred, y_pred_test\n\n\nclass BiomassPredictor_Pipeline:\n    \"\"\"牧草バイオマス予測の全体パイプライン\"\"\"\n    \n    def __init__(self, config: Config = None) -> None:\n        \"\"\"\n        パイプラインの初期化\n        \n        Args:\n            config: パイプライン設定(Noneの場合はデフォルト設定を使用)\n        \"\"\"\n        self.config = config or Config()\n        self.feature_extractor = FeatureExtractor(self.config)\n        self.predictor = BiomassPredictor(self.config)\n    \n    def _load_and_prepare_data(self) -> tuple[pl.DataFrame, pl.DataFrame]:\n        \"\"\"\n        訓練データとテストデータを読み込み、整形する\n        \n        Returns:\n            (訓練データ, テストデータ)\n        \"\"\"\n        labels = self.config.labels\n        data_path = self.config.data_path\n        \n        # 訓練データの読み込みと整形\n        train = pl.read_csv(data_path / 'train.csv')\n        df_train = (\n            train\n            .with_columns([\n                # 各ラベルごとに列を作成\n                pl.when(pl.col('target_name') == label)\n                  .then(pl.col('target'))\n                  .alias(label)\n                for label in labels\n            ])\n            .group_by('image_path')\n            .agg([\n                # 各ラベルの平均値を計算\n                pl.col(label).mean()\n                for label in labels\n            ] + [\n                # グループ情報を作成(日付+州)\n                pl.concat_str([\"Sampling_Date\", \"State\"], separator=\" \")\n                  .alias(\"group\")\n                  .first()\n            ])\n            .sort('image_path')\n        )\n        \n        # テストデータの読み込みと整形\n        test = pl.read_csv(data_path / 'test.csv')\n        df_test = (\n            test\n            .group_by('image_path')\n            .len()\n            .sort('image_path')\n        )\n        \n        return df_train, df_test, test\n    \n    def _extract_features(\n        self,\n        df_train: pl.DataFrame,\n        df_test: pl.DataFrame\n    ) -> tuple[pl.DataFrame, pl.DataFrame]:\n        \"\"\"\n        訓練データとテストデータから特徴量を抽出\n        \n        Args:\n            df_train: 訓練データ\n            df_test: テストデータ\n            \n        Returns:\n            (特徴量を追加した訓練データ, 特徴量を追加したテストデータ)\n        \"\"\"\n        # 訓練データの特徴量抽出\n        print(\"訓練データから特徴量を抽出中...\")\n        self.feature_extractor.extract_features(\n            df_train['image_path'].to_list(),\n            'features_train.ndjson'\n        )\n        features_train = pl.read_ndjson('features_train.ndjson')\n        \n        # テストデータの特徴量抽出\n        print(\"テストデータから特徴量を抽出中...\")\n        self.feature_extractor.extract_features(\n            df_test['image_path'].to_list(),\n            'features_test.ndjson'\n        )\n        features_test = pl.read_ndjson('features_test.ndjson')\n        \n        # 元のデータと特徴量を結合\n        df_train_aug = pl.concat([df_train, features_train], how='horizontal')\n        df_test_aug = pl.concat([df_test, features_test], how='horizontal')\n        \n        return df_train_aug, df_test_aug\n    \n    def _train_and_predict(\n        self,\n        df_train: pl.DataFrame,\n        df_test: pl.DataFrame,\n        feature_columns: list[str]\n    ) -> np.ndarray:\n        \"\"\"\n        複数のモデルを訓練してアンサンブル予測を実行\n        \n        Args:\n            df_train: 訓練データ\n            df_test: テストデータ\n            feature_columns: 特徴量カラム名のリスト\n            \n        Returns:\n            テストデータに対する予測値\n        \"\"\"\n        print(\"\\n=== GradientBoostingRegressor ===\")\n        _, pred_test_gb = self.predictor.cross_validate(\n            GradientBoostingRegressor(),\n            df_train,\n            df_test,\n            feature_columns\n        )\n        \n        print(\"\\n=== CatBoostRegressor ===\")\n        _, pred_test_cb = self.predictor.cross_validate(\n            catboost.CatBoostRegressor(verbose=False, iterations=100),\n            df_train,\n            df_test,\n            feature_columns\n        )\n        \n        # アンサンブル予測(平均)\n        pred_test = (pred_test_gb + pred_test_cb) / 2\n        \n        return pred_test\n    \n    def _create_submission(\n        self,\n        test: pl.DataFrame,\n        df_test: pl.DataFrame,\n        predictions: np.ndarray\n    ) -> pl.DataFrame:\n        \"\"\"\n        提出用ファイルを作成\n        \n        Args:\n            test: 元のテストデータ\n            df_test: 整形済みテストデータ\n            predictions: 予測値\n            \n        Returns:\n            提出用データフレーム\n        \"\"\"\n        labels = self.config.labels\n        \n        # 予測値をデータフレームに変換\n        pred_with_id = pl.concat([\n            df_test,\n            pl.DataFrame(predictions, schema=labels),\n        ], how='horizontal')\n        \n        # 元のテストデータと結合し、必要な形式に整形\n        submission = (\n            test\n            .join(pred_with_id, on='image_path')\n            .with_columns(\n                # target_nameに対応する予測値を選択\n                pl.coalesce(*[\n                    pl.when(pl.col('target_name') == col).then(pl.col(col))\n                    for col in labels\n                ]).alias('target')\n            )\n            .select('sample_id', 'target')\n        )\n        \n        return submission\n    \n    def run(self) -> None:\n        \"\"\"パイプライン全体を実行\"\"\"\n        print(f\"Device: {self.config.device}\")\n        print(f\"Model: {self.config.model_path}\\n\")\n        \n        # 1. データ読み込み\n        print(\"=== データ読み込み ===\")\n        df_train, df_test, test = self._load_and_prepare_data()\n        print(f\"訓練データ: {len(df_train)} 画像\")\n        print(f\"テストデータ: {len(df_test)} 画像\\n\")\n        \n        # 2. 特徴量抽出\n        print(\"=== 特徴量抽出 ===\")\n        df_train_aug, df_test_aug = self._extract_features(df_train, df_test)\n        \n        # 特徴量カラムを取得\n        feature_columns = sorted([\n            col for col in df_train_aug.columns \n            if col.startswith('x_')\n        ])\n        print(f\"特徴量次元数: {len(feature_columns)}\\n\")\n        \n        # 3. モデル訓練と予測\n        print(\"=== モデル訓練と予測 ===\")\n        predictions = self._train_and_predict(\n            df_train_aug,\n            df_test_aug,\n            feature_columns\n        )\n        \n        # 4. 提出ファイル作成\n        print(\"\\n=== 提出ファイル作成 ===\")\n        submission = self._create_submission(test, df_test, predictions)\n        submission.write_csv('submission.csv')\n        print(\"提出ファイルを submission.csv に保存しました\")\n\n\ndef main() -> None:\n    \"\"\"メイン実行関数\"\"\"\n    # 設定を作成\n    config = Config()\n    \n    # パイプラインを実行\n    pipeline = BiomassPredictor_Pipeline(config)\n    pipeline.run()\n\n\nif __name__ == \"__main__\":\n    main()","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-19T12:58:38.350782Z","iopub.execute_input":"2025-11-19T12:58:38.351058Z","execution_failed":"2025-11-19T13:02:06.361Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}