{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":5348282,"sourceType":"datasetVersion","datasetId":3105264},{"sourceId":11256758,"sourceType":"datasetVersion","datasetId":7035066},{"sourceId":14451968,"sourceType":"datasetVersion","datasetId":9230857},{"sourceId":14616207,"sourceType":"datasetVersion","datasetId":9325426},{"sourceId":14627907,"sourceType":"datasetVersion","datasetId":9255363},{"sourceId":714180,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":540408,"modelId":553604}],"dockerImageVersionId":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport sys\nimport glob\nimport joblib\nimport importlib\nimport numpy as np\nimport pandas as pd\nimport torch\nfrom pathlib import Path\nfrom PIL import Image\nfrom tqdm.auto import tqdm\n\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\nfrom transformers import AutoImageProcessor, AutoModel, CLIPProcessor, CLIPModel\n\n# =========================================================\n# Common\n# =========================================================\nCOMP_DIR = \"/kaggle/input/csiro-biomass\"\nCONV_DATASET_DIR = \"/kaggle/input/csiro-convnext\"\nMODEL_DATASET_DIR = \"/kaggle/input/csiro-biomass-models\"\nHF_DATASET_DIR = \"/kaggle/input/csiro-dataset\"\n\nTARGET_NAMES = ['Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'Dry_Total_g', 'GDM_g']\nMAX_VALS = np.array([71.7865, 83.8407, 157.9836, 185.70, 157.9836], dtype=np.float32)\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"DEVICE:\", DEVICE)\n\ndef post_process(df: pd.DataFrame) -> pd.DataFrame:\n    cols = [\"Dry_Green_g\", \"Dry_Clover_g\", \"Dry_Dead_g\", \"GDM_g\", \"Dry_Total_g\"]\n    Y = df[cols].values.T\n    C = np.array([[1, 1, 0, -1, 0],\n                  [0, 0, 1,  1, -1]], dtype=np.float32)\n    P = np.eye(5, dtype=np.float32) - C.T @ np.linalg.inv(C @ C.T) @ C\n    Y_rec = (P @ Y).T\n    Y_rec = np.clip(Y_rec, a_min=0.0, a_max=None)\n    df[cols] = Y_rec\n    return df\n\n# =========================================================\n# 1) ConvNeXt predictor (your upper code, almost 그대로)\n# =========================================================\nROOT = Path(\"/kaggle/input/csiro-biomass-models\")\nsys.path.insert(0, str(ROOT))\nimportlib.invalidate_caches()\nfrom src.model_factory import create_model_from_name\n\n# load conv fold models\nconv_models = []\nmeta = None\nfor fold_dir in sorted(Path(CONV_DATASET_DIR).glob(\"fold_*\")):\n    ckpt = torch.load(fold_dir / \"best.pt\", map_location=\"cpu\")\n    meta = meta or ckpt\n    model = create_model_from_name(\n        ckpt[\"model_name\"],\n        num_outputs=len(ckpt[\"targets_order\"]),\n        pretrained=False,\n        source=ckpt.get(\"model_source\", \"timm\"),\n    )\n    model.load_state_dict(ckpt[\"model_state\"])\n    model.to(DEVICE).eval()\n    conv_models.append((model, ckpt))\n\ndef build_conv_transforms(image_size: int, normalize_cfg: dict):\n    mean = normalize_cfg.get(\"mean\", [0.485, 0.456, 0.406])\n    std = normalize_cfg.get(\"std\", [0.229, 0.224, 0.225])\n    return A.Compose([\n        A.Resize(image_size, image_size),\n        A.Normalize(mean=mean, std=std),\n        ToTensorV2(),\n    ])\n\nconv_tfms = build_conv_transforms(\n    image_size=int(meta.get(\"image_size\", 512)),\n    normalize_cfg=meta.get(\"normalize\", {}),\n)\n\ndef predict_conv_image(path: str) -> np.ndarray:\n    img = Image.open(path).convert(\"RGB\")\n    aug = conv_tfms(image=np.array(img))\n    x = aug[\"image\"].unsqueeze(0).to(DEVICE)\n    preds = []\n    with torch.no_grad():\n        for model, ck in conv_models:\n            out = model(x).detach().cpu().numpy()[0]\n            pred_map = dict(zip(ck[\"targets_order\"], out))\n            preds.append([pred_map[t] for t in TARGET_NAMES])\n    return np.mean(np.asarray(preds, dtype=np.float32), axis=0)\n\ndef run_conv_predictions(unique_paths: np.ndarray) -> pd.DataFrame:\n    conv_preds = {\n        rel_path: predict_conv_image(os.path.join(COMP_DIR, rel_path))\n        for rel_path in unique_paths\n    }\n    conv_df = (\n        pd.DataFrame.from_dict(conv_preds, orient=\"index\", columns=TARGET_NAMES)\n          .rename_axis(\"image_path\")\n          .reset_index()\n    )\n    # ここでは post_process しない（最後にまとめて一回）\n    return conv_df\n\n# =========================================================\n# 2) ViT-tree predictor (your lower codeから「conv埋め込み」を完全排除)\n# =========================================================\nTREE_MODEL_ROOT = os.path.join(MODEL_DATASET_DIR, \"artifacts/tree_models\")\nSRC_DIR = os.path.join(MODEL_DATASET_DIR, \"src\")\nfor extra in (SRC_DIR, HF_DATASET_DIR):\n    if extra and os.path.isdir(extra) and extra not in sys.path:\n        sys.path.append(extra)\n\ndef load_model_from_input(target_name: str) -> str:\n    if \"clip\" in target_name.lower():\n        patterns = [\n            \"/kaggle/input/*openai*clip*vit*large*336*\",\n            \"/kaggle/input/*clip*vit*large*336*\",\n            \"/kaggle/input/*clip*vit*large*\",\n            \"/kaggle/input/*openai*clip*\",\n        ]\n        candidates = []\n        for pat in patterns:\n            candidates.extend(glob.glob(pat))\n        for path in candidates:\n            if os.path.exists(os.path.join(path, \"config.json\")):\n                return path\n            nested = glob.glob(os.path.join(path, \"*\", \"config.json\"))\n            if nested:\n                return os.path.dirname(nested[0])\n        raise FileNotFoundError(\"CLIP model dataset not found in Input!\")\n\n    candidates = glob.glob(f\"/kaggle/input/*{target_name}*\")\n    for path in candidates:\n        if os.path.exists(os.path.join(path, \"config.json\")):\n            return path\n        nested = glob.glob(os.path.join(path, \"*\", \"config.json\"))\n        if nested:\n            return os.path.dirname(nested[0])\n    raise FileNotFoundError(f\"Model {target_name} not found.\")\n\ndef get_embeddings(image_paths, model_path, model_type=\"siglip\", batch_size=32):\n    if model_type == \"clip\":\n        processor = CLIPProcessor.from_pretrained(model_path)\n        model = CLIPModel.from_pretrained(model_path).to(DEVICE).eval()\n    else:\n        processor = AutoImageProcessor.from_pretrained(model_path)\n        model = AutoModel.from_pretrained(model_path).to(DEVICE).eval()\n\n    embeddings = []\n    for i in tqdm(range(0, len(image_paths), batch_size), desc=f\"Extract {model_type} (TTA)\"):\n        batch_paths = image_paths[i:i+batch_size]\n        images = []\n        for p in batch_paths:\n            try:\n                img = Image.open(p).convert(\"RGB\")\n            except Exception:\n                img = Image.new(\"RGB\", (224, 224))\n            images.append(img)\n\n        images_flipped = [img.transpose(Image.FLIP_LEFT_RIGHT) for img in images]\n        combined_images = images + images_flipped\n\n        with torch.no_grad():\n            if model_type == \"clip\":\n                inputs = processor(images=combined_images, return_tensors=\"pt\", padding=True).to(DEVICE)\n                out = model.get_image_features(**inputs)\n            else:\n                inputs = processor(images=combined_images, return_tensors=\"pt\").to(DEVICE)\n                if model_type == \"siglip\":\n                    out = model.get_image_features(**inputs)\n                else:\n                    outputs = model(**inputs)\n                    tokens = outputs.last_hidden_state if hasattr(outputs, \"last_hidden_state\") else outputs[0]\n                    out = tokens[:, 0, :]\n\n        out = out / out.norm(dim=-1, keepdim=True)\n        emb = out.detach().cpu().numpy().astype(np.float32)\n        n = len(images)\n        emb_avg = (emb[:n] + emb[n:]) * 0.5\n        embeddings.append(emb_avg)\n\n    return np.vstack(embeddings)\n\ndef infer_dim_from_scaler(scaler):\n    for attr in (\"n_features_in_\", \"mean_\", \"scale_\"):\n        if hasattr(scaler, attr):\n            val = getattr(scaler, attr)\n            if isinstance(val, (list, tuple, np.ndarray)):\n                return len(val)\n            if isinstance(val, (int, float)):\n                return int(val)\n    return None\n\ndef build_feature_cache(full_paths):\n    sig_path = load_model_from_input(\"siglip\")\n    dino_path = load_model_from_input(\"dinov2\")\n    clip_path = load_model_from_input(\"clip\")\n    emb_sig = get_embeddings(full_paths, sig_path, \"siglip\")\n    emb_dino = get_embeddings(full_paths, dino_path, \"dinov2\")\n    emb_clip = get_embeddings(full_paths, clip_path, \"clip\")\n\n    stacked2 = np.hstack([emb_sig, emb_dino]).astype(np.float32)\n    stacked3 = np.hstack([emb_sig, emb_dino, emb_clip]).astype(np.float32)\n\n    cache = {\n        emb_sig.shape[1]: emb_sig,\n        stacked2.shape[1]: stacked2,\n        stacked3.shape[1]: stacked3,\n        \"emb_sig\": emb_sig,\n        \"emb_dino\": emb_dino,\n        \"emb_clip\": emb_clip,\n    }\n    return cache\n\ndef get_features_for_dim(dim: int, cache: dict) -> np.ndarray:\n    if dim in cache:\n        return cache[dim]\n    # conv embeddings は使わない（ここで完全排除）\n    stacked3 = cache[list(k for k in cache.keys() if isinstance(k, int))][-1] if False else None  # never used\n    # 代替: 3種結合から先頭dimを切る（dimが小さい時だけの保険）\n    emb_sig = cache[\"emb_sig\"]\n    emb_dino = cache[\"emb_dino\"]\n    emb_clip = cache[\"emb_clip\"]\n    stacked = np.hstack([emb_sig, emb_dino, emb_clip]).astype(np.float32)\n    if dim <= stacked.shape[1]:\n        return stacked[:, :dim]\n    raise ValueError(f\"Cannot prepare feature matrix with dim={dim} (max={stacked.shape[1]})\")\n\ndef predict_tree_group(group_name: str, weight: float, feat_cache: dict) -> np.ndarray | None:\n    group_dir = os.path.join(TREE_MODEL_ROOT, group_name)\n    if not os.path.isdir(group_dir):\n        print(f\"Skip {group_name}: not found\")\n        return None\n\n    n = feat_cache[\"emb_sig\"].shape[0]\n    group_pred = np.zeros((n, len(TARGET_NAMES)), dtype=np.float32)\n    used_folds = 0\n\n    for fold in range(5):\n        engine_path = os.path.join(group_dir, f\"engine_fold_{fold}.pkl\")\n        model_path = os.path.join(group_dir, f\"models_fold_{fold}.pkl\")\n        if not (os.path.exists(engine_path) and os.path.exists(model_path)):\n            continue\n\n        try:\n            scaler = joblib.load(engine_path)\n            models = joblib.load(model_path)\n        except Exception as exc:\n            print(f\"  [{group_name}] fold {fold} load failed: {exc}\")\n            continue\n\n        expected_dim = infer_dim_from_scaler(scaler)\n        if expected_dim is None:\n            print(f\"  [{group_name}] fold {fold} missing dim info; skipping\")\n            continue\n\n        X_raw = get_features_for_dim(expected_dim, feat_cache)\n        X_proc = scaler.transform(X_raw)\n\n        if isinstance(models, list):\n            preds = []\n            for idx, mdl in enumerate(models):\n                if getattr(mdl, \"_n_classes\", None) is None:\n                    mdl._n_classes = 1\n                pred = mdl.predict(X_proc).reshape(-1).astype(np.float32) * MAX_VALS[idx]\n                preds.append(pred)\n            fold_pred = np.column_stack(preds).astype(np.float32)\n        else:\n            fold_pred = (models.predict(X_proc).astype(np.float32) * MAX_VALS)\n\n        group_pred += fold_pred\n        used_folds += 1\n\n    if used_folds == 0:\n        print(f\"Skip {group_name}: no valid folds\")\n        return None\n\n    group_pred /= float(used_folds)\n    print(f\"Added {group_name}: folds={used_folds}, weight={weight}\")\n    return group_pred * np.float32(weight)\n\ndef run_vit_tree_predictions(unique_paths: np.ndarray, full_paths: list[str],\n                             model_group_weights: dict[str, float]) -> pd.DataFrame:\n    feat_cache = build_feature_cache(full_paths)\n\n    final_pred = np.zeros((len(full_paths), len(TARGET_NAMES)), dtype=np.float32)\n    total_w = 0.0\n    for group, w in model_group_weights.items():\n        if w <= 0:\n            continue\n        contrib = predict_tree_group(group, w, feat_cache)\n        if contrib is None:\n            continue\n        final_pred += contrib\n        total_w += float(w)\n\n    if total_w == 0.0:\n        raise RuntimeError(\"No tree-model predictions were produced. Check MODEL_GROUP_WEIGHTS / dataset paths.\")\n\n    final_pred /= np.float32(total_w)\n    vit_df = pd.DataFrame(final_pred, columns=TARGET_NAMES)\n    vit_df[\"image_path\"] = unique_paths\n    # ここでも post_process しない（最後にまとめて一回）\n    return vit_df\n\n# =========================================================\n# 3) Ensemble + submission\n# =========================================================\ntest_df = pd.read_csv(os.path.join(COMP_DIR, \"test.csv\"))\nunique_paths = test_df[\"image_path\"].drop_duplicates().to_numpy()\nfull_paths = [os.path.join(COMP_DIR, p) for p in unique_paths]\nprint(\"n_unique_images:\", len(unique_paths), \"n_rows:\", len(test_df))\n\n# ---- (A) conv preds per image ----\nconv_df = run_conv_predictions(unique_paths)\n\n# ---- (B) vit preds per image ----\n# vit系だけに重みを入れる（conv_* は 0 のまま）\nMODEL_GROUP_WEIGHTS = {\n    \"vit_lgbm\": 1.0,\n    \"vit_xgb\":  1.0,\n    \"vit_cat\":  1.0,\n    \"ridge\": 1.0,\n    \"conv_lgbm\": 0.0,\n    \"conv_xgb\":  0.0,\n    \"conv_cat\":  0.0,\n}\nvit_df = run_vit_tree_predictions(unique_paths, full_paths, MODEL_GROUP_WEIGHTS)\n\n# ---- align & ensemble ----\nalpha = 0.3  # convの比率（例: 0.5で単純平均）\nmerged = conv_df.merge(vit_df, on=\"image_path\", suffixes=(\"_conv\", \"_vit\"), how=\"inner\")\nassert len(merged) == len(unique_paths), \"merge mismatch: image_path not aligned\"\n\nens = pd.DataFrame({\"image_path\": merged[\"image_path\"].values})\nfor t in TARGET_NAMES:\n    ens[t] = (np.float32(alpha) * merged[f\"{t}_conv\"].to_numpy(np.float32) +\n              np.float32(1.0 - alpha) * merged[f\"{t}_vit\"].to_numpy(np.float32))\n\n# 最後に制約射影を1回だけ\nens = post_process(ens)\n\n# ---- build submission (sample_id 行に展開) ----\npred_map = dict(zip(ens[\"image_path\"].values, ens[TARGET_NAMES].to_numpy(np.float32)))\ntarget_to_idx = {t: i for i, t in enumerate(TARGET_NAMES)}\n\npred_vals = []\nfor _, row in test_df.iterrows():\n    vec = pred_map.get(row[\"image_path\"])\n    if vec is None:\n        pred_vals.append(np.float32(0.0))\n    else:\n        pred_vals.append(np.float32(vec[target_to_idx[row[\"target_name\"]]]))\n\nsubmission = test_df[[\"sample_id\"]].copy()\nsubmission[\"target\"] = np.asarray(pred_vals, dtype=np.float32)\n\n# safety\nassert len(submission) == len(test_df)\nassert submission[\"target\"].isna().sum() == 0\nassert np.isfinite(submission[\"target\"].values).all()\n\nout_path = \"/kaggle/working/submission.csv\"\nsubmission.to_csv(out_path, index=False)\nprint(\"saved:\", out_path, \"rows:\", len(submission))\nprint(submission.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-26T15:45:03.009468Z","iopub.execute_input":"2026-01-26T15:45:03.009744Z","iopub.status.idle":"2026-01-26T15:45:24.587393Z","shell.execute_reply.started":"2026-01-26T15:45:03.009720Z","shell.execute_reply":"2026-01-26T15:45:24.586766Z"}},"outputs":[],"execution_count":null}]}