![]() | ![]() | ![]() | ![]() |
Ten samouczek pokazuje, jak klasyczna sieć neuronowa może nauczyć się poprawiać błędy kalibracji kubitów. Przedstawia Cirq , platformę Python do tworzenia, edytowania i wywoływania obwodów kwantowych o dużej skali pośredniej (NISQ) i pokazuje, jak Cirq współpracuje z TensorFlow Quantum.
Ustawiać
pip install -q tensorflow==2.3.1
Zainstaluj TensorFlow Quantum:
pip install -q tensorflow-quantum
Teraz zaimportuj TensorFlow i zależności modułów:
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. Podstawy
1.1 Obwody kwantowe i sparametryzowane obwody kwantowe
Przed zapoznaniem się z TensorFlow Quantum (TFQ) przyjrzyjmy się niektórym podstawom Cirq . Cirq to biblioteka Pythona do obliczeń kwantowych od Google. Używasz go do definiowania obwodów, w tym bramek statycznych i sparametryzowanych.
Cirq używa symboli SymPy do reprezentowania dowolnych parametrów.
a, b = sympy.symbols('a b')
Poniższy kod tworzy obwód z dwoma kubitami przy użyciu parametrów:
# Create two qubits
q0, q1 = cirq.GridQubit.rect(1, 2)
# Create a circuit on these qubits using the parameters you created above.
circuit = cirq.Circuit(
cirq.rx(a).on(q0),
cirq.ry(b).on(q1), cirq.CNOT(control=q0, target=q1))
SVGCircuit(circuit)
findfont: Font family ['Arial'] not found. Falling back to DejaVu Sans.
Aby ocenić obwody, możesz użyć interfejsu cirq.Simulator
. cirq.ParamResolver
parametry w obwodzie zastępujesz określonymi liczbami, przekazując obiekt cirq.ParamResolver
. Poniższy kod oblicza wyjściowy wektor stanu surowego sparametryzowanego obwodu:
# Calculate a state vector with a=0.5 and b=-0.5.
resolver = cirq.ParamResolver({a: 0.5, b: -0.5})
output_state_vector = cirq.Simulator().simulate(circuit, resolver).final_state_vector
output_state_vector
array([ 0.9387913 +0.j , -0.23971277+0.j , 0. +0.06120872j, 0. -0.23971277j], dtype=complex64)
Wektory stanu nie są bezpośrednio dostępne poza symulacją (zwróć uwagę na liczby zespolone w powyższym wyniku). Aby być realistycznym fizycznie, musisz określić pomiar, który przekształca wektor stanu na liczbę rzeczywistą, którą mogą zrozumieć klasyczne komputery. Cirq określa pomiary za pomocą kombinacji operatorów Pauliego $ \ hat {X} $, $ \ hat {Y} $ i $ \ hat {Z} $. Jako ilustracja, poniższy kod mierzy $ \ hat {Z} _0 $ i $ \ frac {1} {2} \ hat {Z} _0 + \ hat {X} _1 $ na zasymulowanym wektorze stanu:
z0 = cirq.Z(q0)
qubit_map={q0: 0, q1: 1}
z0.expectation_from_state_vector(output_state_vector, qubit_map).real
0.8775825500488281
z0x1 = 0.5 * z0 + cirq.X(q1)
z0x1.expectation_from_state_vector(output_state_vector, qubit_map).real
-0.04063427448272705
1.2 Obwody kwantowe jako tensory
TensorFlow Quantum (TFQ) udostępnia tfq.convert_to_tensor
, która przekształca obiekty Cirq w tensory. Pozwala to na wysyłanie obiektów Cirq do naszych warstwkwantowych ioperacji kwantowych . Funkcję można wywołać na listach lub tablicach Cirq Circuits i Cirq Paulis:
# Rank 1 tensor containing 1 circuit.
circuit_tensor = tfq.convert_to_tensor([circuit])
print(circuit_tensor.shape)
print(circuit_tensor.dtype)
(1,) <dtype: 'string'>
Ten koduje Cirq obiektów jak tf.string
tensory że tfq
operacje Decode, ile potrzeba.
# Rank 1 tensor containing 2 Pauli operators.
pauli_tensor = tfq.convert_to_tensor([z0, z0x1])
pauli_tensor.shape
TensorShape([2])
1.3 Symulacja obwodu dozowania
TFQ zapewnia metody obliczania wartości oczekiwanych, próbek i wektorów stanu. Na razie skupmy się na wartościach oczekiwanych .
Interfejs najwyższego poziomu do obliczania wartości oczekiwanych to warstwa tfq.layers.Expectation
, która jest tf.keras.Layer
. W najprostszej postaci ta warstwa jest równoważna symulacji sparametryzowanego obwodu w wielu cirq.ParamResolvers
; jednak TFQ pozwala na grupowanie zgodnie z semantyką TensorFlow, a obwody są symulowane przy użyciu wydajnego kodu C ++.
Utwórz zbiór wartości, które zastąpią nasze parametry a
i b
:
batch_vals = np.array(np.random.uniform(0, 2 * np.pi, (5, 2)), dtype=np.float32)
Dozowanie obwodu ponad wartościami parametrów w Cirq wymaga pętli:
cirq_results = []
cirq_simulator = cirq.Simulator()
for vals in batch_vals:
resolver = cirq.ParamResolver({a: vals[0], b: vals[1]})
final_state_vector = cirq_simulator.simulate(circuit, resolver).final_state_vector
cirq_results.append(
[z0.expectation_from_state_vector(final_state_vector, {
q0: 0,
q1: 1
}).real])
print('cirq batch results: \n {}'.format(np.array(cirq_results)))
cirq batch results: [[-0.97633868] [ 0.68726736] [-0.67144978] [-0.92119527] [ 0.41925651]]
Ta sama operacja jest uproszczona w TFQ:
tfq.layers.Expectation()(circuit,
symbol_names=[a, b],
symbol_values=batch_vals,
operators=z0)
<tf.Tensor: shape=(5, 1), dtype=float32, numpy= array([[-0.9763384 ], [ 0.6872687 ], [-0.67144877], [-0.9211948 ], [ 0.41925824]], dtype=float32)>
2. Hybrydowa optymalizacja kwantowo-klasyczna
Teraz, gdy znasz już podstawy, użyjmy TensorFlow Quantum do zbudowania hybrydowej kwantowo-klasycznej sieci neuronowej . Nauczysz klasyczną sieć neuronową do sterowania pojedynczym kubitem. Sterowanie zostanie zoptymalizowane, aby poprawnie przygotować kubit w stanie 0
lub 1
, przezwyciężając symulowany systematyczny błąd kalibracji. Ten rysunek przedstawia architekturę:
Nawet bez sieci neuronowej jest to prosty problem do rozwiązania, ale temat jest podobny do rzeczywistych problemów kontroli kwantowej, które można rozwiązać za pomocą TFQ. Przedstawia kompleksowy przykład kwantowo-klasycznego obliczenia przy użyciu tfq.layers.ControlledPQC
(parametryczny obwód kwantowy) wewnątrz tf.keras.Model
.
W celu wykonania tego samouczka ta architektura została podzielona na 3 części:
- Obwód wejściowy lub obwód punktu danych : pierwsze trzy bramki $ R $.
- Kontrolowany obwód : pozostałe trzy bramki $ R $.
- Sterownik : Klasyczna sieć neuronowa ustawiająca parametry sterowanego obwodu.
2.1 Definicja kontrolowanego obwodu
Zdefiniuj możliwy do nauczenia obrót pojedynczego bitu, jak pokazano na powyższym rysunku. Będzie to odpowiadać naszemu kontrolowanemu obwodowi.
# Parameters that the classical NN will feed values into.
control_params = sympy.symbols('theta_1 theta_2 theta_3')
# Create the parameterized circuit.
qubit = cirq.GridQubit(0, 0)
model_circuit = cirq.Circuit(
cirq.rz(control_params[0])(qubit),
cirq.ry(control_params[1])(qubit),
cirq.rx(control_params[2])(qubit))
SVGCircuit(model_circuit)
2.2 Kontroler
Teraz zdefiniuj sieć kontrolerów:
# The classical neural network layers.
controller = tf.keras.Sequential([
tf.keras.layers.Dense(10, activation='elu'),
tf.keras.layers.Dense(3)
])
Po otrzymaniu zestawu poleceń sterownik wyprowadza pakiet sygnałów sterujących dla sterowanego obwodu.
Sterownik jest inicjalizowany losowo, więc te wyjścia nie są jeszcze przydatne.
controller(tf.constant([[0.0],[1.0]])).numpy()
array([[0. , 0. , 0. ], [0.08031327, 0.03002123, 0.0143298 ]], dtype=float32)
2.3 Podłączyć sterownik do obwodu
Za pomocą tfq
podłącz kontroler do kontrolowanego obwodu, jako pojedynczy keras.Model
.
Więcej informacji na temat tego stylu definiowania modelu można znaleźć w przewodniku po funkcjonalnym interfejsie API Keras .
Najpierw zdefiniuj dane wejściowe do modelu:
# This input is the simulated miscalibration that the model will learn to correct.
circuits_input = tf.keras.Input(shape=(),
# The circuit-tensor has dtype `tf.string`
dtype=tf.string,
name='circuits_input')
# Commands will be either `0` or `1`, specifying the state to set the qubit to.
commands_input = tf.keras.Input(shape=(1,),
dtype=tf.dtypes.float32,
name='commands_input')
Następnie zastosuj operacje do tych danych wejściowych, aby zdefiniować obliczenia.
dense_2 = controller(commands_input)
# TFQ layer for classically controlled circuits.
expectation_layer = tfq.layers.ControlledPQC(model_circuit,
# Observe Z
operators = cirq.Z(qubit))
expectation = expectation_layer([circuits_input, dense_2])
Teraz tf.keras.Model
to obliczenie jako tf.keras.Model
:
# The full Keras model is built from our layers.
model = tf.keras.Model(inputs=[circuits_input, commands_input],
outputs=expectation)
Architekturę sieci wskazuje wykres na poniższym modelu. Porównaj ten wykres modelowy z diagramem architektury, aby zweryfikować poprawność.
tf.keras.utils.plot_model(model, show_shapes=True, dpi=70)
Ten model ma dwa wejścia: polecenia dla sterownika i obwód wejściowy, którego wyjście sterownik próbuje poprawić.
2.4 Zbiór danych
Model próbuje wyprowadzić poprawną wartość pomiaru $ \ hat {Z} $ dla każdego polecenia. Polecenia i prawidłowe wartości są zdefiniowane poniżej.
# The command input values to the classical NN.
commands = np.array([[0], [1]], dtype=np.float32)
# The desired Z expectation value at output of quantum circuit.
expected_outputs = np.array([[1], [-1]], dtype=np.float32)
To nie jest cały zestaw danych szkoleniowych dla tego zadania. Każdy punkt danych w zbiorze danych również wymaga obwodu wejściowego.
2.4 Definicja obwodu wejściowego
Poniższy obwód wejściowy definiuje przypadkową błędną kalibrację, którą model nauczy się korygować.
random_rotations = np.random.uniform(0, 2 * np.pi, 3)
noisy_preparation = cirq.Circuit(
cirq.rx(random_rotations[0])(qubit),
cirq.ry(random_rotations[1])(qubit),
cirq.rz(random_rotations[2])(qubit)
)
datapoint_circuits = tfq.convert_to_tensor([
noisy_preparation
] * 2) # Make two copied of this circuit
Istnieją dwie kopie obwodu, po jednej dla każdego punktu danych.
datapoint_circuits.shape
TensorShape([2])
2.5 Szkolenie
Po zdefiniowaniu danych wejściowych można przetestować model tfq
.
model([datapoint_circuits, commands]).numpy()
array([[-0.14540803], [-0.16774747]], dtype=float32)
Teraz uruchom standardowy proces szkoleniowy, aby dostosować te wartości do expected_outputs
.
optimizer = tf.keras.optimizers.Adam(learning_rate=0.05)
loss = tf.keras.losses.MeanSquaredError()
model.compile(optimizer=optimizer, loss=loss)
history = model.fit(x=[datapoint_circuits, commands],
y=expected_outputs,
epochs=30,
verbose=0)
plt.plot(history.history['loss'])
plt.title("Learning to Control a Qubit")
plt.xlabel("Iterations")
plt.ylabel("Error in Control")
plt.show()
Z tego wykresu widać, że sieć neuronowa nauczyła się przezwyciężać systematyczne błędy kalibracji.
2.6 Weryfikacja wyjść
Teraz użyj wyszkolonego modelu, aby poprawić błędy kalibracji kubitu. Z Cirq:
def check_error(command_values, desired_values):
"""Based on the value in `command_value` see how well you could prepare
the full circuit to have `desired_value` when taking expectation w.r.t. Z."""
params_to_prepare_output = controller(command_values).numpy()
full_circuit = noisy_preparation + model_circuit
# Test how well you can prepare a state to get expectation the expectation
# value in `desired_values`
for index in [0, 1]:
state = cirq_simulator.simulate(
full_circuit,
{s:v for (s,v) in zip(control_params, params_to_prepare_output[index])}
).final_state_vector
expt = cirq.Z(qubit).expectation_from_state_vector(state, {qubit: 0}).real
print(f'For a desired output (expectation) of {desired_values[index]} with'
f' noisy preparation, the controller\nnetwork found the following '
f'values for theta: {params_to_prepare_output[index]}\nWhich gives an'
f' actual expectation of: {expt}\n')
check_error(commands, expected_outputs)
For a desired output (expectation) of [1.] with noisy preparation, the controller network found the following values for theta: [-1.8619336 -1.523817 -3.2972674] Which gives an actual expectation of: 0.9911799430847168 For a desired output (expectation) of [-1.] with noisy preparation, the controller network found the following values for theta: [1.0268601 1.7161715 0.44205332] Which gives an actual expectation of: -0.9540284872055054
Wartość funkcji straty podczas treningu daje ogólne pojęcie o tym, jak dobrze model się uczy. Im niższa strata, tym wartości oczekiwane w powyższej komórce są desired_values
. Jeśli nie przejmujesz się wartościami parametrów, zawsze możesz sprawdzić dane wyjściowe z góry za pomocą tfq
:
model([datapoint_circuits, commands])
<tf.Tensor: shape=(2, 1), dtype=float32, numpy= array([[ 0.9911797], [-0.9540284]], dtype=float32)>
3 Nauka przygotowywania stanów własnych różnych operatorów
Wybór stanów własnych $ \ pm \ hat {Z} $ odpowiadających 1 i 0 był arbitralny. Równie łatwo mógłbyś chcieć, aby 1 odpowiadało $ + \ hat {Z} $ własnemu stanowi, a 0 odpowiadało $ - \ hat {X} $ eigenstate. Jednym ze sposobów osiągnięcia tego jest określenie innego operatora pomiaru dla każdego polecenia, jak pokazano na poniższym rysunku:
Wymaga to użycia tfq.layers.Expectation
. Teraz twoje dane wejściowe urosły i obejmują trzy obiekty: obwód, polecenie i operator. Wynik jest nadal wartością oczekiwaną.
3.1 Nowa definicja modelu
Przyjrzyjmy się modelowi, aby wykonać to zadanie:
# Define inputs.
commands_input = tf.keras.layers.Input(shape=(1),
dtype=tf.dtypes.float32,
name='commands_input')
circuits_input = tf.keras.Input(shape=(),
# The circuit-tensor has dtype `tf.string`
dtype=tf.dtypes.string,
name='circuits_input')
operators_input = tf.keras.Input(shape=(1,),
dtype=tf.dtypes.string,
name='operators_input')
Oto sieć kontrolerów:
# Define classical NN.
controller = tf.keras.Sequential([
tf.keras.layers.Dense(10, activation='elu'),
tf.keras.layers.Dense(3)
])
Połącz obwód i kontroler w jeden keras.Model
za pomocą tfq
:
dense_2 = controller(commands_input)
# Since you aren't using a PQC or ControlledPQC you must append
# your model circuit onto the datapoint circuit tensor manually.
full_circuit = tfq.layers.AddCircuit()(circuits_input, append=model_circuit)
expectation_output = tfq.layers.Expectation()(full_circuit,
symbol_names=control_params,
symbol_values=dense_2,
operators=operators_input)
# Contruct your Keras model.
two_axis_control_model = tf.keras.Model(
inputs=[circuits_input, commands_input, operators_input],
outputs=[expectation_output])
3.2 Zbiór danych
Teraz uwzględnisz również operatory, które chcesz mierzyć dla każdego punktu danych, który model_circuit
dla model_circuit
:
# The operators to measure, for each command.
operator_data = tfq.convert_to_tensor([[cirq.X(qubit)], [cirq.Z(qubit)]])
# The command input values to the classical NN.
commands = np.array([[0], [1]], dtype=np.float32)
# The desired expectation value at output of quantum circuit.
expected_outputs = np.array([[1], [-1]], dtype=np.float32)
3.3 Szkolenie
Teraz, gdy masz nowe wejścia i wyjścia, możesz ponownie trenować za pomocą keras.
optimizer = tf.keras.optimizers.Adam(learning_rate=0.05)
loss = tf.keras.losses.MeanSquaredError()
two_axis_control_model.compile(optimizer=optimizer, loss=loss)
history = two_axis_control_model.fit(
x=[datapoint_circuits, commands, operator_data],
y=expected_outputs,
epochs=30,
verbose=1)
Epoch 1/30 1/1 [==============================] - 0s 1ms/step - loss: 1.7510 Epoch 2/30 1/1 [==============================] - 0s 759us/step - loss: 0.8703 Epoch 3/30 1/1 [==============================] - 0s 1ms/step - loss: 0.3938 Epoch 4/30 1/1 [==============================] - 0s 749us/step - loss: 0.1695 Epoch 5/30 1/1 [==============================] - 0s 722us/step - loss: 0.0820 Epoch 6/30 1/1 [==============================] - 0s 685us/step - loss: 0.0609 Epoch 7/30 1/1 [==============================] - 0s 676us/step - loss: 0.0704 Epoch 8/30 1/1 [==============================] - 0s 734us/step - loss: 0.0826 Epoch 9/30 1/1 [==============================] - 0s 732us/step - loss: 0.0723 Epoch 10/30 1/1 [==============================] - 0s 907us/step - loss: 0.0431 Epoch 11/30 1/1 [==============================] - 0s 731us/step - loss: 0.0178 Epoch 12/30 1/1 [==============================] - 0s 859us/step - loss: 0.0064 Epoch 13/30 1/1 [==============================] - 0s 763us/step - loss: 0.0035 Epoch 14/30 1/1 [==============================] - 0s 684us/step - loss: 0.0031 Epoch 15/30 1/1 [==============================] - 0s 710us/step - loss: 0.0030 Epoch 16/30 1/1 [==============================] - 0s 671us/step - loss: 0.0038 Epoch 17/30 1/1 [==============================] - 0s 716us/step - loss: 0.0062 Epoch 18/30 1/1 [==============================] - 0s 707us/step - loss: 0.0095 Epoch 19/30 1/1 [==============================] - 0s 700us/step - loss: 0.0120 Epoch 20/30 1/1 [==============================] - 0s 703us/step - loss: 0.0117 Epoch 21/30 1/1 [==============================] - 0s 697us/step - loss: 0.0089 Epoch 22/30 1/1 [==============================] - 0s 717us/step - loss: 0.0053 Epoch 23/30 1/1 [==============================] - 0s 739us/step - loss: 0.0025 Epoch 24/30 1/1 [==============================] - 0s 725us/step - loss: 9.8351e-04 Epoch 25/30 1/1 [==============================] - 0s 709us/step - loss: 3.7235e-04 Epoch 26/30 1/1 [==============================] - 0s 660us/step - loss: 3.4534e-04 Epoch 27/30 1/1 [==============================] - 0s 744us/step - loss: 6.7898e-04 Epoch 28/30 1/1 [==============================] - 0s 740us/step - loss: 0.0013 Epoch 29/30 1/1 [==============================] - 0s 770us/step - loss: 0.0020 Epoch 30/30 1/1 [==============================] - 0s 756us/step - loss: 0.0028
plt.plot(history.history['loss'])
plt.title("Learning to Control a Qubit")
plt.xlabel("Iterations")
plt.ylabel("Error in Control")
plt.show()
Funkcja straty spadła do zera.
controller
jest dostępny jako samodzielny model. Zadzwoń do kontrolera i sprawdź jego odpowiedź na każdy sygnał polecenia. Prawidłowe porównanie tych wyników z zawartością random_rotations
wymagałoby trochę pracy.
controller.predict(np.array([0,1]))
array([[ 1.741399 , -0.22516271, 0.7868666 ], [-0.31710118, 1.6385193 , 1.8622308 ]], dtype=float32)