{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.13.15","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os, glob, cv2, numpy as np, pandas as pd\nimport torch, torch.nn as nn, torch.nn.functional as F\nfrom PIL import Image\nfrom tqdm.auto import tqdm\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nSIZE = 512\n\nSTAGE_A_CKPT = \"/kaggle/input/notebooks/madi08orazbaev/madeye/checkpoints/stage_a/attn_unet_best.pt\"\nRFMID_ROOT = \"/kaggle/input/datasets/andrewmvd/retinal-disease-classification\"\nAPTOS_ROOT = \"/kaggle/input/competitions/aptos2019-blindness-detection\"\nCACHE_ROOT = \"/kaggle/working/cache\"\n\nprint(device, os.path.exists(STAGE_A_CKPT), os.path.exists(RFMID_ROOT), os.path.exists(APTOS_ROOT))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T15:39:31.106155Z","iopub.execute_input":"2026-10-14T15:39:31.10683Z","iopub.status.idle":"2026-10-14T15:39:39.669699Z","shell.execute_reply.started":"2026-10-14T15:39:31.106784Z","shell.execute_reply":"2026-10-14T15:39:39.668769Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class DoubleConv(nn.Module):\n    def __init__(self, in_ch, out_ch):\n        super().__init__()\n        self.conv = nn.Sequential(\n            nn.Conv2d(in_ch, out_ch, 3, padding=1, bias=False), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True),\n            nn.Conv2d(out_ch, out_ch, 3, padding=1, bias=False), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True),\n        )\n    def forward(self, x): return self.conv(x)\n\nclass Down(nn.Module):\n    def __init__(self, in_ch, out_ch):\n        super().__init__()\n        self.mpconv = nn.Sequential(nn.MaxPool2d(2), DoubleConv(in_ch, out_ch))\n    def forward(self, x): return self.mpconv(x)\n\nclass AttentionGate(nn.Module):\n    def __init__(self, gate_ch, skip_ch, inter_ch):\n        super().__init__()\n        self.W_g = nn.Sequential(nn.Conv2d(gate_ch, inter_ch, 1), nn.BatchNorm2d(inter_ch))\n        self.W_x = nn.Sequential(nn.Conv2d(skip_ch, inter_ch, 1), nn.BatchNorm2d(inter_ch))\n        self.psi = nn.Sequential(nn.Conv2d(inter_ch, 1, 1), nn.BatchNorm2d(1), nn.Sigmoid())\n        self.relu = nn.ReLU(inplace=True)\n    def forward(self, g, x):\n        g1, x1 = self.W_g(g), self.W_x(x)\n        if g1.shape[2:] != x1.shape[2:]:\n            g1 = F.interpolate(g1, size=x1.shape[2:], mode=\"bilinear\", align_corners=True)\n        return x * self.psi(self.relu(g1 + x1))\n\nclass AttnUp(nn.Module):\n    def __init__(self, in_ch, skip_ch, out_ch):\n        super().__init__()\n        self.up = nn.Upsample(scale_factor=2, mode=\"bilinear\", align_corners=True)\n        self.attn = AttentionGate(in_ch, skip_ch, skip_ch // 2)\n        self.conv = DoubleConv(in_ch + skip_ch, out_ch)\n    def forward(self, x1, x2):\n        x1 = self.up(x1)\n        dY, dX = x2.shape[2] - x1.shape[2], x2.shape[3] - x1.shape[3]\n        x1 = F.pad(x1, [dX // 2, dX - dX // 2, dY // 2, dY - dY // 2])\n        x2 = self.attn(g=x1, x=x2)\n        return self.conv(torch.cat([x2, x1], dim=1))\n\nclass AttentionUNet(nn.Module):\n    def __init__(self, n_channels=3, n_classes=1):\n        super().__init__()\n        self.inc = DoubleConv(n_channels, 64)\n        self.down1 = Down(64, 128); self.down2 = Down(128, 256)\n        self.down3 = Down(256, 512); self.down4 = Down(512, 1024)\n        self.up1 = AttnUp(1024, 512, 512); self.up2 = AttnUp(512, 256, 256)\n        self.up3 = AttnUp(256, 128, 128); self.up4 = AttnUp(128, 64, 64)\n        self.outc = nn.Conv2d(64, n_classes, 1)\n    def forward(self, x):\n        x1 = self.inc(x); x2 = self.down1(x1); x3 = self.down2(x2)\n        x4 = self.down3(x3); x5 = self.down4(x4)\n        x = self.up1(x5, x4); x = self.up2(x, x3); x = self.up3(x, x2); x = self.up4(x, x1)\n        return self.outc(x)\n\ndef preprocess_image(img):\n    img = img.astype(np.float32) / 255.0\n    g = img[:, :, 1]\n    g_eq = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)).apply((g * 255).astype(np.uint8)).astype(np.float32) / 255.0\n    g_norm = (g_eq - g_eq.mean()) / (g_eq.std() + 1e-6)\n    return np.stack([g_norm, g_norm, g_norm], axis=-1).astype(np.float32)\n\ndef compute_fov(img_rgb, thr=10):\n    gray = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY)\n    m = (gray > thr).astype(np.uint8)\n    m = cv2.morphologyEx(m, cv2.MORPH_CLOSE, np.ones((15, 15), np.uint8))\n    n, lab, stats, _ = cv2.connectedComponentsWithStats(m)\n    if n > 1:\n        m = (lab == 1 + np.argmax(stats[1:, cv2.CC_STAT_AREA])).astype(np.uint8)\n    return m.astype(np.float32)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T15:39:39.671372Z","iopub.execute_input":"2026-10-14T15:39:39.672057Z","iopub.status.idle":"2026-10-14T15:39:39.689713Z","shell.execute_reply.started":"2026-10-14T15:39:39.672024Z","shell.execute_reply":"2026-10-14T15:39:39.68886Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"stage_a_model = AttentionUNet(n_channels=3, n_classes=1).to(device)\nckpt = torch.load(STAGE_A_CKPT, map_location=device)\nstage_a_model.load_state_dict(ckpt[\"model_state_dict\"])\nstage_a_model.eval()\nfor p in stage_a_model.parameters():\n    p.requires_grad = False\nprint(\"Stage A loaded, best_score:\", ckpt.get(\"best_score\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T15:39:39.690762Z","iopub.execute_input":"2026-10-14T15:39:39.691139Z","iopub.status.idle":"2026-10-14T15:39:42.255661Z","shell.execute_reply.started":"2026-10-14T15:39:39.691106Z","shell.execute_reply":"2026-10-14T15:39:42.254819Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def crop_resize(img_rgb, size=SIZE, work=1024, thr=10):\n    h, w = img_rgb.shape[:2]\n    s = work / max(h, w)\n    small = cv2.resize(img_rgb, (round(w * s), round(h * s)), interpolation=cv2.INTER_AREA) if s < 1 else img_rgb\n    fov = compute_fov(small, thr)\n    ys, xs = np.where(fov > 0)\n    if len(ys) > 100:\n        small = small[ys.min():ys.max() + 1, xs.min():xs.max() + 1]\n    return cv2.resize(small, (size, size), interpolation=cv2.INTER_AREA)\n\n@torch.inference_mode()\ndef vessel_map_from_square(img_sq):\n    fov = compute_fov(img_sq)\n    x = preprocess_image(img_sq)\n    t = torch.from_numpy(x.transpose(2, 0, 1)).float().unsqueeze(0).to(device)\n    p = torch.sigmoid(stage_a_model(t)).float().cpu().numpy()[0, 0]\n    return p * fov\n\ndef cache_split(paths, name):\n    img_dir, ves_dir = f\"{CACHE_ROOT}/{name}/img\", f\"{CACHE_ROOT}/{name}/vessel\"\n    os.makedirs(img_dir, exist_ok=True); os.makedirs(ves_dir, exist_ok=True)\n    bad = 0\n    for p in tqdm(paths, desc=name):\n        stem = os.path.splitext(os.path.basename(p))[0]\n        out_i, out_v = f\"{img_dir}/{stem}.jpg\", f\"{ves_dir}/{stem}.png\"\n        if os.path.exists(out_i) and os.path.exists(out_v):\n            continue\n        try:\n            sq = crop_resize(np.array(Image.open(p).convert(\"RGB\")))\n            v = vessel_map_from_square(sq)\n            Image.fromarray(sq).save(out_i, quality=95)\n            Image.fromarray((v * 255).astype(np.uint8)).save(out_v)\n        except Exception as e:\n            bad += 1\n            print(\"skip\", p, type(e).__name__, e)\n    print(name, \"done, skipped:\", bad)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T15:39:42.257616Z","iopub.execute_input":"2026-10-14T15:39:42.257937Z","iopub.status.idle":"2026-10-14T15:39:42.267133Z","shell.execute_reply.started":"2026-10-14T15:39:42.257916Z","shell.execute_reply":"2026-10-14T15:39:42.266402Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"rfmid_splits = {\n    \"rfmid_train\": f\"{RFMID_ROOT}/Training_Set/Training_Set/Training\",\n    \"rfmid_val\":   f\"{RFMID_ROOT}/Evaluation_Set/Evaluation_Set/Validation\",\n    \"rfmid_test\":  f\"{RFMID_ROOT}/Test_Set/Test_Set/Test\",\n}\nfor name, folder in rfmid_splits.items():\n    cache_split(sorted(glob.glob(f\"{folder}/*.png\")), name)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T15:39:42.268046Z","iopub.execute_input":"2026-10-14T15:39:42.268425Z","iopub.status.idle":"2026-10-14T15:56:05.962262Z","shell.execute_reply.started":"2026-10-14T15:39:42.268395Z","shell.execute_reply":"2026-10-14T15:56:05.961278Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cache_split(sorted(glob.glob(f\"{APTOS_ROOT}/train_images/*.png\")), \"aptos_train\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T15:56:05.963418Z","iopub.execute_input":"2026-10-14T15:56:05.963749Z","iopub.status.idle":"2026-10-14T16:14:19.699827Z","shell.execute_reply.started":"2026-10-14T15:56:05.963726Z","shell.execute_reply":"2026-10-14T16:14:19.698872Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for d in sorted(os.listdir(CACHE_ROOT)):\n    print(d, len(os.listdir(f\"{CACHE_ROOT}/{d}/img\")), len(os.listdir(f\"{CACHE_ROOT}/{d}/vessel\")))\n\nimport matplotlib.pyplot as plt\nsamples = []\nfor name in [\"rfmid_train\", \"aptos_train\"]:\n    stems = sorted(os.listdir(f\"{CACHE_ROOT}/{name}/vessel\"))[:2]\n    samples += [(name, s[:-4]) for s in stems]\n\nfig, ax = plt.subplots(len(samples), 3, figsize=(10, 3.3 * len(samples)))\nfor i, (name, stem) in enumerate(samples):\n    im = np.array(Image.open(f\"{CACHE_ROOT}/{name}/img/{stem}.jpg\"))\n    ve = np.array(Image.open(f\"{CACHE_ROOT}/{name}/vessel/{stem}.png\"))\n    ov = im.copy(); ov[ve > 127] = [255, 40, 40]\n    for j, (a, t) in enumerate([(im, \"image\"), (ve, \"vessel map\"), (ov, \"overlay\")]):\n        ax[i, j].imshow(a, cmap=\"gray\" if j == 1 else None)\n        ax[i, j].axis(\"off\"); ax[i, j].set_title(f\"{name}/{stem} {t}\", fontsize=8)\nplt.tight_layout(); plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T16:14:19.701094Z","iopub.execute_input":"2026-10-14T16:14:19.701429Z","iopub.status.idle":"2026-10-14T16:14:20.958228Z","shell.execute_reply.started":"2026-10-14T16:14:19.701398Z","shell.execute_reply":"2026-10-14T16:14:20.957403Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"rfmid_csv = {\n    \"train\": f\"{RFMID_ROOT}/Training_Set/Training_Set/RFMiD_Training_Labels.csv\",\n    \"val\":   f\"{RFMID_ROOT}/Evaluation_Set/Evaluation_Set/RFMiD_Validation_Labels.csv\",\n    \"test\":  f\"{RFMID_ROOT}/Test_Set/Test_Set/RFMiD_Testing_Labels.csv\",\n}\nlab = {k: pd.read_csv(v) for k, v in rfmid_csv.items()}\ncols = [c for c in lab[\"train\"].columns if c != \"ID\"]\n\ncnt = pd.DataFrame({k: lab[k][cols].sum() for k in lab})\ncnt[\"total\"] = cnt.sum(axis=1)\nprint(cnt.sort_values(\"train\", ascending=False).to_string())\nprint({k: len(v) for k, v in lab.items()})","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T16:14:20.959251Z","iopub.execute_input":"2026-10-14T16:14:20.959582Z","iopub.status.idle":"2026-10-14T16:14:21.13351Z","shell.execute_reply.started":"2026-10-14T16:14:20.959549Z","shell.execute_reply":"2026-10-14T16:14:21.132906Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os, random, numpy as np, pandas as pd, cv2, torch\nimport torch.nn as nn\nfrom PIL import Image\nfrom torch.utils.data import Dataset, DataLoader, Subset\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nRFMID_ROOT = \"/kaggle/input/datasets/andrewmvd/retinal-disease-classification\"\nCACHE_ROOT = \"/kaggle/working/cache\"\n\nrfmid_csv = {\n    \"train\": f\"{RFMID_ROOT}/Training_Set/Training_Set/RFMiD_Training_Labels.csv\",\n    \"val\":   f\"{RFMID_ROOT}/Evaluation_Set/Evaluation_Set/RFMiD_Validation_Labels.csv\",\n    \"test\":  f\"{RFMID_ROOT}/Test_Set/Test_Set/RFMiD_Testing_Labels.csv\",\n}\nlab = {k: pd.read_csv(v) for k, v in rfmid_csv.items()}\ncols = [c for c in lab[\"train\"].columns if c != \"ID\"]\ncnt = pd.DataFrame({k: lab[k][cols].sum() for k in lab})\n\n# метка берётся, только если в val И в test >= 10 положительных\nTARGETS = [c for c in cols if cnt.loc[c, \"val\"] >= 10 and cnt.loc[c, \"test\"] >= 10]\nprint(len(TARGETS), \"targets:\", TARGETS)\n\n\ndef fov_eroded(img_rgb, thr=10, erode=7):\n    gray = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY)\n    m = (gray > thr).astype(np.uint8)\n    m = cv2.morphologyEx(m, cv2.MORPH_CLOSE, np.ones((15, 15), np.uint8))\n    n, labm, stats, _ = cv2.connectedComponentsWithStats(m)\n    if n > 1:\n        m = (labm == 1 + np.argmax(stats[1:, cv2.CC_STAT_AREA])).astype(np.uint8)\n    return cv2.erode(m, np.ones((erode, erode), np.uint8)).astype(np.float32)\n\n\nclass RFMiDDS(Dataset):\n    def __init__(self, split, df, targets, size=384, train=False):\n        self.split, self.size, self.train = split, size, train\n        self.ids = df[\"ID\"].astype(str).tolist()\n        self.y = df[targets].values.astype(np.float32)\n\n    def __len__(self):\n        return len(self.ids)\n\n    def __getitem__(self, i):\n        stem, s = self.ids[i], self.size\n        img = np.array(Image.open(f\"{CACHE_ROOT}/rfmid_{self.split}/img/{stem}.jpg\").convert(\"RGB\"))\n        ves = np.array(Image.open(f\"{CACHE_ROOT}/rfmid_{self.split}/vessel/{stem}.png\").convert(\"L\"))\n        img = cv2.resize(img, (s, s), interpolation=cv2.INTER_AREA)\n        ves = cv2.resize(ves, (s, s), interpolation=cv2.INTER_AREA).astype(np.float32) / 255.0\n        ves = ves * fov_eroded(img)          # убираем краевые артефакты по кругу сетчатки\n\n        if self.train:\n            if random.random() < 0.5: img, ves = img[:, ::-1], ves[:, ::-1]\n            if random.random() < 0.5: img, ves = img[::-1], ves[::-1]\n            M = cv2.getRotationMatrix2D((s / 2, s / 2), random.uniform(-20, 20), 1.0)\n            img = cv2.warpAffine(np.ascontiguousarray(img), M, (s, s), flags=cv2.INTER_LINEAR, borderValue=0)\n            ves = cv2.warpAffine(np.ascontiguousarray(ves), M, (s, s), flags=cv2.INTER_LINEAR, borderValue=0)\n            a, b = random.uniform(0.85, 1.15), random.uniform(-15, 15)\n            img = np.clip(img.astype(np.float32) * a + b, 0, 255).astype(np.uint8)\n\n        x = torch.from_numpy(np.ascontiguousarray(img)).permute(2, 0, 1).float() / 255.0\n        v = torch.from_numpy(np.ascontiguousarray(ves))[None]\n        return x, v, torch.from_numpy(self.y[i])\n\n\n# проверка глазами: фото / карта сосудов / soft / hard\nimport matplotlib.pyplot as plt\nds = RFMiDDS(\"train\", lab[\"train\"], TARGETS, size=384, train=False)\nfig, ax = plt.subplots(3, 4, figsize=(13, 10))\nfor r, idx in enumerate([0, 1, 2]):\n    x, v, y = ds[idx]\n    soft = x * (0.3 + 0.7 * v)\n    hard = x * (0.3 + 0.7 * (v > 0.5).float())\n    for c, (im, t) in enumerate([(x, \"raw\"), (v.expand(3, -1, -1), \"vessel map\"), (soft, \"soft\"), (hard, \"hard\")]):\n        ax[r, c].imshow(im.permute(1, 2, 0).numpy()); ax[r, c].axis(\"off\")\n        ax[r, c].set_title(t + (f\" | {[TARGETS[j] for j in np.where(y.numpy() > 0)[0]]}\" if c == 0 else \"\"), fontsize=8)\nplt.tight_layout(); plt.show()\nx, v, y = ds[0]; print(\"shapes:\", x.shape, v.shape, y.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T16:14:21.134673Z","iopub.execute_input":"2026-10-14T16:14:21.135028Z","iopub.status.idle":"2026-10-14T16:14:22.497209Z","shell.execute_reply.started":"2026-10-14T16:14:21.134999Z","shell.execute_reply":"2026-10-14T16:14:22.496496Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import time, torchvision\nfrom sklearn.metrics import roc_auc_score\n\n\ndef set_seed(seed):\n    random.seed(seed); np.random.seed(seed); torch.manual_seed(seed); torch.cuda.manual_seed_all(seed)\n\n\ndef make_backbone():\n    try:\n        m = torchvision.models.resnet18(weights=torchvision.models.ResNet18_Weights.DEFAULT)\n    except Exception as e:\n        print(\"pretrained weights unavailable:\", type(e).__name__)\n        m = torchvision.models.resnet18(weights=None)\n    m.fc = nn.Identity()\n    return m\n\n\nclass DRNet(nn.Module):\n    \"\"\"mode: raw | dualraw | soft | hard | shuffled\"\"\"\n    def __init__(self, n_out, mode=\"soft\", alpha=0.3, thr=0.5):\n        super().__init__()\n        self.mode, self.alpha, self.thr = mode, alpha, thr\n        self.a = make_backbone()\n        self.b = None if mode == \"raw\" else make_backbone()\n        self.head = nn.Sequential(nn.Dropout(0.3), nn.Linear(512 * (1 if mode == \"raw\" else 2), n_out))\n        self.register_buffer(\"mean\", torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1))\n        self.register_buffer(\"std\", torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1))\n\n    def norm(self, x):\n        return (x - self.mean) / self.std\n\n    def forward(self, x, v):\n        fa = self.a(self.norm(x))\n        if self.mode == \"raw\":\n            return self.head(fa)\n        if self.mode == \"dualraw\":\n            xb = x\n        else:\n            if self.mode == \"hard\":       m = (v > self.thr).float()\n            elif self.mode == \"shuffled\": m = v.roll(1, dims=0)\n            else:                         m = v\n            xb = x * (self.alpha + (1 - self.alpha) * m)\n        return self.head(torch.cat([fa, self.b(self.norm(xb))], dim=1))\n\n\ndef evaluate(model, loader):\n    model.eval(); ps, ys = [], []\n    with torch.inference_mode():\n        for x, v, y in loader:\n            with torch.autocast(\"cuda\", enabled=device.type == \"cuda\"):\n                lo = model(x.to(device), v.to(device))\n            ps.append(torch.sigmoid(lo.float()).cpu().numpy()); ys.append(y.numpy())\n    P, Y = np.concatenate(ps), np.concatenate(ys)\n    aucs = [roc_auc_score(Y[:, j], P[:, j]) for j in range(Y.shape[1])]\n    return float(np.mean(aucs)), aucs\n\n\ndef run(mode, seed, epochs=15, bs=32, lr=3e-4, size=384, alpha=0.3, limit=None, workers=4):\n    set_seed(seed)\n    tr = RFMiDDS(\"train\", lab[\"train\"], TARGETS, size, True)\n    va = RFMiDDS(\"val\", lab[\"val\"], TARGETS, size, False)\n    te = RFMiDDS(\"test\", lab[\"test\"], TARGETS, size, False)\n    pos = tr.y.sum(0); neg = len(tr.y) - pos\n    pw = torch.tensor(np.clip(neg / pos, 0.2, 10), dtype=torch.float32, device=device)\n    tr_use = Subset(tr, list(range(limit))) if limit else tr\n    g = torch.Generator(); g.manual_seed(seed)\n    dl_tr = DataLoader(tr_use, bs, shuffle=True, drop_last=True, num_workers=workers, pin_memory=True, generator=g)\n    dl_va = DataLoader(va, bs, shuffle=False, num_workers=workers, pin_memory=True)\n    dl_te = DataLoader(te, bs, shuffle=False, num_workers=workers, pin_memory=True)\n\n    model = DRNet(len(TARGETS), mode, alpha).to(device)\n    loss_fn = nn.BCEWithLogitsLoss(pos_weight=pw)\n    opt = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4)\n    sched = torch.optim.lr_scheduler.OneCycleLR(opt, max_lr=lr, total_steps=epochs * len(dl_tr))\n    scaler = torch.amp.GradScaler(\"cuda\", enabled=device.type == \"cuda\")\n\n    best, best_state, t0 = -1.0, None, time.time()\n    for ep in range(epochs):\n        model.train(); tl = 0.0\n        for x, v, y in dl_tr:\n            x, v, y = x.to(device, non_blocking=True), v.to(device, non_blocking=True), y.to(device, non_blocking=True)\n            opt.zero_grad(set_to_none=True)\n            with torch.autocast(\"cuda\", enabled=device.type == \"cuda\"):\n                lo = model(x, v)\n            loss = loss_fn(lo.float(), y)\n            scaler.scale(loss).backward(); scaler.step(opt); scaler.update(); sched.step()\n            tl += loss.item()\n        vauc, _ = evaluate(model, dl_va)\n        print(f\"[{mode} s{seed}] ep {ep:02d} | loss {tl/len(dl_tr):.4f} | val AUC {vauc:.4f} | {time.time()-t0:.0f}s\")\n        if vauc > best:\n            best, best_state = vauc, {k: t.detach().clone() for k, t in model.state_dict().items()}\n    model.load_state_dict(best_state)\n    tauc, taucs = evaluate(model, dl_te)\n    print(f\"[{mode} s{seed}] best val {best:.4f} -> TEST macro-AUC {tauc:.4f}\")\n    return {\"mode\": mode, \"seed\": seed, \"val_auc\": best, \"test_auc\": tauc, **{f\"auc_{t}\": a for t, a in zip(TARGETS, taucs)}}\n\n\n# тест на 1 эпоху и 256 снимках: проверяем, что всё работает, и сколько длится эпоха\nresults = []\nr = run(\"soft\", seed=0, epochs=1, limit=256)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T16:14:22.499271Z","iopub.execute_input":"2026-10-14T16:14:22.499605Z","iopub.status.idle":"2026-10-14T16:14:45.301758Z","shell.execute_reply.started":"2026-10-14T16:14:22.499583Z","shell.execute_reply":"2026-10-14T16:14:45.300915Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for mode in [\"raw\", \"dualraw\", \"soft\", \"hard\", \"shuffled\"]:\n    results.append(run(mode, seed=0))\npd.DataFrame(results)[[\"mode\", \"seed\", \"val_auc\", \"test_auc\"]]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-14T16:14:45.303145Z","iopub.execute_input":"2026-10-14T16:14:45.303733Z","iopub.status.idle":"2026-10-14T16:43:38.325255Z","shell.execute_reply.started":"2026-10-14T16:14:45.303708Z","shell.execute_reply":"2026-10-14T16:43:38.32455Z"}},"outputs":[],"execution_count":null}]}