OSCR

EEG-Based Supported Diagnosis of ADHD Using Subject-Specific HMMs and Stationary RKHS Embeddings.

Code ↔ Paper

12 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 12 matches · 2 of them tie a paragraph to a whole file, not to given lines: weak matches, whose lines are not tinted
  1. [1] § 2. Materials and Methods › 2.3. Preprocessing ↔ baseline/baseline.py, lines 131–143 · score 0.83 · 13–30 Hz, 0.5–4 Hz, 8–13 Hz, 4–8 Hz, theta, delta
  2. [2] § 3. Results › 3.1. Performance Analysis on the Synthetic Dataset ↔ single held out test/rkhs-knn-svm-eeg-dataset.ipynb, lines 1037–1185 · score 0.77 · full training, RKHS kernel, AUC ROC, Optuna, cross validation, Matthews
  3. [3] § 3. Results › 3.3. Performance Analysis on the Real EEG Dataset ↔ baseline/baseline.py, lines 1–61 · score 0.73 · Alim, Imtiaz, reimplemented, Hjorth, baseline, theta
  4. [4] § 2. Materials and Methods › 2.4. Hidden Markov Model-Based Time-Series Representation ↔ training-hmm.ipynb, lines 649–745 · score 0.71 · Baum Welch, log likelihood, EM, iterations, fit, training
  5. [5] § 2. Materials and Methods › 2.1. Synthetic EEG Benchmark Signals ↔ Synthetic_EEG_data_generation.ipynb, the whole file · a weak match · score 0.66 · frequency bands, EEG signals, healthy, noise, simulated, synthetic
  6. [6] § 3. Results › 3.2. Hyperparameter Tuning ↔ single held out test/rkhs-knn-svm-eeg-dataset.ipynb, lines 1037–1185 · score 0.65 · kernel transformation, AUC ROC, confusion matrices, Matthews, balanced accuracy, bandwidth
  7. [7] § 2. Materials and Methods ↔ single held out test/rkhs-knn-svm-eeg-dataset.ipynb, lines 832–908 · score 0.62 · HMM GMM model, distance computation, precomputed kernel, RKHS, EEG
  8. [8] § 2. Materials and Methods › 2.1. Synthetic EEG Benchmark Signals ↔ baseline/baseline.py, lines 1–61 · score 0.61 · wave, Hz, brain, theta, phase, delta
  9. [9] § 2. Materials and Methods › 2.6. Classification with Precomputed Kernels ↔ single held out test/rkhs-knn-svm-eeg-dataset.ipynb, lines 798–830 · score 0.60 · positive semi definite, kernel matrix, SVM
  10. [10] § 2. Materials and Methods › 2.1. Synthetic EEG Benchmark Signals ↔ Synthetic_EEG_data_generation.ipynb, the whole file · a weak match · score 0.57 · wave, uniform, noise, theta, delta, simulated
  11. [11] § 3. Results › 3.2. Hyperparameter Tuning ↔ training-hmm.ipynb, lines 1143–1229 · score 0.52 · Log likelihood, HMM convergence, iteration, errors, fit
  12. [12] § 2. Materials and Methods › 2.6. Classification with Precomputed Kernels ↔ nested CV/PPK_with_knn_and_svm.ipynb, lines 293–317 · score 0.51 · Gram matrix, unnecessary, majority, formulation, SVM, distances

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

Jupyter notebook · 1,221 lines · 47 KB · MIT · 4 matches

  1. # %%
  2. # --- Install required packages ---
  3. !pip install -q tensorflow tensorflow-probability scikit-learn matplotlib pandas optuna
  4. # %% [markdown]
  5. # # RKHS with precomputed distance and KNN Classification
  6. # %%
  7. import numpy as np
  8. import pandas as pd
  9. from sklearn.neighbors import KNeighborsClassifier
  10. from sklearn.model_selection import StratifiedKFold, train_test_split
  11. from sklearn.metrics import (
  12. classification_report, accuracy_score, confusion_matrix,
  13. balanced_accuracy_score, roc_auc_score, matthews_corrcoef,
  14. roc_curve, precision_recall_curve, auc
  15. )
  16. import matplotlib.pyplot as plt
  17. import seaborn as sns
  18. import optuna
  19. from optuna.samplers import TPESampler
  20. import warnings
  21. import scipy.linalg
  22. warnings.filterwarnings('ignore')
  23. # -----------------------------
  24. # Stationary distribution utils
  25. # -----------------------------
  26. def compute_stationary_distribution(A, max_iter=1000, rel_tol=1e-6, abs_tol=1e-10):
  27. A = np.asarray(A, dtype=float)
  28. n = A.shape[0]
  29. # Row-stochastic guard (and all-zero row fix)
  30. rs = A.sum(axis=1, keepdims=True)
  31. bad = (rs[:, 0] == 0)
  32. if bad.any():
  33. A[bad] = 1.0 / n
  34. rs = A.sum(axis=1, keepdims=True)
  35. A = A / rs
  36. pi = np.ones(n) / n
  37. for _ in range(max_iter):
  38. pi_new = pi @ A
  39. if np.linalg.norm(pi_new - pi, 1) <= rel_tol * np.linalg.norm(pi, 1) + abs_tol:
  40. pi = pi_new
  41. break
  42. pi = pi_new
  43. pi = np.maximum(pi, 0.0)
  44. s = pi.sum()
  45. return pi / (s if s > 0 else 1.0)
  46. # -----------------------------------------
  47. # Exact multivariate Gaussian-Gaussian RBF
  48. # -----------------------------------------
  49. def k_gauss_of_gaussians(mu1, S1, mu2, S2, ell, eps=1e-9):
  50. mu1 = np.asarray(mu1); mu2 = np.asarray(mu2)
  51. S1 = np.asarray(S1); S2 = np.asarray(S2)
  52. D = mu1.shape[0]
  53. S1 = S1 + eps * np.eye(D)
  54. S2 = S2 + eps * np.eye(D)
  55. L_det = np.linalg.cholesky(np.eye(D) + (S1 + S2) / max(ell, eps))
  56. log_det_term = -np.sum(np.log(np.diag(L_det)))
  57. M = S1 + S2 + ell * np.eye(D)
  58. Lm = np.linalg.cholesky(M)
  59. d = mu1 - mu2
  60. y = scipy.linalg.solve_triangular(Lm, d, lower=True)
  61. quad = -0.5 * float(y @ y)
  62. return float(np.exp(log_det_term + quad))
  63. # -------------------------------------------------------
  64. # CSV extraction, with auto-detected feature dimensionality
  65. # -------------------------------------------------------
  66. def infer_n_features(df, n_states, n_components):
  67. f_idxs = []
  68. prefix = f"gmm_mean_0_0_f"
  69. for c in df.columns:
  70. if c.startswith(prefix):
  71. try:
  72. f_idxs.append(int(c.split('f')[-1]))
  73. except Exception:
  74. pass
  75. return (max(f_idxs) + 1) if f_idxs else 1
  76. def extract_hmm_parameters_from_csv(df, n_states=3, n_components=3, n_features=None):
  77. if n_features is None:
  78. n_features = infer_n_features(df, n_states, n_components)
  79. models = []
  80. print(f"Extracting HMM parameters from {len(df)} models...")
  81. print(f"Config inferred: n_states={n_states}, n_components={n_components}, n_features={n_features}")
  82. has_pi_s = all([f"stationary_pi_{i}" in df.columns for i in range(n_states)])
  83. has_w_eff = f"w_eff_0_0" in df.columns
  84. for idx, row in df.iterrows():
  85. try:
  86. # Transition matrix A
  87. A = np.zeros((n_states, n_states), dtype=float)
  88. for i in range(n_states):
  89. for j in range(n_states):
  90. A[i, j] = row[f"A_{i}{j}"]
  91. rs = A.sum(axis=1, keepdims=True); rs[rs == 0] = 1
  92. A = A / rs
  93. # stationary pi
  94. if has_pi_s:
  95. pi_s = np.array([row[f"stationary_pi_{i}"] for i in range(n_states)], dtype=float)
  96. if (not np.isfinite(pi_s).all()) or (pi_s.sum() <= 1e-12) or (pi_s < -1e-12).any():
  97. pi_s = compute_stationary_distribution(A)
  98. else:
  99. pi_s = np.maximum(pi_s, 0.0)
  100. s = pi_s.sum()
  101. pi_s = pi_s / (s if s > 0 else 1.0)
  102. else:
  103. pi_s = compute_stationary_distribution(A)
  104. # mixture weights alpha and means/covs
  105. alpha = np.zeros((n_states, n_components), dtype=float)
  106. mu = np.zeros((n_states, n_components, n_features), dtype=float)
  107. sigma = np.zeros((n_states, n_components, n_features, n_features), dtype=float)
  108. for i in range(n_states):
  109. for k in range(n_components):
  110. if has_w_eff:
  111. alpha[i, k] = row.get(f"gmm_weight_{i}_{k}", 0.0)
  112. else:
  113. alpha[i, k] = row[f"gmm_weight_{i}_{k}"]
  114. for f in range(n_features):
  115. mu[i, k, f] = row[f"gmm_mean_{i}_{k}_f{f}"]
  116. Si = np.zeros((n_features, n_features), dtype=float)
  117. for f1 in range(n_features):
  118. for f2 in range(f1, n_features):
  119. Si[f1, f2] = row[f"gmm_cov_{i}_{k}_f{f1}f{f2}"]
  120. if f1 != f2:
  121. Si[f2, f1] = Si[f1, f2]
  122. Si += 1e-6 * np.eye(n_features)
  123. try:
  124. np.linalg.cholesky(Si)
  125. except np.linalg.LinAlgError:
  126. Si = np.eye(n_features) * 0.1
  127. sigma[i, k] = Si
  128. if not has_w_eff:
  129. alpha = np.maximum(alpha, 0.0)
  130. sums = alpha.sum(axis=1, keepdims=True); sums[sums == 0] = 1.0
  131. alpha = alpha / sums
  132. model = {
  133. 'pi_stationary': pi_s,
  134. 'alpha': alpha,
  135. 'mu': mu,
  136. 'sigma': sigma,
  137. 'transition_matrix': A,
  138. 'n_states': n_states,
  139. 'n_components': n_components,
  140. 'n_features': n_features,
  141. 'model_id': idx,
  142. 'dataset_type': row.get('dataset_type', 'unknown'),
  143. 'series_id': row.get('series_id', idx)
  144. }
  145. if has_w_eff:
  146. w_eff = np.array(
  147. [row[f"w_eff_{i}_{k}"] for i in range(n_states) for k in range(n_components)],
  148. dtype=float
  149. )
  150. model['w_eff'] = w_eff
  151. models.append(model)
  152. except Exception as e:
  153. print(f"Error processing model {idx}: {str(e)}")
  154. continue
  155. print(f"Successfully extracted parameters for {len(models)} models")
  156. return models
  157. # --------------------------
  158. # Collapse to component lists
  159. # --------------------------
  160. def model_to_components(model):
  161. K = model['n_states']; M = model['n_components']; D = model['n_features']
  162. pi_s = model['pi_stationary']
  163. alpha = model['alpha']
  164. mu = model['mu']
  165. sigma = model['sigma']
  166. if 'w_eff' in model:
  167. w = model['w_eff'].copy()
  168. else:
  169. w = np.array([pi_s[i] * alpha[i, k] for i in range(K) for k in range(M)], dtype=float)
  170. MU = np.array([mu[i, k, :] for i in range(K) for k in range(M)], dtype=float)
  171. SIG = np.array([sigma[i, k, :, :] for i in range(K) for k in range(M)], dtype=float)
  172. return w, MU, SIG
  173. # -------------------------------------
  174. # Distance (MMD²) and matrix computation
  175. # -------------------------------------
  176. def enhanced_rkhs_distance_multivariate(modelP, modelQ, sigma_kernel):
  177. wP, MUP, SIGP = model_to_components(modelP)
  178. wQ, MUQ, SIGQ = model_to_components(modelQ)
  179. term_PP = 0.0
  180. for i in range(len(wP)):
  181. for j in range(len(wP)):
  182. term_PP += wP[i] * wP[j] * k_gauss_of_gaussians(MUP[i], SIGP[i], MUP[j], SIGP[j], sigma_kernel)
  183. term_QQ = 0.0
  184. for i in range(len(wQ)):
  185. for j in range(len(wQ)):
  186. term_QQ += wQ[i] * wQ[j] * k_gauss_of_gaussians(MUQ[i], SIGQ[i], MUQ[j], SIGQ[j], sigma_kernel)
  187. term_PQ = 0.0
  188. for i in range(len(wP)):
  189. for j in range(len(wQ)):
  190. term_PQ += wP[i] * wQ[j] * k_gauss_of_gaussians(MUP[i], SIGP[i], MUQ[j], SIGQ[j], sigma_kernel)
  191. dist2 = term_PP + term_QQ - 2.0 * term_PQ
  192. return max(float(dist2), 0.0)
  193. def compute_distance_matrix(models, sigma_kernel):
  194. n = len(models)
  195. D = np.zeros((n, n), dtype=float)
  196. print(f"Computing {n}x{n} RKHS distance matrix (σ={sigma_kernel:.6g})...")
  197. comps = [model_to_components(m) for m in models]
  198. def dist2_from_comps(cP, cQ):
  199. wP, MUP, SIGP = cP
  200. wQ, MUQ, SIGQ = cQ
  201. term_PP = 0.0
  202. for i in range(len(wP)):
  203. for j in range(len(wP)):
  204. term_PP += wP[i] * wP[j] * k_gauss_of_gaussians(MUP[i], SIGP[i], MUP[j], SIGP[j], sigma_kernel)
  205. term_QQ = 0.0
  206. for i in range(len(wQ)):
  207. for j in range(len(wQ)):
  208. term_QQ += wQ[i] * wQ[j] * k_gauss_of_gaussians(MUQ[i], SIGQ[i], MUQ[j], SIGQ[j], sigma_kernel)
  209. term_PQ = 0.0
  210. for i in range(len(wP)):
  211. for j in range(len(wQ)):
  212. term_PQ += wP[i] * wQ[j] * k_gauss_of_gaussians(MUP[i], SIGP[i], MUQ[j], SIGQ[j], sigma_kernel)
  213. return max(float(term_PP + term_QQ - 2.0 * term_PQ), 0.0)
  214. for i in range(n):
  215. D[i, i] = 0.0
  216. for j in range(i + 1, n):
  217. d2 = dist2_from_comps(comps[i], comps[j])
  218. D[i, j] = D[j, i] = d2
  219. if (i + 1) % 20 == 0:
  220. print(f" Processed {i + 1}/{n} rows")
  221. print(f"Done. Stats: min={D.min():.6f}, max={D.max():.6f}, mean={D.mean():.6f}")
  222. return D
  223. # ----------------------------
  224. # Cross-validation (precompute)
  225. # ----------------------------
  226. def kfold_cross_validation(models, labels, sigma_kernel, k_neighbors, n_splits=5, random_state=42):
  227. skf = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=random_state)
  228. full_D = compute_distance_matrix(models, sigma_kernel)
  229. fold_results = {
  230. 'accuracy': [], 'balanced_accuracy': [], 'auc_roc': [], 'mcc': [],
  231. 'predictions': [], 'probabilities': [], 'true_labels': [],
  232. 'confusion_matrices': []
  233. }
  234. print(f"Performing {n_splits}-fold cross-validation...")
  235. for fold, (train_idx, val_idx) in enumerate(skf.split(models, labels)):
  236. print(f" Fold {fold + 1}/{n_splits}")
  237. train_D = full_D[np.ix_(train_idx, train_idx)]
  238. val_train_D = full_D[np.ix_(val_idx, train_idx)]
  239. knn = KNeighborsClassifier(n_neighbors=k_neighbors, metric='precomputed', weights='distance')
  240. knn.fit(train_D, labels[train_idx])
  241. val_pred = knn.predict(val_train_D)
  242. val_prob = knn.predict_proba(val_train_D)[:, 1]
  243. fold_results['predictions'].extend(val_pred.tolist())
  244. fold_results['probabilities'].extend(val_prob.tolist())
  245. fold_results['true_labels'].extend(labels[val_idx].tolist())
  246. acc = accuracy_score(labels[val_idx], val_pred)
  247. bacc = balanced_accuracy_score(labels[val_idx], val_pred)
  248. auc_ = roc_auc_score(labels[val_idx], val_prob)
  249. mcc = matthews_corrcoef(labels[val_idx], val_pred)
  250. cm = confusion_matrix(labels[val_idx], val_pred)
  251. fold_results['accuracy'].append(acc)
  252. fold_results['balanced_accuracy'].append(bacc)
  253. fold_results['auc_roc'].append(auc_)
  254. fold_results['mcc'].append(mcc)
  255. fold_results['confusion_matrices'].append(cm)
  256. print(f" Acc={acc:.4f}, BalAcc={bacc:.4f}, AUC={auc_:.4f}, MCC={mcc:.4f}")
  257. for k in ['accuracy', 'balanced_accuracy', 'auc_roc', 'mcc']:
  258. fold_results[k] = np.array(fold_results[k])
  259. fold_results['predictions'] = np.array(fold_results['predictions'])
  260. fold_results['probabilities'] = np.array(fold_results['probabilities'])
  261. fold_results['true_labels'] = np.array(fold_results['true_labels'])
  262. return fold_results
  263. # -----------------------------
  264. # Optuna (precompute per sigma too)
  265. # -----------------------------
  266. def hyperparameter_optimization(models, labels, sigma_kernel_range=(1e-6, 1.0),
  267. k_neighbors_range=(1, 15), n_trials=50,
  268. n_splits=5, timeout=1800):
  269. print("Starting hyperparameter optimization...")
  270. min_k, max_k = k_neighbors_range
  271. max_k = min(max_k, len(models) - 1)
  272. valid_k_values = [k for k in range(min_k, max_k + 1) if k % 2 == 1]
  273. print(f"Search space:")
  274. print(f"- sigma_kernel: log-uniform from {sigma_kernel_range[0]:.1e} to {sigma_kernel_range[1]:.1e}")
  275. print(f"- k_neighbors: {valid_k_values}")
  276. print(f"- n_trials: {n_trials}, CV folds: {n_splits}")
  277. def objective(trial):
  278. sigma_kernel = trial.suggest_float('sigma_kernel', sigma_kernel_range[0], sigma_kernel_range[1], log=True)
  279. k_neighbors = trial.suggest_categorical('k_neighbors', valid_k_values)
  280. try:
  281. full_D = compute_distance_matrix(models, sigma_kernel)
  282. skf = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=42)
  283. scores = []
  284. for train_idx, val_idx in skf.split(models, labels):
  285. train_D = full_D[np.ix_(train_idx, train_idx)]
  286. val_train_D = full_D[np.ix_(val_idx, train_idx)]
  287. knn = KNeighborsClassifier(n_neighbors=k_neighbors, metric='precomputed', weights='distance')
  288. knn.fit(train_D, labels[train_idx])
  289. pred = knn.predict(val_train_D)
  290. scores.append(balanced_accuracy_score(labels[val_idx], pred))
  291. return float(np.mean(scores))
  292. except Exception as e:
  293. print(f"Trial failed: {e}")
  294. return 0.0
  295. study = optuna.create_study(direction='maximize', sampler=TPESampler(seed=42),
  296. study_name="RKHS_KNN_Optimization")
  297. study.optimize(objective, n_trials=n_trials, timeout=timeout)
  298. print("\nOptimization completed!")
  299. print(f"Best balanced accuracy: {study.best_value:.4f}")
  300. print(f"Best parameters: {study.best_params}")
  301. return study
  302. # -----------------
  303. # Evaluation plots
  304. # -----------------
  305. def create_evaluation_plots(fold_results, class_names=['Control', 'ADHD']):
  306. fig, axes = plt.subplots(2, 3, figsize=(18, 12))
  307. overall_cm = confusion_matrix(fold_results['true_labels'], fold_results['predictions'])
  308. sns.heatmap(overall_cm, annot=True, fmt='d', cmap='Blues',
  309. xticklabels=class_names, yticklabels=class_names, ax=axes[0,0])
  310. axes[0,0].set_title('Overall Confusion Matrix'); axes[0,0].set_xlabel('Predicted'); axes[0,0].set_ylabel('Actual')
  311. fpr, tpr, _ = roc_curve(fold_results['true_labels'], fold_results['probabilities'])
  312. auc_score = auc(fpr, tpr)
  313. axes[0,1].plot(fpr, tpr, lw=2, label=f'ROC (AUC={auc_score:.3f})')
  314. axes[0,1].plot([0, 1], [0, 1], lw=2, linestyle='--', label='Random')
  315. axes[0,1].set_xlabel('FPR'); axes[0,1].set_ylabel('TPR'); axes[0,1].set_title('ROC Curve'); axes[0,1].legend(); axes[0,1].grid(True, alpha=0.3)
  316. precision, recall, _ = precision_recall_curve(fold_results['true_labels'], fold_results['probabilities'])
  317. pr_auc = auc(recall, precision)
  318. axes[0,2].plot(recall, precision, lw=2, label=f'PR (AUC={pr_auc:.3f})')
  319. axes[0,2].set_xlabel('Recall'); axes[0,2].set_ylabel('Precision'); axes[0,2].set_title('Precision-Recall'); axes[0,2].legend(); axes[0,2].grid(True, alpha=0.3)
  320. metrics_data = [fold_results['accuracy'], fold_results['balanced_accuracy'], fold_results['auc_roc'], fold_results['mcc']]
  321. axes[1,0].boxplot(metrics_data, labels=['Acc', 'Bal.Acc', 'AUC', 'MCC'])
  322. axes[1,0].set_title('CV Metric Distributions'); axes[1,0].set_ylabel('Score'); axes[1,0].grid(True, alpha=0.3)
  323. folds = np.arange(1, len(fold_results['accuracy']) + 1)
  324. axes[1,1].plot(folds, fold_results['balanced_accuracy'], 'o-', label='Balanced Acc')
  325. axes[1,1].plot(folds, fold_results['auc_roc'], 'o-', label='AUC-ROC')
  326. axes[1,1].set_xlabel('Fold'); axes[1,1].set_ylabel('Score'); axes[1,1].set_title('Performance by Fold'); axes[1,1].legend(); axes[1,1].grid(True, alpha=0.3)
  327. class_counts = np.bincount(fold_results['true_labels'])
  328. axes[1,2].pie(class_counts, labels=class_names, autopct='%1.1f%%', startangle=90)
  329. axes[1,2].set_title('Class Distribution')
  330. plt.tight_layout(); plt.show()
  331. # -----------------------
  332. # Full analysis pipeline
  333. # -----------------------
  334. def run_complete_analysis(csv_path, test_size=0.2, random_state=42,
  335. sigma_kernel_range=(1e-6, 1.0), k_neighbors_range=(1, 15),
  336. n_trials=50, n_splits=5, timeout=1800,
  337. n_states=3, n_components=3, n_features=None):
  338. print("=== RKHS-KNN ANALYSIS FOR HMM-GMM MODELS ===\n")
  339. print("Loading CSV...")
  340. df = pd.read_csv(csv_path)
  341. df['class_label'] = (df['dataset_type'] == 'ADHD').astype(int)
  342. print(f"Dataset summary:")
  343. print(f" Total: {len(df)} | ADHD: {df['class_label'].sum()} | Control: {(1-df['class_label']).sum()}")
  344. print(f" Class balance: {df['class_label'].mean():.3f}")
  345. train_df, test_df = train_test_split(
  346. df, test_size=test_size, random_state=random_state, stratify=df['class_label']
  347. )
  348. if n_features is None:
  349. n_features = infer_n_features(train_df, n_states, n_components)
  350. print("\nExtracting HMM-GMM parameters...")
  351. train_models = extract_hmm_parameters_from_csv(train_df, n_states, n_components, n_features)
  352. train_labels = train_df['class_label'].values
  353. test_models = extract_hmm_parameters_from_csv(test_df, n_states, n_components, n_features)
  354. test_labels = test_df['class_label'].values
  355. study = hyperparameter_optimization(
  356. train_models, train_labels,
  357. sigma_kernel_range=sigma_kernel_range,
  358. k_neighbors_range=k_neighbors_range,
  359. n_trials=n_trials, n_splits=n_splits, timeout=timeout
  360. )
  361. best_params = study.best_params
  362. print("\nCross-validation with best params...")
  363. fold_results = kfold_cross_validation(
  364. train_models, train_labels,
  365. best_params['sigma_kernel'], best_params['k_neighbors'],
  366. n_splits=n_splits, random_state=random_state
  367. )
  368. print("\n" + "="*60)
  369. print("CROSS-VALIDATION RESULTS")
  370. print("="*60)
  371. print(f" σ_kernel: {best_params['sigma_kernel']:.6g}")
  372. print(f" k_neighbors: {best_params['k_neighbors']}")
  373. for name, key in [('Accuracy', 'accuracy'), ('Balanced Accuracy', 'balanced_accuracy'),
  374. ('AUC-ROC','auc_roc'), ('MCC','mcc')]:
  375. vals = fold_results[key]
  376. print(f" {name}: {vals.mean():.4f} ± {vals.std():.4f}")
  377. print("\n" + "="*60)
  378. print("FINAL TEST SET EVALUATION")
  379. print("="*60)
  380. train_D = compute_distance_matrix(train_models, best_params['sigma_kernel'])
  381. all_models = train_models + test_models
  382. full_D = compute_distance_matrix(all_models, best_params['sigma_kernel'])
  383. n_train = len(train_models)
  384. test_train_D = full_D[n_train:, :n_train]
  385. final_knn = KNeighborsClassifier(n_neighbors=best_params['k_neighbors'], metric='precomputed')
  386. final_knn.fit(train_D, train_labels)
  387. test_pred = final_knn.predict(test_train_D)
  388. test_prob = final_knn.predict_proba(test_train_D)[:, 1]
  389. test_accuracy = accuracy_score(test_labels, test_pred)
  390. test_bal_acc = balanced_accuracy_score(test_labels, test_pred)
  391. test_auc = roc_auc_score(test_labels, test_prob)
  392. test_mcc = matthews_corrcoef(test_labels, test_pred)
  393. test_cm = confusion_matrix(test_labels, test_pred)
  394. print(f" Accuracy: {test_accuracy:.4f}")
  395. print(f" Balanced Accuracy: {test_bal_acc:.4f}")
  396. print(f" AUC-ROC: {test_auc:.4f}")
  397. print(f" Matthews Corr: {test_mcc:.4f}")
  398. print("\nTest Confusion Matrix:")
  399. print(f" Control ADHD")
  400. print(f"Control {test_cm[0,0]:<7} {test_cm[0,1]:<7}")
  401. print(f"ADHD {test_cm[1,0]:<7} {test_cm[1,1]:<7}")
  402. create_evaluation_plots(fold_results)
  403. return {
  404. 'study': study,
  405. 'best_params': best_params,
  406. 'fold_results': fold_results,
  407. 'test_results': {
  408. 'accuracy': test_accuracy,
  409. 'balanced_accuracy': test_bal_acc,
  410. 'auc_roc': test_auc,
  411. 'mcc': test_mcc,
  412. 'confusion_matrix': test_cm
  413. }
  414. }
  415. if __name__ == "__main__":
  416. results = run_complete_analysis(
  417. # csv_path='.../synthetic_hmms_results.csv',
  418. csv_path='.../NxGx/hmm_results.csv',
  419. test_size=0.2,
  420. random_state=42,
  421. sigma_kernel_range=(0.001, 0.9),
  422. k_neighbors_range=(1, 11),
  423. n_trials=200,
  424. n_splits=7, # k-fold
  425. timeout=(60 * 60 * 3), # 60[sec]*60[min]*X[hour]
  426. n_states=3, #4, 5
  427. n_components=3, #4 ,5
  428. n_features=None
  429. )
  430. print("\n" + "="*60)
  431. print("ANALYSIS COMPLETE")
  432. print("="*60)
  433. print(f"Best σ_kernel: {results['best_params']['sigma_kernel']:.6g}")
  434. print(f"Best k_neighbors: {results['best_params']['k_neighbors']}")
  435. print(f"Final test balanced accuracy: {results['test_results']['balanced_accuracy']:.4f}")
  436. # %% [markdown]
  437. # # RKHS with precomputed distance and SVM Classification
  438. # %%
  439. import numpy as np
  440. import pandas as pd
  441. from sklearn.svm import SVC
  442. from sklearn.model_selection import StratifiedKFold, train_test_split
  443. from sklearn.metrics import (
  444. classification_report, accuracy_score, confusion_matrix,
  445. balanced_accuracy_score, roc_auc_score, matthews_corrcoef,
  446. roc_curve, precision_recall_curve, auc
  447. )
  448. import matplotlib.pyplot as plt
  449. import seaborn as sns
  450. import optuna
  451. from optuna.samplers import TPESampler
  452. import warnings
  453. import scipy.linalg
  454. warnings.filterwarnings('ignore')
  455. # -----------------------------
  456. # Stationary distribution utils
  457. # -----------------------------
  458. def compute_stationary_distribution(A, max_iter=1000, rel_tol=1e-6, abs_tol=1e-10):
  459. A = np.asarray(A, dtype=float)
  460. n = A.shape[0]
  461. rs = A.sum(axis=1, keepdims=True)
  462. bad = (rs[:, 0] == 0)
  463. if bad.any():
  464. A[bad] = 1.0 / n
  465. rs = A.sum(axis=1, keepdims=True)
  466. A = A / rs
  467. pi = np.ones(n) / n
  468. for _ in range(max_iter):
  469. pi_new = pi @ A
  470. if np.linalg.norm(pi_new - pi, 1) <= rel_tol * np.linalg.norm(pi, 1) + abs_tol:
  471. pi = pi_new
  472. break
  473. pi = pi_new
  474. pi = np.maximum(pi, 0.0)
  475. s = pi.sum()
  476. return pi / (s if s > 0 else 1.0)
  477. # -----------------------------------------
  478. # Exact multivariate Gaussian-Gaussian RBF
  479. # -----------------------------------------
  480. def k_gauss_of_gaussians(mu1, S1, mu2, S2, ell, eps=1e-9):
  481. mu1 = np.asarray(mu1); mu2 = np.asarray(mu2)
  482. S1 = np.asarray(S1); S2 = np.asarray(S2)
  483. D = mu1.shape[0]
  484. S1 = S1 + eps * np.eye(D)
  485. S2 = S2 + eps * np.eye(D)
  486. L_det = np.linalg.cholesky(np.eye(D) + (S1 + S2) / max(ell, eps))
  487. log_det_term = -np.sum(np.log(np.diag(L_det)))
  488. M = S1 + S2 + ell * np.eye(D)
  489. Lm = np.linalg.cholesky(M)
  490. d = mu1 - mu2
  491. y = scipy.linalg.solve_triangular(Lm, d, lower=True)
  492. quad = -0.5 * float(y @ y)
  493. return float(np.exp(log_det_term + quad))
  494. # -------------------------------------------------------
  495. # CSV extraction, with auto-detected feature dimensionality
  496. # -------------------------------------------------------
  497. def infer_n_features(df, n_states, n_components):
  498. f_idxs = []
  499. prefix = f"gmm_mean_0_0_f"
  500. for c in df.columns:
  501. if c.startswith(prefix):
  502. try:
  503. f_idxs.append(int(c.split('f')[-1]))
  504. except Exception:
  505. pass
  506. return (max(f_idxs) + 1) if f_idxs else 1
  507. def extract_hmm_parameters_from_csv(df, n_states=3, n_components=3, n_features=None):
  508. if n_features is None:
  509. n_features = infer_n_features(df, n_states, n_components)
  510. models = []
  511. print(f"Extracting HMM parameters from {len(df)} models...")
  512. print(f"Config inferred: n_states={n_states}, n_components={n_components}, n_features={n_features}")
  513. has_pi_s = all([f"stationary_pi_{i}" in df.columns for i in range(n_states)])
  514. has_w_eff = f"w_eff_0_0" in df.columns
  515. for idx, row in df.iterrows():
  516. try:
  517. A = np.zeros((n_states, n_states), dtype=float)
  518. for i in range(n_states):
  519. for j in range(n_states):
  520. A[i, j] = row[f"A_{i}{j}"]
  521. rs = A.sum(axis=1, keepdims=True); rs[rs == 0] = 1
  522. A = A / rs
  523. if has_pi_s:
  524. pi_s = np.array([row[f"stationary_pi_{i}"] for i in range(n_states)], dtype=float)
  525. if (not np.isfinite(pi_s).all()) or (pi_s.sum() <= 1e-12) or (pi_s < -1e-12).any():
  526. pi_s = compute_stationary_distribution(A)
  527. else:
  528. pi_s = np.maximum(pi_s, 0.0)
  529. s = pi_s.sum()
  530. pi_s = pi_s / (s if s > 0 else 1.0)
  531. else:
  532. pi_s = compute_stationary_distribution(A)
  533. alpha = np.zeros((n_states, n_components), dtype=float)
  534. mu = np.zeros((n_states, n_components, n_features), dtype=float)
  535. sigma = np.zeros((n_states, n_components, n_features, n_features), dtype=float)
  536. for i in range(n_states):
  537. for k in range(n_components):
  538. if has_w_eff:
  539. alpha[i, k] = row.get(f"gmm_weight_{i}_{k}", 0.0)
  540. else:
  541. alpha[i, k] = row[f"gmm_weight_{i}_{k}"]
  542. for f in range(n_features):
  543. mu[i, k, f] = row[f"gmm_mean_{i}_{k}_f{f}"]
  544. Si = np.zeros((n_features, n_features), dtype=float)
  545. for f1 in range(n_features):
  546. for f2 in range(f1, n_features):
  547. Si[f1, f2] = row[f"gmm_cov_{i}_{k}_f{f1}f{f2}"]
  548. if f1 != f2:
  549. Si[f2, f1] = Si[f1, f2]
  550. Si += 1e-6 * np.eye(n_features)
  551. try:
  552. np.linalg.cholesky(Si)
  553. except np.linalg.LinAlgError:
  554. Si = np.eye(n_features) * 0.1
  555. sigma[i, k] = Si
  556. if not has_w_eff:
  557. alpha = np.maximum(alpha, 0.0)
  558. sums = alpha.sum(axis=1, keepdims=True); sums[sums == 0] = 1.0
  559. alpha = alpha / sums
  560. model = {
  561. 'pi_stationary': pi_s,
  562. 'alpha': alpha,
  563. 'mu': mu,
  564. 'sigma': sigma,
  565. 'transition_matrix': A,
  566. 'n_states': n_states,
  567. 'n_components': n_components,
  568. 'n_features': n_features,
  569. 'model_id': idx,
  570. 'dataset_type': row.get('dataset_type', 'unknown'),
  571. 'series_id': row.get('series_id', idx)
  572. }
  573. if has_w_eff:
  574. w_eff = np.array(
  575. [row[f"w_eff_{i}_{k}"] for i in range(n_states) for k in range(n_components)],
  576. dtype=float
  577. )
  578. model['w_eff'] = w_eff
  579. models.append(model)
  580. except Exception as e:
  581. print(f"Error processing model {idx}: {str(e)}")
  582. continue
  583. print(f"Successfully extracted parameters for {len(models)} models")
  584. return models
  585. # --------------------------
  586. # Collapse to component lists
  587. # --------------------------
  588. def model_to_components(model):
  589. K = model['n_states']; M = model['n_components']; D = model['n_features']
  590. pi_s = model['pi_stationary']
  591. alpha = model['alpha']
  592. mu = model['mu']
  593. sigma = model['sigma']
  594. if 'w_eff' in model:
  595. w = model['w_eff'].copy()
  596. else:
  597. w = np.array([pi_s[i] * alpha[i, k] for i in range(K) for k in range(M)], dtype=float)
  598. MU = np.array([mu[i, k, :] for i in range(K) for k in range(M)], dtype=float)
  599. SIG = np.array([sigma[i, k, :, :] for i in range(K) for k in range(M)], dtype=float)
  600. return w, MU, SIG
  601. # -------------------------------------
  602. # Distance (MMD²) and matrix computation
  603. # -------------------------------------
  604. def enhanced_rkhs_distance_multivariate(modelP, modelQ, sigma_kernel):
  605. wP, MUP, SIGP = model_to_components(modelP)
  606. wQ, MUQ, SIGQ = model_to_components(modelQ)
  607. term_PP = 0.0
  608. for i in range(len(wP)):
  609. for j in range(len(wP)):
  610. term_PP += wP[i] * wP[j] * k_gauss_of_gaussians(MUP[i], SIGP[i], MUP[j], SIGP[j], sigma_kernel)
  611. term_QQ = 0.0
  612. for i in range(len(wQ)):
  613. for j in range(len(wQ)):
  614. term_QQ += wQ[i] * wQ[j] * k_gauss_of_gaussians(MUQ[i], SIGQ[i], MUQ[j], SIGQ[j], sigma_kernel)
  615. term_PQ = 0.0
  616. for i in range(len(wP)):
  617. for j in range(len(wQ)):
  618. term_PQ += wP[i] * wQ[j] * k_gauss_of_gaussians(MUP[i], SIGP[i], MUQ[j], SIGQ[j], sigma_kernel)
  619. dist2 = term_PP + term_QQ - 2.0 * term_PQ
  620. return max(float(dist2), 0.0)
  621. def compute_distance_matrix(models, sigma_kernel):
  622. n = len(models)
  623. D = np.zeros((n, n), dtype=float)
  624. print(f"Computing {n}x{n} RKHS distance matrix (σ_kernel={sigma_kernel:.6g})...")
  625. comps = [model_to_components(m) for m in models]
  626. def dist2_from_comps(cP, cQ):
  627. wP, MUP, SIGP = cP
  628. wQ, MUQ, SIGQ = cQ
  629. term_PP = 0.0
  630. for i in range(len(wP)):
  631. for j in range(len(wP)):
  632. term_PP += wP[i] * wP[j] * k_gauss_of_gaussians(MUP[i], SIGP[i], MUP[j], SIGP[j], sigma_kernel)
  633. term_QQ = 0.0
  634. for i in range(len(wQ)):
  635. for j in range(len(wQ)):
  636. term_QQ += wQ[i] * wQ[j] * k_gauss_of_gaussians(MUQ[i], SIGQ[i], MUQ[j], SIGQ[j], sigma_kernel)
  637. term_PQ = 0.0
  638. for i in range(len(wP)):
  639. for j in range(len(wQ)):
  640. term_PQ += wP[i] * wQ[j] * k_gauss_of_gaussians(MUP[i], SIGP[i], MUQ[j], SIGQ[j], sigma_kernel)
  641. return max(float(term_PP + term_QQ - 2.0 * term_PQ), 0.0)
  642. for i in range(n):
  643. D[i, i] = 0.0
  644. for j in range(i + 1, n):
  645. d2 = dist2_from_comps(comps[i], comps[j])
  646. D[i, j] = D[j, i] = d2
  647. if (i + 1) % 20 == 0:
  648. print(f" Processed {i + 1}/{n} rows")
  649. print(f"Done. Stats: min={D.min():.6f}, max={D.max():.6f}, mean={D.mean():.6f}")
  650. return D
  651. # Distance to kernel transformation
  652. def distance_to_kernel(D, gamma, epsilon=0.0):
  653. """
  654. Transform distance matrix to kernel (similarity) matrix using exponential transformation.
  655. Args:
  656. D: Distance matrix (n x n)
  657. gamma: Kernel bandwidth parameter
  658. epsilon: Optional diagonal regularization (default 0.0)
  659. Returns:
  660. K: Kernel matrix (n x n)
  661. """
  662. n = D.shape[0]
  663. K = np.exp(-D / (2 * gamma**2))
  664. if epsilon > 0:
  665. K += epsilon * np.eye(n)
  666. return K
  667. # Kernel matrix diagnostics
  668. def check_kernel_psd(K, verbose=True):
  669. """
  670. Check if kernel matrix is positive semi-definite.
  671. Args:
  672. K: Kernel matrix
  673. verbose: Print diagnostics
  674. Returns:
  675. is_psd: Boolean indicating if K is PSD
  676. min_eigval: Minimum eigenvalue
  677. """
  678. eigvals = np.linalg.eigvalsh(K)
  679. min_eigval = eigvals.min()
  680. max_eigval = eigvals.max()
  681. condition = max_eigval / max(abs(min_eigval), 1e-10)
  682. if verbose:
  683. print(f"Kernel diagnostics:")
  684. print(f" Min eigenvalue: {min_eigval:.6e}")
  685. print(f" Max eigenvalue: {max_eigval:.6e}")
  686. print(f" Condition number: {condition:.6e}")
  687. if min_eigval < -1e-10:
  688. print(f" WARNING: Kernel has negative eigenvalues!")
  689. elif min_eigval < 1e-12:
  690. print(f" WARNING: Very small positive eigenvalues, may be numerically unstable")
  691. else:
  692. print(f" Status: Kernel is well-conditioned")
  693. is_psd = min_eigval >= -1e-10
  694. return is_psd, min_eigval
  695. # ----------------------------
  696. # Cross-validation with SVM
  697. # ----------------------------
  698. def kfold_cross_validation_svm(models, labels, sigma_kernel, gamma, C,
  699. n_splits=5, random_state=42, epsilon=0.0):
  700. """
  701. K-fold cross-validation using SVM with precomputed kernel.
  702. Args:
  703. models: List of HMM-GMM models
  704. labels: Class labels
  705. sigma_kernel: Bandwidth for MMD² distance computation
  706. gamma: Bandwidth for kernel transformation
  707. C: SVM regularization parameter
  708. n_splits: Number of CV folds
  709. random_state: Random seed
  710. epsilon: Diagonal regularization (default 0.0)
  711. """
  712. skf = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=random_state)
  713. # Stage 1: Compute distance matrix
  714. full_D = compute_distance_matrix(models, sigma_kernel)
  715. # Stage 2: Transform to kernel matrix
  716. full_K = distance_to_kernel(full_D, gamma, epsilon)
  717. fold_results = {
  718. 'accuracy': [], 'balanced_accuracy': [], 'auc_roc': [], 'mcc': [],
  719. 'predictions': [], 'probabilities': [], 'true_labels': [],
  720. 'confusion_matrices': []
  721. }
  722. print(f"Performing {n_splits}-fold cross-validation with SVM...")
  723. print(f" σ_kernel={sigma_kernel:.6g}, γ={gamma:.6g}, C={C:.6g}")
  724. for fold, (train_idx, val_idx) in enumerate(skf.split(models, labels)):
  725. print(f" Fold {fold + 1}/{n_splits}")
  726. # Extract kernel submatrices for train and validation
  727. train_K = full_K[np.ix_(train_idx, train_idx)]
  728. val_train_K = full_K[np.ix_(val_idx, train_idx)]
  729. # Train SVM with precomputed kernel
  730. svm = SVC(kernel='precomputed', C=C, probability=True, random_state=random_state)
  731. svm.fit(train_K, labels[train_idx])
  732. # Predict on validation set
  733. val_pred = svm.predict(val_train_K)
  734. val_prob = svm.predict_proba(val_train_K)[:, 1]
  735. fold_results['predictions'].extend(val_pred.tolist())
  736. fold_results['probabilities'].extend(val_prob.tolist())
  737. fold_results['true_labels'].extend(labels[val_idx].tolist())
  738. # Compute metrics
  739. acc = accuracy_score(labels[val_idx], val_pred)
  740. bacc = balanced_accuracy_score(labels[val_idx], val_pred)
  741. auc_ = roc_auc_score(labels[val_idx], val_prob)
  742. mcc = matthews_corrcoef(labels[val_idx], val_pred)
  743. cm = confusion_matrix(labels[val_idx], val_pred)
  744. fold_results['accuracy'].append(acc)
  745. fold_results['balanced_accuracy'].append(bacc)
  746. fold_results['auc_roc'].append(auc_)
  747. fold_results['mcc'].append(mcc)
  748. fold_results['confusion_matrices'].append(cm)
  749. print(f" Acc={acc:.4f}, BalAcc={bacc:.4f}, AUC={auc_:.4f}, MCC={mcc:.4f}")
  750. # Convert to arrays
  751. for k in ['accuracy', 'balanced_accuracy', 'auc_roc', 'mcc']:
  752. fold_results[k] = np.array(fold_results[k])
  753. fold_results['predictions'] = np.array(fold_results['predictions'])
  754. fold_results['probabilities'] = np.array(fold_results['probabilities'])
  755. fold_results['true_labels'] = np.array(fold_results['true_labels'])
  756. return fold_results
  757. # -----------------------------
  758. # Optuna optimization for SVM
  759. # -----------------------------
  760. def hyperparameter_optimization_svm(models, labels,
  761. sigma_kernel_range=(1e-6, 1.0),
  762. gamma_range=(1e-6, 1.0),
  763. C_range=(1e-3, 1e3),
  764. n_trials=50, n_splits=5,
  765. timeout=1800, epsilon=0.0):
  766. """
  767. Hyperparameter optimization for SVM with RKHS kernel.
  768. Optimizes:
  769. - sigma_kernel: Bandwidth for MMD² distance computation
  770. - gamma: Bandwidth for kernel transformation
  771. - C: SVM regularization parameter
  772. """
  773. print("Starting hyperparameter optimization for SVM...")
  774. print(f"Search space:")
  775. print(f" sigma_kernel: log-uniform from {sigma_kernel_range[0]:.1e} to {sigma_kernel_range[1]:.1e}")
  776. print(f" gamma: log-uniform from {gamma_range[0]:.1e} to {gamma_range[1]:.1e}")
  777. print(f" C: log-uniform from {C_range[0]:.1e} to {C_range[1]:.1e}")
  778. print(f" n_trials: {n_trials}, CV folds: {n_splits}")
  779. def objective(trial):
  780. # Sample hyperparameters
  781. sigma_kernel = trial.suggest_float('sigma_kernel', sigma_kernel_range[0],
  782. sigma_kernel_range[1], log=True)
  783. gamma = trial.suggest_float('gamma', gamma_range[0], gamma_range[1], log=True)
  784. C = trial.suggest_float('C', C_range[0], C_range[1], log=True)
  785. try:
  786. # Stage 1: Compute distance matrix
  787. full_D = compute_distance_matrix(models, sigma_kernel)
  788. # Stage 2: Transform to kernel matrix
  789. full_K = distance_to_kernel(full_D, gamma, epsilon)
  790. # Cross-validation
  791. skf = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=42)
  792. scores = []
  793. for train_idx, val_idx in skf.split(models, labels):
  794. train_K = full_K[np.ix_(train_idx, train_idx)]
  795. val_train_K = full_K[np.ix_(val_idx, train_idx)]
  796. svm = SVC(kernel='precomputed', C=C, probability=True, random_state=42)
  797. svm.fit(train_K, labels[train_idx])
  798. pred = svm.predict(val_train_K)
  799. scores.append(balanced_accuracy_score(labels[val_idx], pred))
  800. return float(np.mean(scores))
  801. except Exception as e:
  802. print(f"Trial failed: {e}")
  803. return 0.0
  804. study = optuna.create_study(direction='maximize', sampler=TPESampler(seed=42),
  805. study_name="RKHS_SVM_Optimization")
  806. study.optimize(objective, n_trials=n_trials, timeout=timeout)
  807. print("\nOptimization completed!")
  808. print(f"Best balanced accuracy: {study.best_value:.4f}")
  809. print(f"Best parameters:")
  810. print(f" sigma_kernel: {study.best_params['sigma_kernel']:.6g}")
  811. print(f" gamma: {study.best_params['gamma']:.6g}")
  812. print(f" C: {study.best_params['C']:.6g}")
  813. return study
  814. # -----------------
  815. # Evaluation plots
  816. # -----------------
  817. def create_evaluation_plots(fold_results, class_names=['Control', 'ADHD']):
  818. fig, axes = plt.subplots(2, 3, figsize=(18, 12))
  819. overall_cm = confusion_matrix(fold_results['true_labels'], fold_results['predictions'])
  820. sns.heatmap(overall_cm, annot=True, fmt='d', cmap='Blues',
  821. xticklabels=class_names, yticklabels=class_names, ax=axes[0,0])
  822. axes[0,0].set_title('Overall Confusion Matrix')
  823. axes[0,0].set_xlabel('Predicted')
  824. axes[0,0].set_ylabel('Actual')
  825. fpr, tpr, _ = roc_curve(fold_results['true_labels'], fold_results['probabilities'])
  826. auc_score = auc(fpr, tpr)
  827. axes[0,1].plot(fpr, tpr, lw=2, label=f'ROC (AUC={auc_score:.3f})')
  828. axes[0,1].plot([0, 1], [0, 1], lw=2, linestyle='--', label='Random')
  829. axes[0,1].set_xlabel('FPR')
  830. axes[0,1].set_ylabel('TPR')
  831. axes[0,1].set_title('ROC Curve')
  832. axes[0,1].legend()
  833. axes[0,1].grid(True, alpha=0.3)
  834. precision, recall, _ = precision_recall_curve(fold_results['true_labels'],
  835. fold_results['probabilities'])
  836. pr_auc = auc(recall, precision)
  837. axes[0,2].plot(recall, precision, lw=2, label=f'PR (AUC={pr_auc:.3f})')
  838. axes[0,2].set_xlabel('Recall')
  839. axes[0,2].set_ylabel('Precision')
  840. axes[0,2].set_title('Precision-Recall')
  841. axes[0,2].legend()
  842. axes[0,2].grid(True, alpha=0.3)
  843. metrics_data = [fold_results['accuracy'], fold_results['balanced_accuracy'],
  844. fold_results['auc_roc'], fold_results['mcc']]
  845. axes[1,0].boxplot(metrics_data, labels=['Acc', 'Bal.Acc', 'AUC', 'MCC'])
  846. axes[1,0].set_title('CV Metric Distributions')
  847. axes[1,0].set_ylabel('Score')
  848. axes[1,0].grid(True, alpha=0.3)
  849. folds = np.arange(1, len(fold_results['accuracy']) + 1)
  850. axes[1,1].plot(folds, fold_results['balanced_accuracy'], 'o-', label='Balanced Acc')
  851. axes[1,1].plot(folds, fold_results['auc_roc'], 'o-', label='AUC-ROC')
  852. axes[1,1].set_xlabel('Fold')
  853. axes[1,1].set_ylabel('Score')
  854. axes[1,1].set_title('Performance by Fold')
  855. axes[1,1].legend()
  856. axes[1,1].grid(True, alpha=0.3)
  857. class_counts = np.bincount(fold_results['true_labels'])
  858. axes[1,2].pie(class_counts, labels=class_names, autopct='%1.1f%%', startangle=90)
  859. axes[1,2].set_title('Class Distribution')
  860. plt.tight_layout()
  861. plt.show()
  862. # -----------------------
  863. # Full analysis pipeline with SVM
  864. # -----------------------
  865. def run_complete_analysis_svm(csv_path, test_size=0.2, random_state=42,
  866. sigma_kernel_range=(1e-6, 1.0),
  867. gamma_range=(1e-6, 1.0),
  868. C_range=(1e-3, 1e3),
  869. n_trials=50, n_splits=5, timeout=1800,
  870. n_states=3, n_components=3, n_features=None,
  871. epsilon=0.0, check_psd=True):
  872. """
  873. Complete analysis pipeline using SVM with RKHS kernel.
  874. Args:
  875. csv_path: Path to CSV with HMM-GMM parameters
  876. test_size: Fraction for test set
  877. random_state: Random seed
  878. sigma_kernel_range: Range for distance computation bandwidth
  879. gamma_range: Range for kernel transformation bandwidth
  880. C_range: Range for SVM regularization
  881. n_trials: Number of Optuna trials
  882. n_splits: Number of CV folds
  883. timeout: Optimization timeout in seconds
  884. n_states: Number of HMM states
  885. n_components: Number of GMM components per state
  886. n_features: Number of features (auto-detected if None)
  887. epsilon: Diagonal regularization for kernel matrix
  888. check_psd: Whether to check kernel PSD property
  889. """
  890. print("=== RKHS-SVM ANALYSIS FOR HMM-GMM MODELS ===\n")
  891. print("Loading CSV...")
  892. df = pd.read_csv(csv_path)
  893. df['class_label'] = (df['dataset_type'] == 'ADHD').astype(int)
  894. print(f"Dataset summary:")
  895. print(f" Total: {len(df)} | ADHD: {df['class_label'].sum()} | Control: {(1-df['class_label']).sum()}")
  896. print(f" Class balance: {df['class_label'].mean():.3f}")
  897. train_df, test_df = train_test_split(
  898. df, test_size=test_size, random_state=random_state, stratify=df['class_label']
  899. )
  900. if n_features is None:
  901. n_features = infer_n_features(train_df, n_states, n_components)
  902. print("\nExtracting HMM-GMM parameters...")
  903. train_models = extract_hmm_parameters_from_csv(train_df, n_states, n_components, n_features)
  904. train_labels = train_df['class_label'].values
  905. test_models = extract_hmm_parameters_from_csv(test_df, n_states, n_components, n_features)
  906. test_labels = test_df['class_label'].values
  907. # Hyperparameter optimization
  908. study = hyperparameter_optimization_svm(
  909. train_models, train_labels,
  910. sigma_kernel_range=sigma_kernel_range,
  911. gamma_range=gamma_range,
  912. C_range=C_range,
  913. n_trials=n_trials, n_splits=n_splits, timeout=timeout,
  914. epsilon=epsilon
  915. )
  916. best_params = study.best_params
  917. # Cross-validation with best parameters
  918. print("\nCross-validation with best params...")
  919. fold_results = kfold_cross_validation_svm(
  920. train_models, train_labels,
  921. best_params['sigma_kernel'],
  922. best_params['gamma'],
  923. best_params['C'],
  924. n_splits=n_splits,
  925. random_state=random_state,
  926. epsilon=epsilon
  927. )
  928. print("\n" + "="*60)
  929. print("CROSS-VALIDATION RESULTS")
  930. print("="*60)
  931. print(f" σ_kernel: {best_params['sigma_kernel']:.6g}")
  932. print(f" γ (gamma): {best_params['gamma']:.6g}")
  933. print(f" C: {best_params['C']:.6g}")
  934. for name, key in [('Accuracy', 'accuracy'), ('Balanced Accuracy', 'balanced_accuracy'),
  935. ('AUC-ROC','auc_roc'), ('MCC','mcc')]:
  936. vals = fold_results[key]
  937. print(f" {name}: {vals.mean():.4f} ± {vals.std():.4f}")
  938. # Final test set evaluation
  939. print("\n" + "="*60)
  940. print("FINAL TEST SET EVALUATION")
  941. print("="*60)
  942. # Compute distance and kernel matrices
  943. train_D = compute_distance_matrix(train_models, best_params['sigma_kernel'])
  944. train_K = distance_to_kernel(train_D, best_params['gamma'], epsilon)
  945. if check_psd:
  946. print("\nChecking training kernel matrix...")
  947. check_kernel_psd(train_K)
  948. all_models = train_models + test_models
  949. full_D = compute_distance_matrix(all_models, best_params['sigma_kernel'])
  950. full_K = distance_to_kernel(full_D, best_params['gamma'], epsilon)
  951. n_train = len(train_models)
  952. test_train_K = full_K[n_train:, :n_train]
  953. # Train final SVM on full training set
  954. final_svm = SVC(kernel='precomputed', C=best_params['C'],
  955. probability=True, random_state=random_state)
  956. final_svm.fit(train_K, train_labels)
  957. # Predict on test set
  958. test_pred = final_svm.predict(test_train_K)
  959. test_prob = final_svm.predict_proba(test_train_K)[:, 1]
  960. # Compute test metrics
  961. test_accuracy = accuracy_score(test_labels, test_pred)
  962. test_bal_acc = balanced_accuracy_score(test_labels, test_pred)
  963. test_auc = roc_auc_score(test_labels, test_prob)
  964. test_mcc = matthews_corrcoef(test_labels, test_pred)
  965. test_cm = confusion_matrix(test_labels, test_pred)
  966. print(f" Accuracy: {test_accuracy:.4f}")
  967. print(f" Balanced Accuracy: {test_bal_acc:.4f}")
  968. print(f" AUC-ROC: {test_auc:.4f}")
  969. print(f" Matthews Corr: {test_mcc:.4f}")
  970. print("\nTest Confusion Matrix:")
  971. print(f" Control ADHD")
  972. print(f"Control {test_cm[0,0]:<7} {test_cm[0,1]:<7}")
  973. print(f"ADHD {test_cm[1,0]:<7} {test_cm[1,1]:<7}")
  974. # Create evaluation plots
  975. create_evaluation_plots(fold_results)
  976. return {
  977. 'study': study,
  978. 'best_params': best_params,
  979. 'fold_results': fold_results,
  980. 'test_results': {
  981. 'accuracy': test_accuracy,
  982. 'balanced_accuracy': test_bal_acc,
  983. 'auc_roc': test_auc,
  984. 'mcc': test_mcc,
  985. 'confusion_matrix': test_cm,
  986. 'predictions': test_pred,
  987. 'probabilities': test_prob
  988. },
  989. 'final_model': final_svm
  990. }
  991. if __name__ == "__main__":
  992. results = run_complete_analysis_svm(
  993. # csv_path='.../synthetic_hmms_results.csv',
  994. csv_path='.../NxGx/hmm_results.csv',
  995. test_size=0.2,
  996. random_state=42,
  997. # Hyperparameter ranges
  998. sigma_kernel_range=(1e-4, 1e-1), # For distance computation
  999. gamma_range=(1e-5, 1e-2), # For kernel transformation
  1000. C_range=(1e-3, 1e+3), # SVM regularization
  1001. # Optimization settings
  1002. n_trials=250,
  1003. n_splits=7, # k-fold
  1004. timeout=(60 * 60 * 4), # hours
  1005. # Model configuration
  1006. n_states=3,# 4, 5
  1007. n_components=3, # 4, 5
  1008. n_features=None, # Auto-detect
  1009. # Kernel settings
  1010. epsilon=0.0, # No regularization by default
  1011. check_psd=True # Check kernel properties
  1012. )
  1013. print("\n" + "="*60)
  1014. print("ANALYSIS COMPLETE")
  1015. print("="*60)
  1016. print(f"Best σ_kernel: {results['best_params']['sigma_kernel']:.6g}")
  1017. print(f"Best γ (gamma): {results['best_params']['gamma']:.6g}")
  1018. print(f"Best C: {results['best_params']['C']:.6g}")
  1019. print(f"Final test balanced accuracy: {results['test_results']['balanced_accuracy']:.4f}")

rkhs-knn-svm-eeg-dataset.ipynb at commit d2eddf4, under MIT · at the source

Overview

  1. Automatics Research Group, Universidad Tecnológica de Pereira (UTP), Pereira 660003, Colombia; (C.K.V.-M.); (J.G.-G.)
  2. Psiquiatría Neurociencias y Comunidad, Universidad Tecnológica de Pereira (UTP), Pereira 660003, Colombia
Institutions: Technological University of Pereira (Colombia)
Journal: Sensors (Basel, Switzerland), volume 26, issue 15, article 4773
Dates: received 19 May 2026; accepted 22 July 2026; published online 27 July 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.3390/s26154773 · PMID 42590549 · PMCID PMC13469683 · OpenAlex W7171436422
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: EEG (modality), human (organism), ADHD (population)
Methods: Spectral & time-frequency, Preprocessing, Machine learning, Statistics, Physiology & signal measures, Smoothing, state filtering, decompositions
Keywords: EEG, ADHD, hidden Markov model, Gaussian mixture model, RKHS embedding, kernel classification
MeSH: Attention Deficit Disorder with Hyperactivity*, Electroencephalography*, Algorithms, Hidden Markov Models, Humans, Markov Chains (* major topic)
Topic: Attention Deficit Hyperactivity Disorder (Psychiatry and Mental health, Medicine), according to OpenAlex
Funding: Ministerio de Ciencia, Tecnología e Innovación (111091991908)
Citations: not cited yet (Europe PMC); 42 references in the paper

Abstract

Electroencephalography (EEG) provides a non-invasive means of supporting Attention-Deficit/Hyperactivity Disorder (ADHD) assessment. Nonetheless, existing pipelines often rely on handcrafted descriptors, segment-wise decisions, or deep architectures with limited subject-level generalization. This work introduces Hidden Markov Model-Induced Stationary RKHS Distance Learning (HIS), a probabilistic framework that represents each subject by a Hidden Markov Model with Gaussian-mixture emissions trained directly from frontal EEG recordings. Rather than vectorizing model parameters, each HMM is mapped to its induced stationary observation distribution and embedded into a Reproducing Kernel Hilbert Space (RKHS), where pairwise subject similarities are computed through a closed-form Hilbert embedding distance. These similarities are subsequently exploited by precomputed-kernel classifiers for subject-level prediction. The proposed method was evaluated against the Probability Product Kernel baseline using both a controlled synthetic EEG benchmark and a public pediatric ADHD dataset under progressively more rigorous validation protocols, culminating in repeated nested cross-validation with bootstrap confidence intervals and permutation testing. On the synthetic benchmark, HIS achieved 95.0% held-out accuracy and consistently outperformed the baseline across classifiers. On a real EEG dataset with 121 subjects, the primary evaluation protocol yielded a balanced accuracy of 73.5% (95% CI: 69.8–77.0%), an AUC of 79.6%, and an MCC of 0.483 (permutation p < 0.001) using an SVM with compact subject-specific HMMs. Complementary hyperparameter analyses and t-SNE visualizations demonstrated that HIS induces more stable and discriminative subject representations than the baseline. These results establish stationary RKHS embeddings of subject-specific HMMs as a leakage-aware framework for EEG-based ADHD decision support and underscore the critical influence of statistically rigorous evaluation protocols on reported classification performance.

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 12 matches between paragraphs and lines of code.

leonlpz/HMM_RKHS_Classification_Project

License: MIT
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: d2eddf4778b0944543c6ea141c817cdd2323cc00, 11 July 2026
Languages: Jupyter (6), Python (1)
Size: 1,158 files, 7 scripts
Software Heritage: not archived
Found in: “Data Availability Statement”
Holds: README, license file, 6 notebooks
Not found: CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (7 files), pandas (7 files), scikit-learn (7 files), Matplotlib (5 files), SciPy (5 files), seaborn (4 files), Numba (3 files), TensorFlow (3 files)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
9 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;
  • 7 scripts, each with its path and the digest of its content;
  • 12 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

No dataset and no data link were found in the paper.

Data Availability Statement

The dataset EEG data for ADHD / Control children, used in the validation of the current study, is publicly available at https://ieee-dataport.org/open-access/eeg-data-adhd-control-children. The code, baseline reimplementation, full per-configuration results, and trained model artifacts supporting the findings of this study are openly available in the project’s repository (https://github.com/leonlpz/HMM_RKHS_Classification_Project (accessed on 19 May 2026)).

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, 5 authors, 6 keywords, 6 MeSH terms, 1 funder, 39 references.

Cite

This paper

Lopez-Ortiz, L., Valencia-Marin, C. K., Gil-González, J., Herrera-Gómez, P. M., & Cárdenas-Peña, D. (2026). EEG-Based Supported Diagnosis of ADHD Using Subject-Specific HMMs and Stationary RKHS Embeddings. Sensors (Basel, Switzerland), 26(15), 4773. https://doi.org/10.3390/s26154773

BibTeX

@article{lopezortiz2026eeg,
author = {Lopez-Ortiz, Leonardo and Valencia-Marin, Cristhian K and Gil-González, Julián and Herrera-Gómez, Paula M and Cárdenas-Peña, David},
title = {{EEG-Based Supported Diagnosis of ADHD Using Subject-Specific HMMs and Stationary RKHS Embeddings}},
journal = {Sensors (Basel, Switzerland)},
year = {2026},
month = jul,
volume = {26},
number = {15},
pages = {4773},
publisher = {Multidisciplinary Digital Publishing Institute (MDPI)},
issn = {1424-8220},
doi = {10.3390/s26154773},
url = {https://doi.org/10.3390/s26154773},
pmid = {42590549},
pmcid = {PMC13469683}
}

RIS

TY - JOUR
AU - Lopez-Ortiz, Leonardo
AU - Valencia-Marin, Cristhian K
AU - Gil-González, Julián
AU - Herrera-Gómez, Paula M
AU - Cárdenas-Peña, David
TI - EEG-Based Supported Diagnosis of ADHD Using Subject-Specific HMMs and Stationary RKHS Embeddings
T2 - Sensors (Basel, Switzerland)
J2 - Sensors (Basel)
PY - 2026
DA - 2026/07/27
VL - 26
IS - 15
SP - 4773
SN - 1424-8220
PB - Multidisciplinary Digital Publishing Institute (MDPI)
DO - 10.3390/s26154773
UR - https://doi.org/10.3390/s26154773
LA - en
ER -

CSL-JSON

{
"id": "10.3390/s26154773",
"type": "article-journal",
"title": "EEG-Based Supported Diagnosis of ADHD Using Subject-Specific HMMs and Stationary RKHS Embeddings",
"container-title": "Sensors (Basel, Switzerland)",
"author": [
{
"family": "Lopez-Ortiz",
"given": "Leonardo"
},
{
"family": "Valencia-Marin",
"given": "Cristhian K"
},
{
"family": "Gil-González",
"given": "Julián"
},
{
"family": "Herrera-Gómez",
"given": "Paula M"
},
{
"family": "Cárdenas-Peña",
"given": "David"
}
],
"container-title-short": "Sensors (Basel)",
"volume": "26",
"issue": "15",
"page": "4773",
"DOI": "10.3390/s26154773",
"PMID": "42590549",
"PMCID": "PMC13469683",
"ISSN": "1424-8220",
"publisher": "Multidisciplinary Digital Publishing Institute (MDPI)",
"URL": "https://doi.org/10.3390/s26154773",
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
27
]
]
}
}

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.0357213
Integrating socioeconomic context with multimodal EEG data for improved ADHD risk screening.
Journal: PloS one
In common: ADHD, EEG, 5 references
[2] doi:10.21203/rs.3.rs-9010375/v1 [code]
Interpretable metrics for evaluating fidelity, diversity, and privacy in synthetic EEG data
Journal: Research Square (preprint)
In common: Numba, seaborn, scikit-learn, 4 other tools, EEG, 1 reference
[3] doi:10.7554/elife.110588 [code]
Opening the black box toward a modular approach to spike sorting.
Journal: eLife
In common: Numba, TensorFlow, seaborn, 5 other tools
[4] doi:10.1038/s42003-026-10957-8 [code]
Brain defence by the extracellular matrix protein Cochlin.
Journal: Communications biology
In common: Numba, TensorFlow, seaborn, 5 other tools
[5] doi:10.1371/journal.pcbi.1014571 [code]
SynAPSeg: A novel dataset and image analysis framework for deep learning-based synapse detection and quantification.
Journal: PLoS computational biology
In common: Numba, TensorFlow, seaborn, 5 other tools
[6] doi:10.1038/s41467-026-74460-8 [code]
Spike-based alignment learning solves the weight transport problem.
Journal: Nature communications
In common: Numba, TensorFlow, seaborn, 5 other tools
[7] doi:10.1093/nar/gkag706 [code]
scDifformer: diffusion-based post-training for virtual cell modeling across large-scale single-cell data.
Journal: Nucleic acids research
In common: Numba, TensorFlow, seaborn, 5 other tools
[8] doi:10.1038/s41467-026-74358-5 [code]
Brain-inspired spatial intelligence for embodied agents.
Journal: Nature communications
In common: Numba, TensorFlow, seaborn, 5 other tools
[9] doi:10.1371/journal.pone.0346575 [code]
Statistically valid explainable black-box machine learning: applications in sex classification across species using brain imaging.
Journal: PloS one
In common: Numba, TensorFlow, seaborn, 5 other tools
[10] doi:10.1038/s44320-026-00208-7 [code]
Interpretable deep generative ensemble learning for single-cell omics with Hydra.
Journal: Molecular systems biology
In common: Numba, TensorFlow, seaborn, 5 other tools

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.