{"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,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":293691576,"sourceType":"kernelVersion"},{"sourceId":268942,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":230141,"modelId":251887},{"sourceId":711299,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":540408,"modelId":553604},{"sourceId":719660,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":547330,"modelId":560139},{"sourceId":128815,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":108529,"modelId":132847},{"sourceId":139474,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":118113,"modelId":141350}],"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":"code","source":"import os\nimport gc\nimport random\nimport numpy as np\nimport pandas as pd\nimport cv2\nimport torch\nimport torch.nn as nn\nfrom PIL import Image\nfrom tqdm.auto import tqdm\nfrom pathlib import Path\nfrom copy import deepcopy\n\n# Требуем все зависимости\nimport timm\nfrom transformers import AutoModel, AutoImageProcessor, AutoTokenizer\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 torchvision import transforms as T\n\n# =========================================================================================\n# CONFIGURATION\n# =========================================================================================\nclass Config:\n    def __init__(self):\n        # Paths из твоего блокнота\n        self.DATA_PATH = Path(\"/kaggle/input/csiro-biomass/\")\n        self.SPLIT_PATH = Path(\"/kaggle/input/csiro-datasplit/csiro_data_split.csv\")\n        self.SIGLIP_PATH = \"/kaggle/input/google-siglip-so400m-patch14-384/transformers/default/1\"\n        \n        # Из твоего блокнота вижу, что есть DinoV3 модели\n        self.DINOV3_MODEL_DIR = \"/kaggle/input/baseline-dinov3/pytorch/default/1/baseline-dinov3\"\n        \n        # General\n        self.SEED = 927\n        self.DEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n        \n        # SigLIP specific\n        self.PATCH_SIZE = 520\n        self.OVERLAP = 16\n        \n        # DinoV3 specific (из твоего блокнота)\n        self.DINOV3_MODEL_NAME = 'vit_huge_plus_patch16_dinov3.lvd1689m'\n        self.DINOV3_IMG_SIZE = 512\n        self.DINOV3_BATCH_SIZE = 1\n        self.DINOV3_TTA_STEPS = 1\n        \n        # Blending weights\n        self.SIGLIP_WEIGHT = 0.4  # SigLIP из твоего блокнота\n        self.DINOV3_WEIGHT = 0.6  # DinoV3 из твоего блокнота\n        \n        # Target definitions\n        self.TARGET_NAMES = ['Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'Dry_Total_g', 'GDM_g']\n        self.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        }\n        \n        self.ALL_TARGET_COLS = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n\ncfg = Config()\n\n# Проверяем критические пути\ndef validate_critical_paths():\n    critical_paths = [\n        cfg.DATA_PATH,\n        cfg.SPLIT_PATH,\n        Path(cfg.SIGLIP_PATH),\n    ]\n    \n    for path in critical_paths:\n        if not path.exists():\n            raise FileNotFoundError(f\"Critical path does not exist: {path}\")\n    \n    # DinoV3 модель может быть не обязательной, если будем использовать только SigLIP\n    if not Path(cfg.DINOV3_MODEL_DIR).exists():\n        print(f\"Warning: DinoV3 model directory not found: {cfg.DINOV3_MODEL_DIR}\")\n        print(\"Will use SigLIP only pipeline\")\n        cfg.USE_DINOV3 = False\n    else:\n        cfg.USE_DINOV3 = True\n\nvalidate_critical_paths()\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\n\nseeding(cfg.SEED)\n\n# =========================================================================================\n# 1. SIGLIP PIPELINE (Score: 0.72) - ИЗ ТВОЕГО БЛОКНОТА\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\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\ndef post_process_biomass(df_preds):\n    ordered_cols = [\"Dry_Green_g\", \"Dry_Clover_g\", \"Dry_Dead_g\", \"GDM_g\", \"Dry_Total_g\"]\n    \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    \n    return df_out\n\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)\n            x1 = max(0, x2 - patch_size)\n            patch = image[y1:y2, x1:x2, :]\n            patches.append(patch)\n    return patches\n\ndef compute_siglip_embeddings(model_path, df):\n    print(f\"Computing SigLIP 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, local_files_only=True)\n    \n    EMBEDDINGS = []\n    \n    for _, row in tqdm(df.iterrows(), total=len(df), desc=\"SigLIP Embeddings\"):\n        img_path = row['image_path']\n        img = cv2.imread(img_path)\n        if img is None: \n            raise ValueError(f\"Image not found: {img_path}\")\n        \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        \n        inputs = processor(images=images, return_tensors=\"pt\").to(cfg.DEVICE)\n        with torch.no_grad():\n            features = model.get_image_features(**inputs)\n        \n        avg_embed = features.mean(dim=0).cpu().numpy()\n        EMBEDDINGS.append(avg_embed)\n        \n        if len(EMBEDDINGS) % 50 == 0:\n            torch.cuda.empty_cache()\n    \n    torch.cuda.empty_cache()\n    return np.stack(EMBEDDINGS)\n\ndef generate_semantic_features(image_embeddings_np, model_path):\n    print(\"Generating Semantic Features...\")\n    model = AutoModel.from_pretrained(model_path, local_files_only=True).to(cfg.DEVICE)\n    tokenizer = AutoTokenizer.from_pretrained(model_path, local_files_only=True)\n    \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    \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            \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    \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    \n    torch.cuda.empty_cache()\n    return df_scores.values\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        \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        \n        features = [self.pca.transform(X_scaled)]\n        \n        if self.pls_fitted_:\n            features.append(self.pls.transform(X_scaled))\n            \n        features.append(self.gmm.predict_proba(X_scaled))\n        \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            \n        return np.hstack(features)\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    n_splits = int(train_data['fold'].nunique())\n    \n    X_train_full = train_data[[f\"emb{i}\" for i in range(1152)]].values.astype(np.float32)\n    X_test_raw = test_data[[f\"emb{i}\" for i in range(1152)]].values.astype(np.float32)\n    y_train_full = train_data[cfg.TARGET_NAMES].values.astype(np.float32)\n    \n    for fold in range(n_splits):\n        print(f\"Processing Fold {fold}...\")\n        train_mask = train_data['fold'] != fold\n        \n        X_tr = X_train_full[train_mask]\n        y_tr = y_train_full[train_mask] / target_max_arr\n        \n        sem_tr_fold = sem_tr[train_mask]\n        \n        engine = deepcopy(feature_engine)\n        engine.fit(X_tr, y=y_tr, X_semantic=sem_tr_fold)\n        \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        \n        fold_test_pred = np.zeros([len(test_data), len(cfg.TARGET_NAMES)])\n        \n        for k in range(len(cfg.TARGET_NAMES)):\n            target_name = cfg.TARGET_NAMES[k]\n            \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        \n        del engine, X_tr, y_tr, sem_tr_fold, x_tr_eng, x_te_eng\n        gc.collect()\n        \n    return y_pred_test_accum / n_splits\n\ndef run_siglip_pipeline():\n    print(\"=\" * 70)\n    print(\"RUNNING SIGLIP PIPELINE (Score: 0.72)\")\n    print(\"=\" * 70)\n    \n    # Load data\n    train_df = pd.read_csv(cfg.SPLIT_PATH)\n    test_df_raw = pd.read_csv(cfg.DATA_PATH / 'test.csv')\n    \n    cols_to_keep = [c for c in train_df.columns if not c.startswith('emb')]\n    train_df = train_df[cols_to_keep]\n    \n    if not str(train_df['image_path'].iloc[0]).startswith('/'):\n        train_df['image_path'] = train_df['image_path'].apply(\n            lambda p: str(cfg.DATA_PATH / 'train' / os.path.basename(p)) \n        )\n    \n    test_df = pivot_table(test_df_raw)\n    test_df['image_path'] = test_df['image_path'].apply(\n        lambda p: str(cfg.DATA_PATH / p)\n    )\n    \n    # Compute SigLIP embeddings\n    train_embeddings = compute_siglip_embeddings(cfg.SIGLIP_PATH, train_df)\n    test_embeddings = compute_siglip_embeddings(cfg.SIGLIP_PATH, test_df)\n    \n    emb_cols = [f\"emb{i}\" for i in range(train_embeddings.shape[1])]\n    train_feat_df = pd.concat([train_df, pd.DataFrame(train_embeddings, columns=emb_cols)], axis=1)\n    test_feat_df = pd.concat([test_df, pd.DataFrame(test_embeddings, columns=emb_cols)], axis=1)\n    \n    # Generate semantic features\n    all_emb = np.vstack([train_embeddings, test_embeddings])\n    all_semantic = generate_semantic_features(all_emb, cfg.SIGLIP_PATH)\n    sem_train = all_semantic[:len(train_df)]\n    sem_test = all_semantic[len(train_df):]\n    \n    # Model parameters из твоего блокнота\n    params_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': 927,\n        'allow_writing_files': False\n    }\n    params_xgb = {\n        'n_estimators': 1354, 'learning_rate': 0.010, 'max_depth': 3, 'subsample': 0.60, \n        'random_state': 927\n    }\n    params_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': 927\n    }\n    params_hist = {\n        'max_iter': 300, 'learning_rate': 0.05, 'max_depth': None, 'l2_regularization': 0.44,\n        'random_state': 927\n    }\n    \n    feat_engine = SupervisedEmbeddingEngine(n_pca=0.75, n_pls=8, n_gmm=6)\n    \n    print(\"Training & Inferring Models...\")\n    \n    pred_hist = cross_validate_predict(\n        HistGradientBoostingRegressor, params_hist, \n        train_feat_df, test_feat_df, sem_train, sem_test, feat_engine\n    )\n    \n    pred_gb = cross_validate_predict(\n        GradientBoostingRegressor, params_xgb, \n        train_feat_df, test_feat_df, sem_train, sem_test, feat_engine\n    )\n    \n    pred_cat = cross_validate_predict(\n        CatBoostRegressor, params_cat, \n        train_feat_df, test_feat_df, sem_train, sem_test, feat_engine\n    )\n    \n    pred_lgbm = cross_validate_predict(\n        LGBMRegressor, params_lgbm, \n        train_feat_df, test_feat_df, sem_train, sem_test, feat_engine\n    )\n    \n    final_pred = (pred_hist + pred_gb + pred_cat + pred_lgbm) / 4.0\n    test_feat_df[cfg.TARGET_NAMES] = final_pred\n    \n    test_processed = post_process_biomass(test_feat_df)\n    \n    sub_df = melt_table(test_processed)\n    output_path = \"submission_siglip.csv\"\n    sub_df.to_csv(output_path, index=False)\n    \n    print(f\"✓ SigLIP submission generated: {output_path}\")\n    return sub_df\n\n# =========================================================================================\n# 2. DINOV3 PIPELINE (Score: 0.70) - ИЗ ТВОЕГО БЛОКНОТА\n# =========================================================================================\ndef clean_image(img):\n    h, w = img.shape[:2]\n    img = img[0:int(h*0.90), :]\n    \n    try:\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    except:\n        pass\n    \n    return img\n\nclass 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\n\nclass 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        nf = self.backbone.num_features\n        \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        \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        \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                # Попробуем загрузить из timm\n                print(\"Loading backbone weights from timm...\")\n                sd = timm.create_model(self.model_name, pretrained=True, num_classes=0, global_pool='').state_dict()\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        \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        \n        gdm    = green + clover\n        total  = gdm + dead\n        \n        return total, gdm, green, clover, dead\n\nclass BiomassTestDataset:\n    def __init__(self, img_dir):\n        self.img_dir = img_dir\n        if not os.path.exists(img_dir):\n            raise FileNotFoundError(f\"Image directory not found: {img_dir}\")\n        \n        self.image_files = [f for f in os.listdir(img_dir) \n                          if f.lower().endswith(('.jpg', '.jpeg', '.png'))]\n        self.image_files.sort()\n        \n        if len(self.image_files) == 0:\n            raise FileNotFoundError(f\"No images found in {img_dir}\")\n    \n    def __len__(self):\n        return len(self.image_files)\n    \n    def __getitem__(self, idx):\n        img_path = os.path.join(self.img_dir, self.image_files[idx])\n        img = cv2.imread(img_path)\n        if img is None:\n            raise ValueError(f\"Could not read image: {img_path}\")\n        \n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        img = clean_image(img)\n        \n        h, w = img.shape[:2]\n        mid = w // 2\n        left = img[:, :mid]\n        right = img[:, mid:]\n        \n        return left, right, self.image_files[idx]\n\ndef get_tta_transforms():\n    base_transform = T.Compose([\n        T.ToPILImage(),\n        T.Resize((cfg.DINOV3_IMG_SIZE, cfg.DINOV3_IMG_SIZE)),\n        T.ToTensor(),\n        T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n    ])\n    \n    hflip_transform = T.Compose([\n        T.ToPILImage(),\n        T.Resize((cfg.DINOV3_IMG_SIZE, cfg.DINOV3_IMG_SIZE)),\n        T.RandomHorizontalFlip(p=1.0),\n        T.ToTensor(),\n        T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n    ])\n    \n    transforms = [base_transform]\n    if cfg.DINOV3_TTA_STEPS > 1:\n        transforms.append(hflip_transform)\n    \n    return transforms\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(left_np).unsqueeze(0).to(cfg.DEVICE)\n        right_tensor = tfm(right_np).unsqueeze(0).to(cfg.DEVICE)\n        \n        total, gdm, green, clover, dead = model(left_tensor, right_tensor)\n        \n        all_tta_preds.append([\n            total.cpu().item(),\n            gdm.cpu().item(),\n            green.cpu().item()\n        ])\n    \n    return np.mean(all_tta_preds, axis=0)\n\ndef run_dinov3_inference():\n    print(\"Running DinoV3 inference...\")\n    \n    test_dir = cfg.DATA_PATH / 'test'\n    dataset = BiomassTestDataset(test_dir)\n    \n    tta_transforms = get_tta_transforms()\n    accumulated_preds = np.zeros((len(dataset), 3), dtype=np.float32)\n    filenames = dataset.image_files.copy()\n    \n    model_dir = Path(cfg.DINOV3_MODEL_DIR)\n    if not model_dir.exists():\n        raise FileNotFoundError(f\"DinoV3 model directory not found: {model_dir}\")\n    \n    n_folds = 5\n    successful_folds = 0\n    \n    for fold in range(n_folds):\n        print(f\"  Processing DinoV3 Fold {fold}...\")\n        \n        weight_path = model_dir / f'best_model_fold{fold}.pth'\n        if not weight_path.exists():\n            # Попробуем альтернативные имена\n            alt_names = [\n                f'model_fold{fold}.pth',\n                f'fold{fold}.pth',\n                f'checkpoint_fold{fold}.pth',\n            ]\n            found = False\n            for alt_name in alt_names:\n                alt_path = model_dir / alt_name\n                if alt_path.exists():\n                    weight_path = alt_path\n                    found = True\n                    break\n            \n            if not found:\n                raise FileNotFoundError(f\"No model file found for fold {fold}\")\n        \n        model = BiomassModel(cfg.DINOV3_MODEL_NAME, pretrained=False)\n        \n        if torch.cuda.is_available():\n            state = torch.load(weight_path)\n        else:\n            state = torch.load(weight_path, map_location='cpu')\n        \n        if isinstance(state, dict):\n            if 'model_state_dict' in state:\n                sd = state['model_state_dict']\n            elif 'state_dict' in state:\n                sd = state['state_dict']\n            else:\n                sd = state\n        else:\n            sd = state\n        \n        model.load_state_dict(sd, strict=False)\n        model.to(cfg.DEVICE)\n        model.eval()\n        \n        for i in tqdm(range(len(dataset)), desc=f\"Fold {fold}\", leave=False):\n            left, right, filename = dataset[i]\n            pred = predict_with_tta(model, left, right, tta_transforms)\n            accumulated_preds[i] += pred\n        \n        successful_folds += 1\n        \n        del model\n        torch.cuda.empty_cache()\n        gc.collect()\n    \n    if successful_folds == 0:\n        raise RuntimeError(\"No DinoV3 folds were processed successfully\")\n    \n    final_predictions = accumulated_preds / successful_folds\n    \n    pred_total = final_predictions[:, 0]\n    pred_gdm = final_predictions[:, 1]\n    pred_green = final_predictions[:, 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    preds_wide = pd.DataFrame(preds_all, columns=cfg.ALL_TARGET_COLS)\n    preds_wide.insert(0, 'image_path', [f\"test/{fn}\" for fn in filenames])\n    \n    return preds_wide\n\ndef run_dinov3_pipeline():\n    print(\"=\" * 70)\n    print(\"RUNNING DINOV3 PIPELINE (Score: 0.70)\")\n    print(\"=\" * 70)\n    \n    preds_wide = run_dinov3_inference()\n    test_df = pd.read_csv(cfg.DATA_PATH / 'test.csv')\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    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    \n    submission = submission[['sample_id', 'target']]\n    submission['target'] = submission['target'].fillna(0.0)\n    submission = submission.sort_values('sample_id').reset_index(drop=True)\n    \n    output_path = \"submission_dinov3.csv\"\n    submission.to_csv(output_path, index=False)\n    \n    print(f\"✓ DinoV3 submission generated: {output_path}\")\n    return submission\n\n# =========================================================================================\n# FINAL ENSEMBLE\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    for col in ordered_cols:\n        if col not in df_wide.columns:\n            df_wide[col] = 0.0\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        inv_CCt = np.linalg.inv(C @ C_T)\n        P = np.eye(5) - C_T @ inv_CCt @ C\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(siglip_df, dinov3_df, weights):\n    print(f\"--- Starting Final Ensemble ---\")\n    print(f\"Weights: SigLIP: {weights['siglip']:.2f}, DinoV3: {weights['dinov3']:.2f}\")\n    \n    siglip_df = siglip_df.sort_values('sample_id').reset_index(drop=True)\n    dinov3_df = dinov3_df.sort_values('sample_id').reset_index(drop=True)\n    \n    if not siglip_df['sample_id'].equals(dinov3_df['sample_id']):\n        raise ValueError(\"Sample IDs do not match between submissions!\")\n    \n    merged = pd.merge(\n        siglip_df, \n        dinov3_df, \n        on='sample_id', \n        suffixes=('_siglip', '_dinov3')\n    )\n    \n    merged[['image_id', 'target_name']] = merged['sample_id'].str.rsplit('__', n=1, expand=True)\n    \n    ensemble_results = []\n    \n    for target in cfg.ALL_TARGET_COLS:\n        target_data = merged[merged['target_name'] == target].copy()\n        \n        if target == 'Dry_Clover_g':\n            # Для Clover используем только DinoV3 (лучше детектирует)\n            print(f\"Using DinoV3 only for {target}\")\n            target_data['target'] = target_data['target_dinov3']\n        else:\n            target_data['target'] = (\n                target_data['target_siglip'] * weights['siglip'] + \n                target_data['target_dinov3'] * weights['dinov3']\n            )\n        \n        ensemble_results.append(target_data[['sample_id', 'target']])\n    \n    ensemble_df = pd.concat(ensemble_results, ignore_index=True)\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    for col in cfg.ALL_TARGET_COLS:\n        if col not in wide_df.columns:\n            wide_df[col] = 0.0\n    \n    print(\"Applying Mass Balance Constraints...\")\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=cfg.ALL_TARGET_COLS,\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# =========================================================================================\n# MAIN EXECUTION\n# =========================================================================================\ndef main():\n    print(\"=\" * 70)\n    print(\"CSIRO - Image2Biomass Prediction Pipeline\")\n    print(\"Blending Two Pipelines:\")\n    print(\"1. SigLIP + ML Models (Score: 0.72)\")\n    print(\"2. DinoV3 + Mamba (Score: 0.70)\")\n    print(\"Target Final Score: 0.73\")\n    print(\"=\" * 70)\n    \n    # Запускаем оба пайплайна\n    siglip_predictions = run_siglip_pipeline()\n    \n    if cfg.USE_DINOV3:\n        dinov3_predictions = run_dinov3_pipeline()\n    else:\n        # Если DinoV3 нет, используем только SigLIP\n        print(\"\\n\" + \"=\" * 70)\n        print(\"DinoV3 not available, using SigLIP only\")\n        print(\"=\" * 70)\n        \n        final_output_path = \"submission.csv\"\n        siglip_predictions.to_csv(final_output_path, index=False)\n        \n        print(f\"✓ Final submission generated: {final_output_path}\")\n        return siglip_predictions\n    \n    print(\"\\n\" + \"=\" * 70)\n    print(\"FINAL ENSEMBLING\")\n    print(\"=\" * 70)\n    \n    final_weights = {\n        'siglip': cfg.SIGLIP_WEIGHT,\n        'dinov3': cfg.DINOV3_WEIGHT\n    }\n    \n    final_submission = robust_ensemble(siglip_predictions, dinov3_predictions, final_weights)\n    \n    final_output_path = \"submission.csv\"\n    final_submission.to_csv(final_output_path, index=False)\n    \n    print(f\"\\n✓ Final submission generated: {final_output_path}\")\n    print(f\"\\nSubmission shape: {final_submission.shape}\")\n    \n    print(f\"\\nFirst 10 predictions:\")\n    print(final_submission.head(10))\n    \n    print(f\"\\nPrediction statistics:\")\n    print(f\"  Min: {final_submission['target'].min():.4f}\")\n    print(f\"  Max: {final_submission['target'].max():.4f}\")\n    print(f\"  Mean: {final_submission['target'].mean():.4f}\")\n    print(f\"  Std: {final_submission['target'].std():.4f}\")\n    \n    print(f\"\\n\" + \"=\" * 70)\n    print(\"VALIDATION CHECKS\")\n    print(\"=\" * 70)\n    \n    test_df = pd.read_csv(cfg.DATA_PATH / 'test.csv')\n    print(f\"✓ Expected rows: {len(test_df)}\")\n    print(f\"✓ Actual rows: {len(final_submission)}\")\n    print(f\"✓ Match: {len(final_submission) == len(test_df)}\")\n    print(f\"✓ No missing values: {not final_submission['target'].isna().any()}\")\n    print(f\"✓ All sample_ids unique: {final_submission['sample_id'].is_unique}\")\n    \n    return final_submission\n\nif __name__ == \"__main__\":\n    submission = main()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#vibecode eats brains","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}