tensorflow/quantum

How to apply `PQC` or `ControlledPQC`?

Shuhul24 opened this issue · 16 comments

I am using ControlledPQC and PQC for a couple of times in quantum machine learning. My question is that since I don't have any encoding data to be integrated into the quantum circuit, how can I just apply a ControlledPQC or PQC on just states that initialized as 0 states. In tutorials of PQC, they have been using some classical data and then encoding them and then applying the tfq.layers.PQC and finally compiling all these into tf.keras.Sequential. But in my case, I have input as qubits initialized as states?
Is there any way to apply PQC in this case?

Edit: I came across tfq.layers.Expectation which takes a circuit and a bunch of parameters as an input. But I don't get to know how to use tfq.layers.Expectation epoch-wise on a bunch of data? Is there any sample of code available where I can apply it on a very small data, say tf.random.normal? How can tfq.layers.Expectation be used as tf.keras.layers.Layer class?

To apply it on a |0> state you can just define an tensor in_ = tfq.convert_to_tensor([cirq.Circuit()]) then do PQC(in_). You can input arbitrary circuits (I think they have to be serializable though) as an input as well (not just an empty circuit).

Thanks for the reply!
I have a piece of code that I implemented:

gen_params = sympy.symbols("a0:13")

def encode_circuit_gen(params):
  qubits = [cirq.GridQubit(1, 0), cirq.GridQubit(1, 1)]
  circuit = cirq.Circuit()
  circuit.append(cirq.H(cirq.GridQubit(1, 0)))
  circuit.append(cirq.H(cirq.GridQubit(1, 1)))
  circuit.append(cirq.Rx(rads=params[0]).on(cirq.GridQubit(1, 0)))
  circuit.append(cirq.Rx(rads=params[1]).on(cirq.GridQubit(1, 1)))
  return circuit

class QuantumLayer(tf.keras.layers.Layer):
  def __init__(self) -> None:
    super().__init__()
    ops = [cirq.Z(cirq.GridQubit(1, 0)), cirq.Z(cirq.GridQubit(1, 1))]
    qubits = [cirq.GridQubit(1, 0), cirq.GridQubit(1, 1)]
    encoded = encode_circuit_gen(gen_enc_param)
    ansatz_circuit = ansatz_gen(qubits, gen_params)
    circuit = encoded + ansatz_circuit
    self.quantum_operation = tfq.layers.ControlledPQC(circuit, ops,
                              differentiator=tfq.differentiators.ParameterShift())
    self.quantum_weights = tf.Variable(initial_value = np.random.uniform(-np.pi, np.pi, len(gen_params)), dtype="float32", trainable=True)
    self.circuit_tensor = tfq.convert_to_tensor([cirq.Circuit()])

  def call(self, inputs):

    circuit_batch_dim = tf.gather(tf.shape(inputs), 0)
    tiled_b = tf.tile(tf.expand_dims(self.quantum_weights, 0), [circuit_batch_dim, 1])
    quantum_inputs = tf.concat([inputs, tiled_b], axis=1)
    tiled_circuits = tf.tile(self.circuit_tensor, [circuit_batch_dim])
    quantum_output = self.quantum_operation([tiled_circuits, quantum_inputs])
    return quantum_output  

Is this circuit taking in |0> state or an encoded part? This has been taken from https://github.com/lockwo/quantum_computation/blob/master/TFQ/RL_QVC/atari_qddqn.py and https://github.com/lockwo/quantum_computation/blob/master/TFQ/RL_QVC/policies.py

Good to see my code be useful to others. The input flow goes circuit_tensor -> encoded -> ansatz_circuit -> Z, Z ops. The circuit tensor input is the |0> state. It looks like the layer input (i.e. the input to the call function) are the encoding parameters.

Thanks for the reply!
I have updated my above code as follows:

class QuantumLayer(tf.keras.layers.Layer):
  def __init__(self, batch_size) -> None:
    super().__init__()
    ops = [cirq.Z(cirq.GridQubit(1, 0)), cirq.Z(cirq.GridQubit(1, 1))]
    qubits = [cirq.GridQubit(1, 0), cirq.GridQubit(1, 1)]
    # encoded = encode_circuit_gen(gen_enc_param)
    ansatz_circuit = ansatz_gen(qubits, gen_params)
    circuit = ansatz_circuit
    self.batch_size = batch_size
    self.quantum_operation = tfq.layers.ControlledPQC(circuit, ops)
                              # differentiator=tfq.differentiators.ParameterShift())
    self.quantum_weights = tf.Variable(initial_value = np.random.uniform(-np.pi, np.pi, len(gen_params)), dtype="float32", trainable=True)
    self.circuit_tensor = tfq.convert_to_tensor([cirq.Circuit()])

  def call(self):

    batch = tf.constant(self.batch_size)
    tiled = tf.tile(tf.expand_dims(self.quantum_weights, 0), [batch, 1])

    # circuit_batch_dim = tf.gather(tf.shape(inputs), 0)
    # tiled_b = tf.tile(tf.expand_dims(self.quantum_weights, 0), [circuit_batch_dim, 1])
    # quantum_inputs = tf.concat([inputs, tiled_b], axis=1)

    circuits = tf.tile(self.circuit_tensor, [batch])
    quantum_output = self.quantum_operation([circuits, tiled])

    return quantum_output 

In this I haven't taken anything input, and the initial states, I suppose, are to be |0>. But the error I am coming up is when I call the function inside tf.keras.Sequential layer.

quantum = tf.keras.Sequential([
    QuantumLayer(1),
])
print(quantum)

But the output comes out to be:
python <keras.engine.sequential.Sequential object at 0x7f40337cfe80>
which is not what I desire. I need the measured values to be in the output rather than this keras.engine.sequential.Sequential. Is this possible anyhow?

You have to actually call it (not just print what it is). So do quantum() rather than quantum.

After doing quantum(), I'm getting an error:

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
[<ipython-input-30-9ac82edd80fb>](https://localhost:8080/#) in <module>
      2     Generator(1),
      3 ])
----> 4 generator()

1 frames
[/usr/local/lib/python3.8/dist-packages/keras/engine/base_layer.py](https://localhost:8080/#) in _split_out_first_arg(self, args, kwargs)
   3074       inputs = kwargs.pop(self._call_fn_args[0])
   3075     else:
-> 3076       raise ValueError(
   3077           'The first argument to `Layer.call` must always be passed.')
   3078     return inputs, args, kwargs

ValueError: The first argument to `Layer.call` must always be passed.

Is there something that I am missing?

Are you doing quantum or quantum()? It seems like the object is being printed, not the result of the call.

After doing quantum(), I'm getting an error:

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
[<ipython-input-30-9ac82edd80fb>](https://localhost:8080/#) in <module>
      2     Generator(1),
      3 ])
----> 4 generator()

1 frames
[/usr/local/lib/python3.8/dist-packages/keras/engine/base_layer.py](https://localhost:8080/#) in _split_out_first_arg(self, args, kwargs)
   3074       inputs = kwargs.pop(self._call_fn_args[0])
   3075     else:
-> 3076       raise ValueError(
   3077           'The first argument to `Layer.call` must always be passed.')
   3078     return inputs, args, kwargs

ValueError: The first argument to `Layer.call` must always be passed.

Is there something that I am missing?

It looks like TF models require an input to all call functions (makes sense, TF tries to make a compute graph mapping inputs to outputs) and it doesn't like that you aren't passing anything when calling

Well, in my case batch_size is the input that I am using. Following is the tweaked code:

class QuantumLayer(tf.keras.layers.Layer):
  def __init__(self) -> None:
    super(QuantumLayer, self).__init__()
    ops = [cirq.Z(cirq.GridQubit(1, 0)), cirq.Z(cirq.GridQubit(1, 1))]
    qubits = [cirq.GridQubit(1, 0), cirq.GridQubit(1, 1)]
    # encoded = encode_circuit_gen(gen_enc_param)
    ansatz_circuit = ansatz_gen(qubits, gen_params)
    circuit = ansatz_circuit
    self.quantum_operation = tfq.layers.ControlledPQC(circuit, ops)
                              # differentiator=tfq.differentiators.ParameterShift())
    self.quantum_weights = tf.Variable(initial_value = np.random.uniform(-np.pi, np.pi, NUM_GPARAMS), dtype="float32", trainable=True)
    self.circuit_tensor = tfq.convert_to_tensor([cirq.Circuit()])

  def call(self, batch_size):

    batch = tf.constant(batch_size)
    tiled = tf.tile(tf.expand_dims(self.quantum_weights, 0), [batch, 1])

    # circuit_batch_dim = tf.gather(tf.shape(inputs), 0)
    # tiled_b = tf.tile(tf.expand_dims(self.quantum_weights, 0), [circuit_batch_dim, 1])
    # quantum_inputs = tf.concat([inputs, tiled_b], axis=1)

    circuits = tf.tile(self.circuit_tensor, [batch])
    quantum_output = self.quantum_operation([circuits, tiled])

    return quantum_output  

Here, in the call function, I have applied batch_size which is what I am taking as an input (which isn't necessary, yet Layer.call needs an input argument). Yet I'm getting an error.

Can I use tfq.layers.ControlledPQC as a variable and then apply tape.gradient and optimizer.apply_gradient?

You can apply it to the trainable variables of a ControlledPQC, yes

Cool. Will take this into consideration and try to implement it in an another way.
Thanks a lot for the replies!

I have got one doubt that is kinda creating a problem. Say that I have built a circuit on 2 qubits, which are, cirq.GridQubit(1,0) and cirq.GridQubit(1, 1). Consider that the circuit consists a bunch of rotation operations and CNOT gates in it. Now, I have applied this quantum circuit through tfq.layers.ControlledPQC and for the observable part I have applied cirq.Z(cirq.GridQubit(1,0)). Does this observable means that it will measure the expectation value in |0>,|1> basis AFTER all the operations (rotations and entangling gates) has occured or BEFORE all these operations. Because in the MNIST example they have considered the readout at cirq.GridQubit(-1, -1) which kind of creates a doubt that the observable will be giving the same output as in the MNIST example?

It is after, the ControlledPQC circuit is applied after the input then the result is generated

@Shuhul24 It looks you may have received enough information to answer your question. Can this issue be closed?