OSCR

Improved motor imagery BCI performance via task-unaware compression in the BELT Bayesian Edge-Cloud architecture.

Code ↔ Paper

9 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 9 matches
  1. [1] § Materials and methods › Implementation › Data augmentation. ↔ train_GAN.py, lines 137–177 · score 0.72 · latent vector, dense layer, tanh, cGAN, ReLU, concatenated
  2. [2] § Materials and methods › Implementation › Data augmentation. ↔ model_comparison.ipynb, lines 276–391 · score 0.68 · dense layers, EEG channels, norm, flattening, sigmoid, dropout
  3. [3] § Materials and methods › Implementation › Data augmentation. ↔ train_GAN.py, lines 75–135 · score 0.67 · EEG channels, leaky, flattening, dropout, bias, dense
  4. [4] § Materials and methods › Implementation › Detector. ↔ model_comparison.ipynb, lines 276–391 · score 0.62 · dropout layer, dense layer, Batch normalization, classifier
  5. [5] § Materials and methods › Implementation › Detector. ↔ train_GAN.py, lines 75–135 · score 0.58 · Conv1D, EEG channel, variable, padding, kernels, batch
  6. [6] § Materials and methods › Implementation › Detector. ↔ prior.ipynb, lines 13–65 · score 0.56 · Conv1D, spatial filter, activation, bias, kernels, linear
  7. [7] § Materials and methods › Implementation › Detector. ↔ compressor.ipynb, lines 355–419 · score 0.56 · Conv1D, spatial filter, activation, bias, kernels, linear
  8. [8] § Materials and methods › Model › Detector. ↔ model_comparison.ipynb, lines 57–123 · score 0.55 · Conv1D, spatial filters, activation, bias, stride, kernel
  9. [9] § Materials and methods › Implementation › EEG dataset and task framing. ↔ segmentation.ipynb, lines 254–305 · score 0.51 · cue onset, rejected, segments

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Python · 551 lines · 20 KB · GPL-3.0 · 3 matches

  1. import multiprocessing.process
  2. import sys
  3. k_fold = sys.argv
  4. PROJ_NAME = "DualDataset"
  5. def process_gan_training(k_fold: int, EPOCH_RESUME_FROM = 0, MAX_RAM_GB = 4, max_epochs: int = 300):
  6. global PROJ_NAME
  7. import TSA
  8. import tools
  9. # import analysis as A
  10. import importlib
  11. import numpy as np
  12. import os
  13. latent_dim = 100
  14. TSA.setup_proj(PROJ_NAME, 'ab', ignore_folds=False)
  15. # Data
  16. Segments = tools.load_segments('ab', splitSegments=TSA.cv_select_k_fold(k_fold))
  17. XY_spl = tools.seg2XY(Segments)
  18. X = [XY_spl['tr'][0], XY_spl['te'][0]]
  19. X = np.concatenate(X, axis=0)
  20. Y = [XY_spl['tr'][1], XY_spl['te'][1]]
  21. Y = np.concatenate(Y, axis=0)
  22. X_train = np.zeros((X.shape[0], X.shape[1], 5))
  23. X_train[:, :, 0:3] = X
  24. for i in range(1000):
  25. X_train[:, i, 3:] = Y
  26. Xv, Yv = XY_spl['val']
  27. X_val = np.zeros((Xv.shape[0], Xv.shape[1], 5))
  28. X_val[:, :, 0:3] = Xv
  29. for i in range(1000):
  30. X_val[:, i, 3:] = Yv
  31. # Models
  32. from tensorflow.keras.layers import Layer
  33. from keras.saving import register_keras_serializable
  34. import keras.models as KM
  35. import keras.layers as KL
  36. import tensorflow as tf
  37. from keras.models import Model
  38. from keras.layers import Input, Dense, Reshape, RepeatVector, LSTM, Concatenate
  39. if EPOCH_RESUME_FROM > 0:
  40. EPOCH_LAST = EPOCH_RESUME_FROM
  41. discriminator, _ = TSA.checkpoint_model_read(f"GAN2_{EPOCH_LAST}/disc", cv_k=k_fold, exp_name=None)
  42. generator, _ = TSA.checkpoint_model_read(f"GAN2_{EPOCH_LAST}/gen", cv_k=k_fold, exp_name=None)
  43. else:
  44. from tensorflow.keras.layers import Layer
  45. from keras.saving import register_keras_serializable
  46. import keras.models as KM
  47. import keras.layers as KL
  48. import tensorflow as tf
  49. input_shape = (1000, 5)
  50. @register_keras_serializable()
  51. class NTBSquareLayer(Layer):
  52. def call(self, x):
  53. return tf.square(x)
  54. @register_keras_serializable()
  55. class NTBLogLayer(Layer):
  56. def call(self, x):
  57. return tf.math.log(x + 1e-8)
  58. def build_discriminator(T, C, num_classes):
  59. # Input: Real or generated data (batch_size, T, C + num_subjects + num_classes)
  60. disc_input = KL.Input(shape=(T, C + num_classes)) # Shape: (T, C + 11)
  61. # Process EEG data (C channels)
  62. eeg_data = disc_input[:, :, :C]
  63. eeg_channels = KL.Conv1D(
  64. 18,
  65. kernel_size=25,
  66. #groups=3,
  67. use_bias=False,
  68. activation="linear",
  69. padding="same",
  70. name="LTI-BP-Filts",
  71. )(eeg_data)
  72. # Filts: Spatial filters
  73. eeg_channels = KL.Conv1D(
  74. 16,
  75. kernel_size=1,
  76. strides=1,
  77. use_bias=False,
  78. activation="linear",
  79. padding="valid",
  80. name="LTI-Spatial-Filts",
  81. )(eeg_channels)
  82. # NTB
  83. eeg_channels = KL.BatchNormalization()(eeg_channels)
  84. eeg_channels = NTBSquareLayer(name="NTB-square")(eeg_channels)
  85. eeg_channels = KL.AveragePooling1D(20, strides=13, name="NTB-avg")(eeg_channels)
  86. # KL.Lambda(lambda x: tf.math.log(x + 1e-8), name="NTB-log"),
  87. eeg_channels = NTBLogLayer(name="NTB-log")(eeg_channels)
  88. # Cfier
  89. eeg_flat = KL.Flatten()(eeg_channels)
  90. eeg_flat = KL.BatchNormalization()(eeg_flat)
  91. # Process condition variables (class and subject codes)
  92. condition_data = disc_input[:, :, C:] # Remaining channels
  93. condition_flat = KL.Flatten()(condition_data)
  94. # Concatenate EEG features and condition variables
  95. combined = KL.Concatenate()([eeg_flat, condition_flat])
  96. # Fully connected layers
  97. x = combined
  98. x = KL.Dense(256)(x)
  99. x = KL.LeakyReLU(alpha=0.2)(x)
  100. x = KL.Dropout(0.2)(x)
  101. x = KL.Dense(128)(x)
  102. x = KL.LeakyReLU(alpha=0.2)(x)
  103. x = KL.Dropout(0.2)(x)
  104. x = KL.Dense(64)(x)
  105. x = KL.LeakyReLU(alpha=0.2)(x)
  106. x = KL.Dropout(0.2)(x)
  107. x = KL.Dense(32)(x)
  108. x = KL.LeakyReLU(alpha=0.2)(x)
  109. x = KL.Dropout(0.2)(x)
  110. x = KL.Dense(16)(x)
  111. x = KL.LeakyReLU(alpha=0.2)(x)
  112. x = KL.Dropout(0.2)(x)
  113. x = KL.Dense(8)(x)
  114. x = KL.LeakyReLU(alpha=0.2)(x)
  115. x = KL.Dropout(0.2)(x)
  116. x = KL.Dense(4)(x)
  117. x = KL.LeakyReLU(alpha=0.2)(x)
  118. x = KL.Dropout(0.2)(x)
  119. # Output layer for binary classification (real/fake)
  120. disc_output = KL.Dense(1, activation="sigmoid")(x)
  121. # Create the model
  122. discriminator = KM.Model(disc_input, disc_output, name="Discriminator")
  123. return discriminator
  124. discriminator = build_discriminator(1000, 3, 2)
  125. from keras.models import Model
  126. from keras.layers import Input, Dense, Reshape, RepeatVector, LSTM, Concatenate
  127. def build_generator(latent_dim, T, C, num_classes):
  128. # Input: concatenated latent vector + subject label + class label
  129. gen_input = Input(shape=(latent_dim + num_classes,)) # Shape: (batch_size, latent_dim + 9 + 2)
  130. # Process the latent vector through dense layers
  131. x = Dense(32, activation='relu')(gen_input)
  132. x = Dense(32, activation='relu')(gen_input)
  133. x = Dense(32, activation='relu')(gen_input)
  134. x = Dense(T * C, activation='tanh')(x)
  135. x = Reshape((T, C))(x) # Shape: (batch_size, T, C)
  136. # Extract the subject and class labels from the input
  137. subject_and_class_labels = gen_input[:, - num_classes:] # Last (9 + 2) part of input
  138. # Repeat the subject and class labels across T time steps
  139. repeated_labels = RepeatVector(T)(subject_and_class_labels) # Shape: (batch_size, T, 9 + 2)
  140. # Concatenate the generated sequence and the repeated labels
  141. gen_output = Concatenate(axis=-1)([x, repeated_labels]) # Shape: (batch_size, T, C + 9 + 2)
  142. # Create the model
  143. generator = Model(gen_input, gen_output, name="Generator")
  144. return generator
  145. generator = build_generator(latent_dim, 1000, 3, 2)
  146. def build_cgan(generator, discriminator):
  147. # Make the discriminator non-trainable when training the CGAN
  148. discriminator.trainable = False
  149. # Generator inputs
  150. latent_input = Input(shape=(latent_dim + 2,), name="generator_input")
  151. # Generator output
  152. generated_data = generator(latent_input) # Output shape: (batch_size, T, C + 9 + 2)
  153. # Discriminator prediction on generated data
  154. validity = discriminator(generated_data) # Output shape: (batch_size, 1)
  155. # CGAN model (generator -> discriminator)
  156. cgan = Model(inputs=latent_input, outputs=validity, name="CGAN")
  157. return cgan
  158. cgan = build_cgan(generator, discriminator)
  159. cgan.summary()
  160. def compile_cgan(generator, discriminator, cgan_model):
  161. # Compile discriminator (real/fake classification)
  162. discriminator.trainable = True
  163. optimizer_D = TSA.KO.Adam(0.0002, 0.5)
  164. discriminator.compile(optimizer=optimizer_D, loss='binary_crossentropy', metrics=['accuracy'])
  165. # Compile CGAN (train the generator through the discriminator's feedback)
  166. discriminator.trainable = False
  167. generator.trainable = True
  168. optimizer_G = TSA.KO.Adam(0.0002, 0.5)
  169. cgan_model.compile(optimizer=optimizer_G, loss='binary_crossentropy', metrics=['accuracy'])
  170. return optimizer_D, optimizer_G
  171. opt_D, opt_G = compile_cgan(generator, discriminator, cgan)
  172. # Phase 1 - Train
  173. real_mixed = X_train.copy()
  174. real_mixed[:, :, 3:] = 1 - real_mixed[:, :, 3:]
  175. import numpy as np
  176. def generate_disc_data_uniform(real: bool, fake: bool, mixed_class_real: bool = False):
  177. real_data = X_train.copy()
  178. batch_size = X_train.shape[0]
  179. if fake:
  180. fake_data = np.random.normal(-1, 1, size=real_data.shape)
  181. fake_data[:, :, 3:] = 0
  182. fake_data[0::2, :, 3] = 1
  183. fake_data[1::2, :, 3] = 1
  184. if mixed_class_real:
  185. real_mixed = X_train.copy()
  186. real_mixed[:, :, 3:] = 1 - real_mixed[:, :, 3:]
  187. # Combine real and fake data
  188. DL = []
  189. LL = []
  190. if real:
  191. DL.append(real_data)
  192. LL.append(np.ones((batch_size, 1)))
  193. if fake:
  194. DL.append(fake_data)
  195. LL.append(np.zeros((batch_size, 1)))
  196. if mixed_class_real:
  197. DL.append(real_mixed)
  198. LL.append(np.zeros((batch_size, 1)))
  199. combined_data = np.concatenate(DL, axis=0)
  200. labels = np.concatenate(LL, axis=0) # Real = 1, Fake = 0
  201. # Add noise to labels (label smoothing)
  202. #labels += 0.05 * np.random.random(labels.shape)
  203. #labels = np.clip(labels, 0.0, 1.0) # Ensure labels stay within [0, 1] range
  204. return combined_data, labels
  205. X_val_c0 = X_val.copy()
  206. X_val_c0[:, :, 3] = 1
  207. X_val_c0[:, :, 4] = 0
  208. X_val_c1 = X_val.copy()
  209. X_val_c1[:, :, 3] = 0
  210. X_val_c1[:, :, 4] = 1
  211. def measure_classification_of_disc():
  212. Yhat_0 = discriminator.predict(X_val_c0)
  213. Yhat_1 = discriminator.predict(X_val_c1)
  214. Yv_hat = np.zeros_like(Yv)
  215. is_c_0 = Yhat_0 >= Yhat_1
  216. is_c_1 = Yhat_0 < Yhat_1
  217. Yv_hat[is_c_0.flatten(), 0] = 1
  218. Yv_hat[is_c_0.flatten(), 1] = 0
  219. Yv_hat[is_c_1.flatten(), 0] = 0
  220. Yv_hat[is_c_1.flatten(), 1] = 1
  221. Cv = np.argmax(Yv, axis=1)
  222. Cv_hat = np.argmax(Yv_hat, axis=1)
  223. return np.mean(Cv == Cv_hat)
  224. if EPOCH_RESUME_FROM == 0:
  225. discriminator.trainable = True
  226. X, Y = generate_disc_data_uniform(real=True, fake=False, mixed_class_real=True)
  227. for epoch in range(20):
  228. print("----------")
  229. discriminator.fit(X, Y, validation_data=(X_val, np.ones((X_val.shape[0], 1))), batch_size=16, epochs=1, shuffle=True)
  230. #print(discriminator.evaluate(X_val, np.ones((X_val.shape[0], 1)), batch_size=64, return_dict=True))
  231. print(measure_classification_of_disc())
  232. # Training Phase 2
  233. epoch = EPOCH_RESUME_FROM + 1
  234. import numpy as np
  235. import tensorflow as tf
  236. from keras.utils import disable_interactive_logging, enable_interactive_logging
  237. import time
  238. enable_interactive_logging()
  239. # Training parameters
  240. batch_size = X_train.shape[0]//5
  241. epochs = 2500
  242. acc_D_qualified = 0.9
  243. acc_G_prime = 0.8
  244. lr_D_max = 1e-2 / 100
  245. B_D_NQ = 1
  246. lr_D_NQ = 1e-3 / 100
  247. lr_G_NQ = 1e-7 / 100
  248. alpha = 0.2
  249. lr_D_base_QP = 1e-3 / 100
  250. lr_G_QP = 1e-3 / 100
  251. B_D_QP = 200
  252. lr_D_base_QNP = 1e-4 / 100
  253. lr_G_QNP = 1e-4 / 100
  254. B_D_QNP = 20
  255. B_G_samples_QNP = 5
  256. B_G_rep_QNP = 100
  257. prime = False
  258. qualified = False
  259. batches_per_epoch = len(X_train) // batch_size
  260. TR_LOGS = []
  261. #epoch = 2059
  262. rk = 1
  263. lr_d_prev = lr_D_base_QNP
  264. lr_d_next = lr_D_base_QNP
  265. lr_g = lr_G_QNP
  266. import gc
  267. import psutil
  268. gc.collect()
  269. def get_ram_usage_MB():
  270. process = psutil.Process()
  271. mem_info = process.memory_info()
  272. return mem_info.rss / 1024**2
  273. USED_RAM = {}
  274. tic = time.time()
  275. while epoch < epochs + 1:
  276. Primes = []
  277. Qualified = []
  278. used_ram_MB = get_ram_usage_MB()
  279. USED_RAM[epoch] = used_ram_MB
  280. print(f"@{epoch} - RAM usage: {get_ram_usage_MB():.2f} MB")
  281. disable_interactive_logging()
  282. if used_ram_MB > MAX_RAM_GB * 1024:
  283. print("The RAM usage exceeds maximum allowed RAM!")
  284. return
  285. # print("Relearn the uniform!")
  286. #X, Y = generate_disc_data_uniform(real=True, fake=True)
  287. #discriminator.fit(X, Y, batch_size=64, epochs=1, shuffle=True)
  288. for batch_no in range(batches_per_epoch):
  289. Primes.append(prime)
  290. Qualified.append(qualified)
  291. # Control the mode
  292. if not qualified: #NQ mode:
  293. print("NQ")
  294. lr_d = lr_D_NQ
  295. lr_g = lr_G_NQ
  296. lr_d_base = lr_D_NQ
  297. B_D = B_D_NQ
  298. B_G_samples = 5
  299. B_G_rep = 1
  300. elif prime: #QP mode:
  301. print("QP")
  302. lr_d = alpha*lr_d_prev + (1-alpha)*lr_d_next
  303. lr_d_base = lr_D_base_QP
  304. lr_g = lr_G_QP
  305. B_D = B_D_QP
  306. B_G_samples = B_D
  307. B_G_rep = 1
  308. else: #QNP mode:
  309. print("QNP")
  310. lr_d = alpha*lr_d_prev + (1-alpha)*lr_d_next
  311. lr_d_base = lr_D_base_QNP
  312. lr_g = lr_G_QNP
  313. B_D = B_D_QNP
  314. B_G_samples = B_G_samples_QNP
  315. B_G_rep = B_G_rep_QNP
  316. lr_d_prev = lr_d
  317. # Update learning rate of disc and gen
  318. opt_D.learning_rate = lr_d
  319. opt_G.learning_rate = lr_g
  320. print("Discriminator...")
  321. # 1. Train the discriminator
  322. real_data = X_train[batch_size*batch_no:batch_size*(batch_no+1),:,:]
  323. # Generate fake data
  324. noise = np.random.normal(0, 1, (batch_size, latent_dim))
  325. fake_class_labels = np.zeros((batch_size, 2)) # Replace with actual class labels
  326. fake_class_labels[0::2, 0] = 1
  327. fake_class_labels[1::2, 1] = 1
  328. generator_input = np.concatenate([noise, fake_class_labels], axis=-1)
  329. # fake_data = generate_disc_data_uniform(real=False, fake=True)[0][0:batch_size]#generator.predict(generator_input) # Shape: (batch_size, T, C+2)
  330. fake_data = generator.predict(generator_input) # Shape: (batch_size, T, C+2)
  331. # Combine real and fake data
  332. #print(real_data.shape, fake_data.shape, combined_data.shape)
  333. combined_data = np.concatenate([real_data, fake_data], axis=0)
  334. labels = np.concatenate(
  335. [np.ones((batch_size, 1)), np.zeros((batch_size, 1))]
  336. ) # Real = 1, Fake = 0
  337. #combined_data, labels = generate_disc_data_uniform(True, True, False)
  338. #print(labels.shape, np.mean(labels))
  339. # Add noise to labels (label smoothing)
  340. #labels += 0.05 * np.random.random(labels.shape)
  341. #labels = np.clip(labels, 0.0, 1.0) # Ensure labels stay within [0, 1] range
  342. # Train discriminator
  343. discriminator.trainable = True
  344. generator.trainable = False
  345. qualified = False
  346. for rep_d in range(B_D):
  347. #d_loss = discriminator.train_on_batch(combined_data, labels)
  348. #combined_data, labels = generate_disc_data_uniform(real=True, fake=True)
  349. dHist = discriminator.fit(combined_data, labels).history
  350. d_loss = dHist['loss']
  351. d_acc = dHist['accuracy'][0]
  352. if d_acc >= acc_D_qualified:
  353. qualified = True
  354. break
  355. del dHist
  356. print("Generator...")
  357. # 2. Train the generator (via the CGAN model)
  358. G_Loss = []
  359. discriminator.trainable = False
  360. generator.trainable = True
  361. prime = False
  362. noise = None
  363. for g_batch_n in range(B_G_samples):
  364. if noise is not None:
  365. del noise, generator_input, misleading_labels
  366. gc.collect()
  367. noise = None
  368. noise = np.random.normal(0, 1, (batch_size, latent_dim))
  369. generator_input = np.concatenate([noise, fake_class_labels], axis=-1)
  370. misleading_labels = np.ones((batch_size, 1)) # Generator tries to fool discriminator
  371. for g_batch_n_rep in range(B_G_rep):
  372. gHist = cgan.fit(generator_input, misleading_labels).history
  373. g_loss = gHist['loss'][0]
  374. g_acc = gHist['accuracy'][0]
  375. G_Loss.append(g_loss)
  376. del gHist
  377. if g_acc > acc_G_prime:
  378. prime = True
  379. break
  380. if prime:
  381. break
  382. # update rk and prime
  383. g_loss = np.mean(G_Loss)
  384. rk = 10**(1 - g_loss/d_loss[0])
  385. lr_d_next = min(lr_D_max, lr_d_base*rk)
  386. # freeup!
  387. del noise, fake_class_labels, generator_input, fake_data, combined_data, labels
  388. gc.collect()
  389. if epoch % 10 == 0:
  390. print("Discriminator recovery...")
  391. # discriminator recovery
  392. discAccBinBr = measure_classification_of_disc()
  393. X, Y = generate_disc_data_uniform(real=True, fake=False, mixed_class_real=True)
  394. discriminator.fit(X, Y, batch_size=16, epochs=1, shuffle=True)#, class_weight={0:1/3, 1:2/3})
  395. drHist = discriminator.evaluate(X, Y, batch_size=64, return_dict=True)
  396. print(drHist)
  397. del drHist, X, Y
  398. else:
  399. discAccBinBr = 0
  400. discAccBin = measure_classification_of_disc()
  401. # Print loss metrics
  402. if epoch % 1 == 0:
  403. took = time.time() - tic
  404. #print(epoch, took, d_loss, g_loss)
  405. _lrD = tf.keras.backend.get_value(opt_D.learning_rate)
  406. tr_log = f"Epoch {epoch}/{epochs}: Discriminator Loss: {d_loss[0]:.4f}, Generator Loss: {g_loss:.4f}, Took: {took:.1f}s, log10(lr_D): {np.log10(_lrD):.1f}, prime: {np.mean(Primes)}, qualified: {np.mean(Qualified)}, disc_bincla_power: {discAccBinBr*100:.1f}%%, {discAccBin*100:.1f}%%"
  407. print("\r \r", end='')
  408. print(tr_log)
  409. TR_LOGS.append(tr_log)
  410. tic = time.time()
  411. # Save network periodically
  412. if epoch % 50 == 0 or epoch <= 2 or epoch == max_epochs:
  413. print("SAVING MODELS")
  414. TSA.checkpoint_model_write(f"GAN2_{epoch}/gen", generator, cv_k=k_fold, exp_name=None)
  415. TSA.checkpoint_model_write(f"GAN2_{epoch}/disc", discriminator, cv_k=k_fold, exp_name=None)
  416. if epoch == max_epochs:
  417. return
  418. epoch += 1
  419. del Primes, Qualified
  420. TSA.KB.clear_session(True)
  421. gc.collect()
  422. if __name__ == "__main__":
  423. # k_fold = int(sys.argv[1])
  424. MAX_EPOCHS = 300
  425. MAX_RAM_GB = 5
  426. import multiprocessing
  427. import time
  428. for k_fold in range(0, 10):
  429. print("Working on fold %d" % k_fold)
  430. def find_last_saved_epoch():
  431. import os
  432. try:
  433. all = os.listdir(f"./Output/Models/{PROJ_NAME}/{k_fold}/None/Saved")
  434. except:
  435. all = []
  436. if len(all) == 0:
  437. return 0
  438. else:
  439. all_epochs = [int(name.split("_")[1]) for name in all if name[0] != '_']
  440. return max(all_epochs)
  441. while True:
  442. epoch_resume = find_last_saved_epoch()
  443. if epoch_resume >= MAX_EPOCHS:
  444. print("Maximum epochs is already reached")
  445. break
  446. print("Resuming from epoch %d" % epoch_resume)
  447. p = multiprocessing.Process(target=process_gan_training, args=(k_fold, epoch_resume, 5, 300))
  448. p.start()
  449. print("PROCESS STARTED!")
  450. p.join()
  451. print("PROCESS EXITED!")
  452. time.sleep(5)

train_GAN.py at commit 2f5270f, under GPL-3.0 · at the source

Overview

  1. School of Electrical and Computer Engineering, College of Engineering, University of Tehran, Tehran, Iran
  2. School of Cognitive Sciences, Institute for Research in Fundamental Sciences (IPM), Tehran, Iran
  3. Departments of Research Administration and Radiology, Henry Ford Health System, Detroit, Michigan, United States of America
Journal: PloS one, volume 21, issue 8, article e0354976
Dates: received 2 October 2025; accepted 15 July 2026; published online 21 August 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1371/journal.pone.0354976 · PMID 42627854 · PMCID PMC13497277 · OpenAlex W7203873887
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: EEG (modality), human (organism), methods / tools (subfield)
Methods: Spectral & time-frequency, Connectivity, Statistics, Machine learning
MeSH: Brain-Computer Interfaces*, Algorithms, Bayes Theorem, Compression Algorithms, Data Compression, Electroencephalography, Humans, Signal Processing, Computer-Assisted (* major topic)
Journal subjects: Research and Analysis Methods, Bioassays and Physiological Analysis, Electrophysiological Techniques, Brain Electrophysiology, Electroencephalography, Biology and Life Sciences, Physiology, Electrophysiology, Neurophysiology, Neuroscience, Brain Mapping, Medicine and Health Sciences, Clinical Medicine, Clinical Neurophysiology, Imaging Techniques, Neuroimaging, Engineering and Technology, Electronics Engineering, Computer Engineering, Man-Computer Interface, Computer and Information Sciences, Computer Architecture, Computing Methods, Cloud Computing, Physical Sciences, Mathematics, Statistics, Statistical Data, Data Management, Data Compression, Signal Processing, Signal Filtering, Linear Filters
Topic: EEG and Brain-Computer Interfaces (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Citations: not cited yet (Europe PMC); 47 references in the paper

Abstract

Brain–computer interface (BCI) systems have advanced with deep learning, but they are still limited by designs tied to specific applications, poor scalability, weak portability, the need for user-specific adaptation, and privacy concerns. We present BELT, a modular Bayesian Edge–Cloud architecture based on three principles: (i) Bayesian priors and posteriors to balance generalization and subject-specific learning, (ii) lightweight classifiers suitable for embedded devices, and (iii) task-aware compression to reduce bandwidth and improve privacy in edge–cloud communication. To show feasibility, we implement BELT-lite as an instantiation of BELT, a lightweight version built only from linear time-invariant operations, making it directly compatible with digital signal processing hardware. Using the BCI Competition IV-2a and IV-2b motor imagery datasets (18 subjects total, ten-fold cross-validation), BELT-lite achieved strong posterior performance after subject-specific fine-tuning: mean accuracy of 87.9%±6.8% on Dataset B and 80.6%±8.6% on Dataset A with data augmentation. After adaptation, four subjects from Dataset B and two from Dataset A exceeded 90% accuracy. On ARM Cortex-A7 hardware, BELT-lite achieved a mean latency of 6.75 ms per sample, significantly faster than EEGNet’s 8.36 ms (p < 10-17)—a 21% speed improvement—at the cost of a modest but statistically significant accuracy reduction of approximately 2.7 percentage points compared to EEGNet. Network Tuning Blocks allowed partial parameter freezing: classifier-only fine-tuning incurred a modest 2–5% accuracy drop while substantially reducing training cost. Compression via the task-unaware autoencoder reduced data size by 3.3× while maintaining high accuracy: prior-model performance stayed within ≈1% of the uncompressed baseline (with slight improvements in some configurations), full posterior fine-tuning showed a ≈1% drop, and classifier-only fine-tuning incurred a ≈3% drop—an acceptable trade-off for privacy-preserving edge–cloud communication, where only a compressed latent representation is transmitted instead of raw EEG. Notably, this task-unaware autoencoder (trained solely to reconstruct the input) consistently outperformed autoencoders that also incorporated classification objectives (task-aware or task-only), providing the best accuracy–compression trade-off across all fine-tuning scenarios. These findings show that BELT provides a principled design for modular and scalable BCIs, while BELT-lite demonstrates that the approach supports accurate, efficient, and portable implementations. Together, they point toward BCI systems that are more practical, mass-producible, and privacy-aware, enabling wider use in real-world settings.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repository

Its files are read in the Code ↔ Paper reader above, with 9 matches between paragraphs and lines of code.

adanayi/BELT

License: GPL-3.0
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 2f5270fb5916581751e5614393c67874e1076208, 2 June 2026
Languages: Jupyter (6), Python (3)
Size: 12 files, 9 scripts
Software Heritage: not archived
Found in: “Data Availability”
Holds: README, license file, 6 notebooks
Not found: CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (9 files), Keras (8 files), TensorFlow (7 files), SciPy (6 files), Matplotlib (3 files), pandas (2 files), scikit-posthocs (2 files), seaborn (2 files), MNE-Python (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
11 files

The paper's code and data availability statement is in the Data section.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 9 scripts, each with its path and the digest of its content;
  • 9 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

Datasets cited

Data Availability

All code underlying the findings is available at: https://github.com/adanayi/BELT, in the “research” folder. The repository will be made public upon acceptance. The dataset used in this study is the publicly available BCI Competition IV Dataset 2b, which can be accessed at: http://www.bbci.de/competition/iv/.

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 2 authors, 8 MeSH terms, 37 references.

Cite

This paper

Danayi, A., & Soltanian-Zadeh, H. (2026). Improved motor imagery BCI performance via task-unaware compression in the BELT Bayesian Edge-Cloud architecture. PloS one, 21(8), e0354976. https://doi.org/10.1371/journal.pone.0354976

BibTeX

@article{danayi2026improved,
author = {Danayi, Abolfazl and Soltanian-Zadeh, Hamid},
title = {{Improved motor imagery BCI performance via task-unaware compression in the BELT Bayesian Edge-Cloud architecture}},
journal = {PloS one},
year = {2026},
month = aug,
volume = {21},
number = {8},
pages = {e0354976},
publisher = {PLOS},
issn = {1932-6203},
doi = {10.1371/journal.pone.0354976},
url = {https://doi.org/10.1371/journal.pone.0354976},
pmid = {42627854},
pmcid = {PMC13497277}
}

RIS

TY - JOUR
AU - Danayi, Abolfazl
AU - Soltanian-Zadeh, Hamid
TI - Improved motor imagery BCI performance via task-unaware compression in the BELT Bayesian Edge-Cloud architecture
T2 - PloS one
J2 - PLoS One
PY - 2026
DA - 2026/08/21
VL - 21
IS - 8
SP - e0354976
SN - 1932-6203
PB - PLOS
DO - 10.1371/journal.pone.0354976
UR - https://doi.org/10.1371/journal.pone.0354976
LA - en
ER -

CSL-JSON

{
"id": "10.1371/journal.pone.0354976",
"type": "article-journal",
"title": "Improved motor imagery BCI performance via task-unaware compression in the BELT Bayesian Edge-Cloud architecture",
"container-title": "PloS one",
"author": [
{
"family": "Danayi",
"given": "Abolfazl"
},
{
"family": "Soltanian-Zadeh",
"given": "Hamid"
}
],
"container-title-short": "PLoS One",
"volume": "21",
"issue": "8",
"page": "e0354976",
"DOI": "10.1371/journal.pone.0354976",
"PMID": "42627854",
"PMCID": "PMC13497277",
"ISSN": "1932-6203",
"publisher": "PLOS",
"URL": "https://doi.org/10.1371/journal.pone.0354976",
"language": "en",
"issued": {
"date-parts": [
[
2026,
8,
21
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1371/journal.pone.0347671 [code]
RMETNet: A cross-subject motor imagery EEG signal classification model based on TSLANet and riemannian geometry features.
Journal: PloS one
In common: TensorFlow, MNE-Python, seaborn, 4 other tools, methods / tools, EEG, 2 references
[2] doi:10.1038/s41746-026-02778-0 [code]
Trust-gated synthetic EEG augmentation reduces performance drops when generalizing to new patients.
Journal: NPJ digital medicine
In common: MNE-Python, pandas, SciPy, 2 other tools, bbci.de/competition/iv, EEG, 1 reference
[3] doi:10.1038/s41598-026-52330-z [code]
SHAP analysis of an improved EEG-based mental workload classification framework: utilizing data augmentation and explainable AI.
Journal: Scientific reports
In common: Keras, TensorFlow, MNE-Python, 4 other tools, methods / tools, EEG, 1 reference
[4] doi:10.1038/s41597-025-05174-7 [code]
A large-scale MEG and EEG dataset for object recognition in naturalistic scenes
Journal: n/a
In common: Keras, TensorFlow, MNE-Python, 5 other tools, methods / tools, EEG
[5] doi:10.1038/s41598-026-47627-y [code]
QuantumNeuroXAI: a quantum-inspired deep learning framework with explainability for brain signal analysis and neurological disorder detection.
Journal: Scientific reports
In common: MNE-Python, pandas, SciPy, 2 other tools, bbci.de/competition/iv, methods / tools, EEG
[6] doi:10.3390/s26175327 [code]
Subject Identity Confounds qEEG Emotion Recognition on DEAP and DREAMER.
Journal: Sensors (Basel, Switzerland)
In common: Keras, TensorFlow, MNE-Python, 5 other tools, EEG
[7] doi:10.3390/s26051730 [code]
SFE-GAT: Structure-Feature Evolution Graph Attention Network for Motor Imagery Decoding.
Journal: Sensors (Basel, Switzerland)
In common: SciPy, Matplotlib, NumPy, bbci.de/competition/iv, EEG, 2 references
[8] doi:10.3389/fpsyg.2026.1774068 [code]
Analysis of cognitive mechanisms in phoneme perception and pronunciation errors among Korean language learners.
Journal: Frontiers in psychology
In common: Keras, TensorFlow, MNE-Python, 5 other tools, EEG
[9] doi:10.3390/biomimetics11060377
PG-MCTFormer: A Prior-Guided Multi-Scale Convolutional Transformer for Interpretable Motor Imagery EEG Classification.
Journal: Biomimetics (Basel, Switzerland)
In common: bbci.de/competition/iv, EEG, 3 references
[10] doi:10.1038/s41598-026-52672-8 [code]
Validation of portable, semi-dry electrode-based electroencephalography device for its application in brain-computer interface solutions.
Journal: Scientific reports
In common: Keras, SciPy, Matplotlib, 1 other tool, methods / tools, EEG, 2 references

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.