{"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":"none","dataSources":[{"sourceId":37333,"databundleVersionId":3949526,"sourceType":"competition"}],"dockerImageVersionId":31089,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","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\nfrom tqdm.auto import tqdm \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   = 0\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)\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# Focal Loss（或 Weighted CE）就能用到 alpha / weight\ncriterion = FocalLoss(alpha=w, gamma=2)\ntrain_mode  = \"cnn\"           # \"cnn\" | \"xgb\"  (决定提交用哪个模型)\n\n\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 tqdm(dl, leave=False, desc=\"batch\"):\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 tqdm(dl, leave=False, desc=\"embed\"):\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":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-08-03T21:46:26.904026Z","iopub.execute_input":"2025-08-03T21:46:26.904295Z","iopub.status.idle":"2025-08-03T23:45:04.501402Z","shell.execute_reply.started":"2025-08-03T21:46:26.904264Z","shell.execute_reply":"2025-08-03T23:45:04.499539Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","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      = False\n    DEBUG_FRAC = 0.05\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":{"execution":{"iopub.status.busy":"2025-08-04T13:28:30.578032Z","iopub.execute_input":"2025-08-04T13:28:30.579905Z","execution_failed":"2025-08-04T17:27:24.897Z"}}}]}