{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","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":[{"sourceType":"competition","sourceId":59093,"databundleVersionId":7469972}],"dockerImageVersionId":31329,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"5643de41","cell_type":"markdown","source":"# HMS — Harmful Brain Activity Classification (Kaggle Notebook)\n\nЦей ноутбук з нуля:\n- читає `train.csv` / `test.csv`\n- будує таргети як ймовірності з vote-колонок\n- тренує простий baseline на **Kaggle spectrograms**\n- робить inference на test і зберігає **`submission.csv`** у `/kaggle/working`\n- робить **фінальну перевірку** `submission.csv`, щоб Kaggle не відхилив файл\n\n## Як засабмітити результат (Code Competition)\n1. У ноутбуку **Add data** → підключи **Competition data: HMS - Harmful Brain Activity Classification** (і переконайся, що в Input є `train.csv`).\n2. **Internet = Off** (обов’язково для HMS code competition): у редакторі ноутбука справа **Settings → Internet → Off**, потім **Save Version** (інакше Submit буде `Cannot submit`).\n3. **Run All** (або хоча б від комірки з читанням даних до кінця).\n4. Перевір Output: має бути **`/kaggle/working/submission.csv`**.\n   - Після читання `sample_submission.csv` ноутбук одразу пише **stub** `submission.csv`, щоб commit **завжди** мав output-файл (навіть якщо тренування далі впаде).\n   - Якщо все дійшло до кінця — остання комірка **перезаписує** `submission.csv` реальними передбаченнями і друкує **`SUBMISSION OK`**.\n5. **Save Version** (commit) → обов’язково версія, де в Output реально є `submission.csv` → **Submit**.\n\n> Важливо: це **baseline** (простий, стабільний, швидкий). Його можна покращувати (крос-валідація, сильніші моделі, аугментації, мультимодальність, псевдо-лейбли тощо).","metadata":{}},{"id":"281639ed","cell_type":"code","source":"import os\nimport math\nimport random\nfrom dataclasses import dataclass\n\nimport numpy as np\nimport pandas as pd\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\n\nfrom sklearn.model_selection import GroupKFold\n\n# Repro\nSEED = 42\n\ndef seed_everything(seed: int = SEED):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n\nseed_everything(SEED)\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nTARGETS = [\n    \"seizure_vote\",\n    \"lpd_vote\",\n    \"gpd_vote\",\n    \"lrda_vote\",\n    \"grda_vote\",\n    \"other_vote\",\n]\n\n@dataclass\nclass CFG:\n    # Paths (Kaggle)\n    # You can override locally with env var HMS_DATA_DIR=/path/to/dataset\n    data_dir: str = os.environ.get(\n        \"HMS_DATA_DIR\",\n        \"/kaggle/input/hms-harmful-brain-activity-classification\",\n    )\n\n    # Training\n    fold: int = 0\n    n_folds: int = 5\n    epochs: int = 6\n    batch_size: int = 64\n    lr: float = 1e-3\n    weight_decay: float = 1e-4\n\n    # Augmentations (work well for spectrograms)\n    do_aug: bool = True\n    # SpecAugment-style masking\n    time_mask_max: int = 40\n    freq_mask_max: int = 30\n    # MixUp (good with soft labels + KL)\n    mixup_alpha: float = 0.3\n\n    # Spectrogram windowing\n    # Kaggle spectrograms are 10 minutes; common baselines take a 300-column window.\n    # We'll try to slice around the label offset.\n    window_cols: int = 300\n\n    # Performance\n    num_workers: int = 2\n    amp: bool = True\n\ncfg = CFG()\nprint(\"device:\", DEVICE)\nprint(\"data_dir exists:\", os.path.exists(cfg.data_dir))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-21T08:45:36.397780Z","iopub.execute_input":"2026-04-21T08:45:36.398502Z","iopub.status.idle":"2026-04-21T08:45:36.410611Z","shell.execute_reply.started":"2026-04-21T08:45:36.398465Z","shell.execute_reply":"2026-04-21T08:45:36.409781Z"}},"outputs":[],"execution_count":null},{"id":"1cdedbfb","cell_type":"code","source":"def resolve_data_dir(preferred: str) -> str:\n    \"\"\"Find HMS dataset root under /kaggle/input.\n\n    Kaggle sometimes mounts competition data under a non-obvious folder name, or nested paths.\n    We search shallowly for a directory that contains the expected HMS files.\n    \"\"\"\n\n    def is_hms_root(path: str) -> bool:\n        return (\n            os.path.exists(os.path.join(path, \"train.csv\"))\n            and os.path.exists(os.path.join(path, \"test.csv\"))\n            and os.path.exists(os.path.join(path, \"sample_submission.csv\"))\n            and os.path.isdir(os.path.join(path, \"train_spectrograms\"))\n            and os.path.isdir(os.path.join(path, \"test_spectrograms\"))\n        )\n\n    if is_hms_root(preferred):\n        return preferred\n\n    kaggle_input = \"/kaggle/input\"\n    if not os.path.isdir(kaggle_input):\n        return preferred\n\n    candidates: list[str] = []\n\n    # 1) Direct children of /kaggle/input\n    for name in sorted(os.listdir(kaggle_input)):\n        root = os.path.join(kaggle_input, name)\n        if os.path.isdir(root) and is_hms_root(root):\n            candidates.append(root)\n\n    # 2) Shallow walk (some mounts nest one extra level)\n    max_depth = 4\n    for top in sorted([os.path.join(kaggle_input, n) for n in os.listdir(kaggle_input)]):\n        if not os.path.isdir(top):\n            continue\n        for dirpath, dirnames, filenames in os.walk(top):\n            depth = dirpath[len(top) :].count(os.sep)\n            if depth > max_depth:\n                dirnames[:] = []\n                continue\n            if is_hms_root(dirpath):\n                candidates.append(dirpath)\n\n    candidates = sorted(set(candidates), key=len)\n\n    if not candidates:\n        # Helpful debug for Kaggle UI\n        try:\n            mounts = sorted(os.listdir(kaggle_input))\n        except Exception:\n            mounts = []\n        raise FileNotFoundError(\n            \"Could not locate HMS dataset root (folder containing train.csv + train_spectrograms/...).\\n\"\n            \"Fix: Add data -> Competition data -> HMS - Harmful Brain Activity Classification.\\n\"\n            \"Also: make sure you uploaded the latest version of hms_hbac_baseline.ipynb into Kaggle.\\n\"\n            f\"Tried preferred path: {preferred}\\n\"\n            f\"/kaggle/input mounts: {mounts[:50]}{' ...' if len(mounts) > 50 else ''}\"\n        )\n\n    # Prefer official slug-like mount if present\n    for c in candidates:\n        bn = os.path.basename(c).lower()\n        if \"hms\" in bn and \"harmful\" in bn:\n            return c\n    return candidates[0]\n\n\ncfg.data_dir = resolve_data_dir(cfg.data_dir)\nprint(\"Using cfg.data_dir =\", cfg.data_dir)\n\ntrain_path = os.path.join(cfg.data_dir, \"train.csv\")\ntest_path = os.path.join(cfg.data_dir, \"test.csv\")\nsub_path = os.path.join(cfg.data_dir, \"sample_submission.csv\")\n\ntrain = pd.read_csv(train_path)\ntest = pd.read_csv(test_path)\nsub = pd.read_csv(sub_path)\n\nprint(\"train:\", train.shape)\nprint(\"test:\", test.shape)\nprint(\"sample_submission:\", sub.shape)\n\n# Kaggle Submit safety: ensure *some* submission.csv exists even if later cells fail.\n# This will be overwritten by the final inference cell when training completes.\nstub = sub[[\"eeg_id\"]].copy()\nstub[TARGETS] = 1.0 / len(TARGETS)\n\nout_path_stub = \"/kaggle/working/submission.csv\" if os.path.exists(\"/kaggle/working\") else \"submission.csv\"\nif out_path_stub.startswith(\"/kaggle/working\"):\n    os.makedirs(\"/kaggle/working\", exist_ok=True)\nstub.to_csv(out_path_stub, index=False)\nprint(\"wrote stub submission (will be overwritten later):\", out_path_stub, \"rows:\", len(stub))\n\n# Kaggle UI debug: prove the file exists on disk for THIS run/version\nprint(\"submission exists:\", os.path.exists(out_path_stub), \"size_bytes:\", (os.path.getsize(out_path_stub) if os.path.exists(out_path_stub) else -1))\nif os.path.exists(\"/kaggle/working\"):\n    try:\n        print(\"/kaggle/working files:\", sorted(os.listdir(\"/kaggle/working\")))\n    except Exception as e:\n        print(\"could not list /kaggle/working:\", e)\n\ndisplay(train.head())\ndisplay(test.head())\ndisplay(sub.head())\n\n# normalize votes -> target probabilities\ntrain[\"total_votes\"] = train[TARGETS].sum(axis=1)\ntrain = train[train[\"total_votes\"] > 0].reset_index(drop=True)\ntrain[TARGETS] = train[TARGETS].div(train[\"total_votes\"], axis=0)\n\nprint(\"train after total_votes>0:\", train.shape)\nprint(\"targets sum (min/max):\", train[TARGETS].sum(axis=1).min(), train[TARGETS].sum(axis=1).max())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-21T08:45:36.411913Z","iopub.execute_input":"2026-04-21T08:45:36.412160Z","iopub.status.idle":"2026-04-21T08:45:58.964760Z","shell.execute_reply.started":"2026-04-21T08:45:36.412137Z","shell.execute_reply":"2026-04-21T08:45:58.963954Z"}},"outputs":[],"execution_count":null},{"id":"cbefec52","cell_type":"code","source":"SPECTRO_COLS = [\"LL\", \"LP\", \"RL\", \"RP\"]\n\n_spectro_cache = {}\n\ndef _select_region_cols(cols, region: str):\n    # Common Kaggle format: columns like \"0.5_LL\" ... \"20.0_LL\"\n    out = [c for c in cols if str(c).endswith(f\"_{region}\")]\n    if out:\n        return out\n\n    # Alternate: columns like \"LL_0.5\" (rare)\n    out = [c for c in cols if str(c).startswith(f\"{region}_\")]\n    if out:\n        return out\n\n    # Fallback: any column containing region token\n    out = [c for c in cols if f\"{region}\" in str(c)]\n    return out\n\n\ndef load_spectrogram_image(spectrogram_id: int, offset_seconds: float, *, is_test: bool) -> np.ndarray:\n    \"\"\"Return stacked image as float32 array with shape (H, W).\n\n    - Reads `/train_spectrograms/{id}.parquet` or `/test_spectrograms/{id}.parquet`\n    - Builds 4 panels (LL/LP/RL/RP) and stacks vertically\n    - Crops a `cfg.window_cols` time window around `offset_seconds`\n    \"\"\"\n    key = (int(spectrogram_id), bool(is_test))\n    if key in _spectro_cache:\n        df = _spectro_cache[key]\n    else:\n        folder = \"test_spectrograms\" if is_test else \"train_spectrograms\"\n        path = os.path.join(cfg.data_dir, folder, f\"{int(spectrogram_id)}.parquet\")\n        df = pd.read_parquet(path)\n        _spectro_cache[key] = df\n\n    cols = list(df.columns)\n    if \"time\" in cols:\n        cols_wo_time = [c for c in cols if c != \"time\"]\n        df0 = df[cols_wo_time]\n    else:\n        df0 = df\n\n    panels = []\n    for region in SPECTRO_COLS:\n        rcols = _select_region_cols(df0.columns, region)\n        if not rcols:\n            # If something is off, make a zero panel with same time length as df0\n            panels.append(np.zeros((1, len(df0)), dtype=np.float32))\n            continue\n        arr = df0[rcols].to_numpy(dtype=np.float32)  # (time, freq)\n        arr = np.nan_to_num(arr, nan=0.0, posinf=0.0, neginf=0.0)\n        arr = arr.T  # (freq, time)\n        panels.append(arr)\n\n    # Make all panels same freq size (use min to be safe)\n    min_f = min(p.shape[0] for p in panels)\n    panels = [p[:min_f] for p in panels]\n\n    img = np.vstack(panels)  # (freq*4, time)\n\n    # Window crop along time axis\n    T = img.shape[1]\n    win = int(cfg.window_cols)\n    if win <= 0:\n        win = T\n\n    # Kaggle spectrogram time axis typically spans 600s with 300 columns -> ~2s per column\n    center = int(round(float(offset_seconds) / 2.0))\n    start = max(0, min(T - win, center - win // 2))\n    end = start + win\n\n    if T >= win:\n        img = img[:, start:end]\n    else:\n        pad = win - T\n        img = np.pad(img, ((0, 0), (0, pad)), mode=\"constant\", constant_values=0.0)\n\n    # log + per-image normalize\n    img = np.log1p(img)\n    m = float(img.mean())\n    s = float(img.std())\n    img = (img - m) / (s + 1e-6)\n\n    return img.astype(np.float32)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-21T08:45:58.965808Z","iopub.execute_input":"2026-04-21T08:45:58.966250Z","iopub.status.idle":"2026-04-21T08:45:58.976893Z","shell.execute_reply.started":"2026-04-21T08:45:58.966224Z","shell.execute_reply":"2026-04-21T08:45:58.976154Z"}},"outputs":[],"execution_count":null},{"id":"d4cad9d8","cell_type":"code","source":"def _rand_mask_2d(x: torch.Tensor, *, dim: int, max_width: int) -> torch.Tensor:\n    \"\"\"Zero-mask along a dimension.\n\n    x: (C,H,W)\n    dim=1 -> freq axis (H), dim=2 -> time axis (W)\n    \"\"\"\n    if max_width <= 0:\n        return x\n    width = int(torch.randint(low=0, high=max_width + 1, size=(1,)).item())\n    if width <= 0:\n        return x\n    L = x.shape[dim]\n    if width >= L:\n        return x\n    start = int(torch.randint(low=0, high=L - width, size=(1,)).item())\n    sl = [slice(None)] * x.ndim\n    sl[dim] = slice(start, start + width)\n    x = x.clone()\n    x[tuple(sl)] = 0.0\n    return x\n\n\nclass HMSDataset(Dataset):\n    def __init__(self, df: pd.DataFrame, *, is_test: bool, augment: bool = False):\n        self.df = df.reset_index(drop=True)\n        self.is_test = is_test\n        self.augment = bool(augment)\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx: int):\n        row = self.df.iloc[idx]\n        spectrogram_id = int(row[\"spectrogram_id\"])\n\n        # Kaggle description sometimes spells it as \"spectogram_label_offset_seconds\".\n        offset = float(\n            row.get(\n                \"spectrogram_label_offset_seconds\",\n                row.get(\"spectogram_label_offset_seconds\", 0.0),\n            )\n        )\n\n        img = load_spectrogram_image(spectrogram_id, offset, is_test=self.is_test)  # (H, W)\n        x = torch.from_numpy(img).unsqueeze(0).unsqueeze(0)  # (1, 1, H, W)\n        x = F.interpolate(x, size=(256, 256), mode=\"bilinear\", align_corners=False)\n        x = x.squeeze(0)  # (1, 256, 256)\n\n        if self.augment and (not self.is_test):\n            # SpecAugment-style: mask time/freq\n            x = _rand_mask_2d(x, dim=1, max_width=int(cfg.freq_mask_max))\n            x = _rand_mask_2d(x, dim=2, max_width=int(cfg.time_mask_max))\n\n        if self.is_test:\n            return {\n                \"x\": x,\n                \"eeg_id\": int(row[\"eeg_id\"]),\n            }\n\n        y = torch.tensor(row[TARGETS].to_numpy(dtype=np.float32))\n        return {\n            \"x\": x,\n            \"y\": y,\n        }\n\n\ndef make_loaders(train_df: pd.DataFrame, val_df: pd.DataFrame):\n    ds_tr = HMSDataset(train_df, is_test=False, augment=bool(cfg.do_aug))\n    ds_va = HMSDataset(val_df, is_test=False, augment=False)\n\n    dl_tr = DataLoader(\n        ds_tr,\n        batch_size=cfg.batch_size,\n        shuffle=True,\n        num_workers=cfg.num_workers,\n        pin_memory=True,\n        drop_last=True,\n    )\n    dl_va = DataLoader(\n        ds_va,\n        batch_size=cfg.batch_size,\n        shuffle=False,\n        num_workers=cfg.num_workers,\n        pin_memory=True,\n        drop_last=False,\n    )\n    return dl_tr, dl_va","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-21T08:45:58.978638Z","iopub.execute_input":"2026-04-21T08:45:58.978864Z","iopub.status.idle":"2026-04-21T08:45:59.000029Z","shell.execute_reply.started":"2026-04-21T08:45:58.978841Z","shell.execute_reply":"2026-04-21T08:45:58.999478Z"}},"outputs":[],"execution_count":null},{"id":"ccc69c5f","cell_type":"code","source":"class SmallCNN(nn.Module):\n    def __init__(self, in_ch: int = 1, n_classes: int = 6):\n        super().__init__()\n        self.conv1 = nn.Sequential(\n            nn.Conv2d(in_ch, 32, 3, padding=1),\n            nn.BatchNorm2d(32),\n            nn.SiLU(),\n            nn.MaxPool2d(2),\n        )\n        self.conv2 = nn.Sequential(\n            nn.Conv2d(32, 64, 3, padding=1),\n            nn.BatchNorm2d(64),\n            nn.SiLU(),\n            nn.MaxPool2d(2),\n        )\n        self.conv3 = nn.Sequential(\n            nn.Conv2d(64, 128, 3, padding=1),\n            nn.BatchNorm2d(128),\n            nn.SiLU(),\n            nn.MaxPool2d(2),\n        )\n        self.conv4 = nn.Sequential(\n            nn.Conv2d(128, 256, 3, padding=1),\n            nn.BatchNorm2d(256),\n            nn.SiLU(),\n            nn.MaxPool2d(2),\n        )\n\n        self.head = nn.Sequential(\n            nn.AdaptiveAvgPool2d((1, 1)),\n            nn.Flatten(),\n            nn.Dropout(0.2),\n            nn.Linear(256, n_classes),\n        )\n\n    def forward(self, x):\n        x = self.conv1(x)\n        x = self.conv2(x)\n        x = self.conv3(x)\n        x = self.conv4(x)\n        return self.head(x)\n\n\ndef kl_loss_from_logits(logits: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:\n    # targets are probabilities\n    log_probs = F.log_softmax(logits, dim=1)\n    return F.kl_div(log_probs, targets, reduction=\"batchmean\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-21T08:45:59.001015Z","iopub.execute_input":"2026-04-21T08:45:59.001262Z","iopub.status.idle":"2026-04-21T08:45:59.020609Z","shell.execute_reply.started":"2026-04-21T08:45:59.001238Z","shell.execute_reply":"2026-04-21T08:45:59.020081Z"}},"outputs":[],"execution_count":null},{"id":"7c1d4ca8","cell_type":"code","source":"# Build folds\ntrain = train.copy()\ntrain[\"fold\"] = -1\n\ngkf = GroupKFold(n_splits=cfg.n_folds)\nfor f, (_, va_idx) in enumerate(gkf.split(train, groups=train[\"patient_id\"])):\n    train.loc[va_idx, \"fold\"] = f\n\ntr_df = train[train[\"fold\"] != cfg.fold].reset_index(drop=True)\nva_df = train[train[\"fold\"] == cfg.fold].reset_index(drop=True)\nprint(\"fold sizes:\", len(tr_df), len(va_df))\n\ntrain_loader, val_loader = make_loaders(tr_df, va_df)\n\nmodel = SmallCNN(in_ch=1, n_classes=len(TARGETS)).to(DEVICE)\noptimizer = torch.optim.AdamW(model.parameters(), lr=cfg.lr, weight_decay=cfg.weight_decay)\nscaler = torch.cuda.amp.GradScaler(enabled=(cfg.amp and DEVICE.type == \"cuda\"))\n\n# Scheduler: OneCycle works well for quick improvements\nsteps_per_epoch = max(1, len(train_loader))\nscheduler = torch.optim.lr_scheduler.OneCycleLR(\n    optimizer,\n    max_lr=cfg.lr,\n    epochs=cfg.epochs,\n    steps_per_epoch=steps_per_epoch,\n    pct_start=0.1,\n    div_factor=10.0,\n    final_div_factor=100.0,\n)\n\nbest_val = 1e9\nbest_path = \"/kaggle/working/best_model.pt\" if os.path.exists(\"/kaggle/working\") else \"best_model.pt\"\n\n\ndef _mixup(x: torch.Tensor, y: torch.Tensor, alpha: float):\n    if alpha <= 0:\n        return x, y\n    lam = np.random.beta(alpha, alpha)\n    idx = torch.randperm(x.size(0), device=x.device)\n    x2 = x[idx]\n    y2 = y[idx]\n    x = x * lam + x2 * (1.0 - lam)\n    y = y * lam + y2 * (1.0 - lam)\n    return x, y\n\n\ndef run_train_epoch(model, loader):\n    model.train()\n    total = 0.0\n    n = 0\n    for batch in loader:\n        x = batch[\"x\"].to(DEVICE, non_blocking=True)\n        y = batch[\"y\"].to(DEVICE, non_blocking=True)\n\n        if (not torch.isnan(y).any()) and cfg.mixup_alpha > 0:\n            x, y = _mixup(x, y, float(cfg.mixup_alpha))\n\n        optimizer.zero_grad(set_to_none=True)\n        with torch.cuda.amp.autocast(enabled=(cfg.amp and DEVICE.type == \"cuda\")):\n            logits = model(x)\n            loss = kl_loss_from_logits(logits, y)\n\n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n        scheduler.step()\n\n        total += float(loss.detach().cpu()) * x.size(0)\n        n += x.size(0)\n    return total / max(n, 1)\n\n\ndef run_valid_epoch(model, loader):\n    model.eval()\n    total = 0.0\n    n = 0\n    with torch.no_grad():\n        for batch in loader:\n            x = batch[\"x\"].to(DEVICE, non_blocking=True)\n            y = batch[\"y\"].to(DEVICE, non_blocking=True)\n            logits = model(x)\n            loss = kl_loss_from_logits(logits, y)\n            total += float(loss.detach().cpu()) * x.size(0)\n            n += x.size(0)\n    return total / max(n, 1)\n\n\nfor epoch in range(cfg.epochs):\n    tr_loss = run_train_epoch(model, train_loader)\n    va_loss = run_valid_epoch(model, val_loader)\n    print(f\"epoch {epoch+1}/{cfg.epochs} | train_KL={tr_loss:.5f} | val_KL={va_loss:.5f}\")\n\n    if va_loss < best_val:\n        best_val = va_loss\n        torch.save({\"model\": model.state_dict(), \"cfg\": cfg.__dict__}, best_path)\n        print(\"saved best ->\", best_path)\n\nprint(\"best_val_KL:\", best_val)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-21T08:45:59.021595Z","iopub.execute_input":"2026-04-21T08:45:59.021834Z"}},"outputs":[],"execution_count":null},{"id":"ce737d5a","cell_type":"code","source":"def kaggle_kl_divergence(y_true: np.ndarray, y_pred: np.ndarray, eps: float = 1e-8) -> float:\n    \"\"\"Mean KL divergence used by Kaggle for this comp.\n\n    y_true, y_pred: (N, 6) probabilities.\n    \"\"\"\n    y_true = np.asarray(y_true, dtype=np.float64)\n    y_pred = np.asarray(y_pred, dtype=np.float64)\n\n    y_true = np.clip(y_true, eps, 1.0)\n    y_true = y_true / y_true.sum(axis=1, keepdims=True)\n\n    y_pred = np.clip(y_pred, eps, 1.0)\n    y_pred = y_pred / y_pred.sum(axis=1, keepdims=True)\n\n    kl = np.sum(y_true * (np.log(y_true) - np.log(y_pred)), axis=1)\n    return float(np.mean(kl))\n\n\n# Validate best checkpoint on the whole validation fold\nckpt = torch.load(best_path, map_location=\"cpu\")\nval_model = SmallCNN(in_ch=1, n_classes=len(TARGETS)).to(DEVICE)\nval_model.load_state_dict(ckpt[\"model\"], strict=True)\nval_model.eval()\n\nval_ds = HMSDataset(va_df, is_test=False)\nval_loader_full = DataLoader(\n    val_ds,\n    batch_size=cfg.batch_size,\n    shuffle=False,\n    num_workers=cfg.num_workers,\n    pin_memory=True,\n    drop_last=False,\n)\n\ny_true = []\ny_pred = []\nwith torch.no_grad():\n    for batch in val_loader_full:\n        x = batch[\"x\"].to(DEVICE, non_blocking=True)\n        y = batch[\"y\"].cpu().numpy()\n        logits = val_model(x)\n        p = F.softmax(logits, dim=1).detach().cpu().numpy()\n        y_true.append(y)\n        y_pred.append(p)\n\ny_true = np.concatenate(y_true, axis=0)\ny_pred = np.concatenate(y_pred, axis=0)\n\nval_kl = kaggle_kl_divergence(y_true, y_pred)\nprint(f\"VALIDATION (fold={cfg.fold}) KL-div: {val_kl:.6f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-21T10:07:47.976673Z","iopub.execute_input":"2026-04-21T10:07:47.976953Z","iopub.status.idle":"2026-04-21T10:10:01.377874Z","shell.execute_reply.started":"2026-04-21T10:07:47.976922Z","shell.execute_reply":"2026-04-21T10:10:01.376869Z"}},"outputs":[],"execution_count":null},{"id":"e39feb24","cell_type":"markdown","source":"## Submit checklist (після run)\n\n- У **Output** є файл **`submission.csv`** (не `sample_submission.csv`).\n- Якщо тренування дійшло до кінця: у логах останньої комірки має бути **`SUBMISSION OK`** (це означає, що stub перезаписано реальними прогнозами).\n- Далі: **Save Version → Submit** у змаганні HMS (вибирай версію, де `submission.csv` реально є в Output).","metadata":{}},{"id":"d1345801","cell_type":"code","source":"# Load best model\nckpt = torch.load(best_path, map_location=\"cpu\")\nmodel = SmallCNN(in_ch=1, n_classes=len(TARGETS)).to(DEVICE)\nmodel.load_state_dict(ckpt[\"model\"], strict=True)\nmodel.eval()\nprint(\"loaded best model from\", best_path)\n\n# Predict on test\ntest_ds = HMSDataset(test, is_test=True)\ntest_loader = DataLoader(\n    test_ds,\n    batch_size=cfg.batch_size,\n    shuffle=False,\n    num_workers=cfg.num_workers,\n    pin_memory=True,\n)\n\nall_eeg = []\nall_p = []\n\nwith torch.no_grad():\n    for batch in test_loader:\n        x = batch[\"x\"].to(DEVICE, non_blocking=True)\n        eeg_id = batch[\"eeg_id\"].cpu().numpy()\n        logits = model(x)\n        p = F.softmax(logits, dim=1).detach().cpu().numpy()\n        all_eeg.append(eeg_id)\n        all_p.append(p)\n\nall_eeg = np.concatenate(all_eeg, axis=0)\nall_p = np.concatenate(all_p, axis=0)\n\npred_df = pd.DataFrame(all_p, columns=TARGETS)\npred_df[\"eeg_id\"] = all_eeg\n\n# Some eeg_id appear multiple times in test metadata -> average\npred_df = pred_df.groupby(\"eeg_id\", as_index=False)[TARGETS].mean()\n\n# Align to sample_submission\nsubmission = sub[[\"eeg_id\"]].merge(pred_df, on=\"eeg_id\", how=\"left\")\n\n# Safety: if any eeg_id missing (shouldn't), fill uniform\nmiss = submission[TARGETS].isna().any(axis=1)\nif miss.any():\n    submission.loc[miss, TARGETS] = 1.0 / len(TARGETS)\n\n# Ensure probabilities sum to 1\nprobs = submission[TARGETS].to_numpy(dtype=np.float64)\nprobs = np.clip(probs, 1e-8, 1.0)\nprobs = probs / probs.sum(axis=1, keepdims=True)\nsubmission[TARGETS] = probs\n\nout_path = \"/kaggle/working/submission.csv\" if os.path.exists(\"/kaggle/working\") else \"submission.csv\"\nsubmission.to_csv(out_path, index=False)\nprint(\"saved:\", out_path)\ndisplay(submission.head())\n\n# --- Final submission validation (Kaggle-friendly) ---\nexpected_cols = [\"eeg_id\"] + TARGETS\nif list(submission.columns) != expected_cols:\n    raise ValueError(f\"Bad header/order. Got: {list(submission.columns)} Expected: {expected_cols}\")\n\nif submission[\"eeg_id\"].duplicated().any():\n    raise ValueError(\"Duplicate eeg_id in submission\")\n\nmissing = submission[TARGETS].isna().any().any()\nif missing:\n    raise ValueError(\"NaNs in submission probabilities\")\n\nif (submission[TARGETS].to_numpy() < 0).any():\n    raise ValueError(\"Negative probabilities in submission\")\n\nrow_sums = submission[TARGETS].sum(axis=1).astype(float)\nif (row_sums - 1.0).abs().max() > 1e-3:\n    raise ValueError(f\"Rows must sum to ~1. min={row_sums.min()} max={row_sums.max()}\")\n\nif len(submission) != len(sub):\n    print(\"WARNING: submission row count != sample_submission row count:\", len(submission), \"vs\", len(sub))\n\nprint(\"SUBMISSION OK:\", out_path)\nprint(\"rows:\", len(submission))\nprint(\"row sum min/max:\", float(row_sums.min()), float(row_sums.max()))\n\nprint(\"final submission exists:\", os.path.exists(out_path), \"size_bytes:\", (os.path.getsize(out_path) if os.path.exists(out_path) else -1))\nif os.path.exists(\"/kaggle/working\"):\n    try:\n        print(\"/kaggle/working files (final):\", sorted(os.listdir(\"/kaggle/working\")))\n    except Exception as e:\n        print(\"could not list /kaggle/working:\", e)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-21T10:10:01.379554Z","iopub.execute_input":"2026-04-21T10:10:01.379849Z","iopub.status.idle":"2026-04-21T10:10:01.605878Z","shell.execute_reply.started":"2026-04-21T10:10:01.379818Z","shell.execute_reply":"2026-04-21T10:10:01.605023Z"}},"outputs":[],"execution_count":null}]}