{"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":13694216,"sourceType":"datasetVersion","datasetId":8710363}],"dockerImageVersionId":31193,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## Data Used:\n* **Input Images**: 2000x1000px pasture images (split into left/right 1000x1000)\n* **Metadata**: NDVI, Height, Species, State (optional)\n* **Prediction Targets**: 5 biomass components (Dry_Total_g, GDM_g, Dry_Green_g, Dry_Dead_g, Dry_Clover_g)\n* **Evaluation Metric**: Weighted R² score\n\n## Preprocessing:\n* **Image Splitting**: Left/right split for Two-Stream architecture\n* **TTA**: 3 patterns - Original, Horizontal Flip, Vertical Flip\n* **Normalization**: ImageNet standard normalization\n* **Resizing**: Model-specific sizes (224/384/448px)\n\n## Model Definition:\n* **Architecture**: Two-Stream + Meta MLP + Three-Head\n* **Backbone Selection**: Factory Pattern with 5 model choices\n* **Output**: 3 independent predictions (Total, GDM, Green) → Reconstruct 5 components\n\n## Training Configuration:\n* **Inference Settings**: 5-Fold Ensemble, batch size 8\n* **TTA**: 3 Views × 5 Folds = 15 predictions averaged\n* **Post-processing**: Apply physical constraints (Total = Green + Dead + Clover)\n\n## Others:\n* **Device Management**: Automatic GPU/CPU selection\n* **Memory Management**: Explicit garbage collection after inference\n* **Logging**: Progress bars and detailed information display\n\n## Reference Notebook\n* **EDA Notebook**: https://www.kaggle.com/code/takahitomizunobyts/csiro-customizable-eda\n* **Training Notebook**: Cominng soon...","metadata":{}},{"cell_type":"code","source":"\"\"\"\nCSIRO Biomass Competition - Unified Inference Pipeline\n================================================================================\nMulti-Backbone Support (ConvNeXt/EfficientNetV2/MaxViT/CoAtNet/EVA-02)\nMeta Features ON/OFF Support\n\nArchitecture:\n    Input → Preprocessing (Split+TTA) → Model (Two-Stream+Meta+3Head)\n         → Inference (TTA×Fold Ensemble) → Postprocessing (5-target reconstruction) → Output\n\nData Flow:\n    1. Load test.csv → Extract unique images\n    2. Load 5-Fold models (Switch backbone via Factory)\n    3. TTA inference (3 Views × 5 Folds)\n    4. Postprocessing (Calculate Clover/Dead, Apply constraints)\n    5. Generate submission.csv\n================================================================================\n\"\"\"\n\nfrom __future__ import annotations\nfrom dataclasses import dataclass, field\nfrom pathlib import Path\nfrom enum import Enum\nfrom typing import Optional, Dict, List, Tuple\nfrom collections import OrderedDict\nimport gc\nimport logging\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\n\n# Configure logging\nlogging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')\nlogger = logging.getLogger(__name__)\n\n\n# ============================================================================\n# Enums and Types\n# ============================================================================\n\nclass ModelType(str, Enum):\n    \"\"\"Supported backbone models with their default image sizes\"\"\"\n    EVA02 = \"eva02_base_patch14_448.mim_in22k_ft_in1k\"\n    \n    @property\n    def default_img_size(self) -> int:\n        \"\"\"Get recommended image size\"\"\"\n        size_map = {\n            self.EVA02: 448\n        }\n        return size_map[self]\n\n\n# ============================================================================\n# Configuration\n# ============================================================================\n\n@dataclass\nclass InferenceConfig:\n    \"\"\"Unified configuration for inference pipeline\"\"\"\n    \n    # Paths\n    base_path: Path = Path('/kaggle/input/csiro-biomass')\n    test_csv: Path = field(init=False)\n    test_image_dir: Path = field(init=False)\n    model_dir: Path = Path('/kaggle/input/eva02-exp1/EVA02_exp1')\n    submission_file: str = 'submission.csv'\n    \n    # Model selection\n    model_type: ModelType = ModelType.EVA02\n    img_size: Optional[int] = None  # Auto-set from model_type if None\n    \n    # Model architecture\n    dropout_rate: float = 0.3\n    use_meta_features: bool = False  # Toggle meta features\n    meta_input_dim: int = 10\n    meta_hidden_dim: int = 128\n    meta_dropout_rate: float = 0.2\n    \n    # Inference settings\n    batch_size: int = 8\n    num_workers: int = 2\n    n_folds: int = 5\n    enable_tta: bool = True  # Enable Test Time Augmentation\n    \n    # Device\n    device: torch.device = field(default_factory=lambda: torch.device(\n        'cuda' if torch.cuda.is_available() else 'cpu'\n    ))\n    \n    # Target columns\n    predicted_cols: List[str] = field(default_factory=lambda: [\n        'Dry_Total_g', 'GDM_g', 'Dry_Green_g'\n    ])\n    all_target_cols: List[str] = field(default_factory=lambda: [\n        'Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g'\n    ])\n    \n    def __post_init__(self) -> None:\n        \"\"\"Post-initialization setup\"\"\"\n        self.test_csv = self.base_path / 'test.csv'\n        self.test_image_dir = self.base_path / 'test'\n        \n        # Set default image size if not specified\n        if self.img_size is None:\n            self.img_size = self.model_type.default_img_size\n    \n    def display_info(self) -> None:\n        \"\"\"Display configuration information\"\"\"\n        print(f\"{'='*70}\")\n        print(f\"Inference Pipeline Configuration\")\n        print(f\"{'='*70}\")\n        print(f\"Device: {self.device}\")\n        print(f\"Backbone: {self.model_type.value}\")\n        print(f\"Image Size: {self.img_size}x{self.img_size}\")\n        print(f\"Batch Size: {self.batch_size}\")\n        print(f\"Number of Folds: {self.n_folds}\")\n        print(f\"TTA: {'Enabled (3 Views)' if self.enable_tta else 'Disabled'}\")\n        print(f\"Meta Features: {'Enabled' if self.use_meta_features else 'Disabled (Zero-filled)'}\")\n        if self.use_meta_features:\n            print(f\"  - Input Dimension: {self.meta_input_dim}\")\n            print(f\"  - Hidden Dimension: {self.meta_hidden_dim}\")\n        print(f\"{'='*70}\\n\")\n\n\n# ============================================================================\n# Data Processing Components\n# ============================================================================\n\nclass TTATransformFactory:\n    \"\"\"Factory for creating Test Time Augmentation transforms\"\"\"\n    \n    def __init__(self, img_size: int):\n        \"\"\"\n        Initialize with target image size\n        \n        Args:\n            img_size: Target size for resizing\n        \"\"\"\n        self.img_size = img_size\n        self.base_transforms = [\n            A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n            ToTensorV2()\n        ]\n    \n    def get_tta_transforms(self) -> List[A.Compose]:\n        \"\"\"\n        Create TTA transforms\n        \n        Returns:\n            List of transforms: [Original, HFlip, VFlip]\n        \"\"\"\n        transforms = []\n        \n        # Original\n        transforms.append(A.Compose([\n            A.Resize(self.img_size, self.img_size),\n            *self.base_transforms\n        ]))\n        \n        # Horizontal Flip\n        transforms.append(A.Compose([\n            A.HorizontalFlip(p=1.0),\n            A.Resize(self.img_size, self.img_size),\n            *self.base_transforms\n        ]))\n        \n        # Vertical Flip  \n        transforms.append(A.Compose([\n            A.VerticalFlip(p=1.0),\n            A.Resize(self.img_size, self.img_size),\n            *self.base_transforms\n        ]))\n        \n        return transforms\n    \n    def get_single_transform(self) -> A.Compose:\n        \"\"\"Get single transform when TTA is disabled\"\"\"\n        return self.get_tta_transforms()[0]\n\n\nclass TestBiomassDataset(Dataset):\n    \"\"\"\n    Test dataset with Two-Stream and Meta feature support\n    \n    Splits images into left/right halves for Two-Stream input\n    Meta features are zero-filled when disabled\n    \"\"\"\n    \n    def __init__(\n        self,\n        df: pd.DataFrame,\n        transform: A.Compose,\n        image_dir: Path,\n        use_meta_features: bool,\n        meta_dim: int\n    ):\n        \"\"\"\n        Initialize dataset\n        \n        Args:\n            df: Test dataframe (unique images only)\n            transform: Albumentations transform\n            image_dir: Image directory path\n            use_meta_features: Whether to use meta features\n            meta_dim: Meta feature dimension\n        \"\"\"\n        self.df = df\n        self.transform = transform\n        self.image_dir = image_dir\n        self.use_meta_features = use_meta_features\n        self.meta_dim = meta_dim\n        self.image_paths = df['image_path'].values\n    \n    def __len__(self) -> int:\n        return len(self.df)\n    \n    def __getitem__(self, idx: int) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:\n        \"\"\"\n        Get single sample\n        \n        Returns:\n            Tuple of (img_left, img_right, meta_features)\n        \"\"\"\n        # Load image\n        img_path = self.image_paths[idx]\n        full_path = self.image_dir / Path(img_path).name\n        \n        image = cv2.imread(str(full_path))\n        if image is None:\n            logger.warning(f\"Failed to load image: {full_path}\")\n            image = np.zeros((1000, 2000, 3), dtype=np.uint8)\n        \n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        \n        # Split into left/right (2000x1000 → 1000x1000 × 2)\n        height, width = image.shape[:2]\n        mid = width // 2\n        img_left = self.transform(image=image[:, :mid])['image']\n        img_right = self.transform(image=image[:, mid:])['image']\n        \n        # Generate meta features\n        if self.use_meta_features:\n            # TODO: Extract actual meta features from dataframe\n            meta = torch.zeros(self.meta_dim, dtype=torch.float32)\n        else:\n            meta = torch.zeros(self.meta_dim, dtype=torch.float32)\n        \n        return img_left, img_right, meta\n\n\n# ============================================================================\n# Model Components\n# ============================================================================\n\nclass MetaFeatureMLP(nn.Module):\n    \"\"\"MLP for processing meta features\"\"\"\n    \n    def __init__(self, input_dim: int, hidden_dim: int, dropout: float = 0.2):\n        \"\"\"\n        Initialize MLP\n        \n        Args:\n            input_dim: Input feature dimension\n            hidden_dim: Hidden layer dimension\n            dropout: Dropout rate\n        \"\"\"\n        super().__init__()\n        self.mlp = nn.Sequential(\n            nn.Linear(input_dim, hidden_dim * 2),\n            nn.ReLU(inplace=True),\n            nn.Dropout(dropout),\n            nn.Linear(hidden_dim * 2, hidden_dim),\n            nn.ReLU(inplace=True)\n        )\n    \n    def forward(self, x: torch.Tensor) -> torch.Tensor:\n        \"\"\"Forward pass\"\"\"\n        return self.mlp(x)\n\n\nclass ModelBackboneFactory:\n    \"\"\"Factory for creating model backbones\"\"\"\n    \n    @staticmethod\n    def create_backbone(model_type: ModelType) -> nn.Module:\n        \"\"\"\n        Create backbone from ModelType\n        \n        Args:\n            model_type: Type of model to create\n            \n        Returns:\n            Timm backbone model\n        \"\"\"\n        backbone = timm.create_model(\n            model_type.value,\n            pretrained=False,  # Weights loaded later\n            num_classes=0,     # Feature extraction only\n            global_pool='avg'  # Global Average Pooling\n        )\n        return backbone\n\n\nclass HybridBiomassModel(nn.Module):\n    \"\"\"\n    Two-Stream + Meta Fusion + Three-Head Model\n    \n    Architecture:\n        1. Shared Backbone: Extract features from left/right images\n        2. Meta MLP: Process meta features (skip if disabled)\n        3. Fusion: Concatenate [feat_left, feat_right, meta_feat]\n        4. Three Heads: Predict Total, GDM, Green independently\n    \"\"\"\n    \n    def __init__(self, config: InferenceConfig):\n        \"\"\"Initialize model with configuration\"\"\"\n        super().__init__()\n        \n        self.use_meta_features = config.use_meta_features\n        \n        # Shared backbone\n        self.backbone = ModelBackboneFactory.create_backbone(config.model_type)\n        n_features = self.backbone.num_features\n        \n        # Meta MLP (only when meta features enabled)\n        if self.use_meta_features:\n            self.meta_mlp = MetaFeatureMLP(\n                config.meta_input_dim,\n                config.meta_hidden_dim,\n                config.meta_dropout_rate\n            )\n            n_fused = n_features * 2 + config.meta_hidden_dim\n        else:\n            self.meta_mlp = None\n            n_fused = n_features * 2\n        \n        # Three prediction heads\n        self.head_total = self._create_head(n_fused, config.dropout_rate)\n        self.head_gdm = self._create_head(n_fused, config.dropout_rate)\n        self.head_green = self._create_head(n_fused, config.dropout_rate)\n    \n    def _create_head(self, in_features: int, dropout: float) -> nn.Sequential:\n        \"\"\"Create prediction head\"\"\"\n        return nn.Sequential(\n            nn.Linear(in_features, in_features // 2),\n            nn.ReLU(inplace=True),\n            nn.Dropout(dropout),\n            nn.Linear(in_features // 2, 1)\n        )\n    \n    def forward(\n        self,\n        img_left: torch.Tensor,\n        img_right: torch.Tensor,\n        meta: torch.Tensor\n    ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:\n        \"\"\"\n        Forward pass\n        \n        Returns:\n            Tuple of (pred_total, pred_gdm, pred_green)\n        \"\"\"\n        # Extract image features\n        feat_l = self.backbone(img_left)\n        feat_r = self.backbone(img_right)\n        img_feat = torch.cat([feat_l, feat_r], dim=1)\n        \n        # Fusion\n        if self.use_meta_features and self.meta_mlp is not None:\n            meta_feat = self.meta_mlp(meta)\n            fused = torch.cat([img_feat, meta_feat], dim=1)\n        else:\n            fused = img_feat\n        \n        # Generate predictions\n        pred_total = self.head_total(fused)\n        pred_gdm = self.head_gdm(fused)\n        pred_green = self.head_green(fused)\n        \n        return pred_total, pred_gdm, pred_green\n\n\n# ============================================================================\n# Model Management\n# ============================================================================\n\nclass ModelLoader:\n    \"\"\"Handles loading of trained models\"\"\"\n    \n    def __init__(self, config: InferenceConfig):\n        \"\"\"Initialize with configuration\"\"\"\n        self.config = config\n    \n    def load_all_folds(self) -> List[nn.Module]:\n        \"\"\"\n        Load all fold models\n        \n        Returns:\n            List of loaded models\n        \"\"\"\n        logger.info(f\"Loading {self.config.n_folds}-Fold trained models...\")\n        \n        models = []\n        \n        for fold in range(self.config.n_folds):\n            model_path = self.config.model_dir / f'model_fold{fold}.pth'\n            \n            if not model_path.exists():\n                raise FileNotFoundError(f\"Model file not found: {model_path}\")\n            \n            # Initialize model\n            model = HybridBiomassModel(self.config)\n            \n            # Load weights\n            state_dict = torch.load(model_path, map_location=self.config.device)\n            state_dict = self._remove_module_prefix(state_dict)\n            \n            model.load_state_dict(state_dict)\n            model.eval()\n            model.to(self.config.device)\n            \n            models.append(model)\n            logger.info(f\"  ✓ Fold {fold} loaded\")\n        \n        logger.info(f\"✓ All {len(models)} models loaded successfully\\n\")\n        return models\n    \n    @staticmethod\n    def _remove_module_prefix(state_dict: Dict) -> Dict:\n        \"\"\"Remove 'module.' prefix from DataParallel models\"\"\"\n        if not any(k.startswith('module.') for k in state_dict.keys()):\n            return state_dict\n        \n        new_state_dict = OrderedDict()\n        for key, value in state_dict.items():\n            new_key = key.replace('module.', '')\n            new_state_dict[new_key] = value\n        \n        return new_state_dict\n\n\n# ============================================================================\n# Inference Engine\n# ============================================================================\n\nclass InferenceEngine:\n    \"\"\"Handles inference with TTA and Fold Ensemble\"\"\"\n    \n    def __init__(self, models: List[nn.Module], config: InferenceConfig):\n        \"\"\"Initialize with models and configuration\"\"\"\n        self.models = models\n        self.config = config\n    \n    def predict_with_tta(\n        self,\n        test_df: pd.DataFrame,\n        transforms: List[A.Compose]\n    ) -> Dict[str, np.ndarray]:\n        \"\"\"\n        Perform TTA and Fold Ensemble inference\n        \n        Args:\n            test_df: Test dataframe (unique images)\n            transforms: List of TTA transforms\n            \n        Returns:\n            Dictionary with predictions for each target\n        \"\"\"\n        n_views = len(transforms)\n        logger.info(f\"Starting inference: {n_views} Views × {self.config.n_folds} Folds\")\n        \n        all_view_preds: List[Dict[str, np.ndarray]] = []\n        \n        for view_idx, transform in enumerate(transforms):\n            logger.info(f\"Processing View {view_idx + 1}/{n_views}\")\n            \n            # Create dataset and dataloader\n            dataset = TestBiomassDataset(\n                test_df,\n                transform,\n                self.config.test_image_dir,\n                self.config.use_meta_features,\n                self.config.meta_input_dim\n            )\n            \n            loader = DataLoader(\n                dataset,\n                batch_size=self.config.batch_size,\n                shuffle=False,\n                num_workers=self.config.num_workers,\n                pin_memory=True\n            )\n            \n            # Predict for single view\n            view_preds = self._predict_single_view(loader)\n            all_view_preds.append(view_preds)\n            \n            logger.info(f\"✓ View {view_idx + 1} completed\")\n        \n        # Average across TTA views\n        logger.info(f\"Averaging predictions across {n_views} views...\")\n        final_preds = {\n            'total': np.mean([p['total'] for p in all_view_preds], axis=0),\n            'gdm': np.mean([p['gdm'] for p in all_view_preds], axis=0),\n            'green': np.mean([p['green'] for p in all_view_preds], axis=0)\n        }\n        \n        logger.info(\"✓ Inference completed\\n\")\n        return final_preds\n    \n    def _predict_single_view(self, loader: DataLoader) -> Dict[str, np.ndarray]:\n        \"\"\"\n        Predict for single TTA view with fold averaging\n        \n        Args:\n            loader: DataLoader for single view\n            \n        Returns:\n            Dictionary with predictions\n        \"\"\"\n        view_preds = {'total': [], 'gdm': [], 'green': []}\n        \n        with torch.no_grad():\n            for img_left, img_right, meta in tqdm(loader, desc=\"  Inference\", leave=False):\n                # Move to device\n                img_left = img_left.to(self.config.device)\n                img_right = img_right.to(self.config.device)\n                meta = meta.to(self.config.device)\n                \n                # Collect predictions from all folds\n                fold_preds = {'total': [], 'gdm': [], 'green': []}\n                \n                for model in self.models:\n                    pred_total, pred_gdm, pred_green = model(img_left, img_right, meta)\n                    fold_preds['total'].append(pred_total.cpu())\n                    fold_preds['gdm'].append(pred_gdm.cpu())\n                    fold_preds['green'].append(pred_green.cpu())\n                \n                # Average across folds\n                avg_total = torch.mean(torch.stack(fold_preds['total']), dim=0)\n                avg_gdm = torch.mean(torch.stack(fold_preds['gdm']), dim=0)\n                avg_green = torch.mean(torch.stack(fold_preds['green']), dim=0)\n                \n                view_preds['total'].append(avg_total.numpy())\n                view_preds['gdm'].append(avg_gdm.numpy())\n                view_preds['green'].append(avg_green.numpy())\n        \n        # Concatenate and flatten\n        return {k: np.concatenate(v).flatten() for k, v in view_preds.items()}\n\n\n# ============================================================================\n# Postprocessing\n# ============================================================================\n\nclass PostProcessor:\n    \"\"\"Handles postprocessing of predictions\"\"\"\n    \n    @staticmethod\n    def reconstruct_targets(\n        pred_total: np.ndarray,\n        pred_gdm: np.ndarray,\n        pred_green: np.ndarray\n    ) -> Dict[str, np.ndarray]:\n        \"\"\"\n        Reconstruct 5 targets from 3 predictions\n        \n        Physical constraints:\n            - Clover = GDM - Green\n            - Dead = Total - GDM\n            - All predictions >= 0\n            \n        Args:\n            pred_total: Total biomass predictions\n            pred_gdm: GDM predictions\n            pred_green: Green biomass predictions\n            \n        Returns:\n            Dictionary with all 5 targets\n        \"\"\"\n        # Calculate derived targets\n        pred_clover = np.maximum(0, pred_gdm - pred_green)\n        pred_dead = np.maximum(0, pred_total - pred_gdm)\n        \n        # Apply non-negativity constraint\n        return {\n            'Dry_Green_g': np.maximum(0, pred_green),\n            'Dry_Dead_g': pred_dead,\n            'Dry_Clover_g': pred_clover,\n            'GDM_g': np.maximum(0, pred_gdm),\n            'Dry_Total_g': np.maximum(0, pred_total)\n        }\n\n\nclass SubmissionCreator:\n    \"\"\"Creates submission CSV file\"\"\"\n    \n    def __init__(self, config: InferenceConfig):\n        \"\"\"Initialize with configuration\"\"\"\n        self.config = config\n        self.postprocessor = PostProcessor()\n    \n    def create_submission(\n        self,\n        predictions: Dict[str, np.ndarray],\n        test_df_long: pd.DataFrame,\n        test_df_unique: pd.DataFrame\n    ) -> None:\n        \"\"\"\n        Create submission CSV from predictions\n        \n        Args:\n            predictions: Dictionary with predictions\n            test_df_long: Long format test dataframe\n            test_df_unique: Unique images dataframe\n        \"\"\"\n        logger.info(\"Creating submission CSV...\")\n        \n        # Reconstruct all 5 targets\n        all_preds = self.postprocessor.reconstruct_targets(\n            predictions['total'],\n            predictions['gdm'],\n            predictions['green']\n        )\n        \n        # Create wide format dataframe\n        preds_wide = pd.DataFrame({\n            'image_path': test_df_unique['image_path'],\n            **all_preds\n        })\n        \n        # Convert to long format\n        preds_long = preds_wide.melt(\n            id_vars=['image_path'],\n            value_vars=self.config.all_target_cols,\n            var_name='target_name',\n            value_name='target'\n        )\n        \n        # Merge with sample_id\n        submission = pd.merge(\n            test_df_long[['sample_id', 'image_path', 'target_name']],\n            preds_long,\n            on=['image_path', 'target_name'],\n            how='left'\n        )\n        \n        # Save submission\n        submission = submission[['sample_id', 'target']]\n        submission.to_csv(self.config.submission_file, index=False)\n        \n        logger.info(f\"✅ Submission saved: {self.config.submission_file}\")\n        print(\"\\n--- First 5 rows ---\")\n        print(submission.head())\n        print(\"\\n--- Statistics ---\")\n        print(submission['target'].describe())\n\n\n# ============================================================================\n# Pipeline Orchestrator\n# ============================================================================\n\nclass InferencePipeline:\n    \"\"\"Orchestrates the entire inference pipeline\"\"\"\n    \n    def __init__(self, config: InferenceConfig):\n        \"\"\"Initialize pipeline with configuration\"\"\"\n        self.config = config\n        self.model_loader = ModelLoader(config)\n        self.transform_factory = TTATransformFactory(config.img_size)\n        self.submission_creator = SubmissionCreator(config)\n    \n    def run(self) -> None:\n        \"\"\"Execute the complete pipeline\"\"\"\n        print(f\"\\n{'='*70}\")\n        print(f\"🚀 Starting Inference Pipeline\")\n        print(f\"{'='*70}\")\n        \n        try:\n            # 1. Load test data\n            test_df_long, test_df_unique = self._load_test_data()\n            \n            # 2. Load models\n            models = self.model_loader.load_all_folds()\n            \n            # 3. Prepare transforms\n            if self.config.enable_tta:\n                transforms = self.transform_factory.get_tta_transforms()\n            else:\n                transforms = [self.transform_factory.get_single_transform()]\n            \n            # 4. Run inference\n            engine = InferenceEngine(models, self.config)\n            predictions = engine.predict_with_tta(test_df_unique, transforms)\n            \n            # 5. Create submission\n            self.submission_creator.create_submission(\n                predictions,\n                test_df_long,\n                test_df_unique\n            )\n            \n            print(\"\\n✨ Pipeline completed successfully ✨\")\n            \n        except Exception as e:\n            logger.error(f\"Pipeline failed: {e}\")\n            raise\n        \n        finally:\n            # Clean up memory\n            gc.collect()\n            if torch.cuda.is_available():\n                torch.cuda.empty_cache()\n    \n    def _load_test_data(self) -> Tuple[pd.DataFrame, pd.DataFrame]:\n        \"\"\"\n        Load test data\n        \n        Returns:\n            Tuple of (test_df_long, test_df_unique)\n        \"\"\"\n        logger.info(f\"Loading test data: {self.config.test_csv}\")\n        \n        if not self.config.test_csv.exists():\n            raise FileNotFoundError(f\"Test CSV not found: {self.config.test_csv}\")\n        \n        test_df_long = pd.read_csv(self.config.test_csv)\n        test_df_unique = (\n            test_df_long\n            .drop_duplicates(subset=['image_path'])\n            .reset_index(drop=True)\n        )\n        \n        logger.info(f\"  Long format: {len(test_df_long):,} rows\")\n        logger.info(f\"  Unique images: {len(test_df_unique):,} images\\n\")\n        \n        return test_df_long, test_df_unique\n\n\n# ============================================================================\n# Main Entry Point\n# ============================================================================\n\ndef main():\n    \"\"\"Main entry point\"\"\"\n    \n    # Initialize configuration\n    config = InferenceConfig(\n        model_type=ModelType.EVA02,      # Model selection\n        use_meta_features=False,          # Meta features OFF\n        enable_tta=True,                  # TTA enabled\n        batch_size=8,\n        n_folds=5\n    )\n    \n    # Display configuration\n    config.display_info()\n    \n    # Execute pipeline\n    pipeline = InferencePipeline(config)\n    pipeline.run()\n    \n    print(f\"\\n{'='*70}\")\n    print(\"🎊 All processing completed!\")\n    print(f\"{'='*70}\\n\")\n\n\nif __name__ == '__main__':\n    main()","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-11T12:58:29.875035Z","iopub.execute_input":"2025-11-11T12:58:29.875334Z","iopub.status.idle":"2025-11-11T12:58:51.860769Z","shell.execute_reply.started":"2025-11-11T12:58:29.875312Z","shell.execute_reply":"2025-11-11T12:58:51.859806Z"}},"outputs":[],"execution_count":null}]}