From fefc10e2a510b2152c2efe43c377f6da6bbd2a6f Mon Sep 17 00:00:00 2001 From: trbromley Date: Mon, 2 Mar 2020 16:33:08 -0500 Subject: [PATCH 01/65] Add first commit of keras interface --- pennylane/interfaces/keras.py | 105 ++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 pennylane/interfaces/keras.py diff --git a/pennylane/interfaces/keras.py b/pennylane/interfaces/keras.py new file mode 100644 index 00000000000..d52a6ece38b --- /dev/null +++ b/pennylane/interfaces/keras.py @@ -0,0 +1,105 @@ +# Copyright 2018-2020 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +This module contains the :func:`to_keras` function to convert quantum nodes into Keras layers. +""" +import tensorflow as tf +import pennylane as qml + +major, minor, patch = tf.__version__.split(".") + +if (int(major) == 1 and int(minor) < 4) or int(major) == 0: + raise ImportError("Must use TensorFlow v1.4.0 or above") +else: + from tensorflow import keras + + +class QuantumLayers(keras.layers.Layer): + """TODO - This is a layer for a specific architecture given here: + https://github.com/XanaduAI/qml/issues/28#issuecomment-574889614. + """ + def __init__( + self, + units, + device="default.qubit", + n_layers=1, + rotations_initializer="random_uniform", + rotations_regularizer=None, + rotations_constraint=None, + **kwargs, + ): + if "input_shape" not in kwargs and "input_dim" in kwargs: + kwargs["input_shape"] = (kwargs.pop("input_dim"),) + if "dynamic" in kwargs: + del kwargs["dynamic"] + super(QuantumLayers, self).__init__(dynamic=True, **kwargs) + + self.units = units + self.device = device + self.n_layers = n_layers + self.rotations_initializer = keras.initializers.get(rotations_initializer) + self.rotations_regularizer = keras.regularizers.get(rotations_regularizer) + self.rotations_constraint = keras.constraints.get(rotations_constraint) + + self.input_spec = keras.layers.InputSpec(min_ndim=2, axes={-1: units}) + self.supports_masking = False + + def circuit(inputs, parameters): + qml.templates.embeddings.AngleEmbedding(inputs, wires=list(range(self.units))) + qml.templates.layers.StronglyEntanglingLayers(parameters, wires=list(range(self.units))) + return [qml.expval(qml.PauliZ(i)) for i in range(self.units)] + + self.dev = qml.device(device, wires=units) + self.layer = qml.QNode(circuit, self.dev, interface="tf") + + def apply_layer(self, *args): + return tf.keras.backend.cast_to_floatx(self.layer(*args)) + + def build(self, input_shape): + # assert len(input_shape) == 2 + input_dim = input_shape[-1] + assert input_dim == self.units + + self.rotations = self.add_weight( + shape=(self.n_layers, input_dim, 3), + initializer=self.rotations_initializer, + name="rotations", + regularizer=self.rotations_regularizer, + constraint=self.rotations_constraint, + ) + self.built = True + + def call(self, inputs): + return tf.stack([self.apply_layer(i, self.rotations) for i in inputs]) + + def compute_output_shape(self, input_shape): + return input_shape + + def get_config(self): + config = { + "units": self.units, + "device": self.device, + "n_layers": self.n_layers, + "rotations_initializer": keras.initializers.serialize(self.rotations_initializer), + "rotations_regularizer": keras.regularizers.serialize(self.rotations_regularizer), + "rotations_constraint": keras.constraints.serialize(self.rotations_constraint), + } + base_config = super(QuantumLayers, self).get_config() + return dict(list(base_config.items()) + list(config.items())) + + +def to_keras(qnode: qml.QNode): + """TODO + """ + return qnode From bf57067158d423d145b8ce22b72e9e68bb050854 Mon Sep 17 00:00:00 2001 From: trbromley Date: Wed, 4 Mar 2020 17:08:00 -0500 Subject: [PATCH 02/65] Update keras interface --- pennylane/interfaces/keras.py | 153 +++++++++++++++++++--------------- 1 file changed, 85 insertions(+), 68 deletions(-) diff --git a/pennylane/interfaces/keras.py b/pennylane/interfaces/keras.py index d52a6ece38b..e0ce54af3fb 100644 --- a/pennylane/interfaces/keras.py +++ b/pennylane/interfaces/keras.py @@ -14,92 +14,109 @@ """ This module contains the :func:`to_keras` function to convert quantum nodes into Keras layers. """ +import functools +import inspect +from typing import Optional + import tensorflow as tf -import pennylane as qml -major, minor, patch = tf.__version__.split(".") +import pennylane as qml +from pennylane.interfaces.tf import to_tf -if (int(major) == 1 and int(minor) < 4) or int(major) == 0: - raise ImportError("Must use TensorFlow v1.4.0 or above") +if int(tf.__version__.split(".")[0]) < 2: + raise ImportError("TensorFlow version 2 or above is required for Keras QNodes") else: - from tensorflow import keras + from tensorflow.keras.layers import Layer -class QuantumLayers(keras.layers.Layer): - """TODO - This is a layer for a specific architecture given here: - https://github.com/XanaduAI/qml/issues/28#issuecomment-574889614. - """ +class _KerasQNode(Layer): def __init__( self, - units, - device="default.qubit", - n_layers=1, - rotations_initializer="random_uniform", - rotations_regularizer=None, - rotations_constraint=None, - **kwargs, + qnode: qml.QNode, + weight_shapes: dict, + output_dim: int, + input_dim: Optional[int] = None, + weight_specs: Optional[dict] = None, + **kwargs ): - if "input_shape" not in kwargs and "input_dim" in kwargs: - kwargs["input_shape"] = (kwargs.pop("input_dim"),) - if "dynamic" in kwargs: - del kwargs["dynamic"] - super(QuantumLayers, self).__init__(dynamic=True, **kwargs) - self.units = units - self.device = device - self.n_layers = n_layers - self.rotations_initializer = keras.initializers.get(rotations_initializer) - self.rotations_regularizer = keras.regularizers.get(rotations_regularizer) - self.rotations_constraint = keras.constraints.get(rotations_constraint) + self.sig = qnode.func.sig + defaults = [ + name for name, sig in self.sig.items() if sig.par.default != inspect.Parameter.empty + ] + if len(defaults) != 1: + raise TypeError("Keras QNodes must have a single default argument") + self.input_arg = defaults[0] + + if self.input_arg in {weight_shapes.keys()}: + raise ValueError("Input argument dimension should not be specified in weight_shapes") + if {weight_shapes.keys()} | {self.input_arg} != {self.sig.keys()}: + raise ValueError("Must specify a shape for every non-input parameter in QNode") + if qnode.func.var_pos: + raise TypeError("Keras QNodes cannot have a variable number of positional arguments") + if qnode.func.var_keyword: + raise TypeError("Keras QNodes cannot have a variable number of keyword arguments") + if len(weight_shapes.keys()) != len({weight_shapes.keys()}): + raise ValueError("A shape is specified multiple times in weight_shapes") + + self.qnode = qnode + self.input_dim = input_dim if isinstance(input_dim, (int, type(None))) else input_dim[0] + self.weight_shapes = { + weight: ((size,) if isinstance(size, int) else tuple(size)) + for weight, size in weight_shapes.items() + } + self.output_dim = output_dim if isinstance(output_dim, int) else output_dim[0] - self.input_spec = keras.layers.InputSpec(min_ndim=2, axes={-1: units}) - self.supports_masking = False + if weight_specs: + self.weight_specs = weight_specs + else: + self.weight_specs = {} + self.qnode_weights = {} - def circuit(inputs, parameters): - qml.templates.embeddings.AngleEmbedding(inputs, wires=list(range(self.units))) - qml.templates.layers.StronglyEntanglingLayers(parameters, wires=list(range(self.units))) - return [qml.expval(qml.PauliZ(i)) for i in range(self.units)] + super(_KerasQNode, self).__init__(dynamic=True, **kwargs) - self.dev = qml.device(device, wires=units) - self.layer = qml.QNode(circuit, self.dev, interface="tf") + def build(self, input_shape): + if self.input_dim and input_shape[-1] != self.input_dim: + raise ValueError("QNode can only accept inputs of size {}".format(self.input_dim)) - def apply_layer(self, *args): - return tf.keras.backend.cast_to_floatx(self.layer(*args)) + for weight, size in self.weight_shapes.items(): + spec = self.weight_specs.pop(weight, {}) + self.qnode_weights[weight] = self.add_weight(name=weight, shape=size, **spec) - def build(self, input_shape): - # assert len(input_shape) == 2 - input_dim = input_shape[-1] - assert input_dim == self.units - - self.rotations = self.add_weight( - shape=(self.n_layers, input_dim, 3), - initializer=self.rotations_initializer, - name="rotations", - regularizer=self.rotations_regularizer, - constraint=self.rotations_constraint, - ) - self.built = True + super(_KerasQNode, self).build(input_shape) + + def call(self, inputs, **kwargs): + qnode = self.qnode + for arg in self.sig: + if arg is not self.input_arg: + w = self.qnode_weights[arg] + qnode = functools.partial(qnode, w) + + outputs = tf.stack([qnode(**{self.input_arg: x}) for x in inputs]) + input_shape = tf.shape(inputs) - def call(self, inputs): - return tf.stack([self.apply_layer(i, self.rotations) for i in inputs]) + return tf.reshape(outputs, self.compute_output_shape(input_shape)) def compute_output_shape(self, input_shape): - return input_shape - - def get_config(self): - config = { - "units": self.units, - "device": self.device, - "n_layers": self.n_layers, - "rotations_initializer": keras.initializers.serialize(self.rotations_initializer), - "rotations_regularizer": keras.regularizers.serialize(self.rotations_regularizer), - "rotations_constraint": keras.constraints.serialize(self.rotations_constraint), - } - base_config = super(QuantumLayers, self).get_config() - return dict(list(base_config.items()) + list(config.items())) + return tf.TensorShape([input_shape[0], self.output_dim]) + + @property + def interface(self): + return "keras" + + def __str__(self): + detail = "" + return detail.format( + self.qnode.device.short_name, + self.qnode.func.__name__, + self.qnode.num_wires, + self.interface, + ) + + def __repr__(self): + return self.__str__() def to_keras(qnode: qml.QNode): - """TODO - """ - return qnode + qnode = to_tf(qnode) + return _KerasQNode(qnode) From 094b2d7016819f22ebab39fc7020253dd59dfa1e Mon Sep 17 00:00:00 2001 From: trbromley Date: Thu, 5 Mar 2020 08:43:37 -0500 Subject: [PATCH 03/65] Make qnode.func visible in TF QNode --- pennylane/interfaces/tf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pennylane/interfaces/tf.py b/pennylane/interfaces/tf.py index 28dc2710483..677c3f58a50 100644 --- a/pennylane/interfaces/tf.py +++ b/pennylane/interfaces/tf.py @@ -67,6 +67,7 @@ def __repr__(self): jacobian = qnode.jacobian metric_tensor = qnode.metric_tensor draw = qnode.draw + func = qnode.func @qnode_str @tf.custom_gradient From 58d2f3ee795b105a45197c56ccd5b4b3d56ef6a8 Mon Sep 17 00:00:00 2001 From: trbromley Date: Tue, 10 Mar 2020 13:14:33 -0400 Subject: [PATCH 04/65] Move to new location --- pennylane/interfaces/keras.py => qnn.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pennylane/interfaces/keras.py => qnn.py (100%) diff --git a/pennylane/interfaces/keras.py b/qnn.py similarity index 100% rename from pennylane/interfaces/keras.py rename to qnn.py From f3461dadc4918608fbc3d3df360078884b2b267a Mon Sep 17 00:00:00 2001 From: trbromley Date: Tue, 10 Mar 2020 14:05:15 -0400 Subject: [PATCH 05/65] Update --- qnn.py | 35 +++++++++++------------------------ 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/qnn.py b/qnn.py index e0ce54af3fb..5e5c9425d63 100644 --- a/qnn.py +++ b/qnn.py @@ -11,9 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -""" -This module contains the :func:`to_keras` function to convert quantum nodes into Keras layers. -""" import functools import inspect from typing import Optional @@ -21,15 +18,14 @@ import tensorflow as tf import pennylane as qml -from pennylane.interfaces.tf import to_tf if int(tf.__version__.split(".")[0]) < 2: - raise ImportError("TensorFlow version 2 or above is required for Keras QNodes") + raise ImportError("TensorFlow version 2 or above is required for this module") else: from tensorflow.keras.layers import Layer -class _KerasQNode(Layer): +class KerasLayer(Layer): def __init__( self, qnode: qml.QNode, @@ -45,17 +41,18 @@ def __init__( name for name, sig in self.sig.items() if sig.par.default != inspect.Parameter.empty ] if len(defaults) != 1: - raise TypeError("Keras QNodes must have a single default argument") + raise TypeError("Conversion to a Keras layer requires a QNode with a single " + "default argument") self.input_arg = defaults[0] if self.input_arg in {weight_shapes.keys()}: raise ValueError("Input argument dimension should not be specified in weight_shapes") if {weight_shapes.keys()} | {self.input_arg} != {self.sig.keys()}: - raise ValueError("Must specify a shape for every non-input parameter in QNode") + raise ValueError("Must specify a shape for every non-input parameter in the QNode") if qnode.func.var_pos: - raise TypeError("Keras QNodes cannot have a variable number of positional arguments") + raise TypeError("Cannot have a variable number of positional arguments") if qnode.func.var_keyword: - raise TypeError("Keras QNodes cannot have a variable number of keyword arguments") + raise TypeError("Cannot have a variable number of keyword arguments") if len(weight_shapes.keys()) != len({weight_shapes.keys()}): raise ValueError("A shape is specified multiple times in weight_shapes") @@ -73,17 +70,17 @@ def __init__( self.weight_specs = {} self.qnode_weights = {} - super(_KerasQNode, self).__init__(dynamic=True, **kwargs) + super(KerasLayer, self).__init__(dynamic=True, **kwargs) def build(self, input_shape): if self.input_dim and input_shape[-1] != self.input_dim: raise ValueError("QNode can only accept inputs of size {}".format(self.input_dim)) for weight, size in self.weight_shapes.items(): - spec = self.weight_specs.pop(weight, {}) + spec = self.weight_specs.get(weight, {}) self.qnode_weights[weight] = self.add_weight(name=weight, shape=size, **spec) - super(_KerasQNode, self).build(input_shape) + super(KerasLayer, self).build(input_shape) def call(self, inputs, **kwargs): qnode = self.qnode @@ -100,23 +97,13 @@ def call(self, inputs, **kwargs): def compute_output_shape(self, input_shape): return tf.TensorShape([input_shape[0], self.output_dim]) - @property - def interface(self): - return "keras" - def __str__(self): - detail = "" + detail = "" return detail.format( self.qnode.device.short_name, self.qnode.func.__name__, self.qnode.num_wires, - self.interface, ) def __repr__(self): return self.__str__() - - -def to_keras(qnode: qml.QNode): - qnode = to_tf(qnode) - return _KerasQNode(qnode) From 73c0d98c53d3ac443d3a05e3c8ea994b5e65ca6f Mon Sep 17 00:00:00 2001 From: trbromley Date: Tue, 10 Mar 2020 14:09:58 -0400 Subject: [PATCH 06/65] Move to right place --- qnn.py => pennylane/qnn.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename qnn.py => pennylane/qnn.py (100%) diff --git a/qnn.py b/pennylane/qnn.py similarity index 100% rename from qnn.py rename to pennylane/qnn.py From 64cee5beab5cadbcc65b52a7ced90b4fbc515645 Mon Sep 17 00:00:00 2001 From: trbromley Date: Tue, 10 Mar 2020 15:22:12 -0400 Subject: [PATCH 07/65] Update --- pennylane/__init__.py | 1 + pennylane/qnn.py | 18 +++++++----------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/pennylane/__init__.py b/pennylane/__init__.py index 8b63aca502e..7e32b7723db 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -27,6 +27,7 @@ import pennylane.init import pennylane.templates +import pennylane.qnn from pennylane.templates import template, broadcast from pennylane.about import about from pennylane.vqe import Hamiltonian, VQECost diff --git a/pennylane/qnn.py b/pennylane/qnn.py index 5e5c9425d63..a7d5819512f 100644 --- a/pennylane/qnn.py +++ b/pennylane/qnn.py @@ -17,7 +17,7 @@ import tensorflow as tf -import pennylane as qml +from pennylane.qnodes import QNode if int(tf.__version__.split(".")[0]) < 2: raise ImportError("TensorFlow version 2 or above is required for this module") @@ -28,7 +28,7 @@ class KerasLayer(Layer): def __init__( self, - qnode: qml.QNode, + qnode: QNode, weight_shapes: dict, output_dim: int, input_dim: Optional[int] = None, @@ -45,15 +45,15 @@ def __init__( "default argument") self.input_arg = defaults[0] - if self.input_arg in {weight_shapes.keys()}: + if self.input_arg in set(weight_shapes.keys()): raise ValueError("Input argument dimension should not be specified in weight_shapes") - if {weight_shapes.keys()} | {self.input_arg} != {self.sig.keys()}: + if set(weight_shapes.keys()) | set(self.input_arg) != set(self.sig.keys()): raise ValueError("Must specify a shape for every non-input parameter in the QNode") if qnode.func.var_pos: raise TypeError("Cannot have a variable number of positional arguments") if qnode.func.var_keyword: raise TypeError("Cannot have a variable number of keyword arguments") - if len(weight_shapes.keys()) != len({weight_shapes.keys()}): + if len(weight_shapes.keys()) != len(set(weight_shapes.keys())): raise ValueError("A shape is specified multiple times in weight_shapes") self.qnode = qnode @@ -98,12 +98,8 @@ def compute_output_shape(self, input_shape): return tf.TensorShape([input_shape[0], self.output_dim]) def __str__(self): - detail = "" - return detail.format( - self.qnode.device.short_name, - self.qnode.func.__name__, - self.qnode.num_wires, - ) + detail = "" + return detail.format(self.qnode.func.__name__) def __repr__(self): return self.__str__() From 2a41627b4d0187cbc80dbf8d7e4f8c98503ce733 Mon Sep 17 00:00:00 2001 From: trbromley Date: Tue, 10 Mar 2020 18:08:23 -0400 Subject: [PATCH 08/65] Start working on tests --- pennylane/qnn.py | 9 ++- tests/test_qnn.py | 172 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 tests/test_qnn.py diff --git a/pennylane/qnn.py b/pennylane/qnn.py index a7d5819512f..d47b9110b45 100644 --- a/pennylane/qnn.py +++ b/pennylane/qnn.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from collections.abc import Iterable import functools import inspect from typing import Optional @@ -53,16 +54,14 @@ def __init__( raise TypeError("Cannot have a variable number of positional arguments") if qnode.func.var_keyword: raise TypeError("Cannot have a variable number of keyword arguments") - if len(weight_shapes.keys()) != len(set(weight_shapes.keys())): - raise ValueError("A shape is specified multiple times in weight_shapes") self.qnode = qnode - self.input_dim = input_dim if isinstance(input_dim, (int, type(None))) else input_dim[0] + self.input_dim = input_dim[0] if isinstance(input_dim, Iterable) else input_dim self.weight_shapes = { - weight: ((size,) if isinstance(size, int) else tuple(size)) + weight: (tuple(size) if isinstance(size, Iterable) else (size,)) for weight, size in weight_shapes.items() } - self.output_dim = output_dim if isinstance(output_dim, int) else output_dim[0] + self.output_dim = output_dim[0] if isinstance(output_dim, Iterable) else output_dim if weight_specs: self.weight_specs = weight_specs diff --git a/tests/test_qnn.py b/tests/test_qnn.py new file mode 100644 index 00000000000..5a245228ef6 --- /dev/null +++ b/tests/test_qnn.py @@ -0,0 +1,172 @@ +# Copyright 2018-2020 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Tests for the pennylane.qnn module. +""" +import pytest +import numpy as np + +import pennylane as qml +from pennylane.qnn import KerasLayer + + +@pytest.fixture() +def get_circuit(n_qubits, output_dim): + """Fixture for getting a sample quantum circuit with a controllable qubit number and output + dimension. Returns both the circuit and the shape of the weights""" + + dev = qml.device("default.qubit", wires=n_qubits) + weight_shapes = {"w1": (3, n_qubits, 3), "w2": (2, n_qubits, 3), "w3": (1,), "w4": 1, + "w5": [3]} + + @qml.qnode(dev, interface='tf') + def circuit(w1, w2, w3, w4, w5, x=None): + """A circuit that embeds data using the AngleEmbedding and then performs a variety of + operations. The output is a PauliZ measurement on the first output_dim qubits.""" + qml.templates.AngleEmbedding(x) + qml.templates.StronglyEntanglingLayers(w1, wires=list(range(n_qubits))) + qml.templates.StronglyEntanglingLayers(w2, wires=list(range(n_qubits))) + qml.RX(w3, wires=0) + qml.RX(w4, wires=0) + qml.Rot(w5, wires=0) + return [qml.expval(qml.PauliZ(i)) for i in range(output_dim)] + + return circuit, weight_shapes + + +def indices(n_max): + a, b = np.tril_indices(n_max) + return zip(*[a + 1, b + 1]) + + +@pytest.mark.usefixtures("get_circuit") +class TestKerasLayer: + """Unit tests for the pennylane.qnn.KerasLayer class.""" + + @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) + def test_too_many_defaults(self, get_circuit, output_dim): + """Test if a TypeError is raised when instantiated with a QNode that has two defaults""" + c, w = get_circuit + c.func.sig['x2'] = c.func.sig['x'] + with pytest.raises(TypeError, match="Conversion to a Keras layer requires"): + KerasLayer(c, w, output_dim) + + @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) + def test_no_defaults(self, get_circuit, output_dim): + """Test if a TypeError is raised when instantiated with a QNode that has no defaults""" + c, w = get_circuit + del c.func.sig['x'] + with pytest.raises(TypeError, match="Conversion to a Keras layer requires"): + KerasLayer(c, w, output_dim) + + @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) + def test_input_in_weight_shapes(self, get_circuit, n_qubits, output_dim): + """Test if a ValueError is raised when instantiated with a weight_shapes dictionary that + contains the shape of the input""" + c, w = get_circuit + w['x'] = n_qubits + with pytest.raises(ValueError, match="Input argument dimension should not"): + KerasLayer(c, w, output_dim) + + @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) + def test_weight_shape_unspecified(self, get_circuit, output_dim): + """Test if a ValueError is raised when instantiated with a weight missing from the + weight_shapes dictionary""" + c, w = get_circuit + del w['w1'] + with pytest.raises(ValueError, match="Must specify a shape for every non-input parameter"): + KerasLayer(c, w, output_dim) + + @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) + def test_var_pos(self, get_circuit, monkeypatch, output_dim): + """Test if a TypeError is raised when instantiated with a variable number of positional + arguments""" + c, w = get_circuit + + class FuncPatch: + """Patch for variable number of keyword arguments""" + sig = c.func.sig + var_pos = True + var_keyword = False + + with monkeypatch.context() as m: + m.setattr(c, 'func', FuncPatch) + + with pytest.raises(TypeError, match="Cannot have a variable number of positional"): + KerasLayer(c, w, output_dim) + + @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) + def test_var_keyword(self, get_circuit, monkeypatch, output_dim): + """Test if a TypeError is raised when instantiated with a variable number of keyword + arguments""" + c, w = get_circuit + + class FuncPatch: + """Patch for variable number of keyword arguments""" + sig = c.func.sig + var_pos = False + var_keyword = True + + with monkeypatch.context() as m: + m.setattr(c, 'func', FuncPatch) + + with pytest.raises(TypeError, match="Cannot have a variable number of keyword"): + KerasLayer(c, w, output_dim) + + @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) + @pytest.mark.parametrize("input_dim", zip(*[[None, [1], (1,), 1], [None, 1, 1, 1]])) + def test_input_dim(self, get_circuit, input_dim, output_dim): + """Test if the input_dim is correctly processed, i.e., that an iterable is mapped to + its first element while an int or None is left unchanged.""" + c, w = get_circuit + layer = KerasLayer(c, w, output_dim, input_dim[0]) + assert layer.input_dim == input_dim[1] + + @pytest.mark.parametrize("n_qubits", [1]) + @pytest.mark.parametrize("output_dim", zip(*[[[1], (1,), 1], [1, 1, 1]])) + def test_output_dim(self, get_circuit, output_dim): + """Test if the output_dim is correctly processed, i.e., that an iterable is mapped to + its first element while an int is left unchanged.""" + c, w = get_circuit + layer = KerasLayer(c, w, output_dim[0]) + assert layer.output_dim == output_dim[1] + + @pytest.mark.parametrize("n_qubits, output_dim", indices(2)) + def test_weight_shapes(self, get_circuit, output_dim, n_qubits): + """Test if the weight_shapes input argument is correctly processed to be a dictionary + with values that are tuples.""" + c, w = get_circuit + layer = KerasLayer(c, w, output_dim) + assert layer.weight_shapes == {'w1': (3, n_qubits, 3), 'w2': (2, n_qubits, 3), 'w3': (1,), 'w4': (1,), + 'w5': (3,)} + + @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) + @pytest.mark.parametrize("weight_specs", zip(*[[None, {"w1": {}}], [{}, {"w1": {}}]])) + def test_weight_specs_initialize(self, get_circuit, output_dim, weight_specs): + """Test if the weight_specs input argument is correctly processed, so that it + initializes to an empty dictionary if not specified but is left unchanged if already a + dictionary""" + c, w = get_circuit + layer = KerasLayer(c, w, output_dim, weight_specs=weight_specs[0]) + assert layer.weight_specs == weight_specs[1] + + @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) + def test_build_wrong_input_shape(self, get_circuit, output_dim): + """Test if the build() method raises a ValueError if the user has specified an input + dimension but build() is called with a different dimension. Note that the input_shape + passed to build is a tuple to include a batch dimension""" + c, w = get_circuit + layer = KerasLayer(c, w, output_dim, input_dim=4) + with pytest.raises(ValueError, match="QNode can only accept inputs of size"): + layer.build(input_shape=(10, 3)) From 80e2112d920477d5a6aff163c1c244e3be9b891c Mon Sep 17 00:00:00 2001 From: trbromley Date: Wed, 11 Mar 2020 12:05:14 -0400 Subject: [PATCH 09/65] Add unit tests --- pennylane/qnn.py | 5 ++- tests/test_qnn.py | 90 ++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 89 insertions(+), 6 deletions(-) diff --git a/pennylane/qnn.py b/pennylane/qnn.py index d47b9110b45..ef6a462ba78 100644 --- a/pennylane/qnn.py +++ b/pennylane/qnn.py @@ -86,7 +86,10 @@ def call(self, inputs, **kwargs): for arg in self.sig: if arg is not self.input_arg: w = self.qnode_weights[arg] - qnode = functools.partial(qnode, w) + if w.shape == (1,): + qnode = functools.partial(qnode, w[0]) + else: + qnode = functools.partial(qnode, w) outputs = tf.stack([qnode(**{self.input_arg: x}) for x in inputs]) input_shape = tf.shape(inputs) diff --git a/tests/test_qnn.py b/tests/test_qnn.py index 5a245228ef6..06984b02fc7 100644 --- a/tests/test_qnn.py +++ b/tests/test_qnn.py @@ -16,6 +16,9 @@ """ import pytest import numpy as np +import tensorflow as tf +from tensorflow.keras.layers import Layer +from tensorflow.keras.initializers import RandomNormal import pennylane as qml from pennylane.qnn import KerasLayer @@ -34,12 +37,12 @@ def get_circuit(n_qubits, output_dim): def circuit(w1, w2, w3, w4, w5, x=None): """A circuit that embeds data using the AngleEmbedding and then performs a variety of operations. The output is a PauliZ measurement on the first output_dim qubits.""" - qml.templates.AngleEmbedding(x) - qml.templates.StronglyEntanglingLayers(w1, wires=list(range(n_qubits))) - qml.templates.StronglyEntanglingLayers(w2, wires=list(range(n_qubits))) - qml.RX(w3, wires=0) + qml.templates.AngleEmbedding(x, wires=list(range(n_qubits))) + # qml.templates.StronglyEntanglingLayers(w1, wires=list(range(n_qubits))) + # qml.templates.StronglyEntanglingLayers(w2, wires=list(range(n_qubits))) + # qml.RX(w3, wires=0) qml.RX(w4, wires=0) - qml.Rot(w5, wires=0) + qml.Rot(*w5, wires=0) return [qml.expval(qml.PauliZ(i)) for i in range(output_dim)] return circuit, weight_shapes @@ -170,3 +173,80 @@ def test_build_wrong_input_shape(self, get_circuit, output_dim): layer = KerasLayer(c, w, output_dim, input_dim=4) with pytest.raises(ValueError, match="QNode can only accept inputs of size"): layer.build(input_shape=(10, 3)) + + @pytest.mark.parametrize("n_qubits, output_dim", indices(2)) + def test_qnode_weights(self, get_circuit, n_qubits, output_dim): + """Test if the build() method correctly initializes the weights in the qnode_weights + dictionary, i.e., that each value of the dictionary has correct shape and name.""" + c, w = get_circuit + layer = KerasLayer(c, w, output_dim) + layer.build(input_shape=(10, n_qubits)) + + for weight, shape in layer.weight_shapes.items(): + assert layer.qnode_weights[weight].shape == shape + assert layer.qnode_weights[weight].name[:-2] == weight + + @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) + def test_qnode_weights_with_spec(self, get_circuit, monkeypatch, output_dim, n_qubits): + """Test if the build() method correctly passes on user specified weight_specs to the + inherited add_weight() method. This is done by monkeypatching add_weight() so that it + simply returns its input keyword arguments. The qnode_weights dictionary should then have + values that are the input keyword arguments, and we check that the specified weight_specs + keywords are there.""" + + def add_weight_dummy(*args, **kwargs): + return kwargs + + weight_specs = { + "w1": {"initializer": "random_uniform", "trainable": False}, + "w2": {"initializer": RandomNormal(mean=0, stddev=0.5)}, + "w3": {}, + "w4": {}, + "w5": {}, + } + + with monkeypatch.context() as m: + m.setattr(Layer, 'add_weight', add_weight_dummy) + c, w = get_circuit + layer = KerasLayer(c, w, output_dim, weight_specs=weight_specs) + layer.build(input_shape=(10, n_qubits)) + + for weight in layer.weight_shapes: + assert all(item in layer.qnode_weights[weight].items() for item in weight_specs[ + weight].items()) + + @pytest.mark.parametrize("n_qubits, output_dim", indices(3)) + @pytest.mark.parametrize("input_shape", [(10, 4), (8, 3)]) + def test_compute_output_shape(self, get_circuit, output_dim, input_shape): + """Test if the compute_output_shape() method performs correctly, i.e., that it replaces + the last element in the input_shape tuple with the specified output_dim and that the + output shape is of type tf.TensorShape""" + c, w = get_circuit + layer = KerasLayer(c, w, output_dim) + + assert layer.compute_output_shape(input_shape) == (input_shape[0], output_dim) + assert isinstance(layer.compute_output_shape(input_shape), tf.TensorShape) + + @pytest.mark.parametrize("n_qubits, output_dim", indices(4)) + @pytest.mark.parametrize("batch_size", [5, 10, 15]) + def test_call(self, get_circuit, output_dim, batch_size, n_qubits): + """Test if the call() method performs correctly, i.e., that it outputs with shape + (batch_size, output_dim) with results that agree with directly calling the QNode""" + c, w = get_circuit + layer = KerasLayer(c, w, output_dim) + x = tf.ones((batch_size, n_qubits)) + + layer_out = layer(x) + weights = [w[0] if w.shape == (1,) else w for w in layer.qnode_weights.values()] + + assert layer_out.shape == (batch_size, output_dim) + assert np.allclose(layer_out[0], c(*weights, x=x[0])) + + @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) + def test_str_repr(self, get_circuit, output_dim): + """Tests the __str__ and __repr__ representations""" + c, w = get_circuit + layer = KerasLayer(c, w, output_dim) + + assert layer.__str__() == "" + assert layer.__repr__() == "" From 518a60f324f7a03fe004ecd747e6dc41c210953c Mon Sep 17 00:00:00 2001 From: trbromley Date: Wed, 11 Mar 2020 12:34:29 -0400 Subject: [PATCH 10/65] Update tests --- tests/test_qnn.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/test_qnn.py b/tests/test_qnn.py index 06984b02fc7..552f033e888 100644 --- a/tests/test_qnn.py +++ b/tests/test_qnn.py @@ -38,9 +38,11 @@ def circuit(w1, w2, w3, w4, w5, x=None): """A circuit that embeds data using the AngleEmbedding and then performs a variety of operations. The output is a PauliZ measurement on the first output_dim qubits.""" qml.templates.AngleEmbedding(x, wires=list(range(n_qubits))) - # qml.templates.StronglyEntanglingLayers(w1, wires=list(range(n_qubits))) + + qml.templates.StronglyEntanglingLayers(w1, wires=list(range(n_qubits))) + # print(w2.shape, n_qubits, output_dim) # qml.templates.StronglyEntanglingLayers(w2, wires=list(range(n_qubits))) - # qml.RX(w3, wires=0) + qml.RX(w3, wires=0) qml.RX(w4, wires=0) qml.Rot(*w5, wires=0) return [qml.expval(qml.PauliZ(i)) for i in range(output_dim)] @@ -250,3 +252,7 @@ def test_str_repr(self, get_circuit, output_dim): assert layer.__str__() == "" assert layer.__repr__() == "" + + +# class TestKerasLayerIntegration: +# """Integration tests for the pennylane.qnn.KerasLayer class.""" From a2b2ae550410f3df115567bea2e59307c08efb87 Mon Sep 17 00:00:00 2001 From: trbromley Date: Wed, 11 Mar 2020 13:16:47 -0400 Subject: [PATCH 11/65] Update test --- tests/test_qnn.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/test_qnn.py b/tests/test_qnn.py index 552f033e888..24efbdbc1e5 100644 --- a/tests/test_qnn.py +++ b/tests/test_qnn.py @@ -38,10 +38,9 @@ def circuit(w1, w2, w3, w4, w5, x=None): """A circuit that embeds data using the AngleEmbedding and then performs a variety of operations. The output is a PauliZ measurement on the first output_dim qubits.""" qml.templates.AngleEmbedding(x, wires=list(range(n_qubits))) - - qml.templates.StronglyEntanglingLayers(w1, wires=list(range(n_qubits))) - # print(w2.shape, n_qubits, output_dim) - # qml.templates.StronglyEntanglingLayers(w2, wires=list(range(n_qubits))) + if n_qubits > 1: + qml.templates.StronglyEntanglingLayers(w1, wires=list(range(n_qubits))) + qml.templates.StronglyEntanglingLayers(w2, wires=list(range(n_qubits))) qml.RX(w3, wires=0) qml.RX(w4, wires=0) qml.Rot(*w5, wires=0) From f652a057a5450922e9414b5a243fe1fd4b8fa3be Mon Sep 17 00:00:00 2001 From: trbromley Date: Wed, 11 Mar 2020 16:19:44 -0400 Subject: [PATCH 12/65] Update integration tests --- tests/test_qnn.py | 78 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 5 deletions(-) diff --git a/tests/test_qnn.py b/tests/test_qnn.py index 24efbdbc1e5..d8ec5d9269c 100644 --- a/tests/test_qnn.py +++ b/tests/test_qnn.py @@ -24,10 +24,10 @@ from pennylane.qnn import KerasLayer -@pytest.fixture() +@pytest.fixture def get_circuit(n_qubits, output_dim): """Fixture for getting a sample quantum circuit with a controllable qubit number and output - dimension. Returns both the circuit and the shape of the weights""" + dimension. Returns both the circuit and the shape of the weights.""" dev = qml.device("default.qubit", wires=n_qubits) weight_shapes = {"w1": (3, n_qubits, 3), "w2": (2, n_qubits, 3), "w3": (1,), "w4": 1, @@ -41,6 +41,12 @@ def circuit(w1, w2, w3, w4, w5, x=None): if n_qubits > 1: qml.templates.StronglyEntanglingLayers(w1, wires=list(range(n_qubits))) qml.templates.StronglyEntanglingLayers(w2, wires=list(range(n_qubits))) + else: # TODO tidy up + for w in w1: + qml.Rot(w[0][0], w[0][1], w[0][2], wires=0) + for w in w2: + qml.Rot(w[0][0], w[0][1], w[0][2], wires=0) + qml.RX(w3, wires=0) qml.RX(w4, wires=0) qml.Rot(*w5, wires=0) @@ -49,7 +55,29 @@ def circuit(w1, w2, w3, w4, w5, x=None): return circuit, weight_shapes +@pytest.mark.usefixtures("get_circuit") +@pytest.fixture +def model(get_circuit, n_qubits, output_dim): + """Fixture for creating a hybrid Keras model. The model is composed of KerasLayers sandwiched + between Dense layers.""" + c, w = get_circuit + layer1 = KerasLayer(c, w, output_dim) + layer2 = KerasLayer(c, w, output_dim) + + model = tf.keras.models.Sequential([ + tf.keras.layers.Dense(n_qubits), + layer1, + tf.keras.layers.Dense(n_qubits), + layer2, + tf.keras.layers.Dense(output_dim), + ]) + + return model + + def indices(n_max): + """Returns an iterator over the number of qubits and output dimension, up to value n_max. + The output dimension never exceeds the number of qubits.""" a, b = np.tril_indices(n_max) return zip(*[a + 1, b + 1]) @@ -196,6 +224,8 @@ def test_qnode_weights_with_spec(self, get_circuit, monkeypatch, output_dim, n_q keywords are there.""" def add_weight_dummy(*args, **kwargs): + """Dummy function for mocking out the add_weight method to simply return the input + keyword arguments""" return kwargs weight_specs = { @@ -245,7 +275,7 @@ def test_call(self, get_circuit, output_dim, batch_size, n_qubits): @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) def test_str_repr(self, get_circuit, output_dim): - """Tests the __str__ and __repr__ representations""" + """Test the __str__ and __repr__ representations""" c, w = get_circuit layer = KerasLayer(c, w, output_dim) @@ -253,5 +283,43 @@ def test_str_repr(self, get_circuit, output_dim): assert layer.__repr__() == "" -# class TestKerasLayerIntegration: -# """Integration tests for the pennylane.qnn.KerasLayer class.""" +tf.keras.backend.set_floatx('float64') # TODO fix + + +@pytest.mark.usefixtures("get_circuit", "model") +class TestKerasLayerIntegration: + """Integration tests for the pennylane.qnn.KerasLayer class.""" + + @pytest.mark.parametrize("n_qubits, output_dim", indices(2)) + @pytest.mark.parametrize("batch_size", [5, 10]) + def test_train_model(self, model, batch_size, n_qubits, output_dim): + """Test if a model can train using the KerasLayer. The model is composed of a single + KerasLayer sandwiched between two Dense layers, and the dataset is simply input and output + vectors of zeros. The test checks that the loss function after two epochs is less than + the loss function after one epoch, indicating that training is taking place.""" + + x = np.zeros((5, n_qubits)) + y = np.zeros((5, output_dim)) + + model.compile(optimizer='sgd', loss='mse') + + result = model.fit(x, y, epochs=2, batch_size=batch_size, verbose=0) + loss = result.history['loss'] + + assert loss[0] > loss[-1] + + @pytest.mark.parametrize("n_qubits, output_dim", indices(2)) + @pytest.mark.parametrize("batch_size", [5, 10]) + def test_model_gradients(self, model, output_dim, batch_size, n_qubits): + """Test if a gradient can be calculated with respect to all of the trainable variables in + the model""" + x = np.zeros((5, n_qubits)) + y = np.zeros((5, output_dim)) + + with tf.GradientTape() as tape: + out = model(x) + loss = tf.keras.losses.mean_squared_error(out, y) + + gradients = tape.gradient(loss, model.trainable_variables) + + assert all([not isinstance(g, type(None)) for g in gradients]) From 12ddea79fb311cd05c30690ff77d36b8dc14a3d3 Mon Sep 17 00:00:00 2001 From: trbromley Date: Wed, 11 Mar 2020 16:59:27 -0400 Subject: [PATCH 13/65] Add to integration tests --- tests/test_qnn.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/tests/test_qnn.py b/tests/test_qnn.py index d8ec5d9269c..7b6b6f1fb03 100644 --- a/tests/test_qnn.py +++ b/tests/test_qnn.py @@ -15,10 +15,12 @@ Tests for the pennylane.qnn module. """ import pytest +import os import numpy as np import tensorflow as tf from tensorflow.keras.layers import Layer from tensorflow.keras.initializers import RandomNormal +import tempfile import pennylane as qml from pennylane.qnn import KerasLayer @@ -309,8 +311,7 @@ def test_train_model(self, model, batch_size, n_qubits, output_dim): assert loss[0] > loss[-1] @pytest.mark.parametrize("n_qubits, output_dim", indices(2)) - @pytest.mark.parametrize("batch_size", [5, 10]) - def test_model_gradients(self, model, output_dim, batch_size, n_qubits): + def test_model_gradients(self, model, output_dim, n_qubits): """Test if a gradient can be calculated with respect to all of the trainable variables in the model""" x = np.zeros((5, n_qubits)) @@ -323,3 +324,21 @@ def test_model_gradients(self, model, output_dim, batch_size, n_qubits): gradients = tape.gradient(loss, model.trainable_variables) assert all([not isinstance(g, type(None)) for g in gradients]) + + @pytest.mark.parametrize("n_qubits, output_dim", indices(2)) + def test_model_save_weights(self, model, n_qubits): + """Test if the model can be successfully saved and reloaded using the get_weights() + method""" + _, filename = tempfile.mkstemp() + prediction = model.predict(np.ones(n_qubits)) + weights = model.get_weights() + model.save_weights(filename) + model.load_weights(filename) + prediction_loaded = model.predict(np.ones(n_qubits)) + weights_loaded = model.get_weights() + + assert np.allclose(prediction, prediction_loaded) + for i, w in enumerate(weights): + assert np.allclose(w, weights_loaded[i]) + + os.remove(filename) From 9e2bf39f797fa73fa47aa2c66ebe9a99cf6cd297 Mon Sep 17 00:00:00 2001 From: trbromley Date: Thu, 12 Mar 2020 09:24:34 -0400 Subject: [PATCH 14/65] Allow for multiple types of tf.float --- pennylane/interfaces/tf.py | 16 ++++++++++++---- tests/test_qnn.py | 3 --- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/pennylane/interfaces/tf.py b/pennylane/interfaces/tf.py index 677c3f58a50..f46838d6fde 100644 --- a/pennylane/interfaces/tf.py +++ b/pennylane/interfaces/tf.py @@ -100,6 +100,7 @@ def grad(grad_output, **tfkwargs): jacobian = qnode.jacobian(args, kwargs) grad_output_np = grad_output.numpy() + grad_output_type = grad_output.dtype # perform the vector-Jacobian product if not grad_output_np.shape: @@ -111,17 +112,24 @@ def grad(grad_output, **tfkwargs): grad_input = unflatten(temp.flat, args) if isinstance(grad_input, list): - grad_input = [tf.convert_to_tensor(i) for i in grad_input] + grad_input = [tf.convert_to_tensor(i, dtype=grad_output_type) for i in grad_input] elif isinstance(grad_input, tuple): - grad_input = tuple(tf.convert_to_tensor(i) for i in grad_input) + grad_input = tuple(tf.convert_to_tensor(i, dtype=grad_output_type) for i in grad_input) else: - grad_input = tf.convert_to_tensor(grad_input) + grad_input = tf.convert_to_tensor(grad_input, dtype=grad_output_type) if variables is not None: return grad_input, variables return grad_input - return res, grad + if args: + target_dtype = args[0].dtype + elif kwargs: + target_dtype = kwargs.values()[0].dtype + else: + target_dtype = tf.float32 + + return tf.cast(res, target_dtype), grad return _TFQNode diff --git a/tests/test_qnn.py b/tests/test_qnn.py index 7b6b6f1fb03..0b44a4e0607 100644 --- a/tests/test_qnn.py +++ b/tests/test_qnn.py @@ -285,9 +285,6 @@ def test_str_repr(self, get_circuit, output_dim): assert layer.__repr__() == "" -tf.keras.backend.set_floatx('float64') # TODO fix - - @pytest.mark.usefixtures("get_circuit", "model") class TestKerasLayerIntegration: """Integration tests for the pennylane.qnn.KerasLayer class.""" From c47ce830b71bd34984d702aca6020e17434d32ff Mon Sep 17 00:00:00 2001 From: trbromley Date: Thu, 12 Mar 2020 09:41:54 -0400 Subject: [PATCH 15/65] Revert changes --- pennylane/interfaces/tf.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/pennylane/interfaces/tf.py b/pennylane/interfaces/tf.py index f46838d6fde..677c3f58a50 100644 --- a/pennylane/interfaces/tf.py +++ b/pennylane/interfaces/tf.py @@ -100,7 +100,6 @@ def grad(grad_output, **tfkwargs): jacobian = qnode.jacobian(args, kwargs) grad_output_np = grad_output.numpy() - grad_output_type = grad_output.dtype # perform the vector-Jacobian product if not grad_output_np.shape: @@ -112,24 +111,17 @@ def grad(grad_output, **tfkwargs): grad_input = unflatten(temp.flat, args) if isinstance(grad_input, list): - grad_input = [tf.convert_to_tensor(i, dtype=grad_output_type) for i in grad_input] + grad_input = [tf.convert_to_tensor(i) for i in grad_input] elif isinstance(grad_input, tuple): - grad_input = tuple(tf.convert_to_tensor(i, dtype=grad_output_type) for i in grad_input) + grad_input = tuple(tf.convert_to_tensor(i) for i in grad_input) else: - grad_input = tf.convert_to_tensor(grad_input, dtype=grad_output_type) + grad_input = tf.convert_to_tensor(grad_input) if variables is not None: return grad_input, variables return grad_input - if args: - target_dtype = args[0].dtype - elif kwargs: - target_dtype = kwargs.values()[0].dtype - else: - target_dtype = tf.float32 - - return tf.cast(res, target_dtype), grad + return res, grad return _TFQNode From 27a559a2dc5312ab5cee1fecd5920b3121bda8e6 Mon Sep 17 00:00:00 2001 From: trbromley Date: Thu, 12 Mar 2020 10:16:59 -0400 Subject: [PATCH 16/65] Allow for output type to be set as an argument --- pennylane/interfaces/tf.py | 12 +++++++----- pennylane/qnn.py | 3 ++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/pennylane/interfaces/tf.py b/pennylane/interfaces/tf.py index 677c3f58a50..8a0ff00814d 100644 --- a/pennylane/interfaces/tf.py +++ b/pennylane/interfaces/tf.py @@ -32,11 +32,13 @@ from tensorflow import Variable # pylint: disable=unused-import,ungrouped-imports -def to_tf(qnode): +def to_tf(qnode, dtype=None): """Function that accepts a :class:`~.QNode`, and returns a TensorFlow eager-execution-compatible QNode. Args: qnode (~pennylane.qnode.QNode): a PennyLane QNode + dtype (tf.DType): target output type of QNode, uses default output type of QNode if not + specified Returns: function: the QNode as a TensorFlow function @@ -111,17 +113,17 @@ def grad(grad_output, **tfkwargs): grad_input = unflatten(temp.flat, args) if isinstance(grad_input, list): - grad_input = [tf.convert_to_tensor(i) for i in grad_input] + grad_input = [tf.convert_to_tensor(i, dtype=dtype) for i in grad_input] elif isinstance(grad_input, tuple): - grad_input = tuple(tf.convert_to_tensor(i) for i in grad_input) + grad_input = tuple(tf.convert_to_tensor(i, dtype=dtype) for i in grad_input) else: - grad_input = tf.convert_to_tensor(grad_input) + grad_input = tf.convert_to_tensor(grad_input, dtype=dtype) if variables is not None: return grad_input, variables return grad_input - return res, grad + return tf.convert_to_tensor(res, dtype=dtype), grad return _TFQNode diff --git a/pennylane/qnn.py b/pennylane/qnn.py index ef6a462ba78..13920f26b41 100644 --- a/pennylane/qnn.py +++ b/pennylane/qnn.py @@ -19,6 +19,7 @@ import tensorflow as tf from pennylane.qnodes import QNode +from pennylane.interfaces.tf import to_tf if int(tf.__version__.split(".")[0]) < 2: raise ImportError("TensorFlow version 2 or above is required for this module") @@ -55,7 +56,7 @@ def __init__( if qnode.func.var_keyword: raise TypeError("Cannot have a variable number of keyword arguments") - self.qnode = qnode + self.qnode = to_tf(qnode, dtype=tf.keras.backend.floatx()) self.input_dim = input_dim[0] if isinstance(input_dim, Iterable) else input_dim self.weight_shapes = { weight: (tuple(size) if isinstance(size, Iterable) else (size,)) From 7b2ecfc5dab9613b578dbcf915774d5957ebfe8a Mon Sep 17 00:00:00 2001 From: trbromley Date: Thu, 12 Mar 2020 10:23:09 -0400 Subject: [PATCH 17/65] Simplify circuit in test --- tests/test_qnn.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/tests/test_qnn.py b/tests/test_qnn.py index 0b44a4e0607..042bf74aed4 100644 --- a/tests/test_qnn.py +++ b/tests/test_qnn.py @@ -40,15 +40,8 @@ def circuit(w1, w2, w3, w4, w5, x=None): """A circuit that embeds data using the AngleEmbedding and then performs a variety of operations. The output is a PauliZ measurement on the first output_dim qubits.""" qml.templates.AngleEmbedding(x, wires=list(range(n_qubits))) - if n_qubits > 1: - qml.templates.StronglyEntanglingLayers(w1, wires=list(range(n_qubits))) - qml.templates.StronglyEntanglingLayers(w2, wires=list(range(n_qubits))) - else: # TODO tidy up - for w in w1: - qml.Rot(w[0][0], w[0][1], w[0][2], wires=0) - for w in w2: - qml.Rot(w[0][0], w[0][1], w[0][2], wires=0) - + qml.templates.StronglyEntanglingLayers(w1, wires=list(range(n_qubits))) + qml.templates.StronglyEntanglingLayers(w2, wires=list(range(n_qubits))) qml.RX(w3, wires=0) qml.RX(w4, wires=0) qml.Rot(*w5, wires=0) From 709962fce0315376d316720da528c55d6c53200e Mon Sep 17 00:00:00 2001 From: trbromley Date: Thu, 12 Mar 2020 14:48:24 -0400 Subject: [PATCH 18/65] Update tests --- pennylane/qnn.py | 57 ++++++++++++---------- tests/test_qnn.py | 119 +++++++++++++++++++++++++++++++++++----------- 2 files changed, 124 insertions(+), 52 deletions(-) diff --git a/pennylane/qnn.py b/pennylane/qnn.py index 13920f26b41..a1e86027f16 100644 --- a/pennylane/qnn.py +++ b/pennylane/qnn.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from collections.abc import Iterable +from collections import Iterable import functools import inspect from typing import Optional @@ -26,6 +26,8 @@ else: from tensorflow.keras.layers import Layer +INPUT_ARG = 'inputs' + class KerasLayer(Layer): def __init__( @@ -37,19 +39,13 @@ def __init__( weight_specs: Optional[dict] = None, **kwargs ): - self.sig = qnode.func.sig - defaults = [ - name for name, sig in self.sig.items() if sig.par.default != inspect.Parameter.empty - ] - if len(defaults) != 1: - raise TypeError("Conversion to a Keras layer requires a QNode with a single " - "default argument") - self.input_arg = defaults[0] - - if self.input_arg in set(weight_shapes.keys()): - raise ValueError("Input argument dimension should not be specified in weight_shapes") - if set(weight_shapes.keys()) | set(self.input_arg) != set(self.sig.keys()): + if INPUT_ARG not in self.sig: + raise TypeError("QNode must include an argument with name {} for inputting data".format(INPUT_ARG)) + if INPUT_ARG in set(weight_shapes.keys()): + raise ValueError("{} argument should not have its dimension specified in " + "weight_shapes".format(INPUT_ARG)) + if set(weight_shapes.keys()) | {INPUT_ARG} != set(self.sig.keys()): raise ValueError("Must specify a shape for every non-input parameter in the QNode") if qnode.func.var_pos: raise TypeError("Cannot have a variable number of positional arguments") @@ -64,10 +60,18 @@ def __init__( } self.output_dim = output_dim[0] if isinstance(output_dim, Iterable) else output_dim + defaults = { + name for name, sig in self.sig.items() if sig.par.default != inspect.Parameter.empty + } + self.input_is_default = True if INPUT_ARG in defaults else False + if defaults - {INPUT_ARG} != set(): + raise TypeError("Only the argument {} is permitted to have a default".format(INPUT_ARG)) + if weight_specs: self.weight_specs = weight_specs else: self.weight_specs = {} + self.qnode_weights = {} super(KerasLayer, self).__init__(dynamic=True, **kwargs) @@ -83,19 +87,24 @@ def build(self, input_shape): super(KerasLayer, self).build(input_shape) def call(self, inputs, **kwargs): - qnode = self.qnode - for arg in self.sig: - if arg is not self.input_arg: - w = self.qnode_weights[arg] - if w.shape == (1,): - qnode = functools.partial(qnode, w[0]) + outputs = [] + for x in inputs: + qnode = self.qnode + for arg in self.sig: + if arg is not INPUT_ARG: + w = self.qnode_weights[arg] + if w.shape == (1,): + qnode = functools.partial(qnode, w[0]) + else: + qnode = functools.partial(qnode, w) else: - qnode = functools.partial(qnode, w) - - outputs = tf.stack([qnode(**{self.input_arg: x}) for x in inputs]) - input_shape = tf.shape(inputs) + if self.input_is_default: + qnode = functools.partial(qnode, **{INPUT_ARG: x}) + else: + qnode = functools.partial(qnode, x) + outputs.append(qnode()) - return tf.reshape(outputs, self.compute_output_shape(input_shape)) + return tf.stack(outputs) def compute_output_shape(self, input_shape): return tf.TensorShape([input_shape[0], self.output_dim]) diff --git a/tests/test_qnn.py b/tests/test_qnn.py index 042bf74aed4..fad9c727f11 100644 --- a/tests/test_qnn.py +++ b/tests/test_qnn.py @@ -32,19 +32,20 @@ def get_circuit(n_qubits, output_dim): dimension. Returns both the circuit and the shape of the weights.""" dev = qml.device("default.qubit", wires=n_qubits) - weight_shapes = {"w1": (3, n_qubits, 3), "w2": (2, n_qubits, 3), "w3": (1,), "w4": 1, - "w5": [3]} + weight_shapes = {"w1": (3, n_qubits, 3), "w2": (1,), "w3": 1, + "w4": [3], "w5": (2, n_qubits, 3)} @qml.qnode(dev, interface='tf') - def circuit(w1, w2, w3, w4, w5, x=None): + def circuit(inputs, w1, w2, w3, w4, w5): """A circuit that embeds data using the AngleEmbedding and then performs a variety of - operations. The output is a PauliZ measurement on the first output_dim qubits.""" - qml.templates.AngleEmbedding(x, wires=list(range(n_qubits))) + operations. The output is a PauliZ measurement on the first output_dim qubits. One set of + parameters, w5, are specified as non-trainable.""" + qml.templates.AngleEmbedding(inputs, wires=list(range(n_qubits))) qml.templates.StronglyEntanglingLayers(w1, wires=list(range(n_qubits))) - qml.templates.StronglyEntanglingLayers(w2, wires=list(range(n_qubits))) + qml.RX(w2, wires=0) qml.RX(w3, wires=0) - qml.RX(w4, wires=0) - qml.Rot(*w5, wires=0) + qml.Rot(*w4, wires=0) + qml.templates.StronglyEntanglingLayers(w5, wires=list(range(n_qubits))) return [qml.expval(qml.PauliZ(i)) for i in range(output_dim)] return circuit, weight_shapes @@ -82,19 +83,12 @@ class TestKerasLayer: """Unit tests for the pennylane.qnn.KerasLayer class.""" @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) - def test_too_many_defaults(self, get_circuit, output_dim): - """Test if a TypeError is raised when instantiated with a QNode that has two defaults""" + def test_no_input(self, get_circuit, output_dim): + """Test if a TypeError is raised when instantiated with a QNode that does not have an + INPUT_ARG argument""" c, w = get_circuit - c.func.sig['x2'] = c.func.sig['x'] - with pytest.raises(TypeError, match="Conversion to a Keras layer requires"): - KerasLayer(c, w, output_dim) - - @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) - def test_no_defaults(self, get_circuit, output_dim): - """Test if a TypeError is raised when instantiated with a QNode that has no defaults""" - c, w = get_circuit - del c.func.sig['x'] - with pytest.raises(TypeError, match="Conversion to a Keras layer requires"): + del c.func.sig[qml.qnn.INPUT_ARG] + with pytest.raises(TypeError, match="QNode must include an argument with name"): KerasLayer(c, w, output_dim) @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) @@ -102,8 +96,8 @@ def test_input_in_weight_shapes(self, get_circuit, n_qubits, output_dim): """Test if a ValueError is raised when instantiated with a weight_shapes dictionary that contains the shape of the input""" c, w = get_circuit - w['x'] = n_qubits - with pytest.raises(ValueError, match="Input argument dimension should not"): + w[qml.qnn.INPUT_ARG] = n_qubits + with pytest.raises(ValueError, match="{} argument should not have its dimension".format(qml.qnn.INPUT_ARG)): KerasLayer(c, w, output_dim) @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) @@ -175,8 +169,22 @@ def test_weight_shapes(self, get_circuit, output_dim, n_qubits): with values that are tuples.""" c, w = get_circuit layer = KerasLayer(c, w, output_dim) - assert layer.weight_shapes == {'w1': (3, n_qubits, 3), 'w2': (2, n_qubits, 3), 'w3': (1,), 'w4': (1,), - 'w5': (3,)} + assert layer.weight_shapes == {"w1": (3, n_qubits, 3), "w2": (1,), "w3": (1,), + "w4": (3,), "w5": (2, n_qubits, 3)} + + @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) + def test_non_input_defaults(self, get_circuit, output_dim, n_qubits): + """Test if a TypeError is raised when default arguments that are not INPUT_ARG are + present in the QNode""" + c, w = get_circuit + + @qml.qnode(qml.device('default.qubit', wires=n_qubits), interface='tf') + def c_dummy(inputs, w1, w2, w3, w4, w5, w6=None): + """Dummy version of the circuit with a default argument""" + return c(inputs, w1, w2, w3, w4, w5) + + with pytest.raises(TypeError, match="Only the argument {} is permitted".format(qml.qnn.INPUT_ARG)): + KerasLayer(c_dummy, {**w, **{"w6": 1}}, output_dim) @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) @pytest.mark.parametrize("weight_specs", zip(*[[None, {"w1": {}}], [{}, {"w1": {}}]])) @@ -266,7 +274,63 @@ def test_call(self, get_circuit, output_dim, batch_size, n_qubits): weights = [w[0] if w.shape == (1,) else w for w in layer.qnode_weights.values()] assert layer_out.shape == (batch_size, output_dim) - assert np.allclose(layer_out[0], c(*weights, x=x[0])) + assert np.allclose(layer_out[0], c(x[0], *weights)) + + @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) + @pytest.mark.parametrize("batch_size", [5]) + def test_call_shuffled_args(self, get_circuit, output_dim, batch_size, n_qubits): + """Test if the call() method performs correctly when the inputs argument is not the first + positional argument, i.e., that it outputs with shape (batch_size, output_dim) with + results that agree with directly calling the QNode""" + c, w = get_circuit + + @qml.qnode(qml.device('default.qubit', wires=n_qubits), interface='tf') + def c_shuffled(w1, inputs, w2, w3, w4, w5): + """Version of the circuit with a shuffled signature""" + qml.templates.AngleEmbedding(inputs, wires=list(range(n_qubits))) + qml.templates.StronglyEntanglingLayers(w1, wires=list(range(n_qubits))) + qml.RX(w2, wires=0) + qml.RX(w3, wires=0) + qml.Rot(*w4, wires=0) + qml.templates.StronglyEntanglingLayers(w5, wires=list(range(n_qubits))) + return [qml.expval(qml.PauliZ(i)) for i in range(output_dim)] + + layer = KerasLayer(c_shuffled, w, output_dim) + x = tf.ones((batch_size, n_qubits)) + + layer_out = layer(x) + weights = [w[0] if w.shape == (1,) else w for w in layer.qnode_weights.values()] + + assert layer_out.shape == (batch_size, output_dim) + assert np.allclose(layer_out[0], c(x[0], *weights)) + + @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) + @pytest.mark.parametrize("batch_size", [5]) + def test_call_default_input(self, get_circuit, output_dim, batch_size, n_qubits): + """Test if the call() method performs correctly when the inputs argument is a default + argument, i.e., that it outputs with shape (batch_size, output_dim) with results that + agree with directly calling the QNode""" + c, w = get_circuit + + @qml.qnode(qml.device('default.qubit', wires=n_qubits), interface='tf') + def c_default(w1, w2, w3, w4, w5, inputs=None): + """Version of the circuit with inputs as a default argument""" + qml.templates.AngleEmbedding(inputs, wires=list(range(n_qubits))) + qml.templates.StronglyEntanglingLayers(w1, wires=list(range(n_qubits))) + qml.RX(w2, wires=0) + qml.RX(w3, wires=0) + qml.Rot(*w4, wires=0) + qml.templates.StronglyEntanglingLayers(w5, wires=list(range(n_qubits))) + return [qml.expval(qml.PauliZ(i)) for i in range(output_dim)] + + layer = KerasLayer(c_default, w, output_dim) + x = tf.ones((batch_size, n_qubits)) + + layer_out = layer(x) + weights = [w[0] if w.shape == (1,) else w for w in layer.qnode_weights.values()] + + assert layer_out.shape == (batch_size, output_dim) + assert np.allclose(layer_out[0], c(x[0], *weights)) @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) def test_str_repr(self, get_circuit, output_dim): @@ -304,15 +368,14 @@ def test_train_model(self, model, batch_size, n_qubits, output_dim): def test_model_gradients(self, model, output_dim, n_qubits): """Test if a gradient can be calculated with respect to all of the trainable variables in the model""" - x = np.zeros((5, n_qubits)) - y = np.zeros((5, output_dim)) + x = tf.zeros((5, n_qubits)) + y = tf.zeros((5, output_dim)) with tf.GradientTape() as tape: out = model(x) loss = tf.keras.losses.mean_squared_error(out, y) gradients = tape.gradient(loss, model.trainable_variables) - assert all([not isinstance(g, type(None)) for g in gradients]) @pytest.mark.parametrize("n_qubits, output_dim", indices(2)) From e05ec43d28210b32ebf556d9e0b65c4da88bb335 Mon Sep 17 00:00:00 2001 From: trbromley Date: Thu, 12 Mar 2020 16:56:07 -0400 Subject: [PATCH 19/65] Run black --- pennylane/qnn.py | 12 ++++++--- tests/test_qnn.py | 64 +++++++++++++++++++++++++++++------------------ 2 files changed, 47 insertions(+), 29 deletions(-) diff --git a/pennylane/qnn.py b/pennylane/qnn.py index a1e86027f16..7f635c0439b 100644 --- a/pennylane/qnn.py +++ b/pennylane/qnn.py @@ -26,7 +26,7 @@ else: from tensorflow.keras.layers import Layer -INPUT_ARG = 'inputs' +INPUT_ARG = "inputs" class KerasLayer(Layer): @@ -41,10 +41,14 @@ def __init__( ): self.sig = qnode.func.sig if INPUT_ARG not in self.sig: - raise TypeError("QNode must include an argument with name {} for inputting data".format(INPUT_ARG)) + raise TypeError( + "QNode must include an argument with name {} for inputting data".format(INPUT_ARG) + ) if INPUT_ARG in set(weight_shapes.keys()): - raise ValueError("{} argument should not have its dimension specified in " - "weight_shapes".format(INPUT_ARG)) + raise ValueError( + "{} argument should not have its dimension specified in " + "weight_shapes".format(INPUT_ARG) + ) if set(weight_shapes.keys()) | {INPUT_ARG} != set(self.sig.keys()): raise ValueError("Must specify a shape for every non-input parameter in the QNode") if qnode.func.var_pos: diff --git a/tests/test_qnn.py b/tests/test_qnn.py index fad9c727f11..033ecf50bcf 100644 --- a/tests/test_qnn.py +++ b/tests/test_qnn.py @@ -32,10 +32,9 @@ def get_circuit(n_qubits, output_dim): dimension. Returns both the circuit and the shape of the weights.""" dev = qml.device("default.qubit", wires=n_qubits) - weight_shapes = {"w1": (3, n_qubits, 3), "w2": (1,), "w3": 1, - "w4": [3], "w5": (2, n_qubits, 3)} + weight_shapes = {"w1": (3, n_qubits, 3), "w2": (1,), "w3": 1, "w4": [3], "w5": (2, n_qubits, 3)} - @qml.qnode(dev, interface='tf') + @qml.qnode(dev, interface="tf") def circuit(inputs, w1, w2, w3, w4, w5): """A circuit that embeds data using the AngleEmbedding and then performs a variety of operations. The output is a PauliZ measurement on the first output_dim qubits. One set of @@ -60,13 +59,15 @@ def model(get_circuit, n_qubits, output_dim): layer1 = KerasLayer(c, w, output_dim) layer2 = KerasLayer(c, w, output_dim) - model = tf.keras.models.Sequential([ - tf.keras.layers.Dense(n_qubits), - layer1, - tf.keras.layers.Dense(n_qubits), - layer2, - tf.keras.layers.Dense(output_dim), - ]) + model = tf.keras.models.Sequential( + [ + tf.keras.layers.Dense(n_qubits), + layer1, + tf.keras.layers.Dense(n_qubits), + layer2, + tf.keras.layers.Dense(output_dim), + ] + ) return model @@ -97,7 +98,9 @@ def test_input_in_weight_shapes(self, get_circuit, n_qubits, output_dim): contains the shape of the input""" c, w = get_circuit w[qml.qnn.INPUT_ARG] = n_qubits - with pytest.raises(ValueError, match="{} argument should not have its dimension".format(qml.qnn.INPUT_ARG)): + with pytest.raises( + ValueError, match="{} argument should not have its dimension".format(qml.qnn.INPUT_ARG) + ): KerasLayer(c, w, output_dim) @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) @@ -105,7 +108,7 @@ def test_weight_shape_unspecified(self, get_circuit, output_dim): """Test if a ValueError is raised when instantiated with a weight missing from the weight_shapes dictionary""" c, w = get_circuit - del w['w1'] + del w["w1"] with pytest.raises(ValueError, match="Must specify a shape for every non-input parameter"): KerasLayer(c, w, output_dim) @@ -117,12 +120,13 @@ def test_var_pos(self, get_circuit, monkeypatch, output_dim): class FuncPatch: """Patch for variable number of keyword arguments""" + sig = c.func.sig var_pos = True var_keyword = False with monkeypatch.context() as m: - m.setattr(c, 'func', FuncPatch) + m.setattr(c, "func", FuncPatch) with pytest.raises(TypeError, match="Cannot have a variable number of positional"): KerasLayer(c, w, output_dim) @@ -135,12 +139,13 @@ def test_var_keyword(self, get_circuit, monkeypatch, output_dim): class FuncPatch: """Patch for variable number of keyword arguments""" + sig = c.func.sig var_pos = False var_keyword = True with monkeypatch.context() as m: - m.setattr(c, 'func', FuncPatch) + m.setattr(c, "func", FuncPatch) with pytest.raises(TypeError, match="Cannot have a variable number of keyword"): KerasLayer(c, w, output_dim) @@ -169,8 +174,13 @@ def test_weight_shapes(self, get_circuit, output_dim, n_qubits): with values that are tuples.""" c, w = get_circuit layer = KerasLayer(c, w, output_dim) - assert layer.weight_shapes == {"w1": (3, n_qubits, 3), "w2": (1,), "w3": (1,), - "w4": (3,), "w5": (2, n_qubits, 3)} + assert layer.weight_shapes == { + "w1": (3, n_qubits, 3), + "w2": (1,), + "w3": (1,), + "w4": (3,), + "w5": (2, n_qubits, 3), + } @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) def test_non_input_defaults(self, get_circuit, output_dim, n_qubits): @@ -178,12 +188,14 @@ def test_non_input_defaults(self, get_circuit, output_dim, n_qubits): present in the QNode""" c, w = get_circuit - @qml.qnode(qml.device('default.qubit', wires=n_qubits), interface='tf') + @qml.qnode(qml.device("default.qubit", wires=n_qubits), interface="tf") def c_dummy(inputs, w1, w2, w3, w4, w5, w6=None): """Dummy version of the circuit with a default argument""" return c(inputs, w1, w2, w3, w4, w5) - with pytest.raises(TypeError, match="Only the argument {} is permitted".format(qml.qnn.INPUT_ARG)): + with pytest.raises( + TypeError, match="Only the argument {} is permitted".format(qml.qnn.INPUT_ARG) + ): KerasLayer(c_dummy, {**w, **{"w6": 1}}, output_dim) @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) @@ -240,14 +252,16 @@ def add_weight_dummy(*args, **kwargs): } with monkeypatch.context() as m: - m.setattr(Layer, 'add_weight', add_weight_dummy) + m.setattr(Layer, "add_weight", add_weight_dummy) c, w = get_circuit layer = KerasLayer(c, w, output_dim, weight_specs=weight_specs) layer.build(input_shape=(10, n_qubits)) for weight in layer.weight_shapes: - assert all(item in layer.qnode_weights[weight].items() for item in weight_specs[ - weight].items()) + assert all( + item in layer.qnode_weights[weight].items() + for item in weight_specs[weight].items() + ) @pytest.mark.parametrize("n_qubits, output_dim", indices(3)) @pytest.mark.parametrize("input_shape", [(10, 4), (8, 3)]) @@ -284,7 +298,7 @@ def test_call_shuffled_args(self, get_circuit, output_dim, batch_size, n_qubits) results that agree with directly calling the QNode""" c, w = get_circuit - @qml.qnode(qml.device('default.qubit', wires=n_qubits), interface='tf') + @qml.qnode(qml.device("default.qubit", wires=n_qubits), interface="tf") def c_shuffled(w1, inputs, w2, w3, w4, w5): """Version of the circuit with a shuffled signature""" qml.templates.AngleEmbedding(inputs, wires=list(range(n_qubits))) @@ -312,7 +326,7 @@ def test_call_default_input(self, get_circuit, output_dim, batch_size, n_qubits) agree with directly calling the QNode""" c, w = get_circuit - @qml.qnode(qml.device('default.qubit', wires=n_qubits), interface='tf') + @qml.qnode(qml.device("default.qubit", wires=n_qubits), interface="tf") def c_default(w1, w2, w3, w4, w5, inputs=None): """Version of the circuit with inputs as a default argument""" qml.templates.AngleEmbedding(inputs, wires=list(range(n_qubits))) @@ -357,10 +371,10 @@ def test_train_model(self, model, batch_size, n_qubits, output_dim): x = np.zeros((5, n_qubits)) y = np.zeros((5, output_dim)) - model.compile(optimizer='sgd', loss='mse') + model.compile(optimizer="sgd", loss="mse") result = model.fit(x, y, epochs=2, batch_size=batch_size, verbose=0) - loss = result.history['loss'] + loss = result.history["loss"] assert loss[0] > loss[-1] From 0b89ad3c93956754cc85087d3b017c3c7e2b46a3 Mon Sep 17 00:00:00 2001 From: trbromley Date: Thu, 12 Mar 2020 19:03:28 -0400 Subject: [PATCH 20/65] Apply isort --- pennylane/qnn.py | 17 ++++++++--------- tests/test_qnn.py | 7 ++++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pennylane/qnn.py b/pennylane/qnn.py index 7f635c0439b..9b013cc9982 100644 --- a/pennylane/qnn.py +++ b/pennylane/qnn.py @@ -6,25 +6,24 @@ # http://www.apache.org/licenses/LICENSE-2.0 + + + # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from collections import Iterable import functools import inspect +# See the License for the specific language governing permissions and +# limitations under the License. +from collections.abc import Iterable from typing import Optional import tensorflow as tf +from tensorflow.keras.layers import Layer -from pennylane.qnodes import QNode from pennylane.interfaces.tf import to_tf - -if int(tf.__version__.split(".")[0]) < 2: - raise ImportError("TensorFlow version 2 or above is required for this module") -else: - from tensorflow.keras.layers import Layer +from pennylane.qnodes import QNode INPUT_ARG = "inputs" diff --git a/tests/test_qnn.py b/tests/test_qnn.py index 033ecf50bcf..cf8cc354df0 100644 --- a/tests/test_qnn.py +++ b/tests/test_qnn.py @@ -14,13 +14,14 @@ """ Tests for the pennylane.qnn module. """ -import pytest import os +import tempfile + import numpy as np +import pytest import tensorflow as tf -from tensorflow.keras.layers import Layer from tensorflow.keras.initializers import RandomNormal -import tempfile +from tensorflow.keras.layers import Layer import pennylane as qml from pennylane.qnn import KerasLayer From ebed9ce29dadd3fc534b526ee3d8a9300077b18b Mon Sep 17 00:00:00 2001 From: trbromley Date: Thu, 12 Mar 2020 19:33:44 -0400 Subject: [PATCH 21/65] Tidy up due to PyLint --- pennylane/qnn.py | 21 +++++---------------- tests/test_qnn.py | 29 ----------------------------- 2 files changed, 5 insertions(+), 45 deletions(-) diff --git a/pennylane/qnn.py b/pennylane/qnn.py index 9b013cc9982..84e5dbe9b1b 100644 --- a/pennylane/qnn.py +++ b/pennylane/qnn.py @@ -6,16 +6,13 @@ # http://www.apache.org/licenses/LICENSE-2.0 - - - # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -import functools -import inspect # See the License for the specific language governing permissions and # limitations under the License. +import functools +import inspect from collections.abc import Iterable from typing import Optional @@ -34,7 +31,6 @@ def __init__( qnode: QNode, weight_shapes: dict, output_dim: int, - input_dim: Optional[int] = None, weight_specs: Optional[dict] = None, **kwargs ): @@ -56,7 +52,6 @@ def __init__( raise TypeError("Cannot have a variable number of keyword arguments") self.qnode = to_tf(qnode, dtype=tf.keras.backend.floatx()) - self.input_dim = input_dim[0] if isinstance(input_dim, Iterable) else input_dim self.weight_shapes = { weight: (tuple(size) if isinstance(size, Iterable) else (size,)) for weight, size in weight_shapes.items() @@ -66,30 +61,24 @@ def __init__( defaults = { name for name, sig in self.sig.items() if sig.par.default != inspect.Parameter.empty } - self.input_is_default = True if INPUT_ARG in defaults else False + self.input_is_default = INPUT_ARG in defaults if defaults - {INPUT_ARG} != set(): raise TypeError("Only the argument {} is permitted to have a default".format(INPUT_ARG)) - if weight_specs: - self.weight_specs = weight_specs - else: - self.weight_specs = {} + self.weight_specs = weight_specs if weight_specs is not None else {} self.qnode_weights = {} super(KerasLayer, self).__init__(dynamic=True, **kwargs) def build(self, input_shape): - if self.input_dim and input_shape[-1] != self.input_dim: - raise ValueError("QNode can only accept inputs of size {}".format(self.input_dim)) - for weight, size in self.weight_shapes.items(): spec = self.weight_specs.get(weight, {}) self.qnode_weights[weight] = self.add_weight(name=weight, shape=size, **spec) super(KerasLayer, self).build(input_shape) - def call(self, inputs, **kwargs): + def call(self, inputs): outputs = [] for x in inputs: qnode = self.qnode diff --git a/tests/test_qnn.py b/tests/test_qnn.py index cf8cc354df0..021412f1eb5 100644 --- a/tests/test_qnn.py +++ b/tests/test_qnn.py @@ -151,15 +151,6 @@ class FuncPatch: with pytest.raises(TypeError, match="Cannot have a variable number of keyword"): KerasLayer(c, w, output_dim) - @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) - @pytest.mark.parametrize("input_dim", zip(*[[None, [1], (1,), 1], [None, 1, 1, 1]])) - def test_input_dim(self, get_circuit, input_dim, output_dim): - """Test if the input_dim is correctly processed, i.e., that an iterable is mapped to - its first element while an int or None is left unchanged.""" - c, w = get_circuit - layer = KerasLayer(c, w, output_dim, input_dim[0]) - assert layer.input_dim == input_dim[1] - @pytest.mark.parametrize("n_qubits", [1]) @pytest.mark.parametrize("output_dim", zip(*[[[1], (1,), 1], [1, 1, 1]])) def test_output_dim(self, get_circuit, output_dim): @@ -199,26 +190,6 @@ def c_dummy(inputs, w1, w2, w3, w4, w5, w6=None): ): KerasLayer(c_dummy, {**w, **{"w6": 1}}, output_dim) - @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) - @pytest.mark.parametrize("weight_specs", zip(*[[None, {"w1": {}}], [{}, {"w1": {}}]])) - def test_weight_specs_initialize(self, get_circuit, output_dim, weight_specs): - """Test if the weight_specs input argument is correctly processed, so that it - initializes to an empty dictionary if not specified but is left unchanged if already a - dictionary""" - c, w = get_circuit - layer = KerasLayer(c, w, output_dim, weight_specs=weight_specs[0]) - assert layer.weight_specs == weight_specs[1] - - @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) - def test_build_wrong_input_shape(self, get_circuit, output_dim): - """Test if the build() method raises a ValueError if the user has specified an input - dimension but build() is called with a different dimension. Note that the input_shape - passed to build is a tuple to include a batch dimension""" - c, w = get_circuit - layer = KerasLayer(c, w, output_dim, input_dim=4) - with pytest.raises(ValueError, match="QNode can only accept inputs of size"): - layer.build(input_shape=(10, 3)) - @pytest.mark.parametrize("n_qubits, output_dim", indices(2)) def test_qnode_weights(self, get_circuit, n_qubits, output_dim): """Test if the build() method correctly initializes the weights in the qnode_weights From 6747bc4ed235c0345250dec2bbf5e43de9a0711f Mon Sep 17 00:00:00 2001 From: trbromley Date: Thu, 12 Mar 2020 20:01:38 -0400 Subject: [PATCH 22/65] Add to docstrings --- pennylane/qnn.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/pennylane/qnn.py b/pennylane/qnn.py index 84e5dbe9b1b..591d684e508 100644 --- a/pennylane/qnn.py +++ b/pennylane/qnn.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +"""Test""" import functools import inspect from collections.abc import Iterable @@ -72,6 +73,11 @@ def __init__( super(KerasLayer, self).__init__(dynamic=True, **kwargs) def build(self, input_shape): + """Initializes the :class:`~.KerasLayer` weights. + + Args: + input_shape (tuple or tf.TensorShape): shape of input data + """ for weight, size in self.weight_shapes.items(): spec = self.weight_specs.get(weight, {}) self.qnode_weights[weight] = self.add_weight(name=weight, shape=size, **spec) @@ -79,6 +85,15 @@ def build(self, input_shape): super(KerasLayer, self).build(input_shape) def call(self, inputs): + """Evaluates the :class:`~.KerasLayer` on input data using the + :attr:`~.KerasLayer.qnode_weights`. + + Args: + inputs (tensor): data to be processed + + Returns: + tensor: output data + """ outputs = [] for x in inputs: qnode = self.qnode @@ -99,6 +114,15 @@ def call(self, inputs): return tf.stack(outputs) def compute_output_shape(self, input_shape): + """Computes the output shape after passing data of shape ``input_shape`` through the + :class:`~.KerasLayer`. + + Args: + input_shape (tuple or tf.TensorShape): shape of input data + + Returns: + tf.TensorShape: shape of output data + """ return tf.TensorShape([input_shape[0], self.output_dim]) def __str__(self): From 9263b588f6a3991b17183ac1f3f858c4db83233b Mon Sep 17 00:00:00 2001 From: trbromley Date: Fri, 13 Mar 2020 10:00:58 -0400 Subject: [PATCH 23/65] Add to documentation --- doc/code/qml_qnn.rst | 11 ++++++++++ doc/index.rst | 1 + pennylane/qnn.py | 49 +++++++++++++++++++++++++++++++++++++++----- 3 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 doc/code/qml_qnn.rst diff --git a/doc/code/qml_qnn.rst b/doc/code/qml_qnn.rst new file mode 100644 index 00000000000..5c2e42c576f --- /dev/null +++ b/doc/code/qml_qnn.rst @@ -0,0 +1,11 @@ +qml.qnn +======= + +.. currentmodule:: pennylane.qnn + +.. automodapi:: pennylane.qnn + :no-heading: + :include-all-objects: + :no-inheritance-diagram: + :no-inherited-members: + :skip: INPUT_ARG, Iterable, Layer, Optional diff --git a/doc/index.rst b/doc/index.rst index a72ad88b513..1e1c268c3b5 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -203,6 +203,7 @@ PennyLane is **free** and **open source**, released under the Apache License, Ve code/qml_operation code/qml_plugins code/qml_qchem + code/qml_qnn code/qml_qnodes code/qml_templates code/qml_utils diff --git a/pennylane/qnn.py b/pennylane/qnn.py index 591d684e508..6a517a07fb0 100644 --- a/pennylane/qnn.py +++ b/pennylane/qnn.py @@ -11,7 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Test""" +"""This module contains the :class:`~.KerasLayer` class for integrating QNodes with the Keras +layer API.""" import functools import inspect from collections.abc import Iterable @@ -27,6 +28,44 @@ class KerasLayer(Layer): + """A Keras layer for integrating PennyLane QNodes with the Keras API. + + **Example usage:** + + .. code-block:: python + + n_qubits = 2 + dev = qml.dev("default.qubit", wires=n_qubits) + + @qml.qnode(dev, interface="tf") + def circuit(inputs, weights): + qml.templates.AngleEmbedding(inputs, wires=list(range(n_qubits))) + qml.templates.StronglyEntanglingLayers(weights, wires=list(range(n_qubits))) + return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) + + weight_shapes = {"weights": (3, n_qubits, 3)} + weight_specs = {"weights": {"initializer": "random_uniform"}} + + qnode = qml.qnn.KerasLayer(circuit, weight_shapes, 2, weight_specs) + + The resulting ``qnode`` can be treated as a Keras layer and can be combined with other layers + using the `Sequential `__ or + `Model `__ APIs. + + Args: + qnode (qml.QNode): the PennyLane QNode to be converted into a Keras layer + weight_shapes (dict[str, tuple]): a dictionary mapping from all weights used in the QNode to + their corresponding sizes + output_dim (int): the dimension of data output from the QNode + weight_specs (dict[str, dict]): an optional dictionary for users to provide additional + specifications for weights used in the QNode, such as the method of parameter + initialization. This specification is provided as a dictionary with keys given by the + arguments of the `add_weight() + `__ + (e.g., ``initializer``) and values being the corresponding specification. + **kwargs: additional keyword arguments passed to the `Layer + `__ base class + """ def __init__( self, qnode: QNode, @@ -36,6 +75,7 @@ def __init__( **kwargs ): self.sig = qnode.func.sig + print(self.sig) if INPUT_ARG not in self.sig: raise TypeError( "QNode must include an argument with name {} for inputting data".format(INPUT_ARG) @@ -73,7 +113,7 @@ def __init__( super(KerasLayer, self).__init__(dynamic=True, **kwargs) def build(self, input_shape): - """Initializes the :class:`~.KerasLayer` weights. + """Initializes the QNode weights. Args: input_shape (tuple or tf.TensorShape): shape of input data @@ -85,8 +125,7 @@ def build(self, input_shape): super(KerasLayer, self).build(input_shape) def call(self, inputs): - """Evaluates the :class:`~.KerasLayer` on input data using the - :attr:`~.KerasLayer.qnode_weights`. + """Evaluates the QNode on input data using the initialized weights. Args: inputs (tensor): data to be processed @@ -115,7 +154,7 @@ def call(self, inputs): def compute_output_shape(self, input_shape): """Computes the output shape after passing data of shape ``input_shape`` through the - :class:`~.KerasLayer`. + QNode. Args: input_shape (tuple or tf.TensorShape): shape of input data From c45b887746cf5dec98da399c3ce2833f47e4c0ee Mon Sep 17 00:00:00 2001 From: trbromley Date: Fri, 13 Mar 2020 11:02:21 -0400 Subject: [PATCH 24/65] Finalize docstrings --- pennylane/qnn.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/pennylane/qnn.py b/pennylane/qnn.py index 6a517a07fb0..a0715baf65e 100644 --- a/pennylane/qnn.py +++ b/pennylane/qnn.py @@ -30,6 +30,27 @@ class KerasLayer(Layer): """A Keras layer for integrating PennyLane QNodes with the Keras API. + This class converts a :class:`~.QNode` to a Keras layer. The QNode must have a signature that + satisfies the following conditions: + + - Contain an ``inputs`` argument for inputting data. All other arguments are treated as + weights within the QNode. + - All arguments must accept an array or tensor, e.g., arguments should not use nested lists + of different lengths. + - All arguments, except ``inputs``, must have no default value. + - The ``inputs`` argument is permitted to have a default value provided the gradient with + respect to ``inputs`` is not required. + - There cannot be a variable number of positional or keyword arguments, e.g., no ``*args`` or + ``**kwargs`` present in the signature. + + The QNode weights are initialized within the :class:`~.KerasLayer`. Upon instantiation, + a ``weight_shapes`` dictionary must hence be passed which describes the shapes of all + weights in the QNode. The optional ``weight_specs`` argument allows for a more fine-grained + specification of the QNode weights, such as the method of initialization and any + regularization or constraints. If not specified, weights will be added using the Keras + default initialization and without any regularization + or constraints. + **Example usage:** .. code-block:: python @@ -62,7 +83,7 @@ def circuit(inputs, weights): initialization. This specification is provided as a dictionary with keys given by the arguments of the `add_weight() `__ - (e.g., ``initializer``) and values being the corresponding specification. + (e.g., ``initializer``) method and values being the corresponding specification. **kwargs: additional keyword arguments passed to the `Layer `__ base class """ @@ -75,7 +96,6 @@ def __init__( **kwargs ): self.sig = qnode.func.sig - print(self.sig) if INPUT_ARG not in self.sig: raise TypeError( "QNode must include an argument with name {} for inputting data".format(INPUT_ARG) From 3206c01ae2e51db0a928a404beabd4b3009da5fa Mon Sep 17 00:00:00 2001 From: trbromley Date: Fri, 13 Mar 2020 11:55:08 -0400 Subject: [PATCH 25/65] Test with multiple input interfaces --- pennylane/interfaces/torch.py | 1 + pennylane/qnn.py | 1 + tests/test_qnn.py | 27 +++++++++++++++++++++++---- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/pennylane/interfaces/torch.py b/pennylane/interfaces/torch.py index 13802b61a7f..ffdc9f1a0e6 100644 --- a/pennylane/interfaces/torch.py +++ b/pennylane/interfaces/torch.py @@ -199,6 +199,7 @@ def __repr__(self): jacobian = qnode.jacobian metric_tensor = qnode.metric_tensor draw = qnode.draw + func = qnode.func @qnode_str def custom_apply(*args, **kwargs): diff --git a/pennylane/qnn.py b/pennylane/qnn.py index a0715baf65e..fe045558064 100644 --- a/pennylane/qnn.py +++ b/pennylane/qnn.py @@ -96,6 +96,7 @@ def __init__( **kwargs ): self.sig = qnode.func.sig + if INPUT_ARG not in self.sig: raise TypeError( "QNode must include an argument with name {} for inputting data".format(INPUT_ARG) diff --git a/tests/test_qnn.py b/tests/test_qnn.py index 021412f1eb5..f1f344d46c6 100644 --- a/tests/test_qnn.py +++ b/tests/test_qnn.py @@ -28,14 +28,14 @@ @pytest.fixture -def get_circuit(n_qubits, output_dim): +def get_circuit(n_qubits, output_dim, interface): """Fixture for getting a sample quantum circuit with a controllable qubit number and output dimension. Returns both the circuit and the shape of the weights.""" dev = qml.device("default.qubit", wires=n_qubits) weight_shapes = {"w1": (3, n_qubits, 3), "w2": (1,), "w3": 1, "w4": [3], "w5": (2, n_qubits, 3)} - @qml.qnode(dev, interface="tf") + @qml.qnode(dev, interface=interface) def circuit(inputs, w1, w2, w3, w4, w5): """A circuit that embeds data using the AngleEmbedding and then performs a variety of operations. The output is a PauliZ measurement on the first output_dim qubits. One set of @@ -84,6 +84,7 @@ def indices(n_max): class TestKerasLayer: """Unit tests for the pennylane.qnn.KerasLayer class.""" + @pytest.mark.parametrize("interface", ["tf"]) @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) def test_no_input(self, get_circuit, output_dim): """Test if a TypeError is raised when instantiated with a QNode that does not have an @@ -93,6 +94,7 @@ def test_no_input(self, get_circuit, output_dim): with pytest.raises(TypeError, match="QNode must include an argument with name"): KerasLayer(c, w, output_dim) + @pytest.mark.parametrize("interface", ["tf"]) @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) def test_input_in_weight_shapes(self, get_circuit, n_qubits, output_dim): """Test if a ValueError is raised when instantiated with a weight_shapes dictionary that @@ -104,6 +106,7 @@ def test_input_in_weight_shapes(self, get_circuit, n_qubits, output_dim): ): KerasLayer(c, w, output_dim) + @pytest.mark.parametrize("interface", ["tf"]) @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) def test_weight_shape_unspecified(self, get_circuit, output_dim): """Test if a ValueError is raised when instantiated with a weight missing from the @@ -113,6 +116,7 @@ def test_weight_shape_unspecified(self, get_circuit, output_dim): with pytest.raises(ValueError, match="Must specify a shape for every non-input parameter"): KerasLayer(c, w, output_dim) + @pytest.mark.parametrize("interface", ["tf"]) @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) def test_var_pos(self, get_circuit, monkeypatch, output_dim): """Test if a TypeError is raised when instantiated with a variable number of positional @@ -132,6 +136,7 @@ class FuncPatch: with pytest.raises(TypeError, match="Cannot have a variable number of positional"): KerasLayer(c, w, output_dim) + @pytest.mark.parametrize("interface", ["tf"]) @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) def test_var_keyword(self, get_circuit, monkeypatch, output_dim): """Test if a TypeError is raised when instantiated with a variable number of keyword @@ -151,6 +156,7 @@ class FuncPatch: with pytest.raises(TypeError, match="Cannot have a variable number of keyword"): KerasLayer(c, w, output_dim) + @pytest.mark.parametrize("interface", ["tf"]) @pytest.mark.parametrize("n_qubits", [1]) @pytest.mark.parametrize("output_dim", zip(*[[[1], (1,), 1], [1, 1, 1]])) def test_output_dim(self, get_circuit, output_dim): @@ -160,6 +166,7 @@ def test_output_dim(self, get_circuit, output_dim): layer = KerasLayer(c, w, output_dim[0]) assert layer.output_dim == output_dim[1] + @pytest.mark.parametrize("interface", ["tf"]) @pytest.mark.parametrize("n_qubits, output_dim", indices(2)) def test_weight_shapes(self, get_circuit, output_dim, n_qubits): """Test if the weight_shapes input argument is correctly processed to be a dictionary @@ -174,6 +181,7 @@ def test_weight_shapes(self, get_circuit, output_dim, n_qubits): "w5": (2, n_qubits, 3), } + @pytest.mark.parametrize("interface", ["tf"]) @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) def test_non_input_defaults(self, get_circuit, output_dim, n_qubits): """Test if a TypeError is raised when default arguments that are not INPUT_ARG are @@ -190,6 +198,7 @@ def c_dummy(inputs, w1, w2, w3, w4, w5, w6=None): ): KerasLayer(c_dummy, {**w, **{"w6": 1}}, output_dim) + @pytest.mark.parametrize("interface", ["tf"]) @pytest.mark.parametrize("n_qubits, output_dim", indices(2)) def test_qnode_weights(self, get_circuit, n_qubits, output_dim): """Test if the build() method correctly initializes the weights in the qnode_weights @@ -202,6 +211,7 @@ def test_qnode_weights(self, get_circuit, n_qubits, output_dim): assert layer.qnode_weights[weight].shape == shape assert layer.qnode_weights[weight].name[:-2] == weight + @pytest.mark.parametrize("interface", ["tf"]) @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) def test_qnode_weights_with_spec(self, get_circuit, monkeypatch, output_dim, n_qubits): """Test if the build() method correctly passes on user specified weight_specs to the @@ -235,6 +245,7 @@ def add_weight_dummy(*args, **kwargs): for item in weight_specs[weight].items() ) + @pytest.mark.parametrize("interface", ["tf"]) @pytest.mark.parametrize("n_qubits, output_dim", indices(3)) @pytest.mark.parametrize("input_shape", [(10, 4), (8, 3)]) def test_compute_output_shape(self, get_circuit, output_dim, input_shape): @@ -247,6 +258,7 @@ def test_compute_output_shape(self, get_circuit, output_dim, input_shape): assert layer.compute_output_shape(input_shape) == (input_shape[0], output_dim) assert isinstance(layer.compute_output_shape(input_shape), tf.TensorShape) + @pytest.mark.parametrize("interface", qml.qnodes.decorator.ALLOWED_INTERFACES) @pytest.mark.parametrize("n_qubits, output_dim", indices(4)) @pytest.mark.parametrize("batch_size", [5, 10, 15]) def test_call(self, get_circuit, output_dim, batch_size, n_qubits): @@ -254,14 +266,16 @@ def test_call(self, get_circuit, output_dim, batch_size, n_qubits): (batch_size, output_dim) with results that agree with directly calling the QNode""" c, w = get_circuit layer = KerasLayer(c, w, output_dim) - x = tf.ones((batch_size, n_qubits)) + x = np.ones((batch_size, n_qubits), dtype=np.float32) layer_out = layer(x) - weights = [w[0] if w.shape == (1,) else w for w in layer.qnode_weights.values()] + weights = [w[0].numpy() if w.shape == (1,) else w.numpy() for w in + layer.qnode_weights.values()] assert layer_out.shape == (batch_size, output_dim) assert np.allclose(layer_out[0], c(x[0], *weights)) + @pytest.mark.parametrize("interface", ["tf"]) @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) @pytest.mark.parametrize("batch_size", [5]) def test_call_shuffled_args(self, get_circuit, output_dim, batch_size, n_qubits): @@ -290,6 +304,7 @@ def c_shuffled(w1, inputs, w2, w3, w4, w5): assert layer_out.shape == (batch_size, output_dim) assert np.allclose(layer_out[0], c(x[0], *weights)) + @pytest.mark.parametrize("interface", ["tf"]) @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) @pytest.mark.parametrize("batch_size", [5]) def test_call_default_input(self, get_circuit, output_dim, batch_size, n_qubits): @@ -318,6 +333,7 @@ def c_default(w1, w2, w3, w4, w5, inputs=None): assert layer_out.shape == (batch_size, output_dim) assert np.allclose(layer_out[0], c(x[0], *weights)) + @pytest.mark.parametrize("interface", ["tf"]) @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) def test_str_repr(self, get_circuit, output_dim): """Test the __str__ and __repr__ representations""" @@ -332,6 +348,7 @@ def test_str_repr(self, get_circuit, output_dim): class TestKerasLayerIntegration: """Integration tests for the pennylane.qnn.KerasLayer class.""" + @pytest.mark.parametrize("interface", qml.qnodes.decorator.ALLOWED_INTERFACES) @pytest.mark.parametrize("n_qubits, output_dim", indices(2)) @pytest.mark.parametrize("batch_size", [5, 10]) def test_train_model(self, model, batch_size, n_qubits, output_dim): @@ -350,6 +367,7 @@ def test_train_model(self, model, batch_size, n_qubits, output_dim): assert loss[0] > loss[-1] + @pytest.mark.parametrize("interface", qml.qnodes.decorator.ALLOWED_INTERFACES) @pytest.mark.parametrize("n_qubits, output_dim", indices(2)) def test_model_gradients(self, model, output_dim, n_qubits): """Test if a gradient can be calculated with respect to all of the trainable variables in @@ -364,6 +382,7 @@ def test_model_gradients(self, model, output_dim, n_qubits): gradients = tape.gradient(loss, model.trainable_variables) assert all([not isinstance(g, type(None)) for g in gradients]) + @pytest.mark.parametrize("interface", ["tf"]) @pytest.mark.parametrize("n_qubits, output_dim", indices(2)) def test_model_save_weights(self, model, n_qubits): """Test if the model can be successfully saved and reloaded using the get_weights() From f0e8bbcd3b8bd1af65f54d430a120bc7c514dbc9 Mon Sep 17 00:00:00 2001 From: trbromley Date: Fri, 13 Mar 2020 11:59:39 -0400 Subject: [PATCH 26/65] Run black --- pennylane/qnn.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pennylane/qnn.py b/pennylane/qnn.py index fe045558064..fea880a9b22 100644 --- a/pennylane/qnn.py +++ b/pennylane/qnn.py @@ -87,6 +87,7 @@ def circuit(inputs, weights): **kwargs: additional keyword arguments passed to the `Layer `__ base class """ + def __init__( self, qnode: QNode, From 7c658e211e53eb1c6db401fd0ee98592fbd1f760 Mon Sep 17 00:00:00 2001 From: trbromley Date: Fri, 13 Mar 2020 12:14:39 -0400 Subject: [PATCH 27/65] Remove from __init__ because this module requires TF explicitly --- pennylane/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pennylane/__init__.py b/pennylane/__init__.py index 7e32b7723db..8b63aca502e 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -27,7 +27,6 @@ import pennylane.init import pennylane.templates -import pennylane.qnn from pennylane.templates import template, broadcast from pennylane.about import about from pennylane.vqe import Hamiltonian, VQECost From 5715f4e78dff0022bf8fbfc0038f5d52a98fb118 Mon Sep 17 00:00:00 2001 From: trbromley Date: Fri, 13 Mar 2020 14:00:15 -0400 Subject: [PATCH 28/65] Update tests for cases where TF is not available --- tests/test_qnn.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/test_qnn.py b/tests/test_qnn.py index f1f344d46c6..976205590f4 100644 --- a/tests/test_qnn.py +++ b/tests/test_qnn.py @@ -19,9 +19,13 @@ import numpy as np import pytest -import tensorflow as tf -from tensorflow.keras.initializers import RandomNormal -from tensorflow.keras.layers import Layer + +try: + import tensorflow as tf + from tensorflow.keras.initializers import RandomNormal + from tensorflow.keras.layers import Layer +except ImportError: + pass import pennylane as qml from pennylane.qnn import KerasLayer @@ -80,7 +84,7 @@ def indices(n_max): return zip(*[a + 1, b + 1]) -@pytest.mark.usefixtures("get_circuit") +@pytest.mark.usefixtures("get_circuit", "skip_if_no_tf_support") class TestKerasLayer: """Unit tests for the pennylane.qnn.KerasLayer class.""" @@ -344,7 +348,7 @@ def test_str_repr(self, get_circuit, output_dim): assert layer.__repr__() == "" -@pytest.mark.usefixtures("get_circuit", "model") +@pytest.mark.usefixtures("get_circuit", "model", "skip_if_no_tf_support") class TestKerasLayerIntegration: """Integration tests for the pennylane.qnn.KerasLayer class.""" From fca88d667dd8f538059a7998132a6e9ba9c96320 Mon Sep 17 00:00:00 2001 From: trbromley Date: Fri, 13 Mar 2020 14:43:19 -0400 Subject: [PATCH 29/65] Fix tensor flow versioning --- pennylane/__init__.py | 1 + pennylane/qnn.py | 17 ++++++++++++++--- tests/test_qnn.py | 35 +++++++++++++++++++++++++++++------ 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/pennylane/__init__.py b/pennylane/__init__.py index 8b63aca502e..7e32b7723db 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -27,6 +27,7 @@ import pennylane.init import pennylane.templates +import pennylane.qnn from pennylane.templates import template, broadcast from pennylane.about import about from pennylane.vqe import Hamiltonian, VQECost diff --git a/pennylane/qnn.py b/pennylane/qnn.py index fea880a9b22..f906a90cf8d 100644 --- a/pennylane/qnn.py +++ b/pennylane/qnn.py @@ -18,10 +18,18 @@ from collections.abc import Iterable from typing import Optional -import tensorflow as tf -from tensorflow.keras.layers import Layer +try: + import tensorflow as tf + from tensorflow.keras.layers import Layer + from pennylane.interfaces.tf import to_tf + + CORRECT_TF_VERSION = int(tf.__version__.split(".")[0]) > 1 +except ImportError: + from abc import ABC + + Layer = ABC + CORRECT_TF_VERSION = False -from pennylane.interfaces.tf import to_tf from pennylane.qnodes import QNode INPUT_ARG = "inputs" @@ -96,6 +104,9 @@ def __init__( weight_specs: Optional[dict] = None, **kwargs ): + if not CORRECT_TF_VERSION: + raise ImportError("KerasLayer requires TensorFlow version 2 and above") + self.sig = qnode.func.sig if INPUT_ARG not in self.sig: diff --git a/tests/test_qnn.py b/tests/test_qnn.py index 976205590f4..9515c7ea761 100644 --- a/tests/test_qnn.py +++ b/tests/test_qnn.py @@ -22,14 +22,23 @@ try: import tensorflow as tf - from tensorflow.keras.initializers import RandomNormal from tensorflow.keras.layers import Layer + from tensorflow.keras.initializers import RandomNormal + + CORRECT_TF_VERSION = int(tf.__version__.split(".")[0]) > 1 except ImportError: - pass + from abc import ABC + + Layer = ABC + CORRECT_TF_VERSION = False import pennylane as qml from pennylane.qnn import KerasLayer +min_tf = pytest.mark.skipif( + not CORRECT_TF_VERSION, reason="TensorFlow version 2 and above required" +) + @pytest.fixture def get_circuit(n_qubits, output_dim, interface): @@ -84,10 +93,22 @@ def indices(n_max): return zip(*[a + 1, b + 1]) -@pytest.mark.usefixtures("get_circuit", "skip_if_no_tf_support") +@min_tf +@pytest.mark.usefixtures("get_circuit") class TestKerasLayer: """Unit tests for the pennylane.qnn.KerasLayer class.""" + @pytest.mark.parametrize("interface", ["tf"]) + @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) + def test_bad_tf_version(self, get_circuit, output_dim, monkeypatch): + """Test if an ImportError is raised when instantiated with an incorrect version of + TensorFlow""" + c, w = get_circuit + with monkeypatch.context() as m: + m.setattr(qml.qnn, "CORRECT_TF_VERSION", False) + with pytest.raises(ImportError, match="KerasLayer requires TensorFlow version 2"): + KerasLayer(c, w, output_dim) + @pytest.mark.parametrize("interface", ["tf"]) @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) def test_no_input(self, get_circuit, output_dim): @@ -273,8 +294,9 @@ def test_call(self, get_circuit, output_dim, batch_size, n_qubits): x = np.ones((batch_size, n_qubits), dtype=np.float32) layer_out = layer(x) - weights = [w[0].numpy() if w.shape == (1,) else w.numpy() for w in - layer.qnode_weights.values()] + weights = [ + w[0].numpy() if w.shape == (1,) else w.numpy() for w in layer.qnode_weights.values() + ] assert layer_out.shape == (batch_size, output_dim) assert np.allclose(layer_out[0], c(x[0], *weights)) @@ -348,7 +370,8 @@ def test_str_repr(self, get_circuit, output_dim): assert layer.__repr__() == "" -@pytest.mark.usefixtures("get_circuit", "model", "skip_if_no_tf_support") +@min_tf +@pytest.mark.usefixtures("get_circuit", "model") class TestKerasLayerIntegration: """Integration tests for the pennylane.qnn.KerasLayer class.""" From 865688af5d59e264bfddfa86f2e0d9bd5c96b159 Mon Sep 17 00:00:00 2001 From: trbromley Date: Fri, 13 Mar 2020 14:48:23 -0400 Subject: [PATCH 30/65] Skip variable in docs --- doc/code/qml_qnn.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/code/qml_qnn.rst b/doc/code/qml_qnn.rst index 5c2e42c576f..df4056c840e 100644 --- a/doc/code/qml_qnn.rst +++ b/doc/code/qml_qnn.rst @@ -8,4 +8,4 @@ qml.qnn :include-all-objects: :no-inheritance-diagram: :no-inherited-members: - :skip: INPUT_ARG, Iterable, Layer, Optional + :skip: CORRECT_TF_VERSION, INPUT_ARG, Iterable, Layer, Optional From 26c2a9893056481349110fb5ce7e3bcbf998a29f Mon Sep 17 00:00:00 2001 From: Tom Bromley <49409390+trbromley@users.noreply.github.com> Date: Tue, 17 Mar 2020 09:17:11 -0400 Subject: [PATCH 31/65] Apply suggestions from code review Co-Authored-By: Nathan Killoran Co-Authored-By: Josh Izaac --- pennylane/interfaces/tf.py | 2 +- pennylane/qnn.py | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pennylane/interfaces/tf.py b/pennylane/interfaces/tf.py index 8a0ff00814d..c94d1e7d432 100644 --- a/pennylane/interfaces/tf.py +++ b/pennylane/interfaces/tf.py @@ -37,7 +37,7 @@ def to_tf(qnode, dtype=None): Args: qnode (~pennylane.qnode.QNode): a PennyLane QNode - dtype (tf.DType): target output type of QNode, uses default output type of QNode if not + dtype (tf.DType): target output type of QNode; uses default output type of QNode if not specified Returns: diff --git a/pennylane/qnn.py b/pennylane/qnn.py index f906a90cf8d..a8e13799e07 100644 --- a/pennylane/qnn.py +++ b/pennylane/qnn.py @@ -41,7 +41,7 @@ class KerasLayer(Layer): This class converts a :class:`~.QNode` to a Keras layer. The QNode must have a signature that satisfies the following conditions: - - Contain an ``inputs`` argument for inputting data. All other arguments are treated as + - Contain an ``inputs`` named argument for input data. All other arguments are treated as weights within the QNode. - All arguments must accept an array or tensor, e.g., arguments should not use nested lists of different lengths. @@ -52,14 +52,13 @@ class KerasLayer(Layer): ``**kwargs`` present in the signature. The QNode weights are initialized within the :class:`~.KerasLayer`. Upon instantiation, - a ``weight_shapes`` dictionary must hence be passed which describes the shapes of all + a ``weight_shapes`` dictionary must be passed which describes the shapes of all weights in the QNode. The optional ``weight_specs`` argument allows for a more fine-grained specification of the QNode weights, such as the method of initialization and any regularization or constraints. If not specified, weights will be added using the Keras - default initialization and without any regularization - or constraints. + default initialization and without any regularization or constraints. - **Example usage:** + **Example:** .. code-block:: python @@ -86,7 +85,7 @@ def circuit(inputs, weights): weight_shapes (dict[str, tuple]): a dictionary mapping from all weights used in the QNode to their corresponding sizes output_dim (int): the dimension of data output from the QNode - weight_specs (dict[str, dict]): an optional dictionary for users to provide additional + weight_specs (dict[str, dict]): An optional dictionary for users to provide additional specifications for weights used in the QNode, such as the method of parameter initialization. This specification is provided as a dictionary with keys given by the arguments of the `add_weight() @@ -155,7 +154,7 @@ def build(self, input_shape): spec = self.weight_specs.get(weight, {}) self.qnode_weights[weight] = self.add_weight(name=weight, shape=size, **spec) - super(KerasLayer, self).build(input_shape) + super().build(input_shape) def call(self, inputs): """Evaluates the QNode on input data using the initialized weights. From c85e5f2a18ba48f912c7b1599d18b3415e0fe76b Mon Sep 17 00:00:00 2001 From: trbromley Date: Tue, 17 Mar 2020 11:50:35 -0400 Subject: [PATCH 32/65] Apply suggestions from code review --- doc/code/qml_qnn.rst | 1 - pennylane/qnn.py | 57 +++++++++++++++++++++++++------------------- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/doc/code/qml_qnn.rst b/doc/code/qml_qnn.rst index df4056c840e..b01ab078019 100644 --- a/doc/code/qml_qnn.rst +++ b/doc/code/qml_qnn.rst @@ -5,7 +5,6 @@ qml.qnn .. automodapi:: pennylane.qnn :no-heading: - :include-all-objects: :no-inheritance-diagram: :no-inherited-members: :skip: CORRECT_TF_VERSION, INPUT_ARG, Iterable, Layer, Optional diff --git a/pennylane/qnn.py b/pennylane/qnn.py index a8e13799e07..677fc6692e3 100644 --- a/pennylane/qnn.py +++ b/pennylane/qnn.py @@ -38,7 +38,7 @@ class KerasLayer(Layer): """A Keras layer for integrating PennyLane QNodes with the Keras API. - This class converts a :class:`~.QNode` to a Keras layer. The QNode must have a signature that + This class converts a :func:`~.QNode` to a Keras layer. The QNode must have a signature that satisfies the following conditions: - Contain an ``inputs`` named argument for input data. All other arguments are treated as @@ -58,14 +58,31 @@ class KerasLayer(Layer): regularization or constraints. If not specified, weights will be added using the Keras default initialization and without any regularization or constraints. - **Example:** + Args: + qnode (qml.QNode): the PennyLane QNode to be converted into a Keras layer + weight_shapes (dict[str, tuple]): a dictionary mapping from all weights used in the QNode to + their corresponding sizes + output_dim (int): the dimension of data output from the QNode + weight_specs (dict[str, dict]): An optional dictionary for users to provide additional + specifications for weights used in the QNode, such as the method of parameter + initialization. This specification is provided as a dictionary with keys given by the + arguments of the `add_weight() + `__ + method and values being the corresponding specification. + **kwargs: additional keyword arguments passed to the `Layer + `__ base class + + **Example** + + The following shows how a circuit composed of templates from the :doc:`/code/qml_templates` + module can be converted into a Keras layer. .. code-block:: python n_qubits = 2 - dev = qml.dev("default.qubit", wires=n_qubits) + dev = qml.device("default.qubit", wires=n_qubits) - @qml.qnode(dev, interface="tf") + @qml.qnode(dev) def circuit(inputs, weights): qml.templates.AngleEmbedding(inputs, wires=list(range(n_qubits))) qml.templates.StronglyEntanglingLayers(weights, wires=list(range(n_qubits))) @@ -74,25 +91,12 @@ def circuit(inputs, weights): weight_shapes = {"weights": (3, n_qubits, 3)} weight_specs = {"weights": {"initializer": "random_uniform"}} - qnode = qml.qnn.KerasLayer(circuit, weight_shapes, 2, weight_specs) + keras_layer = qml.qnn.KerasLayer(circuit, weight_shapes, output_dim=2, + weight_specs=weight_specs) - The resulting ``qnode`` can be treated as a Keras layer and can be combined with other layers - using the `Sequential `__ or - `Model `__ APIs. - - Args: - qnode (qml.QNode): the PennyLane QNode to be converted into a Keras layer - weight_shapes (dict[str, tuple]): a dictionary mapping from all weights used in the QNode to - their corresponding sizes - output_dim (int): the dimension of data output from the QNode - weight_specs (dict[str, dict]): An optional dictionary for users to provide additional - specifications for weights used in the QNode, such as the method of parameter - initialization. This specification is provided as a dictionary with keys given by the - arguments of the `add_weight() - `__ - (e.g., ``initializer``) method and values being the corresponding specification. - **kwargs: additional keyword arguments passed to the `Layer - `__ base class + The resulting ``keras_layer`` can be combined with other layers using the `Sequential + `__ or + `Model `__ Keras APIs. """ def __init__( @@ -112,15 +116,19 @@ def __init__( raise TypeError( "QNode must include an argument with name {} for inputting data".format(INPUT_ARG) ) + if INPUT_ARG in set(weight_shapes.keys()): raise ValueError( "{} argument should not have its dimension specified in " "weight_shapes".format(INPUT_ARG) ) + if set(weight_shapes.keys()) | {INPUT_ARG} != set(self.sig.keys()): raise ValueError("Must specify a shape for every non-input parameter in the QNode") + if qnode.func.var_pos: raise TypeError("Cannot have a variable number of positional arguments") + if qnode.func.var_keyword: raise TypeError("Cannot have a variable number of keyword arguments") @@ -142,7 +150,7 @@ def __init__( self.qnode_weights = {} - super(KerasLayer, self).__init__(dynamic=True, **kwargs) + super().__init__(dynamic=True, **kwargs) def build(self, input_shape): """Initializes the QNode weights. @@ -200,5 +208,4 @@ def __str__(self): detail = "" return detail.format(self.qnode.func.__name__) - def __repr__(self): - return self.__str__() + __repr__ = __str__ From c8b078ac1c3696267eeec78af7f1554f96a3f954 Mon Sep 17 00:00:00 2001 From: trbromley Date: Tue, 17 Mar 2020 14:11:02 -0400 Subject: [PATCH 33/65] Apply suggestions from code review --- pennylane/qnn.py | 7 +-- tests/test_qnn.py | 120 ++++++++++++++++++++++------------------------ 2 files changed, 60 insertions(+), 67 deletions(-) diff --git a/pennylane/qnn.py b/pennylane/qnn.py index 677fc6692e3..06fa2108472 100644 --- a/pennylane/qnn.py +++ b/pennylane/qnn.py @@ -134,7 +134,7 @@ def __init__( self.qnode = to_tf(qnode, dtype=tf.keras.backend.floatx()) self.weight_shapes = { - weight: (tuple(size) if isinstance(size, Iterable) else (size,)) + weight: (tuple(size) if isinstance(size, Iterable) else (size,) if size > 1 else ()) for weight, size in weight_shapes.items() } self.output_dim = output_dim[0] if isinstance(output_dim, Iterable) else output_dim @@ -179,10 +179,7 @@ def call(self, inputs): for arg in self.sig: if arg is not INPUT_ARG: w = self.qnode_weights[arg] - if w.shape == (1,): - qnode = functools.partial(qnode, w[0]) - else: - qnode = functools.partial(qnode, w) + qnode = functools.partial(qnode, w) else: if self.input_is_default: qnode = functools.partial(qnode, **{INPUT_ARG: x}) diff --git a/tests/test_qnn.py b/tests/test_qnn.py index 9515c7ea761..b6b3666c0f0 100644 --- a/tests/test_qnn.py +++ b/tests/test_qnn.py @@ -20,24 +20,10 @@ import numpy as np import pytest -try: - import tensorflow as tf - from tensorflow.keras.layers import Layer - from tensorflow.keras.initializers import RandomNormal - - CORRECT_TF_VERSION = int(tf.__version__.split(".")[0]) > 1 -except ImportError: - from abc import ABC - - Layer = ABC - CORRECT_TF_VERSION = False - import pennylane as qml from pennylane.qnn import KerasLayer -min_tf = pytest.mark.skipif( - not CORRECT_TF_VERSION, reason="TensorFlow version 2 and above required" -) +tf = pytest.importorskip("tensorflow", minversion="2") @pytest.fixture @@ -46,19 +32,29 @@ def get_circuit(n_qubits, output_dim, interface): dimension. Returns both the circuit and the shape of the weights.""" dev = qml.device("default.qubit", wires=n_qubits) - weight_shapes = {"w1": (3, n_qubits, 3), "w2": (1,), "w3": 1, "w4": [3], "w5": (2, n_qubits, 3)} + weight_shapes = { + "w1": (3, n_qubits, 3), + "w2": (1,), + "w3": 1, + "w4": [3], + "w5": (2, n_qubits, 3), + "w6": 3, + "w7": 0, + } @qml.qnode(dev, interface=interface) - def circuit(inputs, w1, w2, w3, w4, w5): + def circuit(inputs, w1, w2, w3, w4, w5, w6, w7): """A circuit that embeds data using the AngleEmbedding and then performs a variety of operations. The output is a PauliZ measurement on the first output_dim qubits. One set of parameters, w5, are specified as non-trainable.""" qml.templates.AngleEmbedding(inputs, wires=list(range(n_qubits))) qml.templates.StronglyEntanglingLayers(w1, wires=list(range(n_qubits))) - qml.RX(w2, wires=0) - qml.RX(w3, wires=0) - qml.Rot(*w4, wires=0) + qml.RX(w2[0], wires=0 % n_qubits) + qml.RX(w3, wires=1 % n_qubits) + qml.Rot(*w4, wires=2 % n_qubits) qml.templates.StronglyEntanglingLayers(w5, wires=list(range(n_qubits))) + qml.Rot(*w6, wires=3 % n_qubits) + qml.RX(w7, wires=4 % n_qubits) return [qml.expval(qml.PauliZ(i)) for i in range(output_dim)] return circuit, weight_shapes @@ -86,20 +82,19 @@ def model(get_circuit, n_qubits, output_dim): return model -def indices(n_max): +def indicies_up_to(n_max): """Returns an iterator over the number of qubits and output dimension, up to value n_max. The output dimension never exceeds the number of qubits.""" a, b = np.tril_indices(n_max) return zip(*[a + 1, b + 1]) -@min_tf @pytest.mark.usefixtures("get_circuit") class TestKerasLayer: """Unit tests for the pennylane.qnn.KerasLayer class.""" @pytest.mark.parametrize("interface", ["tf"]) - @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) + @pytest.mark.parametrize("n_qubits, output_dim", indicies_up_to(1)) def test_bad_tf_version(self, get_circuit, output_dim, monkeypatch): """Test if an ImportError is raised when instantiated with an incorrect version of TensorFlow""" @@ -110,7 +105,7 @@ def test_bad_tf_version(self, get_circuit, output_dim, monkeypatch): KerasLayer(c, w, output_dim) @pytest.mark.parametrize("interface", ["tf"]) - @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) + @pytest.mark.parametrize("n_qubits, output_dim", indicies_up_to(1)) def test_no_input(self, get_circuit, output_dim): """Test if a TypeError is raised when instantiated with a QNode that does not have an INPUT_ARG argument""" @@ -120,7 +115,7 @@ def test_no_input(self, get_circuit, output_dim): KerasLayer(c, w, output_dim) @pytest.mark.parametrize("interface", ["tf"]) - @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) + @pytest.mark.parametrize("n_qubits, output_dim", indicies_up_to(1)) def test_input_in_weight_shapes(self, get_circuit, n_qubits, output_dim): """Test if a ValueError is raised when instantiated with a weight_shapes dictionary that contains the shape of the input""" @@ -132,7 +127,7 @@ def test_input_in_weight_shapes(self, get_circuit, n_qubits, output_dim): KerasLayer(c, w, output_dim) @pytest.mark.parametrize("interface", ["tf"]) - @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) + @pytest.mark.parametrize("n_qubits, output_dim", indicies_up_to(1)) def test_weight_shape_unspecified(self, get_circuit, output_dim): """Test if a ValueError is raised when instantiated with a weight missing from the weight_shapes dictionary""" @@ -142,7 +137,7 @@ def test_weight_shape_unspecified(self, get_circuit, output_dim): KerasLayer(c, w, output_dim) @pytest.mark.parametrize("interface", ["tf"]) - @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) + @pytest.mark.parametrize("n_qubits, output_dim", indicies_up_to(1)) def test_var_pos(self, get_circuit, monkeypatch, output_dim): """Test if a TypeError is raised when instantiated with a variable number of positional arguments""" @@ -162,7 +157,7 @@ class FuncPatch: KerasLayer(c, w, output_dim) @pytest.mark.parametrize("interface", ["tf"]) - @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) + @pytest.mark.parametrize("n_qubits, output_dim", indicies_up_to(1)) def test_var_keyword(self, get_circuit, monkeypatch, output_dim): """Test if a TypeError is raised when instantiated with a variable number of keyword arguments""" @@ -192,7 +187,7 @@ def test_output_dim(self, get_circuit, output_dim): assert layer.output_dim == output_dim[1] @pytest.mark.parametrize("interface", ["tf"]) - @pytest.mark.parametrize("n_qubits, output_dim", indices(2)) + @pytest.mark.parametrize("n_qubits, output_dim", indicies_up_to(2)) def test_weight_shapes(self, get_circuit, output_dim, n_qubits): """Test if the weight_shapes input argument is correctly processed to be a dictionary with values that are tuples.""" @@ -201,30 +196,32 @@ def test_weight_shapes(self, get_circuit, output_dim, n_qubits): assert layer.weight_shapes == { "w1": (3, n_qubits, 3), "w2": (1,), - "w3": (1,), + "w3": (), "w4": (3,), "w5": (2, n_qubits, 3), + "w6": (3,), + "w7": (), } @pytest.mark.parametrize("interface", ["tf"]) - @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) + @pytest.mark.parametrize("n_qubits, output_dim", indicies_up_to(1)) def test_non_input_defaults(self, get_circuit, output_dim, n_qubits): """Test if a TypeError is raised when default arguments that are not INPUT_ARG are present in the QNode""" c, w = get_circuit @qml.qnode(qml.device("default.qubit", wires=n_qubits), interface="tf") - def c_dummy(inputs, w1, w2, w3, w4, w5, w6=None): + def c_dummy(inputs, w1, w2, w3, w4, w5, w6, w7, w8=None): """Dummy version of the circuit with a default argument""" - return c(inputs, w1, w2, w3, w4, w5) + return c(inputs, w1, w2, w3, w4, w5, w6, w7) with pytest.raises( TypeError, match="Only the argument {} is permitted".format(qml.qnn.INPUT_ARG) ): - KerasLayer(c_dummy, {**w, **{"w6": 1}}, output_dim) + KerasLayer(c_dummy, {**w, **{"w8": 1}}, output_dim) @pytest.mark.parametrize("interface", ["tf"]) - @pytest.mark.parametrize("n_qubits, output_dim", indices(2)) + @pytest.mark.parametrize("n_qubits, output_dim", indicies_up_to(2)) def test_qnode_weights(self, get_circuit, n_qubits, output_dim): """Test if the build() method correctly initializes the weights in the qnode_weights dictionary, i.e., that each value of the dictionary has correct shape and name.""" @@ -237,7 +234,7 @@ def test_qnode_weights(self, get_circuit, n_qubits, output_dim): assert layer.qnode_weights[weight].name[:-2] == weight @pytest.mark.parametrize("interface", ["tf"]) - @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) + @pytest.mark.parametrize("n_qubits, output_dim", indicies_up_to(1)) def test_qnode_weights_with_spec(self, get_circuit, monkeypatch, output_dim, n_qubits): """Test if the build() method correctly passes on user specified weight_specs to the inherited add_weight() method. This is done by monkeypatching add_weight() so that it @@ -252,14 +249,16 @@ def add_weight_dummy(*args, **kwargs): weight_specs = { "w1": {"initializer": "random_uniform", "trainable": False}, - "w2": {"initializer": RandomNormal(mean=0, stddev=0.5)}, + "w2": {"initializer": tf.keras.initializers.RandomNormal(mean=0, stddev=0.5)}, "w3": {}, "w4": {}, "w5": {}, + "w6": {}, + "w7": {}, } with monkeypatch.context() as m: - m.setattr(Layer, "add_weight", add_weight_dummy) + m.setattr(tf.keras.layers.Layer, "add_weight", add_weight_dummy) c, w = get_circuit layer = KerasLayer(c, w, output_dim, weight_specs=weight_specs) layer.build(input_shape=(10, n_qubits)) @@ -271,7 +270,7 @@ def add_weight_dummy(*args, **kwargs): ) @pytest.mark.parametrize("interface", ["tf"]) - @pytest.mark.parametrize("n_qubits, output_dim", indices(3)) + @pytest.mark.parametrize("n_qubits, output_dim", indicies_up_to(3)) @pytest.mark.parametrize("input_shape", [(10, 4), (8, 3)]) def test_compute_output_shape(self, get_circuit, output_dim, input_shape): """Test if the compute_output_shape() method performs correctly, i.e., that it replaces @@ -284,7 +283,7 @@ def test_compute_output_shape(self, get_circuit, output_dim, input_shape): assert isinstance(layer.compute_output_shape(input_shape), tf.TensorShape) @pytest.mark.parametrize("interface", qml.qnodes.decorator.ALLOWED_INTERFACES) - @pytest.mark.parametrize("n_qubits, output_dim", indices(4)) + @pytest.mark.parametrize("n_qubits, output_dim", indicies_up_to(4)) @pytest.mark.parametrize("batch_size", [5, 10, 15]) def test_call(self, get_circuit, output_dim, batch_size, n_qubits): """Test if the call() method performs correctly, i.e., that it outputs with shape @@ -294,15 +293,12 @@ def test_call(self, get_circuit, output_dim, batch_size, n_qubits): x = np.ones((batch_size, n_qubits), dtype=np.float32) layer_out = layer(x) - weights = [ - w[0].numpy() if w.shape == (1,) else w.numpy() for w in layer.qnode_weights.values() - ] - + weights = [w.numpy() for w in layer.qnode_weights.values()] assert layer_out.shape == (batch_size, output_dim) assert np.allclose(layer_out[0], c(x[0], *weights)) @pytest.mark.parametrize("interface", ["tf"]) - @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) + @pytest.mark.parametrize("n_qubits, output_dim", indicies_up_to(1)) @pytest.mark.parametrize("batch_size", [5]) def test_call_shuffled_args(self, get_circuit, output_dim, batch_size, n_qubits): """Test if the call() method performs correctly when the inputs argument is not the first @@ -311,27 +307,29 @@ def test_call_shuffled_args(self, get_circuit, output_dim, batch_size, n_qubits) c, w = get_circuit @qml.qnode(qml.device("default.qubit", wires=n_qubits), interface="tf") - def c_shuffled(w1, inputs, w2, w3, w4, w5): + def c_shuffled(w1, inputs, w2, w3, w4, w5, w6, w7): """Version of the circuit with a shuffled signature""" qml.templates.AngleEmbedding(inputs, wires=list(range(n_qubits))) qml.templates.StronglyEntanglingLayers(w1, wires=list(range(n_qubits))) - qml.RX(w2, wires=0) + qml.RX(w2[0], wires=0) qml.RX(w3, wires=0) qml.Rot(*w4, wires=0) qml.templates.StronglyEntanglingLayers(w5, wires=list(range(n_qubits))) + qml.Rot(*w6, wires=0) + qml.RX(w7, wires=0) return [qml.expval(qml.PauliZ(i)) for i in range(output_dim)] layer = KerasLayer(c_shuffled, w, output_dim) x = tf.ones((batch_size, n_qubits)) layer_out = layer(x) - weights = [w[0] if w.shape == (1,) else w for w in layer.qnode_weights.values()] + weights = [w.numpy() for w in layer.qnode_weights.values()] assert layer_out.shape == (batch_size, output_dim) assert np.allclose(layer_out[0], c(x[0], *weights)) @pytest.mark.parametrize("interface", ["tf"]) - @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) + @pytest.mark.parametrize("n_qubits, output_dim", indicies_up_to(1)) @pytest.mark.parametrize("batch_size", [5]) def test_call_default_input(self, get_circuit, output_dim, batch_size, n_qubits): """Test if the call() method performs correctly when the inputs argument is a default @@ -340,27 +338,29 @@ def test_call_default_input(self, get_circuit, output_dim, batch_size, n_qubits) c, w = get_circuit @qml.qnode(qml.device("default.qubit", wires=n_qubits), interface="tf") - def c_default(w1, w2, w3, w4, w5, inputs=None): + def c_default(w1, w2, w3, w4, w5, w6, w7, inputs=None): """Version of the circuit with inputs as a default argument""" qml.templates.AngleEmbedding(inputs, wires=list(range(n_qubits))) qml.templates.StronglyEntanglingLayers(w1, wires=list(range(n_qubits))) - qml.RX(w2, wires=0) + qml.RX(w2[0], wires=0) qml.RX(w3, wires=0) qml.Rot(*w4, wires=0) qml.templates.StronglyEntanglingLayers(w5, wires=list(range(n_qubits))) + qml.Rot(*w6, wires=0) + qml.RX(w7, wires=0) return [qml.expval(qml.PauliZ(i)) for i in range(output_dim)] layer = KerasLayer(c_default, w, output_dim) x = tf.ones((batch_size, n_qubits)) layer_out = layer(x) - weights = [w[0] if w.shape == (1,) else w for w in layer.qnode_weights.values()] + weights = [w.numpy() for w in layer.qnode_weights.values()] assert layer_out.shape == (batch_size, output_dim) assert np.allclose(layer_out[0], c(x[0], *weights)) @pytest.mark.parametrize("interface", ["tf"]) - @pytest.mark.parametrize("n_qubits, output_dim", indices(1)) + @pytest.mark.parametrize("n_qubits, output_dim", indicies_up_to(1)) def test_str_repr(self, get_circuit, output_dim): """Test the __str__ and __repr__ representations""" c, w = get_circuit @@ -370,13 +370,12 @@ def test_str_repr(self, get_circuit, output_dim): assert layer.__repr__() == "" -@min_tf @pytest.mark.usefixtures("get_circuit", "model") class TestKerasLayerIntegration: """Integration tests for the pennylane.qnn.KerasLayer class.""" @pytest.mark.parametrize("interface", qml.qnodes.decorator.ALLOWED_INTERFACES) - @pytest.mark.parametrize("n_qubits, output_dim", indices(2)) + @pytest.mark.parametrize("n_qubits, output_dim", indicies_up_to(2)) @pytest.mark.parametrize("batch_size", [5, 10]) def test_train_model(self, model, batch_size, n_qubits, output_dim): """Test if a model can train using the KerasLayer. The model is composed of a single @@ -389,13 +388,10 @@ def test_train_model(self, model, batch_size, n_qubits, output_dim): model.compile(optimizer="sgd", loss="mse") - result = model.fit(x, y, epochs=2, batch_size=batch_size, verbose=0) - loss = result.history["loss"] - - assert loss[0] > loss[-1] + model.fit(x, y, batch_size=batch_size, verbose=0) @pytest.mark.parametrize("interface", qml.qnodes.decorator.ALLOWED_INTERFACES) - @pytest.mark.parametrize("n_qubits, output_dim", indices(2)) + @pytest.mark.parametrize("n_qubits, output_dim", indicies_up_to(2)) def test_model_gradients(self, model, output_dim, n_qubits): """Test if a gradient can be calculated with respect to all of the trainable variables in the model""" @@ -407,10 +403,10 @@ def test_model_gradients(self, model, output_dim, n_qubits): loss = tf.keras.losses.mean_squared_error(out, y) gradients = tape.gradient(loss, model.trainable_variables) - assert all([not isinstance(g, type(None)) for g in gradients]) + assert all([g.dtype == tf.keras.backend.floatx() for g in gradients]) @pytest.mark.parametrize("interface", ["tf"]) - @pytest.mark.parametrize("n_qubits, output_dim", indices(2)) + @pytest.mark.parametrize("n_qubits, output_dim", indicies_up_to(2)) def test_model_save_weights(self, model, n_qubits): """Test if the model can be successfully saved and reloaded using the get_weights() method""" From 492cda3f775fec554a744c761c25aeb4085733b6 Mon Sep 17 00:00:00 2001 From: trbromley Date: Tue, 17 Mar 2020 14:22:26 -0400 Subject: [PATCH 34/65] Apply suggestions from code review --- tests/test_qnn.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_qnn.py b/tests/test_qnn.py index b6b3666c0f0..035764ac305 100644 --- a/tests/test_qnn.py +++ b/tests/test_qnn.py @@ -417,9 +417,8 @@ def test_model_save_weights(self, model, n_qubits): model.load_weights(filename) prediction_loaded = model.predict(np.ones(n_qubits)) weights_loaded = model.get_weights() + os.remove(filename) assert np.allclose(prediction, prediction_loaded) for i, w in enumerate(weights): assert np.allclose(w, weights_loaded[i]) - - os.remove(filename) From c31abd9ff020148ddcb59b2a418c8dce9bf20424 Mon Sep 17 00:00:00 2001 From: trbromley Date: Tue, 17 Mar 2020 14:34:38 -0400 Subject: [PATCH 35/65] Apply isort --- pennylane/qnn.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pennylane/qnn.py b/pennylane/qnn.py index 06fa2108472..a569ae9cd55 100644 --- a/pennylane/qnn.py +++ b/pennylane/qnn.py @@ -18,6 +18,8 @@ from collections.abc import Iterable from typing import Optional +from pennylane.qnodes import QNode + try: import tensorflow as tf from tensorflow.keras.layers import Layer @@ -30,7 +32,6 @@ Layer = ABC CORRECT_TF_VERSION = False -from pennylane.qnodes import QNode INPUT_ARG = "inputs" From 901dad0b94e8c72e09e569f6f28bd10842ddfa0d Mon Sep 17 00:00:00 2001 From: trbromley Date: Tue, 17 Mar 2020 14:37:41 -0400 Subject: [PATCH 36/65] Revert isort due to lint errors --- pennylane/qnn.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pennylane/qnn.py b/pennylane/qnn.py index a569ae9cd55..06fa2108472 100644 --- a/pennylane/qnn.py +++ b/pennylane/qnn.py @@ -18,8 +18,6 @@ from collections.abc import Iterable from typing import Optional -from pennylane.qnodes import QNode - try: import tensorflow as tf from tensorflow.keras.layers import Layer @@ -32,6 +30,7 @@ Layer = ABC CORRECT_TF_VERSION = False +from pennylane.qnodes import QNode INPUT_ARG = "inputs" From ee8b9693d472d4809d817423c5584a3c5d3c614f Mon Sep 17 00:00:00 2001 From: trbromley Date: Tue, 17 Mar 2020 15:40:30 -0400 Subject: [PATCH 37/65] Restructure --- doc/code/qml_qnn.rst | 3 +-- pennylane/qnn/__init__.py | 17 +++++++++++++++++ pennylane/{qnn.py => qnn/keras.py} | 3 +-- tests/{test_qnn.py => qnn/test_keras.py} | 19 ++++++++++--------- 4 files changed, 29 insertions(+), 13 deletions(-) create mode 100644 pennylane/qnn/__init__.py rename pennylane/{qnn.py => qnn/keras.py} (98%) rename tests/{test_qnn.py => qnn/test_keras.py} (97%) diff --git a/doc/code/qml_qnn.rst b/doc/code/qml_qnn.rst index b01ab078019..72b15dbb892 100644 --- a/doc/code/qml_qnn.rst +++ b/doc/code/qml_qnn.rst @@ -4,7 +4,6 @@ qml.qnn .. currentmodule:: pennylane.qnn .. automodapi:: pennylane.qnn - :no-heading: + :no-heading: :no-inheritance-diagram: :no-inherited-members: - :skip: CORRECT_TF_VERSION, INPUT_ARG, Iterable, Layer, Optional diff --git a/pennylane/qnn/__init__.py b/pennylane/qnn/__init__.py new file mode 100644 index 00000000000..38bcd8389f6 --- /dev/null +++ b/pennylane/qnn/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2018-2020 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This module contains the :class:`~.KerasLayer` class for integrating QNodes with the Keras +layer API.""" + +from .keras import KerasLayer diff --git a/pennylane/qnn.py b/pennylane/qnn/keras.py similarity index 98% rename from pennylane/qnn.py rename to pennylane/qnn/keras.py index 06fa2108472..a365abcfa25 100644 --- a/pennylane/qnn.py +++ b/pennylane/qnn/keras.py @@ -11,8 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""This module contains the :class:`~.KerasLayer` class for integrating QNodes with the Keras -layer API.""" +"""This module is for PennyLane-Keras integration.""" import functools import inspect from collections.abc import Iterable diff --git a/tests/test_qnn.py b/tests/qnn/test_keras.py similarity index 97% rename from tests/test_qnn.py rename to tests/qnn/test_keras.py index 035764ac305..5cc2520c1ef 100644 --- a/tests/test_qnn.py +++ b/tests/qnn/test_keras.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -Tests for the pennylane.qnn module. +Tests for the pennylane.qnn.keras module. """ import os import tempfile @@ -21,7 +21,7 @@ import pytest import pennylane as qml -from pennylane.qnn import KerasLayer +from pennylane.qnn.keras import KerasLayer tf = pytest.importorskip("tensorflow", minversion="2") @@ -91,7 +91,7 @@ def indicies_up_to(n_max): @pytest.mark.usefixtures("get_circuit") class TestKerasLayer: - """Unit tests for the pennylane.qnn.KerasLayer class.""" + """Unit tests for the pennylane.qnn.keras.KerasLayer class.""" @pytest.mark.parametrize("interface", ["tf"]) @pytest.mark.parametrize("n_qubits, output_dim", indicies_up_to(1)) @@ -100,7 +100,7 @@ def test_bad_tf_version(self, get_circuit, output_dim, monkeypatch): TensorFlow""" c, w = get_circuit with monkeypatch.context() as m: - m.setattr(qml.qnn, "CORRECT_TF_VERSION", False) + m.setattr(qml.qnn.keras, "CORRECT_TF_VERSION", False) with pytest.raises(ImportError, match="KerasLayer requires TensorFlow version 2"): KerasLayer(c, w, output_dim) @@ -110,7 +110,7 @@ def test_no_input(self, get_circuit, output_dim): """Test if a TypeError is raised when instantiated with a QNode that does not have an INPUT_ARG argument""" c, w = get_circuit - del c.func.sig[qml.qnn.INPUT_ARG] + del c.func.sig[qml.qnn.keras.INPUT_ARG] with pytest.raises(TypeError, match="QNode must include an argument with name"): KerasLayer(c, w, output_dim) @@ -120,9 +120,10 @@ def test_input_in_weight_shapes(self, get_circuit, n_qubits, output_dim): """Test if a ValueError is raised when instantiated with a weight_shapes dictionary that contains the shape of the input""" c, w = get_circuit - w[qml.qnn.INPUT_ARG] = n_qubits + w[qml.qnn.keras.INPUT_ARG] = n_qubits with pytest.raises( - ValueError, match="{} argument should not have its dimension".format(qml.qnn.INPUT_ARG) + ValueError, match="{} argument should not have its dimension".format( + qml.qnn.keras.INPUT_ARG) ): KerasLayer(c, w, output_dim) @@ -216,7 +217,7 @@ def c_dummy(inputs, w1, w2, w3, w4, w5, w6, w7, w8=None): return c(inputs, w1, w2, w3, w4, w5, w6, w7) with pytest.raises( - TypeError, match="Only the argument {} is permitted".format(qml.qnn.INPUT_ARG) + TypeError, match="Only the argument {} is permitted".format(qml.qnn.keras.INPUT_ARG) ): KerasLayer(c_dummy, {**w, **{"w8": 1}}, output_dim) @@ -372,7 +373,7 @@ def test_str_repr(self, get_circuit, output_dim): @pytest.mark.usefixtures("get_circuit", "model") class TestKerasLayerIntegration: - """Integration tests for the pennylane.qnn.KerasLayer class.""" + """Integration tests for the pennylane.qnn.keras.KerasLayer class.""" @pytest.mark.parametrize("interface", qml.qnodes.decorator.ALLOWED_INTERFACES) @pytest.mark.parametrize("n_qubits, output_dim", indicies_up_to(2)) From 929e522f173d7b400a0d97f3e1ede04c26dbf367 Mon Sep 17 00:00:00 2001 From: trbromley Date: Tue, 17 Mar 2020 15:41:58 -0400 Subject: [PATCH 38/65] Apply black --- tests/qnn/test_keras.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/qnn/test_keras.py b/tests/qnn/test_keras.py index 5cc2520c1ef..9a7c6cad742 100644 --- a/tests/qnn/test_keras.py +++ b/tests/qnn/test_keras.py @@ -122,8 +122,8 @@ def test_input_in_weight_shapes(self, get_circuit, n_qubits, output_dim): c, w = get_circuit w[qml.qnn.keras.INPUT_ARG] = n_qubits with pytest.raises( - ValueError, match="{} argument should not have its dimension".format( - qml.qnn.keras.INPUT_ARG) + ValueError, + match="{} argument should not have its dimension".format(qml.qnn.keras.INPUT_ARG), ): KerasLayer(c, w, output_dim) From 02874c023e660ca50156c77515614d60340df420 Mon Sep 17 00:00:00 2001 From: trbromley Date: Mon, 23 Mar 2020 09:18:28 -0400 Subject: [PATCH 39/65] Apply suggestions and make class attribute --- pennylane/interfaces/tf.py | 4 ++-- pennylane/qnn/keras.py | 28 ++++++++++++++++------------ tests/qnn/test_keras.py | 18 +++++++++++------- 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/pennylane/interfaces/tf.py b/pennylane/interfaces/tf.py index c94d1e7d432..bea2458307f 100644 --- a/pennylane/interfaces/tf.py +++ b/pennylane/interfaces/tf.py @@ -37,8 +37,8 @@ def to_tf(qnode, dtype=None): Args: qnode (~pennylane.qnode.QNode): a PennyLane QNode - dtype (tf.DType): target output type of QNode; uses default output type of QNode if not - specified + dtype (tf.DType): target output type of QNode; uses the TensorFlow equivalent of the + QNode output type if ``dtype`` is not specified Returns: function: the QNode as a TensorFlow function diff --git a/pennylane/qnn/keras.py b/pennylane/qnn/keras.py index a365abcfa25..be1149a31d7 100644 --- a/pennylane/qnn/keras.py +++ b/pennylane/qnn/keras.py @@ -31,8 +31,6 @@ from pennylane.qnodes import QNode -INPUT_ARG = "inputs" - class KerasLayer(Layer): """A Keras layer for integrating PennyLane QNodes with the Keras API. @@ -98,6 +96,8 @@ def circuit(inputs, weights): `Model `__ Keras APIs. """ + input_arg = "inputs" + def __init__( self, qnode: QNode, @@ -111,18 +111,20 @@ def __init__( self.sig = qnode.func.sig - if INPUT_ARG not in self.sig: + if self.input_arg not in self.sig: raise TypeError( - "QNode must include an argument with name {} for inputting data".format(INPUT_ARG) + "QNode must include an argument with name {} for inputting data".format( + self.input_arg + ) ) - if INPUT_ARG in set(weight_shapes.keys()): + if self.input_arg in set(weight_shapes.keys()): raise ValueError( "{} argument should not have its dimension specified in " - "weight_shapes".format(INPUT_ARG) + "weight_shapes".format(self.input_arg) ) - if set(weight_shapes.keys()) | {INPUT_ARG} != set(self.sig.keys()): + if set(weight_shapes.keys()) | {self.input_arg} != set(self.sig.keys()): raise ValueError("Must specify a shape for every non-input parameter in the QNode") if qnode.func.var_pos: @@ -141,9 +143,11 @@ def __init__( defaults = { name for name, sig in self.sig.items() if sig.par.default != inspect.Parameter.empty } - self.input_is_default = INPUT_ARG in defaults - if defaults - {INPUT_ARG} != set(): - raise TypeError("Only the argument {} is permitted to have a default".format(INPUT_ARG)) + self.input_is_default = self.input_arg in defaults + if defaults - {self.input_arg} != set(): + raise TypeError( + "Only the argument {} is permitted to have a default".format(self.input_arg) + ) self.weight_specs = weight_specs if weight_specs is not None else {} @@ -176,12 +180,12 @@ def call(self, inputs): for x in inputs: qnode = self.qnode for arg in self.sig: - if arg is not INPUT_ARG: + if arg is not self.input_arg: w = self.qnode_weights[arg] qnode = functools.partial(qnode, w) else: if self.input_is_default: - qnode = functools.partial(qnode, **{INPUT_ARG: x}) + qnode = functools.partial(qnode, **{self.input_arg: x}) else: qnode = functools.partial(qnode, x) outputs.append(qnode()) diff --git a/tests/qnn/test_keras.py b/tests/qnn/test_keras.py index 9a7c6cad742..51f92412f8e 100644 --- a/tests/qnn/test_keras.py +++ b/tests/qnn/test_keras.py @@ -108,9 +108,9 @@ def test_bad_tf_version(self, get_circuit, output_dim, monkeypatch): @pytest.mark.parametrize("n_qubits, output_dim", indicies_up_to(1)) def test_no_input(self, get_circuit, output_dim): """Test if a TypeError is raised when instantiated with a QNode that does not have an - INPUT_ARG argument""" + argument with name equal to the input_arg class attribute of KerasLayer""" c, w = get_circuit - del c.func.sig[qml.qnn.keras.INPUT_ARG] + del c.func.sig[qml.qnn.keras.KerasLayer.input_arg] with pytest.raises(TypeError, match="QNode must include an argument with name"): KerasLayer(c, w, output_dim) @@ -118,12 +118,15 @@ def test_no_input(self, get_circuit, output_dim): @pytest.mark.parametrize("n_qubits, output_dim", indicies_up_to(1)) def test_input_in_weight_shapes(self, get_circuit, n_qubits, output_dim): """Test if a ValueError is raised when instantiated with a weight_shapes dictionary that - contains the shape of the input""" + contains the shape of the input argument given by the input_arg class attribute of + KerasLayer""" c, w = get_circuit - w[qml.qnn.keras.INPUT_ARG] = n_qubits + w[qml.qnn.keras.KerasLayer.input_arg] = n_qubits with pytest.raises( ValueError, - match="{} argument should not have its dimension".format(qml.qnn.keras.INPUT_ARG), + match="{} argument should not have its dimension".format( + qml.qnn.keras.KerasLayer.input_arg + ), ): KerasLayer(c, w, output_dim) @@ -207,7 +210,7 @@ def test_weight_shapes(self, get_circuit, output_dim, n_qubits): @pytest.mark.parametrize("interface", ["tf"]) @pytest.mark.parametrize("n_qubits, output_dim", indicies_up_to(1)) def test_non_input_defaults(self, get_circuit, output_dim, n_qubits): - """Test if a TypeError is raised when default arguments that are not INPUT_ARG are + """Test if a TypeError is raised when default arguments that are not the input argument are present in the QNode""" c, w = get_circuit @@ -217,7 +220,8 @@ def c_dummy(inputs, w1, w2, w3, w4, w5, w6, w7, w8=None): return c(inputs, w1, w2, w3, w4, w5, w6, w7) with pytest.raises( - TypeError, match="Only the argument {} is permitted".format(qml.qnn.keras.INPUT_ARG) + TypeError, + match="Only the argument {} is permitted".format(qml.qnn.keras.KerasLayer.input_arg), ): KerasLayer(c_dummy, {**w, **{"w8": 1}}, output_dim) From e57b987f2522ca81d0d8b1cc1fc549285189c2bd Mon Sep 17 00:00:00 2001 From: trbromley Date: Mon, 23 Mar 2020 09:28:20 -0400 Subject: [PATCH 40/65] Apply suggestions from code review --- tests/qnn/test_keras.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/qnn/test_keras.py b/tests/qnn/test_keras.py index 51f92412f8e..98aa30e9727 100644 --- a/tests/qnn/test_keras.py +++ b/tests/qnn/test_keras.py @@ -385,8 +385,7 @@ class TestKerasLayerIntegration: def test_train_model(self, model, batch_size, n_qubits, output_dim): """Test if a model can train using the KerasLayer. The model is composed of a single KerasLayer sandwiched between two Dense layers, and the dataset is simply input and output - vectors of zeros. The test checks that the loss function after two epochs is less than - the loss function after one epoch, indicating that training is taking place.""" + vectors of zeros.""" x = np.zeros((5, n_qubits)) y = np.zeros((5, output_dim)) From 65e5cba979f7bbcc172f8572951bb28a985415ab Mon Sep 17 00:00:00 2001 From: trbromley Date: Tue, 24 Mar 2020 17:40:35 -0400 Subject: [PATCH 41/65] Apply suggestions from code review --- pennylane/qnn/keras.py | 155 ++++++++++++++++++++++++++++++---------- tests/qnn/test_keras.py | 22 ++++++ 2 files changed, 140 insertions(+), 37 deletions(-) diff --git a/pennylane/qnn/keras.py b/pennylane/qnn/keras.py index be1149a31d7..21ae8c41ea5 100644 --- a/pennylane/qnn/keras.py +++ b/pennylane/qnn/keras.py @@ -33,67 +33,148 @@ class KerasLayer(Layer): - """A Keras layer for integrating PennyLane QNodes with the Keras API. - - This class converts a :func:`~.QNode` to a Keras layer. The QNode must have a signature that - satisfies the following conditions: - - - Contain an ``inputs`` named argument for input data. All other arguments are treated as - weights within the QNode. - - All arguments must accept an array or tensor, e.g., arguments should not use nested lists - of different lengths. - - All arguments, except ``inputs``, must have no default value. - - The ``inputs`` argument is permitted to have a default value provided the gradient with - respect to ``inputs`` is not required. - - There cannot be a variable number of positional or keyword arguments, e.g., no ``*args`` or - ``**kwargs`` present in the signature. - - The QNode weights are initialized within the :class:`~.KerasLayer`. Upon instantiation, - a ``weight_shapes`` dictionary must be passed which describes the shapes of all - weights in the QNode. The optional ``weight_specs`` argument allows for a more fine-grained - specification of the QNode weights, such as the method of initialization and any - regularization or constraints. If not specified, weights will be added using the Keras - default initialization and without any regularization or constraints. + """A Keras Layer_ for integrating PennyLane QNodes with the Keras API. Args: - qnode (qml.QNode): the PennyLane QNode to be converted into a Keras layer + qnode (qml.QNode): the PennyLane QNode to be converted into a Keras Layer_ weight_shapes (dict[str, tuple]): a dictionary mapping from all weights used in the QNode to - their corresponding sizes - output_dim (int): the dimension of data output from the QNode + their corresponding shapes + output_dim (int): the output dimension of the QNode weight_specs (dict[str, dict]): An optional dictionary for users to provide additional specifications for weights used in the QNode, such as the method of parameter initialization. This specification is provided as a dictionary with keys given by the arguments of the `add_weight() `__ method and values being the corresponding specification. - **kwargs: additional keyword arguments passed to the `Layer - `__ base class + **kwargs: additional keyword arguments passed to the Layer_ base class + + This class converts a :func:`~.QNode` to a Keras Layer_, which can be used within the Keras + `Sequential `__ or + `Model `__ classes for + creating quantum and hybrid models: + + .. code-block:: python + + qlayer = qml.qnn.KerasLayer(qnode, weight_shapes, output_dim=2) + model = tf.keras.models.Sequential([qlayer, tf.keras.layers.Dense(2)]) + + The signature of QNode must contain an ``inputs`` named argument for input data, with all + other arguments to be treated as internal weights. A valid ``qnode`` for the example + above would be: + + .. code-block:: python + + n_qubits = 2 + dev = qml.device("default.qubit", wires=n_qubits) + + @qml.qnode(dev) + def qnode(inputs, weights_0, weight_1): + qml.RX(inputs, wires=0) + qml.Rot(*weights_0, wires=0) + qml.RY(weight_1, wires=1) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) + + The internal weights of the QNode are automatically initialized within the + :class:`~.KerasLayer` and must have their shapes specified in a ``weight_shapes`` dictionary. + For example: + + .. code-block:: + + weight_shapes = {"weights_0": 3, "weight_1": 1} **Example** - The following shows how a circuit composed of templates from the :doc:`/code/qml_templates` - module can be converted into a Keras layer. + The code block below shows how a circuit composed of templates from the + :doc:`/code/qml_templates` module can be combined with classical + `Dense `__ layers to learn the + two-dimensional `moons `__ dataset. .. code-block:: python + import pennylane as qml + import tensorflow as tf + import sklearn.datasets + n_qubits = 2 dev = qml.device("default.qubit", wires=n_qubits) @qml.qnode(dev) - def circuit(inputs, weights): + def qnode(inputs, weights): qml.templates.AngleEmbedding(inputs, wires=list(range(n_qubits))) qml.templates.StronglyEntanglingLayers(weights, wires=list(range(n_qubits))) return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) weight_shapes = {"weights": (3, n_qubits, 3)} - weight_specs = {"weights": {"initializer": "random_uniform"}} - - keras_layer = qml.qnn.KerasLayer(circuit, weight_shapes, output_dim=2, - weight_specs=weight_specs) - The resulting ``keras_layer`` can be combined with other layers using the `Sequential - `__ or - `Model `__ Keras APIs. + q_layer = qml.qnn.KerasLayer(qnode, weight_shapes, output_dim=2) + model = tf.keras.models.Sequential( + [tf.keras.layers.Dense(2), q_layer, tf.keras.layers.Dense(2, activation="softmax"),] + ) + + data = sklearn.datasets.make_moons() + X = tf.constant(data[0]) + Y = tf.one_hot(data[1], depth=2) + + opt = tf.keras.optimizers.SGD(learning_rate=0.5) + model.compile(opt, loss='mae') + + The model can be trained using: + + >>> model.fit(X, Y, epochs=8, batch_size=5) + Train on 100 samples + Epoch 1/8 + 100/100 [==============================] - 9s 90ms/sample - loss: 0.3524 + Epoch 2/8 + 100/100 [==============================] - 9s 87ms/sample - loss: 0.2441 + Epoch 3/8 + 100/100 [==============================] - 9s 87ms/sample - loss: 0.1908 + Epoch 4/8 + 100/100 [==============================] - 9s 87ms/sample - loss: 0.1832 + Epoch 5/8 + 100/100 [==============================] - 9s 88ms/sample - loss: 0.1596 + Epoch 6/8 + 100/100 [==============================] - 9s 87ms/sample - loss: 0.1637 + Epoch 7/8 + 100/100 [==============================] - 9s 86ms/sample - loss: 0.1613 + Epoch 8/8 + 100/100 [==============================] - 9s 87ms/sample - loss: 0.1474 + + .. UsageDetails:: + + The QNode must have a signature that satisfies the following conditions: + + - Contain an ``inputs`` named argument for input data. + - All other arguments must accept an array or tensor and are treated as internal + weights of the QNode. + - All other arguments must have no default value. + - The ``inputs`` argument is permitted to have a default value provided the gradient with + respect to ``inputs`` is not required. + - There cannot be a variable number of positional or keyword arguments, e.g., no ``*args`` + or ``**kwargs`` present in the signature. + + The optional ``weight_specs`` argument allows for a more fine-grained + specification of the QNode weights, such as the method of initialization and any + regularization or constraints. For example, the initialization method of the ``weights`` + argument in the example above could be specified by: + + .. code-block:: + + weight_specs = {"weights": {"initializer": "random_uniform"}} + + The values of ``weight_specs`` are dictionaries with keys given by arguments of + the Keras + `add_weight() `__ + method. For the ``"initializer"`` argument, one can specify a string such as + ``"random_uniform"`` or an instance of an `Initializer + `__ class, such as + `tf.keras.initializers.RandomUniform `__. + + If ``weight_specs`` is not specified, weights will be added using the Keras default + initialization and without any regularization or constraints. + + .. _Layer: https://www.tensorflow.org/api_docs/python/tf/keras/layers/Layer """ input_arg = "inputs" @@ -205,7 +286,7 @@ def compute_output_shape(self, input_shape): return tf.TensorShape([input_shape[0], self.output_dim]) def __str__(self): - detail = "" + detail = "" return detail.format(self.qnode.func.__name__) __repr__ = __str__ diff --git a/tests/qnn/test_keras.py b/tests/qnn/test_keras.py index 98aa30e9727..467a174bdff 100644 --- a/tests/qnn/test_keras.py +++ b/tests/qnn/test_keras.py @@ -374,6 +374,28 @@ def test_str_repr(self, get_circuit, output_dim): assert layer.__str__() == "" assert layer.__repr__() == "" + @pytest.mark.parametrize("interface", ["tf"]) + @pytest.mark.parametrize("n_qubits, output_dim", indicies_up_to(1)) + def test_gradients(self, get_circuit, output_dim, n_qubits): + """Test if the gradients of the KerasLayer are equal to the gradients of the circuit when + taken with respect to the trainable variables""" + c, w = get_circuit + layer = KerasLayer(c, w, output_dim) + x = tf.ones((1, n_qubits)) + + with tf.GradientTape() as tape: + out_layer = layer(x) + + g_layer = tape.gradient(out_layer, layer.trainable_variables) + + with tf.GradientTape() as tape: + out_circuit = c(x[0], *layer.trainable_variables) + + g_circuit = tape.gradient(out_circuit, layer.trainable_variables) + + for i in range(len(out_layer)): + assert np.allclose(g_layer[i], g_circuit[i]) + @pytest.mark.usefixtures("get_circuit", "model") class TestKerasLayerIntegration: From b2613489a95975f1224b2235d040b0ca46c32ea8 Mon Sep 17 00:00:00 2001 From: trbromley Date: Tue, 24 Mar 2020 18:02:27 -0400 Subject: [PATCH 42/65] Fix failing test --- pennylane/qnn/keras.py | 4 ++-- test.py | 23 +++++++++++++++++++++++ tests/qnn/test_keras.py | 4 ++-- 3 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 test.py diff --git a/pennylane/qnn/keras.py b/pennylane/qnn/keras.py index 21ae8c41ea5..31d09a3fa67 100644 --- a/pennylane/qnn/keras.py +++ b/pennylane/qnn/keras.py @@ -108,9 +108,9 @@ def qnode(inputs, weights): weight_shapes = {"weights": (3, n_qubits, 3)} - q_layer = qml.qnn.KerasLayer(qnode, weight_shapes, output_dim=2) + qlayer = qml.qnn.KerasLayer(qnode, weight_shapes, output_dim=2) model = tf.keras.models.Sequential( - [tf.keras.layers.Dense(2), q_layer, tf.keras.layers.Dense(2, activation="softmax"),] + [tf.keras.layers.Dense(2), qlayer, tf.keras.layers.Dense(2, activation="softmax"),] ) data = sklearn.datasets.make_moons() diff --git a/test.py b/test.py new file mode 100644 index 00000000000..b6bb46374d7 --- /dev/null +++ b/test.py @@ -0,0 +1,23 @@ +import pennylane as qml +import tensorflow as tf +import sklearn.datasets + +n_qubits = 2 +dev = qml.device("default.qubit", wires=n_qubits) + +@qml.qnode(dev) +def qnode(inputs, weights): + qml.templates.AngleEmbedding(inputs, wires=list(range(n_qubits))) + qml.templates.StronglyEntanglingLayers(weights, wires=list(range(n_qubits))) + return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) + +weight_shapes = {"weights": (3, n_qubits, 3)} + +q_layer = qml.qnn.KerasLayer(qnode, weight_shapes, output_dim=2) +model = tf.keras.models.Sequential( + [tf.keras.layers.Dense(2), q_layer, tf.keras.layers.Dense(2, activation="softmax"),] +) + +data = sklearn.datasets.make_moons() +X = tf.constant(data[0]) +Y = tf.one_hot(data[1], depth=2) diff --git a/tests/qnn/test_keras.py b/tests/qnn/test_keras.py index 467a174bdff..11e34f88acb 100644 --- a/tests/qnn/test_keras.py +++ b/tests/qnn/test_keras.py @@ -371,8 +371,8 @@ def test_str_repr(self, get_circuit, output_dim): c, w = get_circuit layer = KerasLayer(c, w, output_dim) - assert layer.__str__() == "" - assert layer.__repr__() == "" + assert layer.__str__() == "" + assert layer.__repr__() == "" @pytest.mark.parametrize("interface", ["tf"]) @pytest.mark.parametrize("n_qubits, output_dim", indicies_up_to(1)) From 6f4f394bdcd20c022295c58cc46b0d154e05c0ec Mon Sep 17 00:00:00 2001 From: trbromley Date: Tue, 24 Mar 2020 18:10:09 -0400 Subject: [PATCH 43/65] Update changelog --- .github/CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 2abbd356dd9..962de98adb8 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -2,6 +2,40 @@

New features since last release

+* PennyLane QNodes can now be converted into Keras layers, allowing for creation of quantum and + hybrid models using the Keras API. + [(#529)](https://github.com/XanaduAI/pennylane/pull/529) + + For example, here is a simple QNode: + + ```python + n_qubits = 2 + dev = qml.device("default.qubit", wires=n_qubits) + + @qml.qnode(dev) + def qnode(inputs, weights_0, weight_1): + qml.RX(inputs, wires=0) + qml.Rot(*weights_0, wires=0) + qml.RY(weight_1, wires=1) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) + ``` + + This can be converted into a Keras Layer using: + + ```python + from pennylane.qnn import KerasLayer + + weight_shapes = {"weights_0": 3, "weight_1": 1} + qlayer = qml.qnn.KerasLayer(qnode, weight_shapes, output_dim=2) + ``` + + A hybrid model can then be easily constructed: + + ```python + model = tf.keras.models.Sequential([qlayer, tf.keras.layers.Dense(2)]) + ``` + * Added the ``BasicEntanglerLayers`` template, which is a simple layer architecture of rotations and CNOT nearest-neighbour entanglers. [(#555)](https://github.com/XanaduAI/pennylane/pull/555) From 11a2aa723b3f5664b46ae5a99f6eb32c80a5e006 Mon Sep 17 00:00:00 2001 From: Tom Bromley <49409390+trbromley@users.noreply.github.com> Date: Mon, 30 Mar 2020 16:00:22 -0400 Subject: [PATCH 44/65] Apply suggestions from code review Co-Authored-By: Josh Izaac --- .github/CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 5722d179b4b..14c31c34654 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -6,8 +6,6 @@ hybrid models using the Keras API. [(#529)](https://github.com/XanaduAI/pennylane/pull/529) - For example, here is a simple QNode: - ```python n_qubits = 2 dev = qml.device("default.qubit", wires=n_qubits) @@ -21,7 +19,7 @@ return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) ``` - This can be converted into a Keras Layer using: + The above QNode can be converted into a Keras layer using the `KerasLayer` class: ```python from pennylane.qnn import KerasLayer From 7f6c59a362d9b36b2efe3efe909cb14f62e48d35 Mon Sep 17 00:00:00 2001 From: trbromley Date: Mon, 30 Mar 2020 16:30:17 -0400 Subject: [PATCH 45/65] Apply suggestions from code review --- pennylane/qnn/keras.py | 19 ++++++++++++------- tests/qnn/test_keras.py | 12 ++++-------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/pennylane/qnn/keras.py b/pennylane/qnn/keras.py index 31d09a3fa67..15a6877a663 100644 --- a/pennylane/qnn/keras.py +++ b/pennylane/qnn/keras.py @@ -29,8 +29,6 @@ Layer = ABC CORRECT_TF_VERSION = False -from pennylane.qnodes import QNode - class KerasLayer(Layer): """A Keras Layer_ for integrating PennyLane QNodes with the Keras API. @@ -181,9 +179,9 @@ def qnode(inputs, weights): def __init__( self, - qnode: QNode, + qnode, weight_shapes: dict, - output_dim: int, + output_dim, weight_specs: Optional[dict] = None, **kwargs ): @@ -219,6 +217,8 @@ def __init__( weight: (tuple(size) if isinstance(size, Iterable) else (size,) if size > 1 else ()) for weight, size in weight_shapes.items() } + + # Allows output_dim to be specified as an int, e.g., 5, or as a length-1 tuple, e.g., (5,) self.output_dim = output_dim[0] if isinstance(output_dim, Iterable) else output_dim defaults = { @@ -258,14 +258,19 @@ def call(self, inputs): tensor: output data """ outputs = [] - for x in inputs: + for x in inputs: # iterate over batch + + # The QNode can require some passed arguments to be positional and others to be keyword. + # The following loops through input arguments in order and uses functools.partial to + # bind the argument to the QNode. qnode = self.qnode + for arg in self.sig: - if arg is not self.input_arg: + if arg is not self.input_arg: # Non-input arguments must always be positional w = self.qnode_weights[arg] qnode = functools.partial(qnode, w) else: - if self.input_is_default: + if self.input_is_default: # The input argument can be positional or keyword qnode = functools.partial(qnode, **{self.input_arg: x}) else: qnode = functools.partial(qnode, x) diff --git a/tests/qnn/test_keras.py b/tests/qnn/test_keras.py index 11e34f88acb..7693fe14277 100644 --- a/tests/qnn/test_keras.py +++ b/tests/qnn/test_keras.py @@ -14,9 +14,6 @@ """ Tests for the pennylane.qnn.keras module. """ -import os -import tempfile - import numpy as np import pytest @@ -433,17 +430,16 @@ def test_model_gradients(self, model, output_dim, n_qubits): @pytest.mark.parametrize("interface", ["tf"]) @pytest.mark.parametrize("n_qubits, output_dim", indicies_up_to(2)) - def test_model_save_weights(self, model, n_qubits): + def test_model_save_weights(self, model, n_qubits, tmpdir): """Test if the model can be successfully saved and reloaded using the get_weights() method""" - _, filename = tempfile.mkstemp() prediction = model.predict(np.ones(n_qubits)) weights = model.get_weights() - model.save_weights(filename) - model.load_weights(filename) + file = str(tmpdir) + '/model' + model.save_weights(file) + model.load_weights(file) prediction_loaded = model.predict(np.ones(n_qubits)) weights_loaded = model.get_weights() - os.remove(filename) assert np.allclose(prediction, prediction_loaded) for i, w in enumerate(weights): From 365d66b00369b18a77701cbb68a343351765cc96 Mon Sep 17 00:00:00 2001 From: trbromley Date: Mon, 30 Mar 2020 16:36:34 -0400 Subject: [PATCH 46/65] Apply suggestions from code review --- tests/qnn/test_keras.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/qnn/test_keras.py b/tests/qnn/test_keras.py index 7693fe14277..3fe84bc16d5 100644 --- a/tests/qnn/test_keras.py +++ b/tests/qnn/test_keras.py @@ -90,7 +90,7 @@ def indicies_up_to(n_max): class TestKerasLayer: """Unit tests for the pennylane.qnn.keras.KerasLayer class.""" - @pytest.mark.parametrize("interface", ["tf"]) + @pytest.mark.parametrize("interface", ["tf"]) # required for the get_circuit fixture @pytest.mark.parametrize("n_qubits, output_dim", indicies_up_to(1)) def test_bad_tf_version(self, get_circuit, output_dim, monkeypatch): """Test if an ImportError is raised when instantiated with an incorrect version of From 8c933847fdf82016813b40e244967faeca9be688 Mon Sep 17 00:00:00 2001 From: Tom Bromley <49409390+trbromley@users.noreply.github.com> Date: Mon, 30 Mar 2020 16:47:06 -0400 Subject: [PATCH 47/65] Apply suggestions from code review Co-Authored-By: Josh Izaac --- pennylane/qnn/__init__.py | 3 +-- pennylane/qnn/keras.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pennylane/qnn/__init__.py b/pennylane/qnn/__init__.py index 38bcd8389f6..90db27feebf 100644 --- a/pennylane/qnn/__init__.py +++ b/pennylane/qnn/__init__.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""This module contains the :class:`~.KerasLayer` class for integrating QNodes with the Keras -layer API.""" +"""This module contains classes and functions for constructing quantum neural networks from QNodes.""" from .keras import KerasLayer diff --git a/pennylane/qnn/keras.py b/pennylane/qnn/keras.py index 15a6877a663..79a2752ffb4 100644 --- a/pennylane/qnn/keras.py +++ b/pennylane/qnn/keras.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""This module is for PennyLane-Keras integration.""" +"""This module contains the classes and functions for integrating QNodes with the Keras Layer API.""" import functools import inspect from collections.abc import Iterable From 0bfca8cde78faf4c22d4b30baadcb4afb7cf84c8 Mon Sep 17 00:00:00 2001 From: trbromley Date: Mon, 30 Mar 2020 16:47:33 -0400 Subject: [PATCH 48/65] Explain import --- pennylane/qnn/keras.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pennylane/qnn/keras.py b/pennylane/qnn/keras.py index 15a6877a663..ccedfd16d05 100644 --- a/pennylane/qnn/keras.py +++ b/pennylane/qnn/keras.py @@ -24,8 +24,9 @@ CORRECT_TF_VERSION = int(tf.__version__.split(".")[0]) > 1 except ImportError: + # The following allows this module to be imported even if TensorFlow is not installed. Users + # will instead see an ImportError when instantiating the KerasLayer. from abc import ABC - Layer = ABC CORRECT_TF_VERSION = False From 1c44624d157f3203dc788fb5665165d3c0cc67f1 Mon Sep 17 00:00:00 2001 From: Tom Bromley <49409390+trbromley@users.noreply.github.com> Date: Mon, 30 Mar 2020 16:53:29 -0400 Subject: [PATCH 49/65] Apply suggestions from code review Co-Authored-By: Josh Izaac Co-Authored-By: Maria Schuld --- pennylane/qnn/keras.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pennylane/qnn/keras.py b/pennylane/qnn/keras.py index 79a2752ffb4..0418f0457e7 100644 --- a/pennylane/qnn/keras.py +++ b/pennylane/qnn/keras.py @@ -31,7 +31,10 @@ class KerasLayer(Layer): - """A Keras Layer_ for integrating PennyLane QNodes with the Keras API. + """This class converts a :func:`~.QNode` to a Keras Layer_, which can be used within the Keras + `Sequential `__ or + `Model `__ classes for + creating quantum and hybrid models. Args: qnode (qml.QNode): the PennyLane QNode to be converted into a Keras Layer_ @@ -54,9 +57,10 @@ class KerasLayer(Layer): .. code-block:: python qlayer = qml.qnn.KerasLayer(qnode, weight_shapes, output_dim=2) - model = tf.keras.models.Sequential([qlayer, tf.keras.layers.Dense(2)]) + clayer = tf.keras.layers.Dense(2) + model = tf.keras.models.Sequential([qlayer, clayer]) - The signature of QNode must contain an ``inputs`` named argument for input data, with all + The signature of QNode **must** contain an ``inputs`` named argument for input data, with all other arguments to be treated as internal weights. A valid ``qnode`` for the example above would be: From 3e6e2e7c8dd15dc10bfd9ebde9633266203c3a90 Mon Sep 17 00:00:00 2001 From: trbromley Date: Mon, 30 Mar 2020 16:53:59 -0400 Subject: [PATCH 50/65] Apply suggestions from code review --- pennylane/qnn/keras.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pennylane/qnn/keras.py b/pennylane/qnn/keras.py index 00046157872..8d37a0fc53f 100644 --- a/pennylane/qnn/keras.py +++ b/pennylane/qnn/keras.py @@ -108,9 +108,9 @@ def qnode(inputs, weights): weight_shapes = {"weights": (3, n_qubits, 3)} qlayer = qml.qnn.KerasLayer(qnode, weight_shapes, output_dim=2) - model = tf.keras.models.Sequential( - [tf.keras.layers.Dense(2), qlayer, tf.keras.layers.Dense(2, activation="softmax"),] - ) + clayer1 = tf.keras.layers.Dense(2) + clayer2 = tf.keras.layers.Dense(2, activation="softmax") + model = tf.keras.models.Sequential([clayer1, qlayer, clayer2]) data = sklearn.datasets.make_moons() X = tf.constant(data[0]) From ee97ad6bf24b8ac4f35dc1501289b4db2ac1e9e5 Mon Sep 17 00:00:00 2001 From: trbromley Date: Mon, 30 Mar 2020 16:57:07 -0400 Subject: [PATCH 51/65] Apply suggestions from code review --- pennylane/qnn/keras.py | 119 ++++++++++++++++++++--------------------- test.py | 23 -------- 2 files changed, 57 insertions(+), 85 deletions(-) delete mode 100644 test.py diff --git a/pennylane/qnn/keras.py b/pennylane/qnn/keras.py index d0b244abb5b..6db1d90bdac 100644 --- a/pennylane/qnn/keras.py +++ b/pennylane/qnn/keras.py @@ -50,10 +50,7 @@ class KerasLayer(Layer): method and values being the corresponding specification. **kwargs: additional keyword arguments passed to the Layer_ base class - This class converts a :func:`~.QNode` to a Keras Layer_, which can be used within the Keras - `Sequential `__ or - `Model `__ classes for - creating quantum and hybrid models: + **Example** .. code-block:: python @@ -86,66 +83,64 @@ def qnode(inputs, weights_0, weight_1): weight_shapes = {"weights_0": 3, "weight_1": 1} - **Example** - - The code block below shows how a circuit composed of templates from the - :doc:`/code/qml_templates` module can be combined with classical - `Dense `__ layers to learn the - two-dimensional `moons `__ dataset. - - .. code-block:: python - - import pennylane as qml - import tensorflow as tf - import sklearn.datasets - - n_qubits = 2 - dev = qml.device("default.qubit", wires=n_qubits) - - @qml.qnode(dev) - def qnode(inputs, weights): - qml.templates.AngleEmbedding(inputs, wires=list(range(n_qubits))) - qml.templates.StronglyEntanglingLayers(weights, wires=list(range(n_qubits))) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - weight_shapes = {"weights": (3, n_qubits, 3)} - - qlayer = qml.qnn.KerasLayer(qnode, weight_shapes, output_dim=2) - clayer1 = tf.keras.layers.Dense(2) - clayer2 = tf.keras.layers.Dense(2, activation="softmax") - model = tf.keras.models.Sequential([clayer1, qlayer, clayer2]) - - data = sklearn.datasets.make_moons() - X = tf.constant(data[0]) - Y = tf.one_hot(data[1], depth=2) - - opt = tf.keras.optimizers.SGD(learning_rate=0.5) - model.compile(opt, loss='mae') - - The model can be trained using: - - >>> model.fit(X, Y, epochs=8, batch_size=5) - Train on 100 samples - Epoch 1/8 - 100/100 [==============================] - 9s 90ms/sample - loss: 0.3524 - Epoch 2/8 - 100/100 [==============================] - 9s 87ms/sample - loss: 0.2441 - Epoch 3/8 - 100/100 [==============================] - 9s 87ms/sample - loss: 0.1908 - Epoch 4/8 - 100/100 [==============================] - 9s 87ms/sample - loss: 0.1832 - Epoch 5/8 - 100/100 [==============================] - 9s 88ms/sample - loss: 0.1596 - Epoch 6/8 - 100/100 [==============================] - 9s 87ms/sample - loss: 0.1637 - Epoch 7/8 - 100/100 [==============================] - 9s 86ms/sample - loss: 0.1613 - Epoch 8/8 - 100/100 [==============================] - 9s 87ms/sample - loss: 0.1474 - .. UsageDetails:: + The code block below shows how a circuit composed of templates from the + :doc:`/code/qml_templates` module can be combined with classical + `Dense `__ layers to learn the + two-dimensional `moons `__ dataset. + + .. code-block:: python + + import pennylane as qml + import tensorflow as tf + import sklearn.datasets + + n_qubits = 2 + dev = qml.device("default.qubit", wires=n_qubits) + + @qml.qnode(dev) + def qnode(inputs, weights): + qml.templates.AngleEmbedding(inputs, wires=list(range(n_qubits))) + qml.templates.StronglyEntanglingLayers(weights, wires=list(range(n_qubits))) + return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) + + weight_shapes = {"weights": (3, n_qubits, 3)} + + qlayer = qml.qnn.KerasLayer(qnode, weight_shapes, output_dim=2) + clayer1 = tf.keras.layers.Dense(2) + clayer2 = tf.keras.layers.Dense(2, activation="softmax") + model = tf.keras.models.Sequential([clayer1, qlayer, clayer2]) + + data = sklearn.datasets.make_moons() + X = tf.constant(data[0]) + Y = tf.one_hot(data[1], depth=2) + + opt = tf.keras.optimizers.SGD(learning_rate=0.5) + model.compile(opt, loss='mae') + + The model can be trained using: + + >>> model.fit(X, Y, epochs=8, batch_size=5) + Train on 100 samples + Epoch 1/8 + 100/100 [==============================] - 9s 90ms/sample - loss: 0.3524 + Epoch 2/8 + 100/100 [==============================] - 9s 87ms/sample - loss: 0.2441 + Epoch 3/8 + 100/100 [==============================] - 9s 87ms/sample - loss: 0.1908 + Epoch 4/8 + 100/100 [==============================] - 9s 87ms/sample - loss: 0.1832 + Epoch 5/8 + 100/100 [==============================] - 9s 88ms/sample - loss: 0.1596 + Epoch 6/8 + 100/100 [==============================] - 9s 87ms/sample - loss: 0.1637 + Epoch 7/8 + 100/100 [==============================] - 9s 86ms/sample - loss: 0.1613 + Epoch 8/8 + 100/100 [==============================] - 9s 87ms/sample - loss: 0.1474 + The QNode must have a signature that satisfies the following conditions: - Contain an ``inputs`` named argument for input data. diff --git a/test.py b/test.py deleted file mode 100644 index b6bb46374d7..00000000000 --- a/test.py +++ /dev/null @@ -1,23 +0,0 @@ -import pennylane as qml -import tensorflow as tf -import sklearn.datasets - -n_qubits = 2 -dev = qml.device("default.qubit", wires=n_qubits) - -@qml.qnode(dev) -def qnode(inputs, weights): - qml.templates.AngleEmbedding(inputs, wires=list(range(n_qubits))) - qml.templates.StronglyEntanglingLayers(weights, wires=list(range(n_qubits))) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - -weight_shapes = {"weights": (3, n_qubits, 3)} - -q_layer = qml.qnn.KerasLayer(qnode, weight_shapes, output_dim=2) -model = tf.keras.models.Sequential( - [tf.keras.layers.Dense(2), q_layer, tf.keras.layers.Dense(2, activation="softmax"),] -) - -data = sklearn.datasets.make_moons() -X = tf.constant(data[0]) -Y = tf.one_hot(data[1], depth=2) From d812c2a1e572c65dd13d041d1995f4e9ecedf47d Mon Sep 17 00:00:00 2001 From: trbromley Date: Mon, 30 Mar 2020 17:17:20 -0400 Subject: [PATCH 52/65] Fix pylint and apply black --- pennylane/qnn/keras.py | 10 +++------- tests/qnn/test_keras.py | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/pennylane/qnn/keras.py b/pennylane/qnn/keras.py index 6db1d90bdac..022c86f6da1 100644 --- a/pennylane/qnn/keras.py +++ b/pennylane/qnn/keras.py @@ -27,6 +27,7 @@ # The following allows this module to be imported even if TensorFlow is not installed. Users # will instead see an ImportError when instantiating the KerasLayer. from abc import ABC + Layer = ABC CORRECT_TF_VERSION = False @@ -56,7 +57,7 @@ class KerasLayer(Layer): qlayer = qml.qnn.KerasLayer(qnode, weight_shapes, output_dim=2) clayer = tf.keras.layers.Dense(2) - model = tf.keras.models.Sequential([qlayer, clayer]) + model = tf.keras.models.Sequential([qlayer, clayer]) The signature of QNode **must** contain an ``inputs`` named argument for input data, with all other arguments to be treated as internal weights. A valid ``qnode`` for the example @@ -178,12 +179,7 @@ def qnode(inputs, weights): input_arg = "inputs" def __init__( - self, - qnode, - weight_shapes: dict, - output_dim, - weight_specs: Optional[dict] = None, - **kwargs + self, qnode, weight_shapes: dict, output_dim, weight_specs: Optional[dict] = None, **kwargs ): if not CORRECT_TF_VERSION: raise ImportError("KerasLayer requires TensorFlow version 2 and above") diff --git a/tests/qnn/test_keras.py b/tests/qnn/test_keras.py index 3fe84bc16d5..621f223ff90 100644 --- a/tests/qnn/test_keras.py +++ b/tests/qnn/test_keras.py @@ -435,7 +435,7 @@ def test_model_save_weights(self, model, n_qubits, tmpdir): method""" prediction = model.predict(np.ones(n_qubits)) weights = model.get_weights() - file = str(tmpdir) + '/model' + file = str(tmpdir) + "/model" model.save_weights(file) model.load_weights(file) prediction_loaded = model.predict(np.ones(n_qubits)) From a7affd5dddb81d98b43215833489d725d0aeafb7 Mon Sep 17 00:00:00 2001 From: trbromley Date: Mon, 30 Mar 2020 17:25:42 -0400 Subject: [PATCH 53/65] Fix docs building --- pennylane/qnn/keras.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pennylane/qnn/keras.py b/pennylane/qnn/keras.py index 022c86f6da1..c88afe9e099 100644 --- a/pennylane/qnn/keras.py +++ b/pennylane/qnn/keras.py @@ -33,7 +33,10 @@ class KerasLayer(Layer): - """This class converts a :func:`~.QNode` to a Keras Layer_, which can be used within the Keras + """Converts a :func:`~.QNode` to a Keras + `Layer `__. + + The result can be used within the Keras `Sequential `__ or `Model `__ classes for creating quantum and hybrid models. From b09442e22b5e0491572e3677ef275967650ab029 Mon Sep 17 00:00:00 2001 From: trbromley Date: Mon, 30 Mar 2020 17:58:53 -0400 Subject: [PATCH 54/65] Improve docs --- pennylane/qnn/keras.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pennylane/qnn/keras.py b/pennylane/qnn/keras.py index c88afe9e099..14284fb9ce9 100644 --- a/pennylane/qnn/keras.py +++ b/pennylane/qnn/keras.py @@ -62,8 +62,8 @@ class KerasLayer(Layer): clayer = tf.keras.layers.Dense(2) model = tf.keras.models.Sequential([qlayer, clayer]) - The signature of QNode **must** contain an ``inputs`` named argument for input data, with all - other arguments to be treated as internal weights. A valid ``qnode`` for the example + The signature of the QNode **must** contain an ``inputs`` named argument for input data, + with all other arguments to be treated as internal weights. A valid ``qnode`` for the example above would be: .. code-block:: python @@ -91,9 +91,9 @@ def qnode(inputs, weights_0, weight_1): The code block below shows how a circuit composed of templates from the :doc:`/code/qml_templates` module can be combined with classical - `Dense `__ layers to learn the - two-dimensional `moons `__ dataset. + `Dense `__ layers to learn + the two-dimensional `moons `__ dataset. .. code-block:: python From 2cd15857ca11bca235d2ef2a6ae0abe1caed4a61 Mon Sep 17 00:00:00 2001 From: trbromley Date: Tue, 31 Mar 2020 14:03:13 -0400 Subject: [PATCH 55/65] Change example to have multidimensional input --- pennylane/qnn/keras.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pennylane/qnn/keras.py b/pennylane/qnn/keras.py index 14284fb9ce9..ab359f7188b 100644 --- a/pennylane/qnn/keras.py +++ b/pennylane/qnn/keras.py @@ -73,7 +73,8 @@ class KerasLayer(Layer): @qml.qnode(dev) def qnode(inputs, weights_0, weight_1): - qml.RX(inputs, wires=0) + qml.RX(inputs[0], wires=0) + qml.RX(inputs[1], wires=1) qml.Rot(*weights_0, wires=0) qml.RY(weight_1, wires=1) qml.CNOT(wires=[0, 1]) From b335b0fa49c083b6510194eb33e30eec2c8e0428 Mon Sep 17 00:00:00 2001 From: trbromley Date: Tue, 31 Mar 2020 14:38:57 -0400 Subject: [PATCH 56/65] Reorder Usage details --- pennylane/qnn/keras.py | 68 ++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/pennylane/qnn/keras.py b/pennylane/qnn/keras.py index ab359f7188b..228a7a8e181 100644 --- a/pennylane/qnn/keras.py +++ b/pennylane/qnn/keras.py @@ -90,6 +90,39 @@ def qnode(inputs, weights_0, weight_1): .. UsageDetails:: + The QNode must have a signature that satisfies the following conditions: + + - Contain an ``inputs`` named argument for input data. + - All other arguments must accept an array or tensor and are treated as internal + weights of the QNode. + - All other arguments must have no default value. + - The ``inputs`` argument is permitted to have a default value provided the gradient with + respect to ``inputs`` is not required. + - There cannot be a variable number of positional or keyword arguments, e.g., no ``*args`` + or ``**kwargs`` present in the signature. + + The optional ``weight_specs`` argument allows for a more fine-grained + specification of the QNode weights, such as the method of initialization and any + regularization or constraints. For example, the initialization method of the ``weights`` + argument in the example above could be specified by: + + .. code-block:: + + weight_specs = {"weights": {"initializer": "random_uniform"}} + + The values of ``weight_specs`` are dictionaries with keys given by arguments of + the Keras + `add_weight() `__ + method. For the ``"initializer"`` argument, one can specify a string such as + ``"random_uniform"`` or an instance of an `Initializer + `__ class, such as + `tf.keras.initializers.RandomUniform `__. + + If ``weight_specs`` is not specified, weights will be added using the Keras default + initialization and without any regularization or constraints. + + **Additional example** + The code block below shows how a circuit composed of templates from the :doc:`/code/qml_templates` module can be combined with classical `Dense `__ layers to learn @@ -107,8 +140,8 @@ def qnode(inputs, weights_0, weight_1): @qml.qnode(dev) def qnode(inputs, weights): - qml.templates.AngleEmbedding(inputs, wires=list(range(n_qubits))) - qml.templates.StronglyEntanglingLayers(weights, wires=list(range(n_qubits))) + qml.templates.AngleEmbedding(inputs, wires=range(n_qubits)) + qml.templates.StronglyEntanglingLayers(weights, wires=range(n_qubits)) return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) weight_shapes = {"weights": (3, n_qubits, 3)} @@ -146,37 +179,6 @@ def qnode(inputs, weights): Epoch 8/8 100/100 [==============================] - 9s 87ms/sample - loss: 0.1474 - The QNode must have a signature that satisfies the following conditions: - - - Contain an ``inputs`` named argument for input data. - - All other arguments must accept an array or tensor and are treated as internal - weights of the QNode. - - All other arguments must have no default value. - - The ``inputs`` argument is permitted to have a default value provided the gradient with - respect to ``inputs`` is not required. - - There cannot be a variable number of positional or keyword arguments, e.g., no ``*args`` - or ``**kwargs`` present in the signature. - - The optional ``weight_specs`` argument allows for a more fine-grained - specification of the QNode weights, such as the method of initialization and any - regularization or constraints. For example, the initialization method of the ``weights`` - argument in the example above could be specified by: - - .. code-block:: - - weight_specs = {"weights": {"initializer": "random_uniform"}} - - The values of ``weight_specs`` are dictionaries with keys given by arguments of - the Keras - `add_weight() `__ - method. For the ``"initializer"`` argument, one can specify a string such as - ``"random_uniform"`` or an instance of an `Initializer - `__ class, such as - `tf.keras.initializers.RandomUniform `__. - - If ``weight_specs`` is not specified, weights will be added using the Keras default - initialization and without any regularization or constraints. - .. _Layer: https://www.tensorflow.org/api_docs/python/tf/keras/layers/Layer """ From daef030d04885c42cb456f727d09d542c64d7482 Mon Sep 17 00:00:00 2001 From: trbromley Date: Tue, 31 Mar 2020 14:54:47 -0400 Subject: [PATCH 57/65] Update input_arg to property --- pennylane/qnn/keras.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/pennylane/qnn/keras.py b/pennylane/qnn/keras.py index 228a7a8e181..a343b31c55b 100644 --- a/pennylane/qnn/keras.py +++ b/pennylane/qnn/keras.py @@ -181,9 +181,6 @@ def qnode(inputs, weights): .. _Layer: https://www.tensorflow.org/api_docs/python/tf/keras/layers/Layer """ - - input_arg = "inputs" - def __init__( self, qnode, weight_shapes: dict, output_dim, weight_specs: Optional[dict] = None, **kwargs ): @@ -297,3 +294,16 @@ def __str__(self): return detail.format(self.qnode.func.__name__) __repr__ = __str__ + + _input_arg = "inputs" + + @property + def input_arg(self): + """Name of the argument to be used as the input to the Keras + `Layer `__. Defaults to + ``"inputs"``.""" + return self._input_arg + + @input_arg.setter + def input_arg(self, arg): + self._input_arg = arg From 1741f791c71a1bfafc483999588e956e842111bc Mon Sep 17 00:00:00 2001 From: trbromley Date: Tue, 31 Mar 2020 15:04:52 -0400 Subject: [PATCH 58/65] Remove setter and fix tests --- pennylane/qnn/keras.py | 4 ---- tests/qnn/test_keras.py | 8 ++++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/pennylane/qnn/keras.py b/pennylane/qnn/keras.py index a343b31c55b..c5cde0d605c 100644 --- a/pennylane/qnn/keras.py +++ b/pennylane/qnn/keras.py @@ -303,7 +303,3 @@ def input_arg(self): `Layer `__. Defaults to ``"inputs"``.""" return self._input_arg - - @input_arg.setter - def input_arg(self, arg): - self._input_arg = arg diff --git a/tests/qnn/test_keras.py b/tests/qnn/test_keras.py index 621f223ff90..1c0b5ad4aa6 100644 --- a/tests/qnn/test_keras.py +++ b/tests/qnn/test_keras.py @@ -107,7 +107,7 @@ def test_no_input(self, get_circuit, output_dim): """Test if a TypeError is raised when instantiated with a QNode that does not have an argument with name equal to the input_arg class attribute of KerasLayer""" c, w = get_circuit - del c.func.sig[qml.qnn.keras.KerasLayer.input_arg] + del c.func.sig[qml.qnn.keras.KerasLayer._input_arg] with pytest.raises(TypeError, match="QNode must include an argument with name"): KerasLayer(c, w, output_dim) @@ -118,11 +118,11 @@ def test_input_in_weight_shapes(self, get_circuit, n_qubits, output_dim): contains the shape of the input argument given by the input_arg class attribute of KerasLayer""" c, w = get_circuit - w[qml.qnn.keras.KerasLayer.input_arg] = n_qubits + w[qml.qnn.keras.KerasLayer._input_arg] = n_qubits with pytest.raises( ValueError, match="{} argument should not have its dimension".format( - qml.qnn.keras.KerasLayer.input_arg + qml.qnn.keras.KerasLayer._input_arg ), ): KerasLayer(c, w, output_dim) @@ -218,7 +218,7 @@ def c_dummy(inputs, w1, w2, w3, w4, w5, w6, w7, w8=None): with pytest.raises( TypeError, - match="Only the argument {} is permitted".format(qml.qnn.keras.KerasLayer.input_arg), + match="Only the argument {} is permitted".format(qml.qnn.keras.KerasLayer._input_arg), ): KerasLayer(c_dummy, {**w, **{"w8": 1}}, output_dim) From e0c35485c59a10dfdd19069d6e8462b4e8570f34 Mon Sep 17 00:00:00 2001 From: trbromley Date: Tue, 31 Mar 2020 15:12:58 -0400 Subject: [PATCH 59/65] Clarify docstring --- pennylane/qnn/keras.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/qnn/keras.py b/pennylane/qnn/keras.py index c5cde0d605c..4a56cdf19ac 100644 --- a/pennylane/qnn/keras.py +++ b/pennylane/qnn/keras.py @@ -300,6 +300,6 @@ def __str__(self): @property def input_arg(self): """Name of the argument to be used as the input to the Keras - `Layer `__. Defaults to + `Layer `__. Set to ``"inputs"``.""" return self._input_arg From 54beadf5f98b633be558240c6bb072e8d5e861d4 Mon Sep 17 00:00:00 2001 From: trbromley Date: Tue, 31 Mar 2020 15:20:23 -0400 Subject: [PATCH 60/65] Apply black --- pennylane/qnn/keras.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pennylane/qnn/keras.py b/pennylane/qnn/keras.py index 4a56cdf19ac..19b03412d85 100644 --- a/pennylane/qnn/keras.py +++ b/pennylane/qnn/keras.py @@ -181,6 +181,7 @@ def qnode(inputs, weights): .. _Layer: https://www.tensorflow.org/api_docs/python/tf/keras/layers/Layer """ + def __init__( self, qnode, weight_shapes: dict, output_dim, weight_specs: Optional[dict] = None, **kwargs ): From bc017a02e6c9ca842f3e0ef91f25d5cffa127479 Mon Sep 17 00:00:00 2001 From: Nathan Killoran Date: Tue, 31 Mar 2020 18:30:40 -0400 Subject: [PATCH 61/65] Update pennylane/qnn/keras.py --- pennylane/qnn/keras.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/qnn/keras.py b/pennylane/qnn/keras.py index 19b03412d85..52f22c50bbc 100644 --- a/pennylane/qnn/keras.py +++ b/pennylane/qnn/keras.py @@ -50,7 +50,7 @@ class KerasLayer(Layer): specifications for weights used in the QNode, such as the method of parameter initialization. This specification is provided as a dictionary with keys given by the arguments of the `add_weight() - `__ + `__. method and values being the corresponding specification. **kwargs: additional keyword arguments passed to the Layer_ base class From a76896742a9b0b4c3f914ea198098adba0890a29 Mon Sep 17 00:00:00 2001 From: trbromley Date: Wed, 1 Apr 2020 09:11:10 -0400 Subject: [PATCH 62/65] Fix typo in changelog --- .github/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index abbd6eb33dd..eae342607f7 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -84,6 +84,7 @@ 1: ──H──────────────╭X──RZ(0.4)──╭X──────H───────────┤ ⟨Z⟩ 2: ──RX(1.571)──╭X──╰C───────────╰C──╭X──RX(-1.571)──┤ ⟨Z⟩ 3: ─────────────╰C───────────────────╰C──────────────┤ ⟨Z⟩ + ``` * Added the ``SimplifiedTwoDesign`` template, which implements the circuit design of `Cerezo et al. (2020) `_. From 66407b1784d03e9e792cc3119105f9df3f269383 Mon Sep 17 00:00:00 2001 From: trbromley Date: Wed, 1 Apr 2020 09:14:24 -0400 Subject: [PATCH 63/65] Fix link --- .github/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index eae342607f7..72957802237 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -87,7 +87,7 @@ ``` * Added the ``SimplifiedTwoDesign`` template, which implements the circuit - design of `Cerezo et al. (2020) `_. + design of [Cerezo et al. (2020)](). [(#556)](https://github.com/XanaduAI/pennylane/pull/556) Date: Wed, 1 Apr 2020 09:22:34 -0400 Subject: [PATCH 64/65] Update changelog to reflect current example --- .github/CHANGELOG.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 72957802237..3e65893714a 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -10,13 +10,14 @@ n_qubits = 2 dev = qml.device("default.qubit", wires=n_qubits) - @qml.qnode(dev) - def qnode(inputs, weights_0, weight_1): - qml.RX(inputs, wires=0) - qml.Rot(*weights_0, wires=0) - qml.RY(weight_1, wires=1) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) + @qml.qnode(dev) + def qnode(inputs, weights_0, weight_1): + qml.RX(inputs[0], wires=0) + qml.RX(inputs[1], wires=1) + qml.Rot(*weights_0, wires=0) + qml.RY(weight_1, wires=1) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) ``` The above QNode can be converted into a Keras layer using the `KerasLayer` class: From 861ec8020003a63066bf05a2703bb3bdd3e30935 Mon Sep 17 00:00:00 2001 From: trbromley Date: Wed, 1 Apr 2020 09:23:08 -0400 Subject: [PATCH 65/65] Update indentation --- .github/CHANGELOG.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 3e65893714a..96c6beea384 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -10,14 +10,14 @@ n_qubits = 2 dev = qml.device("default.qubit", wires=n_qubits) - @qml.qnode(dev) - def qnode(inputs, weights_0, weight_1): - qml.RX(inputs[0], wires=0) - qml.RX(inputs[1], wires=1) - qml.Rot(*weights_0, wires=0) - qml.RY(weight_1, wires=1) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) + @qml.qnode(dev) + def qnode(inputs, weights_0, weight_1): + qml.RX(inputs[0], wires=0) + qml.RX(inputs[1], wires=1) + qml.Rot(*weights_0, wires=0) + qml.RY(weight_1, wires=1) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) ``` The above QNode can be converted into a Keras layer using the `KerasLayer` class: