from qiskit.circuit import QuantumCircuit, Parameter
= Parameter('angle') # niezdefiniowany parametr
theta
= QuantumCircuit(2)
qc 0)
qc.rz(theta, 0, 1)
qc.crz(theta, 'mpl') qc.draw(
Parameterized Quantum Circuit
Parametryzowane algorytmy kwantowe, czyli takie w których realizujemy obwody przez bramki parametryzowane (liczbami) są podstawowym budulcem algorytmów kwantowego uczenia maszynowego. Bardzo często mozna się spotkać z innymi nazwami: parameterized trial states
, variational forms
, lub ansatzes
.
Ponizszy przykład przedstawia obwód z dwoma bramkami parametryzowanymi jedną liczbą \(\theta\). Do oznaczenia parametru wykorzystano obiekt Parameter
.
print(qc.parameters)
ParameterView([Parameter(angle)])
Jezeli chcemy zastosować wiele parametrów dla róznych bramek mozemy uzyc klasy kilku obiektów na podstawie klasy Parameters
lub zastosować klasę ParameterVector
.
from qiskit.circuit import ParameterVector
= ParameterVector('theta', length=2)
theta_list
= QuantumCircuit(2)
qc 0], 0)
qc.rz(theta_list[1], 0, 1)
qc.crz(theta_list['mpl') qc.draw(
qc.parameters
ParameterView([ParameterVectorElement(theta[0]), ParameterVectorElement(theta[1])])
Poniewaz wszystkie bramki kwantowe uzywane w obwodach są unitarne, parametryzowany obwód równiez moze być opisany jako unitarna operacja wykonywana na n kubitach \(U_{\theta}\) działająca na pewien stan początkowy \(|\phi_0\rangle\). \[ |\psi_{\theta}\rangle = U_{\theta} |\phi_0\rangle \]
- Skąd wziąć informacje jak powinny wyglądać obwody w zagadnieniach kwantowego uczenia maszynowego?
- Jak wybrać postać obwodu parametryzowanego?
- Jak realizować PQC jako model uczenia maszynowego?
W ogólności realizowane jest to w formie testowej sprwadz publikację.
Przetestujmy dwa parametryczne obwody i zobaczmy jakie mozliwosci kodowania stanów one reprezentują.
Expressibility and entangling capability
Expressibility
(zdolność do generowania stanów w przestrzeni Hilberta) oraz entangling capability
(mozliwość splątania kubitów) to dwie miary pozwalające porównać rózne PQC.
import numpy as np
import matplotlib.pyplot as plt
# First, we need to define the circuits:
= Parameter('θ')
theta_param = Parameter('Φ')
phi_param
# Circuit A
= QuantumCircuit(1)
qc_A 0)
qc_A.h(0)
qc_A.rz(theta_param, 'mpl'))
display(qc_A.draw(# Circuit B
= QuantumCircuit(1)
qc_B 0)
qc_B.h(0)
qc_B.rz(theta_param, 0)
qc_B.rx(phi_param, 'mpl')) display(qc_B.draw(
# Next we uniformly sample the parameter space for the two parameters theta and phi
0)
np.random.seed(= 1000
num_param = [2*np.pi*np.random.uniform() for i in range(num_param)]
theta = [2*np.pi*np.random.uniform() for i in range(num_param)]
phi
# Then we take the parameter value lists, build the state vectors corresponding
# to each circuit, and plot them on the Bloch sphere:
from qiskit.visualization.bloch import Bloch
from qiskit.quantum_info import Statevector
def state_to_bloch(state_vec):
# Converts state vectors to points on the Bloch sphere
= np.angle(state_vec.data[1])-np.angle(state_vec.data[0])
phi = 2*np.arccos(np.abs(state_vec.data[0]))
theta return [np.sin(theta)*np.cos(phi),np.sin(theta)*np.sin(phi),np.cos(theta)]
# Bloch sphere plot formatting
= plt.figaspect(1/2)
width, height =plt.figure(figsize=(width, height))
fig= fig.add_subplot(1, 2, 1, projection='3d'), fig.add_subplot(1, 2, 2, projection='3d')
ax1, ax2 = Bloch(axes=ax1), Bloch(axes=ax2)
b1,b2 = ['tab:blue'],['tab:blue']
b1.point_color, b2.point_color = ['o'],['o']
b1.point_marker, b2.point_marker=[2],[2]
b1.point_size, b2.point_size
# Calculate state vectors for circuit A and circuit B for each set of sampled parameters
# and add to their respective Bloch sphere
for i in range(num_param):
=Statevector.from_instruction(qc_A.bind_parameters({theta_param:theta[i]}))
state_1=Statevector.from_instruction(qc_B.bind_parameters({theta_param:theta[i], phi_param:phi[i]}))
state_2
b1.add_points(state_to_bloch(state_1))
b2.add_points(state_to_bloch(state_2))
b1.show() b2.show()
Biorąc pod uwagę otrzymane wyniki widać, ze pierwszy obwód ma małe expressibility natomiast drugi duze (w drugim przypadku jednak rozkład nie jest jednorodny).
Entangling capability
Drugą, wazną cechą obwodów jest mozliwosc wykorzystania splątania. Aby zmierzyć jak bardzo stany są splątane mozemy uzyć miary Meyer’a-Wallach’a. Dla stanów separowalnych miara ta przyjmuje wartość \(0\), natomiast dla stanów Bella \(1\).
from qiskit import QuantumRegister, QuantumCircuit
= QuantumRegister(4, 'q')
q1 = QuantumCircuit(q1)
c1 0,[0,1,2,3])
c1.rz(0,q1)
c1.rx('mpl') c1.draw(
Ten obwód nie ma operacji wprowadzających stan splątany - brak bramek dwukubitowych. Miara M-W = 0.
= QuantumRegister(4, 'q')
q2 = QuantumCircuit(q1)
c2 0,[0,1,2,3])
c2.rz(0,q1)
c2.rx(0,1)
c2.cx(2,3)
c2.cx(1,2)
c2.cx(
'mpl') c2.draw(
W tym przypadku istnieją bramki dwukubitowe wprowadzające stany splątane dlatego miara M-W będzie większa od 0. Więcej info tutaj
PQC dla ML
W QML parametryzowane obwody są uzywane do dwóch głównych zadań.
- Zakodowanie klasycznych danych na układ kwantowy - dane przekładane są na parametry kątów
- Jako model kwantowy gdzie parametry ustalane są z wykorzystaniem klasycznego optymalizatora
Kodowanie danych - Quantum Feature Map
Aby zrealizować modele uczenia maszynowego na klasycznych danych z wykorzystaniem PQC musimy wykonać i wybrać kilka operacji pozwalających operować na naszych danych za pomocą obwodów kwantowych.
Zadanie to często nazywane jest reprezentacją danych klasycznych w pewnej przestrzeni Hilberta, a więc moze być nazywane embeddingiem danych. Tak jak ma to miejsce dla danych grafowych czy tez w sytuacjii gdy stosujemy rózne metody zmieniające postać danych np. kernel methods w SVM.
W przypadku obwodów kwantowych ich działanie opiera się na przetworzeniu początkowego stanu kwantowego wyrazanego jako iloczyn tensorowy kubitów. Dlatego naturalnym sposobem jest przedstawienie danych jako wektor w pewnej przestrzeni Hilberta.
Tak przygotowany i sparametryzowany stan mozna wykorzystać np w modelach typu Variational Quantum Circuit
, gdzie oprócz stanu budujemy kolejny PQC, tym razem realizujący parametry naszego modelu jako parametry bramek kwantowych. Całość optymalizowana jest za pomocą róznego rodzaju optymalizatorów.
Nasze zadanie sprowadzić mozna do pobrania klasycznego punktu danych \(\vec{x}\) oraz zakodowania go za pomocą bramek (parametryzowanych) w obwodzie kwantowym. \[ x_i \to |\phi(x_i)\rangle \]
QFM (Quantum Feature Map) \(V(\Phi(\vec{x}))\) przetwarza klasyczne dane na dane kwantowe.
\(\Phi(.)\) to klasyczna funkcja zastosowana do klasycznych danych.
\(V\) - to parametryzowany obwód zmieniający dane klasyczne na kwantowe.
Trzy podstawowe czynniki związane z wyborem feature map
:
- głębokość obwodu - sekcja bramek kodujących moze być realizowana wiele razy (poprzez parametr
reps
) - klasyczna funkcja modyfikująca i kodująca klasyczne dane nadające się do uzytku w obwodach kwantowych
- zbiór bramek kwantowych
ZFeatureMap
The first order Pauli Z-evolution circuit. Documentacja
\[ \Phi_S \colon x \to x_i \] Otrzymany obwód nie zawiera oddziaływania między zmiennymi (zakodowanych danych), co wiąze się z brakiem wykorzystania efektu splątania.
from qiskit.circuit.library import ZFeatureMap
= ZFeatureMap(feature_dimension=3, reps=1)
qc_z 'mpl') qc_z.draw(
'mpl') qc_z.decompose().draw(
ZZFeatureMap
Second-order Pauli-Z evolution circuit. Documentacja
from qiskit.circuit.library import ZZFeatureMap
= ZZFeatureMap(feature_dimension=3, reps=1,
qc_zz ='full',
entanglement=True)
insert_barriers'mpl') qc_zz.draw(
'mpl') qc_zz.decompose().draw(
= ZZFeatureMap(feature_dimension=3, reps=1, entanglement='linear',insert_barriers=True)
qc_zz 'mpl') qc_zz.draw(
'mpl') qc_zz.decompose().draw(
Pauli Feature Map
from qiskit.circuit.library import PauliFeatureMap
= PauliFeatureMap(feature_dimension=3, reps=1, paulis = ['Z','ZZ','ZY'])
qc_p 'mpl') qc_p.draw(
'mpl') qc_p.decompose().draw(
Zaobaczmy jakiemu kodowaniu odpowiada PauliFeatureMap
dla operatorów Z
i ZZ
.
= PauliFeatureMap(feature_dimension=3, reps=1, paulis = ['Z','ZZ'])
qc_p 'mpl'))
display(qc_p.draw('mpl') qc_p.decompose().draw(
Jeśli mamy juz określony sposób kodowania danych, komputer kwantowy moze je przeanalizować w odpowiedniej przestrzeni Hilberta i np znaleźć hiperplaszczyzne dla procesu klasyfikacji.
Model Circuit
Kolejnym elementem jest obwód realizujący model. Tutaj równiez istnieje wiele mozliwych implementacji. W ogólności tworzymy parametryzowany operator unitarny \(U(w)\) dla którego: \[ |\psi(x:\theta)\rangle = U(w)|\psi(x)\rangle \]
Jednym z przykładowych modeli w bibliotece Qiskit jest obwód realizowany jako RealAmplitudes
. Ilośc parametrów modelu moze być ustalana za pomocą głębokości obwodu - czyli ile razy powtórzymy dany schemat.
from qiskit.circuit.library import TwoLocal
= TwoLocal(num_qubits=3, reps=2, rotation_blocks=['ry','rz'],
qc_twolocal ='cz', skip_final_rotation_layer=True,
entanglement_blocks=True)
insert_barriers
'mpl') qc_twolocal.decompose().draw(
= TwoLocal(3, rotation_blocks='ry',
qc_13 ='crz', entanglement='sca',
entanglement_blocks=3, skip_final_rotation_layer=True,
reps=True)
insert_barriers
'mpl') qc_13.decompose().draw(
NLocal
from qiskit import QuantumCircuit
from qiskit.circuit import ParameterVector
from qiskit.circuit.library import NLocal
# rotation block:
= QuantumCircuit(2)
rot = ParameterVector('r', 2)
params 0], 0)
rot.ry(params[1], 1)
rot.rz(params[
# entanglement block:
= QuantumCircuit(4)
ent = ParameterVector('e', 3)
params 0], 0, 1)
ent.crx(params[1], 1, 2)
ent.crx(params[2], 2, 3)
ent.crx(params[
= NLocal(num_qubits=6, rotation_blocks=rot,
qc_nlocal =ent, entanglement='linear',
entanglement_blocks=True, insert_barriers=True)
skip_final_rotation_layer
'mpl') qc_nlocal.decompose().draw(
Optymalizatory
Zainstaluj bibliotekę qiskit-algorithms pip install qiskit-algorithms
.
from qiskit-algorithms.optimizers import COBYLA
- COBYLA (Constrained Optimization By Linear Approximation optimizer)
- SPSA (Simultaneous Perturbation Sochastic Approximation optimizer)
- SLSQP (Sequential Least Squares Programming optimizer)
Przykład Klasyfikacji danych
from qiskit.utils import algorithm_globals
= 42
algorithm_globals.random_seed
import numpy as np
np.random.seed(algorithm_globals.random_seed)
# Tworzymy zbiór danych
from qiskit_machine_learning.datasets import ad_hoc_data
= (
TRAIN_DATA, TRAIN_LABELS, TEST_DATA, TEST_LABELS =20,
ad_hoc_data(training_size=5,
test_size=2,
n=0.3,
gap=False)
one_hot )
len(TRAIN_DATA), len(TRAIN_LABELS), len(TEST_DATA), len(TEST_LABELS)
(40, 40, 10, 10)
0], TRAIN_LABELS[0] TRAIN_DATA[
(array([4.90088454, 4.1469023 ]), 0)
Do zakodowania danych w stanie kwantowym uzyjemy: ZZFeatureMap o głębokości 2.
from qiskit.circuit.library import ZZFeatureMap
= ZZFeatureMap(feature_dimension=2, reps=2) FEATURE_MAP
Jako model wykorzystamy TwoLocal z bramkami ry rz i cz do splątania.
from qiskit.circuit.library import TwoLocal
= TwoLocal(2, ['ry', 'rz'], 'cz', reps=2)
VAR_FORM
= FEATURE_MAP.compose(VAR_FORM)
AD_HOC_CIRCUIT
AD_HOC_CIRCUIT.measure_all()'mpl') AD_HOC_CIRCUIT.decompose().draw(
def circuit_instance(data, variational):
"""Assigns parameter values to `AD_HOC_CIRCUIT`.
Args:
data (list): Data values for the feature map
variational (list): Parameter values for `VAR_FORM`
Returns:
QuantumCircuit: `AD_HOC_CIRCUIT` with parameters assigned
"""
= {}
parameters for i, p in enumerate(FEATURE_MAP.ordered_parameters):
= data[i]
parameters[p] for i, p in enumerate(VAR_FORM.ordered_parameters):
= variational[i]
parameters[p] return AD_HOC_CIRCUIT.assign_parameters(parameters)
poniewaz wynikami są bitstringi musimy podać ich interpretacje i przeliczać je na klasę rozwiązań. Jednym z przykładowych rozwiązań jest wykorzystanie funkcji parity.
def parity(bitstring):
"""Returns 1 if parity of `bitstring` is even, otherwise 0."""
= sum(int(k) for k in list(bitstring))
hamming_weight return (hamming_weight+1) % 2
Pomocniczo zdefiniujemy funkcję obliczającą prawdopodobieństwo (częstotliwość) dla danej klasy wynikowej. Nasz obwód będzie uruchamiany wiele razy, dzięki czemu uzyskamy zliczenia w róznych eksperymentach.
def label_probability(results):
"""Converts a dict of bitstrings and their counts,
to parities and their counts"""
= sum(results.values())
shots = {0: 0, 1: 0}
probabilities for bitstring, counts in results.items():
= parity(bitstring)
label += counts / shots
probabilities[label] return probabilities
Posiadając powyzsze elementy mozemy zdefiniować funkcję realizującą klasyfikację.
from qiskit import BasicAer, execute
def classification_probability(data, variational):
"""Classify data points using given parameters.
Args:
data (list): Set of data points to classify
variational (list): Parameters for `VAR_FORM`
Returns:
list[dict]: Probability of circuit classifying
each data point as 0 or 1.
"""
= [circuit_instance(d, variational) for d in data]
circuits = BasicAer.get_backend('qasm_simulator')
backend = execute(circuits, backend).result()
results = [
classification for c in circuits]
label_probability(results.get_counts(c)) return classification
Poniewaz będziemy chcieli trenować model będziemy potrzebowali zdefiniować funkcję straty i kosztu
def cross_entropy_loss(classification, expected):
"""Calculate accuracy of predictions using cross entropy loss.
Args:
classification (dict): Dict where keys are possible classes,
and values are the probability our
circuit chooses that class.
expected (int): Correct classification of the data point.
Returns:
float: Cross entropy loss
"""
= classification.get(expected) # Prob. of correct classification
p return -np.log(p + 1e-10)
def cost_function(data, labels, variational):
"""Evaluates performance of our circuit with `variational`
parameters on `data`.
Args:
data (list): List of data points to classify
labels (list): List of correct labels for each data point
variational (list): Parameters to use in circuit
Returns:
float: Cost (metric of performance)
"""
= classification_probability(data, variational)
classifications = 0
cost for i, classification in enumerate(classifications):
+= cross_entropy_loss(classification, labels[i])
cost /= len(data)
cost return cost
Mozemy teraz przypisac optymalizator
class OptimizerLog:
"""Log to store optimizer's intermediate results"""
def __init__(self):
self.evaluations = []
self.parameters = []
self.costs = []
def update(self, evaluation, parameter, cost, _stepsize, _accept):
"""Save intermediate results. Optimizer passes five values
but we ignore the last two."""
self.evaluations.append(evaluation)
self.parameters.append(parameter)
self.costs.append(cost)
# Set up the optimization
from qiskit_algorithms.optimizers import SPSA
= OptimizerLog()
log = SPSA(maxiter=100, callback=log.update) optimizer
Losujemy parametry początkowe, i wskazujemy optymalizowaną funkcję
= np.random.random(VAR_FORM.num_parameters)
initial_point def objective_function(variational):
"""Cost function of circuit parameters on training data.
The optimizer will attempt to minimize this."""
return cost_function(TRAIN_DATA, TRAIN_LABELS, variational)
uruchamiamy całą procedurę
# Run the optimization
= optimizer.minimize(objective_function, initial_point)
result
= result.x
opt_var = result.fun
opt_value
import matplotlib.pyplot as plt
= plt.figure()
fig
plt.plot(log.evaluations, log.costs)'Steps')
plt.xlabel('Cost')
plt.ylabel( plt.show()
Dla zbioru testowego mozemy sprawdzic jakosc przewidywań
def test_classifier(data, labels, variational):
"""Gets classifier's most likely predictions and accuracy of those
predictions.
Args:
data (list): List of data points to classify
labels (list): List of correct labels for each data point
variational (list): List of parameter values for classifier
Returns:
float: Average accuracy of classifier over `data`
list: Classifier's label predictions for each data point
"""
= classification_probability(data, variational)
probability = [0 if p[0] >= p[1] else 1 for p in probability]
predictions = 0
accuracy # pylint: disable=invalid-name
for i, prediction in enumerate(predictions):
if prediction == labels[i]:
+= 1
accuracy /= len(labels)
accuracy return accuracy, predictions
= test_classifier(TEST_DATA, TEST_LABELS, opt_var)
accuracy, predictions accuracy
1.0
from matplotlib.lines import Line2D
=(9, 6))
plt.figure(figsize
for feature, label in zip(TRAIN_DATA, TRAIN_LABELS):
= 'C0' if label == 0 else 'C1'
COLOR 0], feature[1],
plt.scatter(feature[='o', s=100, color=COLOR)
marker
for feature, label, pred in zip(TEST_DATA, TEST_LABELS, predictions):
= 'C0' if pred == 0 else 'C1'
COLOR 0], feature[1],
plt.scatter(feature[='s', s=100, color=COLOR)
markerif label != pred: # mark wrongly classified
0], feature[1], marker='o', s=500,
plt.scatter(feature[=2.5, facecolor='none', edgecolor='C3')
linewidths
= [
legend_elements 0], [0], marker='o', c='w', mfc='C1', label='A', ms=10),
Line2D([0], [0], marker='o', c='w', mfc='C0', label='B', ms=10),
Line2D([0], [0], marker='s', c='w', mfc='C1', label='predict A',
Line2D([=10),
ms0], [0], marker='s', c='w', mfc='C0', label='predict B',
Line2D([=10),
ms0], [0], marker='o', c='w', mfc='none', mec='C3',
Line2D([='wrongly classified', mew=2, ms=15)
label
]
=legend_elements, bbox_to_anchor=(1, 1),
plt.legend(handles='upper left')
loc
'Training & Test Data')
plt.title('x')
plt.xlabel('y')
plt.ylabel( plt.show()