{"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":"none","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"},{"sourceId":4534,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":3326,"modelId":986},{"sourceId":268942,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":230141,"modelId":251887}],"dockerImageVersionId":31192,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"031203cd","cell_type":"markdown","source":"# CSIRO Biomass — HGB + Small CNN + DINOv2 Base (518px) + SigLIP (384px) Ensemble\n\n\n\n\n**Pipeline:**\n- Tabular **HGB** sobre features sencillas de imagen (RGB/HSV/LAB + textura).\n- **Small CNN** (targets en log1p) + *consistency loss* en escala original para `Dry_Total_g`.\n- **DINOv2 Base** congelado (518px) → embeddings → **Ridge** por target.\n- **SigLIP 384** congelado → embeddings → **Ridge** por target.\n- **Blending 4‑vías** (Tabular, CNN, DINO, SigLIP) + consistencia suave del total.\n- `submission.csv` sin depender de `sample_submission.csv`.\n\n","metadata":{}},{"id":"ae72a953","cell_type":"code","source":"\nimport os, glob, random\nfrom pathlib import Path\nimport numpy as np, pandas as pd\nfrom PIL import Image\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\n\nimport timm\n\npd.set_option(\"display.max_columns\", 120)\npd.set_option(\"display.width\", 220)\n\nINPUT_DIR = Path(\"/kaggle/input/csiro-biomass\")\nsubdirs = [p for p in INPUT_DIR.iterdir() if p.is_dir()]\nCOMP_DIR = subdirs[0] if len(subdirs) == 1 else INPUT_DIR\nprint(\"Usando carpeta de datos:\", COMP_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\")\ndevice\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T16:35:04.508596Z","iopub.execute_input":"2025-11-09T16:35:04.509157Z","iopub.status.idle":"2025-11-09T16:35:22.518552Z","shell.execute_reply.started":"2025-11-09T16:35:04.509126Z","shell.execute_reply":"2025-11-09T16:35:22.517674Z"}},"outputs":[],"execution_count":null},{"id":"4e9f8dc6","cell_type":"code","source":"\ntrain = pd.read_csv(COMP_DIR / \"train.csv\")\ntest  = pd.read_csv(COMP_DIR / \"test.csv\")\n\nprint(\"train shape:\", train.shape)\nprint(\"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\")\n               .loc[:, target_names])\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","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T16:35:22.519499Z","iopub.execute_input":"2025-11-09T16:35:22.520060Z","iopub.status.idle":"2025-11-09T16:35:22.598652Z","shell.execute_reply.started":"2025-11-09T16:35:22.520038Z","shell.execute_reply":"2025-11-09T16:35:22.597882Z"}},"outputs":[],"execution_count":null},{"id":"f76158af","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             \"mean_H\":0.0,\"mean_S\":0.0,\"mean_V\":0.0,\"std_H\":0.0,\"std_S\":0.0,\"std_V\":0.0,\n             \"mean_L\":0.0,\"mean_A\":0.0,\"mean_Blab\":0.0,\"std_L\":0.0,\"std_A\":0.0,\"std_Blab\":0.0,\n             \"edge_density\":0.0,\"lap_var\":0.0,\"entropy\":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        hsv = np.array(Image.fromarray(arr.astype(np.uint8)).convert(\"HSV\")).astype(np.float32)\n        H,S,V = hsv[:,:,0], hsv[:,:,1], hsv[:,:,2]\n        feats[\"mean_H\"],feats[\"mean_S\"],feats[\"mean_V\"] = H.mean(),S.mean(),V.mean()\n        feats[\"std_H\"],feats[\"std_S\"],feats[\"std_V\"]    = H.std(),S.std(),V.std()\n        lab = np.array(Image.fromarray(arr.astype(np.uint8)).convert(\"LAB\")).astype(np.float32)\n        L,A,Bl = lab[:,:,0], lab[:,:,1], lab[:,:,2]\n        feats[\"mean_L\"],feats[\"mean_A\"],feats[\"mean_Blab\"] = L.mean(),A.mean(),Bl.mean()\n        feats[\"std_L\"],feats[\"std_A\"],feats[\"std_Blab\"]    = L.std(),A.std(),Bl.std()\n        try:\n            from scipy.ndimage import sobel, laplace\n            sob = np.hypot(sobel(gray,0), sobel(gray,1))\n            feats[\"edge_density\"] = (sob>sob.mean()).mean()\n            feats[\"lap_var\"] = laplace(gray).var()\n        except Exception:\n            pass\n        hist,_ = np.histogram(gray, bins=64, range=(0,255), density=True)\n        hist = hist + 1e-12\n        feats[\"entropy\"] = float(-(hist*np.log(hist)).sum())\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 predict_tabular(train_df, y_wide_df, test_df, target_names):\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)\n    X = Xtr.values\n    Y = y_wide_df[target_names].values\n    Ylog = np.log1p(Y)\n\n    kf = KFold(n_splits=5, shuffle=True, random_state=42)\n    oof = np.zeros_like(Ylog)\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 = Ylog[tr_idx], Ylog[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, Y_tr[:,j], sample_weight=w)\n            pred_va[:,j] = hgb.predict(X_va)\n        oof[va_idx] = pred_va\n\n    calib = {}\n    for j,t in enumerate(target_names):\n        r = Ridge(alpha=1e-6).fit(np.expm1(oof[:,[j]]), np.expm1(Ylog[:,j:j+1]))\n        calib[t] = (float(r.coef_[0][0]), float(r.intercept_[0]))\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, Ylog[:,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)\n    Xte = Xts.values\n    plog = np.column_stack([m.predict(Xte) for m in models])\n    P = np.expm1(plog); P = np.clip(P, 0, None)\n    for j,t in enumerate(target_names):\n        a,b = calib[t]; P[:,j] = a*P[:,j] + b\n    return pd.DataFrame(P, index=test_images, columns=target_names)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T16:35:22.600956Z","iopub.execute_input":"2025-11-09T16:35:22.601297Z","iopub.status.idle":"2025-11-09T16:35:22.696892Z","shell.execute_reply.started":"2025-11-09T16:35:22.601267Z","shell.execute_reply":"2025-11-09T16:35:22.695969Z"}},"outputs":[],"execution_count":null},{"id":"9594249e","cell_type":"code","source":"\ndef load_state_dict_flex(model, wpath, strict=False, key_candidates=(\"state_dict\",\"model\",\"weights\")):\n    print(f\"Intentando cargar pesos desde: {os.path.basename(wpath)}\")\n    sd = None\n    try:\n        if wpath.endswith(\".safetensors\"):\n            try:\n                from safetensors.torch import load_file as safe_load_file\n            except Exception as e:\n                raise RuntimeError(\"No se pudo importar safetensors.torch.\") from e\n            sd = safe_load_file(wpath)\n        else:\n            sd = torch.load(wpath, map_location=\"cpu\", weights_only=False)\n    except Exception as e:\n        print(\"Primera carga falló:\", e); raise\n\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\n    def strip_prefix_if_present(state_dict, prefix=\"module.\"):\n        if all(k.startswith(prefix) for k in state_dict.keys()):\n            return {k[len(prefix):]: v for k,v in state_dict.items()}\n        return state_dict\n\n    if isinstance(sd, dict):\n        sd = strip_prefix_if_present(sd, \"module.\")\n        missing, unexpected = model.load_state_dict(sd, strict=strict)\n        print(f\"Cargados con strict={strict}. Missing: {len(missing)}, Unexpected: {len(unexpected)}\")\n    else:\n        raise RuntimeError(\"El archivo de pesos no contiene un state_dict válido.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T16:35:22.697805Z","iopub.execute_input":"2025-11-09T16:35:22.698171Z","iopub.status.idle":"2025-11-09T16:35:22.716557Z","shell.execute_reply.started":"2025-11-09T16:35:22.698146Z","shell.execute_reply":"2025-11-09T16:35:22.715563Z"}},"outputs":[],"execution_count":null},{"id":"4170a668","cell_type":"code","source":"\n\nDINO_DIR = \"/kaggle/input/dinov2/pytorch/base/1\"\nassert os.path.exists(DINO_DIR), \"Falta DINOv2 Base en /kaggle/input/dinov2/pytorch/base/1\"\n\nmodel_dino = timm.create_model(\"vit_base_patch14_dinov2\", pretrained=False, num_classes=0, img_size=518)\nmodel_dino.eval().to(device)\n\nweights = []\nfor ext in (\"*.safetensors\",\"*.pt\",\"*.bin\"):\n    weights += glob.glob(os.path.join(DINO_DIR, \"**\", ext), recursive=True)\nif weights:\n    try:\n        load_state_dict_flex(model_dino, weights[0], strict=False)\n    except Exception as e:\n        print(\"Aviso: no se pudieron cargar completamente los pesos DINO:\", e)\nelse:\n    print(\"Aviso: no se encontraron pesos explícitos en la carpeta DINO.\")\n\nIMG_SIZE_DINO = 518\ndino_tf = T.Compose([\n    T.Resize((IMG_SIZE_DINO, IMG_SIZE_DINO)),\n    T.ToTensor(),\n    T.Normalize(mean=(0.485,0.456,0.406), std=(0.229,0.224,0.225)),\n])\n\nclass ImgOnlyDS(Dataset):\n    def __init__(self, rel_paths): self.rel = rel_paths\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\",(IMG_SIZE_DINO, IMG_SIZE_DINO), (0,0,0))\n        return dino_tf(im)\n\ndef dino_embeddings(paths, bs=8):\n    dl = DataLoader(ImgOnlyDS(paths), batch_size=bs, shuffle=False, num_workers=2, pin_memory=True)\n    out = []\n    with torch.no_grad():\n        for xb in dl:\n            z = model_dino(xb.to(device))\n            if isinstance(z, (list, tuple)): z = z[0]\n            out.append(z.detach().cpu().numpy())\n    return np.concatenate(out, 0)\n\ntrain_imgs = y_wide.index.to_list()\ntest_imgs  = test[\"image_path\"].drop_duplicates().to_list()\nE_train = dino_embeddings(train_imgs, bs=8)\nE_test  = dino_embeddings(test_imgs,  bs=8)\n\nscaler_dino = StandardScaler()\nEtrS = scaler_dino.fit_transform(E_train)\nEteS = scaler_dino.transform(E_test)\n\nY = y_wide[target_names].values\npreds_dino = np.zeros((len(test_imgs), len(target_names)), dtype=np.float32)\nfor j,t in enumerate(target_names):\n    w = np.full(len(EtrS), TARGET_WEIGHTS[t], dtype=np.float32)\n    reg = Ridge(alpha=1.0, fit_intercept=True)\n    reg.fit(EtrS, Y[:,j], sample_weight=w)\n    preds_dino[:,j] = reg.predict(EteS)\npreds_dino_df = pd.DataFrame(np.clip(preds_dino,0,None), index=test_imgs, columns=target_names)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T16:35:22.717536Z","iopub.execute_input":"2025-11-09T16:35:22.717813Z","iopub.status.idle":"2025-11-09T16:52:28.229950Z","shell.execute_reply.started":"2025-11-09T16:35:22.717784Z","shell.execute_reply":"2025-11-09T16:52:28.228758Z"}},"outputs":[],"execution_count":null},{"id":"e4fe9a08","cell_type":"code","source":"\nSIGLIP_DIR_CAND = [\n    \"/kaggle/input/google-siglip-so400m-patch14-384/transformers/default/1\",\n    \"/kaggle/input/google-siglip-so400m-patch14-384\",\n]\nSIGLIP_DIR = next((p for p in SIGLIP_DIR_CAND if os.path.exists(p)), None)\nassert SIGLIP_DIR is not None, \"Agrega el recurso de SigLIP en Add data → Models y ajusta SIGLIP_DIR_CAND.\"\n\narch_candidates = [\n    \"vit_so400m_patch14_siglip_384\",\n    \"vit_base_patch14_siglip_384\",\n    \"vit_large_patch14_384\",\n]\nsiglip_model, arch_siglip = None, None\nfor cand in arch_candidates:\n    try:\n        siglip_model = timm.create_model(cand, pretrained=False, num_classes=0)\n        arch_siglip = cand; break\n    except Exception:\n        pass\nassert siglip_model is not None, \"No pude crear el modelo SigLIP con timm; ajusta arch_candidates.\"\n\nwts = []\nfor ext in (\"*.safetensors\",\"*.pt\",\"*.bin\"):\n    wts += glob.glob(os.path.join(SIGLIP_DIR, \"**\", ext), recursive=True)\nif wts:\n    try:\n        load_state_dict_flex(siglip_model, wts[0], strict=False)\n    except Exception as e:\n        print(\"Aviso: no se pudieron cargar completamente los pesos SigLIP:\", e)\nelse:\n    print(\"Aviso: no se encontraron archivos de pesos en la carpeta SigLIP.\")\n\nsiglip_model.eval().to(device)\n\nIMG_SIZE_SIG = 384\nsiglip_tf = T.Compose([\n    T.Resize((IMG_SIZE_SIG, IMG_SIZE_SIG)),\n    T.ToTensor(),\n    T.Normalize((0.485,0.456,0.406), (0.229,0.224,0.225)),\n])\n\nclass ImgOnlyDS_Sig(Dataset):\n    def __init__(self, rel): self.rel = rel\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\", (IMG_SIZE_SIG, IMG_SIZE_SIG), (0,0,0))\n        return siglip_tf(im)\n\ndef siglip_embeddings(paths, bs=16):\n    dl = DataLoader(ImgOnlyDS_Sig(paths), batch_size=bs, shuffle=False, num_workers=2, pin_memory=True)\n    out = []\n    with torch.no_grad():\n        for xb in dl:\n            zb = siglip_model(xb.to(device))\n            if isinstance(zb, (list, tuple)): zb = zb[0]\n            out.append(zb.detach().cpu().numpy())\n    return np.concatenate(out, 0)\n\nE_train_sig = siglip_embeddings(train_imgs, bs=16)\nE_test_sig  = siglip_embeddings(test_imgs,  bs=16)\n\nsc_sig = StandardScaler()\nEtrS_sig = sc_sig.fit_transform(E_train_sig)\nEteS_sig = sc_sig.transform(E_test_sig)\n\nY = y_wide[target_names].values\npreds_sig = np.zeros((len(test_imgs), len(target_names)), dtype=np.float32)\nfor j,t in enumerate(target_names):\n    w = np.full(len(EtrS_sig), TARGET_WEIGHTS[t], dtype=np.float32)\n    reg = Ridge(alpha=1.0, fit_intercept=True)\n    reg.fit(EtrS_sig, Y[:,j], sample_weight=w)\n    preds_sig[:,j] = reg.predict(EteS_sig)\npreds_siglip_df = pd.DataFrame(np.clip(preds_sig,0,None), index=test_imgs, columns=target_names)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T16:52:28.231432Z","iopub.execute_input":"2025-11-09T16:52:28.231719Z","iopub.status.idle":"2025-11-09T17:22:50.785091Z","shell.execute_reply.started":"2025-11-09T16:52:28.231682Z","shell.execute_reply":"2025-11-09T17:22:50.783413Z"}},"outputs":[],"execution_count":null},{"id":"f2bdefa4","cell_type":"code","source":"\n\nprint(\">> Entrenando TABULAR (HGB) ...\")\npreds_tab = predict_tabular(train, y_wide, test, target_names)\n\nprint(\"\\\\n>> Entrenando Small CNN ...\")\ndef train_cnn_predict_wrapper(*args, **kwargs):\n    try:\n        return train_cnn_predict(*args, **kwargs)\n    except Exception as e:\n        print(\"Aviso: CNN falló, continuo sin CNN. Error:\", e)\n        test_images = test[\"image_path\"].unique().tolist()\n        return pd.DataFrame(np.zeros((len(test_images), len(target_names))), index=test_images, columns=target_names)\n\npreds_cnn = train_cnn_predict_wrapper(y_wide, test, target_names,\n                                      epochs=8, batch_size=16, lr=1e-3, wd=1e-5, use_tta=True)\n\nBLEND4 = {\n    \"Dry_Green_g\": (0.30, 0.25, 0.25, 0.20),  # (tab, cnn, dino, siglip)\n    \"Dry_Dead_g\":  (0.30, 0.25, 0.25, 0.20),\n    \"Dry_Clover_g\":(0.30, 0.25, 0.25, 0.20),\n    \"GDM_g\":       (0.30, 0.30, 0.25, 0.15),\n    \"Dry_Total_g\": (0.25, 0.25, 0.30, 0.20),\n}\n\ntest_imgs_unique = test[\"image_path\"].drop_duplicates().tolist()\npreds_ens = preds_siglip_df.copy()\nfor t in target_names:\n    a,b,c,d = BLEND4[t]\n    preds_ens[t] = (\n        a*preds_tab.loc[test_imgs_unique, t].values +\n        b*preds_cnn.loc[test_imgs_unique, t].values +\n        c*preds_dino_df.loc[test_imgs_unique, t].values +\n        d*preds_siglip_df.loc[test_imgs_unique, t].values\n    )\n\ntotal_parts = preds_ens[\"Dry_Green_g\"] + preds_ens[\"Dry_Dead_g\"] + preds_ens[\"Dry_Clover_g\"]\npreds_ens[\"Dry_Total_g\"] = 0.7*preds_ens[\"Dry_Total_g\"] + 0.3*total_parts\npreds_ens = preds_ens.clip(lower=0)\n\npreds_long = (preds_ens.reset_index()\n              .melt(id_vars=\"index\", var_name=\"target_name\", value_name=\"pred\")\n              .rename(columns={\"index\":\"image_path\"}))\nsubmission = (test[[\"sample_id\",\"image_path\",\"target_name\"]]\n              .merge(preds_long, on=[\"image_path\",\"target_name\"], how=\"left\")\n              [[\"sample_id\",\"pred\"]].rename(columns={\"pred\":\"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)\n\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-09T17:22:50.787160Z","iopub.execute_input":"2025-11-09T17:22:50.787735Z","iopub.status.idle":"2025-11-09T17:26:51.159914Z","shell.execute_reply.started":"2025-11-09T17:22:50.787687Z","shell.execute_reply":"2025-11-09T17:26:51.158988Z"}},"outputs":[],"execution_count":null}]}