{"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":4534,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":3326,"modelId":986}],"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`（画像パス、メタデータ、5つのターゲット変数）\n- **テストデータ**: `/kaggle/input/csiro-biomass/test.csv`（画像パス、予測対象）\n- **画像**: 70cm×30cm quadratの牧草画像（JPEG形式）\n- **事前学習モデル**: DINOv2-base（`/kaggle/input/dinov2/pytorch/base/1/`）\n\n## 前処理\n\n- **画像処理**: `AutoImageProcessor`によるDINOv2用の正規化・リサイズ\n- **特徴抽出**: DINOv2の`pooler_output`（768次元埋め込み）を使用\n- **データ形式**: 1画像あたり5ターゲット（long format）→画像単位で集約\n\n## モデルの定義\n\n- **特徴抽出器**: DINOv2-base（frozen、推論のみ）\n- **回帰モデル**: Lasso回帰×5ターゲット×5fold（計25モデル）\n- **アンサンブル**: 5fold平均でテスト予測\n\n## 学習の設定\n\n- **交差検証**: 5-fold CV（sklearn.KFold、random_state=42）\n- **負値処理**: 予測値<0を0にクリッピング\n- **評価指標**: R²スコア（学習・検証それぞれ）\n\n## その他\n\n- **データ構造の課題**: train.csvがlong format（1画像5行）だが、特徴抽出は画像単位\n- **改善余地**: データローダー化、GPU最適化、ハイパーパラメータ探索未実施","metadata":{}},{"cell_type":"code","source":"\"\"\"\nCSIRO Image2Biomass Prediction - Baseline Pipeline\n牧草バイオマス予測のための特徴抽出+Lasso回帰パイプライン\n\"\"\"\n\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom typing import Optional\n\nimport numpy as np\nimport pandas as pd\nimport torch\nfrom PIL import Image\nfrom sklearn.linear_model import Lasso\nfrom sklearn.metrics import r2_score\nfrom sklearn.model_selection import KFold\nfrom transformers import AutoImageProcessor, AutoModel\n\n\n@dataclass\nclass Config:\n    \"\"\"パイプライン設定\"\"\"\n    \n    # データパス\n    data_root: Path = Path(\"/kaggle/input/csiro-biomass\")\n    dinov2_path: Path = Path(\"/kaggle/input/dinov2/pytorch/base/1\")\n    \n    # 交差検証設定\n    n_splits: int = 5\n    random_state: int = 42\n    \n    # ターゲット変数（評価順序と一致）\n    target_names: list[str] = None\n    target_weights: dict[str, float] = None\n    \n    def __post_init__(self) -> None:\n        \"\"\"デフォルト値の初期化\"\"\"\n        if self.target_names is None:\n            # train.csvの出現順序\n            self.target_names = [\n                \"Dry_Clover_g\",  # index 0\n                \"Dry_Dead_g\",     # index 1\n                \"Dry_Green_g\",    # index 2\n                \"Dry_Total_g\",    # index 3\n                \"GDM_g\"           # index 4\n            ]\n        \n        if self.target_weights is None:\n            # コンペ評価重み\n            self.target_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    \"\"\"DINOv2による画像特徴抽出器\"\"\"\n    \n    def __init__(self, model_path: Path, device: str = \"cuda\") -> None:\n        \"\"\"\n        初期化\n        \n        Args:\n            model_path: DINOv2モデルのパス\n            device: 計算デバイス（'cuda' or 'cpu'）\n        \"\"\"\n        self.device = device\n        self.processor = AutoImageProcessor.from_pretrained(str(model_path))\n        self.model = AutoModel.from_pretrained(str(model_path)).to(device)\n        self.model.eval()  # 推論モード固定\n    \n    def extract(self, image_path: Path) -> np.ndarray:\n        \"\"\"\n        単一画像から特徴ベクトルを抽出\n        \n        Args:\n            image_path: 画像ファイルパス\n            \n        Returns:\n            特徴ベクトル（shape: [768,]）\n        \"\"\"\n        img = Image.open(image_path)\n        inputs = torch.tensor(self.processor(img).pixel_values).to(self.device)\n        \n        with torch.no_grad():\n            outputs = self.model(inputs)\n            embedding = outputs.pooler_output.cpu().numpy()\n        \n        return embedding.flatten()  # [1, 768] -> [768]\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        self.feature_extractor = FeatureExtractor(config.dinov2_path)\n        \n        # 各ターゲット×各foldの回帰器を格納\n        # regressors[target_idx][fold_idx] = Lasso()\n        self.regressors: list[list[Optional[Lasso]]] = [\n            [None for _ in range(config.n_splits)] \n            for _ in range(len(config.target_names))\n        ]\n    \n    def _load_and_prepare_train_data(\n        self\n    ) -> tuple[np.ndarray, list[np.ndarray]]:\n        \"\"\"\n        学習データ読み込みと特徴抽出\n        \n        Returns:\n            embeddings: 画像特徴（shape: [N_images, 768]）\n            targets_list: ターゲットリスト（各要素shape: [N_images, 1]）\n        \"\"\"\n        train_df = pd.read_csv(self.config.data_root / \"train.csv\")\n        \n        embeddings: list[np.ndarray] = []\n        # 各ターゲットのリスト（5つ）\n        targets_list: list[list[np.ndarray]] = [[] for _ in range(5)]\n        \n        print(f\"Total samples in train.csv: {len(train_df)}\")\n        print(f\"Expected images: {len(train_df) // 5}\")\n        \n        for idx, row in train_df.iterrows():\n            file_path = self.config.data_root / row[\"image_path\"]\n            target_value = np.array([[row[\"target\"]]])\n            \n            # long format: 5行で1画像\n            target_idx = idx % 5\n            targets_list[target_idx].append(target_value)\n            \n            # 画像特徴は最初の行でのみ抽出\n            if target_idx == 0:\n                embedding = self.feature_extractor.extract(file_path)\n                embeddings.append(embedding)\n                \n                if (len(embeddings) % 100) == 0:\n                    print(f\"Processed {len(embeddings)} images\")\n        \n        # numpy配列に変換\n        embeddings_array = np.array(embeddings)\n        targets_array_list = [\n            np.concatenate(targets) for targets in targets_list\n        ]\n        \n        return embeddings_array, targets_array_list\n    \n    def train(self) -> dict[str, list[tuple[float, float]]]:\n        \"\"\"\n        5-fold CVでLasso回帰を学習\n        \n        Returns:\n            各ターゲットのfold別スコア\n            {target_name: [(train_r2, val_r2), ...]}\n        \"\"\"\n        print(\"\\n=== Feature Extraction ===\")\n        embeddings, targets_list = self._load_and_prepare_train_data()\n        \n        kf = KFold(\n            n_splits=self.config.n_splits,\n            shuffle=True,\n            random_state=self.config.random_state\n        )\n        \n        all_scores: dict[str, list[tuple[float, float]]] = {}\n        \n        for target_idx, target_name in enumerate(self.config.target_names):\n            print(f\"\\n=== Training {target_name} (Target {target_idx + 1}/5) ===\")\n            \n            y = targets_list[target_idx]\n            fold_scores: list[tuple[float, float]] = []\n            \n            for fold_idx, (train_idxs, val_idxs) in enumerate(kf.split(embeddings)):\n                X_train, y_train = embeddings[train_idxs], y[train_idxs]\n                X_val, y_val = embeddings[val_idxs], y[val_idxs]\n                \n                # Lasso学習（デフォルトハイパーパラメータ使用）\n                reg = Lasso()\n                reg.fit(X_train, y_train.ravel())  # Lassoは1次元ラベル必須\n                \n                # 予測と評価\n                train_preds = np.clip(reg.predict(X_train), 0.0, None)  # 負値除去\n                val_preds = np.clip(reg.predict(X_val), 0.0, None)\n                \n                train_r2 = r2_score(y_train, train_preds)\n                val_r2 = r2_score(y_val, val_preds)\n                \n                print(f\"  Fold {fold_idx + 1}: Train R²={train_r2:.4f}, Val R²={val_r2:.4f}\")\n                \n                fold_scores.append((train_r2, val_r2))\n                self.regressors[target_idx][fold_idx] = reg\n            \n            # 平均スコア表示\n            avg_train = np.mean([s[0] for s in fold_scores])\n            avg_val = np.mean([s[1] for s in fold_scores])\n            print(f\"  Average: Train R²={avg_train:.4f}, Val R²={avg_val:.4f}\")\n            \n            all_scores[target_name] = fold_scores\n        \n        return all_scores\n    \n    def predict(self) -> pd.DataFrame:\n        \"\"\"\n        テストデータに対して予測\n        \n        Returns:\n            submission形式のDataFrame（sample_id, target列）\n        \"\"\"\n        print(\"\\n=== Test Prediction ===\")\n        test_df = pd.read_csv(self.config.data_root / \"test.csv\")\n        \n        # 画像IDごとに特徴抽出（重複除去）\n        test_embeddings: dict[str, np.ndarray] = {}\n        unique_image_ids = test_df[\"sample_id\"].str.split(\"__\").str[0].unique()\n        \n        for idx, image_id in enumerate(unique_image_ids):\n            # sample_idから画像パスを取得\n            image_row = test_df[\n                test_df[\"sample_id\"].str.startswith(image_id)\n            ].iloc[0]\n            file_path = self.config.data_root / image_row[\"image_path\"]\n            \n            embedding = self.feature_extractor.extract(file_path)\n            test_embeddings[image_id] = embedding\n            \n            if (idx + 1) % 100 == 0:\n                print(f\"Processed {idx + 1}/{len(unique_image_ids)} images\")\n        \n        # ターゲット名→インデックスのマッピング\n        target_name_to_idx = {\n            name: idx for idx, name in enumerate(self.config.target_names)\n        }\n        \n        # 予測実行\n        sample_ids: list[str] = []\n        predictions: list[float] = []\n        \n        for _, row in test_df.iterrows():\n            sample_id = row[\"sample_id\"]\n            image_id, target_name = sample_id.split(\"__\")\n            \n            X = test_embeddings[image_id].reshape(1, -1)  # [768] -> [1, 768]\n            target_idx = target_name_to_idx[target_name]\n            \n            # 5foldの平均予測\n            fold_preds = []\n            for reg in self.regressors[target_idx]:\n                pred = np.clip(reg.predict(X)[0], 0.0, None)  # 負値除去\n                fold_preds.append(pred)\n            \n            avg_pred = np.mean(fold_preds)\n            \n            sample_ids.append(sample_id)\n            predictions.append(float(avg_pred))\n        \n        submission = pd.DataFrame({\n            \"sample_id\": sample_ids,\n            \"target\": predictions\n        })\n        \n        return submission\n\n\ndef main() -> None:\n    \"\"\"メインパイプライン実行\"\"\"\n    config = Config()\n    predictor = BiomassPredictor(config)\n    \n    # 学習\n    cv_scores = predictor.train()\n    \n    # テスト予測\n    submission = predictor.predict()\n    \n    # 保存\n    output_path = Path(\"/kaggle/working/submission.csv\")\n    submission.to_csv(output_path, index=False)\n    \n    print(f\"\\n=== Submission saved to {output_path} ===\")\n    print(submission.head(10))\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:46:02.125096Z","iopub.execute_input":"2025-11-19T12:46:02.125373Z","iopub.status.idle":"2025-11-19T12:47:28.130246Z","shell.execute_reply.started":"2025-11-19T12:46:02.125351Z","shell.execute_reply":"2025-11-19T12:47:28.12931Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}