Skip to content

Commit 8e18006

Browse files
Added max_circuits_per_job and removed deepcopy dependency of the quantum kernel trainer fixing #701 and #600 (#772) (#780)
* Added an option for num_circuits per job for kernels to fix #701 * Updated documentation and format the style. * Removed deepcopy dependency in quantum_kernel_trainer.py * Added release notes * quick fix for spell test * Added unit tests for max_circuits_per_job * Update fix-701-max_circuits_per_job-and-600-deepcopy-dependency-e6eda2e5b986c1be.yaml Small release note bugfix * Update fix-701-max_circuits_per_job-and-600-deepcopy-dependency-e6eda2e5b986c1be.yaml * Minor modifications for the unit test * Removed copy of TrainableKernel --------- Co-authored-by: oscar-wallis <oscar.wallis@outlook.com> (cherry picked from commit 2f49e9e) Co-authored-by: M. Emre Sahin <40424147+OkuyanBoga@users.noreply.github.com>
1 parent 65dd13e commit 8e18006

File tree

4 files changed

+81
-22
lines changed

4 files changed

+81
-22
lines changed

qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This code is part of a Qiskit project.
22
#
3-
# (C) Copyright IBM 2021, 2023.
3+
# (C) Copyright IBM 2021, 2024.
44
#
55
# This code is licensed under the Apache License, Version 2.0. You may
66
# obtain a copy of this license in the LICENSE.txt file in the root directory
@@ -13,7 +13,6 @@
1313
"""Quantum Kernel Trainer"""
1414
from __future__ import annotations
1515

16-
import copy
1716
from functools import partial
1817
from typing import Sequence
1918

@@ -96,7 +95,8 @@ def __init__(
9695
):
9796
"""
9897
Args:
99-
quantum_kernel: a trainable quantum kernel to be trained.
98+
quantum_kernel: a trainable quantum kernel to be trained. The
99+
:attr:`~.TrainableKernel.parameter_values` will be modified in place after the training.
100100
loss: A loss function available via string is "svc_loss" which is the same as
101101
:class:`~qiskit_machine_learning.utils.loss_functions.SVCLoss`. If a string is
102102
passed as the loss function, then the underlying
@@ -179,7 +179,7 @@ def fit(
179179
) -> QuantumKernelTrainerResult:
180180
"""
181181
Train the QuantumKernel by minimizing loss over the kernel parameters. The input
182-
quantum kernel will not be altered, and an optimized quantum kernel will be returned.
182+
quantum kernel will be altered.
183183
184184
Args:
185185
data (numpy.ndarray): ``(N, D)`` array of training data, where ``N`` is the
@@ -198,9 +198,6 @@ def fit(
198198
msg = "Quantum kernel cannot be fit because there are no user parameters specified."
199199
raise ValueError(msg)
200200

201-
# Bind inputs to objective function
202-
output_kernel = copy.deepcopy(self._quantum_kernel)
203-
204201
# Randomly initialize the initial point if one was not passed
205202
if self._initial_point is None:
206203
self._initial_point = algorithm_globals.random.random(num_params)
@@ -222,11 +219,13 @@ def fit(
222219
result.optimizer_evals = opt_results.nfev
223220
result.optimal_value = opt_results.fun
224221
result.optimal_point = opt_results.x
225-
result.optimal_parameters = dict(zip(output_kernel.training_parameters, opt_results.x))
222+
result.optimal_parameters = dict(
223+
zip(self.quantum_kernel.training_parameters, opt_results.x)
224+
)
226225

227226
# Return the QuantumKernel in optimized state
228-
output_kernel.assign_training_parameters(result.optimal_parameters)
229-
result.quantum_kernel = output_kernel
227+
self.quantum_kernel.assign_training_parameters(result.optimal_parameters)
228+
result.quantum_kernel = self.quantum_kernel
230229

231230
return result
232231

qiskit_machine_learning/kernels/fidelity_quantum_kernel.py

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This code is part of a Qiskit project.
22
#
3-
# (C) Copyright IBM 2022, 2023.
3+
# (C) Copyright IBM 2022, 2024.
44
#
55
# This code is licensed under the Apache License, Version 2.0. You may
66
# obtain a copy of this license in the LICENSE.txt file in the root directory
@@ -46,6 +46,7 @@ def __init__(
4646
fidelity: BaseStateFidelity | None = None,
4747
enforce_psd: bool = True,
4848
evaluate_duplicates: str = "off_diagonal",
49+
max_circuits_per_job: int = None,
4950
) -> None:
5051
"""
5152
Args:
@@ -73,6 +74,8 @@ def __init__(
7374
- ``none`` when training the diagonal is set to `1` and if two identical samples
7475
are found in the dataset the corresponding matrix element is set to `1`.
7576
When inferring, matrix elements for identical samples are set to `1`.
77+
max_circuits_per_job: Maximum number of circuits per job for the backend. Please
78+
check the backend specifications. Use ``None`` for all entries per job. Default ``None``.
7679
Raises:
7780
ValueError: When unsupported value is passed to `evaluate_duplicates`.
7881
"""
@@ -84,10 +87,15 @@ def __init__(
8487
f"Unsupported value passed as evaluate_duplicates: {evaluate_duplicates}"
8588
)
8689
self._evaluate_duplicates = eval_duplicates
87-
8890
if fidelity is None:
8991
fidelity = ComputeUncompute(sampler=Sampler())
9092
self._fidelity = fidelity
93+
if max_circuits_per_job is not None:
94+
if max_circuits_per_job < 1:
95+
raise ValueError(
96+
f"Unsupported value passed as max_circuits_per_job: {max_circuits_per_job}"
97+
)
98+
self.max_circuits_per_job = max_circuits_per_job
9199

92100
def evaluate(self, x_vec: np.ndarray, y_vec: np.ndarray | None = None) -> np.ndarray:
93101
x_vec, y_vec = self._validate_input(x_vec, y_vec)
@@ -208,17 +216,38 @@ def _get_kernel_entries(
208216
back from the async job.
209217
"""
210218
num_circuits = left_parameters.shape[0]
219+
kernel_entries = []
220+
# Check if it is trivial case, only identical samples
211221
if num_circuits != 0:
212-
job = self._fidelity.run(
213-
[self._feature_map] * num_circuits,
214-
[self._feature_map] * num_circuits,
215-
left_parameters,
216-
right_parameters,
217-
)
218-
kernel_entries = job.result().fidelities
219-
else:
220-
# trivial case, only identical samples
221-
kernel_entries = []
222+
if self.max_circuits_per_job is None:
223+
job = self._fidelity.run(
224+
[self._feature_map] * num_circuits,
225+
[self._feature_map] * num_circuits,
226+
left_parameters,
227+
right_parameters,
228+
)
229+
kernel_entries = job.result().fidelities
230+
else:
231+
# Determine the number of chunks needed
232+
num_chunks = (
233+
num_circuits + self.max_circuits_per_job - 1
234+
) // self.max_circuits_per_job
235+
for i in range(num_chunks):
236+
# Determine the range of indices for this chunk
237+
start_idx = i * self.max_circuits_per_job
238+
end_idx = min((i + 1) * self.max_circuits_per_job, num_circuits)
239+
# Extract the parameters for this chunk
240+
chunk_left_parameters = left_parameters[start_idx:end_idx]
241+
chunk_right_parameters = right_parameters[start_idx:end_idx]
242+
# Execute this chunk
243+
job = self._fidelity.run(
244+
[self._feature_map] * (end_idx - start_idx),
245+
[self._feature_map] * (end_idx - start_idx),
246+
chunk_left_parameters,
247+
chunk_right_parameters,
248+
)
249+
# Extend the kernel_entries list with the results from this chunk
250+
kernel_entries.extend(job.result().fidelities)
222251
return kernel_entries
223252

224253
def _is_trivial(
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
fixes:
3+
- |
4+
Added a `max_circuits_per_job` parameter to the :class:`.FidelityQuantumKernel` used
5+
in the case that if more circuits are submitted than the job limit for the
6+
backend, the circuits are split up and run through separate jobs.
7+
- |
8+
Removed :class:`.QuantumKernelTrainer` dependency on `copy.deepcopy` that was
9+
throwing an error with real backends. Now, it modifies the :class:`.TrainableKernel`
10+
in place. If you would like to use the initial kernel, please call
11+
:meth:`~.TrainableKernel.assign_training_parameters` of the :class:`~.TrainableKernel`
12+
using the :attr:`~.QuantumKernelTrainer.initial_point` attribute of
13+
:class:`~.QuantumKernelTrainer`.
14+

test/kernels/test_fidelity_qkernel.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,27 @@ def test_defaults(self):
106106

107107
self.assertGreaterEqual(score, 0.5)
108108

109+
def test_max_circuits_per_job(self):
110+
"""Test max_circuits_per_job parameters."""
111+
kernel_all = FidelityQuantumKernel(feature_map=self.feature_map, max_circuits_per_job=None)
112+
kernel_matrix_all = kernel_all.evaluate(x_vec=self.sample_train)
113+
with self.subTest("Check when max_circuits_per_job > left_parameters"):
114+
kernel_more = FidelityQuantumKernel(
115+
feature_map=self.feature_map, max_circuits_per_job=20
116+
)
117+
kernel_matrix_more = kernel_more.evaluate(x_vec=self.sample_train)
118+
np.testing.assert_equal(kernel_matrix_all, kernel_matrix_more)
119+
with self.subTest("Check when max_circuits_per_job = 1"):
120+
kernel_1 = FidelityQuantumKernel(feature_map=self.feature_map, max_circuits_per_job=1)
121+
kernel_matrix_1 = kernel_1.evaluate(x_vec=self.sample_train)
122+
np.testing.assert_equal(kernel_matrix_all, kernel_matrix_1)
123+
109124
def test_exceptions(self):
110125
"""Test quantum kernel raises exceptions and warnings."""
111126
with self.assertRaises(ValueError, msg="Unsupported value of 'evaluate_duplicates'."):
112127
_ = FidelityQuantumKernel(evaluate_duplicates="wrong")
128+
with self.assertRaises(ValueError, msg="Unsupported value of 'max_circuits_per_job'."):
129+
_ = FidelityQuantumKernel(max_circuits_per_job=-1)
113130

114131
@idata(
115132
# params, fidelity, feature map, enforce_psd, duplicate

0 commit comments

Comments
 (0)