MorganBrizon commited on
Commit
546b54d
·
verified ·
1 Parent(s): 5abeca7

Upload 3 files

Browse files
Files changed (3) hide show
  1. model1_2dcnn.h5 +3 -0
  2. prediction.py +261 -0
  3. preprocessing.py +101 -0
model1_2dcnn.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7667171590e5ee5d9a189a22f454b08ec8d5dfcdfd42b9dda4c96ea0f32c07f3
3
+ size 1679000
prediction.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import torch
3
+ import tensorflow as tf
4
+ import pandas as pd
5
+ import joblib
6
+ from sklearn.metrics import confusion_matrix, f1_score, accuracy_score
7
+ from preprocessing import preprocess_eeg_file
8
+ from preprocessing_2dcnn import convert_epoch_to_spectrogram
9
+ from preprocessing_epilepsynet import *
10
+ from EpilepsyNet_model import TimeSeriesAttentionClassifier
11
+ from eegnet_model import EEGNet
12
+
13
+
14
+
15
+ def aggregate_predictions(spectrogram_list, model, threshold=0.5):
16
+
17
+ # Convert each spectrogram to channels-last format.
18
+ X = np.array([np.transpose(s, (1, 2, 0)) for s in spectrogram_list])
19
+ print(f'---Aggregating predictions from {len(spectrogram_list)} segments---')
20
+ preds = model.predict(X)
21
+ mean_prob = np.mean(preds[:, 1])
22
+ final_label = 1 if mean_prob >= threshold else 0
23
+ return final_label, mean_prob
24
+
25
+ def predict_eeg_recording(edf_path, model_name='2DCNN', threshold=0.5):
26
+
27
+
28
+ if model_name == '2DCNN':
29
+
30
+ model = tf.keras.models.load_model('model1_2dcnn.h5')
31
+ channels = ["EEG FP1-REF", "EEG FP2-REF", "EEG F3-REF", "EEG F4-REF", "EEG C3-REF"]
32
+ #Process the edf file
33
+ preprocessed_df = preprocess_eeg_file(edf_path, fmin=1.0, fmax=45.0, segment_lenght=5, overlap=2,desired=channels)
34
+
35
+ if preprocessed_df is None or preprocessed_df.empty:
36
+ raise ValueError("EEG file could not be preprocessed or no valid segments found.")
37
+
38
+ # Convert each 5-second segment (each row) into a spectrogram.
39
+ spectrogram_list = preprocessed_df.apply(
40
+ lambda row: convert_epoch_to_spectrogram(row, channels, fs=250, nperseg=128, noverlap=64), axis=1
41
+ ).tolist()
42
+
43
+ return aggregate_predictions(spectrogram_list, model, threshold)
44
+
45
+ elif model_name == 'EEGNet':
46
+ loaded = joblib.load("eegnet_model.joblib")
47
+ # Extract the actual state dictionary.
48
+ state_dict = loaded["model_state_dict"]
49
+
50
+ model = EEGNet(n_channels=21, n_samples=1250, num_classes=2)
51
+ model.load_state_dict(state_dict)
52
+
53
+ # Define the channels to use for EEGNet
54
+ channels = [
55
+ 'EEG FP1-REF', # Left frontal pole
56
+ 'EEG FP2-REF', # Right frontal pole
57
+ 'EEG F3-REF', # Left frontal
58
+ 'EEG F4-REF', # Right frontal
59
+ 'EEG C3-REF', # Left central
60
+ 'EEG C4-REF', # Right central
61
+ 'EEG P3-REF', # Left parietal
62
+ 'EEG P4-REF', # Right parietal
63
+ 'EEG O1-REF', # Left occipital
64
+ 'EEG O2-REF', # Right occipital
65
+ 'EEG F7-REF', # Left lateral frontal
66
+ 'EEG F8-REF', # Right lateral frontal
67
+ 'EEG T3-REF', # Left temporal (anterior)
68
+ 'EEG T4-REF', # Right temporal (anterior)
69
+ 'EEG T5-REF', # Left temporal (posterior)
70
+ 'EEG T6-REF', # Right temporal (posterior)
71
+ 'EEG FZ-REF', # Frontal midline
72
+ 'EEG CZ-REF', # Central midline
73
+ 'EEG PZ-REF', # Parietal midline
74
+ 'EEG ROC-REF', # Right occipital (often used as reference or an extra site)
75
+ 'EEG LOC-REF' # Left occipital (often used as reference or an extra site)
76
+ ]
77
+ # Process the edf file
78
+ preprocessed_df = preprocess_eeg_file(
79
+ edf_path, fmin=1.0, fmax=45.0, segment_lenght=5, overlap=0, desired=channels
80
+ )
81
+
82
+ if preprocessed_df is None or preprocessed_df.empty:
83
+ raise ValueError("EEG file could not be preprocessed or no valid segments found.")
84
+
85
+ # For EEGNet, we use the raw time series data directly.
86
+ # Convert each 5-second segment (row) to a 2D timeseries array of shape (n_channels, n_samples)
87
+ timeseries_list = preprocessed_df.apply(
88
+ lambda row: convert_epoch_to_timeseries(row, channels), axis=1
89
+ ).tolist()
90
+
91
+ return aggregate_predictions_EEGNET(timeseries_list, model, threshold)
92
+
93
+ elif model_name == 'EpilepsyNet':
94
+
95
+ raw = mne.io.read_raw_edf(edf_path,
96
+ preload=True,
97
+ verbose='ERROR')
98
+
99
+ eeg_cols = ['EEG FP1', 'EEG FP2', 'EEG F3', 'EEG F4',
100
+ 'EEG C3', 'EEG C4', 'EEG P3', 'EEG P4',
101
+ 'EEG O1', 'EEG O2', 'EEG F7', 'EEG F8',
102
+ 'EEG T3', 'EEG T4', 'EEG T5', 'EEG T6',
103
+ 'EEG T1', 'EEG T2', 'EEG FZ', 'EEG CZ',
104
+ 'EEG PZ']
105
+
106
+ parameters = {
107
+ 'eeg_cols':eeg_cols,
108
+ 'segment_duration':60.0, # 60 second segments
109
+ 'n_segments_per_file':12, # Split into 12 epochs (5 sec each)
110
+ 'samples_per_segment':1250, # 1250 samples per segment (250 Hz sampling rate)
111
+ 'random_state':42
112
+ }
113
+
114
+ X = process_raw_files(
115
+ raw_file=raw,
116
+ eeg_cols=eeg_cols,
117
+ segment_duration=parameters['segment_duration'],
118
+ n_segments_per_file=parameters['n_segments_per_file'],
119
+ random_state=parameters['random_state']
120
+ )
121
+
122
+ X_std = standardize_data(X)
123
+ corr_matrix = compute_correlation_matrix(X_std)
124
+ # print('Correlation matrix shape :',corr_matrix.shape)
125
+
126
+ upper_triangle_matrix = extract_upper_triangle(corr_matrix)
127
+ # print('Upper Triangle shape :',upper_triangle_matrix.shape)
128
+
129
+ X_tensor = torch.tensor(upper_triangle_matrix, dtype=torch.float32)
130
+ X_tensor = X_tensor.unsqueeze(0)
131
+
132
+ # Model parameters
133
+ input_dim = 210 # Size of flattened upper triangle (21*20/2)
134
+ embed_dim = 256 # Embedding dimension
135
+ num_heads = 16 # Number of attention heads7
136
+
137
+ model = TimeSeriesAttentionClassifier(input_dim, embed_dim, num_heads)
138
+ model.load_state_dict(torch.load('EpilepsyNet.pth'))
139
+ model.eval()
140
+ print('¨'*50)
141
+ print('Model Prediction :')
142
+
143
+ outputs, _ = model(X_tensor)
144
+ # For binary classification with sigmoid, prediction is 1 if output > 0.5
145
+ predicted = (outputs >= 0.5).float()
146
+
147
+ return int(predicted), outputs.float().squeeze().item()
148
+
149
+
150
+ def convert_epoch_to_timeseries(epoch_row, channels):
151
+
152
+ ts_list = []
153
+ for ch in channels:
154
+ # Check if the channel is in the epoch_row; if not, skip it.
155
+ if ch in epoch_row:
156
+ ts = epoch_row[ch]
157
+ ts_list.append(ts)
158
+ return np.stack(ts_list, axis=0)
159
+
160
+
161
+ def aggregate_predictions_EEGNET(segment_list, model, threshold):
162
+ """
163
+ Given a list of segments (raw time series data for EEGNet) and a trained
164
+ PyTorch model, predict on each segment and then aggregate the predictions.
165
+
166
+ For each segment, the model returns a probability vector.
167
+ This function averages the predicted probabilities across segments,
168
+ and then compares the average probability for class 1 with the provided threshold
169
+ to decide the final predicted class.
170
+
171
+ Parameters:
172
+ segment_list : list of numpy arrays
173
+ Each element is a 2D numpy array with shape (n_channels, n_samples)
174
+ representing one EEG segment.
175
+ model : a trained PyTorch model that accepts input of shape
176
+ (batch_size, n_channels, n_samples) and outputs probabilities (or logits) for each class.
177
+ threshold : float
178
+ The probability threshold to decide class 1.
179
+
180
+ Returns:
181
+ final_class : int
182
+ The aggregated predicted class (0 or 1).
183
+ """
184
+ model.eval()
185
+ preds = []
186
+
187
+ with torch.no_grad():
188
+ for seg in segment_list:
189
+ # Convert the segment to a torch tensor (float32)
190
+ # Expected shape: (n_channels, n_samples)
191
+ seg_tensor = torch.tensor(seg, dtype=torch.float32)
192
+ # Add a batch dimension -> shape: (1, 1, n_channels, n_samples)
193
+ seg_tensor = seg_tensor.unsqueeze(0).unsqueeze(0)
194
+
195
+ # Forward pass: get the model's output.
196
+ # If your model returns logits, you may need to apply softmax.
197
+ output = model(seg_tensor)
198
+
199
+ # Check if the output is probabilities already or logits.
200
+ # For safety, let's apply softmax to ensure we have probabilities.
201
+ prob = torch.softmax(output, dim=1)[0].cpu().numpy()
202
+
203
+ preds.append(prob)
204
+
205
+ # Average the predictions over all segments
206
+ avg_pred = np.mean(preds, axis=0)
207
+ # For binary classification assume avg_pred[1] is the probability for class 1.
208
+ final_class = int(avg_pred[1] >= threshold)
209
+
210
+ return final_class, avg_pred[1]
211
+
212
+
213
+
214
+ import numpy as np
215
+
216
+ def predict_ensemble_eeg_recording(edf_path, ensemble_method, threshold=0.5):
217
+ """
218
+ The function aggregates the probability scalars from each model and then:
219
+ - For soft voting (method="average"): averages the probabilities
220
+ - For hard voting (method="voting"): uses majority voting (each model votes 1 if
221
+ its probability is >= threshold, else 0)
222
+
223
+ Parameters:
224
+ edf_path : str
225
+ Path to the EEG EDF file.
226
+ ensemble_method : str
227
+ Aggregation method, either "average" for soft voting or "voting" for hard voting.
228
+ threshold : float, default=0.5
229
+ The probability threshold to decide class 1.
230
+
231
+ Returns:
232
+ final_class : int
233
+ The final aggregated predicted class (0 or 1).
234
+ aggregated : float or list
235
+ For "average", the average probability as a float;
236
+ for "voting", the list of votes from each model.
237
+ """
238
+ # Lists to collect probabilities and votes.
239
+ pred_prob_list = []
240
+ votes = []
241
+
242
+ # Iterate over the three model types.
243
+ for model_name in ["2DCNN", "EEGNet", "EpilepsyNet"]:
244
+ pred_label, prob = predict_eeg_recording(edf_path, model_name=model_name, threshold=threshold)
245
+ pred_prob_list.append(prob)
246
+ votes.append(int(prob >= threshold))
247
+ print(f"Prediction from {model_name}: label={pred_label}, probability={prob}")
248
+
249
+ if ensemble_method.lower() == "average":
250
+ # Soft voting: average the probabilities.
251
+ avg_prob = np.mean(pred_prob_list)
252
+ final_class = int(avg_prob >= threshold)
253
+ print("Averaged probability:", avg_prob)
254
+ return final_class, avg_prob
255
+ elif ensemble_method.lower() == "voting":
256
+ # Hard voting: majority decision.
257
+ final_class = int(round(np.mean(votes)))
258
+ print("Votes from each model:", votes)
259
+ return final_class, votes
260
+ else:
261
+ raise ValueError("Ensemble method must be either 'average' or 'voting'.")
preprocessing.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import mne
3
+ import numpy as np
4
+ import pandas as pd
5
+
6
+
7
+ def standardize_dataframe(df):
8
+ # Make a copy to avoid modifying the original dataframe
9
+ df_standardized = df.copy()
10
+
11
+ # Only standardize numeric columns
12
+ numeric_columns = df.select_dtypes(include=np.number).columns
13
+
14
+ for column in numeric_columns:
15
+ mean = df[column].mean()
16
+ std = df[column].std()
17
+
18
+ df_standardized[column] = (df[column] - mean) / std
19
+
20
+ return df_standardized
21
+
22
+ desired = ["EEG FP1-REF", "EEG FP2-REF",
23
+ "EEG F3-REF", "EEG F4-REF",
24
+ "EEG C3-REF"]
25
+
26
+ def select_relevant_channels(raw, desired = desired):
27
+
28
+ # For relevant channel criteria check documentation
29
+ '''“EEG FP1-REF” for the left frontal pole
30
+
31
+ “EEG FP2-REF” for the right frontal pole
32
+
33
+ “EEG F3-REF” for the left frontal region
34
+
35
+ “EEG F4-REF” for the right frontal region
36
+
37
+ “EEG C3-REF” for the left central region'''
38
+
39
+
40
+ #check if all desired channels are present; if not, skip this file
41
+ if not all(ch in raw.ch_names for ch in desired):
42
+ print("Skipping file because it doesn't have the full set of desired channels.")
43
+ return None
44
+ raw.pick_channels(desired, verbose=False)
45
+ return raw
46
+
47
+
48
+ def collapse_epoch_df_by_channel(epoch_df):
49
+ # Identify channel columns (exclude time, epoch, condition)
50
+ channel_cols = [col for col in epoch_df.columns if col not in ['time', 'epoch']]
51
+ # Group by epoch
52
+ grouped = epoch_df.groupby('epoch')
53
+ rows = []
54
+ for epoch_num, group in grouped:
55
+ group_sorted = group.sort_values('time')
56
+ # For each channel, extract the 1D array for this epoch
57
+ row = {'epoch': epoch_num}
58
+ for ch in channel_cols:
59
+ row[ch] = group_sorted[ch].values # 1D array of length = number of time samples in the epoch
60
+ rows.append(row)
61
+ return pd.DataFrame(rows)
62
+
63
+ def preprocess_eeg_file(edf_path, fmin=1.0, fmax=45.0, segment_lenght=5, overlap=2,desired=desired):
64
+
65
+ # 1. Charger le fichier EDF avec MNE
66
+ raw = mne.io.read_raw_edf(edf_path, preload=True, verbose=False)
67
+
68
+ # Resample (to 250 because it's the lowest sampling rate )
69
+ raw.resample(250, verbose=False)
70
+
71
+ # Filtrage passe-bande (1-45 Hz)
72
+ raw.filter(fmin, fmax, fir_design='firwin', verbose=False)
73
+
74
+ # Skip EEGs less than 5s
75
+ if raw.times[-1] < 5:
76
+ print(f"Skipping {edf_path}: duration ({raw.times[-1]:.2f} s) is less than required 5s.")
77
+ return None
78
+
79
+ # Suppression des canaux non EEG
80
+ eeg_channels = mne.pick_types(raw.info, eeg=True, exclude=[])
81
+ raw.pick(eeg_channels, verbose=False)
82
+
83
+ # Selectionner les channels pertinents (channel selection from EDA ?)
84
+ print(raw.ch_names)
85
+ raw = select_relevant_channels(raw,desired=desired)
86
+ if raw is None:
87
+ return None
88
+
89
+ # Segmentation
90
+ epochs = mne.make_fixed_length_epochs(raw, duration=segment_lenght, preload=False, overlap=overlap, verbose=False)
91
+
92
+ # Transform to dataframe and standadize
93
+
94
+ df = epochs.to_data_frame() # epochs is returned by preprocess_eeg_file()
95
+ df_std = standardize_dataframe(df.drop(['time','epoch', 'condition'], axis=1))
96
+ result = pd.concat([df[['time','epoch']], df_std], axis=1)
97
+
98
+ return collapse_epoch_df_by_channel(result)
99
+
100
+
101
+