Oblicz gradienty

Zobacz na TensorFlow.org Uruchom w Google Colab Wyświetl źródło na GitHub Pobierz notatnik

W tym samouczku omówiono algorytmy obliczania gradientu dla wartości oczekiwanych obwodów kwantowych.

Obliczanie gradientu wartości oczekiwanej pewnej obserwowalnej w obwodzie kwantowym jest procesem złożonym. Wartości oczekiwane obserwabli nie mają luksusu posiadania analitycznych formuł gradientów, które zawsze są łatwe do zapisania — w przeciwieństwie do tradycyjnych przekształceń uczenia maszynowego, takich jak mnożenie macierzy lub dodawanie wektorów, które mają analityczne formuły gradientów, które są łatwe do zapisania. W rezultacie istnieją różne metody obliczania gradientów kwantowych, które przydają się w różnych scenariuszach. Ten samouczek porównuje i kontrastuje dwa różne schematy różnicowania.

Ustawiać

pip install tensorflow==2.7.0

Zainstaluj TensorFlow Quantum:

pip install tensorflow-quantum
# Update package resources to account for version changes.
import importlib, pkg_resources
importlib.reload(pkg_resources)
<module 'pkg_resources' from '/tmpfs/src/tf_docs_env/lib/python3.7/site-packages/pkg_resources/__init__.py'>

Teraz zaimportuj TensorFlow i zależności modułu:

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
2022-02-04 12:25:24.733670: E tensorflow/stream_executor/cuda/cuda_driver.cc:271] failed call to cuInit: CUDA_ERROR_NO_DEVICE: no CUDA-capable device is detected

1. Wstępne

Stwórzmy bardziej konkretne pojęcie obliczania gradientu dla obwodów kwantowych. Załóżmy, że masz sparametryzowany obwód taki jak ten:

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

Wraz z obserwowalnym:

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

Patrząc na ten operator wiesz, że \(⟨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

a jeśli zdefiniujesz \(f_{1}(\alpha) = ⟨Y(\alpha)| X | Y(\alpha)⟩\) to \(f_{1}^{'}(\alpha) = \pi \cos(\pi \alpha)\). Sprawdźmy to:

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. Potrzeba wyróżnika

W przypadku większych obwodów nie zawsze będziesz miał szczęście mieć wzór, który precyzyjnie oblicza gradienty danego obwodu kwantowego. W przypadku, gdy prosta formuła nie wystarczy do obliczenia gradientu, klasa tfq.differentiators.Differentiator umożliwia zdefiniowanie algorytmów obliczania gradientów Twoich obwodów. Na przykład możesz odtworzyć powyższy przykład w TensorFlow Quantum (TFQ) za pomocą:

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)>

Jeśli jednak przełączysz się na szacowanie oczekiwań na podstawie próbkowania (co miałoby miejsce na prawdziwym urządzeniu), wartości mogą się nieco zmienić. Oznacza to, że masz teraz niedoskonałe oszacowanie:

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)>

Może to szybko spowodować poważny problem z dokładnością, jeśli chodzi o gradienty:

# 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 0x7ff07d556190>

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 0x7ff07adb8dd0>

png

Tutaj widać, że chociaż formuła różnic skończonych pozwala szybko obliczyć same gradienty w przypadku analitycznym, to w przypadku metod opartych na próbkowaniu było zbyt głośno. Należy zastosować bardziej ostrożne techniki, aby zapewnić, że można obliczyć dobre nachylenie. Następnie przyjrzymy się znacznie wolniejszej technice, która nie byłaby tak dobrze dopasowana do analitycznych obliczeń gradientu oczekiwań, ale działa znacznie lepiej w przypadku rzeczywistych próbek:

# 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 0x7ff07ad9ff90>

png

Z powyższego widać, że pewne wyróżniki najlepiej wykorzystać do konkretnych scenariuszy badawczych. Ogólnie rzecz biorąc, wolniejsze metody oparte na próbkach, które są odporne na szum urządzenia itp., są świetnymi wyróżnikami podczas testowania lub implementowania algorytmów w bardziej „rzeczywistym” otoczeniu. Szybsze metody, takie jak różnica skończona, świetnie nadają się do obliczeń analitycznych i chcesz uzyskać wyższą przepustowość, ale nie martwisz się jeszcze o rentowność swojego algorytmu.

3. Wiele obserwowalnych

Przedstawmy drugi obserwowalny i zobaczmy, jak TensorFlow Quantum obsługuje wiele obserwowalnych dla jednego obwodu.

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

Jeśli ten obserwowalny jest używany z tym samym obwodem co poprzednio, masz \(f_{2}(\alpha) = ⟨Y(\alpha)| Z | Y(\alpha)⟩ = \cos(\pi \alpha)\) i \(f_{2}^{'}(\alpha) = -\pi \sin(\pi \alpha)\). Wykonaj szybkie sprawdzenie:

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

To pasuje (wystarczająco blisko).

Teraz, jeśli zdefiniujesz \(g(\alpha) = f_{1}(\alpha) + f_{2}(\alpha)\) to \(g'(\alpha) = f_{1}^{'}(\alpha) + f^{'}_{2}(\alpha)\). Zdefiniowanie więcej niż jednego obserwowalnego w TensorFlow Quantum do użycia wraz z obwodem jest równoznaczne z dodaniem większej liczby warunków do \(g\).

Oznacza to, że gradient określonego symbolu w obwodzie jest równy sumie gradientów w odniesieniu do każdego obserwowalnego dla tego symbolu zastosowanego do tego obwodu. Jest to zgodne z pobieraniem gradientów i propagacją wsteczną TensorFlow (gdzie podajesz sumę gradientów na wszystkich obserwowalnych jako gradient dla konkretnego symbolu).

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)>

Tutaj widzisz, że pierwszy wpis to oczekiwanie na Pauli X, a drugi to oczekiwanie na Pauli Z. Teraz, gdy weźmiesz gradient:

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]]

Tutaj sprawdziłeś, że suma gradientów dla każdego obserwowalnego jest rzeczywiście gradientem \(\alpha\). To zachowanie jest obsługiwane przez wszystkie wyróżniki TensorFlow Quantum i odgrywa kluczową rolę w kompatybilności z resztą TensorFlow.

4. Zaawansowane użycie

Wszystkie wyróżniki, które istnieją w podklasie TensorFlow Quantum tfq.differentiators.Differentiator . Aby zaimplementować wyróżnik, użytkownik musi zaimplementować jeden z dwóch interfejsów. Standardem jest implementacja get_gradient_circuits , która informuje klasę bazową, które obwody należy zmierzyć, aby uzyskać oszacowanie gradientu. Alternatywnie można przeciążyć differentiate_analytic i differentiate_sampled ; klasa tfq.differentiators.Adjoint podąża tą drogą.

Poniższe używa TensorFlow Quantum do zaimplementowania gradientu obwodu. Użyjesz małego przykładu przesuwania parametrów.

Przypomnij sobie obwód, który zdefiniowałeś powyżej, \(|\alpha⟩ = Y^{\alpha}|0⟩\). Tak jak poprzednio, możesz zdefiniować funkcję jako wartość oczekiwaną tego obwodu względem obserwowalnego \(X\) , \(f(\alpha) = ⟨\alpha|X|\alpha⟩\). Używając reguł przesuwania parametrów , dla tego obwodu, możesz znaleźć pochodną

\[\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)\]

Funkcja get_gradient_circuits zwraca składniki tej pochodnej.

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)

Klasa bazowa Differentiator wykorzystuje komponenty zwrócone z get_gradient_circuits do obliczenia pochodnej, tak jak we wzorze przesunięcia parametrów, który widzieliśmy powyżej. Ten nowy wyróżnik może być teraz używany z istniejącymi obiektami tfq.layer :

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

Ten nowy wyróżnik może być teraz używany do generowania różnicowalnych operacji.

# 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.8016]]
Gradient: [[1.7932211]]
---Original---
Forward:  0.80901700258255
Gradient: 1.8063604831695557