Gradienten berechnen

Ansicht auf TensorFlow.org In Google Colab ausführen Quelle auf GitHub anzeigen Notizbuch herunterladen

In diesem Tutorial werden Algorithmen zur Gradientenberechnung für die Erwartungswerte von Quantenschaltungen untersucht.

Die Berechnung des Gradienten des Erwartungswerts einer bestimmten beobachtbaren Menge in einer Quantenschaltung ist ein komplizierter Prozess. Erwartungswerte von Observablen haben nicht den Luxus, analytische Gradientenformeln zu haben, die immer leicht aufzuschreiben sind - im Gegensatz zu herkömmlichen Transformationen des maschinellen Lernens wie Matrixmultiplikation oder Vektoraddition mit analytischen Gradientenformeln, die leicht aufzuschreiben sind. Infolgedessen gibt es verschiedene Methoden zur Berechnung von Quantengradienten, die für verschiedene Szenarien nützlich sind. In diesem Tutorial werden zwei verschiedene Differenzierungsschemata verglichen und gegenübergestellt.

Einrichten

pip install -q tensorflow==2.4.1

Installieren Sie TensorFlow Quantum:

pip install -q tensorflow-quantum

Importieren Sie nun TensorFlow und die Modulabhängigkeiten:

import tensorflow as tf
import tensorflow_quantum as tfq

import cirq
import sympy
import numpy as np

# visualization tools
%matplotlib inline
import matplotlib.pyplot as plt
from cirq.contrib.svg import SVGCircuit

1. Vorläufig

Lassen Sie uns den Begriff der Gradientenberechnung für Quantenschaltungen etwas konkreter machen. Angenommen, Sie haben eine parametrisierte Schaltung wie diese:

qubit = cirq.GridQubit(0, 0)
my_circuit = cirq.Circuit(cirq.Y(qubit)**sympy.Symbol('alpha'))
SVGCircuit(my_circuit)
findfont: Font family ['Arial'] not found. Falling back to DejaVu Sans.

svg

Zusammen mit einem beobachtbaren:

pauli_x = cirq.X(qubit)
pauli_x
cirq.X(cirq.GridQubit(0, 0))

Wenn Sie sich diesen Operator ansehen, wissen Sie, dass $ ⟨Y (\ alpha) | X | Y (\ alpha)⟩ = \ sin (\ pi \ alpha) $

def my_expectation(op, alpha):
    """Compute ⟨Y(alpha)| `op` | Y(alpha)⟩"""
    params = {'alpha': alpha}
    sim = cirq.Simulator()
    final_state_vector = sim.simulate(my_circuit, params).final_state_vector
    return op.expectation_from_state_vector(final_state_vector, {qubit: 0}).real


my_alpha = 0.3
print("Expectation=", my_expectation(pauli_x, my_alpha))
print("Sin Formula=", np.sin(np.pi * my_alpha))
Expectation= 0.80901700258255
Sin Formula= 0.8090169943749475

und wenn Sie $ f_ {1} (\ alpha) = ⟨Y (\ alpha) | definieren X | Y (\ alpha)⟩ $ dann $ f_ {1} ^ {'} (\ alpha) = \ pi \ cos (\ pi \ alpha) $. Lassen Sie uns dies überprüfen:

def my_grad(obs, alpha, eps=0.01):
    grad = 0
    f_x = my_expectation(obs, alpha)
    f_x_prime = my_expectation(obs, alpha + eps)
    return ((f_x_prime - f_x) / eps).real


print('Finite difference:', my_grad(pauli_x, my_alpha))
print('Cosine formula:   ', np.pi * np.cos(np.pi * my_alpha))
Finite difference: 1.8063604831695557
Cosine formula:    1.8465818304904567

2. Die Notwendigkeit eines Unterscheidungsmerkmals

Bei größeren Schaltkreisen haben Sie nicht immer das Glück, eine Formel zu haben, die die Gradienten einer bestimmten Quantenschaltung genau berechnet. Für den Fall, dass eine einfache Formel nicht ausreicht, um den Gradienten zu berechnen, können Sie mit der Klasse tfq.differentiators.Differentiator Algorithmen zur Berechnung der Gradienten Ihrer Schaltungen definieren. Zum Beispiel können Sie das obige Beispiel in TensorFlow Quantum (TFQ) neu erstellen mit:

expectation_calculation = tfq.layers.Expectation(
    differentiator=tfq.differentiators.ForwardDifference(grid_spacing=0.01))

expectation_calculation(my_circuit,
                        operators=pauli_x,
                        symbol_names=['alpha'],
                        symbol_values=[[my_alpha]])
<tf.Tensor: shape=(1, 1), dtype=float32, numpy=array([[0.80901706]], dtype=float32)>

Wenn Sie jedoch zur Schätzung der Erwartung basierend auf der Stichprobe wechseln (was auf einem echten Gerät passieren würde), können sich die Werte geringfügig ändern. Dies bedeutet, dass Sie jetzt eine unvollständige Schätzung haben:

sampled_expectation_calculation = tfq.layers.SampledExpectation(
    differentiator=tfq.differentiators.ForwardDifference(grid_spacing=0.01))

sampled_expectation_calculation(my_circuit,
                                operators=pauli_x,
                                repetitions=500,
                                symbol_names=['alpha'],
                                symbol_values=[[my_alpha]])
<tf.Tensor: shape=(1, 1), dtype=float32, numpy=array([[0.836]], dtype=float32)>

Dies kann schnell zu einem ernsthaften Genauigkeitsproblem führen, wenn es um Farbverläufe geht:

# Make input_points = [batch_size, 1] array.
input_points = np.linspace(0, 5, 200)[:, np.newaxis].astype(np.float32)
exact_outputs = expectation_calculation(my_circuit,
                                        operators=pauli_x,
                                        symbol_names=['alpha'],
                                        symbol_values=input_points)
imperfect_outputs = sampled_expectation_calculation(my_circuit,
                                                    operators=pauli_x,
                                                    repetitions=500,
                                                    symbol_names=['alpha'],
                                                    symbol_values=input_points)
plt.title('Forward Pass Values')
plt.xlabel('$x$')
plt.ylabel('$f(x)$')
plt.plot(input_points, exact_outputs, label='Analytic')
plt.plot(input_points, imperfect_outputs, label='Sampled')
plt.legend()
<matplotlib.legend.Legend at 0x7fdbdcfa5210>

png

# Gradients are a much different story.
values_tensor = tf.convert_to_tensor(input_points)

with tf.GradientTape() as g:
    g.watch(values_tensor)
    exact_outputs = expectation_calculation(my_circuit,
                                            operators=pauli_x,
                                            symbol_names=['alpha'],
                                            symbol_values=values_tensor)
analytic_finite_diff_gradients = g.gradient(exact_outputs, values_tensor)

with tf.GradientTape() as g:
    g.watch(values_tensor)
    imperfect_outputs = sampled_expectation_calculation(
        my_circuit,
        operators=pauli_x,
        repetitions=500,
        symbol_names=['alpha'],
        symbol_values=values_tensor)
sampled_finite_diff_gradients = g.gradient(imperfect_outputs, values_tensor)

plt.title('Gradient Values')
plt.xlabel('$x$')
plt.ylabel('$f^{\'}(x)$')
plt.plot(input_points, analytic_finite_diff_gradients, label='Analytic')
plt.plot(input_points, sampled_finite_diff_gradients, label='Sampled')
plt.legend()
<matplotlib.legend.Legend at 0x7fdb21fdded0>

png

Hier können Sie sehen, dass die Finite-Differenzen-Formel zwar schnell ist, um die Gradienten selbst im analytischen Fall zu berechnen, aber bei den stichprobenbasierten Methoden viel zu verrauscht war. Es müssen vorsichtigere Techniken angewendet werden, um sicherzustellen, dass ein guter Gradient berechnet werden kann. Als nächstes werden Sie sich eine viel langsamere Technik ansehen, die für analytische Erwartungsgradientenberechnungen nicht so gut geeignet wäre, aber im realen stichprobenbasierten Fall eine viel bessere Leistung erbringt:

# A smarter differentiation scheme.
gradient_safe_sampled_expectation = tfq.layers.SampledExpectation(
    differentiator=tfq.differentiators.ParameterShift())

with tf.GradientTape() as g:
    g.watch(values_tensor)
    imperfect_outputs = gradient_safe_sampled_expectation(
        my_circuit,
        operators=pauli_x,
        repetitions=500,
        symbol_names=['alpha'],
        symbol_values=values_tensor)

sampled_param_shift_gradients = g.gradient(imperfect_outputs, values_tensor)

plt.title('Gradient Values')
plt.xlabel('$x$')
plt.ylabel('$f^{\'}(x)$')
plt.plot(input_points, analytic_finite_diff_gradients, label='Analytic')
plt.plot(input_points, sampled_param_shift_gradients, label='Sampled')
plt.legend()
<matplotlib.legend.Legend at 0x7fda600ef1d0>

png

Aus dem Obigen können Sie ersehen, dass bestimmte Unterscheidungsmerkmale am besten für bestimmte Forschungsszenarien verwendet werden. Im Allgemeinen sind die langsameren stichprobenbasierten Methoden, die gegenüber Geräuschen von Geräten usw. robust sind, ein großes Unterscheidungsmerkmal beim Testen oder Implementieren von Algorithmen in einer "realeren" Umgebung. Schnellere Methoden wie die endliche Differenz eignen sich hervorragend für analytische Berechnungen, und Sie möchten einen höheren Durchsatz, sind jedoch noch nicht mit der Gerätelebensfähigkeit Ihres Algorithmus befasst.

3. Mehrere Observablen

Lassen Sie uns eine zweite Observable vorstellen und sehen, wie TensorFlow Quantum mehrere Observable für eine einzelne Schaltung unterstützt.

pauli_z = cirq.Z(qubit)
pauli_z
cirq.Z(cirq.GridQubit(0, 0))

Wenn dieses Observable mit derselben Schaltung wie zuvor verwendet wird, haben Sie $ f_ {2} (\ alpha) = ⟨Y (\ alpha) | Z | Y (\ alpha)⟩ = \ cos (\ pi \ alpha) $ und $ f_ {2} ^ {'} (\ alpha) = - \ pi \ sin (\ pi \ alpha) $. Führen Sie eine schnelle Überprüfung durch:

test_value = 0.

print('Finite difference:', my_grad(pauli_z, test_value))
print('Sin formula:      ', -np.pi * np.sin(np.pi * test_value))
Finite difference: -0.04934072494506836
Sin formula:       -0.0

Es ist ein Match (nah genug).

Wenn Sie nun $ g (\ alpha) = f_ {1} (\ alpha) + f_ {2} (\ alpha) $ definieren, dann ist $ g '(\ alpha) = f_ {1} ^ {'} (\ alpha) + f ^ {'} _ {2} (\ alpha) $. Das Definieren von mehr als einem in TensorFlow Quantum beobachtbaren Element zur Verwendung zusammen mit einer Schaltung entspricht dem Hinzufügen weiterer Begriffe zu $ ​​g $.

Dies bedeutet, dass der Gradient eines bestimmten Symbols in einer Schaltung gleich der Summe der Gradienten in Bezug auf jedes für dieses Symbol beobachtbare Symbol ist, das auf diese Schaltung angewendet wird. Dies ist kompatibel mit TensorFlow-Gradientenaufnahme und Backpropagation (wobei Sie die Summe der Gradienten über alle Observablen als Gradienten für ein bestimmtes Symbol angeben).

sum_of_outputs = tfq.layers.Expectation(
    differentiator=tfq.differentiators.ForwardDifference(grid_spacing=0.01))

sum_of_outputs(my_circuit,
               operators=[pauli_x, pauli_z],
               symbol_names=['alpha'],
               symbol_values=[[test_value]])
<tf.Tensor: shape=(1, 2), dtype=float32, numpy=array([[1.9106855e-15, 1.0000000e+00]], dtype=float32)>

Hier sehen Sie, dass der erste Eintrag die Erwartung für Pauli X und der zweite die Erwartung für Pauli Z ist. Wenn Sie nun den Gradienten nehmen:

test_value_tensor = tf.convert_to_tensor([[test_value]])

with tf.GradientTape() as g:
    g.watch(test_value_tensor)
    outputs = sum_of_outputs(my_circuit,
                             operators=[pauli_x, pauli_z],
                             symbol_names=['alpha'],
                             symbol_values=test_value_tensor)

sum_of_gradients = g.gradient(outputs, test_value_tensor)

print(my_grad(pauli_x, test_value) + my_grad(pauli_z, test_value))
print(sum_of_gradients.numpy())
3.0917350202798843
[[3.0917213]]

Hier haben Sie überprüft, dass die Summe der Gradienten für jedes beobachtbare Element tatsächlich der Gradient von $ \ alpha $ ist. Dieses Verhalten wird von allen TensorFlow Quantum-Unterscheidungsmerkmalen unterstützt und spielt eine entscheidende Rolle für die Kompatibilität mit dem Rest von TensorFlow.

4. Erweiterte Verwendung

Alle Unterscheidungsmerkmale, die innerhalb der TensorFlow Quantum-Unterklasse tfq.differentiators.Differentiator . Um ein Unterscheidungsmerkmal zu implementieren, muss ein Benutzer eine von zwei Schnittstellen implementieren. Der Standard besteht darin, get_gradient_circuits zu implementieren, die der Basisklasse get_gradient_circuits , welche Schaltkreise gemessen werden müssen, um eine Schätzung des Gradienten zu erhalten. Alternativ können Sie differentiate_analytic und differentiate_sampled überladen. Die Klasse tfq.differentiators.Adjoint nimmt diesen Weg.

Im Folgenden wird TensorFlow Quantum verwendet, um den Gradienten einer Schaltung zu implementieren. Sie verwenden ein kleines Beispiel für die Parameterverschiebung.

Erinnern Sie sich an die oben definierte Schaltung $ | \ alpha⟩ = Y ^ {\ alpha} | 0⟩ $. Nach wie vor können Sie eine Funktion als Erwartungswert dieser Schaltung für das beobachtbare $ X $ definieren, $ f (\ alpha) = ⟨\ alpha | X | \ alpha⟩ $. Mithilfe von Parameterverschiebungsregeln können Sie für diese Schaltung feststellen, dass die Ableitung ist

$$\frac{\partial}{\partial \alpha} f(\alpha) = \frac{\pi}{2} f\left(\alpha + \frac{1}{2}\right) - \frac{ \pi}{2} f\left(\alpha - \frac{1}{2}\right)$$

Die Funktion get_gradient_circuits gibt die Komponenten dieser Ableitung zurück.

class MyDifferentiator(tfq.differentiators.Differentiator):
    """A Toy differentiator for <Y^alpha | X |Y^alpha>."""

    def __init__(self):
        pass

    def get_gradient_circuits(self, programs, symbol_names, symbol_values):
        """Return circuits to compute gradients for given forward pass circuits.

        Every gradient on a quantum computer can be computed via measurements
        of transformed quantum circuits.  Here, you implement a custom gradient
        for a specific circuit.  For a real differentiator, you will need to
        implement this function in a more general way.  See the differentiator
        implementations in the TFQ library for examples.
        """

        # The two terms in the derivative are the same circuit...
        batch_programs = tf.stack([programs, programs], axis=1)

        # ... with shifted parameter values.
        shift = tf.constant(1/2)
        forward = symbol_values + shift
        backward = symbol_values - shift
        batch_symbol_values = tf.stack([forward, backward], axis=1)

        # Weights are the coefficients of the terms in the derivative.
        num_program_copies = tf.shape(batch_programs)[0]
        batch_weights = tf.tile(tf.constant([[[np.pi/2, -np.pi/2]]]),
                                [num_program_copies, 1, 1])

        # The index map simply says which weights go with which circuits.
        batch_mapper = tf.tile(
            tf.constant([[[0, 1]]]), [num_program_copies, 1, 1])

        return (batch_programs, symbol_names, batch_symbol_values,
                batch_weights, batch_mapper)

Die Differentiator Basisklasse verwendet die von get_gradient_circuits zurückgegebenen get_gradient_circuits , um die Ableitung zu berechnen, wie in der oben get_gradient_circuits Parameterverschiebungsformel. Dieses neue Unterscheidungsmerkmal kann jetzt mit vorhandenen tfq.layer Objekten verwendet werden:

custom_dif = MyDifferentiator()
custom_grad_expectation = tfq.layers.Expectation(differentiator=custom_dif)

# Now let's get the gradients with finite diff.
with tf.GradientTape() as g:
    g.watch(values_tensor)
    exact_outputs = expectation_calculation(my_circuit,
                                            operators=[pauli_x],
                                            symbol_names=['alpha'],
                                            symbol_values=values_tensor)

analytic_finite_diff_gradients = g.gradient(exact_outputs, values_tensor)

# Now let's get the gradients with custom diff.
with tf.GradientTape() as g:
    g.watch(values_tensor)
    my_outputs = custom_grad_expectation(my_circuit,
                                         operators=[pauli_x],
                                         symbol_names=['alpha'],
                                         symbol_values=values_tensor)

my_gradients = g.gradient(my_outputs, values_tensor)

plt.subplot(1, 2, 1)
plt.title('Exact Gradient')
plt.plot(input_points, analytic_finite_diff_gradients.numpy())
plt.xlabel('x')
plt.ylabel('f(x)')
plt.subplot(1, 2, 2)
plt.title('My Gradient')
plt.plot(input_points, my_gradients.numpy())
plt.xlabel('x')
Text(0.5, 0, 'x')

png

Dieses neue Unterscheidungsmerkmal kann jetzt verwendet werden, um differenzierbare Operationen zu generieren.

# Create a noisy sample based expectation op.
expectation_sampled = tfq.get_sampled_expectation_op(
    cirq.DensityMatrixSimulator(noise=cirq.depolarize(0.01)))

# Make it differentiable with your differentiator:
# Remember to refresh the differentiator before attaching the new op
custom_dif.refresh()
differentiable_op = custom_dif.generate_differentiable_op(
    sampled_op=expectation_sampled)

# Prep op inputs.
circuit_tensor = tfq.convert_to_tensor([my_circuit])
op_tensor = tfq.convert_to_tensor([[pauli_x]])
single_value = tf.convert_to_tensor([[my_alpha]])
num_samples_tensor = tf.convert_to_tensor([[5000]])

with tf.GradientTape() as g:
    g.watch(single_value)
    forward_output = differentiable_op(circuit_tensor, ['alpha'], single_value,
                                       op_tensor, num_samples_tensor)

my_gradients = g.gradient(forward_output, single_value)

print('---TFQ---')
print('Foward:  ', forward_output.numpy())
print('Gradient:', my_gradients.numpy())
print('---Original---')
print('Forward: ', my_expectation(pauli_x, my_alpha))
print('Gradient:', my_grad(pauli_x, my_alpha))
---TFQ---
Foward:   [[0.7836]]
Gradient: [[1.8045309]]
---Original---
Forward:  0.80901700258255
Gradient: 1.8063604831695557