{"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":11848,"databundleVersionId":862157,"sourceType":"competition"}],"dockerImageVersionId":31154,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"\nimport os\nimport math\nimport random\nimport time\nimport gc\nfrom pathlib import Path\nfrom dataclasses import dataclass, asdict\n\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader, WeightedRandomSampler\nfrom torch.optim.lr_scheduler import CosineAnnealingLR\nfrom torchvision import models, transforms\n\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.metrics import roc_auc_score\n\nSEED = 42\ndef set_seed(seed=SEED):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n\nset_seed()\n\nDEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(\"Device:\", DEVICE)\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-10-22T18:02:29.971978Z","iopub.execute_input":"2025-10-22T18:02:29.972232Z","iopub.status.idle":"2025-10-22T18:02:36.911744Z","shell.execute_reply.started":"2025-10-22T18:02:29.972212Z","shell.execute_reply":"2025-10-22T18:02:36.910863Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# Try to autodetect Kaggle input path. Fallback to a relative path.\nDEFAULT_KAGGLE_DIR = Path(\"/kaggle/input/histopathologic-cancer-detection\")\nLOCAL_DIR = Path(\"../input/histopathologic-cancer-detection\")\n\nif DEFAULT_KAGGLE_DIR.exists():\n    DATA_DIR = DEFAULT_KAGGLE_DIR\nelif LOCAL_DIR.exists():\n    DATA_DIR = LOCAL_DIR\nelse:\n    # Edit this path if running elsewhere\n    DATA_DIR = Path(\"./histopathologic-cancer-detection\")\n\nTRAIN_DIR = DATA_DIR / \"train\"\nTEST_DIR  = DATA_DIR / \"test\"\nLABELS_CSV = DATA_DIR / \"train_labels.csv\"\n\nprint(\"DATA_DIR:\", DATA_DIR)\nprint(\"TRAIN_DIR:\", TRAIN_DIR.exists(), \"TEST_DIR:\", TEST_DIR.exists(), \"LABELS_CSV:\", LABELS_CSV.exists())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T18:02:36.913256Z","iopub.execute_input":"2025-10-22T18:02:36.913764Z","iopub.status.idle":"2025-10-22T18:02:36.920700Z","shell.execute_reply.started":"2025-10-22T18:02:36.913744Z","shell.execute_reply":"2025-10-22T18:02:36.920096Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n@dataclass\nclass Config:\n    image_size: int = 128              # ResNet can take 128x128\n    batch_size: int = 256\n    epochs: int = 2\n    lr: float = 3e-4\n    weight_decay: float = 1e-4\n    num_workers: int = 2\n    folds: int = 5\n    train_fold: int = 0                # which fold to train in this run\n    patience: int = 2                  # early stopping patience\n    t_max: int = 6                     # CosineAnnealingLR T_max\n    model_name: str = \"resnet18\"\n    save_dir: str = \"./checkpoints\"\n    mixed_precision: bool = True\n\nCFG = Config()\nos.makedirs(CFG.save_dir, exist_ok=True)\nprint(asdict(CFG))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T18:02:36.921336Z","iopub.execute_input":"2025-10-22T18:02:36.921570Z","iopub.status.idle":"2025-10-22T18:02:36.936764Z","shell.execute_reply.started":"2025-10-22T18:02:36.921549Z","shell.execute_reply":"2025-10-22T18:02:36.936214Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nclass HistoDataset(Dataset):\n    def __init__(self, df, img_dir, image_size=128, is_train=True):\n        self.df = df.reset_index(drop=True).copy()\n        self.img_dir = Path(img_dir)\n        self.is_train = is_train\n        size = image_size\n\n        self.train_tfms = transforms.Compose([\n            transforms.RandomHorizontalFlip(),\n            transforms.RandomVerticalFlip(),\n            transforms.RandomRotation(15),\n            transforms.ColorJitter(brightness=0.1, contrast=0.1, saturation=0.1, hue=0.02),\n            transforms.Resize((size, size)),\n            transforms.ToTensor(),\n            transforms.Normalize(mean=[0.5,0.5,0.5], std=[0.25,0.25,0.25]),\n        ])\n        self.valid_tfms = transforms.Compose([\n            transforms.Resize((size, size)),\n            transforms.ToTensor(),\n            transforms.Normalize(mean=[0.5,0.5,0.5], std=[0.25,0.25,0.25]),\n        ])\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_id = row[\"id\"]\n        path = self.img_dir / f\"{img_id}.tif\"\n        with Image.open(path) as im:\n            im = im.convert(\"RGB\")\n            if self.is_train:\n                im = self.train_tfms(im)\n            else:\n                im = self.valid_tfms(im)\n\n        if \"label\" in row:\n            y = torch.tensor(row[\"label\"], dtype=torch.float32)\n            return im, y\n        else:\n            return im, img_id\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T18:02:36.938219Z","iopub.execute_input":"2025-10-22T18:02:36.938469Z","iopub.status.idle":"2025-10-22T18:02:36.948147Z","shell.execute_reply.started":"2025-10-22T18:02:36.938453Z","shell.execute_reply":"2025-10-22T18:02:36.947370Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\ndef build_model(model_name=\"resnet18\"):\n    if model_name == \"resnet18\":\n        model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1)\n        # Replace the final layer for binary classification\n        in_features = model.fc.in_features\n        model.fc = nn.Linear(in_features, 1)\n    else:\n        raise ValueError(\"Unsupported model_name\")\n    return model\n\ndef get_class_weights(labels):\n    # Compute positive and negative weights for BCEWithLogitsLoss pos_weight\n    pos = labels.sum()\n    neg = len(labels) - pos\n    if pos == 0:\n        return torch.tensor([1.0])\n    pos_weight = torch.tensor([neg / max(pos, 1.0)])\n    return pos_weight\n\ndef train_one_epoch(model, loader, criterion, optimizer, scaler=None):\n    model.train()\n    running_loss = 0.0\n    for imgs, targets in loader:\n        imgs = imgs.to(DEVICE)\n        targets = targets.view(-1, 1).to(DEVICE)\n\n        optimizer.zero_grad(set_to_none=True)\n        if scaler is not None:\n            with torch.autocast(device_type=DEVICE if DEVICE != \"cpu\" else \"cpu\", dtype=torch.float16, enabled=True):\n                logits = model(imgs)\n                loss = criterion(logits, targets)\n            scaler.scale(loss).backward()\n            scaler.step(optimizer)\n            scaler.update()\n        else:\n            logits = model(imgs)\n            loss = criterion(logits, targets)\n            loss.backward()\n            optimizer.step()\n\n        running_loss += loss.item() * imgs.size(0)\n    return running_loss / len(loader.dataset)\n\n@torch.no_grad()\ndef validate(model, loader, criterion):\n    model.eval()\n    running_loss = 0.0\n    probs_list = []\n    targets_list = []\n    for imgs, targets in loader:\n        imgs = imgs.to(DEVICE)\n        targets = targets.view(-1, 1).to(DEVICE)\n        logits = model(imgs)\n        loss = criterion(logits, targets)\n        running_loss += loss.item() * imgs.size(0)\n        probs = torch.sigmoid(logits).detach().cpu().numpy().ravel().tolist()\n        probs_list.extend(probs)\n        targets_list.extend(targets.detach().cpu().numpy().ravel().tolist())\n\n    avg_loss = running_loss / len(loader.dataset)\n    try:\n        auc = roc_auc_score(targets_list, probs_list)\n    except Exception:\n        auc = float(\"nan\")\n    return avg_loss, auc, np.array(probs_list), np.array(targets_list)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T18:02:36.948785Z","iopub.execute_input":"2025-10-22T18:02:36.949027Z","iopub.status.idle":"2025-10-22T18:02:36.961960Z","shell.execute_reply.started":"2025-10-22T18:02:36.949006Z","shell.execute_reply":"2025-10-22T18:02:36.961257Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nassert LABELS_CSV.exists(), \"train_labels.csv not found. Please set DATA_DIR correctly.\"\nlabels_df = pd.read_csv(LABELS_CSV)\nprint(labels_df.head(), labels_df.shape, labels_df.label.mean())\nskf = StratifiedKFold(n_splits=CFG.folds, shuffle=True, random_state=SEED)\n\nfolds = []\nfor f, (_, val_idx) in enumerate(skf.split(labels_df[\"id\"], labels_df[\"label\"])):\n    fold = labels_df.copy()\n    fold[\"fold\"] = -1\n    fold.loc[val_idx, \"fold\"] = f\n    folds.append(fold)\n\n# Use one fold for a quick baseline\ndf = folds[CFG.train_fold]\ntrain_df = df[df.fold != CFG.train_fold].drop(columns=[\"fold\"])\nvalid_df = df[df.fold == CFG.train_fold].drop(columns=[\"fold\"])\n\nprint(\"Train size:\", len(train_df), \"Valid size:\", len(valid_df))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T18:02:36.962553Z","iopub.execute_input":"2025-10-22T18:02:36.962853Z","iopub.status.idle":"2025-10-22T18:02:37.412268Z","shell.execute_reply.started":"2025-10-22T18:02:36.962805Z","shell.execute_reply":"2025-10-22T18:02:37.411518Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\ntrain_ds = HistoDataset(train_df, TRAIN_DIR, image_size=CFG.image_size, is_train=True)\nvalid_ds = HistoDataset(valid_df, TRAIN_DIR, image_size=CFG.image_size, is_train=False)\n\n# Weighted sampling to reduce class imbalance\nlabels_np = train_df[\"label\"].values\nclass_sample_count = np.array([len(np.where(labels_np == t)[0]) for t in [0,1]])\nweights = 1. / class_sample_count\nsamples_weight = np.array([weights[t] for t in labels_np])\nsamples_weight = torch.from_numpy(samples_weight).float()\nsampler = WeightedRandomSampler(samples_weight, num_samples=len(samples_weight), replacement=True)\n\ntrain_loader = DataLoader(train_ds, batch_size=CFG.batch_size, sampler=sampler,\n                          num_workers=CFG.num_workers, pin_memory=True, drop_last=False)\nvalid_loader = DataLoader(valid_ds, batch_size=CFG.batch_size, shuffle=False,\n                          num_workers=CFG.num_workers, pin_memory=True, drop_last=False)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T18:02:37.413057Z","iopub.execute_input":"2025-10-22T18:02:37.413298Z","iopub.status.idle":"2025-10-22T18:02:37.473470Z","shell.execute_reply.started":"2025-10-22T18:02:37.413279Z","shell.execute_reply":"2025-10-22T18:02:37.472860Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nmodel = build_model(CFG.model_name).to(DEVICE)\n\n# Loss\npos_weight = get_class_weights(train_df[\"label\"].values).to(DEVICE)\ncriterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight)\n\noptimizer = optim.AdamW(model.parameters(), lr=CFG.lr, weight_decay=CFG.weight_decay)\nscheduler = CosineAnnealingLR(optimizer, T_max=CFG.t_max)\n\nscaler = torch.cuda.amp.GradScaler(enabled=CFG.mixed_precision and DEVICE == \"cuda\")\n\nbest_auc = -1.0\nbest_path = os.path.join(CFG.save_dir, f\"{CFG.model_name}_fold{CFG.train_fold}.pt\")\nno_improve = 0\n\nfor epoch in range(1, CFG.epochs + 1):\n    t0 = time.time()\n    train_loss = train_one_epoch(model, train_loader, criterion, optimizer, scaler=scaler)\n    val_loss, val_auc, _, _ = validate(model, valid_loader, criterion)\n    scheduler.step()\n    dt = time.time() - t0\n    print(f\"Epoch {epoch:02d} | {dt:.1f}s | train_loss {train_loss:.4f} | val_loss {val_loss:.4f} | val_auc {val_auc:.4f}\")\n\n    if val_auc > best_auc:\n        best_auc = val_auc\n        torch.save({\"model\": model.state_dict(), \"cfg\": asdict(CFG)}, best_path)\n        no_improve = 0\n        print(\"Saved new best model to\", best_path)\n    else:\n        no_improve += 1\n        if no_improve >= CFG.patience:\n            print(\"Early stopping triggered\")\n            break\n\nprint(\"Best AUC:\", best_auc)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T18:02:37.474224Z","iopub.execute_input":"2025-10-22T18:02:37.474532Z","iopub.status.idle":"2025-10-22T18:27:39.925147Z","shell.execute_reply.started":"2025-10-22T18:02:37.474509Z","shell.execute_reply":"2025-10-22T18:27:39.924181Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# Build test loader\nassert TEST_DIR.exists(), \"test directory not found\"\ntest_ids = [p.stem for p in sorted(TEST_DIR.glob(\"*.tif\"))]\ntest_df = pd.DataFrame({\"id\": test_ids})\n\ntest_ds = HistoDataset(test_df, TEST_DIR, image_size=CFG.image_size, is_train=False)\ntest_loader = DataLoader(test_ds, batch_size=CFG.batch_size, shuffle=False,\n                         num_workers=CFG.num_workers, pin_memory=True, drop_last=False)\n\n# Load best checkpoint\nckpt = torch.load(best_path, map_location=DEVICE)\nmodel = build_model(CFG.model_name).to(DEVICE)\nmodel.load_state_dict(ckpt[\"model\"])\nmodel.eval()\n\nprobs = []\nwith torch.no_grad():\n    for imgs, ids in test_loader:\n        imgs = imgs.to(DEVICE)\n        logits = model(imgs)\n        p = torch.sigmoid(logits).detach().cpu().numpy().ravel()\n        probs.extend(p.tolist())\n\nsubmission = pd.DataFrame({\"id\": test_df[\"id\"], \"label\": np.array(probs)})\nsubmission_path = \"submission.csv\"\nsubmission.to_csv(submission_path, index=False)\nprint(\"Wrote\", submission_path, \"with\", len(submission), \"rows\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T18:27:39.926396Z","iopub.execute_input":"2025-10-22T18:27:39.927014Z","iopub.status.idle":"2025-10-22T18:32:30.006358Z","shell.execute_reply.started":"2025-10-22T18:27:39.926989Z","shell.execute_reply":"2025-10-22T18:32:30.005227Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# Sanity check on validation predictions and optimal threshold\nval_loss, val_auc, val_probs, val_targets = validate(model, valid_loader, criterion)\nprint(\"Validation AUC with reloaded model:\", val_auc)\n\n# Find a threshold that maximizes F1 on the validation set\nbest_thr, best_f1 = 0.5, -1\nfor thr in np.linspace(0.1, 0.9, 17):\n    preds = (val_probs >= thr).astype(int)\n    tp = ((preds == 1) & (val_targets == 1)).sum()\n    fp = ((preds == 1) & (val_targets == 0)).sum()\n    fn = ((preds == 0) & (val_targets == 1)).sum()\n    precision = tp / max(tp + fp, 1)\n    recall = tp / max(tp + fn, 1)\n    f1 = 2 * precision * recall / max(precision + recall, 1e-9)\n    if f1 > best_f1:\n        best_f1, best_thr = f1, thr\nprint(f\"Best F1 {best_f1:.4f} at thr {best_thr:.2f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-22T18:32:30.008968Z","iopub.execute_input":"2025-10-22T18:32:30.009210Z","iopub.status.idle":"2025-10-22T18:33:19.286928Z","shell.execute_reply.started":"2025-10-22T18:32:30.009190Z","shell.execute_reply":"2025-10-22T18:33:19.285948Z"}},"outputs":[],"execution_count":null}]}