{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":37333,"databundleVersionId":3949526,"sourceType":"competition"}],"dockerImageVersionId":31040,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# ========================================\n# Mayo-Clinic-STRIP-AI  End-to-End Notebook\n# ========================================\n# ⚙️  Section 0 — 配置\n# ----------------------------------------\nimport os, random, gc, json, zipfile, math, warnings, hashlib, time, sys\nfrom pathlib import Path\nimport numpy as np, pandas as pd\nimport torch, torch.nn as nn, torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader, SubsetRandomSampler\nimport albumentations as A\nimport albumentations.pytorch\nimport tifffile, cv2\nimport timm\nfrom sklearn.model_selection import StratifiedGroupKFold\nfrom sklearn.metrics import roc_auc_score\nwarnings.filterwarnings(\"ignore\")\n\nclass CFG:\n    COMP          = \"mayo-clinic-strip-ai\"\n    TILE_SIZE     = 1024          # 读入后再缩放\n    BATCH_SIZE    = 16\n    EPOCHS        = 5             # DEBUG 模式下先跑 1 个 epoch\n    LR            = 1e-4\n    IMG_MEAN      = (0.485,0.456,0.406)\n    IMG_STD       = (0.229,0.224,0.225)\n    DEBUG         = True          # 🚀 改成 False 训练全量\n    DEBUG_FRAC    = 0.05          # 只用 5 % 样本\n    NUM_WORKERS   = 0\n    SEED          = 42\n    DEVICE        = \"cuda\" if torch.cuda.is_available() else \"cpu\"\ncfg = CFG()\n\ndef set_seed(seed=42):\n    random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)\nset_seed(cfg.SEED)\n\n# ----------------------------------------\n# Section 1 — 下载 / 解压 数据\n# ----------------------------------------\n# ----------------------------------------\n# Section 1 — 直接读取已挂载的数据\n# ----------------------------------------\ndata_dir = Path(\"/kaggle/input/mayo-clinic-strip-ai\")\ntrain_csv = pd.read_csv(data_dir / \"train.csv\")\ntest_csv  = pd.read_csv(data_dir / \"test.csv\")\n\n# ----------------------------------------\n# Section 2 — ⏩ 可选小批量调试\n# ----------------------------------------\nif cfg.DEBUG:\n    train_csv = (train_csv.groupby(\"label\", group_keys=False)    # ← 改这里\n                           .apply(lambda x: x.sample(frac=cfg.DEBUG_FRAC,\n                                                     random_state=cfg.SEED))\n                           .reset_index(drop=True))\n\n# === Section 3 — patient-level split  ===\nsgkf = StratifiedGroupKFold(n_splits=5, shuffle=True, random_state=cfg.SEED)\ntrain_idx, val_idx = next(sgkf.split(train_csv,\n                                    y=train_csv[\"label\"],\n                                    groups=train_csv[\"patient_id\"]))\n\ntrain_df = train_csv.iloc[train_idx].reset_index(drop=True)\nval_df   = train_csv.iloc[val_idx].reset_index(drop=True)\n\nlabel_map = {lbl: i for i, lbl in enumerate(sorted(train_csv[\"label\"].unique()))}\ntrain_df[\"label_id\"] = train_df[\"label\"].map(label_map)\nval_df  [\"label_id\"] = val_df[\"label\"].map(label_map)\nnum_classes = len(label_map)\n\n\n# ----------------------------------------\n# Section 4 — Dataset & 预处理\n# ----------------------------------------\ntfm_train = A.Compose([\n    A.RandomResizedCrop(size=(cfg.TILE_SIZE, cfg.TILE_SIZE),  # ✅ 用 size\n                        scale=(0.8, 1.0)),\n    A.HorizontalFlip(), A.VerticalFlip(), A.RandomRotate90(),\n    A.Normalize(cfg.IMG_MEAN, cfg.IMG_STD), albumentations.pytorch.ToTensorV2(),\n])\n\ntfm_val = A.Compose([\n    A.Resize(height=cfg.TILE_SIZE, width=cfg.TILE_SIZE),      # ✅ 用 height/width\n    A.Normalize(cfg.IMG_MEAN, cfg.IMG_STD), albumentations.pytorch.ToTensorV2(),\n])\n\nclass TileDataset(Dataset):\n    def __init__(self, df, transforms=None, split=\"train\"):\n        self.df = df\n        self.tfm = transforms\n        self.split = split            # \"train\" / \"test\"\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        folder = \"train\" if self.split == \"train\" else \"test\"\n        img_path = data_dir / folder / f\"{row.image_id}.tif\"\n        img = tifffile.imread(img_path)\n        if img.ndim == 2:\n            img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)\n        if self.tfm:\n            img = self.tfm(image=img)[\"image\"]\n    \n        label = row.label_id if \"label_id\" in row else -1\n        return img.float(), torch.tensor(label).long(), row.patient_id   #  ← 加这一项\n\ntrain_dl = DataLoader(\n    TileDataset(train_df, tfm_train, split=\"train\"),\n    batch_size=cfg.BATCH_SIZE,\n    shuffle=True,\n    num_workers=cfg.NUM_WORKERS,\n    pin_memory=True,\n)\n\nval_dl = DataLoader(\n    TileDataset(val_df, tfm_val, split=\"train\"),\n    batch_size=cfg.BATCH_SIZE,\n    shuffle=False,\n    num_workers=cfg.NUM_WORKERS,\n    pin_memory=True,\n)\n\ntest_ds = TileDataset(test_csv, tfm_val, split=\"test\")\ntest_dl = DataLoader(\n    test_ds,\n    batch_size=cfg.BATCH_SIZE,\n    shuffle=False,\n    num_workers=cfg.NUM_WORKERS,\n    pin_memory=True,\n)\nsample_id = train_df.image_id.iloc[0]\nprint((data_dir / \"train\" / f\"{sample_id}.tif\").exists())  # 应该 True\n# ----------------------------------------\n# Section 5 — 模型、损失、优化器\n# ----------------------------------------\nmodel = timm.create_model(\"resnet50\", pretrained=True, num_classes=num_classes)\nmodel = model.to(cfg.DEVICE)\ncriterion = nn.CrossEntropyLoss()\noptimizer = optim.AdamW(model.parameters(), lr=cfg.LR)\nscheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer,\n                                                 T_max=cfg.EPOCHS)\n\n# ----------------------------------------\n# Section 6 — 训练 & 验证循环\n# ----------------------------------------\ndef train_one_epoch(dl):\n    model.train(); total=0; correct=0\n    for x,y in dl:\n        x,y = x.to(cfg.DEVICE), y.to(cfg.DEVICE)\n        optimizer.zero_grad()\n        out = model(x); loss = criterion(out,y)\n        loss.backward(); optimizer.step()\n        preds = out.argmax(1)\n        total += y.size(0); correct += (preds==y).sum().item()\n    return correct/total\n\n@torch.no_grad()\ndef validate(dl):\n    model.eval(); total=0; y_true=[]; y_prob=[]\n    for x,y in dl:\n        x,y = x.to(cfg.DEVICE), y.to(cfg.DEVICE)\n        out = model(x); prob = out.softmax(1)[:,1]\n        y_true.append(y.cpu().numpy()); y_prob.append(prob.cpu().numpy())\n        total += y.size(0)\n    y_true = np.concatenate(y_true); y_prob = np.concatenate(y_prob)\n    auc = roc_auc_score(y_true, y_prob)\n    return auc\n\nbest_auc = 0.0\nfor epoch in range(cfg.EPOCHS):\n    acc  = train_one_epoch(train_dl)\n    auc  = validate(val_dl)\n    scheduler.step()\n    print(f\"Epoch {epoch+1}/{cfg.EPOCHS}  train-acc={acc:.4f}  val-AUC={auc:.4f}\")\n    if auc > best_auc:\n        best_auc = auc\n        torch.save(model.state_dict(), \"best_model.pth\")\n\n# ----------------------------------------\n# Section 7 — inference & submission\n# ----------------------------------------\nmodel.eval()\n\nprobs_ce, probs_laa = [], []\n\nwith torch.no_grad():\n    for x, _ in test_dl:                    # _ = dummy label -1\n        x = x.to(cfg.DEVICE, non_blocking=True)\n        p = model(x).softmax(1).cpu().numpy()  # shape (B, 2)  [CE, LAA]\n        probs_ce.extend(p[:, 0])\n        probs_laa.extend(p[:, 1])\n\n# ① 按 test_csv 顺序写入两列概率\ntest_preds = test_csv.copy()\ntest_preds[\"CE\"]  = np.array(probs_ce,  dtype=np.float32)\ntest_preds[\"LAA\"] = np.array(probs_laa, dtype=np.float32)\n\n# ② patient-level 汇聚（平均）\nsub_df = (\n    test_preds.groupby(\"patient_id\")[[\"CE\", \"LAA\"]]\n              .mean()\n              .reset_index()\n)\n\n# ③ clip 避免 log(0)\neps = 1e-15\nsub_df[[\"CE\", \"LAA\"]] = sub_df[[\"CE\", \"LAA\"]].clip(eps, 1 - eps)\n\n# ④ 保存\nsub_df.to_csv(\"submission.csv\", index=False)\nprint(\"✅ submission.csv saved!  shape:\", sub_df.shape)\nprint(sub_df.head())\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","execution":{"iopub.status.busy":"2025-08-02T03:24:12.625383Z","iopub.execute_input":"2025-08-02T03:24:12.625744Z","iopub.status.idle":"2025-08-02T03:39:38.014984Z","shell.execute_reply.started":"2025-08-02T03:24:12.625714Z","shell.execute_reply":"2025-08-02T03:39:37.995295Z"}}},{"cell_type":"markdown","source":"# =========================================\n# Mayo-Clinic-STRIP-AI  |  CNN & XGBoost NB\n# =========================================\nimport os, random, time, warnings, gc, joblib\nfrom pathlib import Path\nfrom collections import defaultdict\n\nimport numpy as np, pandas as pd\nimport torch, torch.nn as nn, torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nimport albumentations as A; import albumentations.pytorch\nimport tifffile, cv2, timm\nfrom sklearn.model_selection import StratifiedGroupKFold\nfrom sklearn.metrics import roc_auc_score, log_loss\nimport xgboost as xgb\nwarnings.filterwarnings(\"ignore\")\n\n# ---------------------\n# 0️⃣  配置\n# ---------------------\nclass CFG:\n    COMP          = \"mayo-clinic-strip-ai\"\n    TILE_SIZE     = 640          # 小一点防 OOM\n    BATCH_SIZE    = 24\n    EPOCHS        = 16\n    LR            = 3e-4 \n    IMG_MEAN      = (0.485,0.456,0.406)\n    IMG_STD       = (0.229,0.224,0.225)\n    NUM_WORKERS   = 2\n    SEED          = 42\n    DEVICE        = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n    DEBUG         = True      # ← True: 只抽样；False: 全量训练\n    DEBUG_FRAC    = 0.5      # 抽多少？5 %的 tile\n    BY_PATIENT    = True      # True ⇒ 抽患者；False ⇒ 抽 tile\n    \ncfg = CFG()\nrandom.seed(cfg.SEED); np.random.seed(cfg.SEED); torch.manual_seed(cfg.SEED)\n\n# ----------------------------------------\n# 1️⃣  数据加载\n# ----------------------------------------\ndata_dir  = Path(\"/kaggle/input/mayo-clinic-strip-ai\")\ntrain_csv = pd.read_csv(data_dir / \"train.csv\")\n\n# ---------- DEBUG 抽样 ----------\nif CFG.DEBUG:\n    if CFG.BY_PATIENT:\n        pats = train_csv.patient_id.unique()\n        rnd  = np.random.RandomState(CFG.SEED)\n        sel  = rnd.choice(\n            pats, size=int(len(pats)*CFG.DEBUG_FRAC), replace=False)\n        train_csv = train_csv[train_csv.patient_id.isin(sel)]\n    else:  # tile-level 分层抽样\n        train_csv = (train_csv\n            .groupby(\"label\", group_keys=False)\n            .apply(lambda x: x.sample(frac=CFG.DEBUG_FRAC,\n                                      random_state=CFG.SEED))\n            .reset_index(drop=True))\n    print(f\"[DEBUG] using {len(train_csv)} tiles ({len(train_csv.patient_id.unique())} patients)\")\ntest_csv  = pd.read_csv(data_dir / \"test.csv\")\n\nlabel_map = {lbl:i for i,lbl in enumerate(sorted(train_csv[\"label\"].unique()))}\ntrain_csv[\"label_id\"] = train_csv[\"label\"].map(label_map)\nnum_classes = len(label_map)\n\nsgkf = StratifiedGroupKFold(n_splits=5, shuffle=True, random_state=cfg.SEED)\ntr_idx, va_idx = next(sgkf.split(train_csv, train_csv.label_id, train_csv.patient_id))\ntrain_df = train_csv.iloc[tr_idx].reset_index(drop=True)\nval_df   = train_csv.iloc[va_idx].reset_index(drop=True)\n\n# ----------------------------------------\n# 2️⃣  Dataset\n# ----------------------------------------\ntfm_train = A.Compose([\n    A.RandomResizedCrop(size=(cfg.TILE_SIZE, cfg.TILE_SIZE), scale=(0.7,1.0)),\n    A.HorizontalFlip(p=0.5),\n    A.VerticalFlip(p=0.5),\n    A.RandomRotate90(p=0.5),\n    A.ColorJitter(0.1,0.1,0.1,0.05,p=0.3),           # add mild color jitter\n    A.Normalize(cfg.IMG_MEAN, cfg.IMG_STD),\n    A.pytorch.ToTensorV2(),\n])\n\ntfm_val = A.Compose([\n    # 下面两种写法均可，二选一\n    A.Resize(height=cfg.TILE_SIZE, width=cfg.TILE_SIZE),      # 原写法保留\n    # A.Resize(size=(cfg.TILE_SIZE, cfg.TILE_SIZE)),          # 或也用 size=\n    A.Normalize(cfg.IMG_MEAN, cfg.IMG_STD),\n    A.pytorch.ToTensorV2(),\n])\n\nclass TileDataset(Dataset):\n    def __init__(self, df, transforms=None, split=\"train\"):\n        self.df, self.tfm, self.split = df, transforms, split\n    def __len__(self): return len(self.df)\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        folder = \"train\" if self.split==\"train\" else \"test\"\n        img_path = data_dir / folder / f\"{row.image_id}.tif\"\n        img = tifffile.imread(img_path)\n        if img.ndim==2: img=cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)\n        if self.tfm: img=self.tfm(image=img)[\"image\"]\n        label = row.label_id if \"label_id\" in row else -1\n        return img.float(), torch.tensor(label).long(), row.patient_id\n\ntrain_dl = DataLoader(TileDataset(train_df, tfm_train, \"train\"),\n                      batch_size=cfg.BATCH_SIZE, shuffle=True,\n                      num_workers=cfg.NUM_WORKERS, pin_memory=True)\n\nval_dl   = DataLoader(TileDataset(val_df, tfm_val, \"train\"),\n                      batch_size=cfg.BATCH_SIZE, shuffle=False,\n                      num_workers=cfg.NUM_WORKERS, pin_memory=True)\n\ntest_dl  = DataLoader(TileDataset(test_csv, tfm_val, \"test\"),\n                      batch_size=cfg.BATCH_SIZE, shuffle=False,\n                      num_workers=cfg.NUM_WORKERS, pin_memory=True)\n\n# ----------------------------------------\n# 3️⃣  端到端 CNN  (可选)\n# ----------------------------------------\nloss_type   = \"focal\"      # \"ce\" | \"weighted\" | \"focal\"\n# 计算每个类别的出现频率（在 train_df 上）\nfreq = train_df.label_id.value_counts(normalize=True).sort_index()  # e.g. [0.85, 0.15]\n\n# 反比频率做权重；转换到 GPU\nw = torch.tensor(1.0 / freq.values, dtype=torch.float32).to(cfg.DEVICE)\n\n# Focal Loss（或 Weighted CE）就能用到 alpha / weight\ncriterion = FocalLoss(alpha=w, gamma=2)\ntrain_mode  = \"xgb\"           # \"cnn\" | \"xgb\"  (决定提交用哪个模型)\n\nclass FocalLoss(nn.Module):\n    def __init__(self, alpha=None, gamma=2):\n        super().__init__()\n        self.alpha = alpha\n        self.gamma = gamma\n        self.ce = nn.CrossEntropyLoss(reduction=\"none\")\n    def forward(self, logits, targets):\n        ce_loss = self.ce(logits, targets)\n        pt = torch.exp(-ce_loss)\n        if self.alpha is not None:\n            at = self.alpha.gather(0, targets)\n            ce_loss = ce_loss * at\n        loss = ((1-pt)**self.gamma * ce_loss).mean()\n        return loss\n\nif train_mode == \"cnn\":\n    model = timm.create_model(\"resnet50d\", pretrained=True, num_classes=num_classes).to(cfg.DEVICE)\n    optimizer = optim.AdamW(model.parameters(), lr=cfg.LR, weight_decay=1e-4)\n\n    scheduler = optim.lr_scheduler.CosineAnnealingLR(\n        optimizer, T_max=cfg.EPOCHS, eta_min=1e-6\n    )\n    if loss_type == \"ce\":\n        criterion = nn.CrossEntropyLoss()\n    elif loss_type == \"weighted\":\n        freq = train_df.label_id.value_counts(normalize=True).sort_index()\n        w = torch.tensor(1.0/freq.values, dtype=torch.float32).to(cfg.DEVICE)\n        criterion = nn.CrossEntropyLoss(weight=w)\n    else:  # focal\n        alpha = None\n        criterion = FocalLoss(alpha=alpha, gamma=2)\n\n    optimizer = optim.AdamW(model.parameters(), lr=cfg.LR)\n    sch = optim.lr_scheduler.CosineAnnealingLR(optimizer,T_max=cfg.EPOCHS)\n\n    def run_epoch(dl, train=True):\n        model.train() if train else model.eval()\n        total, correct, loss_sum = 0,0,0\n        for x,y,_ in dl:\n            x,y = x.to(cfg.DEVICE), y.to(cfg.DEVICE)\n            with torch.set_grad_enabled(train):\n                out = model(x); loss=criterion(out,y)\n            if train:\n                optimizer.zero_grad(); loss.backward(); optimizer.step()\n            pred = out.argmax(1); total+=y.size(0); correct+=(pred==y).sum().item()\n            loss_sum += loss.item()*y.size(0)\n        return correct/total, loss_sum/total\n\n    for ep in range(cfg.EPOCHS):\n        tr_acc, tr_loss = run_epoch(train_dl,True)\n        va_acc, _ = run_epoch(val_dl,False)\n        sch.step()\n        print(f\"Epoch {ep+1}/{cfg.EPOCHS}  train-acc={tr_acc:.3f}  val-acc={va_acc:.3f}\")\n\n# ----------------------------------------\n# 4️⃣  CNN → XGBoost\n# ----------------------------------------\nif train_mode == \"xgb\":\n    fe_model = timm.create_model(\n        \"resnet50d\",  # or \"convnext_tiny\"\n        pretrained=True, num_classes=0, global_pool=\"avg\"\n    ).to(cfg.DEVICE).eval()\n\n    def get_embed(dl):\n        feats,lbls,pids = [],[],[]\n        with torch.no_grad():\n            for x,y,pid in dl:\n                f = fe_model(x.to(cfg.DEVICE)).cpu().numpy()\n                feats.append(f); lbls.extend(y.numpy()); pids.extend(pid)\n        return np.vstack(feats), np.array(lbls), np.array(pids)\n\n    def agg(feats, labels, pids):\n        bag=defaultdict(list)\n        for f,y,p in zip(feats,labels,pids): bag[p].append((f,y))\n        X,y,ids=[],[],[]\n        for p,lst in bag.items():\n            vecs,ys=zip(*lst)\n            X.append(np.mean(vecs,0)); y.append(ys[0]); ids.append(p)\n        return np.vstack(X), np.array(y), np.array(ids)\n\n    print(\"⏳  Extracting embeddings …\")\n    tr_f,tr_y,tr_pid = agg(*get_embed(train_dl))\n    va_f,va_y,va_pid = agg(*get_embed(val_dl))\n\n    pos_ratio = (tr_y==1).mean()\n    params = dict(\n        objective=\"binary:logistic\",\n        eval_metric=\"logloss\",\n        eta=0.05,max_depth=6,\n        subsample=0.9,colsample_bytree=0.5,\n        seed=cfg.SEED,\n        scale_pos_weight=(1-pos_ratio)/pos_ratio  # 处理不平衡\n    )\n    dtrain,dval = xgb.DMatrix(tr_f,tr_y), xgb.DMatrix(va_f,va_y)\n    print(\"⏳  Training XGBoost …\")\n    params.update({\n    \"eta\":0.03, \"max_depth\":7,\n    \"subsample\":0.8,\"colsample_bytree\":0.6,\n    \"lambda\":1.0,\"alpha\":0.3,\n    \"min_child_weight\":3,\n    \"scale_pos_weight\":(1-pos_ratio)/pos_ratio,\n    })\n    model_xgb = xgb.train(params,dtrain,500,\n                          evals=[(dtrain,\"tr\"),(dval,\"val\")],\n                          early_stopping_rounds=200,verbose_eval=50)\n    pred_val = model_xgb.predict(dval)\n    print(f\"Val AUC {roc_auc_score(va_y,pred_val):.4f}  logloss {log_loss(va_y,pred_val):.4f}\")\n\n# ----------------------------------------\n# 5️⃣  生成 submission.csv\n# ----------------------------------------\ndef make_submission():\n    # --- 抽取 test patient-level ---------------\n    if train_mode==\"cnn\":\n        model.eval()\n        probs_ce,probs_laa,patient_ids=[],[],[]\n        with torch.no_grad():\n            for x,_,pid in test_dl:\n                p=model(x.to(cfg.DEVICE)).softmax(1).cpu().numpy()\n                probs_ce.extend(p[:,label_map[\"CE\"]])\n                probs_laa.extend(p[:,label_map[\"LAA\"]])\n                patient_ids.extend(pid)\n    else:\n        f_test,_,pid_test = get_embed(test_dl)\n        X_test,_,ids = agg(f_test, np.zeros_like(pid_test), pid_test)\n        p_laa = model_xgb.predict(xgb.DMatrix(X_test))\n        p_ce  = 1 - p_laa\n        patient_ids, probs_ce, probs_laa = ids, p_ce, p_laa\n\n    sub = pd.DataFrame({\"patient_id\":patient_ids,\n                        \"CE\":probs_ce,\"LAA\":probs_laa})\n    sub = sub.groupby(\"patient_id\")[[\"CE\",\"LAA\"]].mean().reset_index()\n    sub[[\"CE\",\"LAA\"]] = sub[[\"CE\",\"LAA\"]].clip(1e-15,1-1e-15)\n    sub.to_csv(\"submission.csv\",index=False)\n    print(\"✅ submission.csv saved! shape:\",sub.shape)\n    print(sub.head())\nmake_submission()","metadata":{"execution":{"iopub.status.busy":"2025-08-03T20:04:44.646511Z","iopub.execute_input":"2025-08-03T20:04:44.647905Z","iopub.status.idle":"2025-08-03T21:39:11.269442Z","shell.execute_reply.started":"2025-08-03T20:04:44.647861Z","shell.execute_reply":"2025-08-03T21:39:11.268314Z"}}},{"cell_type":"markdown","source":"# ============================================================\n# Baseline ❶ : handcrafted descriptors  ➜  SVM (linear / RBF)\n# ============================================================\n!pip install -q scikit-image mahotas  # HOG / Haralick utils\n\nfrom skimage.color import rgb2gray, rgb2hsv\nfrom skimage.feature import hog, local_binary_pattern\nimport mahotas            as mh\nfrom sklearn.decomposition import PCA\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.svm import SVC\nfrom sklearn.pipeline import Pipeline\nfrom sklearn.metrics import roc_auc_score, log_loss\nfrom sklearn.model_selection import ParameterGrid, StratifiedKFold, GroupKFold\nimport cv2, joblib, gc, time\nfrom sklearn.base import clone  \nimport os, random, time, warnings, gc, joblib\nfrom pathlib import Path\nfrom collections import defaultdict\n\nimport numpy as np, pandas as pd\nimport torch, torch.nn as nn, torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nimport albumentations as A; import albumentations.pytorch\nimport tifffile, cv2, timm\nfrom sklearn.model_selection import StratifiedGroupKFold\nfrom sklearn.metrics import roc_auc_score, log_loss\nimport xgboost as xgb\n\n# ---------------------\n# 0️⃣  配置\n# ---------------------\nclass CFG:\n    COMP          = \"mayo-clinic-strip-ai\"\n    TILE_SIZE     = 640          # 小一点防 OOM\n    BATCH_SIZE    = 24\n    EPOCHS        = 16\n    LR            = 3e-4 \n    IMG_MEAN      = (0.485,0.456,0.406)\n    IMG_STD       = (0.229,0.224,0.225)\n    NUM_WORKERS   = 2\n    SEED          = 42\n    DEVICE        = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n    DEBUG         = False      # ← True: 只抽样；False: 全量训练\n    DEBUG_FRAC    = 0.5      # 抽多少？5 %的 tile\n    BY_PATIENT    = True      # True ⇒ 抽患者；False ⇒ 抽 tile\n    \ncfg = CFG()\nrandom.seed(cfg.SEED); np.random.seed(cfg.SEED); torch.manual_seed(cfg.SEED)\n\n# ----------------------------------------\n# 1️⃣  数据加载\n# ----------------------------------------\ndata_dir  = Path(\"/kaggle/input/mayo-clinic-strip-ai\")\ntrain_csv = pd.read_csv(data_dir / \"train.csv\")\n\n# ---------- DEBUG 抽样 ----------\nif CFG.DEBUG:\n    if CFG.BY_PATIENT:\n        pats = train_csv.patient_id.unique()\n        rnd  = np.random.RandomState(CFG.SEED)\n        sel  = rnd.choice(\n            pats, size=int(len(pats)*CFG.DEBUG_FRAC), replace=False)\n        train_csv = train_csv[train_csv.patient_id.isin(sel)]\n    else:  # tile-level 分层抽样\n        train_csv = (train_csv\n            .groupby(\"label\", group_keys=False)\n            .apply(lambda x: x.sample(frac=CFG.DEBUG_FRAC,\n                                      random_state=CFG.SEED))\n            .reset_index(drop=True))\n    print(f\"[DEBUG] using {len(train_csv)} tiles ({len(train_csv.patient_id.unique())} patients)\")\ntest_csv  = pd.read_csv(data_dir / \"test.csv\")\n\nlabel_map = {lbl:i for i,lbl in enumerate(sorted(train_csv[\"label\"].unique()))}\ntrain_csv[\"label_id\"] = train_csv[\"label\"].map(label_map)\nnum_classes = len(label_map)\n\nsgkf = StratifiedGroupKFold(n_splits=5, shuffle=True, random_state=cfg.SEED)\ntr_idx, va_idx = next(sgkf.split(train_csv, train_csv.label_id, train_csv.patient_id))\ntrain_df = train_csv.iloc[tr_idx].reset_index(drop=True)\nval_df   = train_csv.iloc[va_idx].reset_index(drop=True)\n\n# ----------------------------------------\n# 2️⃣  Dataset\n# ----------------------------------------\ntfm_train = A.Compose([\n    A.RandomResizedCrop(size=(cfg.TILE_SIZE, cfg.TILE_SIZE), scale=(0.7,1.0)),\n    A.HorizontalFlip(p=0.5),\n    A.VerticalFlip(p=0.5),\n    A.RandomRotate90(p=0.5),\n    A.ColorJitter(0.1,0.1,0.1,0.05,p=0.3),           # add mild color jitter\n    A.Normalize(cfg.IMG_MEAN, cfg.IMG_STD),\n    A.pytorch.ToTensorV2(),\n])\n\ntfm_val = A.Compose([\n    # 下面两种写法均可，二选一\n    A.Resize(height=cfg.TILE_SIZE, width=cfg.TILE_SIZE),      # 原写法保留\n    # A.Resize(size=(cfg.TILE_SIZE, cfg.TILE_SIZE)),          # 或也用 size=\n    A.Normalize(cfg.IMG_MEAN, cfg.IMG_STD),\n    A.pytorch.ToTensorV2(),\n])\n\nclass TileDataset(Dataset):\n    def __init__(self, df, transforms=None, split=\"train\"):\n        self.df, self.tfm, self.split = df, transforms, split\n    def __len__(self): return len(self.df)\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        folder = \"train\" if self.split==\"train\" else \"test\"\n        img_path = data_dir / folder / f\"{row.image_id}.tif\"\n        img = tifffile.imread(img_path)\n        if img.ndim==2: img=cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)\n        if self.tfm: img=self.tfm(image=img)[\"image\"]\n        label = row.label_id if \"label_id\" in row else -1\n        return img.float(), torch.tensor(label).long(), row.patient_id\n\ntrain_dl = DataLoader(TileDataset(train_df, tfm_train, \"train\"),\n                      batch_size=cfg.BATCH_SIZE, shuffle=True,\n                      num_workers=cfg.NUM_WORKERS, pin_memory=True)\n\nval_dl   = DataLoader(TileDataset(val_df, tfm_val, \"train\"),\n                      batch_size=cfg.BATCH_SIZE, shuffle=False,\n                      num_workers=cfg.NUM_WORKERS, pin_memory=True)\n\ntest_dl  = DataLoader(TileDataset(test_csv, tfm_val, \"test\"),\n                      batch_size=cfg.BATCH_SIZE, shuffle=False,\n                      num_workers=cfg.NUM_WORKERS, pin_memory=True)\n\n# 1️⃣  feature extractor -------------------------------------------------\ndef tile_features(img_rgb):          # img_rgb: H,W,C, float32 0-1\n    img_gray = rgb2gray(img_rgb)\n\n    # HOG\n    hog_feat = hog(img_gray, pixels_per_cell=(16,16),\n                   cells_per_block=(2,2), orientations=9,\n                   feature_vector=True)\n\n    # LBP histogram\n    lbp = local_binary_pattern(img_gray, P=8, R=1.0, method=\"uniform\")\n    lbp_hist, _ = np.histogram(lbp, bins=np.arange(0,10), density=True)\n\n    # Haralick textures (mean over 4 directions)\n    har = mh.features.haralick((img_gray*255).astype(np.uint8)).mean(axis=0)\n\n    # HSV 8×8×8 histogram\n    hsv = cv2.cvtColor((img_rgb*255).astype(np.uint8), cv2.COLOR_RGB2HSV)\n    hsv_hist = cv2.calcHist([hsv], [0,1,2], None,            # ← 改成 None\n                            [8,8,8], [0,180,0,256,0,256]).ravel()\n    hsv_hist /= (hsv_hist.sum() + 1e-6)\n\n    return np.hstack([hog_feat, lbp_hist, har, hsv_hist])\n\n# 2️⃣  cache descriptors -------------------------------------------------\ndef build_matrix(df, split):\n    feats, lbls, pids = [], [], []\n    ds = TileDataset(df, tfm_val, split=split)   # reuse your dataset / tfm_val\n    for img, y, pid in DataLoader(ds, batch_size=1, shuffle=False):\n        feat = tile_features(img[0].permute(1,2,0).numpy())  # C,H,W → H,W,C\n        feats.append(feat); lbls.append(y.item()); pids.append(pid[0])\n    return np.vstack(feats), np.array(lbls), np.array(pids)\n\nprint(\"Extracting handcrafted descriptors …\")\nX_train, y_train, pid_train = build_matrix(train_df, \"train\")\nX_val,   y_val,   pid_val   = build_matrix(val_df,   \"train\")\nprint(\"Descriptor dim:\", X_train.shape[1])\n\n# 3️⃣  nested patient-level CV ------------------------------------------\nouter = GroupKFold(n_splits=5)\ninner = StratifiedKFold(n_splits=2, shuffle=True, random_state=CFG.SEED)\n\nparam_grid = {\n    \"svm__kernel\": [\"linear\", \"rbf\"],\n    \"svm__C\": [0.01, 0.1, 1, 10, 100],\n    \"svm__gamma\":  [\"scale\", 0.01, 0.001]   # only used if kernel='rbf'\n}\n\noof_pred, oof_true = [], []\nbest_models = []\n\nfor fold, (tr_idx, va_idx) in enumerate(outer.split(X_train, y_train, pid_train)):\n    print(f\"\\n★ Outer fold {fold}\")\n    X_tr, y_tr = X_train[tr_idx], y_train[tr_idx]\n    X_va, y_va = X_train[va_idx], y_train[va_idx]\n\n    # pipeline: scaling → PCA (95 %) → SVM\n    base_pipe = Pipeline([\n        (\"scaler\", StandardScaler()),\n        (\"pca\",    PCA(n_components=0.95, svd_solver=\"full\", whiten=True)),\n        (\"svm\",    SVC(probability=True, class_weight=\"balanced\")),\n    ])\n\n    best_auc, best_clf = -1, None\n    for params in ParameterGrid(param_grid):\n        # skip gamma for linear kernel\n        if params[\"svm__kernel\"]==\"linear\": params.pop(\"svm__gamma\", None)\n        pipe = base_pipe.set_params(**params)\n        # inner CV\n        cv_auc = []\n        for tr2, va2 in inner.split(X_tr, y_tr):\n            pipe.fit(X_tr[tr2], y_tr[tr2])\n            p = pipe.predict_proba(X_tr[va2])[:,1]\n            cv_auc.append(roc_auc_score(y_tr[va2], p))\n        m_auc = np.mean(cv_auc)\n        if m_auc > best_auc:\n            best_auc  = m_auc\n            best_clf  = clone(pipe)\n        \n    # fit best on outer-train, eval outer-val\n    best_clf.fit(X_tr, y_tr)\n    pred_va = best_clf.predict_proba(X_va)[:,1]\n    print(f\"  best inner-CV AUC={best_auc:.3f}  outer-val AUC={roc_auc_score(y_va,pred_va):.3f}\")\n    oof_pred.extend(pred_va); oof_true.extend(y_va)\n    best_models.append(best_clf)\n\nprint(\"\\nOverall OOF AUC\", roc_auc_score(oof_true, oof_pred))\n\n# 4️⃣  fit on FULL train & predict test patients -------------------------\npipe_final = best_models[np.argmax([m.score(X_train,y_train) for m in best_models])]\n# (or re-fit best params on full X_train if you prefer)\n\n# cache test tile descriptors\nX_test, _, pid_test = build_matrix(test_csv.assign(label_id=0), \"test\")\n\n# patient-level aggregation\nbag = defaultdict(list)\nprob = pipe_final.predict_proba(X_test)[:,1]\nfor p, pr in zip(pid_test, prob): bag[p].append(pr)\nsub = pd.DataFrame({\n    \"patient_id\": list(bag.keys()),\n    \"LAA\":        [np.mean(v) for v in bag.values()]\n})\nsub[\"CE\"] = 1 - sub[\"LAA\"]\nsub[[\"CE\",\"LAA\"]] = sub[[\"CE\",\"LAA\"]].clip(1e-15, 1-1e-15)\nsub = sub[[\"patient_id\",\"CE\",\"LAA\"]]\nsub.to_csv(\"submission.csv\", index=False)\nprint(\"✅ submission.csv saved!\", sub.head())","metadata":{"execution":{"iopub.status.busy":"2025-08-04T01:52:42.943822Z","iopub.execute_input":"2025-08-04T01:52:42.944734Z","execution_failed":"2025-08-04T07:14:17.581Z"}}},{"cell_type":"markdown","source":"# =========================================\n# Mayo-Clinic-STRIP-AI | HOG+LBP+Har+HSV → PCA128 → Linear-SVM  (patient-max pooling)\n# =========================================\n!pip install -q scikit-image mahotas tqdm\n\nfrom pathlib import Path\nfrom collections import defaultdict\nimport random, warnings\nimport numpy as np, pandas as pd\nimport cv2, tifffile, mahotas as mh\nfrom tqdm.auto import tqdm\nfrom skimage.color import rgb2gray\nfrom skimage.feature import hog, local_binary_pattern\nfrom sklearn.decomposition import PCA\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.pipeline import Pipeline\nfrom sklearn.svm import SVC\nfrom sklearn.metrics import roc_auc_score, roc_curve\nfrom sklearn.base import clone\nimport matplotlib.pyplot as plt\n\nwarnings.filterwarnings(\"ignore\")\n\n# ---------- CONFIG ----------\nclass CFG:\n    TILE_SIZE  = 512\n    DEBUG      = True\n    DEBUG_FRAC = 0.5\n    SEED       = 42\nrandom.seed(CFG.SEED); np.random.seed(CFG.SEED)\n\nDATA = Path(\"/kaggle/input/mayo-clinic-strip-ai\")\ntrain_csv = pd.read_csv(DATA/\"train.csv\")\ntest_csv  = pd.read_csv(DATA/\"test.csv\")\n\nif CFG.DEBUG:\n    pats = np.random.RandomState(CFG.SEED).choice(\n        train_csv.patient_id.unique(),\n        size=int(len(train_csv.patient_id.unique())*CFG.DEBUG_FRAC),\n        replace=False)\n    train_csv = train_csv[train_csv.patient_id.isin(pats)]\n    print(f\"[DEBUG] {len(train_csv)} tiles  ({train_csv.patient_id.nunique()} pts)\")\n\nlabel_map = {l:i for i,l in enumerate(sorted(train_csv.label.unique()))}\ntrain_csv[\"label_id\"] = train_csv.label.map(label_map)\n\nval_pat = (train_csv.groupby(\"patient_id\").first()\n           .sample(frac=0.2, random_state=CFG.SEED).index)\ntrain_df = train_csv[~train_csv.patient_id.isin(val_pat)].reset_index(drop=True)\nval_df   = train_csv[ train_csv.patient_id.isin(val_pat)].reset_index(drop=True)\n\n# ---------- handcrafted descriptor ----------\ndef tile_features(img_rgb):\n    img_gray = rgb2gray(img_rgb); img_u8 = (img_gray*255).astype(np.uint8)\n    hog_feat = hog(img_gray, pixels_per_cell=(32,32), cells_per_block=(2,2),\n                   orientations=9, feature_vector=True)\n    lbp = local_binary_pattern(img_u8, 8, 1, \"uniform\")\n    lbp_hist,_ = np.histogram(lbp, bins=np.arange(11), density=True)\n    har = mh.features.haralick(img_u8).mean(axis=0)\n    hsv = cv2.cvtColor((img_rgb*255).astype(np.uint8), cv2.COLOR_RGB2HSV)\n    hsv_hist = cv2.calcHist([hsv],[0,1,2],None,[8,8,8],[0,180,0,256,0,256]).ravel()\n    hsv_hist /= hsv_hist.sum()+1e-6\n    return np.hstack([hog_feat, lbp_hist, har, hsv_hist])\n\ndef build_matrix(df):\n    X,y,pid = [],[],[]\n    for _,row in tqdm(df.iterrows(), total=len(df), desc=\"extract\"):\n        folder = \"train\" if \"label_id\" in row else \"test\"\n        img = tifffile.imread(DATA/folder/f\"{row.image_id}.tif\")\n        if img.ndim==2: img=cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)\n        img = cv2.resize(img,(CFG.TILE_SIZE,CFG.TILE_SIZE),\n                         interpolation=cv2.INTER_AREA)\n        X.append(tile_features(img/255.0))\n        y.append(row.label_id if \"label_id\" in row else 0)\n        pid.append(row.patient_id)\n    return np.vstack(X), np.array(y), np.array(pid)\n\n### <<<  patient-level MAX pooling  --------------------------------------\ndef pool_max(X, y, pid):\n    bag = defaultdict(list)\n    for f,l,p in zip(X,y,pid): bag[p].append((f,l))\n    Xp, yp, ids = [],[],[]\n    for p,lst in bag.items():\n        vec, lbl = zip(*lst)\n        Xp.append(np.max(vec, axis=0))   # ← 关键：feature-wise max\n        yp.append(lbl[0])                # tile 同患者标签一致\n        ids.append(p)\n    return np.vstack(Xp), np.array(yp), np.array(ids)\n# ------------------------------------------------------------------------\n\nprint(\"⏳ extracting train/val descriptors …\")\nX_tr_tl, y_tr_tl, pid_tr = build_matrix(train_df)\nX_va_tl, y_va_tl, pid_va = build_matrix(val_df)\n\n### <<< pool 每位患者\nX_tr, y_tr, _ = pool_max(X_tr_tl, y_tr_tl, pid_tr)\nX_va, y_va, _ = pool_max(X_va_tl, y_va_tl, pid_va)\nprint(\"train shape:\", X_tr.shape, \"| val shape:\", X_va.shape)\n\npca_dim = min(128, X_tr.shape[0]-1, X_tr.shape[1])\n\npipe = Pipeline([\n    (\"scaler\", StandardScaler(with_mean=False)),\n    (\"pca\",    PCA(n_components=pca_dim, whiten=True, random_state=CFG.SEED)),\n    (\"svm\",    SVC(kernel=\"linear\", probability=True,\n                   class_weight=\"balanced\", C=0.1, random_state=CFG.SEED)),\n])\n\npipe.fit(X_tr, y_tr)\npred = pipe.predict_proba(X_va)[:,1]\nprint(f\"Val AUC = {roc_auc_score(y_va, pred):.3f}\")\n\n# ---------- test ----------\nprint(\"⏳ extracting test descriptors …\")\nX_test_tl,_,pid_test = build_matrix(test_csv.assign(label_id=0))\nX_test,_,ids = pool_max(X_test_tl, np.zeros_like(pid_test), pid_test)\nprob_laa = pipe.predict_proba(X_test)[:,1]\n\nsub = pd.DataFrame({\"patient_id\":ids, \"LAA\":prob_laa})\nsub[\"CE\"] = 1 - sub[\"LAA\"]\nsub[[\"CE\",\"LAA\"]] = sub[[\"CE\",\"LAA\"]].clip(1e-15,1-1e-15)\nsub.to_csv(\"submission.csv\", index=False)\nprint(\"✅ submission.csv saved!\", sub.head())\n\n# ---------- ROC ----------\nfpr,tpr,_ = roc_curve(y_va, pred)\nplt.figure(figsize=(5,4)); plt.plot(fpr,tpr,label=f\"AUC={roc_auc_score(y_va,pred):.3f}\")\nplt.plot([0,1],[0,1],'--',c='grey'); plt.xlabel(\"FPR\"); plt.ylabel(\"TPR\")\nplt.title(\"Patient-level Max-pool ROC\"); plt.legend(); plt.grid(alpha=.3); plt.show()","metadata":{"execution":{"iopub.status.busy":"2025-08-04T19:01:37.639009Z","iopub.execute_input":"2025-08-04T19:01:37.640148Z","iopub.status.idle":"2025-08-04T20:02:05.235386Z","shell.execute_reply.started":"2025-08-04T19:01:37.640096Z","shell.execute_reply":"2025-08-04T20:02:05.231604Z"}}},{"cell_type":"code","source":"# ============================================================\n# Mayo-Clinic-STRIP-AI  |  ConvNeXt Embeddings ➜ XGBoost (5-fold)\n# ============================================================\n!pip install -q timm tqdm xgboost albumentations --upgrade\n\nimport os, random, gc, warnings, time\nfrom pathlib import Path\nfrom collections import defaultdict\n\nimport numpy as np, pandas as pd\nimport cv2, tifffile\nfrom tqdm.auto import tqdm\n\nimport torch, torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport albumentations as A; import albumentations.pytorch\nimport timm, xgboost as xgb\n\nfrom sklearn.model_selection import StratifiedGroupKFold\nfrom sklearn.metrics import roc_auc_score, log_loss\n\nwarnings.filterwarnings(\"ignore\")\n\n# -----------------------------\n# 0️⃣  CONFIG\n# -----------------------------\nclass CFG:\n    TILE_SIZE  = 512\n    BATCH      = 16\n    NUM_WK     = 0\n    SEED       = 42\n\n    EPOCHS_CNN = 0     # no end-to-end training, just feature extractor\n    DEBUG      = True\n    DEBUG_FRAC = 0.5\n    BY_PATIENT = True\n\n    DEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n\nrandom.seed(CFG.SEED); np.random.seed(CFG.SEED); torch.manual_seed(CFG.SEED)\n\n# -----------------------------\n# 1️⃣  LOAD CSV\n# -----------------------------\nDATA = Path(\"/kaggle/input/mayo-clinic-strip-ai\")\ntrain_csv = pd.read_csv(DATA/\"train.csv\")\ntest_csv  = pd.read_csv(DATA/\"test.csv\")\n\nif CFG.DEBUG:\n    if CFG.BY_PATIENT:\n        pats = train_csv.patient_id.unique()\n        subset = np.random.RandomState(CFG.SEED).choice(\n            pats, size=int(len(pats)*CFG.DEBUG_FRAC), replace=False)\n        train_csv = train_csv[train_csv.patient_id.isin(subset)]\n    else:\n        train_csv = (train_csv.groupby(\"label\", group_keys=False)\n                     .apply(lambda x: x.sample(frac=CFG.DEBUG_FRAC,\n                                               random_state=CFG.SEED))\n                     .reset_index(drop=True))\n    print(f\"[DEBUG] using {len(train_csv)} tiles ({train_csv.patient_id.nunique()} patients)\")\n\nlabel_map = {l:i for i,l in enumerate(sorted(train_csv.label.unique()))}\ntrain_csv[\"label_id\"] = train_csv.label.map(label_map)\n\n# -----------------------------\n# 2️⃣  DATASET + TRANSFORMS\n# -----------------------------\ntfm = A.Compose([\n    A.Resize(CFG.TILE_SIZE, CFG.TILE_SIZE),\n    A.Normalize((0.485,0.456,0.406),(0.229,0.224,0.225)),\n    A.pytorch.ToTensorV2(),\n])\n\nclass TileDS(Dataset):\n    def __init__(self, df, split):\n        self.df, self.split = df, split\n    def __len__(self): return len(self.df)\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        folder = \"train\" if self.split==\"train\" else \"test_images\"\n        img = tifffile.imread(DATA/folder/f\"{row.image_id}.tif\")\n        if img.ndim==2: img=cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)\n        img = tfm(image=img)[\"image\"]\n        y   = row.label_id if \"label_id\" in row else -1\n        return img.float(), y, row.patient_id\n\n# -----------------------------\n# 3️⃣  FEATURE EXTRACTOR (ConvNeXt-Tiny)\n# -----------------------------\nfe_model = timm.create_model(\n    \"convnext_tiny_in22ft1k\", pretrained=True,\n    num_classes=0, global_pool=\"avg\"\n).to(CFG.DEVICE).eval()\n\n@torch.no_grad()\ndef get_embeds(dl):\n    feats,lbls,pids = [],[],[]\n    for x,y,pid in tqdm(dl, leave=False):\n        f = fe_model(x.to(CFG.DEVICE,non_blocking=True)).cpu().numpy()\n        feats.append(f); lbls.extend(y.numpy()); pids.extend(pid)\n    return np.vstack(feats), np.array(lbls), np.array(pids)\n\ndef pool_max(feats, labels, pids):\n    bag = defaultdict(list)\n    for f,y,p in zip(feats,labels,pids): bag[p].append((f,y))\n    X,y,ids=[],[],[]\n    for p,lst in bag.items():\n        vecs,ys = zip(*lst)\n        X.append(np.max(vecs,0)); y.append(ys[0]); ids.append(p)\n    return np.vstack(X), np.array(y), np.array(ids)\n\n# -----------------------------\n# 4️⃣  5-FOLD CV + XGBoost\n# -----------------------------\nouter = StratifiedGroupKFold(n_splits=5, shuffle=True, random_state=CFG.SEED)\noof_pred, oof_true = [], []\nfold_models        = []\n\nparams = dict(\n    objective=\"binary:logistic\",\n    eval_metric=\"logloss\",\n    eta=0.03, max_depth=7,\n    subsample=0.8, colsample_bytree=0.6,\n    lambda_=1.0, alpha=0.3,\n    min_child_weight=3,\n    seed=CFG.SEED\n)\nEMBED_BATCH = 8\ndef build_embed_loader(df, split):\n    return DataLoader(TileDS(df, split),\n                      batch_size=EMBED_BATCH,\n                      shuffle=False,\n                      num_workers=0,\n                      pin_memory=False)\nfor fold,(tr_idx,va_idx) in enumerate(\n        outer.split(train_csv, train_csv.label_id, train_csv.patient_id)):\n    print(f\"\\n★ Fold {fold}\")\n\n    tr_df = train_csv.iloc[tr_idx].reset_index(drop=True)\n    va_df = train_csv.iloc[va_idx].reset_index(drop=True)\n\n    tr_dl = DataLoader(TileDS(tr_df,\"train\"),\n                       batch_size=CFG.BATCH, shuffle=False,\n                       num_workers=CFG.NUM_WK)\n    va_dl = DataLoader(TileDS(va_df,\"train\"),\n                       batch_size=CFG.BATCH, shuffle=False,\n                       num_workers=CFG.NUM_WK)\n\n    tr_f,tr_y,_ = pool_max(*get_embeds(tr_dl))\n    va_f,va_y,va_pid = pool_max(*get_embeds(va_dl))\n\n    pos_ratio = (tr_y==1).mean()\n    params[\"scale_pos_weight\"] = (1-pos_ratio)/pos_ratio\n\n    mdl = xgb.train(params,\n                    xgb.DMatrix(tr_f,tr_y),\n                    num_boost_round=2000,\n                    evals=[(xgb.DMatrix(va_f,va_y),\"val\")],\n                    early_stopping_rounds=200,\n                    verbose_eval=100)\n    preds = mdl.predict(xgb.DMatrix(va_f))\n    auc   = roc_auc_score(va_y, preds)\n    print(f\"  fold AUC = {auc:.3f}\")\n\n    oof_pred.extend(preds); oof_true.extend(va_y)\n    fold_models.append(mdl)\n\nprint(\"\\nOOF AUC =\", roc_auc_score(oof_true, oof_pred))\n\n# -----------------------------\n# 5️⃣  PREDICT TEST & SUBMIT\n# -----------------------------\ntest_dl = DataLoader(TileDS(test_csv,\"test\"),\n                     batch_size=CFG.BATCH, shuffle=False,\n                     num_workers=CFG.NUM_WK)\n\nf_test, _, pid_test = pool_max(*get_embeds(test_dl))\n\nbag = defaultdict(list)\nfor mdl in fold_models:\n    bag_p = mdl.predict(xgb.DMatrix(f_test))\n    for p,pr in zip(pid_test, bag_p): bag[p].append(pr)\n\nsub = pd.DataFrame({\n    \"patient_id\": list(bag.keys()),\n    \"LAA\": [np.mean(v) for v in bag.values()]\n})\nsub[\"CE\"] = 1 - sub[\"LAA\"]\nsub[[\"CE\",\"LAA\"]] = sub[[\"CE\",\"LAA\"]].clip(1e-15,1-1e-15)\nsub = sub[[\"patient_id\",\"CE\",\"LAA\"]]\nsub.to_csv(\"submission.csv\", index=False)\nprint(\"\\n✅ submission.csv saved!\", sub.head())","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}