Skip to content

Commit aadd7db

Browse files
committed
Initial commit
0 parents  commit aadd7db

12 files changed

Lines changed: 767 additions & 0 deletions

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Auto detect text files and perform LF normalization
2+
* text=auto

Kfold_trainer.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import os
2+
import numpy as np
3+
from tqdm import tqdm
4+
5+
import torch
6+
from torch import nn
7+
from torch import optim
8+
from torch.autograd import Variable
9+
from torch.utils.data import TensorDataset, DataLoader
10+
11+
from sklearn.model_selection import StratifiedKFold
12+
from sklearn.metrics import accuracy_score
13+
14+
from model import Transformer
15+
from early_stop_tool import EarlyStopping
16+
from data_loader import data_generator
17+
from args import Config, Path
18+
19+
20+
def set_random_seed(seed=0):
21+
np.random.seed(seed)
22+
torch.manual_seed(seed) # CPU
23+
torch.cuda.manual_seed(seed) # GPU
24+
25+
26+
def test(model, test_loader, config):
27+
criterion = nn.CrossEntropyLoss()
28+
model.eval()
29+
30+
pred = []
31+
label = []
32+
33+
test_loss = 0
34+
35+
with torch.no_grad():
36+
for batch_idx, (data, target) in enumerate(test_loader):
37+
data = data.to(config.device)
38+
target = target.to(config.device)
39+
data, target = Variable(data), Variable(target)
40+
41+
output = model(data)
42+
test_loss += criterion(output, target.long()).item()
43+
44+
pred.extend(np.argmax(output.data.cpu().numpy(), axis=1))
45+
label.extend(target.data.cpu().numpy())
46+
47+
accuracy = accuracy_score(label, pred, normalize=True, sample_weight=None)
48+
49+
return accuracy, test_loss
50+
51+
52+
def train(save_all_checkpoint=False):
53+
config = Config()
54+
path = Path()
55+
56+
dataset, labels, val_loader = data_generator(path_labels=path.path_labels, path_dataset=path.path_TF)
57+
58+
kf = StratifiedKFold(n_splits=config.num_fold, shuffle=True, random_state=0)
59+
60+
for fold, (train_idx, test_idx) in enumerate(kf.split(dataset, labels)):
61+
print('\n', '-' * 15, '>', f'Fold {fold}', '<', '-' * 15)
62+
if not os.path.exists('./Kfold_models/fold{}'.format(fold)):
63+
os.makedirs('./Kfold_models/fold{}'.format(fold))
64+
65+
X_train, X_test = dataset[train_idx], dataset[test_idx]
66+
y_train, y_test = labels[train_idx], labels[test_idx]
67+
train_set = TensorDataset(X_train, y_train)
68+
test_set = TensorDataset(X_test, y_test)
69+
train_loader = DataLoader(dataset=train_set, batch_size=config.batch_size, shuffle=False)
70+
test_loader = DataLoader(dataset=test_set, batch_size=config.batch_size, shuffle=False)
71+
72+
model = Transformer(config)
73+
model = model.to(config.device)
74+
75+
criterion = nn.CrossEntropyLoss()
76+
77+
# AdamW optimizer
78+
optimizer = optim.AdamW(model.parameters(), lr=config.learning_rate, weight_decay=0.01)
79+
80+
# apply early_stop. If you want to view the full training process, set the save_all_checkpoint True
81+
early_stopping = EarlyStopping(patience=20, verbose=True, save_all_checkpoint=save_all_checkpoint)
82+
83+
# evaluating indicator
84+
train_ACC = []
85+
train_LOSS = []
86+
test_ACC = []
87+
test_LOSS = []
88+
val_ACC = []
89+
val_LOSS = []
90+
91+
for epoch in range(config.num_epochs):
92+
running_loss = 0.0
93+
correct = 0
94+
95+
model.train()
96+
97+
loop = tqdm(enumerate(train_loader), total=len(train_loader))
98+
for batch_idx, (data, target) in loop:
99+
data = data.to(config.device)
100+
target = target.to(config.device)
101+
data, target = Variable(data), Variable(target)
102+
103+
optimizer.zero_grad()
104+
output = model(data)
105+
106+
loss = criterion(output, target.long())
107+
108+
loss.backward()
109+
110+
optimizer.step()
111+
112+
running_loss += loss.item()
113+
114+
train_acc_batch = np.sum(np.argmax(np.array(output.data.cpu()), axis=1) == np.array(target.data.cpu())) / (target.shape[0])
115+
loop.set_postfix(train_acc=train_acc_batch, loss=loss.item())
116+
correct += np.sum(np.argmax(np.array(output.data.cpu()), axis=1) == np.array(target.data.cpu()))
117+
118+
train_acc = correct / len(train_loader.dataset)
119+
test_acc, test_loss = test(model, test_loader, config)
120+
val_acc, val_loss = test(model, val_loader, config)
121+
print('Epoch: ', epoch,
122+
'| train loss: %.4f' % running_loss, '| train acc: %.4f' % train_acc,
123+
'| val acc: %.4f' % val_acc, '| val loss: %.4f' % val_loss,
124+
'| test acc: %.4f' % test_acc, '| test loss: %.4f' % test_loss)
125+
126+
train_ACC.append(train_acc)
127+
train_LOSS.append(running_loss)
128+
test_ACC.append(test_acc)
129+
test_LOSS.append(test_loss)
130+
val_ACC.append(val_acc)
131+
val_LOSS.append(val_loss)
132+
133+
# Check whether to continue training. If save_all_checkpoint=False, the model name will be ‘model.pkl'
134+
early_stopping(val_acc, model, path='./Kfold_models/fold{}/model_{}_epoch{}.pkl'.format(fold, fold, epoch))
135+
136+
if early_stopping.early_stop:
137+
print("Early stopping at epoch ", epoch)
138+
break
139+
140+
np.save('./Kfold_models/fold{}/train_LOSS.npy'.format(fold), np.array(train_LOSS))
141+
np.save('./Kfold_models/fold{}/train_ACC.npy'.format(fold), np.array(train_ACC))
142+
np.save('./Kfold_models/fold{}/test_LOSS.npy'.format(fold), np.array(test_LOSS))
143+
np.save('./Kfold_models/fold{}/test_ACC.npy'.format(fold), np.array(test_ACC))
144+
np.save('./Kfold_models/fold{}/val_LOSS.npy'.format(fold), np.array(val_LOSS))
145+
np.save('./Kfold_models/fold{}/val_ACC.npy'.format(fold), np.array(val_ACC))
146+
147+
del model
148+
149+
150+
if __name__ == '__main__':
151+
set_random_seed(0)
152+
train(save_all_checkpoint=False)

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# MultiChannelSleepNet
2+
### MultiChannelSleepNet: A Transformer-Based Deep Learning Method for Automatic Sleep Stage Classification With Multi-Channel PSG
3+
#### *by: Yang Dai, Shanshan Liang, Lukang Wang, Qingtian Duan, Hui Yang, Chunqing Zhang\*, Xiaowei Chen\*, and Xiang Liao\*
4+
5+
6+
## Abstract
7+
![AttnSleep Architecture](imgs/MutiChannelSleepNet.png)
8+
Automatic sleep staging aims to classify sleep states by machine intelligence, which plays an essential role in the measurement of sleep quality and diagnosis of sleep disorders. Although many automatic methods have been proposed for accurate sleep staging, most of these approaches utilize only single channel EEG signals to classify sleep stages. Multi-channel polysomnography (PSG) data provide more information to train classifier and may achieve higher sleep staging performance. Here we propose a novel transformer encoder-based deep learning method named MultiChannelSleepNet for sleep stage classification with multi-channel PSG data. The proposed model architecture is implemented based on the transformer encoder for single-channel feature extraction and multi-channel feature fusion. In each single-channel feature extraction block, transformer encoders are used to extract features from time-frequency maps of each channel. Based on our integration strategy, the feature maps extracted from each channel are fused for further feature extraction. Transformer encoders and a residual connection are applied in this module to preserve original information of each channel. The experimental results on three public datasets show that our method outperforms state-of-the-art methods in terms of different evaluation metrics.
9+
10+
11+
## Requirmenets:
12+
- python3.6
13+
- pytorch=='1.9.1'
14+
- numpy
15+
- sklearn
16+
- scipy=='1.5.4'
17+
- mne=='0.23.4'
18+
- tqdm
19+
20+
## Data
21+
We used three public datasets in this study:
22+
23+
- SleepEDF-20 (The first 39 records in SleepEDF-78)
24+
- [SleepEDF-78](https://physionet.org/content/sleep-edfx/1.0.0/)
25+
- [SHHS](https://sleepdata.org/datasets/shhs)
26+
27+
This project currently only provides pre-processing code for SleepEDF-20 and SleepEDF-78. We will update the code of SHHS later.
28+
After downloading the datasets, please place them in the folder with the corresponding name in the directory `dataset`.
29+
You can run the `dataset_prepare.py` to extract events from the original record (.edf)
30+
31+
## Reproducibility
32+
If you want to update the training parameters, you can edit the `args.py` file. In this file, you can update:
33+
34+
- Device (GPU or CPU).
35+
- Batch size.
36+
- Number of folds (as we use K-fold cross validation).
37+
- The number of training epochs.
38+
- Parameters in our model (dropout rate, number of transformer encoder, etc)
39+
40+
To easily reproduce the results you can follow the next steps:
41+
42+
1. Run `dataset_prepare.py` to extract events from the original record (.edf).
43+
2. Run `data_preprocess_TF` to preprocess the data. The original signals will be converted to time-frequency images, and normalized.
44+
3. Run `Kfold_trainer.py` to perform the standard K-fold cross validation.
45+
4. Run `result_evaluate.py` to get the evaluation report. It concludes the various valuation metrics we described in paper.
46+
47+
48+
## Contact
49+
Yang Dai
50+
Center for Neurointelligence, School of Medicine
51+
Chongqing University, Chongqing 400030, China
52+
Email: valar_d@163.com

args.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import torch
2+
import os
3+
4+
5+
class Config(object):
6+
"""args in model and trainer"""
7+
def __init__(self):
8+
self.device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
9+
self.num_fold = 10
10+
self.num_classes = 5
11+
self.num_epochs = 200 # Because early stopping is used, this parameter can be relatively large
12+
self.batch_size = 64
13+
self.pad_size = 29 # time dimension of TF image
14+
self.learning_rate = 5e-6
15+
self.dropout = 0.1 # dropout rate in transformer encoder
16+
self.dim_model = 128 # frequency of TF image
17+
self.forward_hidden = 1024 # hidden units of transformer encoder
18+
self.fc_hidden = 1024 # hidden units of FC layers
19+
self.num_head = 8
20+
self.num_encoder = 16 # number of encoders in single-channel feature extraction block
21+
self.num_encoder_multi = 4 # number of encoders in multi-channel feature fusion block
22+
23+
24+
class Path(object):
25+
"""path of files in this project"""
26+
def __init__(self):
27+
self.path_PSG = 'dataset/sleepEDF-78/sleep-cassette'
28+
self.path_hypnogram = 'dataset/sleepEDF-78/Hypnogram'
29+
self.path_raw_data = 'data/sleepEDF-78/data_array/raw_data'
30+
self.path_labels = 'data/sleepEDF-78/data_array/raw_data/labels'
31+
self.path_TF = 'data/sleepEDF-78/data_array/TF_data'
32+
33+
if not os.path.exists(self.path_hypnogram):
34+
os.makedirs(self.path_hypnogram)
35+
36+
if not os.path.exists(self.path_raw_data):
37+
os.makedirs(self.path_raw_data)
38+
39+
if not os.path.exists(self.path_TF):
40+
os.makedirs(self.path_TF)
41+

data_loader.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import os
2+
import numpy as np
3+
4+
import torch
5+
from torch.utils.data import TensorDataset, DataLoader
6+
from sklearn.model_selection import train_test_split
7+
8+
from args import Config, Path
9+
10+
11+
def data_generator(path_labels, path_dataset):
12+
config = Config()
13+
dir_annotation = os.listdir(path_labels)
14+
15+
first = True
16+
for f in dir_annotation:
17+
if first:
18+
labels = np.load(os.path.join(path_labels, f))
19+
first = False
20+
else:
21+
temp = np.load(os.path.join(path_labels, f))
22+
labels = np.append(labels, temp, axis=0)
23+
labels = torch.from_numpy(labels)
24+
25+
dataset_EEG_FpzCz = np.load(os.path.join(path_dataset, 'TF_EEG_Fpz-Cz_mean_std.npy')).astype('float32')
26+
dataset_EEG_PzOz = np.load(os.path.join(path_dataset, 'TF_EEG_Pz-Oz_mean_std.npy')).astype('float32')
27+
dataset_EOG = np.load(os.path.join(path_dataset, 'TF_EOG_mean_std.npy')).astype('float32')
28+
29+
dataset = np.stack((dataset_EEG_FpzCz, dataset_EEG_PzOz, dataset_EOG), axis=1)
30+
dataset = torch.from_numpy(dataset)
31+
32+
print('dataset: ', dataset.shape)
33+
34+
# hold out the validation set
35+
X_train_test, X_val, y_train_test, y_val = train_test_split(dataset, labels, test_size=1/(config.num_fold+1), random_state=0, stratify=labels)
36+
37+
val_set = TensorDataset(X_val, y_val)
38+
val_loader = DataLoader(dataset=val_set, batch_size=config.batch_size, shuffle=False)
39+
40+
print('val_set:', len(X_val))
41+
return X_train_test, y_train_test, val_loader

data_preprocess_TF.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import os
2+
import numpy as np
3+
4+
from scipy.fftpack import fft
5+
from scipy import signal
6+
from tqdm import tqdm
7+
8+
from args import Path
9+
10+
11+
def data_array_concat(path_array):
12+
"""concat data from each subject"""
13+
dir_PSG = os.listdir(path_array)
14+
first = True
15+
print('Preparing dataset:')
16+
for f in tqdm(dir_PSG):
17+
if first:
18+
data_channel = np.load(os.path.join(path_array, f)).astype('float32')
19+
first = False
20+
else:
21+
temp = np.load(os.path.join(path_array, f)).astype('float32')
22+
data_channel = np.append(data_channel, temp, axis=0)
23+
data_channel = np.squeeze(data_channel, axis=1)
24+
return data_channel
25+
26+
27+
def spectrogram(x, window, n_overlap, nfft):
28+
"""
29+
Transform to time-frequency images. This function imitates function spectrogram in Matlab
30+
Args:
31+
x (numpy array): Data
32+
window (int): Size of window function
33+
n_overlap (int):Number of coincidence points between two segments
34+
nfft (int): Number of points during Fast Fourier Transform
35+
"""
36+
len_x = len(x)
37+
step = window - n_overlap
38+
nn = nfft // 2 + 1
39+
num_win = int(np.floor((len_x - n_overlap) / (window - n_overlap)))
40+
spectrogram_data = []
41+
# Hamming window default
42+
win = signal.hamming(window)
43+
for i in range(num_win):
44+
subdata = x[i * step: i * step + window]
45+
F = fft(subdata * win, n=nfft)
46+
spectrogram_data.append(F[:nn])
47+
spectrogram_data = np.array(spectrogram_data)
48+
return spectrogram_data
49+
50+
51+
def data_normalize(dataset, channel):
52+
"""normalize datasets of each channel to zero mean and unit variance"""
53+
for i in tqdm(range(dataset.shape[0])):
54+
if True in np.isinf(dataset[i]):
55+
for j in range(29):
56+
if True in np.isinf(dataset[i][j]):
57+
for k in range(128):
58+
if np.isinf(dataset[i][j][k]):
59+
if k != 127:
60+
print('location of inf: ', i, ',', j, ',', k)
61+
if k == 0:
62+
if j == 0:
63+
dataset[i][j][k] = dataset[i][j+1][k]
64+
else:
65+
dataset[i][j][k] = dataset[i][j-1][k]
66+
else:
67+
dataset[i][j][k] = dataset[i][j][k-1]
68+
69+
dataset = (dataset - np.mean(dataset)) / np.std(dataset)
70+
71+
ans1 = np.isinf(dataset)
72+
ans2 = np.isnan(dataset)
73+
74+
if not ((True in ans1) and (True in ans2)):
75+
np.save('./data/sleepEDF-78/data_array/TF_data/TF_{}_mean_std.npy'.format(channel), dataset)
76+
77+
78+
if __name__ == '__main__':
79+
path = Path()
80+
81+
fs = 100
82+
overlap = 1
83+
nfft = 256
84+
win_size = 2
85+
86+
for channel in ['EEG_Fpz-Cz', 'EEG_Pz-Oz', 'EOG']:
87+
print('-' * 15, 'Processing channel:{}'.format(channel), '-' * 15)
88+
data_channel = data_array_concat(path_array=os.path.join(path.path_raw_data, channel))
89+
X = np.zeros([data_channel.shape[0], 29, int(nfft / 2)])
90+
print('Transform to TF images:')
91+
for i in tqdm(range(data_channel.shape[0])):
92+
Xi = spectrogram(data_channel[i, :], win_size * fs, overlap * fs, nfft)
93+
Xi = 20 * np.log10(abs(Xi))
94+
X[i, :, :] = Xi[:, 1:129]
95+
96+
print('Normalize:')
97+
data_normalize(dataset=X, channel=channel)

0 commit comments

Comments
 (0)