PennyLaneAI / qml

Introductions to key concepts in quantum programming, as well as tutorials and implementations from cutting-edge quantum computing research.
https://pennylane.ai/qml
Apache License 2.0
527 stars 185 forks source link

TensorFlow equivalent of tutorial_quantum_transfer_learning.py #28

Closed rdisipio closed 4 years ago

rdisipio commented 4 years ago

Issue description

I'm trying to create a TensorFlow-based equivalent of tutorial_quantum_transfer_learning.py but I get an error related to the conversion of Pennylane objects to Tensors.

Source code and tracebacks

Training...
Train on 11640 samples, validate on 2910 samples
Epoch 1/20
  128/11640 [..............................] - ETA: 1:17Traceback (most recent call last):
  File "./train.py", line 69, in <module>
    callbacks=[],
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/tensorflow_core/python/keras/engine/training.py", line 728, in fit
    use_multiprocessing=use_multiprocessing)
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/tensorflow_core/python/keras/engine/training_v2.py", line 324, in fit
    total_epochs=epochs)
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/tensorflow_core/python/keras/engine/training_v2.py", line 123, in run_one_epoch
    batch_outs = execution_function(iterator)
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/tensorflow_core/python/keras/engine/training_v2_utils.py", line 86, in execution_function
    distributed_function(input_fn))
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/tensorflow_core/python/keras/engine/training_v2_utils.py", line 73, in distributed_function
    per_replica_function, args=(model, x, y, sample_weights))
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/tensorflow_core/python/distribute/distribute_lib.py", line 760, in experimental_run_v2
    return self._extended.call_for_each_replica(fn, args=args, kwargs=kwargs)
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/tensorflow_core/python/distribute/distribute_lib.py", line 1787, in call_for_each_replica
    return self._call_for_each_replica(fn, args, kwargs)
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/tensorflow_core/python/distribute/distribute_lib.py", line 2132, in _call_for_each_replica
    return fn(*args, **kwargs)
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/tensorflow_core/python/autograph/impl/api.py", line 258, in wrapper
    return func(*args, **kwargs)
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/tensorflow_core/python/keras/engine/training_v2_utils.py", line 264, in train_on_batch
    output_loss_metrics=model._output_loss_metrics)
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/tensorflow_core/python/keras/engine/training_eager.py", line 311, in train_on_batch
    output_loss_metrics=output_loss_metrics))
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/tensorflow_core/python/keras/engine/training_eager.py", line 252, in _process_single_batch
    training=training))
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/tensorflow_core/python/keras/engine/training_eager.py", line 127, in _model_loss
    outs = model(inputs, **kwargs)
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/tensorflow_core/python/keras/engine/base_layer.py", line 891, in __call__
    outputs = self.call(cast_inputs, *args, **kwargs)
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/tensorflow_core/python/keras/engine/network.py", line 708, in call
    convert_kwargs_to_constants=base_layer_utils.call_context().saving)
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/tensorflow_core/python/keras/engine/network.py", line 860, in _run_internal_graph
    output_tensors = layer(computed_tensors, **kwargs)
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/tensorflow_core/python/keras/engine/base_layer.py", line 891, in __call__
    outputs = self.call(cast_inputs, *args, **kwargs)
  File "/Users/Riccardo/development/qnlp/models.py", line 126, in call
    q = tf.dtypes.cast(q, tf.float32)
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/tensorflow_core/python/util/dispatch.py", line 180, in wrapper
    return target(*args, **kwargs)
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/tensorflow_core/python/ops/math_ops.py", line 702, in cast
    x = ops.convert_to_tensor(x, name="x")
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/tensorflow_core/python/framework/ops.py", line 1184, in convert_to_tensor
    return convert_to_tensor_v2(value, dtype, preferred_dtype, name)
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/tensorflow_core/python/framework/ops.py", line 1242, in convert_to_tensor_v2
    as_ref=False)
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/tensorflow_core/python/framework/ops.py", line 1296, in internal_convert_to_tensor
    ret = conversion_func(value, dtype=dtype, name=name, as_ref=as_ref)
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/tensorflow_core/python/framework/constant_op.py", line 286, in _constant_tensor_conversion_function
    return constant(v, dtype=dtype, name=name)
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/tensorflow_core/python/framework/constant_op.py", line 227, in constant
    allow_broadcast=True)
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/tensorflow_core/python/framework/constant_op.py", line 235, in _constant_impl
    t = convert_to_eager_tensor(value, ctx, dtype)
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/tensorflow_core/python/framework/constant_op.py", line 96, in convert_to_eager_tensor
    return ops.EagerTensor(value, ctx.device_name, dtype)
ValueError: Attempt to convert a value (<error computing repr()>) with an unsupported type (<class 'pennylane.interfaces.tf.TFQNode.<locals>.qnode_str'>) to a Tensor.

Additional information

It would be nice if you could provide some more examples of interplay between "classical" and quantum ML using TensorFlow instead of (or beside) PyTorch.

josh146 commented 4 years ago

Hi @rdisipio! Thanks for logging this bug. Have you had a look at our PennyLane TensorFlow quickstart guide?

It would also be great if you could add a minimal non-working code example that leads to the above traceback (this might be something as simple as the defined QNode and how it is called).

rdisipio commented 4 years ago

Hi,

thanks for the quick reply. The main script is here:

https://github.com/rdisipio/qnlp/blob/master/train.py

in turns, this create a model defined in:

https://github.com/rdisipio/qnlp/blob/master/models.py

The model is mostly a TF re-implementation of the PyTorch class in the example. As I said, I'm having a hard time to figure out where the offending line is as the traceback never reference any part of my code. I assume I'm passing an object in the wrong format (if I can suggest, an assert statement, properly placed, may help to catch such a situation).

rdisipio commented 4 years ago

I think I'm getting closer to where the issue arises. These lines seem to reproduce the error:

q = qml.QNode(self.variational_quantum_circuit(q_in), self.dev).to_tf()
q = tf.convert_to_tensor(q, dtype=tf.float32)

where q_in is a numpy array like this: [ 0.08495745 -0.20793235 0.03586568 -0.016984 ]

rdisipio commented 4 years ago

getting there...so the solution was to convert the QNode object to a tensor before putting it into the list but also expanding its dimensions, and then concatenate the elements:

q_out = []
for q_in in x.numpy(): 
   q = qml.QNode(self.variational_quantum_circuit(q_in), self.dev).to_tf()
   q = tf.convert_to_tensor(q_in, dtype=tf.float32)
   q = tf.expand_dims(q, 0) #(1,4)
   q_out.append(q)
q_out = tf.concat(q_out, axis=0)
q_out = layers.Dense(self.output_dim)(q_out)

But now I get another error:

ValueError: No gradients provided for any variable: ['dressed_quantum_circuit/kernel:0'].

In the examples, a tf.GradientTape() context is created to define the loss and gradient. Is there a known way to make it work with the Keras fit API? Or do I have to define a training loop manually? That would be a significant feature to let people just replace a classical layer with a quantum one without having to change large parts of the code.

josh146 commented 4 years ago

As I said, I'm having a hard time to figure out where the offending line is as the traceback never reference any part of my code. I assume I'm passing an object in the wrong format (if I can suggest, an assert statement, properly placed, may help to catch such a situation).

This is something we'll definitely look into! Unfortunately, TensorFlow makes this quite hard to catch, as their custom gradient decorator wraps and replaces the QNode, leading to the very tensorflow heavy tracebacks.


In our current development version of PennyLane (v0.8-dev) we have refactored the QNodes in such a way that it may solve the issue you are having --- could I get you to try the development branch?

It can be installed via git:

pip install git+https://github.com/XanaduAI/pennylane.git#egg=pennylane

Once installed, QNode is now a constructor function, that now has the same signature as the decorator:

q = qml.QNode(self.variational_quantum_circuit(q_in), self.dev, interface="tf")
trbromley commented 4 years ago

Hi @rdisipio - I've had a go at making a similar Keras quantum layer before and I think you're on the right track. It took me a bit of playing to get things to work, some things I can quickly spot from your code are:

rdisipio commented 4 years ago

Hi @trbromley ,

thanks for the suggestions. I'm passing the dynamic flag before kwargs. Also, forcing tensor to be tf.float64 did not change much. Yeah, it seems to require lots of fiddling to make it work, but it's worth doing so IMO as some pre-trained models are available only for TF.

rdisipio commented 4 years ago

Hi @josh146 ,

thanks for taking care of this. I adapted the line you pointed out to comply with the new API. But now I get:


  File "/Users/Riccardo/development/qnlp/models.py", line 135, in call
    q_out = self.run_quantum_circuit(x)
  File "/Users/Riccardo/development/qnlp/models.py", line 114, in run_quantum_circuit
    q = qml.QNode(self.variational_quantum_circuit(q_in), self.dev, interface="tf") #0.8-dev
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/pennylane/qnodes/decorator.py", line 106, in QNode
    node = PARAMETER_SHIFT_QNODES[model](func, device, mutable=mutable, properties=properties)
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/pennylane/qnodes/jacobian.py", line 32, in __init__
    super().__init__(func, device, mutable=mutable, properties=properties)
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/pennylane/qnodes/base.py", line 193, in __init__
    _get_signature(self.func)
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/pennylane/qnodes/base.py", line 70, in _get_signature
    sig = inspect.signature(func)
  File "/Users/Riccardo/.pyenv/versions/3.6.9/lib/python3.6/inspect.py", line 3065, in signature
    return Signature.from_callable(obj, follow_wrapped=follow_wrapped)
  File "/Users/Riccardo/.pyenv/versions/3.6.9/lib/python3.6/inspect.py", line 2815, in from_callable
    follow_wrapper_chains=follow_wrapped)
  File "/Users/Riccardo/.pyenv/versions/3.6.9/lib/python3.6/inspect.py", line 2193, in _signature_from_callable
    raise TypeError('{!r} is not a callable object'.format(obj))
TypeError: [<pennylane.ops.qubit.PauliZ object at 0x148a4d2e8>, <pennylane.ops.qubit.PauliZ object at 0x141f8e9e8>, <pennylane.ops.qubit.PauliZ object at 0x149405c18>, <pennylane.ops.qubit.PauliZ object at 0x149405630>] is not a callable object```
josh146 commented 4 years ago

Ah, this is occurring because the first argument of QNode must be the quantum circuit function itself, un-evaluated:

q = qml.QNode(self.variational_quantum_circuit, self.dev, interface="tf")

I also have a feeling this will fail as variational_quantum_circuit is a method, not a function, and the QNode was designed to only be used with functions :thinking:

rdisipio commented 4 years ago

Uhm. But then, how do I pass arguments to the function?

  File "/Users/Riccardo/development/qnlp/models.py", line 122, in run_quantum_circuit
    q = qml.QNode(self._variational_quantum_circuit, self.dev, interface="tf") #0.8-dev
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/pennylane/qnodes/decorator.py", line 106, in QNode
    node = PARAMETER_SHIFT_QNODES[model](func, device, mutable=mutable, properties=properties)
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/pennylane/qnodes/jacobian.py", line 32, in __init__
    super().__init__(func, device, mutable=mutable, properties=properties)
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/pennylane/qnodes/base.py", line 193, in __init__
    _get_signature(self.func)
  File "/Users/Riccardo/development/qc/lib/python3.6/site-packages/pennylane/qnodes/base.py", line 73, in _get_signature
    func.var_pos = False
AttributeError: 'method' object has no attribute 'var_pos'
trbromley commented 4 years ago

Not sure if this helps @rdisipio, I had the following in my __init__:

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")

You can then treat self.layer as the function you use later on in the call method.

rdisipio commented 4 years ago

Hi,

thanks for the suggestion. I'm still quite confused. In a derived class from Keras.layers.Layer, the parameters are defined in the build() function, and the inputs in the call() function. So how do I pass these parameters to circuit(inputs, parameters) in __init__?

Also, the error I'm still getting

ValueError: No gradients provided for any variable: ['dressed_quantum_circuit/kernel:0'].

seems to be related to the model's kernel, which I define in build() as this:

 def build(self, input_shape):
        print("Building dressed_quantum_circuit")
        #print(input_shape) #(None, 512)
        self.kernel = self.add_weight(name='kernel', 
                                      shape=(self.q_depth, self.n_qubits),
                                      initializer='uniform',
                                      trainable=True)
        super(dressed_quantum_circuit, self).build(input_shape)  # Be sure to call this at the end

So it's probably not related to pennylane, but still, do you have any idea why these parameters do not seem to be picked up by the gradient?

rdisipio commented 4 years ago

I made some progress. I can get rid of the error concerning missing gradient for kernel variables by defining custom weights rather than calling a Dense layer from call(). However, I still get some troubles apparently due to the dynamic=True flag. My understanding is that this is needed to manipulate information that does not result in a static graph, such as in RNNs (not sure why that's the case). Is this correct for the QNode as well?

If I set dynamic=False, I do not know how to loop over the inputs to apply the quantum operations. The tensor object has a .numpy() function only if dynamic=True.

for idx, element in enumerate(w):
        qml.RY(element, wires=idx)

This only works if w is a numpy array, or I get the error:

    OperatorNotAllowedInGraphError: iterating over `tf.Tensor` is not allowed in Graph execution. Use Eager execution or decorate this function with @tf.function.

On the other hand, if I set dynamic=True, the quantum layer does not complain, but I get this other error:

E tensorflow/core/common_runtime/executor.cc:642] Executor failed to create kernel. Invalid argument: Value for attr 'T' of string is not in the list of allowed values: float, double, int32, uint8, int16, int8, complex64, int64, qint8, quint8, qint32, bfloat16, uint16, complex128, half, uint32, uint64, variant
    ; NodeDef: {{node Func/_303}}; Op<name=AddN; signature=inputs:N*T -> sum:T; attr=N:int,min=1; attr=T:type,allowed=[DT_FLOAT, DT_DOUBLE, DT_INT32, DT_UINT8, DT_INT16, ..., DT_COMPLEX128, DT_HALF, DT_UINT32, DT_UINT64, DT_VARIANT]; is_commutative=true; is_aggregate=true>
     [[Func/_303]]
2020-01-15 15:48:31.783444: W tensorflow/core/common_runtime/base_collective_executor.cc:216] BaseCollectiveExecutor::StartAbort Invalid argument: Value for attr 'T' of string is not in the list of allowed values: float, double, int32, uint8, int16, int8, complex64, int64, qint8, quint8, qint32, bfloat16, uint16, complex128, half, uint32, uint64, variant
    ; NodeDef: {{node Func/_303}}; Op<name=AddN; signature=inputs:N*T -> sum:T; attr=N:int,min=1; attr=T:type,allowed=[DT_FLOAT, DT_DOUBLE, DT_INT32, DT_UINT8, DT_INT16, ..., DT_COMPLEX128, DT_HALF, DT_UINT32, DT_UINT64, DT_VARIANT]; is_commutative=true; is_aggregate=true>
     [[Func/_303]]
     [[gradients/StatefulPartitionedCall_grad/PartitionedCall/gradients/StatefulPartitionedCall_grad/SymbolicGradient]]
trbromley commented 4 years ago

Hi @rdisipio, let me share with you an example that I got to work:

class QuantumLayers(Layer):
    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 = initializers.get(rotations_initializer)
        self.rotations_regularizer = regularizers.get(rotations_regularizer)
        self.rotations_constraint = constraints.get(rotations_constraint)

        self.input_spec = 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": initializers.serialize(self.rotations_initializer),
            "rotations_regularizer": regularizers.serialize(self.rotations_regularizer),
            "rotations_constraint": constraints.serialize(self.rotations_constraint),
        }
        base_config = super(QuantumLayers, self).get_config()
        return dict(list(base_config.items()) + list(config.items()))

Note that this layer applies the quantum circuit alone - there is no dressing with classical layers so you have to be careful about the input dimension.

rdisipio commented 4 years ago

Hi @trbromley ,

thanks for sharing the code. Finally, I can make my code work by following your example, with the main difference that I have to calculate the documents' embeddings outside the network as pass them as input to the network instead of strings. I assume that's a quirk of TensorFlow and has not much to do with PennyLane (to be checked later).

Also, I wonder if the quantum layer you are using is of the same kind of the one that appears in the transfer learning paper (arXiv:1912.08278 references arXiv:1804.00633).

josh146 commented 4 years ago

Closing this for now as the code is now working :)

@rdisipio, if you can write your ML example as a single python script, feel free to make a PR here to add it to the list of demos on https://pennylane.ai/qml!