-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Docs, examples, correct expectation shape (#31)
Co-authored-by: Mario Dagrada <[email protected]> Co-authored-by: Niklas Heim <[email protected]> Co-authored-by: Dominik Seitz <[email protected]> Co-authored-by: Roland Guichard <[email protected]> Co-authored-by: Joao Moutinho <[email protected]> Co-authored-by: Vytautas Abramavicius <[email protected]>
- Loading branch information
1 parent
d799bb0
commit 3a30973
Showing
74 changed files
with
5,476 additions
and
63 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
In `qadence`, the `QuantumModel` is the central class point for executing | ||
`QuantumCircuit`s. The idea of a `QuantumModel` is to decouple the backend | ||
execution from the management of circuit parameters and desired quantum | ||
computation output. | ||
|
||
In the following, we create a custom `QuantumModel` instance which introduces | ||
some additional optimizable parameters: | ||
* an adjustable scaling factor in front of the observable to measured | ||
* adjustable scale and shift factors to be applied to the model output before returning the result | ||
|
||
This can be easily done using PyTorch flexible model definition, and it will | ||
automatically work with the rest of `qadence` infrastructure. | ||
|
||
|
||
```python exec="on" source="material-block" session="custom-model" | ||
import torch | ||
from qadence import QuantumModel, QuantumCircuit | ||
|
||
|
||
class CustomQuantumModel(QuantumModel): | ||
|
||
def __init__(self, circuit: QuantumCircuit, observable, backend="pyqtorch", diff_mode="ad"): | ||
super().__init__(circuit, observable=observable, backend=backend, diff_mode=diff_mode) | ||
|
||
self.n_qubits = circuit.n_qubits | ||
|
||
# define some additional parameters which will scale and shift (variationally) the | ||
# output of the QuantumModel | ||
# you can use all torch machinery for building those | ||
self.scale_out = torch.nn.Parameter(torch.ones(1)) | ||
self.shift_out = torch.nn.Parameter(torch.ones(1)) | ||
|
||
# override the forward pass of the model | ||
# the forward pass is the output of your QuantumModel and in this case | ||
# it's the (scaled) expectation value of the total magnetization with | ||
# a variable coefficient in front | ||
def forward(self, values: dict[str, torch.Tensor]) -> torch.Tensor: | ||
|
||
# scale the observable | ||
res = self.expectation(values) | ||
|
||
# scale and shift the result before returning | ||
return self.shift_out + res * self.scale_out | ||
``` | ||
|
||
The custom model can be used like any other `QuantumModel`: | ||
```python exec="on" source="material-block" result="json" session="custom-model" | ||
from qadence import Parameter, RX, CNOT, QuantumCircuit | ||
from qadence import chain, kron, total_magnetization | ||
from sympy import acos | ||
|
||
def quantum_circuit(n_qubits): | ||
|
||
x = Parameter("x", trainable=False) | ||
fm = kron(RX(i, acos(x) * (i+1)) for i in range(n_qubits)) | ||
|
||
ansatz = kron(RX(i, f"theta{i}") for i in range(n_qubits)) | ||
ansatz = chain(ansatz, CNOT(0, n_qubits-1)) | ||
|
||
block = chain(fm, ansatz) | ||
block.tag = "circuit" | ||
return QuantumCircuit(n_qubits, block) | ||
|
||
n_qubits = 4 | ||
batch_size = 10 | ||
circuit = quantum_circuit(n_qubits) | ||
observable = total_magnetization(n_qubits) | ||
|
||
model = CustomQuantumModel(circuit, observable, backend="pyqtorch") | ||
|
||
values = {"x": torch.rand(batch_size)} | ||
res = model(values) | ||
print("Model output: ", res) | ||
assert len(res) == batch_size | ||
``` | ||
|
||
|
||
## Quantum model with wavefunction overlaps | ||
|
||
`QuantumModel`'s can also use different quantum operations in their forward | ||
pass, such as wavefunction overlaps described [here](../tutorials/overlap.md). Beware that the resulting overlap tensor | ||
has to be differentiable to apply gradient-based optimization. This is only applicable to the `"EXACT"` overlap method. | ||
|
||
Here we show how to use overlap calculation when fitting a parameterized quantum circuit to act as a standard Hadamard gate. | ||
|
||
```python exec="on" source="material-block" result="json" session="custom-model" | ||
from qadence import RY, RX, H, Overlap | ||
|
||
# create a quantum model which acts as an Hadamard gate after training | ||
class LearnHadamard(QuantumModel): | ||
def __init__( | ||
self, | ||
train_circuit: QuantumCircuit, | ||
target_circuit: QuantumCircuit, | ||
backend="pyqtorch", | ||
): | ||
super().__init__(circuit=train_circuit, backend=backend) | ||
self.overlap_fn = Overlap(train_circuit, target_circuit, backend=backend, method="exact", diff_mode='ad') | ||
|
||
def forward(self): | ||
return self.overlap_fn() | ||
|
||
# compute the wavefunction of the associated train circuit | ||
def wavefunction(self): | ||
return model.overlap_fn.run({}) | ||
|
||
|
||
train_circuit = QuantumCircuit(1, chain(RX(0, "phi"), RY(0, "theta"))) | ||
target_circuit = QuantumCircuit(1, H(0)) | ||
|
||
model = LearnHadamard(train_circuit, target_circuit) | ||
|
||
# get the overlap between model and target circuit wavefunctions | ||
print(model()) | ||
``` | ||
|
||
This model can then be trained with the standard Qadence helper functions. | ||
|
||
```python exec="on" source="material-block" result="json" session="custom-model" | ||
from qadence import run | ||
from qadence.ml_tools import train_with_grad, TrainConfig | ||
|
||
criterion = torch.nn.MSELoss() | ||
optimizer = torch.optim.Adam(model.parameters(), lr=1e-1) | ||
|
||
def loss_fn(model: LearnHadamard, _unused) -> tuple[torch.Tensor, dict]: | ||
loss = criterion(torch.tensor([[1.0]]), model()) | ||
return loss, {} | ||
|
||
config = TrainConfig(max_iter=2500) | ||
model, optimizer = train_with_grad( | ||
model, None, optimizer, config, loss_fn=loss_fn | ||
) | ||
|
||
wf_target = run(target_circuit) | ||
assert torch.allclose(wf_target, model.wavefunction(), atol=1e-2) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
# Differentiability | ||
|
||
Many application in quantum computing and quantum machine learning more specifically requires the differentiation | ||
of a quantum circuit with respect to its parameters. | ||
|
||
In Qadence, we perform quantum computations via the `QuantumModel` interface. The derivative of the outputs of quantum | ||
models with respect to feature and variational parameters in the quantum circuit can be implemented in Qadence | ||
with two different modes: | ||
|
||
- Automatic differentiation (AD) mode [^1]. This mode allows to differentiation both | ||
`run()` and `expectation()` methods of the `QuantumModel` and it is the fastest | ||
available differentiation method. Under the hood, it is based on the PyTorch autograd engine wrapped by | ||
the `DifferentiableBackend` class. This mode is not working on quantum devices. | ||
- Generalized parameter shift rule (GPSR) mode. This is general implementation of the well known parameter | ||
shift rule algorithm [^2] which works for arbitrary quantum operations [^3]. This mode is only applicable to | ||
the `expectation()` method of `QuantumModel` but it is compatible with execution or quantum devices. | ||
|
||
## Automatic differentiation | ||
|
||
Automatic differentiation [^1] is a procedure to derive a complex function defined as a sequence of elementary | ||
mathematical operations in | ||
the form of a computer program. Automatic differentiation is a cornerstone of modern machine learning and a crucial | ||
ingredient of its recent successes. In its so-called *reverse mode*, it follows this sequence of operations in reverse order by systematically applying the chain rule to recover the exact value of derivative. Reverse mode automatic differentiation | ||
is implemented in Qadence leveraging the PyTorch `autograd` engine. | ||
|
||
!!! warning "Only available with PyQTorch backend" | ||
Currently, automatic differentiation mode is only | ||
available when the `pyqtorch` backend is selected. | ||
|
||
## Generalized parameter shift rule | ||
|
||
The generalized parameter shift rule implementation in Qadence was introduced in [^3]. Here the standard parameter shift rules, | ||
which only works for quantum operations whose generator has a single gap in its eigenvalue spectrum, was generalized | ||
to work with arbitrary generators of quantum operations. | ||
|
||
For this, we define the differentiable function as quantum expectation value | ||
|
||
$$ | ||
f(x) = \left\langle 0\right|\hat{U}^{\dagger}(x)\hat{C}\hat{U}(x)\left|0\right\rangle | ||
$$ | ||
|
||
where $\hat{U}(x)={\rm exp}{\left( -i\frac{x}{2}\hat{G}\right)}$ is the quantum evolution operator with generator $\hat{G}$ representing the structure of the underlying quantum circuit and $\hat{C}$ is the cost operator. Then using the eigenvalue spectrum $\left\{ \lambda_n\right\}$ of the generator $\hat{G}$ we calculate the full set of corresponding unique non-zero spectral gaps $\left\{ \Delta_s\right\}$ (differences between eigenvalues). It can be shown that the final expression of derivative of $f(x)$ is then given by the following expression: | ||
|
||
$\begin{equation} | ||
\frac{{\rm d}f\left(x\right)}{{\rm d}x}=\overset{S}{\underset{s=1}{\sum}}\Delta_{s}R_{s}, | ||
\end{equation}$ | ||
|
||
where $S$ is the number of unique non-zero spectral gaps and $R_s$ are real quantities that are solutions of a system of linear equations | ||
|
||
$\begin{equation} | ||
\begin{cases} | ||
F_{1} & =4\overset{S}{\underset{s=1}{\sum}}{\rm sin}\left(\frac{\delta_{1}\Delta_{s}}{2}\right)R_{s},\\ | ||
F_{2} & =4\overset{S}{\underset{s=1}{\sum}}{\rm sin}\left(\frac{\delta_{2}\Delta_{s}}{2}\right)R_{s},\\ | ||
& ...\\ | ||
F_{S} & =4\overset{S}{\underset{s=1}{\sum}}{\rm sin}\left(\frac{\delta_{M}\Delta_{s}}{2}\right)R_{s}. | ||
\end{cases} | ||
\end{equation}$ | ||
|
||
Here $F_s=f(x+\delta_s)-f(x-\delta_s)$ denotes the difference between values of functions evaluated at shifted arguments $x\pm\delta_s$. | ||
|
||
## Usage | ||
|
||
### Basics | ||
|
||
In Qadence, the GPSR differentiation engine can be selected by passing `diff_mode="gpsr"` or, equivalently, `diff_mode=DiffMode.GPSR` to a `QuantumModel` instance. The code in the box below shows how to create `QuantumModel` instances with both AD and GPSR engines. | ||
|
||
```python exec="on" source="material-block" session="differentiability" | ||
from qadence import (FeatureParameter, HamEvo, X, I, Z, | ||
total_magnetization, QuantumCircuit, | ||
QuantumModel, BackendName, DiffMode) | ||
import torch | ||
|
||
n_qubits = 2 | ||
|
||
# define differentiation parameter | ||
x = FeatureParameter("x") | ||
|
||
# define generator and HamEvo block | ||
generator = X(0) + X(1) + 0.2 * (Z(0) + I(1)) * (I(0) + Z(1)) | ||
block = HamEvo(generator, x) | ||
|
||
# create quantum circuit | ||
circuit = QuantumCircuit(n_qubits, block) | ||
|
||
# create total magnetization cost operator | ||
obs = total_magnetization(n_qubits) | ||
|
||
# create models with AD and GPSR differentiation engines | ||
model_ad = QuantumModel(circuit, obs, | ||
backend=BackendName.PYQTORCH, | ||
diff_mode=DiffMode.AD) | ||
model_gpsr = QuantumModel(circuit, obs, | ||
backend=BackendName.PYQTORCH, | ||
diff_mode=DiffMode.GPSR) | ||
|
||
# generate value for circuit's parameter | ||
xs = torch.linspace(0, 2*torch.pi, 100, requires_grad=True) | ||
values = {"x": xs} | ||
|
||
# calculate function f(x) | ||
exp_val_ad = model_ad.expectation(values) | ||
exp_val_gpsr = model_gpsr.expectation(values) | ||
|
||
# calculate derivative df/dx using the PyTorch | ||
# autograd engine | ||
dexpval_x_ad = torch.autograd.grad( | ||
exp_val_ad, values["x"], torch.ones_like(exp_val_ad), create_graph=True | ||
)[0] | ||
dexpval_x_gpsr = torch.autograd.grad( | ||
exp_val_gpsr, values["x"], torch.ones_like(exp_val_gpsr), create_graph=True | ||
)[0] | ||
``` | ||
|
||
We can plot the resulting derivatives and see that in both cases they coincide. | ||
|
||
```python exec="on" source="material-block" session="differentiability" | ||
import matplotlib.pyplot as plt | ||
|
||
# plot f(x) and df/dx derivatives calculated using AD and GPSR | ||
# differentiation engines | ||
fig, ax = plt.subplots() | ||
ax.scatter(xs.detach().numpy(), | ||
exp_val_ad.detach().numpy(), | ||
label="f(x)") | ||
ax.scatter(xs.detach().numpy(), | ||
dexpval_x_ad.detach().numpy(), | ||
label="df/dx AD") | ||
ax.scatter(xs.detach().numpy(), | ||
dexpval_x_gpsr.detach().numpy(), | ||
s=5, | ||
label="df/dx GPSR") | ||
plt.legend() | ||
from docs import docsutils # markdown-exec: hide | ||
print(docsutils.fig_to_html(plt.gcf())) # markdown-exec: hide | ||
``` | ||
|
||
### Low-level control on the shift values | ||
|
||
In order to get a finer control over the GPSR differentiation engine we can use the low-level Qadence API to define a `DifferentiableBackend`. | ||
|
||
```python exec="on" source="material-block" session="differentiability" | ||
from qadence import DifferentiableBackend | ||
from qadence.backends.pyqtorch import Backend as PyQBackend | ||
|
||
# define differentiable quantum backend | ||
quantum_backend = PyQBackend() | ||
conv = quantum_backend.convert(circuit, obs) | ||
pyq_circ, pyq_obs, embedding_fn, params = conv | ||
diff_backend = DifferentiableBackend(quantum_backend, diff_mode=DiffMode.GPSR, shift_prefac=0.2) | ||
|
||
# calculate function f(x) | ||
expval = diff_backend.expectation(pyq_circ, pyq_obs, embedding_fn(params, values)) | ||
``` | ||
|
||
Here we passed an additional argument `shift_prefac` to the `DifferentiableBackend` instance that governs the magnitude of shifts $\delta\equiv\alpha\delta^\prime$ shown in equation (2) above. In this relation $\delta^\prime$ is set internally and $\alpha$ is the value passed by `shift_prefac` and the resulting shift value $\delta$ is then used in all the following GPSR calculations. | ||
|
||
Tuning parameter $\alpha$ is useful to improve results | ||
when the generator $\hat{G}$ or the quantum operation is a dense matrix, for example a complex `HamEvo` operation; if many entries of this matrix are sufficiently larger than 0 the operation is equivalent to a strongly interacting system. In such case parameter $\alpha$ should be gradually lowered in order to achieve exact derivative values. | ||
|
||
|
||
## References | ||
|
||
[^1]: [A. G. Baydin et al., Automatic Differentiation in Machine Learning: a Survey](https://www.jmlr.org/papers/volume18/17-468/17-468.pdf) | ||
|
||
[^2]: [Schuld et al., Evaluating analytic gradients on quantum hardware (2018).](https://arxiv.org/abs/1811.11184) | ||
|
||
[^3]: [Kyriienko et al., General quantum circuit differentiation rules](https://arxiv.org/abs/2108.01218) |
Oops, something went wrong.