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 without given lines: weak matches, whose lines are not tinted
- [1] § 2. Materials and Methods › 2.3. Preprocessing ↔ baseline/baseline.py, lines 131 to 143 · score 0.83 · 13–30 Hz, 0.5–4 Hz, 8–13 Hz, 4–8 Hz, theta, delta · Report a mismatch
- [2] § 3. Results › 3.1. Performance Analysis on the Synthetic Dataset ↔ single held out test/rkhs-knn-svm-eeg-dataset.ipynb, lines 1037 to 1185 · score 0.77 · full training, RKHS kernel, AUC ROC, Optuna, cross validation, Matthews · Report a mismatch
- [3] § 3. Results › 3.3. Performance Analysis on the Real EEG Dataset ↔ baseline/baseline.py, lines 1 to 61 · score 0.73 · Alim, Imtiaz, reimplemented, Hjorth, baseline, theta · Report a mismatch
- [4] § 2. Materials and Methods › 2.4. Hidden Markov Model-Based Time-Series Representation ↔ training-hmm.ipynb, lines 649 to 745 · score 0.71 · Baum Welch, log likelihood, EM, iterations, fit, training · Report a mismatch
- [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 · Report a mismatch
- [6] § 3. Results › 3.2. Hyperparameter Tuning ↔ single held out test/rkhs-knn-svm-eeg-dataset.ipynb, lines 1037 to 1185 · score 0.65 · kernel transformation, AUC ROC, confusion matrices, Matthews, balanced accuracy, bandwidth · Report a mismatch
- [7] § 2. Materials and Methods ↔ single held out test/rkhs-knn-svm-eeg-dataset.ipynb, lines 832 to 908 · score 0.62 · HMM GMM model, distance computation, precomputed kernel, RKHS, EEG · Report a mismatch
- [8] § 2. Materials and Methods › 2.1. Synthetic EEG Benchmark Signals ↔ baseline/baseline.py, lines 1 to 61 · score 0.61 · wave, Hz, brain, theta, phase, delta · Report a mismatch
- [9] § 2. Materials and Methods › 2.6. Classification with Precomputed Kernels ↔ single held out test/rkhs-knn-svm-eeg-dataset.ipynb, lines 798 to 830 · score 0.60 · positive semi definite, kernel matrix, SVM · Report a mismatch
- [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 · Report a mismatch
- [11] § 3. Results › 3.2. Hyperparameter Tuning ↔ training-hmm.ipynb, lines 1143 to 1229 · score 0.52 · Log likelihood, HMM convergence, iteration, errors, fit · Report a mismatch
- [12] § 2. Materials and Methods › 2.6. Classification with Precomputed Kernels ↔ nested CV/PPK_with_knn_and_svm.ipynb, lines 293 to 317 · score 0.51 · Gram matrix, unnecessary, majority, formulation, SVM, distances · Report a mismatch
Paper
Loaded from Europe PMC by your browser; OSCR does not store it: 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.3390/s23073763 [code]
- A Novel OpenBCI Framework for EEG-Based Neurophysiological Experiments.Journal: Sensors (Basel, Switzerland)In common: TensorFlow, seaborn, scikit-learn, 3 other tools, EEG, 1 reference, author David Cárdenas-Peña
- [2] doi:10.1016/j.heliyon.2024.e26028 [code]
- A novel approach to identify the brain regions that best classify ADHD by means of EEG and deep learning.Journal: HeliyonIn common: TensorFlow, scikit-learn, pandas, 3 other tools, ADHD, EEG, 3 references
- [3] 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
- [4] doi:10.3390/s25227067
- Takens-Based Kernel Transfer Entropy Connectivity Network for Motor Imagery Classification.Journal: Sensors (Basel, Switzerland)In common: EEG, 2 references, author David Cárdenas-Peña
- [5] 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
- [6] doi:10.1371/journal.pone.0347671 [code]
- RMETNet: A cross-subject motor imagery EEG signal classification model based on TSLANet and riemannian geometry features.Journal: PloS oneIn common: TensorFlow, seaborn, scikit-learn, 4 other tools, EEG, 1 reference
- [7] doi:10.1038/s41598-024-62230-9 [code]
- Feasibility of decoding covert speech in ECoG with a Transformer trained on overt speech.Journal: Scientific reportsIn common: TensorFlow, seaborn, scikit-learn, 4 other tools, 1 reference
- [8] doi:10.1007/s10484-024-09675-w [code]
- Theta/
Beta Ratio Neurofeedback Effects on Resting and Task-Related Theta Activity in Children with ADHD. Journal: Applied psychophysiology and biofeedbackIn common: seaborn, pandas, SciPy, 2 other tools, ADHD, EEG, 1 reference - [9] doi:10.1038/s41598-026-42310-8 [code]
- Sleep awake detection from leg-worn wearables using deep sensor fusion.Journal: Scientific reportsIn common: TensorFlow, seaborn, scikit-learn, 4 other tools, ADHD
- [10] doi:10.1038/s41598-026-50791-w [code]
- Hybrid Vi+ECNN framework for advanced ADHD diagnostic accuracy in medical imaging.Journal: Scientific reportsIn common: seaborn, scikit-learn, pandas, 2 other tools, ADHD, 1 reference
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 the code's README
If you are an author or a maintainer of this paper's code, you can add this badge. It says what OSCR knows of the paper (its code, its validated tracing map) and links the code to this page. Paste one of these snippets under the README's title. Keep the two marks around it, so that the badge can be replaced in place and is never added twice. OSCR changes nothing in your repository.
Markdown
<!-- oscr-badge:start -->
[](https://openscicode.org/badge/doi/10.3390/s26154773)
<!-- oscr-badge:end -->reStructuredText
.. oscr-badge:start
.. image:: https://openscicode.org/badge/doi/10.3390/s26154773.svg
:target: https://openscicode.org/badge/doi/10.3390/s26154773
:alt: OSCR
.. oscr-badge:endHTML
<!-- oscr-badge:start -->
<a href="https://openscicode.org/badge/doi/10.3390/s26154773"><img src="https://openscicode.org/badge/doi/10.3390/s26154773.svg" alt="OSCR"></a>
<!-- oscr-badge:end -->On GitHub, open the README, choose Edit (the pencil), paste the snippet under the title, then "Commit changes".
If you choose to, you can have the badge kept up to date for you, with oscr badge --write from the command line or with the GitHub Action in your repository's workflows. Every other form and option (a card, AsciiDoc, LaTeX, shields.io) is on the badge 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: code errors and mismatches between the code and the paper about this paper and its code, from readers and its authors, beside the paper and its tracing map.
Report a mismatch between the code and the paper · Report a code error (signed in)
Reproductions: reports from readers who ran the authors' code and did not get the paper's result, what they ran, with which environment, commit and data.
Report a reproduction failure (signed in)
Versions of its code: the releases of its code tied to a version of this paper (the preprint, the accepted manuscript, the version of record…), each with the tracing map versioned with it.
Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.
