{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","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":"competition","sourceId":18647,"databundleVersionId":1126921},{"sourceType":"kernelVersion","sourceId":32832152}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install imagecodecs \"numpy>=2.0,<2.1\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-09T09:50:16.108625Z","iopub.execute_input":"2026-06-09T09:50:16.108803Z","iopub.status.idle":"2026-06-09T09:50:23.119466Z","shell.execute_reply.started":"2026-06-09T09:50:16.108782Z","shell.execute_reply":"2026-06-09T09:50:23.118759Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os, random, cv2\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nimport timm\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.metrics import cohen_kappa_score\nimport albumentations as A\nimport skimage.io\nimport imagecodecs\nfrom albumentations.pytorch import ToTensorV2\nimport warnings\nwarnings.filterwarnings(\"ignore\")\n\n# --- Config ---\nsz = 128          # tile size\nN = 16            # tiles per image (keep same as iafoss)\nbs = 8           # batch size\nnfolds = 3\nSEED = 2020\nEPOCHS = 16\nTRAIN = '/kaggle/working/train_tiles/'\nLABELS = '/kaggle/input/competitions/prostate-cancer-grade-assessment/train.csv'\n\ndef seed_everything(seed):\n    random.seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.backends.cudnn.deterministic = True\n\nseed_everything(SEED)\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"Using device: {device}\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-06-09T09:50:23.121402Z","iopub.execute_input":"2026-06-09T09:50:23.121751Z","iopub.status.idle":"2026-06-09T09:50:37.377651Z","shell.execute_reply.started":"2026-06-09T09:50:23.121721Z","shell.execute_reply":"2026-06-09T09:50:37.376953Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import zipfile\n\nprint(\"Extracting tiles...\")\nwith zipfile.ZipFile('/kaggle/input/notebooks/iafoss/panda-16x128x128-tiles/train.zip', 'r') as z:\n    z.extractall('/kaggle/working/train_tiles')\n\nprint(f\"Done. Files: {len(os.listdir('/kaggle/working/train_tiles'))}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-09T09:50:37.378486Z","iopub.execute_input":"2026-06-09T09:50:37.378938Z","iopub.status.idle":"2026-06-09T09:51:06.953881Z","shell.execute_reply.started":"2026-06-09T09:50:37.378916Z","shell.execute_reply":"2026-06-09T09:51:06.953031Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class PANDADataset(Dataset):\n    def __init__(self, df, tile_dir, n_tiles=N, tile_size=sz, \n                 transform=None, is_train=True):\n        self.df = df.reset_index(drop=True)\n        self.tile_dir = tile_dir\n        self.n_tiles = n_tiles\n        self.tile_size = tile_size\n        self.transform = transform\n        self.is_train = is_train\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        image_id = row.image_id\n        \n        if 'isup_grade' in row:\n            label = row.isup_grade\n        else:\n            label = -1\n\n        tiles = []\n        for i in range(self.n_tiles):\n            path = os.path.join(self.tile_dir, f'{image_id}_{i}.png')\n            if os.path.exists(path):\n                img = np.array(Image.open(path).convert('RGB'))\n            else:\n                img = np.ones((self.tile_size, self.tile_size, 3), dtype=np.uint8) * 255\n            \n            if self.is_train and tile_transform:\n                img = tile_transform(image=img)['image']\n                \n            tiles.append(img)\n\n        if self.is_train:\n            random.shuffle(tiles)\n\n        n_cols = 4\n        n_rows = int(np.ceil(self.n_tiles / n_cols))\n        rows = []\n        for r in range(n_rows):\n            row_tiles = tiles[r*n_cols : (r+1)*n_cols]\n            while len(row_tiles) < n_cols:\n                row_tiles.append(np.ones((self.tile_size, self.tile_size, 3), \n                                         dtype=np.uint8) * 255)\n            rows.append(np.concatenate(row_tiles, axis=1))\n        image = np.concatenate(rows, axis=0)\n\n        if self.transform:\n            image = self.transform(image=image)['image']\n\n        return image, label","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-09T09:51:06.955246Z","iopub.execute_input":"2026-06-09T09:51:06.955537Z","iopub.status.idle":"2026-06-09T09:51:06.964987Z","shell.execute_reply.started":"2026-06-09T09:51:06.955505Z","shell.execute_reply":"2026-06-09T09:51:06.964087Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_transform = A.Compose([\n    A.VerticalFlip(p=0.5),\n    A.HorizontalFlip(p=0.5),\n    A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=0.75),\n    A.ShiftScaleRotate(shift_limit=0.05, scale_limit=0.1, \n                       rotate_limit=15, p=0.5),\n    A.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225]),\n    A.HueSaturationValue(hue_shift_limit=10, sat_shift_limit=20, val_shift_limit=10, p=0.5),\n    A.ColorJitter(brightness=0.1, contrast=0.1, saturation=0.1, hue=0.05, p=0.5),\n    A.GaussNoise(p=0.3),\n    ToTensorV2()\n])\n\nval_transform = A.Compose([\n    A.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225]),\n    ToTensorV2()\n])\n\ntile_transform = A.Compose([\n    A.VerticalFlip(p=0.5),\n    A.HorizontalFlip(p=0.5),\n    A.RandomRotate90(p=0.5)\n])\n\nslide_transform = A.Compose([\n    A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=0.75),\n    A.ShiftScaleRotate(shift_limit=0.05, scale_limit=0.1, rotate_limit=15, p=0.5),\n    A.HueSaturationValue(hue_shift_limit=10, sat_shift_limit=20, val_shift_limit=10, p=0.5),\n    A.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225]),\n    ToTensorV2()\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-09T09:59:15.803171Z","iopub.execute_input":"2026-06-09T09:59:15.803819Z","iopub.status.idle":"2026-06-09T09:59:15.818592Z","shell.execute_reply.started":"2026-06-09T09:59:15.803793Z","shell.execute_reply":"2026-06-09T09:59:15.817634Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class AdaptiveConcatPool2d(nn.Module):\n    def forward(self, x):\n        avg = nn.functional.adaptive_avg_pool2d(x, 1)\n        mx  = nn.functional.adaptive_max_pool2d(x, 1)\n        return torch.cat([avg, mx], dim=1)\n\nclass Model(nn.Module):\n    def __init__(self, backbone='resnet50d', n_classes=5, pretrained=True):\n        super().__init__()\n        self.enc = timm.create_model(backbone, pretrained=pretrained, num_classes=0)\n        nc = self.enc.num_features\n        self.pool = nn.Sequential(\n            AdaptiveConcatPool2d(),\n            nn.Flatten(),\n            nn.Linear(nc*2, 512),\n            nn.ReLU(inplace=True),\n            nn.Dropout(0.4) \n        )\n\n        self.cls_head = nn.Linear(512, n_classes)\n\n        self.reg_head = nn.Linear(512, 1)\n\n    def forward(self, x):\n        x = self.enc.forward_features(x)\n        x = self.pool(x)\n\n        logits = self.cls_head(x)\n\n        raw_reg = self.reg_head(x)\n        reg_out = torch.sigmoid(raw_reg) * (6.0 - (-1.0)) + (-1.0)\n        return logits, reg_out.squeeze(-1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-09T09:59:17.855277Z","iopub.execute_input":"2026-06-09T09:59:17.855893Z","iopub.status.idle":"2026-06-09T09:59:17.86241Z","shell.execute_reply.started":"2026-06-09T09:59:17.855863Z","shell.execute_reply":"2026-06-09T09:59:17.861694Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def to_ordinal(labels, n_classes=5):\n    return (labels.unsqueeze(1) > torch.arange(n_classes).to(labels.device)).float()\n\ndef train_one_fold(fold, df):\n    print(f\"\\n{'='*40}\\nFold {fold}\\n{'='*40}\")\n    \n    train_df = df[df.fold != fold]\n    val_df   = df[df.fold == fold]\n    \n    train_ds = PANDADataset(train_df, TRAIN, transform=train_transform, is_train=True)\n    val_ds   = PANDADataset(val_df,   TRAIN, transform=val_transform,   is_train=False)\n    \n    train_dl = DataLoader(train_ds, batch_size=bs, shuffle=True,  num_workers=2)\n    val_dl   = DataLoader(val_ds,   batch_size=bs, shuffle=False, num_workers=2)\n    \n    model = Model().to(device)\n    optimizer = optim.Adam(model.parameters(), lr=1e-4, weight_decay=1e-5)\n    scheduler = optim.lr_scheduler.OneCycleLR(\n        optimizer, max_lr=1e-3, \n        steps_per_epoch=len(train_dl), epochs=EPOCHS\n    )\n    criterion_cls = nn.BCEWithLogitsLoss()\n    criterion_reg = nn.MSELoss()\n    scaler = torch.cuda.amp.GradScaler()\n    \n    best_kappa = 0\n    for epoch in range(EPOCHS):\n        model.train()\n        train_loss = 0\n        for x, y in train_dl:\n            x, y = x.to(device), y.to(device)\n            optimizer.zero_grad()\n            \n            with torch.cuda.amp.autocast():\n                logits, reg_out = model(x)\n                \n                loss_cls = criterion_cls(logits, to_ordinal(y))\n                loss_reg = criterion_reg(reg_out, y.float())\n\n                loss = loss_cls + loss_reg\n            \n            scaler.scale(loss).backward()\n            scaler.step(optimizer)\n            scaler.update()\n            scheduler.step()\n            train_loss += loss.item()\n        \n        model.eval()\n        preds, targets = [], []\n        with torch.no_grad():\n            for x, y in val_dl:\n                x = x.to(device)\n                with torch.cuda.amp.autocast():\n                    logits, _ = model(x)\n                preds.extend((logits.sigmoid() > 0.5).sum(1).cpu().numpy())\n                targets.extend(y.numpy())\n        \n        kappa = cohen_kappa_score(targets, preds, weights='quadratic')\n        print(f\"Epoch {epoch+1}/{EPOCHS} | loss: {train_loss/len(train_dl):.4f} | Kappa: {kappa:.4f}\")\n        \n        if kappa > best_kappa:\n            best_kappa = kappa\n            torch.save(model.state_dict(), f'model_fold{fold}.pth')\n            print(f\"    Saved (best kappa: {best_kappa:.4f})\")\n    \n    return best_kappa","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-09T10:19:27.663192Z","iopub.execute_input":"2026-06-09T10:19:27.663515Z","iopub.status.idle":"2026-06-09T10:19:27.675548Z","shell.execute_reply.started":"2026-06-09T10:19:27.663486Z","shell.execute_reply":"2026-06-09T10:19:27.674619Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import gc\ntorch.cuda.empty_cache()\ngc.collect()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-09T10:19:29.160199Z","iopub.execute_input":"2026-06-09T10:19:29.160846Z","iopub.status.idle":"2026-06-09T10:19:29.701814Z","shell.execute_reply.started":"2026-06-09T10:19:29.160816Z","shell.execute_reply":"2026-06-09T10:19:29.701186Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df = pd.read_csv(LABELS)\n\nprint(f\"Amount of samples: {len(df)}\")\n\n# get rid of suspicious slides\nsuspicious_df = pd.read_csv('/kaggle/input/datasets/dannellyz/panda-analysis/PANDA_Suspicious_Slides.csv')\nsuspicious_ids = suspicious_df['image_id'].tolist()\ndf = df[~df.image_id.isin(suspicious_ids)].reset_index(drop=True)\n\nprint(f\"Amount of samples after removing suspicious slides: {len(df)}\")\n\n# filter all available slides\navailable = set([f[:32] for f in os.listdir(TRAIN)])\ndf = df[df.image_id.isin(available)].reset_index(drop=True)\n\n# stratified kfold\nskf = StratifiedKFold(n_splits=nfolds, shuffle=True, random_state=SEED)\ndf['fold'] = -1\nfor fold, (_, val_idx) in enumerate(skf.split(df, df.isup_grade)):\n    df.loc[val_idx, 'fold'] = fold\n\n# train all folds\nresults = []\nfor fold in range(nfolds):\n    kappa = train_one_fold(fold, df)\n    results.append(kappa)\n\nprint(f\"\\nMean Kappa across folds: {np.mean(results):.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-09T10:19:30.832106Z","iopub.execute_input":"2026-06-09T10:19:30.832827Z","iopub.status.idle":"2026-06-09T10:24:03.500319Z","shell.execute_reply.started":"2026-06-09T10:19:30.832796Z","shell.execute_reply":"2026-06-09T10:24:03.499067Z"}},"outputs":[],"execution_count":null}]}