Ciao, molti mondi

Visualizza su TensorFlow.org Esegui in Google Colab Visualizza l'origine su GitHub Scarica quaderno

Questo tutorial mostra come una rete neurale classica può imparare a correggere gli errori di calibrazione dei qubit. Introduce Cirq , un framework Python per creare, modificare e invocare circuiti Noisy Intermediate Scale Quantum (NISQ) e dimostra come Cirq si interfaccia con TensorFlow Quantum.

Impostare

pip install tensorflow==2.7.0

Installa 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'>

Ora importa TensorFlow e le dipendenze del modulo:

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:27:31.677071: E tensorflow/stream_executor/cuda/cuda_driver.cc:271] failed call to cuInit: CUDA_ERROR_NO_DEVICE: no CUDA-capable device is detected

1. Le basi

1.1 Circ e circuiti quantistici parametrizzati

Prima di esplorare TensorFlow Quantum (TFQ), diamo un'occhiata ad alcune nozioni di base su Cirq . Circq è una libreria Python per l'informatica quantistica di Google. Lo usi per definire i circuiti, comprese le porte statiche e parametrizzate.

Cirq utilizza i simboli SymPy per rappresentare parametri liberi.

a, b = sympy.symbols('a b')

Il codice seguente crea un circuito a due qubit usando i tuoi parametri:

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

svg

Per valutare i circuiti, è possibile utilizzare l'interfaccia cirq.Simulator . Sostituisci i parametri liberi in un circuito con numeri specifici passando un oggetto cirq.ParamResolver . Il codice seguente calcola l'uscita del vettore di stato grezzo del circuito parametrizzato:

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

I vettori di stato non sono direttamente accessibili al di fuori della simulazione (notare i numeri complessi nell'output sopra). Per essere fisicamente realistici, è necessario specificare una misura, che converta un vettore di stato in un numero reale che i computer classici possono comprendere. Cirq specifica le misurazioni utilizzando le combinazioni degli operatori Pauli \(\hat{X}\), \(\hat{Y}\)e \(\hat{Z}\). A titolo illustrativo, il codice seguente misura \(\hat{Z}_0\) e \(\frac{1}{2}\hat{Z}_0 + \hat{X}_1\) sul vettore di stato appena simulato:

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 I circuiti quantistici come tensori

TensorFlow Quantum (TFQ) fornisce tfq.convert_to_tensor , una funzione che converte gli oggetti Cirq in tensori. Ciò ti consente di inviare oggetti Cirq ai nostri livelli quantistici e operazioni quantistiche . La funzione può essere chiamata su elenchi o array di Cirq Circuits e 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'>

Questo codifica gli oggetti Cirq come tensori tf.string che le operazioni tfq decodificano secondo necessità.

# Rank 1 tensor containing 2 Pauli operators.
pauli_tensor = tfq.convert_to_tensor([z0, z0x1])
pauli_tensor.shape
TensorShape([2])

1.3 Simulazione del circuito di dosaggio

TFQ fornisce metodi per calcolare valori di aspettativa, campioni e vettori di stato. Per ora, concentriamoci sui valori delle aspettative .

L'interfaccia di livello più alto per il calcolo dei valori di aspettativa è il livello tfq.layers.Expectation , che è un tf.keras.Layer . Nella sua forma più semplice, questo livello equivale a simulare un circuito parametrizzato su molti cirq.ParamResolvers ; tuttavia, TFQ consente il batch seguendo la semantica TensorFlow e i circuiti vengono simulati utilizzando codice C++ efficiente.

Crea un batch di valori da sostituire ai nostri a b :

batch_vals = np.array(np.random.uniform(0, 2 * np.pi, (5, 2)), dtype=np.float32)

L'esecuzione del circuito in batch sui valori dei parametri in Cirq richiede un ciclo:

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.66652703]
 [ 0.49764055]
 [ 0.67326665]
 [-0.95549959]
 [-0.81297827]]

La stessa operazione è semplificata in 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.666526  ],
       [ 0.49764216],
       [ 0.6732664 ],
       [-0.9554999 ],
       [-0.8129788 ]], dtype=float32)>

2. Ottimizzazione ibrida quantistica-classica

Ora che hai visto le basi, usiamo TensorFlow Quantum per costruire una rete neurale ibrida quantistica-classica . Addestrerai una rete neurale classica per controllare un singolo qubit. Il controllo sarà ottimizzato per preparare correttamente il qubit nello stato 0 o 1 , superando un errore di calibrazione sistematico simulato. Questa figura mostra l'architettura:

Anche senza una rete neurale questo è un problema semplice da risolvere, ma il tema è simile ai veri problemi di controllo quantistico che potresti risolvere usando TFQ. Dimostra un esempio end-to-end di un calcolo quantistico classico utilizzando il tfq.layers.ControlledPQC (Parametrized Quantum Circuit) all'interno di un tf.keras.Model .

Per l'implementazione di questo tutorial, questa architettura è divisa in 3 parti:

  • Il circuito di ingresso o il circuito del punto dati: le prime tre porte \(R\) .
  • Il circuito controllato : le altre tre porte \(R\) .
  • Il controller : la classica rete neurale che imposta i parametri del circuito controllato.

2.1 La definizione del circuito controllato

Definire una rotazione a bit singolo apprendibile, come indicato nella figura sopra. Questo corrisponderà al nostro circuito controllato.

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

svg

2.2 Il controllore

Ora definisci la rete del controller:

# The classical neural network layers.
controller = tf.keras.Sequential([
    tf.keras.layers.Dense(10, activation='elu'),
    tf.keras.layers.Dense(3)
])

Dato un batch di comandi, il controller emette un batch di segnali di controllo per il circuito controllato.

Il controller viene inizializzato in modo casuale, quindi queste uscite non sono ancora utili.

controller(tf.constant([[0.0],[1.0]])).numpy()
array([[0.        , 0.        , 0.        ],
       [0.5815686 , 0.21376055, 0.57181627]], dtype=float32)

2.3 Collegare il controller al circuito

Utilizzare tfq per collegare il controller al circuito controllato, come un unico keras.Model .

Consulta la guida dell'API funzionale Keras per ulteriori informazioni su questo stile di definizione del modello.

Per prima cosa definisci gli input per il modello:

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

Quindi applica le operazioni a quegli input, per definire il calcolo.

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

Ora impacchetta questo calcolo come tf.keras.Model :

# The full Keras model is built from our layers.
model = tf.keras.Model(inputs=[circuits_input, commands_input],
                       outputs=expectation)

L'architettura di rete è indicata dal grafico del modello sottostante. Confronta questo grafico modello con il diagramma dell'architettura per verificarne la correttezza.

tf.keras.utils.plot_model(model, show_shapes=True, dpi=70)

png

Questo modello accetta due input: i comandi per il controller e il circuito di input la cui uscita il controller sta tentando di correggere.

2.4 Il set di dati

Il modello tenta di emettere il valore di misurazione corretto corretto di \(\hat{Z}\) per ogni comando. I comandi ei valori corretti sono definiti di seguito.

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

Questo non è l'intero set di dati di addestramento per questa attività. Ogni punto dati nel set di dati necessita anche di un circuito di ingresso.

2.4 Definizione del circuito di ingresso

Il circuito di ingresso di seguito definisce l'errata calibrazione casuale che il modello imparerà a correggere.

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

Ci sono due copie del circuito, una per ogni datapoint.

datapoint_circuits.shape
TensorShape([2])

2.5 Formazione

Con gli input definiti è possibile eseguire il test del modello tfq .

model([datapoint_circuits, commands]).numpy()
array([[0.95853525],
       [0.6272128 ]], dtype=float32)

Ora esegui un processo di addestramento standard per regolare questi valori verso gli 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()

png

Da questa trama si può vedere che la rete neurale ha imparato a superare la sistematica errata calibrazione.

2.6 Verificare le uscite

Ora usa il modello addestrato, per correggere gli errori di calibrazione dei qubit. Con Circo:

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: [-0.6788422   0.3395225  -0.59394693]
Which gives an actual expectation of: 0.9171845316886902

For a desired output (expectation) of [-1.] with noisy preparation, the controller
network found the following values for theta: [-5.203663   -0.29528576  3.2887425 ]
Which gives an actual expectation of: -0.9511058330535889

Il valore della funzione di perdita durante l'allenamento fornisce un'idea approssimativa di quanto bene stia imparando il modello. Minore è la perdita, più vicini sono i valori di aspettativa nella cella sopra a desired_values . Se non sei così interessato ai valori dei parametri, puoi sempre controllare gli output dall'alto usando tfq :

model([datapoint_circuits, commands])
<tf.Tensor: shape=(2, 1), dtype=float32, numpy=
array([[ 0.91718477],
       [-0.9511056 ]], dtype=float32)>

3 Imparare a preparare autostati di diversi operatori

La scelta degli autostati \(\pm \hat{Z}\) corrispondenti a 1 e 0 era arbitraria. Avresti potuto facilmente desiderare che 1 corrispondesse all'autostato \(+ \hat{Z}\) e 0 all'autostato \(-\hat{X}\) . Un modo per ottenere ciò è specificare un operatore di misurazione diverso per ciascun comando, come indicato nella figura seguente:

Ciò richiede l'uso di tfq.layers.Expectation . Ora il tuo input è cresciuto fino a includere tre oggetti: circuito, comando e operatore. L'output è ancora il valore atteso.

3.1 Nuova definizione del modello

Diamo un'occhiata al modello per svolgere questo compito:

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

Ecco la rete del controller:

# Define classical NN.
controller = tf.keras.Sequential([
    tf.keras.layers.Dense(10, activation='elu'),
    tf.keras.layers.Dense(3)
])

Combina il circuito e il controller in un unico keras.Model usando 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 Il set di dati

Ora includerai anche gli operatori che desideri misurare per ogni punto dati fornito per 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 Formazione

Ora che hai i tuoi nuovi input e output puoi allenarti ancora una volta usando 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 320ms/step - loss: 2.4404
Epoch 2/30
1/1 [==============================] - 0s 3ms/step - loss: 1.8713
Epoch 3/30
1/1 [==============================] - 0s 3ms/step - loss: 1.1400
Epoch 4/30
1/1 [==============================] - 0s 3ms/step - loss: 0.5071
Epoch 5/30
1/1 [==============================] - 0s 3ms/step - loss: 0.1611
Epoch 6/30
1/1 [==============================] - 0s 3ms/step - loss: 0.0426
Epoch 7/30
1/1 [==============================] - 0s 3ms/step - loss: 0.0117
Epoch 8/30
1/1 [==============================] - 0s 3ms/step - loss: 0.0032
Epoch 9/30
1/1 [==============================] - 0s 2ms/step - loss: 0.0147
Epoch 10/30
1/1 [==============================] - 0s 3ms/step - loss: 0.0452
Epoch 11/30
1/1 [==============================] - 0s 3ms/step - loss: 0.0670
Epoch 12/30
1/1 [==============================] - 0s 3ms/step - loss: 0.0648
Epoch 13/30
1/1 [==============================] - 0s 3ms/step - loss: 0.0471
Epoch 14/30
1/1 [==============================] - 0s 3ms/step - loss: 0.0289
Epoch 15/30
1/1 [==============================] - 0s 3ms/step - loss: 0.0180
Epoch 16/30
1/1 [==============================] - 0s 3ms/step - loss: 0.0138
Epoch 17/30
1/1 [==============================] - 0s 2ms/step - loss: 0.0130
Epoch 18/30
1/1 [==============================] - 0s 3ms/step - loss: 0.0137
Epoch 19/30
1/1 [==============================] - 0s 3ms/step - loss: 0.0148
Epoch 20/30
1/1 [==============================] - 0s 3ms/step - loss: 0.0156
Epoch 21/30
1/1 [==============================] - 0s 3ms/step - loss: 0.0157
Epoch 22/30
1/1 [==============================] - 0s 3ms/step - loss: 0.0149
Epoch 23/30
1/1 [==============================] - 0s 3ms/step - loss: 0.0135
Epoch 24/30
1/1 [==============================] - 0s 3ms/step - loss: 0.0119
Epoch 25/30
1/1 [==============================] - 0s 3ms/step - loss: 0.0100
Epoch 26/30
1/1 [==============================] - 0s 3ms/step - loss: 0.0082
Epoch 27/30
1/1 [==============================] - 0s 3ms/step - loss: 0.0064
Epoch 28/30
1/1 [==============================] - 0s 3ms/step - loss: 0.0047
Epoch 29/30
1/1 [==============================] - 0s 3ms/step - loss: 0.0034
Epoch 30/30
1/1 [==============================] - 0s 3ms/step - loss: 0.0024
plt.plot(history.history['loss'])
plt.title("Learning to Control a Qubit")
plt.xlabel("Iterations")
plt.ylabel("Error in Control")
plt.show()

png

La funzione di perdita è scesa a zero.

Il controller è disponibile come modello autonomo. Chiamare il controller e verificarne la risposta a ciascun segnale di comando. Ci vorrebbe del lavoro per confrontare correttamente questi output con il contenuto di random_rotations .

controller.predict(np.array([0,1]))
array([[3.6335812 , 1.8470774 , 0.71675825],
       [5.3085413 , 0.08116499, 2.8337662 ]], dtype=float32)