Skip to content

Commit

Permalink
fixes #297: fitting in batches without all labels (#317) (#321)
Browse files Browse the repository at this point in the history
* fixes #297: fitting in batches without all labels

* test vqc with incomplete batch labels

* uncomment other vqc tests

* add reno note for fix of issue #297

* fix typo

* remove docstring reference from reno note

* make quantum instance selection explicit for vqc tests

* assert that all batches assume the correct number of classes

* fix style

Co-authored-by: Manoel Marques <[email protected]>
(cherry picked from commit dcbd78c)

Co-authored-by: Declan Millar <[email protected]>
  • Loading branch information
mergify[bot] and declanmillar authored Feb 17, 2022
1 parent 72ffc2b commit 00c1d77
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 7 deletions.
18 changes: 14 additions & 4 deletions qiskit_machine_learning/algorithms/classifiers/vqc.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
"""An implementation of quantum neural network classifier."""
"""An implementation of variational quantum classifier."""

from typing import Union, Optional, Callable, cast
import numpy as np
Expand All @@ -27,7 +27,17 @@


class VQC(NeuralNetworkClassifier):
"""Quantum neural network classifier."""
"""Variational quantum classifier.
The variational quantum classifier is a variational algorithm where the
measured expectation value is interpreted as the output of a classifier.
Only supports one-hot encoded labels;
e.g., data like ``[1, 0, 0]``, ``[0, 1, 0]``, ``[0, 0, 1]``.
Multi-label classification is not supported;
e.g., data like ``[1, 1, 0]``, ``[0, 1, 1]``, ``[1, 0, 1]``, ``[1, 1, 1]``.
"""

def __init__(
self,
Expand Down Expand Up @@ -155,12 +165,12 @@ def fit(self, X: np.ndarray, y: np.ndarray): # pylint: disable=invalid-name
Args:
X: The input data.
y: The target values.
y: The target values. Required to be one-hot encoded.
Returns:
self: returns a trained classifier.
"""
num_classes = len(np.unique(y, axis=0))
num_classes = y.shape[-1]
cast(CircuitQNN, self._neural_network).set_interpret(
self._get_interpret(num_classes), num_classes
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
fixes:
- |
Previously, VQC would throw an error if trained on batches of data where
not all of the target labels that can be found in the full dataset were
present. This is because VQC interpreted the number of unique targets in
the current batch as the number of classes. Currently, VQC is hard-coded
to expect one-hot-encoded targets. Therefore, VQC will now determine the
number of classes from the shape of the target array.
89 changes: 86 additions & 3 deletions test/algorithms/classifiers/test_vqc.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@

""" Test Neural Network Classifier """

from typing import Callable
import unittest
import functools

from test import QiskitMachineLearningTestCase

Expand All @@ -33,6 +35,8 @@ class TestVQC(QiskitMachineLearningTestCase):
def setUp(self):
super().setUp()

self.num_classes_by_batch = []

# specify quantum instances
algorithm_globals.random_seed = 12345
self.sv_quantum_instance = QuantumInstance(
Expand Down Expand Up @@ -63,8 +67,10 @@ def test_vqc(self, config):

if q_i == "statevector":
quantum_instance = self.sv_quantum_instance
else:
elif q_i == "qasm":
quantum_instance = self.qasm_quantum_instance
else:
quantum_instance = None

if opt == "bfgs":
optimizer = L_BFGS_B(maxiter=5)
Expand Down Expand Up @@ -169,8 +175,10 @@ def test_multiclass(self, config):

if q_i == "statevector":
quantum_instance = self.sv_quantum_instance
else:
elif q_i == "qasm":
quantum_instance = self.qasm_quantum_instance
else:
quantum_instance = None

if opt == "bfgs":
optimizer = L_BFGS_B(maxiter=5)
Expand Down Expand Up @@ -245,8 +253,10 @@ def test_warm_start(self, config):

if q_i == "statevector":
quantum_instance = self.sv_quantum_instance
else:
elif q_i == "qasm":
quantum_instance = self.qasm_quantum_instance
else:
quantum_instance = None

if opt == "bfgs":
optimizer = L_BFGS_B(maxiter=5)
Expand Down Expand Up @@ -294,6 +304,79 @@ def test_warm_start(self, config):
score = classifier.score(X, y)
self.assertGreater(score, 0.5)

def get_num_classes(self, func: Callable) -> Callable:
"""Wrapper to record the number of classes assumed when building CircuitQNN."""

@functools.wraps(func)
def wrapper(num_classes: int):
self.num_classes_by_batch.append(num_classes)
return func(num_classes)

return wrapper

@data(
# optimizer, quantum instance
("cobyla", "statevector"),
("cobyla", "qasm"),
("bfgs", "statevector"),
("bfgs", "qasm"),
(None, "statevector"),
(None, "qasm"),
)
def test_batches_with_incomplete_labels(self, config):
"""Test VQC when some batches do not include all possible labels."""
opt, q_i = config

if q_i == "statevector":
quantum_instance = self.sv_quantum_instance
elif q_i == "qasm":
quantum_instance = self.qasm_quantum_instance
else:
quantum_instance = None

if opt == "bfgs":
optimizer = L_BFGS_B(maxiter=5)
elif opt == "cobyla":
optimizer = COBYLA(maxiter=25)
else:
optimizer = None

num_inputs = 2
feature_map = ZZFeatureMap(num_inputs)
ansatz = RealAmplitudes(num_inputs, reps=1)

# Construct the data.
features = algorithm_globals.random.random((15, num_inputs))
target = np.asarray([0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2])
num_classes = len(np.unique(target))

# One-hot encode the target.
target_onehot = np.zeros((target.size, int(target.max() + 1)))
target_onehot[np.arange(target.size), target.astype(int)] = 1

# Initialize the VQC.
classifier = VQC(
feature_map=feature_map,
ansatz=ansatz,
optimizer=optimizer,
warm_start=True,
quantum_instance=quantum_instance,
)

classifier._get_interpret = self.get_num_classes(classifier._get_interpret)

# Fit the VQC to the first third of the data.
classifier.fit(features[:5, :], target_onehot[:5])

# Fit the VQC to the second third of the data with a warm start.
classifier.fit(features[5:10, :], target_onehot[5:10])

# Fit the VQC to the third third of the data with a warm start.
classifier.fit(features[10:, :], target_onehot[10:])

# Check all batches assume the correct number of classes
self.assertTrue((np.asarray(self.num_classes_by_batch) == num_classes).all())


if __name__ == "__main__":
unittest.main()

0 comments on commit 00c1d77

Please sign in to comment.