주 콘텐츠로 건너뛰기

양자 커널

양자 커널 소개

"양자 커널 방법"이란 양자 컴퓨터를 사용하여 커널을 추정하는 모든 방법을 가리킵니다. 여기서 "커널"은 커널 행렬 또는 그 개별 원소를 의미합니다. 특징 맵(feature mapping) Φ(x)\Phi(\vec{x})xRd\vec{x}\in \mathbb{R}^dΦ(x)Rd\Phi(\vec{x})\in \mathbb{R}^{d'}로 매핑하는 함수이며, 일반적으로 d>dd'>d이고, 이 매핑의 목표는 데이터의 범주를 초평면으로 분리 가능하게 만드는 것입니다. 커널 함수는 특징 맵이 적용된 공간의 벡터를 인수로 받아 내적을 반환합니다. 즉, K:Rd×RdRK:\mathbb{R}^d\times\mathbb{R}^d\rightarrow \mathbb{R}이며 K(x,y)=Φ(x)Φ(y)K(x,y) = \langle \Phi(x)|\Phi(y)\rangle입니다. 고전적 관점에서는 커널 함수를 쉽게 계산할 수 있는 특징 맵에 관심을 갖습니다. 이는 종종 Φ(x)\Phi(x)Φ(y)\Phi(y)를 직접 구성하지 않고도, 원래 데이터 벡터만으로 특징 맵 공간에서의 내적을 표현할 수 있는 커널 함수를 찾는 것을 의미합니다. 양자 커널 방법에서는 특징 맵이 양자 Circuit에 의해 수행되며, 커널은 해당 Circuit의 측정과 상대적 측정 확률을 사용하여 추정합니다.

이 레슨에서는 상당한 얽힘(entanglement)을 사용하는 사전 코딩된 인코딩 Circuit의 깊이와, 직접 코딩한 Circuit의 깊이를 비교해 살펴볼 것입니다. 이는 어느 방법이 더 낫다고 주장하려는 것이 아닙니다. 사전 코딩된 Circuit이 너무 깊거나, 직접 구축한 Circuit의 얽힘이 충분하지 않을 수도 있습니다. 이 두 방법은 단지 여러분이 탐구할 수 있도록 제시하는 것입니다.

커널 행렬 추정 과정을 자세히 살펴보기 전에, Qiskit 패턴의 언어를 사용하여 전체 워크플로를 개략적으로 설명하겠습니다.

1단계: 고전적 입력을 양자 문제로 변환

  • 입력: 훈련 데이터셋
  • 출력: 커널 행렬 원소를 계산하기 위한 추상 Circuit

데이터셋이 주어지면, 출발점은 데이터를 양자 Circuit으로 인코딩하는 것입니다. 다시 말해, 데이터를 양자 컴퓨터 상태의 힐베르트 공간으로 매핑해야 합니다. 이를 위해 데이터에 의존하는 Circuit을 구성합니다. 이 방법은 다양하며, 이전 레슨에서 여러 옵션을 다루었습니다. 데이터를 인코딩하는 Circuit을 직접 구성하거나, zz_feature_map과 같은 사전 제작된 특징 맵을 사용할 수 있습니다. 이 레슨에서는 두 가지 방법을 모두 사용합니다.

단일 커널 행렬 원소를 계산하기 위해서는 두 개의 서로 다른 점을 인코딩해야 내적을 추정할 수 있다는 점에 유의하세요. 전체 양자 커널 워크플로는 물론 매핑된 데이터 벡터 간의 많은 내적과 고전적 머신러닝 방법을 포함합니다. 하지만 반복되는 핵심 단계는 단일 커널 행렬 원소의 추정입니다. 이를 위해 데이터 의존적 양자 Circuit을 선택하고 두 데이터 벡터를 특징 공간으로 매핑합니다.

Classical_Review_background_kernel_circuit

커널 행렬을 생성하는 작업에서 특히 관심을 갖는 것은 모든 NN개의 Qubit가 0|0\rangle 상태에 있는 0N|0\rangle^{\otimes N} 상태를 측정할 확률입니다. 이를 이해하기 위해, 하나의 데이터 벡터 xi\vec{x}_i를 인코딩하고 매핑하는 Circuit을 Φ(xi)\Phi(\vec{x}_i), xj\vec{x}_j를 인코딩하고 매핑하는 Circuit을 Φ(xj)\Phi(\vec{x}_j)로 나타내고, 매핑된 상태를 다음과 같이 정의합니다.

ψ(xi)=Φ(xi)0N|\psi(\vec{x}_i)\rangle = \Phi(\vec{x}_i)|0\rangle^{\otimes N} ψ(xj)=Φ(xj)0N.|\psi(\vec{x}_j)\rangle = \Phi(\vec{x}_j)|0\rangle^{\otimes N}.

이 상태들이 바로 데이터를 고차원으로 매핑한 것이므로, 우리가 원하는 커널 원소는 내적

ψ(xj)ψ(xi)=0NΦ(xj)Φ(xi)0N\langle\psi(\vec{x}_j)|\psi(\vec{x}_i)\rangle = \langle 0 |^{\otimes N}\Phi^\dagger(\vec{x}_j)\Phi(\vec{x}_i)|0\rangle^{\otimes N}

입니다. 기본 초기 상태 0N|0\rangle^{\otimes N}에 두 Circuit Φ(xj)\Phi^\dagger(\vec{x}_j)Φ(xi)\Phi(\vec{x}_i)를 작용시킨 후, 상태 0N|0\rangle^{\otimes N}을 측정할 확률은

P0=0NΦ(xj)Φ(xi)0N2P_0 = |\langle0|^{\otimes N}\Phi^\dagger(\vec{x}_j)\Phi(\vec{x}_i)|0\rangle^{\otimes N}|^2

입니다. 이것이 바로 우리가 원하는 값(2||^2까지)입니다. Circuit의 측정 레이어는 측정 확률(또는 특정 오류 완화 방법이 사용되는 경우 이른바 "준확률")을 반환합니다. 우리가 관심을 갖는 확률은 영 상태, 0N|0\rangle^{\otimes N}의 확률입니다.

2단계: 양자 실행을 위한 문제 최적화

  • 입력: 특정 Backend에 최적화되지 않은 추상 Circuit
  • 출력: 선택한 QPU에 최적화된 대상 Circuit과 관측량

이 단계에서는 Qiskit의 generate_preset_pass_manager 함수를 사용하여, 실험을 실행할 실제 양자 컴퓨터에 맞게 Circuit 최적화 루틴을 지정합니다. optimization_level=3으로 설정하면 가장 높은 수준의 최적화를 제공하는 사전 설정 패스 매니저를 사용하게 됩니다. 여기서 "최적화"란 실제 양자 컴퓨터에서 Circuit 구현을 최적화하는 것을 의미합니다. 여기에는 추상 양자 Circuit의 Qubit에 대응하는 물리적 Qubit를 선택하여 Gate 깊이를 최소화하거나, 가장 낮은 오류율을 가진 물리적 Qubit를 선택하는 것 등이 포함됩니다. 이것은 머신러닝 문제의 최적화(COBYLA와 같은 고전적 최적화기에서의 최적화)와는 직접적인 관련이 없습니다.

2단계를 어떻게 구현하느냐에 따라, 행렬 원소에 관여하는 각 점 쌍이 서로 다른 Circuit을 생성하므로 Circuit을 두 번 이상 최적화해야 할 수도 있습니다.

3단계: Qiskit Runtime Primitives를 사용한 실행

  • 입력: 대상 Circuit
  • 출력: 확률 분포

Qiskit Runtime의 Sampler 프리미티브를 사용하여 Circuit을 샘플링한 상태의 확률 분포를 재구성합니다. 이것이 "준확률 분포"로 불리는 경우가 있는데, 이 용어는 잡음이 문제가 되고 오류 완화와 같은 추가 단계가 도입될 때 적용됩니다. 이러한 경우, 모든 확률의 합이 정확히 1이 되지 않을 수 있으므로 "준확률"이라고 합니다.

4단계: 후처리 및 고전적 형식으로 결과 반환

  • 입력: 확률 분포
  • 출력: 단일 커널 행렬 원소, 또는 반복 시 커널 행렬

양자 Circuit에서 0N|0\rangle^{\otimes N}을 측정할 확률을 계산하고, 사용된 두 데이터 벡터에 해당하는 위치에 커널 행렬을 채웁니다. 전체 커널 행렬을 채우려면 각 원소마다 양자 실험을 수행해야 합니다. 커널 행렬이 완성되면, pre-calculated kernels를 허용하는 많은 고전적 머신러닝 알고리즘에서 이를 사용할 수 있습니다. 예를 들어 qml_svc = SVC(kernel="precomputed")와 같이 사용합니다. 이후 고전적 워크스트림을 사용하여 테스트 데이터에 모델을 적용하고 정확도 점수를 얻을 수 있습니다. 정확도 점수에 만족하지 못한다면 특징 맵 등 계산의 여러 측면을 재검토해야 할 수 있습니다.

레슨 개요

이 레슨에서는 실제 양자 컴퓨터에서의 시간을 최대한 활용할 수 있도록 이러한 단계들을 여러 방식으로 수행합니다. 양자 커널 방법을 다음과 같이 적용할 것입니다.

  • 비교적 적은 수의 특징을 가진 데이터에 대해 단일 커널 행렬 원소를 실제 Backend에서 계산합니다. 각 단계에서 일어나는 일을 쉽게 따라갈 수 있습니다.
  • 비교적 적은 수의 특징을 가진 전체 데이터셋을 시뮬레이션 Backend에서 계산합니다. 양자 워크스트림이 고전적 머신러닝 방법과 어떻게 연결되는지 확인할 수 있습니다.
  • 많은 특징을 가진 데이터에 대해 단일 커널 행렬 원소를 실제 양자 컴퓨터에서 계산합니다. IBM® 양자 컴퓨터의 시간을 존중하기 위해, 대규모 데이터셋 전체에 대한 커널 행렬은 추정하지 않습니다.
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy pandas qiskit qiskit-ibm-runtime scikit-learn
# If you have not already, install scikit learn
#!pip install scikit-learn

단일 커널 행렬 원소

Step 1: 고전적 입력을 양자 문제로 매핑하기

먼저 특징(feature)이 10개인 소규모 데이터셋을 살펴보겠습니다. 데이터셋의 크기는 얼마든지 커질 수 있습니다. 커널 행렬 원소를 하나씩 계산하기 때문입니다. 최소 두 개의 데이터 포인트가 필요하므로, 여기서는 그 두 개만으로 시작하겠습니다(다음 예제에서는 전체 데이터셋을 불러옵니다). 필요한 패키지를 몇 가지 가져오겠습니다.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# Two mock data points, including category labels, as in training
small_data = [
[-0.194, 0.114, -0.006, 0.301, -0.359, -0.088, -0.156, 0.342, -0.016, 0.143, 1],
[-0.1, 0.002, 0.244, 0.127, -0.064, -0.086, 0.072, 0.043, -0.053, 0.02, -1],
]

# Data points with labels removed, for inner product
train_data = [small_data[0][:-1], small_data[1][:-1]]

z_feature_map을 사용해 볼 수 있습니다.

# from qiskit.circuit.library import zz_feature_map
# fm = zz_feature_map(feature_dimension=np.shape(train_data)[1], entanglement='linear', reps=1)

from qiskit.circuit.library import z_feature_map

fm = z_feature_map(feature_dimension=np.shape(train_data)[1])

unitary1 = fm.assign_parameters(train_data[0])
unitary2 = fm.assign_parameters(train_data[1])

위의 두 유니터리는 서론에서 설명한 U1U_1U2U_2에 정확히 대응됩니다. 이 둘은 unitary_overlap을 사용해 결합할 수 있습니다. 언제나 그렇듯이, Circuit 깊이에 주의를 기울여야 합니다.

from qiskit.circuit.library import unitary_overlap

overlap_circ = unitary_overlap(unitary1, unitary2)
overlap_circ.measure_all()

print("circuit depth = ", overlap_circ.decompose().depth())
overlap_circ.decompose().draw("mpl", scale=0.6, style="iqp")
circuit depth =  9

Output of the previous code cell

Step 2: 양자 실행을 위한 문제 최적화하기

가장 한가한 Backend를 선택한 뒤, 해당 Backend에서 실행할 수 있도록 Circuit을 최적화합니다.

# Import needed packages
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService

# Get the least busy backend
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=fm.num_qubits
)
print(backend)
<IBMBackend('ibm_brisbane')>
# Apply level 3 optimization to our overlap circuit
pm = generate_preset_pass_manager(optimization_level=3, backend=backend)
overlap_ibm = pm.run(overlap_circ)

복잡한 Circuit의 경우 이 단계에서 Circuit 깊이가 크게 증가할 수 있습니다. 실제 양자 컴퓨터에서 사용하는 네이티브 게이트로 매핑되면서, Qubit 간 정보 이동이 필요할 수 있기 때문입니다. 이 간단한 사례에서는 깊이가 거의 변하지 않습니다.

print("circuit depth = ", overlap_ibm.decompose().depth())
overlap_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)
circuit depth =  10
1

Step 3: Qiskit Runtime Primitives를 사용하여 실행

아래에서 시뮬레이터로 실행하는 구문은 주석 처리되어 있습니다. 이 데이터셋은 특징(feature) 수가 적으므로, 시뮬레이터를 사용하는 것도 여전히 가능합니다. 하지만 유틸리티 규모의 계산에서는 시뮬레이션이 일반적으로 실행 가능하지 않습니다. 시뮬레이터는 축소된 코드를 디버그하는 용도로만 사용해야 합니다.

# Run this for a simulator
# from qiskit.primitives import StatevectorSampler

# from qiskit_ibm_runtime import Options, Session, Sampler

# num_shots = 10000

# Evaluate the problem using state vector-based primitives from Qiskit
# sampler = StatevectorSampler()
# results = sampler.run([overlap_circ], shots=num_shots).result()
# .get_counts() returns counts associated with a state labeled by bit results such as |001101...01>.
# counts_bit = results[0].data.meas.get_counts()
# .get_int_counts returns the same counts, but labeled by integer equivalent of the above bit string.
# counts = results[0].data.meas.get_int_counts()
# Benchmarked on an Eagle processor, 7-11-24, took 4 sec.

# Import our runtime primitive
from qiskit_ibm_runtime import Session, SamplerV2 as Sampler

num_shots = 10000

# Use sampler and get the counts

sampler = Sampler(mode=backend)
results = sampler.run([overlap_ibm], shots=num_shots).result()
# .get_counts() returns counts associated with a state labeled by bit results such as |001101...01>.
counts_bit = results[0].data.meas.get_counts()
# .get_int_counts returns the same counts, but labeled by integer equivalent of the above bit string.
counts = results[0].data.meas.get_int_counts()

Step 4: 후처리 및 고전적 형식으로 결과 반환

소개에서 설명한 것처럼, 여기서 가장 유용한 측정값은 영(零) 상태 00000|00000\rangle을 측정할 확률입니다.

counts.get(0, 0.0) / num_shots
0.6525

이것이 우리가 원하는 결과입니다. 두 데이터 포인트에 해당하는 벡터의 내적(모 제곱까지)을 추정한 값입니다. 측정 확률(또는 준확률)의 전체 분포를 살펴보고 싶다면, 아래와 같이 plot_distribution 함수를 사용할 수 있습니다. Qubit 수가 많아질수록 이런 그림은 금방 다루기 어려워진다는 점을 유의하세요.

from qiskit.visualization import plot_distribution

plot_distribution(counts_bit)

Output of the previous code cell

또는 아래와 같은 시각화 함수를 정의하여, 가장 확률이 높은 상위 10개의 측정값만 확인할 수도 있습니다. 이는 문제를 해결하거나 데이터에 대한 직관을 얻으려 할 때 유용할 수 있습니다. 하지만 커널 행렬 원소는 영 상태의 측정 확률입니다.

def visualize_counts(probs, num_qubits):
"""Visualize the outputs from the Qiskit Sampler primitive."""
zero_prob = probs.get(0, 0.0)
top_10 = dict(sorted(probs.items(), key=lambda item: item[1], reverse=True)[:10])
top_10.update({0: zero_prob})
by_key = dict(sorted(top_10.items(), key=lambda item: item[0]))
xvals, yvals = list(zip(*by_key.items()))
xvals = [bin(xval)[2:].zfill(num_qubits) for xval in xvals]
plt.bar(xvals, yvals)
plt.xticks(rotation=75)
plt.title("Results of sampling")
plt.xlabel("Measured bitstring")
plt.ylabel("Counts")
plt.show()

visualize_counts(counts, overlap_circ.num_qubits)

Output of the previous code cell

고차원 특징 공간에서의 두 데이터 포인트 간 내적 하나에 관한 이 정보만으로는, 두 벡터의 겹침이 최대 겹침(1.0)에 비해 상당히 크다는 것 외에는 말할 수 없습니다. 이는 두 데이터 포인트가 본질적으로 유사하여 동일한 클래스로 분류될 것이라는 지표일 수 있습니다. 혹은 우리의 특징 맵이 유사한 데이터는 강한 겹침, 상이한 데이터는 작은 겹침을 갖도록 매핑하는 데 효과적이지 않다는 지표일 수도 있습니다. 어느 쪽이 맞는지 알려면, 전체 데이터셋에 특징 맵을 적용하여 결과로 나오는 커널 행렬이 높은 정확도로 클래스를 효과적으로 분리할 수 있는지 확인해야 합니다.

z_feature_map을 사용한 결과, 트랜스파일된 2-Qubit 깊이가 낮았다는 점(실제로 깊이 1)은 주목할 만합니다. 회로가 너무 깊어지면 노이즈가 많이 발생하고, 특징 맵이 데이터에 잘 맞더라도 영 상태를 측정할 확률이 매우 낮아집니다. 예를 들어, zz_feature_map, entanglement='linear', reps=1을 사용하여 위 과정을 동일한 데이터 포인트로 반복했을 때 dist.get(0,0.0) = 0.0015가 나왔습니다. 이는 zz_feature_map의 훨씬 더 깊은 회로 깊이와 2-Qubit 깊이 때문입니다. 아래 그림은 그 계산에 대한 확률 분포를 보여줍니다.

Bad results from a zz feature map.

몇 가지 데이터 포인트를 가지고 깊이를 얼마나 낮추어야 좋은 결과를 얻을 수 있는지 실험해 보는 것이 좋습니다. 다음은 예외가 있을 수 있는 대략적인 조언입니다. 일반적으로, 트랜스파일된 2-Qubit 깊이가 10 이하이면 문제가 없습니다. 50~60 정도는 최첨단 수준이며, 고급 오류 완화 기법 등의 도구가 필요합니다. 그 사이에서는 데이터의 유사성, 특징 맵의 표현력, 회로 폭, 기타 요인에 따라 결과가 달라질 수 있습니다. 일반적으로 후처리 단계에는 고전적인 머신러닝 과정도 포함됩니다. 다음 섹션에서는 이 과정을 전체 데이터셋으로 확장하고 고전적인 머신러닝 워크플로를 소개합니다.

이해도 확인

아래 질문을 읽고 답을 생각해 본 다음, 삼각형을 클릭하여 정답을 확인하세요.

10-Qubit 양자 회로에서, 일반적으로 측정될 수 있는 서로 다른 상태는 몇 가지입니까?

정답:

2102^{10} 또는 1024가지입니다.

양자 컴퓨팅을 처음 접하는 누군가가 2-Qubit 깊이가 매우 높은 양자 회로를 사용하려 하고, 오류 완화를 사용하지 않는다고 가정합시다. 이 경우 각 Qubit의 오류율이 10%라고 추가로 가정합시다. 이 회로에 해당하는 진정한(오류 없는) 커널 행렬 원소가 매우 크다면, 예를 들어 1.0이라면, 10개의 Qubit 모두가 0|0\rangle 상태에 있는 것으로 측정될 확률은 얼마입니까?

정답:

각 Qubit이 0|0\rangle 상태에서 올바르게 측정될 확률은 0.90입니다. 10개의 Qubit 모두가 올바른 상태에서 측정될 확률은 0.90100.90^{10}, 즉 약 35%입니다.

회로 깊이를 모니터링하는 것이 왜 그토록 중요한지 자신의 말로 설명하세요. 이는 일반적으로도 중요하지만, 양자 커널 추정의 맥락에서 설명해 보세요.

정답:

이 QKE 워크플로에서, 우리의 추정은 영 상태, 즉 모든 Qubit이 0|0\rangle 상태에서 발견되는 상태의 측정을 기반으로 합니다. 회로가 매우 깊어지면 높은 오류율이 발생합니다. 그 오류율이 많은 Qubit에 걸쳐 누적되면, 영 상태를 측정할 확률이 상당히 감소합니다.

전체 커널 행렬

이 섹션에서는 앞서 살펴본 과정을 전체 데이터셋의 이진 분류로 확장합니다. 여기에는 두 가지 중요한 요소가 추가됩니다. (1) 후처리 단계에서 고전 머신러닝을 적용할 수 있으며, (2) 훈련에 대한 정확도 점수를 얻을 수 있습니다.

1단계: 고전 입력을 양자 문제로 변환

이제 분류에 사용할 기존 데이터셋을 불러옵니다. 이 데이터셋은 128개의 행(데이터 포인트)으로 구성되어 있으며, 각 포인트에는 14개의 피처가 있습니다. 15번째 원소는 각 포인트의 이진 카테고리(±1\pm 1)를 나타냅니다. 아래에서 데이터셋을 불러오거나, 여기에서 데이터셋과 그 구조를 직접 확인할 수 있습니다.

처음 90개의 데이터 포인트를 훈련에 사용하고, 다음 30개의 포인트를 테스트에 사용합니다.

!wget https://raw.githubusercontent.com/qiskit-community/prototype-quantum-kernel-training/main/data/dataset_graph7.csv

df = pd.read_csv("dataset_graph7.csv", sep=",", header=None)

# Prepare training data

train_size = 90
X_train = df.values[0:train_size, :-1]
train_labels = df.values[0:train_size, -1]

# Prepare testing data
test_size = 30
X_test = df.values[train_size : train_size + test_size, :-1]
test_labels = df.values[train_size : train_size + test_size, -1]
--2024-07-11 23:05:22--  https://raw.githubusercontent.com/qiskit-community/prototype-quantum-kernel-training/main/data/dataset_graph7.csv
Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.110.133, 185.199.111.133, 185.199.109.133, ...
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.110.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 49405 (48K) [text/plain]
Saving to: ‘dataset_graph7.csv.15’

dataset_graph7.csv. 100%[===================>] 48.25K --.-KB/s in 0.02s

2024-07-11 23:05:23 (2.11 MB/s) - ‘dataset_graph7.csv.15’ saved [49405/49405]

여러 출력을 저장할 수 있도록 미리 적절한 크기의 커널 행렬과 테스트 행렬을 구성합니다.

# Empty kernel matrix
num_samples = np.shape(X_train)[0]
kernel_matrix = np.full((num_samples, num_samples), np.nan)
test_matrix = np.full((test_size, num_samples), np.nan)

이제 고전 데이터를 양자 Circuit으로 인코딩하고 매핑하기 위한 피처 맵을 생성합니다. 직접 피처 맵을 구성하거나, 미리 만들어진 피처 맵을 사용할 수 있습니다. 아래의 피처 맵을 자유롭게 수정하거나, ZFeatureMap으로 바꿔 볼 수 있습니다. 단, 항상 Circuit 깊이에 주의해야 합니다. 앞서 살펴봤듯이, 상대적으로 피처 수가 적은 6-Qubit 예시에서도 zz_feature_map을 사용하면 트랜스파일된 Circuit 깊이가 감당할 수 없을 만큼 높아졌습니다. Circuit의 규모와 복잡도가 증가할수록 깊이도 급격히 늘어나, 노이즈가 결과를 압도하는 지점에 쉽게 도달할 수 있습니다. 데이터 구조에 대한 정보가 있다면, 그 지식을 활용해 가장 유용한 피처 맵 구조를 만드는 것이 좋습니다.

from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit

# Prepare feature map for computing overlap
num_features = np.shape(X_train)[1]
num_qubits = int(num_features / 2)

# To use a custom feature map use the lines below.
entangler_map = [[0, 2], [3, 4], [2, 5], [1, 4], [2, 3], [4, 6]]

fm = QuantumCircuit(num_qubits)
training_param = Parameter("θ")
feature_params = ParameterVector("x", num_qubits * 2)
fm.ry(training_param, fm.qubits)
for cz in entangler_map:
fm.cz(cz[0], cz[1])
for i in range(num_qubits):
fm.rz(-2 * feature_params[2 * i + 1], i)
fm.rx(-2 * feature_params[2 * i], i)

2단계 및 3단계: 문제 최적화 및 Primitive를 이용한 실행

오버랩 Circuit을 구성하고, 실제 양자 컴퓨터에서 실행하는 경우라면 앞서와 같이 실행에 맞게 최적화합니다. 그러나 이 예시에서는 모든 데이터 포인트를 순회하며 전체 커널 행렬을 계산할 것입니다. 데이터 벡터 쌍 xi\vec{x}_ixj\vec{x}_j 각각에 대해 서로 다른 오버랩 Circuit을 생성합니다. 따라서 2단계와 3단계는 여러 번의 반복 안에서 함께 수행됩니다.

아래 코드 셀은 앞서 단일 데이터 포인트 쌍에 대해 수행했던 것과 완전히 동일한 과정을 수행합니다. 이번에는 두 개의 for 루프 안에서 실행되고, 마지막에 kernel_matrix[x_1,x_2] = ... 라인이 추가되어 각 계산 결과를 저장합니다. 커널 행렬의 대칭성을 활용하여 계산 횟수를 절반으로 줄였습니다. 또한 대각 원소는 노이즈가 없는 경우 1이어야 하므로 1로 설정했습니다. 구현 방식과 요구되는 정밀도에 따라, 대각 원소를 이용해 노이즈를 추정하거나 오류 완화 목적으로 활용할 수도 있습니다.

커널 행렬이 완전히 채워지면, 테스트 데이터에 대해 동일한 과정을 반복하여 test_matrix를 채웁니다. 이 역시 커널 행렬의 일종이지만, 두 행렬을 구분하기 위해 다른 이름을 사용합니다.

# To use a simulator
from qiskit.primitives import StatevectorSampler

# Remember to insert your token in the QiskitRuntimeService constructor to use real quantum computers
# service = QiskitRuntimeService()
# backend = service.least_busy(
# operational=True, simulator=False, min_num_qubits=fm.num_qubits
# )

num_shots = 10000

# Evaluate the problem using state vector-based primitives from Qiskit.
sampler = StatevectorSampler()

for x1 in range(0, train_size):
for x2 in range(x1 + 1, train_size):
unitary1 = fm.assign_parameters(list(X_train[x1]) + [np.pi / 2])
unitary2 = fm.assign_parameters(list(X_train[x2]) + [np.pi / 2])

# Create the overlap circuit
overlap_circ = unitary_overlap(unitary1, unitary2)
overlap_circ.measure_all()

# These lines run the qiskit sampler primitive.
counts = (
sampler.run([overlap_circ], shots=num_shots)
.result()[0]
.data.meas.get_int_counts()
)

# Assign the probability of the 0 state to the kernel matrix, and the transposed element (since this is an inner product)
kernel_matrix[x1, x2] = counts.get(0, 0.0) / num_shots
kernel_matrix[x2, x1] = counts.get(0, 0.0) / num_shots
# Fill in on-diagonal elements with 1, again, since this is an inner-product corresponding to probability (or alter the code to check these entries and verify they yield 1)
kernel_matrix[x1, x1] = 1

print("training done")

# Similar process to above, but for testing data.
for x1 in range(0, test_size):
for x2 in range(0, train_size):
unitary1 = fm.assign_parameters(list(X_test[x1]) + [np.pi / 2])
unitary2 = fm.assign_parameters(list(X_train[x2]) + [np.pi / 2])

# Create the overlap circuit
overlap_circ = unitary_overlap(unitary1, unitary2)
overlap_circ.measure_all()

counts = (
sampler.run([overlap_circ], shots=num_shots)
.result()[0]
.data.meas.get_int_counts()
)

test_matrix[x1, x2] = counts.get(0, 0.0) / num_shots

print("test matrix done")
training done
test matrix done

4단계: 후처리 및 고전 형식으로 결과 반환

양자 커널 방법을 통해 커널 행렬과 동일한 형식의 test_matrix를 얻었으므로, 이제 고전 머신러닝 알고리즘을 적용해 테스트 데이터에 대한 예측을 수행하고 정확도를 확인할 수 있습니다. 먼저 Scikit-Learn의 sklearn.svc, 즉 서포트 벡터 분류기(SVC)를 불러옵니다. kernel = precomputed를 지정하여 미리 계산된 커널을 사용하도록 설정합니다.

# import a support vector classifier from a classical ML package.
from sklearn.svm import SVC

# Specify that you want to use a pre-computed kernel matrix
qml_svc = SVC(kernel="precomputed")

SVC.fit을 사용하여 커널 행렬과 훈련 레이블을 입력하면 피팅이 완료됩니다. 그런 다음 SVC.score로 test_matrix와 테스트 레이블을 입력하여 피팅 결과에 대한 테스트 데이터의 정확도를 반환합니다.

# Feed in the pre-computed matrix and the labels of the training data. The classical algorithm gives you a fit.
qml_svc.fit(kernel_matrix, train_labels)

# Now use the .score to test your data, using the matrix of test data, and test labels as your inputs.
qml_score_precomputed_kernel = qml_svc.score(test_matrix, test_labels)
print(f"Precomputed kernel classification test score: {qml_score_precomputed_kernel}")
Precomputed kernel classification test score: 1.0

훈련된 모델의 정확도가 100%임을 확인할 수 있습니다. 이는 훌륭한 결과이며, QKE가 실제로 작동함을 보여줍니다. 그러나 이것이 양자 우위를 의미하는 것은 아닙니다. 고전 커널로도 이 분류 문제를 100% 정확도로 풀 수 있었을 가능성이 높습니다. 양자 커널이 현재의 유틸리티 시대에 가장 유용하게 활용될 수 있는 데이터 유형과 데이터 관계를 규명하기 위해 아직 많은 연구가 필요합니다.

학습자가 이 워크플로의 여러 부분을 수정하고 다양한 양자 피처 맵의 효과를 직접 탐구해보기를 권장합니다. 다음과 같은 사항들을 고려해보세요:

  • 정확도는 얼마나 견고한가요? 이 특정 훈련 데이터뿐만 아니라 광범위한 데이터 유형에서도 유지되나요?
  • 데이터의 어떤 구조가 양자 피처 맵이 유용할 것이라는 판단을 가능하게 하나요?
  • 훈련 데이터의 양을 늘리거나 줄이면 정확도는 어떻게 변하나요?
  • 어떤 피처 맵을 사용할 수 있으며, 피처 맵에 따라 결과는 어떻게 달라지나요?
  • 피처 수를 늘리면 정확도와 실행 시간은 어떻게 영향을 받나요?
  • 실제 양자 컴퓨터에서도 유지될 것으로 예상되는 경향이 있나요?

더 많은 피처와 Qubit으로 확장하기

이 섹션에서는 훨씬 더 많은 수의 피처에 대해 단일 행렬 요소 계산을 반복하며, 유틸리티 수준으로 확장하는 경로를 제시합니다. 단일 행렬 요소로 제한하는 이유는, 양자 컴퓨터에서 주어진 시간을 과도하게 사용하지 않으면서 전체 과정을 보여주기 위해서입니다.

1단계: 고전 입력을 양자 문제로 매핑하기

각 데이터 포인트가 42개의 피처를 갖는 데이터셋을 시작점으로 가정합니다. 첫 번째 예제와 마찬가지로, 두 개의 데이터 포인트가 필요한 단일 커널 행렬 요소를 계산합니다. 아래의 두 포인트는 42개의 피처와 하나의 카테고리 변수(±1\pm 1)를 가집니다.

# Two mock data points, including category labels, as in training

large_data = [
[
-0.028,
-1.49,
-1.698,
0.107,
-1.536,
-1.538,
-1.356,
-1.514,
-0.109,
-1.8,
-0.122,
-1.651,
-1.955,
-0.123,
-1.732,
0.091,
-0.048,
-0.128,
-0.026,
0.082,
-1.263,
0.065,
0.004,
-0.055,
-0.08,
-0.173,
-1.734,
-0.39,
-1.451,
0.078,
-1.578,
-0.025,
-0.184,
-0.119,
-1.336,
0.055,
-0.204,
-1.578,
0.132,
-0.121,
-1.599,
-0.187,
-1,
],
[
-1.414,
-1.439,
-1.606,
0.246,
-1.673,
0.002,
-1.317,
-1.262,
-0.178,
-1.814,
0.013,
-1.619,
-1.86,
-0.25,
-0.212,
-0.214,
-0.033,
0.071,
-0.11,
-1.607,
0.441,
-0.143,
-0.009,
-1.655,
-1.579,
0.381,
-1.86,
-0.079,
-0.088,
-0.058,
-1.481,
-0.064,
-0.065,
-1.507,
0.177,
-0.131,
-0.153,
0.07,
-1.627,
0.593,
-1.547,
-0.16,
-1,
],
]
train_data = [large_data[0][:-1], large_data[1][:-1]]

앞서 설명한 바와 같이, zz_feature_map은 비교적 적은 피처(14개)를 사용할 때도 꽤 깊은 Circuit을 만들었습니다. 피처 수가 증가함에 따라 Circuit 깊이를 면밀히 모니터링해야 합니다. 이를 설명하기 위해, 먼저 zz_feature_map을 사용하여 결과 Circuit의 깊이를 확인해 보겠습니다.

from qiskit.circuit.library import zz_feature_map

fm = zz_feature_map(
feature_dimension=np.shape(train_data)[1], entanglement="linear", reps=1
)

unitary1 = fm.assign_parameters(train_data[0])
unitary2 = fm.assign_parameters(train_data[1])
from qiskit.circuit.library import unitary_overlap

overlap_circ = unitary_overlap(unitary1, unitary2)
overlap_circ.measure_all()

print("circuit depth = ", overlap_circ.decompose(reps=2).depth())
print(
"two-qubit depth",
overlap_circ.decompose().depth(lambda instr: len(instr.qubits) > 1),
)
# overlap_circ.draw("mpl", scale=0.6, style="iqp")
circuit depth =  251
two-qubit depth 165

앞서 설명했듯이, 어느 정도가 너무 깊은 것인지를 정확히 판단하는 일은 복잡합니다. 하지만 트랜스파일 이전에 이미 2-Qubit 깊이가 100을 초과한다면, 이는 시작 자체가 불가능한 수준입니다. 이것이 이 수업 전반에 걸쳐 커스텀 피처 맵을 강조한 이유입니다. 전체 데이터셋의 구조에 대해 알고 있다면, 그 구조를 반영한 얽힘 맵(entanglement map)을 설계해야 합니다. 여기서는 두 데이터 포인트 간의 내적만 계산하므로, 데이터 구조에 대한 세밀한 고려보다는 낮은 Circuit 깊이를 우선시합니다.

from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit

# Prepare feature map for computing overlap

entangler_map = [
[3, 4],
[2, 5],
[1, 4],
[2, 3],
[4, 6],
[7, 9],
[10, 11],
[9, 12],
[8, 11],
[9, 10],
[11, 13],
[14, 16],
[17, 18],
[16, 19],
[15, 18],
[16, 17],
[18, 20],
]
# Use the entangler map above to build a feature map

num_features = np.shape(train_data)[1]
num_qubits = int(num_features / 2)

fm = QuantumCircuit(num_qubits)
training_param = Parameter("θ")
feature_params = ParameterVector("x", num_qubits * 2)
fm.ry(training_param, fm.qubits)
for cz in entangler_map:
fm.cz(cz[0], cz[1])
for i in range(num_qubits):
fm.rz(-2 * feature_params[2 * i + 1], i)
fm.rx(-2 * feature_params[2 * i], i)
from qiskit.circuit.library import unitary_overlap

# Assign features of each data point to a unitary, an instance of the general feature map.

unitary1 = fm.assign_parameters(list(train_data[0]) + [np.pi / 2])
unitary2 = fm.assign_parameters(list(train_data[1]) + [np.pi / 2])

# Create the overlap circuit

overlap_circ = unitary_overlap(unitary1, unitary2)
overlap_circ.measure_all()

깊이는 아직 확인하지 않겠습니다. 실제로 중요한 것은 트랜스파일된 2-Qubit 깊이이기 때문입니다.

2단계: 양자 실행을 위한 문제 최적화

가장 한가한 Backend를 선택한 후, 해당 Backend에서 실행하기 위해 Circuit을 최적화합니다.

# Import needed packages
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService

# Get the least busy backend
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=fm.num_qubits
)
print(backend)
<IBMBackend('ibm_brisbane')>

소규모 작업에서는 사전 설정된 패스 매니저(preset pass manager)가 일반적으로 동일한 깊이의 동일한 Circuit을 안정적으로 반환합니다. 하지만 매우 크고 복잡한 Circuit에서는 실행할 때마다 다른 트랜스파일된 Circuit을 반환할 수 있습니다. 이는 휴리스틱을 사용하기 때문이며, 매우 큰 Circuit일수록 가능한 최적화의 탐색 공간이 복잡해지기 때문입니다. 패스 매니저를 여러 번 실행하여 가장 얕은 Circuit을 선택하는 것이 유용한 경우가 많습니다. 이는 고전적인 오버헤드만 추가할 뿐이며, 양자 컴퓨터의 결과를 크게 향상시킬 수 있습니다.

여기서는 유니터리 오버랩 Circuit을 20번 트랜스파일하고, 얻어진 Circuit들의 깊이를 살펴봅니다.

# Apply level 3 optimization to our overlap circuit
transpiled_qcs = []
transpiled_depths = []
transpiled_twoqubit_depths = []
for i in range(1, 20):
pm = generate_preset_pass_manager(optimization_level=3, backend=backend)
overlap_ibm = pm.run(overlap_circ)
transpiled_qcs.append(overlap_ibm)
transpiled_depths.append(overlap_ibm.decompose().depth())
transpiled_twoqubit_depths.append(
overlap_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)
)

print("circuit depth = ", overlap_ibm.decompose().depth())
circuit depth =  61
print(transpiled_depths)
print(transpiled_twoqubit_depths)
[61, 60, 60, 69, 60, 60, 60, 65, 60, 60, 69, 61, 77, 77, 65, 60, 60, 77, 61]
[13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13]

서로 다른 트랜스파일 실행에서 총 게이트 깊이에 다소 차이가 있음을 알 수 있습니다. 현재 Circuit은 2-Qubit 트랜스파일 깊이의 변동이 나타날 만큼 크거나 복잡하지 않습니다. 얻어진 가장 깊은 Circuit의 깊이가 77인데, 그보다 약간 낮은 깊이 60을 가진 transpiled_qcs[1]을 사용하겠습니다.

overlap_ibm = transpiled_qcs[1]

3단계: Qiskit Runtime Primitive를 사용하여 실행하기

유틸리티 수준에 가까워질수록 시뮬레이터는 유용하지 않습니다. 여기서는 실제 양자 컴퓨터를 위한 구문만 보여줍니다.

# Run on ibm_osaka, 7-12-24, required 22 sec.

# Import our runtime primitive
from qiskit_ibm_runtime import SamplerV2 as Sampler

# Open a Runtime session:
session = Session(backend=backend)
num_shots = 10000
# Use sampler and get the counts

sampler = Sampler(mode=session)
options = sampler.options
options.dynamical_decoupling.enable = True
options.twirling.enable_gates = True
counts = (
sampler.run([overlap_ibm], shots=num_shots).result()[0].data.meas.get_int_counts()
)

# Close session after done
session.close()

4단계: 후처리 및 고전 형식으로 결과 반환하기

소개에서 설명한 바와 같이, 여기서 가장 유용한 측정값은 영 상태 00000|00000\rangle을 측정할 확률입니다.

counts.get(0, 0.0) / num_shots
0.0138

단일 커널 행렬 요소에 대한 이 과정은 데이터셋의 다른 데이터 쌍 사이에서 반복하여 전체 커널 행렬을 얻을 수 있습니다. 커널 행렬의 차원은 피처 수가 아니라 훈련 데이터의 포인트 수에 의해 결정됩니다. 따라서 커널 행렬을 예측 모델로 변환하는 계산 비용은 피처 수나 Qubit 수에 따라 확장되지 않습니다. 피처 수가 많은 비교적 작은 데이터셋이더라도, 데이터는 여전히 효과적인 분류를 이끌어내는 피처 맵에 맞춰져야 합니다.

확장성과 향후 연구

커널 방법은 0|0\rangle 상태를 가능한 한 정확하게 측정해야 합니다. 하지만 게이트 오류와 읽기 오류로 인해 어느 Qubit이든 1|1\rangle 상태로 잘못 측정될 확률 pp가 0이 아닙니다. 0|0\rangle의 확률이 100%100\%여야 한다는 단순화된 가정 하에서도, 예를 들어 NN개의 비트에 많은 피처를 인코딩할 경우, 모든 비트가 0|0\rangle으로 올바르게 측정될 확률은 (1p)N(1-p)^N으로 줄어듭니다. NN이 커질수록 이 방법은 점점 신뢰하기 어려워집니다. 이 어려움을 극복하고 커널 추정을 더 많은 피처로 확장하는 것은 현재 활발히 연구 중인 분야입니다. 이 문제에 대해 더 자세히 알고 싶다면 Thanasilp, Wang, Cerezo, Holmes의 연구를 참고하세요. 현재 양자 컴퓨터로 무엇을 할 수 있는지 탐구해 보고, 오류 수정 시대에 무엇이 가능해질지도 기대해 보세요.

복습

양자 커널을 계산하는 과정은 다음을 포함합니다.

  • 훈련 데이터 포인트 쌍을 이용하여 커널 행렬 요소 계산
  • 피처 매핑을 통한 데이터 인코딩 및 매핑
  • 실제 양자 컴퓨터 / Backend에서 실행하기 위한 Circuit 최적화

이렇게 얻은 양자 커널은 이 수업에서 본 것처럼 고전 머신러닝 알고리즘에서 활용할 수 있습니다.

양자 커널을 사용할 때 염두에 두어야 할 핵심 사항은 다음과 같습니다.

  • 해당 데이터셋이 양자 커널 방법으로부터 이점을 얻을 가능성이 있는가?
  • 다양한 피처 맵과 얽힘 구조를 시도해 보세요.
  • Circuit 깊이가 허용 가능한 수준인가?
  • 패스 매니저를 여러 번 실행하고 얻을 수 있는 가장 얕은 깊이의 Circuit을 사용해 보세요.

양자 커널 방법은 양자 친화적인 피처를 가진 데이터셋과 적합한 양자 피처 맵이 올바르게 결합될 때 강력한 도구가 될 수 있습니다. 양자 커널이 유용할 가능성이 높은 곳을 더 잘 이해하려면 Liu, Arunachalam & Temme (2021)을 읽어보세요.