{"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":[{"sourceId":39272,"databundleVersionId":4629629,"sourceType":"competition"},{"sourceId":289735675,"sourceType":"kernelVersion"}],"dockerImageVersionId":31236,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip -q install pylibjpeg pylibjpeg-libjpeg pylibjpeg-openjpeg timm pydicom","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import random\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\n\nimport torchvision.transforms as T\nimport timm\nimport pydicom\n\nfrom sklearn.metrics import average_precision_score\n\nDEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(\"DEVICE:\", DEVICE)\n\nSEED = 42          # 42 / 43 / 44\nIMG_SIZE = 224\nBATCH_SIZE = 32\nEPOCHS = 3\nLR = 2e-4\n\nNUM_WORKERS = 0    # <- 0 = pas de bugs DataLoader (après tu peux mettre 2)\nOUT_DIR = Path(\"/kaggle/working/mammo01_runs\") / f\"seed_{SEED}\"\nOUT_DIR.mkdir(parents=True, exist_ok=True)\nprint(\"OUT_DIR:\", OUT_DIR)\n\ndef set_seed(seed: int):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n\nset_seed(SEED)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"TOP /kaggle/input =\", [p.name for p in Path(\"/kaggle/input\").iterdir()])\nprint(\"train_images dirs =\", len(list(Path(\"/kaggle/input\").rglob(\"train_images\"))))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def find_splits_root():\n    for d in Path(\"/kaggle/input\").iterdir():\n        if (d / \"mammo_patient_split_v1\" / \"seed_42\" / \"train.csv\").exists():\n            return d / \"mammo_patient_split_v1\"\n        if (d / \"seed_42\" / \"train.csv\").exists():\n            return d\n    raise FileNotFoundError(\"Splits introuvables. Ajoute ton dataset splits en input.\")\n\nSPLITS_ROOT = find_splits_root()\nprint(\"SPLITS_ROOT:\", SPLITS_ROOT)\n\nseed_dir = SPLITS_ROOT / f\"seed_{SEED}\"\ndf_train = pd.read_csv(seed_dir / \"train.csv\")\ndf_val   = pd.read_csv(seed_dir / \"val.csv\")\ndf_test  = pd.read_csv(seed_dir / \"test.csv\")\n\nprint(\"Shapes:\", df_train.shape, df_val.shape, df_test.shape)\nprint(\"Pos rate:\", df_train[\"label\"].mean(), df_val[\"label\"].mean(), df_test[\"label\"].mean())","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def find_train_images_dir():\n    candidates = [p for p in Path(\"/kaggle/input\").rglob(\"train_images\") if p.is_dir()]\n    if not candidates:\n        raise FileNotFoundError(\"train_images introuvable. Ajoute RSNA en input.\")\n    # on prend le plus gros\n    candidates = sorted(candidates, key=lambda x: len(list(x.glob(\"*\"))), reverse=True)\n    return candidates[0]\n\nTRAIN_IMG_DIR = find_train_images_dir()\nprint(\"TRAIN_IMG_DIR:\", TRAIN_IMG_DIR)\n\ndef add_paths(df):\n    df = df.copy()\n    df[\"dcm_path\"] = df.apply(\n        lambda r: str(TRAIN_IMG_DIR / str(int(r[\"patient_id\"])) / f\"{int(r['image_id'])}.dcm\"),\n        axis=1\n    )\n    return df\n\ndf_train = add_paths(df_train)\ndf_val   = add_paths(df_val)\ndf_test  = add_paths(df_test)\n\n# vérif rapide\nfrom pathlib import Path as P\nsample = df_train[\"dcm_path\"].sample(50, random_state=0).tolist()\nprint(\"exists sample =\", sum(P(x).exists() for x in sample), \"/ 50\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def read_dicom_uint8(path: str):\n    dcm = pydicom.dcmread(path)\n    img = dcm.pixel_array.astype(np.float32)\n\n    # inversion si besoin\n    if getattr(dcm, \"PhotometricInterpretation\", \"\") == \"MONOCHROME1\":\n        img = img.max() - img\n\n    # normalisation simple\n    lo, hi = np.percentile(img, (1, 99))\n    img = np.clip(img, lo, hi)\n    img -= img.min()\n    if img.max() > 0:\n        img /= img.max()\n\n    img = (img * 255.0).clip(0,255).astype(np.uint8)\n    return img\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class MammoDS(Dataset):\n    def __init__(self, df, tf):\n        self.df = df.reset_index(drop=True)\n        self.tf = tf\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, i):\n        r = self.df.iloc[i]\n        img = read_dicom_uint8(r[\"dcm_path\"])\n        x = self.tf(img)\n        y = int(r[\"label\"])\n        meta = {\"patient_id\": int(r[\"patient_id\"]), \"image_id\": int(r[\"image_id\"]), \"path\": r[\"dcm_path\"]}\n        return x, y, meta\n\ntrain_tf = T.Compose([\n    T.ToPILImage(),\n    T.Resize((IMG_SIZE, IMG_SIZE)),\n    T.RandomRotation(10),\n    T.ToTensor(),\n    T.Lambda(lambda t: t.repeat(3,1,1)),\n])\n\neval_tf = T.Compose([\n    T.ToPILImage(),\n    T.Resize((IMG_SIZE, IMG_SIZE)),\n    T.ToTensor(),\n    T.Lambda(lambda t: t.repeat(3,1,1)),\n])\n\ntrain_loader = DataLoader(MammoDS(df_train, train_tf), batch_size=BATCH_SIZE, shuffle=True,  num_workers=NUM_WORKERS)\nval_loader   = DataLoader(MammoDS(df_val,   eval_tf),  batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS)\ntest_loader  = DataLoader(MammoDS(df_test,  eval_tf),  batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS)\n\nprint(\"Loaders OK\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = timm.create_model(\"tf_efficientnet_b0_ns\", pretrained=True, num_classes=1).to(DEVICE)\n\npos = float(df_train[\"label\"].sum())\nneg = float(len(df_train) - pos)\npos_weight = torch.tensor([neg / max(pos, 1.0)], device=DEVICE)\ncrit = nn.BCEWithLogitsLoss(pos_weight=pos_weight)\n\nopt = torch.optim.AdamW(model.parameters(), lr=LR)\n\ndef eval_loader(loader):\n    model.eval()\n    ys, ps, losses = [], [], []\n    with torch.no_grad():\n        for x, y, _ in loader:\n            x = x.to(DEVICE)\n            y = torch.tensor(y, dtype=torch.float32, device=DEVICE).view(-1,1)\n            logit = model(x)\n            loss = crit(logit, y)\n            p = torch.sigmoid(logit).detach().cpu().numpy().reshape(-1)\n            ys.append(y.detach().cpu().numpy().reshape(-1))\n            ps.append(p)\n            losses.append(loss.item())\n    y_all = np.concatenate(ys)\n    p_all = np.concatenate(ps)\n    ap = average_precision_score(y_all, p_all)\n    return float(np.mean(losses)), float(ap)\n\nbest_ap = -1\nbest_path = OUT_DIR / \"model_best.pth\"\nhist = []\n\nfor epoch in range(1, EPOCHS+1):\n    model.train()\n    tr_losses = []\n    for x, y, _ in train_loader:\n        x = x.to(DEVICE)\n        y = torch.tensor(y, dtype=torch.float32, device=DEVICE).view(-1,1)\n        opt.zero_grad()\n        logit = model(x)\n        loss = crit(logit, y)\n        loss.backward()\n        opt.step()\n        tr_losses.append(loss.item())\n\n    tr_loss = float(np.mean(tr_losses))\n    va_loss, va_ap = eval_loader(val_loader)\n    hist.append({\"epoch\": epoch, \"train_loss\": tr_loss, \"val_loss\": va_loss, \"val_auc_pr\": va_ap})\n    print(f\"Epoch {epoch} | train_loss={tr_loss:.4f} | val_loss={va_loss:.4f} | val_AUC-PR={va_ap:.4f}\")\n\n    if va_ap > best_ap:\n        best_ap = va_ap\n        torch.save(model.state_dict(), best_path)\n        print(\" saved:\", best_path)\n\npd.DataFrame(hist).to_csv(OUT_DIR/\"train_history.csv\", index=False)\nprint(\"Saved train_history.csv\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.load_state_dict(torch.load(best_path, map_location=DEVICE))\n\ndef export_preds(loader, out_csv):\n    model.eval()\n    rows = []\n    with torch.no_grad():\n        for x, y, meta in loader:\n            x = x.to(DEVICE)\n            logit = model(x).detach().cpu().numpy().reshape(-1)\n            prob = 1/(1+np.exp(-logit))\n            for i in range(len(y)):\n                rows.append({\n                    \"patient_id\": meta[\"patient_id\"][i].item() if hasattr(meta[\"patient_id\"][i], \"item\") else int(meta[\"patient_id\"][i]),\n                    \"image_id\": meta[\"image_id\"][i].item() if hasattr(meta[\"image_id\"][i], \"item\") else int(meta[\"image_id\"][i]),\n                    \"path\": meta[\"path\"][i],\n                    \"label\": int(y[i]),\n                    \"logit\": float(logit[i]),\n                    \"prob_raw\": float(prob[i]),\n                })\n    pd.DataFrame(rows).to_csv(out_csv, index=False)\n    print(\"Exported:\", out_csv, \"| n=\", len(rows))\n\nexport_preds(val_loader,  OUT_DIR/\"preds_val.csv\")\nexport_preds(test_loader, OUT_DIR/\"preds_test.csv\")\n\nprint(\"DONE. Files:\")\nfor p in sorted(OUT_DIR.iterdir()):\n    print(\" -\", p.name)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}