Giter Site home page Giter Site logo

Comments (29)

mariaschuld avatar mariaschuld commented on August 20, 2024 4

Maybe we can speak about (maximum) one measurement per subsystem per QNode. We do not get into trouble with non-commuting measurements then.

from pennylane.

co9olguy avatar co9olguy commented on August 20, 2024 1

I'm hearing arguments in favour of one Qnode <-> one measurement, and there are also suggestions (my own included) to have multiple measurements per Qnode.

As @cgogolin points out, we will be constrained by what the plugins/backends actually allow. Since our main qubit plugin only supports one measurement, I'm thinking it it is the path of least resistance to force Qnodes to only have one measurement for our initial version. Multiple measurements can be stitched together from multiple Qnode outputs.

Any contrary opinions on this particular point?

from pennylane.

josh146 avatar josh146 commented on August 20, 2024

Currently, the user defined quantum function takes an arbitrary number of positional arguments:

def circuit(x,y,z):

Each of these can either be a variational parameter or input data, depending on how the function is defined in the cost function.

Sneaky edit: the following suggestions seem more apt for the cost function, not the quantum node/quantum function. Perhaps the quantum function should remain arbitrary and up to the user, and only the cost function is restricted.

Some suggestions are:

  1. Use two positional arguments, each accepting an array. The first accepts only parameters, the second only input data.

    def circuit(params, input_data):

    While easier to code, perhaps too restrictive for the user, who is used to any combination of arguments in a user defined function?

  2. Use positional arguments for the parameters to be varied, but use a keyword argument accepting an array for input data:

    def circuit(x, y, z, *, input_data):
  3. Support only keyword arguments, each accepting an array:

    def circuit(*, parameters, input_data):

Downsides of 2. and 3. are that the user would have to use the (not commonly known) syntax * for specifying keyword-only arguments.

from pennylane.

josh146 avatar josh146 commented on August 20, 2024

Another factor in the design is how to get autograd to play nicely; we want the wrapped quantum function to be called exactly as it is defined, to avoid confusing the user.

At the moment, my qfunc decorator works as follows:

def qfunc(device):
    def qfunc_decorator(func):
        qnode = QNode(func, device)
        @wraps(func)
        def wrapper(*args, **kwargs):
            return qnode(*args, **kwargs)
        return wrapper
    return qfunc_decorator

i.e. if the function is defined with multiple positional arguments, it must be called with a single argument, that is unwrapped behind the scenes. This is confusing, but for some reason, the only way I could get autograd to work with multiple arguments; otherwise I get ArgNum errors.

@smite, do you have any suggestions?

from pennylane.

mariaschuld avatar mariaschuld commented on August 20, 2024

I am very much in favour of 1. However, we should keep in mind that a circuit might not take any data, or even any parameters. For example when the quantum node is already optimized and one wants to now optimize a classical node, the parameters of the quantum node are fixed. Or if the quantum node is part of the ML model but not trainable (think of the kernel methods where the quantum node takes data and computes kernel values that are part of a classically trained model).

from pennylane.

josh146 avatar josh146 commented on August 20, 2024

Actually, thinking about it some more, do we need to put constraints on the arguments of the quantum function? Perhaps we would need constraints on the arguments to the cost function, since that is being passed on to the optimizer.

from pennylane.

smite avatar smite commented on August 20, 2024

My suggestion is to leave the QNode fairly abstract, a R^n \to R^m mapping, with one input argument and one output argument, which are NumPy 1D arrays. The classical cost function can distinguish between trainable parameters and data. See for example tests/test_optimization.py, especially the map_data() function there.

from pennylane.

co9olguy avatar co9olguy commented on August 20, 2024

Programmatically, there is not really any need to distinguish between data and parameters for quantum functions. Within the variational approach, these are all used the same way: as arguments to one-parameter gates. Later on, we can tell the optimizer which we are optimizing (and this retains the flexibility to 'freeze' some parameters while updating others). Let's remember that something being a "parameter" of the quantum circuit does not require that it is trainable/updatable. A parameter is simply something which determines which function from the class of possible functions is the one that is applied to the inputs.

However, there is an argument for making things conceptually clear for the user. Users might not be too familiar with the ideas developing around variational circuits, especially the notion of circuit gradients. Explicitly specifying which parts of the circuit are parameters will make it clearer what is happening with the automatic differentiation.

from pennylane.

co9olguy avatar co9olguy commented on August 20, 2024

@smite I am thinking along the same lines, but does the input have to be a 1D array? Doesn't autograd support tuples, lists, nested inputs as well?

from pennylane.

mariaschuld avatar mariaschuld commented on August 20, 2024

I think there could be two options. Make circuit(...) something that has fully user defined inputs, or use keyword arguments "weights" and "data" (which default to None). If it is user defined, do we get into trouble because computing the gradient of a QNode assumes that all arguments are trainable parameters?

from pennylane.

mariaschuld avatar mariaschuld commented on August 20, 2024

By the way, can we call all trainable parameters simpy "weights"? That is clearly separated from circuit parameters, hyperparameters and has an intuitive meaning in ML...

from pennylane.

co9olguy avatar co9olguy commented on August 20, 2024

@smite: an example based on my earlier comment:
There is an neural network example in autograd where they structure the params into a list of tuples (rather than a 1D array). This does not impact the automatic differentiation in any way, but it makes things much easier to parse for anyone reading the code. Can we do something similar for a Qnode, where the first argument is the params, but it can come in any form that auto grad supports, e.g.:

def my_quantum_function(params):
    # params is a list of (weights, bias) tuples, 
    # i.e., [(W_1, b_1),(W_2, b_2),(W_3, b_3),...]
    for W, b in params:
        Sgate(W)
        Dgate(b)
        ...

This would certainly make things more flexible for the user

from pennylane.

co9olguy avatar co9olguy commented on August 20, 2024

As @smite mentions above, we likely want to allow for a quantum node to output a vector in R^m. This has multiple use-cases:

  1. a batch of outputs, all measuring the same expectation values, but for different inputs
  2. a set of expectation values, each measured on the same circuit, but potentially for different operators (like how in VQE we measure multiple output operators to build up the Hamiltonian)
  3. a set of expectation values, each measured on the same circuit, but on different qubits/qumodes

A smart way to do this is for us to make measurements/expvals a separate abstraction. All the gates a user declares in my_quantum_function get put in the same same circuit, but the user can declare multiple measurements that the device is responsible for evaluating expectation values of. The device should evaluate each of these separately (on the same input circuit), then the plugin stitches them together and returns the collection back to openqml as a list/array.

Sketch pseudocode:

def my_quantum_function(...):
    # declare the gates of the circuit
    RotX(...)
    RotY(...)
    CNOT(...)
   
    # now declare the measurements that should be performed
    # on the above circuit. The following code should return a
    # 3-dimensional vector of expectation values
    return Expectation([SigmaX, SigmaY, SigmaZ])

from pennylane.

mariaschuld avatar mariaschuld commented on August 20, 2024

Two quick comments, if I may:

  • How would the pseudocode example reflect that the Sigmas act on user-defined qubits?
  • If "Expectation" can also be an array of measurement results (if in the device the shots are >0), isn't the name misleading since the user has to average in order to estimate the expectation?

Maybe Expectation could have attributes
"shots",
"measurement_results" (listing all samples of the measurement)
"expectation" (averaging over samples if shots>0, else the simulated exact value),
"exact" (True if it is the exact value in case of simulations)

Sorry just contributing my two cents...

from pennylane.

cgogolin avatar cgogolin commented on August 20, 2024

Some thoughts from my side:

  1. As @josh146 said already said above: Can we not leave circuit() up to the user? The optimizer should only care about cost(). A user should in principle even be able to define cost() without referring to a circuit, no?
  2. I think it would be good to avoid the * syntax.
  3. It would be great if the weights/parameters could also be given as dictionaries. This seems to be the most "natural" data type, especially for more complex circuits. This would allow the user to come up with their own naming scheme for the weights. The trained weights should then likewise be returned as an dictionary, so that the user can easily retrieve a specific weight they might be interested in.
  4. If the return value of Expectation() can then be either a description of the statistics (e.g., expectation value and maybe variance), or a specimen from the statistics, i.e., a finite set of samples, then maybe could call it measurement_statistics()?

from pennylane.

mariaschuld avatar mariaschuld commented on August 20, 2024

Quick comment on number 3: The dictionary idea is indeed important for more complex machine learning tasks, but if I am not mistaken this would cause major fixes because we cannot apply autograd directly to cost. I thought about this at length for the QMLT and came to the conclusion that within a realistic 5-year timeframe, having weights as nested arrays or tuples is enough for the type of experiments people do. In 5 years we could always wrap a more abstract argument type around everything. What do you think @cgogolin?

from pennylane.

cgogolin avatar cgogolin commented on August 20, 2024

If this causes major headaches with autograd, then let's forget about it. I naively thought that it should be easy to serialize a dictionary into an array, then use autograd, and then de-serialize the array back into a dictionary.

from pennylane.

josh146 avatar josh146 commented on August 20, 2024

@cgogolin I agree with your points 1, 2. We should be able to support both def circuit(x,y,z), def circuit(params), and any combination of the two (even including keyword arguments - these could be automatically excluded/included from autograd); the only constraint would be ensuring all work with autograd.

@co9olguy, regarding your proposal for expectation values - it makes sense to implement multiple expectation values on different wires, that can be run by a single hardware device:

def circuit():
   ...
   qm.expectation.PauliZ(0)
   qm.expectation.PauliZ(1)
   qm.expectation.PauliZ(3)

which would then automatically return a vector to the user. (Or perhaps we could use a return statement? more of an interface decision).

However, for multiple expectation values on the same wire, I would rather the user define a new device/qnode altogether, like @mariaschuld does in her VQE example:

def ansatz(params):
   qm.Rot(...)
   qm.CNOT(...)

@qfunc
def circuit_meas_x(params):
   ansatz(params)
   qm.expectation.PauliX(1)

@qfunc
def circuit_meas_z(params):
   ansatz(params)
   qm.expectation.PauliZ(1)

This already works with the current refactor, and has the benefits that:

  1. While it is more verbose, it is simple, clear, and very low level - design decisions are left up to the user.
  2. It is a bare-bones solution that doesn't require additional design constraints now - it gives us the flexibility to make these decisions in later versions.
  3. I like that it encapsulates the idea that each QNode is a single quantum device/circuit that can be run agnostically on different frameworks, without any nuance about having to re-run the operations and dynamically change the expectation/measurement.

Thoughts?

from pennylane.

cgogolin avatar cgogolin commented on August 20, 2024

Concerning multiple measurements on different wires: Keep in mind that some backends/plugins only support one measurement. IBM for example only allows to measure all qubits at once and only exactly one time. For this backend in the ProjectQ plugin is thus only have one observable available wich I would call "AllPauliZ" and I somehow need to "return" (i.e., save to self._out) multiple values from/in execute(). How shall I do that? What are the expected/legal types for self._out after execute()?

from pennylane.

josh146 avatar josh146 commented on August 20, 2024

Good point. self._out should ideally be a list, in order to support multiple measurements

from pennylane.

cgogolin avatar cgogolin commented on August 20, 2024

+1 for only one measurement, but keep in mind that even a sigle measurement can return multiple values.

from pennylane.

co9olguy avatar co9olguy commented on August 20, 2024

Ok let's go with one Qnode <-> max one expectation value per subsystem as Maria suggested

from pennylane.

cgogolin avatar cgogolin commented on August 20, 2024

Commit 8a3237b added the return statement return qm.expectation.PauliZ(1) to circuit(), but, as far as I can tell, PauliZ(1) not does not actually return anything. Can you please update me about the outcome of the discussion on return statements in circuit()?

from pennylane.

cgogolin avatar cgogolin commented on August 20, 2024

@josh146 told me that the plan is to "enforce" the return via documentation but leave it purely as sytactic sugar, i.e., it doesn't actually do anything. I think I like this very much! It only gets a bit subtle when when multiple values have to be returned. How is the syntax supposed to look like in this case?

Maybe like this?

def circuit():
   ...
   return [qm.expectation.PauliZ(0), qm.expectation.PauliZ(1), qm.expectation.PauliZ(3)]

This relates to my question on the lifecycle of plugins. Are they expected to be able to cope with multiple calls to expectation()? Could it happen that if the user uses some fancy data type to wrap the multiple return values (instead of an [...]array) that the calls toexpectation()` do not happen in the right order?

from pennylane.

josh146 avatar josh146 commented on August 20, 2024

The way it currently works, where return is simply syntactic convention (but not explicitly used in the codebase), this would also work fine with minimal adjustments. Consider the case above:

def circuit():
   ...
   return [qm.expectation.PauliZ(0), qm.expectation.PauliZ(1), qm.expectation.PauliZ(3)]

On the function return, Python will evaluate the three expectation calls from left-to-right. The simplest solution is to modify device._observe to act in a similar manner to device._queue, and 'queue' the observable for each wire when called.

Since Python will always evaluate them from left-to-right, this will preserve the order provided by the user, and the plugin will return the resulting expectation values in the same order when looping through device._observe.

from pennylane.

smite avatar smite commented on August 20, 2024

I would suggest making the return statement functional. This can be done with the syntax

return [qm.expectation.PauliZ(0), qm.expectation.PauliZ(1), qm.expectation.PauliZ(3)]

qm.expectation.PauliZ(3) returns an Expectation instance, so the circuit-defining function returns a list of them. The caller can go through that list, and store in each Expectation instance its index in the list. This enables Device.execute_queued() to construct a corresponding Numpy array of the measured expectation values and return it.

Edit:
The difference to Josh's suggestion above is that only the expectation values that are explicitly returned affect the output of the circuit.

from pennylane.

josh146 avatar josh146 commented on August 20, 2024

@smite I am currently implementing exactly that!

from pennylane.

josh146 avatar josh146 commented on August 20, 2024

Preliminarily implemented in a42497b. Note that this makes the return statement required, simply by checking that it is provided. This is a temporary solution to #31.

from pennylane.

co9olguy avatar co9olguy commented on August 20, 2024

Closing this issue as the API is largely established at this point. If there are any bugs, report them in separate issues

from pennylane.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.