MorganBrizon commited on
Commit
148cade
·
verified ·
1 Parent(s): ef08a91

Upload 4 files

Browse files
Files changed (4) hide show
  1. EpilepsyNet.pth +3 -0
  2. EpilepsyNet_model.py +278 -0
  3. app.py +69 -0
  4. eegnet_model.py +50 -0
EpilepsyNet.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f39a4c7a0ce846a6d57977917288297090c0a73a77acd5fcbfab2ab460bbf6de
3
+ size 3617604
EpilepsyNet_model.py ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import torch
3
+ import torch.nn as nn
4
+ import torch.optim as optim
5
+
6
+
7
+
8
+ def extract_upper_triangle(corr_matrices):
9
+ """
10
+ Extract upper triangles from correlation matrices
11
+
12
+ Args:
13
+ corr_matrices: numpy array of shape (n_segments, n_channels, n_channels)
14
+
15
+ Returns:
16
+ numpy array of shape (n_segments, n_features) where n_features = n_channels*(n_channels-1)/2
17
+ """
18
+ n_segments, n_channels, _ = corr_matrices.shape
19
+ n_features = n_channels * (n_channels - 1) // 2
20
+
21
+ flattened = np.zeros((n_segments, n_features))
22
+
23
+ for i in range(n_segments):
24
+ # Get upper triangle indices (excluding diagonal)
25
+ upper_indices = np.triu_indices(n_channels, k=1)
26
+ # Extract values
27
+ flattened[i] = corr_matrices[i][upper_indices]
28
+
29
+ return flattened
30
+
31
+ class MultiHeadAttention(nn.Module):
32
+ def __init__(self, embed_dim, num_heads, dropout=0.3):
33
+ super(MultiHeadAttention, self).__init__()
34
+ self.embed_dim = embed_dim
35
+ self.num_heads = num_heads
36
+ self.head_dim = embed_dim // num_heads
37
+ assert self.head_dim * num_heads == embed_dim, "embed_dim must be divisible by num_heads"
38
+
39
+ # Linear projections for Q, K, V
40
+ self.q_proj = nn.Linear(embed_dim, embed_dim)
41
+ self.k_proj = nn.Linear(embed_dim, embed_dim)
42
+ self.v_proj = nn.Linear(embed_dim, embed_dim)
43
+
44
+ # Final projection after concatenating heads
45
+ self.out_proj = nn.Linear(embed_dim, embed_dim)
46
+
47
+ # Dropout
48
+ self.dropout = nn.Dropout(dropout)
49
+
50
+ # Softmax for attention weights
51
+ self.softmax = nn.Softmax(dim=-1)
52
+
53
+ def forward(self, x, mask=None):
54
+ batch_size = x.size(0)
55
+
56
+ # Project Q, K, V
57
+ Q = self.q_proj(x) # (batch_size, seq_len, embed_dim)
58
+ K = self.k_proj(x) # (batch_size, seq_len, embed_dim)
59
+ V = self.v_proj(x) # (batch_size, seq_len, embed_dim)
60
+
61
+ # Split into multiple heads
62
+ Q = Q.view(batch_size, -1, self.num_heads, self.head_dim).transpose(1, 2) # (batch_size, num_heads, seq_len, head_dim)
63
+ K = K.view(batch_size, -1, self.num_heads, self.head_dim).transpose(1, 2) # (batch_size, num_heads, seq_len, head_dim)
64
+ V = V.view(batch_size, -1, self.num_heads, self.head_dim).transpose(1, 2) # (batch_size, num_heads, seq_len, head_dim)
65
+
66
+ # Calculate attention scores
67
+ scores = torch.matmul(Q, K.transpose(-2, -1)) / (self.head_dim ** 0.5) # (batch_size, num_heads, seq_len, seq_len)
68
+
69
+ # Apply mask (if provided)
70
+ if mask is not None:
71
+ scores = scores.masked_fill(mask == 0, float('-inf'))
72
+
73
+ # Apply softmax to get attention weights
74
+ attn_weights = self.softmax(scores) # (batch_size, num_heads, seq_len, seq_len)
75
+ attn_weights = self.dropout(attn_weights)
76
+
77
+ # Calculate weighted output
78
+ attn_output = torch.matmul(attn_weights, V) # (batch_size, num_heads, seq_len, head_dim)
79
+
80
+ # Recompose heads
81
+ attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, -1, self.embed_dim) # (batch_size, seq_len, embed_dim)
82
+
83
+ # Pass through final projection
84
+ output = self.out_proj(attn_output) # (batch_size, seq_len, embed_dim)
85
+
86
+ return output, attn_weights
87
+
88
+ class PositionalEncoding(nn.Module):
89
+ def __init__(self, embed_dim, max_seq_length=100):
90
+ super(PositionalEncoding, self).__init__()
91
+
92
+ # Create positional encoding matrix
93
+ pe = torch.zeros(max_seq_length, embed_dim)
94
+ position = torch.arange(0, max_seq_length, dtype=torch.float).unsqueeze(1)
95
+ div_term = torch.exp(torch.arange(0, embed_dim, 2).float() * (-np.log(10000.0) / embed_dim))
96
+
97
+ pe[:, 0::2] = torch.sin(position * div_term)
98
+ pe[:, 1::2] = torch.cos(position * div_term)
99
+
100
+ # Register as buffer (not a parameter)
101
+ self.register_buffer('pe', pe.unsqueeze(0))
102
+
103
+ def forward(self, x):
104
+ # Add positional encoding to input
105
+ # x: [batch_size, seq_len, embed_dim]
106
+ return x + self.pe[:, :x.size(1)]
107
+
108
+ class TimeSeriesAttentionClassifier(nn.Module):
109
+ def __init__(self, input_dim, embed_dim, num_heads, num_classes=2, dropout=0.2):
110
+ super(TimeSeriesAttentionClassifier, self).__init__()
111
+
112
+ # Project flattened correlation features to embedding space
113
+ self.embedding = nn.Linear(input_dim, embed_dim)
114
+
115
+ # Positional encoding
116
+ self.pos_encoding = PositionalEncoding(embed_dim)
117
+
118
+ # Multi-head attention
119
+ self.attention = MultiHeadAttention(embed_dim, num_heads, dropout)
120
+
121
+ # Layer normalization
122
+ self.layer_norm1 = nn.LayerNorm(embed_dim)
123
+ self.layer_norm2 = nn.LayerNorm(embed_dim)
124
+
125
+ # Feed-forward network
126
+ self.ffn = nn.Sequential(
127
+ nn.Linear(embed_dim, embed_dim * 4),
128
+ nn.GELU(),
129
+ nn.Dropout(dropout),
130
+ nn.Linear(embed_dim * 4, embed_dim)
131
+ )
132
+
133
+ # Output layer
134
+ self.classifier = nn.Sequential(
135
+ nn.Linear(embed_dim, embed_dim // 2),
136
+ nn.GELU(),
137
+ nn.Dropout(dropout),
138
+ nn.Linear(embed_dim // 2, 1),
139
+ nn.Sigmoid()
140
+ )
141
+
142
+ def forward(self, x):
143
+ # batch_size, seq_len, input_dim = x.shape
144
+
145
+ # Project to embedding space
146
+ x = self.embedding(x)
147
+
148
+ # Add positional encoding
149
+ x = self.pos_encoding(x)
150
+
151
+ # Self-attention (use x for query, key, and value)
152
+ residual = x
153
+ x, attention_weights = self.attention(x)
154
+ x = self.layer_norm1(x + residual)
155
+
156
+ # Feed-forward network with residual connection
157
+ residual = x
158
+ x = self.ffn(x)
159
+ x = self.layer_norm2(x + residual)
160
+
161
+ # Global average pooling over sequence dimension
162
+ x = torch.mean(x, dim=1)
163
+
164
+ # Classification
165
+ logits = self.classifier(x)
166
+
167
+ return logits, attention_weights
168
+
169
+ def train_model(model, train_loader, val_loader, num_epochs=50, learning_rate=1e-4, weight_decay=1e-5, patience=10, scheduler_factor=0.5, min_lr=1e-6):
170
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
171
+ model = model.to(device)
172
+
173
+ # Changed from CrossEntropyLoss to BCELoss for binary classification with sigmoid
174
+ criterion = nn.BCELoss()
175
+ # optimizer = optim.Adam(model.parameters(), lr=learning_rate)
176
+
177
+ # Add L2 regularization through weight_decay parameter in Adam
178
+ optimizer = optim.Adam(model.parameters(), lr=learning_rate, weight_decay=weight_decay)
179
+
180
+ # Learning rate scheduler - reduce LR when validation loss plateaus
181
+ scheduler = optim.lr_scheduler.ReduceLROnPlateau(
182
+ optimizer,
183
+ mode='min',
184
+ factor=scheduler_factor,
185
+ patience=patience,
186
+ verbose=True,
187
+ min_lr=min_lr
188
+ )
189
+
190
+ train_losses = []
191
+ val_losses = []
192
+ val_accuracies = []
193
+
194
+
195
+ # Track best model and early stopping
196
+ best_val_loss = float('inf')
197
+ best_model_state = None
198
+ early_stop_counter = 0
199
+ early_stop_patience = patience * 2 # Stop after 2x the scheduler patience
200
+
201
+ for epoch in range(num_epochs):
202
+ # Training
203
+ model.train()
204
+ train_loss = 0.0
205
+ for inputs, labels in train_loader:
206
+ inputs, labels = inputs.to(device), labels.to(device)
207
+
208
+ # Convert labels to float and reshape for BCE loss
209
+ labels = labels.float().view(-1, 1)
210
+
211
+ optimizer.zero_grad()
212
+ outputs, _ = model(inputs)
213
+ loss = criterion(outputs, labels)
214
+ loss.backward()
215
+ optimizer.step()
216
+ train_loss += loss.item()
217
+
218
+ train_loss /= len(train_loader)
219
+ train_losses.append(train_loss)
220
+
221
+ # Validation
222
+ model.eval()
223
+ val_loss = 0.0
224
+ correct = 0
225
+ total = 0
226
+
227
+ with torch.no_grad():
228
+ for inputs, labels in val_loader:
229
+ inputs, labels = inputs.to(device), labels.to(device)
230
+ # Convert labels to float and reshape for BCE loss
231
+ labels = labels.float().view(-1, 1)
232
+
233
+ outputs, _ = model(inputs)
234
+ loss = criterion(outputs, labels)
235
+ val_loss += loss.item()
236
+
237
+ # For binary classification with sigmoid, prediction is 1 if output > 0.5
238
+ predicted = (outputs > 0.5).float()
239
+ total += labels.size(0)
240
+ correct += (predicted == labels).sum().item()
241
+
242
+ val_loss /= len(val_loader)
243
+ val_losses.append(val_loss)
244
+
245
+ accuracy = 100 * correct / total
246
+ val_accuracies.append(accuracy)
247
+
248
+ # Learning rate scheduler step based on validation loss
249
+ scheduler.step(val_loss)
250
+
251
+ # Print current learning rate
252
+ current_lr = optimizer.param_groups[0]['lr']
253
+
254
+
255
+ # Print epoch results
256
+ print(f'Epoch {epoch+1}/{num_epochs}, LR: {current_lr:.6f}, Train Loss: {train_loss:.4f}, '
257
+ f'Val Loss: {val_loss:.4f}, Val Accuracy: {accuracy:.2f}%')
258
+
259
+ # Save best model
260
+ if val_loss < best_val_loss:
261
+ best_val_loss = val_loss
262
+ best_model_state = model.state_dict().copy()
263
+ early_stop_counter = 0
264
+ else:
265
+ early_stop_counter += 1
266
+
267
+ # Early stopping
268
+ if early_stop_counter >= early_stop_patience:
269
+ print(f"Early stopping triggered after {epoch+1} epochs")
270
+ break
271
+
272
+ # Load best model weights
273
+ if best_model_state is not None:
274
+ model.load_state_dict(best_model_state)
275
+ print(f"Loaded best model with validation loss: {best_val_loss:.4f}")
276
+
277
+ return train_losses, val_losses, val_accuracies
278
+
app.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tempfile
3
+ import uvicorn
4
+ from fastapi import FastAPI, UploadFile, File, HTTPException
5
+ from fastapi.responses import JSONResponse
6
+
7
+ # Import your prediction functions
8
+ from prediction import predict_eeg_recording, predict_ensemble_eeg_recording
9
+
10
+ app = FastAPI(title="EEG Epilepsy Prediction API")
11
+
12
+ @app.get("/", tags=["Introduction Endpoints"])
13
+ async def index():
14
+ """
15
+ Simply returns a welcome message!
16
+ """
17
+ message = (
18
+ "Hello world! Welcome to the EEG Epilepsy Prediction API. "
19
+ "Submit an EEG recording EDF file to the `/predict` endpoint to receive a prediction."
20
+ )
21
+ return message
22
+
23
+ @app.post("/predict", tags=["Machine Learning"])
24
+ async def predict_endpoint(
25
+ file: UploadFile = File(...),
26
+ model_choice: str = "2DCNN",
27
+ ensemble_method: str = None
28
+ ):
29
+ """
30
+
31
+ Query parameters:
32
+ - model_choice: Choose one model among "2DCNN", "EEGNet", "EpilepsyNet", or "ensemble".
33
+ - ensemble_method: (Optional, required if model_choice is "ensemble")
34
+ The ensemble method to use ("average" or "voting").
35
+
36
+ """
37
+ print("Saving uploaded file as temporary file...")
38
+ try:
39
+ suffix = os.path.splitext(file.filename)[1]
40
+ with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
41
+ tmp.write(await file.read())
42
+ tmp_path = tmp.name
43
+ except Exception as e:
44
+ raise HTTPException(status_code=500, detail="Error saving temporary file")
45
+
46
+ print("Performing prediction using model_choice =", model_choice)
47
+ try:
48
+ if model_choice.lower() == "ensemble":
49
+ if ensemble_method is None:
50
+ raise HTTPException(status_code=400, detail="ensemble_method must be specified when using ensemble model_choice")
51
+ pred_label, mean_prob = predict_ensemble_eeg_recording(tmp_path, ensemble_method=ensemble_method, threshold=0.5)
52
+ else:
53
+ pred_label, mean_prob = predict_eeg_recording(tmp_path, model_name=model_choice, threshold=0.5)
54
+ except Exception as e:
55
+ os.remove(tmp_path)
56
+ raise HTTPException(status_code=400, detail=f"Prediction failed: {e}")
57
+
58
+ os.remove(tmp_path)
59
+
60
+
61
+ response = {
62
+ "prediction": "epilepsy" if pred_label == 1 else "no epilepsy",
63
+ "confidence": mean_prob
64
+ }
65
+ print("Prediction complete, returning response...")
66
+ return JSONResponse(content=response)
67
+
68
+ if __name__ == "__main__":
69
+ uvicorn.run(app, host="0.0.0.0", port=7860)
eegnet_model.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch.nn as nn
2
+ import torch.nn.functional as F
3
+ import torch
4
+
5
+ class EEGNet(nn.Module):
6
+ def __init__(self, n_channels=21, n_samples=1250, num_classes=2, dropout_rate=0.5):
7
+ super(EEGNet, self).__init__()
8
+
9
+ # Temporal convolution: learn temporal filters across time dimension
10
+ self.firstconv = nn.Sequential(
11
+ nn.Conv2d(1, 8, kernel_size=(1, 64), padding=(0, 32), bias=False), # shape: (B, 8, C, T)
12
+ nn.BatchNorm2d(8)
13
+ )
14
+
15
+ # Depthwise spatial convolution: one spatial filter per temporal filter
16
+ self.depthwiseConv = nn.Sequential(
17
+ nn.Conv2d(8, 16, kernel_size=(n_channels, 1), groups=8, bias=False), # shape: (B, 16, 1, T)
18
+ nn.BatchNorm2d(16),
19
+ nn.ELU(),
20
+ nn.AvgPool2d(kernel_size=(1, 4)),
21
+ nn.Dropout(dropout_rate)
22
+ )
23
+
24
+ # Separable convolution: combines temporal filters again
25
+ self.separableConv = nn.Sequential(
26
+ nn.Conv2d(16, 16, kernel_size=(1, 16), padding=(0, 8), bias=False),
27
+ nn.BatchNorm2d(16),
28
+ nn.ELU(),
29
+ nn.AvgPool2d(kernel_size=(1, 8)),
30
+ nn.Dropout(dropout_rate)
31
+ )
32
+
33
+ # Dynamically compute the flattened feature size after conv layers
34
+ dummy_input = torch.zeros(1, 1, n_channels, n_samples)
35
+ with torch.no_grad():
36
+ x = self.firstconv(dummy_input)
37
+ x = self.depthwiseConv(x)
38
+ x = self.separableConv(x)
39
+ flattened_size = x.reshape(1, -1).shape[1] # dynamically computed
40
+
41
+ # Final classification layer
42
+ self.classifier = nn.Linear(flattened_size, num_classes)
43
+
44
+ def forward(self, x):
45
+ x = self.firstconv(x)
46
+ x = self.depthwiseConv(x)
47
+ x = self.separableConv(x)
48
+ x = x.reshape(x.size(0), -1) # flatten
49
+ x = self.classifier(x)
50
+ return x