{"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"}],"dockerImageVersionId":31193,"isInternetEnabled":true,"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-10T06:59:08.227348Z","iopub.execute_input":"2025-12-10T06:59:08.228006Z","iopub.status.idle":"2025-12-10T06:59:08.368937Z","shell.execute_reply.started":"2025-12-10T06:59:08.227985Z","shell.execute_reply":"2025-12-10T06:59:08.368173Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import r2_score, mean_squared_error\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nimport timm\nfrom tqdm import tqdm\nimport random\nfrom torch.optim.lr_scheduler import CosineAnnealingLR","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T06:59:08.370127Z","iopub.execute_input":"2025-12-10T06:59:08.370663Z","iopub.status.idle":"2025-12-10T06:59:08.377658Z","shell.execute_reply.started":"2025-12-10T06:59:08.370624Z","shell.execute_reply":"2025-12-10T06:59:08.376931Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==========================================\n# 1. 設定 (Configuration)\n# ==========================================\nclass CFG:\n    # パス設定 \n    BASE_PATH = \"/kaggle/input/csiro-biomass\"\n    TRAIN_CSV = os.path.join(BASE_PATH, \"train.csv\")\n    TRAIN_IMG_DIR = os.path.join(BASE_PATH, \"train\")\n    \n    # モデル設定\n    MODEL_NAME = 'vit_large_patch14_dinov2.lvd142m'\n    IMG_SIZE = 518\n    BATCH_SIZE = 8\n    EPOCHS = 10           # スコア確認用なので少なくてOK\n    LR = 1e-3\n    SEED = 42\n    DEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    \n    # ターゲット列と重み\n    TARGET_COLS = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n    WEIGHTS = np.array([0.1, 0.1, 0.1, 0.2, 0.5])\n\n#seed固定\ndef seed_everything(seed):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.backends.cudnn.deterministic = True\n\nseed_everything(CFG.SEED)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T06:59:08.378460Z","iopub.execute_input":"2025-12-10T06:59:08.378987Z","iopub.status.idle":"2025-12-10T06:59:08.394993Z","shell.execute_reply.started":"2025-12-10T06:59:08.378958Z","shell.execute_reply":"2025-12-10T06:59:08.394348Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==========================================\n# 2. データ準備 & Dataset\n# ==========================================\n#データをカラムごと→画像ごとに並べる（縦持ち→横持ち変換）\ndef prepare_data(csv_path):\n    df = pd.read_csv(csv_path)\n    df['image_id'] = df['sample_id'].apply(lambda x: x.split('__')[0])\n    df['target_name'] = df['sample_id'].apply(lambda x: x.split('__')[1])#文字列の調整\n    df_wide = df.pivot(index='image_id', columns='target_name', values='target').reset_index()\n    return df_wide[['image_id'] + CFG.TARGET_COLS].copy()\n\n\ndef get_transforms(mode='train'):\n    if mode == 'train':\n        return A.Compose([\n            A.Resize(CFG.IMG_SIZE, CFG.IMG_SIZE), #画像をリサイズ\n            A.HorizontalFlip(p=0.5), #左右反転\n            A.VerticalFlip(p=0.5), #上下反転\n            A.RandomRotate90(p=0.5), #90度回転\n            A.RandomBrightnessContrast(p=0.2), # 明るさとコントラストをランダムに変化\n            A.HueSaturationValue(p=0.2),       # 色相・彩度を少しずらす\n            A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), #画素数標準化（統計値）\n            ToTensorV2(), #NumpyをTensorに\n        ])\n    else:\n        return A.Compose([\n            A.Resize(CFG.IMG_SIZE, CFG.IMG_SIZE),\n            A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n            ToTensorV2(),\n        ])\n\nclass BiomassDataset(Dataset):\n    def __init__(self, df, img_dir, transform=None, mode='train'):\n        self.df = df\n        self.img_dir = img_dir\n        self.transform = transform\n        self.mode = mode # 'train' か 'valid' かを知るために追加\n        self.image_ids = df['image_id'].values\n        # ターゲット（目的変数）の取得\n        self.targets = np.log1p(df[CFG.TARGET_COLS].values.astype(np.float32))\n    \n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        img_id = self.image_ids[idx]\n        \n        # 1. 画像パスの生成 (png なければ jpg)\n        img_path = os.path.join(self.img_dir, f\"{img_id}.png\")\n        if not os.path.exists(img_path):\n            img_path = os.path.join(self.img_dir, f\"{img_id}.jpg\")\n            \n        # 2. 画像読み込み\n        image = cv2.imread(img_path)\n        \n        # エラーハンドリング（読み込めなかった場合）\n        if image is None:\n            # 元画像はおそらく横長なので、横幅を2倍にしたダミーを作る\n            image = np.zeros((1000, 2000, 3), dtype=np.uint8)\n        else:\n            image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n\n        # 3. ★ここが重要：左右に分割する処理★\n        h, w, c = image.shape\n        mid = w // 2 # 真ん中の座標\n        \n        left_img = image[:, :mid]  # 左半分 [高さ全部, 0〜真ん中]\n        right_img = image[:, mid:] # 右半分 [高さ全部, 真ん中〜最後]\n\n        # 4. 学習モードに応じた画像の選択\n        if self.mode == 'train':\n            # 学習時は「左」か「右」をランダムに選ぶ（データ拡張効果）\n            if np.random.rand() > 0.5:\n                use_image = left_img\n            else:\n                use_image = right_img\n        else:\n            # 検証/テスト時は、とりあえず「左」を使う\n            # (より精度を上げるなら、左右両方推論して平均をとるTTAが必要ですが、まずはこれでOK)\n            use_image = left_img\n\n        # 5. Transform（リサイズ・正規化など）の適用\n        if self.transform:\n            # 分割後の画像に対してリサイズがかかるので画質劣化が少ない\n            use_image = self.transform(image=use_image)['image']\n            \n        return use_image, torch.tensor(self.targets[idx])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T06:59:08.396169Z","iopub.execute_input":"2025-12-10T06:59:08.396404Z","iopub.status.idle":"2025-12-10T06:59:08.418066Z","shell.execute_reply.started":"2025-12-10T06:59:08.396388Z","shell.execute_reply":"2025-12-10T06:59:08.417531Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==========================================\n# 3. モデル & Loss\n# ==========================================\nclass DINOv2Regressor(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.backbone = timm.create_model(CFG.MODEL_NAME, pretrained=True, num_classes=0)\n        # 凍結(バックボーン部分は学習させない)\n        for param in self.backbone.parameters():\n            param.requires_grad = False\n        \n        #Dinov2から受け取った特徴をさらに変換    \n        self.head = nn.Sequential(\n            nn.Linear(self.backbone.num_features, 512),\n            nn.BatchNorm1d(512),\n            nn.Dropout(0.3),\n            nn.Linear(512, 5)\n        )\n        init_bias = 4.0 \n        self.head[-1].bias.data.fill_(init_bias)\n\n    def forward(self, x):\n        return self.head(self.backbone(x))\n\nclass WeightedLoss(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.weights = torch.tensor(CFG.WEIGHTS).to(CFG.DEVICE)\n        #重み入力\n    \n    def forward(self, preds, targets):\n        #重みつきMSE\n        mse = torch.mean(self.weights * (preds - targets) ** 2) \n        #整合性ロス (Total vs Sum of parts)\n        #consistency = torch.mean((preds[:, 4] - (preds[:, 0] + preds[:, 1] + preds[:, 2])) ** 2)\n        return mse #+ 0.1 * consistency\n\n# ==========================================\n# 4. スコア計算ロジック\n# ==========================================\n#決定係数計算\ndef calculate_score(y_true, y_pred):\n    y_true, y_pred = np.array(y_true), np.array(y_pred)\n    weights = CFG.WEIGHTS\n    \n    # Global Weighted R2\n    global_mean = np.average(y_true, weights=np.tile(weights, (len(y_true), 1)))\n    rss = np.sum(weights * (y_true - y_pred)**2)\n    tss = np.sum(weights * (y_true - global_mean)**2)\n    \n    r2 = 1 - (rss / tss) if tss != 0 else 0\n    return r2\n\n# ==========================================\n# 5. メイン実行処理\n# ==========================================\nif __name__ == \"__main__\":\n    print(\"--- Loading Data ---\")\n    df = prepare_data(CFG.TRAIN_CSV)\n    \n    # 検証用に分割 (8:2)\n    train_df, valid_df = train_test_split(df, test_size=0.2, random_state=CFG.SEED)\n    print(f\"Train: {len(train_df)}, Valid: {len(valid_df)}\")\n    \n    train_ds = BiomassDataset(train_df, CFG.TRAIN_IMG_DIR, transform=get_transforms('train'), mode='train')\n    valid_ds = BiomassDataset(valid_df, CFG.TRAIN_IMG_DIR, transform=get_transforms('valid'), mode='valid')\n    \n    train_loader = DataLoader(train_ds, batch_size=CFG.BATCH_SIZE, shuffle=True)\n    valid_loader = DataLoader(valid_ds, batch_size=CFG.BATCH_SIZE, shuffle=False)\n    \n    model = DINOv2Regressor().to(CFG.DEVICE)\n    optimizer = optim.AdamW(model.head.parameters(), lr=CFG.LR) #最適化アルゴリズム\n    scheduler = CosineAnnealingLR(optimizer, T_max=CFG.EPOCHS, eta_min=1e-6)\n    criterion = WeightedLoss()\n    best_score = -np.inf\n    \n    print(\"--- Start Training ---\")\n    for epoch in range(CFG.EPOCHS):\n        model.train()\n        train_loss = 0\n        for imgs, targets in tqdm(train_loader, desc=f\"Epoch {epoch+1}\"):\n            imgs, targets = imgs.to(CFG.DEVICE), targets.to(CFG.DEVICE)\n            optimizer.zero_grad()\n            outputs = model(imgs)\n            loss = criterion(outputs, targets)\n            loss.backward()\n            optimizer.step()\n            train_loss += loss.item()\n        scheduler.step()\n            \n            \n        # --- ここでスコア計算 ---\n        model.eval()\n        valid_preds = []\n        valid_targets = []\n        with torch.no_grad():\n            for imgs, targets in valid_loader:\n                imgs = imgs.to(CFG.DEVICE)\n                outputs = model(imgs)\n                valid_preds.append(outputs.cpu().numpy())\n                valid_targets.append(targets.numpy())\n        \n        # 配列にまとめる\n        valid_preds = np.vstack(valid_preds)\n        valid_targets = np.vstack(valid_targets)\n\n        valid_preds = np.expm1(valid_preds)\n        valid_targets = np.expm1(valid_targets)\n\n        \n        # 負の値は0にする（物理的にありえないので）\n        valid_preds = np.maximum(valid_preds, 0)\n\n        \n        print(f\"\\n=== DEBUG: Epoch {epoch+1} ===\")\n        # 配列の形が (N, 5) なので、5列目（インデックス4）が Total です\n        print(f\"Preds (Total): {valid_preds[:10, 4]}\") # 最初の10個を表示\n        print(f\"True  (Total): {valid_targets[:10, 4]}\")\n        print(\"==============================\\n\")\n            \n        # スコア算出\n        score = calculate_score(valid_targets, valid_preds)\n        print(f\"Epoch {epoch+1} | Train Loss: {train_loss/len(train_loader):.4f} | Val Score (R2): {score:.4f}\")\n        if score > best_score:\n            best_score = score\n            torch.save(model.state_dict(), \"best_model.pth\")\n            print(f\"  >>> Saved Best Model! Score: {best_score:.4f}\")\n\n    model.load_state_dict(torch.load(\"best_model.pth\"))\n\n    \n    # --- 最終結果の詳細表示 ---\n    print(\"\\n\" + \"=\"*30)\n    print(f\"Final Validation Score: {score:.4f}\")\n    print(\"=\"*30)\n    print(f\"{'Target':<12} | {'RMSE':<10} | {'R2':<10}\")\n    print(\"-\" * 36)\n    for i, col in enumerate(CFG.TARGET_COLS):\n        rmse = np.sqrt(mean_squared_error(valid_targets[:, i], valid_preds[:, i]))\n        r2_local = r2_score(valid_targets[:, i], valid_preds[:, i])\n        print(f\"{col:<12} | {rmse:<10.2f} | {r2_local:<10.4f}\")\n    print(\"-\" * 36)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T06:59:08.418803Z","iopub.execute_input":"2025-12-10T06:59:08.419049Z","iopub.status.idle":"2025-12-10T07:12:14.975177Z","shell.execute_reply.started":"2025-12-10T06:59:08.419028Z","shell.execute_reply":"2025-12-10T07:12:14.974535Z"}},"outputs":[],"execution_count":null}]}