diff --git a/examples/quantum/classification.py b/examples/quantum/classification.py index a7fd775..d9ebacb 100644 --- a/examples/quantum/classification.py +++ b/examples/quantum/classification.py @@ -5,6 +5,7 @@ import copy import os import math +import sys import time # Import the time module import json # Import the json module for checkpoints from concurrent.futures import ProcessPoolExecutor, as_completed @@ -208,7 +209,11 @@ def run_fold(fold_num, train_index, test_index, X_cv_pool, y_cv_pool, model_q.fit(X_train_fold, y_train_fold) print(f"Retraining Quantum Model (D={dimensionality}; epochs=10)...") - error_rate, epochs = model_q.retrain(X_train_fold, y_train_fold, epochs=10) + error_rate, epochs = model_q.retrain(X_train_fold, y_train_fold, epochs=10, verbose=False) + # Capture the per-epoch error curve so it can be saved separately from the + # rest of the fold results (see the aggregated epochs.json produced by the + # main script). + fold_epoch_history = list(getattr(model_q, "retrain_history_", [])) print(f"\nEvaluating Quantum Model (D={dimensionality})...") y_pred_q, scores_q = model_q.predict(X_test_fold) @@ -302,6 +307,9 @@ def run_fold(fold_num, train_index, test_index, X_cv_pool, y_cv_pool, 'q_matrix': fold_matrix_q.tolist(), 'q_time': fold_time_q, 'q_roc_data': fold_roc_data_q, + 'q_epochs': fold_epoch_history, + 'q_final_epoch': int(epochs), + 'q_final_error': float(error_rate), # VQC/QSVC entries are disabled; re-enable together with the # corresponding model blocks above. @@ -479,7 +487,20 @@ def remove_contradicting(Xs, ys): print(f"\n--- Launching {len(fold_splits)} folds in parallel ---") + def _render_progress(completed, total, width=40): + """Render a simple in-place progress bar on stderr.""" + pct = completed / total if total else 0.0 + filled = int(width * pct) + bar = "#" * filled + "-" * (width - filled) + sys.stderr.write(f"\rProgress: [{bar}] {pct * 100:6.2f}% ({completed}/{total} folds)") + sys.stderr.flush() + if completed == total: + sys.stderr.write("\n") + sys.stderr.flush() + results_by_fold = {} + total_folds = len(fold_splits) + _render_progress(0, total_folds) with ProcessPoolExecutor(max_workers=N_SPLITS) as executor: futures = [ executor.submit( @@ -500,7 +521,24 @@ def remove_contradicting(Xs, ys): for future in as_completed(futures): fold_num, fold_data = future.result() results_by_fold[fold_num] = fold_data - print(f"--- COMPLETED FOLD {fold_num}/{N_SPLITS} ---") + _render_progress(len(results_by_fold), total_folds) + + # --- Aggregate per-epoch training curves into a single JSON file --- + # At the end of the run we have 5 fold__results.json files (one per + # fold) plus this single epochs.json file that groups the per-epoch + # error history for each fold in one place. + epochs_by_fold = {} + for fold_num in sorted(results_by_fold.keys()): + fold_result = results_by_fold[fold_num] + epochs_by_fold[f"fold_{fold_num}"] = { + "epochs": fold_result.get("q_epochs", []), + "final_epoch": fold_result.get("q_final_epoch"), + "final_error": fold_result.get("q_final_error"), + } + epochs_file = "epochs.json" + with open(epochs_file, "w") as f: + json.dump(epochs_by_fold, f, indent=4) + print(f"--- SAVED PER-EPOCH HISTORY FOR ALL FOLDS TO {epochs_file} ---") # Aggregate results in deterministic fold order (1..N_SPLITS) so the # downstream summary is identical to the sequential version. diff --git a/hdlib/model/classification.py b/hdlib/model/classification.py index 8d88c1f..b1aa5ff 100644 --- a/hdlib/model/classification.py +++ b/hdlib/model/classification.py @@ -1594,8 +1594,13 @@ def predict(self, test_points: List[List[float]]) -> Tuple[List[str], List[List[ return predictions, similarities - def retrain(self, train_points: List[List[float]], train_labels: List[str], epochs: int=10, lr: float=1.0) -> Tuple[float, int]: + def retrain(self, train_points: List[List[float]], train_labels: List[str], epochs: int=10, lr: float=1.0, verbose: bool=True) -> Tuple[float, int]: """Retrain the model by adjusting class prototypes based on misclassified samples. + + Per-epoch error rates are recorded on ``self.retrain_history_`` as a list + of ``{"epoch": int, "error": float}`` entries (starting at epoch 0), so + callers can access the training curve without parsing stdout. Setting + ``verbose=False`` suppresses the per-epoch prints. """ if not hasattr(self, "classes_"): @@ -1605,7 +1610,9 @@ def retrain(self, train_points: List[List[float]], train_labels: List[str], epoc predictions, _ = self.predict(train_points) best_error = sum(1 for p, t in zip(predictions, train_labels) if p != t) / len(train_labels) - print(f"\tepoch 0: {best_error}") + self.retrain_history_ = [{"epoch": 0, "error": float(best_error)}] + if verbose: + print(f"\tepoch 0: {best_error}") if best_error == 0.0: return best_error, 0 @@ -1658,7 +1665,9 @@ def retrain(self, train_points: List[List[float]], train_labels: List[str], epoc predictions, _ = self.predict(train_points) current_error = sum(1 for p, t in zip(predictions, train_labels) if p != t) / len(train_labels) - print(f"\tepoch {epoch}: {current_error}") + self.retrain_history_.append({"epoch": epoch, "error": float(current_error)}) + if verbose: + print(f"\tepoch {epoch}: {current_error}") # 5. Check early stopping condition if current_error >= best_error: