{"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":"nvidiaTeslaT4","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":13550547,"sourceType":"datasetVersion","datasetId":8606054}],"dockerImageVersionId":31153,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"### 🔗 Training Notebook Source\n\n**The model used for this inference was trained and developed in the following Kaggle Notebook.**\n\n[**View the full Training Notebook here**](https://www.kaggle.com/code/tasmim/train-csiro-image2biomass-prediction?scriptVersionId=272144434)\n","metadata":{}},{"cell_type":"code","source":"# ============================================================================\n# CSIRO Image2Biomass Prediction - INFERENCE ONLY\n# ============================================================================\n\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nimport timm\nimport cv2\nfrom tqdm import tqdm\nimport warnings\nimport os\nwarnings.filterwarnings('ignore')\n\n# ============================================================================\n# CONFIGURATION\n# ============================================================================\nclass CFG:\n    # Paths\n    test_csv = '/kaggle/input/csiro-biomass/test.csv'\n    test_dir = '/kaggle/input/csiro-biomass/test'\n    model_dir = '/kaggle/input/csiro-models'  # Directory containing model checkpoints\n    output_file = 'submission.csv'\n    \n    # Model settings (MUST match training configuration)\n    model_name = 'tf_efficientnetv2_m'\n    img_size = 512\n    n_folds = 5  # Number of folds to ensemble\n    \n    # Inference settings\n    batch_size = 32  # Can be larger than training since no gradients\n    num_workers = 4\n    use_tta = True  # Test-Time Augmentation\n    tta_steps = 10   # Number of TTA augmentations\n    \n    # Target names (order matters!)\n    targets = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n    \n    # Device\n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    \nprint(f\"Using device: {CFG.device}\")\nprint(f\"Will ensemble {CFG.n_folds} models\")\nprint(f\"TTA enabled: {CFG.use_tta} ({CFG.tta_steps} augmentations)\" if CFG.use_tta else \"TTA disabled\")\n\n# ============================================================================\n# DATASET CLASS\n# ============================================================================\nclass BiomassTestDataset(Dataset):\n    \"\"\"\n    Dataset for test/inference data.\n    Loads images and applies tabular feature scaling.\n    Note: Test data may not have metadata features (NDVI, Height).\n    \"\"\"\n    def __init__(self, df, img_dir, transform=None, tabular_scaler=None, has_metadata=True):\n        self.df = df.reset_index(drop=True)\n        self.img_dir = img_dir\n        self.transform = transform\n        self.has_metadata = has_metadata\n        \n        # Prepare tabular features\n        if has_metadata:\n            # Use actual metadata from CSV\n            tabular_data = df[['Pre_GSHH_NDVI', 'Height_Ave_cm']].fillna(0).values\n            \n            if tabular_scaler is not None:\n                self.tabular_features = tabular_scaler.transform(tabular_data)\n            else:\n                self.tabular_features = tabular_data\n        else:\n            # No metadata available - use zeros (scaled mean)\n            # This is reasonable since StandardScaler centers data around 0\n            print(\"  ⚠ No metadata in test set - using zero values (scaled mean)\")\n            self.tabular_features = np.zeros((len(df), 2), dtype=np.float32)\n    \n    def __len__(self):\n        return len(self.df)\n    \n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        \n        # Load image\n        img_path = f\"{self.img_dir}/{row['image_path'].split('/')[-1]}\"\n        image = cv2.imread(img_path)\n        \n        if image is None:\n            raise ValueError(f\"Failed to load image: {img_path}\")\n        \n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        \n        # Apply transforms\n        if self.transform:\n            augmented = self.transform(image=image)\n            image = augmented['image']\n        \n        # Get tabular features\n        tabular = torch.tensor(self.tabular_features[idx], dtype=torch.float32)\n        \n        return image, tabular\n\n# ============================================================================\n# AUGMENTATION TRANSFORMS\n# ============================================================================\ndef get_inference_transforms():\n    \"\"\"Standard transforms for inference (no augmentation)\"\"\"\n    return A.Compose([\n        A.Resize(CFG.img_size, CFG.img_size),\n        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ToTensorV2(),\n    ])\n\ndef get_tta_transforms():    \n    \"\"\"\n    Test-Time Augmentation transforms.\n    Returns list of diverse augmentation pipelines (10 steps).\n    \"\"\"\n    base = A.Compose([\n        A.Resize(CFG.img_size, CFG.img_size),\n        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ToTensorV2(),\n    ])\n    \n    return [\n        # 1. Base (No TTA)\n        base,\n        # 2. Horizontal flip\n        A.Compose([A.HorizontalFlip(p=1.0), base]),\n        # 3. Vertical flip\n        A.Compose([A.VerticalFlip(p=1.0), base]),\n        # 4. Rotate 90 degrees\n        A.Compose([A.Rotate(limit=(90, 90), p=1.0, border_mode=cv2.BORDER_REFLECT_101), base]),\n        # 5. Rotate 270 degrees\n        A.Compose([A.Rotate(limit=(-90, -90), p=1.0, border_mode=cv2.BORDER_REFLECT_101), base]),\n        \n        # 6. Slight rotation and shift\n        A.Compose([\n            A.ShiftScaleRotate(shift_limit=0.05, scale_limit=0, rotate_limit=10, p=1.0, border_mode=cv2.BORDER_REFLECT_101),\n            base\n        ]),\n        # 7. Brightness variation\n        A.Compose([\n            A.RandomBrightnessContrast(brightness_limit=0.15, contrast_limit=0.15, p=1.0),\n            base\n        ]),\n        # 8. Hue, Saturation, Value variation\n        A.Compose([\n            A.HueSaturationValue(hue_shift_limit=10, sat_shift_limit=15, val_shift_limit=15, p=1.0),\n            base\n        ]),\n        # 9. Mild Perspective Distortion\n        A.Compose([\n            A.Perspective(scale=(0.02, 0.05), p=1.0, keep_size=True, fit_output=True),\n            base\n        ]),\n        # 10. Gaussian Blur (for texture generalization)\n        A.Compose([\n            A.GaussianBlur(blur_limit=(3, 5), p=1.0),\n            base\n        ]),\n    ][:CFG.tta_steps] # Use slicing to respect CFG.tta_steps\n# ============================================================================\n# MODEL ARCHITECTURE\n# ============================================================================\nclass BiomassModel(nn.Module):\n    \"\"\"\n    Multi-modal model combining image and tabular features.\n    MUST match the architecture used during training!\n    \"\"\"\n    def __init__(self, model_name, pretrained=False):\n        super(BiomassModel, self).__init__()\n        \n        # Image encoder (EfficientNet backbone)\n        self.backbone = timm.create_model(\n            model_name, \n            pretrained=pretrained,\n            num_classes=0,\n            global_pool='avg'\n        )\n        \n        # Get feature dimension\n        with torch.no_grad():\n            dummy_input = torch.randn(1, 3, CFG.img_size, CFG.img_size)\n            img_features = self.backbone(dummy_input).shape[1]\n        \n        # Tabular feature encoder\n        self.tabular_encoder = nn.Sequential(\n            nn.Linear(2, 64),\n            nn.BatchNorm1d(64),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(64, 128),\n            nn.BatchNorm1d(128),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n        )\n        \n        # Fusion layer\n        fusion_dim = img_features + 128\n        self.fusion = nn.Sequential(\n            nn.Linear(fusion_dim, 512),\n            nn.BatchNorm1d(512),\n            nn.ReLU(),\n            nn.Dropout(0.4),\n            nn.Linear(512, 256),\n            nn.BatchNorm1d(256),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n        )\n        \n        # Output heads (5 targets)\n        self.head_green = nn.Linear(256, 1)\n        self.head_dead = nn.Linear(256, 1)\n        self.head_clover = nn.Linear(256, 1)\n        self.head_gdm = nn.Linear(256, 1)\n        self.head_total = nn.Linear(256, 1)\n    \n    def forward(self, image, tabular):\n        # Extract features\n        img_features = self.backbone(image)\n        tab_features = self.tabular_encoder(tabular)\n        \n        # Fuse\n        combined = torch.cat([img_features, tab_features], dim=1)\n        fused = self.fusion(combined)\n        \n        # Predict\n        out_green = self.head_green(fused)\n        out_dead = self.head_dead(fused)\n        out_clover = self.head_clover(fused)\n        out_gdm = self.head_gdm(fused)\n        out_total = self.head_total(fused)\n        \n        outputs = torch.cat([out_green, out_dead, out_clover, out_gdm, out_total], dim=1)\n        return outputs\n\n# ============================================================================\n# INFERENCE FUNCTIONS\n# ============================================================================\ndef predict_single_tta(model, dataset, device, tta_transform, has_metadata):\n    \"\"\"\n    Make predictions with a single TTA transform.\n    \"\"\"\n    # Create dataset with specific TTA transform\n    dataset_tta = BiomassTestDataset(\n        dataset.df, \n        dataset.img_dir,\n        transform=tta_transform,\n        tabular_scaler=None,  # Already scaled in original dataset\n        has_metadata=has_metadata\n    )\n    dataset_tta.tabular_features = dataset.tabular_features  # Use same tabular features\n    \n    loader = DataLoader(\n        dataset_tta, \n        batch_size=CFG.batch_size,\n        shuffle=False, \n        num_workers=CFG.num_workers,\n        pin_memory=True\n    )\n    \n    model.eval()\n    predictions = []\n    \n    with torch.no_grad():\n        for images, tabular in loader:\n            images = images.to(device)\n            tabular = tabular.to(device)\n            \n            outputs = model(images, tabular)\n            predictions.append(outputs.cpu().numpy())\n    \n    return np.vstack(predictions)\n\ndef predict_with_tta(model, dataset, device, has_metadata):\n    \"\"\"\n    Make predictions with Test-Time Augmentation.\n    Averages predictions across multiple augmentations.\n    \"\"\"\n    if not CFG.use_tta:\n        # No TTA - single prediction\n        tta_transforms = [get_inference_transforms()]\n    else:\n        # Multiple TTA transforms\n        tta_transforms = get_tta_transforms()[:CFG.tta_steps]\n    \n    print(f\"  Making predictions with {len(tta_transforms)} TTA variations...\")\n    \n    all_tta_preds = []\n    for tta_idx, tta_transform in enumerate(tta_transforms):\n        preds = predict_single_tta(model, dataset, device, tta_transform, has_metadata)\n        all_tta_preds.append(preds)\n        print(f\"    TTA {tta_idx + 1}/{len(tta_transforms)} complete\")\n    \n    # Average across TTA augmentations\n    avg_preds = np.mean(all_tta_preds, axis=0)\n    return avg_preds\n\ndef load_model_checkpoint(fold):\n    \"\"\"Load model checkpoint with all necessary components\"\"\"\n    checkpoint_path = os.path.join(CFG.model_dir, f'best_model_fold{fold}.pth')\n    \n    if not os.path.exists(checkpoint_path):\n        raise FileNotFoundError(f\"Model checkpoint not found: {checkpoint_path}\")\n    \n    # Load with weights_only=False to allow loading sklearn scalers\n    # This is safe since we trust our own checkpoints\n    checkpoint = torch.load(checkpoint_path, map_location=CFG.device, weights_only=False)\n    \n    # Handle different checkpoint formats\n    if isinstance(checkpoint, dict) and 'model_state_dict' in checkpoint:\n        # New format with scalers\n        model_state = checkpoint['model_state_dict']\n        tabular_scaler = checkpoint.get('tabular_scaler', None)\n        target_scaler = checkpoint.get('target_scaler', None)\n    else:\n        # Old format (just model weights)\n        model_state = checkpoint\n        tabular_scaler = None\n        target_scaler = None\n        print(f\"  Warning: Fold {fold} checkpoint doesn't contain scalers\")\n    \n    return model_state, tabular_scaler, target_scaler\n\n# ============================================================================\n# MAIN INFERENCE PIPELINE\n# ============================================================================\ndef main():\n    print(\"=\"*70)\n    print(\"CSIRO BIOMASS PREDICTION - INFERENCE\")\n    print(\"=\"*70)\n    \n    # 1. Load test data\n    print(\"\\n[1/5] Loading test data...\")\n    test_df = pd.read_csv(CFG.test_csv)\n    print(f\"✓ Loaded {len(test_df)} test samples\")\n    print(f\"✓ Columns in test data: {test_df.columns.tolist()}\")\n    \n    # Get unique images (test.csv has 5 rows per image, one per target)\n    test_df_unique = test_df.drop_duplicates(subset=['image_path']).reset_index(drop=True)\n    print(f\"✓ Found {len(test_df_unique)} unique images\")\n    \n    # Check if metadata features are available\n    has_metadata = 'Pre_GSHH_NDVI' in test_df_unique.columns and 'Height_Ave_cm' in test_df_unique.columns\n    \n    if has_metadata:\n        print(f\"✓ Test data has metadata features (NDVI, Height)\")\n    else:\n        print(f\"⚠ Test data does NOT have metadata features\")\n        print(f\"  Will use zero values (scaled mean) for tabular features\")\n        # Add dummy columns so dataset creation doesn't fail\n        test_df_unique['Pre_GSHH_NDVI'] = 0.0\n        test_df_unique['Height_Ave_cm'] = 0.0\n    \n    # 2. Verify model checkpoints exist\n    print(\"\\n[2/5] Checking model checkpoints...\")\n    FOLD_VALIDATION_SCORES = [0.6078, 0.6534, 0.7948, 0.7265, 0.7791]\n    available_folds = []\n    for fold in range(CFG.n_folds):\n        checkpoint_path = os.path.join(CFG.model_dir, f'best_model_fold{fold}.pth')\n        if os.path.exists(checkpoint_path):\n            available_folds.append(fold)\n            print(f\"✓ Found checkpoint for fold {fold}\")\n        else:\n            print(f\"✗ Missing checkpoint for fold {fold}\")\n    \n    if len(available_folds) == 0:\n        raise FileNotFoundError(\"No model checkpoints found! Train models first.\")\n    \n    print(f\"\\n✓ Will use {len(available_folds)} models for ensemble\")\n    \n    # 3. Generate predictions from each fold\n    print(\"\\n[3/5] Generating predictions...\")\n    all_fold_predictions = []\n    \n    for fold in available_folds:\n        print(f\"\\n--- Fold {fold + 1}/{CFG.n_folds} ---\")\n        \n        # Load checkpoint\n        model_state, tabular_scaler, target_scaler = load_model_checkpoint(fold)\n        \n        # Create model and load weights\n        model = BiomassModel(CFG.model_name, pretrained=False).to(CFG.device)\n        model.load_state_dict(model_state)\n        model.eval()\n        print(f\"✓ Model loaded successfully\")\n        \n        # Create dataset\n        test_dataset = BiomassTestDataset(\n            test_df_unique,\n            CFG.test_dir,\n            transform=get_inference_transforms(),\n            tabular_scaler=tabular_scaler,\n            has_metadata=has_metadata\n        )\n        \n        # Make predictions with TTA\n        fold_predictions = predict_with_tta(model, test_dataset, CFG.device, has_metadata)\n        \n        # Inverse transform if target scaler exists\n        if target_scaler is not None:\n            fold_predictions = target_scaler.inverse_transform(fold_predictions)\n            print(f\"✓ Predictions scaled back to original range\")\n        \n        all_fold_predictions.append(fold_predictions)\n        print(f\"✓ Fold {fold} predictions: shape {fold_predictions.shape}\")\n        \n        # Print sample predictions\n        print(f\"  Sample prediction: {fold_predictions[0]}\")\n        \n        # Clean up\n        del model\n        torch.cuda.empty_cache()\n    \n    # 4. Ensemble predictions\n    print(\"\\n[4/5] Ensembling predictions from all folds...\")\n    if len(available_folds) < 2 or all(score == 0.0 for score in FOLD_VALIDATION_SCORES):\n        print(\"⚠ Simple averaging due to insufficient folds or missing scores.\")\n        final_predictions = np.mean(all_fold_predictions, axis=0)\n    else:\n        # --- WEIGHTED ENSEMBLE LOGIC ---\n        print(f\"✓ Using Weighted Ensemble based on R² scores: {FOLD_VALIDATION_SCORES}\")\n        \n        # 1. Filter scores to only include available folds\n        available_scores = [FOLD_VALIDATION_SCORES[fold] for fold in available_folds]\n        \n        # 2. Normalize weights (must be non-negative)\n        weights = np.array([max(0.0, score) for score in available_scores])\n        weights = weights / weights.sum()\n        \n        print(f\"Normalized Weights: {weights}\")\n        \n        # 3. Apply weights to predictions\n        final_predictions = np.sum(\n            np.stack(all_fold_predictions, axis=0) * weights[:, np.newaxis, np.newaxis], \n            axis=0\n        )\n\n    \n    # 5. Create submission file\n    print(\"\\n[5/5] Creating submission file...\")\n    submission_rows = []\n    \n    for idx, row in test_df_unique.iterrows():\n        # Extract image ID from path\n        image_id = row['image_path'].split('/')[-1].replace('.jpg', '')\n        \n        # Create one row per target\n        for target_idx, target_name in enumerate(CFG.targets):\n            sample_id = f\"{image_id}__{target_name}\"\n            \n            # Get prediction and ensure non-negative\n            prediction = max(0.0, final_predictions[idx, target_idx])\n            \n            submission_rows.append({\n                'sample_id': sample_id,\n                'target': prediction\n            })\n    \n    # Create DataFrame and save\n    submission_df = pd.DataFrame(submission_rows)\n    submission_df.to_csv(CFG.output_file, index=False)\n    \n    print(f\"\\n{'='*70}\")\n    print(f\"✓ INFERENCE COMPLETE!\")\n    print(f\"{'='*70}\")\n    print(f\"✓ Submission file saved: {CFG.output_file}\")\n    print(f\"✓ Total predictions: {len(submission_df)}\")\n    print(f\"✓ Expected format: sample_id, target\")\n    print(f\"\\nFirst few rows:\")\n    print(submission_df.head(10))\n    print(f\"\\nLast few rows:\")\n    print(submission_df.tail(5))\n    \n    # Validation checks\n    print(f\"\\n--- Submission Validation ---\")\n    expected_rows = len(test_df_unique) * 5  # 5 targets per image\n    if len(submission_df) == expected_rows:\n        print(f\"✓ Row count correct: {len(submission_df)} rows\")\n    else:\n        print(f\"⚠ Warning: Expected {expected_rows} rows, got {len(submission_df)}\")\n    \n    # Check for any NaN or negative values\n    if submission_df['target'].isna().sum() > 0:\n        print(f\"⚠ Warning: {submission_df['target'].isna().sum()} NaN values found\")\n    else:\n        print(f\"✓ No NaN values\")\n    \n    if (submission_df['target'] < 0).sum() > 0:\n        print(f\"⚠ Warning: {(submission_df['target'] < 0).sum()} negative values found\")\n    else:\n        print(f\"✓ No negative values\")\n    \n    print(f\"\\n✓ Ready for submission!\")\n\nif __name__ == '__main__':\n    main()\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-01T09:19:57.581915Z","iopub.execute_input":"2025-11-01T09:19:57.582665Z","iopub.status.idle":"2025-11-01T09:21:03.074352Z","shell.execute_reply.started":"2025-11-01T09:19:57.582635Z","shell.execute_reply":"2025-11-01T09:21:03.072930Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sub = pd.read_csv('/kaggle/working/submission.csv')\nsub.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-01T09:21:17.943147Z","iopub.execute_input":"2025-11-01T09:21:17.943510Z","iopub.status.idle":"2025-11-01T09:21:17.967018Z","shell.execute_reply.started":"2025-11-01T09:21:17.943474Z","shell.execute_reply":"2025-11-01T09:21:17.966047Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}