{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","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,"execution":{"iopub.status.busy":"2026-08-11T15:58:19.631886Z","iopub.execute_input":"2026-08-11T15:58:19.632333Z","iopub.status.idle":"2026-08-11T15:58:19.638141Z","shell.execute_reply.started":"2026-08-11T15:58:19.632306Z","shell.execute_reply":"2026-08-11T15:58:19.637316Z"}},"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 # Updated AMP import\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,"execution":{"iopub.status.busy":"2026-08-11T15:58:19.639874Z","iopub.execute_input":"2026-08-11T15:58:19.640155Z","iopub.status.idle":"2026-08-11T15:58:29.862406Z","shell.execute_reply.started":"2026-08-11T15:58:19.640133Z","shell.execute_reply":"2026-08-11T15:58:29.861796Z"}},"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\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-11T15:58:29.863401Z","iopub.execute_input":"2026-08-11T15:58:29.863962Z","iopub.status.idle":"2026-08-11T15:58:30.127295Z","shell.execute_reply.started":"2026-08-11T15:58:29.863921Z","shell.execute_reply":"2026-08-11T15:58:30.126402Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# 2. DATA LOADING & STRATIFIED SPLITS (Exact same 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,"execution":{"iopub.status.busy":"2026-08-11T15:58:30.129273Z","iopub.execute_input":"2026-08-11T15:58:30.129643Z","iopub.status.idle":"2026-08-11T15:58:30.526663Z","shell.execute_reply.started":"2026-08-11T15:58:30.129618Z","shell.execute_reply":"2026-08-11T15:58:30.525944Z"}},"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,"execution":{"iopub.status.busy":"2026-08-11T15:58:30.527579Z","iopub.execute_input":"2026-08-11T15:58:30.527831Z","iopub.status.idle":"2026-08-11T15:58:30.538155Z","shell.execute_reply.started":"2026-08-11T15:58:30.527796Z","shell.execute_reply":"2026-08-11T15:58:30.537300Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# 4. VANILLA DENSENET121 ARCHITECTURE\n# ============================================================================\nclass VanillaDenseNet121(nn.Module):\n    def __init__(self, num_classes):\n        super().__init__()\n        # Load standard ImageNet weights\n        self.backbone = models.densenet121(weights=models.DenseNet121_Weights.IMAGENET1K_V1)\n        \n        # Replace the final classification layer for our specific number of classes\n        in_features = self.backbone.classifier.in_features\n        self.backbone.classifier = nn.Linear(in_features, num_classes)\n\n    def forward(self, x):\n        # The backbone includes the Global Average Pooling (GAP) internally\n        logits = self.backbone(x)\n        \n        # We return logits and 'None' for attention maps so it is perfectly \n        # compatible with your existing train/eval loops that expect two outputs.\n        return logits, None","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-11T15:58:30.539173Z","iopub.execute_input":"2026-08-11T15:58:30.539430Z","iopub.status.idle":"2026-08-11T15:58:30.553397Z","shell.execute_reply.started":"2026-08-11T15:58:30.539409Z","shell.execute_reply":"2026-08-11T15:58:30.552705Z"}},"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,"execution":{"iopub.status.busy":"2026-08-11T15:58:30.554246Z","iopub.execute_input":"2026-08-11T15:58:30.554418Z","iopub.status.idle":"2026-08-11T15:58:30.569732Z","shell.execute_reply.started":"2026-08-11T15:58:30.554400Z","shell.execute_reply":"2026-08-11T15:58:30.569176Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# 6. EVALUATION FUNCTIONS (Dynamic Thresholding)\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,"execution":{"iopub.status.busy":"2026-08-11T15:58:30.570821Z","iopub.execute_input":"2026-08-11T15:58:30.571624Z","iopub.status.idle":"2026-08-11T15:58:30.750801Z","shell.execute_reply.started":"2026-08-11T15:58:30.571589Z","shell.execute_reply":"2026-08-11T15:58:30.749993Z"}},"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)\n\n# ============================================================================\n# 8. EXECUTION PIPELINE\n# ============================================================================\nmodel = VanillaDenseNet121(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, 'vanilla_best_model.pth')\n        print(f\"[*] 🟢 New best Vanilla 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,"execution":{"iopub.status.busy":"2026-08-11T15:58:30.751845Z","iopub.execute_input":"2026-08-11T15:58:30.752194Z","iopub.status.idle":"2026-08-11T15:58:33.632277Z","shell.execute_reply.started":"2026-08-11T15:58:30.752162Z","shell.execute_reply":"2026-08-11T15:58:33.628413Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# 9. FINAL TESTING WITH DYNAMIC THRESHOLDING\n# ============================================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\"🚀 RECOVERING BEST VANILLA MODEL FOR FINAL BENCHMARKING 🚀\")\nprint(\"=\"*60)\n\nbest_weights = torch.load('vanilla_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 VANILLA 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,"execution":{"iopub.status.busy":"2026-08-11T15:58:33.635805Z","iopub.status.idle":"2026-08-11T15:58:33.636232Z","shell.execute_reply.started":"2026-08-11T15:58:33.636028Z","shell.execute_reply":"2026-08-11T15:58:33.636053Z"}},"outputs":[],"execution_count":null}]}