{"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,"sourceType":"competition"}],"dockerImageVersionId":31154,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ============================================================\n# CSIRO Image2Biomass Prediction - Corrected & Optimized\n# ============================================================\n# Improvements:\n# - fixed backbone feature dimension detection (use backbone.num_features)\n# - corrected WeightedMSELoss to weight per-target MSE correctly\n# - safer image path handling\n# - reduced default img_size and batch_size for Kaggle P100\n# - consistent target scaling and inverse transform for R² evaluation\n# ============================================================\n\nimport os\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nimport timm\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.preprocessing import StandardScaler\nimport cv2\nfrom tqdm import tqdm\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# ============================================================\n# CONFIGURATION\n# ============================================================\nclass CFG:\n    # Paths\n    train_csv = '/kaggle/input/csiro-biomass/train.csv'\n    test_csv = '/kaggle/input/csiro-biomass/test.csv'\n    train_dir = '/kaggle/input/csiro-biomass/train'\n    test_dir = '/kaggle/input/csiro-biomass/test'\n    output_dir = '/kaggle/working'\n    \n    # Model\n    model_name = 'tf_efficientnetv2_m'  # EfficientNetV2-M\n    img_size = 512        # lowered from 800 to be safer on P100\n    pretrained = True\n    \n    # Training\n    n_folds = 5\n    seed = 42\n    epochs = 30\n    batch_size = 8        # lowered from 16 to reduce OOM risk\n    num_workers = 4\n    lr = 3e-4\n    weight_decay = 1e-5\n    warmup_epochs = 2\n    \n    # Augmentation / TTA\n    use_tta = True\n    tta_steps = 5\n    \n    # Targets\n    targets = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n    target_weights = [0.1, 0.1, 0.1, 0.2, 0.5]\n    \n    use_target_scaling = True\n    \n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\n# reproducibility\ndef set_seed(seed=42):\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    import random\n    random.seed(seed)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n\nset_seed(CFG.seed)\n\n# ============================================================\n# DATA PREPROCESSING\n# ============================================================\ndef prepare_data(train_csv_path):\n    df = pd.read_csv(train_csv_path)\n    # create image_id (if sample_id contains __)\n    if 'sample_id' in df.columns:\n        df['image_id'] = df['sample_id'].apply(lambda x: x.split('__')[0] if '__' in str(x) else str(x))\n    else:\n        df['image_id'] = df['image_path'].apply(lambda x: os.path.basename(x).split('.')[0])\n    \n    # metadata columns commonly present; guard if missing\n    metadata_cols = [c for c in ['image_path', 'Sampling_Date', 'State', 'Species', 'Pre_GSHH_NDVI', 'Height_Ave_cm'] if c in df.columns]\n    \n    # pivot long -> wide if necessary\n    if 'target_name' in df.columns and 'target' in df.columns:\n        df_pivot = df.pivot_table(\n            index=['image_id'] + metadata_cols,\n            columns='target_name',\n            values='target',\n            aggfunc='first'\n        ).reset_index()\n    else:\n        df_pivot = df.copy()\n        if 'image_path' not in df_pivot.columns:\n            # try to reconstruct image_path from image_id\n            df_pivot['image_path'] = df_pivot['image_id'].astype(str) + '.jpg'\n    \n    # ensure targets exist\n    for t in CFG.targets:\n        if t not in df_pivot.columns:\n            df_pivot[t] = 0.0\n        else:\n            df_pivot[t] = df_pivot[t].fillna(0.0)\n    \n    # biomass bin for stratification\n    try:\n        df_pivot['biomass_bin'] = pd.qcut(df_pivot['Dry_Total_g'], q=10, labels=False, duplicates='drop')\n    except Exception:\n        df_pivot['biomass_bin'] = pd.cut(df_pivot['Dry_Total_g'], bins=10, labels=False)\n    df_pivot['biomass_bin'] = df_pivot['biomass_bin'].fillna(0).astype(int)\n    \n    print(f\"Prepared {len(df_pivot)} unique images\")\n    return df_pivot\n\n# ============================================================\n# DATASET\n# ============================================================\nclass BiomassDataset(Dataset):\n    def __init__(self, df, img_dir, transform=None, is_test=False, scaler=None):\n        self.df = df.reset_index(drop=True)\n        self.img_dir = img_dir\n        self.transform = transform\n        self.is_test = is_test\n        \n        # prepare tabular features\n        if 'Pre_GSHH_NDVI' in df.columns and 'Height_Ave_cm' in df.columns:\n            tabular_data = df[['Pre_GSHH_NDVI','Height_Ave_cm']].fillna(0).values\n        else:\n            tabular_data = np.zeros((len(df), 2), dtype=np.float32)\n        \n        if not is_test:\n            if scaler is None:\n                self.scaler = StandardScaler()\n                self.tabular_features = self.scaler.fit_transform(tabular_data)\n            else:\n                self.scaler = scaler\n                self.tabular_features = self.scaler.transform(tabular_data)\n        else:\n            if scaler is not None:\n                self.scaler = scaler\n                self.tabular_features = self.scaler.transform(tabular_data)\n            else:\n                self.tabular_features = tabular_data.astype(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        # build image path robustly\n        # allow either 'image_path' containing full path or just filename\n        if 'image_path' in row and isinstance(row['image_path'], str) and row['image_path'].strip() != '':\n            fname = os.path.basename(row['image_path'])\n        elif 'image_id' in row:\n            fname = str(row['image_id']) + '.jpg'\n        else:\n            raise ValueError(\"No valid image identifier for row idx {}\".format(idx))\n        \n        img_path = os.path.join(self.img_dir, fname)\n        image = cv2.imread(img_path)\n        if image is None:\n            # try alternate path directly from image_path if absolute\n            alt = row.get('image_path', None)\n            if isinstance(alt, str) and os.path.exists(alt):\n                image = cv2.imread(alt)\n            else:\n                raise FileNotFoundError(f\"Failed to load image: {img_path}\")\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        \n        if self.transform:\n            augmented = self.transform(image=image)\n            image = augmented['image']\n        \n        tabular = torch.tensor(self.tabular_features[idx], dtype=torch.float32)\n        \n        if self.is_test:\n            return image, tabular\n        else:\n            targets = torch.tensor([\n                row.get('Dry_Green_g', 0.0),\n                row.get('Dry_Dead_g', 0.0),\n                row.get('Dry_Clover_g', 0.0),\n                row.get('GDM_g', 0.0),\n                row.get('Dry_Total_g', 0.0)\n            ], dtype=torch.float32)\n            return image, tabular, targets\n\n# ============================================================\n# AUGMENTATIONS\n# ============================================================\ndef get_train_transforms():\n    return A.Compose([\n        A.Resize(CFG.img_size, CFG.img_size),\n        A.HorizontalFlip(p=0.5),\n        A.VerticalFlip(p=0.5),\n        A.RandomRotate90(p=0.5),\n        A.ShiftScaleRotate(shift_limit=0.08, scale_limit=0.12, rotate_limit=15, p=0.5),\n        A.OneOf([\n            A.HueSaturationValue(hue_shift_limit=20, sat_shift_limit=30, val_shift_limit=20, p=1),\n            A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=1),\n        ], p=0.7),\n        A.OneOf([\n            A.GaussNoise(var_limit=(5.0, 30.0), p=1),\n            A.GaussianBlur(blur_limit=(3, 7), p=1),\n        ], p=0.3),\n        A.CoarseDropout(max_holes=6, max_height=32, max_width=32, p=0.25),\n        A.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225]),\n        ToTensorV2(),\n    ])\n\ndef get_valid_transforms():\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\n# ============================================================\n# MODEL\n# ============================================================\nclass BiomassModel(nn.Module):\n    def __init__(self, model_name, pretrained=True):\n        super().__init__()\n        self.backbone = timm.create_model(model_name, pretrained=pretrained, num_classes=0, global_pool='avg')\n        # safest way to get features:\n        img_features = getattr(self.backbone, 'num_features', None)\n        if img_features is None:\n            # fallback: forward a small tensor (on CPU) to inspect shape (rare)\n            self.backbone.eval()\n            with torch.no_grad():\n                try:\n                    dummy = torch.randn(1,3,CFG.img_size,CFG.img_size)\n                    feat = self.backbone(dummy)\n                    img_features = feat.shape[1]\n                except Exception:\n                    img_features = 1280  # reasonable default for many backbones\n        self.img_features = img_features\n        \n        # tabular 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_dim = self.img_features + 128\n        self.fusion = nn.Sequential(\n            nn.Linear(fusion_dim, 200),\n            nn.BatchNorm1d(200),\n            nn.ReLU(),\n            nn.Dropout(0.4),\n            nn.Linear(200, 200),\n            nn.BatchNorm1d(200),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n        )\n        \n        self.head = nn.Linear(200, len(CFG.targets))\n    \n    def forward(self, image, tabular):\n        img_feats = self.backbone(image)            # [B, img_features]\n        tab_feats = self.tabular_encoder(tabular)   # [B, 128]\n        combined = torch.cat([img_feats, tab_feats], dim=1)\n        fused = self.fusion(combined)\n        outputs = self.head(fused)\n        return outputs\n\n# ============================================================\n# LOSS & METRIC\n# ============================================================\nclass WeightedMSELoss(nn.Module):\n    def __init__(self, weights):\n        super().__init__()\n        self.register_buffer('weights', torch.tensor(weights, dtype=torch.float32))\n    \n    def forward(self, preds, targets):\n        # preds, targets: [B, T]\n        mse_per_target = ((preds - targets) ** 2).mean(dim=0)  # [T] mean over batch\n        weighted = mse_per_target * self.weights              # [T]\n        return weighted.sum() / self.weights.sum()            # scalar normalized\n\ndef calculate_r2_score(y_true, y_pred):\n    ss_res = np.sum((y_true - y_pred) ** 2)\n    ss_tot = np.sum((y_true - y_true.mean()) ** 2)\n    if ss_tot == 0:\n        return 0.0\n    return 1 - (ss_res / ss_tot)\n\ndef calculate_weighted_r2(y_true, y_pred, weights):\n    scores = []\n    for i in range(y_true.shape[1]):\n        r2 = calculate_r2_score(y_true[:, i], y_pred[:, i])\n        scores.append(r2)\n    weighted = sum(s * w for s, w in zip(scores, weights))\n    return weighted, scores\n\n# ============================================================\n# TRAIN / VALID\n# ============================================================\ndef train_epoch(model, loader, optimizer, criterion, device, scaler):\n    model.train()\n    running = 0.0\n    n_batches = 0\n    pbar = tqdm(loader, desc='Train', leave=False)\n    for batch_idx, (images, tabular, targets) in enumerate(pbar):\n        images = images.to(device)\n        tabular = tabular.to(device)\n        targets = targets.to(device)\n        \n        optimizer.zero_grad()\n        with torch.cuda.amp.autocast(enabled=True):\n            preds = model(images, tabular)\n            loss = criterion(preds, targets)\n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n        \n        running += loss.item()\n        n_batches += 1\n        pbar.set_postfix({'loss': running / n_batches})\n    return running / (n_batches + 1e-12)\n\ndef validate_epoch(model, loader, criterion, device, target_scaler=None):\n    model.eval()\n    running = 0.0\n    all_preds = []\n    all_targets = []\n    with torch.no_grad():\n        for images, tabular, targets in tqdm(loader, desc='Valid', leave=False):\n            images = images.to(device)\n            tabular = tabular.to(device)\n            targets = targets.to(device)\n            preds = model(images, tabular)\n            loss = criterion(preds, targets)\n            running += loss.item()\n            all_preds.append(preds.cpu().numpy())\n            all_targets.append(targets.cpu().numpy())\n    all_preds = np.vstack(all_preds)\n    all_targets = np.vstack(all_targets)\n    \n    if target_scaler is not None:\n        all_preds_orig = target_scaler.inverse_transform(all_preds)\n        all_targets_orig = target_scaler.inverse_transform(all_targets)\n        weighted_r2, indiv = calculate_weighted_r2(all_targets_orig, all_preds_orig, CFG.target_weights)\n    else:\n        weighted_r2, indiv = calculate_weighted_r2(all_targets, all_preds, CFG.target_weights)\n        all_preds_orig, all_targets_orig = all_preds, all_targets\n    \n    return running / (len(loader) + 1e-12), weighted_r2, indiv, all_preds_orig, all_targets_orig\n\n# ============================================================\n# TRAIN K-FOLD\n# ============================================================\ndef train_kfold(df, fold):\n    print(f\"\\n=== Fold {fold + 1}/{CFG.n_folds} ===\")\n    train_df = df[df['fold'] != fold].reset_index(drop=True)\n    valid_df = df[df['fold'] == fold].reset_index(drop=True)\n    print(f\"Train size: {len(train_df)}, Valid size: {len(valid_df)}\")\n    \n    # Optionally scale targets\n    target_scaler = None\n    if CFG.use_target_scaling:\n        target_scaler = StandardScaler()\n        target_scaler.fit(train_df[CFG.targets].values)\n        train_df[CFG.targets] = target_scaler.transform(train_df[CFG.targets].values)\n        valid_df[CFG.targets] = target_scaler.transform(valid_df[CFG.targets].values)\n        print(\"Targets scaled (zero mean, unit var)\")\n    \n    train_ds = BiomassDataset(train_df, CFG.train_dir, transform=get_train_transforms())\n    valid_ds = BiomassDataset(valid_df, CFG.train_dir, transform=get_valid_transforms(), scaler=train_ds.scaler)\n    \n    train_loader = DataLoader(train_ds, batch_size=CFG.batch_size, shuffle=True, num_workers=CFG.num_workers, pin_memory=True)\n    valid_loader = DataLoader(valid_ds, batch_size=CFG.batch_size*2, shuffle=False, num_workers=CFG.num_workers, pin_memory=True)\n    \n    model = BiomassModel(CFG.model_name, pretrained=CFG.pretrained).to(CFG.device)\n    criterion = WeightedMSELoss(CFG.target_weights).to(CFG.device)\n    optimizer = optim.AdamW(model.parameters(), lr=CFG.lr, weight_decay=CFG.weight_decay)\n    \n    warmup_scheduler = optim.lr_scheduler.LambdaLR(optimizer, lambda e: (e+1)/CFG.warmup_epochs if e < CFG.warmup_epochs else 1.0)\n    main_scheduler = optim.lr_scheduler.CosineAnnealingWarmRestarts(optimizer, T_0=10, T_mult=2, eta_min=1e-6)\n    scaler = torch.cuda.amp.GradScaler()\n    \n    best_score = -np.inf\n    patience = 8\n    patience_counter = 0\n    \n    for epoch in range(CFG.epochs):\n        print(f\"\\nEpoch {epoch+1}/{CFG.epochs} LR={optimizer.param_groups[0]['lr']:.6f}\")\n        train_loss = train_epoch(model, train_loader, optimizer, criterion, CFG.device, scaler)\n        valid_loss, weighted_r2, indiv_r2, preds_orig, targets_orig = validate_epoch(model, valid_loader, criterion, CFG.device, target_scaler)\n        \n        # print metrics\n        print(f\"Train Loss: {train_loss:.4f} | Valid Loss: {valid_loss:.4f}\")\n        print(f\"Weighted R²: {weighted_r2:.4f} | Individual R²: {['{:.4f}'.format(x) for x in indiv_r2]}\")\n        \n        # scheduler step\n        if epoch < CFG.warmup_epochs:\n            warmup_scheduler.step()\n        else:\n            main_scheduler.step()\n        \n        # save best by original-scale weighted R2\n        score_to_use = weighted_r2\n        if score_to_use > best_score:\n            best_score = score_to_use\n            ckpt = {\n                'model_state_dict': model.state_dict(),\n                'tabular_scaler': train_ds.scaler,\n                'target_scaler': target_scaler\n            }\n            torch.save(ckpt, os.path.join(CFG.output_dir, f'best_model_fold{fold}.pth'))\n            print(f\"Saved best model (R²={best_score:.4f})\")\n            patience_counter = 0\n        else:\n            patience_counter += 1\n        \n        if patience_counter >= patience:\n            print(f\"Early stopping (patience {patience})\")\n            break\n        \n        # free some memory\n        torch.cuda.empty_cache()\n    \n    return best_score\n\n# ============================================================\n# MAIN\n# ============================================================\ndef main():\n    print(\"Loading CSV:\", CFG.train_csv)\n    df_raw = pd.read_csv(CFG.train_csv)\n    print(\"Raw shape:\", df_raw.shape)\n    if 'target_name' in df_raw.columns and 'target' in df_raw.columns:\n        df = prepare_data(CFG.train_csv)\n    else:\n        df = df_raw.copy()\n        if 'image_path' not in df.columns and 'image_id' in df.columns:\n            df['image_path'] = df['image_id'].astype(str) + '.jpg'\n        if 'biomass_bin' not in df.columns:\n            try:\n                df['biomass_bin'] = pd.qcut(df['Dry_Total_g'], q=10, labels=False, duplicates='drop')\n            except:\n                df['biomass_bin'] = pd.cut(df['Dry_Total_g'], bins=10, labels=False)\n            df['biomass_bin'] = df['biomass_bin'].fillna(0).astype(int)\n    \n    print(\"Final df shape:\", df.shape)\n    # create folds\n    skf = StratifiedKFold(n_splits=CFG.n_folds, shuffle=True, random_state=CFG.seed)\n    df['fold'] = -1\n    for fold, (_, val_idx) in enumerate(skf.split(df, df['biomass_bin'])):\n        df.loc[val_idx, 'fold'] = fold\n    print(\"Fold counts:\\n\", df['fold'].value_counts().sort_index())\n    \n    # train folds\n    fold_scores = []\n    for fold in range(CFG.n_folds):\n        score = train_kfold(df, fold)\n        fold_scores.append(score)\n    print(\"\\nCV results:\", fold_scores)\n    print(\"Mean CV:\", np.mean(fold_scores), \"Std:\", np.std(fold_scores))\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-05T05:09:30.438109Z","iopub.execute_input":"2025-11-05T05:09:30.438355Z","iopub.status.idle":"2025-11-05T05:41:23.152865Z","shell.execute_reply.started":"2025-11-05T05:09:30.438334Z","shell.execute_reply":"2025-11-05T05:41:23.151900Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}