{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"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":"gpu","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":14454181,"sourceType":"datasetVersion","datasetId":9231883},{"sourceId":14473343,"sourceType":"datasetVersion","datasetId":9244385},{"sourceId":608797,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":457027,"modelId":456721}],"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# Kaggle inference script for the vision-only regressor.\n# It mirrors excutePyOnKaggle.py but uses ONLY Qwen3-VL's vision module + a trained value_head.\n\n# -----------------------------\n# 1) Offline wheel installs\n# -----------------------------\n!pip install --no-index --find-links /kaggle/input/wheels/kaggle/working/wheels --no-deps \\\n  transformers==4.57.3 peft==0.18.1 accelerate==1.12.0 qwen-vl-utils tokenizers safetensors huggingface_hub \\\n  numpy packaging regex pyyaml requests tqdm filelock fsspec typing_extensions hf_xet charset_normalizer idna urllib3 certifi psutil pillow av\n!pip install --no-index --find-links /kaggle/input/wheels/kaggle/working/wheels --upgrade --force-reinstall --no-deps tokenizers==0.22.2\n!pip install --no-index --find-links /kaggle/input/wheels/kaggle/working/wheels --upgrade --force-reinstall --no-deps \"protobuf==4.25.3\"\n\n# -----------------------------\n# 2) Imports\n# -----------------------------\nimport os\nimport sys\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nfrom PIL import Image\nfrom tqdm import tqdm\nimport transformers\nfrom transformers import AutoProcessor, AutoModelForVision2Seq\n\nprint(\"python:\", sys.version)\nprint(\"transformers:\", transformers.__version__)\n\n# -----------------------------\n# 3) Vision-only regressor (must match train_vision.py)\n# -----------------------------\nclass VisionOnlyRegressor(nn.Module):\n    def __init__(self, base_model, hidden_size: int):\n        super().__init__()\n\n        # locate vision module\n        visual = None\n        if hasattr(base_model, \"model\") and hasattr(base_model.model, \"visual\"):\n            visual = base_model.model.visual\n        elif hasattr(base_model, \"base_model\") and hasattr(base_model.base_model, \"model\") and hasattr(base_model.base_model.model, \"visual\"):\n            visual = base_model.base_model.model.visual\n        if visual is None:\n            raise AttributeError(\"Could not locate base_model.model.visual\")\n        self.visual = visual\n\n        self.num_labels = 5\n        self.vision_ln = nn.LayerNorm(hidden_size)\n\n        intermediate_size = hidden_size // 2\n        self.value_head = nn.Sequential(\n            nn.Linear(hidden_size, intermediate_size),\n            nn.GELU(),\n            nn.Dropout(0.1),\n            nn.Linear(intermediate_size, intermediate_size // 2),\n            nn.GELU(),\n            nn.Dropout(0.1),\n            nn.Linear(intermediate_size // 2, self.num_labels),\n        )\n\n    @torch.no_grad()\n    def forward(self, pixel_values, image_grid_thw):\n        vision_h = self.visual(hidden_states=pixel_values, grid_thw=image_grid_thw)\n        if isinstance(vision_h, (tuple, list)):\n            vision_h = vision_h[0]\n        if vision_h.dim() == 2:\n            vision_h = vision_h.unsqueeze(0)\n\n        pooled = vision_h.mean(dim=1)  # [Bv, D]\n\n        # Align pooled batch with grid batch (some vision paths may return B=1)\n        expected_bsz = image_grid_thw.size(0)\n        if pooled.size(0) != expected_bsz:\n            if pooled.size(0) == 1:\n                pooled = pooled.expand(expected_bsz, -1)\n            else:\n                pooled = pooled[:expected_bsz]\n\n        pooled = pooled.to(self.vision_ln.weight.dtype)\n        pooled_ln = self.vision_ln(pooled)\n\n        logits = self.value_head(pooled_ln.to(next(self.value_head.parameters()).dtype))\n        return logits\n\n\n# -----------------------------\n# 4) Predict + submission\n# -----------------------------\n\ndef predict():\n    if torch.cuda.is_available():\n        print(f\"CUDA is available. Using GPU: {torch.cuda.get_device_name(0)}\")\n\n    # Base model path (must exist as Kaggle Dataset input)\n    base_model_id = \"/kaggle/input/qwen-3-vl/transformers/4b-instruct/1\"\n\n    # Trained vision-only head path (Kaggle Dataset you upload)\n    # Expect: /kaggle/input/finetune-vision/qwen-vl-biomass-vision-only/value_head.pt\n    # (and optionally a processor folder, but we can use base model processor)\n    vision_ckpt_dir = \"/kaggle/input/qwen3vl-valuehead\"\n    head_path = os.path.join(vision_ckpt_dir, \"value_head.pt\")\n\n    # Competition data\n    test_df = pd.read_csv(\"/kaggle/input/csiro-biomass/test.csv\")\n\n    # Load processor from base model\n    processor = AutoProcessor.from_pretrained(base_model_id, trust_remote_code=True)\n\n    # Match training preprocessing\n    try:\n        processor.image_processor.max_pixels = 512 * 512\n        processor.image_processor.min_pixels = 256 * 256\n    except Exception:\n        pass\n\n    # Load base model (we only use its vision module)\n    print(\"Loading base model...\")\n    base_model = AutoModelForVision2Seq.from_pretrained(\n        base_model_id,\n        device_map=\"auto\",\n        torch_dtype=torch.float16,\n        trust_remote_code=True,\n    )\n    base_model.eval()\n\n    # Determine hidden size\n    if hasattr(base_model.config, \"vision_config\") and hasattr(base_model.config.vision_config, \"out_hidden_size\"):\n        hidden_size = base_model.config.vision_config.out_hidden_size\n    elif hasattr(base_model.config, \"text_config\") and hasattr(base_model.config.text_config, \"hidden_size\"):\n        hidden_size = base_model.config.text_config.hidden_size\n    elif hasattr(base_model.config, \"hidden_size\"):\n        hidden_size = base_model.config.hidden_size\n    else:\n        raise AttributeError(\"Could not infer hidden_size from model config.\")\n\n    model = VisionOnlyRegressor(base_model, hidden_size=hidden_size)\n\n    if not os.path.exists(head_path):\n        raise FileNotFoundError(f\"Missing value_head.pt at: {head_path}\")\n\n    print(\"Loading value head:\", head_path)\n    state = torch.load(head_path, map_location=\"cpu\")\n    model.value_head.load_state_dict(state)\n\n    # Put head on GPU if possible\n    if torch.cuda.is_available():\n        model = model.to(\"cuda\")\n\n    model.eval()\n\n    target_names = [\"Dry_Clover_g\", \"Dry_Dead_g\", \"Dry_Green_g\", \"Dry_Total_g\", \"GDM_g\"]\n    target_idx = {n: i for i, n in enumerate(target_names)}\n\n    results = {}\n\n    print(\"Starting inference...\")\n    for image_rel_path, group_df in tqdm(test_df.groupby(\"image_path\"), desc=\"Inferencing\"):\n        image_path = os.path.join(\"/kaggle/input/csiro-biomass\", image_rel_path)\n\n        try:\n            if not os.path.exists(image_path):\n                for _, row in group_df.iterrows():\n                    results[row[\"sample_id\"]] = 0.0\n                continue\n            image = Image.open(image_path).convert(\"RGB\")\n        except Exception:\n            for _, row in group_df.iterrows():\n                results[row[\"sample_id\"]] = 0.0\n            continue\n\n        image = image.resize((512, 512))\n\n        # IMPORTANT: Qwen3VLProcessor expects some text. Use empty string placeholder.\n        inputs = processor(\n            text=[\"\"],\n            images=[image],\n            padding=\"max_length\",\n            truncation=True,\n            max_length=8,  # keep tiny since we do NOT use text\n            return_tensors=\"pt\",\n        )\n\n        pixel_values = inputs[\"pixel_values\"]\n        image_grid_thw = inputs[\"image_grid_thw\"]\n\n        if torch.cuda.is_available():\n            pixel_values = pixel_values.to(\"cuda\")\n            image_grid_thw = image_grid_thw.to(\"cuda\")\n\n        with torch.no_grad():\n            logits = model(pixel_values=pixel_values, image_grid_thw=image_grid_thw)  # [1, 5]\n            preds = torch.expm1(logits).float().cpu().numpy()[0]\n            preds = np.maximum(preds, 0.0)\n\n        for _, row in group_df.iterrows():\n            s_id = row[\"sample_id\"]\n            t_name = row[\"target_name\"]\n            idx = target_idx.get(t_name, None)\n            results[s_id] = float(preds[idx]) if idx is not None else 0.0\n\n    # Build submission\n    test_df[\"target\"] = test_df[\"sample_id\"].map(results)\n    submission = test_df[[\"sample_id\", \"target\"]]\n\n    submission.to_csv(\"submission.csv\", index=False)\n    print(\"Saved submission.csv\")\n\n\nif __name__ == \"__main__\":\n    predict()\n\n# Kaggle notes:\n# - Upload your trained vision-only head as a Dataset, structured like:\n#   qwen-vl-biomass-vision-only/value_head.pt\n# - Set `vision_ckpt_dir` accordingly.\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-01-12T10:22:14.352284Z","iopub.execute_input":"2026-01-12T10:22:14.352853Z","iopub.status.idle":"2026-01-12T10:23:16.130938Z","shell.execute_reply.started":"2026-01-12T10:22:14.352826Z","shell.execute_reply":"2026-01-12T10:23:16.130080Z"}},"outputs":[],"execution_count":null}]}