Olá, muitos mundos

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

Este tutorial mostra como uma rede neural clássica pode aprender a corrigir erros de calibração de qubit. Ele apresenta o Cirq , uma estrutura Python para criar, editar e invocar circuitos Noisy Intermediate Scale Quantum (NISQ) e demonstra como o Cirq faz interface com o TensorFlow Quantum.

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: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. O básico

1.1 Cirq e circuitos quânticos parametrizados

Antes de explorar o TensorFlow Quantum (TFQ), vejamos alguns conceitos básicos do Cirq . Cirq é uma biblioteca Python para computação quântica do Google. Você o usa para definir circuitos, incluindo portas estáticas e parametrizadas.

O Cirq usa símbolos SymPy para representar parâmetros livres.

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

O código a seguir cria um circuito de dois qubits usando seus parâmetros:

# 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

Para avaliar circuitos, você pode usar a interface cirq.Simulator . Você substitui parâmetros livres em um circuito por números específicos passando um objeto cirq.ParamResolver . O código a seguir calcula a saída do vetor de estado bruto do seu circuito parametrizado:

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

Os vetores de estado não são diretamente acessíveis fora da simulação (observe os números complexos na saída acima). Para ser fisicamente realista, você deve especificar uma medida, que converte um vetor de estado em um número real que os computadores clássicos possam entender. Cirq especifica medições usando combinações dos operadores de Pauli \(\hat{X}\), \(\hat{Y}\)e \(\hat{Z}\). Como ilustração, o código a seguir mede \(\hat{Z}_0\) e \(\frac{1}{2}\hat{Z}_0 + \hat{X}_1\) no vetor de estado que você acabou de simular:

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 Circuitos quânticos como tensores

O TensorFlow Quantum (TFQ) fornece tfq.convert_to_tensor , uma função que converte objetos Cirq em tensores. Isso permite que você envie objetos Cirq para nossas camadas quânticas e operações quânticas . A função pode ser chamada em listas ou arrays de 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'>

Isso codifica os objetos Cirq como tensores tf.string que as operações tfq decodificam conforme necessário.

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

1.3 Simulação de circuito de batelada

O TFQ fornece métodos para calcular valores esperados, amostras e vetores de estado. Por enquanto, vamos nos concentrar nos valores esperados .

A interface de nível mais alto para calcular os valores esperados é a camada tfq.layers.Expectation , que é uma tf.keras.Layer . Na sua forma mais simples, esta camada é equivalente a simular um circuito parametrizado sobre muitos cirq.ParamResolvers ; no entanto, o TFQ permite lotes seguindo a semântica do TensorFlow e os circuitos são simulados usando código C++ eficiente.

Crie um lote de valores para substituir nossos parâmetros a e b :

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

A execução do circuito em lote sobre valores de parâmetro no Cirq requer um loop:

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

A mesma operação é simplificada no 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. Otimização clássica quântica híbrida

Agora que você viu o básico, vamos usar o TensorFlow Quantum para construir uma rede neural quântica clássica híbrida . Você treinará uma rede neural clássica para controlar um único qubit. O controle será otimizado para preparar corretamente o qubit no estado 0 ou 1 , superando um erro sistemático de calibração simulado. Esta figura mostra a arquitetura:

Mesmo sem uma rede neural, esse é um problema simples de resolver, mas o tema é semelhante aos problemas reais de controle quântico que você pode resolver usando o TFQ. Ele demonstra um exemplo de ponta a ponta de uma computação quântica clássica usando a tfq.layers.ControlledPQC (Parametrized Quantum Circuit) dentro de um tf.keras.Model .

Para a implementação deste tutorial, esta arquitetura é dividida em 3 partes:

  • O circuito de entrada ou circuito de ponto de dados: As três primeiras portas \(R\) .
  • O circuito controlado : As outras três portas \(R\) .
  • O controlador : A rede neural clássica que define os parâmetros do circuito controlado.

2.1 A definição do circuito controlado

Defina uma rotação de bit único que pode ser aprendida, conforme indicado na figura acima. Isso corresponderá ao nosso circuito controlado.

# 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 O controlador

Agora defina a rede do controlador:

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

Dado um lote de comandos, o controlador emite um lote de sinais de controle para o circuito controlado.

O controlador é inicializado aleatoriamente, portanto, essas saídas ainda não são úteis.

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

2.3 Conecte o controlador ao circuito

Use tfq para conectar o controlador ao circuito controlado, como um único keras.Model .

Consulte o guia Keras Functional API para saber mais sobre esse estilo de definição de modelo.

Primeiro defina as entradas para o modelo:

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

Em seguida, aplique as operações a essas entradas para definir a computação.

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

Agora empacote este cálculo como um tf.keras.Model :

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

A arquitetura da rede é indicada pelo gráfico do modelo abaixo. Compare este gráfico de modelo com o diagrama de arquitetura para verificar a exatidão.

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

png

Este modelo recebe duas entradas: os comandos para o controlador e o circuito de entrada cuja saída o controlador está tentando corrigir.

2.4 O conjunto de dados

O modelo tenta gerar o valor de medição correto correto de \(\hat{Z}\) para cada comando. Os comandos e valores corretos são definidos abaixo.

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

Este não é todo o conjunto de dados de treinamento para esta tarefa. Cada ponto de dados no conjunto de dados também precisa de um circuito de entrada.

2.4 Definição do circuito de entrada

O circuito de entrada abaixo define o erro de calibração aleatório que o modelo aprenderá a corrigir.

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

Existem duas cópias do circuito, uma para cada ponto de dados.

datapoint_circuits.shape
TensorShape([2])

2.5 Treinamento

Com as entradas definidas, você pode testar o modelo tfq .

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

Agora execute um processo de treinamento padrão para ajustar esses valores para as 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

A partir desse gráfico, você pode ver que a rede neural aprendeu a superar o erro de calibração sistemático.

2.6 Verifique as saídas

Agora use o modelo treinado, para corrigir os erros de calibração do qubit. Com 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: [-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

O valor da função de perda durante o treinamento fornece uma ideia aproximada de quão bem o modelo está aprendendo. Quanto menor a perda, mais próximos os valores esperados na célula acima estarão dos desired_values . Se você não estiver tão preocupado com os valores dos parâmetros, sempre poderá verificar as saídas acima usando tfq :

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

3 Aprendendo a preparar autoestados de diferentes operadores

A escolha dos \(\pm \hat{Z}\) correspondentes a 1 e 0 foi arbitrária. Você poderia facilmente querer que 1 correspondesse ao \(+ \hat{Z}\) e 0 correspondesse ao \(-\hat{X}\) . Uma maneira de fazer isso é especificando um operador de medição diferente para cada comando, conforme indicado na figura abaixo:

Isso requer o uso de tfq.layers.Expectation . Agora sua entrada cresceu para incluir três objetos: circuito, comando e operador. A saída ainda é o valor esperado.

3.1 Nova definição de modelo

Vamos dar uma olhada no modelo para realizar esta tarefa:

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

Aqui está a rede do controlador:

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

Combine o circuito e o controlador em um único 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 O conjunto de dados

Agora você também incluirá os operadores que deseja medir para cada ponto de dados fornecido para 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 Treinamento

Agora que você tem suas novas entradas e saídas, você pode treinar novamente 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

A função de perda caiu para zero.

O controller está disponível como um modelo autônomo. Chame o controlador e verifique sua resposta a cada sinal de comando. Levaria algum trabalho para comparar corretamente essas saídas com o conteúdo de random_rotations .

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