{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"},{"sourceId":13636070,"sourceType":"datasetVersion","datasetId":8667402},{"sourceId":268942,"sourceType":"modelInstanceVersion","modelInstanceId":230141,"modelId":251887},{"sourceId":4534,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":3326,"modelId":986}],"dockerImageVersionId":31154,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"032d60b9","cell_type":"markdown","source":"# CSIRO Biomass Prediction - Two-Stream + DINOv2/SigLIP + CNN/Tabular con OOF-Stacking y TTA (T4-Optimizado)\n\nEste código se basa en el trabajo de Eastern Dundrey en ([https://www.kaggle.com/code/easterndundrey/sticking-close-to-csiro-paper-ensemble])\n\n**Lo que conserva de ([https://www.kaggle.com/code/easterndundrey/sticking-close-to-csiro-paper-ensemble]):**\n- Inference estilo *two‑stream* con **backbone ConvNeXt** (o DINO‑G si lo aportas) y **TTA (3 vistas: original, hflip, vflip)**.\n- Reconstrucción de 5 *targets* a partir de 3 (`Dry_Total_g`, `GDM_g`, `Dry_Green_g`).\n\n**Lo que se añade:**\n- **Stacking con meta‑learner (Ridge) por variable** usando **OOF** de múltiples ramas: Two‑Stream, DINOv2‑518, SigLIP‑384, CNN ligera y Tabular (HGB).\n- **Optimización para T4**: AMP moderna (`torch.amp`), `channels_last`, **TF32**, y *caching* de embeddings.\n- **Normalización 2:1 + Gray‑World** en todas las ramas para coherencia con el paper.\n- **Hard‑constraint** suave para `Dry_Total_g`.\n\n### Pipeline\n1) Cargar datos y definir orden de *targets*.\n2) Normalización 2:1 + Gray‑World (todas las ramas de imagen).\n3) Con los pesos del modelo two‑stream (`/kaggle/input/.../best_model_fold*.pth`), ejecutar **train‑OOF** + **test‑TTA** y obtener predicciones de 3 *targets*; reconstruir 5.\n4) Generar OOF + test de **DINOv2‑518** y **SigLIP‑384** (embeddings + Ridge).\n5) **CNN** ligera con augmentations suaves (OOF + test).\n6) **Tabular (HGB)** con OOF + test.\n7) **Meta‑stacking** (Ridge por *target*) con todas las OOF como *features* y predicción final en test.\n8) Proyección de **constraint** para `Dry_Total_g` y crear `submission.csv`.\n","metadata":{}},{"id":"84bf1d09","cell_type":"markdown","source":"## Configura GPU T4 (AMP/TF32/channels_last), semillas y utilidades.","metadata":{}},{"id":"1ac631c2","cell_type":"code","source":"\nimport os, glob, random, gc\nfrom pathlib import Path\nimport numpy as np, pandas as pd\nfrom PIL import Image, ImageOps\nfrom IPython.display import display\n\nfrom sklearn.model_selection import KFold\nfrom sklearn.ensemble import HistGradientBoostingRegressor\nfrom sklearn.linear_model import Ridge\nfrom sklearn.preprocessing import StandardScaler\n\nimport torch\nfrom torch import nn\nfrom torch.utils.data import Dataset, DataLoader\nimport torchvision.transforms as T\nimport timm\n\npd.set_option(\"display.max_columns\", 120)\npd.set_option(\"display.width\", 220)\n\nINPUT_DIR = Path(\"/kaggle/input/csiro-biomass\")\nCOMP_DIR = INPUT_DIR\n\ndef set_seed(seed=42):\n    random.seed(seed); np.random.seed(seed)\n    torch.manual_seed(seed); torch.cuda.manual_seed_all(seed)\nset_seed(42)\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Device:\", device)\n\ntorch.set_float32_matmul_precision(\"high\")\nif torch.cuda.is_available():\n    torch.backends.cuda.matmul.allow_tf32 = True\n    torch.backends.cudnn.allow_tf32 = True\n    torch.backends.cudnn.benchmark = True\n    CHANNELS_LAST = True\nelse:\n    CHANNELS_LAST = False\n\nAMP_ENABLED = torch.cuda.is_available()\nCACHE_DIR = Path(\"/kaggle/working\"); CACHE_DIR.mkdir(parents=True, exist_ok=True)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T04:15:54.018667Z","iopub.execute_input":"2025-11-10T04:15:54.019297Z","iopub.status.idle":"2025-11-10T04:16:06.353579Z","shell.execute_reply.started":"2025-11-10T04:15:54.019262Z","shell.execute_reply":"2025-11-10T04:16:06.352736Z"}},"outputs":[],"execution_count":null},{"id":"233e5a3f","cell_type":"markdown","source":"## Carga `train.csv` y `test.csv`; crea la matriz ancha `y_wide` y define pesos por *target*.","metadata":{}},{"id":"3cd4c724","cell_type":"code","source":"\ntrain = pd.read_csv(COMP_DIR / \"train.csv\")\ntest  = pd.read_csv(COMP_DIR / \"test.csv\")\nprint(\"train shape:\", train.shape, \"test shape:\", test.shape)\ndisplay(train.head(3)); display(test.head(3))\n\ntarget_names = sorted(train[\"target_name\"].unique())\ny_wide = (train.pivot(index=\"image_path\", columns=\"target_name\", values=\"target\").loc[:, target_names])\n\nTARGET_WEIGHTS = {\"Dry_Green_g\":0.1,\"Dry_Dead_g\":0.1,\"Dry_Clover_g\":0.1,\"GDM_g\":0.2,\"Dry_Total_g\":0.5}\nW_VEC = np.array([TARGET_WEIGHTS[t] for t in target_names], dtype=np.float32)\n\ntrain_imgs = y_wide.index.to_list()\ntest_imgs  = test[\"image_path\"].drop_duplicates().to_list()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T04:16:06.354741Z","iopub.execute_input":"2025-11-10T04:16:06.354972Z","iopub.status.idle":"2025-11-10T04:16:06.422758Z","shell.execute_reply.started":"2025-11-10T04:16:06.354953Z","shell.execute_reply":"2025-11-10T04:16:06.422247Z"}},"outputs":[],"execution_count":null},{"id":"ea75d863","cell_type":"markdown","source":"## Define normalización 2:1 + Gray‑World (paper‑friendly).","metadata":{}},{"id":"7d46f1ec","cell_type":"code","source":"\ndef normalize_2to1_and_white_balance(img: Image.Image) -> Image.Image:\n    w, h = img.size\n    if h > w:\n        img = img.rotate(90, expand=True); w, h = img.size\n    target_ratio = 2.0; cur_ratio = w / h if h else 2.0\n    if abs(cur_ratio - target_ratio) > 1e-3:\n        if cur_ratio < target_ratio:\n            new_w = int(round(h * target_ratio)); pad = (new_w - w) // 2\n            img = ImageOps.expand(img, border=(pad, 0, new_w - w - pad, 0), fill=(0, 0, 0))\n        else:\n            new_h = int(round(w / target_ratio)); pad = (new_h - h) // 2\n            img = ImageOps.expand(img, border=(0, pad, 0, new_h - h - pad), fill=(0, 0, 0))\n    arr = np.asarray(img).astype(np.float32)\n    if arr.ndim == 3 and arr.shape[2] == 3:\n        means = arr.reshape(-1, 3).mean(0) + 1e-6\n        gm = float(np.mean(means)); scale = gm / means\n        arr = np.clip(arr * scale, 0, 255).astype(np.uint8)\n        img = Image.fromarray(arr)\n    return img\n\n_pre_norm = T.Lambda(normalize_2to1_and_white_balance)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T04:16:06.423399Z","iopub.execute_input":"2025-11-10T04:16:06.423624Z","iopub.status.idle":"2025-11-10T04:16:06.430177Z","shell.execute_reply.started":"2025-11-10T04:16:06.423598Z","shell.execute_reply":"2025-11-10T04:16:06.429470Z"}},"outputs":[],"execution_count":null},{"id":"b1745ce7","cell_type":"markdown","source":"## Implementa inferencia propuesta en ([https://www.kaggle.com/code/easterndundrey/sticking-close-to-csiro-paper-ensemble]) (two‑stream + TTA 3 vistas). Si hay pesos en `TS_MODEL_DIR`, corre OOF en train y predicción en test.","metadata":{}},{"id":"3bf46071","cell_type":"code","source":"\n# Config: ajusta a tu carpeta de pesos (5 folds). Ejemplo: /kaggle/input/csiro-exp3/convnext_exp3\nTS_MODEL_DIR = os.environ.get(\"TS_MODEL_DIR\",\"/kaggle/input/csiro-exp3/convnext_exp3\")\nTS_BACKBONE  = os.environ.get(\"TS_BACKBONE\",\"convnext_small\")  # o 'convnext_tiny', etc.\nTS_IMG_SIZE  = int(os.environ.get(\"TS_IMG_SIZE\",\"1000\"))\nTS_BATCH     = int(os.environ.get(\"TS_BATCH\",\"1\"))\nTS_WORKERS   = int(os.environ.get(\"TS_WORKERS\",\"1\"))\nTS_NFOLDS    = int(os.environ.get(\"TS_NFOLDS\",\"5\"))\n\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nimport cv2\n\nclass TTATransformFactory:\n    def __init__(self, img_size:int):\n        self.img_size = img_size\n        self.base = [A.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225]), ToTensorV2()]\n    def get_tta(self):\n        return [\n            A.Compose([A.Resize(self.img_size, self.img_size), *self.base]),\n            A.Compose([A.HorizontalFlip(p=1.0), A.Resize(self.img_size, self.img_size), *self.base]),\n            A.Compose([A.VerticalFlip(p=1.0), A.Resize(self.img_size, self.img_size), *self.base])\n        ]\n\nclass TestTwoStreamDS(Dataset):\n    def __init__(self, df, tf, img_dir):\n        self.df=df; self.tf=tf; self.dir=img_dir; self.paths=df[\"image_path\"].values\n    def __len__(self): return len(self.df)\n    def __getitem__(self,i):\n        img_path = self.dir / Path(self.paths[i]).name\n        image = cv2.imread(str(img_path))\n        if image is None:\n            image = np.zeros((TS_IMG_SIZE, TS_IMG_SIZE*2, 3), dtype=np.uint8)\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        h,w = image.shape[:2]; mid=w//2\n        left = image[:, :mid]; right = image[:, mid:]\n        l = self.tf(image=left)[\"image\"]; r = self.tf(image=right)[\"image\"]\n        return l, r\n\nclass TwoStreamModel(nn.Module):\n    def __init__(self, name):\n        super().__init__()\n        self.backbone = timm.create_model(name, pretrained=False, num_classes=0, global_pool='avg')\n        nf = self.backbone.num_features; nc = nf*2\n        def head():\n            return nn.Sequential(nn.Linear(nc, nc//2), nn.ReLU(), nn.Dropout(0.3), nn.Linear(nc//2, 1))\n        self.head_total = head(); self.head_gdm = head(); self.head_green = head()\n    def forward(self, xl, xr):\n        fl = self.backbone(xl); fr = self.backbone(xr); z = torch.cat([fl,fr], dim=1)\n        return self.head_total(z), self.head_gdm(z), self.head_green(z)\n\ndef _load_fold_models():\n    models=[]\n    ok = TS_MODEL_DIR and os.path.exists(TS_MODEL_DIR)\n    if not ok:\n        print(\"Two‑stream: carpeta de pesos no encontrada; se omitirá esta rama.\")\n        return models\n    for fold in range(TS_NFOLDS):\n        path = Path(TS_MODEL_DIR)/f\"best_model_fold{fold}.pth\"\n        if not path.exists():\n            print(\"Falta:\", path); continue\n        m = TwoStreamModel(TS_BACKBONE).to(device).eval()\n        sd = torch.load(path, map_location=\"cpu\")\n        if any(k.startswith(\"module.\") for k in sd): sd = {k.replace(\"module.\",\"\"):v for k,v in sd.items()}\n        m.load_state_dict(sd, strict=False); m.to(device).eval()\n        models.append(m)\n    if not models: print(\"No se cargó ningún fold.\")\n    return models\n\ndef _predict_3_from_models(models, df_unique, img_dir):\n    if not models: return None\n    tta = TTATransformFactory(TS_IMG_SIZE).get_tta()\n    preds_views=[]\n    for tf in tta:\n        ds = TestTwoStreamDS(df_unique, tf, img_dir)\n        dl = DataLoader(ds, batch_size=TS_BATCH, shuffle=False, num_workers=TS_WORKERS, pin_memory=True)\n        vt, vgdm, vgr = [], [], []\n        with torch.no_grad(), torch.amp.autocast(\"cuda\", enabled=AMP_ENABLED):\n            for xl, xr in dl:\n                xl = xl.to(device); xr = xr.to(device)\n                fold_t, fold_gdm, fold_gr = [], [], []\n                for m in models:\n                    pt, pgdm, pgr = m(xl, xr)\n                    fold_t.append(pt.cpu()); fold_gdm.append(pgdm.cpu()); fold_gr.append(pgr.cpu())\n                vt.append(torch.mean(torch.stack(fold_t),0).numpy())\n                vgdm.append(torch.mean(torch.stack(fold_gdm),0).numpy())\n                vgr.append(torch.mean(torch.stack(fold_gr),0).numpy())\n        preds_views.append({\n            \"total\": np.concatenate(vt).flatten(),\n            \"gdm\":   np.concatenate(vgdm).flatten(),\n            \"green\": np.concatenate(vgr).flatten(),\n        })\n    pred = {\n        \"total\": np.mean([p[\"total\"] for p in preds_views], axis=0),\n        \"gdm\":   np.mean([p[\"gdm\"]   for p in preds_views], axis=0),\n        \"green\": np.mean([p[\"green\"] for p in preds_views], axis=0),\n    }\n    return pred\n\ndef reconstruct_5(pred3, df_unique):\n    if pred3 is None: return None\n    total = pred3[\"total\"]; gdm = pred3[\"gdm\"]; green = pred3[\"green\"]\n    clover = np.maximum(0, gdm - green)\n    dead   = np.maximum(0, total - gdm)\n    wide = pd.DataFrame({\n        \"image_path\": df_unique[\"image_path\"],\n        \"Dry_Green_g\": green,\n        \"Dry_Dead_g\": dead,\n        \"Dry_Clover_g\": clover,\n        \"GDM_g\": gdm,\n        \"Dry_Total_g\": total\n    })\n    return wide\n\n# Build OOF on train (if we can point two‑stream to /kaggle/input/.../train images)\ntrain_unique = train.drop_duplicates(subset=[\"image_path\"]).reset_index(drop=True)\ntest_unique  = test.drop_duplicates(subset=[\"image_path\"]).reset_index(drop=True)\nTS_models = _load_fold_models()\nTS_train_wide = reconstruct_5(_predict_3_from_models(TS_models, train_unique, INPUT_DIR/\"train\"), train_unique) if TS_models else None\nTS_test_wide  = reconstruct_5(_predict_3_from_models(TS_models, test_unique,  INPUT_DIR/\"test\"),  test_unique)  if TS_models else None\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T04:16:06.431484Z","iopub.execute_input":"2025-11-10T04:16:06.431672Z","iopub.status.idle":"2025-11-10T04:28:06.739139Z","shell.execute_reply.started":"2025-11-10T04:16:06.431657Z","shell.execute_reply":"2025-11-10T04:28:06.738317Z"}},"outputs":[],"execution_count":null},{"id":"83263a61","cell_type":"markdown","source":"## Embeddings DINOv2‑518 y SigLIP‑384 con AMP + Ridge; cache en `/kaggle/working`.","metadata":{}},{"id":"099dd625","cell_type":"code","source":"\ndef load_state_dict_flex(model, wpath, strict=False, key_candidates=(\"state_dict\",\"model\",\"weights\")):\n    if wpath.endswith(\".safetensors\"):\n        from safetensors.torch import load_file as safe_load_file\n        sd = safe_load_file(wpath)\n    else:\n        sd = torch.load(wpath, map_location=\"cpu\", weights_only=False)\n    if isinstance(sd, dict):\n        for k in key_candidates:\n            if k in sd and isinstance(sd[k], dict):\n                sd = sd[k]; break\n        if all(k.startswith(\"module.\") for k in sd.keys()):\n            sd = {k[len('module.'):]: v for k,v in sd.items()}\n        model.load_state_dict(sd, strict=False)\n\n# DINOv2‑518\nDINO_DIR = \"/kaggle/input/dinov2/pytorch/base/1\"\nIMG_SIZE_DINO=518\nmodel_dino = None\nif os.path.exists(DINO_DIR):\n    try:\n        model_dino = timm.create_model(\"vit_base_patch14_dinov2\", pretrained=False, num_classes=0, img_size=IMG_SIZE_DINO).to(device).eval()\n        if torch.cuda.is_available(): model_dino.half()\n        dino_w = sum([glob.glob(os.path.join(DINO_DIR, \"**\", ext), recursive=True) for ext in (\"*.safetensors\",\"*.pt\",\"*.bin\")],[])\n        if dino_w: load_state_dict_flex(model_dino, dino_w[0], strict=False)\n    except Exception as e:\n        print(\"DINO init failed:\", e); model_dino=None\ndino_tf = T.Compose([_pre_norm, T.Resize((IMG_SIZE_DINO, IMG_SIZE_DINO)), T.ToTensor(),\n                     T.Normalize((0.485,0.456,0.406),(0.229,0.224,0.225))])\n\n# SigLIP‑384\nSIGLIP_DIR_CAND = [\"/kaggle/input/google-siglip-so400m-patch14-384/transformers/default/1\",\"/kaggle/input/siglip-so400m-384\",\"/kaggle/input/siglip-vitb16-384\"]\nSIGLIP_DIR = next((p for p in SIGLIP_DIR_CAND if os.path.exists(p)), None)\nIMG_SIZE_SIG=384\nsiglip_model=None\nif SIGLIP_DIR:\n    for arch in [\"vit_so400m_patch14_siglip_384\",\"vit_base_patch14_siglip_384\",\"vit_large_patch14_384\"]:\n        try:\n            siglip_model = timm.create_model(arch, pretrained=False, num_classes=0); arch_siglip=arch; break\n        except Exception: pass\n    if siglip_model is not None:\n        sig_w = sum([glob.glob(os.path.join(SIGLIP_DIR, \"**\", ext), recursive=True) for ext in (\"*.safetensors\",\"*.pt\",\"*.bin\")],[])\n        if sig_w: load_state_dict_flex(siglip_model, sig_w[0], strict=False)\n        siglip_model = siglip_model.to(device).eval()\n        if torch.cuda.is_available(): siglip_model.half()\n\nsiglip_tf = T.Compose([_pre_norm, T.Resize((IMG_SIZE_SIG, IMG_SIZE_SIG)), T.ToTensor(),\n                       T.Normalize((0.485,0.456,0.406),(0.229,0.224,0.225))])\n\nclass ImgOnlyDS(Dataset):\n    def __init__(self, rel_paths, tf): self.rel=rel_paths; self.tf=tf\n    def __len__(self): return len(self.rel)\n    def __getitem__(self,i):\n        p = COMP_DIR / self.rel[i]\n        try: im = Image.open(p).convert(\"RGB\")\n        except: im = Image.new(\"RGB\",(512,256),(0,0,0))\n        return self.tf(im)\n\ndef embed_batch(model, tf, paths, bs=8):\n    if model is None: return None\n    dl = DataLoader(ImgOnlyDS(paths, tf), batch_size=bs, shuffle=False, num_workers=2, pin_memory=True)\n    out=[]\n    with torch.no_grad(), torch.amp.autocast(\"cuda\", enabled=AMP_ENABLED):\n        for xb in dl:\n            if CHANNELS_LAST: xb = xb.to(device).to(memory_format=torch.channels_last)\n            else: xb = xb.to(device)\n            z = model(xb); z = z[0] if isinstance(z,(list,tuple)) else z\n            out.append(z.detach().cpu().float().numpy())\n    return np.concatenate(out,0)\n\ndef oof_ridge(Etr, Y, target_names, n_splits=5):\n    kf = KFold(n_splits=n_splits, shuffle=True, random_state=42)\n    oof = np.zeros_like(Y)\n    for tr, va in kf.split(Etr):\n        sc = StandardScaler(); Xtr=sc.fit_transform(Etr[tr]); Xva=sc.transform(Etr[va])\n        pv = np.zeros_like(Y[va])\n        for j,_ in enumerate(target_names):\n            rd = Ridge(alpha=1.0).fit(Xtr, Y[tr, j])\n            pv[:,j] = rd.predict(Xva)\n        oof[va]=np.clip(pv,0,None)\n    sc=StandardScaler(); X=sc.fit_transform(Etr)\n    return oof, sc\n\nY = y_wide[target_names].values\n# DINO\nEtr_dino, Ets_dino = None, None\nif model_dino is not None:\n    tr_cache = CACHE_DIR/\"Etr_dino.npy\"; ts_cache = CACHE_DIR/\"Ets_dino.npy\"\n    if tr_cache.exists(): Etr_dino = np.load(tr_cache)\n    else: Etr_dino = embed_batch(model_dino, dino_tf, train_imgs, bs=8); np.save(tr_cache, Etr_dino)\n    if ts_cache.exists(): Ets_dino = np.load(ts_cache)\n    else: Ets_dino = embed_batch(model_dino, dino_tf, test_imgs,  bs=8); np.save(ts_cache, Ets_dino)\n    dino_oof, dino_sc = oof_ridge(Etr_dino, Y, target_names, n_splits=5)\n\n# SigLIP\nEtr_sig, Ets_sig = None, None\nif siglip_model is not None:\n    tr_cache = CACHE_DIR/\"Etr_sig.npy\"; ts_cache = CACHE_DIR/\"Ets_sig.npy\"\n    if tr_cache.exists(): Etr_sig = np.load(tr_cache)\n    else: Etr_sig = embed_batch(siglip_model, siglip_tf, train_imgs, bs=16); np.save(tr_cache, Etr_sig)\n    if ts_cache.exists(): Ets_sig = np.load(ts_cache)\n    else: Ets_sig = embed_batch(siglip_model, siglip_tf, test_imgs,  bs=16); np.save(ts_cache, Ets_sig)\n    sig_oof, sig_sc = oof_ridge(Etr_sig, Y, target_names, n_splits=5)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T04:28:06.740336Z","iopub.execute_input":"2025-11-10T04:28:06.740641Z","iopub.status.idle":"2025-11-10T04:29:18.484277Z","shell.execute_reply.started":"2025-11-10T04:28:06.740607Z","shell.execute_reply":"2025-11-10T04:29:18.483398Z"}},"outputs":[],"execution_count":null},{"id":"ee70cec6","cell_type":"markdown","source":"## CNN ligera con augmentations suaves, OOF + test.","metadata":{}},{"id":"7b6468cf","cell_type":"code","source":"\ndef get_transforms():\n    norm = T.Normalize(mean=(0.485,0.456,0.406), std=(0.229,0.224,0.225))\n    aug = T.RandomChoice([T.ColorJitter(0.1,0.1,0.1,0.05), T.RandomAutocontrast(p=0.5)], p=[0.7,0.3])\n    train_tf = T.Compose([_pre_norm, T.Resize((224,224)), T.RandomHorizontalFlip(0.5),\n                          T.RandomApply([T.RandomRotation(5)], p=0.3), aug, T.ToTensor(), norm])\n    test_tf  = T.Compose([_pre_norm, T.Resize((224,224)), T.ToTensor(), norm])\n    return train_tf, test_tf\n\nclass TrainDS(Dataset):\n    def __init__(self, image_paths, targets_log, transform=None, root_dir=COMP_DIR):\n        self.image_paths = image_paths; self.targets_log = targets_log; self.transform = transform; self.root=root_dir\n    def __len__(self): return len(self.image_paths)\n    def __getitem__(self, i):\n        pth = self.root / self.image_paths[i]\n        try: im = Image.open(pth).convert(\"RGB\")\n        except Exception: im = Image.new(\"RGB\",(224,224),(0,0,0))\n        if self.transform: im = self.transform(im)\n        y = torch.tensor(self.targets_log[i], dtype=torch.float32); return im, y\n\nclass TestDS(Dataset):\n    def __init__(self, image_paths, transform=None, root_dir=COMP_DIR):\n        self.image_paths = image_paths; self.transform = transform; self.root=root_dir\n    def __len__(self): return len(self.image_paths)\n    def __getitem__(self, i):\n        pth = self.root / self.image_paths[i]\n        try: im = Image.open(pth).convert(\"RGB\")\n        except Exception: im = Image.new(\"RGB\",(224,224),(0,0,0))\n        if self.transform: im = self.transform(im)\n        return im\n\nclass SmallCNN(nn.Module):\n    def __init__(self, n_outputs):\n        super().__init__()\n        self.features = nn.Sequential(\n            nn.Conv2d(3,16,3,padding=1), nn.ReLU(), nn.MaxPool2d(2),\n            nn.Conv2d(16,32,3,padding=1), nn.ReLU(), nn.MaxPool2d(2),\n            nn.Conv2d(32,64,3,padding=1), nn.ReLU(), nn.MaxPool2d(2),\n            nn.Conv2d(64,128,3,padding=1), nn.ReLU(), nn.AdaptiveAvgPool2d((1,1))\n        )\n        self.head = nn.Sequential(nn.Flatten(), nn.Linear(128,128), nn.ReLU(), nn.Linear(128,n_outputs))\n    def forward(self,x): return self.head(self.features(x))\n\ndef cnn_oof_and_test(y_wide_df, test_df, target_names, epochs=6, batch_size=16, lr=1e-3, wd=1e-5, n_splits=5):\n    imgs = y_wide_df.index.tolist()\n    Y = y_wide_df[target_names].values\n    Ylog = np.log1p(Y)\n\n    tr_tf, te_tf = get_transforms()  # <-- te_tf es el transform de test\n\n    kf = KFold(n_splits=n_splits, shuffle=True, random_state=42)\n    oof = np.zeros_like(Y)\n\n    for fold, (tr_idx, va_idx) in enumerate(kf.split(imgs)):\n        tr_paths = [imgs[i] for i in tr_idx]\n        va_paths = [imgs[i] for i in va_idx]\n\n        dl_tr = DataLoader(\n            TrainDS(tr_paths, Ylog[tr_idx], tr_tf),\n            batch_size=batch_size, shuffle=True, num_workers=2,\n            pin_memory=True, persistent_workers=True\n        )\n        dl_va = DataLoader(\n            TrainDS(va_paths, Ylog[va_idx], te_tf),\n            batch_size=1, shuffle=False, num_workers=2,\n            pin_memory=True, persistent_workers=True\n        )\n\n        model = SmallCNN(n_outputs=len(target_names)).to(device).train()\n        if CHANNELS_LAST:\n            model = model.to(memory_format=torch.channels_last)\n        scaler = torch.amp.GradScaler(\"cuda\", enabled=AMP_ENABLED)\n        opt = torch.optim.Adam(model.parameters(), lr=lr, weight_decay=wd)\n\n        w = torch.tensor(W_VEC, device=device).view(1, -1)\n        iT = target_names.index(\"Dry_Total_g\")\n        iG = target_names.index(\"Dry_Green_g\")\n        iD = target_names.index(\"Dry_Dead_g\")\n        iC = target_names.index(\"Dry_Clover_g\")\n        lambda_cons = 0.3\n\n        for ep in range(epochs):\n            run = 0.0\n            for xb, yb in dl_tr:\n                xb = xb.to(device)\n                if CHANNELS_LAST:\n                    xb = xb.to(memory_format=torch.channels_last)\n                yb = yb.to(device)\n                opt.zero_grad(set_to_none=True)\n                with torch.amp.autocast(\"cuda\", enabled=AMP_ENABLED):\n                    pred = model(xb)\n                    base = ((pred - yb)**2 * w).mean()\n                    plin = torch.expm1(pred)\n                    cons = ((plin[:, iT] - (plin[:, iG] + plin[:, iD] + plin[:, iC]))**2).mean()\n                    loss = base + lambda_cons * cons\n                scaler.scale(loss).backward()\n                scaler.step(opt)\n                scaler.update()\n                run += loss.item() * xb.size(0)\n            print(f\"[CNN][fold {fold}] ep {ep+1}/{epochs} loss: {run/len(dl_tr.dataset):.4f}\")\n\n        # Validación\n        model.eval()\n        preds_va = []\n        with torch.no_grad(), torch.amp.autocast(\"cuda\", enabled=AMP_ENABLED):\n            for xb, yb in dl_va:\n                xb = xb.to(device)\n                if CHANNELS_LAST:\n                    xb = xb.to(memory_format=torch.channels_last)\n                pv = model(xb).cpu().float().numpy()\n                preds_va.append(np.expm1(pv))\n        oof[va_idx] = np.clip(np.concatenate(preds_va, 0), 0, None)\n\n    # Entrenamiento full para inferencia en test\n    dl_full = DataLoader(\n        TrainDS(imgs, Ylog, tr_tf),\n        batch_size=batch_size, shuffle=True, num_workers=2,\n        pin_memory=True, persistent_workers=True\n    )\n    model = SmallCNN(n_outputs=len(target_names)).to(device).train()\n    if CHANNELS_LAST:\n        model = model.to(memory_format=torch.channels_last)\n    scaler = torch.amp.GradScaler(\"cuda\", enabled=AMP_ENABLED)\n    opt = torch.optim.Adam(model.parameters(), lr=lr, weight_decay=wd)\n    w = torch.tensor(W_VEC, device=device).view(1, -1)\n    iT = target_names.index(\"Dry_Total_g\")\n    iG = target_names.index(\"Dry_Green_g\")\n    iD = target_names.index(\"Dry_Dead_g\")\n    iC = target_names.index(\"Dry_Clover_g\")\n    lambda_cons = 0.3\n\n    for ep in range(epochs):\n        run = 0.0\n        for xb, yb in dl_full:\n            xb = xb.to(device)\n            if CHANNELS_LAST:\n                xb = xb.to(memory_format=torch.channels_last)\n            yb = yb.to(device)\n            opt.zero_grad(set_to_none=True)\n            with torch.amp.autocast(\"cuda\", enabled=AMP_ENABLED):\n                pred = model(xb)\n                base = ((pred - yb)**2 * w).mean()\n                plin = torch.expm1(pred)\n                cons = ((plin[:, iT] - (plin[:, iG] + plin[:, iD] + plin[:, iC]))**2).mean()\n                loss = base + lambda_cons * cons\n            scaler.scale(loss).backward()\n            scaler.step(opt)\n            scaler.update()\n            run += loss.item() * xb.size(0)\n        print(f\"[CNN][full] ep {ep+1}/{epochs} loss: {run/len(dl_full.dataset):.4f}\")\n\n    # INFERENCIA EN TEST \n    dl_test = DataLoader(\n        TestDS(test_imgs, te_tf),  \n        batch_size=1, shuffle=False, num_workers=2,\n        pin_memory=True, persistent_workers=True\n    )\n    model.eval()\n    preds_ts = []\n    with torch.no_grad(), torch.amp.autocast(\"cuda\", enabled=AMP_ENABLED):\n        for xb in dl_test:\n            xb = xb.to(device)\n            if CHANNELS_LAST:\n                xb = xb.to(memory_format=torch.channels_last)\n            pv = model(xb).cpu().float().numpy()\n            preds_ts.append(np.expm1(pv))\n    preds_ts = np.clip(np.concatenate(preds_ts, 0), 0, None)\n    return oof, preds_ts\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T04:51:11.946191Z","iopub.execute_input":"2025-11-10T04:51:11.946464Z","iopub.status.idle":"2025-11-10T04:51:11.972389Z","shell.execute_reply.started":"2025-11-10T04:51:11.946445Z","shell.execute_reply":"2025-11-10T04:51:11.971623Z"}},"outputs":[],"execution_count":null},{"id":"d5a27e1e","cell_type":"markdown","source":"## Tabular (HGB) con OOF + test.","metadata":{}},{"id":"60dbc79f","cell_type":"code","source":"\ndef extract_image_features(rel_path: str) -> dict:\n    img_path = COMP_DIR / rel_path\n    feats = {\"mean_R\":0.0,\"mean_G\":0.0,\"mean_B\":0.0,\"std_R\":0.0,\"std_G\":0.0,\"std_B\":0.0,\n             \"excess_green\":0.0,\"mean_gray\":0.0,\"mean_g_fraction\":0.0,\"prop_green_pixels\":0.0,\"p90_excess_green\":0.0}\n    try:\n        with Image.open(img_path) as img:\n            img = img.convert(\"RGB\")\n            arr = np.asarray(img).astype(np.float32)\n        R,G,B = arr[:,:,0], arr[:,:,1], arr[:,:,2]\n        feats[\"mean_R\"],feats[\"mean_G\"],feats[\"mean_B\"] = R.mean(),G.mean(),B.mean()\n        feats[\"std_R\"],feats[\"std_G\"],feats[\"std_B\"]    = R.std(),G.std(),B.std()\n        eg = 2*G - R - B; feats[\"excess_green\"] = eg.mean()\n        gray = 0.299*R + 0.587*G + 0.114*B; feats[\"mean_gray\"] = gray.mean()\n        denom = R+G+B+1e-6; feats[\"mean_g_fraction\"] = (G/denom).mean()\n        feats[\"prop_green_pixels\"] = ((G>R)&(G>B)).mean()\n        feats[\"p90_excess_green\"] = float(np.percentile(eg,90))\n    except Exception:\n        pass\n    return feats\n\ndef safe_fill(df: pd.DataFrame) -> pd.DataFrame:\n    df = df.replace([np.inf,-np.inf], np.nan)\n    df = df.fillna(df.mean()).fillna(0)\n    return df\n\ndef tabular_oof_and_test(y_wide_df, test_df, target_names, n_splits=5):\n    train_images = y_wide_df.index.tolist()\n    Xtr = pd.DataFrame([dict(image_path=r, **extract_image_features(r)) for r in train_images]).set_index(\"image_path\")\n    Xtr = safe_fill(Xtr); X = Xtr.values\n    Y = y_wide_df[target_names].values\n    kf = KFold(n_splits=n_splits, shuffle=True, random_state=42)\n    oof = np.zeros_like(Y)\n    for tr_idx, va_idx in kf.split(X):\n        X_tr, X_va = X[tr_idx], X[va_idx]\n        Y_tr, Y_va = Y[tr_idx], Y[va_idx]\n        pred_va = np.zeros_like(Y_va)\n        for j,t in enumerate(target_names):\n            w = np.full(len(X_tr), TARGET_WEIGHTS[t], dtype=np.float32)\n            hgb = HistGradientBoostingRegressor(max_depth=4, learning_rate=0.1, max_iter=400, random_state=42)\n            hgb.fit(X_tr, np.log1p(Y_tr[:,j]), sample_weight=w)\n            pred_va[:,j] = np.expm1(hgb.predict(X_va))\n        oof[va_idx] = np.clip(pred_va, 0, None)\n\n    models=[] \n    for j,t in enumerate(target_names):\n        w = np.full(len(X), TARGET_WEIGHTS[t], dtype=np.float32)\n        hgb = HistGradientBoostingRegressor(max_depth=4, learning_rate=0.1, max_iter=400, random_state=42)\n        hgb.fit(X, np.log1p(Y[:,j]), sample_weight=w)\n        models.append(hgb)\n\n    test_images = test_df[\"image_path\"].unique().tolist()\n    Xts = pd.DataFrame([dict(image_path=r, **extract_image_features(r)) for r in test_images]).set_index(\"image_path\")\n    Xts = safe_fill(Xts); Xte = Xts.values\n    preds_test = np.column_stack([np.expm1(m.predict(Xte)) for m in models])\n    preds_test = np.clip(preds_test, 0, None)\n    return oof, preds_test\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T04:51:19.454248Z","iopub.execute_input":"2025-11-10T04:51:19.454825Z","iopub.status.idle":"2025-11-10T04:51:19.467024Z","shell.execute_reply.started":"2025-11-10T04:51:19.454800Z","shell.execute_reply":"2025-11-10T04:51:19.466166Z"}},"outputs":[],"execution_count":null},{"id":"4451b111","cell_type":"markdown","source":"## Ejecuta todas las ramas (cuando estén disponibles) y realiza **stacking (Ridge por target)**.","metadata":{}},{"id":"8de86ec5","cell_type":"code","source":"\nprint(\">> Ejecutando ramas...\")\n# Two‑stream wide preds (OOF a partir de train y TEST). Si no hay modelos, queda en None.\nTS_train_wide = TS_train_wide  # from earlier cell\nTS_test_wide  = TS_test_wide\n\n# DINO/SigLIP OOF + test (ya calculado embeddings si estaban disponibles)\ndef ridge_full_predict(Etr, Ets, Y):\n    if Etr is None or Ets is None: return None, None\n    sc = StandardScaler(); Xtr = sc.fit_transform(Etr); Xts = sc.transform(Ets)\n    preds = np.column_stack([Ridge(alpha=1.0).fit(Xtr, Y[:,j]).predict(Xts) for j in range(Y.shape[1])])\n    # OOF ya se calculó arriba (dino_oof / sig_oof)\n    return preds, sc\n\ndino_test_preds, _ = ridge_full_predict(globals().get(\"Etr_dino\"), globals().get(\"Ets_dino\"), Y)\nsig_test_preds,  _ = ridge_full_predict(globals().get(\"Etr_sig\"),  globals().get(\"Ets_sig\"),  Y)\n\n# CNN\ncnn_oof, cnn_test = cnn_oof_and_test(y_wide, test, target_names, epochs=6, batch_size=16, lr=1e-3, wd=1e-5, n_splits=5)\n\n# Tabular\ntab_oof, tab_test = tabular_oof_and_test(y_wide, test, target_names, n_splits=5)\n\n# Preparar matrices OOF por target (apilan columnas de cada rama disponible)\ndef collect_oof(j):\n    cols=[]\n    if TS_train_wide is not None: cols.append(TS_train_wide[target_names[j]].values.reshape(-1,1))\n    if globals().get(\"dino_oof\") is not None: cols.append(dino_oof[:,[j]])\n    if globals().get(\"sig_oof\")  is not None: cols.append(sig_oof[:,[j]])\n    cols.append(cnn_oof[:,[j]])\n    cols.append(tab_oof[:,[j]])\n    return np.concatenate(cols,1)\n\ndef collect_test(j):\n    cols=[]\n    if TS_test_wide is not None: cols.append(TS_test_wide[target_names[j]].values.reshape(-1,1))\n    if dino_test_preds is not None: cols.append(dino_test_preds[:,[j]])\n    if sig_test_preds  is not None: cols.append(sig_test_preds[:,[j]])\n    cols.append(cnn_test[:,[j]])\n    cols.append(tab_test[:,[j]])\n    return np.concatenate(cols,1)\n\npreds_stack = np.zeros((len(test_imgs), len(target_names)), dtype=np.float32)\nfor j,t in enumerate(target_names):\n    X_tr = collect_oof(j); y_tr = y_wide[target_names].values[:,j]\n    X_te = collect_test(j)\n    mdl = Ridge(alpha=1e-2).fit(X_tr, y_tr)\n    preds_stack[:,j] = mdl.predict(X_te)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T04:51:33.039754Z","iopub.execute_input":"2025-11-10T04:51:33.040038Z","iopub.status.idle":"2025-11-10T05:03:39.249858Z","shell.execute_reply.started":"2025-11-10T04:51:33.040016Z","shell.execute_reply":"2025-11-10T05:03:39.248636Z"}},"outputs":[],"execution_count":null},{"id":"f9d76bc3","cell_type":"markdown","source":"## Proyección de *constraint* suave y creación de `submission.csv`.","metadata":{}},{"id":"da5328c4","cell_type":"code","source":"\ndef enforce_total_constraint(df_preds, target_names, alpha=0.5):\n    arr = df_preds.values.copy()\n    idx_T = target_names.index(\"Dry_Total_g\")\n    idx_G = target_names.index(\"Dry_Green_g\")\n    idx_D = target_names.index(\"Dry_Dead_g\")\n    idx_C = target_names.index(\"Dry_Clover_g\")\n    parts = arr[:, idx_G] + arr[:, idx_D] + arr[:, idx_C]\n    arr[:, idx_T] = alpha*arr[:, idx_T] + (1-alpha)*parts\n    arr = np.clip(arr, 0, None)\n    return pd.DataFrame(arr, index=df_preds.index, columns=df_preds.columns)\n\npreds_df = pd.DataFrame(preds_stack, index=test_imgs, columns=target_names)\npreds_df = enforce_total_constraint(preds_df, target_names, alpha=0.5)\n\npreds_long = (preds_df.reset_index()\n              .melt(id_vars=\"index\", var_name=\"target_name\", value_name=\"target\")\n              .rename(columns={\"index\":\"image_path\"}))\n\nsubmission = (test[[\"sample_id\",\"image_path\",\"target_name\"]]\n              .merge(preds_long, on=[\"image_path\",\"target_name\"], how=\"left\")\n              [[\"sample_id\",\"target\"]])\n\nassert submission.shape[0] == test.shape[0], \"Submission size mismatch\"\nassert np.isfinite(submission[\"target\"]).all(), \"NaN/Inf en submission\"\nsubmission[\"target\"] = submission[\"target\"].clip(lower=0)\nsubmission.to_csv(\"submission.csv\", index=False)\nprint(\"submission.csv escrito. Filas:\", len(submission))\ndisplay(submission.head())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T05:06:58.797217Z","iopub.execute_input":"2025-11-10T05:06:58.798198Z","iopub.status.idle":"2025-11-10T05:06:58.831007Z","shell.execute_reply.started":"2025-11-10T05:06:58.798164Z","shell.execute_reply":"2025-11-10T05:06:58.830373Z"}},"outputs":[],"execution_count":null}]}