Comments (29)
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.
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.
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:
-
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?
-
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):
-
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.
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.
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.
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.
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.
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.
@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.
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.
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.
@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.
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:
- a batch of outputs, all measuring the same expectation values, but for different inputs
- 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)
- 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.
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.
Some thoughts from my side:
- As @josh146 said already said above: Can we not leave
circuit()
up to the user? The optimizer should only care aboutcost()
. A user should in principle even be able to definecost()
without referring to a circuit, no? - I think it would be good to avoid the
*
syntax. - 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.
- 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 itmeasurement_statistics()
?
from pennylane.
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.
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.
@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:
- While it is more verbose, it is simple, clear, and very low level - design decisions are left up to the user.
- 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.
- 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.
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.
Good point. self._out
should ideally be a list, in order to support multiple measurements
from pennylane.
+1 for only one measurement, but keep in mind that even a sigle measurement can return multiple values.
from pennylane.
Ok let's go with one Qnode <-> max one expectation value per subsystem as Maria suggested
from pennylane.
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.
@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 to
expectation()` do not happen in the right order?
from pennylane.
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.
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.
@smite I am currently implementing exactly that!
from pennylane.
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.
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)
- Propagate the gradient to part of an array only HOT 2
- Prevent writing to a specific file when creating the Hamiltonian HOT 12
- [BUG] Some templates not properly differentiated with parameter shift on the legacy device HOT 1
- Update documentation to remove pennylane.optimize HOT 3
- [BUG] `qml.eigvals` sometimes fails when using `LinearCombination` HOT 1
- [BUG] `qml.Adjoint` and `qml.Pow` do not commute when exponent is fractional HOT 1
- [BUG] JIT + Global measurements without device wires does not work
- [BUG] `StatePrep` and its decomposition differ in derivative
- Support broadcasting/batching of `GlobalPhase`
- [BUG] `qml.pauli_decompose` fails when being jit-ted HOT 1
- [BUG] Allow natural extension of `QubitUnitary` to 1x1 matrices
- [BUG] Documentation inconsistently states that GlobalPhase acts on all wires
- Align definition of GlobalPhase convention HOT 4
- [BUG] Tensor product alters operands
- [BUG] Second-oder derivatives fail with shots enabled for product of input variables HOT 1
- [BUG] `split_non_commuting` is not differentiable with trainable observables
- [BUG] hadamard gradient does not work with wires-broadcasted measurements
- [BUG] Precision error in two-qubit decomposition of qml.QubitUnitary HOT 10
- Broadcast Expand Prevents Torchlayer from detecting Inputs Argument HOT 3
- Add an `is_pure` method in `qml.math.quantum` for quick purity checks on density matrices HOT 3
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from pennylane.