| import torch
|
| import torch.nn as nn
|
| import torch.nn.functional as F
|
| import math |
|
|
| |
| from .preprocessing import INTERNATIONAL_10_20_CHANNELS, NUM_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 = "EEGResNetAttention" |
| 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 ResNet-Attention 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.6944227814674377 |
| MODEL_DEFAULT_STEP_MS = 500.0 |
| MODEL_DEFAULT_PAD_POLICY = "skip" |
| MODEL_DEFAULT_BATCH_SIZE = 32 |
| MODEL_DEFAULT_BATCH_MEMORY_MB = 96.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 Simple1DCNN(nn.Module):
|
| """
|
| Simple 1D CNN for EEG IED classification.
|
|
|
| Architecture:
|
| - 4 convolutional blocks with BatchNorm, ReLU, and MaxPool
|
| - Fully connected classifier with dropout
|
|
|
| Default configuration uses 19 channels (standard 10-20) to match
|
| the vEpiSet preprocessing pipeline.
|
| """
|
|
|
| def __init__(self, num_channels=NUM_CHANNELS, num_classes=2, input_length=2000):
|
| """
|
| Args:
|
| num_channels: Number of EEG channels (default 19 for standard 10-20)
|
| num_classes: Number of output classes (default 2: IED vs Non-IED)
|
| input_length: Number of time samples (default 2000 = 4 seconds at 500 Hz)
|
| """
|
| super(Simple1DCNN, self).__init__()
|
|
|
| self.num_channels = num_channels
|
| self.input_length = input_length
|
|
|
| self.features = nn.Sequential(
|
|
|
| nn.Conv1d(in_channels=num_channels, out_channels=32, kernel_size=5, stride=1, padding=2),
|
| nn.BatchNorm1d(32),
|
| nn.ReLU(),
|
| nn.MaxPool1d(kernel_size=2, stride=2),
|
|
|
|
|
| nn.Conv1d(in_channels=32, out_channels=64, kernel_size=5, stride=1, padding=2),
|
| nn.BatchNorm1d(64),
|
| nn.ReLU(),
|
| nn.MaxPool1d(kernel_size=2, stride=2),
|
|
|
|
|
| nn.Conv1d(in_channels=64, out_channels=128, kernel_size=3, stride=1, padding=1),
|
| nn.BatchNorm1d(128),
|
| nn.ReLU(),
|
| nn.MaxPool1d(kernel_size=2, stride=2),
|
|
|
|
|
| nn.Conv1d(in_channels=128, out_channels=256, kernel_size=3, stride=1, padding=1),
|
| nn.BatchNorm1d(256),
|
| nn.ReLU(),
|
| nn.MaxPool1d(kernel_size=2, stride=2),
|
| )
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| self.flatten_size = 256 * (input_length // 16)
|
|
|
| self.classifier = nn.Sequential(
|
| nn.Linear(self.flatten_size, 512),
|
| nn.ReLU(),
|
| nn.Dropout(0.5),
|
| nn.Linear(512, num_classes)
|
| )
|
|
|
| def forward(self, x):
|
| x = self.features(x)
|
| x = x.view(x.size(0), -1)
|
| x = self.classifier(x)
|
| return x
|
|
|
|
|
|
|
|
|
|
|
|
|
| class SqueezeExcitation1D(nn.Module):
|
| """Squeeze-and-Excitation block for channel attention."""
|
|
|
| def __init__(self, channels, reduction=16):
|
| super().__init__()
|
| self.avg_pool = nn.AdaptiveAvgPool1d(1)
|
| self.fc = nn.Sequential(
|
| nn.Linear(channels, channels // reduction, bias=False),
|
| nn.ReLU(inplace=True),
|
| nn.Linear(channels // reduction, channels, bias=False),
|
| nn.Sigmoid()
|
| )
|
|
|
| def forward(self, x):
|
| b, c, _ = x.size()
|
| y = self.avg_pool(x).view(b, c)
|
| y = self.fc(y).view(b, c, 1)
|
| return x * y.expand_as(x)
|
|
|
|
|
| class ResidualBlock1D(nn.Module):
|
| """Residual block with optional squeeze-excitation."""
|
|
|
| def __init__(self, in_channels, out_channels, kernel_size=3, stride=1,
|
| downsample=None, use_se=True):
|
| super().__init__()
|
| padding = kernel_size // 2
|
|
|
| self.conv1 = nn.Conv1d(in_channels, out_channels, kernel_size,
|
| stride=stride, padding=padding, bias=False)
|
| self.bn1 = nn.BatchNorm1d(out_channels)
|
| self.relu = nn.ReLU(inplace=True)
|
| self.conv2 = nn.Conv1d(out_channels, out_channels, kernel_size,
|
| stride=1, padding=padding, bias=False)
|
| self.bn2 = nn.BatchNorm1d(out_channels)
|
|
|
| self.downsample = downsample
|
| self.se = SqueezeExcitation1D(out_channels) if use_se else nn.Identity()
|
|
|
| def forward(self, x):
|
| identity = x
|
|
|
| out = self.conv1(x)
|
| out = self.bn1(out)
|
| out = self.relu(out)
|
|
|
| out = self.conv2(out)
|
| out = self.bn2(out)
|
| out = self.se(out)
|
|
|
| if self.downsample is not None:
|
| identity = self.downsample(x)
|
|
|
| out += identity
|
| out = self.relu(out)
|
| return out
|
|
|
|
|
| class TemporalAttention(nn.Module):
|
| """Self-attention mechanism for temporal patterns."""
|
|
|
| def __init__(self, channels, num_heads=4):
|
| super().__init__()
|
| self.num_heads = num_heads
|
| self.head_dim = channels // num_heads
|
| self.scale = self.head_dim ** -0.5
|
|
|
| self.qkv = nn.Linear(channels, channels * 3, bias=False)
|
| self.proj = nn.Linear(channels, channels)
|
| self.norm = nn.LayerNorm(channels)
|
|
|
| def forward(self, x):
|
|
|
| x = x.permute(0, 2, 1)
|
| B, T, C = x.shape
|
|
|
|
|
| residual = x
|
| x = self.norm(x)
|
|
|
|
|
| qkv = self.qkv(x).reshape(B, T, 3, self.num_heads, self.head_dim)
|
| qkv = qkv.permute(2, 0, 3, 1, 4)
|
| q, k, v = qkv[0], qkv[1], qkv[2]
|
|
|
| attn = (q @ k.transpose(-2, -1)) * self.scale
|
| attn = attn.softmax(dim=-1)
|
|
|
| x = (attn @ v).transpose(1, 2).reshape(B, T, C)
|
| x = self.proj(x)
|
| x = x + residual
|
|
|
|
|
| return x.permute(0, 2, 1)
|
|
|
|
|
| class MultiScaleConv(nn.Module):
|
| """Multi-scale convolution for capturing different frequency patterns."""
|
|
|
| def __init__(self, in_channels, out_channels):
|
| super().__init__()
|
| self.branch1 = nn.Sequential(
|
| nn.Conv1d(in_channels, out_channels // 4, kernel_size=3, padding=1, bias=False),
|
| nn.BatchNorm1d(out_channels // 4),
|
| nn.ReLU(inplace=True)
|
| )
|
| self.branch2 = nn.Sequential(
|
| nn.Conv1d(in_channels, out_channels // 4, kernel_size=5, padding=2, bias=False),
|
| nn.BatchNorm1d(out_channels // 4),
|
| nn.ReLU(inplace=True)
|
| )
|
| self.branch3 = nn.Sequential(
|
| nn.Conv1d(in_channels, out_channels // 4, kernel_size=7, padding=3, bias=False),
|
| nn.BatchNorm1d(out_channels // 4),
|
| nn.ReLU(inplace=True)
|
| )
|
| self.branch4 = nn.Sequential(
|
| nn.Conv1d(in_channels, out_channels // 4, kernel_size=11, padding=5, bias=False),
|
| nn.BatchNorm1d(out_channels // 4),
|
| nn.ReLU(inplace=True)
|
| )
|
|
|
| def forward(self, x):
|
| return torch.cat([
|
| self.branch1(x),
|
| self.branch2(x),
|
| self.branch3(x),
|
| self.branch4(x)
|
| ], dim=1)
|
|
|
|
|
| class EEGResNetAttention(nn.Module):
|
| """
|
| Advanced EEG IED Detector with ResNet backbone and Attention.
|
|
|
| Features:
|
| - Multi-scale initial convolution for capturing different EEG frequencies
|
| - Residual blocks with Squeeze-and-Excitation attention
|
| - Temporal self-attention for capturing long-range dependencies
|
| - Global average pooling for robust feature aggregation
|
| """
|
|
|
| def __init__(self, num_channels=NUM_CHANNELS, num_classes=2, input_length=2000):
|
| super().__init__()
|
|
|
| self.num_channels = num_channels
|
| self.input_length = input_length
|
|
|
|
|
| self.stem = MultiScaleConv(num_channels, 64)
|
| self.stem_pool = nn.MaxPool1d(kernel_size=2, stride=2)
|
|
|
|
|
| self.layer1 = self._make_layer(64, 64, num_blocks=2, stride=1)
|
| self.layer2 = self._make_layer(64, 128, num_blocks=2, stride=2)
|
| self.layer3 = self._make_layer(128, 256, num_blocks=2, stride=2)
|
| self.layer4 = self._make_layer(256, 512, num_blocks=2, stride=2)
|
|
|
|
|
| self.temporal_attn = TemporalAttention(512, num_heads=8)
|
|
|
|
|
| self.global_pool = nn.AdaptiveAvgPool1d(1)
|
| self.dropout = nn.Dropout(0.5)
|
| self.fc = nn.Linear(512, num_classes)
|
|
|
|
|
| self._init_weights()
|
|
|
| def _make_layer(self, in_channels, out_channels, num_blocks, stride):
|
| downsample = None
|
| if stride != 1 or in_channels != out_channels:
|
| downsample = nn.Sequential(
|
| nn.Conv1d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
|
| nn.BatchNorm1d(out_channels)
|
| )
|
|
|
| layers = [ResidualBlock1D(in_channels, out_channels, stride=stride, downsample=downsample)]
|
| for _ in range(1, num_blocks):
|
| layers.append(ResidualBlock1D(out_channels, out_channels))
|
|
|
| return nn.Sequential(*layers)
|
|
|
| def _init_weights(self):
|
| for m in self.modules():
|
| if isinstance(m, nn.Conv1d):
|
| nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
|
| elif isinstance(m, nn.BatchNorm1d):
|
| nn.init.constant_(m.weight, 1)
|
| nn.init.constant_(m.bias, 0)
|
| elif isinstance(m, nn.Linear):
|
| nn.init.normal_(m.weight, 0, 0.01)
|
| if m.bias is not None:
|
| nn.init.constant_(m.bias, 0)
|
|
|
| def forward(self, x):
|
|
|
| x = self.stem(x)
|
| x = self.stem_pool(x)
|
|
|
|
|
| x = self.layer1(x)
|
| x = self.layer2(x)
|
| x = self.layer3(x)
|
| x = self.layer4(x)
|
|
|
|
|
| x = self.temporal_attn(x)
|
|
|
|
|
| x = self.global_pool(x).squeeze(-1)
|
| x = self.dropout(x)
|
| x = self.fc(x)
|
|
|
| return x
|
|
|
|
|
| class EEGTransformer(nn.Module):
|
| """
|
| Transformer-based EEG IED Detector.
|
|
|
| Uses a CNN backbone for initial feature extraction followed by
|
| Transformer encoder layers for capturing complex temporal patterns.
|
| """
|
|
|
| def __init__(self, num_channels=NUM_CHANNELS, num_classes=2, input_length=2000,
|
| d_model=256, nhead=8, num_layers=4, dim_feedforward=512):
|
| super().__init__()
|
|
|
| self.num_channels = num_channels
|
| self.input_length = input_length
|
|
|
|
|
| self.conv_backbone = nn.Sequential(
|
| nn.Conv1d(num_channels, 64, kernel_size=7, stride=2, padding=3, bias=False),
|
| nn.BatchNorm1d(64),
|
| nn.ReLU(inplace=True),
|
| nn.MaxPool1d(kernel_size=3, stride=2, padding=1),
|
|
|
| nn.Conv1d(64, 128, kernel_size=5, stride=2, padding=2, bias=False),
|
| nn.BatchNorm1d(128),
|
| nn.ReLU(inplace=True),
|
|
|
| nn.Conv1d(128, d_model, kernel_size=3, stride=2, padding=1, bias=False),
|
| nn.BatchNorm1d(d_model),
|
| nn.ReLU(inplace=True),
|
| )
|
|
|
|
|
|
|
| self.seq_len = input_length // 16
|
|
|
|
|
| self.pos_encoding = self._create_positional_encoding(self.seq_len, d_model)
|
|
|
|
|
| encoder_layer = nn.TransformerEncoderLayer(
|
| d_model=d_model,
|
| nhead=nhead,
|
| dim_feedforward=dim_feedforward,
|
| dropout=0.1,
|
| activation='gelu',
|
| batch_first=True
|
| )
|
| self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
|
|
|
|
|
| self.global_pool = nn.AdaptiveAvgPool1d(1)
|
| self.classifier = nn.Sequential(
|
| nn.Linear(d_model, d_model // 2),
|
| nn.GELU(),
|
| nn.Dropout(0.3),
|
| nn.Linear(d_model // 2, num_classes)
|
| )
|
|
|
| def _create_positional_encoding(self, seq_len, d_model):
|
| pe = torch.zeros(seq_len, d_model)
|
| position = torch.arange(0, seq_len, dtype=torch.float).unsqueeze(1)
|
| div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
|
| pe[:, 0::2] = torch.sin(position * div_term)
|
| pe[:, 1::2] = torch.cos(position * div_term)
|
| return nn.Parameter(pe.unsqueeze(0), requires_grad=False)
|
|
|
| def forward(self, x):
|
|
|
| x = self.conv_backbone(x)
|
|
|
|
|
| x = x.permute(0, 2, 1)
|
|
|
|
|
| x = x + self.pos_encoding[:, :x.size(1), :]
|
|
|
|
|
| x = self.transformer(x)
|
|
|
|
|
| x = x.permute(0, 2, 1)
|
|
|
|
|
| x = self.global_pool(x).squeeze(-1)
|
| x = self.classifier(x)
|
|
|
| return x
|
|
|
|
|
| class EEGInceptionNet(nn.Module):
|
| """
|
| Inception-style network for EEG IED detection.
|
|
|
| Uses parallel convolutions with different kernel sizes to capture
|
| multi-scale temporal patterns characteristic of IEDs.
|
| """
|
|
|
| def __init__(self, num_channels=NUM_CHANNELS, num_classes=2, input_length=2000):
|
| super().__init__()
|
|
|
| self.num_channels = num_channels
|
| self.input_length = input_length
|
|
|
|
|
| self.conv1 = nn.Sequential(
|
| nn.Conv1d(num_channels, 32, kernel_size=7, stride=2, padding=3, bias=False),
|
| nn.BatchNorm1d(32),
|
| nn.ReLU(inplace=True),
|
| nn.MaxPool1d(kernel_size=3, stride=2, padding=1)
|
| )
|
|
|
|
|
| self.inception1 = InceptionBlock(32, 64)
|
| self.inception2 = InceptionBlock(64, 128)
|
| self.inception3 = InceptionBlock(128, 256)
|
|
|
|
|
| self.pool = nn.MaxPool1d(kernel_size=2, stride=2)
|
|
|
|
|
| self.channel_attn = SqueezeExcitation1D(256)
|
|
|
|
|
| self.global_pool = nn.AdaptiveAvgPool1d(1)
|
| self.dropout = nn.Dropout(0.5)
|
| self.fc = nn.Linear(256, num_classes)
|
|
|
| def forward(self, x):
|
| x = self.conv1(x)
|
|
|
| x = self.inception1(x)
|
| x = self.pool(x)
|
|
|
| x = self.inception2(x)
|
| x = self.pool(x)
|
|
|
| x = self.inception3(x)
|
| x = self.channel_attn(x)
|
|
|
| x = self.global_pool(x).squeeze(-1)
|
| x = self.dropout(x)
|
| x = self.fc(x)
|
|
|
| return x
|
|
|
|
|
| class EEGResNetTransformer(nn.Module):
|
| """Hybrid model: multi-scale ResNet backbone + Transformer encoder.
|
|
|
| Rationale:
|
| - CNN/ResNet extracts local, frequency-like patterns efficiently.
|
| - Transformer models longer-range temporal dependencies after downsampling.
|
| - Outputs raw logits suitable for nn.CrossEntropyLoss.
|
| """
|
|
|
| def __init__(
|
| self,
|
| num_channels=NUM_CHANNELS,
|
| num_classes=2,
|
| input_length=2000,
|
| d_model=512,
|
| nhead=8,
|
| num_layers=4,
|
| dim_feedforward=1024,
|
| dropout=0.2,
|
| ):
|
| super().__init__()
|
|
|
| self.num_channels = num_channels
|
| self.input_length = input_length
|
|
|
|
|
| self.stem = MultiScaleConv(num_channels, 64)
|
| self.stem_pool = nn.MaxPool1d(kernel_size=2, stride=2)
|
|
|
| self.layer1 = self._make_layer(64, 64, num_blocks=2, stride=1)
|
| self.layer2 = self._make_layer(64, 128, num_blocks=2, stride=2)
|
| self.layer3 = self._make_layer(128, 256, num_blocks=2, stride=2)
|
| self.layer4 = self._make_layer(256, d_model, num_blocks=2, stride=2)
|
|
|
|
|
| encoder_layer = nn.TransformerEncoderLayer(
|
| d_model=d_model,
|
| nhead=nhead,
|
| dim_feedforward=dim_feedforward,
|
| dropout=dropout,
|
| activation='gelu',
|
| batch_first=True,
|
| norm_first=True,
|
| )
|
| self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
|
|
|
|
|
|
|
| self.seq_len = max(1, input_length // 16)
|
| self.pos_embed = nn.Parameter(torch.zeros(1, self.seq_len, d_model))
|
|
|
| self.pre_head_norm = nn.LayerNorm(d_model)
|
| self.dropout = nn.Dropout(dropout)
|
| self.head = nn.Linear(d_model, num_classes)
|
|
|
| self._init_weights()
|
|
|
| def _make_layer(self, in_channels, out_channels, num_blocks, stride):
|
| downsample = None
|
| if stride != 1 or in_channels != out_channels:
|
| downsample = nn.Sequential(
|
| nn.Conv1d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
|
| nn.BatchNorm1d(out_channels),
|
| )
|
|
|
| layers = [ResidualBlock1D(in_channels, out_channels, stride=stride, downsample=downsample)]
|
| for _ in range(1, num_blocks):
|
| layers.append(ResidualBlock1D(out_channels, out_channels))
|
| return nn.Sequential(*layers)
|
|
|
| def _init_weights(self):
|
| for m in self.modules():
|
| if isinstance(m, nn.Conv1d):
|
| nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
|
| elif isinstance(m, nn.BatchNorm1d):
|
| nn.init.constant_(m.weight, 1)
|
| nn.init.constant_(m.bias, 0)
|
| elif isinstance(m, nn.Linear):
|
| nn.init.trunc_normal_(m.weight, std=0.02)
|
| if m.bias is not None:
|
| nn.init.constant_(m.bias, 0)
|
|
|
| nn.init.trunc_normal_(self.pos_embed, std=0.02)
|
|
|
| def forward(self, x):
|
|
|
| x = self.stem(x)
|
| x = self.stem_pool(x)
|
| x = self.layer1(x)
|
| x = self.layer2(x)
|
| x = self.layer3(x)
|
| x = self.layer4(x)
|
|
|
|
|
| x = x.permute(0, 2, 1)
|
|
|
|
|
| if x.size(1) != self.pos_embed.size(1):
|
|
|
| pos = self.pos_embed.transpose(1, 2)
|
| pos = F.interpolate(pos, size=x.size(1), mode='linear', align_corners=False)
|
| pos = pos.transpose(1, 2)
|
| else:
|
| pos = self.pos_embed
|
|
|
| x = x + pos
|
| x = self.transformer(x)
|
|
|
|
|
| x = x.mean(dim=1)
|
| x = self.pre_head_norm(x)
|
| x = self.dropout(x)
|
| return self.head(x)
|
|
|
|
|
| class InceptionBlock(nn.Module):
|
| """Inception block with multiple parallel convolutions."""
|
|
|
| def __init__(self, in_channels, out_channels):
|
| super().__init__()
|
| branch_channels = out_channels // 4
|
|
|
|
|
| self.branch1 = nn.Sequential(
|
| nn.Conv1d(in_channels, branch_channels, kernel_size=1, bias=False),
|
| nn.BatchNorm1d(branch_channels),
|
| nn.ReLU(inplace=True)
|
| )
|
|
|
|
|
| self.branch2 = nn.Sequential(
|
| nn.Conv1d(in_channels, branch_channels, kernel_size=1, bias=False),
|
| nn.BatchNorm1d(branch_channels),
|
| nn.ReLU(inplace=True),
|
| nn.Conv1d(branch_channels, branch_channels, kernel_size=3, padding=1, bias=False),
|
| nn.BatchNorm1d(branch_channels),
|
| nn.ReLU(inplace=True)
|
| )
|
|
|
|
|
| self.branch3 = nn.Sequential(
|
| nn.Conv1d(in_channels, branch_channels, kernel_size=1, bias=False),
|
| nn.BatchNorm1d(branch_channels),
|
| nn.ReLU(inplace=True),
|
| nn.Conv1d(branch_channels, branch_channels, kernel_size=5, padding=2, bias=False),
|
| nn.BatchNorm1d(branch_channels),
|
| nn.ReLU(inplace=True)
|
| )
|
|
|
|
|
| self.branch4 = nn.Sequential(
|
| nn.MaxPool1d(kernel_size=3, stride=1, padding=1),
|
| nn.Conv1d(in_channels, branch_channels, kernel_size=1, bias=False),
|
| nn.BatchNorm1d(branch_channels),
|
| nn.ReLU(inplace=True)
|
| )
|
|
|
| def forward(self, x):
|
| return torch.cat([
|
| self.branch1(x),
|
| self.branch2(x),
|
| self.branch3(x),
|
| self.branch4(x)
|
| ], dim=1)
|
|
|
|
|
|
|
| def get_model(model_name='resnet_attention', num_channels=NUM_CHANNELS, num_classes=2, input_length=2000):
|
| """
|
| Factory function to get model by name.
|
|
|
| Args:
|
| model_name: One of 'simple_cnn', 'resnet_attention', 'transformer', 'inception', 'resnet_transformer'
|
| num_channels: Number of input channels
|
| num_classes: Number of output classes
|
| input_length: Input sequence length
|
|
|
| Returns:
|
| Model instance
|
| """
|
| models = {
|
| 'simple_cnn': Simple1DCNN,
|
| 'resnet_attention': EEGResNetAttention,
|
| 'transformer': EEGTransformer,
|
| 'inception': EEGInceptionNet,
|
| 'resnet_transformer': EEGResNetTransformer,
|
| }
|
|
|
| if model_name not in models:
|
| raise ValueError(f"Unknown model: {model_name}. Choose from {list(models.keys())}")
|
|
|
| return models[model_name](num_channels=num_channels, num_classes=num_classes, input_length=input_length)
|
|
|