{"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,"sourceType":"competition"}],"dockerImageVersionId":31193,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport cv2\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader, random_split, SubsetRandomSampler\nimport pandas as pd\nimport numpy as np\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nfrom tqdm import tqdm\nfrom sklearn.preprocessing import LabelEncoder\nimport random\nimport matplotlib.pyplot as plt\n\n# --- SEEDING ---\ndef seed_everything(seed=42):\n    random.seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n\nseed_everything()\n\n# --- CONFIGURATION ---\nclass Config:\n    PROJECT_NAME = \"CSIRO_Biomass_Prediction\"\n    if os.path.exists('/kaggle/input'):\n        ROOT_DIR = '/kaggle/input/csiro-biomass'\n        OUTPUT_DIR = '/kaggle/working'\n    else:\n        ROOT_DIR = r\"d:\\kaggle_compitition\\CSIRO\\csiro-biomass\"\n        OUTPUT_DIR = r\"d:\\kaggle_compitition\\CSIRO\"\n    \n    TRAIN_CSV = os.path.join(ROOT_DIR, \"train.csv\")\n    TEST_CSV = os.path.join(ROOT_DIR, \"test.csv\")\n    SUBMISSION_FILE = os.path.join(OUTPUT_DIR, \"submission.csv\")\n    MODEL_PATH = os.path.join(OUTPUT_DIR, \"best_model_custom.pth\")\n    \n    BATCH_SIZE = 16 \n    LR = 1e-4 # Start safe\n    EPOCHS = 20 # More epochs for scratch training\n    \n    DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    print(f\"Running on Device: {DEVICE}\")\n    \n    TARGETS = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n    NUM_SPECIES = 15\n    MAX_HEIGHT = 200.0\n\nconfig = Config()\n\n# --- DATASET ---\nclass CSIRODataset(Dataset):\n    def __init__(self, csv_file, root_dir, transform=None, is_test=False, label_encoder=None):\n        self.root_dir = root_dir\n        self.transform = transform\n        self.is_test = is_test\n        self.targets_list = config.TARGETS\n        \n        if not os.path.exists(csv_file):\n            print(f\"WARNING: File {csv_file} not found!\")\n            self.df_grouped = pd.DataFrame()\n            return\n\n        df = pd.read_csv(csv_file)\n        \n        if is_test:\n            if 'image_path' in df.columns:\n                self.df_grouped = df.groupby('image_path').first().reset_index()\n            else:\n                self.df_grouped = df\n        else:\n            # Train csv processing\n            meta_cols = ['Sampling_Date', 'State', 'Species', 'Pre_GSHH_NDVI', 'Height_Ave_cm']\n            available_meta = [c for c in meta_cols if c in df.columns]\n            \n            if 'target_name' in df.columns and 'target' in df.columns:\n                self.df_grouped = df.pivot_table(\n                    index=['image_path'] + available_meta,\n                    columns='target_name',\n                    values='target'\n                ).reset_index()\n            else:\n                self.df_grouped = df\n\n            # Encode Species\n            self.le = label_encoder\n            if self.le is None and 'Species' in self.df_grouped.columns:\n                self.le = LabelEncoder()\n                self.df_grouped['species_idx'] = self.le.fit_transform(self.df_grouped['Species'].astype(str))\n            elif self.le is not None and 'Species' in self.df_grouped.columns:\n                known_classes = set(self.le.classes_)\n                self.df_grouped['species_idx'] = self.df_grouped['Species'].apply(lambda x: self.le.transform([str(x)])[0] if str(x) in known_classes else 0)\n\n    def __len__(self):\n        return len(self.df_grouped)\n\n    def __getitem__(self, idx):\n        row = self.df_grouped.iloc[idx]\n        image_path = os.path.join(self.root_dir, row['image_path'])\n        \n        # Read image\n        image = None\n        if os.path.exists(image_path):\n            image = cv2.imread(image_path)\n            if image is not None:\n                image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        \n        if image is None:\n            image = np.zeros((256, 256, 3), dtype=np.uint8)\n\n        if self.transform:\n            augmented = self.transform(image=image)\n            image = augmented['image']\n        else:\n            tr = A.Compose([A.Resize(256, 256), A.Normalize(), ToTensorV2()])\n            image = tr(image=image)['image']\n            \n        # Targets\n        targets_dict = {}\n        \n        if not self.is_test:\n            # Main Targets: Log1p Scaling\n            try:\n                raw_biomass = row[self.targets_list].values.astype(np.float32)\n                log_biomass = np.log1p(raw_biomass) \n                targets_dict['biomass'] = torch.tensor(log_biomass)\n            except:\n                targets_dict['biomass'] = torch.zeros(len(self.targets_list))\n            \n            # Aux Targets\n            h_meters = float(row['Height_Ave_cm']) / config.MAX_HEIGHT \n            targets_dict['height'] = torch.tensor([h_meters], dtype=torch.float32)\n            targets_dict['ndvi'] = torch.tensor([float(row['Pre_GSHH_NDVI'])], dtype=torch.float32)\n            targets_dict['species'] = torch.tensor(int(row['species_idx']), dtype=torch.long)\n            \n        else:\n             targets_dict['biomass'] = torch.zeros(len(self.targets_list))\n             targets_dict['height'] = torch.tensor([0.0], dtype=torch.float32)\n             targets_dict['ndvi'] = torch.tensor([0.0], dtype=torch.float32)\n             targets_dict['species'] = torch.tensor(0, dtype=torch.long)\n\n        return image, targets_dict, row['image_path']\n\n# --- CUSTOM MODEL ---\nclass CustomBiomassCNN(nn.Module):\n    def __init__(self, num_targets=5, num_species=15):\n        super().__init__()\n        \n        def conv_block(in_c, out_c, pool=True):\n            layers = [\n                nn.Conv2d(in_c, out_c, kernel_size=3, padding=1, bias=False),\n                nn.BatchNorm2d(out_c),\n                nn.ReLU(inplace=True)\n            ]\n            if pool:\n                layers.append(nn.MaxPool2d(2))\n            return nn.Sequential(*layers)\n        \n        # Input: 256x256x3\n        self.enc1 = conv_block(3, 32)    # -> 128x128\n        self.enc2 = conv_block(32, 64)   # -> 64x64\n        self.enc3 = conv_block(64, 128)  # -> 32x32\n        self.enc4 = conv_block(128, 256) # -> 16x16\n        self.enc5 = conv_block(256, 512) # -> 8x8\n        \n        self.pool = nn.AdaptiveAvgPool2d((1, 1))\n        self.dropout = nn.Dropout(0.3)\n        \n        self.feature_dim = 512\n        \n        # Multi-Heads\n        self.head_biomass = nn.Sequential(\n            nn.Linear(self.feature_dim, 256),\n            nn.ReLU(),\n            nn.Linear(256, num_targets)\n        )\n        \n        self.head_height = nn.Sequential(\n            nn.Linear(self.feature_dim, 64),\n            nn.ReLU(),\n            nn.Linear(64, 1)\n        )\n        \n        self.head_ndvi = nn.Sequential(\n             nn.Linear(self.feature_dim, 64),\n            nn.ReLU(),\n            nn.Linear(64, 1)\n        )\n        \n        self.head_species = nn.Sequential(\n            nn.Linear(self.feature_dim, 128),\n            nn.ReLU(),\n            nn.Linear(128, num_species)\n        )\n        \n        # Weight Initialization\n        self.apply(self._init_weights)\n        \n    def _init_weights(self, m):\n        if isinstance(m, nn.Conv2d):\n            nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')\n        elif isinstance(m, nn.Linear):\n            nn.init.xavier_normal_(m.weight)\n            if m.bias is not None:\n                nn.init.constant_(m.bias, 0)\n        \n    def forward(self, x):\n        x = self.enc1(x)\n        x = self.enc2(x)\n        x = self.enc3(x)\n        x = self.enc4(x)\n        x = self.enc5(x)\n        x = self.pool(x)\n        x = torch.flatten(x, 1)\n        x = self.dropout(x)\n        \n        return {\n            'biomass': self.head_biomass(x),\n            'height': self.head_height(x),\n            'ndvi': self.head_ndvi(x),\n            'species': self.head_species(x)\n        }\n\n# --- LOSS ---\nclass MultiTaskLoss(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.regr_loss = nn.HuberLoss(delta=1.0) \n        self.cls_loss = nn.CrossEntropyLoss()\n        self.bio_weights = torch.tensor([0.1, 0.1, 0.1, 0.2, 0.5]).to(config.DEVICE)\n        \n    def forward(self, outputs, targets):\n        # Biomass Log-Space Loss\n        loss_bio = (nn.functional.smooth_l1_loss(outputs['biomass'], targets['biomass'], reduction='none', beta=1.0) * self.bio_weights).mean()\n        \n        # Aux Losses\n        loss_height = nn.functional.mse_loss(outputs['height'], targets['height'])\n        loss_ndvi = nn.functional.mse_loss(outputs['ndvi'], targets['ndvi'])\n        loss_species = self.cls_loss(outputs['species'], targets['species'])\n        \n        # Weighted Sum\n        total_loss = loss_bio + 0.1 * loss_height + 0.1 * loss_ndvi + 0.02 * loss_species\n        return total_loss\n\n# --- AUGMENTATION ---\ndef get_train_transforms():\n    return A.Compose([\n        A.Resize(256, 256),\n        A.HorizontalFlip(p=0.5),\n        A.VerticalFlip(p=0.5),\n        A.RandomRotate90(p=0.5),\n        A.Normalize(),\n        ToTensorV2()\n    ])\n\ndef get_valid_transforms():\n    return A.Compose([\n        A.Resize(256, 256),\n        A.Normalize(),\n        ToTensorV2()\n    ])\n\n# --- MIXUP UTILS ---\ndef mixup_data(x, targets, alpha=1.0):\n    if alpha > 0:\n        lam = np.random.beta(alpha, alpha)\n    else:\n        lam = 1\n    batch_size = x.size(0)\n    index = torch.randperm(batch_size).to(x.device)\n    mixed_x = lam * x + (1 - lam) * x[index, :]\n    targets_b = {k: v[index] for k, v in targets.items()}\n    return mixed_x, targets, targets_b, lam\n\ndef mixup_criterion(criterion, outputs, targets_a, targets_b, lam):\n    loss_a = criterion(outputs, targets_a)\n    loss_b = criterion(outputs, targets_b)\n    return lam * loss_a + (1 - lam) * loss_b\n\n# --- TRAINING LOOP (SINGLE FOLD) ---\ndef train_pipeline():\n    print(f\"--- Starting Custom Training on {config.DEVICE} ---\")\n    \n    # 1. Dataset\n    full_dataset = CSIRODataset(config.TRAIN_CSV, config.ROOT_DIR)\n    \n    # 2. Split\n    generator = torch.Generator().manual_seed(42)\n    train_size = int(0.85 * len(full_dataset))\n    val_size = len(full_dataset) - train_size\n    train_subset, val_subset = random_split(full_dataset, [train_size, val_size], generator=generator)\n    \n    print(f\"Train Size: {train_size} | Val Size: {val_size}\")\n    \n    # 3. Wrappers for Transform (Simplest way for Single Fold)\n    class TransformSubset(Dataset):\n        def __init__(self, subset, transform):\n            self.subset = subset\n            self.transform = transform\n        def __len__(self): return len(self.subset)\n        def __getitem__(self, idx):\n            original_idx = self.subset.indices[idx]\n            # Access underlying dataset\n            return self.subset.dataset.__getitem__(original_idx) # Hacky but works if __getitem__ logic was split. \n            # Actually CSIRODataset applies transform internally. We created 2 dataloaders in KFold logic before.\n            # Here let's just make 2 datasets to be clean.\n    \n    # Clean way:\n    train_dataset = CSIRODataset(config.TRAIN_CSV, config.ROOT_DIR, transform=get_train_transforms())\n    val_dataset = CSIRODataset(config.TRAIN_CSV, config.ROOT_DIR, transform=get_valid_transforms(), label_encoder=train_dataset.le)\n    \n    # Use the indices from random_split to select from these 2 datasets\n    train_loader = DataLoader(train_dataset, batch_size=config.BATCH_SIZE, sampler=SubsetRandomSampler(train_subset.indices), num_workers=0)\n    val_loader = DataLoader(val_dataset, batch_size=config.BATCH_SIZE, sampler=SubsetRandomSampler(val_subset.indices), num_workers=0)\n    \n    # 3. Model\n    model = CustomBiomassCNN(num_targets=5, num_species=config.NUM_SPECIES).to(config.DEVICE)\n    criterion = MultiTaskLoss()\n    optimizer = optim.AdamW(model.parameters(), lr=config.LR, weight_decay=1e-4) # Added weight decay\n    scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=3, verbose=True)\n    \n    best_loss = float('inf')\n    history = {'train_loss': [], 'val_loss': []}\n    \n    for epoch in range(config.EPOCHS):\n        model.train()\n        train_loss = 0.0\n        \n        loop = tqdm(train_loader, desc=f\"Epoch {epoch+1}/{config.EPOCHS}\")\n        for images, targets, _ in loop:\n            images = images.to(config.DEVICE)\n            targets = {k: v.to(config.DEVICE) for k, v in targets.items()}\n            \n            optimizer.zero_grad()\n            \n            if np.random.random() < 0.5:\n                mixed_images, targets_a, targets_b, lam = mixup_data(images, targets, alpha=0.4)\n                outputs = model(mixed_images)\n                loss = mixup_criterion(criterion, outputs, targets_a, targets_b, lam)\n            else:\n                outputs = model(images)\n                loss = criterion(outputs, targets)\n                \n            loss.backward()\n            optimizer.step()\n            train_loss += loss.item() * images.size(0)\n            loop.set_postfix(loss=loss.item())\n        \n        train_loss /= train_size\n        history['train_loss'].append(train_loss)\n        \n        # Val\n        model.eval()\n        val_loss = 0.0\n        with torch.no_grad():\n            for images, targets, _ in val_loader:\n                images = images.to(config.DEVICE)\n                targets = {k: v.to(config.DEVICE) for k, v in targets.items()}\n                outputs = model(images)\n                loss = criterion(outputs, targets)\n                val_loss += loss.item() * images.size(0)\n        \n        val_loss /= val_size\n        history['val_loss'].append(val_loss)\n        print(f\"Epoch {epoch+1}: Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f}\")\n        \n        scheduler.step(val_loss)\n        \n        if val_loss < best_loss:\n            best_loss = val_loss\n            torch.save(model.state_dict(), config.MODEL_PATH)\n            print(f\"Saved Best Model to {config.MODEL_PATH}\")\n            \n    # Plotting\n    plt.figure(figsize=(10, 6))\n    plt.plot(history['train_loss'], label='Train Loss')\n    plt.plot(history['val_loss'], label='Val Loss')\n    plt.title('Training Convergence')\n    plt.xlabel('Epoch')\n    plt.ylabel('Loss')\n    plt.legend()\n    plt.grid(True)\n    plt.savefig(os.path.join(config.OUTPUT_DIR, 'loss_plot.png'))\n    print(f\"Loss plot saved to {os.path.join(config.OUTPUT_DIR, 'loss_plot.png')}\")\n\n# --- INFERENCE ---\ndef inference_pipeline():\n    print(\"--- Starting Single Model Inference ---\")\n    if not os.path.exists(config.MODEL_PATH):\n        print(\"Model not found!\")\n        return\n        \n    model = CustomBiomassCNN(num_targets=5, num_species=config.NUM_SPECIES).to(config.DEVICE)\n    model.load_state_dict(torch.load(config.MODEL_PATH, map_location=config.DEVICE))\n    model.eval()\n    \n    test_ds = CSIRODataset(config.TEST_CSV, config.ROOT_DIR, is_test=True)\n    test_loader = DataLoader(test_ds, batch_size=config.BATCH_SIZE, shuffle=False)\n    \n    predictions_map = {}\n    with torch.no_grad():\n        for images, _, paths in tqdm(test_loader, desc=\"Predicting\"):\n            images = images.to(config.DEVICE)\n            outputs = model(images)\n            \n            # Denormalize Log1p: expm1\n            pred_linear = torch.expm1(outputs['biomass']).cpu().numpy()\n            \n            for path, pred in zip(paths, pred_linear):\n                predictions_map[path] = dict(zip(config.TARGETS, pred))\n                \n    test_df = pd.read_csv(config.TEST_CSV)\n    results = []\n    for _, row in test_df.iterrows():\n        sample_id = row['sample_id']\n        image_path = row['image_path']\n        target_name = row['target_name']\n        val = predictions_map.get(image_path, {}).get(target_name, 0.0)\n        results.append({'sample_id': sample_id, 'target': max(0.0, val)})\n        \n    submission_df = pd.DataFrame(results)\n    submission_df.to_csv(config.SUBMISSION_FILE, index=False)\n    print(\"Submission Saved.\")\n\nif __name__ == \"__main__\":\n    train_pipeline()\n    inference_pipeline()\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-17T05:26:50.419359Z","iopub.execute_input":"2025-12-17T05:26:50.420285Z"}},"outputs":[],"execution_count":null}]}