{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","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":31234,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os, gc, torch, numpy as np, pandas as pd, lightgbm as lgb\nfrom PIL import Image\nimport torchvision.transforms as T\nfrom sklearn.model_selection import KFold\nfrom sklearn.metrics import r2_score\nfrom tqdm import tqdm\n\n# ==========================================\n# 1. CONFIGURATION\n# ==========================================\nclass CFG:\n    DEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n    DINO_MODEL = 'dinov2_vitb14' \n    IMG_SIZE = 518 \n    BATCH_SIZE = 4  # Safe for 518px on Kaggle GPUs\n    SEED = 42\n    BASE_PATH = \"/kaggle/input/csiro-biomass/\"\n\n# ==========================================\n# 2. DEFENSIVE UTILITIES\n# ==========================================\ndef safe_open_image(path):\n    \"\"\"Searches for images in multiple possible Kaggle directories.\"\"\"\n    try:\n        for prefix in [\"test\", \"train\", \"\"]:\n            full_path = os.path.join(CFG.BASE_PATH, prefix, os.path.basename(path))\n            if os.path.exists(full_path):\n                return Image.open(full_path).convert('RGB')\n        if os.path.exists(path):\n            return Image.open(path).convert('RGB')\n    except:\n        pass\n    return Image.fromarray(np.zeros((CFG.IMG_SIZE, CFG.IMG_SIZE, 3), dtype=np.uint8))\n\ndef get_embeddings(df, model):\n    \"\"\"Extracts DINOv2 features with batch processing.\"\"\"\n    transform = T.Compose([\n        T.Resize(CFG.IMG_SIZE),\n        T.CenterCrop(CFG.IMG_SIZE),\n        T.ToTensor(),\n        T.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n    ])\n    \n    model.eval()\n    feat_map = {}\n    paths = df['image_path'].unique()\n    \n    with torch.no_grad():\n        for i in tqdm(range(0, len(paths), CFG.BATCH_SIZE)):\n            batch_paths = paths[i:i+CFG.BATCH_SIZE]\n            tensors = [transform(safe_open_image(p)) for p in batch_paths]\n            inputs = torch.stack(tensors).to(CFG.DEVICE)\n            features = model(inputs).cpu().numpy()\n            \n            for p, f in zip(batch_paths, features):\n                feat_map[p] = f\n                \n    feat_df = pd.DataFrame.from_dict(feat_map, orient='index')\n    feat_df.columns = [f'dino_{i}' for i in range(feat_df.shape[1])]\n    return feat_df.reset_index().rename(columns={'index': 'image_path'})\n\n# ==========================================\n# 3. FEATURE EXTRACTION\n# ==========================================\ntrain_base = pd.read_csv(os.path.join(CFG.BASE_PATH, \"train.csv\"))\ntest_base = pd.read_csv(os.path.join(CFG.BASE_PATH, \"test.csv\"))\n\nprint(\"--- Step 1: Loading DINOv2 Model ---\")\n# Loads from cache if Internet is OFF (after 1st connected run)\ndino = torch.hub.load('facebookresearch/dinov2', CFG.DINO_MODEL).to(CFG.DEVICE)\n\nprint(\"--- Step 2: Extracting Train Features ---\")\ntrain_feats = get_embeddings(train_base, dino)\nprint(\"--- Step 3: Extracting Test Features ---\")\ntest_feats = get_embeddings(test_base, dino)\n\n# MANDATORY CLEANUP: Wipe DINO from GPU to make room for LightGBM\nprint(\"--- Step 4: Cleaning up Deep Learning Model ---\")\ndel dino; gc.collect(); torch.cuda.empty_cache()\n\n# Merge features back to original data\ntrain = train_base.merge(train_feats, on='image_path', how='left').fillna(0)\ntest = test_base.merge(test_feats, on='image_path', how='left').fillna(0)\n\n# ==========================================\n# 4. CROSS-VALIDATION & TRAINING\n# ==========================================\nfeatures = [c for c in train.columns if c.startswith('dino_')] + ['target_name']\ntrain['target_name'] = train['target_name'].astype(str).astype('category')\ntest['target_name'] = test['target_name'].astype(str).astype('category')\n\ny = np.log1p(train['target']) # Log transform for skewed targets\noof_preds = np.zeros(len(train))\ntest_preds = np.zeros(len(test))\nkf = KFold(n_splits=5, shuffle=True, random_state=CFG.SEED)\n\nprint(\"\\n--- Step 5: Training LightGBM with 5-Fold CV ---\")\nfor fold, (t_idx, v_idx) in enumerate(kf.split(train)):\n    lgb_model = lgb.LGBMRegressor(\n        n_estimators=1000, \n        learning_rate=0.05, \n        max_depth=7,\n        num_leaves=31,\n        verbose=-1\n    )\n    \n    lgb_model.fit(\n        train.loc[t_idx, features], y.iloc[t_idx],\n        eval_set=[(train.loc[v_idx, features], y.iloc[v_idx])],\n        callbacks=[lgb.early_stopping(stopping_rounds=50)]\n    )\n    \n    # Store validation predictions for CV score\n    val_preds = lgb_model.predict(train.loc[v_idx, features])\n    oof_preds[v_idx] = val_preds\n    \n    fold_r2 = r2_score(y.iloc[v_idx], val_preds)\n    print(f\"Fold {fold+1} | R2 Score: {fold_r2:.4f}\")\n    \n    # Average test predictions\n    test_preds += lgb_model.predict(test[features]) / 5\n\n# ==========================================\n# 5. SCORING & FINAL SUBMISSION\n# ==========================================\noverall_cv = r2_score(y, oof_preds)\nprint(f\"\\n✅ OVERALL CV R2 SCORE: {overall_cv:.4f}\")\n\nsubmission = test_base[['sample_id']].copy()\nsubmission['target'] = np.maximum(0, np.expm1(test_preds)) # Reverse log transform\nsubmission.to_csv(\"submission.csv\", index=False)\n\nprint(\"\\n🎉 Generated submission file: submission.csv\")\nprint(submission.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T20:15:16.522601Z","iopub.execute_input":"2026-01-27T20:15:16.522951Z","iopub.status.idle":"2026-01-27T20:18:25.021098Z","shell.execute_reply.started":"2026-01-27T20:15:16.522926Z","shell.execute_reply":"2026-01-27T20:18:25.020325Z"}},"outputs":[],"execution_count":null}]}