{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"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":"gpu","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"},{"sourceId":14151075,"sourceType":"datasetVersion","datasetId":9018754},{"sourceId":14214116,"sourceType":"datasetVersion","datasetId":9066801},{"sourceId":293031629,"sourceType":"kernelVersion"},{"sourceId":268942,"sourceType":"modelInstanceVersion","modelInstanceId":230141,"modelId":251887},{"sourceId":711299,"sourceType":"modelInstanceVersion","modelInstanceId":540408,"modelId":553604},{"sourceId":719660,"sourceType":"modelInstanceVersion","modelInstanceId":547330,"modelId":560139}],"dockerImageVersionId":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":441.06159,"end_time":"2026-01-05T05:15:36.197688","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2026-01-05T05:08:15.136098","version":"2.6.0"},"widgets":{"application/vnd.jupyter.widget-state+json":{"state":{"5cea4344fed44fffaac71b6fe02ac50b":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"5fd6570613f943ecb07631ffcecddacb":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"6b204ec19a394a818e17994aae3263e6":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"864a5d2d04374e688a854f72ad0d04d9":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"8f988be66966496085d6e6b8909bf0da":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_864a5d2d04374e688a854f72ad0d04d9","placeholder":"​","style":"IPY_MODEL_5cea4344fed44fffaac71b6fe02ac50b","tabbable":null,"tooltip":null,"value":" 1/1 [00:01&lt;00:00,  1.98s/it]"}},"9a1a60c1824e4af4a02be7dd08d54449":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"af818733f9ec4e22a3500a5692c607b2":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HBoxModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HBoxModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HBoxView","box_style":"","children":["IPY_MODEL_e8a7fcf1682642f9a95f077413387b58","IPY_MODEL_d548382ab4904dd9b1c2742bec3463f0","IPY_MODEL_8f988be66966496085d6e6b8909bf0da"],"layout":"IPY_MODEL_5fd6570613f943ecb07631ffcecddacb","tabbable":null,"tooltip":null}},"b695855014a24a7184902662cc419d1e":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"d372dd276f534b6ba203146ce2f78865":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"ProgressStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"ProgressStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","bar_color":null,"description_width":""}},"d548382ab4904dd9b1c2742bec3463f0":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"FloatProgressModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"FloatProgressModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"ProgressView","bar_style":"success","description":"","description_allow_html":false,"layout":"IPY_MODEL_9a1a60c1824e4af4a02be7dd08d54449","max":1,"min":0,"orientation":"horizontal","style":"IPY_MODEL_d372dd276f534b6ba203146ce2f78865","tabbable":null,"tooltip":null,"value":1}},"e8a7fcf1682642f9a95f077413387b58":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_b695855014a24a7184902662cc419d1e","placeholder":"​","style":"IPY_MODEL_6b204ec19a394a818e17994aae3263e6","tabbable":null,"tooltip":null,"value":"100%"}}},"version_major":2,"version_minor":0}}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Thanks,\n\n## Credits: Eng Adam Al mohammedi, Baidalin, Adilzhan, CigarCat, Mattia Angeli, Khoa, samu2505 \n\n## siglep LB: 65 now: -\n\n## dinov3 Lb: 70 now: +","metadata":{}},{"cell_type":"code","source":"import os\nimport gc\nimport random\nimport warnings\nimport numpy as np\nimport pandas as pd\nimport cv2\nimport torch\nfrom PIL import Image\nfrom tqdm.auto import tqdm\nfrom pathlib import Path\nfrom copy import deepcopy\nfrom dataclasses import dataclass\n\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.decomposition import PCA\nfrom sklearn.cross_decomposition import PLSRegression\nfrom sklearn.mixture import GaussianMixture\nfrom sklearn.ensemble import GradientBoostingRegressor, HistGradientBoostingRegressor\nfrom lightgbm import LGBMRegressor\nfrom catboost import CatBoostRegressor\nfrom transformers import AutoModel, AutoImageProcessor, AutoTokenizer\n\nwarnings.filterwarnings('ignore')\nos.environ[\"TOKENIZERS_PARALLELISM\"] = \"false\"\n\n@dataclass\nclass Config:\n    DATA_PATH: Path = Path(\"/kaggle/input/csiro-biomass/\")\n    SPLIT_PATH: Path = Path(\"/kaggle/input/csiro-datasplit/csiro_data_split.csv\")\n    SIGLIP_PATH: str = \"/kaggle/input/google-siglip-so400m-patch14-384/transformers/default/1\"\n    SEED: int = 42\n    DEVICE: str = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n    PATCH_SIZE: int = 520\n    OVERLAP: int = 16\n    TARGET_NAMES = ['Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'Dry_Total_g', 'GDM_g']\n    TARGET_MAX = {\n        \"Dry_Clover_g\": 71.7865,\n        \"Dry_Dead_g\": 83.8407,\n        \"Dry_Green_g\": 157.9836,\n        \"Dry_Total_g\": 185.70,\n        \"GDM_g\": 157.9836,\n    }\ncfg = Config()\n\ndef seeding(SEED):\n    np.random.seed(SEED)\n    random.seed(SEED)\n    os.environ['PYTHONHASHSEED'] = str(SEED)\n    torch.manual_seed(SEED)\n    if torch.cuda.is_available(): \n        torch.cuda.manual_seed(SEED)\n        torch.backends.cudnn.deterministic = True\n        torch.backends.cudnn.benchmark = False\nseeding(cfg.SEED)\n\n\ndef pivot_table(df: pd.DataFrame) -> pd.DataFrame:\n    if 'target' in df.columns.tolist():\n        df_pt = pd.pivot_table(\n            df, \n            values='target', \n            index=['image_path', 'Sampling_Date', 'State', 'Species', 'Pre_GSHH_NDVI', 'Height_Ave_cm'], \n            columns='target_name', \n            aggfunc='mean'\n        ).reset_index()\n    else:\n        df['target'] = 0\n        df_pt = pd.pivot_table(\n            df, \n            values='target', \n            index='image_path', \n            columns='target_name', \n            aggfunc='mean'\n        ).reset_index()\n    return df_pt\n\n\ndef melt_table(df: pd.DataFrame) -> pd.DataFrame:\n    melted = df.melt(\n        id_vars='image_path',\n        value_vars=cfg.TARGET_NAMES,\n        var_name='target_name',\n        value_name='target'\n    )\n    melted['sample_id'] = (\n        melted['image_path']\n        .str.replace(r'^.*/', '', regex=True)\n        .str.replace('.jpg', '', regex=False)\n        + '__' + melted['target_name']\n    )\n    return melted[['sample_id', 'target']]\n\n\ndef post_process_biomass(df_preds):\n    ordered_cols = [\"Dry_Green_g\", \"Dry_Clover_g\", \"Dry_Dead_g\", \"GDM_g\", \"Dry_Total_g\"]\n    for c in ordered_cols:\n        if c not in df_preds.columns:\n            df_preds[c] = 0.0\n    \n    df_out = df_preds.copy()\n    df_out['Dry_Clover_g'] = 0.0\n    df_out['GDM_g'] = df_out['Dry_Green_g'] + df_out['Dry_Clover_g']\n    df_out['Dry_Total_g'] = df_out['GDM_g'] + df_out['Dry_Dead_g']\n    df_out['GDM_g'] = df_out['GDM_g'].clip(lower=0.0)\n    df_out['Dry_Total_g'] = df_out['Dry_Total_g'].clip(lower=0.0)\n    return df_out\n\n\nprint(\"Loading Data...\")\ntrain_df = pd.read_csv(cfg.SPLIT_PATH) \ncols_to_keep = [c for c in train_df.columns if not c.startswith('emb')]\ntrain_df = train_df[cols_to_keep]\nif not str(train_df['image_path'].iloc[0]).startswith('/'):\n     train_df['image_path'] = train_df['image_path'].apply(lambda p: str(cfg.DATA_PATH / 'train' / os.path.basename(p)))\n\n\ntest_df_raw = pd.read_csv(cfg.DATA_PATH / 'test.csv')\ntest_df = pivot_table(test_df_raw)\ntest_df['image_path'] = test_df['image_path'].apply(lambda p: str(cfg.DATA_PATH / p))\ndef split_image(image, patch_size=520, overlap=16):\n    h, w, c = image.shape\n    stride = patch_size - overlap\n    patches = []\n    for y in range(0, h, stride):\n        for x in range(0, w, stride):\n            y2 = min(y + patch_size, h)\n            x2 = min(x + patch_size, w)\n            y1 = max(0, y2 - patch_size) # Ensure fixed size\n            x1 = max(0, x2 - patch_size)\n            patch = image[y1:y2, x1:x2, :]\n            patches.append(patch)\n    return patches\n\n    \ndef compute_embeddings(model_path, df):\n    print(f\"Computing Embeddings for {len(df)} images...\")\n    model = AutoModel.from_pretrained(model_path, local_files_only=True).eval().to(cfg.DEVICE)\n    processor = AutoImageProcessor.from_pretrained(model_path)\n    EMBEDDINGS = []\n    for _, row in tqdm(df.iterrows(), total=len(df)):\n        try:\n            img = cv2.imread(row['image_path'])\n            if img is None: raise ValueError(\"Image not found\")\n            img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n            patches = split_image(img, patch_size=cfg.PATCH_SIZE, overlap=cfg.OVERLAP)\n            images = [Image.fromarray(p) for p in patches]\n            inputs = processor(images=images, return_tensors=\"pt\").to(cfg.DEVICE)\n            with torch.no_grad():\n                features = model.get_image_features(**inputs)\n            avg_embed = features.mean(dim=0).cpu().numpy()\n            EMBEDDINGS.append(avg_embed)\n        except Exception as e:\n            print(f\"Error processing {row['image_path']}: {e}\")\n            EMBEDDINGS.append(np.zeros(1152))\n        \n    torch.cuda.empty_cache()\n    return np.stack(EMBEDDINGS)\n\n\ntrain_embeddings = compute_embeddings(cfg.SIGLIP_PATH, train_df)\ntest_embeddings = compute_embeddings(cfg.SIGLIP_PATH, test_df)\nemb_cols = [f\"emb{i}\" for i in range(train_embeddings.shape[1])]\ntrain_feat_df = pd.concat([train_df, pd.DataFrame(train_embeddings, columns=emb_cols)], axis=1)\ntest_feat_df = pd.concat([test_df, pd.DataFrame(test_embeddings, columns=emb_cols)], axis=1)\nprint(f\"Train Features Shape: {train_feat_df.shape}\")\nprint(f\"Test Features Shape: {test_feat_df.shape}\")\ndef generate_semantic_features(image_embeddings_np, model_path):\n    print(\"Generating Semantic Features...\")\n    model = AutoModel.from_pretrained(model_path).to(cfg.DEVICE)\n    tokenizer = AutoTokenizer.from_pretrained(model_path)\n    concept_groups = {\n        \"bare\": [\"bare soil\", \"dirt ground\", \"sparse vegetation\", \"exposed earth\"],\n        \"sparse\": [\"low density pasture\", \"thin grass\", \"short clipped grass\"],\n        \"medium\": [\"average pasture cover\", \"medium height grass\", \"grazed pasture\"],\n        \"dense\": [\"dense tall pasture\", \"thick grassy volume\", \"high biomass\", \"overgrown vegetation\"],\n        \"green\": [\"lush green vibrant pasture\", \"photosynthesizing leaves\", \"fresh growth\"],\n        \"dead\": [\"dry brown dead grass\", \"yellow straw\", \"senesced material\", \"standing hay\"],\n        \"clover\": [\"white clover\", \"trifolium repens\", \"broadleaf legume\", \"clover flowers\"],\n        \"grass\": [\"ryegrass\", \"blade-like leaves\", \"fescue\", \"grassy sward\"]\n    }\n    concept_vectors = {}\n    with torch.no_grad():\n        for name, prompts in concept_groups.items():\n            inputs = tokenizer(prompts, padding=\"max_length\", return_tensors=\"pt\").to(cfg.DEVICE)\n            emb = model.get_text_features(**inputs)\n            emb = emb / emb.norm(p=2, dim=-1, keepdim=True)\n            concept_vectors[name] = emb.mean(dim=0, keepdim=True)\n    img_tensor = torch.tensor(image_embeddings_np, dtype=torch.float32).to(cfg.DEVICE)\n    img_tensor = img_tensor / img_tensor.norm(p=2, dim=-1, keepdim=True)\n    scores = {}\n    for name, vec in concept_vectors.items():\n        scores[name] = torch.matmul(img_tensor, vec.T).cpu().numpy().flatten()\n        \n    df_scores = pd.DataFrame(scores)\n    df_scores['ratio_greenness'] = df_scores['green'] / (df_scores['green'] + df_scores['dead'] + 1e-6)\n    df_scores['ratio_clover'] = df_scores['clover'] / (df_scores['clover'] + df_scores['grass'] + 1e-6)\n    df_scores['ratio_cover'] = (df_scores['dense'] + df_scores['medium']) / (df_scores['bare'] + df_scores['sparse'] + 1e-6)\n    torch.cuda.empty_cache()\n    return df_scores.values\n\nall_emb = np.vstack([train_embeddings, test_embeddings])\nall_semantic = generate_semantic_features(all_emb, cfg.SIGLIP_PATH)\nsem_train = all_semantic[:len(train_df)]\nsem_test = all_semantic[len(train_df):]\n\nclass SupervisedEmbeddingEngine:\n    def __init__(self, n_pca=0.80, n_pls=8, n_gmm=6, random_state=42):\n        self.scaler = StandardScaler()\n        self.pca = PCA(n_components=n_pca, random_state=random_state)\n        self.pls = PLSRegression(n_components=n_pls, scale=False)\n        self.gmm = GaussianMixture(n_components=n_gmm, covariance_type='diag', random_state=random_state)\n        self.pls_fitted_ = False\n\n    def fit(self, X, y=None, X_semantic=None):\n        X_scaled = self.scaler.fit_transform(X)\n        self.pca.fit(X_scaled)\n        self.gmm.fit(X_scaled)\n        if y is not None:\n            self.pls.fit(X_scaled, y)\n            self.pls_fitted_ = True\n        return self\n\n    def transform(self, X, X_semantic=None):\n        X_scaled = self.scaler.transform(X)\n        features = [self.pca.transform(X_scaled)]\n        if self.pls_fitted_:\n            features.append(self.pls.transform(X_scaled))\n        features.append(self.gmm.predict_proba(X_scaled))\n        if X_semantic is not None:\n            sem_norm = (X_semantic - np.mean(X_semantic, axis=0)) / (np.std(X_semantic, axis=0) + 1e-6)\n            features.append(sem_norm)\n        return np.hstack(features)\n\n\ndef cross_validate_predict(model_cls, model_params, train_data, test_data, sem_tr, sem_te, feature_engine):\n    target_max_arr = np.array([cfg.TARGET_MAX[t] for t in cfg.TARGET_NAMES], dtype=float)\n    y_pred_test_accum = np.zeros([len(test_data), len(cfg.TARGET_NAMES)], dtype=float)\n    n_splits = int(train_data['fold'].nunique())\n    X_train_full = train_data[emb_cols].values.astype(np.float32)\n    X_test_raw = test_data[emb_cols].values.astype(np.float32)\n    y_train_full = train_data[cfg.TARGET_NAMES].values.astype(np.float32)\n    for fold in range(n_splits):\n        print(f\"Processing Fold {fold}...\")\n        train_mask = train_data['fold'] != fold\n        X_tr = X_train_full[train_mask]\n        y_tr = y_train_full[train_mask] / target_max_arr # Max Scaling\n        sem_tr_fold = sem_tr[train_mask]\n        engine = deepcopy(feature_engine)\n        engine.fit(X_tr, y=y_tr, X_semantic=sem_tr_fold)\n        x_tr_eng = engine.transform(X_tr, X_semantic=sem_tr_fold)\n        x_te_eng = engine.transform(X_test_raw, X_semantic=sem_te)\n        fold_test_pred = np.zeros([len(test_data), len(cfg.TARGET_NAMES)])\n        for k in range(len(cfg.TARGET_NAMES)):\n            target_name = cfg.TARGET_NAMES[k]\n            if target_name == 'Dry_Clover_g':\n                fold_test_pred[:, k] = 0.0\n            else:\n                model = model_cls(**model_params)\n                model.fit(x_tr_eng, y_tr[:, k])\n                pred_raw = model.predict(x_te_eng)\n                fold_test_pred[:, k] = pred_raw * target_max_arr[k]\n            \n        y_pred_test_accum += fold_test_pred\n    return y_pred_test_accum / n_splits\n\n\nparams_cat = {\n    'iterations': 1900, 'learning_rate': 0.045, 'depth': 4, 'l2_leaf_reg': 0.56, \n    'random_strength': 0.045, 'bagging_temperature': 0.98, 'verbose': 0, 'random_state': 42,\n    'allow_writing_files': False\n}\nparams_xgb = {\n    'n_estimators': 1354, 'learning_rate': 0.010, 'max_depth': 3, 'subsample': 0.60, \n    'random_state': 42\n}\nparams_lgbm = {\n    'n_estimators': 807, 'learning_rate': 0.014, 'num_leaves': 48, 'min_child_samples': 19, \n    'subsample': 0.745, 'colsample_bytree': 0.745, 'reg_alpha': 0.21, 'reg_lambda': 3.78,\n    'verbose': -1, 'random_state': 42\n}\nparams_hist = {\n    'max_iter': 300, 'learning_rate': 0.05, 'max_depth': None, 'l2_regularization': 0.44,\n    'random_state': 42\n}\nfeat_engine = SupervisedEmbeddingEngine(n_pca=0.80, n_pls=8, n_gmm=6)\nprint(\"Training & Inferring Models...\")\n\nprint(\"Model: HistGradientBoosting\")\npred_hist = cross_validate_predict(\n    HistGradientBoostingRegressor, params_hist, \n    train_feat_df, test_feat_df, sem_train, sem_test, feat_engine\n)\n\nprint(\"Model: GradientBoosting\")\npred_gb = cross_validate_predict(\n    GradientBoostingRegressor, params_xgb, \n    train_feat_df, test_feat_df, sem_train, sem_test, feat_engine\n)\n\nprint(\"Model: CatBoost\")\npred_cat = cross_validate_predict(\n    CatBoostRegressor, params_cat, \n    train_feat_df, test_feat_df, sem_train, sem_test, feat_engine\n)\n\nprint(\"Model: LightGBM\")\npred_lgbm = cross_validate_predict(\n    LGBMRegressor, params_lgbm, \n    train_feat_df, test_feat_df, sem_train, sem_test, feat_engine\n)\n\n\nprint(\"Ensembling and Post-processing...\")\nfinal_pred = (pred_hist + pred_gb + pred_cat + pred_lgbm) / 4.0\ntest_feat_df[cfg.TARGET_NAMES] = final_pred\ntest_processed = post_process_biomass(test_feat_df)\nsub_df = melt_table(test_processed)\noutput_path = \"submission_siglip.csv\"\nsub_df.to_csv(output_path, index=False)\nprint(f\"✓ Siglip submission generated: {output_path}\")\nprint(sub_df.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T16:54:13.236818Z","iopub.execute_input":"2026-01-23T16:54:13.237172Z","iopub.status.idle":"2026-01-23T17:03:20.495814Z","shell.execute_reply.started":"2026-01-23T16:54:13.237138Z","shell.execute_reply":"2026-01-23T17:03:20.494982Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile csiro_infer.py\nfrom PIL import Image\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom sklearn.model_selection import KFold, GroupKFold, StratifiedGroupKFold\nfrom tqdm.auto import tqdm\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.optim import AdamW\nfrom transformers import get_cosine_schedule_with_warmup\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms as T\nimport os\nfrom pathlib import Path\nimport timm\nimport warnings \n\nwarnings.filterwarnings('ignore')\ntqdm.pandas()\n\n\nclass RegressionDataset(Dataset):\n    def __init__(self, data, transform=None):\n        self.data = data\n        self.transform = transform\n    def __len__(self):\n        return self.data.shape[0]\n\n    def __getitem__(self, idx):\n        item = self.data.iloc[idx]\n        image = item.image\n        targets = [item['Dry_Green_g'], item['Dry_Clover_g'], item['Dry_Dead_g']]\n        width, height = image.size\n        mid_point = width // 2\n        left_image = image.crop((0, 0, mid_point, height))\n        right_image = image.crop((mid_point, 0, width, height))\n\n        if self.transform is not None:\n            left_image = self.transform(left_image)\n            right_image = self.transform(right_image)\n        return left_image, right_image, targets\n\n        \ndef get_test_dataloaders(data, image_size, batch_size):\n    res = []\n    for trans in [None, T.RandomHorizontalFlip(p=1.0), T.RandomVerticalFlip(p=1.0)]:\n        transform = T.Compose([\n            T.Resize(image_size),\n            T.ToTensor(),\n            T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n        ])\n        if trans:\n            transform = T.Compose([\n                T.Resize(image_size),\n                trans,\n                T.ToTensor(),\n                T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n            ])\n        dataset = RegressionDataset(data, transform=transform)\n        res.append(DataLoader(dataset, batch_size=batch_size, shuffle=False, num_workers=4))\n    return res\n\n\nclass FiLM(nn.Module):\n    def __init__(self, feat_dim):\n        super().__init__()\n        self.mlp = nn.Sequential(\n            nn.Linear(feat_dim, feat_dim // 2), \n            nn.ReLU(inplace=True), \n            nn.Linear(feat_dim // 2, feat_dim * 2)\n        )\n\n    def forward(self, context):\n        gamma_beta = self.mlp(context)\n        return torch.chunk(gamma_beta, 2, dim=1)\n\nclass CSIROModelRegressor(nn.Module):\n    def __init__(self, model_name, pretrained=True, num_classes=3, dropout=0.0, freeze_backbone=False):\n        super().__init__()\n        self.backbone = timm.create_model(model_name, pretrained=pretrained, num_classes=0, global_pool='avg')\n        self.film = FiLM(self.backbone.num_features)\n        self.dropout = nn.Dropout(dropout)\n\n        def make_head():\n            return nn.Sequential(\n                nn.Linear(self.backbone.num_features * 2, 8),\n                nn.ReLU(inplace=True),\n                nn.Dropout(dropout),\n                nn.Linear(8, 1),\n            )\n        self.head_green = make_head()\n        self.head_clover = make_head()\n        self.head_dead = make_head()\n        self.softplus = nn.Softplus(beta=1.0)\n        if freeze_backbone:\n            for p in self.backbone.parameters():\n                p.requires_grad = False\n\n    def forward(self, left_img, right_img):\n        left_feat = self.backbone(left_img)\n        right_feat = self.backbone(right_img)\n        context = (left_feat + right_feat) / 2\n        gamma, beta = self.film(context)\n        left_feat_modulated = left_feat * (1 + gamma) + beta\n        right_feat_modulated = right_feat * (1 + gamma) + beta\n        combined = torch.cat([left_feat_modulated, right_feat_modulated], dim=1)\n        green = self.softplus(self.head_green(combined))   \n        clover = self.softplus(self.head_clover(combined))\n        dead = self.softplus(self.head_dead(combined)) \n        logits = torch.cat([green, clover, dead], dim=1)\n        return logits\n\n\ndef predict(model, dataloader, device):\n    model.to(device)\n    model.eval()\n    all_outputs = []\n    with torch.no_grad():\n        for left_images, right_images, targets in dataloader:\n            left_images = left_images.to(device)\n            right_images = right_images.to(device)\n\n            outputs = model(left_images, right_images)\n            all_outputs.append(outputs.detach().cpu())\n    outputs = torch.cat(all_outputs).numpy()\n    return outputs\n\n\ndef predict_loaders(model, dataloaders, device):\n    all_outputs = []\n    for dataloader in dataloaders:\n        outputs = predict(model, dataloader, device)\n        all_outputs.append(outputs)\n    avg_outputs = np.mean(all_outputs, axis=0)\n    return avg_outputs\n\n\ndef predict_folds(dataloaders,models_dir):\n    device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')\n    all_preds = []\n    for model_file in Path(models_dir).glob('*.pth'):\n        model = CSIROModelRegressor(CFG.MODEL_NAME, pretrained=False, num_classes=3)\n        model.load_state_dict(torch.load(model_file))\n        preds = predict_loaders(model, dataloaders, device)\n        all_preds.append(preds)\n    avg_preds = np.mean(all_preds, axis=0)\n    return avg_preds\n\n\nclass CFG:\n    DATA_PATH=\"/kaggle/input/csiro-biomass/\"\n    TEST_DATA_PATH=\"/kaggle/input/csiro-biomass/test.csv\"\n    MODEL_NAME=\"vit_large_patch16_dinov3_qkvb\"\n    MODELS_DIR ='/kaggle/input/modelv3/pytorch/default/1/models_retrained'\n    IMG_SIZE=(512,512)\n\n\ntest_df = pd.read_csv(CFG.TEST_DATA_PATH)\ntest_df['target'] = 0.0\ntest_df[['sample_id_prefix', 'sample_id_suffix']] = test_df.sample_id.str.split('__', expand=True)\n\ntest_data_df = test_df.groupby(['sample_id_prefix', 'image_path']).apply(lambda df: df.set_index('target_name').target)\ntest_data_df.reset_index(inplace=True)\ntest_data_df.columns.name = None\n\ntest_data_df['image'] = test_data_df.image_path.progress_apply(lambda path: Image.open(CFG.DATA_PATH + path).convert('RGB'))\ntest_loaders = get_test_dataloaders(test_data_df, CFG.IMG_SIZE, 32)\npreds = predict_folds(test_loaders,models_dir=CFG.MODELS_DIR)\n\ntest_data_df[['Dry_Green_g', 'Dry_Clover_g', 'Dry_Dead_g']] = preds\ntest_data_df['Dry_Green_g'] = test_data_df['Dry_Green_g'].apply(lambda x: 0.0 if x < 0.09 else x)\ntest_data_df['Dry_Clover_g'] = test_data_df['Dry_Clover_g'].apply(lambda x: 0.0 if x < 0.09 else x)\ntest_data_df['Dry_Dead_g'] = test_data_df['Dry_Dead_g'].apply(lambda x: 0.0 if x < 0.09 else x)\ntest_data_df['Dry_Total_g'] = test_data_df.GDM_g + test_data_df.Dry_Dead_g\nTARGETS = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n\nsubmission_rows = []\nfor idx, row in test_data_df.iterrows():\n    image_id = row['image_path'].split('/')[-1].replace('.jpg', '')\n    for target_name in TARGETS:\n        sample_id = f\"{image_id}__{target_name}\"\n        prediction = row[target_name]\n        if target_name == \"Dry_Clover_g\":\n            prediction = prediction * 0.8\n        elif target_name == \"Dry_Dead_g\":\n            if prediction > 20:\n                prediction *= 1.1\n            elif prediction < 10:\n                prediction *= 0.9\n        submission_rows.append({\n            'sample_id': sample_id,\n            'target': prediction\n        })\n\nsub_df = pd.DataFrame(submission_rows)\nsub_df.to_csv('submission_dinov2026.csv', index=False)\nprint(sub_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:03:20.497398Z","iopub.execute_input":"2026-01-23T17:03:20.497711Z","iopub.status.idle":"2026-01-23T17:03:20.505770Z","shell.execute_reply.started":"2026-01-23T17:03:20.497687Z","shell.execute_reply":"2026-01-23T17:03:20.505093Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!python csiro_infer.py","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:03:20.506650Z","iopub.execute_input":"2026-01-23T17:03:20.507008Z","iopub.status.idle":"2026-01-23T17:04:31.903000Z","shell.execute_reply.started":"2026-01-23T17:03:20.506976Z","shell.execute_reply":"2026-01-23T17:04:31.902028Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport os\n\nW_SIGLIP = 0.35\nW_DINO   = 0.65\nFILES = {\n    'siglip': 'submission_siglip.csv',\n    'dino':   'submission_dinov2026.csv'\n}\n\nOUTPUT_FILE = 'submission72.csv'\nALL_TARGETS = ['Dry_Green_g', 'Dry_Clover_g', 'Dry_Dead_g', 'GDM_g', 'Dry_Total_g']\n\n\ndef enforce_mass_balance(df_wide, fixed_clover=None):\n    ordered_cols = ['Dry_Green_g', 'Dry_Clover_g', 'Dry_Dead_g', 'GDM_g', 'Dry_Total_g']\n    Y = df_wide[ordered_cols].values.T\n    if fixed_clover:\n        clover_fixed = Y[1, :].copy() \n        Y[3, :] = Y[0, :] + clover_fixed\n        Y[4, :] = Y[3, :] + Y[2, :]\n        Y_reconciled = Y\n    else:\n        C = np.array([\n            [1, 1, 0, -1,  0],\n            [0, 0, 1,  1, -1]\n        ])\n        C_T = C.T\n        try:\n            inv_CCt = np.linalg.inv(C @ C_T)\n            P = np.eye(5) - C_T @ inv_CCt @ C\n        except np.linalg.LinAlgError:\n            print(\"Warning: Singular matrix in projection. Skipping constraint enforcement.\")\n            return df_wide\n        Y_reconciled = P @ Y\n        \n    Y_reconciled = Y_reconciled.T\n    Y_reconciled = np.maximum(0, Y_reconciled) \n    df_out = df_wide.copy()\n    df_out[ordered_cols] = Y_reconciled\n    return df_out\n\ndef robust_ensemble(file_paths, weights):\n    print(f\"--- Starting Ensemble ---\")\n    print(f\"Weights: {weights}\")\n    print(\"NOTE: Using DINO-only for Dry_Clover_g (better detection)\")\n    \n    dfs = []\n    for name, path in file_paths.items():\n        if not os.path.exists(path):\n            raise FileNotFoundError(f\"Missing file: {path}\")\n        \n        df = pd.read_csv(path).sort_values('sample_id').reset_index(drop=True)\n        dfs.append(df)\n        print(f\"Loaded {name}: {len(df)} rows\")\n\n    base_ids = dfs[0]['sample_id']\n    if not all(df['sample_id'].equals(base_ids) for df in dfs[1:]):\n        raise ValueError(\"Sample IDs do not match between submission files!\")\n\n    dfs_split = []\n    for df in dfs:\n        df_split = df.copy()\n        df_split[['image_id', 'target_name']] = df_split['sample_id'].str.rsplit('__', n=1, expand=True)\n        dfs_split.append(df_split)\n\n    dino_idx = list(file_paths.keys()).index('dino')\n    siglip_idx = list(file_paths.keys()).index('siglip')\n    dino_df = dfs_split[dino_idx]\n    siglip_df = dfs_split[siglip_idx]\n    \n    ensemble_results = []\n    for target in ALL_TARGETS:\n        dino_target = dino_df[dino_df['target_name'] == target].copy()\n        siglip_target = siglip_df[siglip_df['target_name'] == target].copy()\n        if target == 'Dry_Clover_g':\n            print(f\"Using DINO-only for {target}\")\n            ensemble_results.append(dino_target[['sample_id', 'target']])\n        else:\n            merged = pd.merge(\n                dino_target[['sample_id', 'target']].rename(columns={'target': 'dino_pred'}),\n                siglip_target[['sample_id', 'target']].rename(columns={'target': 'siglip_pred'}),\n                on='sample_id'\n            )\n  \n            w_vec = np.array([weights['dino'], weights['siglip']])\n            w_vec = w_vec / w_vec.sum()\n            merged['target'] = merged['dino_pred'] * w_vec[0] + merged['siglip_pred'] * w_vec[1]\n            ensemble_results.append(merged[['sample_id', 'target']])\n    \n    ensemble_df = pd.concat(ensemble_results, ignore_index=True)\n    print(\"Weighted average complete (DINO-only for Clover).\")\n    ensemble_df[['image_id', 'target_name']] = ensemble_df['sample_id'].str.rsplit('__', n=1, expand=True)\n    wide_df = ensemble_df.pivot(index='image_id', columns='target_name', values='target').reset_index()\n    print(\"Applying Mass Balance Constraints (Dry_Clover_g fixed to DINO values)...\")\n    wide_balanced = enforce_mass_balance(wide_df, fixed_clover=True)\n\n    long_balanced = wide_balanced.melt(\n        id_vars='image_id', \n        value_vars=ALL_TARGETS,\n        var_name='target_name',\n        value_name='target'\n    )\n    long_balanced['sample_id'] = long_balanced['image_id'] + '__' + long_balanced['target_name']\n    final_submission = long_balanced[['sample_id', 'target']].sort_values('sample_id').reset_index(drop=True)\n    return final_submission\n\n\nif __name__ == \"__main__\":\n    ensemble_weights = {\n        'siglip': W_SIGLIP,\n        'dino': W_DINO\n    }\n    try:\n        submission = robust_ensemble(FILES, ensemble_weights)\n        submission.to_csv(OUTPUT_FILE, index=False)\n        print(f\"\\nSuccess! Saved to {OUTPUT_FILE}\")\n        print(submission.head())\n        print(\"\\nStats:\")\n        print(submission['target'].describe())\n    except Exception as e:\n        print(f\"\\nError during ensembling: {e}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:04:31.904496Z","iopub.execute_input":"2026-01-23T17:04:31.904773Z","iopub.status.idle":"2026-01-23T17:04:31.960771Z","shell.execute_reply.started":"2026-01-23T17:04:31.904747Z","shell.execute_reply":"2026-01-23T17:04:31.960004Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import timm, sys\nprint(\"python:\", sys.version)\nprint(\"timm:\", timm.__version__)\nprint(\"timm file:\", timm.__file__)\nprint(\"dinov3 matches:\", timm.list_models(\"*dinov3*\")[:50])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:04:31.962527Z","iopub.execute_input":"2026-01-23T17:04:31.963092Z","iopub.status.idle":"2026-01-23T17:04:31.976259Z","shell.execute_reply.started":"2026-01-23T17:04:31.963069Z","shell.execute_reply":"2026-01-23T17:04:31.975670Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os, gc, math, cv2, numpy as np, pandas as pd\nfrom tqdm import tqdm\nimport torch, torch.nn as nn, torch.optim as optim\nfrom torch.optim.lr_scheduler import LambdaLR\nfrom torch.utils.data import Dataset, DataLoader\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nimport time\nfrom timm.utils import ModelEmaV2\nfrom sklearn.model_selection import KFold, StratifiedGroupKFold\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\ntorch.backends.cuda.matmul.allow_tf32 = True\ntorch.backends.cudnn.allow_tf32 = True\ntorch.backends.cudnn.benchmark = True\ntorch.set_float32_matmul_precision(\"high\")\n\nclass CFG:\n    CREATE_SUBMISSION = True\n    USE_TQDM        = False\n    PRETRAINED_DIR  = None\n    PRETRAINED      = True\n    BASE_PATH       = '/kaggle/input/csiro-biomass'\n    SEED            = 82947501\n    FOLDS_TO_TRAIN   = [0,1,2,3,4]\n    TRAIN_CSV       = os.path.join(BASE_PATH, 'train.csv')\n    TRAIN_IMAGE_DIR = os.path.join(BASE_PATH, 'train')\n    TEST_IMAGE_DIR = '/kaggle/input/csiro-biomass/test'\n    TEST_CSV = '/kaggle/input/csiro-biomass/test.csv'\n    SUBMISSION_DIR  = './'\n    MODEL_DIR = '/kaggle/input/baseline-dinov3/pytorch/default/1/baseline-dinov3'\n    N_FOLDS         = 5\n\n    MODEL_NAME      = 'vit_huge_plus_patch16_dinov3.lvd1689m'\n    BACKBONE_PATH   = '/kaggle/input/vit-huge-plus-patch16-dinov3-lvd1689m/vit_huge_plus_patch16_dinov3.lvd1689m_backbone.pth'\n    DINO_GRAD_CHECKPOINTING = False\n\n    IMG_SIZE        = 512\n\n    VAL_TTA_TIMES   = 1\n    TTA_STEPS       = 1\n    \n    \n    BATCH_SIZE      = 1\n    GRAD_ACC        = 4\n    NUM_WORKERS     = 4\n    EPOCHS          = 1\n    FREEZE_EPOCHS   = 0\n    WARMUP_EPOCHS   = 3\n    LR_REST         = 1e-3\n    LR_BACKBONE     = 5e-4\n    WD              = 1e-2\n    EMA_DECAY       = 0.9\n    CLIP_GRAD_NORM  = 1.0\n    PATIENCE        = 5\n    TARGET_COLS     = ['Dry_Total_g', 'GDM_g', 'Dry_Green_g']\n    DERIVED_COLS    = ['Dry_Clover_g', 'Dry_Dead_g']\n    ALL_TARGET_COLS = ['Dry_Green_g','Dry_Dead_g','Dry_Clover_g','GDM_g','Dry_Total_g']\n    R2_WEIGHTS      = np.array([0.1, 0.1, 0.1, 0.2, 0.5])\n    LOSS_WEIGHTS    = np.array([0.1, 0.1, 0.1, 0.0, 0.0])\n    DEVICE          = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\nos.makedirs(CFG.MODEL_DIR, exist_ok=True)\nprint(f'Device : {CFG.DEVICE}')\nprint(f'Backbone: {CFG.MODEL_NAME} | Input: {CFG.IMG_SIZE}')\nprint(f'Freeze Epochs: {CFG.FREEZE_EPOCHS} | Warmup: {CFG.WARMUP_EPOCHS}')\nprint(f'EMA Decay: {CFG.EMA_DECAY} | Grad Acc: {CFG.GRAD_ACC}')","metadata":{"execution":{"iopub.status.busy":"2026-01-23T17:04:31.977256Z","iopub.execute_input":"2026-01-23T17:04:31.977538Z","iopub.status.idle":"2026-01-23T17:04:31.997097Z","shell.execute_reply.started":"2026-01-23T17:04:31.977509Z","shell.execute_reply":"2026-01-23T17:04:31.996515Z"},"papermill":{"duration":37.523526,"end_time":"2026-01-05T05:09:15.453763","exception":false,"start_time":"2026-01-05T05:08:37.930237","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Metrics\ndef weighted_r2_score(y_true: np.ndarray, y_pred: np.ndarray):\n    weights = CFG.R2_WEIGHTS\n    r2_scores = []\n    for i in range(y_true.shape[1]):\n        yt = y_true[:, i]; yp = y_pred[:, i]\n        ss_res = np.sum((yt - yp) ** 2)\n        ss_tot = np.sum((yt - np.mean(yt)) ** 2)\n        r2 = 1 - ss_res / ss_tot if ss_tot > 0 else 0.0\n        r2_scores.append(r2)\n    r2_scores = np.array(r2_scores)\n    weighted = np.sum(r2_scores * weights) / np.sum(weights)\n    return weighted, r2_scores\n\ndef weighted_r2_score_global(y_true: np.ndarray, y_pred: np.ndarray):\n    weights = CFG.R2_WEIGHTS\n    flat_true = y_true.reshape(-1)\n    flat_pred = y_pred.reshape(-1)\n    w = np.tile(weights, y_true.shape[0])\n    mean_w = np.sum(w * flat_true) / np.sum(w)\n    ss_res = np.sum(w * (flat_true - flat_pred) ** 2)\n    ss_tot = np.sum(w * (flat_true - mean_w) ** 2)\n    global_r2 = 1 - ss_res / ss_tot if ss_tot > 0 else 0.0\n    avg_r2, per_r2 = weighted_r2_score(y_true, y_pred)\n    return global_r2, avg_r2, per_r2\n\ndef analyze_errors(val_df, y_true, y_pred, targets, top_n=5):\n    print(f'\\n--- Top {top_n} High Loss Samples per Target ---')\n    y_true = np.array(y_true)\n    y_pred = np.array(y_pred)\n    \n    for i, target in enumerate(targets):\n        errors = np.abs(y_true[:, i] - y_pred[:, i])\n        top_indices = np.argsort(errors)[::-1][:top_n]\n        \n        print(f'\\nTarget: {target}')\n        print(f'{\"Index\":<6} | {\"Image Path\":<40} | {\"True\":<10} | {\"Pred\":<10} | {\"AbsErr\":<10}')\n        print('-' * 90)\n        \n        for idx in top_indices:\n            path = val_df.iloc[idx]['image_path']\n            path_disp = os.path.basename(path)\n            t_val = y_true[idx, i]\n            p_val = y_pred[idx, i]\n            err = errors[idx]\n            print(f'{idx:<6} | {path_disp:<40} | {t_val:<10.4f} | {p_val:<10.4f} | {err:<10.4f}')\ndef analyze_errors(val_df, y_true, y_pred, targets, top_n=5):\n    print(f'\\n--- Top {top_n} High Loss Samples per Target ---')\n    y_true = np.array(y_true)\n    y_pred = np.array(y_pred)\n    \n    for i, target in enumerate(targets):\n        errors = np.abs(y_true[:, i] - y_pred[:, i])\n        top_indices = np.argsort(errors)[::-1][:top_n]\n        \n        print(f'\\nTarget: {target}')\n        header = f'{\"Index\":<6} | {\"Image Path\":<40} | {\"State\":<6} | {\"True\":<10} | {\"Pred\":<10} | {\"AbsErr\":<10}'\n        print(header)\n        print('-' * len(header))\n        \n        for idx in top_indices:\n            path = val_df.iloc[idx]['image_path']\n            path_disp = os.path.basename(path)\n            state = val_df.iloc[idx]['State'] if 'State' in val_df.columns else 'NA'\n            t_val = y_true[idx, i]\n            p_val = y_pred[idx, i]\n            err = errors[idx]\n            print(f'{idx:<6} | {path_disp:<40} | {str(state):<6} | {t_val:<10.4f} | {p_val:<10.4f} | {err:<10.4f}')\ndef compare_train_val(tr_df, val_df, targets, show_plots=True):\n    \"\"\"Quick comparison of target distributions and metadata between train and val splits.\"\"\"\n    print(\"\\n--- Train / Val Comparison ---\")\n\n    for t in targets:\n        tr = tr_df.get(t, pd.Series(dtype=float)).dropna()\n        val = val_df.get(t, pd.Series(dtype=float)).dropna()\n        print(f\"\\nTarget: {t}\")\n        print(f\"  Train: n={len(tr)} mean={tr.mean():.3f} std={tr.std():.3f} min={tr.min():.3f} max={tr.max():.3f}\")\n        print(f\"  Val  : n={len(val)} mean={val.mean():.3f} std={val.std():.3f} min={val.min():.3f} max={val.max():.3f}\")\n        if show_plots:\n            try:\n                plt.figure(figsize=(6, 3))\n                sns.kdeplot(tr, label='train', fill=True)\n                sns.kdeplot(val, label='val', fill=True)\n                plt.legend()\n                plt.title(f'Distribution: {t}')\n                plt.show()\n            except Exception as e:\n                print('  Could not plot distributions for', t, '-', e)\n\n    if 'Sampling_Date' in tr_df.columns:\n        try:\n            tr_dates = pd.to_datetime(tr_df['Sampling_Date'], errors='coerce')\n            val_dates = pd.to_datetime(val_df['Sampling_Date'], errors='coerce')\n            print(\"\\nSampling_Date range:\")\n            print(f\"  Train: {tr_dates.min()} -> {tr_dates.max()} (missing {tr_dates.isna().sum()})\")\n            print(f\"  Val  : {val_dates.min()} -> {val_dates.max()} (missing {val_dates.isna().sum()})\")\n        except Exception as e:\n            print('  Could not parse Sampling_Date:', e)\n    if 'State' in tr_df.columns:\n        print(\"\\nState distribution (train vs val):\")\n        tr_state = tr_df['State'].value_counts(normalize=True)\n        val_state = val_df['State'].value_counts(normalize=True)\n        state_df = pd.concat([tr_state, val_state], axis=1, keys=['train', 'val']).fillna(0)\n\n        print(state_df)\n\n    \ndef biomass_loss(outputs, labels, w=None):\n    total, gdm, green, clover, dead = outputs\n    mse = nn.MSELoss()\n    huber = nn.SmoothL1Loss(beta=5.0) \n    l_green  = huber(green.squeeze(),  labels[:,0])\n    l_dead   = huber(dead.squeeze(), labels[:,1]) \n    l_clover = huber(clover.squeeze(), labels[:,2])\n    l_gdm    = huber(gdm.squeeze(),    labels[:,3])\n    l_total  = huber(total.squeeze(),  labels[:,4])\n\n    losses = torch.stack([l_green, l_dead, l_clover, l_gdm, l_total])\n    if w is None:\n        return losses.mean()\n    w = torch.as_tensor(w, device=losses.device, dtype=losses.dtype)\n    w = w / w.sum()\n    return (losses * w).sum()","metadata":{"execution":{"iopub.status.busy":"2026-01-23T17:04:31.998149Z","iopub.execute_input":"2026-01-23T17:04:31.998439Z","iopub.status.idle":"2026-01-23T17:04:32.018308Z","shell.execute_reply.started":"2026-01-23T17:04:31.998419Z","shell.execute_reply":"2026-01-23T17:04:32.017589Z"},"papermill":{"duration":0.025723,"end_time":"2026-01-05T05:09:15.502256","exception":false,"start_time":"2026-01-05T05:09:15.476533","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_train_transforms():\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),\n        A.Rotate(limit=(-10, 10), p=0.3, interpolation=cv2.INTER_LINEAR, border_mode=cv2.BORDER_REFLECT_101),\n        A.ColorJitter(brightness=0.1, contrast=0.1, saturation=0.1, hue=0.05, p=0.5),\n        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ToTensorV2()\n    ], p=1.0)\n\ndef get_val_transforms():\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    ], p=1.0)\n\ndef get_tta_transforms(mode=0):\n    transforms_list = [\n        A.Resize(CFG.IMG_SIZE, CFG.IMG_SIZE),\n    ]\n    \n    if mode == 1:\n        transforms_list.append(A.HorizontalFlip(p=1.0))\n    elif mode == 2:\n        transforms_list.append(A.VerticalFlip(p=1.0))\n    elif mode == 3:\n        transforms_list.append(A.RandomRotate90(p=1.0))\n        transforms_list.append(A.Rotate(limit=(90, 90), p=1.0, interpolation=cv2.INTER_LINEAR, border_mode=cv2.BORDER_REFLECT_101))\n    transforms_list.extend([\n        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ToTensorV2()\n    ])\n    \n    return A.Compose(transforms_list, p=1.0)\n\n\ndef clean_image(img):\n    h, w = img.shape[:2]\n    img = img[0:int(h*0.90), :]\n    hsv = cv2.cvtColor(img, cv2.COLOR_RGB2HSV)\n    lower = np.array([5, 150, 150])\n    upper = np.array([25, 255, 255])\n    mask = cv2.inRange(hsv, lower, upper)\n    mask = cv2.dilate(mask, np.ones((3,3), np.uint8), iterations=2)\n    if np.sum(mask) > 0:\n        img = cv2.inpaint(img, mask, 3, cv2.INPAINT_TELEA)\n    return img\n\n\nclass BiomassDataset(Dataset):\n    def __init__(self, df, transform, img_dir):\n        self.df = df\n        self.transform = transform\n        self.img_dir = img_dir\n        self.paths = df['image_path'].values\n        self.labels = df[CFG.ALL_TARGET_COLS].values.astype(np.float32)\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        path = os.path.join(self.img_dir, os.path.basename(self.paths[idx]))\n        img = cv2.imread(path)\n        if img is None:\n            img = np.zeros((1000, 2000, 3), dtype=np.uint8)\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        img = clean_image(img)\n        h, w, _ = img.shape\n        mid = w // 2\n        left = img[:, :mid]\n        right = img[:, mid:]\n        left = self.transform(image=left)['image']\n        right = self.transform(image=right)['image']\n        label = torch.from_numpy(self.labels[idx])\n        return left, right, label","metadata":{"execution":{"iopub.status.busy":"2026-01-23T17:04:32.019230Z","iopub.execute_input":"2026-01-23T17:04:32.019468Z","iopub.status.idle":"2026-01-23T17:04:32.032800Z","shell.execute_reply.started":"2026-01-23T17:04:32.019449Z","shell.execute_reply":"2026-01-23T17:04:32.032067Z"},"papermill":{"duration":0.021348,"end_time":"2026-01-05T05:09:15.546114","exception":false,"start_time":"2026-01-05T05:09:15.524766","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class LocalMambaBlock(nn.Module):\n    def __init__(self, dim, kernel_size=5, dropout=0.0):\n        super().__init__()\n        self.norm = nn.LayerNorm(dim)\n        self.dwconv = nn.Conv1d(dim, dim, kernel_size=kernel_size, padding=kernel_size // 2, groups=dim)\n        self.gate = nn.Linear(dim, dim)\n        self.proj = nn.Linear(dim, dim)\n        self.drop = nn.Dropout(dropout)\n\n    def forward(self, x):\n        shortcut = x\n        x = self.norm(x)\n        g = torch.sigmoid(self.gate(x))\n        x = x * g\n        x = x.transpose(1, 2)\n        x = self.dwconv(x)\n        x = x.transpose(1, 2)\n        x = self.proj(x)\n        x = self.drop(x)\n        return shortcut + x","metadata":{"execution":{"iopub.status.busy":"2026-01-23T17:04:32.033798Z","iopub.execute_input":"2026-01-23T17:04:32.034150Z","iopub.status.idle":"2026-01-23T17:04:32.049055Z","shell.execute_reply.started":"2026-01-23T17:04:32.034105Z","shell.execute_reply":"2026-01-23T17:04:32.048303Z"},"papermill":{"duration":0.026162,"end_time":"2026-01-05T05:09:15.59418","exception":false,"start_time":"2026-01-05T05:09:15.568018","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BiomassModel(nn.Module):\n    def __init__(self, model_name, pretrained=True, backbone_path=None):\n        super().__init__()\n        self.model_name = model_name\n        self.backbone_path = backbone_path\n        \n        self.backbone = timm.create_model(self.model_name, pretrained=False, num_classes=0, global_pool='')\n        if hasattr(self.backbone, 'set_grad_checkpointing') and CFG.DINO_GRAD_CHECKPOINTING:\n            self.backbone.set_grad_checkpointing(True)\n            print(\"✓ Gradient Checkpointing enabled (saves ~50% VRAM)\")\n            \n        nf = self.backbone.num_features\n        self.fusion = nn.Sequential(\n            LocalMambaBlock(nf, kernel_size=5, dropout=0.1),\n            LocalMambaBlock(nf, kernel_size=5, dropout=0.1)\n        )\n        \n        self.pool = nn.AdaptiveAvgPool1d(1)\n        self.head_green_raw  = nn.Sequential(\n            nn.Linear(nf, nf//2), nn.GELU(), nn.Dropout(0.2), \n            nn.Linear(nf//2, 1), nn.Softplus()\n        )\n        self.head_clover_raw = nn.Sequential(\n            nn.Linear(nf, nf//2), nn.GELU(), nn.Dropout(0.2), \n            nn.Linear(nf//2, 1), nn.Softplus()\n        )\n        self.head_dead_raw   = nn.Sequential(\n            nn.Linear(nf, nf//2), nn.GELU(), nn.Dropout(0.2), \n            nn.Linear(nf//2, 1), nn.Softplus()\n        )\n        if pretrained:\n            self.load_pretrained()\n    \n    def load_pretrained(self):\n        try:\n            if self.backbone_path and os.path.exists(self.backbone_path):\n                print(f\"Loading backbone weights from local file: {self.backbone_path}\")\n                sd = torch.load(self.backbone_path, map_location='cpu')\n                if 'model' in sd: sd = sd['model']\n                elif 'state_dict' in sd: sd = sd['state_dict']\n            else:\n                print(\"Downloading backbone weights...\")\n                sd = timm.create_model(self.model_name, pretrained=True, num_classes=0, global_pool='').state_dict()\n            if 'pos_embed' in sd and hasattr(self.backbone, 'pos_embed'):\n                pe_ck = sd['pos_embed']\n                pe_m  = self.backbone.pos_embed\n                if pe_ck.shape != pe_m.shape:\n                    print(f\"Interpolating pos_embed: {pe_ck.shape} -> {pe_m.shape}\")\n            \n            self.backbone.load_state_dict(sd, strict=False)\n            print('Pretrained weights loaded.')\n        except Exception as e:\n            print(f'Warning: pretrained load failed: {e}')\n    \n    def forward(self, left, right):\n        x_l = self.backbone(left)\n        x_r = self.backbone(right)\n        x_cat = torch.cat([x_l, x_r], dim=1)\n        x_fused = self.fusion(x_cat)\n        x_pool = self.pool(x_fused.transpose(1, 2)).flatten(1)\n\n        green  = self.head_green_raw(x_pool)\n        clover = self.head_clover_raw(x_pool)\n        dead   = self.head_dead_raw(x_pool)\n        gdm    = green + clover\n        total  = gdm + dead\n        return total, gdm, green, clover, dead","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:04:32.050024Z","iopub.execute_input":"2026-01-23T17:04:32.050594Z","iopub.status.idle":"2026-01-23T17:04:32.063270Z","shell.execute_reply.started":"2026-01-23T17:04:32.050563Z","shell.execute_reply":"2026-01-23T17:04:32.062618Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def set_backbone_requires_grad(model: BiomassModel, requires_grad: bool):\n    for p in model.backbone.parameters():\n        p.requires_grad = requires_grad\n\n\ndef build_optimizer(model: BiomassModel):\n    backbone_ids = {id(p) for p in model.backbone.parameters()}\n    backbone_params = []\n    rest_params = []\n    for p in model.parameters():\n        if p.requires_grad:\n            if id(p) in backbone_ids:\n                backbone_params.append(p)\n            else:\n                rest_params.append(p)\n    return optim.AdamW([\n        {'params': backbone_params, 'lr': CFG.LR_BACKBONE, 'weight_decay': CFG.WD},\n        {'params': rest_params,     'lr': CFG.LR_REST,     'weight_decay': CFG.WD},\n])\n\n\ndef build_scheduler(optimizer):\n    def lr_lambda(epoch):\n        e = max(0, epoch - 1)\n        if e < CFG.WARMUP_EPOCHS:\n            return float(e + 1) / float(max(1, CFG.WARMUP_EPOCHS))\n        progress = (e - CFG.WARMUP_EPOCHS) / float(max(1, CFG.EPOCHS - CFG.WARMUP_EPOCHS))\n        return 0.5 * (1.0 + math.cos(math.pi * progress))\n    return LambdaLR(optimizer, lr_lambda)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:04:32.064026Z","iopub.execute_input":"2026-01-23T17:04:32.064290Z","iopub.status.idle":"2026-01-23T17:04:32.080267Z","shell.execute_reply.started":"2026-01-23T17:04:32.064258Z","shell.execute_reply":"2026-01-23T17:04:32.079576Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from contextlib import nullcontext\n\nUSE_BF16 = True\nAMP_DTYPE = torch.bfloat16 if USE_BF16 else torch.float16\nscaler = torch.amp.GradScaler(\n    'cuda',\n    enabled=(torch.cuda.is_available() and AMP_DTYPE == torch.float16)\n)\n\n\ndef autocast_ctx():\n    if not torch.cuda.is_available():\n        return nullcontext()\n    return torch.amp.autocast(device_type='cuda', dtype=AMP_DTYPE)\n\n\ndef _as_col(x: torch.Tensor, bs: int) -> torch.Tensor:\n    if x.ndim == 1:\n        return x.view(bs, 1)\n    if x.ndim == 2:\n        return x\n    return x.view(bs, 1)\n\n\ndef _ensure_2d_lab(lab: torch.Tensor, bs: int) -> torch.Tensor:\n    if lab.ndim == 1:\n        return lab.view(bs, -1)\n    return lab\n\n\ndef _pred_pack(p_total, p_gdm, p_green, p_clover, p_dead, bs: int) -> torch.Tensor:\n    pg = _as_col(p_green,  bs)\n    pd = _as_col(p_dead,   bs)\n    pc = _as_col(p_clover, bs)\n    pgdm = _as_col(p_gdm,  bs)\n    pt = _as_col(p_total,  bs)\n    return torch.cat([pg, pd, pc, pgdm, pt], dim=1)  # (B,5)\n\n    \n@torch.inference_mode()\ndef valid_epoch(eval_model, loader, device):\n    eval_model.eval()\n\n    n = len(loader.dataset)\n    n_targets = len(CFG.ALL_TARGET_COLS)\n    preds_cpu  = torch.empty((n, n_targets), dtype=torch.float32)\n    labels_cpu = torch.empty((n, n_targets), dtype=torch.float32)\n    total_loss = 0.0\n    offset = 0\n    for l, r, lab in loader:\n        bs = l.size(0)\n        l   = l.to(device, non_blocking=True)\n        r   = r.to(device, non_blocking=True)\n        lab = lab.to(device, non_blocking=True)\n        with autocast_ctx():\n            p_total, p_gdm, p_green, p_clover, p_dead = eval_model(l, r)\n            loss = biomass_loss((p_total, p_gdm, p_green, p_clover, p_dead),\n                                lab, w=CFG.LOSS_WEIGHTS)\n\n        total_loss += loss.detach().float().item() * bs\n        batch_pred = _pred_pack(p_total, p_gdm, p_green, p_clover, p_dead, bs).float().cpu()\n        batch_lab  = _ensure_2d_lab(lab, bs).float().cpu()\n        if batch_pred.shape[1] != n_targets or batch_lab.shape[1] != n_targets:\n            raise RuntimeError(\n                f\"Target dim mismatch: pred={batch_pred.shape}, lab={batch_lab.shape}, \"\n                f\"CFG.ALL_TARGET_COLS={n_targets}\"\n            )\n        preds_cpu[offset:offset+bs]  = batch_pred\n        labels_cpu[offset:offset+bs] = batch_lab\n        offset += bs\n\n    pred_all    = preds_cpu.numpy()\n    true_labels = labels_cpu.numpy()\n    global_r2, avg_r2, per_r2 = weighted_r2_score_global(true_labels, pred_all)\n    return total_loss / n, global_r2, avg_r2, per_r2, pred_all, true_labels\n\n\n@torch.inference_mode()\ndef valid_epoch_tta(eval_model, loaders, device):\n    eval_model.eval()\n    assert len(loaders) > 0\n    n = len(loaders[0].dataset)\n    n_targets = len(CFG.ALL_TARGET_COLS)\n    labels_cpu = torch.empty((n, n_targets), dtype=torch.float32)\n    preds_sum  = torch.zeros((n, n_targets), dtype=torch.float32)\n    total_loss = 0.0\n    for tta_i, loader in enumerate(loaders):\n        offset = 0\n        tta_loss = 0.0\n\n        for l, r, lab in loader:\n            bs = l.size(0)\n            l   = l.to(device, non_blocking=True)\n            r   = r.to(device, non_blocking=True)\n            lab = lab.to(device, non_blocking=True)\n\n            with autocast_ctx():\n                p_total, p_gdm, p_green, p_clover, p_dead = eval_model(l, r)\n                loss = biomass_loss((p_total, p_gdm, p_green, p_clover, p_dead),\n                                    lab, w=CFG.LOSS_WEIGHTS)\n\n            tta_loss += loss.detach().float().item() * bs\n            batch_pred = _pred_pack(p_total, p_gdm, p_green, p_clover, p_dead, bs).float().cpu()\n            preds_sum[offset:offset+bs] += batch_pred\n\n            if tta_i == 0:\n                batch_lab = _ensure_2d_lab(lab, bs).float().cpu()\n                if batch_lab.shape[1] != n_targets:\n                    raise RuntimeError(f\"Label dim mismatch: {batch_lab.shape} vs {n_targets}\")\n                labels_cpu[offset:offset+bs] = batch_lab\n            offset += bs\n        total_loss += tta_loss / n\n\n    avg_preds   = (preds_sum / len(loaders)).numpy()\n    true_labels = labels_cpu.numpy()\n    global_r2, avg_r2, per_r2 = weighted_r2_score_global(true_labels, avg_preds)\n    return total_loss / len(loaders), global_r2, avg_r2, per_r2, avg_preds, true_labels\n\n\ndef train_epoch(model, loader, opt, scheduler, device, ema: ModelEmaV2 | None = None):\n    model.train()\n    running = 0.0\n    opt.zero_grad(set_to_none=True)\n    itera = tqdm(loader, desc='train', leave=False) if CFG.USE_TQDM else loader\n    for i, (l, r, lab) in enumerate(itera):\n        bs = l.size(0)\n        l   = l.to(device, non_blocking=True)\n        r   = r.to(device, non_blocking=True)\n        lab = lab.to(device, non_blocking=True)\n\n        with autocast_ctx():\n            p_total, p_gdm, p_green, p_clover, p_dead = model(l, r)\n            loss = biomass_loss((p_total, p_gdm, p_green, p_clover, p_dead),\n                                lab, w=CFG.LOSS_WEIGHTS)\n            loss = loss / CFG.GRAD_ACC\n\n        if not torch.isfinite(loss):\n            print(\"Non-finite loss detected; skipping batch\")\n            opt.zero_grad(set_to_none=True)\n            continue\n\n        if scaler.is_enabled():\n            scaler.scale(loss).backward()\n        else:\n            loss.backward()\n\n        running += loss.detach().float().item() * bs * CFG.GRAD_ACC\n\n        do_step = ((i + 1) % CFG.GRAD_ACC == 0) or ((i + 1) == len(loader))\n        if do_step:\n            if scaler.is_enabled():\n                scaler.unscale_(opt)\n                torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)\n                scaler.step(opt)\n                scaler.update()\n            else:\n                torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)\n                opt.step()\n            if ema is not None:\n                ema.update(model.module if hasattr(model, \"module\") else model)\n            opt.zero_grad(set_to_none=True)\n    scheduler.step()\n    return running / len(loader.dataset)","metadata":{"execution":{"iopub.status.busy":"2026-01-23T17:04:32.081182Z","iopub.execute_input":"2026-01-23T17:04:32.081462Z","iopub.status.idle":"2026-01-23T17:04:32.103607Z","shell.execute_reply.started":"2026-01-23T17:04:32.081419Z","shell.execute_reply":"2026-01-23T17:04:32.102910Z"},"papermill":{"duration":0.048482,"end_time":"2026-01-05T05:09:15.664902","exception":false,"start_time":"2026-01-05T05:09:15.61642","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_tta_transforms(num_transforms):\n    normalize = A.Normalize(\n        mean=[0.485, 0.456, 0.406],\n        std=[0.229, 0.224, 0.225]\n    )\n    to_tensor = ToTensorV2()\n    \n    all_tta_transforms = [\n        A.Compose([\n            A.Resize(CFG.IMG_SIZE, CFG.IMG_SIZE),\n            normalize,\n            to_tensor\n        ]),\n        A.Compose([\n            A.Resize(CFG.IMG_SIZE, CFG.IMG_SIZE),\n            A.HorizontalFlip(p=1.0),\n            normalize,\n            to_tensor\n        ]),\n        A.Compose([\n            A.Resize(CFG.IMG_SIZE, CFG.IMG_SIZE),\n            A.VerticalFlip(p=1.0),\n            normalize,\n            to_tensor\n        ]),\n        A.Compose([\n            A.Resize(CFG.IMG_SIZE, CFG.IMG_SIZE),\n            A.HorizontalFlip(p=1.0),\n            A.VerticalFlip(p=1.0),\n            normalize,\n            to_tensor\n        ]),\n    ]\n    tta_transforms = all_tta_transforms[:num_transforms]\n    return tta_transforms\n\nprint(f\"✓ TTA transforms defined ({CFG.TTA_STEPS} views)\")\n\n\ndef clean_image(img):\n    h, w = img.shape[:2]\n    img = img[0:int(h * 0.90), :]\n    hsv = cv2.cvtColor(img, cv2.COLOR_RGB2HSV)\n    lower = np.array([5, 150, 150])\n    upper = np.array([25, 255, 255])\n    mask = cv2.inRange(hsv, lower, upper)\n    mask = cv2.dilate(mask, np.ones((3, 3), np.uint8), iterations=2)\n    if np.sum(mask) > 0:\n        img = cv2.inpaint(img, mask, 3, cv2.INPAINT_TELEA)\n    return img\n\n\nclass BiomassTestDataset(Dataset):\n    def __init__(self, img_dir):\n        self.img_dir = img_dir\n        self.paths = sorted([\n            os.path.join(img_dir, f) for f in os.listdir(img_dir)\n            if f.lower().endswith(('.jpg', '.jpeg', '.png'))\n        ])\n        self.filenames = [os.path.basename(p) for p in self.paths]\n    \n    def __len__(self):\n        return len(self.paths)\n    \n    def __getitem__(self, idx):\n        path = self.paths[idx]\n        img = cv2.imread(path)\n        if img is None:\n            print(f\"Warning: Could not read {path}, using blank image\")\n            img = np.zeros((1000, 2000, 3), dtype=np.uint8)\n \n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        img = clean_image(img)\n        h, w = img.shape[:2]\n        mid = w // 2\n        left = img[:, :mid].copy()\n        right = img[:, mid:].copy()\n        \n        return left, right, self.filenames[idx]\n\nprint(\"✓ Test dataset class defined\")\n\n@torch.no_grad()\ndef predict_with_tta(model, left_np, right_np, tta_transforms):\n    all_tta_preds = []\n\n    for tfm in tta_transforms:\n        left_tensor = tfm(image=left_np)['image'].unsqueeze(0).to(CFG.DEVICE)\n        right_tensor = tfm(image=right_np)['image'].unsqueeze(0).to(CFG.DEVICE)\n        \n        total, gdm, green, clover, dead = model(left_tensor, right_tensor)\n\n        p_total = total.cpu().item()\n        p_gdm = gdm.cpu().item()\n        p_green = green.cpu().item()\n        \n        all_tta_preds.append([p_total, p_gdm, p_green])\n\n    final_pred = np.mean(all_tta_preds, axis=0)\n    return final_pred\n\n\ndef run_inference():\n    print(\"\\n\" + \"=\"*70)\n    print(\"STARTING INFERENCE\")\n    print(\"=\"*70)\n\n    dataset = BiomassTestDataset(CFG.TEST_IMAGE_DIR)\n    loader = DataLoader(\n        dataset,\n        batch_size=1,  \n        shuffle=False,\n        num_workers=CFG.NUM_WORKERS,\n        pin_memory=True\n    )\n    \n    tta_transforms = get_tta_transforms(CFG.TTA_STEPS)\n    accumulated_preds = np.zeros((len(dataset), 3), dtype=np.float32)\n    folds_to_use = getattr(CFG, 'FOLDS_TO_TRAIN', list(range(CFG.N_FOLDS)))\n    if not folds_to_use:\n        folds_to_use = list(range(CFG.N_FOLDS))\n\n    print(f\"Folds requested for inference: {folds_to_use}\")\n    filenames = dataset.filenames.copy()\n    successful_folds = 0\n    \n    for fold in folds_to_use:\n        print(f\"\\nProcessing Fold {fold}...\")\n        model_dir = CFG.MODEL_DIR\n        backbone_path = getattr(CFG, 'BACKBONE_PATH', None)\n        model = BiomassModel(CFG.MODEL_NAME, pretrained=False, backbone_path=backbone_path)\n        \n        weight_path = os.path.join(model_dir, f'best_model_fold{fold}.pth')\n        if not os.path.exists(weight_path):\n            print(f\"Warning: Model file {weight_path} not found! Skipping fold {fold}.\")\n            del model\n            torch.cuda.empty_cache(); gc.collect()\n            continue\n            \n        state = torch.load(weight_path, map_location='cpu')\n        if isinstance(state, dict) and ('model_state_dict' in state or 'state_dict' in state):\n             key = 'model_state_dict' if 'model_state_dict' in state else 'state_dict'\n             sd = state[key]\n        else:\n             sd = state\n        \n        model.load_state_dict(sd)\n        model.to(CFG.DEVICE)\n        model.eval()\n        \n        for i, (left, right, filename) in enumerate(tqdm(loader, desc=f\"Fold {fold}\")):\n            left_np = left[0].numpy()\n            right_np = right[0].numpy()\n\n            pred = predict_with_tta(model, left_np, right_np, tta_transforms)\n            accumulated_preds[i] += pred\n            \n        successful_folds += 1\n        del model\n        torch.cuda.empty_cache(); gc.collect()\n        \n    if successful_folds == 0:\n        raise FileNotFoundError(f\"No model weights found for requested folds: {folds_to_use}\")\n\n    final_predictions = accumulated_preds / successful_folds\n    \n    print(f\"\\nInference complete. Successfully used {successful_folds} fold(s) out of {len(folds_to_use)} requested.\")\n    return final_predictions, filenames\n\n\ndef postprocess_predictions(preds_direct):\n    print(\"\\nPost-processing predictions...\")\n\n    pred_total = preds_direct[:, 0]\n    pred_gdm = preds_direct[:, 1]\n    pred_green = preds_direct[:, 2]\n\n    pred_clover = np.maximum(0, pred_gdm - pred_green)\n    pred_dead = np.maximum(0, pred_total - pred_gdm)\n    \n    preds_all = np.stack([\n        pred_green,\n        pred_dead,\n        pred_clover,\n        pred_gdm,\n        pred_total\n    ], axis=1)\n    \n    print(f\"✓ Post-processing complete\")\n    print(f\"  Output shape: {preds_all.shape}\")\n    print(f\"\\nPrediction statistics:\")\n    for i, col in enumerate(CFG.ALL_TARGET_COLS):\n        print(f\"  {col:15s}: mean={preds_all[:, i].mean():.2f}, \"\n              f\"std={preds_all[:, i].std():.2f}, \"\n              f\"min={preds_all[:, i].min():.2f}, \"\n              f\"max={preds_all[:, i].max():.2f}\")\n    \n    return preds_all\n\n\ndef create_submission(predictions, filenames):\n    \"\"\"\n    Create submission file in the required format.\n    \n    Args:\n        predictions: (n_images, 5) array with all target predictions\n        filenames: list of test image filenames\n    \"\"\"\n    print(\"\\n\" + \"=\"*70)\n    print(\"CREATING SUBMISSION FILE\")\n    print(\"=\"*70)\n  \n    test_df = pd.read_csv(CFG.TEST_CSV)\n    print(f\"\\nTest CSV loaded: {len(test_df)} rows\")\n    print(f\"Sample image_path from test.csv: {test_df['image_path'].iloc[0]}\")\n    print(f\"Sample filename from predictions: {filenames[0]}\")\n    \n    test_path_example = test_df['image_path'].iloc[0]\n    if '/' in test_path_example:\n        prefix = test_path_example.rsplit('/', 1)[0] + '/'\n        corrected_filenames = [prefix + fn for fn in filenames]\n        print(f\"Corrected path format: {corrected_filenames[0]}\")\n    else:\n        corrected_filenames = filenames\n  \n    preds_wide = pd.DataFrame(predictions, columns=CFG.ALL_TARGET_COLS)\n    preds_wide.insert(0, 'image_path', corrected_filenames)\n    \n    print(f\"\\nWide format predictions:\")\n    print(preds_wide.head())\n\n    preds_long = preds_wide.melt(\n        id_vars=['image_path'],\n        value_vars=CFG.ALL_TARGET_COLS,\n        var_name='target_name',\n        value_name='target'\n    )\n    \n    print(f\"\\nLong format predictions (first 10 rows):\")\n    print(preds_long.head(10))\n    print(f\"\\nDebug: Checking if paths match...\")\n    print(f\"Unique paths in test_df: {test_df['image_path'].nunique()}\")\n    print(f\"Unique paths in preds_long: {preds_long['image_path'].nunique()}\")\n    \n    common_paths = set(test_df['image_path'].unique()) & set(preds_long['image_path'].unique())\n    print(f\"Common paths found: {len(common_paths)}\")\n    if len(common_paths) == 0:\n        print(\"\\n❌ ERROR: No matching paths found!\")\n        print(f\"Test CSV paths sample: {list(test_df['image_path'].unique()[:3])}\")\n        print(f\"Prediction paths sample: {list(preds_long['image_path'].unique()[:3])}\")\n        raise ValueError(\"Path mismatch between test.csv and predictions\")\n    \n    submission = pd.merge(\n        test_df[['sample_id', 'image_path', 'target_name']],\n        preds_long,\n        on=['image_path', 'target_name'],\n        how='left'\n    )\n    submission = submission[['sample_id', 'target']]\n    missing_count = submission['target'].isna().sum()\n    if missing_count > 0:\n        print(f\"\\n⚠ Warning: {missing_count} missing predictions found!\")\n        print(\"Sample missing entries:\")\n        print(submission[submission['target'].isna()].head())\n        submission.loc[submission['target'].isna(), 'target'] = 0.0\n\n    submission = submission.sort_values('sample_id').reset_index(drop=True)\n    output_path = os.path.join(CFG.SUBMISSION_DIR, 'submission70.csv')\n    submission.to_csv(output_path, index=False)\n    print(f\"\\n✓ Submission file saved: {output_path}\")\n    print(f\"  Total rows: {len(submission)}\")\n    print(f\"\\nPrediction statistics:\")\n    print(f\"  Min: {submission['target'].min():.4f}\")\n    print(f\"  Max: {submission['target'].max():.4f}\")\n    print(f\"  Mean: {submission['target'].mean():.4f}\")\n    print(f\"  Non-zero values: {(submission['target'] > 0).sum()}/{len(submission)}\")\n    \n    print(f\"\\nFirst 10 rows:\")\n    print(submission.head(10))\n    print(f\"\\nLast 10 rows:\")\n    print(submission.tail(10))\n    \n    print(f\"\\n\" + \"=\"*70)\n    print(\"VALIDATION CHECKS\")\n    print(\"=\"*70)\n    print(f\"✓ Expected rows: {len(test_df)}\")\n    print(f\"✓ Actual rows: {len(submission)}\")\n    print(f\"✓ Match: {len(submission) == len(test_df)}\")\n    print(f\"✓ No missing values: {not submission['target'].isna().any()}\")\n    print(f\"✓ All sample_ids unique: {submission['sample_id'].is_unique}\")\n    print(f\"✓ Has non-zero predictions: {(submission['target'] > 0).any()}\")\n    return submission\n\n\nif CFG.CREATE_SUBMISSION:\n    predictions_direct, test_filenames = run_inference()\n    predictions_all = postprocess_predictions(predictions_direct)\n    submission_df = create_submission(predictions_all, test_filenames)","metadata":{"execution":{"iopub.status.busy":"2026-01-23T17:04:32.104623Z","iopub.execute_input":"2026-01-23T17:04:32.104876Z","iopub.status.idle":"2026-01-23T17:06:59.294859Z","shell.execute_reply.started":"2026-01-23T17:04:32.104856Z","shell.execute_reply":"2026-01-23T17:06:59.294238Z"},"papermill":{"duration":0.015528,"end_time":"2026-01-05T05:09:15.754447","exception":false,"start_time":"2026-01-05T05:09:15.738919","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport os\n\nW_SIGLIP = 0.35\nW_DINO   = 0.65\n\nFILES = {\n    'siglip': 'submission70.csv',\n    'dino':   'submission72.csv'\n}\n\nOUTPUT_FILE = 'submission_siglip_dinov3.csv'\nALL_TARGETS = ['Dry_Green_g', 'Dry_Clover_g', 'Dry_Dead_g', 'GDM_g', 'Dry_Total_g']\n\ndef enforce_mass_balance(df_wide, fixed_clover=None):\n    ordered_cols = ['Dry_Green_g', 'Dry_Clover_g', 'Dry_Dead_g', 'GDM_g', 'Dry_Total_g']\n    Y = df_wide[ordered_cols].values.T\n    if fixed_clover:\n        clover_fixed = Y[1, :].copy()\n        Y[3, :] = Y[0, :] + clover_fixed\n        Y[4, :] = Y[3, :] + Y[2, :] \n        Y_reconciled = Y\n    else:\n        C = np.array([\n            [1, 1, 0, -1,  0],\n            [0, 0, 1,  1, -1]\n        ])\n        C_T = C.T\n        try:\n            inv_CCt = np.linalg.inv(C @ C_T)\n            P = np.eye(5) - C_T @ inv_CCt @ C\n        except np.linalg.LinAlgError:\n            print(\"Warning: Singular matrix in projection. Skipping constraint enforcement.\")\n            return df_wide\n\n        Y_reconciled = P @ Y\n    \n    Y_reconciled = Y_reconciled.T\n    Y_reconciled = np.maximum(0, Y_reconciled) \n    df_out = df_wide.copy()\n    df_out[ordered_cols] = Y_reconciled\n    return df_out\n\n\ndef robust_ensemble(file_paths, weights):\n    print(f\"--- Starting Ensemble ---\")\n    print(f\"Weights: {weights}\")\n    print(\"NOTE: Using DINO-only for Dry_Clover_g (better detection)\")\n    dfs = []\n    for name, path in file_paths.items():\n        if not os.path.exists(path):\n            raise FileNotFoundError(f\"Missing file: {path}\")\n        \n        df = pd.read_csv(path).sort_values('sample_id').reset_index(drop=True)\n        dfs.append(df)\n        print(f\"Loaded {name}: {len(df)} rows\")\n\n    base_ids = dfs[0]['sample_id']\n    if not all(df['sample_id'].equals(base_ids) for df in dfs[1:]):\n        raise ValueError(\"Sample IDs do not match between submission files!\")\n\n    dfs_split = []\n    for df in dfs:\n        df_split = df.copy()\n        df_split[['image_id', 'target_name']] = df_split['sample_id'].str.rsplit('__', n=1, expand=True)\n        dfs_split.append(df_split)\n    \n    dino_idx = list(file_paths.keys()).index('dino')\n    siglip_idx = list(file_paths.keys()).index('siglip')\n    dino_df = dfs_split[dino_idx]\n    siglip_df = dfs_split[siglip_idx]\n    ensemble_results = []\n    for target in ALL_TARGETS:\n        dino_target = dino_df[dino_df['target_name'] == target].copy()\n        siglip_target = siglip_df[siglip_df['target_name'] == target].copy()\n        \n        if target == 'Dry_Clover_g':\n            print(f\"Using DINO-only for {target}\")\n            ensemble_results.append(dino_target[['sample_id', 'target']])\n        else:\n            merged = pd.merge(\n                dino_target[['sample_id', 'target']].rename(columns={'target': 'dino_pred'}),\n                siglip_target[['sample_id', 'target']].rename(columns={'target': 'siglip_pred'}),\n                on='sample_id'\n            )\n            w_vec = np.array([weights['dino'], weights['siglip']])\n            w_vec = w_vec / w_vec.sum()\n            merged['target'] = merged['dino_pred'] * w_vec[0] + merged['siglip_pred'] * w_vec[1]\n            ensemble_results.append(merged[['sample_id', 'target']])\n    \n    ensemble_df = pd.concat(ensemble_results, ignore_index=True)\n    print(\"Weighted average complete (DINO-only for Clover).\")\n\n    ensemble_df[['image_id', 'target_name']] = ensemble_df['sample_id'].str.rsplit('__', n=1, expand=True)\n\n    wide_df = ensemble_df.pivot(index='image_id', columns='target_name', values='target').reset_index()\n    print(\"Applying Mass Balance Constraints (Dry_Clover_g fixed to DINO values)...\")\n    wide_balanced = enforce_mass_balance(wide_df, fixed_clover=True)\n\n    long_balanced = wide_balanced.melt(\n        id_vars='image_id', \n        value_vars=ALL_TARGETS,\n        var_name='target_name',\n        value_name='target'\n    )\n    long_balanced['sample_id'] = long_balanced['image_id'] + '__' + long_balanced['target_name']\n    final_submission = long_balanced[['sample_id', 'target']].sort_values('sample_id').reset_index(drop=True)\n\n    return final_submission\n\n\nif __name__ == \"__main__\":\n    ensemble_weights = {\n        'siglip': W_SIGLIP,\n        'dino': W_DINO\n    }\n    \n    try:\n        submission = robust_ensemble(FILES, ensemble_weights)\n        submission.to_csv(OUTPUT_FILE, index=False)\n        print(f\"\\nSuccess! Saved to {OUTPUT_FILE}\")\n        print(submission.head())\n        print(\"\\nStats:\")\n        print(submission['target'].describe())\n    except Exception as e:\n        print(f\"\\nError during ensembling: {e}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:07:46.989392Z","iopub.execute_input":"2026-01-23T17:07:46.989968Z","iopub.status.idle":"2026-01-23T17:07:47.045344Z","shell.execute_reply.started":"2026-01-23T17:07:46.989942Z","shell.execute_reply":"2026-01-23T17:07:47.044686Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# zhuangjia's sub","metadata":{}},{"cell_type":"code","source":"!pip install --no-deps /kaggle/input/timm-1-0-22/timm-1.0.22-py3-none-any.whl","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:07:52.440598Z","iopub.execute_input":"2026-01-23T17:07:52.441516Z","iopub.status.idle":"2026-01-23T17:07:54.588344Z","shell.execute_reply.started":"2026-01-23T17:07:52.441477Z","shell.execute_reply":"2026-01-23T17:07:54.587583Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\nTRAIN = False\n\nDEBUG = os.environ.get('KAGGLE_KERNEL_RUN_TYPE', '') == 'Interactive'\nLOCAL = os.environ.get('KAGGLE_KERNEL_RUN_TYPE', '') == '' \nif LOCAL:\n    os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:07:54.590233Z","iopub.execute_input":"2026-01-23T17:07:54.590509Z","iopub.status.idle":"2026-01-23T17:07:54.595252Z","shell.execute_reply.started":"2026-01-23T17:07:54.590472Z","shell.execute_reply":"2026-01-23T17:07:54.594537Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"CV_STRATEGY = 'groupby_Sampling_Date' \nNFOLD = 5\nKFOLD_SEED = 42\n\nMODEL_NAME = 'vit_large_patch16_dinov3_qkvb'\nLR = 1e-2","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:07:54.596079Z","iopub.execute_input":"2026-01-23T17:07:54.596473Z","iopub.status.idle":"2026-01-23T17:07:54.610557Z","shell.execute_reply.started":"2026-01-23T17:07:54.596442Z","shell.execute_reply":"2026-01-23T17:07:54.609915Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from PIL import Image\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\n%matplotlib inline\n\nfrom sklearn.model_selection import KFold, GroupKFold, StratifiedGroupKFold\nfrom tqdm.auto import tqdm\ntqdm.pandas()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:07:54.612289Z","iopub.execute_input":"2026-01-23T17:07:54.612546Z","iopub.status.idle":"2026-01-23T17:07:54.627297Z","shell.execute_reply.started":"2026-01-23T17:07:54.612528Z","shell.execute_reply":"2026-01-23T17:07:54.626573Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"DATA_ROOT = '../input/' if LOCAL else '/kaggle/input/csiro-biomass/'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:07:54.628262Z","iopub.execute_input":"2026-01-23T17:07:54.628621Z","iopub.status.idle":"2026-01-23T17:07:54.638981Z","shell.execute_reply.started":"2026-01-23T17:07:54.628593Z","shell.execute_reply":"2026-01-23T17:07:54.638263Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df = pd.read_csv(f'{DATA_ROOT}/train.csv')\ntrain_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:07:54.639819Z","iopub.execute_input":"2026-01-23T17:07:54.639996Z","iopub.status.idle":"2026-01-23T17:07:54.673912Z","shell.execute_reply.started":"2026-01-23T17:07:54.639979Z","shell.execute_reply":"2026-01-23T17:07:54.673191Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df[['sample_id_prefix', 'sample_id_suffix']] = train_df.sample_id.str.split('__', expand=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:07:54.674950Z","iopub.execute_input":"2026-01-23T17:07:54.675199Z","iopub.status.idle":"2026-01-23T17:07:54.683388Z","shell.execute_reply.started":"2026-01-23T17:07:54.675179Z","shell.execute_reply":"2026-01-23T17:07:54.682520Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"(train_df.sample_id_suffix == train_df.target_name).all()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:07:54.684402Z","iopub.execute_input":"2026-01-23T17:07:54.684666Z","iopub.status.idle":"2026-01-23T17:07:54.698075Z","shell.execute_reply.started":"2026-01-23T17:07:54.684639Z","shell.execute_reply":"2026-01-23T17:07:54.697395Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cols = ['sample_id_prefix', 'image_path', 'Sampling_Date', 'State', 'Species', 'Pre_GSHH_NDVI', 'Height_Ave_cm']\nagg_train_df = train_df.groupby(cols).apply(lambda df: df.set_index('target_name').target)\nagg_train_df.reset_index(inplace=True)\nagg_train_df.columns.name = None\n\nagg_train_df['image'] = agg_train_df.image_path.progress_apply(\n    lambda path: Image.open(DATA_ROOT + path).convert('RGB')\n)\n\nagg_train_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:07:54.699010Z","iopub.execute_input":"2026-01-23T17:07:54.699484Z","iopub.status.idle":"2026-01-23T17:08:11.603495Z","shell.execute_reply.started":"2026-01-23T17:07:54.699462Z","shell.execute_reply":"2026-01-23T17:08:11.602887Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"agg_train_df['image_size'] = agg_train_df.image.apply(lambda x: x.size)\nagg_train_df['image_size'].value_counts()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:11.605774Z","iopub.execute_input":"2026-01-23T17:08:11.606028Z","iopub.status.idle":"2026-01-23T17:08:11.612989Z","shell.execute_reply.started":"2026-01-23T17:08:11.606007Z","shell.execute_reply":"2026-01-23T17:08:11.612428Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"np.isclose(\n    agg_train_df[['Dry_Green_g', 'Dry_Clover_g']].sum(axis=1),\n    agg_train_df['GDM_g'],\n    atol=1e-04\n).mean()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:11.613869Z","iopub.execute_input":"2026-01-23T17:08:11.614070Z","iopub.status.idle":"2026-01-23T17:08:11.629227Z","shell.execute_reply.started":"2026-01-23T17:08:11.614053Z","shell.execute_reply":"2026-01-23T17:08:11.628450Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"np.isclose(\n    agg_train_df[['GDM_g', 'Dry_Dead_g']].sum(axis=1),\n    agg_train_df['Dry_Total_g'],\n    atol=1e-04\n).mean()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:11.630338Z","iopub.execute_input":"2026-01-23T17:08:11.631074Z","iopub.status.idle":"2026-01-23T17:08:11.643082Z","shell.execute_reply.started":"2026-01-23T17:08:11.631043Z","shell.execute_reply":"2026-01-23T17:08:11.642516Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(16, 4))\nplt.subplot(1, 3, 1)\nagg_train_df.Dry_Green_g.plot(kind='hist')\n_ = plt.title('Dry_Green_g')\n\nplt.subplot(1, 3, 2)\nagg_train_df.Dry_Clover_g.plot(kind='hist')\n_ = plt.title('Dry_Clover_g')\n\nplt.subplot(1, 3, 3)\nagg_train_df.Dry_Dead_g.plot(kind='hist')\n_ = plt.title('Dry_Dead_g')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:11.643939Z","iopub.execute_input":"2026-01-23T17:08:11.644364Z","iopub.status.idle":"2026-01-23T17:08:12.041567Z","shell.execute_reply.started":"2026-01-23T17:08:11.644334Z","shell.execute_reply":"2026-01-23T17:08:12.040841Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"agg_train_df['Sampling_Date_Month'] = agg_train_df.Sampling_Date.apply(lambda x: x.split('/')[1].strip())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:12.042482Z","iopub.execute_input":"2026-01-23T17:08:12.042749Z","iopub.status.idle":"2026-01-23T17:08:12.048207Z","shell.execute_reply.started":"2026-01-23T17:08:12.042729Z","shell.execute_reply":"2026-01-23T17:08:12.047423Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"agg_train_df = agg_train_df.sort_index().sample(frac=1.0, random_state=31).copy()  # shuffle","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:12.049213Z","iopub.execute_input":"2026-01-23T17:08:12.049549Z","iopub.status.idle":"2026-01-23T17:08:12.062320Z","shell.execute_reply.started":"2026-01-23T17:08:12.049520Z","shell.execute_reply":"2026-01-23T17:08:12.061636Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"agg_train_df['idx'] = agg_train_df.index","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:12.063171Z","iopub.execute_input":"2026-01-23T17:08:12.063454Z","iopub.status.idle":"2026-01-23T17:08:12.075641Z","shell.execute_reply.started":"2026-01-23T17:08:12.063433Z","shell.execute_reply":"2026-01-23T17:08:12.075092Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"half_num = agg_train_df.shape[0] // 6\nhalf_num","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:12.076534Z","iopub.execute_input":"2026-01-23T17:08:12.076833Z","iopub.status.idle":"2026-01-23T17:08:12.088691Z","shell.execute_reply.started":"2026-01-23T17:08:12.076813Z","shell.execute_reply":"2026-01-23T17:08:12.088132Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"head_df = agg_train_df.iloc[:half_num].reset_index(drop=True)\ntail_df = agg_train_df.iloc[half_num:].reset_index(drop=True)\nhead_df.shape[0], tail_df.shape[0], len(set(head_df.idx) | set(tail_df.idx))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:12.089704Z","iopub.execute_input":"2026-01-23T17:08:12.090195Z","iopub.status.idle":"2026-01-23T17:08:12.103071Z","shell.execute_reply.started":"2026-01-23T17:08:12.090166Z","shell.execute_reply":"2026-01-23T17:08:12.102383Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"agg_train_df['fold'] = None","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:12.103984Z","iopub.execute_input":"2026-01-23T17:08:12.104602Z","iopub.status.idle":"2026-01-23T17:08:12.115603Z","shell.execute_reply.started":"2026-01-23T17:08:12.104581Z","shell.execute_reply":"2026-01-23T17:08:12.114804Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"kfold = KFold(n_splits=NFOLD, shuffle=True, random_state=KFOLD_SEED)\nfor i, (trn_idx, val_idx) in enumerate(kfold.split(head_df.index, y=head_df.State, groups=head_df.Sampling_Date)):\n    ori_val_idx = head_df.loc[val_idx, 'idx']\n    agg_train_df.loc[ori_val_idx, 'fold'] = i","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:21.810588Z","iopub.execute_input":"2026-01-23T17:08:21.811177Z","iopub.status.idle":"2026-01-23T17:08:21.820784Z","shell.execute_reply.started":"2026-01-23T17:08:21.811152Z","shell.execute_reply":"2026-01-23T17:08:21.820084Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"kfold = StratifiedGroupKFold(n_splits=NFOLD, shuffle=True, random_state=KFOLD_SEED)\nfor i, (trn_idx, val_idx) in enumerate(kfold.split(tail_df.index, y=tail_df.State, groups=tail_df.Sampling_Date)):\n    ori_val_idx = tail_df.loc[val_idx, 'idx']\n    agg_train_df.loc[ori_val_idx, 'fold'] = i","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:22.031239Z","iopub.execute_input":"2026-01-23T17:08:22.031899Z","iopub.status.idle":"2026-01-23T17:08:22.049991Z","shell.execute_reply.started":"2026-01-23T17:08:22.031876Z","shell.execute_reply":"2026-01-23T17:08:22.049333Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"agg_train_df.sort_index(inplace=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:22.261271Z","iopub.execute_input":"2026-01-23T17:08:22.261805Z","iopub.status.idle":"2026-01-23T17:08:22.265865Z","shell.execute_reply.started":"2026-01-23T17:08:22.261777Z","shell.execute_reply":"2026-01-23T17:08:22.265171Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"agg_train_df.fold.value_counts(dropna=False).sort_index()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:22.744037Z","iopub.execute_input":"2026-01-23T17:08:22.744752Z","iopub.status.idle":"2026-01-23T17:08:22.750967Z","shell.execute_reply.started":"2026-01-23T17:08:22.744722Z","shell.execute_reply":"2026-01-23T17:08:22.750303Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for i in range(NFOLD):\n    trn_df = agg_train_df[agg_train_df.fold != i]\n    val_df = agg_train_df[agg_train_df.fold == i]\n    \n    flag = val_df.Sampling_Date.isin(trn_df.Sampling_Date)\n    print(f'trn({trn_df.shape[0]}) -> val({val_df.shape[0]}): {flag.mean()}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:22.770802Z","iopub.execute_input":"2026-01-23T17:08:22.771275Z","iopub.status.idle":"2026-01-23T17:08:22.783162Z","shell.execute_reply.started":"2026-01-23T17:08:22.771252Z","shell.execute_reply":"2026-01-23T17:08:22.782453Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\n\nclass RegressionDataset(Dataset):\n    def __init__(self, data, transform=None, tta_transform=None, vertical_split=True):\n        self.data = data\n        self.transform = transform\n        self.tta_transform = tta_transform\n        self.vertical_split = vertical_split\n\n    def __len__(self):\n        return self.data.shape[0]\n\n    def __getitem__(self, idx):\n        item = self.data.iloc[idx]\n        image = item.image\n        targets = [item['Dry_Green_g'], item['Dry_Clover_g'], item['Dry_Dead_g']]\n        \n        if self.vertical_split:\n            if self.tta_transform:\n                image = self.tta_transform(image)\n                \n            width, height = image.size\n            mid_point = width // 2\n            left_image = image.crop((0, 0, mid_point, height))\n            right_image = image.crop((mid_point, 0, width, height))\n            \n            if self.transform:\n                left_image = self.transform(left_image)\n                right_image = self.transform(right_image)\n            \n            return left_image, right_image, targets\n        \n        else:\n            if self.transform:\n                image = self.transform(image)\n\n            return image, targets\n\n\ndef create_dataloader(data, target_image_size=(1008, 1008), batch_size=32, shuffle=True, aug=True, tta_transform=None):    \n    if tta_transform:\n        tta_transform = transforms.Compose([\n            tta_transform\n        ])\n        \n        transform = transforms.Compose([\n            transforms.Resize(target_image_size),\n            transforms.ToTensor(),\n            transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n        ])\n    else:\n        tta_transform = None\n        \n        transform = transforms.Compose([\n            transforms.Resize(target_image_size),\n\n            transforms.ToTensor(),\n            transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n        ])\n\n    dataset = RegressionDataset(data, transform=transform, tta_transform=tta_transform)\n    print('dataset size:', len(dataset))\n    dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=shuffle, num_workers=4)\n    return dataloader\n\ndef get_tta_dataloaders(data, target_image_size, batch_size):\n    res = []\n    for transform in [None, transforms.RandomHorizontalFlip(p=1.0)]: # , transforms.RandomHorizontalFlip(p=1.0), transforms.RandomVerticalFlip(p=1.0), transforms.RandomRotation([90, 90])\n        res.append(\n            create_dataloader(data, target_image_size, batch_size, shuffle=False, aug=False, tta_transform=transform)\n        )\n    return res","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:23.331005Z","iopub.execute_input":"2026-01-23T17:08:23.331629Z","iopub.status.idle":"2026-01-23T17:08:23.342891Z","shell.execute_reply.started":"2026-01-23T17:08:23.331600Z","shell.execute_reply":"2026-01-23T17:08:23.342011Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import timm\nimport torch\nimport torch.nn as nn","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:23.344412Z","iopub.execute_input":"2026-01-23T17:08:23.344837Z","iopub.status.idle":"2026-01-23T17:08:23.359091Z","shell.execute_reply.started":"2026-01-23T17:08:23.344810Z","shell.execute_reply":"2026-01-23T17:08:23.358476Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = timm.create_model(MODEL_NAME, pretrained=False, num_classes=3)\nmodel.pretrained_cfg","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:23.359923Z","iopub.execute_input":"2026-01-23T17:08:23.360178Z","iopub.status.idle":"2026-01-23T17:08:27.309603Z","shell.execute_reply.started":"2026-01-23T17:08:23.360155Z","shell.execute_reply":"2026-01-23T17:08:27.308907Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"TARGET_IMAGE_SIZE = (1008, 1008)\n\nclass FiLM(nn.Module):\n    def __init__(self, feat_dim):\n        super().__init__()\n        hidden = max(32, feat_dim // 2)\n        self.mlp = nn.Sequential(\n            nn.Linear(feat_dim, hidden), nn.ReLU(inplace=True), nn.Linear(hidden, feat_dim * 2)\n        )\n\n    def forward(self, context):\n        gamma_beta = self.mlp(context)\n        return torch.chunk(gamma_beta, 2, dim=1)\n\nclass MultiTargetRegressor(nn.Module):\n    def __init__(self, model_name, pretrained=True, num_classes=3, dropout=0.0, freeze_backbone=False):\n        super().__init__()\n        self.backbone = timm.create_model(model_name, pretrained=pretrained, num_classes=0, global_pool='avg')\n        \n        self.film = FiLM(self.backbone.num_features)\n        \n        self.dropout = nn.Dropout(dropout)\n        \n        def make_head():\n            return nn.Sequential(\n                nn.Linear(self.backbone.num_features * 2, 128),\n                nn.ReLU(inplace=True),\n                nn.Dropout(dropout),\n                nn.Linear(128, 1),\n            )\n\n        self.head_green = make_head()\n        self.head_clover = make_head()\n        self.head_dead = make_head()\n        \n        self.softplus = nn.Softplus(beta=1.0)\n    \n        if freeze_backbone:\n            for p in self.backbone.parameters():\n                p.requires_grad = False\n    \n    \n    def forward(self, left_img, right_img):\n        left_feat = self.backbone(left_img)\n        right_feat = self.backbone(right_img)\n        \n        context = (left_feat + right_feat) / 2\n        gamma, beta = self.film(context)\n        \n        left_feat_modulated = left_feat * (1 + gamma) + beta\n        right_feat_modulated = right_feat * (1 + gamma) + beta\n        \n        combined = torch.cat([left_feat_modulated, right_feat_modulated], dim=1)\n        \n        green = self.softplus(self.head_green(combined))    # Bx1\n        clover = self.softplus(self.head_clover(combined)) * 0.8 #Bx1\n        dead = self.softplus(self.head_dead(combined))   # Bx1\n    \n        logits = torch.cat([green, clover, dead], dim=1)  # Bx3 \n\n        return logits","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:27.311190Z","iopub.execute_input":"2026-01-23T17:08:27.311414Z","iopub.status.idle":"2026-01-23T17:08:27.321649Z","shell.execute_reply.started":"2026-01-23T17:08:27.311394Z","shell.execute_reply":"2026-01-23T17:08:27.320889Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.optim import AdamW\nfrom transformers import get_cosine_schedule_with_warmup\n\n\ndef weighted_r2_score(y_true: np.ndarray, y_pred: np.ndarray):\n    weights = np.array([0.1, 0.1, 0.1, 0.2, 0.5])\n    r2_scores = []\n    for i in range(5):\n        y_t = y_true[:, i]\n        y_p = y_pred[:, i]\n        ss_res = np.sum((y_t - y_p) ** 2)\n        ss_tot = np.sum((y_t - np.mean(y_t)) ** 2)\n        r2 = 1 - ss_res / ss_tot if ss_tot > 0 else 0.0\n        r2_scores.append(r2)\n    r2_scores = np.array(r2_scores)\n    weighted_r2 = np.sum(r2_scores * weights) / np.sum(weights)\n    return weighted_r2, r2_scores\n\n\ndef calc_metric(outputs, targets):\n    y_true = np.column_stack((\n        targets,\n        targets[:, :2].sum(axis=1),\n        targets.sum(axis=1),\n    ))\n    \n    y_pred = np.column_stack((\n        outputs,\n        outputs[:, :2].sum(axis=1),\n        outputs.sum(axis=1),\n    ))\n    \n    weighted_r2, r2_scores = weighted_r2_score(y_true, y_pred)\n    return weighted_r2, r2_scores\n\n\ndef train_epoch(model, dataloader, criterion, optimizer, scheduler, device):\n    model.train()\n    total_loss = 0\n    for left_images, right_images, targets in dataloader:\n        left_images = left_images.to(device)\n        right_images = right_images.to(device)\n        targets = torch.stack(targets).T.float().to(device)\n\n        optimizer.zero_grad()\n        outputs = model(left_images, right_images)\n        loss = criterion(outputs, targets)\n        loss.backward()\n        optimizer.step()\n        scheduler.step()\n        total_loss += loss.item()\n    return total_loss / len(dataloader)\n\n\ndef validate(model, dataloader, criterion, device):\n    model.eval()\n    total_loss = 0\n    all_outputs = []\n    all_targets = []\n\n    with torch.no_grad():\n        for left_images, right_images, targets in dataloader:\n            left_images = left_images.to(device)\n            right_images = right_images.to(device)\n            targets = torch.stack(targets).T.float().to(device)\n\n            outputs = model(left_images, right_images)\n            loss = criterion(outputs, targets)\n            total_loss += loss.item()\n\n            all_outputs.append(outputs.detach().cpu())\n            all_targets.append(targets.detach().cpu())\n\n    outputs = torch.cat(all_outputs).numpy()\n    targets = torch.cat(all_targets).numpy()\n    return total_loss / len(dataloader), outputs, targets\n\n\ndef tta_validate(model, dataloaders, criterion, device):\n    if not isinstance(dataloaders, list):\n        dataloaders = [dataloaders]\n    \n    all_loss = []\n    all_outputs = []\n    all_targets = []\n    for dataloader in dataloaders:\n        loss, outputs, targets = validate(model, dataloader, criterion, device)\n        all_loss.append(loss)\n        all_outputs.append(outputs)\n        all_targets.append(targets)\n    \n    avg_loss = np.mean(all_loss)\n    avg_outputs = np.mean(all_outputs, axis=0)\n    avg_targets = np.mean(all_targets, axis=0)\n    weighted_r2, r2_scores = calc_metric(avg_outputs, avg_targets)\n    return avg_loss, weighted_r2, r2_scores\n\n\ndef train_fold(data, fold, batch_size=32, continue_training=False):\n    device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')\n    lr = LR\n    patience = 10\n    num_epochs = 5 if DEBUG else 100\n    warmup_ratio = 0.05\n\n    train_loader = create_dataloader(data[data.fold != fold], TARGET_IMAGE_SIZE, batch_size, shuffle=True, aug=True)\n    val_loader = create_dataloader(data[data.fold == fold], TARGET_IMAGE_SIZE, batch_size, shuffle=False, aug=False)\n    if continue_training:\n        model = MultiTargetRegressor(MODEL_NAME, pretrained=False, num_classes=3, freeze_backbone=False)\n        model_file = f'{OUTPUT_DIR}/best_mode_fold{fold}.pth'\n        model.load_state_dict(torch.load(model_file))\n    else:\n        model = MultiTargetRegressor(MODEL_NAME, pretrained=True, num_classes=3, freeze_backbone=True)\n        \n    model.to(device)\n    criterion = nn.SmoothL1Loss()\n    optimizer = AdamW(model.parameters(), lr=lr / 500 if continue_training else lr)\n    num_training_steps = num_epochs * len(train_loader)\n    warmup_steps = int(warmup_ratio * num_training_steps)\n    scheduler = get_cosine_schedule_with_warmup(optimizer, num_warmup_steps=warmup_steps, num_training_steps=num_training_steps)\n\n    history = []\n    best_score = -float('inf')\n    epochs_without_improvement = 0\n    for epoch in range(num_epochs):\n        train_loss = train_epoch(model, train_loader, criterion, optimizer, scheduler, device)\n        val_loss, weighted_r2, r2_scores = tta_validate(model, val_loader, criterion, device)\n        if epoch % 10 == 0:\n            print(f\"Epoch [{epoch}/{num_epochs}]: Train Loss: {train_loss:.4f}, Val Loss: {val_loss:.4f}, weighted_r2: {weighted_r2:.4f}, lr: {optimizer.param_groups[0]['lr']}\")\n        history.append({\n            'train_loss': train_loss,\n            'val_loss': val_loss,\n            'lr': optimizer.param_groups[0]['lr'],\n            'weighted_r2': weighted_r2,\n            'r2_scores': r2_scores,\n        })\n        \n        if weighted_r2 > best_score:\n            best_loss = val_loss\n            best_score = weighted_r2\n            epochs_without_improvement = 0\n            torch.save(model.state_dict(), f'{OUTPUT_DIR}/best_mode_fold{fold}.pth')\n        else:\n            epochs_without_improvement += 1\n        \n        if epochs_without_improvement >= patience:\n            print(f\"早停: epoch={epoch}, {patience} 个 epoch 无改善\")\n            break\n    print(f\"\\nTraining completed. Best weighted_r2: {best_score:.4f}\")\n    return history, best_score","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:27.322625Z","iopub.execute_input":"2026-01-23T17:08:27.322879Z","iopub.status.idle":"2026-01-23T17:08:27.344002Z","shell.execute_reply.started":"2026-01-23T17:08:27.322845Z","shell.execute_reply":"2026-01-23T17:08:27.343275Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"OUTPUT_DIR = 'trained_models/'\nif not os.path.exists(OUTPUT_DIR):\n    os.mkdir(OUTPUT_DIR)\n\n\nif TRAIN:\n    all_best_score = []\n    for i in range(2 if DEBUG else NFOLD):\n        print(f'### fold={i}')\n        history, best_score = train_fold(agg_train_df, fold=i, batch_size=32, continue_training=False)\n        all_best_score.append(best_score)\n        history = pd.DataFrame(history)\n\n        history.to_json(\n            f'{OUTPUT_DIR}/history_fold{i}.jsonl',\n            orient='records',\n            lines=True,\n            force_ascii=False,\n        )\n\n        # plot\n        plt.figure(figsize=(16, 4))\n\n        plt.subplot(1, 3, 1)\n        plt.title('LR')\n        plt.plot(history.lr)\n\n        plt.subplot(1, 3, 2)\n        plt.title('Loss')\n        plt.plot(history.train_loss, label='train')\n        plt.plot(history.val_loss, label='val')\n        plt.legend()\n\n        plt.subplot(1, 3, 3)\n        plt.title('weighted_r2')\n        plt.plot(history.weighted_r2)\n        plt.show()\n    \n    print('Avg CV:', np.mean(all_best_score))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:27.345559Z","iopub.execute_input":"2026-01-23T17:08:27.345752Z","iopub.status.idle":"2026-01-23T17:08:27.361204Z","shell.execute_reply.started":"2026-01-23T17:08:27.345735Z","shell.execute_reply":"2026-01-23T17:08:27.360323Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if TRAIN:\n    all_best_score = []\n\n    for i in range(2 if DEBUG else NFOLD):\n        print(f'### fold={i}')\n        history, best_score = train_fold(agg_train_df, fold=i, batch_size=8, continue_training=True)\n        all_best_score.append(best_score)\n        history = pd.DataFrame(history)\n\n        history.to_json(\n            f'{OUTPUT_DIR}/history_fold{i}.jsonl',\n            orient='records',\n            lines=True,\n            force_ascii=False,\n        )\n        plt.figure(figsize=(16, 4))\n\n        plt.subplot(1, 3, 1)\n        plt.title('LR')\n        plt.plot(history.lr)\n\n        plt.subplot(1, 3, 2)\n        plt.title('Loss')\n        plt.plot(history.train_loss, label='train')\n        plt.plot(history.val_loss, label='val')\n        plt.legend()\n\n        plt.subplot(1, 3, 3)\n        plt.title('weighted_r2')\n        plt.plot(history.weighted_r2)\n        plt.show()\n    \n    print('Avg CV:', np.mean(all_best_score))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:27.362081Z","iopub.execute_input":"2026-01-23T17:08:27.362512Z","iopub.status.idle":"2026-01-23T17:08:27.377790Z","shell.execute_reply.started":"2026-01-23T17:08:27.362491Z","shell.execute_reply":"2026-01-23T17:08:27.377155Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nfrom pathlib import Path\n\n\ndef get_lastest_saved_models():\n    model_root = '/kaggle/input/csiro-simple-output/pytorch/default/'\n    \n    latest = 1\n    for version in os.listdir(model_root):\n        try:\n            version = int(version)\n        except:\n            continue\n\n        if version > latest:\n            latest = version\n\n    return f'{model_root}/{latest}/trained_models/'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:27.379645Z","iopub.execute_input":"2026-01-23T17:08:27.379856Z","iopub.status.idle":"2026-01-23T17:08:27.394413Z","shell.execute_reply.started":"2026-01-23T17:08:27.379838Z","shell.execute_reply":"2026-01-23T17:08:27.393625Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"SAVED_MODELS = '/kaggle/input/zhuangjia-models-with-new-param'\n\n\ndef predict(model, dataloader, device):\n    model.to(device)\n    model.eval()\n\n    all_outputs = []\n    with torch.no_grad():\n        for left_images, right_images, targets in dataloader:\n            left_images = left_images.to(device)\n            right_images = right_images.to(device)\n    \n            outputs = model(left_images, right_images)\n            all_outputs.append(outputs.detach().cpu())\n    \n    outputs = torch.cat(all_outputs).numpy()\n    return outputs","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:27.395364Z","iopub.execute_input":"2026-01-23T17:08:27.395652Z","iopub.status.idle":"2026-01-23T17:08:27.411639Z","shell.execute_reply.started":"2026-01-23T17:08:27.395622Z","shell.execute_reply":"2026-01-23T17:08:27.410978Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def tta_predict(model, dataloaders, device):\n    all_outputs = []\n    for dataloader in dataloaders:\n        outputs = predict(model, dataloader, device)\n        all_outputs.append(outputs)\n    avg_outputs = np.mean(all_outputs, axis=0)\n    return avg_outputs\n\ndef kfold_predict(dataloaders):\n    device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')\n    \n    all_preds = []\n    for model_file in Path(SAVED_MODELS).glob('*.pth'):\n        print(model_file.name)\n        model = MultiTargetRegressor(MODEL_NAME, pretrained=False, num_classes=3)\n        model.load_state_dict(torch.load(model_file))\n\n        preds = tta_predict(model, dataloaders, device)\n        all_preds.append(preds)\n    avg_preds = np.mean(all_preds, axis=0)\n    return avg_preds","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:27.412697Z","iopub.execute_input":"2026-01-23T17:08:27.413071Z","iopub.status.idle":"2026-01-23T17:08:27.424723Z","shell.execute_reply.started":"2026-01-23T17:08:27.413040Z","shell.execute_reply":"2026-01-23T17:08:27.424186Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_df = pd.read_csv(DATA_ROOT + 'test.csv')\n\ntest_df['target'] = 0.0\ntest_df[['sample_id_prefix', 'sample_id_suffix']] = test_df.sample_id.str.split('__', expand=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:27.425644Z","iopub.execute_input":"2026-01-23T17:08:27.425882Z","iopub.status.idle":"2026-01-23T17:08:27.441547Z","shell.execute_reply.started":"2026-01-23T17:08:27.425863Z","shell.execute_reply":"2026-01-23T17:08:27.440749Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cols = ['sample_id_prefix', 'image_path']\nagg_test_df = test_df.groupby(cols).apply(lambda df: df.set_index('target_name').target)\nagg_test_df.reset_index(inplace=True)\nagg_test_df.columns.name = None\n\nagg_test_df['image'] = agg_test_df.image_path.progress_apply(\n    lambda path: Image.open(DATA_ROOT + path).convert('RGB')\n)\n\nagg_test_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:27.442566Z","iopub.execute_input":"2026-01-23T17:08:27.442866Z","iopub.status.idle":"2026-01-23T17:08:27.521869Z","shell.execute_reply.started":"2026-01-23T17:08:27.442835Z","shell.execute_reply":"2026-01-23T17:08:27.521178Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_loader = get_tta_dataloaders(agg_test_df, TARGET_IMAGE_SIZE, 4)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:27.522783Z","iopub.execute_input":"2026-01-23T17:08:27.523048Z","iopub.status.idle":"2026-01-23T17:08:27.527800Z","shell.execute_reply.started":"2026-01-23T17:08:27.523026Z","shell.execute_reply":"2026-01-23T17:08:27.527185Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"preds = kfold_predict(test_loader)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:27.528742Z","iopub.execute_input":"2026-01-23T17:08:27.529006Z","iopub.status.idle":"2026-01-23T17:08:54.738464Z","shell.execute_reply.started":"2026-01-23T17:08:27.528978Z","shell.execute_reply":"2026-01-23T17:08:54.737663Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"agg_test_df[['Dry_Green_g', 'Dry_Clover_g', 'Dry_Dead_g']] = preds\nagg_test_df['GDM_g'] = agg_test_df.Dry_Green_g + agg_test_df.Dry_Clover_g\nagg_test_df['Dry_Total_g'] = agg_test_df.GDM_g + agg_test_df.Dry_Dead_g","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:54.740631Z","iopub.execute_input":"2026-01-23T17:08:54.740902Z","iopub.status.idle":"2026-01-23T17:08:54.747235Z","shell.execute_reply.started":"2026-01-23T17:08:54.740875Z","shell.execute_reply":"2026-01-23T17:08:54.746437Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"agg_test_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:54.748105Z","iopub.execute_input":"2026-01-23T17:08:54.748403Z","iopub.status.idle":"2026-01-23T17:08:54.766285Z","shell.execute_reply.started":"2026-01-23T17:08:54.748383Z","shell.execute_reply":"2026-01-23T17:08:54.765681Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cols = ['Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'Dry_Total_g', 'GDM_g']\nsub_df = agg_test_df.set_index('sample_id_prefix')[cols].stack()\nsub_df = sub_df.reset_index()\nsub_df.columns = ['sample_id_prefix', 'target_name', 'target']\n\nsub_df['sample_id'] = sub_df.sample_id_prefix + '__' + sub_df.target_name","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:08:54.767380Z","iopub.execute_input":"2026-01-23T17:08:54.768074Z","iopub.status.idle":"2026-01-23T17:08:54.783605Z","shell.execute_reply.started":"2026-01-23T17:08:54.768051Z","shell.execute_reply":"2026-01-23T17:08:54.782859Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cols = ['sample_id', 'target']\nsub_df[cols].to_csv('submission_zhuangjia.csv', index=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:09:03.157117Z","iopub.execute_input":"2026-01-23T17:09:03.158016Z","iopub.status.idle":"2026-01-23T17:09:03.163397Z","shell.execute_reply.started":"2026-01-23T17:09:03.157989Z","shell.execute_reply":"2026-01-23T17:09:03.162767Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!head submission_zhuangjia.csv","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:09:03.578625Z","iopub.execute_input":"2026-01-23T17:09:03.578916Z","iopub.status.idle":"2026-01-23T17:09:04.080796Z","shell.execute_reply.started":"2026-01-23T17:09:03.578892Z","shell.execute_reply":"2026-01-23T17:09:04.079986Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# ensemble","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport os\n\n\nW_SIGLIP = 0.45\nW_DINO   = 0.55\n\nFILES = {\n    'siglip': 'submission_siglip_dinov3.csv',\n    'dino': 'submission_zhuangjia.csv'\n}\n\nOUTPUT_FILE = 'submission.csv'\n\nALL_TARGETS = ['Dry_Green_g', 'Dry_Clover_g', 'Dry_Dead_g', 'GDM_g', 'Dry_Total_g']\n\n\ndef enforce_mass_balance(df_wide, fixed_clover=None):\n    ordered_cols = ['Dry_Green_g', 'Dry_Clover_g', 'Dry_Dead_g', 'GDM_g', 'Dry_Total_g']\n\n    Y = df_wide[ordered_cols].values.T\n    \n    if fixed_clover:\n        clover_fixed = Y[1, :].copy() \n        Y[3, :] = Y[0, :] + clover_fixed  \n        Y[4, :] = Y[3, :] + Y[2, :]  \n        Y_reconciled = Y\n    else:\n        C = np.array([\n            [1, 1, 0, -1,  0],\n            [0, 0, 1,  1, -1]\n        ])\n        \n        C_T = C.T\n        try:\n            inv_CCt = np.linalg.inv(C @ C_T)\n            P = np.eye(5) - C_T @ inv_CCt @ C\n        except np.linalg.LinAlgError:\n            print(\"Warning: Singular matrix in projection. Skipping constraint enforcement.\")\n            return df_wide\n\n        Y_reconciled = P @ Y\n    \n    Y_reconciled = Y_reconciled.T\n    Y_reconciled = np.maximum(0, Y_reconciled) \n\n    df_out = df_wide.copy()\n    df_out[ordered_cols] = Y_reconciled\n    \n    return df_out\n\ndef robust_ensemble(file_paths, weights):\n    print(f\"--- Starting Ensemble ---\")\n    print(f\"Weights: {weights}\")\n    print(\"NOTE: Using DINO-only for Dry_Clover_g (better detection)\")\n    \n    dfs = []\n    for name, path in file_paths.items():\n        if not os.path.exists(path):\n            raise FileNotFoundError(f\"Missing file: {path}\")\n        \n        df = pd.read_csv(path).sort_values('sample_id').reset_index(drop=True)\n        dfs.append(df)\n        print(f\"Loaded {name}: {len(df)} rows\")\n\n    base_ids = dfs[0]['sample_id']\n    if not all(df['sample_id'].equals(base_ids) for df in dfs[1:]):\n        raise ValueError(\"Sample IDs do not match between submission files!\")\n\n    dfs_split = []\n    for df in dfs:\n        df_split = df.copy()\n        df_split[['image_id', 'target_name']] = df_split['sample_id'].str.rsplit('__', n=1, expand=True)\n        dfs_split.append(df_split)\n    \n    dino_idx = list(file_paths.keys()).index('dino')\n    siglip_idx = list(file_paths.keys()).index('siglip')\n    dino_df = dfs_split[dino_idx]\n    siglip_df = dfs_split[siglip_idx]\n    \n    ensemble_results = []\n    \n    for target in ALL_TARGETS:\n        dino_target = dino_df[dino_df['target_name'] == target].copy()\n        siglip_target = siglip_df[siglip_df['target_name'] == target].copy()\n        \n        if target == 'Dry_Clover_g':\n            print(f\"Using DINO-only for {target}\")\n            ensemble_results.append(dino_target[['sample_id', 'target']])\n        else:\n            merged = pd.merge(\n                dino_target[['sample_id', 'target']].rename(columns={'target': 'dino_pred'}),\n                siglip_target[['sample_id', 'target']].rename(columns={'target': 'siglip_pred'}),\n                on='sample_id'\n            )\n            \n            w_vec = np.array([weights['dino'], weights['siglip']])\n            w_vec = w_vec / w_vec.sum()\n            \n            merged['target'] = merged['dino_pred'] * w_vec[0] + merged['siglip_pred'] * w_vec[1]\n            ensemble_results.append(merged[['sample_id', 'target']])\n    \n    ensemble_df = pd.concat(ensemble_results, ignore_index=True)\n    print(\"Weighted average complete (DINO-only for Clover).\")\n\n    ensemble_df[['image_id', 'target_name']] = ensemble_df['sample_id'].str.rsplit('__', n=1, expand=True)\n    wide_df = ensemble_df.pivot(index='image_id', columns='target_name', values='target').reset_index()\n\n    print(\"Applying Mass Balance Constraints (Dry_Clover_g fixed to DINO values)...\")\n    wide_balanced = enforce_mass_balance(wide_df, fixed_clover=True)\n\n    long_balanced = wide_balanced.melt(\n        id_vars='image_id', \n        value_vars=ALL_TARGETS,\n        var_name='target_name',\n        value_name='target'\n    )\n\n    long_balanced['sample_id'] = long_balanced['image_id'] + '__' + long_balanced['target_name']\n    final_submission = long_balanced[['sample_id', 'target']].sort_values('sample_id').reset_index(drop=True)\n\n    return final_submission\n\n\nif __name__ == \"__main__\":\n    ensemble_weights = {\n        'siglip': W_SIGLIP,\n        'dino': W_DINO\n    }\n    \n    try:\n        submission = robust_ensemble(FILES, ensemble_weights)\n        submission.to_csv(OUTPUT_FILE, index=False)\n        print(f\"\\nSuccess! Saved to {OUTPUT_FILE}\")\n        print(submission.head())\n        print(\"\\nStats:\")\n        print(submission['target'].describe())\n        \n    except Exception as e:\n        print(f\"\\nError during ensembling: {e}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-23T17:09:15.224353Z","iopub.execute_input":"2026-01-23T17:09:15.225077Z","iopub.status.idle":"2026-01-23T17:09:15.282825Z","shell.execute_reply.started":"2026-01-23T17:09:15.225045Z","shell.execute_reply":"2026-01-23T17:09:15.282177Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}