diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml index 18037d4..3dbeeb4 100644 --- a/.github/ISSUE_TEMPLATE/question.yml +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -90,5 +90,3 @@ body: options: - label: I agree to follow this project's Code of Conduct required: true - -# Made with Bob diff --git a/CHANGELOG.md b/CHANGELOG.md index 80629df..046288e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,91 @@ All notable changes to QBioCode will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added +- **Data Generation**: New blob dataset generator + - `generate_blobs_datasets()`: Create isotropic Gaussian blob datasets + - `generate_default_blobs_datasets()`: Quick generation with default parameters + - Follows QBioCode data generation patterns + - Useful for clustering and classification benchmarks + +- **Evaluation Metrics**: Added generalized `evaluation_metrics()` function to `qbiocode.evaluation.model_evaluation` + - Supports multiple metrics: accuracy, brier, f1, precision, recall, auc + - Configurable via `metrics` parameter (default: ['accuracy', 'brier']) + - Backward compatible: returns (accuracy, brier) tuple by default + - Supports both binary and multi-class classification + - Provides calibration quality assessment via Brier score + - Handles edge cases (e.g., single class in test set) + - Now available in main API (previously only in tutorial helpers) + +- **Quantum Ensemble Learning**: New unified quantum ensemble classifier + - `compute_qensemble()`: Quantum ensemble with configurable construction methods + - Implements quantum ensemble using controlled operations and superposition + - Two ensemble methods via `ensemble_method` parameter: + - `"swap"` (default): Fixed controlled-SWAP operations (faster, deterministic) + - `"random_unitary"`: Haar-random unitaries (more general, potentially better generalization) + - Three ensemble modes: balanced, unbalanced, and pair_sample + - Support for configurable ensemble depth (d) and operations per qubit (n_swap) + - Quantum cosine similarity classifier using SWAP test + - New utility functions in `qbiocode.utils`: + - `normalize_data()`: Normalize data for quantum state encoding + - `label_to_array()`: Convert binary labels to one-hot encoding + - `prepare_training_set()`: Prepare balanced training subsets + - `retrieve_probabilities()`: Extract probabilities from measurement counts (generic quantum utility) + - `execute_circuit()`: General-purpose Aer simulator execution (reusable across quantum algorithms) + - Based on Macaluso et al., "A variational algorithm for quantum ensemble learning" (2023) + - Integrated from tutorial/QEnsemble with full API compatibility + - Code organization: Extracted reusable functions to utils for broader applicability + +- **Testing Infrastructure**: Comprehensive test suite for core functionality + - `tests/test_data_generation.py`: Tests for data generation utilities + - `tests/test_file_utilities.py`: Tests for file operations and utilities + - `tests/test_generator_dispatch.py`: Tests for generator dispatch logic + - `tests/conftest.py`: Pytest configuration and fixtures + - Test coverage for utility modules and data generation helpers + +- **Code Quality Tools**: Enhanced development tooling + - `isort` integration for consistent import ordering + - Configuration in `pyproject.toml` for isort settings + - Added to `dev` and `all` dependency groups + +- **Documentation**: Testing instructions in README + - Added "Running Tests" section with pytest usage + - Instructions for installing development dependencies + +### Changed +- **Code Formatting**: Applied consistent code style across entire codebase + - Ran `black` formatter on all Python files + - Ran `isort` for standardized import ordering + - Fixed invalid escape sequences in visualization module + - Improved code readability and maintainability + +- **CI/CD Improvements**: Stabilized continuous integration pipeline + - Updated GitHub Actions workflows to Node.js 24 + - Fixed CI code quality checks for import ordering + - Fixed CI type-check issues + - Fixed documentation build process + - Fixed Pandoc compatibility issues + - Enhanced workflow reliability across all platforms + +- **Testing**: Improved test reliability + - Fixed path-order assumptions in duplicate-file tests + - Tests now work consistently across different file systems + +### Fixed +- Invalid escape sequence in `qbiocode/visualization/visualize_correlation.py` +- Import ordering issues throughout codebase +- Type-check errors in CI pipeline +- Documentation build failures +- Path handling in cross-platform tests + +### Planned Features +- Additional quantum ML algorithms +- Enhanced meta-learning capabilities +- More dataset complexity metrics +- Performance optimizations +- Extended Galaxy tool integration ## [0.1.0] - 2026-04-06 @@ -101,57 +186,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - XGBoost for gradient boosting - hydra-core for configuration management -## [Unreleased] - -### Added -- **Testing Infrastructure**: Comprehensive test suite for core functionality - - `tests/test_data_generation.py`: Tests for data generation utilities - - `tests/test_file_utilities.py`: Tests for file operations and utilities - - `tests/test_generator_dispatch.py`: Tests for generator dispatch logic - - `tests/conftest.py`: Pytest configuration and fixtures - - Test coverage for utility modules and data generation helpers - -- **Code Quality Tools**: Enhanced development tooling - - `isort` integration for consistent import ordering - - Configuration in `pyproject.toml` for isort settings - - Added to `dev` and `all` dependency groups - -- **Documentation**: Testing instructions in README - - Added "Running Tests" section with pytest usage - - Instructions for installing development dependencies - -### Changed -- **Code Formatting**: Applied consistent code style across entire codebase - - Ran `black` formatter on all Python files - - Ran `isort` for standardized import ordering - - Fixed invalid escape sequences in visualization module - - Improved code readability and maintainability - -- **CI/CD Improvements**: Stabilized continuous integration pipeline - - Updated GitHub Actions workflows to Node.js 24 - - Fixed CI code quality checks for import ordering - - Fixed CI type-check issues - - Fixed documentation build process - - Fixed Pandoc compatibility issues - - Enhanced workflow reliability across all platforms - -- **Testing**: Improved test reliability - - Fixed path-order assumptions in duplicate-file tests - - Tests now work consistently across different file systems - -### Fixed -- Invalid escape sequence in `qbiocode/visualization/visualize_correlation.py` -- Import ordering issues throughout codebase -- Type-check errors in CI pipeline -- Documentation build failures -- Path handling in cross-platform tests - -### Planned Features -- Additional quantum ML algorithms -- Enhanced meta-learning capabilities -- More dataset complexity metrics -- Performance optimizations -- Extended Galaxy tool integration --- diff --git a/README.md b/README.md index 986644c..5527bf8 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ QBioCode provides tools for benchmarking quantum and classical machine learning - **QProfiler**: Automated ML benchmarking with data complexity analysis - **QSage**: Meta-learning tool for intelligent model selection - **Data Generation**: Create artificial datasets with controlled complexity -- **Quantum ML Support**: QSVC, PQK, VQC, QNN implementations +- **Quantum ML Support**: QSVC, PQK, VQC, QNN, Quantum Ensemble implementations - **Classical ML Baselines**: RF, SVM, LR, DT, NB, MLP, XGBoost - **Comprehensive Documentation**: Detailed tutorials and API reference @@ -34,10 +34,10 @@ QBioCode requires Python **3.10 or higher** and has been tested with Python vers pip install qbiocode # Install with apps support (QProfiler, QSage) -pip install qbiocode[apps] +pip install 'qbiocode[apps]' # Install with all optional dependencies -pip install qbiocode[all] +pip install 'qbiocode[all]' ``` #### Install with Conda @@ -79,7 +79,7 @@ source .env/bin/activate # On Windows: .env\Scripts\activate pip install -e . # Install with apps support (QProfiler, QSage) -pip install -e ".[apps]" +pip install -e '.[apps]' ``` **macOS Users:** XGBoost requires OpenMP. Install it using Homebrew: @@ -94,7 +94,7 @@ For detailed installation instructions, see the [Installation Guide](https://ibm ```bash # Install the package with development dependencies -pip install -e ".[dev]" +pip install -e '.[dev]' # Run the test suite python -m pytest @@ -117,7 +117,7 @@ qbc.generate_data( ) # Run QProfiler -from apps.qprofiler import qprofiler +from qbiocode.apps.qprofiler import qprofiler import yaml config = yaml.safe_load(open('configs/config.yaml')) @@ -142,7 +142,7 @@ QProfiler provides a comprehensive benchmarking pipeline that: qprofiler --config configs/config.yaml # Python API -from apps.qprofiler import qprofiler +from qbiocode.apps.qprofiler import qprofiler qprofiler.main(config) ``` @@ -164,7 +164,7 @@ QSage uses surrogate models trained on extensive benchmarking data to: qsage --data your_data.csv --output predictions.csv # Python API -from apps.sage.sage import QuantumSage +from qbiocode.apps.sage.sage import QuantumSage sage = QuantumSage(data=benchmark_df, features=features, metrics=metrics) predictions = sage.predict(new_dataset_features) ``` @@ -197,7 +197,14 @@ Learn to use meta-learning for model selection: - Analyzing prediction accuracy - Understanding feature importance -### 4. [Quantum Projection Learning](tutorial/Quantum_Projection_Learning/QPL_example.ipynb) +### 4. [Quantum Ensemble Learning](tutorial/QEnsemble/QEnsemble_example_blobs.ipynb) +Learn quantum ensemble methods for improved classification: +- Fixed swap-based ensemble approach +- Random unitary-based ensemble approach +- Quantum superposition for evaluating multiple training configurations +- Comparison with classical ensemble methods + +### 5. [Quantum Projection Learning](tutorial/Quantum_Projection_Learning/QPL_example.ipynb) Advanced quantum ML techniques with classical baselines. ## 🔧 Core Modules @@ -228,6 +235,34 @@ qbc.generate_data(type_of_data='classes', ...) - Projected Quantum Kernel (PQK) - Variational Quantum Classifier (VQC) - Quantum Neural Network (QNN) +- Quantum Ensemble (QEnsemble) - swap and random unitary methods + +**Quantum Ensemble Usage:** +```python +from qbiocode.learning import compute_qensemble +from sklearn.datasets import make_blobs +from sklearn.model_selection import train_test_split + +# Generate data +X, y = make_blobs(n_samples=100, n_features=2, centers=2, random_state=42) +X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3) + +# Run quantum ensemble with swap method +results_swap = compute_qensemble( + X_train, X_test, y_train, y_test, + ensemble_method='swap', + n_ensemble=4, + seed=42 +) + +# Run quantum ensemble with random unitary method +results_random = compute_qensemble( + X_train, X_test, y_train, y_test, + ensemble_method='random_unitary', + n_ensemble=4, + seed=42 +) +``` ### Embeddings - PCA, LLE, Isomap, Spectral Embedding diff --git a/docs/source/api/qbiocode.data_generation.rst b/docs/source/api/qbiocode.data_generation.rst index 7cbdbc7..cb8f0ff 100644 --- a/docs/source/api/qbiocode.data_generation.rst +++ b/docs/source/api/qbiocode.data_generation.rst @@ -12,6 +12,14 @@ qbiocode.data\_generation.generator module :show-inheritance: :undoc-members: +qbiocode.data\_generation.make\_blobs module +-------------------------------------------- + +.. automodule:: qbiocode.data_generation.make_blobs + :members: + :show-inheritance: + :undoc-members: + qbiocode.data\_generation.make\_circles module ---------------------------------------------- diff --git a/docs/source/api/qbiocode.learning.rst b/docs/source/api/qbiocode.learning.rst index c3667b6..0f01517 100644 --- a/docs/source/api/qbiocode.learning.rst +++ b/docs/source/api/qbiocode.learning.rst @@ -44,6 +44,14 @@ qbiocode.learning.compute\_pqk module :show-inheritance: :undoc-members: +qbiocode.learning.compute\_qensemble module +------------------------------------------- + +.. automodule:: qbiocode.learning.compute_qensemble + :members: + :show-inheritance: + :undoc-members: + qbiocode.learning.compute\_qnn module ------------------------------------- diff --git a/docs/source/api/qbiocode.utils.rst b/docs/source/api/qbiocode.utils.rst index 336d7aa..b27ab94 100644 --- a/docs/source/api/qbiocode.utils.rst +++ b/docs/source/api/qbiocode.utils.rst @@ -20,6 +20,14 @@ qbiocode.utils.dataset\_checkpoint module :show-inheritance: :undoc-members: +qbiocode.utils.data\_encoding module +------------------------------------ + +.. automodule:: qbiocode.utils.data_encoding + :members: + :show-inheritance: + :undoc-members: + qbiocode.utils.find\_duplicates module -------------------------------------- diff --git a/docs/source/installation.md b/docs/source/installation.md index 42020a3..cc629d7 100644 --- a/docs/source/installation.md +++ b/docs/source/installation.md @@ -20,13 +20,17 @@ pip install qbiocode Install with apps support (QProfiler, QSage): ```bash -pip install qbiocode[apps] +pip install 'qbiocode[apps]' ``` Install with all optional dependencies: ```bash -pip install qbiocode[all] +pip install 'qbiocode[all]' +``` + +```{note} +**For zsh users:** The quotes around `'qbiocode[apps]'` are required because zsh interprets square brackets as glob patterns. Bash users can omit quotes, but using them works in both shells. ``` ## Install with Conda @@ -60,7 +64,7 @@ conda activate qbiocode pip install qbiocode # Or with apps support -pip install qbiocode[apps] +pip install 'qbiocode[apps]' ``` **Note**: See [docs/CONDA_SUBMISSION.md](../CONDA_SUBMISSION.md) for information about the conda submission process. diff --git a/docs/source/tutorials.md b/docs/source/tutorials.md index c6d4c88..b0f7cc8 100644 --- a/docs/source/tutorials.md +++ b/docs/source/tutorials.md @@ -50,7 +50,40 @@ Explore QSage, an intelligent meta-learning system that predicts which machine l --- -### 4. Quantum Projection Learning (QPL) +### 4. Quantum Ensemble Learning + +Learn how to use quantum ensemble methods to improve classification performance by leveraging quantum superposition to evaluate multiple training set configurations simultaneously. This tutorial demonstrates two quantum ensemble approaches. + +📓 View Tutorial Notebook + +**What You'll Learn:** +- Generate blob datasets for binary classification +- Implement fixed swap-based quantum ensemble method +- Implement random unitary-based quantum ensemble method +- Use quantum SWAP test for cosine similarity measurement +- Compare quantum ensemble with classical baselines (Random Forest, XGBoost) +- Evaluate performance using accuracy and Brier score metrics +- Understand quantum superposition for ensemble learning + +**Key Concepts:** +- Quantum ensemble learning via superposition +- SWAP test for quantum state comparison +- Controlled-SWAP operations for deterministic data rearrangement +- Haar-random unitaries for general mixing +- One-hot encoding for quantum state preparation +- Quantum advantage in ensemble methods + +**Methods:** +1. **Swap Method**: Uses fixed controlled-SWAP operations to create deterministic permutations of training data +2. **Random Unitary Method**: Applies Haar-random unitary transformations for more general data mixing + +**References:** +- Macaluso et al. (2023) - "A variational algorithm for quantum neural networks" +- Rhrissorrakrai et al. (2025) - "Quantum Ensemble Learning" (arXiv:2506.02213) + +--- + +### 5. Quantum Projection Learning (QPL) Learn about Quantum Projection Learning (QPL), a technique that combines quantum feature maps with multiple classical machine learning algorithms. This comprehensive tutorial demonstrates how to systematically evaluate quantum-enhanced features across different learners. @@ -82,7 +115,7 @@ Learn about Quantum Projection Learning (QPL), a technique that combines quantum --- -### 5. Projected Quantum Kernel (PQK) - Ovarian Cancer Survival Prediction +### 6. Projected Quantum Kernel (PQK) - Ovarian Cancer Survival Prediction Learn how to apply Projected Quantum Kernels (PQK) to real-world cancer genomics data for survival prediction. This advanced tutorial demonstrates quantum-enhanced machine learning on multi-omics ovarian cancer data from the Multi-Omics Cancer Benchmark (TCGA preprocessed data). diff --git a/qbiocode/__init__.py b/qbiocode/__init__.py index 67dff52..6e24dbd 100644 --- a/qbiocode/__init__.py +++ b/qbiocode/__init__.py @@ -15,6 +15,7 @@ - data_generation: Synthetic dataset generators - visualization: Result visualization and correlation analysis - utils: Helper functions and utilities +- apps: Command-line applications (QProfiler, QSage) Quick Start ----------- @@ -23,6 +24,9 @@ >>> generate_data(type_of_data='circles', save_path='data/circles') >>> # Train a random forest model >>> results = compute_rf(X_train, y_train, X_test, y_test) +>>> # Use QProfiler programmatically +>>> from qbiocode.apps.qprofiler import qprofiler +>>> qprofiler.main(config) """ # ====== Import data generation functions ====== @@ -76,6 +80,10 @@ plot_results_correlation, ) +# ====== Expose apps submodule ====== +# Apps are available as qbiocode.apps.qprofiler, qbiocode.apps.sage +from . import apps # noqa: F401 + __all__ = [ # Version "__version__", @@ -123,4 +131,6 @@ "generate_spheres_datasets", "generate_spirals_datasets", "generate_swiss_roll_datasets", + # Apps submodule + "apps", ] diff --git a/apps/__init__.py b/qbiocode/apps/__init__.py similarity index 100% rename from apps/__init__.py rename to qbiocode/apps/__init__.py diff --git a/apps/qprofiler/__init__.py b/qbiocode/apps/qprofiler/__init__.py similarity index 100% rename from apps/qprofiler/__init__.py rename to qbiocode/apps/qprofiler/__init__.py diff --git a/apps/qprofiler/cli.py b/qbiocode/apps/qprofiler/cli.py similarity index 100% rename from apps/qprofiler/cli.py rename to qbiocode/apps/qprofiler/cli.py diff --git a/apps/qprofiler/configs/__init__.py b/qbiocode/apps/qprofiler/configs/__init__.py similarity index 100% rename from apps/qprofiler/configs/__init__.py rename to qbiocode/apps/qprofiler/configs/__init__.py diff --git a/apps/qprofiler/configs/config.yaml b/qbiocode/apps/qprofiler/configs/config.yaml similarity index 100% rename from apps/qprofiler/configs/config.yaml rename to qbiocode/apps/qprofiler/configs/config.yaml diff --git a/apps/qprofiler/qprofiler.py b/qbiocode/apps/qprofiler/qprofiler.py similarity index 100% rename from apps/qprofiler/qprofiler.py rename to qbiocode/apps/qprofiler/qprofiler.py diff --git a/apps/qprofiler/qprofiler_batchmode.py b/qbiocode/apps/qprofiler/qprofiler_batchmode.py similarity index 100% rename from apps/qprofiler/qprofiler_batchmode.py rename to qbiocode/apps/qprofiler/qprofiler_batchmode.py diff --git a/apps/sage/__init__.py b/qbiocode/apps/sage/__init__.py similarity index 100% rename from apps/sage/__init__.py rename to qbiocode/apps/sage/__init__.py diff --git a/apps/sage/sage.py b/qbiocode/apps/sage/sage.py similarity index 100% rename from apps/sage/sage.py rename to qbiocode/apps/sage/sage.py diff --git a/qbiocode/data_generation/__init__.py b/qbiocode/data_generation/__init__.py index 1afc139..a33905a 100644 --- a/qbiocode/data_generation/__init__.py +++ b/qbiocode/data_generation/__init__.py @@ -6,6 +6,7 @@ with varying parameters, useful for benchmarking and evaluation. Available dataset generators: +- generate_blobs_datasets: Isotropic Gaussian blobs (clusters) - generate_circles_datasets: 2D concentric circles - generate_moons_datasets: 2D interleaving half-circles - generate_classification_datasets: High-dimensional multi-class data @@ -15,6 +16,7 @@ - generate_swiss_roll_datasets: 3D Swiss roll manifold """ +from .make_blobs import generate_blobs_datasets, generate_default_blobs_datasets from .make_circles import generate_circles_datasets from .make_class import generate_classification_datasets from .make_moons import generate_moons_datasets @@ -24,6 +26,8 @@ from .make_swiss_roll import generate_swiss_roll_datasets __all__ = [ + "generate_blobs_datasets", + "generate_default_blobs_datasets", "generate_circles_datasets", "generate_moons_datasets", "generate_classification_datasets", @@ -32,3 +36,4 @@ "generate_spirals_datasets", "generate_swiss_roll_datasets", ] + diff --git a/qbiocode/data_generation/make_blobs.py b/qbiocode/data_generation/make_blobs.py new file mode 100644 index 0000000..f24c253 --- /dev/null +++ b/qbiocode/data_generation/make_blobs.py @@ -0,0 +1,211 @@ +""" +Generate synthetic blob (Gaussian cluster) datasets. + +This module creates multiple configurations of blob datasets with varying +numbers of samples, features, centers, and cluster standard deviations, +useful for testing clustering and classification algorithms. +""" + +from sklearn.datasets import make_blobs +import pandas as pd +import numpy as np +import json +import itertools +import os + + +def generate_blobs_datasets( + n_samples, + n_features, + centers, + cluster_std, + save_path=None, + random_state=42, +): + """ + Generate multiple blob (Gaussian cluster) datasets with varying parameters. + + Creates a series of synthetic datasets consisting of isotropic Gaussian blobs + for clustering and classification tasks. Each configuration varies the number + of samples, features, cluster centers, and cluster spread. + + Parameters + ---------- + n_samples : list of int + List of sample sizes to generate for each configuration. + Example: [100, 200, 300] + n_features : list of int + List of feature dimensions to generate. + Example: [2, 4, 8] + centers : list of int + List of numbers of cluster centers (classes). + Example: [2, 3, 4] + cluster_std : list of float + List of standard deviations of the clusters. + Example: [0.5, 1.0, 1.5, 2.0] + save_path : str, optional + Directory path to save generated datasets. If None, datasets are not saved. + Default: None + random_state : int, optional + Random seed for reproducibility. + Default: 42 + + Returns + ------- + dict + Dictionary containing generated datasets with keys as configuration strings + and values as tuples of (X, y) where: + - X : pd.DataFrame, shape (n_samples, n_features) + Feature matrix + - y : pd.Series, shape (n_samples,) + Target labels + + Notes + ----- + - Generates all combinations of input parameters + - Each blob is an isotropic Gaussian distribution + - Useful for testing classification and clustering algorithms + - Blobs are well-separated when cluster_std is small relative to center distances + + Examples + -------- + >>> from qbiocode.data_generation import generate_blobs_datasets + >>> + >>> # Generate simple blob datasets + >>> datasets = generate_blobs_datasets( + ... n_samples=[100, 200], + ... n_features=[2, 4], + ... centers=[2, 3], + ... cluster_std=[1.0, 1.5] + ... ) + >>> + >>> # Access a specific configuration + >>> X, y = datasets['n_samples_100_n_features_2_centers_2_cluster_std_1.0'] + >>> print(f"Shape: {X.shape}, Classes: {y.nunique()}") + Shape: (100, 2), Classes: 2 + + >>> # Save datasets to disk + >>> datasets = generate_blobs_datasets( + ... n_samples=[100], + ... n_features=[2], + ... centers=[3], + ... cluster_std=[1.0], + ... save_path='./data/blobs' + ... ) + + See Also + -------- + generate_circles_datasets : Generate concentric circles + generate_moons_datasets : Generate interleaving half-circles + generate_classification_datasets : Generate high-dimensional classification data + + References + ---------- + .. [1] Pedregosa et al., "Scikit-learn: Machine Learning in Python", + JMLR 12, pp. 2825-2830, 2011. + """ + dataset_config = {} + + # Generate all combinations of parameters + param_combinations = list(itertools.product( + n_samples, n_features, centers, cluster_std + )) + + for n_samp, n_feat, n_cent, c_std in param_combinations: + # Generate dataset + X, y = make_blobs( + n_samples=n_samp, + n_features=n_feat, + centers=n_cent, + cluster_std=c_std, + random_state=random_state, + shuffle=True + ) + + # Convert to pandas for consistency with other QBioCode functions + X_df = pd.DataFrame(X, columns=[f'feature_{i}' for i in range(n_feat)]) + y_series = pd.Series(y, name='target') + + # Create configuration key + config_key = f'n_samples_{n_samp}_n_features_{n_feat}_centers_{n_cent}_cluster_std_{c_std}' + + # Store in dictionary + dataset_config[config_key] = (X_df, y_series) + + # Save if path provided + if save_path is not None: + os.makedirs(save_path, exist_ok=True) + + # Save features + X_df.to_csv( + os.path.join(save_path, f'{config_key}_X.csv'), + index=False + ) + + # Save labels + y_series.to_csv( + os.path.join(save_path, f'{config_key}_y.csv'), + index=False, + header=True + ) + + # Save configuration metadata + metadata = { + 'n_samples': n_samp, + 'n_features': n_feat, + 'centers': n_cent, + 'cluster_std': c_std, + 'random_state': random_state, + 'dataset_type': 'blobs' + } + + with open(os.path.join(save_path, f'{config_key}_config.json'), 'w') as f: + json.dump(metadata, f, indent=2) + + return dataset_config + + +# Default parameter configurations for quick generation +DEFAULT_N_SAMPLES = [100, 200, 300] +DEFAULT_N_FEATURES = [2, 4, 8] +DEFAULT_CENTERS = [2, 3, 4] +DEFAULT_CLUSTER_STD = [0.5, 1.0, 1.5, 2.0] + + +def generate_default_blobs_datasets(save_path=None, random_state=42): + """ + Generate blob datasets with default parameter configurations. + + Convenience function that generates a standard set of blob datasets + using predefined parameter ranges suitable for most testing scenarios. + + Parameters + ---------- + save_path : str, optional + Directory path to save generated datasets. If None, datasets are not saved. + Default: None + random_state : int, optional + Random seed for reproducibility. + Default: 42 + + Returns + ------- + dict + Dictionary containing generated datasets. + + Examples + -------- + >>> from qbiocode.data_generation import generate_default_blobs_datasets + >>> datasets = generate_default_blobs_datasets() + >>> print(f"Generated {len(datasets)} dataset configurations") + """ + return generate_blobs_datasets( + n_samples=DEFAULT_N_SAMPLES, + n_features=DEFAULT_N_FEATURES, + centers=DEFAULT_CENTERS, + cluster_std=DEFAULT_CLUSTER_STD, + save_path=save_path, + random_state=random_state + ) + +# Made with Bob diff --git a/qbiocode/evaluation/__init__.py b/qbiocode/evaluation/__init__.py index eddfc06..659b3dd 100644 --- a/qbiocode/evaluation/__init__.py +++ b/qbiocode/evaluation/__init__.py @@ -9,6 +9,7 @@ Available Functions ------------------- - modeleval: Evaluate model performance with multiple metrics +- evaluation_metrics: Calculate accuracy and Brier score from predictions - evaluate: Comprehensive dataset complexity evaluation - model_run: Automated model training and evaluation pipeline @@ -22,11 +23,12 @@ """ from .dataset_evaluation import evaluate -from .model_evaluation import modeleval +from .model_evaluation import evaluation_metrics, modeleval from .model_run import model_run __all__ = [ "modeleval", + "evaluation_metrics", "evaluate", "model_run", ] diff --git a/qbiocode/evaluation/model_evaluation.py b/qbiocode/evaluation/model_evaluation.py index ed493a6..bc3bf3f 100644 --- a/qbiocode/evaluation/model_evaluation.py +++ b/qbiocode/evaluation/model_evaluation.py @@ -2,15 +2,16 @@ import time from typing import Literal - import pandas as pd -from sklearn.metrics import accuracy_score, f1_score, roc_auc_score -from sklearn.preprocessing import MinMaxScaler, OneHotEncoder, OrdinalEncoder, StandardScaler - -from qbiocode.utils.helper_fn import print_results # ====== Scikit-learn imports ====== +from sklearn.preprocessing import StandardScaler, MinMaxScaler +from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder +from sklearn.metrics import f1_score, accuracy_score, roc_auc_score + +from qbiocode.utils.helper_fn import print_results + def modeleval( y_test, y_predicted, beg_time, params, args, model: str, verbose=True, average="weighted" @@ -77,3 +78,124 @@ def modeleval( ], } ) + + +def evaluation_metrics(predictions, y_test, metrics=["accuracy", "brier"], save=False): + """ + Calculate evaluation metrics for classification predictions. + + Computes specified metrics for model predictions. Supports accuracy, Brier score, + F1 score, precision, recall, and AUC-ROC. The Brier score measures the mean + squared difference between predicted probabilities and actual outcomes, providing + a measure of calibration quality. + + Parameters + ---------- + predictions : np.ndarray + Predicted probabilities, shape (n_samples, n_classes) + y_test : np.ndarray + True labels, shape (n_samples,) + metrics : list of str, optional + List of metrics to compute. Options: 'accuracy', 'brier', 'f1', + 'precision', 'recall', 'auc' (default: ['accuracy', 'brier']) + save : bool, optional + Whether to save results (reserved for future use, default: False) + + Returns + ------- + tuple or dict + If metrics=['accuracy', 'brier'] (default): returns (accuracy, brier_score) + Otherwise: returns dict with requested metrics as keys + + Examples + -------- + >>> import numpy as np + >>> from qbiocode.evaluation import evaluation_metrics + >>> + >>> # Binary classification example - default metrics + >>> predictions = np.array([[0.8, 0.2], [0.3, 0.7], [0.9, 0.1]]) + >>> y_test = np.array([0, 1, 0]) + >>> accuracy, brier = evaluation_metrics(predictions, y_test) + >>> print(f"Accuracy: {accuracy:.2f}, Brier Score: {brier:.3f}") + Accuracy: 1.00, Brier Score: 0.060 + + >>> # Multiple metrics + >>> results = evaluation_metrics(predictions, y_test, + ... metrics=['accuracy', 'brier', 'f1', 'auc']) + >>> print(results) + {'accuracy': 1.0, 'brier': 0.06, 'f1': 1.0, 'auc': 1.0} + + Notes + ----- + - For binary classification, Brier score is computed using the probability + of the positive class + - For multi-class classification, the average Brier score across all classes + is returned + - F1, precision, and recall use weighted averaging for multi-class + - AUC uses one-vs-rest for multi-class + - Lower Brier scores indicate better calibrated probability predictions + + References + ---------- + Brier, G. W. (1950). "Verification of forecasts expressed in terms of probability". + Monthly Weather Review, 78(1), 1-3. + """ + import numpy as np + from sklearn.metrics import ( + brier_score_loss, + f1_score, + precision_score, + recall_score, + roc_auc_score, + ) + + # Get predicted classes + y_pred = np.argmax(predictions, axis=1) + + results = {} + + # Calculate requested metrics + if "accuracy" in metrics: + results["accuracy"] = accuracy_score(y_test, y_pred) + + if "brier" in metrics: + if predictions.shape[1] == 2: + # Binary classification: use probability of positive class + results["brier"] = brier_score_loss(y_test, predictions[:, 1]) + else: + # Multi-class: use average Brier score across all classes + results["brier"] = np.mean( + [ + brier_score_loss(y_test == i, predictions[:, i]) + for i in range(predictions.shape[1]) + ] + ) + + if "f1" in metrics: + results["f1"] = f1_score(y_test, y_pred, average="weighted", zero_division=0) + + if "precision" in metrics: + results["precision"] = precision_score(y_test, y_pred, average="weighted", zero_division=0) + + if "recall" in metrics: + results["recall"] = recall_score(y_test, y_pred, average="weighted", zero_division=0) + + if "auc" in metrics: + try: + if predictions.shape[1] == 2: + # Binary classification + results["auc"] = roc_auc_score(y_test, predictions[:, 1]) + else: + # Multi-class: one-vs-rest + results["auc"] = roc_auc_score( + y_test, predictions, multi_class="ovr", average="weighted" + ) + except ValueError: + # Handle cases where AUC cannot be computed (e.g., single class in y_test) + results["auc"] = np.nan + + # For backward compatibility: return tuple if default metrics + if metrics == ["accuracy", "brier"]: + return results["accuracy"], results["brier"] + + return results diff --git a/qbiocode/evaluation/model_run.py b/qbiocode/evaluation/model_run.py index 5e8843b..4ddcd35 100644 --- a/qbiocode/evaluation/model_run.py +++ b/qbiocode/evaluation/model_run.py @@ -42,16 +42,17 @@ def model_run(X_train, X_test, y_train, y_test, data_key, args): # These imports happen inside the function, not at module level from qbiocode.learning.compute_dt import compute_dt, compute_dt_opt from qbiocode.learning.compute_lr import compute_lr, compute_lr_opt + from qbiocode.learning.compute_rf import compute_rf, compute_rf_opt from qbiocode.learning.compute_mlp import compute_mlp, compute_mlp_opt - from qbiocode.learning.compute_nb import compute_nb, compute_nb_opt + from qbiocode.learning.compute_xgb import compute_xgb, compute_xgb_opt from qbiocode.learning.compute_pqk import compute_pqk + from qbiocode.learning.compute_qpl import compute_qpl from qbiocode.learning.compute_qnn import compute_qnn from qbiocode.learning.compute_qsvc import compute_qsvc - from qbiocode.learning.compute_rf import compute_rf, compute_rf_opt + from qbiocode.learning.compute_nb import compute_nb, compute_nb_opt from qbiocode.learning.compute_svc import compute_svc, compute_svc_opt from qbiocode.learning.compute_vqc import compute_vqc - from qbiocode.learning.compute_xgb import compute_xgb, compute_xgb_opt - + # Build model dictionary compute_ml_dict = { "svc_opt": compute_svc_opt, @@ -72,10 +73,12 @@ def model_run(X_train, X_test, y_train, y_test, data_key, args): "vqc": compute_vqc, "qnn": compute_qnn, "pqk": compute_pqk, + "qpl": compute_qpl, + } # Quantum models don't have _opt versions (use separate configs for hyperparameter tuning) - quantum_models = {"qsvc", "qnn", "vqc", "pqk"} + quantum_models = {"qsvc", "qnn", "vqc", "pqk", "qpl"} # Run classical and quantum models n_jobs = len(args["model"]) diff --git a/qbiocode/learning/__init__.py b/qbiocode/learning/__init__.py index 47c4cdd..a436157 100644 --- a/qbiocode/learning/__init__.py +++ b/qbiocode/learning/__init__.py @@ -22,14 +22,20 @@ - Quantum Support Vector Classifier (QSVC) - Variational Quantum Classifier (VQC) - Projected Quantum Kernel (PQK) +- Quantum Ensemble (QEnsemble) - supports both fixed swap and random unitary methods Usage ----- ->>> from qbiocode.learning import compute_rf, compute_qsvc +>>> from qbiocode.learning import compute_rf, compute_qsvc, compute_qensemble >>> # Train classical model >>> results = compute_rf(X_train, y_train, X_test, y_test) >>> # Train quantum model >>> qresults = compute_qsvc(X_train, y_train, X_test, y_test) +>>> # Train quantum ensemble with fixed swaps (default) +>>> qens_results = compute_qensemble(X_train, X_test, y_train, y_test, args) +>>> # Train quantum ensemble with random unitaries +>>> qens_random = compute_qensemble(X_train, X_test, y_train, y_test, args, +... ensemble_method="random_unitary") """ # Classical ML algorithms @@ -48,8 +54,11 @@ compute_xgb_opt = None # type: ignore from .compute_pqk import compute_pqk +from .compute_qpl import compute_qpl # Quantum ML algorithms +from .compute_pqk import compute_pqk +from .compute_qensemble import compute_qensemble from .compute_qnn import compute_qnn from .compute_qsvc import compute_qsvc from .compute_vqc import compute_vqc @@ -71,8 +80,11 @@ "compute_xgb", "compute_xgb_opt", # Quantum algorithms - "compute_qnn", - "compute_qsvc", - "compute_vqc", - "compute_pqk", + 'compute_qnn', + 'compute_qsvc', + 'compute_vqc', + 'compute_pqk', + 'compute_qpl', + 'compute_qensemble', ] + diff --git a/qbiocode/learning/compute_pqk.py b/qbiocode/learning/compute_pqk.py index a822c28..ab7d2bb 100644 --- a/qbiocode/learning/compute_pqk.py +++ b/qbiocode/learning/compute_pqk.py @@ -27,7 +27,6 @@ from qiskit import QuantumCircuit from qiskit.quantum_info import Pauli from sklearn import svm -from sklearn.model_selection import GridSearchCV import qbiocode.utils.qutils as qutils @@ -48,7 +47,6 @@ def compute_pqk( primitive="estimator", entanglement="linear", reps=2, - classical_models=None, ): """ This function generates quantum circuits, computes projections of the data onto these circuits, @@ -77,17 +75,12 @@ def compute_pqk( primitive (str): Primitive type to use, default is 'estimator'. entanglement (str): Entanglement strategy, default is 'linear'. reps (int): Number of repetitions for the feature map, default is 2. - classical_models (list): List of classical models to train on quantum projections. - Options: 'rf', 'mlp', 'svc', 'lr', 'xgb'. - Default is ['rf', 'mlp', 'svc', 'lr', 'xgb']. Returns: modeleval (pd.DataFrame): A DataFrame containing evaluation metrics and model parameters for all models. """ - # Set default classical models if not provided - if classical_models is None: - classical_models = ["rf", "mlp", "svc", "lr", "xgb"] + classical_models = ["svc"] beg_time = time.time() feat_dimension = X_train.shape[1] @@ -103,7 +96,7 @@ def compute_pqk( ) # This function ensures that all multiplicative factors of data features inside single qubit gates are 1.0 - def data_map_func(x: np.ndarray) -> float: + def data_map_func(x: np.ndarray): """ Define a function map from R^n to R. @@ -111,10 +104,16 @@ def data_map_func(x: np.ndarray) -> float: x: data Returns: - float: the mapped value + the mapped value (float or Parameter expression) """ coeff = x[0] / 2 if len(x) == 1 else reduce(lambda m, n: (m * n) / 2, x) - return float(coeff) + # Check if coeff is a numeric type before converting to float + # If it's a Parameter expression, return it as-is for Qiskit to handle + try: + return float(coeff) + except (TypeError, ValueError): + # If conversion fails, it's likely a Parameter expression + return coeff # choose a method for mapping your features onto the circuit feature_map, _ = qutils.get_feature_map( @@ -194,7 +193,7 @@ def data_map_func(x: np.ndarray) -> float: for i in range(len(dat)): if i % 100 == 0: - print("at datapoint {}".format(i)) + print(f"at datapoint {str(i)}") # Get training sample parameters = dat[i] @@ -223,175 +222,27 @@ def data_map_func(x: np.ndarray) -> float: projections_test = np.load(file_projection_test) projections_test = np.array(projections_test).reshape(len(projections_test), -1) - # Check if XGBoost is requested but not available - if "xgb" in classical_models and not XGBOOST_AVAILABLE: - warnings.warn( - "XGBoost is not properly installed or configured and will be skipped.\n" - "On macOS, you may need to install OpenMP:\n" - " brew install libomp\n" - "Then reinstall XGBoost:\n" - " pip install --force-reinstall xgboost\n" - "See installation documentation for more details.\n" - f"Continuing with other models: {[m for m in classical_models if m != 'xgb']}", - UserWarning, - ) - # Remove xgb from the list - classical_models = [m for m in classical_models if m != "xgb"] - - # If no models remain after filtering, raise an error - if not classical_models: - raise ValueError( - "No valid classical models specified. Please provide at least one model from: 'rf', 'mlp', 'svc', 'lr', 'xgb'" - ) - - model_res = [] - for method in classical_models: - if method == "rf": - model = create_rf_model(args["seed"]) - elif method == "svc": - model = create_svc_model(args["seed"]) - elif method == "mlp": - model = create_mlp_model(args["seed"]) - elif method == "lr": - model = create_lr_model(args["seed"]) - elif method == "xgb": - model = create_xgb_model(args["seed"]) - else: - warnings.warn( - f"Unknown model type '{method}' skipped. Valid options: 'rf', 'mlp', 'svc', 'lr', 'xgb'", - UserWarning, - ) - continue - - method_pqk = "pqk_" + method - print(method_pqk) - model.fit(projections_train, y_train) - y_predicted = model.predict(projections_test) - - hyperparameters = { - "feature_map": feature_map.__class__.__name__, - "feature_map_reps": reps, - "entanglement": entanglement, - "best_params": model.best_params_, - # Add other hyperparameters as needed - } - model_params = hyperparameters - - model_res.append( - modeleval( - y_test, y_predicted, beg_time, model_params, args, model=method_pqk, verbose=verbose - ) - ) - - model_res = pd.concat(model_res) - return model_res - - -def create_xgb_model(seed): - # Initialize the XGBoost Classifier - if not XGBOOST_AVAILABLE: - raise ImportError( - "XGBoost is not properly installed or configured.\n" - "On macOS, you may need to install OpenMP:\n" - " brew install libomp\n\n" - "Then reinstall XGBoost:\n" - " pip install --force-reinstall xgboost\n\n" - "See installation documentation for more details." - ) - xgb = XGBClassifier(objective="binary:logistic", eval_metric="logloss") # type: ignore - - xgb_param_distributions = { - "n_estimators": [100, 200, 300], - "learning_rate": [0.01, 0.1, 0.2], - "max_depth": [3, 5, 7], - "subsample": [0.7, 0.8, 1.0], - "colsample_bytree": [0.7, 0.8, 1.0], - "min_child_weight": [1, 3, 5], - } - - # Initialize RandomizedSearchCV - xgb_model = RandomizedSearchCV( - estimator=xgb, - param_distributions=xgb_param_distributions, - n_iter=40, - cv=5, - random_state=seed, - n_jobs=-1, - ) - - return xgb_model - - -def create_lr_model(seed): - # Initialize the Logistic Regression Classifier - lr = LogisticRegression(random_state=seed, max_iter=1000) - - lr_param_distributions = { - "C": [0.001, 0.01, 0.1, 1, 10, 100], - "penalty": ["l1", "l2"], - "solver": ["liblinear", "saga"], - } + model = create_svc_model(args["seed"]) - # Initialize RandomizedSearchCV - lr_model = RandomizedSearchCV( - estimator=lr, - param_distributions=lr_param_distributions, - n_iter=40, - cv=5, - random_state=seed, - n_jobs=-1, - ) + method_pqk = "pqk" + model.fit(projections_train, y_train) + y_predicted = model.predict(projections_test) - return lr_model - - -def create_rf_model(seed): - # Initialize the Random Forest Classifier - rf = RandomForestClassifier(random_state=seed) - - rf_param_distributions = { - "n_estimators": np.arange(100, 1000, 100), - "max_depth": np.arange(5, 20), - "min_samples_split": np.arange(2, 10), - "min_samples_leaf": np.arange(1, 5), - "bootstrap": [True, False], + hyperparameters = { + "feature_map": feature_map.__class__.__name__, + "feature_map_reps": reps, + "entanglement": entanglement, + "best_params": model.best_params_, + # Add other hyperparameters as needed } + model_params = hyperparameters - # Initialize RandomizedSearchCV - rf_model = RandomizedSearchCV( - estimator=rf, - param_distributions=rf_param_distributions, - n_iter=40, - cv=5, - random_state=seed, - n_jobs=-1, + return modeleval( + y_test, y_predicted, beg_time, params=model_params, args=args, model=method_pqk, verbose=verbose ) - return rf_model - - -def create_mlp_model(seed): - mlp_param_distributions = { - "hidden_layer_sizes": [(128, 64, 32, 10), (64, 32, 10), (128, 64, 32)], - "activation": ["identity", "logistic", "tanh", "relu"], - "solver": ["lbfgs", "sgd", "adam"], - "alpha": [0.00005, 0.0005], - } - - # Initialize the MLP Classifier - mlp = MLPClassifier(random_state=seed) - # Initialize RandomizedSearchCV - mlp_model = RandomizedSearchCV( - estimator=mlp, - param_distributions=mlp_param_distributions, - n_iter=40, - cv=5, - random_state=seed, - n_jobs=-1, - ) - return mlp_model def create_svc_model(seed): diff --git a/qbiocode/learning/compute_qensemble.py b/qbiocode/learning/compute_qensemble.py new file mode 100644 index 0000000..a2538c9 --- /dev/null +++ b/qbiocode/learning/compute_qensemble.py @@ -0,0 +1,467 @@ +""" +Quantum Ensemble Learning Module +================================= + +This module implements quantum ensemble learning algorithms using controlled +swap operations and quantum superposition to create ensembles of training data +arrangements. The ensemble leverages quantum superposition to evaluate multiple +training set configurations simultaneously. + +Supports two ensemble construction methods: +- Fixed swap patterns: Deterministic controlled-SWAP operations +- Random unitaries: Haar-random unitary transformations + +References +---------- +- Macaluso et al., "A variational algorithm for quantum ensemble learning" + IET Quantum Communication (2023) +- Rhrissorrakrai et al., "Quantum Ensembling Methods for Healthcare and Life Science" + arXiv:2506.02213 (2025) + https://arxiv.org/abs/2506.02213 +""" + +import time +import numpy as np +import scipy.stats +from typing import List, Literal + +# Qiskit imports +from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister +from qiskit.circuit.library import UnitaryGate + +# Local imports +from qbiocode.evaluation.model_evaluation import modeleval +from qbiocode.utils import ( + normalize_data, + label_to_array, + prepare_training_set, + retrieve_probabilities, + execute_circuit, +) + + +def build_cosine_classifier(train: np.ndarray, test: np.ndarray, + label_train: np.ndarray) -> QuantumCircuit: + """ + Build quantum cosine similarity classifier circuit. + + Implements a quantum cosine similarity classifier using controlled swap + operations and Hadamard gates to measure similarity between training + and test data points. + + Parameters + ---------- + train : np.ndarray + Training data point as normalized vector (length must be power of 2) + test : np.ndarray + Test data point as normalized vector + label_train : np.ndarray + Training label as normalized probability vector [p0, p1] + + Returns + ------- + QuantumCircuit + Quantum circuit implementing the cosine classifier + + Notes + ----- + The circuit computes P(0) = 1/2 + 1/2 * ||^2 + """ + qubits_per = int(np.log2(len(train))) + + c = ClassicalRegister(1, 'c') + x_train = QuantumRegister(qubits_per, 'x_train') + x_test = QuantumRegister(qubits_per, 'x_test') + y_train = QuantumRegister(1, 'y_train') + y_test = QuantumRegister(1, 'y_test') + + qc = QuantumCircuit(x_train, x_test, y_train, y_test, c) + + # Initialize states - convert to list for Qiskit compatibility + qc.initialize(train.tolist() if isinstance(train, np.ndarray) else train, [x_train]) + qc.initialize(test.tolist() if isinstance(test, np.ndarray) else test, [x_test]) + qc.initialize(label_train.tolist() if isinstance(label_train, np.ndarray) else label_train, [y_train]) + qc.barrier() + + # SWAP test + qc.h(y_test) + qc.cswap(y_test, x_train, x_test) + qc.h(y_test) + qc.barrier() + + # Label integration + qc.cx(y_train, y_test) + qc.measure(y_test, c) + + return qc + + +def build_ensemble_circuit(X_data: np.ndarray, Y_data: np.ndarray, + x_test: List[complex], n_swap: int = 1, + d: int = 2, mode: str = "balanced", + ensemble_method: Literal["swap", "random_unitary"] = "swap", + barriers: bool = False) -> QuantumCircuit: + """ + Build quantum ensemble classifier circuit. + + Creates a quantum ensemble learning circuit using either fixed swap + operations or random unitary transformations. + + Parameters + ---------- + X_data : np.ndarray, shape (n_samples, n_features) + Training data points (normalized, n_features must be power of 2) + Y_data : np.ndarray, shape (n_samples, 2) + Training labels as one-hot encoded vectors + x_test : List[complex] + Test data point to classify (normalized) + n_swap : int, optional + Number of swap/unitary operations per control qubit (default: 1) + d : int, optional + Number of control qubits, creates 2^d ensemble members (default: 2) + mode : str, optional + Sampling strategy: "balanced", "unbalanced", or "pair_sample" (default: "balanced") + ensemble_method : {"swap", "random_unitary"}, optional + Method for ensemble construction: + - "swap": Fixed controlled-SWAP operations (faster, deterministic) + - "random_unitary": Haar-random unitaries (more general, slower) + (default: "swap") + barriers : bool, optional + Add barrier gates for visualization (default: False) + + Returns + ------- + QuantumCircuit + Quantum circuit implementing the ensemble classifier + + Notes + ----- + Total qubits: d + 2*n_samples*log2(n_features) + n_samples + 1 (+ 1 for random_unitary) + """ + + def cswap_obs(c, a, b): + """Controlled swap of observation qubits between indices a and b.""" + qubit_indices_a = train_qubit_map[a] + qubit_indices_b = train_qubit_map[b] + for (i, j) in zip(qubit_indices_a, qubit_indices_b): + qc.cswap(control[c], data[i], data[j]) + + def cswap_cosine(a): + """Controlled swap for cosine similarity measurement.""" + qubit_indices_a = train_qubit_map[a] + for (j, i) in enumerate(qubit_indices_a): + qc.cswap(label_test, data[i], data_test[j]) + + def cswap_labels(c, a, b): + """Controlled swap of label qubits.""" + qc.cswap(c, labels[a], labels[b]) + + n_obs = len(X_data) + qubits_per = int(np.log2(X_data.shape[1])) + n_obs_qubits = qubits_per * n_obs + n_test_qubits = qubits_per + + control = QuantumRegister(d, 'control') + data = QuantumRegister(n_obs_qubits, 'x') + labels = QuantumRegister(n_obs, 'y') + data_test = QuantumRegister(n_test_qubits, 'test_data') + label_test = QuantumRegister(1, 'test_label') + c = ClassicalRegister(1) + + # For random unitary method, add prediction qubit + if ensemble_method == "random_unitary": + prediction = QuantumRegister(1, 'pred_qubit') + qc = QuantumCircuit(control, data, labels, data_test, prediction, label_test, c) + else: + qc = QuantumCircuit(control, data, labels, data_test, label_test, c) + + # Initialize test data + qc.initialize(x_test, data_test) + + # Initialize training data and labels + train_qubit_map = {} + for index in range(n_obs): + indices = list(range(index * qubits_per, (index + 1) * qubits_per)) + x_init = X_data[index].tolist() if isinstance(X_data[index], np.ndarray) else X_data[index] + y_init = Y_data[index].tolist() if isinstance(Y_data[index], np.ndarray) else Y_data[index] + qc.initialize(x_init, [data[indices]]) + qc.initialize(y_init, [labels[index]]) + train_qubit_map[index] = indices + + # Create superposition on control qubits + for i in range(d): + qc.h(control[i]) + + if barriers: + qc.barrier() + + # Apply ensemble operations based on method + if ensemble_method == "random_unitary": + # Sample random unitaries from Haar measure + unitary_sampler = scipy.stats.unitary_group(2**(n_obs_qubits + n_obs)) + + if mode == 'balanced': + for i in range(d - 1): + for _ in range(n_swap): + U = unitary_sampler.rvs() + U1 = UnitaryGate(U) + CU1 = U1.control(1) + qc.append(CU1, [control[i]] + [x for x in data] + [x for x in labels]) + + if barriers: + qc.barrier() + + qc.x(control[i]) + + if barriers: + qc.barrier() + + U = unitary_sampler.rvs() + U2 = UnitaryGate(U) + CU2 = U2.control(1) + qc.append(CU2, [control[i]] + [x for x in data] + [x for x in labels]) + + if barriers: + qc.barrier() + + # Final swap for balanced mode + U = np.random.choice(range(int(n_obs / 2)), 1, replace=False) + U = np.insert(U, 1, n_obs - 1) + d1, d2 = U[0], U[1] + cswap_obs(d - 1, d1, d2) + cswap_labels(d - 1, d1, d2) + qc.x(control[d - 1]) + + else: # Fixed swap method + if mode == 'balanced': + for i in range(d - 1): + for j in range(n_swap): + # Swap within first class + U = np.random.choice(range(int(n_obs / 2)), 2, replace=False) + U_b = np.random.choice(range(len(train_qubit_map[U[0]])), 1)[0] + d1 = train_qubit_map[U[0]][U_b] + d2 = train_qubit_map[U[1]][U_b] + qc.cswap(control[i], data[d1], data[d2]) + qc.cswap(control[i], labels[int(U[0])], labels[int(U[1])]) + + # Swap within second class + U = np.random.choice(range(int(n_obs / 2), n_obs), 2, replace=False) + U_b = np.random.choice(range(len(train_qubit_map[U[0]])), 1)[0] + d1 = train_qubit_map[U[0]][U_b] + d2 = train_qubit_map[U[1]][U_b] + qc.cswap(control[i], data[d1], data[d2]) + qc.cswap(control[i], labels[int(U[0])], labels[int(U[1])]) + + qc.x(control[i]) + + for j in range(n_swap): + U = np.random.choice(range(int(n_obs / 2)), 2, replace=False) + U_b = np.random.choice(range(len(train_qubit_map[U[0]])), 1)[0] + d1 = train_qubit_map[U[0]][U_b] + d2 = train_qubit_map[U[1]][U_b] + qc.cswap(control[i], data[d1], data[d2]) + qc.cswap(control[i], labels[int(U[0])], labels[int(U[1])]) + + U = np.random.choice(range(int(n_obs / 2), n_obs), 2, replace=False) + U_b = np.random.choice(range(len(train_qubit_map[U[0]])), 1)[0] + d1 = train_qubit_map[U[0]][U_b] + d2 = train_qubit_map[U[1]][U_b] + qc.cswap(control[i], data[d1], data[d2]) + qc.cswap(control[i], labels[int(U[0])], labels[int(U[1])]) + + if barriers: + qc.barrier() + + # Final swap for balanced mode + qc.x(control[d - 1]) + U = np.random.choice(range(int(n_obs / 2)), 1, replace=False) + U = np.insert(U, 1, n_obs - 1) + U_b = np.random.choice(range(len(train_qubit_map[U[0]])), 1)[0] + d1 = train_qubit_map[U[0]][U_b] + d2 = train_qubit_map[U[1]][U_b] + qc.cswap(control[d - 1], data[d1], data[d2]) + qc.cswap(control[d - 1], labels[int(U[0])], labels[int(U[1])]) + + elif mode == "unbalanced": + for i in range(d): + for j in range(n_swap): + U = np.random.choice(range(n_obs), 2, replace=False) + U_b = np.random.choice(range(len(train_qubit_map[U[0]])), 1)[0] + d1 = train_qubit_map[U[0]][U_b] + d2 = train_qubit_map[U[1]][U_b] + qc.cswap(control[i], data[d1], data[d2]) + qc.cswap(control[i], labels[int(U[0])], labels[int(U[1])]) + + qc.x(control[i]) + + for j in range(n_swap): + U = np.random.choice(range(n_obs), 2, replace=False) + U_b = np.random.choice(range(len(train_qubit_map[U[0]])), 1)[0] + d1 = train_qubit_map[U[0]][U_b] + d2 = train_qubit_map[U[1]][U_b] + qc.cswap(control[i], data[d1], data[d2]) + qc.cswap(control[i], labels[int(U[0])], labels[int(U[1])]) + + elif mode == "pair_sample": + for i in range(d): + for j in range(n_swap): + pairs = np.random.choice(range(n_obs), n_obs, replace=False) + for U in pairs.reshape(int(len(pairs) / 2), 2): + U_b = np.random.choice(range(len(train_qubit_map[U[0]])), 1)[0] + d1 = train_qubit_map[U[0]][U_b] + d2 = train_qubit_map[U[1]][U_b] + qc.cswap(control[i], data[d1], data[d2]) + qc.cswap(control[i], labels[int(U[0])], labels[int(U[1])]) + + qc.x(control[i]) + if barriers: + qc.barrier() + + for j in range(n_swap): + pairs = np.random.choice(range(n_obs), n_obs, replace=False) + for U in pairs.reshape(int(len(pairs) / 2), 2): + U_b = np.random.choice(range(len(train_qubit_map[U[0]])), 1)[0] + d1 = train_qubit_map[U[0]][U_b] + d2 = train_qubit_map[U[1]][U_b] + qc.cswap(control[i], data[d1], data[d2]) + qc.cswap(control[i], labels[int(U[0])], labels[int(U[1])]) + + if barriers: + qc.barrier() + + if barriers: + qc.barrier() + + # Final classification step + ix_cls = n_obs - 1 + qc.h(label_test) + if barriers: + qc.barrier() + cswap_cosine(ix_cls) + if barriers: + qc.barrier() + qc.h(label_test) + qc.cx(labels[ix_cls], label_test) + qc.measure(label_test, c) + + return qc + + +def compute_qensemble(X_train: np.ndarray, X_test: np.ndarray, + y_train: np.ndarray, y_test: np.ndarray, + args: dict, model: str = 'QEnsemble', + data_key: str = '', n_train: int = 4, + n_swap: int = 1, d: int = 2, + mode: str = "balanced", + ensemble_method: Literal["swap", "random_unitary"] = "swap", + n_shots: int = 8192, + seed: int = 123, device: str = 'CPU', + verbose: bool = False): + """ + Compute quantum ensemble classifier predictions. + + This function implements a quantum ensemble learning algorithm using + either fixed swap operations or random unitary transformations to create + superpositions of different training data arrangements. + + Parameters + ---------- + X_train : np.ndarray + Training feature set + X_test : np.ndarray + Testing feature set + y_train : np.ndarray + Training labels + y_test : np.ndarray + Testing labels + args : dict + Dictionary containing arguments for backend and settings + model : str, optional + Model type (default: 'QEnsemble') + data_key : str, optional + Key for the dataset (default: '') + n_train : int, optional + Number of training samples to use (must be even, default: 4) + n_swap : int, optional + Number of swap/unitary operations per control qubit (default: 1) + d : int, optional + Number of control qubits, creates 2^d ensemble members (default: 2) + mode : str, optional + Sampling strategy: "balanced", "unbalanced", or "pair_sample" (default: "balanced") + ensemble_method : {"swap", "random_unitary"}, optional + Method for ensemble construction: + - "swap": Fixed controlled-SWAP operations (faster, deterministic) + - "random_unitary": Haar-random unitaries (more general, slower) + (default: "swap") + n_shots : int, optional + Number of measurement shots (default: 8192) + seed : int, optional + Random seed for reproducibility (default: 123) + device : str, optional + Device type: 'CPU' or 'GPU' (default: 'CPU') + verbose : bool, optional + Print additional information (default: False) + + Returns + ------- + dict + Dictionary containing evaluation results including accuracy, runtime, + model parameters, and other relevant metrics + + Examples + -------- + >>> from qbiocode.learning import compute_qensemble + >>> # Fixed swap ensemble (standard) + >>> results = compute_qensemble(X_train, X_test, y_train, y_test, args, + ... n_train=4, d=2, ensemble_method="swap") + >>> # Random unitary ensemble (advanced) + >>> results = compute_qensemble(X_train, X_test, y_train, y_test, args, + ... n_train=4, d=2, ensemble_method="random_unitary") + """ + beg_time = time.time() + + if verbose: + method_name = "Random Unitary" if ensemble_method == "random_unitary" else "Fixed Swap" + print(f"Running Quantum Ensemble Classifier ({method_name})") + print(f"Training samples: {n_train}, Ensemble depth (d): {d}, Operations: {n_swap}") + print(f"Mode: {mode}, Shots: {n_shots}") + + # Prepare training subset + X_data, Y_data = prepare_training_set(X_train, y_train, n=n_train, seed=seed) + + # Make predictions for each test sample + predictions = [] + qc = None # Initialize to avoid unbound variable + for x_test in X_test: + x_test_norm = normalize_data(x_test) + + # Build and execute circuit + qc = build_ensemble_circuit(X_data, Y_data, x_test_norm, + n_swap=n_swap, d=d, mode=mode, + ensemble_method=ensemble_method) + + if qc.num_qubits > 36: + raise ValueError(f"Circuit has {qc.num_qubits} qubits, exceeds simulation limit of 36") + + counts = execute_circuit(qc, n_shots=n_shots, device=device) + probs = retrieve_probabilities(counts) + predictions.append(probs) + + # Convert probabilities to class predictions + y_predicted = np.array([1 if p[1] > p[0] else 0 for p in predictions]) + + # Model parameters + model_params = { + 'n_train': n_train, + 'n_swap': n_swap, + 'd': d, + 'mode': mode, + 'ensemble_method': ensemble_method, + 'n_shots': n_shots, + 'seed': seed, + 'n_qubits': qc.num_qubits if qc is not None else 0, + 'circuit_depth': qc.depth() if qc is not None else 0 + } + + return modeleval(y_test, y_predicted, beg_time, model_params, args, + model=model, verbose=verbose) diff --git a/qbiocode/learning/compute_qpl.py b/qbiocode/learning/compute_qpl.py new file mode 100644 index 0000000..eebced1 --- /dev/null +++ b/qbiocode/learning/compute_qpl.py @@ -0,0 +1,423 @@ +# ====== Base class imports ====== +import os +import time +import warnings + +import numpy as np +import pandas as pd +from sklearn.ensemble import RandomForestClassifier +from sklearn.linear_model import LogisticRegression +from sklearn.metrics import accuracy_score, auc, classification_report, confusion_matrix, f1_score +from sklearn.model_selection import GridSearchCV, RandomizedSearchCV +from sklearn.neural_network import MLPClassifier +from sklearn.svm import SVC + +try: + from xgboost import XGBClassifier + + XGBOOST_AVAILABLE = True +except Exception: + XGBOOST_AVAILABLE = False + XGBClassifier = None # type: ignore + +# from qiskit.primitives import Sampler +from functools import reduce + +# ====== Qiskit imports ====== +from qiskit import QuantumCircuit +from qiskit.quantum_info import Pauli +from sklearn import svm +from sklearn.model_selection import GridSearchCV + +import qbiocode.utils.qutils as qutils + +# ====== Additional local imports ====== +from qbiocode.evaluation.model_evaluation import modeleval + + +def compute_qpl( + X_train, + X_test, + y_train, + y_test, + args, + model="QPL", + data_key="", + verbose=False, + encoding="Z", + primitive="estimator", + entanglement="linear", + reps=2, + classical_models=None, +): + """ + This function generates quantum circuits, computes projections of the data onto these circuits, + and evaluates the performance of classical machine learning models on the projected data. + It uses a feature map to encode the data into quantum states and then measures the expectation values + of Pauli operators to obtain the features. The classical models are trained on the projected training data and + evaluated on the projected test data. The function returns evaluation metrics and model parameters. + This function requires a quantum backend (simulator or real quantum hardware) for execution. + It supports various configurations such as encoding methods, entanglement strategies, and repetitions + of the feature map. The results are saved to files for training and test projections, which are reused + if they already exist to avoid redundant computations. + This function is part of the main quantum machine learning pipeline (QProfiler.py) and is intended for use in supervised learning tasks. + It leverages quantum computing to enhance feature extraction and classification performance on complex datasets. + The function returns the performance results, including accuracy, F1-score, AUC, runtime, as well as model parameters, and other relevant metrics. + + Args: + X_train (np.ndarray): Training data features. + X_test (np.ndarray): Test data features. + y_train (np.ndarray): Training data labels. + y_test (np.ndarray): Test data labels. + args (dict): Arguments containing backend and other configurations. + model (str): Model type, default is 'QPL'. + data_key (str): Key for the dataset, default is ''. + verbose (bool): If True, print additional information, default is False. + encoding (str): Encoding method for the quantum circuit, default is 'Z'. + primitive (str): Primitive type to use, default is 'estimator'. + entanglement (str): Entanglement strategy, default is 'linear'. + reps (int): Number of repetitions for the feature map, default is 2. + classical_models (list): List of classical models to train on quantum projections. + Options: 'rf', 'mlp', 'svc', 'lr', 'xgb'. + Default is ['rf', 'mlp', 'svc', 'lr', 'xgb']. + + Returns: + modeleval (pd.DataFrame): A DataFrame containing evaluation metrics and model parameters for all models. + """ + + # Set default classical models if not provided + if classical_models is None: + classical_models = ["rf", "mlp", "svc", "lr", "xgb"] + + beg_time = time.time() + feat_dimension = X_train.shape[1] + + if not os.path.exists("qpl_projections"): + os.makedirs("qpl_projections") + + file_projection_train = os.path.join( + "qpl_projections", "qpl_projection_" + data_key + "_train.npy" + ) + file_projection_test = os.path.join( + "qpl_projections", "qpl_projection_" + data_key + "_test.npy" + ) + + # This function ensures that all multiplicative factors of data features inside single qubit gates are 1.0 + def data_map_func(x: np.ndarray): + """ + Define a function map from R^n to R. + + Args: + x: data + + Returns: + the mapped value (float or Parameter expression) + """ + coeff = x[0] / 2 if len(x) == 1 else reduce(lambda m, n: (m * n) / 2, x) + # Check if coeff is a numeric type before converting to float + # If it's a Parameter expression, return it as-is for Qiskit to handle + try: + return float(coeff) + except (TypeError, ValueError): + # If conversion fails, it's likely a Parameter expression + return coeff + + # choose a method for mapping your features onto the circuit + feature_map, _ = qutils.get_feature_map( + feature_map=encoding, + feat_dimension=X_train.shape[1], + reps=reps, + entanglement=entanglement, + data_map_func=data_map_func, + ) + + # Build quantum circuit + circuit = QuantumCircuit(feature_map.num_qubits) + circuit.compose(feature_map, inplace=True) + num_qubits = circuit.num_qubits + + if (not os.path.exists(file_projection_train)) | (not os.path.exists(file_projection_test)): + + # Generate the backend, session and primitive + backend, session, prim = qutils.get_backend_session( + args, "estimator", num_qubits=num_qubits + ) + + # Transpile + if args["backend"] != "simulator": + circuit = qutils.transpile_circuit( + circuit, opt_level=3, backend=backend, PT=True, initial_layout=None + ) + + for f_tr in [file_projection_train, file_projection_test]: + if not os.path.exists(f_tr): + projections = [] + if "train" in f_tr: + dat = X_train.copy() + else: + dat = X_test.copy() + + # Identity operator on all qubits + id = "I" * feat_dimension + + # We group all commuting observables + # These groups are the Pauli X, Y and Z operators on individual qubits + # Apply the circuit layout to the observable if mapped to device + if args["backend"] != "simulator": + observables_x = [] + observables_y = [] + observables_z = [] + for i in range(feat_dimension): + observables_x.append( + Pauli(id[:i] + "X" + id[(i + 1) :]).apply_layout( + circuit.layout, num_qubits=backend.num_qubits + ) + ) + observables_y.append( + Pauli(id[:i] + "Y" + id[(i + 1) :]).apply_layout( + circuit.layout, num_qubits=backend.num_qubits + ) + ) + observables_z.append( + Pauli(id[:i] + "Z" + id[(i + 1) :]).apply_layout( + circuit.layout, num_qubits=backend.num_qubits + ) + ) + else: + observables_x = [ + Pauli(id[:i] + "X" + id[(i + 1) :]) for i in range(feat_dimension) + ] + observables_y = [ + Pauli(id[:i] + "Y" + id[(i + 1) :]) for i in range(feat_dimension) + ] + observables_z = [ + Pauli(id[:i] + "Z" + id[(i + 1) :]) for i in range(feat_dimension) + ] + + # projections[i][j][k] will be the expectation value of the j-th Pauli operator (0: X, 1: Y, 2: Z) + # of datapoint i on qubit k + projections = [] + + for i in range(len(dat)): + if i % 100 == 0: + print(f"at datapoint {str(i)}") + + # Get training sample + parameters = dat[i] + + # We define the primitive unified blocs (PUBs) consisting of the embedding circuit, + # set of observables and the circuit parameters + pub_x = (circuit, observables_x, parameters) + pub_y = (circuit, observables_y, parameters) + pub_z = (circuit, observables_z, parameters) + + job = prim.run([pub_x, pub_y, pub_z]) + job_result_x = job.result()[0].data.evs + job_result_y = job.result()[1].data.evs + job_result_z = job.result()[2].data.evs + + # Record , and on all qubits for the current datapoint + projections.append([job_result_x, job_result_y, job_result_z]) + np.save(f_tr, projections) + + if not isinstance(session, type(None)): + session.close() + + # Load computed projections + projections_train = np.load(file_projection_train) + projections_train = np.array(projections_train).reshape(len(projections_train), -1) + projections_test = np.load(file_projection_test) + projections_test = np.array(projections_test).reshape(len(projections_test), -1) + + # Check if XGBoost is requested but not available + if "xgb" in classical_models and not XGBOOST_AVAILABLE: + warnings.warn( + "XGBoost is not properly installed or configured and will be skipped.\n" + "On macOS, you may need to install OpenMP:\n" + " brew install libomp\n" + "Then reinstall XGBoost:\n" + " pip install --force-reinstall xgboost\n" + "See installation documentation for more details.\n" + f"Continuing with other models: {[m for m in classical_models if m != 'xgb']}", + UserWarning, + ) + # Remove xgb from the list + classical_models = [m for m in classical_models if m != "xgb"] + + # If no models remain after filtering, raise an error + if not classical_models: + raise ValueError( + "No valid classical models specified. Please provide at least one model from: 'rf', 'mlp', 'svc', 'lr', 'xgb'" + ) + + model_res = [] + for method in classical_models: + if method == "rf": + model = create_rf_model(args["seed"]) + elif method == "svc": + model = create_svc_model(args["seed"]) + elif method == "mlp": + model = create_mlp_model(args["seed"]) + elif method == "lr": + model = create_lr_model(args["seed"]) + elif method == "xgb": + model = create_xgb_model(args["seed"]) + else: + warnings.warn( + f"Unknown model type '{method}' skipped. Valid options: 'rf', 'mlp', 'svc', 'lr', 'xgb'", + UserWarning, + ) + continue + + method_qpl = "qpl_" + method + print(method_qpl) + model.fit(projections_train, y_train) + y_predicted = model.predict(projections_test) + + hyperparameters = { + "feature_map": feature_map.__class__.__name__, + "feature_map_reps": reps, + "entanglement": entanglement, + "best_params": model.best_params_, + # Add other hyperparameters as needed + } + model_params = hyperparameters + + model_res.append( + modeleval( + y_test, y_predicted, beg_time, model_params, args, model=method_qpl, verbose=verbose + ) + ) + + model_res = pd.concat(model_res) + return model_res + + +def create_xgb_model(seed): + # Initialize the XGBoost Classifier + if not XGBOOST_AVAILABLE: + raise ImportError( + "XGBoost is not properly installed or configured.\n" + "On macOS, you may need to install OpenMP:\n" + " brew install libomp\n\n" + "Then reinstall XGBoost:\n" + " pip install --force-reinstall xgboost\n\n" + "See installation documentation for more details." + ) + xgb = XGBClassifier(objective="binary:logistic", eval_metric="logloss") # type: ignore + + xgb_param_distributions = { + "n_estimators": [100, 200, 300], + "learning_rate": [0.01, 0.1, 0.2], + "max_depth": [3, 5, 7], + "subsample": [0.7, 0.8, 1.0], + "colsample_bytree": [0.7, 0.8, 1.0], + "min_child_weight": [1, 3, 5], + } + + # Initialize RandomizedSearchCV + xgb_model = RandomizedSearchCV( + estimator=xgb, + param_distributions=xgb_param_distributions, + n_iter=40, + cv=5, + random_state=seed, + n_jobs=-1, + ) + + return xgb_model + + +def create_lr_model(seed): + # Initialize the Logistic Regression Classifier + lr = LogisticRegression(random_state=seed, max_iter=1000) + + lr_param_distributions = { + "C": [0.001, 0.01, 0.1, 1, 10, 100], + "penalty": ["l1", "l2"], + "solver": ["liblinear", "saga"], + } + + # Initialize RandomizedSearchCV + lr_model = RandomizedSearchCV( + estimator=lr, + param_distributions=lr_param_distributions, + n_iter=40, + cv=5, + random_state=seed, + n_jobs=-1, + ) + + return lr_model + + +def create_rf_model(seed): + # Initialize the Random Forest Classifier + rf = RandomForestClassifier(random_state=seed) + + rf_param_distributions = { + "n_estimators": np.arange(100, 1000, 100), + "max_depth": np.arange(5, 20), + "min_samples_split": np.arange(2, 10), + "min_samples_leaf": np.arange(1, 5), + "bootstrap": [True, False], + } + + # Initialize RandomizedSearchCV + rf_model = RandomizedSearchCV( + estimator=rf, + param_distributions=rf_param_distributions, + n_iter=40, + cv=5, + random_state=seed, + n_jobs=-1, + ) + + return rf_model + + +def create_mlp_model(seed): + mlp_param_distributions = { + "hidden_layer_sizes": [(128, 64, 32, 10), (64, 32, 10), (128, 64, 32)], + "activation": ["identity", "logistic", "tanh", "relu"], + "solver": ["lbfgs", "sgd", "adam"], + "alpha": [0.00005, 0.0005], + } + + # Initialize the MLP Classifier + mlp = MLPClassifier(random_state=seed) + + # Initialize RandomizedSearchCV + mlp_model = RandomizedSearchCV( + estimator=mlp, + param_distributions=mlp_param_distributions, + n_iter=40, + cv=5, + random_state=seed, + n_jobs=-1, + ) + + return mlp_model + + +def create_svc_model(seed): + svc_param_distributions = { + "C": [0.1, 1, 10, 100], + "gamma": [0.001, 0.01, 0.1, 1], + "kernel": ["linear", "rbf", "poly", "sigmoid"], + } + + # Initialize the SVC + svc = SVC(random_state=seed) + + # Initialize RandomizedSearchCV + svc_model = RandomizedSearchCV( + estimator=svc, + param_distributions=svc_param_distributions, + n_iter=40, + cv=5, + random_state=seed, + n_jobs=-1, + ) + + return svc_model \ No newline at end of file diff --git a/qbiocode/utils/__init__.py b/qbiocode/utils/__init__.py index cdd23d9..a7050d6 100644 --- a/qbiocode/utils/__init__.py +++ b/qbiocode/utils/__init__.py @@ -24,6 +24,11 @@ - get_ansatz: Get quantum ansatz circuit - get_feature_map: Get quantum feature map - get_optimizer: Get classical optimizer +- normalize_data: Normalize data for quantum state encoding +- label_to_array: Convert binary labels to one-hot encoding +- prepare_training_set: Prepare balanced training subset +- retrieve_probabilities: Extract probabilities from measurement counts +- execute_circuit: Execute quantum circuit on Aer simulator Usage ----- @@ -32,6 +37,10 @@ >>> X_scaled = scaler_fn(X, scaling='StandardScaler') >>> # Encode features for quantum circuits >>> X_encoded = feature_encoding(X, feature_encoding='OneHotEncoder') +>>> # Prepare data for quantum ensemble +>>> from qbiocode.utils import normalize_data, prepare_training_set +>>> X_norm = normalize_data(X[0]) +>>> X_train, Y_train = prepare_training_set(X, y, n=4, seed=42) """ from .combine_evals_results import combine_results, track_progress @@ -43,12 +52,19 @@ from .ibm_account import get_creds, instantiate_runtime_service from .qc_winner_finder import qml_winner from .qutils import ( + execute_circuit, get_ansatz, get_backend_session, get_estimator, get_feature_map, get_optimizer, get_sampler, + retrieve_probabilities, +) +from .data_encoding import ( + label_to_array, + normalize_data, + prepare_training_set, ) __all__ = [ @@ -76,4 +92,10 @@ "get_ansatz", "get_feature_map", "get_optimizer", + # Ensemble utilities + "normalize_data", + "label_to_array", + "prepare_training_set", + "retrieve_probabilities", + "execute_circuit", ] diff --git a/qbiocode/utils/data_encoding.py b/qbiocode/utils/data_encoding.py new file mode 100644 index 0000000..111d247 --- /dev/null +++ b/qbiocode/utils/data_encoding.py @@ -0,0 +1,132 @@ +""" +Quantum Data Encoding Utilities +================================ + +This module provides utility functions for encoding classical data into +quantum states, including normalization, label encoding, and training +set preparation for quantum machine learning algorithms. + +These functions are generic and can be used across different quantum +algorithms, not just ensemble learning. +""" + +import numpy as np +from typing import List, Tuple + + +def normalize_data(x: np.ndarray, C: float = 1.0) -> List[complex]: + """ + Normalize data vector for quantum state encoding. + + Normalizes a classical data vector to unit L2 norm and converts to + complex amplitudes suitable for quantum state initialization. + + Parameters + ---------- + x : np.ndarray + Classical data vector to normalize + C : float, optional + Scaling constant (default: 1.0) + + Returns + ------- + List[complex] + Normalized vector as list of complex numbers + + Examples + -------- + >>> x = np.array([3.0, 4.0]) + >>> x_norm = normalize_data(x) + >>> print([abs(xi) for xi in x_norm]) + [0.6, 0.8] + >>> print(sum([abs(xi)**2 for xi in x_norm])) + 1.0 + """ + M = np.sum(x**2) + x_normed = [complex(i / np.sqrt(M * C), 0) for i in x] + return x_normed + + +def label_to_array(y: np.ndarray) -> np.ndarray: + """ + Convert binary labels to one-hot encoded arrays. + + Transforms binary classification labels (0 or 1) into one-hot encoded + format required by quantum circuits. Label 0 becomes [1, 0] and label + 1 becomes [0, 1]. + + Parameters + ---------- + y : np.ndarray + Binary labels (0 or 1) + + Returns + ------- + np.ndarray + One-hot encoded labels, shape (n_samples, 2) + + Examples + -------- + >>> y = np.array([0, 1, 0]) + >>> label_to_array(y) + array([[1, 0], + [0, 1], + [1, 0]]) + """ + Y = [] + for el in y: + if el == 0: + Y.append([1, 0]) + else: + Y.append([0, 1]) + return np.asarray(Y) + + +def prepare_training_set(X: np.ndarray, y: np.ndarray, + n: int = 4, seed: int = 123) -> Tuple[np.ndarray, np.ndarray]: + """ + Select and prepare balanced training subset for quantum ensemble. + + Creates a balanced training set by selecting equal numbers of samples + from each class and normalizing them for quantum encoding. + + Parameters + ---------- + X : np.ndarray, shape (n_samples, n_features) + Training feature data + y : np.ndarray, shape (n_samples,) + Training labels (binary: 0 or 1) + n : int, optional + Total number of training samples to select (must be even, default: 4) + seed : int, optional + Random seed for reproducibility (default: 123) + + Returns + ------- + Tuple[np.ndarray, np.ndarray] + X_data : Normalized training samples, shape (n, n_features) + Y_data : One-hot encoded labels, shape (n, 2) + + Examples + -------- + >>> X = np.random.rand(20, 4) + >>> y = np.array([0]*10 + [1]*10) + >>> X_data, Y_data = prepare_training_set(X, y, n=4, seed=42) + >>> print(X_data.shape, Y_data.shape) + (4, 4) (4, 2) + """ + np.random.seed(seed) + + # Select balanced samples from each class + ix_y1 = np.random.choice(np.where(y == 1)[0], int(n / 2), replace=False) + ix_y0 = np.random.choice(np.where(y == 0)[0], int(n / 2), replace=False) + + X_selected = np.concatenate([X[ix_y1], X[ix_y0]]) + Y_data = label_to_array(np.concatenate([y[ix_y1], y[ix_y0]])) + + # Normalize each sample + X_data = np.array([normalize_data(x) for x in X_selected]) + + return X_data, Y_data + + diff --git a/qbiocode/utils/qutils.py b/qbiocode/utils/qutils.py index b36f760..af58df3 100644 --- a/qbiocode/utils/qutils.py +++ b/qbiocode/utils/qutils.py @@ -315,3 +315,109 @@ def perturbation(): optimizer == L_BFGS_B(maxiter=max_iter) return optimizer + + + +def retrieve_probabilities(counts: dict) -> list: + """ + Extract probability predictions from measurement counts. + + Converts raw measurement counts from quantum circuit execution into + probability predictions for binary classification. Handles edge cases + where only one outcome is observed. + + Parameters + ---------- + counts : dict + Measurement counts with keys '0' and/or '1' + Example: {'0': 4123, '1': 4069} + + Returns + ------- + list of float + [p0, p1] where p0 is probability of class 0 and p1 is probability + of class 1. Always sums to 1.0. + + Notes + ----- + - Handles missing keys gracefully (assigns probability 0 or 1) + - If only '0' observed: returns [1.0, 0.0] + - If only '1' observed: returns [0.0, 1.0] + - If both observed: returns normalized probabilities + + Examples + -------- + >>> counts = {'0': 6000, '1': 2000} + >>> retrieve_probabilities(counts) + [0.75, 0.25] + + >>> counts = {'0': 8192} # Only one outcome + >>> retrieve_probabilities(counts) + [1.0, 0.0] + """ + state_zero = '0' + state_one = '1' + + try: + p0 = counts[state_zero] / (counts[state_zero] + counts[state_one]) + p1 = 1 - p0 + except KeyError: + if list(counts.keys())[0] == state_zero: + p0, p1 = 1.0, 0.0 + else: + p0, p1 = 0.0, 1.0 + + return [p0, p1] + + +def execute_circuit(qc, n_shots: int = 8192, device: str = 'CPU'): + """ + Execute quantum circuit on Aer simulator. + + General-purpose function for executing quantum circuits using the Qiskit + Aer simulator with statevector method. Useful for custom quantum algorithms + that need direct circuit execution without the full runtime service setup. + + Parameters + ---------- + qc : QuantumCircuit + Quantum circuit to execute + n_shots : int, optional + Number of measurement shots (default: 8192) + device : str, optional + Device type: 'CPU' or 'GPU' (default: 'CPU') + Note: GPU requires qiskit-aer-gpu installation + + Returns + ------- + dict + Measurement counts dictionary with bitstring keys and count values + Example: {'0': 4123, '1': 4069} + + Notes + ----- + - Uses statevector simulation method for exact state evolution + - Automatically transpiles circuit with optimization level 3 + - Parallel threshold set to 50 qubits for statevector parallelization + - For hardware execution, use get_backend_session() instead + + Examples + -------- + >>> from qiskit import QuantumCircuit + >>> from qbiocode.utils import execute_circuit + >>> qc = QuantumCircuit(2, 2) + >>> qc.h(0) + >>> qc.cx(0, 1) + >>> qc.measure([0, 1], [0, 1]) + >>> counts = execute_circuit(qc, n_shots=1024) + >>> print(counts) + {'00': 512, '11': 512} + """ + from qiskit.compiler import transpile + from qiskit_aer import AerSimulator + + backend = AerSimulator(method='statevector', device=device, + statevector_parallel_threshold=50) + tqc = transpile(qc, backend, optimization_level=3) + result = backend.run([tqc], shots=n_shots).result() + return result.get_counts(tqc) diff --git a/setup.py b/setup.py index d6d7a54..f07b13f 100644 --- a/setup.py +++ b/setup.py @@ -90,7 +90,7 @@ def read_requirements(): include_package_data=True, package_data={ 'qbiocode': ['py.typed'], - 'apps.qprofiler': ['configs/*.yaml'], + 'qbiocode.apps.qprofiler': ['configs/*.yaml'], }, # Dependencies @@ -178,9 +178,9 @@ def read_requirements(): # Console scripts for command-line tools entry_points={ 'console_scripts': [ - 'qprofiler=apps.qprofiler.cli:main', - 'qprofiler-batch=apps.qprofiler.qprofiler_batchmode:main', - 'qsage=apps.sage.sage:main', + 'qprofiler=qbiocode.apps.qprofiler.cli:main', + 'qprofiler-batch=qbiocode.apps.qprofiler.qprofiler_batchmode:main', + 'qsage=qbiocode.apps.sage.sage:main', ], }, diff --git a/tutorial/QEnsemble/QEnsemble_example_blobs.ipynb b/tutorial/QEnsemble/QEnsemble_example_blobs.ipynb index 6a69dd6..992cf4f 100644 --- a/tutorial/QEnsemble/QEnsemble_example_blobs.ipynb +++ b/tutorial/QEnsemble/QEnsemble_example_blobs.ipynb @@ -44,27 +44,7 @@ "cell_type": "code", "execution_count": 2, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/krhriss/QBioCode/venv/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", - " from .autonotebook import tqdm as notebook_tqdm\n", - "2026-03-23 13:40:51.672614: I tensorflow/core/util/port.cc:153] oneDNN custom operations are on. You may see slightly different numerical results due to floating-point round-off errors from different computation orders. To turn them off, set the environment variable `TF_ENABLE_ONEDNN_OPTS=0`.\n", - "2026-03-23 13:40:51.689471: E external/local_xla/xla/stream_executor/cuda/cuda_fft.cc:467] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered\n", - "WARNING: All log messages before absl::InitializeLog() is called are written to STDERR\n", - "E0000 00:00:1774291251.707736 108400 cuda_dnn.cc:8579] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered\n", - "E0000 00:00:1774291251.713230 108400 cuda_blas.cc:1407] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered\n", - "W0000 00:00:1774291251.728904 108400 computation_placer.cc:177] computation placer already registered. Please check linkage and avoid linking the same target more than once.\n", - "W0000 00:00:1774291251.728919 108400 computation_placer.cc:177] computation placer already registered. Please check linkage and avoid linking the same target more than once.\n", - "W0000 00:00:1774291251.728921 108400 computation_placer.cc:177] computation placer already registered. Please check linkage and avoid linking the same target more than once.\n", - "W0000 00:00:1774291251.728922 108400 computation_placer.cc:177] computation placer already registered. Please check linkage and avoid linking the same target more than once.\n", - "2026-03-23 13:40:51.734228: I tensorflow/core/platform/cpu_feature_guard.cc:210] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.\n", - "To enable the following instructions: AVX2 AVX512F AVX512_VNNI FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.\n" - ] - } - ], + "outputs": [], "source": [ "import os\n", "import numpy as np\n", @@ -72,10 +52,29 @@ "import pickle\n", "from collections import Counter\n", "from sklearn import datasets\n", + "# Note: QBioCode also provides generate_blobs_datasets() for batch generation\n", + "# from qbiocode.data_generation import generate_blobs_datasets\n", "from sklearn.model_selection import train_test_split\n", - "\n", - "from Utils import *\n", - "from modeling import *" + "from sklearn.preprocessing import StandardScaler\n", + "\n", + "# Use QBioCode API\n", + "from qbiocode.learning import compute_qensemble\n", + "from qbiocode.utils import (\n", + " normalize_data,\n", + " label_to_array,\n", + " prepare_training_set,\n", + " retrieve_probabilities\n", + ")\n", + "# Import helper functions for classical baselines and quantum workflows\n", + "from helper_functions import (\n", + " evaluation_metrics,\n", + " run_random_forest,\n", + " run_xgboost,\n", + " run_lazy_predict,\n", + " run_quantum_cosine,\n", + " run_quantum_ensemble,\n", + " post_process_results\n", + ")" ] }, { @@ -251,17 +250,34 @@ "dataset_name = 'blob'\n", "rerun = False\n", "\n", - "# Select best parameters from grid search\n", - "preds = predictions[dataset_name]['random_forest_gs']\n", - "params_map = dict(zip([str(x) for x in list(preds['best_params'])], list(preds['best_params'])))\n", - "best = dict(Counter([str(x) for x in list(preds['best_params'])]))\n", - "best = pd.DataFrame([best.keys(), best.values()], index=['param', 'cnt']).transpose()\n", - "params = params_map[best[best.cnt == max(best.cnt)]['param'].iloc[0]]\n", - "\n", - "if (rerun) or (dataset_name not in predictions.keys()) or (method not in predictions[dataset_name].keys()):\n", - " if dataset_name not in predictions.keys():\n", - " predictions[dataset_name] = {}\n", - " predictions = run_random_forest(predictions, dataset, method, dataset_name, seed, TEST_SIZE, FILE_PREDICTIONS, params=params)" + "# Check if grid search results exist and have data\n", + "if 'random_forest_gs' in predictions.get(dataset_name, {}):\n", + " # Select best parameters from grid search\n", + " preds = predictions[dataset_name]['random_forest_gs']\n", + " if len(preds) > 0 and 'best_params' in preds.columns:\n", + " try:\n", + " params_list = list(preds['best_params'])\n", + " if len(params_list) > 0 and any(pd.notna(params_list)):\n", + " params_map = dict(zip([str(x) for x in params_list], params_list))\n", + " best = dict(Counter([str(x) for x in params_list]))\n", + " if len(best) > 0:\n", + " best = pd.DataFrame([best.keys(), best.values()], index=['param', 'cnt']).transpose()\n", + " params = params_map[best[best.cnt == max(best.cnt)]['param'].iloc[0]]\n", + " \n", + " if (rerun) or (dataset_name not in predictions.keys()) or (method not in predictions[dataset_name].keys()):\n", + " if dataset_name not in predictions.keys():\n", + " predictions[dataset_name] = {}\n", + " predictions = run_random_forest(predictions, dataset, method, dataset_name, seed, TEST_SIZE, FILE_PREDICTIONS, params=params)\n", + " else:\n", + " print(\"No valid parameters found in grid search results. Skipping RF with best parameters.\")\n", + " else:\n", + " print(\"Grid search parameters list is empty or contains only NaN values. Skipping RF with best parameters.\")\n", + " except Exception as e:\n", + " print(f\"Error processing grid search results: {e}. Skipping RF with best parameters.\")\n", + " else:\n", + " print(\"Random Forest grid search results are empty or missing 'best_params' column. Skipping RF with best parameters.\")\n", + "else:\n", + " print(\"Random Forest grid search not run. Skipping RF with best parameters.\")" ] }, { @@ -275,15 +291,7 @@ "cell_type": "code", "execution_count": 10, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Fitting 3 folds for each of 486 candidates, totalling 1458 fits\n" - ] - } - ], + "outputs": [], "source": [ "method = 'xgb_gs'\n", "dataset_name = 'blob'\n", @@ -304,7 +312,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -312,17 +320,41 @@ "dataset_name = 'blob'\n", "rerun = False\n", "\n", - "# Select best parameters from grid search\n", - "preds = predictions[dataset_name]['xgb_gs']\n", - "params_map = dict(zip([str(x) for x in list(preds['best_params'])], list(preds['best_params'])))\n", - "best = dict(Counter([str(x) for x in list(preds['best_params'])]))\n", - "best = pd.DataFrame([best.keys(), best.values()], index=['param', 'cnt']).transpose()\n", - "params = params_map[best[best.cnt == max(best.cnt)]['param'].iloc[0]]\n", - "\n", - "if (rerun) or (dataset_name not in predictions.keys()) or (method not in predictions[dataset_name].keys()):\n", - " if dataset_name not in predictions.keys():\n", - " predictions[dataset_name] = {}\n", - " predictions = run_xgboost(predictions, dataset, method, dataset_name, seed, TEST_SIZE, FILE_PREDICTIONS, params=params)" + "# Import XGB_AVAILABLE flag from helper_functions\n", + "from helper_functions import XGB_AVAILABLE\n", + "\n", + "# Check if XGBoost is installed and grid search results exist\n", + "if XGB_AVAILABLE and 'xgb_gs' in predictions.get(dataset_name, {}):\n", + " # Select best parameters from grid search\n", + " preds = predictions[dataset_name]['xgb_gs']\n", + " if len(preds) > 0 and 'best_params' in preds.columns:\n", + " try:\n", + " params_list = list(preds['best_params'])\n", + " if len(params_list) > 0 and any(pd.notna(params_list)):\n", + " params_map = dict(zip([str(x) for x in params_list], params_list))\n", + " best = dict(Counter([str(x) for x in params_list]))\n", + " if len(best) > 0:\n", + " best = pd.DataFrame([best.keys(), best.values()], index=['param', 'cnt']).transpose()\n", + " params = params_map[best[best.cnt == max(best.cnt)]['param'].iloc[0]]\n", + " \n", + " if (rerun) or (dataset_name not in predictions.keys()) or (method not in predictions[dataset_name].keys()):\n", + " if dataset_name not in predictions.keys():\n", + " predictions[dataset_name] = {}\n", + " predictions = run_xgboost(predictions, dataset, method, dataset_name, seed, TEST_SIZE, FILE_PREDICTIONS, params=params)\n", + " else:\n", + " print(\"No valid parameters found in XGBoost grid search results. Skipping XGBoost with best parameters.\")\n", + " else:\n", + " print(\"XGBoost grid search parameters list is empty or contains only NaN values. Skipping XGBoost with best parameters.\")\n", + " except Exception as e:\n", + " print(f\"Error processing XGBoost grid search results: {e}. Skipping XGBoost with best parameters.\")\n", + " else:\n", + " print(\"XGBoost grid search results are empty or missing 'best_params' column. Skipping XGBoost with best parameters.\")\n", + "else:\n", + " if not XGB_AVAILABLE:\n", + " print('XGBoost not installed. Install with: pip install xgboost')\n", + " print('Note: On macOS, you may also need to install libomp: brew install libomp')\n", + " else:\n", + " print('XGBoost grid search not run. Skipping XGBoost with best parameters.')" ] }, { @@ -346,7 +378,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -385,1096 +417,9 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.487525\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.450675\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.237271\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.2337\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.5 0.252548\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.238011\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.484292\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.453875\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.239231\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.5 0.247595\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.5 0.239198\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.225961\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.488935\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.444699\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.242059\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.229378\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.5 0.255344\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.236085\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.48329\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.441487\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.239495\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.237111\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.25 0.258009\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.25 0.254578\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.489924\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.441424\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.235133\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.5 0.245345\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.5 0.252325\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.240575\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.469304\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.499036\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.5 0.247595\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.5 0.245248\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.25 0.255539\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.5 0.251043\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.465336\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.50002\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.5 0.257082\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.238909\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.75 0.235245\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.25 0.259995\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.465545\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.49906\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.25 0.255971\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.5 0.241869\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.75 0.247695\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.5 0.254452\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.466466\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.500019\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.238309\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.5 0.243275\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.75 0.243257\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.5 0.255974\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.464958\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.500029\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.5 0.255841\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.245144\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.75 0.24945\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.5 0.238582\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.302415\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.406872\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.19875\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.186597\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.177008\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.183897\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.309782\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.405433\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.197379\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.186444\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.182499\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.191936\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.309916\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.402746\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.18706\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.192336\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.170102\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.181205\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.302746\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.413759\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.200647\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.181212\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.174211\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.187766\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.308143\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.404122\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.198444\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.191853\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.169155\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.189901\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.350365\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.286027\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.173522\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.16749\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.189612\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.164195\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.367221\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.295764\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.170795\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.151512\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.205546\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.162591\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.344123\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.283055\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.164674\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.168683\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.196036\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.166333\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.34874\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.291553\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.172108\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.158979\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.19697\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.167331\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.342511\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.292229\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.167687\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.164232\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.18882\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.156039\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.485641\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.440498\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.234607\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.239022\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.5 0.251861\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.5 0.239058\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.485173\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.442289\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.234421\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.5 0.256029\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.75 0.237629\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.228438\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.485194\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.443832\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.23205\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.234508\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.25 0.253395\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.5 0.254087\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.488543\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.441381\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.240076\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.5 0.24899\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.25 0.258539\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.236571\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.482758\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.444625\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.242303\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.22571\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.5 0.253234\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.233981\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.37843\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.446421\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.224967\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.207096\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.207317\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.216069\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.367381\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.458428\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.225497\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.210301\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.211954\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.212399\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.365223\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.44912\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.224908\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.215259\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.211938\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.215193\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.370988\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.464998\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.224858\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.215531\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.203137\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.219187\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.351778\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.457051\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.22196\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.205621\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.21888\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.202231\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.75 0.130784\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.185246\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.094389\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.086981\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.109623\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.089309\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.75 0.132169\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.173509\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.09367\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.094141\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.097954\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.095476\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.145863\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.159731\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.090855\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.095619\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.098582\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.083659\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.145136\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.17319\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.088379\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.092688\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.097377\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.088975\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.75 0.127643\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.181604\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.086917\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.091718\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.102779\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.091132\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.75 0.146397\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.196109\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.110068\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.110158\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.122822\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.096956\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.141746\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.194211\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.112616\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.102374\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.124198\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.100677\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.144692\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.192966\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.107867\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.099571\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.123152\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.09988\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.75 0.130966\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.202409\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.112276\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.096859\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.113845\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.095594\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.75 0.147966\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.195178\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.107899\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.094636\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.117422\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.098117\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.48993\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.43726\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.239412\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.2421\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.75 0.237931\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.243291\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.486563\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.450658\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.245412\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.228837\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.5 0.254909\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.5 0.249615\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.48519\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.44331\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.225851\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.5 0.23839\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.5 0.25501\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.5 0.256045\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.487535\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.443297\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.245326\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.227154\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.5 0.255887\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.5 0.243966\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.483363\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.443755\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.236428\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.232545\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.0 0.262867\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.225009\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.479955\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.441374\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.5 0.235326\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.237056\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.25 0.25807\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.23163\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.486548\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.447917\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.5 0.243698\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.23952\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.239774\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.239424\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.486109\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.443288\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.231343\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.234627\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.25 0.252793\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.236515\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.488466\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.453896\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.5 0.247585\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.230793\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.75 0.242711\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.224339\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.485693\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.446916\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.234715\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.5 0.248556\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.25 0.255076\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.226069\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.48376\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.486464\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.240901\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.244383\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.25 0.252696\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.24326\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.488596\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.490371\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.244348\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.25 0.267864\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.5 0.253756\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.25 0.256952\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.486365\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.492316\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.5 0.247335\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.25 0.253982\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.75 0.241929\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.25 0.256041\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.491125\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.489911\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.5 0.253908\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.24308\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.25 0.256266\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.25 0.259423\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.490454\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.484563\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.5 0.250432\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.25 0.252351\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.5 0.254966\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.0 0.25592\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.391177\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.473037\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.23211\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.223315\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.206084\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.226904\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.390293\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.476364\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.23857\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.215983\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.221015\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.210242\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.387786\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.476379\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.218236\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.219903\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.213097\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.222397\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.398083\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.469722\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.5 0.230457\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.234777\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.228762\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.5 0.228759\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.402229\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.476887\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.242817\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.248435\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.217479\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.226886\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.420561\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.350533\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.196266\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.201766\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.219685\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.202991\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.429591\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.356611\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.212188\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.20621\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.225528\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.219109\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.411634\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.364371\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.215796\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.190558\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.75 0.220432\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.20772\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.424865\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.355899\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.19461\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.208802\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.230137\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.206343\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.428361\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.349557\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.204234\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.194753\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.75 0.229075\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.20419\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.485659\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.443361\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.5 0.245686\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.25 0.248319\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.25 0.263474\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.23762\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.486647\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.436969\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.5 0.255837\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.23374\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.75 0.246168\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.5 0.251657\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.486571\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.443246\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.229663\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.237593\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.25 0.261548\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.239987\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.487031\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.452136\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.229163\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.246131\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.75 0.235609\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.228527\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.487136\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.441846\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.5 0.245309\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.5 0.242791\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.5 0.255817\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.241706\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.426894\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.495619\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.5 0.249171\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.240383\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.75 0.239479\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.227206\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.426885\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.494667\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.5 0.243726\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.5 0.239269\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.75 0.238457\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.5 0.251576\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.437874\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.496118\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.5 0.246642\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.230041\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.5 0.235073\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.235406\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.431966\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.49611\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.5 0.245543\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.25 0.243305\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.75 0.243865\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.5 0.244802\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.441301\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.49417\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.5 0.24674\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.223976\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.75 0.237711\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.247095\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.169301\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.212526\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.107664\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.0998\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.129119\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.106515\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.166511\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.209666\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.112905\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.103445\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.124583\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.102024\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.169269\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.212124\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.12135\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.105363\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.124082\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.102801\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.165428\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.197047\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.11691\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.105513\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.132311\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.109716\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.172245\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.208094\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.121546\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.101888\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.131241\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.103546\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.245315\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.210063\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.1315\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.120965\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.150201\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.128365\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.238351\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.209736\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.131897\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.116068\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.15851\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.127669\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.2426\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.215789\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.137582\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.125439\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.147468\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.122094\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.236998\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.222559\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.139305\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.117099\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.16157\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.127339\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.252741\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.225593\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.137165\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.121526\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 1.0 0.154299\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 1.0 0.122608\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.487549\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.449383\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.234322\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 1.0 0.223679\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.5 0.24389\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.224842\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.48704\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.440964\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 1.0 0.231041\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.230353\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.0 0.272761\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.24039\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.481899\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.444166\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.5 0.255984\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.2482\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.5 0.258524\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.5 0.24284\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.485636\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.442945\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.75 0.233764\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.243061\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.75 0.244732\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.239053\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 1 0.5 0.487583\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 7 1 2 2 0.5 0.441904\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 1 0.5 0.248416\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 11 1 4 2 0.75 0.235444\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 1 0.5 0.252629\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 12 2 4 2 0.75 0.242694\n" - ] - } - ], + "outputs": [], "source": [ "method = 'qensemble'\n", "rerun = False\n", @@ -1517,196 +462,9 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.241431\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.239368\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.241278\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.2372\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.240851\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.5 0.244194\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.25 0.24823\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.5 0.247313\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.25 0.252163\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.5 0.250763\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.186765\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.185546\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.187389\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.181551\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.187453\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.16659\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.164168\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.163545\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.16424\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.166402\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.236356\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.237953\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.23232\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.237642\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.235438\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.214208\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.212839\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.21448\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.212805\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.209992\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.087841\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.090407\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.088585\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.085788\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.088387\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.101387\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.100218\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.099431\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.098901\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.098849\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.238362\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.236887\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.239315\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.234398\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.239541\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.237143\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.236595\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.235926\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.235542\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.235471\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.0 0.254393\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.25 0.254756\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.25 0.253875\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.25 0.248246\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.25 0.252539\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.221794\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.224848\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.225074\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.226751\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.5 0.223426\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.205492\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.199287\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.201337\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.201425\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.200772\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.240802\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.241487\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.233473\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.239053\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.234241\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.5 0.245271\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.5 0.242796\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.5 0.242967\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.5 0.244752\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.5 0.238962\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.104147\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.103868\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.103903\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.100019\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.104824\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.12326\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.123205\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.122963\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.124772\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 1.0 0.122772\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.239736\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.238743\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.236401\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.232132\n", - " n_feature qubits d n_train n_swap accuracy brier\n", - "0 2 8 1 2 1 0.75 0.238142\n" - ] - } - ], + "outputs": [], "source": [ "method = 'qensemble_random_unitary'\n", "rerun = False\n", @@ -1738,7 +496,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -1746,50 +504,29 @@ "output_type": "stream", "text": [ "Dataset: blob\n", - "Method: random_forest\n", - "Method: xgb\n", - "Method: qcosine\n", - "Method: qensemble\n", - "Method: qensemble_random_unitary\n", - "RF: blob : xgb (n=90) : t=3.125; p=0.001\n" + "Method: random_forest\n" ] }, { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "RF: blob : xgb (n=90) : t=3.933; p=0.0\n" + "ename": "ValueError", + "evalue": "Found input variables with inconsistent numbers of samples: [4, 8]", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mValueError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[30]\u001b[39m\u001b[32m, line 2\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;66;03m# Post-process results\u001b[39;00m\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m total_results_full, sig_bestmethods_df = \u001b[43mpost_process_results\u001b[49m\u001b[43m(\u001b[49m\u001b[43mpredictions\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mDIR_OUTPUT\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdatasets\u001b[49m\u001b[43m=\u001b[49m\u001b[43m[\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mblob\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Documents/Development/QBioCode/tutorial/QEnsemble/helper_functions.py:736\u001b[39m, in \u001b[36mpost_process_results\u001b[39m\u001b[34m(predictions, dir_output, datasets, metrics)\u001b[39m\n\u001b[32m 734\u001b[39m y_true = np.asarray(row.y_test).flatten()\n\u001b[32m 735\u001b[39m y_pred = np.asarray(row.predictions).flatten()\n\u001b[32m--> \u001b[39m\u001b[32m736\u001b[39m f1_scores.append(\u001b[43mf1_score\u001b[49m\u001b[43m(\u001b[49m\u001b[43my_true\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43my_pred\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43maverage\u001b[49m\u001b[43m=\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mweighted\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m)\u001b[49m)\n\u001b[32m 737\u001b[39m results_df[\u001b[33m'\u001b[39m\u001b[33mF1 Score\u001b[39m\u001b[33m'\u001b[39m] = f1_scores\n\u001b[32m 739\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m method == \u001b[33m'\u001b[39m\u001b[33mqcosine\u001b[39m\u001b[33m'\u001b[39m:\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Documents/Development/QBioCode/.env/lib/python3.12/site-packages/sklearn/utils/_param_validation.py:218\u001b[39m, in \u001b[36mvalidate_params..decorator..wrapper\u001b[39m\u001b[34m(*args, **kwargs)\u001b[39m\n\u001b[32m 212\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m 213\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m config_context(\n\u001b[32m 214\u001b[39m skip_parameter_validation=(\n\u001b[32m 215\u001b[39m prefer_skip_nested_validation \u001b[38;5;129;01mor\u001b[39;00m global_skip_validation\n\u001b[32m 216\u001b[39m )\n\u001b[32m 217\u001b[39m ):\n\u001b[32m--> \u001b[39m\u001b[32m218\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 219\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m InvalidParameterError \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[32m 220\u001b[39m \u001b[38;5;66;03m# When the function is just a wrapper around an estimator, we allow\u001b[39;00m\n\u001b[32m 221\u001b[39m \u001b[38;5;66;03m# the function to delegate validation to the estimator, but we replace\u001b[39;00m\n\u001b[32m 222\u001b[39m \u001b[38;5;66;03m# the name of the estimator by the name of the function in the error\u001b[39;00m\n\u001b[32m 223\u001b[39m \u001b[38;5;66;03m# message to avoid confusion.\u001b[39;00m\n\u001b[32m 224\u001b[39m msg = re.sub(\n\u001b[32m 225\u001b[39m \u001b[33mr\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mparameter of \u001b[39m\u001b[33m\\\u001b[39m\u001b[33mw+ must be\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 226\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mparameter of \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfunc.\u001b[34m__qualname__\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m must be\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 227\u001b[39m \u001b[38;5;28mstr\u001b[39m(e),\n\u001b[32m 228\u001b[39m )\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Documents/Development/QBioCode/.env/lib/python3.12/site-packages/sklearn/metrics/_classification.py:1565\u001b[39m, in \u001b[36mf1_score\u001b[39m\u001b[34m(y_true, y_pred, labels, pos_label, average, sample_weight, zero_division)\u001b[39m\n\u001b[32m 1383\u001b[39m \u001b[38;5;129m@validate_params\u001b[39m(\n\u001b[32m 1384\u001b[39m {\n\u001b[32m 1385\u001b[39m \u001b[33m\"\u001b[39m\u001b[33my_true\u001b[39m\u001b[33m\"\u001b[39m: [\u001b[33m\"\u001b[39m\u001b[33marray-like\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33msparse matrix\u001b[39m\u001b[33m\"\u001b[39m],\n\u001b[32m (...)\u001b[39m\u001b[32m 1410\u001b[39m zero_division=\u001b[33m\"\u001b[39m\u001b[33mwarn\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 1411\u001b[39m ):\n\u001b[32m 1412\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"Compute the F1 score, also known as balanced F-score or F-measure.\u001b[39;00m\n\u001b[32m 1413\u001b[39m \n\u001b[32m 1414\u001b[39m \u001b[33;03m The F1 score can be interpreted as a harmonic mean of the precision and\u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 1563\u001b[39m \u001b[33;03m array([0.66666667, 1. , 0.66666667])\u001b[39;00m\n\u001b[32m 1564\u001b[39m \u001b[33;03m \"\"\"\u001b[39;00m\n\u001b[32m-> \u001b[39m\u001b[32m1565\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfbeta_score\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 1566\u001b[39m \u001b[43m \u001b[49m\u001b[43my_true\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 1567\u001b[39m \u001b[43m \u001b[49m\u001b[43my_pred\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 1568\u001b[39m \u001b[43m \u001b[49m\u001b[43mbeta\u001b[49m\u001b[43m=\u001b[49m\u001b[32;43m1\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 1569\u001b[39m \u001b[43m \u001b[49m\u001b[43mlabels\u001b[49m\u001b[43m=\u001b[49m\u001b[43mlabels\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 1570\u001b[39m \u001b[43m \u001b[49m\u001b[43mpos_label\u001b[49m\u001b[43m=\u001b[49m\u001b[43mpos_label\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 1571\u001b[39m \u001b[43m \u001b[49m\u001b[43maverage\u001b[49m\u001b[43m=\u001b[49m\u001b[43maverage\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 1572\u001b[39m \u001b[43m \u001b[49m\u001b[43msample_weight\u001b[49m\u001b[43m=\u001b[49m\u001b[43msample_weight\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 1573\u001b[39m \u001b[43m \u001b[49m\u001b[43mzero_division\u001b[49m\u001b[43m=\u001b[49m\u001b[43mzero_division\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 1574\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Documents/Development/QBioCode/.env/lib/python3.12/site-packages/sklearn/utils/_param_validation.py:191\u001b[39m, in \u001b[36mvalidate_params..decorator..wrapper\u001b[39m\u001b[34m(*args, **kwargs)\u001b[39m\n\u001b[32m 189\u001b[39m global_skip_validation = get_config()[\u001b[33m\"\u001b[39m\u001b[33mskip_parameter_validation\u001b[39m\u001b[33m\"\u001b[39m]\n\u001b[32m 190\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m global_skip_validation:\n\u001b[32m--> \u001b[39m\u001b[32m191\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 193\u001b[39m func_sig = signature(func)\n\u001b[32m 195\u001b[39m \u001b[38;5;66;03m# Map *args/**kwargs to the function signature\u001b[39;00m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Documents/Development/QBioCode/.env/lib/python3.12/site-packages/sklearn/metrics/_classification.py:1785\u001b[39m, in \u001b[36mfbeta_score\u001b[39m\u001b[34m(y_true, y_pred, beta, labels, pos_label, average, sample_weight, zero_division)\u001b[39m\n\u001b[32m 1577\u001b[39m \u001b[38;5;129m@validate_params\u001b[39m(\n\u001b[32m 1578\u001b[39m {\n\u001b[32m 1579\u001b[39m \u001b[33m\"\u001b[39m\u001b[33my_true\u001b[39m\u001b[33m\"\u001b[39m: [\u001b[33m\"\u001b[39m\u001b[33marray-like\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33msparse matrix\u001b[39m\u001b[33m\"\u001b[39m],\n\u001b[32m (...)\u001b[39m\u001b[32m 1606\u001b[39m zero_division=\u001b[33m\"\u001b[39m\u001b[33mwarn\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 1607\u001b[39m ):\n\u001b[32m 1608\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"Compute the F-beta score.\u001b[39;00m\n\u001b[32m 1609\u001b[39m \n\u001b[32m 1610\u001b[39m \u001b[33;03m The F-beta score is the weighted harmonic mean of precision and recall,\u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 1782\u001b[39m \u001b[33;03m ... )\u001b[39;00m\n\u001b[32m 1783\u001b[39m \u001b[33;03m \"\"\"\u001b[39;00m\n\u001b[32m-> \u001b[39m\u001b[32m1785\u001b[39m _, _, f, _ = \u001b[43mprecision_recall_fscore_support\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 1786\u001b[39m \u001b[43m \u001b[49m\u001b[43my_true\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 1787\u001b[39m \u001b[43m \u001b[49m\u001b[43my_pred\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 1788\u001b[39m \u001b[43m \u001b[49m\u001b[43mbeta\u001b[49m\u001b[43m=\u001b[49m\u001b[43mbeta\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 1789\u001b[39m \u001b[43m \u001b[49m\u001b[43mlabels\u001b[49m\u001b[43m=\u001b[49m\u001b[43mlabels\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 1790\u001b[39m \u001b[43m \u001b[49m\u001b[43mpos_label\u001b[49m\u001b[43m=\u001b[49m\u001b[43mpos_label\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 1791\u001b[39m \u001b[43m \u001b[49m\u001b[43maverage\u001b[49m\u001b[43m=\u001b[49m\u001b[43maverage\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 1792\u001b[39m \u001b[43m \u001b[49m\u001b[43mwarn_for\u001b[49m\u001b[43m=\u001b[49m\u001b[43m(\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mf-score\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 1793\u001b[39m \u001b[43m \u001b[49m\u001b[43msample_weight\u001b[49m\u001b[43m=\u001b[49m\u001b[43msample_weight\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 1794\u001b[39m \u001b[43m \u001b[49m\u001b[43mzero_division\u001b[49m\u001b[43m=\u001b[49m\u001b[43mzero_division\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 1795\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 1796\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m f\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Documents/Development/QBioCode/.env/lib/python3.12/site-packages/sklearn/utils/_param_validation.py:191\u001b[39m, in \u001b[36mvalidate_params..decorator..wrapper\u001b[39m\u001b[34m(*args, **kwargs)\u001b[39m\n\u001b[32m 189\u001b[39m global_skip_validation = get_config()[\u001b[33m\"\u001b[39m\u001b[33mskip_parameter_validation\u001b[39m\u001b[33m\"\u001b[39m]\n\u001b[32m 190\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m global_skip_validation:\n\u001b[32m--> \u001b[39m\u001b[32m191\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 193\u001b[39m func_sig = signature(func)\n\u001b[32m 195\u001b[39m \u001b[38;5;66;03m# Map *args/**kwargs to the function signature\u001b[39;00m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Documents/Development/QBioCode/.env/lib/python3.12/site-packages/sklearn/metrics/_classification.py:2100\u001b[39m, in \u001b[36mprecision_recall_fscore_support\u001b[39m\u001b[34m(y_true, y_pred, beta, labels, pos_label, average, warn_for, sample_weight, zero_division)\u001b[39m\n\u001b[32m 1929\u001b[39m \u001b[38;5;250m\u001b[39m\u001b[33;03m\"\"\"Compute precision, recall, F-measure and support for each class.\u001b[39;00m\n\u001b[32m 1930\u001b[39m \n\u001b[32m 1931\u001b[39m \u001b[33;03mThe precision is the ratio ``tp / (tp + fp)`` where ``tp`` is the number of\u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 2097\u001b[39m \u001b[33;03m array([2, 2, 2]))\u001b[39;00m\n\u001b[32m 2098\u001b[39m \u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 2099\u001b[39m _check_zero_division(zero_division)\n\u001b[32m-> \u001b[39m\u001b[32m2100\u001b[39m labels = \u001b[43m_check_set_wise_labels\u001b[49m\u001b[43m(\u001b[49m\u001b[43my_true\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43my_pred\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43maverage\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mlabels\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mpos_label\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 2102\u001b[39m \u001b[38;5;66;03m# Calculate tp_sum, pred_sum, true_sum ###\u001b[39;00m\n\u001b[32m 2103\u001b[39m samplewise = average == \u001b[33m\"\u001b[39m\u001b[33msamples\u001b[39m\u001b[33m\"\u001b[39m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Documents/Development/QBioCode/.env/lib/python3.12/site-packages/sklearn/metrics/_classification.py:1864\u001b[39m, in \u001b[36m_check_set_wise_labels\u001b[39m\u001b[34m(y_true, y_pred, average, labels, pos_label)\u001b[39m\n\u001b[32m 1861\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[33m\"\u001b[39m\u001b[33maverage has to be one of \u001b[39m\u001b[33m\"\u001b[39m + \u001b[38;5;28mstr\u001b[39m(average_options))\n\u001b[32m 1863\u001b[39m y_true, y_pred = attach_unique(y_true, y_pred)\n\u001b[32m-> \u001b[39m\u001b[32m1864\u001b[39m y_type, y_true, y_pred, _ = \u001b[43m_check_targets\u001b[49m\u001b[43m(\u001b[49m\u001b[43my_true\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43my_pred\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 1865\u001b[39m \u001b[38;5;66;03m# Convert to Python primitive type to avoid NumPy type / Python str\u001b[39;00m\n\u001b[32m 1866\u001b[39m \u001b[38;5;66;03m# comparison. See https://github.com/numpy/numpy/issues/6784\u001b[39;00m\n\u001b[32m 1867\u001b[39m present_labels = _tolist(unique_labels(y_true, y_pred))\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Documents/Development/QBioCode/.env/lib/python3.12/site-packages/sklearn/metrics/_classification.py:108\u001b[39m, in \u001b[36m_check_targets\u001b[39m\u001b[34m(y_true, y_pred, sample_weight)\u001b[39m\n\u001b[32m 77\u001b[39m \u001b[38;5;250m\u001b[39m\u001b[33;03m\"\"\"Check that y_true and y_pred belong to the same classification task.\u001b[39;00m\n\u001b[32m 78\u001b[39m \n\u001b[32m 79\u001b[39m \u001b[33;03mThis converts multiclass or binary types to a common shape, and raises a\u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 105\u001b[39m \u001b[33;03msample_weight : array or None\u001b[39;00m\n\u001b[32m 106\u001b[39m \u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 107\u001b[39m xp, _ = get_namespace(y_true, y_pred, sample_weight)\n\u001b[32m--> \u001b[39m\u001b[32m108\u001b[39m \u001b[43mcheck_consistent_length\u001b[49m\u001b[43m(\u001b[49m\u001b[43my_true\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43my_pred\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43msample_weight\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 109\u001b[39m type_true = type_of_target(y_true, input_name=\u001b[33m\"\u001b[39m\u001b[33my_true\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m 110\u001b[39m type_pred = type_of_target(y_pred, input_name=\u001b[33m\"\u001b[39m\u001b[33my_pred\u001b[39m\u001b[33m\"\u001b[39m)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Documents/Development/QBioCode/.env/lib/python3.12/site-packages/sklearn/utils/validation.py:464\u001b[39m, in \u001b[36mcheck_consistent_length\u001b[39m\u001b[34m(*arrays)\u001b[39m\n\u001b[32m 462\u001b[39m lengths = [_num_samples(X) \u001b[38;5;28;01mfor\u001b[39;00m X \u001b[38;5;129;01min\u001b[39;00m arrays \u001b[38;5;28;01mif\u001b[39;00m X \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m]\n\u001b[32m 463\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(\u001b[38;5;28mset\u001b[39m(lengths)) > \u001b[32m1\u001b[39m:\n\u001b[32m--> \u001b[39m\u001b[32m464\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[32m 465\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mFound input variables with inconsistent numbers of samples: \u001b[39m\u001b[38;5;132;01m%r\u001b[39;00m\u001b[33m\"\u001b[39m\n\u001b[32m 466\u001b[39m % [\u001b[38;5;28mint\u001b[39m(l) \u001b[38;5;28;01mfor\u001b[39;00m l \u001b[38;5;129;01min\u001b[39;00m lengths]\n\u001b[32m 467\u001b[39m )\n", + "\u001b[31mValueError\u001b[39m: Found input variables with inconsistent numbers of samples: [4, 8]" ] - }, - { - "data": { - "image/png": "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+/r16zh06BAuXryIbt26AXjxf8wDBgzA8uXLYWtrW+F5L8dgb2+PL774Ai4uLkhPTxfWInz77beRlZWFkydPom/fvorfkFc8fvwYx48fxzfffCMcq+nzGDVqlNznFStWYNOmTbh8+XKlMa5YsQITJkxAQEAAAGD9+vXYv38/fvjhB8yaNavCc14dgvHZZ59h8+bNOH36NPz8/AAA2traGDBgAHbs2IGBAwdWfyOodmQyiEpLal2NqvZM537pVBfS0tJw//59SKVSpKenV9jbSUTl1auJSbm5uQBe9GgBQFJSEkpKSuDj4yOUcXJyQqtWrZCYmFirJDQhIUFIAMskJiaiT58+coPB/fz8sHTpUmRnZ6Np06bV1nv//n38+uuvePPNNystk5iYCDMzM7n2fXx8oKWlhfPnz+Pdd9+ttp2CggJER0fDwcEBdnZ2wnFdXV24uroiISGhVkno6dOnYWBgAGdnZ+GYKp5HcXExNm7cCFNTU7i4uFRaJikpCWFhYcIxLS0t+Pj4IDExUaH4ZTIZjh8/jtTUVCxdulTuux49emDJkiWVnltUVISioiLhc3VDK6hyotISmF7eXut6Bg+ufR0A90sn1SsuLsaHH36IESNGoEOHDhg/fjyuXLkCS0tLdOjQARcvXoS///9NrLt48WK5Ov788088e/YMYvGLiY/nzp2DkZGR3N/tRA1RvZmYJJVKMWXKFHh5eeGNN94AAGRmZkJXVxdmZmZyZa2srJCZmVmr9jIyMsr1OGZmZsLKyqpcW2XfVeWDDz6AgYEBWrRoARMTE3z//feVls3MzISlpaXcsSZNmsDc3Lzadr799lsYGRnByMgIBw8eRFxcXLkZlLa2tsjIyKiynupkZGTAyspK7lV8bZ7Hvn37YGRkBH19faxcuRJxcXGwsLCosOw///yD0tLSCp9Fde3k5ubCyMgIurq6GDhwINasWYO3335broytrS3u3LlT6XiryMhImJqaCj/8h4CIKjN79mzk5uZi9erVmDlzJtq3b49x48YBAEJCQrBp0yZs3rwZf//9N7744gtcvny5XG98cXExAgMDce3aNRw4cADz5s1DcHCw3N+/RA1RvfkTHhQUhJSUFOzYsUMt7T179gz6+voqq2/lypW4dOkS9uzZg7S0NISGhqqs7peNHj0af/zxB06ePIn27dtj+PDhKCwslCsj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ZGTg0aNHGD58OCZPnoxVq1YhJCQEwcHBSE1Nxbx58xAaGgotLS2cP38ex44dg6+vLywtLXH+/Hk8fvwYzs7ONYrXx8cHnp6eGDJkCL766iu0b98e9+/fx/79+/Huu++iW7duqrw9RNViEkpERFRDy5YtQ35+Pt555x0YGxvj888/R25urvB9VFQUwsPDMXnyZPz7779o1aoVwsPDAQAtWrTAgQMHMH36dLi4uMDc3ByBgYGYM2cOAMDExASnTp3CqlWrkJeXh9atW+Prr79G//79axSrSCTCgQMHMHv2bAQEBODx48ewtrZGnz59YGVlVfubQaQkkYzbLRBVKS8vD6ampuixtAdnx7/uilXTE6oq7Al9obCwELdv34aDgwP09evP86kpb29vuLq6YtWqVZoOhUjtlPl95r+oRNR46ACFAwprXc3hiYdVEAy37iSixo1JKBE1HiIAurWvhr2XRES1xySUiIhIhSpaAomIyuO2nURERESkduwJJVJQ3KQ4mJiYaDoMIiKiBoE9oURERESkdkxCiYiIiEjtmIQSERERkdoxCSUiIiIitWMSSkRERHLmz58PV1fXKsuMHTsWQ4YMUUs81DBxdjwREdVbkoWd1dpeq4gram2PqDFjTygRERERqR2TUCIiohoqKCiAv78/jIyMYGNjg6+//hre3t6YMmUKAKCoqAjTpk1DixYtYGhoiJ49e8rtqBQTEwMzMzMcPnwYzs7OMDIyQr9+/fDgwQOhTHx8PHr06AFDQ0OYmZnBy8sLGRkZwvd79uxB165doa+vjzZt2mDBggV4/vy58L1IJMKGDRswaNAgGBgYwNnZGYmJibh58ya8vb1haGiIXr16IS0trdz1bdiwAXZ2djAwMMDw4cORm5tb6b2QSqWIjIyEg4MDxGIxXFxc8L///a8Wd5caOiahRERENTR9+nScPHkSe/bswZEjRxAfH49Lly4J3wcHByMxMRE7duzA5cuX8f7776Nfv374+++/hTJPnz7F8uXLsWXLFpw6dQoSiQTTpk0DADx//hxDhgzBm2++icuXLyMxMREff/wxRCIRACAhIQH+/v747LPPcO3aNWzYsAExMTFYvHixXJyLFi2Cv78/kpOT4eTkhFGjRmHixIkICwvD77//DplMhuDgYLlzbt68iZ9++gm//fYbDh06hD/++AOTJ0+u9F5ERkbi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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" } ], "source": [ @@ -1822,14 +559,13 @@ "### Further Reading\n", "\n", "- See `README.md` for detailed documentation\n", - "- Check `modeling.py` and `modeling_random_unitary.py` for implementation details\n", "- Refer to cited papers for theoretical background" ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": ".env", "language": "python", "name": "python3" }, @@ -1843,9 +579,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.0" + "version": "3.12.4" } }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/tutorial/QEnsemble/README.md b/tutorial/QEnsemble/README.md index 54edb27..5425362 100644 --- a/tutorial/QEnsemble/README.md +++ b/tutorial/QEnsemble/README.md @@ -1,53 +1,78 @@ # Quantum Ensemble Learning Tutorial -This tutorial demonstrates quantum ensemble learning methods for classification tasks using Qiskit. +This tutorial demonstrates quantum ensemble learning methods for classification tasks using the QBioCode framework. ## Overview -Quantum ensemble learning combines multiple quantum classifiers to improve prediction accuracy and robustness. This implementation uses controlled swap operations and quantum superposition to create ensembles of training data arrangements. The tutorial includes both fixed swap-based ensembles and random unitary-based ensembles for more general transformations. +Quantum ensemble learning combines multiple quantum classifiers to improve prediction accuracy and robustness. This implementation uses controlled swap operations and quantum superposition to create ensembles of training data arrangements. **Key Innovation**: By leveraging quantum superposition, the ensemble evaluates multiple training set configurations simultaneously, potentially offering advantages over classical ensemble methods. +**Note**: The core quantum ensemble functionality has been integrated into the QBioCode package as `qbiocode.learning.compute_qensemble`. This tutorial now demonstrates how to use the integrated API. + ## Files -### Core Modules - -- **`modeling.py`**: Main quantum ensemble implementation with fixed swap operations - - `cos_classifier__legacy()`: Legacy single-qubit cosine classifier using SWAP test - - `cos_classifier()`: Multi-qubit quantum cosine similarity classifier - - `state_prep()`: Extract unitary matrix for quantum state preparation - - `quantum_cosine_classifier()`: Cosine classifier with explicit unitary gates - - `ensemble()`: Main quantum ensemble with three modes: - - **balanced**: Class-balanced sampling for balanced datasets - - **unbalanced**: Random sampling for imbalanced datasets - - **pair_sample**: Comprehensive pairwise swaps - - `exec_simulator()`: Execute circuits on Aer simulator (CPU/GPU support) - -- **`modeling_random_unitary.py`**: Quantum ensemble using random unitary transformations - - Implements Haar-random unitaries sampled from the unitary group - - More general transformation approach than fixed swaps - - `run_ensemble()`: Complete workflow with training set selection and evaluation - - `ensemble()`: Random unitary-based ensemble circuit construction - - `state_prep()`: Unitary extraction for state preparation - - `label_to_array()`: Convert binary labels to one-hot encoding - - `evaluation_metrics()`: Calculate accuracy and Brier score - -- **`Utils.py`**: Comprehensive utility functions for workflows and analysis - - **Data Processing**: `normalize_custom()`, `training_set()`, `label_to_array()` - - **Quantum Workflows**: `run_quantum_ensemble()`, `run_quantum_cosine()`, `run_ensemble()` - - **Classical Baselines**: `run_random_forest()`, `run_xgboost()`, `run_lazy_predict()` - - **Evaluation**: `evaluation_metrics()`, `calculate_f1()`, `retrieve_proba()` - - **Visualization**: `plot_figures()`, `post_process_results()` - - **IBM Quantum Support**: Hardware execution with error mitigation and dynamic decoupling - -### Notebooks - -- **`QEnsemble_example_blobs.ipynb`**: Complete tutorial demonstrating quantum ensemble learning on synthetic blob datasets - -### Documentation - -- **`README.md`**: This file - comprehensive tutorial guide -- **`FUNCTIONS.md`**: Quick reference for all functions and parameters +### Tutorial Files + +- **`QEnsemble_example_blobs.ipynb`**: Complete tutorial demonstrating quantum ensemble learning on synthetic blob datasets using the QBioCode API + +- **`README.md`**: This file - tutorial guide + +## Using QBioCode API + +The quantum ensemble functionality is now available through the QBioCode package with a unified interface supporting two construction methods: + +### Basic Usage + +```python +from qbiocode.learning import compute_qensemble + +# Fixed swap ensemble (default, faster) +results = compute_qensemble( + X_train, X_test, y_train, y_test, + args={'backend': 'simulator', 'grid_search': False}, + n_train=4, # Training samples (must be even) + d=2, # Ensemble depth (creates 2^2=4 members) + n_swap=1, # Operations per control qubit + mode="balanced", # Sampling strategy + ensemble_method="swap", # Fixed swap method (default) + n_shots=8192, # Measurement shots + seed=42 +) + +# Random unitary ensemble (advanced, more general) +results = compute_qensemble( + X_train, X_test, y_train, y_test, + args={'backend': 'simulator', 'grid_search': False}, + n_train=4, + d=2, + n_swap=1, + mode="balanced", + ensemble_method="random_unitary", # Use Haar-random unitaries + n_shots=8192, + seed=42 +) +``` + +**Ensemble Methods:** +- **`"swap"`** (default): Uses controlled-SWAP gates for deterministic data rearrangement + - Faster execution + - Deterministic circuit structure + - Good for most applications + +- **`"random_unitary"`**: Uses Haar-random unitaries for more general transformations + - More general transformation space + - Potentially better generalization + - More computationally expensive + - Samples from the full unitary group U(N) + +### Available Utility Functions + +From `qbiocode.utils`: +- `normalize_data()`: Normalize data vectors for quantum state encoding +- `label_to_array()`: Convert binary labels to one-hot encoding +- `prepare_training_set()`: Select and prepare balanced training subsets +- `retrieve_probabilities()`: Extract probabilities from measurement counts ## Key Concepts @@ -89,41 +114,6 @@ The quantum cosine classifier measures similarity between quantum states using t - Potentially better generalization but computationally more expensive - Unitary dimension: 2^(n_obs_qubits + n_obs) -## Usage Example - -```python -from modeling import ensemble, exec_simulator -from Utils import training_set, normalize_custom, evaluation_metrics -import numpy as np - -# Prepare data -X_train, y_train = ... # Your training data -X_test, y_test = ... # Your test data - -# Select training subset -X_data, Y_data = training_set(X_train, y_train, n=4, seed=42) - -# Run ensemble for each test point -predictions = [] -for x_test in X_test: - x_test_norm = normalize_custom(x_test) - - # Create quantum circuit - qc = ensemble(X_data, Y_data, x_test_norm, - n_swap=1, d=2, mode="balanced") - - # Execute on simulator - result = exec_simulator(qc, n_shots=8192) - - # Extract prediction - p0 = result['0'] / (result['0'] + result['1']) - p1 = 1 - p0 - predictions.append([p0, p1]) - -# Evaluate -accuracy, brier = evaluation_metrics(predictions, y_test) -print(f"Accuracy: {accuracy:.3f}, Brier Score: {brier:.3f}") -``` ## Parameters @@ -177,6 +167,9 @@ print(f"Accuracy: {accuracy:.3f}, Brier Score: {brier:.3f}") ### Primary References - **This Tutorial**: Part of QBioCode framework for quantum bioinformatics -- **Quantum Ensemble Paper**: Macaluso et al., "A variational algorithm for quantum ensemble learning", IET Quantum Communication (2023) +- **Quantum Ensemble Methods**: + - Macaluso et al., "A variational algorithm for quantum ensemble learning", IET Quantum Communication (2023) + - Rhrissorrakrai et al., "Quantum Ensembling Methods for Healthcare and Life Science", arXiv:2506.02213 (2025) + [https://arxiv.org/abs/2506.02213](https://arxiv.org/abs/2506.02213) - **Original Implementation**: [GitHub Repository](https://github.com/amacaluso/Quantum-algorithm-for-ensemble-using-bagging) - **QBioCode Documentation**: See main repository for comprehensive guides diff --git a/tutorial/QEnsemble/Utils.py b/tutorial/QEnsemble/Utils.py deleted file mode 100755 index e2c26c3..0000000 --- a/tutorial/QEnsemble/Utils.py +++ /dev/null @@ -1,1308 +0,0 @@ -""" -Utility Functions for Quantum Ensemble Learning - -This module provides comprehensive utility functions for: -- Data preprocessing and normalization -- Model evaluation and metrics calculation -- Visualization of results -- Classical baseline methods (Random Forest, XGBoost, LazyPredict) -- Quantum ensemble execution workflows -- Result post-processing and analysis -""" -import os -import re -import pickle -import warnings -from typing import Any - -import numpy as np -import pandas as pd -import matplotlib.pyplot as plt -import seaborn as sns -from sklearn import datasets -from sklearn.decomposition import PCA -from sklearn.ensemble import RandomForestClassifier -from sklearn.metrics import confusion_matrix, precision_score, recall_score, f1_score -from sklearn.metrics.pairwise import cosine_similarity -from sklearn.model_selection import RandomizedSearchCV, GridSearchCV, StratifiedKFold -from sklearn.preprocessing import StandardScaler, MinMaxScaler -from lazypredict.Supervised import LazyClassifier -from xgboost import XGBClassifier -import umap - -from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler -from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager -from qiskit.circuit.library import XGate - -import modeling_random_unitary -from modeling import * - -warnings.filterwarnings('ignore') - - -def calculate_predicted_classes(preds): - """ - Convert probability predictions to class labels. - - Takes probability predictions in the form [p0, p1] and converts them - to binary class labels by selecting the class with higher probability. - - Parameters - ---------- - preds : list of array-like - Predicted probabilities for each class. Each element should be - [p0, p1] where p0 is probability of class 0 and p1 is probability - of class 1 - - Returns - ------- - list of int - Predicted class labels (0 or 1) based on argmax of probabilities - - Examples - -------- - >>> preds = [[0.7, 0.3], [0.2, 0.8], [0.5, 0.5]] - >>> calculate_predicted_classes(preds) - [0, 1, 1] - """ - return [0 if x[0] > x[1] else 1 for x in preds] - - -def calculate_number_predicted_classes(preds): - """ - Count the number of unique predicted classes. - - Parameters - ---------- - preds : list of array-like - Predicted probabilities for each class - - Returns - ------- - int - Number of unique predicted classes - """ - return len(set(calculate_predicted_classes(preds))) - - -def plot_figures(results_df_agg, dataset_name, method, figsize=(12, 3)): - """ - Create visualization plots for model performance comparison. - - Generates four plots: - - Top performing models by accuracy (>= 0.5) - - Worst performing models by accuracy (<= 0.5) - - Top performing models by F1 score (>= 0.5) - - Worst performing models by F1 score (<= 0.5) - - Parameters - ---------- - results_df_agg : DataFrame - Aggregated results with columns: Model, Accuracy, F1 Score, dataset - dataset_name : str - Name of the dataset for plot titles - method : str - Method name for plot titles - figsize : tuple, optional - Figure size (width, height) in inches (default: (12, 3)) - """ - - results_df_agg_top = results_df_agg[results_df_agg['Accuracy'] >= 0.5] - results_df_agg_top.sort_values('Accuracy', ascending=False) - - plt.figure(figsize=figsize) - sns.barplot(data=results_df_agg_top, - x='Model', - y='Accuracy', - hue='dataset') - plt.xticks(rotation=90) - plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0) - plt.title('Top Performing for ' + dataset_name + ' with ' + method) - plt.show() - plt.close() - - results_df_agg_worst = results_df_agg[results_df_agg['Accuracy'] <= 0.5] - results_df_agg_worst.sort_values('Accuracy', ascending=False) - - plt.figure(figsize=figsize) - sns.barplot(data=results_df_agg_worst, - x='Model', - y='Accuracy', - hue='dataset') - plt.xticks(rotation=90) - plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0) - plt.title('Worst Performing for ' + dataset_name + ' with ' + method) - plt.show() - plt.close() - - results_df_agg_top = results_df_agg[results_df_agg['F1 Score'] >= 0.5] - results_df_agg_top.sort_values('F1 Score', ascending=False) - - plt.figure(figsize=figsize) - sns.barplot(data = results_df_agg_top, - x = 'Model', - y = 'F1 Score', - hue = 'dataset') - plt.xticks(rotation=90) - plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0) - plt.title( 'Top Performing for ' + dataset_name + ' with ' + method ) - plt.show() - plt.close() - - results_df_agg_worst = results_df_agg[ results_df_agg['F1 Score'] <= 0.5 ] - results_df_agg_worst.sort_values('F1 Score', ascending=False) - - plt.figure(figsize=figsize) - sns.barplot(data = results_df_agg_worst, - x = 'Model', - y = 'F1 Score', - hue = 'dataset') - plt.xticks(rotation=90) - plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0) - plt.title( 'Worst Performing for ' + dataset_name + ' with ' + method ) - plt.show() - plt.close() - -def calculate_f1(preds, y_test): - """ - Calculate weighted F1 score from probability predictions. - - Converts probability predictions to class labels and computes the - weighted F1 score, which is the harmonic mean of precision and recall - weighted by class support. - - Parameters - ---------- - preds : list of array-like - Predicted probabilities for each class. Each element should be - [p0, p1] where p0 + p1 ≈ 1 - y_test : array-like, shape (n_samples,) - True binary labels (0 or 1) - - Returns - ------- - float - Weighted F1 score in range [0, 1]. Higher is better. - F1 = 2 * (precision * recall) / (precision + recall) - - Notes - ----- - Uses sklearn's f1_score with average='weighted' to account for - class imbalance. - - Examples - -------- - >>> preds = [[0.9, 0.1], [0.3, 0.7], [0.8, 0.2]] - >>> y_test = np.array([0, 1, 0]) - >>> f1 = calculate_f1(preds, y_test) - >>> print(f"F1 Score: {f1:.3f}") - F1 Score: 1.000 - """ - preds = [1 if p[1] > p[0] else 0 for p in preds] - return f1_score(y_pred=preds, y_true=y_test, average='weighted') - - -def run_quantum_ensemble(predictions, dataset, method, dataset_name, seed, test_size, file_predictions, ds, n_swaps, n_features, - n_trains, n_shots, pca_embed=False, umap_embed=False, device='CPU', instance='', random_unitary=False, select_features=[]): - """ - Run quantum ensemble experiments across multiple parameter configurations. - - This is a comprehensive workflow function that performs a grid search over - quantum ensemble hyperparameters (d, n_swap, n_features, n_train) and - evaluates performance on test data. Results are automatically saved to - disk after each configuration. - - The function handles: - - Data preprocessing (scaling, dimensionality reduction) - - Feature selection (variance-based or PCA/UMAP) - - Circuit construction and execution - - Performance evaluation - - Result aggregation and persistence - - Parameters - ---------- - predictions : dict - Dictionary to store prediction results. Structure: - predictions[dataset_name][method] = DataFrame of results - dataset : dict - Dictionary containing dataset splits. Structure: - dataset[dataset_name][params] = (X_train, X_test, y_train, y_test) - method : str - Method name for results tracking (e.g., 'qensemble', 'qensemble_random_unitary') - dataset_name : str - Name of the dataset being processed - seed : int - Random seed for reproducibility of training set selection - test_size : float - Fraction of data for testing (informational, not used directly) - file_predictions : str - Path to pickle file for saving predictions dictionary - ds : list of int - List of ensemble depths (control qubits) to test. Example: [1, 2, 3] - n_swaps : list of int - List of swap operation counts to test. Example: [1, 2, 3] - n_features : list of int - List of feature counts to test. Must be powers of 2. Example: [2, 4, 8] - n_trains : list of int - List of training sample sizes to test. Example: [4, 8, 16] - n_shots : int - Number of measurement shots per circuit execution - pca_embed : bool, optional - If True, use PCA for dimensionality reduction (default: False) - umap_embed : bool, optional - If True, use UMAP for dimensionality reduction (default: False) - device : str, optional - Execution device: - - 'CPU': Local CPU simulation - - 'GPU': Local GPU simulation (requires qiskit-aer-gpu) - - 'ibm_*': IBM Quantum device name (requires instance) - (default: 'CPU') - instance : str, optional - IBM Quantum instance string (e.g., 'ibm-q/open/main') required - when device is an IBM backend (default: '') - random_unitary : bool, optional - If True, use random unitary ensemble variant from - modeling_random_unitary module (default: False) - select_features : list, optional - List of specific feature names to select. If empty, uses - variance-based selection (default: []) - - Returns - ------- - dict - Updated predictions dictionary with new results appended - - Notes - ----- - - Automatically skips configurations that have already been run - - Uses MinMaxScaler for data normalization - - Feature selection: variance-based (default), PCA, or UMAP - - Results saved after each configuration for fault tolerance - - Skips configurations where n_train <= d (insufficient samples) - - Examples - -------- - >>> predictions = {} - >>> dataset = {'blobs': {(100, 2, 2): (X_train, X_test, y_train, y_test)}} - >>> predictions = run_quantum_ensemble( - ... predictions, dataset, 'qensemble', 'blobs', seed=42, - ... test_size=0.2, file_predictions='results.pkl', - ... ds=[2], n_swaps=[1], n_features=[2], n_trains=[4], - ... n_shots=8192, device='CPU' - ... ) - """ - - if (dataset_name not in predictions.keys()) or (method not in predictions[dataset_name].keys()): - results_df = pd.DataFrame() - predictions[dataset_name][method] = results_df - else: - results_df = predictions[dataset_name][method] - - for k,v in dataset[dataset_name].items(): - for f in n_features: - (X_train_orig, X_test_orig, y_train, y_test) = v - - if len( select_features ) > 0: - X_train = X_train.loc[:,select_features] - X_test = X_test.loc[:,select_features] - - scaler = MinMaxScaler() - X_train = pd.DataFrame( scaler.fit_transform(X_train_orig), index=X_train_orig.index, columns=X_train_orig.columns ) - X_test = pd.DataFrame( scaler.transform(X_test_orig), index=X_test_orig.index, columns=X_test_orig.columns ) - embed = 'none' - - if pca_embed: - embed = 'pca' - embedder = PCA(f) - X_train = embedder.fit_transform(X_train) - X_test = embedder.transform(X_test) - elif umap_embed: - embed = 'umap' - reducer = umap.UMAP(f) - X_train = reducer.fit_transform(X_train) - X_test = reducer.transform(X_test) - else: - vr = X_train.apply(np.var, axis = 0) - vr = vr.sort_values(ascending=False) - X_train = X_train[ list(vr[0:f].index) ].to_numpy() - X_test = X_test[ list(vr[0:f].index) ].to_numpy() - - for d in ds: - for n_train in n_trains: - if n_train > d: - for n_swap in n_swaps: - if (len(results_df) == 0) or (len(results_df[(results_df['dataset'] == dataset_name) & - (results_df['method'] == method) & - (results_df['dataset_params'] == k) & - (results_df['n_feature'] == f ) & - (results_df['n_swap'] == n_swap ) & - (results_df['n_train'] == n_train ) & - (results_df['embed'] == embed ) & - (results_df['select_features'] == ','.join(select_features) ) & - (results_df['d'] == d) - ]) == 0): - if random_unitary: - res = modeling_random_unitary.run_ensemble(d, n_train, seed, n_swap, X_train, X_test, y_train, y_test, n_shots = n_shots) - else: - res = run_ensemble(d, n_train, seed, n_swap, X_train, X_test, y_train, y_test, n_shots = n_shots, device = device, instance = instance) - - res['dataset'] = [dataset_name]*len(res) - res['method'] = [method]*len(res) - res['dataset_params'] = [k]*len(res) - res['embed'] = [embed]*len(res) - res['select_features'] = [','.join(select_features)]*len(res) - results_df = pd.concat( [results_df, res] ) - - predictions[dataset_name][method] = results_df - - # save - pickle.dump( predictions, open( file_predictions, 'wb') ) - - return predictions - - -def run_quantum_cosine(predictions, dataset, method, dataset_name, seed, test_size, file_predictions, n_features, - n_trains, n_shots, pca_embed=False, umap_embed=False, select_features=[]): - """ - Run quantum cosine classifier experiments. - - Parameters - ---------- - predictions : dict - Dictionary to store prediction results - dataset : dict - Dictionary containing dataset splits - method : str - Method name for results tracking - dataset_name : str - Name of the dataset - seed : int - Random seed for reproducibility - test_size : float - Fraction of data for testing - file_predictions : str - Path to save predictions - n_features : list of int - List of feature counts to test - n_trains : list of int - List of training sample sizes to test - n_shots : int - Number of measurement shots - pca_embed : bool, optional - Use PCA for dimensionality reduction (default: False) - umap_embed : bool, optional - Use UMAP for dimensionality reduction (default: False) - select_features : list, optional - Specific features to select (default: []) - - Returns - ------- - dict - Updated predictions dictionary with results - """ - epsilon = 1e-15 - if (dataset_name not in predictions.keys()) or (method not in predictions[dataset_name].keys()): - results_df = pd.DataFrame() - predictions[dataset_name][method] = results_df - else: - results_df = predictions[dataset_name][method] - - for k,v in dataset[dataset_name].items(): - for f in n_features: - (X_train, X_test, y_train, y_test) = v - if len( select_features ) > 0: - X_train = X_train.loc[:,select_features] - X_test = X_test.loc[:,select_features] - - Y_vector_train = label_to_array(y_train) - Y_vector_test = label_to_array(y_test) - test_size = Y_vector_test.shape[0] - train_size = Y_vector_train.shape[0] - scaler = MinMaxScaler() - X_train = scaler.fit_transform(X_train) - X_test = scaler.transform(X_test) - embed = 'none' - - # adding epsilon to avoid zero division - X_train = X_train + epsilon - X_test = X_test + epsilon - - if pca_embed: - embed = 'pca' - embedder = PCA(f) - X_train = embedder.fit_transform(X_train) - X_test = embedder.transform(X_test) - elif umap_embed: - embed = 'umap' - reducer = umap.UMAP(f) - X_train = reducer.fit_transform(X_train) - X_test = reducer.transform(X_test) - - for n_train in n_trains: - preds = [] - - if (len(results_df) == 0) or (len(results_df[(results_df['dataset'] == dataset_name) & - (results_df['method'] == method) & - (results_df['dataset_params'] == k) & - (results_df['n_feature'] == f ) & - (results_df['embed'] == embed ) & - (results_df['select_features'] == ','.join(select_features) ) & - (results_df['n_train'] == n_train ) - ]) == 0): - - for x_test, y_ts in zip(X_test, Y_vector_test): - ix = np.random.choice(train_size, n_train)[0] - x_train: Any | ndarray[Any, dtype[float64]] = X_train[ix] - x_tr = normalize_custom_legacy(x_train) - y_tr = Y_vector_train[ix] - x_ts = normalize_custom_legacy(x_test) - qc = cos_classifier_legacy(x_tr, x_ts, y_tr) - - r = exec_simulator(qc, n_shots=n_shots) - - if '0' not in r.keys(): - r['0'] = 0 - elif '1' not in r.keys(): - r['1'] = 0 - - preds.append(retrieve_proba(r,)) - - a, b = evaluation_metrics(preds, y_test, save=False) - res = pd.DataFrame( [dataset_name, method, k, seed, X_train.shape[1], qc.num_qubits, n_train, embed, ','.join(select_features), a, b, preds, y_test ], - index = ['dataset', 'method', 'dataset_params', 'seed', 'n_feature', 'qubits', 'n_train', 'embed', 'select_features', 'accuracy', 'brier', 'predictions', 'y_test']).transpose() - results_df = pd.concat( [results_df, res]) - - predictions[dataset_name][method] = results_df - - # save - pickle.dump( predictions, open( file_predictions, 'wb') ) - - return predictions - - - -def run_random_forest(predictions, dataset, method, dataset_name, seed, test_size, file_predictions, select_features = [], - params = {}, pca_embed = False, umap_embed = False, n_features = 0): - - if (dataset_name not in predictions.keys()) or (method not in predictions[dataset_name].keys()): - results_df = pd.DataFrame() - predictions[dataset_name][method] = results_df - else: - results_df = predictions[dataset_name][method] - - - # Define the hyperparameter grid - param_distributions = { - 'n_estimators': np.arange(100, 1000, 100), - 'max_depth': np.arange(5, 20), - 'min_samples_split': np.arange(2, 10), - 'min_samples_leaf': np.arange(1, 5), - 'max_features': ['sqrt', 'log2'] - } - - for k,v in dataset[dataset_name].items(): - (X_train, X_test, y_train, y_test) = v - if len( select_features ) > 0: - X_train = X_train.loc[:,select_features] - X_test = X_test.loc[:,select_features] - - scaler = MinMaxScaler() - X_train = scaler.fit_transform(X_train) - X_test = scaler.transform(X_test) - - embed = 'none' - - if pca_embed: - embed = 'pca' - embedder = PCA(n_features) - X_train = embedder.fit_transform(X_train) - X_test = embedder.transform(X_test) - elif umap_embed: - embed = 'umap' - reducer = umap.UMAP(n_features) - X_train = reducer.fit_transform(X_train) - X_test = reducer.transform(X_test) - - if len(params) > 0: - # Initialize the Random Forest Classifier - rf = RandomForestClassifier(random_state=seed, - n_estimators=params['n_estimators'], - min_samples_split=params['min_samples_split'], - max_features=params['max_features'], - max_depth=params['max_depth'], - min_samples_leaf=params['min_samples_leaf'] - ) - - rf.fit(X_train, y_train) - preds = rf.predict_proba(X_test) - else: - # Initialize the Random Forest Classifier - rf = RandomForestClassifier(random_state=seed) - - # Initialize RandomizedSearchCV - rf_random = RandomizedSearchCV(estimator=rf, - param_distributions=param_distributions, - n_iter=10, - cv=3, - random_state=seed, - n_jobs=-1) - rf_random.fit(X_train, y_train) - preds = rf_random.predict_proba(X_test) - params = rf_random.best_params_ - - a, b = evaluation_metrics(preds, y_test, save=False) - - res = pd.DataFrame( [dataset_name, method, k, seed, X_train.shape[1], ','.join(select_features), a, b, preds, y_test, params ], - index = ['dataset', 'method', 'dataset_params', 'seed', 'n_feature', 'select_features', 'accuracy', 'brier', 'predictions', 'y_test', 'best_params']).transpose() - results_df = pd.concat( [results_df, res]) - - predictions[dataset_name][method] = results_df - - # save - pickle.dump( predictions, open( file_predictions, 'wb') ) - - - return predictions - - -def run_xgboost(predictions, dataset, method, dataset_name, seed, test_size, file_predictions, select_features = [], - params = {}, pca_embed = False, umap_embed = False, n_features = 0): - - if (dataset_name not in predictions.keys()) or (method not in predictions[dataset_name].keys()): - results_df = pd.DataFrame() - predictions[dataset_name][method] = results_df - else: - results_df = predictions[dataset_name][method] - - for k,v in dataset[dataset_name].items(): - (X_train, X_test, y_train, y_test) = v - if len( select_features ) > 0: - X_train = X_train.loc[:,select_features] - X_test = X_test.loc[:,select_features] - - scaler = StandardScaler() - X_train = scaler.fit_transform(X_train) - X_test = scaler.transform(X_test) - - embed = 'none' - - if pca_embed: - embed = 'pca' - embedder = PCA(n_features) - X_train = embedder.fit_transform(X_train) - X_test = embedder.transform(X_test) - elif umap_embed: - embed = 'umap' - reducer = umap.UMAP(n_features) - X_train = reducer.fit_transform(X_train) - X_test = reducer.transform(X_test) - - - ##XGB - if len(params) > 0: - # Initialize XGB - xgb = XGBClassifier( - random_state=seed, - n_estimators=params['n_estimators'], - max_depth=params['max_depth'], - learning_rate=params['learning_rate'], - subsample=params['subsample'], - colsample_bytree=params['colsample_bytree'], - min_child_weight=params['min_child_weight'], - eval_metric='logloss' - ) - xgb.fit(X_train, y_train) - preds = xgb.predict_proba(X_test) - else: - xgb = XGBClassifier( - random_state=seed, - eval_metric='logloss' - ) - - cv = StratifiedKFold(n_splits=3, shuffle=True, random_state=seed) - - param_grid = { - 'n_estimators': [100, 200], - 'max_depth': [3, 5, 7], - 'learning_rate': [0.01, 0.1, 0.2], - 'subsample': [0.7, 0.8, 1.0], - 'colsample_bytree': [0.7, 0.8, 1.0], - 'min_child_weight': [1, 3, 5] - } - - xgb_grid = GridSearchCV( - estimator=xgb, - param_grid=param_grid, - scoring='f1_weighted', - n_jobs=-1, - cv=cv, - verbose=1 - ) - - xgb_grid.fit(X_train, y_train) - preds = xgb_grid.predict_proba(X_test) - params = xgb_grid.best_params_ - - a, b = evaluation_metrics(preds, y_test, save=False) - - res = pd.DataFrame( [dataset_name, method, k, seed, X_train.shape[1], ','.join(select_features), a, b, preds, y_test, params ], - index = ['dataset', 'method', 'dataset_params', 'seed', 'n_feature', 'select_features', 'accuracy', 'brier', 'predictions', 'y_test', 'best_params']).transpose() - results_df = pd.concat( [results_df, res]) - - predictions[dataset_name][method] = results_df - - # save - pickle.dump( predictions, open( file_predictions, 'wb') ) - - - return predictions - - -def run_lazy_predict(predictions, dataset, method, dataset_name, seed, test_size, file_predictions, select_features = []): - predictions[dataset_name][method] = {} - - for k,v in dataset[dataset_name].items(): - (X_train, X_test, y_train, y_test) = v - if len( select_features ) > 0: - X_train = X_train.loc[:,select_features] - X_test = X_test.loc[:,select_features] - - scaler = MinMaxScaler() - X_train = scaler.fit_transform(X_train) - X_test = scaler.transform(X_test) - clf = LazyClassifier(verbose=0,ignore_warnings=True, custom_metric=None) - models,preds = clf.fit(X_train, X_test, y_train, y_test) - - predictions[dataset_name][method][k] = {} - predictions[dataset_name][method][k]['seed'] = seed - predictions[dataset_name][method][k]['models'] = models - predictions[dataset_name][method][k]['preds'] = preds - predictions[dataset_name][method][k]['y_test'] = y_test - predictions[dataset_name][method][k]['select_features'] = select_features - - # save - pickle.dump( predictions, open( file_predictions, 'wb') ) - - return predictions - - -def run_ensemble(d, n_train, seed, n_swap, X_train, X_test, y_train, y_test, - mode="pair_sample", n_shots=8192, selectRandom=True, device='CPU', - barriers=False, instance='', resilience_level=1, optimization_level=3, - nthreads=20, dynamicDecoupling=False): - """ - Execute quantum ensemble classifier with comprehensive hardware support. - - This function provides a complete workflow for running quantum ensemble - classification with support for both simulation and IBM Quantum hardware. - It handles training set selection, circuit construction, transpilation, - execution, and evaluation. - - Key features: - - IBM Quantum hardware execution with error mitigation - - Dynamic decoupling for noise reduction - - Parallel transpilation - - Automatic result aggregation - - Parameters - ---------- - d : int - Number of control qubits (ensemble depth) - n_train : int - Number of training samples to use - seed : int - Random seed for training set selection - n_swap : int - Number of swap operations per control qubit - X_train : array-like, shape (n_samples, n_features) - Training feature data - X_test : array-like, shape (n_test, n_features) - Test feature data - y_train : array-like, shape (n_samples,) - Training labels - y_test : array-like, shape (n_test,) - Test labels - mode : str, optional - Ensemble mode: "pair_sample", "balanced", or "unbalanced" - (default: "pair_sample") - n_shots : int, optional - Number of measurement shots (default: 8192) - selectRandom : bool, optional - If True, randomly select training subset; if False, use all - training data (default: True) - device : str, optional - Execution device: 'CPU', 'GPU', or IBM device name like 'ibm_kyoto' - (default: 'CPU') - barriers : bool, optional - Add barrier gates for visualization (default: False) - instance : str, optional - IBM Quantum instance string (default: '') - resilience_level : int, optional - IBM error mitigation level (0-3). Higher = more mitigation - (default: 1) - optimization_level : int, optional - Transpilation optimization level (0-3) (default: 3) - nthreads : int, optional - Number of threads for parallel transpilation (default: 20) - dynamicDecoupling : bool, optional - Enable dynamic decoupling (XY4 sequence) for IBM hardware - (default: False) - - Returns - ------- - DataFrame - Results with columns: seed, n_feature, qubits, d, n_train, n_swap, - accuracy, brier, predictions, y_test. Returns empty DataFrame if - circuit exceeds 36 qubits. - - Notes - ----- - IBM Quantum Features: - - Uses SamplerV2 with twirling enabled - - Supports dynamic decoupling with XY4 sequence - - Parallel circuit transpilation - - Automatic gate duration handling - - Simulation Features: - - CPU/GPU support via AerSimulator - - Statevector method with optimization - - Limited to ~36 qubits - - Examples - -------- - >>> # Local simulation - >>> results = run_ensemble(d=2, n_train=4, seed=42, n_swap=1, - ... X_train, X_test, y_train, y_test, - ... device='CPU', n_shots=8192) - - >>> # IBM Quantum hardware - >>> results = run_ensemble(d=2, n_train=4, seed=42, n_swap=1, - ... X_train, X_test, y_train, y_test, - ... device='ibm_kyoto', instance='ibm-q/open/main', - ... dynamicDecoupling=True, n_shots=4096) - """ - - predictions = [] - qc_list = [] - - if 'ibm' in device: - service = QiskitRuntimeService(instance=instance) - backend = service.backend(device) - sampler = Sampler(mode=backend, options={"default_shots": n_shots}) - - sampler.options.twirling.enable_gates = True - sampler.options.twirling.num_randomizations = "auto" - sampler.options.twirling.enable_measure = True - - - # Set Sampler Options - if dynamicDecoupling: - sampler.options.dynamical_decoupling.enable = True - sampler.options.dynamical_decoupling.sequence_type = 'XY4' - - - # Get gate durations so the transpiler knows how long each operation takes - durations = backend.target.durations() - - # This is the sequence we'll apply to idling qubits - dd_sequence = [XGate(), XGate()] - - for x_test in X_test: - x_test = normalize_custom(x_test) - X_data, Y_data = training_set(X_train, y_train, n=n_train, seed=seed) - - qc_orig = ensemble(X_data, Y_data, x_test, n_swap=n_swap, d=d, mode=mode, barriers = barriers) - pass_manager = generate_preset_pass_manager(optimization_level=optimization_level, backend=backend) - qc = pass_manager.run(qc_orig, num_processes=nthreads) - qc_list.append(qc) - - - - for i in range(len(qc_list)): - result = sampler.run([qc_list[i]]).result() - r = result[0].join_data().get_counts() - predictions.append(retrieve_proba(r)) - else: - for x_test in X_test: - X_data, Y_data = training_set(X_train, y_train, n=n_train, seed=seed, selectRandom = selectRandom) - x_test = normalize_custom(x_test) - - qc = qc_orig = ensemble(X_data, Y_data, x_test, n_swap=n_swap, d=d, mode=mode, barriers = barriers) - if qc.num_qubits <=36: - r = exec_simulator(qc, n_shots=n_shots, device = device) - predictions.append(retrieve_proba(r)) - - if ('ibm' in device) or (qc.num_qubits <=36): - a, b = evaluation_metrics(predictions, y_test, save=False) - - res = pd.DataFrame( [ seed, X_data.shape[1], qc_orig.num_qubits, d, n_train, n_swap, a, b, predictions, y_test ], - index = ['seed', 'n_feature', 'qubits', 'd', 'n_train', 'n_swap', 'accuracy', 'brier', 'predictions', 'y_test' ] ).transpose() - print(res.iloc[:,1:8]) - return res - else: - return pd.DataFrame() - -def post_process_ensemble_results_df( df ): - cmatrices = [] - precisions = [] - recalls = [] - f1s = [] - label_all = [] - pred_all = [] - correct_all = [] - incorrect_all = [] - - for idx, row in df.iterrows(): - labels = [int(x) for x in re.sub( '\[', '', re.sub('\]', '', row['y_test'] ) ).split(' ') ] - preds = [float(x) for x in re.sub( '\[', '', re.sub('\]', '', row['predictions'] ) ).split(', ') ] - preds_bin = [] - for i in np.arange(0,len(preds),2): - if preds[i] > preds[i+1]: - preds_bin.append(0) - else: - preds_bin.append(1) - - correct = [ 1 if labels[i] == preds_bin[i] else 0 for i in range(len(labels))] - correct_all.append(correct) - incorrect_all.append( [np.abs(x-1) for x in correct ]) - label_all.append(labels) - pred_all.append(preds_bin) - cmatrices.append(confusion_matrix( labels, preds_bin)) - precisions.append(precision_score(labels, preds_bin,average='weighted')) - recalls.append(recall_score(labels, preds_bin,average='weighted')) - f1s.append(f1_score(labels, preds_bin,average='weighted')) - - df['confusion_matrix'] = cmatrices - df['precision'] = precisions - df['recall'] = recalls - df['f1'] = f1s - df['labels'] = label_all - df['preds'] = pred_all - df['correct'] = correct_all - df['incorrect'] = incorrect_all - - return(df) - - - - -def create_dir(path): - if not os.path.exists(path): - print('The directory', path, 'does not exist and will be created') - os.makedirs(path) - else: - print('The directory', path, ' already exists') - - -def save_dict(d, name='dict'): - df = pd.DataFrame(list(d.items())) - name = name + '_' + str(np.random.randint(10 ** 6)) + '.csv' - df.to_csv(name) - - -def normalize_custom_legacy(x, C=1): - M = x[0] ** 2 + x[1] ** 2 - x_normed = [ - 1 / np.sqrt(M * C) * complex(x[0], 0), # 00 - 1 / np.sqrt(M * C) * complex(x[1], 0), # 01 - ] - return x_normed - - -def normalize_custom(x, C=1): - """ - Normalize data vector for quantum state encoding. - - Normalizes a classical data vector to unit L2 norm and converts to - complex amplitudes suitable for quantum state initialization. This - ensures the encoded quantum state is properly normalized. - - Parameters - ---------- - x : array-like, shape (n,) - Classical data vector to normalize - C : float, optional - Scaling constant (default: 1) - - Returns - ------- - list of complex - Normalized vector as list of complex numbers with zero imaginary part - - Notes - ----- - - Computes M = Σᵢ xᵢ² - - Returns [x₀/√(MC), x₁/√(MC), ..., xₙ/√(MC)] as complex numbers - - Ensures Σᵢ |xᵢ|² = 1 for valid quantum state - - Examples - -------- - >>> x = np.array([3.0, 4.0]) - >>> x_norm = normalize_custom(x) - >>> print([abs(xi) for xi in x_norm]) - [0.6, 0.8] - >>> print(sum([abs(xi)**2 for xi in x_norm])) - 1.0 - """ - M = sum( x**2 ) - x_normed = [ 1/np.sqrt(M*C)*complex(i,0) for i in x ] - return x_normed - - -def add_label(d, label='0'): - try: - d[label] - print('Label', label, 'exists') - except: - d[label] = 0 - return d - -def label_to_array(y): - Y = [] - for el in y: - if el == 0: - Y.append([1, 0]) - else: - Y.append([0, 1]) - Y = np.asarray(Y) - return Y - - -def evaluation_metrics(predictions, y_test, save=True): - """ - Calculate accuracy and Brier score for binary classification. - - Computes two key metrics for evaluating probabilistic binary classifiers: - accuracy (correctness) and Brier score (calibration quality). - - Parameters - ---------- - predictions : list of array-like - Predicted probabilities. Each element should be [p0, p1] where - p0 + p1 = 1 - y_test : array-like, shape (n_samples,) - True binary labels (0 or 1) - save : bool, optional - Legacy parameter, not currently used (default: True) - - Returns - ------- - tuple of float - (accuracy, brier_score) where: - - accuracy: Fraction of correct predictions (0 to 1) - - brier_score: Mean squared error of probabilities (0 to 1) - Lower Brier score indicates better calibration - - Notes - ----- - - Predictions rounded to nearest integer for accuracy - - Brier score computed using probability of class 1 - - Perfect predictions: accuracy=1.0, brier_score=0.0 - - Uses sklearn.metrics for computation - - Examples - -------- - >>> predictions = [[0.9, 0.1], [0.2, 0.8], [0.6, 0.4]] - >>> y_test = np.array([0, 1, 0]) - >>> acc, brier = evaluation_metrics(predictions, y_test) - >>> print(f"Accuracy: {acc:.3f}, Brier: {brier:.3f}") - Accuracy: 1.000, Brier: 0.030 - - See Also - -------- - sklearn.metrics.accuracy_score : Accuracy calculation - sklearn.metrics.brier_score_loss : Brier score calculation - """ - from sklearn.metrics import brier_score_loss, accuracy_score - labels = label_to_array(y_test) - - predicted_class = np.round(np.asarray(predictions)) - acc = accuracy_score(np.array(predicted_class)[:, 1], - np.array(labels)[:, 1]) - - p0 = [p[0] for p in predictions] - p1 = [p[1] for p in predictions] - - brier = brier_score_loss(y_test, p1) - - return acc, brier - - - -def training_set(X, Y, n=4, seed=123, selectRandom=True): - """ - Select and prepare balanced training subset for quantum ensemble. - - Creates a balanced training set by selecting equal numbers of samples - from each class, normalizing them for quantum encoding, and converting - labels to one-hot format. - - Parameters - ---------- - X : array-like, shape (n_samples, n_features) - Training feature data - Y : array-like, shape (n_samples,) - Training labels (binary: 0 or 1) - n : int, optional - Total number of training samples to select. Must be even for - balanced selection (default: 4) - seed : int, optional - Random seed for reproducible selection (default: 123) - selectRandom : bool, optional - If True, randomly select n/2 samples from each class. - If False, use all training data (default: True) - - Returns - ------- - tuple of (ndarray, ndarray) - X_data : array of normalized training samples, shape (n, n_features) - Each sample is normalized using normalize_custom() - Y_data : one-hot encoded labels, shape (n, 2) - [[1,0] for class 0, [0,1] for class 1] - - Notes - ----- - - Ensures class balance by selecting n/2 samples from each class - - All data vectors are normalized to unit L2 norm - - Labels converted to quantum-compatible one-hot encoding - - Random selection uses numpy's random.choice without replacement - - Examples - -------- - >>> X = np.random.rand(20, 4) - >>> Y = np.array([0]*10 + [1]*10) - >>> X_data, Y_data = training_set(X, Y, n=4, seed=42) - >>> print(X_data.shape, Y_data.shape) - (4, 4) (4, 2) - >>> print(Y_data) - [[0 1] - [0 1] - [1 0] - [1 0]] - """ - np.random.seed(seed) - - X_data = X.copy() - Y_data = Y_vector = label_to_array(Y) - - if selectRandom: - ix_y1 = np.random.choice(np.where(Y == 1)[0], int(n / 2), replace=False) - ix_y0 = np.random.choice(np.where(Y == 0)[0], int(n / 2), replace=False) - - X_data = np.concatenate([X[ix_y1], X[ix_y0]]) - Y_data = np.concatenate([Y_vector[ix_y1], Y_vector[ix_y0]]) - - X_data_new = [] - - for i in range(len(X_data)): - X_data_new.append(normalize_custom(X_data[i])) - - X_data_new = np.array(X_data_new) - - return X_data_new, Y_data - - - -# Define the cosine classifier -def cosine_classifier(x,y): - return 1/2 + (cosine_similarity([x], [y])**2)/2 - -def retrieve_proba(r): - """ - Extract probability predictions from measurement counts. - - Converts raw measurement counts from quantum circuit execution into - probability predictions for binary classification. Handles edge cases - where only one outcome is observed. - - Parameters - ---------- - r : dict - Dictionary of measurement counts with keys '0' and/or '1' - Example: {'0': 4123, '1': 4069} - - Returns - ------- - list of float - [p0, p1] where p0 is probability of class 0 and p1 is probability - of class 1. Always sums to 1.0. - - Notes - ----- - - Handles missing keys gracefully (assigns probability 0 or 1) - - If only '0' observed: returns [1.0, 0.0] - - If only '1' observed: returns [0.0, 1.0] - - If both observed: returns normalized probabilities - - Examples - -------- - >>> counts = {'0': 6000, '1': 2000} - >>> probs = retrieve_proba(counts) - >>> print(probs) - [0.75, 0.25] - - >>> counts = {'0': 8192} # Only one outcome - >>> probs = retrieve_proba(counts) - >>> print(probs) - [1.0, 0.0] - """ - state_zero = '0' - state_one = '1' - p0 = 0 - p1 = 0 - try: - p0 = r[state_zero] / (r[state_zero] + r[state_one]) - p1 = 1 - p0 - except: - if list(r.keys())[0] == state_zero: - p0 = 1 - p1 = 0 - elif list(r.keys())[0] == state_one: - p0 = 0 - p1 = 1 - return [p0, p1] - - -def post_process_results( predictions, dir_output, datasets, metrics = ['Accuracy', 'F1 Score', 'brier'] ): - - total_results = pd.DataFrame() - for dataset_name in datasets: - print(f"Dataset: {dataset_name}") - methods = list( predictions[dataset_name].keys() ) - - all_results = pd.DataFrame() - - for method in methods: - if method not in ['random_forest_gs', 'xgb_gs']: # Ignore the parameter search experiments for the random forest and xgb - print(f"Method: {method}") - results_df = predictions[dataset_name][method] - results_df = results_df.reset_index(drop=True) - - results_df['num_pred_classes'] = [calculate_number_predicted_classes(x) for x in results_df['predictions']] - - results_df.columns = [ re.sub( 'dataset_params', 'split', re.sub( 'accuracy', 'Accuracy', re.sub('method', 'Model', x ) ) ) for x in results_df.columns] - results_df['F1 Score'] = [ calculate_f1( row.predictions, row.y_test) for idx, row in results_df.iterrows() ] - - if method == 'qcosine': - results_df = results_df[ (results_df['n_train']==1) & (results_df['n_feature']==2)] - results_df['Model'] = [ ':'.join( [row['Model'],str(row['n_train']),str(row['n_feature']), row['embed'] ]) for idx, row in results_df.iterrows() ] - elif method in ['random_forest', 'xgb']: - results_df['Model'] = [ ':'.join( [row['Model'],str(row['n_feature']) ]) for idx, row in results_df.iterrows() ] - else: - results_df['Model'] = [ ':'.join( [row['Model'],str(row['d']),str(row['n_train']),str(row['n_swap']),str(row['n_feature']),row['embed'] ]) for idx, row in results_df.iterrows() ] - - all_results = pd.concat( [all_results, results_df] ) - - if method in ['random_forest', 'xgb']: - results_df = results_df.drop([ 'predictions', 'y_test', 'best_params', 'select_features'], axis =1) - else: - results_df = results_df.drop([ 'predictions', 'y_test', 'embed', 'select_features'], axis =1) - - total_results = pd.concat( [total_results, all_results] ) - total_results_full = total_results.reset_index().copy() - - - from typing import Any - import scipy.stats as stats - - metrics = ['Accuracy', 'F1 Score', 'brier'] - methods = ['random_forest', 'xgb', 'qcosine', 'qensemble', 'qensemble_random_unitary'] - methods_cmap = dict(zip( methods[0:2], sns.color_palette('Greys', n_colors=2))) - methods_cmap.update( dict(zip(methods[2:], sns.color_palette(n_colors=len(methods[2:])))) ) - - - total_results_full['key'] = [ '-'.join( [row['Model'], row['dataset']] ) for idx,row in total_results_full.iterrows() ] - total_results_full['method'] = [ re.sub( ':.*', '', x ) for x in total_results['Model'] ] - - total_results = total_results_full[['Model', 'Accuracy', 'F1 Score', 'brier', 'dataset', 'split', 'num_pred_classes']] - total_results['split'] = [ x[0] for x in total_results['split'] ] - - total_results['key'] = [ '-'.join( [row['Model'], row['dataset']] ) for idx,row in total_results.iterrows() ] - total_results['method'] = [ re.sub( ':.*', '', x ) for x in total_results['Model'] ] - - - total_results = total_results.groupby ( ['Model', 'key', 'method', 'dataset']).median() - total_results = total_results.reset_index() - total_results = total_results[ total_results.method.isin(methods)] - - - blob_names = list(set(total_results['dataset'])) - - sig_bestmethods: list[Any] = [] - - for metric in metrics: - max_df = [] - for method in methods: - for bn in blob_names: - b = total_results[ total_results['dataset'] == bn ] - m = b[b['method'] == method] - if len(m) > 0: - if method == 'brier': - mm = m[ m[metric] == min(m[metric])].sort_values('Model') - else: - mm = m[ m[metric] == max(m[metric])].sort_values('Model') - max_df.append(total_results_full[total_results_full['key']==mm['key'].iloc[len(mm)-1]]) - max_df = pd.concat(max_df) - - for method in methods: - for d in blob_names: - a = max_df[ (max_df.dataset == d) & (max_df.method == method) ][metric].apply(float) - b_r = max_df[ (max_df.dataset == d) & (max_df.method == 'random_forest') ][metric].apply(float) - b_x = max_df[ (max_df.dataset == d) & (max_df.method == 'xgb') ][metric].apply(float) - - if metric != 'brier': - if len(a) > 0: - t_statistic, p_value = stats.ttest_ind(a, b_r, alternative='greater') # one-tailed test) - sig_bestmethods.append( [method, 'random_forest', d, metric, round(float(a.median()),3), round(float(b_r.median()),3), - round(float(a.std()), 3), round(float(b_r.std()), 3), round(float(t_statistic),3), round(float(p_value),3) ] ) - if p_value < 0.05: - print(f"RF: {d} : {method} (n={max_df[ (max_df.dataset == d) & (max_df.method == method) ].shape[0]}) : t={round( t_statistic, 3 )}; p={round( p_value, 3 ) }" ) - - t_statistic, p_value = stats.ttest_ind(a, b_x, alternative='greater') # one-tailed test) - sig_bestmethods.append( [method, 'xgb', d, metric, round(float(a.median()),3), round(float(b_x.median()),3), - round(float(a.std()), 3), round(float(b_x.std()), 3), round(float(t_statistic),3), round(float(p_value),3) ] ) - if p_value < 0.05: - print(f"XGB: {d} : {method} (n={max_df[ (max_df.dataset == d) & (max_df.method == method) ].shape[0]}) : t={round( t_statistic, 3 )}; p={round( p_value, 3 ) }" ) - else: - - if len(a) > 0: - t_statistic, p_value = stats.ttest_ind(a, b_r, alternative='less') # one-tailed test) - sig_bestmethods.append( [method, 'random_forest', d, metric, round(float(a.median()),3), round(float(b_r.median()),3), - round(float(a.std()), 3), round(float(b_r.std()), 3), round(float(t_statistic),3), round(float(p_value),3) ] ) - if p_value < 0.05: - print(f"RF: {d} : {method} (n={max_df[ (max_df.dataset == d) & (max_df.method == method) ].shape[0]}) : t={round( t_statistic, 3 )}; p={round( p_value, 3 ) }" ) - - t_statistic, p_value = stats.ttest_ind(a, b_x, alternative='less') # one-tailed test) - sig_bestmethods.append( [method, 'xgb', d, metric, round(float(a.median()),3), round(float(b_x.median()),3), - round(float(a.std()), 3), round(float(b_x.std()), 3), round(float(t_statistic),3), round(float(p_value),3) ] ) - if p_value < 0.05: - print(f"XGB: {d} : {method} (n={max_df[ (max_df.dataset == d) & (max_df.method == method) ].shape[0]}) : t={round( t_statistic, 3 )}; p={round( p_value, 3 ) }" ) - - def reformat_dataset(x): - xs = [ str(i) for i in x] - new_x = [ xs[1], '(' + xs[2] + ',' + xs[3] + ')', '(' + xs[3] + ',' + xs[2] + ')' ] - return ' | '.join( new_x) - - max_df['Blob Config'] = [ reformat_dataset(x) for x in max_df['split'] ] - - max_df = max_df[ ['Blob Config', 'method'] + metrics ] - max_df = max_df.drop_duplicates() - - max_df = max_df.sort_values('method') - max_df = max_df.sort_values('Blob Config') - - plt.figure(figsize=(7,5)) - sns.barplot( data = max_df, y = 'Blob Config', x = metric, hue = 'method', hue_order=methods, errorbar='se', palette=methods_cmap.values() ) - plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.) - sns.despine() - plt.tight_layout() - plt.savefig( os.path.join( dir_output, 'Blob_max_median_'+ re.sub( '\ ', '_', metric ) + '.pdf' ) ) - plt.show() - plt.close() - - sig_bestmethods_df = pd.DataFrame(sig_bestmethods, columns = ['Method', 'Baseline', 'Dataset', 'Metric', 'Median method', 'Median baseline', - 'Std. dev. method', 'Std. dev. baseline', 't statistic', 'p-value']) - sig_bestmethods_df = sig_bestmethods_df[ sig_bestmethods_df['Method'] != sig_bestmethods_df['Baseline']] - sig_bestmethods_df.to_csv( os.path.join( dir_output, 'Blobs_best_stats.csv' ), index = False ) - - return total_results_full, sig_bestmethods_df \ No newline at end of file diff --git a/tutorial/QEnsemble/experiments/Blob_max_median_Accuracy.pdf b/tutorial/QEnsemble/experiments/Blob_max_median_Accuracy.pdf new file mode 100644 index 0000000..2ce74d7 Binary files /dev/null and b/tutorial/QEnsemble/experiments/Blob_max_median_Accuracy.pdf differ diff --git a/tutorial/QEnsemble/experiments/Blob_max_median_F1_Score.pdf b/tutorial/QEnsemble/experiments/Blob_max_median_F1_Score.pdf new file mode 100644 index 0000000..58ee45e Binary files /dev/null and b/tutorial/QEnsemble/experiments/Blob_max_median_F1_Score.pdf differ diff --git a/tutorial/QEnsemble/experiments/Blob_max_median_brier.pdf b/tutorial/QEnsemble/experiments/Blob_max_median_brier.pdf new file mode 100644 index 0000000..24a2a13 Binary files /dev/null and b/tutorial/QEnsemble/experiments/Blob_max_median_brier.pdf differ diff --git a/tutorial/QEnsemble/experiments/Blobs_best_stats.csv b/tutorial/QEnsemble/experiments/Blobs_best_stats.csv new file mode 100644 index 0000000..d527be1 --- /dev/null +++ b/tutorial/QEnsemble/experiments/Blobs_best_stats.csv @@ -0,0 +1,8 @@ +Method,Baseline,Dataset,Metric,Median method,Median baseline,Std. dev. method,Std. dev. baseline,t statistic,p-value +qcosine,random_forest,blob,Accuracy,0.5,0.75,0.244,0.22,-8.097,1.0 +qensemble,random_forest,blob,Accuracy,0.75,0.75,0.375,0.22,-2.851,0.998 +qensemble_random_unitary,random_forest,blob,Accuracy,0.75,0.75,0.195,0.22,0.716,0.237 +qcosine,random_forest,blob,F1 Score,0.5,0.733,0.256,0.293,-6.24,1.0 +qensemble,random_forest,blob,F1 Score,0.733,0.733,0.392,0.293,-1.844,0.967 +qensemble_random_unitary,random_forest,blob,F1 Score,0.733,0.733,0.237,0.293,1.307,0.096 +qcosine,random_forest,blob,brier,0.411,0.233,0.219,0.105,8.476,1.0 diff --git a/tutorial/QEnsemble/experiments/datasets.pkl b/tutorial/QEnsemble/experiments/datasets.pkl new file mode 100644 index 0000000..48ebf4f Binary files /dev/null and b/tutorial/QEnsemble/experiments/datasets.pkl differ diff --git a/tutorial/QEnsemble/experiments/predictions.pkl b/tutorial/QEnsemble/experiments/predictions.pkl new file mode 100644 index 0000000..8ae0924 Binary files /dev/null and b/tutorial/QEnsemble/experiments/predictions.pkl differ diff --git a/tutorial/QEnsemble/helper_functions.py b/tutorial/QEnsemble/helper_functions.py new file mode 100644 index 0000000..847728e --- /dev/null +++ b/tutorial/QEnsemble/helper_functions.py @@ -0,0 +1,916 @@ +""" +Helper functions for Quantum Ensemble Tutorial +=============================================== + +This module provides helper functions for running classical baseline comparisons +in the tutorial notebook. These functions maintain compatibility with the original +tutorial structure while being standalone. +""" + +import os +import re +from collections import Counter +from typing import Any +import numpy as np +import pandas as pd +import pickle +import scipy.stats as stats +import matplotlib.pyplot as plt +import seaborn as sns +from sklearn.preprocessing import MinMaxScaler, StandardScaler +from sklearn.decomposition import PCA +from sklearn.ensemble import RandomForestClassifier +from sklearn.model_selection import RandomizedSearchCV, GridSearchCV, StratifiedKFold +from sklearn.metrics import f1_score + +# Import from QBioCode API +from qbiocode.evaluation import evaluation_metrics + +try: + import umap + UMAP_AVAILABLE = True +except ImportError: + UMAP_AVAILABLE = False + +try: + from xgboost import XGBClassifier + XGB_AVAILABLE = True +except ImportError: + XGB_AVAILABLE = False + +try: + from lazypredict.Supervised import LazyClassifier + LAZY_AVAILABLE = True +except ImportError: + LAZY_AVAILABLE = False + + +def run_random_forest(predictions, dataset, method, dataset_name, seed, test_size, file_predictions, select_features=[], + params={}, pca_embed=False, umap_embed=False, n_features=0): + """ + Run Random Forest classifier with optional hyperparameter search. + + Parameters + ---------- + predictions : dict + Dictionary to store prediction results + dataset : dict + Dictionary containing train/test splits + method : str + Method name (e.g., 'random_forest', 'random_forest_gs') + dataset_name : str + Name of the dataset + seed : int + Random seed for reproducibility + test_size : float + Test set size (not used, kept for compatibility) + file_predictions : str + Path to save predictions + select_features : list, optional + List of features to select (default: []) + params : dict, optional + Fixed hyperparameters (default: {}) + pca_embed : bool, optional + Whether to use PCA embedding (default: False) + umap_embed : bool, optional + Whether to use UMAP embedding (default: False) + n_features : int, optional + Number of features for dimensionality reduction (default: 0) + + Returns + ------- + dict + Updated predictions dictionary + """ + if (dataset_name not in predictions.keys()) or (method not in predictions[dataset_name].keys()): + results_df = pd.DataFrame(columns=['dataset', 'method', 'dataset_params', 'seed', 'n_feature', + 'select_features', 'accuracy', 'brier', 'predictions', 'y_test', 'best_params']) + if dataset_name not in predictions.keys(): + predictions[dataset_name] = {} + predictions[dataset_name][method] = results_df + else: + results_df = predictions[dataset_name][method] + + # Define the hyperparameter grid + param_distributions = { + 'n_estimators': np.arange(100, 1000, 100), + 'max_depth': np.arange(5, 20), + 'min_samples_split': np.arange(2, 10), + 'min_samples_leaf': np.arange(1, 5), + 'max_features': ['sqrt', 'log2'] + } + + for k, v in dataset[dataset_name].items(): + (X_train, X_test, y_train, y_test) = v + if len(select_features) > 0: + X_train = X_train.loc[:, select_features] + X_test = X_test.loc[:, select_features] + + scaler = MinMaxScaler() + X_train = scaler.fit_transform(X_train) + X_test = scaler.transform(X_test) + + embed = 'none' + + if pca_embed: + embed = 'pca' + embedder = PCA(n_features) + X_train = embedder.fit_transform(X_train) + X_test = embedder.transform(X_test) + elif umap_embed: + if not UMAP_AVAILABLE: + raise ImportError("UMAP not available. Install with: pip install umap-learn") + embed = 'umap' + reducer = umap.UMAP(n_features) + X_train = reducer.fit_transform(X_train) + X_test = reducer.transform(X_test) + + if len(params) > 0: + # Initialize the Random Forest Classifier + rf = RandomForestClassifier(random_state=seed, + n_estimators=params['n_estimators'], + min_samples_split=params['min_samples_split'], + max_features=params['max_features'], + max_depth=params['max_depth'], + min_samples_leaf=params['min_samples_leaf'] + ) + + rf.fit(X_train, y_train) + preds = rf.predict_proba(X_test) + else: + # Initialize the Random Forest Classifier + rf = RandomForestClassifier(random_state=seed) + + # Initialize RandomizedSearchCV + rf_random = RandomizedSearchCV(estimator=rf, + param_distributions=param_distributions, + n_iter=10, + cv=3, + random_state=seed, + n_jobs=-1) + rf_random.fit(X_train, y_train) + preds = rf_random.predict_proba(X_test) + params = rf_random.best_params_ + + a, b = evaluation_metrics(preds, y_test, save=False) + + res = pd.DataFrame([dataset_name, method, k, seed, X_train.shape[1], ','.join(select_features), a, b, preds, y_test, params], + index=['dataset', 'method', 'dataset_params', 'seed', 'n_feature', 'select_features', 'accuracy', 'brier', 'predictions', 'y_test', 'best_params']).transpose() + results_df = pd.concat([results_df, res]) + + predictions[dataset_name][method] = results_df + + # save + pickle.dump(predictions, open(file_predictions, 'wb')) + + return predictions + + +def run_xgboost(predictions, dataset, method, dataset_name, seed, test_size, file_predictions, select_features=[], + params={}, pca_embed=False, umap_embed=False, n_features=0): + """ + Run XGBoost classifier with optional hyperparameter search. + + Parameters + ---------- + predictions : dict + Dictionary to store prediction results + dataset : dict + Dictionary containing train/test splits + method : str + Method name (e.g., 'xgb', 'xgb_gs') + dataset_name : str + Name of the dataset + seed : int + Random seed for reproducibility + test_size : float + Test set size (not used, kept for compatibility) + file_predictions : str + Path to save predictions + select_features : list, optional + List of features to select (default: []) + params : dict, optional + Fixed hyperparameters (default: {}) + pca_embed : bool, optional + Whether to use PCA embedding (default: False) + umap_embed : bool, optional + Whether to use UMAP embedding (default: False) + n_features : int, optional + Number of features for dimensionality reduction (default: 0) + + Returns + ------- + dict + Updated predictions dictionary + """ + if not XGB_AVAILABLE: + raise ImportError("XGBoost not available. Install with: pip install xgboost") + + if (dataset_name not in predictions.keys()) or (method not in predictions[dataset_name].keys()): + results_df = pd.DataFrame(columns=['dataset', 'method', 'dataset_params', 'seed', 'n_feature', + 'select_features', 'accuracy', 'brier', 'predictions', 'y_test', 'best_params']) + if dataset_name not in predictions.keys(): + predictions[dataset_name] = {} + predictions[dataset_name][method] = results_df + else: + results_df = predictions[dataset_name][method] + + for k, v in dataset[dataset_name].items(): + (X_train, X_test, y_train, y_test) = v + if len(select_features) > 0: + X_train = X_train.loc[:, select_features] + X_test = X_test.loc[:, select_features] + + scaler = StandardScaler() + X_train = scaler.fit_transform(X_train) + X_test = scaler.transform(X_test) + + embed = 'none' + + if pca_embed: + embed = 'pca' + embedder = PCA(n_features) + X_train = embedder.fit_transform(X_train) + X_test = embedder.transform(X_test) + elif umap_embed: + if not UMAP_AVAILABLE: + raise ImportError("UMAP not available. Install with: pip install umap-learn") + embed = 'umap' + reducer = umap.UMAP(n_features) + X_train = reducer.fit_transform(X_train) + X_test = reducer.transform(X_test) + + ##XGB + if len(params) > 0: + # Initialize XGB + xgb = XGBClassifier( + random_state=seed, + n_estimators=params['n_estimators'], + max_depth=params['max_depth'], + learning_rate=params['learning_rate'], + subsample=params['subsample'], + colsample_bytree=params['colsample_bytree'], + min_child_weight=params['min_child_weight'], + eval_metric='logloss' + ) + xgb.fit(X_train, y_train) + preds = xgb.predict_proba(X_test) + else: + xgb = XGBClassifier( + random_state=seed, + eval_metric='logloss' + ) + + cv = StratifiedKFold(n_splits=3, shuffle=True, random_state=seed) + + param_grid = { + 'n_estimators': [100, 200], + 'max_depth': [3, 5, 7], + 'learning_rate': [0.01, 0.1, 0.2], + 'subsample': [0.7, 0.8, 1.0], + 'colsample_bytree': [0.7, 0.8, 1.0], + 'min_child_weight': [1, 3, 5] + } + + xgb_grid = GridSearchCV( + estimator=xgb, + param_grid=param_grid, + scoring='f1_weighted', + n_jobs=-1, + cv=cv, + verbose=1 + ) + + xgb_grid.fit(X_train, y_train) + preds = xgb_grid.predict_proba(X_test) + params = xgb_grid.best_params_ + + a, b = evaluation_metrics(preds, y_test, save=False) + + res = pd.DataFrame([dataset_name, method, k, seed, X_train.shape[1], ','.join(select_features), a, b, preds, y_test, params], + index=['dataset', 'method', 'dataset_params', 'seed', 'n_feature', 'select_features', 'accuracy', 'brier', 'predictions', 'y_test', 'best_params']).transpose() + results_df = pd.concat([results_df, res]) + + predictions[dataset_name][method] = results_df + + # save + pickle.dump(predictions, open(file_predictions, 'wb')) + + return predictions + + +def run_lazy_predict(predictions, dataset, method, dataset_name, seed, test_size, file_predictions, select_features=[]): + """ + Run LazyPredict for automated model comparison. + + Parameters + ---------- + predictions : dict + Dictionary to store prediction results + dataset : dict + Dictionary containing train/test splits + method : str + Method name + dataset_name : str + Name of the dataset + seed : int + Random seed for reproducibility + test_size : float + Test set size (not used, kept for compatibility) + file_predictions : str + Path to save predictions + select_features : list, optional + List of features to select (default: []) + + Returns + ------- + dict + Updated predictions dictionary + """ + if not LAZY_AVAILABLE: + raise ImportError("LazyPredict not available. Install with: pip install lazypredict") + + if dataset_name not in predictions.keys(): + predictions[dataset_name] = {} + predictions[dataset_name][method] = {} + + for k, v in dataset[dataset_name].items(): + (X_train, X_test, y_train, y_test) = v + if len(select_features) > 0: + X_train = X_train.loc[:, select_features] + X_test = X_test.loc[:, select_features] + + scaler = MinMaxScaler() + X_train = scaler.fit_transform(X_train) + X_test = scaler.transform(X_test) + clf = LazyClassifier(verbose=0, ignore_warnings=True, custom_metric=None) + models, preds = clf.fit(X_train, X_test, y_train, y_test) + + predictions[dataset_name][method][k] = {} + predictions[dataset_name][method][k]['seed'] = seed + predictions[dataset_name][method][k]['models'] = models + predictions[dataset_name][method][k]['preds'] = preds + predictions[dataset_name][method][k]['y_test'] = y_test + predictions[dataset_name][method][k]['select_features'] = select_features + + # save + pickle.dump(predictions, open(file_predictions, 'wb')) + + return predictions + + +def run_quantum_cosine(predictions, dataset, method, dataset_name, seed, test_size, file_predictions, n_features, + n_trains, n_shots, pca_embed=False, umap_embed=False, select_features=[]): + """ + Run quantum cosine classifier experiments. + + This function runs a simple quantum cosine similarity classifier across + multiple parameter configurations. It uses the SWAP test to measure + cosine similarity between training and test quantum states. + + Parameters + ---------- + predictions : dict + Dictionary to store prediction results + dataset : dict + Dictionary containing dataset splits + method : str + Method name for results tracking + dataset_name : str + Name of the dataset + seed : int + Random seed for reproducibility + test_size : float + Fraction of data for testing + file_predictions : str + Path to save predictions + n_features : list of int + List of feature counts to test + n_trains : list of int + List of training sample sizes to test + n_shots : int + Number of measurement shots + pca_embed : bool, optional + Use PCA for dimensionality reduction (default: False) + umap_embed : bool, optional + Use UMAP for dimensionality reduction (default: False) + select_features : list, optional + Specific features to select (default: []) + + Returns + ------- + dict + Updated predictions dictionary with results + """ + from qbiocode.learning.compute_qensemble import build_cosine_classifier + from qbiocode.utils import normalize_data, label_to_array, execute_circuit, retrieve_probabilities + + epsilon = 1e-15 + if (dataset_name not in predictions.keys()) or (method not in predictions[dataset_name].keys()): + results_df = pd.DataFrame() + if dataset_name not in predictions.keys(): + predictions[dataset_name] = {} + predictions[dataset_name][method] = results_df + else: + results_df = predictions[dataset_name][method] + + for k, v in dataset[dataset_name].items(): + for f in n_features: + (X_train, X_test, y_train, y_test) = v + if len(select_features) > 0: + X_train = X_train.loc[:, select_features] + X_test = X_test.loc[:, select_features] + + Y_vector_train = label_to_array(y_train) + Y_vector_test = label_to_array(y_test) + test_size = Y_vector_test.shape[0] + train_size = Y_vector_train.shape[0] + scaler = MinMaxScaler() + X_train = scaler.fit_transform(X_train) + X_test = scaler.transform(X_test) + embed = 'none' + + # adding epsilon to avoid zero division + X_train = X_train + epsilon + X_test = X_test + epsilon + + if pca_embed: + embed = 'pca' + embedder = PCA(f) + X_train = embedder.fit_transform(X_train) + X_test = embedder.transform(X_test) + elif umap_embed: + if not UMAP_AVAILABLE: + raise ImportError("UMAP not available. Install with: pip install umap-learn") + embed = 'umap' + reducer = umap.UMAP(f) + X_train = reducer.fit_transform(X_train) + X_test = reducer.transform(X_test) + + for n_train in n_trains: + preds = [] + + if (len(results_df) == 0) or (len(results_df[(results_df['dataset'] == dataset_name) & + (results_df['method'] == method) & + (results_df['dataset_params'] == k) & + (results_df['n_feature'] == f) & + (results_df['embed'] == embed) & + (results_df['select_features'] == ','.join(select_features)) & + (results_df['n_train'] == n_train) + ]) == 0): + + for x_test, y_ts in zip(X_test, Y_vector_test): + ix = np.random.choice(train_size, n_train)[0] + x_train = X_train[ix] + x_tr = normalize_data(x_train) + y_tr = Y_vector_train[ix] + x_ts = normalize_data(x_test) + qc = build_cosine_classifier(x_tr, x_ts, y_tr) + + r = execute_circuit(qc, n_shots=n_shots) + + if '0' not in r.keys(): + r['0'] = 0 + elif '1' not in r.keys(): + r['1'] = 0 + + preds.append(retrieve_probabilities(r)) + + a, b = evaluation_metrics(np.array(preds), y_test, save=False) + res = pd.DataFrame([dataset_name, method, k, seed, X_train.shape[1], qc.num_qubits, n_train, embed, ','.join(select_features), a, b, preds, y_test], + index=['dataset', 'method', 'dataset_params', 'seed', 'n_feature', 'qubits', 'n_train', 'embed', 'select_features', 'accuracy', 'brier', 'predictions', 'y_test']).transpose() + results_df = pd.concat([results_df, res]) + + predictions[dataset_name][method] = results_df + + # save + pickle.dump(predictions, open(file_predictions, 'wb')) + + return predictions + + +def run_quantum_ensemble(predictions, dataset, method, dataset_name, seed, test_size, file_predictions, ds, n_swaps, n_features, + n_trains, n_shots, pca_embed=False, umap_embed=False, device='CPU', instance='', random_unitary=False, select_features=[]): + """ + Run quantum ensemble experiments across multiple parameter configurations. + + This is a comprehensive workflow function that performs a grid search over + quantum ensemble hyperparameters (d, n_swap, n_features, n_train) and + evaluates performance on test data. Results are automatically saved to + disk after each configuration. + + Parameters + ---------- + predictions : dict + Dictionary to store prediction results + dataset : dict + Dictionary containing dataset splits + method : str + Method name for results tracking + dataset_name : str + Name of the dataset being processed + seed : int + Random seed for reproducibility + test_size : float + Fraction of data for testing + file_predictions : str + Path to pickle file for saving predictions + ds : list of int + List of ensemble depths (control qubits) to test + n_swaps : list of int + List of swap operation counts to test + n_features : list of int + List of feature counts to test (must be powers of 2) + n_trains : list of int + List of training sample sizes to test + n_shots : int + Number of measurement shots per circuit + pca_embed : bool, optional + Use PCA for dimensionality reduction (default: False) + umap_embed : bool, optional + Use UMAP for dimensionality reduction (default: False) + device : str, optional + Execution device: 'CPU' or 'GPU' (default: 'CPU') + instance : str, optional + IBM Quantum instance string (default: '') + random_unitary : bool, optional + Use random unitary ensemble variant (default: False) + select_features : list, optional + Specific feature names to select (default: []) + + Returns + ------- + dict + Updated predictions dictionary with new results + """ + from qbiocode.learning import compute_qensemble + + if (dataset_name not in predictions.keys()) or (method not in predictions[dataset_name].keys()): + results_df = pd.DataFrame() + if dataset_name not in predictions.keys(): + predictions[dataset_name] = {} + predictions[dataset_name][method] = results_df + else: + results_df = predictions[dataset_name][method] + + for k, v in dataset[dataset_name].items(): + for f in n_features: + (X_train_orig, X_test_orig, y_train, y_test) = v + + X_train = X_train_orig.copy() + X_test = X_test_orig.copy() + + if len(select_features) > 0: + X_train = X_train.loc[:, select_features] + X_test = X_test.loc[:, select_features] + + scaler = MinMaxScaler() + X_train = pd.DataFrame(scaler.fit_transform(X_train), index=X_train.index, columns=X_train.columns) + X_test = pd.DataFrame(scaler.transform(X_test), index=X_test.index, columns=X_test.columns) + embed = 'none' + + if pca_embed: + embed = 'pca' + embedder = PCA(f) + X_train = embedder.fit_transform(X_train) + X_test = embedder.transform(X_test) + elif umap_embed: + if not UMAP_AVAILABLE: + raise ImportError("UMAP not available. Install with: pip install umap-learn") + embed = 'umap' + reducer = umap.UMAP(f) + X_train = reducer.fit_transform(X_train) + X_test = reducer.transform(X_test) + else: + vr = X_train.apply(np.var, axis=0) + vr = vr.sort_values(ascending=False) + X_train = X_train[list(vr[0:f].index)].to_numpy() + X_test = X_test[list(vr[0:f].index)].to_numpy() + + for d in ds: + for n_train in n_trains: + if n_train > d: + for n_swap in n_swaps: + if (len(results_df) == 0) or (len(results_df[(results_df['dataset'] == dataset_name) & + (results_df['method'] == method) & + (results_df['dataset_params'] == k) & + (results_df['n_feature'] == f) & + (results_df['n_swap'] == n_swap) & + (results_df['n_train'] == n_train) & + (results_df['embed'] == embed) & + (results_df['select_features'] == ','.join(select_features)) & + (results_df['d'] == d) + ]) == 0): + + # Use QBioCode API + ensemble_method = 'random_unitary' if random_unitary else 'swap' + args = {'grid_search': False} # Required by modeleval + + res = compute_qensemble( + X_train, X_test, y_train, y_test, + args=args, + model='QEnsemble', + data_key=str(k), + n_train=n_train, + n_swap=n_swap, + d=d, + mode="balanced", + ensemble_method=ensemble_method, + n_shots=n_shots, + seed=seed, + device=device, + verbose=False + ) + + # Extract predictions and y_test from modeleval result + # modeleval returns DataFrame with columns like 'y_test_QEnsemble', 'y_predicted_QEnsemble', 'results_QEnsemble' + y_test_col = f'y_test_{res.columns[0].split("_", 2)[-1]}' if len(res.columns) > 0 else 'y_test_QEnsemble' + y_pred_col = f'y_predicted_{res.columns[0].split("_", 2)[-1]}' if len(res.columns) > 0 else 'y_predicted_QEnsemble' + results_col = f'results_{res.columns[0].split("_", 2)[-1]}' if len(res.columns) > 0 else 'results_QEnsemble' + + # Find the actual column names + y_test_col = [col for col in res.columns if col.startswith('y_test_')][0] + y_pred_col = [col for col in res.columns if col.startswith('y_predicted_')][0] + results_col = [col for col in res.columns if col.startswith('results_')][0] + + # Extract values + y_test_val = res[y_test_col].iloc[0] + y_pred_val = res[y_pred_col].iloc[0] + results_dict = res[results_col].iloc[0] + + # Convert result to DataFrame format expected by notebook + res_df = pd.DataFrame({ + 'dataset': [dataset_name], + 'method': [method], + 'dataset_params': [k], + 'n_feature': [f], + 'n_swap': [n_swap], + 'n_train': [n_train], + 'embed': [embed], + 'select_features': [','.join(select_features)], + 'd': [d], + 'seed': [seed], + 'accuracy': [results_dict.get('accuracy', np.nan)], + 'brier': [results_dict.get('brier_score', np.nan)], + 'qubits': [results_dict.get('Model_Parameters', {}).get('n_qubits', np.nan)], + 'runtime': [results_dict.get('time', np.nan)], + 'y_test': [y_test_val], + 'predictions': [y_pred_val] + }) + + results_df = pd.concat([results_df, res_df]) + + predictions[dataset_name][method] = results_df + + # save + pickle.dump(predictions, open(file_predictions, 'wb')) + + return predictions + + +# Made with Bob + +import os +import re +from collections import Counter +from typing import Any +import scipy.stats as stats +import matplotlib.pyplot as plt +import seaborn as sns + + +def calculate_number_predicted_classes(predictions): + """Calculate the number of unique predicted classes.""" + # Convert to numpy array if needed + preds = np.asarray(predictions) + + # If predictions are probabilities (2D array), convert to class labels + if preds.ndim > 1: + preds = np.argmax(preds, axis=1) + + # Flatten and get unique values + preds = preds.flatten() + return len(np.unique(preds)) + + +def post_process_results(predictions, dir_output, datasets, metrics=['Accuracy', 'F1 Score', 'brier']): + """ + Post-process and visualize results from quantum ensemble experiments. + + Parameters + ---------- + predictions : dict + Dictionary containing prediction results for all methods and datasets + dir_output : str + Directory path for saving output files + datasets : list + List of dataset names to process + metrics : list, optional + List of metrics to evaluate (default: ['Accuracy', 'F1 Score', 'brier']) + + Returns + ------- + total_results_full : pd.DataFrame + Full results DataFrame with all experiments + sig_bestmethods_df : pd.DataFrame + Statistical significance results comparing methods + """ + total_results = pd.DataFrame() + for dataset_name in datasets: + print(f"Dataset: {dataset_name}") + methods = list(predictions[dataset_name].keys()) + + all_results = pd.DataFrame() + + for method in methods: + if method not in ['random_forest_gs', 'xgb_gs']: # Ignore the parameter search experiments + print(f"Method: {method}") + results_df = predictions[dataset_name][method] + results_df = results_df.reset_index(drop=True) + + results_df['num_pred_classes'] = [calculate_number_predicted_classes(x) for x in results_df['predictions']] + + results_df.columns = [re.sub('dataset_params', 'split', re.sub('accuracy', 'Accuracy', re.sub('method', 'Model', x))) for x in results_df.columns] + + # Calculate F1 scores + f1_scores = [] + for idx, row in results_df.iterrows(): + # Get y_true and predictions + y_true = np.asarray(row.y_test).flatten() + y_pred = np.asarray(row.predictions) + + # If predictions are probabilities (2D array), convert to class labels + if y_pred.ndim > 1: + y_pred = np.argmax(y_pred, axis=1) + else: + y_pred = y_pred.flatten() + + # Validate that arrays have the same length + if len(y_true) != len(y_pred): + print(f"Warning: Length mismatch at index {idx} - y_true: {len(y_true)}, y_pred: {len(y_pred)}") + print(f" y_true shape: {y_true.shape}, y_pred shape: {y_pred.shape}") + f1_scores.append(np.nan) + continue + + f1_scores.append(f1_score(y_true, y_pred, average='weighted')) + results_df['F1 Score'] = f1_scores + + if method == 'qcosine': + results_df = results_df[(results_df['n_train']==1) & (results_df['n_feature']==2)] + results_df['Model'] = [':'.join([row['Model'], str(row['n_train']), str(row['n_feature']), row['embed']]) for idx, row in results_df.iterrows()] + elif method in ['random_forest', 'xgb']: + results_df['Model'] = [':'.join([row['Model'], str(row['n_feature'])]) for idx, row in results_df.iterrows()] + else: + results_df['Model'] = [':'.join([row['Model'], str(row['d']), str(row['n_train']), str(row['n_swap']), str(row['n_feature']), row['embed']]) for idx, row in results_df.iterrows()] + + all_results = pd.concat([all_results, results_df]) + + if method in ['random_forest', 'xgb']: + results_df = results_df.drop(['predictions', 'y_test', 'best_params', 'select_features'], axis=1) + else: + results_df = results_df.drop(['predictions', 'y_test', 'embed', 'select_features'], axis=1) + + total_results = pd.concat([total_results, all_results]) + + total_results_full = total_results.reset_index().copy() + + methods = ['random_forest', 'xgb', 'qcosine', 'qensemble', 'qensemble_random_unitary'] + methods_cmap = dict(zip(methods[0:2], sns.color_palette('Greys', n_colors=2))) + methods_cmap.update(dict(zip(methods[2:], sns.color_palette(n_colors=len(methods[2:]))))) + + total_results_full['key'] = ['-'.join([row['Model'], row['dataset']]) for idx, row in total_results_full.iterrows()] + total_results_full['method'] = [re.sub(':.*', '', x) for x in total_results['Model']] + + total_results = total_results_full[['Model', 'Accuracy', 'F1 Score', 'brier', 'dataset', 'split', 'num_pred_classes']].copy() + # Extract first element from split if it's a list/tuple, otherwise keep as is + def extract_split_value(x): + try: + if isinstance(x, (list, tuple)) and len(x) > 0: + return x[0] + elif isinstance(x, pd.Series): + if len(x) > 0: + try: + return x.iloc[0] + except (IndexError, KeyError): + return x.values[0] if len(x.values) > 0 else x + else: + return x + elif isinstance(x, np.ndarray) and x.size > 0: + return x.flat[0] + else: + return x + except (IndexError, KeyError, AttributeError, TypeError): + return x + + try: + total_results['split'] = total_results['split'].apply(extract_split_value) + except Exception as e: + print(f"Warning: Could not process split column with apply: {e}") + # If apply fails, try manual iteration + try: + split_values = [] + for val in total_results['split']: + split_values.append(extract_split_value(val)) + total_results['split'] = split_values + except Exception as e2: + print(f"Warning: Could not process split column manually: {e2}") + # Last resort: convert to string + try: + # Use .values to avoid pandas indexing issues + total_results['split'] = [str(x) for x in total_results['split'].values] + except Exception as e3: + print(f"Warning: Could not convert split to string: {e3}") + # If even that fails, just use a placeholder + total_results['split'] = ['unknown'] * len(total_results) + + total_results['key'] = ['-'.join([row['Model'], row['dataset']]) for idx, row in total_results.iterrows()] + total_results['method'] = [re.sub(':.*', '', x) for x in total_results['Model']] + + total_results = total_results.groupby(['Model', 'key', 'method', 'dataset']).median() + total_results = total_results.reset_index() + total_results = total_results[total_results.method.isin(methods)] + + blob_names = list(set(total_results['dataset'])) + + sig_bestmethods: list[Any] = [] + + for metric in metrics: + max_df = [] + for method in methods: + for bn in blob_names: + b = total_results[total_results['dataset'] == bn] + m = b[b['method'] == method] + if len(m) > 0: + if metric == 'brier': + mm = m[m[metric] == min(m[metric])].sort_values('Model') + else: + mm = m[m[metric] == max(m[metric])].sort_values('Model') + # Only append if mm is not empty + if len(mm) > 0: + max_df.append(total_results_full[total_results_full['key']==mm['key'].iloc[-1]]) + max_df = pd.concat(max_df) + + for method in methods: + for d in blob_names: + a = max_df[(max_df.dataset == d) & (max_df.method == method)][metric].apply(float) + b_r = max_df[(max_df.dataset == d) & (max_df.method == 'random_forest')][metric].apply(float) + b_x = max_df[(max_df.dataset == d) & (max_df.method == 'xgb')][metric].apply(float) + + if metric != 'brier': + if len(a) > 1 and len(b_r) > 1: # Need at least 2 samples for t-test + t_statistic, p_value = stats.ttest_ind(a, b_r, alternative='greater') + sig_bestmethods.append([method, 'random_forest', d, metric, round(float(a.median()),3), round(float(b_r.median()),3), + round(float(a.std()), 3), round(float(b_r.std()), 3), round(float(t_statistic),3), round(float(p_value),3)]) + if p_value < 0.05: + print(f"RF: {d} : {method} (n={max_df[(max_df.dataset == d) & (max_df.method == method)].shape[0]}) : t={round(t_statistic, 3)}; p={round(p_value, 3)}") + + if len(a) > 1 and len(b_x) > 1: # Need at least 2 samples for t-test + t_statistic, p_value = stats.ttest_ind(a, b_x, alternative='greater') + sig_bestmethods.append([method, 'xgb', d, metric, round(float(a.median()),3), round(float(b_x.median()),3), + round(float(a.std()), 3), round(float(b_x.std()), 3), round(float(t_statistic),3), round(float(p_value),3)]) + if p_value < 0.05: + print(f"XGB: {d} : {method} (n={max_df[(max_df.dataset == d) & (max_df.method == method)].shape[0]}) : t={round(t_statistic, 3)}; p={round(p_value, 3)}") + else: + if len(a) > 1 and len(b_r) > 1: # Need at least 2 samples for t-test + t_statistic, p_value = stats.ttest_ind(a, b_r, alternative='less') + sig_bestmethods.append([method, 'random_forest', d, metric, round(float(a.median()),3), round(float(b_r.median()),3), + round(float(a.std()), 3), round(float(b_r.std()), 3), round(float(t_statistic),3), round(float(p_value),3)]) + if p_value < 0.05: + print(f"RF: {d} : {method} (n={max_df[(max_df.dataset == d) & (max_df.method == method)].shape[0]}) : t={round(t_statistic, 3)}; p={round(p_value, 3)}") + + if len(a) > 1 and len(b_x) > 1: # Need at least 2 samples for t-test + t_statistic, p_value = stats.ttest_ind(a, b_x, alternative='less') + sig_bestmethods.append([method, 'xgb', d, metric, round(float(a.median()),3), round(float(b_x.median()),3), + round(float(a.std()), 3), round(float(b_x.std()), 3), round(float(t_statistic),3), round(float(p_value),3)]) + if p_value < 0.05: + print(f"XGB: {d} : {method} (n={max_df[(max_df.dataset == d) & (max_df.method == method)].shape[0]}) : t={round(t_statistic, 3)}; p={round(p_value, 3)}") + + def reformat_dataset(x): + xs = [str(i) for i in x] + new_x = [xs[1], '(' + xs[2] + ',' + xs[3] + ')', '(' + xs[3] + ',' + xs[2] + ')'] + return ' | '.join(new_x) + + max_df['Blob Config'] = [reformat_dataset(x) for x in max_df['split']] + + max_df = max_df[['Blob Config', 'method'] + metrics] + max_df = max_df.drop_duplicates() + + max_df = max_df.sort_values('method') + max_df = max_df.sort_values('Blob Config') + + plt.figure(figsize=(7,5)) + sns.barplot(data=max_df, y='Blob Config', x=metric, hue='method', hue_order=methods, errorbar='se', palette=methods_cmap.values()) + plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.) + sns.despine() + plt.tight_layout() + plt.savefig(os.path.join(dir_output, 'Blob_max_median_'+ re.sub('\ ', '_', metric) + '.pdf')) + plt.show() + plt.close() + + sig_bestmethods_df = pd.DataFrame(sig_bestmethods, columns=['Method', 'Baseline', 'Dataset', 'Metric', 'Median method', 'Median baseline', + 'Std. dev. method', 'Std. dev. baseline', 't statistic', 'p-value']) + sig_bestmethods_df = sig_bestmethods_df[sig_bestmethods_df['Method'] != sig_bestmethods_df['Baseline']] + sig_bestmethods_df.to_csv(os.path.join(dir_output, 'Blobs_best_stats.csv'), index=False) + + return total_results_full, sig_bestmethods_df + diff --git a/tutorial/QEnsemble/modeling.py b/tutorial/QEnsemble/modeling.py deleted file mode 100644 index 3347ed7..0000000 --- a/tutorial/QEnsemble/modeling.py +++ /dev/null @@ -1,564 +0,0 @@ -""" -Quantum Ensemble Modeling Module - -This module provides quantum circuit implementations for ensemble learning, -including cosine classifiers and quantum ensemble methods using controlled -swap operations. -""" -import numpy as np - -from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister -from qiskit.compiler import transpile -from qiskit_aer import AerSimulator -import Utils - - -def cos_classifier_legacy(train, test, label_train, printing=False): - """ - Legacy cosine classifier for single qubit data. - - This function implements a quantum cosine similarity classifier using the - controlled-SWAP test (also known as the SWAP test). The circuit measures - the overlap between quantum states encoding the training and test data, - which corresponds to their cosine similarity. - - Algorithm: - 1. Initialize training data, test data, and training label in separate qubits - 2. Apply Hadamard gate to test label qubit (creates superposition) - 3. Perform controlled-SWAP between training and test data qubits - 4. Apply Hadamard gate to test label qubit (interference step) - 5. Apply CNOT gate controlled by training label - 6. Measure test label qubit - - The measurement probability P(0) = 1/2 + 1/2 * ||^2, which - encodes the cosine similarity between the data points. - - Parameters - ---------- - train : array-like, shape (2,) - Training data point as normalized 2D vector [x, y] - test : array-like, shape (2,) - Test data point as normalized 2D vector [x, y] - label_train : array-like, shape (2,) - Training label as normalized vector [p0, p1] where p0 + p1 = 1 - printing : bool, optional - If True, print the quantum circuit diagram (default: False) - - Returns - ------- - QuantumCircuit - Quantum circuit with 4 qubits and 1 classical bit implementing - the cosine classifier - - Notes - ----- - This is a legacy function for 2D data only. For multi-dimensional data, - use `cos_classifier()` instead. - - Examples - -------- - >>> train = np.array([1.0, 0.0]) - >>> test = np.array([0.707, 0.707]) - >>> label = np.array([1.0, 0.0]) # Class 0 - >>> qc = cos_classifier_legacy(train, test, label) - >>> result = exec_simulator(qc, n_shots=1024) - """ - c = ClassicalRegister(1, 'c') - x_train = QuantumRegister(1, 'x^{(i)}') - x_test = QuantumRegister(1, 'x^{(test)}') - y_train = QuantumRegister(1, 'y^{(i)}') - y_test = QuantumRegister(1, 'y^{(test)}') - qc = QuantumCircuit(x_train, x_test, y_train, y_test, c) - qc.initialize(train, [x_train[0]]) - qc.initialize(test, [x_test[0]]) - qc.initialize(label_train, [y_train[0]]) - qc.barrier() - qc.h(y_test) - qc.cswap(y_test, x_train, x_test) - qc.h(y_test) - qc.barrier() - qc.cx(y_train, y_test) - qc.measure(y_test, c) - if printing: - print(qc) - return qc - -def cos_classifier(train, test, label_train, printing=False): - """ - Cosine classifier for multi-qubit data. - - Implements a quantum cosine similarity classifier using controlled swap - operations and Hadamard gates to measure similarity between training - and test data points. This is the generalized version that works with - data of arbitrary dimensionality (must be power of 2). - - Algorithm: - 1. Encode training data, test data, and label into quantum states - 2. Apply Hadamard to test label qubit (superposition) - 3. Controlled-SWAP between all training and test data qubits - 4. Apply Hadamard to test label qubit (interference) - 5. CNOT from training label to test label - 6. Measure test label qubit - - The circuit computes P(0) = 1/2 + 1/2 * ||^2, encoding - the squared cosine similarity between the quantum states. - - Parameters - ---------- - train : array-like, shape (2^n,) - Training data point as normalized vector where length is power of 2 - test : array-like, shape (2^n,) - Test data point as normalized vector where length is power of 2 - label_train : array-like, shape (2,) - Training label as normalized probability vector [p0, p1] - printing : bool, optional - If True, print the quantum circuit diagram (default: False) - - Returns - ------- - QuantumCircuit - Quantum circuit implementing the multi-qubit cosine classifier - - Notes - ----- - - Data dimensionality must be a power of 2 (2, 4, 8, 16, ...) - - All input vectors must be normalized (L2 norm = 1) - - Uses log2(len(train)) qubits per data point - - Examples - -------- - >>> train = np.array([0.5, 0.5, 0.5, 0.5]) # 4D normalized data - >>> test = np.array([0.7, 0.3, 0.5, 0.4]) - >>> test = test / np.linalg.norm(test) # Normalize - >>> label = np.array([1.0, 0.0]) # Class 0 - >>> qc = cos_classifier(train, test, label) - >>> result = exec_simulator(qc, n_shots=8192) - """ - n_obs = len(train) - qubits_per = int(np.log2(len(train))) - n_obs_qubits = qubits_per * n_obs - n_test = len(test) - n_test_qubits = qubits_per * n_test - - c = ClassicalRegister(1, 'c') - x_train = QuantumRegister(qubits_per, 'x_{b}') - x_test = QuantumRegister(qubits_per, 'x^{(test)}') - y_train = QuantumRegister(1, 'y_{b}') - y_test = QuantumRegister(1, 'y^{(test)}') - qc = QuantumCircuit(x_train, x_test, y_train, y_test, c) - - qc.initialize(train, [x_train]) - qc.initialize(test, [x_test]) - qc.initialize(label_train, [y_train]) - qc.barrier() - qc.h(y_test) - qc.cswap(y_test, x_train, x_test) - qc.h(y_test) - qc.barrier() - qc.cx(y_train, y_test) - qc.measure(y_test, c) - if printing: - print(qc) - return qc - - -def state_prep(x): - """ - Prepare a quantum state from classical data using unitary simulation. - - This function extracts the unitary matrix that prepares a quantum state - from classical data. It uses Qiskit's unitary simulator to obtain the - exact unitary transformation that would initialize the given state. - - The unitary can be used in circuits where explicit state initialization - is not available or when the unitary representation is needed for - further manipulation. - - Parameters - ---------- - x : array-like, shape (2^n,) - Classical data vector to encode as quantum state. Will be normalized - internally if not already normalized. - - Returns - ------- - ndarray, shape (2, 2) - Unitary matrix (2x2 for single qubit) representing the state - preparation operation U such that U|0⟩ = |x⟩ - - Notes - ----- - - Uses AerSimulator with unitary method - - Input is automatically normalized using Utils.normalize_custom() - - Currently supports single-qubit states only - - Examples - -------- - >>> x = np.array([0.6, 0.8]) - >>> U = state_prep(x) - >>> print(U.shape) - (2, 2) - """ - backend = AerSimulator(method="unitary") - x = Utils.normalize_custom(x) - qreg = QuantumRegister(1) - qc = QuantumCircuit(qreg) - qc.prepare_state(x, [qreg]) - tqc = transpile(qc, backend) - tqc.save_unitary() - result = backend.run([tqc]).result() - U = result.get_unitary(qc) - return U - - -def quantum_cosine_classifier(train, test, label_train): - """ - Quantum cosine classifier using unitary state preparation. - - This function creates a quantum circuit that implements cosine similarity - classification using unitary gates for state preparation instead of - direct initialization. This approach is useful when working with hardware - that doesn't support arbitrary state initialization or when the unitary - representation is preferred. - - The circuit uses the state_prep() function to extract unitaries for - encoding data and labels, then applies the same SWAP test algorithm - as the standard cosine classifier. - - Parameters - ---------- - train : array-like, shape (n_samples, n_features) - Training data points as 2D array where each row is a sample - test : array-like, shape (n_samples, n_features) - Test data points as 2D array where each row is a sample - label_train : array-like, shape (n_samples,) - Training labels corresponding to training data - - Returns - ------- - QuantumCircuit - Quantum circuit implementing the classifier with explicit unitary - gates for state preparation - - Notes - ----- - - Uses unitary gates labeled as $S_x$ for data and $S_y$ for labels - - More hardware-compatible than direct state initialization - - May have different gate decomposition than initialize() method - - Examples - -------- - >>> train = np.array([[0.7, 0.7], [0.6, 0.8]]) - >>> test = np.array([[0.8, 0.6]]) - >>> labels = np.array([0, 1]) - >>> qc = quantum_cosine_classifier(train, test, labels) - """ - n_obs = len(train) - qubits_per = int(np.log2(train.shape[1])) - n_obs_qubits = qubits_per * n_obs - n_test = len(test) - n_test_qubits = qubits_per * n_test - - c = ClassicalRegister(n_test, 'c') - x_train = QuantumRegister(n_obs_qubits, 'x_{b}') - x_test = QuantumRegister(n_test_qubits, 'x^{(test)}') - y_train = QuantumRegister(n_obs, 'y_{b}') - y_test = QuantumRegister(n_test, 'y^{(test)}') - qc = QuantumCircuit(x_train, x_test, y_train, y_test, c) - - S1 = state_prep(train) - qc.unitary(S1, [0], label='$S_{x}$') - - S2 = state_prep(test) - qc.unitary(S2, [1], label='$S_{x}$') - - S3 = state_prep(label_train) - qc.unitary(S3, [2], label='$S_{y}$') - - qc.barrier() - qc.h(y_test) - qc.cswap(y_test, x_train, x_test) - qc.h(y_test) - qc.barrier() - qc.cx(y_train, y_test) - qc.measure(y_test, c) - return qc - - -def ensemble(X_data, Y_data, x_test, n_swap=1, d=2, mode="balanced", barriers=False): - """ - Quantum ensemble classifier for multi-qubit data. - - This function implements a quantum ensemble learning algorithm using - controlled swap operations to create superpositions of different - training data arrangements. The ensemble leverages quantum superposition - to evaluate multiple training set configurations simultaneously. - - Algorithm Overview: - 1. Initialize training data, labels, and test data in quantum registers - 2. Create superposition over control qubits (2^d ensemble members) - 3. Apply controlled-SWAP operations to rearrange training data - 4. Perform final cosine similarity measurement - 5. Measure to obtain ensemble prediction - - The circuit creates 2^d different arrangements of the training data, - each weighted equally in superposition. The final measurement collapses - to a prediction that incorporates information from all arrangements. - - Parameters - ---------- - X_data : array-like, shape (n_samples, n_features) - Training data points where n_features must be a power of 2 - (e.g., 2, 4, 8, 16). Each sample should be normalized. - Y_data : array-like, shape (n_samples, 2) - Training labels as one-hot encoded vectors [[1,0] or [0,1]] - x_test : array-like, shape (n_features,) - Test data point to classify (must be normalized) - n_swap : int, optional - Number of swap operations per control qubit. More swaps create - more diverse ensemble members (default: 1) - d : int, optional - Number of control qubits determining ensemble depth. Creates - 2^d ensemble members. Typical values: 1-3 (default: 2) - mode : str, optional - Sampling strategy for swap operations: - - "balanced": Swaps within each class separately, maintains balance - - "unbalanced": Random swaps across all training samples - - "pair_sample": All pairwise swaps for comprehensive coverage - (default: "balanced") - barriers : bool, optional - If True, add barrier gates for circuit visualization (default: False) - - Returns - ------- - QuantumCircuit - Quantum circuit implementing the ensemble classifier with: - - d control qubits - - n_samples * log2(n_features) data qubits - - n_samples label qubits - - log2(n_features) test data qubits - - 1 test label qubit - - 1 classical bit for measurement - - Notes - ----- - - Total qubits: d + 2*n_samples*log2(n_features) + n_samples + 1 - - Circuit depth increases with d and n_swap - - Balanced mode requires even number of samples per class - - Simulation limited to ~30-36 qubits on classical hardware - - Mathematical Foundation: - The ensemble creates a superposition state: - |ψ⟩ = (1/√(2^d)) Σᵢ |i⟩|Dᵢ⟩|Lᵢ⟩ - where |Dᵢ⟩ and |Lᵢ⟩ are different arrangements of training data and labels. - - Examples - -------- - >>> from Utils import training_set, normalize_custom - >>> X_train = np.random.rand(8, 4) # 8 samples, 4 features - >>> y_train = np.array([0, 0, 0, 0, 1, 1, 1, 1]) - >>> X_data, Y_data = training_set(X_train, y_train, n=4, seed=42) - >>> x_test = normalize_custom(np.random.rand(4)) - >>> qc = ensemble(X_data, Y_data, x_test, n_swap=2, d=2, mode="balanced") - >>> print(f"Circuit has {qc.num_qubits} qubits and depth {qc.depth()}") - - References - ---------- - Macaluso et al., "A variational algorithm for quantum ensemble learning" - IET Quantum Communication (2023) - """ - n_obs = len(X_data) - qubits_per = int(np.log2(X_data.shape[1])) - n_obs_qubits = qubits_per * n_obs - n_test = 1 - n_test_qubits = qubits_per * n_test - n_reg = d + 2 * n_obs + 1 - - control = QuantumRegister(d) - data = QuantumRegister(n_obs_qubits, 'x') - labels = QuantumRegister(n_obs, 'y') - data_test = QuantumRegister(n_test_qubits, 'test_data') - label_test = QuantumRegister(n_test, 'test_label') - c = ClassicalRegister(n_test) - - - qc = QuantumCircuit(control, data, labels, data_test, label_test, c) - test_qubit_map = {} - for index in range(n_test): - indices = list(range((index * qubits_per),((index+1)) * qubits_per)) - qc.initialize(x_test, data_test[indices]) - test_qubit_map[index] = indices - - train_qubit_map = {} - trainLabel_qubit_map = {} - for index in range(n_obs): - indices = list(range((index * qubits_per),((index+1)) * qubits_per)) - qc.initialize(X_data[index], [data[indices]]) - qc.initialize(Y_data[index], [labels[index]]) - train_qubit_map[index] = indices - trainLabel_qubit_map[index] = index - - for i in range(d): - qc.h(control[i]) - - if barriers: - qc.barrier() - - if mode == 'balanced': - for i in range(d-1): - for j in range(n_swap): - U = np.random.choice(range(int(n_obs / 2)), 2, replace=False) - U_b = np.random.choice( range(len(train_qubit_map[U[0]])), 1 )[0] - d1 = train_qubit_map[U[0]][U_b] - d2 = train_qubit_map[U[1]][U_b] - qc.cswap(control[i], data[d1], data[d2]) - qc.cswap(control[i], labels[int(U[0])], labels[int(U[1])]) - - U = np.random.choice(range(int(n_obs / 2), n_obs), 2, replace=False) - U_b = np.random.choice( range(len(train_qubit_map[U[0]])), 1 )[0] - d1 = train_qubit_map[U[0]][U_b] - d2 = train_qubit_map[U[1]][U_b] - qc.cswap(control[i], data[d1], data[d2]) - qc.cswap(control[i], labels[int(U[0])], labels[int(U[1])]) - - qc.x(control[i]) - - for j in range(n_swap): - U = np.random.choice(range(int(n_obs / 2)), 2, replace=False) - U_b = np.random.choice( range(len(train_qubit_map[U[0]])), 1 )[0] - d1 = train_qubit_map[U[0]][U_b] - d2 = train_qubit_map[U[1]][U_b] - qc.cswap(control[i], data[d1], data[d2]) - qc.cswap(control[i], labels[int(U[0])], labels[int(U[1])]) - - U = np.random.choice(range(int(n_obs / 2), n_obs), 2, replace=False) - U_b = np.random.choice( range(len(train_qubit_map[U[0]])), 1 )[0] - d1 = train_qubit_map[U[0]][U_b] - d2 = train_qubit_map[U[1]][U_b] - qc.cswap(control[i], data[d1], data[d2]) - qc.cswap(control[i], labels[int(U[0])], labels[int(U[1])]) - - if barriers: - qc.barrier() - - qc.x(control[d-1]) - - U = np.random.choice(range(int(n_obs / 2)), 1, replace=False) - U = np.insert(U, 1, n_obs - 1) - U_b = np.random.choice(range(len(train_qubit_map[U[0]])), 1)[0] - d1 = train_qubit_map[U[0]][U_b] - d2 = train_qubit_map[U[1]][U_b] - qc.cswap(control[d-1], data[d1], data[d2]) - qc.cswap(control[d-1], labels[int(U[0])], labels[int(U[1])]) - - elif mode == "unbalanced": - for i in range(d): - for j in range(n_swap): - U = np.random.choice(range(n_obs), 2, replace=False) - U_b = np.random.choice( range(len(train_qubit_map[U[0]])), 1 )[0] - d1 = train_qubit_map[U[0]][U_b] - d2 = train_qubit_map[U[1]][U_b] - qc.cswap(control[i], data[d1], data[d2]) - qc.cswap(control[i], labels[int(U[0])], labels[int(U[1])]) - - qc.x(control[i]) - - for j in range(n_swap): - U = np.random.choice(range(n_obs), 2, replace=False) - U_b = np.random.choice( range(len(train_qubit_map[U[0]])), 1 )[0] - d1 = train_qubit_map[U[0]][U_b] - d2 = train_qubit_map[U[1]][U_b] - qc.cswap(control[i], data[d1], data[d2]) - qc.cswap(control[i], labels[int(U[0])], labels[int(U[1])]) - elif mode == "pair_sample": - for i in range(d): - for j in range(n_swap): - pairs = np.random.choice(range(n_obs), n_obs, replace=False) - for U in pairs.reshape(int(len(pairs)/2),2): - U_b = np.random.choice( range(len(train_qubit_map[U[0]])), 1 )[0] - d1 = train_qubit_map[U[0]][U_b] - d2 = train_qubit_map[U[1]][U_b] - qc.cswap(control[i], data[d1], data[d2]) - qc.cswap(control[i], labels[int(U[0])], labels[int(U[1])]) - - qc.x(control[i]) - if barriers: - qc.barrier() - - for j in range(n_swap): - pairs = np.random.choice(range(n_obs), n_obs, replace=False) - for U in pairs.reshape(int(len(pairs)/2),2): - U_b = np.random.choice( range(len(train_qubit_map[U[0]])), 1 )[0] - d1 = train_qubit_map[U[0]][U_b] - d2 = train_qubit_map[U[1]][U_b] - qc.cswap(control[i], data[d1], data[d2]) - qc.cswap(control[i], labels[int(U[0])], labels[int(U[1])]) - - if barriers: - qc.barrier() - - # Final classification step - ix_cls = n_obs - 1 - d1 = train_qubit_map[ix_cls][0] - - qc.h(label_test[0]) - qc.cswap(label_test[0], data[d1], data_test[0]) - qc.h(label_test[0]) - qc.cx(labels[ix_cls], label_test[0]) - qc.measure(label_test[0], c) - return qc - - -def exec_simulator(qc, n_shots=8192, device='CPU'): - """ - Execute quantum circuit on Aer simulator. - - Runs the quantum circuit using Qiskit's AerSimulator with statevector - method and returns measurement counts. Supports both CPU and GPU - execution for accelerated simulation of larger circuits. - - Parameters - ---------- - qc : QuantumCircuit - Quantum circuit to execute. Must contain measurement operations. - n_shots : int, optional - Number of measurement shots to perform. Higher values give more - accurate probability estimates but take longer to execute. - Typical values: 1024-8192 (default: 8192) - device : str, optional - Device type for simulation: - - 'CPU': Use CPU for simulation (default) - - 'GPU': Use GPU acceleration if available (requires qiskit-aer-gpu) - - Returns - ------- - dict - Dictionary mapping measurement outcomes (as binary strings) to - their counts. For example: {'0': 4123, '1': 4069} - - Notes - ----- - - Uses statevector method with optimization level 3 - - Parallel threshold set to 50 qubits for multi-threading - - GPU support requires CUDA-enabled GPU and qiskit-aer-gpu package - - Memory requirements grow exponentially with qubit count - - Examples - -------- - >>> from modeling import cos_classifier - >>> train = np.array([1.0, 0.0]) - >>> test = np.array([0.707, 0.707]) - >>> label = np.array([1.0, 0.0]) - >>> qc = cos_classifier(train, test, label) - >>> counts = exec_simulator(qc, n_shots=1024, device='CPU') - >>> print(counts) - {'0': 512, '1': 512} - - >>> # For GPU acceleration (if available) - >>> counts_gpu = exec_simulator(qc, n_shots=8192, device='GPU') - """ - backend = AerSimulator(method='statevector', device=device, statevector_parallel_threshold=50) - tqc = transpile(qc, backend, optimization_level=3) - result = backend.run([tqc]).result() - answer = result.get_counts(tqc) - return answer diff --git a/tutorial/QEnsemble/modeling_random_unitary.py b/tutorial/QEnsemble/modeling_random_unitary.py deleted file mode 100644 index c4846df..0000000 --- a/tutorial/QEnsemble/modeling_random_unitary.py +++ /dev/null @@ -1,497 +0,0 @@ -""" -Quantum Ensemble Modeling with Random Unitary Operations - -This module implements quantum ensemble learning using random unitary -transformations from the Haar measure, providing a more general approach -to ensemble construction compared to fixed swap patterns. -""" - -from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister -from qiskit.compiler import transpile -from qiskit_aer import AerSimulator -import Utils -from qiskit_aer import Aer -from qiskit.circuit.library import UnitaryGate -import scipy.stats -import numpy as np -import pandas as pd - -def run_ensemble(d, n_train, seed, n_swap, X_train, X_test, y_train, y_test, mode="balanced", n_shots=8192): - """ - Run quantum ensemble classifier with random unitary operations. - - This is a complete workflow function that executes the random unitary - ensemble method on a dataset and returns comprehensive evaluation metrics. - It constructs quantum circuits using Haar-random unitaries sampled from - the unitary group, providing a more general transformation approach than - fixed swap patterns. - - The function iterates over all test samples, constructs an ensemble - circuit for each, executes on simulator, and aggregates results with - performance metrics. - - Parameters - ---------- - d : int - Number of control qubits (ensemble depth). Creates 2^d ensemble - members. Typical values: 1-3 - n_train : int - Number of training samples to use from the training set. Must be - even for balanced mode and less than total training samples - seed : int - Random seed for reproducibility of training set selection and - random unitary sampling - n_swap : int - Number of random unitary operations per control qubit. More - operations create more diverse transformations - X_train : array-like, shape (n_samples, n_features) - Training feature data (normalized) - X_test : array-like, shape (n_test, n_features) - Test feature data (normalized) - y_train : array-like, shape (n_samples,) - Training labels (binary: 0 or 1) - y_test : array-like, shape (n_test,) - Test labels (binary: 0 or 1) - mode : str, optional - Ensemble mode - currently only "balanced" is fully implemented - (default: "balanced") - n_shots : int, optional - Number of measurement shots per circuit execution (default: 8192) - - Returns - ------- - DataFrame - Results dataframe with columns: - - seed: Random seed used - - n_feature: Number of features - - qubits: Total number of qubits in circuit - - d: Ensemble depth - - n_train: Number of training samples - - n_swap: Number of unitary operations - - accuracy: Classification accuracy - - brier: Brier score (calibration metric) - - predictions: List of probability predictions - - y_test: True test labels - - Notes - ----- - - Returns empty DataFrame if circuit exceeds 36 qubits (simulation limit) - - Uses scipy.stats.unitary_group for Haar-random unitary sampling - - Prints progress information during execution - - More computationally expensive than fixed swap ensemble - - Examples - -------- - >>> X_train = np.random.rand(20, 4) - >>> y_train = np.array([0]*10 + [1]*10) - >>> X_test = np.random.rand(5, 4) - >>> y_test = np.array([0, 0, 1, 1, 0]) - >>> results = run_ensemble(d=2, n_train=4, seed=42, n_swap=1, - ... X_train, X_test, y_train, y_test) - >>> print(f"Accuracy: {results['accuracy'].iloc[0]:.3f}") - """ - predictions = [] - - for x_test in X_test: - X_data, Y_data = Utils.training_set(X_train, y_train, n=n_train, seed=seed) - x_test = Utils.normalize_custom(x_test) - # print("Constructing circuit...",flush=True) - qc = ensemble(X_data, Y_data, x_test, n_swap=n_swap, d=d, mode=mode) - if qc.num_qubits <=36: - # print("Running circuit...",flush=True) - r = exec_simulator(qc, n_shots=n_shots) - # print(r) - predictions.append(Utils.retrieve_proba(r)) - - if qc.num_qubits <=36: - # print(predictions) - # print(y_test) - a, b = evaluation_metrics(predictions, y_test, save=False) - - res = pd.DataFrame( [ seed, X_data.shape[1], qc.num_qubits, d, n_train, n_swap, a, b, predictions, y_test ], - index = ['seed', 'n_feature', 'qubits', 'd', 'n_train', 'n_swap', 'accuracy', 'brier', 'predictions', 'y_test' ] ).transpose() - print(res.iloc[:,1:8]) - return res - else: - return pd.DataFrame() - - -def state_prep(x): - """ - Prepare a quantum state from classical data using unitary simulation. - - Identical to the state_prep function in modeling.py. Extracts the unitary - matrix that prepares a quantum state from classical data using Qiskit's - unitary simulator. - - Parameters - ---------- - x : array-like, shape (2^n,) - Classical data vector to encode as quantum state. Automatically - normalized using Utils.normalize_custom() - - Returns - ------- - ndarray, shape (2, 2) - Unitary matrix representing the state preparation operation - U such that U|0⟩ = |x⟩ - - Notes - ----- - - Uses AerSimulator with unitary method - - Input is automatically normalized - - Currently supports single-qubit states only - - See Also - -------- - modeling.state_prep : Identical function in main modeling module - """ - backend = AerSimulator(method="unitary") - x = Utils.normalize_custom(x) - qreg = QuantumRegister(1) - qc = QuantumCircuit(qreg) - qc.prepare_state(x, [qreg]) - tqc = transpile(qc, backend) - tqc.save_unitary() - result = backend.run([tqc]).result() - U = result.get_unitary(qc) - return U - - - -def ensemble(X_data, Y_data, x_test, n_swap=1, d=2, mode="balanced", barriers=False): - """ - Quantum ensemble classifier using random unitary operations. - - This function implements a quantum ensemble learning algorithm using - random unitary transformations sampled from the Haar measure, providing - a more general approach than fixed swap patterns. Instead of using - controlled-SWAP gates, this method applies controlled random unitaries - to the combined data and label registers. - - Algorithm Overview: - 1. Initialize training data, labels, and test data - 2. Create superposition over control qubits - 3. For each control qubit: - a. Sample random unitary from Haar measure - b. Apply controlled-unitary to data+label registers - c. Flip control qubit (X gate) - d. Apply another controlled random unitary - 4. Perform final cosine similarity measurement - 5. Measure test label qubit - - The random unitaries are sampled from U(2^(n_obs_qubits + n_obs)), the - unitary group acting on the combined space of all data and label qubits. - This provides a more general mixing of training samples than fixed swaps. - - Parameters - ---------- - X_data : array-like, shape (n_samples, n_features) - Training data points where n_features must be a power of 2. - Each sample should be normalized. - Y_data : array-like, shape (n_samples, 2) - Training labels as one-hot encoded vectors - x_test : array-like, shape (n_features,) - Test data point to classify (must be normalized) - n_swap : int, optional - Number of random unitary operations per control qubit. Despite - the name "swap", these are full random unitaries (default: 1) - d : int, optional - Number of control qubits for ensemble depth. Creates 2^d - ensemble members (default: 2) - mode : str, optional - Ensemble mode. Currently "balanced" is the primary mode, which - applies random unitaries in a structured way (default: "balanced") - barriers : bool, optional - If True, add barrier gates for circuit visualization (default: False) - - Returns - ------- - QuantumCircuit - Quantum circuit implementing the random unitary ensemble classifier - with registers: - - control: d qubits for ensemble control - - data: n_samples * log2(n_features) qubits for training data - - labels: n_samples qubits for training labels - - data_test: log2(n_features) qubits for test data - - prediction: 1 qubit for intermediate computation - - label_test: 1 qubit for final measurement - - Notes - ----- - - Random unitaries sampled using scipy.stats.unitary_group - - Unitary dimension: 2^(n_obs_qubits + n_obs) - - Much more general than swap-based ensemble - - Computationally expensive due to large unitary matrices - - Circuit depth increases significantly with n_swap - - Helper functions: cswap_obs, cswap_cosine, cswap_labels - - Mathematical Foundation: - Random unitaries are sampled from the Haar measure on U(N), providing - uniform coverage of the unitary group. This creates maximally diverse - ensemble members while maintaining quantum coherence. - - Examples - -------- - >>> from Utils import training_set, normalize_custom - >>> X_train = np.random.rand(8, 4) - >>> y_train = np.array([0, 0, 0, 0, 1, 1, 1, 1]) - >>> X_data, Y_data = training_set(X_train, y_train, n=4, seed=42) - >>> x_test = normalize_custom(np.random.rand(4)) - >>> qc = ensemble(X_data, Y_data, x_test, n_swap=1, d=2) - >>> print(f"Circuit uses {qc.num_qubits} qubits") - - References - ---------- - Haar measure sampling: Mezzadri, "How to generate random matrices from - the classical compact groups", Notices of the AMS (2007) - """ - - def cswap_obs(c, a, b): - """Controlled swap of observation qubits between indices a and b.""" - qubit_indices_a = train_qubit_map[a] - qubit_indices_b = train_qubit_map[b] - for (i,j) in zip(qubit_indices_a,qubit_indices_b): - qc.cswap(control[c],data[i],data[j]) - - def cswap_cosine(a): - """Controlled swap for cosine similarity measurement.""" - qubit_indices_a = train_qubit_map[a] - for (j, i) in enumerate(qubit_indices_a): - qc.cswap(label_test, data[i], data_test[j]) - - def cswap_labels(c, a, b): - """Controlled swap of label qubits.""" - qc.cswap(c, labels[a], labels[b]) - - n_obs = len(X_data) - qubits_per = int(np.log2(X_data.shape[1])) - n_obs_qubits = qubits_per * n_obs - n_test = 1 - n_test_qubits = qubits_per * n_test - n_reg = d + 2 * n_obs + 1 - - control = QuantumRegister(d, "c") - data = QuantumRegister(n_obs_qubits, 'x') - labels = QuantumRegister(n_obs, 'y') - prediction = QuantumRegister(1, 'pred_qubit') - - data_test = QuantumRegister(n_test_qubits, 'test_data') - label_test = QuantumRegister(n_test, 'test_label') - c = ClassicalRegister(n_test) - - # Sample random unitaries from Haar measure - unitary_sampler = scipy.stats.unitary_group(2**(n_obs_qubits+n_obs)) - - qc = QuantumCircuit(control, data, labels, data_test, prediction, label_test, c) - test_qubit_map = {} - for index in range(n_test): - indices = list(range((index * qubits_per),((index+1)) * qubits_per)) - qc.initialize(x_test, data_test[indices]) - test_qubit_map[index] = indices - - train_qubit_map = {} - trainLabel_qubit_map = {} - for index in range(n_obs): - indices = list(range((index * qubits_per),((index+1)) * qubits_per)) - qc.initialize(X_data[index], [data[indices]]) - qc.initialize(Y_data[index], [labels[index]]) - train_qubit_map[index] = indices - trainLabel_qubit_map[index] = index - - for i in range(d): - qc.h(control[i]) - - if barriers: - qc.barrier() - - if mode == 'balanced': - for i in range(d-1): - for _ in range(n_swap): - U = unitary_sampler.rvs() - U1 = UnitaryGate(U) - CU1 = U1.control(1) - qc.append(CU1,[control[i]]+[x for x in data]+[x for x in labels]) - # print("One gate done.") - qc.barrier() - qc.x(control[i]) - qc.barrier() - U = unitary_sampler.rvs() - U2 = UnitaryGate(U) - CU2 = U2.control(1) - qc.append(CU2, [control[i]]+[x for x in data]+[x for x in labels]) - if barriers: - qc.barrier() - - # Final swap for balanced mode - U = np.random.choice(range(int(n_obs / 2)), 1, replace=False) - U = np.insert(U, 1, n_obs - 1) - d1, d2 = U[0], U[1] - cswap_obs(d-1, d1, d2) - cswap_labels(d-1, d1, d2) - - qc.x(control[d-1]) - - if barriers: - qc.barrier() - - # Classification measurement - qc.h(label_test) - qc.barrier() - cswap_cosine(n_obs-1) - qc.barrier() - qc.h(label_test) - qc.cx(labels[n_obs-1], label_test) - qc.measure(label_test, c) - return qc - - - - -def exec_simulator(qc, n_shots=1024): - """ - Execute quantum circuit on Aer simulator. - - Runs the quantum circuit using Qiskit's statevector simulator backend - and returns measurement counts. This version uses the legacy Aer.get_backend - interface for compatibility. - - Parameters - ---------- - qc : QuantumCircuit - Quantum circuit to execute. Must contain measurement operations. - n_shots : int, optional - Number of measurement shots to perform. Default is 1024, which - is lower than the main modeling.py version (8192) for faster - execution during experimentation (default: 1024) - - Returns - ------- - dict - Dictionary mapping measurement outcomes (binary strings) to counts. - Example: {'0': 512, '1': 512} - - Notes - ----- - - Uses statevector_simulator backend - - Transpiles with optimization level 3 - - Different from modeling.exec_simulator which uses AerSimulator directly - - No GPU support in this version - - Examples - -------- - >>> qc = ensemble(X_data, Y_data, x_test, n_swap=1, d=2) - >>> counts = exec_simulator(qc, n_shots=2048) - >>> print(counts) - {'0': 1024, '1': 1024} - """ - backend = AerSimulator() - tqc = transpile(qc, backend, optimization_level=3) - result = Aer.get_backend("statevector_simulator").run([tqc], shots=n_shots).result() - answer = result.get_counts(tqc) - return answer - - -def label_to_array(y): - """ - Convert binary labels to one-hot encoded arrays. - - Transforms binary classification labels (0 or 1) into one-hot encoded - format required by quantum circuits. Label 0 becomes [1, 0] and label - 1 becomes [0, 1]. - - Parameters - ---------- - y : array-like, shape (n_samples,) - Binary labels where each element is either 0 or 1 - - Returns - ------- - ndarray, shape (n_samples, 2) - One-hot encoded labels where: - - [1, 0] represents class 0 - - [0, 1] represents class 1 - - Examples - -------- - >>> y = np.array([0, 1, 0, 1, 1]) - >>> Y = label_to_array(y) - >>> print(Y) - [[1 0] - [0 1] - [1 0] - [0 1] - [0 1]] - - See Also - -------- - Utils.label_to_array : Identical function in Utils module - """ - Y = [] - for el in y: - if el == 0: - Y.append([1, 0]) - else: - Y.append([0, 1]) - Y = np.asarray(Y) - return Y - -def evaluation_metrics(predictions, y_test, save=True): - """ - Calculate evaluation metrics for predictions. - - Computes classification accuracy and Brier score (calibration metric) - from probability predictions. The Brier score measures the mean squared - difference between predicted probabilities and actual outcomes. - - Parameters - ---------- - predictions : list of array-like - Predicted probabilities for each class. Each element should be - [p0, p1] where p0 + p1 = 1 - y_test : array-like, shape (n_samples,) - True binary labels (0 or 1) - save : bool, optional - Legacy parameter, not currently used (default: True) - - Returns - ------- - tuple of float - (accuracy, brier_score) where: - - accuracy: Fraction of correct predictions (0 to 1) - - brier_score: Mean squared error of probability predictions (0 to 1) - Lower Brier score indicates better calibration - - Notes - ----- - - Predictions are rounded to nearest integer for accuracy calculation - - Brier score uses probability of class 1 (p1) - - Perfect predictions: accuracy=1.0, brier_score=0.0 - - Examples - -------- - >>> predictions = [[0.9, 0.1], [0.3, 0.7], [0.6, 0.4]] - >>> y_test = np.array([0, 1, 0]) - >>> acc, brier = evaluation_metrics(predictions, y_test) - >>> print(f"Accuracy: {acc:.3f}, Brier: {brier:.3f}") - Accuracy: 1.000, Brier: 0.030 - - See Also - -------- - sklearn.metrics.accuracy_score : Accuracy calculation - sklearn.metrics.brier_score_loss : Brier score calculation - Utils.evaluation_metrics : Similar function in Utils module - """ - from sklearn.metrics import brier_score_loss, accuracy_score - labels = label_to_array(y_test) - - predicted_class = np.round(np.asarray(predictions)) - acc = accuracy_score(np.array(predicted_class)[:, 1], - np.array(labels)[:, 1]) - - p0 = [p[0] for p in predictions] - p1 = [p[1] for p in predictions] - - brier = brier_score_loss(y_test, p1) - - return acc, brier \ No newline at end of file diff --git a/tutorial/QProfiler/configs/config.yaml b/tutorial/QProfiler/configs/config.yaml index 3c5c8cb..503ecdd 100644 --- a/tutorial/QProfiler/configs/config.yaml +++ b/tutorial/QProfiler/configs/config.yaml @@ -9,6 +9,8 @@ file_dataset: 'ALL' # or use a list as below, to only select a few datasets # file_dataset: ['file1', 'file2', 'file3', etc] +index_col: False + # choose a backend for the QML methods # backend: 'ibm_cleveland' # backend: 'ibm_least' @@ -22,7 +24,7 @@ name: 'account_name_alias' # ibm_instance: # of ML methods to parallelize -n_jobs: 9 +n_jobs: 1 # select the embedding method for reducing dimensionality (number of features) in your data set embeddings: ['none', 'pca'] @@ -33,7 +35,7 @@ n_components: 3 # ML models to generate # model: ['svc', 'dt', 'lr', 'nb', 'rf', 'mlp', 'qsvc', 'vqc', 'qnn', 'pqk'] -model: ['svc', 'pqk'] +model: ['pqk'] #model: ['xgb'] average: 'weighted' @@ -181,7 +183,7 @@ qnn_args: {'primitive': 'estimator', # Begin QSVC arguments -qsvc_args: {'C': 0.01, +qsvc_args: {'C': 0.01, 'pegasos': False, 'encoding': ZZ, 'entanglement': 'linear', @@ -200,13 +202,20 @@ vqc_args: {'primitive': 'sampler', 'ansatz_type' : 'amp' } -# Begin PRK arguments +# Begin PQK arguments pqk_args: {'encoding': ZZ, 'entanglement': 'pairwise', 'primitive': 'estimator', 'reps': 4 } +# Begin QPL arguments +qpl_args: {'encoding': ZZ, + 'entanglement': 'pairwise', + 'primitive': 'estimator', + 'reps': 4 + } + # set hydra output directories hydra: run: diff --git a/tutorial/QProfiler/example_qprofiler.ipynb b/tutorial/QProfiler/example_qprofiler.ipynb index bfb27e1..d3dd3ac 100644 --- a/tutorial/QProfiler/example_qprofiler.ipynb +++ b/tutorial/QProfiler/example_qprofiler.ipynb @@ -121,7 +121,7 @@ "config = yaml.safe_load(open('configs/config.yaml', 'r'))\n", "\n", "# Import QProfiler\n", - "from apps.qprofiler import qprofiler as profiler\n", + "from qbiocode.apps.qprofiler import qprofiler as profiler\n", "\n", "# Run QProfiler\n", "profiler.main(config)\n", diff --git a/tutorial/QSage/qsage.ipynb b/tutorial/QSage/qsage.ipynb index 6c330aa..1070af7 100644 --- a/tutorial/QSage/qsage.ipynb +++ b/tutorial/QSage/qsage.ipynb @@ -39,7 +39,7 @@ "import dill as pickle\n", "\n", "# Import QSage\n", - "from apps.sage.sage import QuantumSage\n", + "from qbiocode.apps.sage.sage import QuantumSage\n", "\n", "print(\"✓ Imports successful\")" ]