tensorflow / quantum

Hybrid Quantum-Classical Machine Learning in TensorFlow
https://www.tensorflow.org/quantum
Apache License 2.0
1.8k stars 578 forks source link

How to apply `PQC` or `ControlledPQC`? #742

Open Shuhul24 opened 1 year ago

Shuhul24 commented 1 year ago

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?

lockwo commented 1 year ago

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

Shuhul24 commented 1 year ago

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

lockwo commented 1 year ago

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.

Shuhul24 commented 1 year ago

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?

lockwo commented 1 year ago

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

Shuhul24 commented 1 year ago

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?

lockwo commented 1 year ago

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

Shuhul24 commented 1 year ago

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?

lockwo commented 1 year ago

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

Shuhul24 commented 1 year ago

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.

Shuhul24 commented 1 year ago

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

lockwo commented 1 year ago

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

Shuhul24 commented 1 year ago

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

Shuhul24 commented 1 year ago

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?

lockwo commented 1 year ago

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