Calcular gradientes

Veja no TensorFlow.org Executar no Google Colab Ver fonte no GitHub Baixar caderno

Este tutorial explora algoritmos de cálculo de gradiente para os valores esperados de circuitos quânticos.

Calcular o gradiente do valor esperado de um determinado observável em um circuito quântico é um processo complicado. Os valores esperados dos observáveis ​​não têm o luxo de ter fórmulas analíticas de gradiente que são sempre fáceis de anotar – ao contrário das transformações tradicionais de aprendizado de máquina, como multiplicação de matrizes ou adição de vetores, que possuem fórmulas analíticas de gradiente que são fáceis de anotar. Como resultado, existem diferentes métodos de cálculo de gradiente quântico que são úteis para diferentes cenários. Este tutorial compara e contrasta dois esquemas de diferenciação diferentes.

Configurar

pip install tensorflow==2.7.0

Instale o 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'>

Agora importe o TensorFlow e as dependências do módulo:

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. Preliminar

Vamos tornar a noção de cálculo de gradiente para circuitos quânticos um pouco mais concreta. Suponha que você tenha um circuito parametrizado como este:

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

Junto com um observável:

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

Olhando para este operador, você sabe que \(⟨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

e se você definir \(f_{1}(\alpha) = ⟨Y(\alpha)| X | Y(\alpha)⟩\) então \(f_{1}^{'}(\alpha) = \pi \cos(\pi \alpha)\). Vamos verificar isso:

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. A necessidade de um diferencial

Com circuitos maiores, você nem sempre terá a sorte de ter uma fórmula que calcule com precisão os gradientes de um determinado circuito quântico. Caso uma fórmula simples não seja suficiente para calcular o gradiente, a classe tfq.differentiators.Differentiator permite definir algoritmos para calcular os gradientes de seus circuitos. Por exemplo, você pode recriar o exemplo acima no TensorFlow Quantum (TFQ) com:

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

No entanto, se você mudar para estimar a expectativa com base na amostragem (o que aconteceria em um dispositivo real), os valores podem mudar um pouco. Isso significa que agora você tem uma estimativa imperfeita:

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

Isso pode rapidamente se transformar em um sério problema de precisão quando se trata de gradientes:

# 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

Aqui você pode ver que, embora a fórmula de diferença finita seja rápida para calcular os próprios gradientes no caso analítico, quando se trata dos métodos baseados em amostragem, ela era muito barulhenta. Técnicas mais cuidadosas devem ser usadas para garantir que um bom gradiente possa ser calculado. Em seguida, você verá uma técnica muito mais lenta que não seria tão adequada para cálculos de gradiente de expectativa analítica, mas funciona muito melhor no caso baseado em amostra do mundo real:

# 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

A partir do exposto, você pode ver que certos diferenciais são melhor usados ​​para cenários de pesquisa específicos. Em geral, os métodos mais lentos baseados em amostras que são robustos ao ruído do dispositivo, etc., são grandes diferenciais ao testar ou implementar algoritmos em uma configuração mais "do mundo real". Métodos mais rápidos, como diferença finita, são ótimos para cálculos analíticos e você deseja um rendimento maior, mas ainda não está preocupado com a viabilidade do dispositivo do seu algoritmo.

3. Múltiplos observáveis

Vamos apresentar um segundo observável e ver como o TensorFlow Quantum oferece suporte a vários observáveis ​​para um único circuito.

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

Se este observável for usado com o mesmo circuito de antes, você terá \(f_{2}(\alpha) = ⟨Y(\alpha)| Z | Y(\alpha)⟩ = \cos(\pi \alpha)\) e \(f_{2}^{'}(\alpha) = -\pi \sin(\pi \alpha)\). Faça uma verificação rápida:

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

É um jogo (próximo o suficiente).

Agora, se você definir \(g(\alpha) = f_{1}(\alpha) + f_{2}(\alpha)\) , então \(g'(\alpha) = f_{1}^{'}(\alpha) + f^{'}_{2}(\alpha)\). Definir mais de um observável no TensorFlow Quantum para usar junto com um circuito é equivalente a adicionar mais termos a \(g\).

Isso significa que o gradiente de um determinado símbolo em um circuito é igual à soma dos gradientes em relação a cada observável para aquele símbolo aplicado a esse circuito. Isso é compatível com a obtenção e retropropagação de gradiente do TensorFlow (onde você fornece a soma dos gradientes sobre todos os observáveis ​​como o gradiente de um símbolo específico).

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

Aqui você vê a primeira entrada é a expectativa wrt Pauli X, e a segunda é a expectativa wrt Pauli Z. Agora quando você pega o gradiente:

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

Aqui você verificou que a soma dos gradientes para cada observável é de fato o gradiente de \(\alpha\). Esse comportamento é suportado por todos os diferenciadores do TensorFlow Quantum e desempenha um papel crucial na compatibilidade com o restante do TensorFlow.

4. Uso avançado

Todos os diferenciadores que existem dentro da subclasse tfq.differentiators.Differentiator do TensorFlow Quantum . Para implementar um diferenciador, um usuário deve implementar uma das duas interfaces. O padrão é implementar get_gradient_circuits , que informa à classe base quais circuitos devem ser medidos para obter uma estimativa do gradiente. Como alternativa, você pode sobrecarregar differentiate_analytic e differentiate_sampled ; a classe tfq.differentiators.Adjoint segue esse caminho.

O seguinte usa o TensorFlow Quantum para implementar o gradiente de um circuito. Você usará um pequeno exemplo de mudança de parâmetro.

Lembre-se do circuito que você definiu acima, \(|\alpha⟩ = Y^{\alpha}|0⟩\). Como antes, você pode definir uma função como o valor esperado deste circuito em relação ao observável l10n- \(X\) , \(f(\alpha) = ⟨\alpha|X|\alpha⟩\). Usando regras de mudança de parâmetro , para este circuito, você pode descobrir que a derivada é

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

A função get_gradient_circuits retorna os componentes desta derivada.

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)

A classe base Differentiator usa os componentes retornados de get_gradient_circuits para calcular a derivada, como na fórmula de mudança de parâmetro que você viu acima. Este novo diferenciador agora pode ser usado com objetos tfq.layer existentes:

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

Esse novo diferenciador agora pode ser usado para gerar operações diferenciáveis.

# 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