Add gradient methods to FinDiffGradients (#9104)

* implement gradient methods

* fix docs

* arg order/refactor

* fix docs

* revert central difference

* Update qiskit/algorithms/gradients/finite_diff_estimator_gradient.py

* fix docs

* Update qiskit/algorithms/gradients/finite_diff_sampler_gradient.py

Co-authored-by: Julien Gacon <gaconju@gmail.com>

Co-authored-by: Julien Gacon <gaconju@gmail.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
Ikko Hamamura 2022-11-18 00:50:26 +09:00 committed by GitHub
parent 30f45fe79a
commit 2bd4afacdc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 178 additions and 114 deletions

View File

@ -14,6 +14,7 @@
from __future__ import annotations
import sys
from typing import Sequence
import numpy as np
@ -28,14 +29,27 @@ from qiskit.quantum_info.operators.base_operator import BaseOperator
from .base_estimator_gradient import BaseEstimatorGradient
from .estimator_gradient_result import EstimatorGradientResult
if sys.version_info >= (3, 8):
# pylint: disable=no-name-in-module, ungrouped-imports
from typing import Literal
else:
from typing_extensions import Literal
class FiniteDiffEstimatorGradient(BaseEstimatorGradient):
"""
Compute the gradients of the expectation values by finite difference method.
"""
def __init__(self, estimator: BaseEstimator, epsilon: float, options: Options | None = None):
"""
def __init__(
self,
estimator: BaseEstimator,
epsilon: float,
options: Options | None = None,
*,
method: Literal["central", "forward", "backward"] = "central",
):
r"""
Args:
estimator: The estimator used to compute the gradients.
epsilon: The offset size for the finite difference gradients.
@ -43,14 +57,27 @@ class FiniteDiffEstimatorGradient(BaseEstimatorGradient):
The order of priority is: options in ``run`` method > gradient's
default options > primitive's default setting.
Higher priority setting overrides lower priority setting
method: The computation method of the gradients.
- ``central`` computes :math:`\frac{f(x+e)-f(x-e)}{2e}`,
- ``forward`` computes :math:`\frac{f(x+e) - f(x)}{e}`,
- ``backward`` computes :math:`\frac{f(x)-f(x-e)}{e}`
where :math:`e` is epsilon.
Raises:
ValueError: If ``epsilon`` is not positive.
TypeError: If ``method`` is invalid.
"""
if epsilon <= 0:
raise ValueError(f"epsilon ({epsilon}) should be positive.")
self._epsilon = epsilon
self._base_parameter_values_dict = {}
if method not in ("central", "forward", "backward"):
raise TypeError(
f"The argument method should be central, forward, or backward: {method} is given."
)
self._method = method
super().__init__(estimator, options)
def _run(
@ -74,12 +101,25 @@ class FiniteDiffEstimatorGradient(BaseEstimatorGradient):
metadata_.append({"parameters": [circuit.parameters[idx] for idx in indices]})
offset = np.identity(circuit.num_parameters)[indices, :]
if self._method == "central":
plus = parameter_values_ + self._epsilon * offset
minus = parameter_values_ - self._epsilon * offset
n = 2 * len(indices)
job = self._estimator.run(
[circuit] * n, [observable] * n, plus.tolist() + minus.tolist(), **options
)
elif self._method == "forward":
plus = parameter_values_ + self._epsilon * offset
n = len(indices) + 1
job = self._estimator.run(
[circuit] * n, [observable] * n, [parameter_values_] + plus.tolist(), **options
)
elif self._method == "backward":
minus = parameter_values_ - self._epsilon * offset
n = len(indices) + 1
job = self._estimator.run(
[circuit] * n, [observable] * n, [parameter_values_] + minus.tolist(), **options
)
jobs.append(job)
# combine the results
@ -90,8 +130,13 @@ class FiniteDiffEstimatorGradient(BaseEstimatorGradient):
gradients = []
for result in results:
if self._method == "central":
n = len(result.values) // 2 # is always a multiple of 2
gradient_ = (result.values[:n] - result.values[n:]) / (2 * self._epsilon)
elif self._method == "forward":
gradient_ = (result.values[1:] - result.values[0]) / self._epsilon
elif self._method == "backward":
gradient_ = (result.values[0] - result.values[1:]) / self._epsilon
gradients.append(gradient_)
opt = self._get_local_options(options)
return EstimatorGradientResult(gradients=gradients, metadata=metadata_, options=opt)

View File

@ -14,6 +14,7 @@
from __future__ import annotations
import sys
from typing import Sequence
import numpy as np
@ -26,6 +27,12 @@ from qiskit.providers import Options
from .base_sampler_gradient import BaseSamplerGradient
from .sampler_gradient_result import SamplerGradientResult
if sys.version_info >= (3, 8):
# pylint: disable=no-name-in-module, ungrouped-imports
from typing import Literal
else:
from typing_extensions import Literal
class FiniteDiffSamplerGradient(BaseSamplerGradient):
"""Compute the gradients of the sampling probability by finite difference method."""
@ -35,8 +42,10 @@ class FiniteDiffSamplerGradient(BaseSamplerGradient):
sampler: BaseSampler,
epsilon: float,
options: Options | None = None,
*,
method: Literal["central", "forward", "backward"] = "central",
):
"""
r"""
Args:
sampler: The sampler used to compute the gradients.
epsilon: The offset size for the finite difference gradients.
@ -44,13 +53,26 @@ class FiniteDiffSamplerGradient(BaseSamplerGradient):
The order of priority is: options in ``run`` method > gradient's
default options > primitive's default setting.
Higher priority setting overrides lower priority setting
method: The computation method of the gradients.
- ``central`` computes :math:`\frac{f(x+e)-f(x-e)}{2e}`,
- ``forward`` computes :math:`\frac{f(x+e) - f(x)}{e}`,
- ``backward`` computes :math:`\frac{f(x)-f(x-e)}{e}`
where :math:`e` is epsilon.
Raises:
ValueError: If ``epsilon`` is not positive.
TypeError: If ``method`` is invalid.
"""
if epsilon <= 0:
raise ValueError(f"epsilon ({epsilon}) should be positive.")
self._epsilon = epsilon
if method not in ("central", "forward", "backward"):
raise TypeError(
f"The argument method should be central, forward, or backward: {method} is given."
)
self._method = method
super().__init__(sampler, options)
def _run(
@ -70,10 +92,23 @@ class FiniteDiffSamplerGradient(BaseSamplerGradient):
indices = [circuit.parameters.data.index(p) for p in parameters_]
metadata_.append({"parameters": [circuit.parameters[idx] for idx in indices]})
offset = np.identity(circuit.num_parameters)[indices, :]
if self._method == "central":
plus = parameter_values_ + self._epsilon * offset
minus = parameter_values_ - self._epsilon * offset
n = 2 * len(indices)
job = self._sampler.run([circuit] * n, plus.tolist() + minus.tolist(), **options)
elif self._method == "forward":
plus = parameter_values_ + self._epsilon * offset
n = len(indices) + 1
job = self._sampler.run(
[circuit] * n, [parameter_values_] + plus.tolist(), **options
)
elif self._method == "backward":
minus = parameter_values_ - self._epsilon * offset
n = len(indices) + 1
job = self._sampler.run(
[circuit] * n, [parameter_values_] + minus.tolist(), **options
)
jobs.append(job)
# combine the results
@ -84,6 +119,7 @@ class FiniteDiffSamplerGradient(BaseSamplerGradient):
gradients = []
for i, result in enumerate(results):
if self._method == "central":
n = len(result.quasi_dists) // 2
gradient_ = []
for dist_plus, dist_minus in zip(result.quasi_dists[:n], result.quasi_dists[n:]):
@ -92,6 +128,24 @@ class FiniteDiffSamplerGradient(BaseSamplerGradient):
grad_dist[list(dist_minus.keys())] -= list(dist_minus.values())
grad_dist /= 2 * self._epsilon
gradient_.append(dict(enumerate(grad_dist)))
elif self._method == "forward":
gradient_ = []
dist_zero = result.quasi_dists[0]
for dist_plus in result.quasi_dists[1:]:
grad_dist = np.zeros(2 ** circuits[i].num_qubits)
grad_dist[list(dist_plus.keys())] += list(dist_plus.values())
grad_dist[list(dist_zero.keys())] -= list(dist_zero.values())
grad_dist /= self._epsilon
gradient_.append(dict(enumerate(grad_dist)))
elif self._method == "backward":
gradient_ = []
dist_zero = result.quasi_dists[0]
for dist_minus in result.quasi_dists[1:]:
grad_dist = np.zeros(2 ** circuits[i].num_qubits)
grad_dist[list(dist_zero.keys())] += list(dist_zero.values())
grad_dist[list(dist_minus.keys())] -= list(dist_minus.values())
grad_dist /= self._epsilon
gradient_.append(dict(enumerate(grad_dist)))
gradients.append(gradient_)
opt = self._get_local_options(options)

View File

@ -0,0 +1,10 @@
---
features:
- |
:class:`.FiniteDiffEstimatorGradient` and :class:`FiniteDiffSamplerGradient`
have new argument method.
There are three methods, "central", "forward", and "backward".
This option changes the gradient calculation methods.
"central" calculates :math:`\frac{f(x+e)-f(x-e)}{2e}`, "forward"
:math:`\frac{f(x+e) - f(x)}{e}`, and "backward" :math:`\frac{f(x)-f(x-e)}{e}` where
:math:`e` is the offset epsilon.

View File

@ -35,14 +35,20 @@ from qiskit.quantum_info import Operator, SparsePauliOp, Pauli
from qiskit.quantum_info.random import random_pauli_list
from qiskit.test import QiskitTestCase
gradient_factories = [
lambda estimator: FiniteDiffEstimatorGradient(estimator, epsilon=1e-6, method="central"),
lambda estimator: FiniteDiffEstimatorGradient(estimator, epsilon=1e-6, method="forward"),
lambda estimator: FiniteDiffEstimatorGradient(estimator, epsilon=1e-6, method="backward"),
ParamShiftEstimatorGradient,
LinCombEstimatorGradient,
]
@ddt
class TestEstimatorGradient(QiskitTestCase):
"""Test Estimator Gradient"""
@combine(
grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient]
)
@combine(grad=gradient_factories)
def test_gradient_operators(self, grad):
"""Test the estimator gradient for different operators"""
estimator = Estimator()
@ -51,9 +57,6 @@ class TestEstimatorGradient(QiskitTestCase):
qc.h(0)
qc.p(a, 0)
qc.h(0)
if grad is FiniteDiffEstimatorGradient:
gradient = grad(estimator, epsilon=1e-6)
else:
gradient = grad(estimator)
op = SparsePauliOp.from_list([("Z", 1)])
correct_result = -1 / np.sqrt(2)
@ -67,9 +70,7 @@ class TestEstimatorGradient(QiskitTestCase):
value = gradient.run([qc], [op], [param]).result().gradients[0]
self.assertAlmostEqual(value[0], correct_result, 3)
@combine(
grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient]
)
@combine(grad=gradient_factories)
def test_gradient_p(self, grad):
"""Test the estimator gradient for p"""
estimator = Estimator()
@ -78,9 +79,6 @@ class TestEstimatorGradient(QiskitTestCase):
qc.h(0)
qc.p(a, 0)
qc.h(0)
if grad is FiniteDiffEstimatorGradient:
gradient = grad(estimator, epsilon=1e-6)
else:
gradient = grad(estimator)
op = SparsePauliOp.from_list([("Z", 1)])
param_list = [[np.pi / 4], [0], [np.pi / 2]]
@ -90,9 +88,7 @@ class TestEstimatorGradient(QiskitTestCase):
for j, value in enumerate(gradients):
self.assertAlmostEqual(value, correct_results[i][j], 3)
@combine(
grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient]
)
@combine(grad=gradient_factories)
def test_gradient_u(self, grad):
"""Test the estimator gradient for u"""
estimator = Estimator()
@ -103,9 +99,6 @@ class TestEstimatorGradient(QiskitTestCase):
qc.h(0)
qc.u(a, b, c, 0)
qc.h(0)
if grad is FiniteDiffEstimatorGradient:
gradient = grad(estimator, epsilon=1e-6)
else:
gradient = grad(estimator)
op = SparsePauliOp.from_list([("Z", 1)])
@ -116,17 +109,12 @@ class TestEstimatorGradient(QiskitTestCase):
for j, value in enumerate(gradients):
self.assertAlmostEqual(value, correct_results[i][j], 3)
@combine(
grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient]
)
@combine(grad=gradient_factories)
def test_gradient_efficient_su2(self, grad):
"""Test the estimator gradient for EfficientSU2"""
estimator = Estimator()
qc = EfficientSU2(2, reps=1)
op = SparsePauliOp.from_list([("ZI", 1)])
if grad is FiniteDiffEstimatorGradient:
gradient = grad(estimator, epsilon=1e-6)
else:
gradient = grad(estimator)
param_list = [
[np.pi / 4 for param in qc.parameters],
@ -149,9 +137,7 @@ class TestEstimatorGradient(QiskitTestCase):
gradients = gradient.run([qc], [op], [param]).result().gradients[0]
np.testing.assert_allclose(gradients, correct_results[i], atol=1e-3)
@combine(
grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient],
)
@combine(grad=gradient_factories)
def test_gradient_2qubit_gate(self, grad):
"""Test the estimator gradient for 2 qubit gates"""
estimator = Estimator()
@ -165,9 +151,6 @@ class TestEstimatorGradient(QiskitTestCase):
for i, param in enumerate(param_list):
a = Parameter("a")
qc = QuantumCircuit(2)
if grad is FiniteDiffEstimatorGradient:
gradient = grad(estimator, epsilon=1e-6)
else:
gradient = grad(estimator)
if gate is RZZGate:
@ -179,9 +162,7 @@ class TestEstimatorGradient(QiskitTestCase):
gradients = gradient.run([qc], [op], [param]).result().gradients[0]
np.testing.assert_allclose(gradients, correct_results[i], atol=1e-3)
@combine(
grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient]
)
@combine(grad=gradient_factories)
def test_gradient_parameter_coefficient(self, grad):
"""Test the estimator gradient for parameter variables with coefficients"""
estimator = Estimator()
@ -191,9 +172,6 @@ class TestEstimatorGradient(QiskitTestCase):
qc.u(qc.parameters[0], qc.parameters[1], qc.parameters[3], 1)
qc.p(2 * qc.parameters[0] + 1, 0)
qc.rxx(qc.parameters[0] + 2, 0, 1)
if grad is FiniteDiffEstimatorGradient:
gradient = grad(estimator, epsilon=1e-6)
else:
gradient = grad(estimator)
param_list = [[np.pi / 4 for _ in qc.parameters], [np.pi / 2 for _ in qc.parameters]]
correct_results = [
@ -205,9 +183,7 @@ class TestEstimatorGradient(QiskitTestCase):
gradients = gradient.run([qc], [op], [param]).result().gradients[0]
np.testing.assert_allclose(gradients, correct_results[i], atol=1e-3)
@combine(
grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient]
)
@combine(grad=gradient_factories)
def test_gradient_parameters(self, grad):
"""Test the estimator gradient for parameters"""
estimator = Estimator()
@ -216,9 +192,6 @@ class TestEstimatorGradient(QiskitTestCase):
qc = QuantumCircuit(1)
qc.rx(a, 0)
qc.rx(b, 0)
if grad is FiniteDiffEstimatorGradient:
gradient = grad(estimator, epsilon=1e-6)
else:
gradient = grad(estimator)
param_list = [[np.pi / 4, np.pi / 2]]
correct_results = [
@ -229,9 +202,7 @@ class TestEstimatorGradient(QiskitTestCase):
gradients = gradient.run([qc], [op], [param], parameters=[[a]]).result().gradients[0]
np.testing.assert_allclose(gradients, correct_results[i], atol=1e-3)
@combine(
grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient]
)
@combine(grad=gradient_factories)
def test_gradient_multi_arguments(self, grad):
"""Test the estimator gradient for multiple arguments"""
estimator = Estimator()
@ -241,9 +212,6 @@ class TestEstimatorGradient(QiskitTestCase):
qc.rx(a, 0)
qc2 = QuantumCircuit(1)
qc2.rx(b, 0)
if grad is FiniteDiffEstimatorGradient:
gradient = grad(estimator, epsilon=1e-6)
else:
gradient = grad(estimator)
param_list = [[np.pi / 4], [np.pi / 2]]
correct_results = [

View File

@ -34,12 +34,20 @@ from qiskit.primitives import Sampler
from qiskit.result import QuasiDistribution
from qiskit.test import QiskitTestCase
gradient_factories = [
lambda sampler: FiniteDiffSamplerGradient(sampler, epsilon=1e-6, method="central"),
lambda sampler: FiniteDiffSamplerGradient(sampler, epsilon=1e-6, method="forward"),
lambda sampler: FiniteDiffSamplerGradient(sampler, epsilon=1e-6, method="backward"),
ParamShiftSamplerGradient,
LinCombSamplerGradient,
]
@ddt
class TestSamplerGradient(QiskitTestCase):
"""Test Sampler Gradient"""
@combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient])
@combine(grad=gradient_factories)
def test_gradient_p(self, grad):
"""Test the sampler gradient for p"""
sampler = Sampler()
@ -49,9 +57,6 @@ class TestSamplerGradient(QiskitTestCase):
qc.p(a, 0)
qc.h(0)
qc.measure_all()
if grad is FiniteDiffSamplerGradient:
gradient = grad(sampler, epsilon=1e-6)
else:
gradient = grad(sampler)
param_list = [[np.pi / 4], [0], [np.pi / 2]]
correct_results = [
@ -65,7 +70,7 @@ class TestSamplerGradient(QiskitTestCase):
for k in quasi_dist:
self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3)
@combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient])
@combine(grad=gradient_factories)
def test_gradient_u(self, grad):
"""Test the sampler gradient for u"""
sampler = Sampler()
@ -77,9 +82,6 @@ class TestSamplerGradient(QiskitTestCase):
qc.u(a, b, c, 0)
qc.h(0)
qc.measure_all()
if grad is FiniteDiffSamplerGradient:
gradient = grad(sampler, epsilon=1e-6)
else:
gradient = grad(sampler)
param_list = [[np.pi / 4, 0, 0], [np.pi / 4, np.pi / 4, np.pi / 4]]
correct_results = [
@ -92,15 +94,12 @@ class TestSamplerGradient(QiskitTestCase):
for k in quasi_dist:
self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3)
@combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient])
@combine(grad=gradient_factories)
def test_gradient_efficient_su2(self, grad):
"""Test the sampler gradient for EfficientSU2"""
sampler = Sampler()
qc = EfficientSU2(2, reps=1)
qc.measure_all()
if grad is FiniteDiffSamplerGradient:
gradient = grad(sampler, epsilon=1e-6)
else:
gradient = grad(sampler)
param_list = [
[np.pi / 4 for param in qc.parameters],
@ -189,7 +188,7 @@ class TestSamplerGradient(QiskitTestCase):
for k in quasi_dist:
self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3)
@combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient])
@combine(grad=gradient_factories)
def test_gradient_2qubit_gate(self, grad):
"""Test the sampler gradient for 2 qubit gates"""
sampler = Sampler()
@ -211,16 +210,13 @@ class TestSamplerGradient(QiskitTestCase):
qc = QuantumCircuit(2)
qc.append(gate(a), [qc.qubits[0], qc.qubits[1]], [])
qc.measure_all()
if grad is FiniteDiffSamplerGradient:
gradient = grad(sampler, epsilon=1e-6)
else:
gradient = grad(sampler)
gradients = gradient.run([qc], [param]).result().gradients[0]
for j, quasi_dist in enumerate(gradients):
for k in quasi_dist:
self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3)
@combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient])
@combine(grad=gradient_factories)
def test_gradient_parameter_coefficient(self, grad):
"""Test the sampler gradient for parameter variables with coefficients"""
sampler = Sampler()
@ -231,9 +227,6 @@ class TestSamplerGradient(QiskitTestCase):
qc.p(2 * qc.parameters[0] + 1, 0)
qc.rxx(qc.parameters[0] + 2, 0, 1)
qc.measure_all()
if grad is FiniteDiffSamplerGradient:
gradient = grad(sampler, epsilon=1e-6)
else:
gradient = grad(sampler)
param_list = [[np.pi / 4 for _ in qc.parameters], [np.pi / 2 for _ in qc.parameters]]
correct_results = [
@ -297,7 +290,7 @@ class TestSamplerGradient(QiskitTestCase):
for k in quasi_dist:
self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 2)
@combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient])
@combine(grad=gradient_factories)
def test_gradient_parameters(self, grad):
"""Test the sampler gradient for parameters"""
sampler = Sampler()
@ -307,9 +300,6 @@ class TestSamplerGradient(QiskitTestCase):
qc.rx(a, 0)
qc.rz(b, 0)
qc.measure_all()
if grad is FiniteDiffSamplerGradient:
gradient = grad(sampler, epsilon=1e-6)
else:
gradient = grad(sampler)
param_list = [[np.pi / 4, np.pi / 2]]
correct_results = [
@ -321,7 +311,7 @@ class TestSamplerGradient(QiskitTestCase):
for k in quasi_dist:
self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3)
@combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient])
@combine(grad=gradient_factories)
def test_gradient_multi_arguments(self, grad):
"""Test the sampler gradient for multiple arguments"""
sampler = Sampler()
@ -333,9 +323,6 @@ class TestSamplerGradient(QiskitTestCase):
qc2 = QuantumCircuit(1)
qc2.rx(b, 0)
qc2.measure_all()
if grad is FiniteDiffSamplerGradient:
gradient = grad(sampler, epsilon=1e-6)
else:
gradient = grad(sampler)
param_list = [[np.pi / 4], [np.pi / 2]]
correct_results = [