{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.6.6","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# # This Python 3 environment comes with many helpful analytics libraries installed\n# # It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# # For example, here's several helpful packages to load\n\n# import numpy as np # linear algebra\n# import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# # Input data files are available in the read-only \"../input/\" directory\n# # For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\n# import os\n# for dirname, _, filenames in os.walk('/kaggle/input'):\n#     for filename in filenames:\n#         print(os.path.join(dirname, filename))\n\n# # You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# # You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session\n\n# # Use the kagglehub client library to attach Kaggle resources like competitions, datasets, and models to your session\n# # Learn more about kagglehub: https://github.com/Kaggle/kagglehub/blob/main/README.md\n\n# import kagglehub\n# # kagglehub.dataset_download('<owner>/<dataset-slug>')","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# 1. SETUP & IMPORTS\n# ============================================================================\n!pip install -q iterative-stratification\n\nimport os\nimport cv2\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport torchvision.models as models\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom torch.amp import GradScaler, autocast\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nfrom tqdm import tqdm\nfrom sklearn.preprocessing import MultiLabelBinarizer\nfrom iterstrat.ml_stratifiers import MultilabelStratifiedShuffleSplit\nfrom sklearn.metrics import (\n    roc_auc_score, \n    average_precision_score, \n    precision_score, \n    recall_score, \n    f1_score, \n    accuracy_score\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Configuration ---\nEPOCHS = 15\nBATCH_SIZE = 16 \nPATIENCE = 3\nLEARNING_RATE = 1e-4\nWEIGHT_DECAY = 1e-5\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nIMAGE_DIR = \"/kaggle/input/competitions/plant-pathology-2021-fgvc8/train_images\"\nCSV_PATH = \"/kaggle/input/competitions/plant-pathology-2021-fgvc8/train.csv\"\n\n# ============================================================================\n# 2. DATA LOADING & STRATIFIED SPLITS (Seed = 42)\n# ============================================================================\ndf = pd.read_csv(CSV_PATH)\ndf['label_list'] = df['labels'].apply(lambda x: x.split(' '))\n\nmlb = MultiLabelBinarizer()\nencoded_labels = mlb.fit_transform(df['label_list'])\nclasses = list(mlb.classes_)\n\nencoded_df = pd.DataFrame(encoded_labels, columns=classes)\ndf = pd.concat([df, encoded_df], axis=1)\nprint(f\"[*] Total unique classes identified: {classes}\")\n\nY = df[classes].values\n\n# 80% Train / 20% Temp\nmsss1 = MultilabelStratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=42)\ntrain_idx, temp_idx = next(msss1.split(df, Y))\ntrain_df = df.iloc[train_idx].reset_index(drop=True)\ntemp_df = df.iloc[temp_idx].reset_index(drop=True)\n\n# 10% Validation / 10% Test\nY_temp = temp_df[classes].values\nmsss2 = MultilabelStratifiedShuffleSplit(n_splits=1, test_size=0.5, random_state=42)\nval_idx, test_idx = next(msss2.split(temp_df, Y_temp))\nval_df = temp_df.iloc[val_idx].reset_index(drop=True)\ntest_df = temp_df.iloc[test_idx].reset_index(drop=True)\n\nprint(f\"[*] Data Split -> Train: {len(train_df)} | Val: {len(val_df)} | Test: {len(test_df)}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# 3. TRANSFORMS & DATASET CLASS\n# ============================================================================\ntrain_transforms = transforms.Compose([\n    transforms.Resize((512, 512)), \n    transforms.RandomHorizontalFlip(p=0.5), \n    transforms.RandomVerticalFlip(p=0.5),\n    transforms.ToTensor(), \n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\nval_test_transforms = transforms.Compose([\n    transforms.Resize((512, 512)),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n\nclass PlantPathologyDataset(Dataset):\n    def __init__(self, dataframe, image_dir, classes, transform=None):\n        self.dataframe = dataframe\n        self.image_dir = image_dir\n        self.classes = classes\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.dataframe)\n\n    def apply_clahe(self, image_path):\n        img = cv2.imread(image_path)\n        if img is None: raise FileNotFoundError(f\"Cannot load image at {image_path}\")\n        lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)\n        l, a, b = cv2.split(lab)\n        clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))\n        cl = clahe.apply(l)\n        limg = cv2.merge((cl,a,b))\n        rgb_img = cv2.cvtColor(limg, cv2.COLOR_LAB2RGB)\n        return Image.fromarray(rgb_img)\n\n    def __getitem__(self, idx):\n        img_name = self.dataframe.iloc[idx]['image']\n        img_path = os.path.join(self.image_dir, img_name)\n        image = self.apply_clahe(img_path)\n        labels = self.dataframe.iloc[idx][self.classes].values.astype(np.float32)\n        label_tensor = torch.tensor(labels)\n        if self.transform: image = self.transform(image)\n        return image, label_tensor\n\ntrain_dataset = PlantPathologyDataset(train_df, IMAGE_DIR, classes, train_transforms)\nval_dataset = PlantPathologyDataset(val_df, IMAGE_DIR, classes, val_test_transforms)\ntest_dataset = PlantPathologyDataset(test_df, IMAGE_DIR, classes, val_test_transforms)\n\ntrain_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=4)\nval_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=4)\ntest_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=4)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# 4. CBAM ARCHITECTURE MODULES\n# ============================================================================\nclass ChannelAttention(nn.Module):\n    def __init__(self, in_planes, ratio=16):\n        super(ChannelAttention, self).__init__()\n        self.avg_pool = nn.AdaptiveAvgPool2d(1)\n        self.max_pool = nn.AdaptiveMaxPool2d(1)\n           \n        self.fc = nn.Sequential(\n            nn.Conv2d(in_planes, in_planes // ratio, 1, bias=False),\n            nn.ReLU(inplace=True),\n            nn.Conv2d(in_planes // ratio, in_planes, 1, bias=False)\n        )\n        self.sigmoid = nn.Sigmoid()\n\n    def forward(self, x):\n        avg_out = self.fc(self.avg_pool(x))\n        max_out = self.fc(self.max_pool(x))\n        out = avg_out + max_out\n        return self.sigmoid(out)\n\nclass SpatialAttention(nn.Module):\n    def __init__(self, kernel_size=7):\n        super(SpatialAttention, self).__init__()\n        assert kernel_size in (3, 7), 'kernel size must be 3 or 7'\n        padding = 3 if kernel_size == 7 else 1\n        \n        self.conv1 = nn.Conv2d(2, 1, kernel_size, padding=padding, bias=False)\n        self.sigmoid = nn.Sigmoid()\n\n    def forward(self, x):\n        avg_out = torch.mean(x, dim=1, keepdim=True)\n        max_out, _ = torch.max(x, dim=1, keepdim=True)\n        x_cat = torch.cat([avg_out, max_out], dim=1)\n        out = self.conv1(x_cat)\n        return self.sigmoid(out)\n\nclass CBAM(nn.Module):\n    def __init__(self, in_planes, ratio=16, kernel_size=7):\n        super(CBAM, self).__init__()\n        self.ca = ChannelAttention(in_planes, ratio)\n        self.sa = SpatialAttention(kernel_size)\n\n    def forward(self, x):\n        x = x * self.ca(x)\n        x = x * self.sa(x)\n        return x\n\nclass CBAMDenseNet121(nn.Module):\n    def __init__(self, num_classes):\n        super().__init__()\n        # Load the feature extractor part of DenseNet121\n        self.backbone = models.densenet121(weights=models.DenseNet121_Weights.IMAGENET1K_V1).features\n        \n        # DenseNet121 feature depth is 1024\n        self.cbam = CBAM(in_planes=1024)\n        \n        # Global Average Pooling and final classifier\n        self.gap = nn.AdaptiveAvgPool2d(1)\n        self.classifier = nn.Linear(1024, num_classes)\n\n    def forward(self, x):\n        # Extract features and apply ReLU (DenseNet convention)\n        features = F.relu(self.backbone(x), inplace=True)\n        \n        # Apply CBAM Attention\n        attended_features = self.cbam(features)\n        \n        # Collapse spatial dimensions via GAP\n        pooled = self.gap(attended_features)\n        pooled = pooled.view(pooled.size(0), -1)\n        \n        # Final logits\n        logits = self.classifier(pooled)\n        \n        # Return logits and 'None' for compatibility with eval loops\n        return logits, None","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# 5. ASYMMETRIC LOSS (ASL)\n# ============================================================================\nclass AsymmetricLoss(nn.Module):\n    def __init__(self, gamma_neg=4, gamma_pos=1, clip=0.05, eps=1e-8):\n        super(AsymmetricLoss, self).__init__()\n        self.gamma_neg = gamma_neg\n        self.gamma_pos = gamma_pos\n        self.clip = clip\n        self.eps = eps\n\n    def forward(self, x, y):\n        x_sigmoid = torch.sigmoid(x)\n        xs_pos = x_sigmoid\n        xs_neg = 1 - x_sigmoid\n\n        if self.clip is not None and self.clip > 0:\n            xs_neg = (xs_neg + self.clip).clamp(max=1)\n\n        los_pos = y * torch.log(xs_pos.clamp(min=self.eps))\n        los_neg = (1 - y) * torch.log(xs_neg.clamp(min=self.eps))\n        loss = los_pos + los_neg\n\n        pt0 = xs_pos * y\n        pt1 = xs_neg * (1 - y)  \n        pt = pt0 + pt1\n        \n        one_sided_gamma = self.gamma_pos * y + self.gamma_neg * (1 - y)\n        one_sided_w = torch.pow((1 - pt).clamp(min=self.eps), one_sided_gamma)\n\n        loss *= one_sided_w\n        return -loss.sum(dim=-1).mean()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# 6. EVALUATION FUNCTIONS\n# ============================================================================\n@torch.no_grad()\ndef evaluate(model, dataloader, device, threshold=0.5):\n    model.eval()\n    all_preds, all_labels = [], []\n    for images, labels in dataloader:\n        logits, _ = model(images.to(device))\n        all_preds.append(torch.sigmoid(logits).cpu().numpy())\n        all_labels.append(labels.numpy())\n    all_preds, all_labels = np.vstack(all_preds), np.vstack(all_labels)\n    try:\n        macro_auroc = roc_auc_score(all_labels, all_preds, average='macro')\n        macro_map = average_precision_score(all_labels, all_preds, average='macro')\n    except ValueError:\n        macro_auroc, macro_map = 0.0, 0.0 \n    \n    binary_preds = (all_preds > threshold).astype(int)\n    macro_precision = precision_score(all_labels, binary_preds, average='macro', zero_division=0)\n    macro_recall = recall_score(all_labels, binary_preds, average='macro', zero_division=0)\n    macro_f1 = f1_score(all_labels, binary_preds, average='macro', zero_division=0)\n    exact_match_acc = accuracy_score(all_labels, binary_preds)\n    return macro_auroc, macro_map, macro_precision, macro_recall, macro_f1, exact_match_acc\n\n@torch.no_grad()\ndef find_optimal_thresholds(model, dataloader, device, classes):\n    model.eval()\n    all_preds, all_labels = [], []\n    for images, labels in dataloader:\n        logits, _ = model(images.to(device))\n        all_preds.append(torch.sigmoid(logits).cpu().numpy())\n        all_labels.append(labels.numpy())\n    all_preds, all_labels = np.vstack(all_preds), np.vstack(all_labels)\n    \n    best_thresholds = []\n    print(\"\\n--- Optimal Class Thresholds Discovered ---\")\n    for c in range(len(classes)):\n        best_f1, best_t = 0, 0.5\n        for t in np.arange(0.1, 0.9, 0.05):\n            preds = (all_preds[:, c] > t).astype(int)\n            f1 = f1_score(all_labels[:, c], preds, zero_division=0)\n            if f1 > best_f1: best_f1, best_t = f1, t\n        best_thresholds.append(best_t)\n        print(f\"  -> {classes[c].ljust(20)} | Threshold: {best_t:.2f}\")\n    return best_thresholds\n\n@torch.no_grad()\ndef evaluate_custom_thresholds(model, dataloader, device, thresholds=None):\n    model.eval()\n    all_preds, all_labels = [], []\n    for images, labels in dataloader:\n        logits, _ = model(images.to(device))\n        all_preds.append(torch.sigmoid(logits).cpu().numpy())\n        all_labels.append(labels.numpy())\n    all_preds, all_labels = np.vstack(all_preds), np.vstack(all_labels)\n    \n    binary_preds = np.zeros_like(all_preds)\n    for c in range(len(thresholds if thresholds else [0.5]*all_preds.shape[1])):\n        t = thresholds[c] if thresholds else 0.5\n        binary_preds[:, c] = (all_preds[:, c] > t).astype(int)\n    \n    mac_prec = precision_score(all_labels, binary_preds, average='macro', zero_division=0)\n    mac_rec = recall_score(all_labels, binary_preds, average='macro', zero_division=0)\n    mac_f1 = f1_score(all_labels, binary_preds, average='macro', zero_division=0)\n    exact_acc = accuracy_score(all_labels, binary_preds)\n    mac_auroc = roc_auc_score(all_labels, all_preds, average='macro')\n    mac_map = average_precision_score(all_labels, all_preds, average='macro')\n    \n    return mac_auroc, mac_map, mac_prec, mac_rec, mac_f1, exact_acc","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# 7. TRAINING LOOP\n# ============================================================================\ndef train_one_epoch_amp(model, dataloader, optimizer, criterion, scaler, device):\n    model.train()\n    running_loss = 0.0\n    pbar = tqdm(dataloader, desc=\"Training\")\n    for images, labels in pbar:\n        images, labels = images.to(device), labels.to(device)\n        optimizer.zero_grad()\n        with autocast('cuda'):\n            logits, _ = model(images)\n            loss = criterion(logits, labels)\n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n        running_loss += loss.item()\n        pbar.set_postfix({'loss': loss.item()})\n    return running_loss / len(dataloader)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# 8. EXECUTION PIPELINE\n# ============================================================================\nmodel = CBAMDenseNet121(num_classes=len(classes))\n\nif torch.cuda.device_count() > 1:\n    print(f\"[*] Firing up {torch.cuda.device_count()} GPUs!\")\n    model = nn.DataParallel(model)\nmodel = model.to(DEVICE)\n\ncriterion = AsymmetricLoss(gamma_neg=4, gamma_pos=1, clip=0.05)\noptimizer = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE, weight_decay=WEIGHT_DECAY)\nscaler = GradScaler('cuda')\n\nbest_auroc = 0.0\npatience_counter = 0\n\nfor epoch in range(EPOCHS):\n    print(f\"\\n--- Epoch {epoch+1}/{EPOCHS} ---\")\n    train_loss = train_one_epoch_amp(model, train_loader, optimizer, criterion, scaler, DEVICE)\n    val_auroc, val_map, val_prec, val_rec, val_f1, val_acc = evaluate(model, val_loader, DEVICE)\n    \n    print(f\"Train Loss: {train_loss:.4f}\")\n    print(f\"Val AUROC: {val_auroc:.4f} | Val mAP: {val_map:.4f}\")\n    print(f\"Val F1: {val_f1:.4f} | Val Prec: {val_prec:.4f} | Val Rec: {val_rec:.4f} | Val Acc: {val_acc:.4f}\")\n    \n    if val_auroc > best_auroc:\n        best_auroc = val_auroc\n        patience_counter = 0 \n        model_to_save = model.module.state_dict() if isinstance(model, nn.DataParallel) else model.state_dict()\n        torch.save(model_to_save, 'cbam_best_model.pth')\n        print(f\"[*] 🟢 New best CBAM model saved with AUROC: {best_auroc:.4f}\")\n    else:\n        patience_counter += 1\n        print(f\"[*] 🟡 No improvement. Early Stopping Counter: {patience_counter}/{PATIENCE}\")\n        if patience_counter >= PATIENCE:\n            print(f\"\\n[!] 🛑 Early stopping triggered at Epoch {epoch+1}!\")\n            break","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# 9. FINAL TESTING WITH DYNAMIC THRESHOLDING\n# ============================================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\"🚀 RECOVERING BEST CBAM MODEL FOR FINAL BENCHMARKING 🚀\")\nprint(\"=\"*60)\n\nbest_weights = torch.load('cbam_best_model.pth', map_location=DEVICE)\nif isinstance(model, nn.DataParallel):\n    model.module.load_state_dict(best_weights)\nelse:\n    model.load_state_dict(best_weights)\n\noptimal_thresh = find_optimal_thresholds(model, val_loader, DEVICE, classes)\ndyn_auroc, dyn_map, dyn_prec, dyn_rec, dyn_f1, dyn_acc = evaluate_custom_thresholds(model, test_loader, DEVICE, thresholds=optimal_thresh)\n\nprint(\"\\n\" + \"=\"*60)\nprint(\"📊 FINAL CBAM BASELINE METRICS (DYNAMIC SWEEP) 📊\")\nprint(\"=\"*60)\nprint(f\"Macro AUROC      | {dyn_auroc:.4f}\")\nprint(f\"mAP              | {dyn_map:.4f}\")\nprint(f\"Macro F1-Score   | {dyn_f1:.4f}\")\nprint(f\"Macro Precision  | {dyn_prec:.4f}\")\nprint(f\"Macro Recall     | {dyn_rec:.4f}\")\nprint(f\"Exact Match Acc  | {dyn_acc:.4f}\")\nprint(\"=\"*60)","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}