{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"datasetVersion","sourceId":15933325,"datasetId":10194816,"databundleVersionId":16890863},{"sourceType":"datasetVersion","sourceId":4619805,"datasetId":2688675,"databundleVersionId":4681402}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"41e9815a-70a9-42fa-9ed9-00de3f5b46ab","cell_type":"markdown","source":"# Breast Cancer Detection — EfficientNetV2-S\nPaper: Prodan et al., Applied Sciences 2023\nPreprocessing: crop + windowing on-the-fly | 5-fold CV | pos_weight loss","metadata":{}},{"id":"5a6435d2-b35a-4b60-b27c-5b6661432ab4","cell_type":"markdown","source":"## 1. Setup","metadata":{}},{"id":"89a65716-1ded-4702-a708-5d19d7e82bfc","cell_type":"code","source":"import os, time, random, cv2\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom pathlib import Path\nfrom tqdm import tqdm\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms, models\nfrom sklearn.metrics import (\n    roc_auc_score, f1_score, accuracy_score,\n    confusion_matrix, classification_report, roc_curve\n)\nfrom PIL import Image\nimport warnings\nwarnings.filterwarnings('ignore')\n\nSEED = 42\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\ntorch.cuda.manual_seed_all(SEED)\n\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f'Device : {DEVICE}')\nprint(f'GPU    : {torch.cuda.get_device_name(0) if torch.cuda.is_available() else \"None\"}')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-25T19:47:35.471572Z","iopub.execute_input":"2026-04-25T19:47:35.47231Z","iopub.status.idle":"2026-04-25T19:47:44.675374Z","shell.execute_reply.started":"2026-04-25T19:47:35.472271Z","shell.execute_reply":"2026-04-25T19:47:44.674733Z"}},"outputs":[],"execution_count":null},{"id":"a5a290ee-dcc1-4908-b719-ac822d1dd94b","cell_type":"markdown","source":"## 2. Paths and Data","metadata":{}},{"id":"e46881e6-c24d-4866-ac99-5c129c3e8d2d","cell_type":"code","source":"# Add these two datasets via + Add Data:\n# 1. hananabhan/breast-cancer-paper-output  (preprocessing CSV)\n# 2. theoviel/rsna-breast-cancer-512-pngs   (original images)\nPREPROCESS_DIR = Path('/kaggle/input/datasets/hananabhan/breast-cancer-output')\nDATA_DIR       = Path('/kaggle/input/datasets/theoviel/rsna-breast-cancer-512-pngs')\nFOLD_CSV       = PREPROCESS_DIR / 'dataset_with_folds.csv'\nOUT_DIR        = Path('/kaggle/working')\nOUT_DIR.mkdir(exist_ok=True)\n\nFOLD       = 0          # validation fold (0-4)\nIMG_SIZE   = 512\nBATCH_SIZE = 16\nEPOCHS     = 20\nLR         = 3e-4\n\nfull_df  = pd.read_csv(FOLD_CSV)\ntrain_df = full_df[full_df['fold'] != FOLD].reset_index(drop=True)\nval_df   = full_df[full_df['fold'] == FOLD].reset_index(drop=True)\n\nprint(f'Fold      : {FOLD}')\nprint(f'Train     : {len(train_df)} | Pos: {train_df[\"cancer\"].sum()} | Neg: {(train_df[\"cancer\"]==0).sum()}')\nprint(f'Val       : {len(val_df)}   | Pos: {val_df[\"cancer\"].sum()}   | Neg: {(val_df[\"cancer\"]==0).sum()}')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-25T19:47:44.676599Z","iopub.execute_input":"2026-04-25T19:47:44.67758Z","iopub.status.idle":"2026-04-25T19:47:44.761283Z","shell.execute_reply.started":"2026-04-25T19:47:44.67755Z","shell.execute_reply":"2026-04-25T19:47:44.760289Z"}},"outputs":[],"execution_count":null},{"id":"73df44e3-70bc-4613-a456-7e9f0a529331","cell_type":"markdown","source":"## 3. Preprocessing + Dataset","metadata":{}},{"id":"ae89c46a-f131-4fb9-8cd5-b4cca70f0582","cell_type":"code","source":"def crop(image):\n    _, thresh = cv2.threshold(image, 10, 255, cv2.THRESH_BINARY)\n    kernel    = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))\n    thresh    = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)\n    coords    = cv2.findNonZero(thresh)\n    if coords is None:\n        return image\n    x, y, w, h = cv2.boundingRect(coords)\n    pad = 5\n    x = max(0, x - pad); y = max(0, y - pad)\n    w = min(image.shape[1] - x, w + 2*pad)\n    h = min(image.shape[0] - y, h + 2*pad)\n    return image[y:y+h, x:x+w]\n\n\ndef windowing(image, low_pct=2, high_pct=98):\n    nonzero = image[image > 0]\n    if len(nonzero) == 0:\n        return image\n    low  = np.percentile(nonzero, low_pct)\n    high = np.percentile(nonzero, high_pct)\n    image = np.clip(image, low, high)\n    image = ((image - low) / (high - low + 1e-8) * 255).astype(np.uint8)\n    return image\n\n\ndef preprocess_image(img_path, img_size):\n    img = cv2.imread(str(img_path), cv2.IMREAD_GRAYSCALE)\n    if img is None:\n        return np.zeros((img_size, img_size), dtype=np.uint8)\n    img = crop(img)\n    img = windowing(img)\n    img = cv2.resize(img, (img_size, img_size), interpolation=cv2.INTER_AREA)\n    return img\n\n\nclass MammographyDataset(Dataset):\n    def __init__(self, df, img_size, transform=None):\n        self.df        = df.reset_index(drop=True)\n        self.img_size  = img_size\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row      = self.df.iloc[idx]\n        label    = int(row['cancer'])\n        img_path = DATA_DIR / f\"{row['patient_id']}_{row['image_id']}.png\"\n        img      = preprocess_image(img_path, self.img_size)\n        img      = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)\n        img      = Image.fromarray(img)\n        if self.transform:\n            img = self.transform(img)\n        return img, torch.tensor(label, dtype=torch.float32)\n\n\nIMAGENET_MEAN = [0.485, 0.456, 0.406]\nIMAGENET_STD  = [0.229, 0.224, 0.225]\n\nbase_transforms = transforms.Compose([\n    transforms.ToTensor(),\n    transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD)\n])\n\ntrain_dataset = MammographyDataset(train_df, IMG_SIZE, transform=base_transforms)\nval_dataset   = MammographyDataset(val_df,   IMG_SIZE, transform=base_transforms)\n\ntrain_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True,\n                          num_workers=2, pin_memory=True)\nval_loader   = DataLoader(val_dataset,   batch_size=BATCH_SIZE, shuffle=False,\n                          num_workers=2, pin_memory=True)\n\nprint(f'Train batches : {len(train_loader)}')\nprint(f'Val batches   : {len(val_loader)}')\nprint('Preprocessing: crop + windowing applied on-the-fly (paper approach)')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-25T19:47:44.762514Z","iopub.execute_input":"2026-04-25T19:47:44.762882Z","iopub.status.idle":"2026-04-25T19:47:44.777927Z","shell.execute_reply.started":"2026-04-25T19:47:44.762845Z","shell.execute_reply":"2026-04-25T19:47:44.777285Z"}},"outputs":[],"execution_count":null},{"id":"a520a003-7915-4fa6-8326-4336431c1f89","cell_type":"markdown","source":"## 4. Model — EfficientNetV2-S","metadata":{}},{"id":"35c0082f-402d-4e08-8a99-036ddcab9460","cell_type":"code","source":"def build_efficientnet():\n    model = models.efficientnet_v2_s(weights=models.EfficientNet_V2_S_Weights.IMAGENET1K_V1)\n    for param in model.parameters():\n        param.requires_grad = False\n    # Unfreeze last 2 blocks\n    for block in list(model.features.children())[-2:]:\n        for param in block.parameters():\n            param.requires_grad = True\n    in_features = model.classifier[1].in_features\n    model.classifier = nn.Sequential(\n        nn.Dropout(p=0.3),\n        nn.Linear(in_features, 256),\n        nn.ReLU(),\n        nn.Dropout(p=0.2),\n        nn.Linear(256, 1)\n    )\n    return model\n\n\nmodel = build_efficientnet().to(DEVICE)\ntotal     = sum(p.numel() for p in model.parameters())\ntrainable = sum(p.numel() for p in model.parameters() if p.requires_grad)\nprint(f'Total params     : {total:,}')\nprint(f'Trainable params : {trainable:,}')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-25T19:47:44.779565Z","iopub.execute_input":"2026-04-25T19:47:44.779905Z","iopub.status.idle":"2026-04-25T19:47:46.127415Z","shell.execute_reply.started":"2026-04-25T19:47:44.779883Z","shell.execute_reply":"2026-04-25T19:47:46.12666Z"}},"outputs":[],"execution_count":null},{"id":"2f44e70c-287d-492a-9364-cc943feef8c3","cell_type":"markdown","source":"## 5. Loss, Optimizer, Scheduler","metadata":{}},{"id":"efa42f5a-7051-40af-a83d-29022736a93e","cell_type":"code","source":"n_neg      = (train_df['cancer'] == 0).sum()\nn_pos      = (train_df['cancer'] == 1).sum()\npos_weight = torch.tensor([n_neg / n_pos], dtype=torch.float32).to(DEVICE)\nprint(f'pos_weight : {pos_weight.item():.4f}')\n\ncriterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight)\noptimizer = optim.AdamW(\n    filter(lambda p: p.requires_grad, model.parameters()),\n    lr=LR, weight_decay=1e-4\n)\nscheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS, eta_min=1e-6)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-25T19:47:46.128287Z","iopub.execute_input":"2026-04-25T19:47:46.12885Z","iopub.status.idle":"2026-04-25T19:47:46.141073Z","shell.execute_reply.started":"2026-04-25T19:47:46.128825Z","shell.execute_reply":"2026-04-25T19:47:46.140404Z"}},"outputs":[],"execution_count":null},{"id":"3c05dc7b-1d7a-494c-9eea-bb02e515016d","cell_type":"markdown","source":"## 6. Training Functions","metadata":{}},{"id":"c25ff746-7f4f-47f4-8d3b-ca4b925c8042","cell_type":"code","source":"def train_one_epoch(model, loader, criterion, optimizer):\n    model.train()\n    total_loss = 0\n    all_labels, all_preds = [], []\n    for images, labels in tqdm(loader, desc='Train', leave=False):\n        images = images.to(DEVICE)\n        labels = labels.to(DEVICE).unsqueeze(1)\n        optimizer.zero_grad()\n        outputs = model(images)\n        loss    = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n        total_loss += loss.item()\n        all_preds.extend(torch.sigmoid(outputs).detach().cpu().numpy().flatten())\n        all_labels.extend(labels.cpu().numpy().flatten())\n    return total_loss / len(loader), roc_auc_score(all_labels, all_preds)\n\n\ndef evaluate(model, loader, criterion):\n    model.eval()\n    total_loss = 0\n    all_labels, all_preds = [], []\n    with torch.no_grad():\n        for images, labels in tqdm(loader, desc='Eval', leave=False):\n            images = images.to(DEVICE)\n            labels = labels.to(DEVICE).unsqueeze(1)\n            outputs    = model(images)\n            loss       = criterion(outputs, labels)\n            total_loss += loss.item()\n            all_preds.extend(torch.sigmoid(outputs).cpu().numpy().flatten())\n            all_labels.extend(labels.cpu().numpy().flatten())\n    avg_loss  = total_loss / len(loader)\n    auc       = roc_auc_score(all_labels, all_preds)\n    preds_bin = [1 if p >= 0.5 else 0 for p in all_preds]\n    f1        = f1_score(all_labels, preds_bin, zero_division=0)\n    acc       = accuracy_score(all_labels, preds_bin)\n    return avg_loss, auc, f1, acc, all_labels, all_preds\n\n\nprint('Functions defined.')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-25T19:47:46.142035Z","iopub.execute_input":"2026-04-25T19:47:46.142395Z","iopub.status.idle":"2026-04-25T19:47:46.152112Z","shell.execute_reply.started":"2026-04-25T19:47:46.142372Z","shell.execute_reply":"2026-04-25T19:47:46.151302Z"}},"outputs":[],"execution_count":null},{"id":"ce453bc9-d58d-41d9-8f9c-2fc0ab3cda4d","cell_type":"markdown","source":"## 7. Training Loop","metadata":{}},{"id":"f116d7f3-133d-4816-8605-da07ce4f2950","cell_type":"code","source":"PATIENCE     = 5\npatience_cnt = 0\nhistory      = {'train_loss': [], 'train_auc': [], 'val_loss': [], 'val_auc': [], 'val_f1': []}\nbest_auc     = 0.0\nbest_epoch   = 0\n\nfor epoch in range(1, EPOCHS + 1):\n    train_loss, train_auc = train_one_epoch(model, train_loader, criterion, optimizer)\n    val_loss, val_auc, val_f1, val_acc, _, _ = evaluate(model, val_loader, criterion)\n    scheduler.step()\n    history['train_loss'].append(train_loss)\n    history['train_auc'].append(train_auc)\n    history['val_loss'].append(val_loss)\n    history['val_auc'].append(val_auc)\n    history['val_f1'].append(val_f1)\n    print(f'Epoch {epoch:02d}/{EPOCHS} | '\n          f'Train Loss: {train_loss:.4f} | Train AUC: {train_auc:.4f} | '\n          f'Val Loss: {val_loss:.4f} | Val AUC: {val_auc:.4f} | '\n          f'Val F1: {val_f1:.4f} | Val Acc: {val_acc:.4f}')\n    if val_auc > best_auc:\n        best_auc = val_auc; best_epoch = epoch; patience_cnt = 0\n        torch.save(model.state_dict(), OUT_DIR / 'efficientnet_best.pth')\n        print(f'  -- Best model saved (AUC: {best_auc:.4f})')\n    else:\n        patience_cnt += 1\n        if patience_cnt >= PATIENCE:\n            print(f'  -- Early stopping at epoch {epoch}')\n            break\n\nprint(f'\\nBest Val AUC: {best_auc:.4f} at epoch {best_epoch}')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-25T19:47:46.153148Z","iopub.execute_input":"2026-04-25T19:47:46.153496Z","iopub.status.idle":"2026-04-25T20:40:50.254563Z","shell.execute_reply.started":"2026-04-25T19:47:46.153456Z","shell.execute_reply":"2026-04-25T20:40:50.253084Z"}},"outputs":[],"execution_count":null},{"id":"9476c799-2764-4c5d-88da-b6547bfddfc6","cell_type":"markdown","source":"## 8. Training Curves","metadata":{}},{"id":"499631a2-4452-47dc-8bbd-ee0d639debcf","cell_type":"code","source":"epochs_range = range(1, len(history['train_loss']) + 1)\nfig, axes = plt.subplots(1, 3, figsize=(16, 4))\naxes[0].plot(epochs_range, history['train_loss'], label='Train', color='steelblue')\naxes[0].plot(epochs_range, history['val_loss'],   label='Val',   color='tomato')\naxes[0].set_title('Loss'); axes[0].set_xlabel('Epoch'); axes[0].legend()\naxes[1].plot(epochs_range, history['train_auc'], label='Train', color='steelblue')\naxes[1].plot(epochs_range, history['val_auc'],   label='Val',   color='tomato')\naxes[1].set_title('AUC-ROC'); axes[1].set_xlabel('Epoch'); axes[1].legend()\naxes[2].plot(epochs_range, history['val_f1'], color='seagreen')\naxes[2].set_title('Val F1'); axes[2].set_xlabel('Epoch')\nplt.tight_layout()\nplt.savefig(OUT_DIR / 'training_curves.png', bbox_inches='tight')\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-25T20:40:54.474387Z","iopub.execute_input":"2026-04-25T20:40:54.475264Z","iopub.status.idle":"2026-04-25T20:40:55.526538Z","shell.execute_reply.started":"2026-04-25T20:40:54.475178Z","shell.execute_reply":"2026-04-25T20:40:55.525723Z"}},"outputs":[],"execution_count":null},{"id":"4f3bfeb9-650b-4400-a786-4aef0ced821c","cell_type":"markdown","source":"## 10. Evaluation","metadata":{}},{"id":"9d7ba09c-c5cc-48cf-90a3-01f6211c306a","cell_type":"code","source":"model.load_state_dict(torch.load(OUT_DIR / 'efficientnet_best.pth'))\n\nstart = time.time()\ntest_loss, test_auc, test_f1, test_acc, test_labels, test_preds = evaluate(model, val_loader, criterion)\nlatency = (time.time() - start) / len(val_dataset) * 1000\n\nprint(f'=== EfficientNetV2-S Results ===')\nprint(f'AUC-ROC  : {test_auc:.4f}')\nprint(f'F1 Score : {test_f1:.4f}')\nprint(f'Accuracy : {test_acc:.4f}')\nprint(f'Latency  : {latency:.2f} ms/image')\n\npreds_bin = [1 if p >= 0.5 else 0 for p in test_preds]\ncm = confusion_matrix(test_labels, preds_bin)\nfig, axes = plt.subplots(1, 2, figsize=(11, 4))\naxes[0].imshow(cm, cmap='Blues')\naxes[0].set_xticks([0,1]); axes[0].set_yticks([0,1])\naxes[0].set_xticklabels(['Neg','Pos']); axes[0].set_yticklabels(['Neg','Pos'])\naxes[0].set_xlabel('Predicted'); axes[0].set_ylabel('Actual')\naxes[0].set_title('Confusion Matrix')\nfor i in range(2):\n    for j in range(2):\n        axes[0].text(j, i, cm[i,j], ha='center', va='center', fontsize=14,\n                     color='white' if cm[i,j] > cm.max()/2 else 'black')\nfpr, tpr, _ = roc_curve(test_labels, test_preds)\naxes[1].plot(fpr, tpr, linewidth=2, label=f'EfficientNetV2-S AUC={test_auc:.4f}')\naxes[1].plot([0,1],[0,1],'k--')\naxes[1].set_xlabel('FPR'); axes[1].set_ylabel('TPR')\naxes[1].set_title('ROC Curve'); axes[1].legend()\nplt.tight_layout()\nplt.savefig(OUT_DIR / 'evaluation.png', bbox_inches='tight')\nplt.show()\nprint(classification_report(test_labels, preds_bin, target_names=['Negative','Positive']))\n\ntotal_params = sum(p.numel() for p in model.parameters())\nresults = {\n    'model': 'EfficientNetV2-S',\n    'fold': FOLD,\n    'auc': round(test_auc, 4),\n    'f1': round(test_f1, 4),\n    'accuracy': round(test_acc, 4),\n    'latency_ms': round(latency, 2),\n    'total_params': total_params,\n    'best_epoch': best_epoch\n}\npd.DataFrame([results]).to_csv(OUT_DIR / 'efficientnet_results.csv', index=False)\nprint(pd.DataFrame([results]).T.to_string(header=False))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-25T20:41:21.622276Z","iopub.execute_input":"2026-04-25T20:41:21.623083Z","iopub.status.idle":"2026-04-25T20:42:11.266636Z","shell.execute_reply.started":"2026-04-25T20:41:21.623033Z","shell.execute_reply":"2026-04-25T20:42:11.265906Z"}},"outputs":[],"execution_count":null},{"id":"57959fa5-94ed-4055-bf9b-d5b5ebd9f3d8","cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}