Download eegannotationtool/senua_resnet_attention/model.py from SenuaLab/EEG-IED-Detection: direct link, hf CLI and curl.
- Browser
- Download file 24.4 kB
-
https://huggingface.co/SenuaLab/EEG-IED-Detection/resolve/main/eegannotationtool/senua_resnet_attention/model.py
- Command line
-
hf download hf://SenuaLab/EEG-IED-Detection/eegannotationtool/senua_resnet_attention/model.py
-
curl -L -o model.py https://huggingface.co/SenuaLab/EEG-IED-Detection/resolve/main/eegannotationtool/senua_resnet_attention/model.py
24.4 kB
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| import math | |
| # Import preprocessing constants | |
| 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( | |
| # Block 1 | |
| 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), | |
| # Block 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), | |
| # Block 3 | |
| 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), | |
| # Block 4 | |
| 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), | |
| ) | |
| # Calculate size after convolutions | |
| # Input: 2000 | |
| # Pool 1: 1000 | |
| # Pool 2: 500 | |
| # Pool 3: 250 | |
| # Pool 4: 125 | |
| self.flatten_size = 256 * (input_length // 16) # 16 = 2^4 (4 pooling layers) | |
| 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 | |
| # ============================================================================ | |
| # Advanced Model Architectures for Better IED Detection | |
| # ============================================================================ | |
| 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: [B, C, T] -> [B, T, C] | |
| x = x.permute(0, 2, 1) | |
| B, T, C = x.shape | |
| # Residual connection | |
| residual = x | |
| x = self.norm(x) | |
| # Multi-head attention | |
| qkv = self.qkv(x).reshape(B, T, 3, self.num_heads, self.head_dim) | |
| qkv = qkv.permute(2, 0, 3, 1, 4) # [3, B, heads, T, head_dim] | |
| 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 | |
| # Back to [B, C, T] | |
| 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 | |
| # Multi-scale initial convolution | |
| self.stem = MultiScaleConv(num_channels, 64) | |
| self.stem_pool = nn.MaxPool1d(kernel_size=2, stride=2) | |
| # Residual blocks | |
| 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) | |
| # Temporal attention | |
| self.temporal_attn = TemporalAttention(512, num_heads=8) | |
| # Global pooling and classifier | |
| self.global_pool = nn.AdaptiveAvgPool1d(1) | |
| self.dropout = nn.Dropout(0.5) | |
| self.fc = nn.Linear(512, num_classes) | |
| # Initialize weights | |
| 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): | |
| # Multi-scale stem | |
| x = self.stem(x) | |
| x = self.stem_pool(x) | |
| # Residual blocks | |
| x = self.layer1(x) | |
| x = self.layer2(x) | |
| x = self.layer3(x) | |
| x = self.layer4(x) | |
| # Temporal attention | |
| x = self.temporal_attn(x) | |
| # Global pooling and classification | |
| 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 | |
| # CNN backbone for initial feature extraction | |
| 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), | |
| ) | |
| # Calculate sequence length after CNN | |
| # 2000 -> 1000 -> 500 -> 250 -> 125 | |
| self.seq_len = input_length // 16 | |
| # Positional encoding | |
| self.pos_encoding = self._create_positional_encoding(self.seq_len, d_model) | |
| # Transformer encoder | |
| 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) | |
| # Classification head | |
| 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): | |
| # CNN backbone: [B, C, T] -> [B, d_model, T'] | |
| x = self.conv_backbone(x) | |
| # Reshape for transformer: [B, d_model, T'] -> [B, T', d_model] | |
| x = x.permute(0, 2, 1) | |
| # Add positional encoding | |
| x = x + self.pos_encoding[:, :x.size(1), :] | |
| # Transformer encoder | |
| x = self.transformer(x) | |
| # Back to [B, d_model, T'] for pooling | |
| x = x.permute(0, 2, 1) | |
| # Global pooling and classification | |
| 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 | |
| # Initial convolution | |
| 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) | |
| ) | |
| # Inception blocks | |
| self.inception1 = InceptionBlock(32, 64) | |
| self.inception2 = InceptionBlock(64, 128) | |
| self.inception3 = InceptionBlock(128, 256) | |
| # Pooling between inception blocks | |
| self.pool = nn.MaxPool1d(kernel_size=2, stride=2) | |
| # Channel attention | |
| self.channel_attn = SqueezeExcitation1D(256) | |
| # Global pooling and classifier | |
| 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 | |
| # ResNet-ish backbone (same stem idea as EEGResNetAttention) | |
| 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) | |
| # Transformer encoder over time (after downsampling) | |
| 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) | |
| # Learnable positional embeddings (sequence length depends on input length) | |
| # Downsampling: stem_pool(/2) + layer2(/2) + layer3(/2) + layer4(/2) => /16 total | |
| 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: [B, C, T] | |
| 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) # [B, d_model, T'] | |
| # [B, d_model, T'] -> [B, T', d_model] | |
| x = x.permute(0, 2, 1) | |
| # Handle dynamic T' (e.g. if input_length differs) | |
| if x.size(1) != self.pos_embed.size(1): | |
| # Simple interpolate positional embeddings along time | |
| pos = self.pos_embed.transpose(1, 2) # [1, d_model, seq] | |
| 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) | |
| # Mean pool over time | |
| 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 | |
| # 1x1 conv | |
| self.branch1 = nn.Sequential( | |
| nn.Conv1d(in_channels, branch_channels, kernel_size=1, bias=False), | |
| nn.BatchNorm1d(branch_channels), | |
| nn.ReLU(inplace=True) | |
| ) | |
| # 1x1 -> 3x3 conv | |
| 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) | |
| ) | |
| # 1x1 -> 5x5 conv | |
| 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) | |
| ) | |
| # MaxPool -> 1x1 conv | |
| 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) | |
| # Model factory function | |
| 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) | |