{"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":"none","dataSources":[{"sourceId":11848,"databundleVersionId":862157,"sourceType":"competition"}],"dockerImageVersionId":31234,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import pandas as pd\n\nroot = \"/kaggle/input/histopathologic-cancer-detection\"\ndf = pd.read_csv(f\"{root}/train_labels.csv\")\nprint(df.head())\nprint(\"rows:\", len(df))\nprint(df[\"label\"].value_counts())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T18:01:32.740625Z","iopub.execute_input":"2026-01-04T18:01:32.740967Z","iopub.status.idle":"2026-01-04T18:01:35.350088Z","shell.execute_reply.started":"2026-01-04T18:01:32.740937Z","shell.execute_reply":"2026-01-04T18:01:35.348450Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#随机病理图像示例\nimport os\nimport random\nimport matplotlib.pyplot as plt\nfrom PIL import Image\n\n# 图像文件夹路径（按你 notebook 实际路径）\nimg_dir = \"/kaggle/input/histopathologic-cancer-detection/train\"\n\n# 随机选一张图\nimg_name = random.choice(os.listdir(img_dir))\nimg_path = os.path.join(img_dir, img_name)\n\n# 读取并显示\nimg = Image.open(img_path)\n\nplt.figure(figsize=(4, 4))\nplt.imshow(img)\nplt.axis(\"off\")\nplt.title(\"Random histopathology image sample\")\n\n# 保存图片（关键）\nplt.savefig(\"figure_4_1_random_image.png\", dpi=300, bbox_inches=\"tight\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T18:02:21.346159Z","iopub.execute_input":"2026-01-04T18:02:21.346637Z","iopub.status.idle":"2026-01-04T18:02:27.493485Z","shell.execute_reply.started":"2026-01-04T18:02:21.346604Z","shell.execute_reply":"2026-01-04T18:02:27.492448Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================\n# 随机病理图像示例（真实分辨率展示，避免模糊）\n# =========================\nimport os\nimport random\nimport matplotlib.pyplot as plt\nfrom PIL import Image\n\nimg_dir = \"/kaggle/input/histopathologic-cancer-detection/train\"\n\nvalid_ext = (\".png\", \".jpg\", \".jpeg\", \".tif\", \".tiff\")\nimg_files = [f for f in os.listdir(img_dir) if f.lower().endswith(valid_ext)]\nimg_name = random.choice(img_files)\nimg_path = os.path.join(img_dir, img_name)\n\nimg = Image.open(img_path).convert(\"RGB\")\n\n# ---- 关键点：figsize 按“原始分辨率比例”来 ----\n# 96px ÷ 96 DPI ≈ 1 inch → 接近原始显示\nfig, ax = plt.subplots(figsize=(1.5, 1.5), dpi=96)\n\nax.imshow(img, interpolation=\"nearest\")\nax.set_axis_off()\nax.set_title(\"Random histopathology image sample\", fontsize=8)\n\n# 保存时再用高 DPI（用于论文）\nfig.savefig(\n    \"figure_4_1_random_image.png\",\n    dpi=300,\n    bbox_inches=\"tight\",\n    pad_inches=0.02\n)\n\nplt.show()\n\nprint(f\"Image: {img_name}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T02:05:55.561128Z","iopub.execute_input":"2026-01-07T02:05:55.561580Z","iopub.status.idle":"2026-01-07T02:05:59.553072Z","shell.execute_reply.started":"2026-01-07T02:05:55.561550Z","shell.execute_reply":"2026-01-07T02:05:59.552246Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#标签分布柱状图\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\n# 读取标签\ncsv_path = \"/kaggle/input/histopathologic-cancer-detection/train_labels.csv\"\ndf = pd.read_csv(csv_path)\n\n# 统计并画图\nlabel_counts = df[\"label\"].value_counts()\n\nplt.figure(figsize=(4, 4))\nlabel_counts.plot(kind=\"bar\")\nplt.xticks(rotation=0)\nplt.xlabel(\"Label\")\nplt.ylabel(\"Number of samples\")\nplt.title(\"Label distribution\")\n\n# 保存图片\nplt.savefig(\"figure_4_1_label_distribution.png\", dpi=300, bbox_inches=\"tight\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T18:03:22.447456Z","iopub.execute_input":"2026-01-04T18:03:22.447775Z","iopub.status.idle":"2026-01-04T18:03:23.121038Z","shell.execute_reply.started":"2026-01-04T18:03:22.447752Z","shell.execute_reply":"2026-01-04T18:03:23.119830Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================\n# 0) 导入\n# =========================\nimport os\nimport random\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport torchvision.transforms as T\nimport torchvision.models as models\n\n# 为了复现性（可选）\ndef seed_everything(seed=42):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n\nseed_everything(42)\n\n# =========================\n# 1) 路径检查\n# =========================\nroot = \"/kaggle/input/histopathologic-cancer-detection\"\nprint(\"root exists:\", os.path.exists(root))\nprint(\"root content:\", os.listdir(root))\n\ntrain_csv = os.path.join(root, \"train_labels.csv\")\ntrain_dir = os.path.join(root, \"train\")\n\nprint(\"train_csv exists:\", os.path.exists(train_csv))\nprint(\"train_dir exists:\", os.path.exists(train_dir))\n\n# =========================\n# 2) 读取标签 + 抽样（跑得快）\n# =========================\ndf = pd.read_csv(train_csv)  # columns: id,label\nprint(\"total rows:\", len(df))\nprint(\"label counts:\\n\", df[\"label\"].value_counts())\n\n# 抽 20000 张，先跑通（你以后可以改大）\ndf_small = df.sample(20000, random_state=42).reset_index(drop=True)\nprint(\"df_small rows:\", len(df_small))\n\n# =========================\n# 3) Dataset\n# =========================\nclass PatchDataset(Dataset):\n    def __init__(self, df, img_dir):\n        self.df = df\n        self.img_dir = img_dir\n\n        # 这个数据本来就是 96x96，小尺寸训练更适合CPU\n        self.tf = T.Compose([\n            T.Resize((96, 96)),\n            T.ToTensor(),\n            # 不用预训练时，norm 用 0.5/0.5 也行；用 ImageNet norm 也可\n            T.Normalize(mean=[0.5, 0.5, 0.5],\n                        std=[0.5, 0.5, 0.5]),\n        ])\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        img_id = self.df.loc[idx, \"id\"]\n        label = float(self.df.loc[idx, \"label\"])  # 0/1\n        img_path = os.path.join(self.img_dir, f\"{img_id}.tif\")\n\n        img = Image.open(img_path).convert(\"RGB\")\n        x = self.tf(img)\n        y = torch.tensor(label, dtype=torch.float32)\n        return x, y\n\ntrain_ds = PatchDataset(df_small, train_dir)\ntrain_loader = DataLoader(\n    train_ds,\n    batch_size=64,\n    shuffle=True,\n    num_workers=2,\n    pin_memory=True\n)\n\n# =========================\n# 4) 模型（关键：weights=None，避免下载失败）\n# =========================\ndevice = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(\"Using device:\", device)\n\nmodel = models.resnet18(weights=None)  # <- 不会去联网下载\nmodel.fc = nn.Linear(model.fc.in_features, 1)  # 输出一个logit\nmodel = model.to(device)\n\ncriterion = nn.BCEWithLogitsLoss()\noptimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4)\n\n# =========================\n# 5) 训练（先跑 300 step 看效果）\n# =========================\nmodel.train()\nfor step, (x, y) in enumerate(train_loader):\n    x = x.to(device, non_blocking=True)\n    y = y.to(device, non_blocking=True)\n\n    logits = model(x).squeeze(1)   # [B]\n    loss = criterion(logits, y)\n\n    optimizer.zero_grad(set_to_none=True)\n    loss.backward()\n    optimizer.step()\n\n    if step % 50 == 0:\n        with torch.no_grad():\n            prob = torch.sigmoid(logits)\n            acc = ((prob >= 0.5).float() == y).float().mean().item()\n        print(f\"step={step:04d} loss={loss.item():.4f} acc={acc:.4f}\")\n\n    if step == 300:\n        break\n\nprint(\"Training loop finished.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T02:06:53.510244Z","iopub.execute_input":"2026-01-07T02:06:53.510600Z","iopub.status.idle":"2026-01-07T02:15:26.175220Z","shell.execute_reply.started":"2026-01-07T02:06:53.510571Z","shell.execute_reply":"2026-01-07T02:15:26.174032Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# Attention-MIL on Histopathologic Cancer Detection (Pseudo-bags)\n# No internet needed, CPU OK.\n# =========================================================\nimport os\nimport random\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport torchvision.transforms as T\nimport torchvision.models as models\n\n# -------------------------\n# 0) Utils\n# -------------------------\ndef seed_everything(seed=42):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n\nseed_everything(42)\n\ndevice = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(\"Using device:\", device)\n\n# -------------------------\n# 1) Paths + Load CSV\n# -------------------------\nroot = \"/kaggle/input/histopathologic-cancer-detection\"\ntrain_csv = os.path.join(root, \"train_labels.csv\")\ntrain_dir = os.path.join(root, \"train\")\n\ndf = pd.read_csv(train_csv)   # columns: id,label\nprint(\"total rows:\", len(df))\nprint(\"label counts:\\n\", df[\"label\"].value_counts())\n\n# 为了让CPU也能较快：抽一个子集\n# 你之后可以把 sample_n 改大，比如 80000 / 150000\nsample_n = 40000\ndf = df.sample(sample_n, random_state=42).reset_index(drop=True)\nprint(\"working subset rows:\", len(df))\n\ndf0 = df[df[\"label\"] == 0].reset_index(drop=True)\ndf1 = df[df[\"label\"] == 1].reset_index(drop=True)\nprint(\"subset label0:\", len(df0), \"label1:\", len(df1))\n\n\n# -------------------------\n# 2) Pseudo-Bag Dataset\n#    一个bag由K个patch组成，bag label 取 0/1\n# -------------------------\nclass PseudoBagDataset(Dataset):\n    \"\"\"\n    每次getitem随机生成一个bag：\n      - 先随机选择 bag label ∈ {0,1}\n      - 再从对应类别中随机采样 K 个 patch\n    输出：\n      X: [K, 3, H, W]\n      y: scalar (float32)\n    \"\"\"\n    def __init__(self, df0, df1, img_dir, bag_size=32, bags_per_epoch=2000, img_size=96):\n        self.df0 = df0\n        self.df1 = df1\n        self.img_dir = img_dir\n        self.bag_size = bag_size\n        self.bags_per_epoch = bags_per_epoch\n\n        self.tf = T.Compose([\n            T.Resize((img_size, img_size)),\n            T.ToTensor(),\n            T.Normalize([0.5,0.5,0.5], [0.5,0.5,0.5]),\n        ])\n\n    def __len__(self):\n        return self.bags_per_epoch\n\n    def _sample_ids(self, label):\n        src = self.df1 if label == 1 else self.df0\n        idx = np.random.randint(0, len(src), size=self.bag_size)\n        return src.loc[idx, \"id\"].tolist()\n\n    def __getitem__(self, idx):\n        label = np.random.randint(0, 2)  # 0 or 1\n        ids = self._sample_ids(label)\n\n        imgs = []\n        for _id in ids:\n            path = os.path.join(self.img_dir, f\"{_id}.tif\")\n            img = Image.open(path).convert(\"RGB\")\n            imgs.append(self.tf(img))\n\n        X = torch.stack(imgs, dim=0)  # [K, 3, H, W]\n        y = torch.tensor(float(label), dtype=torch.float32)\n        return X, y\n\n\n# -------------------------\n# 3) CNN Feature Extractor\n# -------------------------\nclass ResNet18_Feature(nn.Module):\n    \"\"\"\n    ResNet18 去掉最后分类层，输出 feature:\n      input:  [K,3,H,W]\n      output: [K, D]\n    \"\"\"\n    def __init__(self, out_dim=512):\n        super().__init__()\n        base = models.resnet18(weights=None)  # 不联网\n        # 去掉fc\n        self.backbone = nn.Sequential(*list(base.children())[:-1])  # -> [B, 512, 1, 1]\n        self.out_dim = out_dim\n\n    def forward(self, x):\n        f = self.backbone(x)          # [B, 512, 1, 1]\n        f = f.flatten(1)              # [B, 512]\n        return f\n\n\n# -------------------------\n# 4) Attention-MIL (Ilse-style)\n# -------------------------\nclass AttentionMIL(nn.Module):\n    \"\"\"\n    输入 instance features: [K, D]\n    输出 bag logit: scalar\n    返回 attention weights: [K]\n    \"\"\"\n    def __init__(self, in_dim=512, attn_dim=128, dropout=0.25):\n        super().__init__()\n        self.embed = nn.Sequential(\n            nn.Linear(in_dim, attn_dim),\n            nn.ReLU(inplace=True),\n            nn.Dropout(dropout),\n        )\n        self.attn_V = nn.Linear(attn_dim, attn_dim)\n        self.attn_w = nn.Linear(attn_dim, 1)\n\n        self.classifier = nn.Sequential(\n            nn.Linear(attn_dim, attn_dim),\n            nn.ReLU(inplace=True),\n            nn.Dropout(dropout),\n            nn.Linear(attn_dim, 1),\n        )\n\n    def forward(self, X):\n        # X: [K, D]\n        H = self.embed(X)                      # [K, A]\n        A = torch.tanh(self.attn_V(H))         # [K, A]\n        A = self.attn_w(A).squeeze(-1)         # [K]\n        a = torch.softmax(A, dim=0)            # [K]\n\n        M = torch.sum(a.unsqueeze(-1) * H, dim=0)  # [A]\n        logit = self.classifier(M).squeeze(-1)     # scalar\n        return logit, a\n\n\n# -------------------------\n# 5) Full MIL Model = CNN + AttentionMIL\n# -------------------------\nclass MILModel(nn.Module):\n    def __init__(self, attn_dim=128):\n        super().__init__()\n        self.feat = ResNet18_Feature(out_dim=512)\n        self.mil  = AttentionMIL(in_dim=512, attn_dim=attn_dim)\n\n    def forward(self, bag_imgs):\n        \"\"\"\n        bag_imgs: [K,3,H,W]\n        \"\"\"\n        feats = self.feat(bag_imgs)           # [K,512]\n        logit, attn = self.mil(feats)         # scalar, [K]\n        return logit, attn\n\n\n# -------------------------\n# 6) Train\n# -------------------------\nbag_size = 32\nbags_per_epoch = 1500     # CPU建议 1000~2000\nimg_size = 96\nbatch_size = 1            # MIL常用 batch=1，最简单\n\ntrain_ds = PseudoBagDataset(df0, df1, train_dir,\n                            bag_size=bag_size,\n                            bags_per_epoch=bags_per_epoch,\n                            img_size=img_size)\n\ntrain_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=2)\n\nmodel = MILModel(attn_dim=128).to(device)\ncriterion = nn.BCEWithLogitsLoss()\noptimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4)\n\nepochs = 2  # 先跑通；你之后可以设成 10/20\n\nprint(\"\\nStart Training Attention-MIL ...\")\nfor epoch in range(1, epochs + 1):\n    model.train()\n    running_loss = 0.0\n    running_acc = 0.0\n\n    for step, (bag_imgs, y) in enumerate(train_loader):\n        # bag_imgs: [1,K,3,H,W] -> [K,3,H,W]\n        bag_imgs = bag_imgs.squeeze(0).to(device)\n        y = y.to(device)\n\n        logit, attn = model(bag_imgs)          # scalar, [K]\n        loss = criterion(logit.unsqueeze(0), y)  # make shapes [1]\n\n        optimizer.zero_grad(set_to_none=True)\n        loss.backward()\n        optimizer.step()\n\n        with torch.no_grad():\n            prob = torch.sigmoid(logit).item()\n            pred = 1.0 if prob >= 0.5 else 0.0\n            acc = 1.0 if pred == float(y.item()) else 0.0\n\n        running_loss += loss.item()\n        running_acc += acc\n\n        if step % 50 == 0:\n            print(f\"epoch={epoch} step={step:04d} loss={loss.item():.4f} prob={prob:.3f} y={float(y.item())} \"\n                  f\"attn_max={attn.max().item():.3f} attn_min={attn.min().item():.3f}\")\n\n        # 先跑少一点，确认没问题\n        if step == 300:\n            break\n\n    print(f\"Epoch {epoch} done. avg_loss={running_loss/(step+1):.4f} avg_acc={running_acc/(step+1):.4f}\")\n\nprint(\"Training finished.\")\n\n# -------------------------\n# 7) 导出一个 bag 的 attention（用于你后续可视化/汇报）\n# -------------------------\nmodel.eval()\nwith torch.no_grad():\n    bag_imgs, y = train_ds[0]\n    bag_imgs = bag_imgs.to(device)\n    logit, attn = model(bag_imgs)\n    prob = torch.sigmoid(logit).item()\n    attn_cpu = attn.cpu()\n\nprint(\"\\nExample bag attention exported:\")\nprint(\"bag label:\", float(y.item()), \"prob:\", prob)\nprint(\"top-5 attention weights:\", torch.topk(attn_cpu, k=5).values.numpy())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T02:32:32.980058Z","iopub.execute_input":"2026-01-07T02:32:32.981034Z","iopub.status.idle":"2026-01-07T02:41:05.782054Z","shell.execute_reply.started":"2026-01-07T02:32:32.980986Z","shell.execute_reply":"2026-01-07T02:41:05.781011Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\n# 重新取一个 bag\nbag_imgs, y = train_ds[0]\nbag_imgs = bag_imgs.to(device)\n\nmodel.eval()\nwith torch.no_grad():\n    logit, attn = model(bag_imgs)\n\nattn = attn.cpu().numpy()        # [K]\nbag_imgs = bag_imgs.cpu()        # [K,3,H,W]\n\n# 找 attention 最大和最小的 patch\ntop_idx = attn.argmax()\nlow_idx = attn.argmin()\n\ndef show_patch(img_tensor, title):\n    img = img_tensor.permute(1,2,0).numpy()\n    img = (img * 0.5 + 0.5).clip(0,1)  # 反归一化\n    plt.imshow(img)\n    plt.title(title)\n    plt.axis(\"off\")\n\nplt.figure(figsize=(8,4))\n\nplt.subplot(1,2,1)\nshow_patch(bag_imgs[top_idx], f\"Top attention\\nweight={attn[top_idx]:.3f}\")\n\nplt.subplot(1,2,2)\nshow_patch(bag_imgs[low_idx], f\"Low attention\\nweight={attn[low_idx]:.3f}\")\n\nplt.suptitle(f\"Bag label={int(y.item())}  pred_prob={torch.sigmoid(logit).item():.3f}\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T02:41:21.164572Z","iopub.execute_input":"2026-01-07T02:41:21.164995Z","iopub.status.idle":"2026-01-07T02:41:21.819168Z","shell.execute_reply.started":"2026-01-07T02:41:21.164957Z","shell.execute_reply":"2026-01-07T02:41:21.818162Z"}},"outputs":[],"execution_count":null}]}