{"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":"none","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":622663,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":468398,"modelId":484246}],"dockerImageVersionId":31153,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# =========================\n# Setup & Imports\n# =========================\nfrom pathlib import Path\nimport os\nimport pandas as pd\nimport numpy as np\nimport torch\nimport torchvision\nimport torchvision.transforms as T\nfrom torch.utils.data import Dataset, DataLoader\nfrom PIL import Image\n\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.decomposition import PCA\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import r2_score, mean_squared_error\n\nfrom sklearn.multioutput import MultiOutputRegressor\nfrom xgboost import XGBRegressor\n\n# =========================\n# Constants (edit here)\n# =========================\nCONFIG = {\n    \"DATA_DIR\": \"/kaggle/input/csiro-biomass\",\n    \"TRAIN_CSV\": \"train.csv\",\n    \"TEST_CSV\": \"test.csv\",\n    \"OUT_DIR\": \"./\",\n\n    # Backbone choice + local weights path (uploaded as a Kaggle Dataset)\n    \"BACKBONE\": \"resnet18\",  # \"resnet18\" or \"resnet50\"\n    \"WEIGHTS_DIR\": \"/kaggle/input/resnet-weight/pytorch/default/1\",  # <- your uploaded dataset path\n    \"RESNET18_WEIGHTS\": \"resnet18_imagenet1k_v1_state_dict.pth\",\n    \"RESNET50_WEIGHTS\": \"resnet50_imagenet1k_v2_state_dict.pth\",\n\n    \"IMAGE_SIZE\": 384,\n    \"BATCH_SIZE\": 8,\n    \"NUM_WORKERS\": 0,\n    \"VAL_SIZE\": 0.2,\n    \"RANDOM_STATE\": 42,\n    \"USE_GPU_FOR_XGB\": True,\n    \"EXPORT_PCA_2D\": True\n}\n\nTARGETS = ['Dry_Green_g','Dry_Dead_g','Dry_Clover_g','GDM_g','Dry_Total_g']\n\n# =========================\n# Pivot long → wide helper\n# =========================\ndef pivot_train_long_to_wide(train_long: pd.DataFrame, targets: list) -> pd.DataFrame:\n    print(\"[DEBUG] Raw train_long shape:\", train_long.shape)\n    train_long = train_long.copy()\n    train_long['image_id'] = train_long['image_path'].apply(lambda p: Path(p).stem)\n\n    cnt = train_long.groupby('image_id')['target_name'].nunique()\n    print(\"[DEBUG] target_name nunique per image (value_counts):\\n\", cnt.value_counts())\n\n    pivot = train_long.pivot_table(\n        index=['image_id','image_path','Sampling_Date','State','Species',\n               'Pre_GSHH_NDVI','Height_Ave_cm'],\n        columns='target_name', values='target', aggfunc='first'\n    ).reset_index()\n\n    pivot.columns = [c if isinstance(c, str) else c[1] for c in pivot.columns]\n    for t in targets:\n        if t not in pivot.columns:\n            pivot[t] = np.nan\n\n    before = len(pivot)\n    pivot = pivot.dropna(subset=targets, how='any').reset_index(drop=True)\n    after = len(pivot)\n    print(f\"[DEBUG] After pivot shape: {pivot.shape} (dropped {before-after} rows missing targets)\")\n    return pivot\n\n# =========================\n# Dataset + Transforms\n# =========================\nclass ImageTable(Dataset):\n    def __init__(self, df, root, transform=None):\n        self.df = df.reset_index(drop=True)\n        self.root = Path(root)\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        path = self.root / row['image_path']\n        if not path.exists():\n            print(f\"[WARN] Image not found: {path}\")\n        with Image.open(path) as im:\n            im = im.convert('RGB')\n        if self.transform:\n            im = self.transform(im)\n        return im, str(row['image_path'])\n\ndef build_transform(size):\n    return T.Compose([\n        T.Resize(int(size*1.15)),\n        T.CenterCrop(size),\n        T.ToTensor(),\n        T.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])\n    ])\n\n# =========================\n# Offline weight loading\n# =========================\ndef load_backbone_from_local(backbone:str, weights_dir:Path):\n    backbone = backbone.lower()\n    if backbone == \"resnet18\":\n        model = torchvision.models.resnet18(weights=None)\n        weight_file = weights_dir / CONFIG[\"RESNET18_WEIGHTS\"]\n    elif backbone == \"resnet50\":\n        model = torchvision.models.resnet50(weights=None)\n        weight_file = weights_dir / CONFIG[\"RESNET50_WEIGHTS\"]\n    else:\n        raise ValueError(f\"Unsupported BACKBONE: {backbone}\")\n\n    if not weight_file.exists():\n        raise FileNotFoundError(\n            f\"Weight file not found: {weight_file}\\n\"\n            f\"Make sure your Kaggle Dataset is added to the notebook and paths are correct.\"\n        )\n\n    print(f\"[INFO] Loading local weights: {weight_file}\")\n    state = torch.load(weight_file, map_location=\"cpu\")\n    # Try strict first, then fall back (avoids minor version mismatches)\n    try:\n        model.load_state_dict(state, strict=True)\n    except Exception as e:\n        print(f\"[WARN] Strict load failed: {e}\\nTrying strict=False...\")\n        model.load_state_dict(state, strict=False)\n\n    # Convert to feature-extractor (remove final FC)\n    feat = torch.nn.Sequential(*list(model.children())[:-1])\n    return feat\n\n# =========================\n# Feature Extraction\n# =========================\n@torch.no_grad()\ndef extract_features(model, loader, device):\n    feats = []\n    paths = []\n    for i, (imgs, img_paths) in enumerate(loader):\n        print(f\"[DEBUG] batch {i}, imgs shape {imgs.shape}\")\n        imgs = imgs.to(device)\n        out = model(imgs)\n        if out.ndim > 2:\n            out = out.view(out.size(0), -1)\n        feats.append(out.cpu().numpy())\n        paths.extend(list(img_paths))\n    if not feats:\n        raise ValueError(\"[ERROR] No features extracted; check images/paths.\")\n    feats = np.concatenate(feats, axis=0)\n    return feats, paths\n\n# =========================\n# Main Notebook Flow\n# =========================\n\n# --- I/O & dirs ---\nDATA_DIR = Path(CONFIG['DATA_DIR'])\nOUT_DIR = Path(CONFIG['OUT_DIR'])\nOUT_DIR.mkdir(parents=True, exist_ok=True)\n\n# --- Read + pivot train ---\ntrain_path = DATA_DIR / CONFIG['TRAIN_CSV']\nprint(\"[INFO] Reading:\", train_path)\ntrain_long = pd.read_csv(train_path)\ntrain_wide = pivot_train_long_to_wide(train_long, TARGETS)\nprint(\"[INFO] Train images count:\", len(train_wide))\n\n# --- Backbone & transforms (OFFLINE) ---\ntransform = build_transform(CONFIG['IMAGE_SIZE'])\n\nweights_dir = Path(CONFIG[\"WEIGHTS_DIR\"])\nfeature_extractor = load_backbone_from_local(CONFIG[\"BACKBONE\"], weights_dir)\nfeature_extractor.eval()\ndevice = 'cuda' if torch.cuda.is_available() else 'cpu'\nfeature_extractor.to(device)\nprint(\"[INFO] Using device:\", device)\n\n# --- Train feature extraction ---\ntrain_ds = ImageTable(train_wide, root=DATA_DIR, transform=transform)\ntrain_loader = DataLoader(train_ds, batch_size=CONFIG['BATCH_SIZE'], shuffle=False,\n                          num_workers=CONFIG['NUM_WORKERS'])\nfeats_train, train_paths = extract_features(feature_extractor, train_loader, device)\nnp.save(OUT_DIR/'features_train.npy', feats_train)\nprint(\"[INFO] feats_train:\", feats_train.shape)  # (N, 512) for resnet18, (N, 2048) for resnet50\n\n# --- Scale features ---\nscaler = StandardScaler(with_mean=True, with_std=True)\nXs = scaler.fit_transform(feats_train)\n\n# Optional: PCA export for visualization\nif CONFIG['EXPORT_PCA_2D']:\n    pca = PCA(n_components=2, random_state=CONFIG['RANDOM_STATE'])\n    X2 = pca.fit_transform(Xs)\n    pd.DataFrame({\n        'x': X2[:,0],\n        'y': X2[:,1],\n        'image_path': train_wide['image_path']\n    }).to_csv(OUT_DIR/'pca_2d_train.csv', index=False)\n    print(\"[INFO] Exported 2D PCA to:\", (OUT_DIR/'pca_2d_train.csv').resolve())\n\n# --- Targets ---\ny = train_wide[TARGETS].values\n\n# --- Quick validation split ---\nX_tr, X_va, y_tr, y_va = train_test_split(\n    Xs, y, test_size=CONFIG['VAL_SIZE'], random_state=CONFIG['RANDOM_STATE']\n)\n\n# --- XGBoost Regressor (multi-output) ---\nuse_gpu = (CONFIG['USE_GPU_FOR_XGB'] and device == 'cuda')\ntree_method = 'gpu_hist' if use_gpu else 'hist'\n\nxgb_base = XGBRegressor(\n    n_estimators=800,\n    learning_rate=0.03,\n    max_depth=8,\n    subsample=0.8,\n    colsample_bytree=0.8,\n    reg_lambda=1.0,\n    objective='reg:squarederror',\n    tree_method=tree_method,\n    n_jobs=-1,\n    random_state=CONFIG['RANDOM_STATE']\n)\n\nmodel = MultiOutputRegressor(xgb_base, n_jobs=-1)\n\nprint(\"[INFO] Fitting XGBoost on training split...\")\nmodel.fit(X_tr, y_tr)\n\n# --- Validation metrics ---\npred_va = model.predict(X_va)\nfor j, t in enumerate(TARGETS):\n    rmse = mean_squared_error(y_va[:, j], pred_va[:, j], squared=False)\n    r2 = r2_score(y_va[:, j], pred_va[:, j])\n    print(f\"[VAL] {t}: RMSE={rmse:.4f}  R2={r2:.4f}\")\n\n# --- Retrain on ALL training data ---\nprint(\"[INFO] Refitting XGBoost on ALL training data...\")\nmodel.fit(Xs, y)\n\n# =========================\n# Test Inference & Submission\n# =========================\ntest_long = pd.read_csv(DATA_DIR / CONFIG['TEST_CSV'])\ntest_images_unique = test_long.drop_duplicates('image_path')['image_path'].tolist()\ntest_df_images = pd.DataFrame({'image_path': test_images_unique})\nprint(\"[INFO] Unique test images:\", len(test_df_images))\n\ntest_ds = ImageTable(test_df_images, root=DATA_DIR, transform=transform)\ntest_loader = DataLoader(test_ds, batch_size=CONFIG['BATCH_SIZE'], shuffle=False,\n                         num_workers=CONFIG['NUM_WORKERS'])\n\nfeats_test, test_paths = extract_features(feature_extractor, test_loader, device)\nnp.save(OUT_DIR/'features_test.npy', feats_test)\nprint(\"[INFO] feats_test:\", feats_test.shape)\n\nXs_test = scaler.transform(feats_test)\n\nprint(\"[INFO] Predicting on test...\")\ntest_pred = model.predict(Xs_test)\ntest_pred = np.clip(test_pred, 0.0, None)  # enforce non-negativity\n\n# Map predictions back to long-form rows\nimg_to_idx = {img: i for i, img in enumerate(test_images_unique)}\nrows = []\nmiss = 0\nfor _, row in test_long.iterrows():\n    img = row['image_path']\n    tname = row['target_name']\n    if img not in img_to_idx:\n        miss += 1\n        continue\n    i = img_to_idx[img]\n    t_idx = TARGETS.index(tname)\n    rows.append({'sample_id': row['sample_id'], 'target': float(test_pred[i, t_idx])})\nif miss > 0:\n    print(f(\"[WARN] {miss} rows in test.csv had image_path not found in dedup list.\"))\n\nsub = pd.DataFrame(rows)\nout_file = OUT_DIR / 'submission.csv'\nsub.to_csv(out_file, index=False)\nprint(\"[OK] Wrote submission:\", out_file.resolve())\nprint(sub.head())\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-10-29T00:53:53.409770Z","iopub.execute_input":"2025-10-29T00:53:53.410094Z","iopub.status.idle":"2025-10-29T01:02:53.150040Z","shell.execute_reply.started":"2025-10-29T00:53:53.410071Z","shell.execute_reply":"2025-10-29T01:02:53.149238Z"}},"outputs":[],"execution_count":null}]}