EEG-Based Supported Diagnosis of ADHD Using Subject-Specific HMMs and Stationary RKHS Embeddings.
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] § 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] § 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. 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] § 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] § 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] § 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] § 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] § 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] § 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] § 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] § 3. Results › 3.2. Hyperparameter Tuning ↔ training-hmm.ipynb, lines 1143–1229 · score 0.52 · Log likelihood, HMM convergence, iteration, errors, fit
- [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
- # %%
- # --- Install required packages ---
- !pip install -q tensorflow tensorflow-probability scikit-learn matplotlib pandas optuna
- # %% [markdown]
- # # RKHS with precomputed distance and KNN Classification
- # %%
- import numpy as np
- import pandas as pd
- from sklearn.neighbors import KNeighborsClassifier
- from sklearn.model_selection import StratifiedKFold, train_test_split
- from sklearn.metrics import (
- classification_report, accuracy_score, confusion_matrix,
- balanced_accuracy_score, roc_auc_score, matthews_corrcoef,
- roc_curve, precision_recall_curve, auc
- )
- import matplotlib.pyplot as plt
- import seaborn as sns
- import optuna
- from optuna.samplers import TPESampler
- import warnings
- import scipy.linalg
- warnings.filterwarnings('ignore')
- # -----------------------------
- # Stationary distribution utils
- # -----------------------------
- def compute_stationary_distribution(A, max_iter=1000, rel_tol=1e-6, abs_tol=1e-10):
- A = np.asarray(A, dtype=float)
- n = A.shape[0]
- # Row-stochastic guard (and all-zero row fix)
- rs = A.sum(axis=1, keepdims=True)
- bad = (rs[:, 0] == 0)
- if bad.any():
- A[bad] = 1.0 / n
- rs = A.sum(axis=1, keepdims=True)
- A = A / rs
- pi = np.ones(n) / n
- for _ in range(max_iter):
- pi_new = pi @ A
- if np.linalg.norm(pi_new - pi, 1) <= rel_tol * np.linalg.norm(pi, 1) + abs_tol:
- pi = pi_new
- break
- pi = pi_new
- pi = np.maximum(pi, 0.0)
- s = pi.sum()
- return pi / (s if s > 0 else 1.0)
- # -----------------------------------------
- # Exact multivariate Gaussian-Gaussian RBF
- # -----------------------------------------
- def k_gauss_of_gaussians(mu1, S1, mu2, S2, ell, eps=1e-9):
- mu1 = np.asarray(mu1); mu2 = np.asarray(mu2)
- S1 = np.asarray(S1); S2 = np.asarray(S2)
- D = mu1.shape[0]
- S1 = S1 + eps * np.eye(D)
- S2 = S2 + eps * np.eye(D)
- L_det = np.linalg.cholesky(np.eye(D) + (S1 + S2) / max(ell, eps))
- log_det_term = -np.sum(np.log(np.diag(L_det)))
- M = S1 + S2 + ell * np.eye(D)
- Lm = np.linalg.cholesky(M)
- d = mu1 - mu2
- y = scipy.linalg.solve_triangular(Lm, d, lower=True)
- quad = -0.5 * float(y @ y)
- return float(np.exp(log_det_term + quad))
- # -------------------------------------------------------
- # CSV extraction, with auto-detected feature dimensionality
- # -------------------------------------------------------
- def infer_n_features(df, n_states, n_components):
- f_idxs = []
- prefix = f"gmm_mean_0_0_f"
- for c in df.columns:
- if c.startswith(prefix):
- try:
- f_idxs.append(int(c.split('f')[-1]))
- except Exception:
- pass
- return (max(f_idxs) + 1) if f_idxs else 1
- def extract_hmm_parameters_from_csv(df, n_states=3, n_components=3, n_features=None):
- if n_features is None:
- n_features = infer_n_features(df, n_states, n_components)
- models = []
- print(f"Extracting HMM parameters from {len(df)} models...")
- print(f"Config inferred: n_states={n_states}, n_components={n_components}, n_features={n_features}")
- has_pi_s = all([f"stationary_pi_{i}" in df.columns for i in range(n_states)])
- has_w_eff = f"w_eff_0_0" in df.columns
- for idx, row in df.iterrows():
- try:
- # Transition matrix A
- A = np.zeros((n_states, n_states), dtype=float)
- for i in range(n_states):
- for j in range(n_states):
- A[i, j] = row[f"A_{i}{j}"]
- rs = A.sum(axis=1, keepdims=True); rs[rs == 0] = 1
- A = A / rs
- # stationary pi
- if has_pi_s:
- pi_s = np.array([row[f"stationary_pi_{i}"] for i in range(n_states)], dtype=float)
- if (not np.isfinite(pi_s).all()) or (pi_s.sum() <= 1e-12) or (pi_s < -1e-12).any():
- pi_s = compute_stationary_distribution(A)
- else:
- pi_s = np.maximum(pi_s, 0.0)
- s = pi_s.sum()
- pi_s = pi_s / (s if s > 0 else 1.0)
- else:
- pi_s = compute_stationary_distribution(A)
- # mixture weights alpha and means/covs
- alpha = np.zeros((n_states, n_components), dtype=float)
- mu = np.zeros((n_states, n_components, n_features), dtype=float)
- sigma = np.zeros((n_states, n_components, n_features, n_features), dtype=float)
- for i in range(n_states):
- for k in range(n_components):
- if has_w_eff:
- alpha[i, k] = row.get(f"gmm_weight_{i}_{k}", 0.0)
- else:
- alpha[i, k] = row[f"gmm_weight_{i}_{k}"]
- for f in range(n_features):
- mu[i, k, f] = row[f"gmm_mean_{i}_{k}_f{f}"]
- Si = np.zeros((n_features, n_features), dtype=float)
- for f1 in range(n_features):
- for f2 in range(f1, n_features):
- Si[f1, f2] = row[f"gmm_cov_{i}_{k}_f{f1}f{f2}"]
- if f1 != f2:
- Si[f2, f1] = Si[f1, f2]
- Si += 1e-6 * np.eye(n_features)
- try:
- np.linalg.cholesky(Si)
- except np.linalg.LinAlgError:
- Si = np.eye(n_features) * 0.1
- sigma[i, k] = Si
- if not has_w_eff:
- alpha = np.maximum(alpha, 0.0)
- sums = alpha.sum(axis=1, keepdims=True); sums[sums == 0] = 1.0
- alpha = alpha / sums
- model = {
- 'pi_stationary': pi_s,
- 'alpha': alpha,
- 'mu': mu,
- 'sigma': sigma,
- 'transition_matrix': A,
- 'n_states': n_states,
- 'n_components': n_components,
- 'n_features': n_features,
- 'model_id': idx,
- 'dataset_type': row.get('dataset_type', 'unknown'),
- 'series_id': row.get('series_id', idx)
- }
- if has_w_eff:
- w_eff = np.array(
- [row[f"w_eff_{i}_{k}"] for i in range(n_states) for k in range(n_components)],
- dtype=float
- )
- model['w_eff'] = w_eff
- models.append(model)
- except Exception as e:
- print(f"Error processing model {idx}: {str(e)}")
- continue
- print(f"Successfully extracted parameters for {len(models)} models")
- return models
- # --------------------------
- # Collapse to component lists
- # --------------------------
- def model_to_components(model):
- K = model['n_states']; M = model['n_components']; D = model['n_features']
- pi_s = model['pi_stationary']
- alpha = model['alpha']
- mu = model['mu']
- sigma = model['sigma']
- if 'w_eff' in model:
- w = model['w_eff'].copy()
- else:
- w = np.array([pi_s[i] * alpha[i, k] for i in range(K) for k in range(M)], dtype=float)
- MU = np.array([mu[i, k, :] for i in range(K) for k in range(M)], dtype=float)
- SIG = np.array([sigma[i, k, :, :] for i in range(K) for k in range(M)], dtype=float)
- return w, MU, SIG
- # -------------------------------------
- # Distance (MMD²) and matrix computation
- # -------------------------------------
- def enhanced_rkhs_distance_multivariate(modelP, modelQ, sigma_kernel):
- wP, MUP, SIGP = model_to_components(modelP)
- wQ, MUQ, SIGQ = model_to_components(modelQ)
- term_PP = 0.0
- for i in range(len(wP)):
- for j in range(len(wP)):
- term_PP += wP[i] * wP[j] * k_gauss_of_gaussians(MUP[i], SIGP[i], MUP[j], SIGP[j], sigma_kernel)
- term_QQ = 0.0
- for i in range(len(wQ)):
- for j in range(len(wQ)):
- term_QQ += wQ[i] * wQ[j] * k_gauss_of_gaussians(MUQ[i], SIGQ[i], MUQ[j], SIGQ[j], sigma_kernel)
- term_PQ = 0.0
- for i in range(len(wP)):
- for j in range(len(wQ)):
- term_PQ += wP[i] * wQ[j] * k_gauss_of_gaussians(MUP[i], SIGP[i], MUQ[j], SIGQ[j], sigma_kernel)
- dist2 = term_PP + term_QQ - 2.0 * term_PQ
- return max(float(dist2), 0.0)
- def compute_distance_matrix(models, sigma_kernel):
- n = len(models)
- D = np.zeros((n, n), dtype=float)
- print(f"Computing {n}x{n} RKHS distance matrix (σ={sigma_kernel:.6g})...")
- comps = [model_to_components(m) for m in models]
- def dist2_from_comps(cP, cQ):
- wP, MUP, SIGP = cP
- wQ, MUQ, SIGQ = cQ
- term_PP = 0.0
- for i in range(len(wP)):
- for j in range(len(wP)):
- term_PP += wP[i] * wP[j] * k_gauss_of_gaussians(MUP[i], SIGP[i], MUP[j], SIGP[j], sigma_kernel)
- term_QQ = 0.0
- for i in range(len(wQ)):
- for j in range(len(wQ)):
- term_QQ += wQ[i] * wQ[j] * k_gauss_of_gaussians(MUQ[i], SIGQ[i], MUQ[j], SIGQ[j], sigma_kernel)
- term_PQ = 0.0
- for i in range(len(wP)):
- for j in range(len(wQ)):
- term_PQ += wP[i] * wQ[j] * k_gauss_of_gaussians(MUP[i], SIGP[i], MUQ[j], SIGQ[j], sigma_kernel)
- return max(float(term_PP + term_QQ - 2.0 * term_PQ), 0.0)
- for i in range(n):
- D[i, i] = 0.0
- for j in range(i + 1, n):
- d2 = dist2_from_comps(comps[i], comps[j])
- D[i, j] = D[j, i] = d2
- if (i + 1) % 20 == 0:
- print(f" Processed {i + 1}/{n} rows")
- print(f"Done. Stats: min={D.min():.6f}, max={D.max():.6f}, mean={D.mean():.6f}")
- return D
- # ----------------------------
- # Cross-validation (precompute)
- # ----------------------------
- def kfold_cross_validation(models, labels, sigma_kernel, k_neighbors, n_splits=5, random_state=42):
- skf = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=random_state)
- full_D = compute_distance_matrix(models, sigma_kernel)
- fold_results = {
- 'accuracy': [], 'balanced_accuracy': [], 'auc_roc': [], 'mcc': [],
- 'predictions': [], 'probabilities': [], 'true_labels': [],
- 'confusion_matrices': []
- }
- print(f"Performing {n_splits}-fold cross-validation...")
- for fold, (train_idx, val_idx) in enumerate(skf.split(models, labels)):
- print(f" Fold {fold + 1}/{n_splits}")
- train_D = full_D[np.ix_(train_idx, train_idx)]
- val_train_D = full_D[np.ix_(val_idx, train_idx)]
- knn = KNeighborsClassifier(n_neighbors=k_neighbors, metric='precomputed', weights='distance')
- knn.fit(train_D, labels[train_idx])
- val_pred = knn.predict(val_train_D)
- val_prob = knn.predict_proba(val_train_D)[:, 1]
- fold_results['predictions'].extend(val_pred.tolist())
- fold_results['probabilities'].extend(val_prob.tolist())
- fold_results['true_labels'].extend(labels[val_idx].tolist())
- acc = accuracy_score(labels[val_idx], val_pred)
- bacc = balanced_accuracy_score(labels[val_idx], val_pred)
- auc_ = roc_auc_score(labels[val_idx], val_prob)
- mcc = matthews_corrcoef(labels[val_idx], val_pred)
- cm = confusion_matrix(labels[val_idx], val_pred)
- fold_results['accuracy'].append(acc)
- fold_results['balanced_accuracy'].append(bacc)
- fold_results['auc_roc'].append(auc_)
- fold_results['mcc'].append(mcc)
- fold_results['confusion_matrices'].append(cm)
- print(f" Acc={acc:.4f}, BalAcc={bacc:.4f}, AUC={auc_:.4f}, MCC={mcc:.4f}")
- for k in ['accuracy', 'balanced_accuracy', 'auc_roc', 'mcc']:
- fold_results[k] = np.array(fold_results[k])
- fold_results['predictions'] = np.array(fold_results['predictions'])
- fold_results['probabilities'] = np.array(fold_results['probabilities'])
- fold_results['true_labels'] = np.array(fold_results['true_labels'])
- return fold_results
- # -----------------------------
- # Optuna (precompute per sigma too)
- # -----------------------------
- def hyperparameter_optimization(models, labels, sigma_kernel_range=(1e-6, 1.0),
- k_neighbors_range=(1, 15), n_trials=50,
- n_splits=5, timeout=1800):
- print("Starting hyperparameter optimization...")
- min_k, max_k = k_neighbors_range
- max_k = min(max_k, len(models) - 1)
- valid_k_values = [k for k in range(min_k, max_k + 1) if k % 2 == 1]
- print(f"Search space:")
- print(f"- sigma_kernel: log-uniform from {sigma_kernel_range[0]:.1e} to {sigma_kernel_range[1]:.1e}")
- print(f"- k_neighbors: {valid_k_values}")
- print(f"- n_trials: {n_trials}, CV folds: {n_splits}")
- def objective(trial):
- sigma_kernel = trial.suggest_float('sigma_kernel', sigma_kernel_range[0], sigma_kernel_range[1], log=True)
- k_neighbors = trial.suggest_categorical('k_neighbors', valid_k_values)
- try:
- full_D = compute_distance_matrix(models, sigma_kernel)
- skf = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=42)
- scores = []
- for train_idx, val_idx in skf.split(models, labels):
- train_D = full_D[np.ix_(train_idx, train_idx)]
- val_train_D = full_D[np.ix_(val_idx, train_idx)]
- knn = KNeighborsClassifier(n_neighbors=k_neighbors, metric='precomputed', weights='distance')
- knn.fit(train_D, labels[train_idx])
- pred = knn.predict(val_train_D)
- scores.append(balanced_accuracy_score(labels[val_idx], pred))
- return float(np.mean(scores))
- except Exception as e:
- print(f"Trial failed: {e}")
- return 0.0
- study = optuna.create_study(direction='maximize', sampler=TPESampler(seed=42),
- study_name="RKHS_KNN_Optimization")
- study.optimize(objective, n_trials=n_trials, timeout=timeout)
- print("\nOptimization completed!")
- print(f"Best balanced accuracy: {study.best_value:.4f}")
- print(f"Best parameters: {study.best_params}")
- return study
- # -----------------
- # Evaluation plots
- # -----------------
- def create_evaluation_plots(fold_results, class_names=['Control', 'ADHD']):
- fig, axes = plt.subplots(2, 3, figsize=(18, 12))
- overall_cm = confusion_matrix(fold_results['true_labels'], fold_results['predictions'])
- sns.heatmap(overall_cm, annot=True, fmt='d', cmap='Blues',
- xticklabels=class_names, yticklabels=class_names, ax=axes[0,0])
- axes[0,0].set_title('Overall Confusion Matrix'); axes[0,0].set_xlabel('Predicted'); axes[0,0].set_ylabel('Actual')
- fpr, tpr, _ = roc_curve(fold_results['true_labels'], fold_results['probabilities'])
- auc_score = auc(fpr, tpr)
- axes[0,1].plot(fpr, tpr, lw=2, label=f'ROC (AUC={auc_score:.3f})')
- axes[0,1].plot([0, 1], [0, 1], lw=2, linestyle='--', label='Random')
- 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)
- precision, recall, _ = precision_recall_curve(fold_results['true_labels'], fold_results['probabilities'])
- pr_auc = auc(recall, precision)
- axes[0,2].plot(recall, precision, lw=2, label=f'PR (AUC={pr_auc:.3f})')
- 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)
- metrics_data = [fold_results['accuracy'], fold_results['balanced_accuracy'], fold_results['auc_roc'], fold_results['mcc']]
- axes[1,0].boxplot(metrics_data, labels=['Acc', 'Bal.Acc', 'AUC', 'MCC'])
- axes[1,0].set_title('CV Metric Distributions'); axes[1,0].set_ylabel('Score'); axes[1,0].grid(True, alpha=0.3)
- folds = np.arange(1, len(fold_results['accuracy']) + 1)
- axes[1,1].plot(folds, fold_results['balanced_accuracy'], 'o-', label='Balanced Acc')
- axes[1,1].plot(folds, fold_results['auc_roc'], 'o-', label='AUC-ROC')
- 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)
- class_counts = np.bincount(fold_results['true_labels'])
- axes[1,2].pie(class_counts, labels=class_names, autopct='%1.1f%%', startangle=90)
- axes[1,2].set_title('Class Distribution')
- plt.tight_layout(); plt.show()
- # -----------------------
- # Full analysis pipeline
- # -----------------------
- def run_complete_analysis(csv_path, test_size=0.2, random_state=42,
- sigma_kernel_range=(1e-6, 1.0), k_neighbors_range=(1, 15),
- n_trials=50, n_splits=5, timeout=1800,
- n_states=3, n_components=3, n_features=None):
- print("=== RKHS-KNN ANALYSIS FOR HMM-GMM MODELS ===\n")
- print("Loading CSV...")
- df = pd.read_csv(csv_path)
- df['class_label'] = (df['dataset_type'] == 'ADHD').astype(int)
- print(f"Dataset summary:")
- print(f" Total: {len(df)} | ADHD: {df['class_label'].sum()} | Control: {(1-df['class_label']).sum()}")
- print(f" Class balance: {df['class_label'].mean():.3f}")
- train_df, test_df = train_test_split(
- df, test_size=test_size, random_state=random_state, stratify=df['class_label']
- )
- if n_features is None:
- n_features = infer_n_features(train_df, n_states, n_components)
- print("\nExtracting HMM-GMM parameters...")
- train_models = extract_hmm_parameters_from_csv(train_df, n_states, n_components, n_features)
- train_labels = train_df['class_label'].values
- test_models = extract_hmm_parameters_from_csv(test_df, n_states, n_components, n_features)
- test_labels = test_df['class_label'].values
- study = hyperparameter_optimization(
- train_models, train_labels,
- sigma_kernel_range=sigma_kernel_range,
- k_neighbors_range=k_neighbors_range,
- n_trials=n_trials, n_splits=n_splits, timeout=timeout
- )
- best_params = study.best_params
- print("\nCross-validation with best params...")
- fold_results = kfold_cross_validation(
- train_models, train_labels,
- best_params['sigma_kernel'], best_params['k_neighbors'],
- n_splits=n_splits, random_state=random_state
- )
- print("\n" + "="*60)
- print("CROSS-VALIDATION RESULTS")
- print("="*60)
- print(f" σ_kernel: {best_params['sigma_kernel']:.6g}")
- print(f" k_neighbors: {best_params['k_neighbors']}")
- for name, key in [('Accuracy', 'accuracy'), ('Balanced Accuracy', 'balanced_accuracy'),
- ('AUC-ROC','auc_roc'), ('MCC','mcc')]:
- vals = fold_results[key]
- print(f" {name}: {vals.mean():.4f} ± {vals.std():.4f}")
- print("\n" + "="*60)
- print("FINAL TEST SET EVALUATION")
- print("="*60)
- train_D = compute_distance_matrix(train_models, best_params['sigma_kernel'])
- all_models = train_models + test_models
- full_D = compute_distance_matrix(all_models, best_params['sigma_kernel'])
- n_train = len(train_models)
- test_train_D = full_D[n_train:, :n_train]
- final_knn = KNeighborsClassifier(n_neighbors=best_params['k_neighbors'], metric='precomputed')
- final_knn.fit(train_D, train_labels)
- test_pred = final_knn.predict(test_train_D)
- test_prob = final_knn.predict_proba(test_train_D)[:, 1]
- test_accuracy = accuracy_score(test_labels, test_pred)
- test_bal_acc = balanced_accuracy_score(test_labels, test_pred)
- test_auc = roc_auc_score(test_labels, test_prob)
- test_mcc = matthews_corrcoef(test_labels, test_pred)
- test_cm = confusion_matrix(test_labels, test_pred)
- print(f" Accuracy: {test_accuracy:.4f}")
- print(f" Balanced Accuracy: {test_bal_acc:.4f}")
- print(f" AUC-ROC: {test_auc:.4f}")
- print(f" Matthews Corr: {test_mcc:.4f}")
- print("\nTest Confusion Matrix:")
- print(f" Control ADHD")
- print(f"Control {test_cm[0,0]:<7} {test_cm[0,1]:<7}")
- print(f"ADHD {test_cm[1,0]:<7} {test_cm[1,1]:<7}")
- create_evaluation_plots(fold_results)
- return {
- 'study': study,
- 'best_params': best_params,
- 'fold_results': fold_results,
- 'test_results': {
- 'accuracy': test_accuracy,
- 'balanced_accuracy': test_bal_acc,
- 'auc_roc': test_auc,
- 'mcc': test_mcc,
- 'confusion_matrix': test_cm
- }
- }
- if __name__ == "__main__":
- results = run_complete_analysis(
- # csv_path='.../synthetic_hmms_results.csv',
- csv_path='.../NxGx/hmm_results.csv',
- test_size=0.2,
- random_state=42,
- sigma_kernel_range=(0.001, 0.9),
- k_neighbors_range=(1, 11),
- n_trials=200,
- n_splits=7, # k-fold
- timeout=(60 * 60 * 3), # 60[sec]*60[min]*X[hour]
- n_states=3, #4, 5
- n_components=3, #4 ,5
- n_features=None
- )
- print("\n" + "="*60)
- print("ANALYSIS COMPLETE")
- print("="*60)
- print(f"Best σ_kernel: {results['best_params']['sigma_kernel']:.6g}")
- print(f"Best k_neighbors: {results['best_params']['k_neighbors']}")
- print(f"Final test balanced accuracy: {results['test_results']['balanced_accuracy']:.4f}")
- # %% [markdown]
- # # RKHS with precomputed distance and SVM Classification
- # %%
- import numpy as np
- import pandas as pd
- from sklearn.svm import SVC
- from sklearn.model_selection import StratifiedKFold, train_test_split
- from sklearn.metrics import (
- classification_report, accuracy_score, confusion_matrix,
- balanced_accuracy_score, roc_auc_score, matthews_corrcoef,
- roc_curve, precision_recall_curve, auc
- )
- import matplotlib.pyplot as plt
- import seaborn as sns
- import optuna
- from optuna.samplers import TPESampler
- import warnings
- import scipy.linalg
- warnings.filterwarnings('ignore')
- # -----------------------------
- # Stationary distribution utils
- # -----------------------------
- def compute_stationary_distribution(A, max_iter=1000, rel_tol=1e-6, abs_tol=1e-10):
- A = np.asarray(A, dtype=float)
- n = A.shape[0]
- rs = A.sum(axis=1, keepdims=True)
- bad = (rs[:, 0] == 0)
- if bad.any():
- A[bad] = 1.0 / n
- rs = A.sum(axis=1, keepdims=True)
- A = A / rs
- pi = np.ones(n) / n
- for _ in range(max_iter):
- pi_new = pi @ A
- if np.linalg.norm(pi_new - pi, 1) <= rel_tol * np.linalg.norm(pi, 1) + abs_tol:
- pi = pi_new
- break
- pi = pi_new
- pi = np.maximum(pi, 0.0)
- s = pi.sum()
- return pi / (s if s > 0 else 1.0)
- # -----------------------------------------
- # Exact multivariate Gaussian-Gaussian RBF
- # -----------------------------------------
- def k_gauss_of_gaussians(mu1, S1, mu2, S2, ell, eps=1e-9):
- mu1 = np.asarray(mu1); mu2 = np.asarray(mu2)
- S1 = np.asarray(S1); S2 = np.asarray(S2)
- D = mu1.shape[0]
- S1 = S1 + eps * np.eye(D)
- S2 = S2 + eps * np.eye(D)
- L_det = np.linalg.cholesky(np.eye(D) + (S1 + S2) / max(ell, eps))
- log_det_term = -np.sum(np.log(np.diag(L_det)))
- M = S1 + S2 + ell * np.eye(D)
- Lm = np.linalg.cholesky(M)
- d = mu1 - mu2
- y = scipy.linalg.solve_triangular(Lm, d, lower=True)
- quad = -0.5 * float(y @ y)
- return float(np.exp(log_det_term + quad))
- # -------------------------------------------------------
- # CSV extraction, with auto-detected feature dimensionality
- # -------------------------------------------------------
- def infer_n_features(df, n_states, n_components):
- f_idxs = []
- prefix = f"gmm_mean_0_0_f"
- for c in df.columns:
- if c.startswith(prefix):
- try:
- f_idxs.append(int(c.split('f')[-1]))
- except Exception:
- pass
- return (max(f_idxs) + 1) if f_idxs else 1
- def extract_hmm_parameters_from_csv(df, n_states=3, n_components=3, n_features=None):
- if n_features is None:
- n_features = infer_n_features(df, n_states, n_components)
- models = []
- print(f"Extracting HMM parameters from {len(df)} models...")
- print(f"Config inferred: n_states={n_states}, n_components={n_components}, n_features={n_features}")
- has_pi_s = all([f"stationary_pi_{i}" in df.columns for i in range(n_states)])
- has_w_eff = f"w_eff_0_0" in df.columns
- for idx, row in df.iterrows():
- try:
- A = np.zeros((n_states, n_states), dtype=float)
- for i in range(n_states):
- for j in range(n_states):
- A[i, j] = row[f"A_{i}{j}"]
- rs = A.sum(axis=1, keepdims=True); rs[rs == 0] = 1
- A = A / rs
- if has_pi_s:
- pi_s = np.array([row[f"stationary_pi_{i}"] for i in range(n_states)], dtype=float)
- if (not np.isfinite(pi_s).all()) or (pi_s.sum() <= 1e-12) or (pi_s < -1e-12).any():
- pi_s = compute_stationary_distribution(A)
- else:
- pi_s = np.maximum(pi_s, 0.0)
- s = pi_s.sum()
- pi_s = pi_s / (s if s > 0 else 1.0)
- else:
- pi_s = compute_stationary_distribution(A)
- alpha = np.zeros((n_states, n_components), dtype=float)
- mu = np.zeros((n_states, n_components, n_features), dtype=float)
- sigma = np.zeros((n_states, n_components, n_features, n_features), dtype=float)
- for i in range(n_states):
- for k in range(n_components):
- if has_w_eff:
- alpha[i, k] = row.get(f"gmm_weight_{i}_{k}", 0.0)
- else:
- alpha[i, k] = row[f"gmm_weight_{i}_{k}"]
- for f in range(n_features):
- mu[i, k, f] = row[f"gmm_mean_{i}_{k}_f{f}"]
- Si = np.zeros((n_features, n_features), dtype=float)
- for f1 in range(n_features):
- for f2 in range(f1, n_features):
- Si[f1, f2] = row[f"gmm_cov_{i}_{k}_f{f1}f{f2}"]
- if f1 != f2:
- Si[f2, f1] = Si[f1, f2]
- Si += 1e-6 * np.eye(n_features)
- try:
- np.linalg.cholesky(Si)
- except np.linalg.LinAlgError:
- Si = np.eye(n_features) * 0.1
- sigma[i, k] = Si
- if not has_w_eff:
- alpha = np.maximum(alpha, 0.0)
- sums = alpha.sum(axis=1, keepdims=True); sums[sums == 0] = 1.0
- alpha = alpha / sums
- model = {
- 'pi_stationary': pi_s,
- 'alpha': alpha,
- 'mu': mu,
- 'sigma': sigma,
- 'transition_matrix': A,
- 'n_states': n_states,
- 'n_components': n_components,
- 'n_features': n_features,
- 'model_id': idx,
- 'dataset_type': row.get('dataset_type', 'unknown'),
- 'series_id': row.get('series_id', idx)
- }
- if has_w_eff:
- w_eff = np.array(
- [row[f"w_eff_{i}_{k}"] for i in range(n_states) for k in range(n_components)],
- dtype=float
- )
- model['w_eff'] = w_eff
- models.append(model)
- except Exception as e:
- print(f"Error processing model {idx}: {str(e)}")
- continue
- print(f"Successfully extracted parameters for {len(models)} models")
- return models
- # --------------------------
- # Collapse to component lists
- # --------------------------
- def model_to_components(model):
- K = model['n_states']; M = model['n_components']; D = model['n_features']
- pi_s = model['pi_stationary']
- alpha = model['alpha']
- mu = model['mu']
- sigma = model['sigma']
- if 'w_eff' in model:
- w = model['w_eff'].copy()
- else:
- w = np.array([pi_s[i] * alpha[i, k] for i in range(K) for k in range(M)], dtype=float)
- MU = np.array([mu[i, k, :] for i in range(K) for k in range(M)], dtype=float)
- SIG = np.array([sigma[i, k, :, :] for i in range(K) for k in range(M)], dtype=float)
- return w, MU, SIG
- # -------------------------------------
- # Distance (MMD²) and matrix computation
- # -------------------------------------
- def enhanced_rkhs_distance_multivariate(modelP, modelQ, sigma_kernel):
- wP, MUP, SIGP = model_to_components(modelP)
- wQ, MUQ, SIGQ = model_to_components(modelQ)
- term_PP = 0.0
- for i in range(len(wP)):
- for j in range(len(wP)):
- term_PP += wP[i] * wP[j] * k_gauss_of_gaussians(MUP[i], SIGP[i], MUP[j], SIGP[j], sigma_kernel)
- term_QQ = 0.0
- for i in range(len(wQ)):
- for j in range(len(wQ)):
- term_QQ += wQ[i] * wQ[j] * k_gauss_of_gaussians(MUQ[i], SIGQ[i], MUQ[j], SIGQ[j], sigma_kernel)
- term_PQ = 0.0
- for i in range(len(wP)):
- for j in range(len(wQ)):
- term_PQ += wP[i] * wQ[j] * k_gauss_of_gaussians(MUP[i], SIGP[i], MUQ[j], SIGQ[j], sigma_kernel)
- dist2 = term_PP + term_QQ - 2.0 * term_PQ
- return max(float(dist2), 0.0)
- def compute_distance_matrix(models, sigma_kernel):
- n = len(models)
- D = np.zeros((n, n), dtype=float)
- print(f"Computing {n}x{n} RKHS distance matrix (σ_kernel={sigma_kernel:.6g})...")
- comps = [model_to_components(m) for m in models]
- def dist2_from_comps(cP, cQ):
- wP, MUP, SIGP = cP
- wQ, MUQ, SIGQ = cQ
- term_PP = 0.0
- for i in range(len(wP)):
- for j in range(len(wP)):
- term_PP += wP[i] * wP[j] * k_gauss_of_gaussians(MUP[i], SIGP[i], MUP[j], SIGP[j], sigma_kernel)
- term_QQ = 0.0
- for i in range(len(wQ)):
- for j in range(len(wQ)):
- term_QQ += wQ[i] * wQ[j] * k_gauss_of_gaussians(MUQ[i], SIGQ[i], MUQ[j], SIGQ[j], sigma_kernel)
- term_PQ = 0.0
- for i in range(len(wP)):
- for j in range(len(wQ)):
- term_PQ += wP[i] * wQ[j] * k_gauss_of_gaussians(MUP[i], SIGP[i], MUQ[j], SIGQ[j], sigma_kernel)
- return max(float(term_PP + term_QQ - 2.0 * term_PQ), 0.0)
- for i in range(n):
- D[i, i] = 0.0
- for j in range(i + 1, n):
- d2 = dist2_from_comps(comps[i], comps[j])
- D[i, j] = D[j, i] = d2
- if (i + 1) % 20 == 0:
- print(f" Processed {i + 1}/{n} rows")
- print(f"Done. Stats: min={D.min():.6f}, max={D.max():.6f}, mean={D.mean():.6f}")
- return D
- # Distance to kernel transformation
- def distance_to_kernel(D, gamma, epsilon=0.0):
- """
- Transform distance matrix to kernel (similarity) matrix using exponential transformation.
- Args:
- D: Distance matrix (n x n)
- gamma: Kernel bandwidth parameter
- epsilon: Optional diagonal regularization (default 0.0)
- Returns:
- K: Kernel matrix (n x n)
- """
- n = D.shape[0]
- K = np.exp(-D / (2 * gamma**2))
- if epsilon > 0:
- K += epsilon * np.eye(n)
- return K
- # Kernel matrix diagnostics
- def check_kernel_psd(K, verbose=True):
- """
- Check if kernel matrix is positive semi-definite.
- Args:
- K: Kernel matrix
- verbose: Print diagnostics
- Returns:
- is_psd: Boolean indicating if K is PSD
- min_eigval: Minimum eigenvalue
- """
- eigvals = np.linalg.eigvalsh(K)
- min_eigval = eigvals.min()
- max_eigval = eigvals.max()
- condition = max_eigval / max(abs(min_eigval), 1e-10)
- if verbose:
- print(f"Kernel diagnostics:")
- print(f" Min eigenvalue: {min_eigval:.6e}")
- print(f" Max eigenvalue: {max_eigval:.6e}")
- print(f" Condition number: {condition:.6e}")
- if min_eigval < -1e-10:
- print(f" WARNING: Kernel has negative eigenvalues!")
- elif min_eigval < 1e-12:
- print(f" WARNING: Very small positive eigenvalues, may be numerically unstable")
- else:
- print(f" Status: Kernel is well-conditioned")
- is_psd = min_eigval >= -1e-10
- return is_psd, min_eigval
- # ----------------------------
- # Cross-validation with SVM
- # ----------------------------
- def kfold_cross_validation_svm(models, labels, sigma_kernel, gamma, C,
- n_splits=5, random_state=42, epsilon=0.0):
- """
- K-fold cross-validation using SVM with precomputed kernel.
- Args:
- models: List of HMM-GMM models
- labels: Class labels
- sigma_kernel: Bandwidth for MMD² distance computation
- gamma: Bandwidth for kernel transformation
- C: SVM regularization parameter
- n_splits: Number of CV folds
- random_state: Random seed
- epsilon: Diagonal regularization (default 0.0)
- """
- skf = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=random_state)
- # Stage 1: Compute distance matrix
- full_D = compute_distance_matrix(models, sigma_kernel)
- # Stage 2: Transform to kernel matrix
- full_K = distance_to_kernel(full_D, gamma, epsilon)
- fold_results = {
- 'accuracy': [], 'balanced_accuracy': [], 'auc_roc': [], 'mcc': [],
- 'predictions': [], 'probabilities': [], 'true_labels': [],
- 'confusion_matrices': []
- }
- print(f"Performing {n_splits}-fold cross-validation with SVM...")
- print(f" σ_kernel={sigma_kernel:.6g}, γ={gamma:.6g}, C={C:.6g}")
- for fold, (train_idx, val_idx) in enumerate(skf.split(models, labels)):
- print(f" Fold {fold + 1}/{n_splits}")
- # Extract kernel submatrices for train and validation
- train_K = full_K[np.ix_(train_idx, train_idx)]
- val_train_K = full_K[np.ix_(val_idx, train_idx)]
- # Train SVM with precomputed kernel
- svm = SVC(kernel='precomputed', C=C, probability=True, random_state=random_state)
- svm.fit(train_K, labels[train_idx])
- # Predict on validation set
- val_pred = svm.predict(val_train_K)
- val_prob = svm.predict_proba(val_train_K)[:, 1]
- fold_results['predictions'].extend(val_pred.tolist())
- fold_results['probabilities'].extend(val_prob.tolist())
- fold_results['true_labels'].extend(labels[val_idx].tolist())
- # Compute metrics
- acc = accuracy_score(labels[val_idx], val_pred)
- bacc = balanced_accuracy_score(labels[val_idx], val_pred)
- auc_ = roc_auc_score(labels[val_idx], val_prob)
- mcc = matthews_corrcoef(labels[val_idx], val_pred)
- cm = confusion_matrix(labels[val_idx], val_pred)
- fold_results['accuracy'].append(acc)
- fold_results['balanced_accuracy'].append(bacc)
- fold_results['auc_roc'].append(auc_)
- fold_results['mcc'].append(mcc)
- fold_results['confusion_matrices'].append(cm)
- print(f" Acc={acc:.4f}, BalAcc={bacc:.4f}, AUC={auc_:.4f}, MCC={mcc:.4f}")
- # Convert to arrays
- for k in ['accuracy', 'balanced_accuracy', 'auc_roc', 'mcc']:
- fold_results[k] = np.array(fold_results[k])
- fold_results['predictions'] = np.array(fold_results['predictions'])
- fold_results['probabilities'] = np.array(fold_results['probabilities'])
- fold_results['true_labels'] = np.array(fold_results['true_labels'])
- return fold_results
- # -----------------------------
- # Optuna optimization for SVM
- # -----------------------------
- def hyperparameter_optimization_svm(models, labels,
- sigma_kernel_range=(1e-6, 1.0),
- gamma_range=(1e-6, 1.0),
- C_range=(1e-3, 1e3),
- n_trials=50, n_splits=5,
- timeout=1800, epsilon=0.0):
- """
- Hyperparameter optimization for SVM with RKHS kernel.
- Optimizes:
- - sigma_kernel: Bandwidth for MMD² distance computation
- - gamma: Bandwidth for kernel transformation
- - C: SVM regularization parameter
- """
- print("Starting hyperparameter optimization for SVM...")
- print(f"Search space:")
- print(f" sigma_kernel: log-uniform from {sigma_kernel_range[0]:.1e} to {sigma_kernel_range[1]:.1e}")
- print(f" gamma: log-uniform from {gamma_range[0]:.1e} to {gamma_range[1]:.1e}")
- print(f" C: log-uniform from {C_range[0]:.1e} to {C_range[1]:.1e}")
- print(f" n_trials: {n_trials}, CV folds: {n_splits}")
- def objective(trial):
- # Sample hyperparameters
- sigma_kernel = trial.suggest_float('sigma_kernel', sigma_kernel_range[0],
- sigma_kernel_range[1], log=True)
- gamma = trial.suggest_float('gamma', gamma_range[0], gamma_range[1], log=True)
- C = trial.suggest_float('C', C_range[0], C_range[1], log=True)
- try:
- # Stage 1: Compute distance matrix
- full_D = compute_distance_matrix(models, sigma_kernel)
- # Stage 2: Transform to kernel matrix
- full_K = distance_to_kernel(full_D, gamma, epsilon)
- # Cross-validation
- skf = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=42)
- scores = []
- for train_idx, val_idx in skf.split(models, labels):
- train_K = full_K[np.ix_(train_idx, train_idx)]
- val_train_K = full_K[np.ix_(val_idx, train_idx)]
- svm = SVC(kernel='precomputed', C=C, probability=True, random_state=42)
- svm.fit(train_K, labels[train_idx])
- pred = svm.predict(val_train_K)
- scores.append(balanced_accuracy_score(labels[val_idx], pred))
- return float(np.mean(scores))
- except Exception as e:
- print(f"Trial failed: {e}")
- return 0.0
- study = optuna.create_study(direction='maximize', sampler=TPESampler(seed=42),
- study_name="RKHS_SVM_Optimization")
- study.optimize(objective, n_trials=n_trials, timeout=timeout)
- print("\nOptimization completed!")
- print(f"Best balanced accuracy: {study.best_value:.4f}")
- print(f"Best parameters:")
- print(f" sigma_kernel: {study.best_params['sigma_kernel']:.6g}")
- print(f" gamma: {study.best_params['gamma']:.6g}")
- print(f" C: {study.best_params['C']:.6g}")
- return study
- # -----------------
- # Evaluation plots
- # -----------------
- def create_evaluation_plots(fold_results, class_names=['Control', 'ADHD']):
- fig, axes = plt.subplots(2, 3, figsize=(18, 12))
- overall_cm = confusion_matrix(fold_results['true_labels'], fold_results['predictions'])
- sns.heatmap(overall_cm, annot=True, fmt='d', cmap='Blues',
- xticklabels=class_names, yticklabels=class_names, ax=axes[0,0])
- axes[0,0].set_title('Overall Confusion Matrix')
- axes[0,0].set_xlabel('Predicted')
- axes[0,0].set_ylabel('Actual')
- fpr, tpr, _ = roc_curve(fold_results['true_labels'], fold_results['probabilities'])
- auc_score = auc(fpr, tpr)
- axes[0,1].plot(fpr, tpr, lw=2, label=f'ROC (AUC={auc_score:.3f})')
- axes[0,1].plot([0, 1], [0, 1], lw=2, linestyle='--', label='Random')
- 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)
- precision, recall, _ = precision_recall_curve(fold_results['true_labels'],
- fold_results['probabilities'])
- pr_auc = auc(recall, precision)
- axes[0,2].plot(recall, precision, lw=2, label=f'PR (AUC={pr_auc:.3f})')
- 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)
- metrics_data = [fold_results['accuracy'], fold_results['balanced_accuracy'],
- fold_results['auc_roc'], fold_results['mcc']]
- axes[1,0].boxplot(metrics_data, labels=['Acc', 'Bal.Acc', 'AUC', 'MCC'])
- axes[1,0].set_title('CV Metric Distributions')
- axes[1,0].set_ylabel('Score')
- axes[1,0].grid(True, alpha=0.3)
- folds = np.arange(1, len(fold_results['accuracy']) + 1)
- axes[1,1].plot(folds, fold_results['balanced_accuracy'], 'o-', label='Balanced Acc')
- axes[1,1].plot(folds, fold_results['auc_roc'], 'o-', label='AUC-ROC')
- 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)
- class_counts = np.bincount(fold_results['true_labels'])
- axes[1,2].pie(class_counts, labels=class_names, autopct='%1.1f%%', startangle=90)
- axes[1,2].set_title('Class Distribution')
- plt.tight_layout()
- plt.show()
- # -----------------------
- # Full analysis pipeline with SVM
- # -----------------------
- def run_complete_analysis_svm(csv_path, test_size=0.2, random_state=42,
- sigma_kernel_range=(1e-6, 1.0),
- gamma_range=(1e-6, 1.0),
- C_range=(1e-3, 1e3),
- n_trials=50, n_splits=5, timeout=1800,
- n_states=3, n_components=3, n_features=None,
- epsilon=0.0, check_psd=True):
- """
- Complete analysis pipeline using SVM with RKHS kernel.
- Args:
- csv_path: Path to CSV with HMM-GMM parameters
- test_size: Fraction for test set
- random_state: Random seed
- sigma_kernel_range: Range for distance computation bandwidth
- gamma_range: Range for kernel transformation bandwidth
- C_range: Range for SVM regularization
- n_trials: Number of Optuna trials
- n_splits: Number of CV folds
- timeout: Optimization timeout in seconds
- n_states: Number of HMM states
- n_components: Number of GMM components per state
- n_features: Number of features (auto-detected if None)
- epsilon: Diagonal regularization for kernel matrix
- check_psd: Whether to check kernel PSD property
- """
- print("=== RKHS-SVM ANALYSIS FOR HMM-GMM MODELS ===\n")
- print("Loading CSV...")
- df = pd.read_csv(csv_path)
- df['class_label'] = (df['dataset_type'] == 'ADHD').astype(int)
- print(f"Dataset summary:")
- print(f" Total: {len(df)} | ADHD: {df['class_label'].sum()} | Control: {(1-df['class_label']).sum()}")
- print(f" Class balance: {df['class_label'].mean():.3f}")
- train_df, test_df = train_test_split(
- df, test_size=test_size, random_state=random_state, stratify=df['class_label']
- )
- if n_features is None:
- n_features = infer_n_features(train_df, n_states, n_components)
- print("\nExtracting HMM-GMM parameters...")
- train_models = extract_hmm_parameters_from_csv(train_df, n_states, n_components, n_features)
- train_labels = train_df['class_label'].values
- test_models = extract_hmm_parameters_from_csv(test_df, n_states, n_components, n_features)
- test_labels = test_df['class_label'].values
- # Hyperparameter optimization
- study = hyperparameter_optimization_svm(
- train_models, train_labels,
- sigma_kernel_range=sigma_kernel_range,
- gamma_range=gamma_range,
- C_range=C_range,
- n_trials=n_trials, n_splits=n_splits, timeout=timeout,
- epsilon=epsilon
- )
- best_params = study.best_params
- # Cross-validation with best parameters
- print("\nCross-validation with best params...")
- fold_results = kfold_cross_validation_svm(
- train_models, train_labels,
- best_params['sigma_kernel'],
- best_params['gamma'],
- best_params['C'],
- n_splits=n_splits,
- random_state=random_state,
- epsilon=epsilon
- )
- print("\n" + "="*60)
- print("CROSS-VALIDATION RESULTS")
- print("="*60)
- print(f" σ_kernel: {best_params['sigma_kernel']:.6g}")
- print(f" γ (gamma): {best_params['gamma']:.6g}")
- print(f" C: {best_params['C']:.6g}")
- for name, key in [('Accuracy', 'accuracy'), ('Balanced Accuracy', 'balanced_accuracy'),
- ('AUC-ROC','auc_roc'), ('MCC','mcc')]:
- vals = fold_results[key]
- print(f" {name}: {vals.mean():.4f} ± {vals.std():.4f}")
- # Final test set evaluation
- print("\n" + "="*60)
- print("FINAL TEST SET EVALUATION")
- print("="*60)
- # Compute distance and kernel matrices
- train_D = compute_distance_matrix(train_models, best_params['sigma_kernel'])
- train_K = distance_to_kernel(train_D, best_params['gamma'], epsilon)
- if check_psd:
- print("\nChecking training kernel matrix...")
- check_kernel_psd(train_K)
- all_models = train_models + test_models
- full_D = compute_distance_matrix(all_models, best_params['sigma_kernel'])
- full_K = distance_to_kernel(full_D, best_params['gamma'], epsilon)
- n_train = len(train_models)
- test_train_K = full_K[n_train:, :n_train]
- # Train final SVM on full training set
- final_svm = SVC(kernel='precomputed', C=best_params['C'],
- probability=True, random_state=random_state)
- final_svm.fit(train_K, train_labels)
- # Predict on test set
- test_pred = final_svm.predict(test_train_K)
- test_prob = final_svm.predict_proba(test_train_K)[:, 1]
- # Compute test metrics
- test_accuracy = accuracy_score(test_labels, test_pred)
- test_bal_acc = balanced_accuracy_score(test_labels, test_pred)
- test_auc = roc_auc_score(test_labels, test_prob)
- test_mcc = matthews_corrcoef(test_labels, test_pred)
- test_cm = confusion_matrix(test_labels, test_pred)
- print(f" Accuracy: {test_accuracy:.4f}")
- print(f" Balanced Accuracy: {test_bal_acc:.4f}")
- print(f" AUC-ROC: {test_auc:.4f}")
- print(f" Matthews Corr: {test_mcc:.4f}")
- print("\nTest Confusion Matrix:")
- print(f" Control ADHD")
- print(f"Control {test_cm[0,0]:<7} {test_cm[0,1]:<7}")
- print(f"ADHD {test_cm[1,0]:<7} {test_cm[1,1]:<7}")
- # Create evaluation plots
- create_evaluation_plots(fold_results)
- return {
- 'study': study,
- 'best_params': best_params,
- 'fold_results': fold_results,
- 'test_results': {
- 'accuracy': test_accuracy,
- 'balanced_accuracy': test_bal_acc,
- 'auc_roc': test_auc,
- 'mcc': test_mcc,
- 'confusion_matrix': test_cm,
- 'predictions': test_pred,
- 'probabilities': test_prob
- },
- 'final_model': final_svm
- }
- if __name__ == "__main__":
- results = run_complete_analysis_svm(
- # csv_path='.../synthetic_hmms_results.csv',
- csv_path='.../NxGx/hmm_results.csv',
- test_size=0.2,
- random_state=42,
- # Hyperparameter ranges
- sigma_kernel_range=(1e-4, 1e-1), # For distance computation
- gamma_range=(1e-5, 1e-2), # For kernel transformation
- C_range=(1e-3, 1e+3), # SVM regularization
- # Optimization settings
- n_trials=250,
- n_splits=7, # k-fold
- timeout=(60 * 60 * 4), # hours
- # Model configuration
- n_states=3,# 4, 5
- n_components=3, # 4, 5
- n_features=None, # Auto-detect
- # Kernel settings
- epsilon=0.0, # No regularization by default
- check_psd=True # Check kernel properties
- )
- print("\n" + "="*60)
- print("ANALYSIS COMPLETE")
- print("="*60)
- print(f"Best σ_kernel: {results['best_params']['sigma_kernel']:.6g}")
- print(f"Best γ (gamma): {results['best_params']['gamma']:.6g}")
- print(f"Best C: {results['best_params']['C']:.6g}")
- 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
- Automatics Research Group, Universidad Tecnológica de Pereira (UTP), Pereira 660003, Colombia; (C.K.V.-M.); (J.G.-G.)
- Psiquiatría Neurociencias y Comunidad, Universidad Tecnológica de Pereira (UTP), Pereira 660003, Colombia
Abstract
Electroencephalography (EEG) provides a non-invasive means of supporting Attention-Deficit/
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
d2eddf4778b0944543c6ea141c817cdd2323cc00, 11 July 2026Availability: 1 check, the latest on 27 September 2026: the link answers
- 27 September 2026: the link answers
9 files
- Synthetic_EEG_data_gener
ation.ipynb — Jupyter, 130 lines, 2 matches - baseline/
baseline.py — Python, 866 lines, 3 matches - nested CV/
HIS_with_knn_and_svm.ipy — Jupyter, 1,510 linesnb - nested CV/
PPK_with_knn_and_svm.ipy — Jupyter, 1,384 lines, 1 matchnb - single held out test/
ppk_knn_svm_eeg_dataset. — Jupyter, 1,281 linesipynb - single held out test/
rkhs-knn-svm-eeg-dataset — Jupyter, 1,221 lines, 4 matches.ipynb - training-hmm.ipynb — Jupyter, 1,264 lines, 2 matches
- LICENSE — License, 22 lines
- README.md — Text, 141 lines
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://
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://
BibTeX
@article{lopezortiz2026e
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/
url = {https://
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/
VL - 26
IS - 15
SP - 4773
SN - 1424-8220
PB - Multidisciplinary Digital Publishing Institute (MDPI)
DO - 10.3390/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.3390/
"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":
"volume": "26",
"issue": "15",
"page": "4773",
"DOI": "10.3390/
"PMID": "42590549",
"PMCID": "PMC13469683",
"ISSN": "1424-8220",
"publisher": "Multidisciplinary Digital Publishing Institute (MDPI)",
"URL": "https://
"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 oneIn 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 dataJournal: 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: eLifeIn 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 biologyIn 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 biologyIn 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 communicationsIn 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 researchIn 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 communicationsIn 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 oneIn 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 biologyIn 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.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 1 repository of the authors' code, each at its verified commit and with its license, 7 scripts, and 12 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:0b6a4364f6979885…
Add the badge to its README
The badge links the code to this page. Copy one of these into the README of the paper's code: only you decide where it goes, and nothing is changed for you.
Markdown
[, paste the snippet at the top, then “Commit changes…” and, to review it first, “Create a new branch and start a pull request”. You open the pull request; OSCR asks for no permission.
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.
