From 49dafd3f0ccbbb70c7ec86b266b7081871500ce1 Mon Sep 17 00:00:00 2001 From: bondingelectron Date: Tue, 11 Aug 2026 16:25:18 +0800 Subject: [PATCH] fix(sleep-edf): declare and apply microvolt input scaling (v0.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The scorer returned MNE's SI-unit (volt) arrays while the prompt's preprocessing contract said nothing about units, and the shape contract exemplified inputs with torch.randn (unit scale). The academic convention for EEG is microvolts (MOABB / braindecode expose µV-scale arrays), and bciciv-2a v0.2 already adopted µV — keep the bench consistent. - scale epochs to microvolts in load_subject_recording (x1e6, float32) - declare "input_unit": "microvolts" in the prompt preprocessing dict and note µV in the input shape contract - bump task version 0.1 -> 0.2 (breaking: scores not comparable to v0.1) - add preprocessing contract unit tests (label mapping, µV scaling, interface constants, training hyperparameters) and wire them into the validate workflow, mirroring bciciv-2a v0.2 - document the unit in both READMEs Co-Authored-By: Claude Opus 4.8 --- .github/workflows/validate.yml | 2 + tasks/sleep-edf/README.md | 9 ++- tasks/sleep-edf/checks/README.md | 6 +- .../checks/test_preprocessing_contract.py | 81 +++++++++++++++++++ tasks/sleep-edf/checks/train_and_infer.py | 17 +++- tasks/sleep-edf/prompt/turns.yaml | 2 +- tasks/sleep-edf/task.yaml | 2 +- 7 files changed, 106 insertions(+), 13 deletions(-) create mode 100644 tasks/sleep-edf/checks/test_preprocessing_contract.py diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 6f4314d..b988aab 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -49,6 +49,8 @@ jobs: .venv/bin/python -c 'import numpy, scipy, sklearn; print(numpy.__version__, scipy.__version__, sklearn.__version__)' - name: Verify bciciv-2a preprocessing contract run: .venv/bin/python -m unittest tasks/bciciv-2a/checks/test_preprocessing_contract.py + - name: Verify sleep-edf preprocessing contract + run: .venv/bin/python -m unittest tasks/sleep-edf/checks/test_preprocessing_contract.py validate: strategy: diff --git a/tasks/sleep-edf/README.md b/tasks/sleep-edf/README.md index ad22ae3..8f513a6 100644 --- a/tasks/sleep-edf/README.md +++ b/tasks/sleep-edf/README.md @@ -60,8 +60,8 @@ mirror for cross-subject LOSO training) is off by default; opt in with Design a PyTorch `nn.Module` **SleepAgentModel** that classifies a `(B, 1, 3000)` tensor of preprocessed Fpz-Cz sleep EEG (30 s epoch at -100 Hz) into 5 sleep stages (Wake / N1 / N2 / N3 / REM). The submitted -file goes to: +100 Hz, microvolts) into 5 sleep stages (Wake / N1 / N2 / N3 / REM). +The submitted file goes to: - `EEG_sleep/sleep_agent_model.py` in the agent's workspace. @@ -92,8 +92,9 @@ subject 16-19 EDF or hypnogram — those recordings come from - Loads all recordings for subjects 0-13 / 14-15 / 16-19, applying the frozen preprocessing (pick `EEG Fpz-Cz`, 0.3-35 Hz bandpass, resample to 100 Hz, head/tail Wake crop to 30 min each, 30 s non-overlapping - epoching, `Sleep stage 3` and `Sleep stage 4` merged into label 3, - `Movement time` / `Sleep stage ?` dropped). + epoching, scale MNE volts to microvolts, `Sleep stage 3` and + `Sleep stage 4` merged into label 3, `Movement time` / `Sleep stage ?` + dropped). - Trains once with Adam lr=1e-3, batch 64, class-weighted CrossEntropyLoss, up to 20 epochs, early-stops on val loss (patience 5). - Predicts every 30 s epoch on subjects 16-19; writes diff --git a/tasks/sleep-edf/checks/README.md b/tasks/sleep-edf/checks/README.md index a0a4e5d..afff0b2 100644 --- a/tasks/sleep-edf/checks/README.md +++ b/tasks/sleep-edf/checks/README.md @@ -50,9 +50,9 @@ Otherwise it calls `evaluate_external.py $PWD`, which: - Loads all recordings for subjects 0-13 (train), 14-15 (val), and 16-19 (test), applying the frozen preprocessing (pick `EEG Fpz-Cz`, 0.3-35 Hz bandpass, resample to 100 Hz, head/tail Wake crop to - 30 min each, 30 s non-overlapping epoching, `Sleep stage 3` and - `Sleep stage 4` merged into label 3, `Movement time` / `Sleep stage ?` - dropped). + 30 min each, 30 s non-overlapping epoching, scale MNE volts to + microvolts, `Sleep stage 3` and `Sleep stage 4` merged into label 3, + `Movement time` / `Sleep stage ?` dropped). - Trains a fresh `SleepAgentModel()` with Adam lr=1e-3, batch 64, class-weighted CrossEntropyLoss (inverse-frequency weights over the train pool), up to 20 epochs, early-stops on val loss with diff --git a/tasks/sleep-edf/checks/test_preprocessing_contract.py b/tasks/sleep-edf/checks/test_preprocessing_contract.py new file mode 100644 index 0000000..5cf4585 --- /dev/null +++ b/tasks/sleep-edf/checks/test_preprocessing_contract.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Unit tests for the sleep-edf scorer's frozen preprocessing contract.""" + +from __future__ import annotations + +import importlib.util +import sys +import types +import unittest +from pathlib import Path + +import numpy as np + + +def _load_runner_module(): + """Import contract helpers without requiring the evaluator's torch/MNE stack.""" + mne = types.ModuleType("mne") + mne.set_log_level = lambda _level: None + mne.io = types.SimpleNamespace() + + torch = types.ModuleType("torch") + torch_nn = types.ModuleType("torch.nn") + torch.nn = torch_nn + + sys.modules["mne"] = mne + sys.modules["torch"] = torch + sys.modules["torch.nn"] = torch_nn + + path = Path(__file__).with_name("train_and_infer.py") + spec = importlib.util.spec_from_file_location("sleep_edf_train_and_infer", path) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +RUNNER = _load_runner_module() + + +class PreprocessingContractTest(unittest.TestCase): + def test_label_mapping_merges_stage3_and_stage4_into_n3(self): + mapping = RUNNER.LABEL_MAPPING + self.assertEqual( + mapping, + { + "Sleep stage W": 0, + "Sleep stage 1": 1, + "Sleep stage 2": 2, + "Sleep stage 3": 3, + "Sleep stage 4": 3, + "Sleep stage R": 4, + }, + ) + # The five model logits map directly to labels 0..4 in W/N1/N2/N3/REM order. + self.assertEqual(sorted(set(mapping.values())), [0, 1, 2, 3, 4]) + + def test_mne_volts_are_scaled_to_float32_microvolts(self): + volts = np.asarray([1.0e-6, -22.9e-6], dtype=np.float64) + + microvolts = RUNNER._to_microvolts(volts) + + self.assertEqual(microvolts.dtype, np.float32) + np.testing.assert_allclose(microvolts, [1.0, -22.9], rtol=1e-6) + + def test_model_interface_is_30s_at_100hz(self): + self.assertEqual(RUNNER.WINDOW_S, 30.0) + self.assertEqual(RUNNER.SFREQ_TARGET, 100.0) + self.assertEqual(RUNNER.N_TIMES, 3000) + self.assertEqual(RUNNER.N_CLASSES, 5) + self.assertEqual(RUNNER.EEG_CHANNEL, "EEG Fpz-Cz") + self.assertEqual(RUNNER.CROP_WAKE_MINS, 30) + + def test_training_hyperparameters_match_prompt(self): + self.assertEqual(RUNNER.LR, 1e-3) + self.assertEqual(RUNNER.BATCH_SIZE, 64) + self.assertEqual(RUNNER.MAX_EPOCHS, 20) + self.assertEqual(RUNNER.EARLY_STOP_PATIENCE, 5) + + +if __name__ == "__main__": + unittest.main() diff --git a/tasks/sleep-edf/checks/train_and_infer.py b/tasks/sleep-edf/checks/train_and_infer.py index 8666883..f5c9f18 100755 --- a/tasks/sleep-edf/checks/train_and_infer.py +++ b/tasks/sleep-edf/checks/train_and_infer.py @@ -11,8 +11,9 @@ 2. For every subject in --train-subjects and --val-subjects, loads both PSG + Hypnogram recordings, does the frozen preprocessing (pick `EEG Fpz-Cz`, 0.3-35 Hz bandpass, resample to 100 Hz, crop head/tail - Wake to 30 min each, cut into non-overlapping 30 s epochs) → per-epoch - features (1, 3000) plus labels 0..4 from the hypnogram annotations. + Wake to 30 min each, cut into non-overlapping 30 s epochs, scale + MNE volts to microvolts) → per-epoch features (1, 3000) plus labels + 0..4 from the hypnogram annotations. 3. Concatenates train epochs across subjects 0-13 → one big pool with a class-weighted CrossEntropyLoss. Trains a fresh SleepAgentModel with Adam lr=1e-3, batch 64, up to 20 epochs, early-stops on val loss @@ -51,6 +52,7 @@ N_TIMES = int(round(WINDOW_S * SFREQ_TARGET)) # 3000 EEG_CHANNEL = "EEG Fpz-Cz" CROP_WAKE_MINS = 30 +MICROVOLTS_PER_VOLT = 1e6 # Hypnogram annotation → BPB label mapping. LABEL_MAPPING = { @@ -71,12 +73,19 @@ SEED = 42 +def _to_microvolts(data: np.ndarray) -> np.ndarray: + """Convert MNE's SI-unit EEG arrays to microvolts (the sleep-staging + community convention — MOABB/braindecode expose µV-scale arrays).""" + return (np.asarray(data) * MICROVOLTS_PER_VOLT).astype(np.float32) + + def load_subject_recording(psg_path: Path, hyp_path: Path) -> tuple[np.ndarray, np.ndarray]: """Return (X: (N,1,3000), y: (N,) labels 0..4) for one PSG + Hypnogram pair. Applies: single-channel pick, 0.3-35 Hz filter, resample to 100 Hz, head/tail Wake crop (30 min each), 30 s non-overlapping epoching, - label mapping. `Movement time` / `Sleep stage ?` epochs are dropped. + label mapping, V→µV scaling. `Movement time` / `Sleep stage ?` epochs + are dropped. """ if not psg_path.exists(): raise RuntimeError(f"missing PSG file: {psg_path}") @@ -120,7 +129,7 @@ def load_subject_recording(psg_path: Path, hyp_path: Path) -> tuple[np.ndarray, tmin=0.0, tmax=WINDOW_S - 1.0 / SFREQ_TARGET, baseline=None, preload=True, proj=False, verbose="ERROR", ) - X = epochs.get_data().astype(np.float32) + X = _to_microvolts(epochs.get_data()) # inv: event_id integer → BPB label id_to_desc = {v: k for k, v in event_id.items()} y = np.asarray( diff --git a/tasks/sleep-edf/prompt/turns.yaml b/tasks/sleep-edf/prompt/turns.yaml index 8b1606c..89cdb25 100644 --- a/tasks/sleep-edf/prompt/turns.yaml +++ b/tasks/sleep-edf/prompt/turns.yaml @@ -7,7 +7,7 @@ # subjects 0-13 (with val 14-15) and blind-tests on 16-19. [ { - "send": "You are a deep-learning architect for sleep-staging on EEG. Design a PyTorch model that classifies 30-second single-channel Fpz-Cz sleep epochs into 5 sleep stages on the Sleep-EDF Cassette benchmark.\n\n## Input / output shape contract\n\nThe model's `forward` input tensor shape is fixed to:\n\n (Batch, 1, 3000)\n\nwhere:\n- 1 is the single Fpz-Cz EEG channel\n- 3000 is the number of time points in a 30 s sleep epoch at 100 Hz\n\nThe `forward` output tensor shape must be exactly:\n\n (Batch, 5)\n\ncorresponding to the five stages, in this channel order:\n\n Wake, N1, N2, N3, REM\n\n## Dataset and split\n\n- Dataset: Sleep-EDF Expanded — Sleep Cassette\n- Subjects: 20 (subject 0–19; two PSG + Hypnogram recordings per subject, except subject 13 which has one)\n- **Agent-visible data directory: `./public_data/`** (relative to your workspace; `env/setup-agent.sh` unpacks it from the pinned HF release)\n - Recordings: `public_data/sleep-cassette/SC4nn[0-9]E0-PSG.edf` and matching `-Hypnogram.edf` for subjects 0-15 (62 files total)\n - Split manifests: `public_data/manifests/{subjects.json,default_split.json,loso_folds.json}`\n - Loader: `public_data/get_fold_data.py` (contains zero-leakage asserts and both split loaders)\n - Label mapping: `public_data/label_mapping.json`\n- **Subject 16-19 data does not appear in your workspace.** The scorer reads the four private test subjects (PSG + Hypnogram) from an isolated evaluator-only directory.\n\nThe fixed benchmark split is subject-boundary strict (zero leakage):\n\n| Split | Subjects | Count | Visibility |\n|-------|----------|-------|------------|\n| Train | 0–13 | 14 | public |\n| Val | 14–15 | 2 | public |\n| Test | 16–19 | 4 | private (scorer-only) |\n\nExample split loading:\n\n from get_fold_data import get_default_split\n\n split = get_default_split()\n # split['train_subjects']=[0..13], split['val_subjects']=[14,15]\n # split['test_subjects']=[16..19] # scorer-only mirror; not staged in your workspace\n\nThe manifest also exposes 20-fold LOSO (`get_fold_data(fold_id)`); it is not used by the primary metric and its supplementary recordings are opt-in (`BPB_SLEEP_EDF_INCLUDE_LOSO=1` on setup). Do not use subjects 16-19 for training or validation under the default split — they are the held-out benchmark test set.\n\n## Frozen preprocessing\n\nThe scorer uses the expert preprocessing configuration below. **Design the model only; do not change these parameters, and do not redo any preprocessing inside the model code:**\n\n def get_optimal_sleep_preprocessing():\n return {\n \"eeg_channels\": [\"EEG Fpz-Cz\"],\n \"l_freq\": 0.3,\n \"h_freq\": 35.0,\n \"resample_sfreq\": 100,\n \"window_size_s\": 30,\n \"crop_wake_mins\": 30,\n \"n_classes\": 5,\n \"mapping\": {\n \"Sleep stage W\": 0,\n \"Sleep stage 1\": 1,\n \"Sleep stage 2\": 2,\n \"Sleep stage 3\": 3,\n \"Sleep stage 4\": 3,\n \"Sleep stage R\": 4,\n },\n }\n\nLabel mapping notes:\n\n- Sleep stage 3 and Sleep stage 4 are merged into N3 (label 3), matching the AASM convention.\n- `Movement time` and `Sleep stage ?` epochs are discarded.\n- `crop_wake_mins=30` trims Wake epochs at head and tail to at most 30 minutes each, so the training distribution is not dominated by lights-off/lights-on wake.\n\nDo not redefine the label mapping inside the model. The five output logits correspond directly to labels 0..4.\n\n## Deliverable\n\nWrite a PyTorch `nn.Module` named `SleepAgentModel` for 5-class sleep staging. Save it in your workspace at:\n\n EEG_sleep/sleep_agent_model.py\n\n(The BPB workspace is your current cwd; it is not `/workspace/`.)\n\nHard requirements:\n\n1. The file path must be exactly `EEG_sleep/sleep_agent_model.py`.\n2. The class name must be exactly `SleepAgentModel`.\n3. `forward(x)` must accept `x = torch.randn(2, 1, 3000)`.\n4. `forward` output shape must be exactly `torch.Size([2, 5])`.\n5. **The model file must not perform training, evaluation, data I/O, or file writes.** Top-level code must be pure declaration; the scorer will `import` the module and call `SleepAgentModel()` itself. Do not trigger training inside `if __name__ == '__main__'` either.\n6. No private dependencies. Beyond `torch` and `numpy`, do not import any third-party package.\n7. **Do not directly reuse braindecode's prebuilt sleep-staging models** (DeepSleepNet, TinySleepNet, SleepStagerChambon2018, U-Time, U-Sleep, etc.). You may borrow ideas or reimplement them, but not `from braindecode.models import SleepStagerChambon2018` and wrap it.\n8. Do not read subject 16-19 data and do not include those subjects in training or validation. The scorer holds them separately.\n9. Emit Python code only. Do not include Markdown, explanatory prose, or prompt paraphrases in `sleep_agent_model.py`; `importlib` will fail.\n\n## Model design hints\n\n1. **Temporal modeling**: use temporal convolutions that cover the delta / theta / alpha / sigma / low-beta timescales relevant to sleep-stage discrimination.\n2. **Multi-scale features**: multiple parallel kernel widths (e.g. small + large receptive fields) often help.\n3. **Stage-transition prior**: even though `forward` sees only a single 30 s epoch, you can encode transition regularity via dilated convolutions / TCN / lightweight temporal modules.\n4. **Regularization**: use BatchNorm / Dropout / Pooling sensibly; keep the parameter count modest.\n5. **Class imbalance**: N1 is by far the rarest stage and typically the hardest. The scorer applies class-weighted cross-entropy during training, but architectural choices (e.g. attention on rare-stage-friendly features) can still help.\n\n## Report requirements\n\nIn addition to `EEG_sleep/sleep_agent_model.py`, produce at the workspace root:\n\n report.md\n\nCovering at least:\n\n- A summary of the model architecture (layers, parameter-count estimate, design trade-offs).\n- Why you chose this architecture (which of the design hints above it addresses).\n- Known limitations and problems you could not resolve.\n\nThe scorer will train a single `SleepAgentModel` instance on subjects 0-13, use subjects 14-15 for validation-based early stopping (Adam lr=1e-3, batch=64, up to 20 epochs, class-weighted cross-entropy, patience=5 on val loss), then evaluate on subjects 16-19 and report:\n\n- test_kappa (**primary metric**, Cohen's kappa — standard for sleep-staging with heavy class imbalance)\n- test_accuracy / test_balanced_accuracy / test_macro_f1 (auxiliary aggregates)\n- wake_recall / n1_recall / n2_recall / n3_recall / rem_recall (per-class recall on subjects 16-19)\n\nThe scorer uses the private subject-16-19 Hypnogram files for all metrics; the agent never sees them or the final score.", + "send": "You are a deep-learning architect for sleep-staging on EEG. Design a PyTorch model that classifies 30-second single-channel Fpz-Cz sleep epochs into 5 sleep stages on the Sleep-EDF Cassette benchmark.\n\n## Input / output shape contract\n\nThe model's `forward` input tensor shape is fixed to:\n\n (Batch, 1, 3000)\n\nwhere:\n- 1 is the single Fpz-Cz EEG channel\n- 3000 is the number of time points in a 30 s sleep epoch at 100 Hz\n- input amplitudes are expressed in microvolts (µV)\n\nThe `forward` output tensor shape must be exactly:\n\n (Batch, 5)\n\ncorresponding to the five stages, in this channel order:\n\n Wake, N1, N2, N3, REM\n\n## Dataset and split\n\n- Dataset: Sleep-EDF Expanded — Sleep Cassette\n- Subjects: 20 (subject 0–19; two PSG + Hypnogram recordings per subject, except subject 13 which has one)\n- **Agent-visible data directory: `./public_data/`** (relative to your workspace; `env/setup-agent.sh` unpacks it from the pinned HF release)\n - Recordings: `public_data/sleep-cassette/SC4nn[0-9]E0-PSG.edf` and matching `-Hypnogram.edf` for subjects 0-15 (62 files total)\n - Split manifests: `public_data/manifests/{subjects.json,default_split.json,loso_folds.json}`\n - Loader: `public_data/get_fold_data.py` (contains zero-leakage asserts and both split loaders)\n - Label mapping: `public_data/label_mapping.json`\n- **Subject 16-19 data does not appear in your workspace.** The scorer reads the four private test subjects (PSG + Hypnogram) from an isolated evaluator-only directory.\n\nThe fixed benchmark split is subject-boundary strict (zero leakage):\n\n| Split | Subjects | Count | Visibility |\n|-------|----------|-------|------------|\n| Train | 0–13 | 14 | public |\n| Val | 14–15 | 2 | public |\n| Test | 16–19 | 4 | private (scorer-only) |\n\nExample split loading:\n\n from get_fold_data import get_default_split\n\n split = get_default_split()\n # split['train_subjects']=[0..13], split['val_subjects']=[14,15]\n # split['test_subjects']=[16..19] # scorer-only mirror; not staged in your workspace\n\nThe manifest also exposes 20-fold LOSO (`get_fold_data(fold_id)`); it is not used by the primary metric and its supplementary recordings are opt-in (`BPB_SLEEP_EDF_INCLUDE_LOSO=1` on setup). Do not use subjects 16-19 for training or validation under the default split — they are the held-out benchmark test set.\n\n## Frozen preprocessing\n\nThe scorer uses the expert preprocessing configuration below. **Design the model only; do not change these parameters, and do not redo any preprocessing inside the model code:**\n\n def get_optimal_sleep_preprocessing():\n return {\n \"eeg_channels\": [\"EEG Fpz-Cz\"],\n \"l_freq\": 0.3,\n \"h_freq\": 35.0,\n \"resample_sfreq\": 100,\n \"input_unit\": \"microvolts\",\n \"window_size_s\": 30,\n \"crop_wake_mins\": 30,\n \"n_classes\": 5,\n \"mapping\": {\n \"Sleep stage W\": 0,\n \"Sleep stage 1\": 1,\n \"Sleep stage 2\": 2,\n \"Sleep stage 3\": 3,\n \"Sleep stage 4\": 3,\n \"Sleep stage R\": 4,\n },\n }\n\nLabel mapping notes:\n\n- Sleep stage 3 and Sleep stage 4 are merged into N3 (label 3), matching the AASM convention.\n- `Movement time` and `Sleep stage ?` epochs are discarded.\n- `crop_wake_mins=30` trims Wake epochs at head and tail to at most 30 minutes each, so the training distribution is not dominated by lights-off/lights-on wake.\n\nDo not redefine the label mapping inside the model. The five output logits correspond directly to labels 0..4.\n\n## Deliverable\n\nWrite a PyTorch `nn.Module` named `SleepAgentModel` for 5-class sleep staging. Save it in your workspace at:\n\n EEG_sleep/sleep_agent_model.py\n\n(The BPB workspace is your current cwd; it is not `/workspace/`.)\n\nHard requirements:\n\n1. The file path must be exactly `EEG_sleep/sleep_agent_model.py`.\n2. The class name must be exactly `SleepAgentModel`.\n3. `forward(x)` must accept `x = torch.randn(2, 1, 3000)`.\n4. `forward` output shape must be exactly `torch.Size([2, 5])`.\n5. **The model file must not perform training, evaluation, data I/O, or file writes.** Top-level code must be pure declaration; the scorer will `import` the module and call `SleepAgentModel()` itself. Do not trigger training inside `if __name__ == '__main__'` either.\n6. No private dependencies. Beyond `torch` and `numpy`, do not import any third-party package.\n7. **Do not directly reuse braindecode's prebuilt sleep-staging models** (DeepSleepNet, TinySleepNet, SleepStagerChambon2018, U-Time, U-Sleep, etc.). You may borrow ideas or reimplement them, but not `from braindecode.models import SleepStagerChambon2018` and wrap it.\n8. Do not read subject 16-19 data and do not include those subjects in training or validation. The scorer holds them separately.\n9. Emit Python code only. Do not include Markdown, explanatory prose, or prompt paraphrases in `sleep_agent_model.py`; `importlib` will fail.\n\n## Model design hints\n\n1. **Temporal modeling**: use temporal convolutions that cover the delta / theta / alpha / sigma / low-beta timescales relevant to sleep-stage discrimination.\n2. **Multi-scale features**: multiple parallel kernel widths (e.g. small + large receptive fields) often help.\n3. **Stage-transition prior**: even though `forward` sees only a single 30 s epoch, you can encode transition regularity via dilated convolutions / TCN / lightweight temporal modules.\n4. **Regularization**: use BatchNorm / Dropout / Pooling sensibly; keep the parameter count modest.\n5. **Class imbalance**: N1 is by far the rarest stage and typically the hardest. The scorer applies class-weighted cross-entropy during training, but architectural choices (e.g. attention on rare-stage-friendly features) can still help.\n\n## Report requirements\n\nIn addition to `EEG_sleep/sleep_agent_model.py`, produce at the workspace root:\n\n report.md\n\nCovering at least:\n\n- A summary of the model architecture (layers, parameter-count estimate, design trade-offs).\n- Why you chose this architecture (which of the design hints above it addresses).\n- Known limitations and problems you could not resolve.\n\nThe scorer will train a single `SleepAgentModel` instance on subjects 0-13, use subjects 14-15 for validation-based early stopping (Adam lr=1e-3, batch=64, up to 20 epochs, class-weighted cross-entropy, patience=5 on val loss), then evaluate on subjects 16-19 and report:\n\n- test_kappa (**primary metric**, Cohen's kappa — standard for sleep-staging with heavy class imbalance)\n- test_accuracy / test_balanced_accuracy / test_macro_f1 (auxiliary aggregates)\n- wake_recall / n1_recall / n2_recall / n3_recall / rem_recall (per-class recall on subjects 16-19)\n\nThe scorer uses the private subject-16-19 Hypnogram files for all metrics; the agent never sees them or the final score.", "then": "wait_idle" } ] diff --git a/tasks/sleep-edf/task.yaml b/tasks/sleep-edf/task.yaml index 71629e3..8a62b98 100644 --- a/tasks/sleep-edf/task.yaml +++ b/tasks/sleep-edf/task.yaml @@ -3,7 +3,7 @@ id: sleep-edf created_at: 2026-07-15 domain: eeg-sleep-staging category: eeg-sleep-staging -version: "0.1" +version: "0.2" summary: >- 5-class sleep-staging on Sleep-EDF Cassette (20 subjects, single-channel Fpz-Cz EEG, 30 s epochs → Wake / N1 / N2 / N3 / REM). Subjects 0-13 are