qml.gradients.classical_fisher

classical_fisher(qnode, argnums=0)[source]

Returns a function that computes the classical fisher information matrix (CFIM) of a given QNode or quantum tape.

Given a parametrized (classical) probability distribution \(p(\bm{\theta})\), the classical fisher information matrix quantifies how changes to the parameters \(\bm{\theta}\) are reflected in the probability distribution. For a parametrized quantum state, we apply the concept of classical fisher information to the computational basis measurement. More explicitly, this function implements eq. (15) in arxiv:2103.15191:

\[\text{CFIM}_{i, j} = \sum_{\ell=0}^{2^N-1} \frac{1}{p_\ell(\bm{\theta})} \frac{\partial p_\ell(\bm{\theta})}{ \partial \theta_i} \frac{\partial p_\ell(\bm{\theta})}{\partial \theta_j}\]

for \(N\) qubits.

Parameters
  • tape (QNode or qml.QuantumTape) – A QNode or quantum tape that may have arbitrary return types.

  • argnums (Optional[int or List[int]]) – Arguments to be differentiated in case interface jax is used.

Returns

The function that computes the classical fisher information matrix. This function accepts the same signature as the QNode. If the signature contains one differentiable variable params, the function returns a matrix of size (len(params), len(params)). For multiple differentiable arguments x, y, z, it returns a list of sizes [(len(x), len(x)), (len(y), len(y)), (len(z), len(z))].

Return type

func

See also

metric_tensor(), quantum_fisher()

Example

First, let us define a parametrized quantum state and return its (classical) probability distribution for all computational basis elements:

import pennylane.numpy as np

dev = qml.device("default.qubit")

@qml.qnode(dev)
def circ(params):
    qml.RX(params[0], wires=0)
    qml.CNOT([0, 1])
    qml.CRY(params[1], wires=[1, 0])
    qml.Hadamard(1)
    return qml.probs(wires=[0, 1])

Executing this circuit yields the 2**2=4 elements of \(p_\ell(\bm{\theta})\)

>>> np.random.seed(25)
>>> params = np.random.random(2)
>>> circ(params)
tensor([0.41850088, 0.41850088, 0.08149912, 0.08149912], requires_grad=True)

We can obtain its (2, 2) classical fisher information matrix (CFIM) by simply calling the function returned by classical_fisher():

>>> cfim_func = qml.gradients.classical_fisher(circ)
>>> cfim_func(params)
tensor([[ 0.90156094, -0.12555804],
        [-0.12555804,  0.01748614]], requires_grad=True)

This function has the same signature as the QNode. Here is a small example with multiple arguments:

@qml.qnode(dev)
def circ(x, y):
    qml.RX(x, wires=0)
    qml.RY(y, wires=0)
    return qml.probs(wires=range(1))
>>> x, y = np.array([0.5, 0.6], requires_grad=True)
>>> circ(x, y)
tensor([0.86215007, 0.13784993], requires_grad=True)
>>> qml.gradients.classical_fisher(circ)(x, y)
[tensor([[0.32934729]], requires_grad=True),
tensor([[0.51650396]], requires_grad=True)]

Note how in the case of multiple variables we get a list of matrices with sizes [(n_params0, n_params0), (n_params1, n_params1)], which in this case is simply two (1, 1) matrices.

A typical setting where the classical fisher information matrix is used is in variational quantum algorithms. Closely related to the quantum natural gradient, which employs the quantum fisher information matrix, we can compute a rescaled gradient using the CFIM. In this scenario, typically a Hamiltonian objective function \(\langle H \rangle\) is minimized:

H = qml.Hamiltonian(coeffs=[0.5, 0.5], observables=[qml.Z(0), qml.Z(1)])

@qml.qnode(dev)
def circ(params):
    qml.RX(params[0], wires=0)
    qml.RY(params[1], wires=0)
    qml.RX(params[2], wires=1)
    qml.RY(params[3], wires=1)
    qml.CNOT(wires=(0,1))
    return qml.expval(H)

params = np.random.random(4)

We can compute both the gradient of \(\langle H \rangle\) and the CFIM with the same QNode circ in this example since classical_fisher() ignores the return types and assumes qml.probs() for all wires.

>>> grad = qml.grad(circ)(params)
>>> cfim = qml.gradients.classical_fisher(circ)(params)
>>> print(grad.shape, cfim.shape)
(4,) (4, 4)

Combined together, we can get a rescaled gradient to be employed for optimization schemes like natural gradient descent.

>>> rescaled_grad = cfim @ grad
>>> print(rescaled_grad)
[-0.66772533 -0.16618756 -0.05865127 -0.06696078]

The classical_fisher matrix itself is again differentiable:

@qml.qnode(dev)
def circ(params):
    qml.RX(qml.math.cos(params[0]), wires=0)
    qml.RX(qml.math.cos(params[0]), wires=1)
    qml.RX(qml.math.cos(params[1]), wires=0)
    qml.RX(qml.math.cos(params[1]), wires=1)
    return qml.probs(wires=range(2))

params = np.random.random(2)
>>> qml.gradients.classical_fisher(circ)(params)
tensor([[0.86929514, 0.76134441],
        [0.76134441, 0.6667992 ]], requires_grad=True)
>>> qml.jacobian(qml.gradients.classical_fisher(circ))(params)
array([[[ 1.98284265e+00, -1.60461922e-16],
        [ 8.68304725e-01,  1.07654307e+00]],
       [[ 8.68304725e-01,  1.07654307e+00],
        [ 7.30752264e-17,  1.88571178e+00]]])