Skip to content

Hyperparameter search with QSVC on Iris

This example runs a small hyperparameter optimisation (HPO) for an eigenkit estimator. It loads a dataset, builds a sklearn pipeline, chooses a search range, runs GridSearchCV, and evaluates the selected model.

The grid is small on purpose, so that the example finishes quickly. Make it larger once this first quick test passes.

Imports

The example uses tools from sklearn, and QSVC (the quantum support vector classifier) and the dataset loader from eigenkit.

import numpy as np
from sklearn.metrics import accuracy_score, confusion_matrix
from sklearn.model_selection import GridSearchCV
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

from eigenkit import QSVC
from eigenkit.datasets import load_dataset

Load data

load_dataset("iris") returns a stratified split into a training set and a test set. Iris is small enough for a quick HPO run, and it is still a real classification task with 3 classes.

data = load_dataset("iris", split=True, test_size=0.25, random_state=42)

X_train = np.asarray(data["X_train"], dtype=np.float32)
y_train = np.asarray(data["y_train"], dtype=np.int64)
X_test = np.asarray(data["X_test"], dtype=np.float32)
y_test = np.asarray(data["y_test"], dtype=np.int64)
metadata = data["metadata"]

labels, counts = np.unique(y_train, return_counts=True)
class_counts = {int(label): int(count) for label, count in zip(labels, counts)}

print(f"Dataset: {metadata.name}")
print(f"Train shape: {X_train.shape}, test shape: {X_test.shape}")
print("Train class counts:", class_counts)
Dataset: iris
Train shape: (112, 4), test shape: (38, 4)
Train class counts: {0: 38, 1: 37, 2: 37}

Build the pipeline and search range

StandardScaler gives the features comparable ranges. GridSearchCV tunes QSVC through the name of its step in the pipeline, so C becomes qsvc__C. feature_map="Angle" is the angle encoding without entanglement.

The first grid changes only C. Very small values usually underfit, and larger values should give a useful decision boundary. Once this quick run is stable, add another dimension, such as qsvc__feature_map.

model = Pipeline(
    [
        ("scale", StandardScaler()),
        (
            "qsvc",
            QSVC(
                feature_map="Angle",
                reps=1,
                random_state=42,
            ),
        ),
    ]
)

param_grid = {
    "qsvc__C": [0.001, 0.01, 0.1, 1.0, 10.0],
}

print("Search space:", param_grid)
Search space: {'qsvc__C': [0.001, 0.01, 0.1, 1.0, 10.0]}

Run HPO

Here, GridSearchCV uses 3-fold cross-validation (cv=3) on the training split. In the output, "CV" stands for cross-validation. With refit=True, it trains the selected pipeline again on the whole training split.

search = GridSearchCV(
    model,
    param_grid=param_grid,
    scoring="accuracy",
    cv=3,
    n_jobs=1,
    refit=True,
)

search.fit(X_train, y_train)

print("Best parameters:", search.best_params_)
print(f"Best CV accuracy: {search.best_score_:.3f}")

results = search.cv_results_
for mean, std, params in zip(
    results["mean_test_score"],
    results["std_test_score"],
    results["params"],
):
    print(f"{mean:.3f} +/- {std:.3f}  {params}")
Best parameters: {'qsvc__C': 1.0}
Best CV accuracy: 0.946
0.616 +/- 0.223  {'qsvc__C': 0.001}
0.616 +/- 0.223  {'qsvc__C': 0.01}
0.866 +/- 0.037  {'qsvc__C': 0.1}
0.946 +/- 0.022  {'qsvc__C': 1.0}
0.946 +/- 0.022  {'qsvc__C': 10.0}

Evaluate the selected model

The search does not use the held-out test split. Use it once, at the end, for a fairer check of the selected pipeline.

best_model = search.best_estimator_
y_pred = best_model.predict(X_test)

test_accuracy = accuracy_score(y_test, y_pred)
print(f"Test accuracy: {test_accuracy:.3f}")
print("Confusion matrix:")
print(confusion_matrix(y_test, y_pred))
print("Predictions for the first five test samples:", y_pred[:5].tolist())
Test accuracy: 0.947
Confusion matrix:
[[12  0  0]
 [ 0 12  1]
 [ 0  1 12]]
Predictions for the first five test samples: [0, 1, 1, 1, 0]