aryakoureshi's picture
Release SenuaLab patient-independent EEG IED models v1.0.0
dc9d5f6 verified
Raw
History Blame Contribute Delete
4.39 kB
"""EEG Annotation Tool adapter for SenuaLab IEDNet-Lite."""
from __future__ import annotations
import torch
import torch.nn.functional as F
from torch import nn
from .preprocessing import INTERNATIONAL_10_20_CHANNELS, preprocess_batch as _preprocess
MODEL_CHANNELS = INTERNATIONAL_10_20_CHANNELS
MODEL_INPUT_SAMPLES = 1000
MODEL_SAMPLING_RATE_HZ = 250.0
MODEL_WINDOW_SECONDS = 4.0
MODEL_ENTRY_CLASS = "IEDNetLite"
MODEL_NUM_CLASSES = 2
MODEL_CLASS_LABELS = ["Non-IED", "IED"]
MODEL_NON_IED_CLASS_INDEX = 0
MODEL_IED_CLASS_INDICES = [1]
MODEL_DESCRIPTION = "SenuaLab patient-independent IEDNet-Lite binary IED detector"
MODEL_BATCH_PREPROCESSOR = "preprocess_batch"
MODEL_REQUIRED_REFERENCE = "common average (applied by model adapter)"
MODEL_REQUIRED_FILTERS = ["1-45 Hz zero-phase Butterworth (applied by model adapter)"]
MODEL_REQUIRED_NORMALIZATION = "global four-second window z-score, clipped to [-8,8]"
MODEL_INPUT_UNIT = "scale-invariant after window z-score"
MODEL_SOURCE_SIGNAL_POLICY = "raw"
MODEL_REQUIRES_FULL_WINDOW = True
MODEL_REQUIRES_ALL_CHANNELS = True
MODEL_DEFAULT_THRESHOLD = 0.6004377007484436
MODEL_DEFAULT_STEP_MS = 500.0
MODEL_DEFAULT_PAD_POLICY = "skip"
MODEL_DEFAULT_BATCH_SIZE = 64
MODEL_DEFAULT_BATCH_MEMORY_MB = 64.0
MODEL_VALIDATION_NOTE = "Threshold selected on the vEpiSet validation subjects; not externally validated."
MODEL_PREPROCESSING_NOTE = "The adapter applies the exact released 1-45 Hz, common-average, resampling, and global-window z-score pipeline."
def preprocess_batch(batch, source_sfreq=None, channel_names=None):
return _preprocess(
batch,
source_sfreq=source_sfreq,
target_sfreq=250,
target_samples=1000,
channel_names=channel_names,
)
class SqueezeExcitation(nn.Module):
def __init__(self, channels: int, reduction: int = 8):
super().__init__()
hidden = max(8, channels // reduction)
self.net = nn.Sequential(
nn.AdaptiveAvgPool1d(1),
nn.Conv1d(channels, hidden, 1),
nn.GELU(),
nn.Conv1d(hidden, channels, 1),
nn.Sigmoid(),
)
def forward(self, x):
return x * self.net(x)
class DSResidual(nn.Module):
def __init__(self, in_ch: int, out_ch: int, kernel: int, stride: int = 1, dilation: int = 1):
super().__init__()
pad = dilation * (kernel - 1) // 2
self.body = nn.Sequential(
nn.Conv1d(in_ch, in_ch, kernel, stride=stride, padding=pad, dilation=dilation, groups=in_ch, bias=False),
nn.BatchNorm1d(in_ch),
nn.GELU(),
nn.Conv1d(in_ch, out_ch, 1, bias=False),
nn.BatchNorm1d(out_ch),
SqueezeExcitation(out_ch),
)
self.skip = (
nn.Identity()
if in_ch == out_ch and stride == 1
else nn.Sequential(nn.Conv1d(in_ch, out_ch, 1, stride=stride, bias=False), nn.BatchNorm1d(out_ch))
)
def forward(self, x):
return F.gelu(self.body(x) + self.skip(x))
class IEDNetLite(nn.Module):
def __init__(self, num_channels: int = 19, num_classes: int = 2, input_length: int = 1000, subtypes: int = 5):
super().__init__()
del input_length
branches = []
for kernel in (7, 15, 31):
branches.append(
nn.Sequential(
nn.Conv1d(num_channels, 24, kernel, stride=2, padding=kernel // 2, bias=False),
nn.BatchNorm1d(24),
nn.GELU(),
)
)
self.stem = nn.ModuleList(branches)
self.backbone = nn.Sequential(
DSResidual(72, 72, 9, stride=2),
DSResidual(72, 96, 7, stride=2),
DSResidual(96, 96, 7, dilation=2),
DSResidual(96, 144, 5, stride=2),
DSResidual(144, 144, 5, dilation=2),
DSResidual(144, 192, 3, stride=2),
DSResidual(192, 192, 3, dilation=2),
)
self.dropout = nn.Dropout(0.35)
self.binary_head = nn.Linear(384, num_classes)
self.subtype_head = nn.Linear(384, subtypes)
def forward(self, x):
x = torch.cat([branch(x) for branch in self.stem], dim=1)
x = self.backbone(x)
features = self.dropout(torch.cat([x.mean(-1), x.amax(-1)], dim=1))
return self.binary_head(features), self.subtype_head(features)