{"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":3951115,"sourceType":"datasetVersion","datasetId":1027206},{"sourceId":14125819,"sourceType":"datasetVersion","datasetId":9000120}],"dockerImageVersionId":31193,"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 os, random, math\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nfrom pathlib import Path\nfrom tqdm import tqdm\nimport matplotlib.pyplot as plt\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nimport torchvision.transforms as T\nimport timm\nfrom sklearn.model_selection import KFold\nfrom sklearn.metrics import r2_score\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\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-12T11:31:19.046832Z","iopub.execute_input":"2025-12-12T11:31:19.047563Z","iopub.status.idle":"2025-12-12T11:31:19.638473Z","shell.execute_reply.started":"2025-12-12T11:31:19.047538Z","shell.execute_reply":"2025-12-12T11:31:19.637708Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"SEED = 42\nNUM_FOLDS = 5\nNUM_EPOCHS = 18\nBATCH_SIZE = 16\nIMG_SIZE = 224\nNUM_WORKERS = 2\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nMODEL_DIR = 'models'\nos.makedirs(MODEL_DIR, exist_ok=True)\nPRINT_EVERY = 1\n\nTARGET_COLS = ['Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'Dry_Total_g', 'GDM_g']\nTARGET_WEIGHTS = {'Dry_Green_g':0.1,'Dry_Dead_g':0.1,'Dry_Clover_g':0.1,'GDM_g':0.2,'Dry_Total_g':0.5}\nWEIGHT_VECTOR = np.array([TARGET_WEIGHTS[c] for c in TARGET_COLS], dtype=float)\n\n# ---------- Utils ----------\ndef set_seed(seed=SEED):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n\nset_seed(SEED)\nprint(\"Device:\", DEVICE)\n\nBASE_PATH = '/kaggle/input/csiro-biomass'\nTRAIN_CSV = os.path.join(BASE_PATH, 'train.csv')\nTEST_CSV  = os.path.join(BASE_PATH, 'test.csv')\nSAMPLE_SUB = os.path.join(BASE_PATH, 'sample_submission.csv')\nTRAIN_IMG_DIR = os.path.join(BASE_PATH, 'train')\nTEST_IMG_DIR = os.path.join(BASE_PATH, 'test')\n\nprint(\"Loading CSVs...\")\ntrain_df = pd.read_csv(TRAIN_CSV)\ntest_df  = pd.read_csv(TEST_CSV)\nsample_sub = pd.read_csv(SAMPLE_SUB)\nprint(\"Raw train shape:\", train_df.shape)\nprint(train_df.columns.tolist())\n\n# ---------- Safe reshape ----------\ndef reshape_by_image(df):\n    meta_cols = [c for c in df.columns if c not in ['target_name', 'target', 'image_path']]\n    meta = df.drop_duplicates('image_path').set_index('image_path')\n    meta = meta[[c for c in meta_cols if c in meta.columns]]\n    pivot = df.pivot_table(index='image_path', columns='target_name', values='target', aggfunc='first')\n    df_wide = pivot.join(meta, how='left').reset_index()\n    for c in TARGET_COLS:\n        if c not in df_wide.columns:\n            df_wide[c] = 0.0\n    return df_wide\n\ntrain_wide = reshape_by_image(train_df)\nprint(\"Reshaped train shape:\", train_wide.shape)\nprint(\"Columns:\", train_wide.columns.tolist())\nprint(train_wide.head(2))\n\nprint(\"Missing targets per column:\\n\", train_wide[TARGET_COLS].isnull().sum())\nprint(train_wide[TARGET_COLS].describe().T[['mean','std','min','max']])\n\n# ---------- Metadata processing ----------\nmeta_numeric = []\nif 'Pre_GSHH_NDVI' in train_wide.columns:\n    meta_numeric.append('Pre_GSHH_NDVI')\nif 'Height_Ave_cm' in train_wide.columns:\n    meta_numeric.append('Height_Ave_cm')\n\ncat_cols = []\nif 'State' in train_wide.columns:\n    cat_cols.append('State')\nif 'Species' in train_wide.columns:\n    cat_cols.append('Species')\n\nohe_maps = {}\nfor c in cat_cols:\n    vals = sorted(train_wide[c].fillna('NA').unique().tolist())\n    ohe_maps[c] = {v:i for i,v in enumerate(vals)}\n    print(f\"Column {c}: {len(vals)} unique values\")\n\n\ndef build_meta_vector(row):\n    vec = []\n    for c in meta_numeric:\n        v = row.get(c, 0.0)\n        try:\n            vec.append(float(v))\n        except:\n            vec.append(0.0)\n    for c in cat_cols:\n        one_hot = [0.0] * len(ohe_maps[c])\n        key = row.get(c, 'NA')\n        idx = ohe_maps[c].get(key, None)\n        if idx is not None:\n            one_hot[idx] = 1.0\n        vec.extend(one_hot)\n    return np.array(vec, dtype=float)\n\nif len(train_wide) > 0:\n    META_DIM = build_meta_vector(train_wide.iloc[0]).shape[0]\nelse:\n    META_DIM = len(meta_numeric) + sum(len(ohe_maps[c]) for c in cat_cols)\nprint(\"META_DIM:\", META_DIM, \"Meta numeric:\", meta_numeric, \"Cat cols:\", cat_cols)\n\n# ---------- Transforms ----------\nMEAN = [0.485, 0.456, 0.406]\nSTD  = [0.229, 0.224, 0.225]\n\ntrain_transform = T.Compose([\n    T.Resize((IMG_SIZE, IMG_SIZE)),\n    T.RandomResizedCrop(IMG_SIZE, scale=(0.8,1.0)),\n    T.RandomHorizontalFlip(),\n    T.RandomVerticalFlip(),\n    T.RandomRotation(20),\n    T.ToTensor(),\n    T.Normalize(MEAN, STD)\n])\n\nval_transform = T.Compose([\n    T.Resize((IMG_SIZE, IMG_SIZE)),\n    T.ToTensor(),\n    T.Normalize(MEAN, STD)\n])\n\n# ---------- Dataset ----------\nclass BiomassDataset(Dataset):\n    def __init__(self, df, img_dir, target_cols, transform=None, is_test=False, use_log_targets=True):\n        self.df = df.reset_index(drop=True)\n        self.img_dir = img_dir\n        self.transform = transform\n        self.target_cols = target_cols\n        self.is_test = is_test\n        self.use_log_targets = use_log_targets\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_name = row['image_path'].split('/')[-1]\n        img_path = os.path.join(self.img_dir, img_name)\n        if not os.path.exists(img_path):\n            img_path = row['image_path']\n        img = Image.open(img_path).convert('RGB')\n        if self.transform:\n            img = self.transform(img)\n        meta = build_meta_vector(row)\n        meta = torch.tensor(meta, dtype=torch.float32)\n        if self.is_test:\n            return img, row['image_path'], meta\n        else:\n            targets = row[self.target_cols].values.astype(float)\n            if self.use_log_targets:\n                targets = np.log1p(targets)\n            targets = torch.tensor(targets, dtype=torch.float32)\n            return img, meta, targets\n\n# ---------- Loss & metric ----------\nclass WeightedMSELoss(nn.Module):\n    def __init__(self, weight_dict):\n        super().__init__()\n        weights = torch.tensor([weight_dict[c] for c in TARGET_COLS], dtype=torch.float32)\n        self.register_buffer('weights', weights)\n    def forward(self, preds, targets):\n        mse = (preds - targets)**2\n        weighted = mse * self.weights.unsqueeze(0)\n        return weighted.mean()\n\n\ndef calculate_weighted_r2_original_scale(y_true_log, y_pred_log, weights):\n    try:\n        y_true = np.expm1(y_true_log)\n        y_pred = np.expm1(y_pred_log)\n    except Exception:\n        return np.nan\n    weights = np.array(weights)\n    n, t = y_true.shape\n    y_true_flat = y_true.flatten()\n    y_pred_flat = y_pred.flatten()\n    weights_flat = np.repeat(weights, n)\n    if not (np.isfinite(y_true_flat).all() and np.isfinite(y_pred_flat).all()):\n        return np.nan\n    y_mean_weighted = np.sum(weights_flat * y_true_flat) / np.sum(weights_flat)\n    ss_res = np.sum(weights_flat * (y_true_flat - y_pred_flat)**2)\n    ss_tot = np.sum(weights_flat * (y_true_flat - y_mean_weighted)**2)\n    if ss_tot == 0:\n        return np.nan\n    return 1.0 - (ss_res / ss_tot)\n\n# ---------- Robust ViTWithMeta (adds local weights loading) ----------\nclass ViTWithMeta(nn.Module):\n    def __init__(self, backbone_name='vit_base_patch16_224', pretrained=False, pretrained_weights_path=None, meta_dim=META_DIM, num_targets=len(TARGET_COLS), freeze_backbone=True):\n        super().__init__()\n        # Always create backbone with pretrained=False to avoid timm trying to download\n        # (we'll load local weights manually if provided)\n        try:\n            self.backbone = timm.create_model(backbone_name, pretrained=False, num_classes=0, global_pool='avg')\n        except Exception as e:\n            # As a fallback try without num_classes arg (older timm)\n            print(f\"timm.create_model fallback: {e}\")\n            self.backbone = timm.create_model(backbone_name, pretrained=False)\n\n        # If the user provided a local weights file, load it safely\n        if pretrained_weights_path is not None:\n            if os.path.exists(pretrained_weights_path):\n                print(f\"[ViTWithMeta] Loading local backbone weights from: {pretrained_weights_path}\")\n                state = safe_torch_load(pretrained_weights_path, map_location='cpu')\n                # handle bundle dicts\n                if isinstance(state, dict) and 'backbone_state' in state:\n                    state_dict = state['backbone_state']\n                else:\n                    state_dict = state\n                # strip common prefixes (module.) and attempt load\n                new_state = {}\n                for k, v in state_dict.items():\n                    new_key = k\n                    if k.startswith('module.'):\n                        new_key = k[len('module.'):]\n                    new_state[new_key] = v\n                load_res = self.backbone.load_state_dict(new_state, strict=False)\n                print(f\"[ViTWithMeta] load_state_dict result: missing_keys={len(load_res.missing_keys) if hasattr(load_res,'missing_keys') else 'N/A'}, unexpected_keys={len(load_res.unexpected_keys) if hasattr(load_res,'unexpected_keys') else 'N/A'}\")\n            else:\n                print(f\"[ViTWithMeta] Warning: pretrained_weights_path provided but file not found: {pretrained_weights_path}\")\n\n        # infer backbone feature dim robustly\n        backbone_feat_dim = None\n        if hasattr(self.backbone, 'num_features') and getattr(self.backbone, 'num_features'):\n            backbone_feat_dim = int(self.backbone.num_features)\n        elif hasattr(self.backbone, 'embed_dim') and getattr(self.backbone, 'embed_dim'):\n            backbone_feat_dim = int(self.backbone.embed_dim)\n        elif hasattr(self.backbone, 'head') and hasattr(self.backbone.head, 'in_features'):\n            backbone_feat_dim = int(self.backbone.head.in_features)\n        elif hasattr(self.backbone, 'default_cfg') and isinstance(self.backbone.default_cfg, dict) and 'embed_dim' in self.backbone.default_cfg:\n            backbone_feat_dim = int(self.backbone.default_cfg['embed_dim'])\n        else:\n            try:\n                self.backbone.eval()\n                with torch.no_grad():\n                    dummy = torch.randn(1, 3, IMG_SIZE, IMG_SIZE).to(DEVICE)\n                    feat = self.backbone(dummy)\n                    if isinstance(feat, torch.Tensor):\n                        backbone_feat_dim = int(feat.shape[1])\n            except Exception as e:\n                print(\"Could not infer backbone feature dim automatically:\", e)\n                backbone_feat_dim = None\n\n        if backbone_feat_dim is None:\n            raise RuntimeError(\"Failed to determine backbone feature dimension. Check the backbone model and loaded weights.\")\n\n        print(f\"[ViTWithMeta] Detected backbone feature dim: {backbone_feat_dim} (backbone: {backbone_name})\")\n\n        if freeze_backbone:\n            for p in self.backbone.parameters():\n                p.requires_grad = False\n\n        self.meta_mlp = nn.Sequential(\n            nn.Linear(meta_dim, 128),\n            nn.ReLU(),\n            nn.Dropout(0.1),\n            nn.Linear(128, 64),\n            nn.ReLU()\n        )\n\n        self.head = nn.Sequential(\n            nn.Linear(backbone_feat_dim + 64, 512),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(512, 256),\n            nn.ReLU(),\n            nn.Dropout(0.2),\n            nn.Linear(256, num_targets)\n        )\n\n    def forward(self, x, meta):\n        img_feat = self.backbone(x)\n        if isinstance(img_feat, (list, tuple)):\n            img_feat = img_feat[0]\n        if not isinstance(img_feat, torch.Tensor):\n            img_feat = torch.tensor(img_feat, dtype=torch.float32).to(x.device)\n        meta_feat = self.meta_mlp(meta)\n        feat = torch.cat([img_feat, meta_feat], dim=1)\n        out = self.head(feat)\n        return out\n\n# ---------- Training helpers ----------\ndef train_one_epoch(model, loader, criterion, optimizer):\n    model.train()\n    running_loss = 0.0\n    pbar = tqdm(loader, desc='Train', leave=False)\n    for batch in pbar:\n        images, meta, targets = batch\n        images = images.to(DEVICE)\n        meta = meta.to(DEVICE)\n        targets = targets.to(DEVICE)\n        optimizer.zero_grad()\n        preds = model(images, meta)\n        loss = criterion(preds, targets)\n        loss.backward()\n        optimizer.step()\n        running_loss += loss.item() * images.size(0)\n        pbar.set_postfix({'loss':f'{loss.item():.4f}'})\n    return running_loss / len(loader.dataset)\n\n\ndef validate_one_epoch(model, loader, criterion):\n    model.eval()\n    running_loss = 0.0\n    all_preds = []\n    all_targets = []\n    with torch.no_grad():\n        for batch in tqdm(loader, desc='Val', leave=False):\n            images, meta, targets = batch\n            images = images.to(DEVICE)\n            meta = meta.to(DEVICE)\n            targets = targets.to(DEVICE)\n            preds = model(images, meta)\n            loss = criterion(preds, targets)\n            running_loss += loss.item() * images.size(0)\n            all_preds.append(preds.cpu().numpy())\n            all_targets.append(targets.cpu().numpy())\n    all_preds = np.vstack(all_preds) if len(all_preds) > 0 else np.zeros((0, len(TARGET_COLS)))\n    all_targets = np.vstack(all_targets) if len(all_targets) > 0 else np.zeros((0, len(TARGET_COLS)))\n    return running_loss / len(loader.dataset), all_preds, all_targets\n\n# ---------- TTA helpers ----------\ndef _apply_tta_tensor(img_tensor, tta_index):\n    if tta_index == 0:\n        return img_tensor\n    if tta_index == 1:\n        return torch.flip(img_tensor, dims=[3])  # horizontal\n    if tta_index == 2:\n        return torch.flip(img_tensor, dims=[2])  # vertical\n    if tta_index == 3:\n        return torch.flip(img_tensor, dims=[2,3])  # both\n\nTTA_TRANSFORMS = [0,1,2,3]\n\ndef predict_test_with_tta(model, test_df, img_dir, base_transform, tta_transforms=[0,1,2,3], batch_size=8):\n    model.eval()\n    preds_dict = {}\n    image_paths = test_df['image_path'].unique().tolist()\n    with torch.no_grad():\n        for i in range(0, len(image_paths), batch_size):\n            batch_paths = image_paths[i:i+batch_size]\n            imgs = []\n            metas = []\n            for p in batch_paths:\n                img_name = p.split('/')[-1]\n                fp = os.path.join(img_dir, img_name)\n                if not os.path.exists(fp):\n                    fp = p\n                img = Image.open(fp).convert('RGB')\n                img_t = base_transform(img)\n                imgs.append(img_t)\n                row = test_df[test_df['image_path']==p].iloc[0]\n                meta_v = build_meta_vector(row)\n                metas.append(torch.tensor(meta_v, dtype=torch.float32))\n            imgs = torch.stack(imgs).to(DEVICE)\n            metas = torch.stack(metas).to(DEVICE)\n            tta_preds = []\n            for t_i in tta_transforms:\n                imgs_t = _apply_tta_tensor(imgs, t_i)\n                out = model(imgs_t, metas)\n                tta_preds.append(out.cpu().numpy())\n            avg_preds = np.mean(tta_preds, axis=0)\n            for idx, p in enumerate(batch_paths):\n                preds_dict[p] = avg_preds[idx]\n    return preds_dict\n\n# ---------- K-Fold training (with safe torch.load) ----------\ndef safe_torch_load(path, map_location=None):\n    # Try PyTorch>=2.6 style weights_only=False first; otherwise fallback\n    try:\n        return torch.load(path, map_location=map_location, weights_only=False)\n    except TypeError:\n        # older PyTorch doesn't support weights_only arg\n        return torch.load(path, map_location=map_location)\n\n\ndef run_kfold_training(df, img_dir, test_df=None, backbone_name='vit_base_patch16_224', pretrained_weights_path=None):\n    kf = KFold(n_splits=NUM_FOLDS, shuffle=True, random_state=SEED)\n    oof_preds = np.zeros((len(df), len(TARGET_COLS)))\n    oof_targets = np.zeros((len(df), len(TARGET_COLS)))\n    fold = 0\n    test_preds_accum = []\n    sample_indices = np.arange(len(df))\n\n    for tr_idx, val_idx in kf.split(sample_indices):\n        fold += 1\n        print(f\"\\n===== Fold {fold} / {NUM_FOLDS} =====\")\n        tr_df = df.iloc[tr_idx].reset_index(drop=True)\n        vl_df = df.iloc[val_idx].reset_index(drop=True)\n\n        tr_ds = BiomassDataset(tr_df, img_dir, TARGET_COLS, transform=train_transform, is_test=False, use_log_targets=True)\n        vl_ds = BiomassDataset(vl_df, img_dir, TARGET_COLS, transform=val_transform, is_test=False, use_log_targets=True)\n        tr_loader = DataLoader(tr_ds, batch_size=BATCH_SIZE, shuffle=True, num_workers=NUM_WORKERS, pin_memory=True)\n        vl_loader = DataLoader(vl_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS, pin_memory=True)\n\n        model = ViTWithMeta(backbone_name=backbone_name, pretrained=False, pretrained_weights_path=pretrained_weights_path, meta_dim=META_DIM, freeze_backbone=True).to(DEVICE)\n        criterion = WeightedMSELoss(TARGET_WEIGHTS).to(DEVICE)\n        optimizer = optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr=1e-3, weight_decay=1e-5)\n        scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=3)\n\n        best_val_r2 = -np.inf\n        best_ckpt = os.path.join(MODEL_DIR, f'fold{fold}_best.pth')\n        any_ckpt_saved = False\n\n        for epoch in range(NUM_EPOCHS):\n            tr_loss = train_one_epoch(model, tr_loader, criterion, optimizer)\n            val_loss, val_preds_log, val_targets_log = validate_one_epoch(model, vl_loader, criterion)\n            val_r2 = calculate_weighted_r2_original_scale(val_targets_log, val_preds_log, WEIGHT_VECTOR)\n            scheduler.step(val_loss)\n            if (epoch+1) % PRINT_EVERY == 0:\n                print(f\"Fold {fold} Epoch {epoch+1}/{NUM_EPOCHS}  Train Loss: {tr_loss:.4f}  Val Loss: {val_loss:.4f}  Val W-R2: {val_r2 if np.isfinite(val_r2) else 'nan'}\")\n            if np.isfinite(val_r2) and val_r2 > best_val_r2:\n                best_val_r2 = val_r2\n                torch.save({'epoch': epoch, 'model_state': model.state_dict(), 'val_r2': best_val_r2}, best_ckpt)\n                any_ckpt_saved = True\n\n        if not any_ckpt_saved:\n            print(f\"Warning: no valid checkpoint saved for fold {fold}. Saving final model as fallback.\")\n            torch.save({'epoch': epoch, 'model_state': model.state_dict(), 'val_r2': None}, best_ckpt)\n            any_ckpt_saved = True\n            best_val_r2 = np.nan\n\n        print(f\"Fold {fold} best Val W-R2: {best_val_r2}\")\n\n        # Safe load\n        ckpt = safe_torch_load(best_ckpt, map_location=DEVICE)\n        model.load_state_dict(ckpt['model_state'])\n        model.eval()\n\n        vl_loader_full = DataLoader(vl_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS, pin_memory=True)\n        preds_log = []\n        targs_log = []\n        with torch.no_grad():\n            for images, meta, targets in vl_loader_full:\n                images = images.to(DEVICE)\n                meta = meta.to(DEVICE)\n                out = model(images, meta)\n                preds_log.append(out.cpu().numpy())\n                targs_log.append(targets.cpu().numpy())\n        preds_log = np.vstack(preds_log)\n        targs_log = np.vstack(targs_log)\n        oof_preds[val_idx] = preds_log\n        oof_targets[val_idx] = targs_log\n\n        if test_df is not None:\n            test_preds_fold = predict_test_with_tta(model, test_df, TEST_IMG_DIR, val_transform, tta_transforms=TTA_TRANSFORMS, batch_size=BATCH_SIZE)\n            test_preds_accum.append(test_preds_fold)\n\n    overall_r2 = calculate_weighted_r2_original_scale(oof_targets, oof_preds, WEIGHT_VECTOR)\n    print(\"\\n=== OOF Weighted R² (original scale):\", overall_r2)\n    return oof_preds, oof_targets, test_preds_accum\n\n# ---------- Run training ----------\nprint(\"Start K-Fold training...\")\n# If you have saved local backbone weights (see comments), pass path here: pretrained_weights_path='path/to/vit_backbone.pth'\npretrained_weights_path='/kaggle/input/vit-backbone-weights/vit_base_patch16_224_backbone.pth'\noof_preds_log, oof_targets_log, test_preds_per_fold = run_kfold_training(train_wide, TRAIN_IMG_DIR, test_df=test_df, backbone_name='vit_base_patch16_224', pretrained_weights_path=None)\n\n# If your preds are log-targets, exponentiate back\noof_preds = np.expm1(oof_preds_log)\noof_targets = np.expm1(oof_targets_log)\n\nfor i, col in enumerate(TARGET_COLS):\n    try:\n        r2 = r2_score(oof_targets[:,i], oof_preds[:,i])\n    except:\n        r2 = float('nan')\n    print(f\"{col} OOF R2: {r2:.4f}\")\n\noverall_r2 = calculate_weighted_r2_original_scale(oof_targets_log, oof_preds_log, WEIGHT_VECTOR)\nprint(\"Overall weighted R2 (oof):\", overall_r2)\n\n# ---------- Submission ----------\nif len(test_preds_per_fold) > 0:\n    keys = list(test_preds_per_fold[0].keys())\n    final_test_preds_log = {}\n    for k in keys:\n        arrs = [fold_preds[k] for fold_preds in test_preds_per_fold]\n        arrs = np.stack(arrs, axis=0)\n        final_test_preds_log[k] = np.mean(arrs, axis=0)\n    sub_rows = []\n    for _, row in test_df.iterrows():\n        img_path = row['image_path']\n        tname = row['target_name']\n        preds_log = final_test_preds_log[img_path]\n        target_idx = TARGET_COLS.index(tname)\n        pred_val = float(np.expm1(preds_log[target_idx]))\n        sub_rows.append({'sample_id': row['sample_id'], 'target': max(0.0, pred_val)})\n    submission_df = pd.DataFrame(sub_rows)\n    submission_df.to_csv('submission.csv', index=False)\n    print(\"Saved submission.csv\")\nelse:\n    print(\"No test predictions produced (maybe test images missing).\")\n\n# ---------- Quick OOF plot ----------\nif oof_targets.shape[0] > 0:\n    col_idx = TARGET_COLS.index('Dry_Total_g')\n    plt.figure(figsize=(6,6))\n    plt.scatter(oof_targets[:,col_idx], oof_preds[:,col_idx], alpha=0.6)\n    mn = min(oof_targets[:,col_idx].min(), oof_preds[:,col_idx].min())\n    mx = max(oof_targets[:,col_idx].max(), oof_preds[:,col_idx].max())\n    plt.plot([mn,mx],[mn,mx],'r--')\n    plt.xlabel('True Dry_Total_g')\n    plt.ylabel('Pred Dry_Total_g')\n    plt.title('OOF preds vs true (Dry_Total_g)')\n    plt.grid(True)\n    plt.show()\n\nprint(\"Pipeline finished.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-12T11:31:19.639758Z","iopub.execute_input":"2025-12-12T11:31:19.640013Z","iopub.status.idle":"2025-12-12T11:50:35.932801Z","shell.execute_reply.started":"2025-12-12T11:31:19.639995Z","shell.execute_reply":"2025-12-12T11:50:35.931990Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}