aryakoureshi commited on
Commit
dc9d5f6
·
verified ·
1 Parent(s): 1d59951

Release SenuaLab patient-independent EEG IED models v1.0.0

Browse files

Five reviewed checkpoints, safetensors, aggregate vEpiSet results, provenance, hashes, and EEGAnnotationTool model pack. No private hospital data.

Files changed (49) hide show
  1. MANIFEST.json +247 -0
  2. MODEL_LICENSE.md +37 -0
  3. README.md +164 -0
  4. eegannotationtool/senua_eegdino_medium/manifest.json +48 -0
  5. eegannotationtool/senua_eegdino_medium/model.pth +3 -0
  6. eegannotationtool/senua_eegdino_medium/model.py +73 -0
  7. eegannotationtool/senua_eegdino_medium/preprocessing.py +58 -0
  8. eegannotationtool/senua_eegpt/manifest.json +48 -0
  9. eegannotationtool/senua_eegpt/model.pth +3 -0
  10. eegannotationtool/senua_eegpt/model.py +116 -0
  11. eegannotationtool/senua_eegpt/preprocessing.py +58 -0
  12. eegannotationtool/senua_eegpt_linear_probe/manifest.json +48 -0
  13. eegannotationtool/senua_eegpt_linear_probe/model.pth +3 -0
  14. eegannotationtool/senua_eegpt_linear_probe/model.py +77 -0
  15. eegannotationtool/senua_eegpt_linear_probe/preprocessing.py +58 -0
  16. eegannotationtool/senua_iednet_lite/manifest.json +48 -0
  17. eegannotationtool/senua_iednet_lite/model.pth +3 -0
  18. eegannotationtool/senua_iednet_lite/model.py +119 -0
  19. eegannotationtool/senua_iednet_lite/preprocessing.py +58 -0
  20. eegannotationtool/senua_resnet_attention/manifest.json +48 -0
  21. eegannotationtool/senua_resnet_attention/model.pth +3 -0
  22. eegannotationtool/senua_resnet_attention/model.py +677 -0
  23. eegannotationtool/senua_resnet_attention/preprocessing.py +58 -0
  24. ensembles/ensemble.json +25 -0
  25. ensembles/ensemble_4model.json +53 -0
  26. ensembles/ensemble_eegdino.json +51 -0
  27. ensembles/ensemble_eegpt.json +50 -0
  28. models/eegdino_medium/config.json +130 -0
  29. models/eegdino_medium/model.safetensors +3 -0
  30. models/eegpt/config.json +122 -0
  31. models/eegpt/model.safetensors +3 -0
  32. models/eegpt_linear_probe/config.json +122 -0
  33. models/eegpt_linear_probe/model.safetensors +3 -0
  34. models/iednet_lite/config.json +89 -0
  35. models/iednet_lite/model.safetensors +3 -0
  36. models/resnet_attention/config.json +89 -0
  37. models/resnet_attention/model.safetensors +3 -0
  38. pytorch/eegdino_medium/config.json +130 -0
  39. pytorch/eegdino_medium/model.pth +3 -0
  40. pytorch/eegpt/config.json +122 -0
  41. pytorch/eegpt/model.pth +3 -0
  42. pytorch/eegpt_linear_probe/config.json +122 -0
  43. pytorch/eegpt_linear_probe/model.pth +3 -0
  44. pytorch/iednet_lite/config.json +89 -0
  45. pytorch/iednet_lite/model.pth +3 -0
  46. pytorch/resnet_attention/config.json +89 -0
  47. pytorch/resnet_attention/model.pth +3 -0
  48. results/benchmark_results.csv +8 -0
  49. results/model_comparison.png +0 -0
MANIFEST.json ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "files": [
3
+ {
4
+ "bytes": 1069,
5
+ "path": "eegannotationtool/senua_eegdino_medium/manifest.json",
6
+ "sha256": "c73c5997c96a0f7b46ad885348f443b82a8a078b59a6731d8664750563660d47"
7
+ },
8
+ {
9
+ "bytes": 137840831,
10
+ "path": "eegannotationtool/senua_eegdino_medium/model.pth",
11
+ "sha256": "e80ee1f66edcac1059b6e68c7458d8aa171167fbc9d50495daaafdc20e358491"
12
+ },
13
+ {
14
+ "bytes": 2712,
15
+ "path": "eegannotationtool/senua_eegdino_medium/model.py",
16
+ "sha256": "1bda5a0ee0fc8e58835eec379294659a02f804cf37a3227d056807553f3a497a"
17
+ },
18
+ {
19
+ "bytes": 2040,
20
+ "path": "eegannotationtool/senua_eegdino_medium/preprocessing.py",
21
+ "sha256": "1b39acec0d87439763009017a60614dda25a0605241c2563c8d1cf2c0c45ca44"
22
+ },
23
+ {
24
+ "bytes": 1062,
25
+ "path": "eegannotationtool/senua_eegpt/manifest.json",
26
+ "sha256": "aaa71a18f34e62ab9a0fc7dac718b23b0508d730bc93ce61921a7b1d68cc90a0"
27
+ },
28
+ {
29
+ "bytes": 102394167,
30
+ "path": "eegannotationtool/senua_eegpt/model.pth",
31
+ "sha256": "9a0a55ed656049b4109d0e923ee0d461151a8a793995a24438717efecd2f24d5"
32
+ },
33
+ {
34
+ "bytes": 4422,
35
+ "path": "eegannotationtool/senua_eegpt/model.py",
36
+ "sha256": "ed238c01a7c275f4ccc5315af11a2fc1694b497843f120bfa5a40d9f560c7a7f"
37
+ },
38
+ {
39
+ "bytes": 2040,
40
+ "path": "eegannotationtool/senua_eegpt/preprocessing.py",
41
+ "sha256": "1b39acec0d87439763009017a60614dda25a0605241c2563c8d1cf2c0c45ca44"
42
+ },
43
+ {
44
+ "bytes": 1073,
45
+ "path": "eegannotationtool/senua_eegpt_linear_probe/manifest.json",
46
+ "sha256": "8c7474f782ef8089fe03eb307b61d9c324c79e4ec979245bd1fab898a6617dff"
47
+ },
48
+ {
49
+ "bytes": 101312967,
50
+ "path": "eegannotationtool/senua_eegpt_linear_probe/model.pth",
51
+ "sha256": "a51fd32505d51d80677ed6504b94d0b8a6718cb1de21a07cd3253ed1e1a3405a"
52
+ },
53
+ {
54
+ "bytes": 2681,
55
+ "path": "eegannotationtool/senua_eegpt_linear_probe/model.py",
56
+ "sha256": "e788f854165481d594b5727a3c080e76a52286113cdcab0057a305ff451dc5f9"
57
+ },
58
+ {
59
+ "bytes": 2040,
60
+ "path": "eegannotationtool/senua_eegpt_linear_probe/preprocessing.py",
61
+ "sha256": "1b39acec0d87439763009017a60614dda25a0605241c2563c8d1cf2c0c45ca44"
62
+ },
63
+ {
64
+ "bytes": 1074,
65
+ "path": "eegannotationtool/senua_iednet_lite/manifest.json",
66
+ "sha256": "9dabb0feb431ff763b804c0435363ad443351177e58355c4399e7572f10b7cfe"
67
+ },
68
+ {
69
+ "bytes": 1041083,
70
+ "path": "eegannotationtool/senua_iednet_lite/model.pth",
71
+ "sha256": "383fe1de4aa8cf01b5f1c0a6734d92214ec0e7f96a6cd18a50fa52c52ccb210c"
72
+ },
73
+ {
74
+ "bytes": 4388,
75
+ "path": "eegannotationtool/senua_iednet_lite/model.py",
76
+ "sha256": "1a221ee13a67fb51cf26915e049a2eccfc4250d1df1adb150e312d6c677feab0"
77
+ },
78
+ {
79
+ "bytes": 2040,
80
+ "path": "eegannotationtool/senua_iednet_lite/preprocessing.py",
81
+ "sha256": "1b39acec0d87439763009017a60614dda25a0605241c2563c8d1cf2c0c45ca44"
82
+ },
83
+ {
84
+ "bytes": 1079,
85
+ "path": "eegannotationtool/senua_resnet_attention/manifest.json",
86
+ "sha256": "a1ef046db3b2edcd752d1b93e0a540508b620f1d1a4b6f76cb20d2a64cac1784"
87
+ },
88
+ {
89
+ "bytes": 20037039,
90
+ "path": "eegannotationtool/senua_resnet_attention/model.pth",
91
+ "sha256": "df088c4fd087b453ef3aca76f54a975a69f015324f8d862d2d4ba5316cb91e1a"
92
+ },
93
+ {
94
+ "bytes": 24358,
95
+ "path": "eegannotationtool/senua_resnet_attention/model.py",
96
+ "sha256": "f3be755ac6c0e3b95e1f48db9fe7af0df88aeda5f36d7d7d793fdf51c6355290"
97
+ },
98
+ {
99
+ "bytes": 2040,
100
+ "path": "eegannotationtool/senua_resnet_attention/preprocessing.py",
101
+ "sha256": "1b39acec0d87439763009017a60614dda25a0605241c2563c8d1cf2c0c45ca44"
102
+ },
103
+ {
104
+ "bytes": 627,
105
+ "path": "ensembles/ensemble.json",
106
+ "sha256": "793881c7515f44ae70e77a1ba599f606c15b0fbd551318fbf6e4ea5b18a7ebf2"
107
+ },
108
+ {
109
+ "bytes": 1390,
110
+ "path": "ensembles/ensemble_4model.json",
111
+ "sha256": "e44924fde4bc43d8cd9305f2a8a0e1727c0164a31d8e1ca494c5af55b90eba74"
112
+ },
113
+ {
114
+ "bytes": 1425,
115
+ "path": "ensembles/ensemble_eegdino.json",
116
+ "sha256": "0d2f9c2d9ac605423d47a9739ea9466cfd19544553ab233db34d0db09cc57172"
117
+ },
118
+ {
119
+ "bytes": 1326,
120
+ "path": "ensembles/ensemble_eegpt.json",
121
+ "sha256": "813ac3a4bcfa50c6c4ae81a79ac8faec9ae2e8dc5bfdab454a98572fd5698dd9"
122
+ },
123
+ {
124
+ "bytes": 1646,
125
+ "path": "MODEL_LICENSE.md",
126
+ "sha256": "747884c797383451a74c7444148d0fd1182245960aca48582df35f758a8c3831"
127
+ },
128
+ {
129
+ "bytes": 3165,
130
+ "path": "models/eegdino_medium/config.json",
131
+ "sha256": "e53ddd10b026440bed401b4b1c3468aaf9b43e4e5f9074655cf56389848a66f2"
132
+ },
133
+ {
134
+ "bytes": 137804848,
135
+ "path": "models/eegdino_medium/model.safetensors",
136
+ "sha256": "72320fedeed09b66508afaa79ce0ca114273673f6ce36adccedce87c8ce0ea1d"
137
+ },
138
+ {
139
+ "bytes": 2811,
140
+ "path": "models/eegpt/config.json",
141
+ "sha256": "fd8db52c30918c858c1924edfeb513388685e00cc662d920b027d8900f04e1d7"
142
+ },
143
+ {
144
+ "bytes": 102373540,
145
+ "path": "models/eegpt/model.safetensors",
146
+ "sha256": "e91757b80c98dd7325ea1068bff9dfe72bac577195f5b25b2570ce4d9f357226"
147
+ },
148
+ {
149
+ "bytes": 2824,
150
+ "path": "models/eegpt_linear_probe/config.json",
151
+ "sha256": "267732d264b787020c3e2b4285e56798bf644e8a26876645e31cff0ad273b2e7"
152
+ },
153
+ {
154
+ "bytes": 101295352,
155
+ "path": "models/eegpt_linear_probe/model.safetensors",
156
+ "sha256": "24ec011a90388079df98322244b96e076eeebf6b0671fe987a7d0d5cf66f9579"
157
+ },
158
+ {
159
+ "bytes": 2019,
160
+ "path": "models/iednet_lite/config.json",
161
+ "sha256": "9fc65e9684a39fe22ea8660bd56526319965704d5968c66309be350d6b743b38"
162
+ },
163
+ {
164
+ "bytes": 1014056,
165
+ "path": "models/iednet_lite/model.safetensors",
166
+ "sha256": "7c5dc3c8e40050b69a07a7cf6a1ce7bbd2b12347f1b50d7feaf1fdfdd108b082"
167
+ },
168
+ {
169
+ "bytes": 2030,
170
+ "path": "models/resnet_attention/config.json",
171
+ "sha256": "d85aa13650584b0c0489dc2aaec89b4007d2aa61cf25ff55ad73cca621fac651"
172
+ },
173
+ {
174
+ "bytes": 20011088,
175
+ "path": "models/resnet_attention/model.safetensors",
176
+ "sha256": "971fa0d2b38f0cc76ca479931872ae957123aeb51263de59b11098be1531b1e1"
177
+ },
178
+ {
179
+ "bytes": 3165,
180
+ "path": "pytorch/eegdino_medium/config.json",
181
+ "sha256": "e53ddd10b026440bed401b4b1c3468aaf9b43e4e5f9074655cf56389848a66f2"
182
+ },
183
+ {
184
+ "bytes": 137840831,
185
+ "path": "pytorch/eegdino_medium/model.pth",
186
+ "sha256": "e80ee1f66edcac1059b6e68c7458d8aa171167fbc9d50495daaafdc20e358491"
187
+ },
188
+ {
189
+ "bytes": 2811,
190
+ "path": "pytorch/eegpt/config.json",
191
+ "sha256": "fd8db52c30918c858c1924edfeb513388685e00cc662d920b027d8900f04e1d7"
192
+ },
193
+ {
194
+ "bytes": 102394167,
195
+ "path": "pytorch/eegpt/model.pth",
196
+ "sha256": "9a0a55ed656049b4109d0e923ee0d461151a8a793995a24438717efecd2f24d5"
197
+ },
198
+ {
199
+ "bytes": 2824,
200
+ "path": "pytorch/eegpt_linear_probe/config.json",
201
+ "sha256": "267732d264b787020c3e2b4285e56798bf644e8a26876645e31cff0ad273b2e7"
202
+ },
203
+ {
204
+ "bytes": 101312967,
205
+ "path": "pytorch/eegpt_linear_probe/model.pth",
206
+ "sha256": "a51fd32505d51d80677ed6504b94d0b8a6718cb1de21a07cd3253ed1e1a3405a"
207
+ },
208
+ {
209
+ "bytes": 2019,
210
+ "path": "pytorch/iednet_lite/config.json",
211
+ "sha256": "9fc65e9684a39fe22ea8660bd56526319965704d5968c66309be350d6b743b38"
212
+ },
213
+ {
214
+ "bytes": 1041083,
215
+ "path": "pytorch/iednet_lite/model.pth",
216
+ "sha256": "383fe1de4aa8cf01b5f1c0a6734d92214ec0e7f96a6cd18a50fa52c52ccb210c"
217
+ },
218
+ {
219
+ "bytes": 2030,
220
+ "path": "pytorch/resnet_attention/config.json",
221
+ "sha256": "d85aa13650584b0c0489dc2aaec89b4007d2aa61cf25ff55ad73cca621fac651"
222
+ },
223
+ {
224
+ "bytes": 20037039,
225
+ "path": "pytorch/resnet_attention/model.pth",
226
+ "sha256": "df088c4fd087b453ef3aca76f54a975a69f015324f8d862d2d4ba5316cb91e1a"
227
+ },
228
+ {
229
+ "bytes": 5952,
230
+ "path": "README.md",
231
+ "sha256": "f4b252478ceba9980edc00f5edcd7bb8550bb0e758a9a9f7274a4b2445b5077b"
232
+ },
233
+ {
234
+ "bytes": 1839,
235
+ "path": "results/benchmark_results.csv",
236
+ "sha256": "4c09b55325f8571ce374aa274a6ed395a4fc69346e333ae1a884247cb5fa2f08"
237
+ },
238
+ {
239
+ "bytes": 76548,
240
+ "path": "results/model_comparison.png",
241
+ "sha256": "8af73e7c56b761565ec67badab0f456ed69ba0c9c88724b2e5c9d65c7d07e0d3"
242
+ }
243
+ ],
244
+ "privacy_review": "public vEpiSet metadata and aggregate results only; no hospital data",
245
+ "repository": "SenuaLab/EEG-IED-Detection",
246
+ "schema_version": 1
247
+ }
MODEL_LICENSE.md ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Model artifact terms and provenance
2
+
3
+ There is no single license covering every artifact in this multi-model
4
+ repository. The SenuaLab source repository is MIT licensed. vEpiSet is CC BY
5
+ 4.0. Upstream foundation-model code and checkpoints retain their respective
6
+ terms.
7
+
8
+ ## From-scratch checkpoints
9
+
10
+ `iednet_lite` and `resnet_attention` are SenuaLab architectures/checkpoints
11
+ trained on vEpiSet. SenuaLab makes its contribution available under CC BY 4.0,
12
+ with required attribution to SenuaLab and the vEpiSet publication.
13
+
14
+ ## EEGPT checkpoints
15
+
16
+ `eegpt` and `eegpt_linear_probe` contain encoder parameters derived from
17
+ `braindecode/eegpt-pretrained`. The original EEGPT code is Apache-2.0 and the
18
+ curated Braindecode model card identifies BSD-3-Clause. SenuaLab's downstream
19
+ head/configuration is made available under CC BY 4.0. Upstream notices and
20
+ attribution must be retained.
21
+
22
+ ## EEG-DINO Medium checkpoint
23
+
24
+ `eegdino_medium` contains encoder parameters derived from
25
+ `braindecode/eegdino-medium-pretrained`. The curated model code is identified as
26
+ BSD-3-Clause. SenuaLab's downstream task head and fine-tuning contribution are
27
+ made available under CC BY 4.0. The upstream model card warns that pretraining
28
+ corpus terms may add restrictions; users must independently verify eligibility,
29
+ especially for commercial deployment.
30
+
31
+ ## No clinical warranty
32
+
33
+ Artifacts are provided as-is for research and engineering evaluation. They are
34
+ not medical devices, have no warranty of clinical fitness, and require expert
35
+ review. These terms do not override applicable upstream licenses, dataset
36
+ terms, law, institutional policy, or regulatory requirements.
37
+
README.md ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: other
3
+ library_name: pytorch
4
+ pipeline_tag: audio-classification
5
+ tags:
6
+ - eeg
7
+ - epilepsy
8
+ - interictal-epileptiform-discharge
9
+ - neuroscience
10
+ - eegpt
11
+ - eeg-dino
12
+ - braindecode
13
+ datasets:
14
+ - vEpiSet
15
+ base_model:
16
+ - braindecode/eegpt-pretrained
17
+ - braindecode/eegdino-medium-pretrained
18
+ ---
19
+
20
+ # SenuaLab EEG IED Detection
21
+
22
+ Five patient-independent binary IED checkpoints, aggregate benchmark artifacts,
23
+ and an app-ready model pack for
24
+ [SenuaLab EEG Annotation Tool](https://github.com/SenuaLab/EEGAnnotationTool).
25
+ Training and evaluation use only the public
26
+ [vEpiSet](https://doi.org/10.6084/m9.figshare.28069568) dataset.
27
+
28
+ > **Research use only.** These models are not medical devices and must not be
29
+ > used as the sole basis for diagnosis or treatment. Every candidate requires
30
+ > review by a qualified EEG professional against the original recording.
31
+
32
+ > **Privacy.** No private hospital recording, patient identifier, physician
33
+ > annotation, local path, or hospital-derived patient result is included. The
34
+ > model parameters, public configuration metadata, and aggregate vEpiSet
35
+ > results were reviewed before upload.
36
+
37
+ Source, methodology, and reproducibility documentation:
38
+ [github.com/SenuaLab/EEG-IED-Detection](https://github.com/SenuaLab/EEG-IED-Detection)
39
+
40
+ ## Models
41
+
42
+ | Folder | Model | Params | Rate | Window | Test AUROC | Test AUPRC | Test F1 |
43
+ |---|---|---:|---:|---:|---:|---:|---:|
44
+ | `iednet_lite` | IEDNet-Lite | 0.25M | 250 Hz | 4 s | 0.8793 | 0.6848 | 0.6372 |
45
+ | `resnet_attention` | ResNet-Attention | 4.99M | 250 Hz | 4 s | 0.8998 | 0.7018 | 0.6667 |
46
+ | `eegpt` | EEGPT + temporal head | 25.59M | 250 Hz | 4 s | 0.8641 | 0.6532 | 0.6139 |
47
+ | `eegpt_linear_probe` | EEGPT linear probe | 25.32M | 250 Hz | 4 s | 0.7519 | 0.3722 | 0.3227 |
48
+ | `eegdino_medium` | EEG-DINO Medium | 34.45M | 200 Hz | 4 s | **0.9363** | **0.8020** | **0.7415** |
49
+
50
+ All test thresholds were selected on validation subjects and then frozen. The
51
+ test split contains 3,908 windows from 13 held-out vEpiSet subjects. Results are
52
+ single-dataset, four-second window metrics, not prospective clinical claims.
53
+
54
+ ## Files
55
+
56
+ ```text
57
+ models/<name>/
58
+ ├── model.safetensors # tensor-only weights (preferred)
59
+ └── config.json # reviewed input/provenance/metric contract
60
+
61
+ pytorch/<name>/
62
+ ├── model.pth # sanitized checkpoint for the Python runner
63
+ └── config.json
64
+
65
+ eegannotationtool/senua_<name>/
66
+ ├── model.py # executable architecture adapter
67
+ ├── preprocessing.py # released preprocessing contract
68
+ ├── model.pth # app-compatible sanitized checkpoint
69
+ └── manifest.json
70
+
71
+ ensembles/ # validation-selected ensemble definitions
72
+ results/ # aggregate benchmark table and figure
73
+ MANIFEST.json # SHA-256 and byte size for every release file
74
+ ```
75
+
76
+ PyTorch `.pth` is included because the current EEG Annotation Tool model
77
+ discovery contract supports `.pth/.pt/.ckpt`. Prefer `safetensors` elsewhere and
78
+ load executable/model files only from this official repository.
79
+
80
+ ## Input contract
81
+
82
+ - 19 standard 10-20 channels in this order:
83
+ `Fp1, Fp2, F3, F4, C3, C4, P3, P4, O1, O2, F7, F8, T3, T4, T5, T6, Fz, Cz, Pz`.
84
+ - Four-second full windows.
85
+ - Fourth-order zero-phase 1-45 Hz Butterworth band-pass.
86
+ - Common-average reference across the 19 channels.
87
+ - Resampling to the rate listed in the table.
88
+ - One global z-score over the complete channel-time window, clipped to `[-8,8]`.
89
+ - Binary output order: `[Non-IED, IED]`.
90
+
91
+ Legacy temporal aliases T7/T8/P7/P8 map to T3/T4/T5/T6.
92
+
93
+ ## Download
94
+
95
+ ```bash
96
+ hf download SenuaLab/EEG-IED-Detection \
97
+ pytorch/iednet_lite/model.pth \
98
+ --local-dir ./weights
99
+ ```
100
+
101
+ Or from Python:
102
+
103
+ ```python
104
+ from huggingface_hub import hf_hub_download
105
+
106
+ path = hf_hub_download(
107
+ repo_id="SenuaLab/EEG-IED-Detection",
108
+ filename="pytorch/iednet_lite/model.pth",
109
+ )
110
+ ```
111
+
112
+ The source repository runner accepts model short names and handles the download:
113
+
114
+ ```bash
115
+ python -m eeg_ied_detector.robust_predict \
116
+ --data-dir /path/to/brainvision-recordings \
117
+ --model-paths iednet_lite resnet_attention \
118
+ --output-dir ./predictions \
119
+ --no-label-comparison
120
+ ```
121
+
122
+ EEGPT and EEG-DINO require `braindecode[hub]==1.6.1`.
123
+
124
+ ## EEG Annotation Tool
125
+
126
+ Use the installer from the source repository:
127
+
128
+ ```bash
129
+ python scripts/install_eeg_annotation_models.py \
130
+ --models iednet_lite resnet_attention
131
+ ```
132
+
133
+ IED Finder discovers the installed folders, validates channels and the declared
134
+ input contract, and uses the validation-selected threshold as its starting
135
+ point. Foundation-model adapters are included but need Braindecode in the
136
+ application's Python environment.
137
+
138
+ ## Evaluation limitations
139
+
140
+ - One public dataset and one fixed internal held-out split.
141
+ - No prospective, external multi-center, or medical-device validation.
142
+ - Four-second window metrics do not equal continuous event-level sensitivity.
143
+ - Performance can shift with montage, hardware, preprocessing, age, disease
144
+ mix, artifacts, prevalence, and annotation policy.
145
+ - Threshold changes require a prospectively defined validation protocol and
146
+ must not use the final evaluation cohort.
147
+ - Automated output is a review candidate, never a diagnosis.
148
+
149
+ ## Model and data provenance
150
+
151
+ - vEpiSet: Lin et al., Scientific Data 12, 229 (2025),
152
+ https://doi.org/10.1038/s41597-025-04523-8, CC BY 4.0.
153
+ - EEGPT: NeurIPS 2024, upstream code
154
+ [BINE022/EEGPT](https://github.com/BINE022/EEGPT), encoder checkpoint
155
+ [braindecode/eegpt-pretrained](https://huggingface.co/braindecode/eegpt-pretrained).
156
+ - EEG-DINO: MICCAI 2025,
157
+ [paper](https://papers.miccai.org/miccai-2025/paper/3347_paper.pdf), encoder
158
+ checkpoint
159
+ [braindecode/eegdino-medium-pretrained](https://huggingface.co/braindecode/eegdino-medium-pretrained)
160
+ at revision `191f73eb50d68a184b4cabb623938df914235d3c`.
161
+
162
+ See `MODEL_LICENSE.md` for the per-artifact licensing boundary and upstream
163
+ conditions.
164
+
eegannotationtool/senua_eegdino_medium/manifest.json ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "finder_defaults": {
3
+ "pad_policy": "skip",
4
+ "step_ms": 500.0,
5
+ "threshold": 0.619140625
6
+ },
7
+ "important": "Research screening model; every candidate requires expert review. Threshold selected on public vEpiSet validation subjects.",
8
+ "input": {
9
+ "channel_aliases": {
10
+ "P7": "T5",
11
+ "P8": "T6",
12
+ "T7": "T3",
13
+ "T8": "T4"
14
+ },
15
+ "channel_names": [
16
+ "Fp1",
17
+ "Fp2",
18
+ "F3",
19
+ "F4",
20
+ "C3",
21
+ "C4",
22
+ "P3",
23
+ "P4",
24
+ "O1",
25
+ "O2",
26
+ "F7",
27
+ "F8",
28
+ "T3",
29
+ "T4",
30
+ "T5",
31
+ "T6",
32
+ "Fz",
33
+ "Cz",
34
+ "Pz"
35
+ ],
36
+ "input_samples": 800,
37
+ "normalization": "global four-second window z-score clipped to [-8,8]",
38
+ "reference": "common average",
39
+ "required_filters": [
40
+ "1-45 Hz zero-phase Butterworth"
41
+ ],
42
+ "sampling_rate_hz": 200.0,
43
+ "window_seconds": 4.0
44
+ },
45
+ "name": "SenuaLab eegdino_medium",
46
+ "schema_version": 1,
47
+ "source": "https://huggingface.co/SenuaLab/EEG-IED-Detection"
48
+ }
eegannotationtool/senua_eegdino_medium/model.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e80ee1f66edcac1059b6e68c7458d8aa171167fbc9d50495daaafdc20e358491
3
+ size 137840831
eegannotationtool/senua_eegdino_medium/model.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """EEG Annotation Tool adapter for SenuaLab EEG-DINO Medium."""
2
+
3
+ from __future__ import annotations
4
+
5
+ try:
6
+ from braindecode.models import EEGDINO
7
+ except ImportError as exc:
8
+ raise ImportError("This model requires braindecode[hub]==1.6.1") from exc
9
+
10
+ from .preprocessing import INTERNATIONAL_10_20_CHANNELS, preprocess_batch as _preprocess
11
+
12
+
13
+ MODEL_CHANNELS = INTERNATIONAL_10_20_CHANNELS
14
+ MODEL_INPUT_SAMPLES = 800
15
+ MODEL_SAMPLING_RATE_HZ = 200.0
16
+ MODEL_WINDOW_SECONDS = 4.0
17
+ MODEL_ENTRY_CLASS = "SenuaEEGDINOMedium"
18
+ MODEL_NUM_CLASSES = 2
19
+ MODEL_CLASS_LABELS = ["Non-IED", "IED"]
20
+ MODEL_NON_IED_CLASS_INDEX = 0
21
+ MODEL_IED_CLASS_INDICES = [1]
22
+ MODEL_DESCRIPTION = "SenuaLab partially fine-tuned EEG-DINO Medium binary IED detector"
23
+ MODEL_BATCH_PREPROCESSOR = "preprocess_batch"
24
+ MODEL_REQUIRED_REFERENCE = "common average (applied by model adapter)"
25
+ MODEL_REQUIRED_FILTERS = ["1-45 Hz zero-phase Butterworth (applied by model adapter)"]
26
+ MODEL_REQUIRED_NORMALIZATION = "global four-second window z-score, clipped to [-8,8]"
27
+ MODEL_INPUT_UNIT = "scale-invariant after window z-score"
28
+ MODEL_SOURCE_SIGNAL_POLICY = "raw"
29
+ MODEL_REQUIRES_FULL_WINDOW = True
30
+ MODEL_REQUIRES_ALL_CHANNELS = True
31
+ MODEL_DEFAULT_THRESHOLD = 0.619140625
32
+ MODEL_DEFAULT_STEP_MS = 500.0
33
+ MODEL_DEFAULT_PAD_POLICY = "skip"
34
+ MODEL_DEFAULT_BATCH_SIZE = 8
35
+ MODEL_DEFAULT_BATCH_MEMORY_MB = 384.0
36
+ MODEL_VALIDATION_NOTE = "Threshold selected on vEpiSet validation subjects; requires Braindecode 1.6.1 and is not externally validated."
37
+ MODEL_PREPROCESSING_NOTE = "The adapter applies the released 1-45 Hz, common-average, 200 Hz resampling, and global-window z-score pipeline."
38
+
39
+
40
+ def preprocess_batch(batch, source_sfreq=None, channel_names=None):
41
+ return _preprocess(
42
+ batch,
43
+ source_sfreq=source_sfreq,
44
+ target_sfreq=200,
45
+ target_samples=800,
46
+ channel_names=channel_names,
47
+ )
48
+
49
+
50
+ class SenuaEEGDINOMedium(EEGDINO):
51
+ def __init__(self, num_channels: int = 19, num_classes: int = 2, input_length: int = 800):
52
+ if num_channels != 19 or input_length != 800:
53
+ raise ValueError("SenuaEEGDINOMedium requires 19 channels and 800 samples")
54
+ super().__init__(
55
+ n_outputs=num_classes,
56
+ n_chans=19,
57
+ n_times=800,
58
+ sfreq=200,
59
+ patch_size=200,
60
+ n_layer=16,
61
+ nhead=8,
62
+ dim_feedforward=1024,
63
+ channels_kernel_stride_padding_norm=(
64
+ (64, 49, 25, 24, (8, 64)),
65
+ (128, 3, 1, 1, (8, 128)),
66
+ (64, 3, 1, 1, (8, 64)),
67
+ ),
68
+ n_channel_embeddings=19,
69
+ n_global_tokens=1,
70
+ global_token_layer=1,
71
+ drop_prob=0.1,
72
+ )
73
+
eegannotationtool/senua_eegdino_medium/preprocessing.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Exact four-second preprocessing used by the released SenuaLab models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+
7
+ import numpy as np
8
+ from scipy.signal import butter, resample, resample_poly, sosfiltfilt
9
+
10
+
11
+ NUM_CHANNELS = 19
12
+ INTERNATIONAL_10_20_CHANNELS = [
13
+ "Fp1", "Fp2", "F3", "F4", "C3", "C4", "P3", "P4", "O1", "O2",
14
+ "F7", "F8", "T3", "T4", "T5", "T6", "Fz", "Cz", "Pz",
15
+ ]
16
+ DATASET_29_CHANNEL_ORDER = INTERNATIONAL_10_20_CHANNELS
17
+
18
+
19
+ def normalize_sample(sample):
20
+ x = np.asarray(sample, dtype=np.float32)
21
+ mean = x.mean()
22
+ std = x.std()
23
+ return np.clip((x - mean) / max(float(std), 1e-7), -8.0, 8.0).astype(np.float32)
24
+
25
+
26
+ def preprocess_batch(
27
+ batch,
28
+ *,
29
+ source_sfreq=None,
30
+ target_sfreq=250,
31
+ target_samples=1000,
32
+ channel_names=None,
33
+ ):
34
+ """Filter, average-reference, resample, and globally normalize `[B,C,T]`."""
35
+
36
+ del channel_names # IED Finder has already arranged the declared channel order.
37
+ x = np.asarray(batch, dtype=np.float32)
38
+ if x.ndim == 2:
39
+ x = x[None]
40
+ if x.ndim != 3 or x.shape[1] != NUM_CHANNELS:
41
+ raise ValueError(f"Expected [batch,19,time], received {x.shape}")
42
+ sfreq = float(source_sfreq or target_sfreq)
43
+ if sfreq <= 90.0:
44
+ raise ValueError(f"Sampling rate must be above the 45 Hz cutoff; received {sfreq}")
45
+ sos = butter(4, [1.0, 45.0], btype="bandpass", fs=sfreq, output="sos")
46
+ x = sosfiltfilt(sos, x, axis=-1).astype(np.float32, copy=False)
47
+ x -= x.mean(axis=1, keepdims=True)
48
+ if not math.isclose(sfreq, float(target_sfreq)):
49
+ up = int(target_sfreq)
50
+ down = int(round(sfreq))
51
+ common = math.gcd(up, down)
52
+ x = resample_poly(x, up // common, down // common, axis=-1).astype(np.float32)
53
+ if x.shape[-1] != int(target_samples):
54
+ x = resample(x, int(target_samples), axis=-1).astype(np.float32)
55
+ mean = x.mean(axis=(1, 2), keepdims=True)
56
+ std = x.std(axis=(1, 2), keepdims=True)
57
+ return np.clip((x - mean) / np.maximum(std, 1e-7), -8.0, 8.0).astype(np.float32)
58
+
eegannotationtool/senua_eegpt/manifest.json ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "finder_defaults": {
3
+ "pad_policy": "skip",
4
+ "step_ms": 500.0,
5
+ "threshold": 0.8916015625
6
+ },
7
+ "important": "Research screening model; every candidate requires expert review. Threshold selected on public vEpiSet validation subjects.",
8
+ "input": {
9
+ "channel_aliases": {
10
+ "P7": "T5",
11
+ "P8": "T6",
12
+ "T7": "T3",
13
+ "T8": "T4"
14
+ },
15
+ "channel_names": [
16
+ "Fp1",
17
+ "Fp2",
18
+ "F3",
19
+ "F4",
20
+ "C3",
21
+ "C4",
22
+ "P3",
23
+ "P4",
24
+ "O1",
25
+ "O2",
26
+ "F7",
27
+ "F8",
28
+ "T3",
29
+ "T4",
30
+ "T5",
31
+ "T6",
32
+ "Fz",
33
+ "Cz",
34
+ "Pz"
35
+ ],
36
+ "input_samples": 1000,
37
+ "normalization": "global four-second window z-score clipped to [-8,8]",
38
+ "reference": "common average",
39
+ "required_filters": [
40
+ "1-45 Hz zero-phase Butterworth"
41
+ ],
42
+ "sampling_rate_hz": 250.0,
43
+ "window_seconds": 4.0
44
+ },
45
+ "name": "SenuaLab eegpt",
46
+ "schema_version": 1,
47
+ "source": "https://huggingface.co/SenuaLab/EEG-IED-Detection"
48
+ }
eegannotationtool/senua_eegpt/model.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9a0a55ed656049b4109d0e923ee0d461151a8a793995a24438717efecd2f24d5
3
+ size 102394167
eegannotationtool/senua_eegpt/model.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """EEG Annotation Tool adapter for the SenuaLab EEGPT temporal-head model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import mne
8
+ import torch
9
+ from torch import nn
10
+
11
+ try:
12
+ from braindecode.models import EEGPT
13
+ except ImportError as exc: # imported by EEG Annotation Tool during discovery
14
+ raise ImportError("This model requires braindecode[hub]==1.6.1") from exc
15
+
16
+ from .preprocessing import INTERNATIONAL_10_20_CHANNELS, preprocess_batch as _preprocess
17
+
18
+
19
+ EEGPT_CHANNELS = [
20
+ "Fp1", "Fp2", "F3", "F4", "C3", "C4", "P3", "P4", "O1", "O2",
21
+ "F7", "F8", "T7", "T8", "P7", "P8", "Fz", "Cz", "Pz",
22
+ ]
23
+ MODEL_CHANNELS = INTERNATIONAL_10_20_CHANNELS
24
+ MODEL_CHANNEL_ALIASES = {"T3": "T7", "T4": "T8", "T5": "P7", "T6": "P8"}
25
+ MODEL_INPUT_SAMPLES = 1000
26
+ MODEL_SAMPLING_RATE_HZ = 250.0
27
+ MODEL_WINDOW_SECONDS = 4.0
28
+ MODEL_ENTRY_CLASS = "SenuaEEGPT"
29
+ MODEL_NUM_CLASSES = 2
30
+ MODEL_CLASS_LABELS = ["Non-IED", "IED"]
31
+ MODEL_NON_IED_CLASS_INDEX = 0
32
+ MODEL_IED_CLASS_INDICES = [1]
33
+ MODEL_DESCRIPTION = "SenuaLab EEGPT encoder with an IED-specific temporal head"
34
+ MODEL_BATCH_PREPROCESSOR = "preprocess_batch"
35
+ MODEL_REQUIRED_REFERENCE = "common average (applied by model adapter)"
36
+ MODEL_REQUIRED_FILTERS = ["1-45 Hz zero-phase Butterworth (applied by model adapter)"]
37
+ MODEL_REQUIRED_NORMALIZATION = "global four-second window z-score, clipped to [-8,8]"
38
+ MODEL_INPUT_UNIT = "scale-invariant after window z-score"
39
+ MODEL_SOURCE_SIGNAL_POLICY = "raw"
40
+ MODEL_REQUIRES_FULL_WINDOW = True
41
+ MODEL_REQUIRES_ALL_CHANNELS = True
42
+ MODEL_DEFAULT_THRESHOLD = 0.8916015625
43
+ MODEL_DEFAULT_STEP_MS = 500.0
44
+ MODEL_DEFAULT_PAD_POLICY = "skip"
45
+ MODEL_DEFAULT_BATCH_SIZE = 16
46
+ MODEL_DEFAULT_BATCH_MEMORY_MB = 256.0
47
+ MODEL_VALIDATION_NOTE = "Threshold selected on vEpiSet validation subjects; requires Braindecode 1.6.1 and is not externally validated."
48
+ MODEL_PREPROCESSING_NOTE = "The adapter applies the released 1-45 Hz, common-average, resampling, and global-window z-score pipeline."
49
+
50
+
51
+ def preprocess_batch(batch, source_sfreq=None, channel_names=None):
52
+ return _preprocess(
53
+ batch,
54
+ source_sfreq=source_sfreq,
55
+ target_sfreq=250,
56
+ target_samples=1000,
57
+ channel_names=channel_names,
58
+ )
59
+
60
+
61
+ def _chs_info() -> list[dict[str, Any]]:
62
+ info = mne.create_info(EEGPT_CHANNELS, sfreq=250.0, ch_types="eeg")
63
+ info.set_montage("standard_1020")
64
+ return info["chs"]
65
+
66
+
67
+ class EEGPTIEDHead(nn.Module):
68
+ def __init__(self, hidden: int = 128, n_outputs: int = 2):
69
+ super().__init__()
70
+ flattened_embedding = 4 * 512
71
+ self.input_norm = nn.LayerNorm(flattened_embedding)
72
+ self.patch_projection = nn.Sequential(
73
+ nn.Linear(flattened_embedding, hidden), nn.GELU(), nn.Dropout(0.20)
74
+ )
75
+ self.temporal = nn.Sequential(
76
+ nn.Conv1d(hidden, hidden, 5, padding=2, groups=hidden, bias=False),
77
+ nn.BatchNorm1d(hidden),
78
+ nn.Conv1d(hidden, hidden, 1, bias=False),
79
+ nn.GELU(),
80
+ nn.Dropout(0.20),
81
+ nn.Conv1d(hidden, hidden, 3, padding=2, dilation=2, groups=hidden, bias=False),
82
+ nn.BatchNorm1d(hidden),
83
+ nn.Conv1d(hidden, hidden, 1, bias=False),
84
+ nn.GELU(),
85
+ )
86
+ self.attention = nn.Conv1d(hidden, 1, 1)
87
+ self.classifier = nn.Sequential(
88
+ nn.LayerNorm(hidden * 3), nn.Dropout(0.35), nn.Linear(hidden * 3, n_outputs)
89
+ )
90
+
91
+ def forward(self, z: torch.Tensor) -> torch.Tensor:
92
+ patches = self.input_norm(z.flatten(2))
93
+ patches = self.patch_projection(patches).transpose(1, 2)
94
+ patches = patches + self.temporal(patches)
95
+ weights = self.attention(patches).softmax(dim=-1)
96
+ pooled = torch.cat(
97
+ [(patches * weights).sum(-1), patches.mean(-1), patches.amax(-1)], dim=1
98
+ )
99
+ return self.classifier(pooled)
100
+
101
+
102
+ class SenuaEEGPT(EEGPT):
103
+ def __init__(self, num_channels: int = 19, num_classes: int = 2, input_length: int = 1000):
104
+ if num_channels != 19 or input_length != 1000:
105
+ raise ValueError("SenuaEEGPT requires 19 channels and 1,000 samples")
106
+ super().__init__(
107
+ n_outputs=num_classes,
108
+ n_chans=19,
109
+ chs_info=_chs_info(),
110
+ n_times=1000,
111
+ sfreq=250.0,
112
+ chan_proj_type="none",
113
+ return_encoder_output=False,
114
+ )
115
+ self.final_layer = EEGPTIEDHead(n_outputs=num_classes)
116
+
eegannotationtool/senua_eegpt/preprocessing.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Exact four-second preprocessing used by the released SenuaLab models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+
7
+ import numpy as np
8
+ from scipy.signal import butter, resample, resample_poly, sosfiltfilt
9
+
10
+
11
+ NUM_CHANNELS = 19
12
+ INTERNATIONAL_10_20_CHANNELS = [
13
+ "Fp1", "Fp2", "F3", "F4", "C3", "C4", "P3", "P4", "O1", "O2",
14
+ "F7", "F8", "T3", "T4", "T5", "T6", "Fz", "Cz", "Pz",
15
+ ]
16
+ DATASET_29_CHANNEL_ORDER = INTERNATIONAL_10_20_CHANNELS
17
+
18
+
19
+ def normalize_sample(sample):
20
+ x = np.asarray(sample, dtype=np.float32)
21
+ mean = x.mean()
22
+ std = x.std()
23
+ return np.clip((x - mean) / max(float(std), 1e-7), -8.0, 8.0).astype(np.float32)
24
+
25
+
26
+ def preprocess_batch(
27
+ batch,
28
+ *,
29
+ source_sfreq=None,
30
+ target_sfreq=250,
31
+ target_samples=1000,
32
+ channel_names=None,
33
+ ):
34
+ """Filter, average-reference, resample, and globally normalize `[B,C,T]`."""
35
+
36
+ del channel_names # IED Finder has already arranged the declared channel order.
37
+ x = np.asarray(batch, dtype=np.float32)
38
+ if x.ndim == 2:
39
+ x = x[None]
40
+ if x.ndim != 3 or x.shape[1] != NUM_CHANNELS:
41
+ raise ValueError(f"Expected [batch,19,time], received {x.shape}")
42
+ sfreq = float(source_sfreq or target_sfreq)
43
+ if sfreq <= 90.0:
44
+ raise ValueError(f"Sampling rate must be above the 45 Hz cutoff; received {sfreq}")
45
+ sos = butter(4, [1.0, 45.0], btype="bandpass", fs=sfreq, output="sos")
46
+ x = sosfiltfilt(sos, x, axis=-1).astype(np.float32, copy=False)
47
+ x -= x.mean(axis=1, keepdims=True)
48
+ if not math.isclose(sfreq, float(target_sfreq)):
49
+ up = int(target_sfreq)
50
+ down = int(round(sfreq))
51
+ common = math.gcd(up, down)
52
+ x = resample_poly(x, up // common, down // common, axis=-1).astype(np.float32)
53
+ if x.shape[-1] != int(target_samples):
54
+ x = resample(x, int(target_samples), axis=-1).astype(np.float32)
55
+ mean = x.mean(axis=(1, 2), keepdims=True)
56
+ std = x.std(axis=(1, 2), keepdims=True)
57
+ return np.clip((x - mean) / np.maximum(std, 1e-7), -8.0, 8.0).astype(np.float32)
58
+
eegannotationtool/senua_eegpt_linear_probe/manifest.json ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "finder_defaults": {
3
+ "pad_policy": "skip",
4
+ "step_ms": 500.0,
5
+ "threshold": 0.40234375
6
+ },
7
+ "important": "Research screening model; every candidate requires expert review. Threshold selected on public vEpiSet validation subjects.",
8
+ "input": {
9
+ "channel_aliases": {
10
+ "P7": "T5",
11
+ "P8": "T6",
12
+ "T7": "T3",
13
+ "T8": "T4"
14
+ },
15
+ "channel_names": [
16
+ "Fp1",
17
+ "Fp2",
18
+ "F3",
19
+ "F4",
20
+ "C3",
21
+ "C4",
22
+ "P3",
23
+ "P4",
24
+ "O1",
25
+ "O2",
26
+ "F7",
27
+ "F8",
28
+ "T3",
29
+ "T4",
30
+ "T5",
31
+ "T6",
32
+ "Fz",
33
+ "Cz",
34
+ "Pz"
35
+ ],
36
+ "input_samples": 1000,
37
+ "normalization": "global four-second window z-score clipped to [-8,8]",
38
+ "reference": "common average",
39
+ "required_filters": [
40
+ "1-45 Hz zero-phase Butterworth"
41
+ ],
42
+ "sampling_rate_hz": 250.0,
43
+ "window_seconds": 4.0
44
+ },
45
+ "name": "SenuaLab eegpt_linear_probe",
46
+ "schema_version": 1,
47
+ "source": "https://huggingface.co/SenuaLab/EEG-IED-Detection"
48
+ }
eegannotationtool/senua_eegpt_linear_probe/model.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a51fd32505d51d80677ed6504b94d0b8a6718cb1de21a07cd3253ed1e1a3405a
3
+ size 101312967
eegannotationtool/senua_eegpt_linear_probe/model.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """EEG Annotation Tool adapter for the experimental SenuaLab EEGPT linear probe."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import mne
8
+
9
+ try:
10
+ from braindecode.models import EEGPT
11
+ except ImportError as exc:
12
+ raise ImportError("This model requires braindecode[hub]==1.6.1") from exc
13
+
14
+ from .preprocessing import INTERNATIONAL_10_20_CHANNELS, preprocess_batch as _preprocess
15
+
16
+
17
+ EEGPT_CHANNELS = [
18
+ "Fp1", "Fp2", "F3", "F4", "C3", "C4", "P3", "P4", "O1", "O2",
19
+ "F7", "F8", "T7", "T8", "P7", "P8", "Fz", "Cz", "Pz",
20
+ ]
21
+ MODEL_CHANNELS = INTERNATIONAL_10_20_CHANNELS
22
+ MODEL_CHANNEL_ALIASES = {"T3": "T7", "T4": "T8", "T5": "P7", "T6": "P8"}
23
+ MODEL_INPUT_SAMPLES = 1000
24
+ MODEL_SAMPLING_RATE_HZ = 250.0
25
+ MODEL_WINDOW_SECONDS = 4.0
26
+ MODEL_ENTRY_CLASS = "SenuaEEGPTLinearProbe"
27
+ MODEL_NUM_CLASSES = 2
28
+ MODEL_CLASS_LABELS = ["Non-IED", "IED"]
29
+ MODEL_NON_IED_CLASS_INDEX = 0
30
+ MODEL_IED_CLASS_INDICES = [1]
31
+ MODEL_DESCRIPTION = "Experimental SenuaLab EEGPT frozen-encoder linear probe"
32
+ MODEL_BATCH_PREPROCESSOR = "preprocess_batch"
33
+ MODEL_REQUIRED_REFERENCE = "common average (applied by model adapter)"
34
+ MODEL_REQUIRED_FILTERS = ["1-45 Hz zero-phase Butterworth (applied by model adapter)"]
35
+ MODEL_REQUIRED_NORMALIZATION = "global four-second window z-score, clipped to [-8,8]"
36
+ MODEL_INPUT_UNIT = "scale-invariant after window z-score"
37
+ MODEL_SOURCE_SIGNAL_POLICY = "raw"
38
+ MODEL_REQUIRES_FULL_WINDOW = True
39
+ MODEL_REQUIRES_ALL_CHANNELS = True
40
+ MODEL_DEFAULT_THRESHOLD = 0.40234375
41
+ MODEL_DEFAULT_STEP_MS = 500.0
42
+ MODEL_DEFAULT_PAD_POLICY = "skip"
43
+ MODEL_DEFAULT_BATCH_SIZE = 16
44
+ MODEL_DEFAULT_BATCH_MEMORY_MB = 256.0
45
+ MODEL_VALIDATION_NOTE = "Experimental ablation; not recommended for deployment and not externally validated."
46
+
47
+
48
+ def preprocess_batch(batch, source_sfreq=None, channel_names=None):
49
+ return _preprocess(
50
+ batch,
51
+ source_sfreq=source_sfreq,
52
+ target_sfreq=250,
53
+ target_samples=1000,
54
+ channel_names=channel_names,
55
+ )
56
+
57
+
58
+ def _chs_info() -> list[dict[str, Any]]:
59
+ info = mne.create_info(EEGPT_CHANNELS, sfreq=250.0, ch_types="eeg")
60
+ info.set_montage("standard_1020")
61
+ return info["chs"]
62
+
63
+
64
+ class SenuaEEGPTLinearProbe(EEGPT):
65
+ def __init__(self, num_channels: int = 19, num_classes: int = 2, input_length: int = 1000):
66
+ if num_channels != 19 or input_length != 1000:
67
+ raise ValueError("SenuaEEGPTLinearProbe requires 19 channels and 1,000 samples")
68
+ super().__init__(
69
+ n_outputs=num_classes,
70
+ n_chans=19,
71
+ chs_info=_chs_info(),
72
+ n_times=1000,
73
+ sfreq=250.0,
74
+ chan_proj_type="none",
75
+ return_encoder_output=False,
76
+ )
77
+
eegannotationtool/senua_eegpt_linear_probe/preprocessing.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Exact four-second preprocessing used by the released SenuaLab models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+
7
+ import numpy as np
8
+ from scipy.signal import butter, resample, resample_poly, sosfiltfilt
9
+
10
+
11
+ NUM_CHANNELS = 19
12
+ INTERNATIONAL_10_20_CHANNELS = [
13
+ "Fp1", "Fp2", "F3", "F4", "C3", "C4", "P3", "P4", "O1", "O2",
14
+ "F7", "F8", "T3", "T4", "T5", "T6", "Fz", "Cz", "Pz",
15
+ ]
16
+ DATASET_29_CHANNEL_ORDER = INTERNATIONAL_10_20_CHANNELS
17
+
18
+
19
+ def normalize_sample(sample):
20
+ x = np.asarray(sample, dtype=np.float32)
21
+ mean = x.mean()
22
+ std = x.std()
23
+ return np.clip((x - mean) / max(float(std), 1e-7), -8.0, 8.0).astype(np.float32)
24
+
25
+
26
+ def preprocess_batch(
27
+ batch,
28
+ *,
29
+ source_sfreq=None,
30
+ target_sfreq=250,
31
+ target_samples=1000,
32
+ channel_names=None,
33
+ ):
34
+ """Filter, average-reference, resample, and globally normalize `[B,C,T]`."""
35
+
36
+ del channel_names # IED Finder has already arranged the declared channel order.
37
+ x = np.asarray(batch, dtype=np.float32)
38
+ if x.ndim == 2:
39
+ x = x[None]
40
+ if x.ndim != 3 or x.shape[1] != NUM_CHANNELS:
41
+ raise ValueError(f"Expected [batch,19,time], received {x.shape}")
42
+ sfreq = float(source_sfreq or target_sfreq)
43
+ if sfreq <= 90.0:
44
+ raise ValueError(f"Sampling rate must be above the 45 Hz cutoff; received {sfreq}")
45
+ sos = butter(4, [1.0, 45.0], btype="bandpass", fs=sfreq, output="sos")
46
+ x = sosfiltfilt(sos, x, axis=-1).astype(np.float32, copy=False)
47
+ x -= x.mean(axis=1, keepdims=True)
48
+ if not math.isclose(sfreq, float(target_sfreq)):
49
+ up = int(target_sfreq)
50
+ down = int(round(sfreq))
51
+ common = math.gcd(up, down)
52
+ x = resample_poly(x, up // common, down // common, axis=-1).astype(np.float32)
53
+ if x.shape[-1] != int(target_samples):
54
+ x = resample(x, int(target_samples), axis=-1).astype(np.float32)
55
+ mean = x.mean(axis=(1, 2), keepdims=True)
56
+ std = x.std(axis=(1, 2), keepdims=True)
57
+ return np.clip((x - mean) / np.maximum(std, 1e-7), -8.0, 8.0).astype(np.float32)
58
+
eegannotationtool/senua_iednet_lite/manifest.json ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "finder_defaults": {
3
+ "pad_policy": "skip",
4
+ "step_ms": 500.0,
5
+ "threshold": 0.6004377007484436
6
+ },
7
+ "important": "Research screening model; every candidate requires expert review. Threshold selected on public vEpiSet validation subjects.",
8
+ "input": {
9
+ "channel_aliases": {
10
+ "P7": "T5",
11
+ "P8": "T6",
12
+ "T7": "T3",
13
+ "T8": "T4"
14
+ },
15
+ "channel_names": [
16
+ "Fp1",
17
+ "Fp2",
18
+ "F3",
19
+ "F4",
20
+ "C3",
21
+ "C4",
22
+ "P3",
23
+ "P4",
24
+ "O1",
25
+ "O2",
26
+ "F7",
27
+ "F8",
28
+ "T3",
29
+ "T4",
30
+ "T5",
31
+ "T6",
32
+ "Fz",
33
+ "Cz",
34
+ "Pz"
35
+ ],
36
+ "input_samples": 1000,
37
+ "normalization": "global four-second window z-score clipped to [-8,8]",
38
+ "reference": "common average",
39
+ "required_filters": [
40
+ "1-45 Hz zero-phase Butterworth"
41
+ ],
42
+ "sampling_rate_hz": 250.0,
43
+ "window_seconds": 4.0
44
+ },
45
+ "name": "SenuaLab iednet_lite",
46
+ "schema_version": 1,
47
+ "source": "https://huggingface.co/SenuaLab/EEG-IED-Detection"
48
+ }
eegannotationtool/senua_iednet_lite/model.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:383fe1de4aa8cf01b5f1c0a6734d92214ec0e7f96a6cd18a50fa52c52ccb210c
3
+ size 1041083
eegannotationtool/senua_iednet_lite/model.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """EEG Annotation Tool adapter for SenuaLab IEDNet-Lite."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import torch
6
+ import torch.nn.functional as F
7
+ from torch import nn
8
+
9
+ from .preprocessing import INTERNATIONAL_10_20_CHANNELS, preprocess_batch as _preprocess
10
+
11
+
12
+ MODEL_CHANNELS = INTERNATIONAL_10_20_CHANNELS
13
+ MODEL_INPUT_SAMPLES = 1000
14
+ MODEL_SAMPLING_RATE_HZ = 250.0
15
+ MODEL_WINDOW_SECONDS = 4.0
16
+ MODEL_ENTRY_CLASS = "IEDNetLite"
17
+ MODEL_NUM_CLASSES = 2
18
+ MODEL_CLASS_LABELS = ["Non-IED", "IED"]
19
+ MODEL_NON_IED_CLASS_INDEX = 0
20
+ MODEL_IED_CLASS_INDICES = [1]
21
+ MODEL_DESCRIPTION = "SenuaLab patient-independent IEDNet-Lite binary IED detector"
22
+ MODEL_BATCH_PREPROCESSOR = "preprocess_batch"
23
+ MODEL_REQUIRED_REFERENCE = "common average (applied by model adapter)"
24
+ MODEL_REQUIRED_FILTERS = ["1-45 Hz zero-phase Butterworth (applied by model adapter)"]
25
+ MODEL_REQUIRED_NORMALIZATION = "global four-second window z-score, clipped to [-8,8]"
26
+ MODEL_INPUT_UNIT = "scale-invariant after window z-score"
27
+ MODEL_SOURCE_SIGNAL_POLICY = "raw"
28
+ MODEL_REQUIRES_FULL_WINDOW = True
29
+ MODEL_REQUIRES_ALL_CHANNELS = True
30
+ MODEL_DEFAULT_THRESHOLD = 0.6004377007484436
31
+ MODEL_DEFAULT_STEP_MS = 500.0
32
+ MODEL_DEFAULT_PAD_POLICY = "skip"
33
+ MODEL_DEFAULT_BATCH_SIZE = 64
34
+ MODEL_DEFAULT_BATCH_MEMORY_MB = 64.0
35
+ MODEL_VALIDATION_NOTE = "Threshold selected on the vEpiSet validation subjects; not externally validated."
36
+ MODEL_PREPROCESSING_NOTE = "The adapter applies the exact released 1-45 Hz, common-average, resampling, and global-window z-score pipeline."
37
+
38
+
39
+ def preprocess_batch(batch, source_sfreq=None, channel_names=None):
40
+ return _preprocess(
41
+ batch,
42
+ source_sfreq=source_sfreq,
43
+ target_sfreq=250,
44
+ target_samples=1000,
45
+ channel_names=channel_names,
46
+ )
47
+
48
+
49
+ class SqueezeExcitation(nn.Module):
50
+ def __init__(self, channels: int, reduction: int = 8):
51
+ super().__init__()
52
+ hidden = max(8, channels // reduction)
53
+ self.net = nn.Sequential(
54
+ nn.AdaptiveAvgPool1d(1),
55
+ nn.Conv1d(channels, hidden, 1),
56
+ nn.GELU(),
57
+ nn.Conv1d(hidden, channels, 1),
58
+ nn.Sigmoid(),
59
+ )
60
+
61
+ def forward(self, x):
62
+ return x * self.net(x)
63
+
64
+
65
+ class DSResidual(nn.Module):
66
+ def __init__(self, in_ch: int, out_ch: int, kernel: int, stride: int = 1, dilation: int = 1):
67
+ super().__init__()
68
+ pad = dilation * (kernel - 1) // 2
69
+ self.body = nn.Sequential(
70
+ nn.Conv1d(in_ch, in_ch, kernel, stride=stride, padding=pad, dilation=dilation, groups=in_ch, bias=False),
71
+ nn.BatchNorm1d(in_ch),
72
+ nn.GELU(),
73
+ nn.Conv1d(in_ch, out_ch, 1, bias=False),
74
+ nn.BatchNorm1d(out_ch),
75
+ SqueezeExcitation(out_ch),
76
+ )
77
+ self.skip = (
78
+ nn.Identity()
79
+ if in_ch == out_ch and stride == 1
80
+ else nn.Sequential(nn.Conv1d(in_ch, out_ch, 1, stride=stride, bias=False), nn.BatchNorm1d(out_ch))
81
+ )
82
+
83
+ def forward(self, x):
84
+ return F.gelu(self.body(x) + self.skip(x))
85
+
86
+
87
+ class IEDNetLite(nn.Module):
88
+ def __init__(self, num_channels: int = 19, num_classes: int = 2, input_length: int = 1000, subtypes: int = 5):
89
+ super().__init__()
90
+ del input_length
91
+ branches = []
92
+ for kernel in (7, 15, 31):
93
+ branches.append(
94
+ nn.Sequential(
95
+ nn.Conv1d(num_channels, 24, kernel, stride=2, padding=kernel // 2, bias=False),
96
+ nn.BatchNorm1d(24),
97
+ nn.GELU(),
98
+ )
99
+ )
100
+ self.stem = nn.ModuleList(branches)
101
+ self.backbone = nn.Sequential(
102
+ DSResidual(72, 72, 9, stride=2),
103
+ DSResidual(72, 96, 7, stride=2),
104
+ DSResidual(96, 96, 7, dilation=2),
105
+ DSResidual(96, 144, 5, stride=2),
106
+ DSResidual(144, 144, 5, dilation=2),
107
+ DSResidual(144, 192, 3, stride=2),
108
+ DSResidual(192, 192, 3, dilation=2),
109
+ )
110
+ self.dropout = nn.Dropout(0.35)
111
+ self.binary_head = nn.Linear(384, num_classes)
112
+ self.subtype_head = nn.Linear(384, subtypes)
113
+
114
+ def forward(self, x):
115
+ x = torch.cat([branch(x) for branch in self.stem], dim=1)
116
+ x = self.backbone(x)
117
+ features = self.dropout(torch.cat([x.mean(-1), x.amax(-1)], dim=1))
118
+ return self.binary_head(features), self.subtype_head(features)
119
+
eegannotationtool/senua_iednet_lite/preprocessing.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Exact four-second preprocessing used by the released SenuaLab models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+
7
+ import numpy as np
8
+ from scipy.signal import butter, resample, resample_poly, sosfiltfilt
9
+
10
+
11
+ NUM_CHANNELS = 19
12
+ INTERNATIONAL_10_20_CHANNELS = [
13
+ "Fp1", "Fp2", "F3", "F4", "C3", "C4", "P3", "P4", "O1", "O2",
14
+ "F7", "F8", "T3", "T4", "T5", "T6", "Fz", "Cz", "Pz",
15
+ ]
16
+ DATASET_29_CHANNEL_ORDER = INTERNATIONAL_10_20_CHANNELS
17
+
18
+
19
+ def normalize_sample(sample):
20
+ x = np.asarray(sample, dtype=np.float32)
21
+ mean = x.mean()
22
+ std = x.std()
23
+ return np.clip((x - mean) / max(float(std), 1e-7), -8.0, 8.0).astype(np.float32)
24
+
25
+
26
+ def preprocess_batch(
27
+ batch,
28
+ *,
29
+ source_sfreq=None,
30
+ target_sfreq=250,
31
+ target_samples=1000,
32
+ channel_names=None,
33
+ ):
34
+ """Filter, average-reference, resample, and globally normalize `[B,C,T]`."""
35
+
36
+ del channel_names # IED Finder has already arranged the declared channel order.
37
+ x = np.asarray(batch, dtype=np.float32)
38
+ if x.ndim == 2:
39
+ x = x[None]
40
+ if x.ndim != 3 or x.shape[1] != NUM_CHANNELS:
41
+ raise ValueError(f"Expected [batch,19,time], received {x.shape}")
42
+ sfreq = float(source_sfreq or target_sfreq)
43
+ if sfreq <= 90.0:
44
+ raise ValueError(f"Sampling rate must be above the 45 Hz cutoff; received {sfreq}")
45
+ sos = butter(4, [1.0, 45.0], btype="bandpass", fs=sfreq, output="sos")
46
+ x = sosfiltfilt(sos, x, axis=-1).astype(np.float32, copy=False)
47
+ x -= x.mean(axis=1, keepdims=True)
48
+ if not math.isclose(sfreq, float(target_sfreq)):
49
+ up = int(target_sfreq)
50
+ down = int(round(sfreq))
51
+ common = math.gcd(up, down)
52
+ x = resample_poly(x, up // common, down // common, axis=-1).astype(np.float32)
53
+ if x.shape[-1] != int(target_samples):
54
+ x = resample(x, int(target_samples), axis=-1).astype(np.float32)
55
+ mean = x.mean(axis=(1, 2), keepdims=True)
56
+ std = x.std(axis=(1, 2), keepdims=True)
57
+ return np.clip((x - mean) / np.maximum(std, 1e-7), -8.0, 8.0).astype(np.float32)
58
+
eegannotationtool/senua_resnet_attention/manifest.json ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "finder_defaults": {
3
+ "pad_policy": "skip",
4
+ "step_ms": 500.0,
5
+ "threshold": 0.6944227814674377
6
+ },
7
+ "important": "Research screening model; every candidate requires expert review. Threshold selected on public vEpiSet validation subjects.",
8
+ "input": {
9
+ "channel_aliases": {
10
+ "P7": "T5",
11
+ "P8": "T6",
12
+ "T7": "T3",
13
+ "T8": "T4"
14
+ },
15
+ "channel_names": [
16
+ "Fp1",
17
+ "Fp2",
18
+ "F3",
19
+ "F4",
20
+ "C3",
21
+ "C4",
22
+ "P3",
23
+ "P4",
24
+ "O1",
25
+ "O2",
26
+ "F7",
27
+ "F8",
28
+ "T3",
29
+ "T4",
30
+ "T5",
31
+ "T6",
32
+ "Fz",
33
+ "Cz",
34
+ "Pz"
35
+ ],
36
+ "input_samples": 1000,
37
+ "normalization": "global four-second window z-score clipped to [-8,8]",
38
+ "reference": "common average",
39
+ "required_filters": [
40
+ "1-45 Hz zero-phase Butterworth"
41
+ ],
42
+ "sampling_rate_hz": 250.0,
43
+ "window_seconds": 4.0
44
+ },
45
+ "name": "SenuaLab resnet_attention",
46
+ "schema_version": 1,
47
+ "source": "https://huggingface.co/SenuaLab/EEG-IED-Detection"
48
+ }
eegannotationtool/senua_resnet_attention/model.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:df088c4fd087b453ef3aca76f54a975a69f015324f8d862d2d4ba5316cb91e1a
3
+ size 20037039
eegannotationtool/senua_resnet_attention/model.py ADDED
@@ -0,0 +1,677 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import torch.nn.functional as F
4
+ import math
5
+
6
+ # Import preprocessing constants
7
+ from .preprocessing import INTERNATIONAL_10_20_CHANNELS, NUM_CHANNELS, preprocess_batch as _preprocess
8
+
9
+
10
+ MODEL_CHANNELS = INTERNATIONAL_10_20_CHANNELS
11
+ MODEL_INPUT_SAMPLES = 1000
12
+ MODEL_SAMPLING_RATE_HZ = 250.0
13
+ MODEL_WINDOW_SECONDS = 4.0
14
+ MODEL_ENTRY_CLASS = "EEGResNetAttention"
15
+ MODEL_NUM_CLASSES = 2
16
+ MODEL_CLASS_LABELS = ["Non-IED", "IED"]
17
+ MODEL_NON_IED_CLASS_INDEX = 0
18
+ MODEL_IED_CLASS_INDICES = [1]
19
+ MODEL_DESCRIPTION = "SenuaLab patient-independent ResNet-Attention binary IED detector"
20
+ MODEL_BATCH_PREPROCESSOR = "preprocess_batch"
21
+ MODEL_REQUIRED_REFERENCE = "common average (applied by model adapter)"
22
+ MODEL_REQUIRED_FILTERS = ["1-45 Hz zero-phase Butterworth (applied by model adapter)"]
23
+ MODEL_REQUIRED_NORMALIZATION = "global four-second window z-score, clipped to [-8,8]"
24
+ MODEL_INPUT_UNIT = "scale-invariant after window z-score"
25
+ MODEL_SOURCE_SIGNAL_POLICY = "raw"
26
+ MODEL_REQUIRES_FULL_WINDOW = True
27
+ MODEL_REQUIRES_ALL_CHANNELS = True
28
+ MODEL_DEFAULT_THRESHOLD = 0.6944227814674377
29
+ MODEL_DEFAULT_STEP_MS = 500.0
30
+ MODEL_DEFAULT_PAD_POLICY = "skip"
31
+ MODEL_DEFAULT_BATCH_SIZE = 32
32
+ MODEL_DEFAULT_BATCH_MEMORY_MB = 96.0
33
+ MODEL_VALIDATION_NOTE = "Threshold selected on the vEpiSet validation subjects; not externally validated."
34
+ MODEL_PREPROCESSING_NOTE = "The adapter applies the exact released 1-45 Hz, common-average, resampling, and global-window z-score pipeline."
35
+
36
+
37
+ def preprocess_batch(batch, source_sfreq=None, channel_names=None):
38
+ return _preprocess(
39
+ batch,
40
+ source_sfreq=source_sfreq,
41
+ target_sfreq=250,
42
+ target_samples=1000,
43
+ channel_names=channel_names,
44
+ )
45
+
46
+
47
+ class Simple1DCNN(nn.Module):
48
+ """
49
+ Simple 1D CNN for EEG IED classification.
50
+
51
+ Architecture:
52
+ - 4 convolutional blocks with BatchNorm, ReLU, and MaxPool
53
+ - Fully connected classifier with dropout
54
+
55
+ Default configuration uses 19 channels (standard 10-20) to match
56
+ the vEpiSet preprocessing pipeline.
57
+ """
58
+
59
+ def __init__(self, num_channels=NUM_CHANNELS, num_classes=2, input_length=2000):
60
+ """
61
+ Args:
62
+ num_channels: Number of EEG channels (default 19 for standard 10-20)
63
+ num_classes: Number of output classes (default 2: IED vs Non-IED)
64
+ input_length: Number of time samples (default 2000 = 4 seconds at 500 Hz)
65
+ """
66
+ super(Simple1DCNN, self).__init__()
67
+
68
+ self.num_channels = num_channels
69
+ self.input_length = input_length
70
+
71
+ self.features = nn.Sequential(
72
+ # Block 1
73
+ nn.Conv1d(in_channels=num_channels, out_channels=32, kernel_size=5, stride=1, padding=2),
74
+ nn.BatchNorm1d(32),
75
+ nn.ReLU(),
76
+ nn.MaxPool1d(kernel_size=2, stride=2),
77
+
78
+ # Block 2
79
+ nn.Conv1d(in_channels=32, out_channels=64, kernel_size=5, stride=1, padding=2),
80
+ nn.BatchNorm1d(64),
81
+ nn.ReLU(),
82
+ nn.MaxPool1d(kernel_size=2, stride=2),
83
+
84
+ # Block 3
85
+ nn.Conv1d(in_channels=64, out_channels=128, kernel_size=3, stride=1, padding=1),
86
+ nn.BatchNorm1d(128),
87
+ nn.ReLU(),
88
+ nn.MaxPool1d(kernel_size=2, stride=2),
89
+
90
+ # Block 4
91
+ nn.Conv1d(in_channels=128, out_channels=256, kernel_size=3, stride=1, padding=1),
92
+ nn.BatchNorm1d(256),
93
+ nn.ReLU(),
94
+ nn.MaxPool1d(kernel_size=2, stride=2),
95
+ )
96
+
97
+ # Calculate size after convolutions
98
+ # Input: 2000
99
+ # Pool 1: 1000
100
+ # Pool 2: 500
101
+ # Pool 3: 250
102
+ # Pool 4: 125
103
+ self.flatten_size = 256 * (input_length // 16) # 16 = 2^4 (4 pooling layers)
104
+
105
+ self.classifier = nn.Sequential(
106
+ nn.Linear(self.flatten_size, 512),
107
+ nn.ReLU(),
108
+ nn.Dropout(0.5),
109
+ nn.Linear(512, num_classes)
110
+ )
111
+
112
+ def forward(self, x):
113
+ x = self.features(x)
114
+ x = x.view(x.size(0), -1)
115
+ x = self.classifier(x)
116
+ return x
117
+
118
+
119
+ # ============================================================================
120
+ # Advanced Model Architectures for Better IED Detection
121
+ # ============================================================================
122
+
123
+ class SqueezeExcitation1D(nn.Module):
124
+ """Squeeze-and-Excitation block for channel attention."""
125
+
126
+ def __init__(self, channels, reduction=16):
127
+ super().__init__()
128
+ self.avg_pool = nn.AdaptiveAvgPool1d(1)
129
+ self.fc = nn.Sequential(
130
+ nn.Linear(channels, channels // reduction, bias=False),
131
+ nn.ReLU(inplace=True),
132
+ nn.Linear(channels // reduction, channels, bias=False),
133
+ nn.Sigmoid()
134
+ )
135
+
136
+ def forward(self, x):
137
+ b, c, _ = x.size()
138
+ y = self.avg_pool(x).view(b, c)
139
+ y = self.fc(y).view(b, c, 1)
140
+ return x * y.expand_as(x)
141
+
142
+
143
+ class ResidualBlock1D(nn.Module):
144
+ """Residual block with optional squeeze-excitation."""
145
+
146
+ def __init__(self, in_channels, out_channels, kernel_size=3, stride=1,
147
+ downsample=None, use_se=True):
148
+ super().__init__()
149
+ padding = kernel_size // 2
150
+
151
+ self.conv1 = nn.Conv1d(in_channels, out_channels, kernel_size,
152
+ stride=stride, padding=padding, bias=False)
153
+ self.bn1 = nn.BatchNorm1d(out_channels)
154
+ self.relu = nn.ReLU(inplace=True)
155
+ self.conv2 = nn.Conv1d(out_channels, out_channels, kernel_size,
156
+ stride=1, padding=padding, bias=False)
157
+ self.bn2 = nn.BatchNorm1d(out_channels)
158
+
159
+ self.downsample = downsample
160
+ self.se = SqueezeExcitation1D(out_channels) if use_se else nn.Identity()
161
+
162
+ def forward(self, x):
163
+ identity = x
164
+
165
+ out = self.conv1(x)
166
+ out = self.bn1(out)
167
+ out = self.relu(out)
168
+
169
+ out = self.conv2(out)
170
+ out = self.bn2(out)
171
+ out = self.se(out)
172
+
173
+ if self.downsample is not None:
174
+ identity = self.downsample(x)
175
+
176
+ out += identity
177
+ out = self.relu(out)
178
+ return out
179
+
180
+
181
+ class TemporalAttention(nn.Module):
182
+ """Self-attention mechanism for temporal patterns."""
183
+
184
+ def __init__(self, channels, num_heads=4):
185
+ super().__init__()
186
+ self.num_heads = num_heads
187
+ self.head_dim = channels // num_heads
188
+ self.scale = self.head_dim ** -0.5
189
+
190
+ self.qkv = nn.Linear(channels, channels * 3, bias=False)
191
+ self.proj = nn.Linear(channels, channels)
192
+ self.norm = nn.LayerNorm(channels)
193
+
194
+ def forward(self, x):
195
+ # x: [B, C, T] -> [B, T, C]
196
+ x = x.permute(0, 2, 1)
197
+ B, T, C = x.shape
198
+
199
+ # Residual connection
200
+ residual = x
201
+ x = self.norm(x)
202
+
203
+ # Multi-head attention
204
+ qkv = self.qkv(x).reshape(B, T, 3, self.num_heads, self.head_dim)
205
+ qkv = qkv.permute(2, 0, 3, 1, 4) # [3, B, heads, T, head_dim]
206
+ q, k, v = qkv[0], qkv[1], qkv[2]
207
+
208
+ attn = (q @ k.transpose(-2, -1)) * self.scale
209
+ attn = attn.softmax(dim=-1)
210
+
211
+ x = (attn @ v).transpose(1, 2).reshape(B, T, C)
212
+ x = self.proj(x)
213
+ x = x + residual
214
+
215
+ # Back to [B, C, T]
216
+ return x.permute(0, 2, 1)
217
+
218
+
219
+ class MultiScaleConv(nn.Module):
220
+ """Multi-scale convolution for capturing different frequency patterns."""
221
+
222
+ def __init__(self, in_channels, out_channels):
223
+ super().__init__()
224
+ self.branch1 = nn.Sequential(
225
+ nn.Conv1d(in_channels, out_channels // 4, kernel_size=3, padding=1, bias=False),
226
+ nn.BatchNorm1d(out_channels // 4),
227
+ nn.ReLU(inplace=True)
228
+ )
229
+ self.branch2 = nn.Sequential(
230
+ nn.Conv1d(in_channels, out_channels // 4, kernel_size=5, padding=2, bias=False),
231
+ nn.BatchNorm1d(out_channels // 4),
232
+ nn.ReLU(inplace=True)
233
+ )
234
+ self.branch3 = nn.Sequential(
235
+ nn.Conv1d(in_channels, out_channels // 4, kernel_size=7, padding=3, bias=False),
236
+ nn.BatchNorm1d(out_channels // 4),
237
+ nn.ReLU(inplace=True)
238
+ )
239
+ self.branch4 = nn.Sequential(
240
+ nn.Conv1d(in_channels, out_channels // 4, kernel_size=11, padding=5, bias=False),
241
+ nn.BatchNorm1d(out_channels // 4),
242
+ nn.ReLU(inplace=True)
243
+ )
244
+
245
+ def forward(self, x):
246
+ return torch.cat([
247
+ self.branch1(x),
248
+ self.branch2(x),
249
+ self.branch3(x),
250
+ self.branch4(x)
251
+ ], dim=1)
252
+
253
+
254
+ class EEGResNetAttention(nn.Module):
255
+ """
256
+ Advanced EEG IED Detector with ResNet backbone and Attention.
257
+
258
+ Features:
259
+ - Multi-scale initial convolution for capturing different EEG frequencies
260
+ - Residual blocks with Squeeze-and-Excitation attention
261
+ - Temporal self-attention for capturing long-range dependencies
262
+ - Global average pooling for robust feature aggregation
263
+ """
264
+
265
+ def __init__(self, num_channels=NUM_CHANNELS, num_classes=2, input_length=2000):
266
+ super().__init__()
267
+
268
+ self.num_channels = num_channels
269
+ self.input_length = input_length
270
+
271
+ # Multi-scale initial convolution
272
+ self.stem = MultiScaleConv(num_channels, 64)
273
+ self.stem_pool = nn.MaxPool1d(kernel_size=2, stride=2)
274
+
275
+ # Residual blocks
276
+ self.layer1 = self._make_layer(64, 64, num_blocks=2, stride=1)
277
+ self.layer2 = self._make_layer(64, 128, num_blocks=2, stride=2)
278
+ self.layer3 = self._make_layer(128, 256, num_blocks=2, stride=2)
279
+ self.layer4 = self._make_layer(256, 512, num_blocks=2, stride=2)
280
+
281
+ # Temporal attention
282
+ self.temporal_attn = TemporalAttention(512, num_heads=8)
283
+
284
+ # Global pooling and classifier
285
+ self.global_pool = nn.AdaptiveAvgPool1d(1)
286
+ self.dropout = nn.Dropout(0.5)
287
+ self.fc = nn.Linear(512, num_classes)
288
+
289
+ # Initialize weights
290
+ self._init_weights()
291
+
292
+ def _make_layer(self, in_channels, out_channels, num_blocks, stride):
293
+ downsample = None
294
+ if stride != 1 or in_channels != out_channels:
295
+ downsample = nn.Sequential(
296
+ nn.Conv1d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
297
+ nn.BatchNorm1d(out_channels)
298
+ )
299
+
300
+ layers = [ResidualBlock1D(in_channels, out_channels, stride=stride, downsample=downsample)]
301
+ for _ in range(1, num_blocks):
302
+ layers.append(ResidualBlock1D(out_channels, out_channels))
303
+
304
+ return nn.Sequential(*layers)
305
+
306
+ def _init_weights(self):
307
+ for m in self.modules():
308
+ if isinstance(m, nn.Conv1d):
309
+ nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
310
+ elif isinstance(m, nn.BatchNorm1d):
311
+ nn.init.constant_(m.weight, 1)
312
+ nn.init.constant_(m.bias, 0)
313
+ elif isinstance(m, nn.Linear):
314
+ nn.init.normal_(m.weight, 0, 0.01)
315
+ if m.bias is not None:
316
+ nn.init.constant_(m.bias, 0)
317
+
318
+ def forward(self, x):
319
+ # Multi-scale stem
320
+ x = self.stem(x)
321
+ x = self.stem_pool(x)
322
+
323
+ # Residual blocks
324
+ x = self.layer1(x)
325
+ x = self.layer2(x)
326
+ x = self.layer3(x)
327
+ x = self.layer4(x)
328
+
329
+ # Temporal attention
330
+ x = self.temporal_attn(x)
331
+
332
+ # Global pooling and classification
333
+ x = self.global_pool(x).squeeze(-1)
334
+ x = self.dropout(x)
335
+ x = self.fc(x)
336
+
337
+ return x
338
+
339
+
340
+ class EEGTransformer(nn.Module):
341
+ """
342
+ Transformer-based EEG IED Detector.
343
+
344
+ Uses a CNN backbone for initial feature extraction followed by
345
+ Transformer encoder layers for capturing complex temporal patterns.
346
+ """
347
+
348
+ def __init__(self, num_channels=NUM_CHANNELS, num_classes=2, input_length=2000,
349
+ d_model=256, nhead=8, num_layers=4, dim_feedforward=512):
350
+ super().__init__()
351
+
352
+ self.num_channels = num_channels
353
+ self.input_length = input_length
354
+
355
+ # CNN backbone for initial feature extraction
356
+ self.conv_backbone = nn.Sequential(
357
+ nn.Conv1d(num_channels, 64, kernel_size=7, stride=2, padding=3, bias=False),
358
+ nn.BatchNorm1d(64),
359
+ nn.ReLU(inplace=True),
360
+ nn.MaxPool1d(kernel_size=3, stride=2, padding=1),
361
+
362
+ nn.Conv1d(64, 128, kernel_size=5, stride=2, padding=2, bias=False),
363
+ nn.BatchNorm1d(128),
364
+ nn.ReLU(inplace=True),
365
+
366
+ nn.Conv1d(128, d_model, kernel_size=3, stride=2, padding=1, bias=False),
367
+ nn.BatchNorm1d(d_model),
368
+ nn.ReLU(inplace=True),
369
+ )
370
+
371
+ # Calculate sequence length after CNN
372
+ # 2000 -> 1000 -> 500 -> 250 -> 125
373
+ self.seq_len = input_length // 16
374
+
375
+ # Positional encoding
376
+ self.pos_encoding = self._create_positional_encoding(self.seq_len, d_model)
377
+
378
+ # Transformer encoder
379
+ encoder_layer = nn.TransformerEncoderLayer(
380
+ d_model=d_model,
381
+ nhead=nhead,
382
+ dim_feedforward=dim_feedforward,
383
+ dropout=0.1,
384
+ activation='gelu',
385
+ batch_first=True
386
+ )
387
+ self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
388
+
389
+ # Classification head
390
+ self.global_pool = nn.AdaptiveAvgPool1d(1)
391
+ self.classifier = nn.Sequential(
392
+ nn.Linear(d_model, d_model // 2),
393
+ nn.GELU(),
394
+ nn.Dropout(0.3),
395
+ nn.Linear(d_model // 2, num_classes)
396
+ )
397
+
398
+ def _create_positional_encoding(self, seq_len, d_model):
399
+ pe = torch.zeros(seq_len, d_model)
400
+ position = torch.arange(0, seq_len, dtype=torch.float).unsqueeze(1)
401
+ div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
402
+ pe[:, 0::2] = torch.sin(position * div_term)
403
+ pe[:, 1::2] = torch.cos(position * div_term)
404
+ return nn.Parameter(pe.unsqueeze(0), requires_grad=False)
405
+
406
+ def forward(self, x):
407
+ # CNN backbone: [B, C, T] -> [B, d_model, T']
408
+ x = self.conv_backbone(x)
409
+
410
+ # Reshape for transformer: [B, d_model, T'] -> [B, T', d_model]
411
+ x = x.permute(0, 2, 1)
412
+
413
+ # Add positional encoding
414
+ x = x + self.pos_encoding[:, :x.size(1), :]
415
+
416
+ # Transformer encoder
417
+ x = self.transformer(x)
418
+
419
+ # Back to [B, d_model, T'] for pooling
420
+ x = x.permute(0, 2, 1)
421
+
422
+ # Global pooling and classification
423
+ x = self.global_pool(x).squeeze(-1)
424
+ x = self.classifier(x)
425
+
426
+ return x
427
+
428
+
429
+ class EEGInceptionNet(nn.Module):
430
+ """
431
+ Inception-style network for EEG IED detection.
432
+
433
+ Uses parallel convolutions with different kernel sizes to capture
434
+ multi-scale temporal patterns characteristic of IEDs.
435
+ """
436
+
437
+ def __init__(self, num_channels=NUM_CHANNELS, num_classes=2, input_length=2000):
438
+ super().__init__()
439
+
440
+ self.num_channels = num_channels
441
+ self.input_length = input_length
442
+
443
+ # Initial convolution
444
+ self.conv1 = nn.Sequential(
445
+ nn.Conv1d(num_channels, 32, kernel_size=7, stride=2, padding=3, bias=False),
446
+ nn.BatchNorm1d(32),
447
+ nn.ReLU(inplace=True),
448
+ nn.MaxPool1d(kernel_size=3, stride=2, padding=1)
449
+ )
450
+
451
+ # Inception blocks
452
+ self.inception1 = InceptionBlock(32, 64)
453
+ self.inception2 = InceptionBlock(64, 128)
454
+ self.inception3 = InceptionBlock(128, 256)
455
+
456
+ # Pooling between inception blocks
457
+ self.pool = nn.MaxPool1d(kernel_size=2, stride=2)
458
+
459
+ # Channel attention
460
+ self.channel_attn = SqueezeExcitation1D(256)
461
+
462
+ # Global pooling and classifier
463
+ self.global_pool = nn.AdaptiveAvgPool1d(1)
464
+ self.dropout = nn.Dropout(0.5)
465
+ self.fc = nn.Linear(256, num_classes)
466
+
467
+ def forward(self, x):
468
+ x = self.conv1(x)
469
+
470
+ x = self.inception1(x)
471
+ x = self.pool(x)
472
+
473
+ x = self.inception2(x)
474
+ x = self.pool(x)
475
+
476
+ x = self.inception3(x)
477
+ x = self.channel_attn(x)
478
+
479
+ x = self.global_pool(x).squeeze(-1)
480
+ x = self.dropout(x)
481
+ x = self.fc(x)
482
+
483
+ return x
484
+
485
+
486
+ class EEGResNetTransformer(nn.Module):
487
+ """Hybrid model: multi-scale ResNet backbone + Transformer encoder.
488
+
489
+ Rationale:
490
+ - CNN/ResNet extracts local, frequency-like patterns efficiently.
491
+ - Transformer models longer-range temporal dependencies after downsampling.
492
+ - Outputs raw logits suitable for nn.CrossEntropyLoss.
493
+ """
494
+
495
+ def __init__(
496
+ self,
497
+ num_channels=NUM_CHANNELS,
498
+ num_classes=2,
499
+ input_length=2000,
500
+ d_model=512,
501
+ nhead=8,
502
+ num_layers=4,
503
+ dim_feedforward=1024,
504
+ dropout=0.2,
505
+ ):
506
+ super().__init__()
507
+
508
+ self.num_channels = num_channels
509
+ self.input_length = input_length
510
+
511
+ # ResNet-ish backbone (same stem idea as EEGResNetAttention)
512
+ self.stem = MultiScaleConv(num_channels, 64)
513
+ self.stem_pool = nn.MaxPool1d(kernel_size=2, stride=2)
514
+
515
+ self.layer1 = self._make_layer(64, 64, num_blocks=2, stride=1)
516
+ self.layer2 = self._make_layer(64, 128, num_blocks=2, stride=2)
517
+ self.layer3 = self._make_layer(128, 256, num_blocks=2, stride=2)
518
+ self.layer4 = self._make_layer(256, d_model, num_blocks=2, stride=2)
519
+
520
+ # Transformer encoder over time (after downsampling)
521
+ encoder_layer = nn.TransformerEncoderLayer(
522
+ d_model=d_model,
523
+ nhead=nhead,
524
+ dim_feedforward=dim_feedforward,
525
+ dropout=dropout,
526
+ activation='gelu',
527
+ batch_first=True,
528
+ norm_first=True,
529
+ )
530
+ self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
531
+
532
+ # Learnable positional embeddings (sequence length depends on input length)
533
+ # Downsampling: stem_pool(/2) + layer2(/2) + layer3(/2) + layer4(/2) => /16 total
534
+ self.seq_len = max(1, input_length // 16)
535
+ self.pos_embed = nn.Parameter(torch.zeros(1, self.seq_len, d_model))
536
+
537
+ self.pre_head_norm = nn.LayerNorm(d_model)
538
+ self.dropout = nn.Dropout(dropout)
539
+ self.head = nn.Linear(d_model, num_classes)
540
+
541
+ self._init_weights()
542
+
543
+ def _make_layer(self, in_channels, out_channels, num_blocks, stride):
544
+ downsample = None
545
+ if stride != 1 or in_channels != out_channels:
546
+ downsample = nn.Sequential(
547
+ nn.Conv1d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
548
+ nn.BatchNorm1d(out_channels),
549
+ )
550
+
551
+ layers = [ResidualBlock1D(in_channels, out_channels, stride=stride, downsample=downsample)]
552
+ for _ in range(1, num_blocks):
553
+ layers.append(ResidualBlock1D(out_channels, out_channels))
554
+ return nn.Sequential(*layers)
555
+
556
+ def _init_weights(self):
557
+ for m in self.modules():
558
+ if isinstance(m, nn.Conv1d):
559
+ nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
560
+ elif isinstance(m, nn.BatchNorm1d):
561
+ nn.init.constant_(m.weight, 1)
562
+ nn.init.constant_(m.bias, 0)
563
+ elif isinstance(m, nn.Linear):
564
+ nn.init.trunc_normal_(m.weight, std=0.02)
565
+ if m.bias is not None:
566
+ nn.init.constant_(m.bias, 0)
567
+
568
+ nn.init.trunc_normal_(self.pos_embed, std=0.02)
569
+
570
+ def forward(self, x):
571
+ # x: [B, C, T]
572
+ x = self.stem(x)
573
+ x = self.stem_pool(x)
574
+ x = self.layer1(x)
575
+ x = self.layer2(x)
576
+ x = self.layer3(x)
577
+ x = self.layer4(x) # [B, d_model, T']
578
+
579
+ # [B, d_model, T'] -> [B, T', d_model]
580
+ x = x.permute(0, 2, 1)
581
+
582
+ # Handle dynamic T' (e.g. if input_length differs)
583
+ if x.size(1) != self.pos_embed.size(1):
584
+ # Simple interpolate positional embeddings along time
585
+ pos = self.pos_embed.transpose(1, 2) # [1, d_model, seq]
586
+ pos = F.interpolate(pos, size=x.size(1), mode='linear', align_corners=False)
587
+ pos = pos.transpose(1, 2)
588
+ else:
589
+ pos = self.pos_embed
590
+
591
+ x = x + pos
592
+ x = self.transformer(x)
593
+
594
+ # Mean pool over time
595
+ x = x.mean(dim=1)
596
+ x = self.pre_head_norm(x)
597
+ x = self.dropout(x)
598
+ return self.head(x)
599
+
600
+
601
+ class InceptionBlock(nn.Module):
602
+ """Inception block with multiple parallel convolutions."""
603
+
604
+ def __init__(self, in_channels, out_channels):
605
+ super().__init__()
606
+ branch_channels = out_channels // 4
607
+
608
+ # 1x1 conv
609
+ self.branch1 = nn.Sequential(
610
+ nn.Conv1d(in_channels, branch_channels, kernel_size=1, bias=False),
611
+ nn.BatchNorm1d(branch_channels),
612
+ nn.ReLU(inplace=True)
613
+ )
614
+
615
+ # 1x1 -> 3x3 conv
616
+ self.branch2 = nn.Sequential(
617
+ nn.Conv1d(in_channels, branch_channels, kernel_size=1, bias=False),
618
+ nn.BatchNorm1d(branch_channels),
619
+ nn.ReLU(inplace=True),
620
+ nn.Conv1d(branch_channels, branch_channels, kernel_size=3, padding=1, bias=False),
621
+ nn.BatchNorm1d(branch_channels),
622
+ nn.ReLU(inplace=True)
623
+ )
624
+
625
+ # 1x1 -> 5x5 conv
626
+ self.branch3 = nn.Sequential(
627
+ nn.Conv1d(in_channels, branch_channels, kernel_size=1, bias=False),
628
+ nn.BatchNorm1d(branch_channels),
629
+ nn.ReLU(inplace=True),
630
+ nn.Conv1d(branch_channels, branch_channels, kernel_size=5, padding=2, bias=False),
631
+ nn.BatchNorm1d(branch_channels),
632
+ nn.ReLU(inplace=True)
633
+ )
634
+
635
+ # MaxPool -> 1x1 conv
636
+ self.branch4 = nn.Sequential(
637
+ nn.MaxPool1d(kernel_size=3, stride=1, padding=1),
638
+ nn.Conv1d(in_channels, branch_channels, kernel_size=1, bias=False),
639
+ nn.BatchNorm1d(branch_channels),
640
+ nn.ReLU(inplace=True)
641
+ )
642
+
643
+ def forward(self, x):
644
+ return torch.cat([
645
+ self.branch1(x),
646
+ self.branch2(x),
647
+ self.branch3(x),
648
+ self.branch4(x)
649
+ ], dim=1)
650
+
651
+
652
+ # Model factory function
653
+ def get_model(model_name='resnet_attention', num_channels=NUM_CHANNELS, num_classes=2, input_length=2000):
654
+ """
655
+ Factory function to get model by name.
656
+
657
+ Args:
658
+ model_name: One of 'simple_cnn', 'resnet_attention', 'transformer', 'inception', 'resnet_transformer'
659
+ num_channels: Number of input channels
660
+ num_classes: Number of output classes
661
+ input_length: Input sequence length
662
+
663
+ Returns:
664
+ Model instance
665
+ """
666
+ models = {
667
+ 'simple_cnn': Simple1DCNN,
668
+ 'resnet_attention': EEGResNetAttention,
669
+ 'transformer': EEGTransformer,
670
+ 'inception': EEGInceptionNet,
671
+ 'resnet_transformer': EEGResNetTransformer,
672
+ }
673
+
674
+ if model_name not in models:
675
+ raise ValueError(f"Unknown model: {model_name}. Choose from {list(models.keys())}")
676
+
677
+ return models[model_name](num_channels=num_channels, num_classes=num_classes, input_length=input_length)
eegannotationtool/senua_resnet_attention/preprocessing.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Exact four-second preprocessing used by the released SenuaLab models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+
7
+ import numpy as np
8
+ from scipy.signal import butter, resample, resample_poly, sosfiltfilt
9
+
10
+
11
+ NUM_CHANNELS = 19
12
+ INTERNATIONAL_10_20_CHANNELS = [
13
+ "Fp1", "Fp2", "F3", "F4", "C3", "C4", "P3", "P4", "O1", "O2",
14
+ "F7", "F8", "T3", "T4", "T5", "T6", "Fz", "Cz", "Pz",
15
+ ]
16
+ DATASET_29_CHANNEL_ORDER = INTERNATIONAL_10_20_CHANNELS
17
+
18
+
19
+ def normalize_sample(sample):
20
+ x = np.asarray(sample, dtype=np.float32)
21
+ mean = x.mean()
22
+ std = x.std()
23
+ return np.clip((x - mean) / max(float(std), 1e-7), -8.0, 8.0).astype(np.float32)
24
+
25
+
26
+ def preprocess_batch(
27
+ batch,
28
+ *,
29
+ source_sfreq=None,
30
+ target_sfreq=250,
31
+ target_samples=1000,
32
+ channel_names=None,
33
+ ):
34
+ """Filter, average-reference, resample, and globally normalize `[B,C,T]`."""
35
+
36
+ del channel_names # IED Finder has already arranged the declared channel order.
37
+ x = np.asarray(batch, dtype=np.float32)
38
+ if x.ndim == 2:
39
+ x = x[None]
40
+ if x.ndim != 3 or x.shape[1] != NUM_CHANNELS:
41
+ raise ValueError(f"Expected [batch,19,time], received {x.shape}")
42
+ sfreq = float(source_sfreq or target_sfreq)
43
+ if sfreq <= 90.0:
44
+ raise ValueError(f"Sampling rate must be above the 45 Hz cutoff; received {sfreq}")
45
+ sos = butter(4, [1.0, 45.0], btype="bandpass", fs=sfreq, output="sos")
46
+ x = sosfiltfilt(sos, x, axis=-1).astype(np.float32, copy=False)
47
+ x -= x.mean(axis=1, keepdims=True)
48
+ if not math.isclose(sfreq, float(target_sfreq)):
49
+ up = int(target_sfreq)
50
+ down = int(round(sfreq))
51
+ common = math.gcd(up, down)
52
+ x = resample_poly(x, up // common, down // common, axis=-1).astype(np.float32)
53
+ if x.shape[-1] != int(target_samples):
54
+ x = resample(x, int(target_samples), axis=-1).astype(np.float32)
55
+ mean = x.mean(axis=(1, 2), keepdims=True)
56
+ std = x.std(axis=(1, 2), keepdims=True)
57
+ return np.clip((x - mean) / np.maximum(std, 1e-7), -8.0, 8.0).astype(np.float32)
58
+
ensembles/ensemble.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "models": [
3
+ "iednet_lite",
4
+ "resnet_attention"
5
+ ],
6
+ "test_metrics": {
7
+ "accuracy": 0.93014329580348,
8
+ "auprc": 0.7211157678062519,
9
+ "auroc": 0.8995878534153646,
10
+ "balanced_accuracy": 0.8135185936874053,
11
+ "f1": 0.6784452296819788,
12
+ "false_positives_per_minute": 0.5483592400690847,
13
+ "fn": 146,
14
+ "fp": 127,
15
+ "n": 3908,
16
+ "precision": 0.6939759036144578,
17
+ "prevalence": 0.11105424769703173,
18
+ "sensitivity": 0.663594470046083,
19
+ "specificity": 0.9634427173287277,
20
+ "threshold": 0.51321724,
21
+ "tn": 3347,
22
+ "tp": 288
23
+ },
24
+ "threshold": 0.51321724
25
+ }
ensembles/ensemble_4model.json ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "aggregation": "equal_probability_mean",
3
+ "models": [
4
+ "iednet_lite",
5
+ "resnet_attention",
6
+ "eegpt",
7
+ "eegdino_medium"
8
+ ],
9
+ "selection_score_validation_auprc_plus_025_f1": 0.8335915544686525,
10
+ "test_metrics": {
11
+ "accuracy": 0.9483111566018424,
12
+ "auprc": 0.7888269120699914,
13
+ "auroc": 0.9200864088462283,
14
+ "balanced_accuracy": 0.8600306689058151,
15
+ "f1": 0.7623529411764706,
16
+ "false_positives_per_minute": 0.39723661485319517,
17
+ "fn": 110,
18
+ "fp": 92,
19
+ "n": 3908,
20
+ "precision": 0.7788461538461539,
21
+ "prevalence": 0.11105424769703173,
22
+ "sensitivity": 0.7465437788018433,
23
+ "specificity": 0.973517559009787,
24
+ "threshold": 0.5273836475,
25
+ "tn": 3382,
26
+ "tp": 324
27
+ },
28
+ "threshold": 0.5273836475,
29
+ "validation_metrics": {
30
+ "accuracy": 0.9512570548999487,
31
+ "auprc": 0.6725428653300758,
32
+ "auroc": 0.8953611234795638,
33
+ "balanced_accuracy": 0.7657309048759975,
34
+ "f1": 0.6441947565543071,
35
+ "false_positives_per_minute": 0.19262981574539362,
36
+ "fn": 144,
37
+ "fp": 46,
38
+ "n": 3898,
39
+ "precision": 0.7889908256880734,
40
+ "prevalence": 0.08106721395587481,
41
+ "sensitivity": 0.5443037974683544,
42
+ "specificity": 0.9871580122836404,
43
+ "threshold": 0.5273836475,
44
+ "tn": 3536,
45
+ "tp": 172
46
+ },
47
+ "weights": [
48
+ 0.25,
49
+ 0.25,
50
+ 0.25,
51
+ 0.25
52
+ ]
53
+ }
ensembles/ensemble_eegdino.json ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "aggregation": "equal_probability_mean",
3
+ "models": [
4
+ "iednet_lite",
5
+ "resnet_attention",
6
+ "eegdino_medium"
7
+ ],
8
+ "selection_score_validation_auprc_plus_025_f1": 0.8454267628396437,
9
+ "test_metrics": {
10
+ "accuracy": 0.9437052200614124,
11
+ "auprc": 0.7895565635533457,
12
+ "auroc": 0.9261677928734589,
13
+ "balanced_accuracy": 0.8463503736778014,
14
+ "f1": 0.7399527186761229,
15
+ "false_positives_per_minute": 0.4274611398963731,
16
+ "fn": 121,
17
+ "fp": 99,
18
+ "n": 3908,
19
+ "precision": 0.7597087378640777,
20
+ "prevalence": 0.11105424769703173,
21
+ "sensitivity": 0.7211981566820277,
22
+ "specificity": 0.9715025906735751,
23
+ "threshold": 0.5274549233333333,
24
+ "tn": 3375,
25
+ "tp": 313
26
+ },
27
+ "threshold": 0.5274549233333333,
28
+ "validation_metrics": {
29
+ "accuracy": 0.9510005130836326,
30
+ "auprc": 0.6860339734658296,
31
+ "auroc": 0.9016363462884041,
32
+ "balanced_accuracy": 0.7598205514209585,
33
+ "f1": 0.6375711574952562,
34
+ "false_positives_per_minute": 0.18006700167504186,
35
+ "fn": 148,
36
+ "fp": 43,
37
+ "n": 3898,
38
+ "precision": 0.7962085308056872,
39
+ "prevalence": 0.08106721395587481,
40
+ "sensitivity": 0.5316455696202531,
41
+ "specificity": 0.9879955332216639,
42
+ "threshold": 0.5274549233333333,
43
+ "tn": 3539,
44
+ "tp": 168
45
+ },
46
+ "weights": [
47
+ 0.3333333333333333,
48
+ 0.3333333333333333,
49
+ 0.3333333333333333
50
+ ]
51
+ }
ensembles/ensemble_eegpt.json ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "aggregation": "equal_probability_mean",
3
+ "models": [
4
+ "iednet_lite",
5
+ "resnet_attention",
6
+ "eegpt"
7
+ ],
8
+ "test_metrics": {
9
+ "accuracy": 0.936028659160696,
10
+ "auprc": 0.7451331821970812,
11
+ "auroc": 0.9004447787249058,
12
+ "balanced_accuracy": 0.8248940781950977,
13
+ "f1": 0.7030878859857482,
14
+ "false_positives_per_minute": 0.4835924006908463,
15
+ "fn": 138,
16
+ "fp": 112,
17
+ "n": 3908,
18
+ "precision": 0.7254901960784313,
19
+ "prevalence": 0.11105424769703173,
20
+ "sensitivity": 0.6820276497695853,
21
+ "specificity": 0.9677605066206102,
22
+ "threshold": 0.5290762755,
23
+ "tn": 3362,
24
+ "tp": 296
25
+ },
26
+ "threshold": 0.5290762755,
27
+ "validation_metrics": {
28
+ "accuracy": 0.9499743458183684,
29
+ "auprc": 0.6261332028552472,
30
+ "auroc": 0.8396483118829026,
31
+ "balanced_accuracy": 0.7578195124709342,
32
+ "f1": 0.6313799621928167,
33
+ "false_positives_per_minute": 0.19262981574539362,
34
+ "fn": 149,
35
+ "fp": 46,
36
+ "n": 3898,
37
+ "precision": 0.784037558685446,
38
+ "prevalence": 0.08106721395587481,
39
+ "sensitivity": 0.5284810126582279,
40
+ "specificity": 0.9871580122836404,
41
+ "threshold": 0.5290762755,
42
+ "tn": 3536,
43
+ "tp": 167
44
+ },
45
+ "weights": [
46
+ 0.3333333333333333,
47
+ 0.3333333333333333,
48
+ 0.3333333333333333
49
+ ]
50
+ }
models/eegdino_medium/config.json ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "binary_labels": [
3
+ "Non-IED",
4
+ "IED"
5
+ ],
6
+ "channels": [
7
+ "Fp1",
8
+ "Fp2",
9
+ "F3",
10
+ "F4",
11
+ "C3",
12
+ "C4",
13
+ "P3",
14
+ "P4",
15
+ "O1",
16
+ "O2",
17
+ "F7",
18
+ "F8",
19
+ "T3",
20
+ "T4",
21
+ "T5",
22
+ "T6",
23
+ "Fz",
24
+ "Cz",
25
+ "Pz"
26
+ ],
27
+ "eegdino_channel_names": [
28
+ "Fp1",
29
+ "Fp2",
30
+ "F3",
31
+ "F4",
32
+ "C3",
33
+ "C4",
34
+ "P3",
35
+ "P4",
36
+ "O1",
37
+ "O2",
38
+ "F7",
39
+ "F8",
40
+ "T3",
41
+ "T4",
42
+ "T5",
43
+ "T6",
44
+ "Fz",
45
+ "Cz",
46
+ "Pz"
47
+ ],
48
+ "epoch": 11,
49
+ "format_version": 2,
50
+ "model_name": "eegdino_medium",
51
+ "parameters": 34445250,
52
+ "preprocessing": {
53
+ "bandpass_hz": [
54
+ 1.0,
55
+ 45.0
56
+ ],
57
+ "normalization": "global_window_zscore_clip8",
58
+ "reference": "average"
59
+ },
60
+ "pretrained": {
61
+ "intentionally_not_loaded": [
62
+ "final_layer.* (new downstream binary IED head)"
63
+ ],
64
+ "loaded_tensors": 227,
65
+ "loaded_values": 33755456,
66
+ "missing_after_load": [
67
+ "final_layer.token_proj.0.weight",
68
+ "final_layer.token_proj.0.bias",
69
+ "final_layer.time_proj.0.weight",
70
+ "final_layer.time_proj.0.bias",
71
+ "final_layer.classifier.0.weight",
72
+ "final_layer.classifier.0.bias",
73
+ "final_layer.classifier.3.weight",
74
+ "final_layer.classifier.3.bias",
75
+ "final_layer.classifier.6.weight",
76
+ "final_layer.classifier.6.bias"
77
+ ],
78
+ "repo_id": "braindecode/eegdino-medium-pretrained",
79
+ "requested_revision": "191f73eb50d68a184b4cabb623938df914235d3c",
80
+ "resolved_revision": "191f73eb50d68a184b4cabb623938df914235d3c",
81
+ "unexpected_after_load": [],
82
+ "weights_file": "model.safetensors",
83
+ "weights_sha256": "17a73d056afde262d1be0cc21434ca751855691726a0500b3f3c809e2eecdf9e"
84
+ },
85
+ "release_name": "eegdino_medium",
86
+ "sfreq": 200,
87
+ "split_seed": 2026,
88
+ "subtype_labels": [],
89
+ "test_metrics": {
90
+ "accuracy": 0.9398669396110543,
91
+ "auprc": 0.8019567240287226,
92
+ "auroc": 0.9362860777493904,
93
+ "balanced_accuracy": 0.8683870171836076,
94
+ "f1": 0.7414741474147415,
95
+ "false_positives_per_minute": 0.5958549222797928,
96
+ "fn": 97,
97
+ "fp": 138,
98
+ "n": 3908,
99
+ "precision": 0.7094736842105264,
100
+ "prevalence": 0.11105424769703173,
101
+ "sensitivity": 0.7764976958525346,
102
+ "specificity": 0.9602763385146805,
103
+ "threshold": 0.619140625,
104
+ "tn": 3336,
105
+ "tp": 337
106
+ },
107
+ "training_strategy": "partial_last_2",
108
+ "validation_metrics": {
109
+ "accuracy": 0.9461262185736276,
110
+ "auprc": 0.6916968240811774,
111
+ "auroc": 0.8983352946165426,
112
+ "balanced_accuracy": 0.7903503099180855,
113
+ "f1": 0.6452702702702703,
114
+ "false_positives_per_minute": 0.35594639865996647,
115
+ "fn": 125,
116
+ "fp": 85,
117
+ "n": 3898,
118
+ "precision": 0.6920289855072463,
119
+ "prevalence": 0.08106721395587481,
120
+ "sensitivity": 0.6044303797468354,
121
+ "specificity": 0.9762702400893356,
122
+ "threshold": 0.619140625,
123
+ "tn": 3497,
124
+ "tp": 191
125
+ },
126
+ "validation_threshold_f1": 0.619140625,
127
+ "validation_threshold_sensitivity80": 0.056121826171875,
128
+ "window_samples": 800,
129
+ "window_seconds": 4.0
130
+ }
models/eegdino_medium/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:72320fedeed09b66508afaa79ce0ca114273673f6ce36adccedce87c8ce0ea1d
3
+ size 137804848
models/eegpt/config.json ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "binary_labels": [
3
+ "Non-IED",
4
+ "IED"
5
+ ],
6
+ "channels": [
7
+ "Fp1",
8
+ "Fp2",
9
+ "F3",
10
+ "F4",
11
+ "C3",
12
+ "C4",
13
+ "P3",
14
+ "P4",
15
+ "O1",
16
+ "O2",
17
+ "F7",
18
+ "F8",
19
+ "T3",
20
+ "T4",
21
+ "T5",
22
+ "T6",
23
+ "Fz",
24
+ "Cz",
25
+ "Pz"
26
+ ],
27
+ "eegpt_channel_names": [
28
+ "Fp1",
29
+ "Fp2",
30
+ "F3",
31
+ "F4",
32
+ "C3",
33
+ "C4",
34
+ "P3",
35
+ "P4",
36
+ "O1",
37
+ "O2",
38
+ "F7",
39
+ "F8",
40
+ "T7",
41
+ "T8",
42
+ "P7",
43
+ "P8",
44
+ "Fz",
45
+ "Cz",
46
+ "Pz"
47
+ ],
48
+ "epoch": 28,
49
+ "format_version": 2,
50
+ "model_name": "eegpt",
51
+ "parameters": 25589507,
52
+ "preprocessing": {
53
+ "bandpass_hz": [
54
+ 1.0,
55
+ 45.0
56
+ ],
57
+ "normalization": "global_window_zscore_clip8",
58
+ "reference": "average"
59
+ },
60
+ "pretrained": {
61
+ "intentionally_not_loaded": [
62
+ "chans_id (downstream 19-channel identity buffer)",
63
+ "final_layer.* (new binary IED classification head)"
64
+ ],
65
+ "loaded_tensors": 102,
66
+ "loaded_values": 25287168,
67
+ "missing_after_load": [
68
+ "chans_id"
69
+ ],
70
+ "repo_id": "braindecode/eegpt-pretrained",
71
+ "resolved_revision": "e41cb3ae2ce4fd9eb736862292c91f8128d15618",
72
+ "unexpected_after_load": [],
73
+ "weights_file": "model.safetensors",
74
+ "weights_sha256": "fb34c20609983324679b9534f9d17a2289a232d0276dd2b8b7d5b088f876f621"
75
+ },
76
+ "release_name": "eegpt",
77
+ "sfreq": 250,
78
+ "split_seed": 2026,
79
+ "subtype_labels": [],
80
+ "test_metrics": {
81
+ "accuracy": 0.9201637666325486,
82
+ "auprc": 0.65324809994628,
83
+ "auroc": 0.8640698911466086,
84
+ "balanced_accuracy": 0.767579570688379,
85
+ "f1": 0.6138613861386139,
86
+ "false_positives_per_minute": 0.5440414507772021,
87
+ "fn": 186,
88
+ "fp": 126,
89
+ "n": 3908,
90
+ "precision": 0.6631016042780749,
91
+ "prevalence": 0.11105424769703173,
92
+ "sensitivity": 0.5714285714285714,
93
+ "specificity": 0.9637305699481865,
94
+ "threshold": 0.8916015625,
95
+ "tn": 3348,
96
+ "tp": 248
97
+ },
98
+ "trainable_probe_parameters": 302339,
99
+ "training_strategy": "pretrained_encoder_frozen_ied_temporal_head",
100
+ "validation_metrics": {
101
+ "accuracy": 0.9420215495125706,
102
+ "auprc": 0.5628674715868012,
103
+ "auroc": 0.8119094063849486,
104
+ "balanced_accuracy": 0.7289665627716642,
105
+ "f1": 0.5703422053231939,
106
+ "false_positives_per_minute": 0.25125628140703515,
107
+ "fn": 166,
108
+ "fp": 60,
109
+ "n": 3898,
110
+ "precision": 0.7142857142857143,
111
+ "prevalence": 0.08106721395587481,
112
+ "sensitivity": 0.47468354430379744,
113
+ "specificity": 0.983249581239531,
114
+ "threshold": 0.8916015625,
115
+ "tn": 3522,
116
+ "tp": 150
117
+ },
118
+ "validation_threshold_f1": 0.8916015625,
119
+ "validation_threshold_sensitivity80": 0.007965087890625,
120
+ "window_samples": 1000,
121
+ "window_seconds": 4.0
122
+ }
models/eegpt/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e91757b80c98dd7325ea1068bff9dfe72bac577195f5b25b2570ce4d9f357226
3
+ size 102373540
models/eegpt_linear_probe/config.json ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "binary_labels": [
3
+ "Non-IED",
4
+ "IED"
5
+ ],
6
+ "channels": [
7
+ "Fp1",
8
+ "Fp2",
9
+ "F3",
10
+ "F4",
11
+ "C3",
12
+ "C4",
13
+ "P3",
14
+ "P4",
15
+ "O1",
16
+ "O2",
17
+ "F7",
18
+ "F8",
19
+ "T3",
20
+ "T4",
21
+ "T5",
22
+ "T6",
23
+ "Fz",
24
+ "Cz",
25
+ "Pz"
26
+ ],
27
+ "eegpt_channel_names": [
28
+ "Fp1",
29
+ "Fp2",
30
+ "F3",
31
+ "F4",
32
+ "C3",
33
+ "C4",
34
+ "P3",
35
+ "P4",
36
+ "O1",
37
+ "O2",
38
+ "F7",
39
+ "F8",
40
+ "T7",
41
+ "T8",
42
+ "P7",
43
+ "P8",
44
+ "Fz",
45
+ "Cz",
46
+ "Pz"
47
+ ],
48
+ "epoch": 19,
49
+ "format_version": 2,
50
+ "model_name": "eegpt_linear",
51
+ "parameters": 25320946,
52
+ "preprocessing": {
53
+ "bandpass_hz": [
54
+ 1.0,
55
+ 45.0
56
+ ],
57
+ "normalization": "global_window_zscore_clip8",
58
+ "reference": "average"
59
+ },
60
+ "pretrained": {
61
+ "intentionally_not_loaded": [
62
+ "chans_id (downstream 19-channel identity buffer)",
63
+ "final_layer.* (new binary IED classification head)"
64
+ ],
65
+ "loaded_tensors": 102,
66
+ "loaded_values": 25287168,
67
+ "missing_after_load": [
68
+ "chans_id"
69
+ ],
70
+ "repo_id": "braindecode/eegpt-pretrained",
71
+ "resolved_revision": "e41cb3ae2ce4fd9eb736862292c91f8128d15618",
72
+ "unexpected_after_load": [],
73
+ "weights_file": "model.safetensors",
74
+ "weights_sha256": "fb34c20609983324679b9534f9d17a2289a232d0276dd2b8b7d5b088f876f621"
75
+ },
76
+ "release_name": "eegpt_linear_probe",
77
+ "sfreq": 250,
78
+ "split_seed": 2026,
79
+ "subtype_labels": [],
80
+ "test_metrics": {
81
+ "accuracy": 0.7131525076765609,
82
+ "auprc": 0.3722106087915067,
83
+ "auroc": 0.7519221126525154,
84
+ "balanced_accuracy": 0.6702979871540794,
85
+ "f1": 0.32265861027190335,
86
+ "false_positives_per_minute": 4.119170984455959,
87
+ "fn": 167,
88
+ "fp": 954,
89
+ "n": 3908,
90
+ "precision": 0.21867321867321868,
91
+ "prevalence": 0.11105424769703173,
92
+ "sensitivity": 0.6152073732718893,
93
+ "specificity": 0.7253886010362695,
94
+ "threshold": 0.40234375,
95
+ "tn": 2520,
96
+ "tp": 267
97
+ },
98
+ "trainable_probe_parameters": 33778,
99
+ "training_strategy": "pretrained_encoder_frozen_linear_probe",
100
+ "validation_metrics": {
101
+ "accuracy": 0.8496664956387892,
102
+ "auprc": 0.23998456130581225,
103
+ "auroc": 0.7217963940659697,
104
+ "balanced_accuracy": 0.6715018482002135,
105
+ "f1": 0.3310502283105023,
106
+ "false_positives_per_minute": 1.7378559463986598,
107
+ "fn": 171,
108
+ "fp": 415,
109
+ "n": 3898,
110
+ "precision": 0.25892857142857145,
111
+ "prevalence": 0.08106721395587481,
112
+ "sensitivity": 0.4588607594936709,
113
+ "specificity": 0.884142936906756,
114
+ "threshold": 0.40234375,
115
+ "tn": 3167,
116
+ "tp": 145
117
+ },
118
+ "validation_threshold_f1": 0.40234375,
119
+ "validation_threshold_sensitivity80": 0.049224853515625,
120
+ "window_samples": 1000,
121
+ "window_seconds": 4.0
122
+ }
models/eegpt_linear_probe/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:24ec011a90388079df98322244b96e076eeebf6b0671fe987a7d0d5cf66f9579
3
+ size 101295352
models/iednet_lite/config.json ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "binary_labels": [
3
+ "Non-IED",
4
+ "IED"
5
+ ],
6
+ "channels": [
7
+ "Fp1",
8
+ "Fp2",
9
+ "F3",
10
+ "F4",
11
+ "C3",
12
+ "C4",
13
+ "P3",
14
+ "P4",
15
+ "O1",
16
+ "O2",
17
+ "F7",
18
+ "F8",
19
+ "T3",
20
+ "T4",
21
+ "T5",
22
+ "T6",
23
+ "Fz",
24
+ "Cz",
25
+ "Pz"
26
+ ],
27
+ "epoch": 13,
28
+ "format_version": 1,
29
+ "model_name": "iednet_lite",
30
+ "parameters": 245260,
31
+ "preprocessing": {
32
+ "bandpass_hz": [
33
+ 1.0,
34
+ 45.0
35
+ ],
36
+ "normalization": "global_window_zscore_clip8",
37
+ "reference": "average"
38
+ },
39
+ "release_name": "iednet_lite",
40
+ "sfreq": 250,
41
+ "split_seed": 2026,
42
+ "subtype_labels": [
43
+ "Centro-Parietal-IED",
44
+ "Frontal-IED",
45
+ "Generalized-IED",
46
+ "Occipital-IED",
47
+ "Temporal-IED"
48
+ ],
49
+ "test_metrics": {
50
+ "accuracy": 0.9201637666325486,
51
+ "auprc": 0.6847588452968619,
52
+ "auroc": 0.8793068455863041,
53
+ "balanced_accuracy": 0.7937914036861053,
54
+ "f1": 0.6372093023255814,
55
+ "false_positives_per_minute": 0.6563039723661486,
56
+ "fn": 160,
57
+ "fp": 152,
58
+ "n": 3908,
59
+ "precision": 0.6431924882629108,
60
+ "prevalence": 0.11105424769703173,
61
+ "sensitivity": 0.631336405529954,
62
+ "specificity": 0.9562464018422567,
63
+ "threshold": 0.6004377007484436,
64
+ "tn": 3322,
65
+ "tp": 274
66
+ },
67
+ "validation_metrics": {
68
+ "accuracy": 0.9502308876346844,
69
+ "auprc": 0.6136477369244443,
70
+ "auroc": 0.8519717080479754,
71
+ "balanced_accuracy": 0.752188332661903,
72
+ "f1": 0.6269230769230769,
73
+ "false_positives_per_minute": 0.17169179229480735,
74
+ "fn": 153,
75
+ "fp": 41,
76
+ "n": 3898,
77
+ "precision": 0.7990196078431373,
78
+ "prevalence": 0.08106721395587481,
79
+ "sensitivity": 0.5158227848101266,
80
+ "specificity": 0.9885538805136795,
81
+ "threshold": 0.6004377007484436,
82
+ "tn": 3541,
83
+ "tp": 163
84
+ },
85
+ "validation_threshold_f1": 0.6004377007484436,
86
+ "validation_threshold_sensitivity80": 0.017428677529096603,
87
+ "window_samples": 1000,
88
+ "window_seconds": 4.0
89
+ }
models/iednet_lite/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7c5dc3c8e40050b69a07a7cf6a1ce7bbd2b12347f1b50d7feaf1fdfdd108b082
3
+ size 1014056
models/resnet_attention/config.json ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "binary_labels": [
3
+ "Non-IED",
4
+ "IED"
5
+ ],
6
+ "channels": [
7
+ "Fp1",
8
+ "Fp2",
9
+ "F3",
10
+ "F4",
11
+ "C3",
12
+ "C4",
13
+ "P3",
14
+ "P4",
15
+ "O1",
16
+ "O2",
17
+ "F7",
18
+ "F8",
19
+ "T3",
20
+ "T4",
21
+ "T5",
22
+ "T6",
23
+ "Fz",
24
+ "Cz",
25
+ "Pz"
26
+ ],
27
+ "epoch": 11,
28
+ "format_version": 1,
29
+ "model_name": "resnet_attention",
30
+ "parameters": 4989538,
31
+ "preprocessing": {
32
+ "bandpass_hz": [
33
+ 1.0,
34
+ 45.0
35
+ ],
36
+ "normalization": "global_window_zscore_clip8",
37
+ "reference": "average"
38
+ },
39
+ "release_name": "resnet_attention",
40
+ "sfreq": 250,
41
+ "split_seed": 2026,
42
+ "subtype_labels": [
43
+ "Centro-Parietal-IED",
44
+ "Frontal-IED",
45
+ "Generalized-IED",
46
+ "Occipital-IED",
47
+ "Temporal-IED"
48
+ ],
49
+ "test_metrics": {
50
+ "accuracy": 0.932446264073695,
51
+ "auprc": 0.7018171290865336,
52
+ "auroc": 0.8998458595650639,
53
+ "balanced_accuracy": 0.7906183923232226,
54
+ "f1": 0.6666666666666666,
55
+ "false_positives_per_minute": 0.4058721934369603,
56
+ "fn": 170,
57
+ "fp": 94,
58
+ "n": 3908,
59
+ "precision": 0.7374301675977654,
60
+ "prevalence": 0.11105424769703173,
61
+ "sensitivity": 0.6082949308755761,
62
+ "specificity": 0.9729418537708693,
63
+ "threshold": 0.6944227814674377,
64
+ "tn": 3380,
65
+ "tp": 264
66
+ },
67
+ "validation_metrics": {
68
+ "accuracy": 0.9461262185736276,
69
+ "auprc": 0.5585556572790095,
70
+ "auroc": 0.7953542324845042,
71
+ "balanced_accuracy": 0.741298793545788,
72
+ "f1": 0.5992366412213741,
73
+ "false_positives_per_minute": 0.21356783919597988,
74
+ "fn": 159,
75
+ "fp": 51,
76
+ "n": 3898,
77
+ "precision": 0.7548076923076923,
78
+ "prevalence": 0.08106721395587481,
79
+ "sensitivity": 0.49683544303797467,
80
+ "specificity": 0.9857621440536013,
81
+ "threshold": 0.6944227814674377,
82
+ "tn": 3531,
83
+ "tp": 157
84
+ },
85
+ "validation_threshold_f1": 0.6944227814674377,
86
+ "validation_threshold_sensitivity80": 0.007963554933667183,
87
+ "window_samples": 1000,
88
+ "window_seconds": 4.0
89
+ }
models/resnet_attention/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:971fa0d2b38f0cc76ca479931872ae957123aeb51263de59b11098be1531b1e1
3
+ size 20011088
pytorch/eegdino_medium/config.json ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "binary_labels": [
3
+ "Non-IED",
4
+ "IED"
5
+ ],
6
+ "channels": [
7
+ "Fp1",
8
+ "Fp2",
9
+ "F3",
10
+ "F4",
11
+ "C3",
12
+ "C4",
13
+ "P3",
14
+ "P4",
15
+ "O1",
16
+ "O2",
17
+ "F7",
18
+ "F8",
19
+ "T3",
20
+ "T4",
21
+ "T5",
22
+ "T6",
23
+ "Fz",
24
+ "Cz",
25
+ "Pz"
26
+ ],
27
+ "eegdino_channel_names": [
28
+ "Fp1",
29
+ "Fp2",
30
+ "F3",
31
+ "F4",
32
+ "C3",
33
+ "C4",
34
+ "P3",
35
+ "P4",
36
+ "O1",
37
+ "O2",
38
+ "F7",
39
+ "F8",
40
+ "T3",
41
+ "T4",
42
+ "T5",
43
+ "T6",
44
+ "Fz",
45
+ "Cz",
46
+ "Pz"
47
+ ],
48
+ "epoch": 11,
49
+ "format_version": 2,
50
+ "model_name": "eegdino_medium",
51
+ "parameters": 34445250,
52
+ "preprocessing": {
53
+ "bandpass_hz": [
54
+ 1.0,
55
+ 45.0
56
+ ],
57
+ "normalization": "global_window_zscore_clip8",
58
+ "reference": "average"
59
+ },
60
+ "pretrained": {
61
+ "intentionally_not_loaded": [
62
+ "final_layer.* (new downstream binary IED head)"
63
+ ],
64
+ "loaded_tensors": 227,
65
+ "loaded_values": 33755456,
66
+ "missing_after_load": [
67
+ "final_layer.token_proj.0.weight",
68
+ "final_layer.token_proj.0.bias",
69
+ "final_layer.time_proj.0.weight",
70
+ "final_layer.time_proj.0.bias",
71
+ "final_layer.classifier.0.weight",
72
+ "final_layer.classifier.0.bias",
73
+ "final_layer.classifier.3.weight",
74
+ "final_layer.classifier.3.bias",
75
+ "final_layer.classifier.6.weight",
76
+ "final_layer.classifier.6.bias"
77
+ ],
78
+ "repo_id": "braindecode/eegdino-medium-pretrained",
79
+ "requested_revision": "191f73eb50d68a184b4cabb623938df914235d3c",
80
+ "resolved_revision": "191f73eb50d68a184b4cabb623938df914235d3c",
81
+ "unexpected_after_load": [],
82
+ "weights_file": "model.safetensors",
83
+ "weights_sha256": "17a73d056afde262d1be0cc21434ca751855691726a0500b3f3c809e2eecdf9e"
84
+ },
85
+ "release_name": "eegdino_medium",
86
+ "sfreq": 200,
87
+ "split_seed": 2026,
88
+ "subtype_labels": [],
89
+ "test_metrics": {
90
+ "accuracy": 0.9398669396110543,
91
+ "auprc": 0.8019567240287226,
92
+ "auroc": 0.9362860777493904,
93
+ "balanced_accuracy": 0.8683870171836076,
94
+ "f1": 0.7414741474147415,
95
+ "false_positives_per_minute": 0.5958549222797928,
96
+ "fn": 97,
97
+ "fp": 138,
98
+ "n": 3908,
99
+ "precision": 0.7094736842105264,
100
+ "prevalence": 0.11105424769703173,
101
+ "sensitivity": 0.7764976958525346,
102
+ "specificity": 0.9602763385146805,
103
+ "threshold": 0.619140625,
104
+ "tn": 3336,
105
+ "tp": 337
106
+ },
107
+ "training_strategy": "partial_last_2",
108
+ "validation_metrics": {
109
+ "accuracy": 0.9461262185736276,
110
+ "auprc": 0.6916968240811774,
111
+ "auroc": 0.8983352946165426,
112
+ "balanced_accuracy": 0.7903503099180855,
113
+ "f1": 0.6452702702702703,
114
+ "false_positives_per_minute": 0.35594639865996647,
115
+ "fn": 125,
116
+ "fp": 85,
117
+ "n": 3898,
118
+ "precision": 0.6920289855072463,
119
+ "prevalence": 0.08106721395587481,
120
+ "sensitivity": 0.6044303797468354,
121
+ "specificity": 0.9762702400893356,
122
+ "threshold": 0.619140625,
123
+ "tn": 3497,
124
+ "tp": 191
125
+ },
126
+ "validation_threshold_f1": 0.619140625,
127
+ "validation_threshold_sensitivity80": 0.056121826171875,
128
+ "window_samples": 800,
129
+ "window_seconds": 4.0
130
+ }
pytorch/eegdino_medium/model.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e80ee1f66edcac1059b6e68c7458d8aa171167fbc9d50495daaafdc20e358491
3
+ size 137840831
pytorch/eegpt/config.json ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "binary_labels": [
3
+ "Non-IED",
4
+ "IED"
5
+ ],
6
+ "channels": [
7
+ "Fp1",
8
+ "Fp2",
9
+ "F3",
10
+ "F4",
11
+ "C3",
12
+ "C4",
13
+ "P3",
14
+ "P4",
15
+ "O1",
16
+ "O2",
17
+ "F7",
18
+ "F8",
19
+ "T3",
20
+ "T4",
21
+ "T5",
22
+ "T6",
23
+ "Fz",
24
+ "Cz",
25
+ "Pz"
26
+ ],
27
+ "eegpt_channel_names": [
28
+ "Fp1",
29
+ "Fp2",
30
+ "F3",
31
+ "F4",
32
+ "C3",
33
+ "C4",
34
+ "P3",
35
+ "P4",
36
+ "O1",
37
+ "O2",
38
+ "F7",
39
+ "F8",
40
+ "T7",
41
+ "T8",
42
+ "P7",
43
+ "P8",
44
+ "Fz",
45
+ "Cz",
46
+ "Pz"
47
+ ],
48
+ "epoch": 28,
49
+ "format_version": 2,
50
+ "model_name": "eegpt",
51
+ "parameters": 25589507,
52
+ "preprocessing": {
53
+ "bandpass_hz": [
54
+ 1.0,
55
+ 45.0
56
+ ],
57
+ "normalization": "global_window_zscore_clip8",
58
+ "reference": "average"
59
+ },
60
+ "pretrained": {
61
+ "intentionally_not_loaded": [
62
+ "chans_id (downstream 19-channel identity buffer)",
63
+ "final_layer.* (new binary IED classification head)"
64
+ ],
65
+ "loaded_tensors": 102,
66
+ "loaded_values": 25287168,
67
+ "missing_after_load": [
68
+ "chans_id"
69
+ ],
70
+ "repo_id": "braindecode/eegpt-pretrained",
71
+ "resolved_revision": "e41cb3ae2ce4fd9eb736862292c91f8128d15618",
72
+ "unexpected_after_load": [],
73
+ "weights_file": "model.safetensors",
74
+ "weights_sha256": "fb34c20609983324679b9534f9d17a2289a232d0276dd2b8b7d5b088f876f621"
75
+ },
76
+ "release_name": "eegpt",
77
+ "sfreq": 250,
78
+ "split_seed": 2026,
79
+ "subtype_labels": [],
80
+ "test_metrics": {
81
+ "accuracy": 0.9201637666325486,
82
+ "auprc": 0.65324809994628,
83
+ "auroc": 0.8640698911466086,
84
+ "balanced_accuracy": 0.767579570688379,
85
+ "f1": 0.6138613861386139,
86
+ "false_positives_per_minute": 0.5440414507772021,
87
+ "fn": 186,
88
+ "fp": 126,
89
+ "n": 3908,
90
+ "precision": 0.6631016042780749,
91
+ "prevalence": 0.11105424769703173,
92
+ "sensitivity": 0.5714285714285714,
93
+ "specificity": 0.9637305699481865,
94
+ "threshold": 0.8916015625,
95
+ "tn": 3348,
96
+ "tp": 248
97
+ },
98
+ "trainable_probe_parameters": 302339,
99
+ "training_strategy": "pretrained_encoder_frozen_ied_temporal_head",
100
+ "validation_metrics": {
101
+ "accuracy": 0.9420215495125706,
102
+ "auprc": 0.5628674715868012,
103
+ "auroc": 0.8119094063849486,
104
+ "balanced_accuracy": 0.7289665627716642,
105
+ "f1": 0.5703422053231939,
106
+ "false_positives_per_minute": 0.25125628140703515,
107
+ "fn": 166,
108
+ "fp": 60,
109
+ "n": 3898,
110
+ "precision": 0.7142857142857143,
111
+ "prevalence": 0.08106721395587481,
112
+ "sensitivity": 0.47468354430379744,
113
+ "specificity": 0.983249581239531,
114
+ "threshold": 0.8916015625,
115
+ "tn": 3522,
116
+ "tp": 150
117
+ },
118
+ "validation_threshold_f1": 0.8916015625,
119
+ "validation_threshold_sensitivity80": 0.007965087890625,
120
+ "window_samples": 1000,
121
+ "window_seconds": 4.0
122
+ }
pytorch/eegpt/model.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9a0a55ed656049b4109d0e923ee0d461151a8a793995a24438717efecd2f24d5
3
+ size 102394167
pytorch/eegpt_linear_probe/config.json ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "binary_labels": [
3
+ "Non-IED",
4
+ "IED"
5
+ ],
6
+ "channels": [
7
+ "Fp1",
8
+ "Fp2",
9
+ "F3",
10
+ "F4",
11
+ "C3",
12
+ "C4",
13
+ "P3",
14
+ "P4",
15
+ "O1",
16
+ "O2",
17
+ "F7",
18
+ "F8",
19
+ "T3",
20
+ "T4",
21
+ "T5",
22
+ "T6",
23
+ "Fz",
24
+ "Cz",
25
+ "Pz"
26
+ ],
27
+ "eegpt_channel_names": [
28
+ "Fp1",
29
+ "Fp2",
30
+ "F3",
31
+ "F4",
32
+ "C3",
33
+ "C4",
34
+ "P3",
35
+ "P4",
36
+ "O1",
37
+ "O2",
38
+ "F7",
39
+ "F8",
40
+ "T7",
41
+ "T8",
42
+ "P7",
43
+ "P8",
44
+ "Fz",
45
+ "Cz",
46
+ "Pz"
47
+ ],
48
+ "epoch": 19,
49
+ "format_version": 2,
50
+ "model_name": "eegpt_linear",
51
+ "parameters": 25320946,
52
+ "preprocessing": {
53
+ "bandpass_hz": [
54
+ 1.0,
55
+ 45.0
56
+ ],
57
+ "normalization": "global_window_zscore_clip8",
58
+ "reference": "average"
59
+ },
60
+ "pretrained": {
61
+ "intentionally_not_loaded": [
62
+ "chans_id (downstream 19-channel identity buffer)",
63
+ "final_layer.* (new binary IED classification head)"
64
+ ],
65
+ "loaded_tensors": 102,
66
+ "loaded_values": 25287168,
67
+ "missing_after_load": [
68
+ "chans_id"
69
+ ],
70
+ "repo_id": "braindecode/eegpt-pretrained",
71
+ "resolved_revision": "e41cb3ae2ce4fd9eb736862292c91f8128d15618",
72
+ "unexpected_after_load": [],
73
+ "weights_file": "model.safetensors",
74
+ "weights_sha256": "fb34c20609983324679b9534f9d17a2289a232d0276dd2b8b7d5b088f876f621"
75
+ },
76
+ "release_name": "eegpt_linear_probe",
77
+ "sfreq": 250,
78
+ "split_seed": 2026,
79
+ "subtype_labels": [],
80
+ "test_metrics": {
81
+ "accuracy": 0.7131525076765609,
82
+ "auprc": 0.3722106087915067,
83
+ "auroc": 0.7519221126525154,
84
+ "balanced_accuracy": 0.6702979871540794,
85
+ "f1": 0.32265861027190335,
86
+ "false_positives_per_minute": 4.119170984455959,
87
+ "fn": 167,
88
+ "fp": 954,
89
+ "n": 3908,
90
+ "precision": 0.21867321867321868,
91
+ "prevalence": 0.11105424769703173,
92
+ "sensitivity": 0.6152073732718893,
93
+ "specificity": 0.7253886010362695,
94
+ "threshold": 0.40234375,
95
+ "tn": 2520,
96
+ "tp": 267
97
+ },
98
+ "trainable_probe_parameters": 33778,
99
+ "training_strategy": "pretrained_encoder_frozen_linear_probe",
100
+ "validation_metrics": {
101
+ "accuracy": 0.8496664956387892,
102
+ "auprc": 0.23998456130581225,
103
+ "auroc": 0.7217963940659697,
104
+ "balanced_accuracy": 0.6715018482002135,
105
+ "f1": 0.3310502283105023,
106
+ "false_positives_per_minute": 1.7378559463986598,
107
+ "fn": 171,
108
+ "fp": 415,
109
+ "n": 3898,
110
+ "precision": 0.25892857142857145,
111
+ "prevalence": 0.08106721395587481,
112
+ "sensitivity": 0.4588607594936709,
113
+ "specificity": 0.884142936906756,
114
+ "threshold": 0.40234375,
115
+ "tn": 3167,
116
+ "tp": 145
117
+ },
118
+ "validation_threshold_f1": 0.40234375,
119
+ "validation_threshold_sensitivity80": 0.049224853515625,
120
+ "window_samples": 1000,
121
+ "window_seconds": 4.0
122
+ }
pytorch/eegpt_linear_probe/model.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a51fd32505d51d80677ed6504b94d0b8a6718cb1de21a07cd3253ed1e1a3405a
3
+ size 101312967
pytorch/iednet_lite/config.json ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "binary_labels": [
3
+ "Non-IED",
4
+ "IED"
5
+ ],
6
+ "channels": [
7
+ "Fp1",
8
+ "Fp2",
9
+ "F3",
10
+ "F4",
11
+ "C3",
12
+ "C4",
13
+ "P3",
14
+ "P4",
15
+ "O1",
16
+ "O2",
17
+ "F7",
18
+ "F8",
19
+ "T3",
20
+ "T4",
21
+ "T5",
22
+ "T6",
23
+ "Fz",
24
+ "Cz",
25
+ "Pz"
26
+ ],
27
+ "epoch": 13,
28
+ "format_version": 1,
29
+ "model_name": "iednet_lite",
30
+ "parameters": 245260,
31
+ "preprocessing": {
32
+ "bandpass_hz": [
33
+ 1.0,
34
+ 45.0
35
+ ],
36
+ "normalization": "global_window_zscore_clip8",
37
+ "reference": "average"
38
+ },
39
+ "release_name": "iednet_lite",
40
+ "sfreq": 250,
41
+ "split_seed": 2026,
42
+ "subtype_labels": [
43
+ "Centro-Parietal-IED",
44
+ "Frontal-IED",
45
+ "Generalized-IED",
46
+ "Occipital-IED",
47
+ "Temporal-IED"
48
+ ],
49
+ "test_metrics": {
50
+ "accuracy": 0.9201637666325486,
51
+ "auprc": 0.6847588452968619,
52
+ "auroc": 0.8793068455863041,
53
+ "balanced_accuracy": 0.7937914036861053,
54
+ "f1": 0.6372093023255814,
55
+ "false_positives_per_minute": 0.6563039723661486,
56
+ "fn": 160,
57
+ "fp": 152,
58
+ "n": 3908,
59
+ "precision": 0.6431924882629108,
60
+ "prevalence": 0.11105424769703173,
61
+ "sensitivity": 0.631336405529954,
62
+ "specificity": 0.9562464018422567,
63
+ "threshold": 0.6004377007484436,
64
+ "tn": 3322,
65
+ "tp": 274
66
+ },
67
+ "validation_metrics": {
68
+ "accuracy": 0.9502308876346844,
69
+ "auprc": 0.6136477369244443,
70
+ "auroc": 0.8519717080479754,
71
+ "balanced_accuracy": 0.752188332661903,
72
+ "f1": 0.6269230769230769,
73
+ "false_positives_per_minute": 0.17169179229480735,
74
+ "fn": 153,
75
+ "fp": 41,
76
+ "n": 3898,
77
+ "precision": 0.7990196078431373,
78
+ "prevalence": 0.08106721395587481,
79
+ "sensitivity": 0.5158227848101266,
80
+ "specificity": 0.9885538805136795,
81
+ "threshold": 0.6004377007484436,
82
+ "tn": 3541,
83
+ "tp": 163
84
+ },
85
+ "validation_threshold_f1": 0.6004377007484436,
86
+ "validation_threshold_sensitivity80": 0.017428677529096603,
87
+ "window_samples": 1000,
88
+ "window_seconds": 4.0
89
+ }
pytorch/iednet_lite/model.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:383fe1de4aa8cf01b5f1c0a6734d92214ec0e7f96a6cd18a50fa52c52ccb210c
3
+ size 1041083
pytorch/resnet_attention/config.json ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "binary_labels": [
3
+ "Non-IED",
4
+ "IED"
5
+ ],
6
+ "channels": [
7
+ "Fp1",
8
+ "Fp2",
9
+ "F3",
10
+ "F4",
11
+ "C3",
12
+ "C4",
13
+ "P3",
14
+ "P4",
15
+ "O1",
16
+ "O2",
17
+ "F7",
18
+ "F8",
19
+ "T3",
20
+ "T4",
21
+ "T5",
22
+ "T6",
23
+ "Fz",
24
+ "Cz",
25
+ "Pz"
26
+ ],
27
+ "epoch": 11,
28
+ "format_version": 1,
29
+ "model_name": "resnet_attention",
30
+ "parameters": 4989538,
31
+ "preprocessing": {
32
+ "bandpass_hz": [
33
+ 1.0,
34
+ 45.0
35
+ ],
36
+ "normalization": "global_window_zscore_clip8",
37
+ "reference": "average"
38
+ },
39
+ "release_name": "resnet_attention",
40
+ "sfreq": 250,
41
+ "split_seed": 2026,
42
+ "subtype_labels": [
43
+ "Centro-Parietal-IED",
44
+ "Frontal-IED",
45
+ "Generalized-IED",
46
+ "Occipital-IED",
47
+ "Temporal-IED"
48
+ ],
49
+ "test_metrics": {
50
+ "accuracy": 0.932446264073695,
51
+ "auprc": 0.7018171290865336,
52
+ "auroc": 0.8998458595650639,
53
+ "balanced_accuracy": 0.7906183923232226,
54
+ "f1": 0.6666666666666666,
55
+ "false_positives_per_minute": 0.4058721934369603,
56
+ "fn": 170,
57
+ "fp": 94,
58
+ "n": 3908,
59
+ "precision": 0.7374301675977654,
60
+ "prevalence": 0.11105424769703173,
61
+ "sensitivity": 0.6082949308755761,
62
+ "specificity": 0.9729418537708693,
63
+ "threshold": 0.6944227814674377,
64
+ "tn": 3380,
65
+ "tp": 264
66
+ },
67
+ "validation_metrics": {
68
+ "accuracy": 0.9461262185736276,
69
+ "auprc": 0.5585556572790095,
70
+ "auroc": 0.7953542324845042,
71
+ "balanced_accuracy": 0.741298793545788,
72
+ "f1": 0.5992366412213741,
73
+ "false_positives_per_minute": 0.21356783919597988,
74
+ "fn": 159,
75
+ "fp": 51,
76
+ "n": 3898,
77
+ "precision": 0.7548076923076923,
78
+ "prevalence": 0.08106721395587481,
79
+ "sensitivity": 0.49683544303797467,
80
+ "specificity": 0.9857621440536013,
81
+ "threshold": 0.6944227814674377,
82
+ "tn": 3531,
83
+ "tp": 157
84
+ },
85
+ "validation_threshold_f1": 0.6944227814674377,
86
+ "validation_threshold_sensitivity80": 0.007963554933667183,
87
+ "window_samples": 1000,
88
+ "window_seconds": 4.0
89
+ }
pytorch/resnet_attention/model.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:df088c4fd087b453ef3aca76f54a975a69f015324f8d862d2d4ba5316cb91e1a
3
+ size 20037039
results/benchmark_results.csv ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ model,threshold,n,prevalence,auroc,auprc,accuracy,balanced_accuracy,f1,precision,sensitivity,specificity,false_positives_per_minute,tn,fp,fn,tp
2
+ iednet_lite,0.6004377007484436,3908,0.11105424769703173,0.8793068455863041,0.6847588452968619,0.9201637666325486,0.7937914036861053,0.6372093023255814,0.6431924882629108,0.631336405529954,0.9562464018422567,0.6563039723661486,3322,152,160,274
3
+ resnet_attention,0.6944227814674377,3908,0.11105424769703173,0.8998458595650639,0.7018171290865336,0.932446264073695,0.7906183923232226,0.6666666666666666,0.7374301675977654,0.6082949308755761,0.9729418537708693,0.4058721934369603,3380,94,170,264
4
+ eegpt,0.8916015625,3908,0.11105424769703173,0.8640698911466086,0.65324809994628,0.9201637666325486,0.767579570688379,0.6138613861386139,0.6631016042780749,0.5714285714285714,0.9637305699481865,0.5440414507772021,3348,126,186,248
5
+ eegdino_medium,0.619140625,3908,0.11105424769703173,0.9362860777493904,0.8019567240287226,0.9398669396110543,0.8683870171836076,0.7414741474147415,0.7094736842105264,0.7764976958525346,0.9602763385146805,0.5958549222797928,3336,138,97,337
6
+ ensemble_eegpt,0.5290762755,3908,0.11105424769703173,0.9004447787249058,0.7451331821970812,0.936028659160696,0.8248940781950977,0.7030878859857482,0.7254901960784313,0.6820276497695853,0.9677605066206102,0.4835924006908463,3362,112,138,296
7
+ ensemble_eegdino,0.5274549233333333,3908,0.11105424769703173,0.9261677928734589,0.7895565635533457,0.9437052200614124,0.8463503736778014,0.7399527186761229,0.7597087378640777,0.7211981566820277,0.9715025906735751,0.4274611398963731,3375,99,121,313
8
+ ensemble_4model,0.5273836475,3908,0.11105424769703173,0.9200864088462283,0.7888269120699914,0.9483111566018424,0.8600306689058151,0.7623529411764706,0.7788461538461539,0.7465437788018433,0.973517559009787,0.39723661485319517,3382,92,110,324
results/model_comparison.png ADDED