Skip to main content
IBM Quantum Platform

투영 양자 커널을 이용한 특징 분류 향상

사용 예상 시간: Heron 프로세서( r3 )에서 80분(참고: 이는 예상치일 뿐입니다. 런타임은 다를 수 있습니다.)


학습 성과

  • 투영 양자 커널(PQK)의 작동 원리와, 어떤 경우에 잠재적인 양자 이점을 제공하는지.
  • 실제 데이터셋을 사용하여 하드웨어에서 PQK를 실행하는 방법.

전제조건


배경

이 튜토리얼에서는 퀀텀 커널 방법으로 CAR T 세포 세포 독성 예측 향상 [1] 논문을 기반으로 실제 생물학적 데이터 세트에서 Qiskit으로 투영된 양자 커널 (PQK)을 실행하는 방법을 시연합니다.

PQK는 양자 컴퓨터로 특징 선택을 강화하여 클래식 데이터를 양자 특징 공간으로 인코딩하고 이를 다시 클래식 영역으로 투영하는 양자 머신러닝(QML)에 사용되는 방법입니다. 여기에는 일반적으로 데이터를 고차원 힐버트 공간으로 변환하는 피처 매핑이라는 프로세스를 통해 양자 회로를 사용하여 기존 데이터를 양자 상태로 인코딩하는 작업이 포함됩니다. "투영" 측면은 특정 관측값을 측정하여 양자 상태에서 고전적인 정보를 추출하여 서포트 벡터 머신과 같은 고전적인 커널 기반 알고리즘에 사용할 수 있는 커널 행렬을 구성하는 것을 말합니다. 이 접근 방식은 양자 시스템의 계산적 이점을 활용하여 기존 방식에 비해 특정 작업에서 잠재적으로 더 나은 성능을 달성할 수 있습니다.

PQK의 주요 구성 요소는 양자 특징 맵에 대한 투영 측정을 통해 얻어지는 환원 밀도 행렬(RDM)입니다. 특히, 일반적으로 각 큐비트에 대해 단일 큐비트 축소 밀도 행렬(1 RDM)을 계산한다. 그런 다음, 이러한 측정된 값들은 지수 커널과 같은 고전적인 커널 함수의 입력값으로 사용되어 최종 커널 행렬을 구성합니다.

PQK는 표준 양자 커널에 비해 잠재적인 이점을 제공하며, 특히 단기적으로 도입될 양자 하드웨어의 경우 그 이점이 두드러집니다. 표준 양자 커널은 일반적으로 전역 상태 중첩을 추정하는 방식에 의존하는데, 큐비트 수가 증가함에 따라 이를 정확하게 측정하기가 점점 더 어려워지며, 노이즈에 매우 민감합니다. 반면, PQK는 단일 큐비트 축소 밀도 행렬(1 RDM)과 같은 국소 관측량을 사용하므로, 샘플링 오버헤드가 줄어들고 하드웨어 노이즈에 대한 내성이 향상되며 확장성도 더 우수합니다. PQK는 고전적 커널 함수를 적용하기 전에 양자 상태를 국소적 측정 특징에 투영함으로써, 유용한 양자 상관관계를 유지하면서도 단기적으로 상용화될 장치에 더 실용적인 방식을 제공할 수 있다.


요구사항

이 튜토리얼을 시작하기 전에 다음이 설치되어 있는지 확인하세요:

  • Qiskit SDK v2.0 또는 이후 버전, 시각화 지원 기능 포함
  • Qiskit Runtime v0.40 이상 (pip install qiskit-ibm-runtime)
  • 카테고리 인코더 2.8.1 (pip install category-encoders)
  • NumPy 2.3.2 (pip install numpy)
  • 판다스 2.3.2 (pip install pandas)
  • Scikit-learn 1.7.1 (pip install scikit-learn)
  • Tqdm 4.67.1 (pip install tqdm)

설정

import warnings

# Standard libraries
import os
import urllib.request
from pathlib import Path
import numpy as np
import pandas as pd

# Machine learning and data processing
import category_encoders as ce
from scipy.linalg import inv, sqrtm
from sklearn.metrics.pairwise import rbf_kernel
from sklearn.model_selection import GridSearchCV, StratifiedKFold
from sklearn.svm import SVC

# Qiskit and IBM Quantum Compute Service
from qiskit import QuantumCircuit
from qiskit.circuit import ParameterVector
from qiskit.circuit.library import UnitaryGate, ZZFeatureMap
from qiskit.quantum_info import SparsePauliOp, random_unitary
from qiskit.transpiler import generate_preset_pass_manager
from qiskit_ibm_runtime import (
    Batch,
    EstimatorOptions,
    EstimatorV2 as Estimator,
    QiskitRuntimeService,
)

# Progress bar
import tqdm

warnings.filterwarnings("ignore")

소규모 시뮬레이터 예시

이 튜토리얼에서는 소규모 시뮬레이터 예제를 생략합니다. 우리의 주된 목표는 투영된 양자 커널이 더 큰 시스템과 실제 하드웨어로 어떻게 확장될 수 있는지 보여주는 것이기 때문입니다.


대규모 하드웨어 예시

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

데이터셋 준비

이 튜토리얼에서는 2진 분류 작업에 실제 생물학적 데이터 세트를 사용하며, 이 데이터 세트는 Daniels 등(2022)에서 생성하고 논문과 함께 제공되는 보충 자료에서 다운로드할 수 있습니다. 이 데이터는 특정 암을 치료하기 위해 면역 요법에 사용되는 유전자 조작 T 세포인 CAR T 세포로 구성되어 있습니다. 면역 세포의 일종인 T세포는 실험실에서 암세포의 특정 단백질을 표적으로 하는 키메라 항원 수용체(CAR)를 발현하도록 변형됩니다. 이렇게 변형된 T세포는 암세포를 더 효과적으로 인식하고 파괴할 수 있습니다. 데이터의 특징은 CAR T세포 모티프로, T세포로 설계된 CAR의 특정 구조적 또는 기능적 구성 요소를 나타냅니다. 이러한 모티프를 기반으로 주어진 CAR T 세포의 세포 독성을 예측하여 독성 또는 무독성으로 분류하는 것이 우리의 과제입니다.

다음은 이 데이터 집합을 전처리하는 헬퍼 함수를 보여줍니다.

def preprocess_data(dir_root, args):
    """
    Preprocess the training and test data.
    """
    # Read from the csv files
    train_data = pd.read_csv(
        os.path.join(dir_root, args["file_train_data"]),
        sep=",",
    )
    test_data = pd.read_csv(
        os.path.join(dir_root, args["file_test_data"]),
        sep=",",
    )

    # Fix the last motif ID
    train_data[train_data == 17] = 14
    train_data.columns = [
        "Cell Number",
        "motif",
        "motif.1",
        "motif.2",
        "motif.3",
        "motif.4",
        "Nalm 6 Cytotoxicity",
    ]
    test_data[test_data == 17] = 14
    test_data.columns = [
        "Cell Number",
        "motif",
        "motif.1",
        "motif.2",
        "motif.3",
        "motif.4",
        "Nalm 6 Cytotoxicity",
    ]

    # Adjust motif at the third position
    if args["filter_for_spacer_motif_third_position"]:
        train_data = train_data[
            (train_data["motif.2"] == 14) | (train_data["motif.2"] == 0)
        ]
        test_data = test_data[
            (test_data["motif.2"] == 14) | (test_data["motif.2"] == 0)
        ]

    train_data = train_data[
        args["motifs_to_use"] + [args["label_name"], "Cell Number"]
    ]
    test_data = test_data[
        args["motifs_to_use"] + [args["label_name"], "Cell Number"]
    ]

    # Adjust motif at the last position
    if not args["allow_spacer_motif_last_position"]:
        last_motif = args["motifs_to_use"][len(args["motifs_to_use"]) - 1]
        train_data = train_data[
            (train_data[last_motif] != 14) & (train_data[last_motif] != 0)
        ]
        test_data = test_data[
            (test_data[last_motif] != 14) & (test_data[last_motif] != 0)
        ]

    # Get the labels
    train_labels = np.array(train_data[args["label_name"]])
    test_labels = np.array(test_data[args["label_name"]])

    # For the classification task use the threshold to binarize labels
    train_labels[train_labels > args["label_binarization_threshold"]] = 1
    train_labels[train_labels < 1] = args["min_label_value"]
    test_labels[test_labels > args["label_binarization_threshold"]] = 1
    test_labels[test_labels < 1] = args["min_label_value"]

    # Reduce data to just the motifs of interest
    train_data = train_data[args["motifs_to_use"]]
    test_data = test_data[args["motifs_to_use"]]

    # Get the class and motif counts
    min_class = np.min(np.unique(np.concatenate([train_data, test_data])))
    max_class = np.max(np.unique(np.concatenate([train_data, test_data])))

    num_class = max_class - min_class + 1
    num_motifs = len(args["motifs_to_use"])
    print(str(max_class) + ":" + str(min_class) + ":" + str(num_class))

    train_data = train_data - min_class
    test_data = test_data - min_class

    return (
        train_data,
        test_data,
        train_labels,
        test_labels,
        num_class,
        num_motifs,
    )


def data_encoder(args, train_data, test_data, num_class, num_motifs):
    """
    Use one-hot or binary encoding for classical data representation.
    """
    if args["encoder"] == "one-hot":
        # Transform to one-hot encoding
        train_data = np.eye(num_class)[train_data]
        test_data = np.eye(num_class)[test_data]

        train_data = train_data.reshape(
            train_data.shape[0], train_data.shape[1] * train_data.shape[2]
        )
        test_data = test_data.reshape(
            test_data.shape[0], test_data.shape[1] * test_data.shape[2]
        )

    elif args["encoder"] == "binary":
        # Transform to binary encoding
        encoder = ce.BinaryEncoder()

        base_array = np.unique(np.concatenate([train_data, test_data]))
        base = pd.DataFrame(base_array).astype("category")
        base.columns = ["motif"]
        for motif_name in args["motifs_to_use"][1:]:
            base[motif_name] = base.loc[:, "motif"]
        encoder.fit(base)

        train_data = encoder.transform(train_data.astype("category"))
        test_data = encoder.transform(test_data.astype("category"))

        train_data = np.reshape(
            train_data.values, (train_data.shape[0], num_motifs, -1)
        )
        test_data = np.reshape(
            test_data.values, (test_data.shape[0], num_motifs, -1)
        )

        train_data = train_data.reshape(
            train_data.shape[0], train_data.shape[1] * train_data.shape[2]
        )
        test_data = test_data.reshape(
            test_data.shape[0], test_data.shape[1] * test_data.shape[2]
        )

    else:
        raise ValueError("Invalid encoding type.")

    return train_data, test_data

다음 셀을 실행하면 필요한 폴더 구조를 자동으로 생성하고 교육 및 테스트 파일을 모두 사용자 환경에 직접 다운로드하여 이 튜토리얼을 실행할 수 있습니다. 이러한 파일이 이미 로컬에 있는 경우 이 단계를 수행하면 버전 일관성을 유지하기 위해 안전하게 덮어쓰게 됩니다.

## Download dataset


def download_pqk_dataset(data_dir="data_tutorial/pqk"):
    """Download the four CSV files from the Qiskit documentation repo."""
    data_dir = Path(data_dir)
    data_dir.mkdir(parents=True, exist_ok=True)

    base_url = (
        "https://raw.githubusercontent.com/Qiskit/documentation/main/"
        "datasets/tutorials/pqk"
    )
    files = [
        "train_data.csv",
        "test_data.csv",
        "projections_train.csv",
        "projections_test.csv",
    ]

    for filename in files:
        url = f"{base_url}/{filename}"
        dest = data_dir / filename
        print(f"  {filename} ...", end=" ", flush=True)
        urllib.request.urlretrieve(url, dest)
        print(f"OK ({dest.stat().st_size:,} bytes)")

    print(f"\nAll files saved to {data_dir}/")
    return data_dir


DATA_DIR = download_pqk_dataset()

Output:

  train_data.csv ... OK (5,012 bytes)
  test_data.csv ... OK (2,194 bytes)
  projections_train.csv ... OK (779,730 bytes)
  projections_test.csv ... OK (335,529 bytes)

All files saved to data_tutorial/pqk/
args = {
    "file_train_data": "train_data.csv",
    "file_test_data": "test_data.csv",
    "motifs_to_use": ["motif", "motif.1", "motif.2", "motif.3"],
    "label_name": "Nalm 6 Cytotoxicity",
    "label_binarization_threshold": 0.62,
    "filter_for_spacer_motif_third_position": False,
    "allow_spacer_motif_last_position": True,
    "min_label_value": -1,
    "encoder": "one-hot",
}

# dir_root points to the folder where the downloaded CSVs live
dir_root = str(DATA_DIR)

# Preprocess data
train_data, test_data, train_labels, test_labels, num_class, num_motifs = (
    preprocess_data(dir_root=dir_root, args=args)
)

# Encode the data
train_data, test_data = data_encoder(
    args, train_data, test_data, num_class, num_motifs
)

Output:

14:0:15

또한 확장을 위해 데이터 집합을 변환하여 11π/2\pi/2 으로 표시합니다.

# Change 1 to pi/2
angle = np.pi / 2

tmp = pd.DataFrame(train_data).astype("float64")
tmp[tmp == 1] = angle
train_data = tmp.values

tmp = pd.DataFrame(test_data).astype("float64")
tmp[tmp == 1] = angle
test_data = tmp.values

학습 및 테스트 데이터 세트의 크기와 모양을 확인합니다.

print(train_data.shape, train_labels.shape)
print(test_data.shape, test_labels.shape)

Output:

(172, 60) (172,)
(74, 60) (74,)

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

양자 회로

이제 기존 데이터 집합을 더 높은 차원의 특징 공간에 포함시키는 특징 맵을 구성합니다. 이 임베딩을 위해, 우리는 ZZFeatureMap 를 사용합니다.

feature_dimension = train_data.shape[1]
reps = 24
insert_barriers = True
entanglement = "pairwise"

# ZZFeatureMap with linear entanglement and a repetition of 2
embed = ZZFeatureMap(
    feature_dimension=feature_dimension,
    reps=reps,
    entanglement=entanglement,
    insert_barriers=insert_barriers,
    name="ZZFeatureMap",
)
embed.decompose().draw(output="mpl", style="iqp", fold=-1)

Output:

Output of the previous code cell

또 다른 양자 임베딩 옵션은 1D-Heisenberg 해밀턴 진화 안사츠입니다. ZZFeatureMap 를 계속 진행하려면 이 섹션 실행을 건너뛰셔도 됩니다.

feature_dimension = train_data.shape[1]
num_qubits = feature_dimension + 1
embed2 = QuantumCircuit(num_qubits)
num_trotter_steps = 6
pv_length = feature_dimension * num_trotter_steps
pv = ParameterVector("theta", pv_length)

# Add Haar random single qubit unitary to each qubit as initial state
np.random.seed(42)
seeds_unitary = np.random.randint(0, 100, num_qubits)
for i in range(num_qubits):
    rand_gate = UnitaryGate(random_unitary(2, seed=seeds_unitary[i]))
    embed2.append(rand_gate, [i])


def trotter_circ(feature_dimension, num_trotter_steps):
    num_qubits = feature_dimension + 1
    circ = QuantumCircuit(num_qubits)
    # Even
    for i in range(0, feature_dimension, 2):
        circ.rzz(2 * pv[i] / num_trotter_steps, i, i + 1)
    for i in range(0, feature_dimension, 2):
        circ.rxx(2 * pv[i] / num_trotter_steps, i, i + 1)
    for i in range(0, feature_dimension, 2):
        circ.ryy(2 * pv[i] / num_trotter_steps, i, i + 1)
    # Odd
    for i in range(1, feature_dimension, 2):
        circ.rzz(2 * pv[i] / num_trotter_steps, i, i + 1)
    for i in range(1, feature_dimension, 2):
        circ.rxx(2 * pv[i] / num_trotter_steps, i, i + 1)
    for i in range(1, feature_dimension, 2):
        circ.ryy(2 * pv[i] / num_trotter_steps, i, i + 1)
    return circ


# Hamiltonian evolution ansatz
for step in range(num_trotter_steps):
    circ = trotter_circ(feature_dimension, num_trotter_steps)
    if step % 2 == 0:
        embed2 = embed2.compose(circ)
    else:
        reverse_circ = circ.reverse_ops()
        embed2 = embed2.compose(reverse_circ)


embed2.draw(output="mpl", style="iqp", fold=-1)

Output:

Output of the previous code cell

3단계: Qiskit primitives 명령어로 실행합니다

조치 1-RDMs

이 단계에서는 양자 특징 맵에 대한 투영 측정을 통해 모든 단일 큐비트 축소 밀도 행렬(1-RDM)을 구하며, 이 결과는 나중에 고전적 지수 커널 함수에 입력될 것입니다.

모든 데이터를 실행하기 전에 데이터 세트에서 단일 데이터 포인트가 주어졌을 때 1-RDM을 계산하는 방법을 살펴보겠습니다. 1-RDM은 모든 큐비트에 대한 폴리 X, YZ 연산자의 단일 큐비트 측정값 모음입니다. 이는 단일 큐비트 RDM을 다음과 같이 완전히 표현할 수 있기 때문입니다: ρ=12(I+σxσx+σyσy+σzσz)\rho = \frac{1}{2} \big( I + \braket \sigma_x \sigma_x + \braket \sigma_y \sigma_y + \braket \sigma_z \sigma_z \big)

먼저 사용할 백엔드를 선택합니다.

service = QiskitRuntimeService()
backend = service.least_busy(
    operational=True, simulator=False, min_num_qubits=133
)
target = backend.target

그런 다음 양자 회로를 실행하고 투영을 측정합니다. ZNE(제로 노이즈 추정)를 포함한 오류 완화 기능을 사용하도록 설정합니다.

# Let's select the ZZFeatureMap embedding for this example
qc = embed
num_qubits = feature_dimension

# Identity operator on all qubits
id = "I" * num_qubits

# Let's select the first training datapoint as an example
parameters = train_data[0]

# Bind parameter to the circuit and simplify it
qc_bound = qc.assign_parameters(parameters)
transpiler = generate_preset_pass_manager(
    optimization_level=3, basis_gates=["u3", "cz"]
)
transpiled_circuit = transpiler.run(qc_bound)

# Transpile for hardware
transpiler = generate_preset_pass_manager(optimization_level=3, target=target)
transpiled_circuit = transpiler.run(transpiled_circuit)

# We group all commuting observables
# These groups are the Pauli X, Y and Z operators on individual qubits
observables_x = [
    SparsePauliOp(id[:i] + "X" + id[(i + 1) :]).apply_layout(
        transpiled_circuit.layout
    )
    for i in range(num_qubits)
]
observables_y = [
    SparsePauliOp(id[:i] + "Y" + id[(i + 1) :]).apply_layout(
        transpiled_circuit.layout
    )
    for i in range(num_qubits)
]
observables_z = [
    SparsePauliOp(id[:i] + "Z" + id[(i + 1) :]).apply_layout(
        transpiled_circuit.layout
    )
    for i in range(num_qubits)
]

# We define the primitive unified blocs (PUBs) consisting of the embedding circuit,
# set of observables and the circuit parameters
pub_x = (transpiled_circuit, observables_x)
pub_y = (transpiled_circuit, observables_y)
pub_z = (transpiled_circuit, observables_z)

# Experiment options for error mitigation
num_randomizations = 300
shots_per_randomization = 100
noise_factors = [1, 3, 5]

experimental_opts = {}
experimental_opts["resilience"] = {
    "measure_mitigation": True,
    "zne_mitigation": True,
    "zne": {
        "noise_factors": noise_factors,
        "amplifier": "gate_folding",
        "extrapolated_noise_factors": [0] + noise_factors,
    },
}
experimental_opts["twirling"] = {
    "num_randomizations": num_randomizations,
    "shots_per_randomization": shots_per_randomization,
    "strategy": "active-accum",
}

# We define and run the estimator to obtain <X>, <Y> and <Z> on all qubits
estimator = Estimator(mode=backend, options=experimental_opts)

job = estimator.run([pub_x, pub_y, pub_z])

다음으로 결과를 검색합니다.

job_result_x = job.result()[0].data.evs
job_result_y = job.result()[1].data.evs
job_result_z = job.result()[2].data.evs
print(job_result_x)
print(job_result_y)
print(job_result_z)

Output:

[ 0.03530987 -0.06207794 -0.03529884 -0.1418671   0.00209782  0.0045834
  0.00407694  0.02528003  0.00233791  0.01800766  0.00718357  0.01927931
  0.0073651  -0.02009021  0.01144208  0.01333925  0.00521008  0.00535276
 -0.04354042 -0.0383848  -0.04472125  0.00641964 -0.03954627  0.03207479
  0.01823132  0.02546267 -0.          0.16288225  0.03246113  0.
  0.06107868  0.01082782  0.00240078  0.13147612  0.14033432  0.14925945
  0.11577918  0.00016128 -0.          0.00604693  0.02433089  0.02033885
  0.01492506  0.00494294  0.00926954  0.00569533  0.09867722  0.05662552
 -0.00001734  0.          0.          0.04625459 -0.02480763  0.01360688
  0.11511306  0.01260572 -0.01656313 -0.02510078 -0.03256272  0.00058607]
[-0.0756078  -0.05445208 -0.0228333  -0.00015029  0.00006226  0.02925132
 -0.00325556 -0.00889965  0.0177611  -0.00437065  0.01682502 -0.00229805
 -0.01041899 -0.03208967 -0.03515749  0.17477371  0.03783633  0.2126005
  0.          0.          0.00754466 -0.08242599  0.          0.03263675
  0.00399151 -0.01984418 -0.02106749 -0.02580491  0.03973411 -0.02037816
 -0.01769352 -0.09720746  0.00098896 -0.11840454  0.14392615  0.13647983
  0.08683845  0.04492138  0.0046172   0.04171398 -0.0000869  -0.00270916
 -0.0019876  -0.00440696  0.0307905  -0.0284622   0.11237189  0.15042867
  0.1020601  -0.03812461  0.00302523 -0.05240398 -0.01304566 -0.00403933
 -0.01324601 -0.03658085  0.00934269 -0.00105112 -0.          0.01761827]
[ 0.57921657  0.2865493   0.          0.00028356  0.03177571  0.01152152
  0.00843001  0.02320127  0.00273558  0.00976802  0.00060077  0.00942531
  0.00096361 -0.03950026  0.00560635  0.00591487  0.00788236  0.01346192
  0.60752971  0.80203507  0.65649176  0.00069473  0.06010304  0.05922109
  0.01670672  0.02900743  0.0162253   0.0668811   0.01573204 -0.00288162
  0.04216451  0.00848301  0.00052577 -0.33798808  0.68075471  0.89471233
  0.72272544  0.08096828  0.02387351  0.01723619  0.00774532  0.05513527
  0.08285531  0.08102448  0.10677406  0.27778995  0.28883482  0.21497224
  0.17569826  0.00063149  0.0320076   0.06735008 -0.00053637 -0.0006907
  0.00991596  0.00414575 -0.08425133 -0.09569482  0.00219474  0.00241873]

회로 크기와 2쿼비트 게이트 깊이를 출력합니다.

print(f"qubits: {qc.num_qubits}")
print(
    f"2q-depth: {transpiled_circuit.depth(lambda x: x.operation.num_qubits==2)}"
)
print(
    f"2q-size: {transpiled_circuit.size(lambda x: x.operation.num_qubits==2)}"
)
print(f"Operator counts: {transpiled_circuit.count_ops()}")
transpiled_circuit.draw("mpl", fold=-1, style="clifford", idle_wires=False)

Output:

qubits: 60
2q-depth: 96
2q-size: 2832
Operator counts: OrderedDict([('rz', 8640), ('sx', 7104), ('cz', 2832), ('x', 720), ('barrier', 47)])
Output of the previous code cell

이제 전체 학습 데이터 세트를 반복하여 모든 1-RDM을 얻을 수 있습니다.

또한 양자 하드웨어에서 실행한 실험의 결과도 제공합니다. 아래 플래그( True)를 설정하여 직접 교육을 실행하거나 저희가 제공하는 투영 결과를 사용할 수 있습니다.

# Set this to True if you want to run the training on hardware
run_experiment = False
# Identity operator on all qubits
id = "I" * num_qubits

# projections_train[i][j][k] will be the expectation value of the j-th
# Pauli operator (0: X, 1: Y, 2: Z) of datapoint i on qubit k
projections_train = []
jobs_train = []

# Experiment options for error mitigation
num_randomizations = 300
shots_per_randomization = 100
noise_factors = [1, 3, 5]

experimental_opts = {}
experimental_opts["resilience"] = {
    "measure_mitigation": True,
    "zne_mitigation": True,
    "zne": {
        "noise_factors": noise_factors,
        "amplifier": "gate_folding",
        "return_all_extrapolated": True,
        "return_unextrapolated": True,
        "extrapolated_noise_factors": [0] + noise_factors,
    },
}
experimental_opts["twirling"] = {
    "num_randomizations": num_randomizations,
    "shots_per_randomization": shots_per_randomization,
    "strategy": "active-accum",
}
options = EstimatorOptions(experimental=experimental_opts)

if run_experiment:
    with Batch(backend=backend):
        for i in tqdm.tqdm(
            range(len(train_data)), desc="Training data progress"
        ):
            # Get training sample
            parameters = train_data[i]

            # Bind parameter to the circuit and simplify it
            qc_bound = qc.assign_parameters(parameters)
            transpiler = generate_preset_pass_manager(
                optimization_level=3, basis_gates=["u3", "cz"]
            )
            transpiled_circuit = transpiler.run(qc_bound)

            # Transpile for hardware
            transpiler = generate_preset_pass_manager(
                optimization_level=3, target=target
            )
            transpiled_circuit = transpiler.run(transpiled_circuit)

            # We group all commuting observables
            # These groups are the Pauli X, Y and Z operators on individual qubits
            observables_x = [
                SparsePauliOp(id[:i] + "X" + id[(i + 1) :]).apply_layout(
                    transpiled_circuit.layout
                )
                for i in range(num_qubits)
            ]
            observables_y = [
                SparsePauliOp(id[:i] + "Y" + id[(i + 1) :]).apply_layout(
                    transpiled_circuit.layout
                )
                for i in range(num_qubits)
            ]
            observables_z = [
                SparsePauliOp(id[:i] + "Z" + id[(i + 1) :]).apply_layout(
                    transpiled_circuit.layout
                )
                for i in range(num_qubits)
            ]

            # We define the primitive unified blocs (PUBs) consisting
            # of the embedding circuit,
            # set of observables and the circuit parameters
            pub_x = (transpiled_circuit, observables_x)
            pub_y = (transpiled_circuit, observables_y)
            pub_z = (transpiled_circuit, observables_z)

            # We define and run the estimator to obtain <X>, <Y> and <Z>
            # on all qubits
            estimator = Estimator(options=options)

            job = estimator.run([pub_x, pub_y, pub_z])
            jobs_train.append(job)

작업이 완료되면 결과를 검색할 수 있습니다.

if run_experiment:
    for i in tqdm.tqdm(
        range(len(train_data)), desc="Retrieving training data results"
    ):
        # Completed job
        job = jobs_train[i]

        # Job results
        job_result_x = job.result()[0].data.evs
        job_result_y = job.result()[1].data.evs
        job_result_z = job.result()[2].data.evs

        # Record <X>, <Y> and <Z> on all qubits for the current datapoint
        projections_train.append([job_result_x, job_result_y, job_result_z])

테스트 세트에 대해 이 과정을 반복합니다.

# Identity operator on all qubits
id = "I" * num_qubits

# projections_test[i][j][k] will be the expectation value of the
# j-th Pauli operator (0: X, 1: Y, 2: Z) of datapoint i on qubit k
projections_test = []
jobs_test = []

# Experiment options for error mitigation
num_randomizations = 300
shots_per_randomization = 100
noise_factors = [1, 3, 5]

experimental_opts = {}
experimental_opts["resilience"] = {
    "measure_mitigation": True,
    "zne_mitigation": True,
    "zne": {
        "noise_factors": noise_factors,
        "amplifier": "gate_folding",
        "return_all_extrapolated": True,
        "return_unextrapolated": True,
        "extrapolated_noise_factors": [0] + noise_factors,
    },
}
experimental_opts["twirling"] = {
    "num_randomizations": num_randomizations,
    "shots_per_randomization": shots_per_randomization,
    "strategy": "active-accum",
}
options = EstimatorOptions(experimental=experimental_opts)

if run_experiment:
    with Batch(backend=backend):
        for i in tqdm.tqdm(range(len(test_data)), desc="Test data progress"):
            # Get test sample
            parameters = test_data[i]

            # Bind parameter to the circuit and simplify it
            qc_bound = qc.assign_parameters(parameters)
            transpiler = generate_preset_pass_manager(
                optimization_level=3, basis_gates=["u3", "cz"]
            )
            transpiled_circuit = transpiler.run(qc_bound)

            # Transpile for hardware
            transpiler = generate_preset_pass_manager(
                optimization_level=3, target=target
            )
            transpiled_circuit = transpiler.run(transpiled_circuit)

            # We group all commuting observables
            # These groups are the Pauli X, Y and Z operators on individual qubits
            observables_x = [
                SparsePauliOp(id[:i] + "X" + id[(i + 1) :]).apply_layout(
                    transpiled_circuit.layout
                )
                for i in range(num_qubits)
            ]
            observables_y = [
                SparsePauliOp(id[:i] + "Y" + id[(i + 1) :]).apply_layout(
                    transpiled_circuit.layout
                )
                for i in range(num_qubits)
            ]
            observables_z = [
                SparsePauliOp(id[:i] + "Z" + id[(i + 1) :]).apply_layout(
                    transpiled_circuit.layout
                )
                for i in range(num_qubits)
            ]

            # We define the primitive unified blocs (PUBs) consisting of
            # the embedding circuit,
            # set of observables and the circuit parameters
            pub_x = (transpiled_circuit, observables_x)
            pub_y = (transpiled_circuit, observables_y)
            pub_z = (transpiled_circuit, observables_z)

            # We define and run the estimator to obtain <X>, <Y> and <Z> on all qubits
            estimator = Estimator(options=options)

            job = estimator.run([pub_x, pub_y, pub_z])
            jobs_test.append(job)

이전과 마찬가지로 결과를 검색할 수 있습니다.

if run_experiment:
    for i in tqdm.tqdm(
        range(len(test_data)), desc="Retrieving test data results"
    ):
        # Completed job
        job = jobs_test[i]

        # Job results
        job_result_x = job.result()[0].data.evs
        job_result_y = job.result()[1].data.evs
        job_result_z = job.result()[2].data.evs

        # Record <X>, <Y> and <Z> on all qubits for the current datapoint
        projections_test.append([job_result_x, job_result_y, job_result_z])

4단계: 후처리 수행 및 원하는 클래식 형식으로 결과 반환

예측된 양자 커널 정의

투영된 양자 커널은 다음 커널 함수로 정의됩니다: kPQ(xi,xj)=exp(γkP{X,Y,Z}(Tr[Pρk(xi)]Tr[Pρk(xj)])2)k^{\textrm{PQ}}(x_i, x_j) = \textrm{exp} \Big(-\gamma \sum_k \sum_{P \in \{ X,Y,Z \}} (\textrm{Tr}[P \rho_k(x_i)] - \textrm{Tr}[P \rho_k(x_j)])^2 \Big) 위의 방정식에서 γ>0\gamma>0 는 조정 가능한 하이퍼파라미터입니다. KijPQ=kPQ(xi,xj)K^{\textrm{PQ}}_{ij} = k^{\textrm{PQ}}(x_i, x_j) 은 커널 매트릭스 KPQK^{\textrm{PQ}} 의 항목입니다.

1-RDM의 정의를 사용하면 커널 함수 내의 개별 용어는 Tr[Pρk(xi)]=P\textrm{Tr}[P \rho_k (x_i)] = \braket P 로 평가할 수 있으며, 여기서 P{X,Y,Z}P \in \{ X,Y,Z \}. 이러한 기대값은 위에서 측정한 것과 정확히 일치합니다.

scikit-learn 을 사용하면 실제로 커널을 훨씬 더 쉽게 계산할 수 있습니다. 이는 쉽게 사용할 수 있는 방사형 기준 함수('rbf') 커널( exp(γxx2) \textrm{exp} (-\gamma \lVert x - x' \rVert^2) ) 때문입니다. 먼저, 새로 투영된 훈련 및 테스트 데이터 세트를 2차원 배열로 재구성하기만 하면 됩니다.

전체 데이터 집합을 검토하는 데는 QPU에서 약 80분이 소요될 수 있습니다. 튜토리얼의 나머지 부분을 쉽게 실행할 수 있도록 이전에 실행한 실험의 예제를 추가로 제공합니다( Download dataset 코드 블록에서 다운로드한 파일에 포함되어 있음). 직접 교육을 수행한 경우 자신의 결과를 가지고 튜토리얼을 계속 진행할 수 있습니다.

# ---------------------------------------------------------------------------
# Load projections — either from the hardware run above, or from the
# pre-computed CSVs that were downloaded alongside the motif data.
# ---------------------------------------------------------------------------

if run_experiment:
    projections_train = np.array(projections_train).reshape(
        len(projections_train), -1
    )
    projections_test = np.array(projections_test).reshape(
        len(projections_test), -1
    )
else:
    projections_train = np.loadtxt(DATA_DIR / "projections_train.csv")
    projections_test = np.loadtxt(DATA_DIR / "projections_test.csv")

지원 벡터 기계(SVM)

이제 이 사전 계산된 커널에서 클래식 SVM을 실행하고 테스트 세트와 학습 세트 사이에서 커널을 사용하여 예측할 수 있습니다.

# Range of 'C' and 'gamma' values as SVC hyperparameters.
#
# This is a reduced grid so the tutorial runs quickly (154 candidates).
# The optimal (C, gamma) reported below lie within it, so the results
# are unchanged. The full grid used originally had 6622 candidates and
# took roughly one hours to search:
#
#   C_range = [0.001, 0.005, 0.007]
#   C_range.extend([x * 0.01 for x in range(1, 11)])   # 0.01 .. 0.10
#   C_range.extend([x * 0.25 for x in range(1, 60)])   # 0.25 .. 14.75
#   C_range.extend([20, 50, 100, 200, 500, 700, 1000,
#                   1100, 1200, 1300, 1400, 1500, 1700, 2000])
#   gamma_range = ["auto", "scale", 0.001, 0.005, 0.007]
#   gamma_range.extend([x * 0.01 for x in range(1, 11)])
#   gamma_range.extend([x * 0.25 for x in range(1, 60)])
#   gamma_range.extend([20, 50, 100])

C_range = [
    0.001,
    0.01,
    0.1,
    0.5,
    1.0,
    2.0,
    4.0,
    6.0,
    8.5,
    10.75,
    14.0,
    20,
    50,
    100,
]
gamma_range = [
    0.001,
    0.005,
    0.007,
    0.01,
    0.02,
    0.03,
    0.04,
    0.05,
    0.1,
    0.5,
    1.0,
]

param_grid = dict(C=C_range, gamma=gamma_range)

# Support vector classifier
svc = SVC(kernel="rbf")

# Define the cross validation
cv = StratifiedKFold(n_splits=10)

# Grid search for hyperparameter tuning (q: quantum)
grid_search_q = GridSearchCV(
    svc, param_grid, cv=cv, verbose=1, n_jobs=-1, scoring="f1_weighted"
)
grid_search_q.fit(projections_train, train_labels)

# Best model with best parameters
best_svc_q = grid_search_q.best_estimator_
print(
    f"The best parameters are {grid_search_q.best_params_} with a score of {grid_search_q.best_score_:.4f}"
)

# Test accuracy
accuracy_q = best_svc_q.score(projections_test, test_labels)
print(f"Test accuracy with best model: {accuracy_q:.4f}")

Output:

Fitting 10 folds for each of 154 candidates, totalling 1540 fits
The best parameters are {'C': 8.5, 'gamma': 0.01} with a score of 0.6980
Test accuracy with best model: 0.8108

고전적 벤치마킹

양자 투영을 하지 않고도 방사형 기저 함수를 커널로 사용하는 고전적인 SVM을 실행할 수 있습니다. 이 결과는 저희의 대표적인 벤치마크입니다.

# Support vector classifier
svc = SVC(kernel="rbf")

# Grid search for hyperparameter tuning (c: classical)
grid_search_c = GridSearchCV(
    svc, param_grid, cv=cv, verbose=1, n_jobs=-1, scoring="f1_weighted"
)
grid_search_c.fit(train_data, train_labels)

# Best model with best parameters
best_svc_c = grid_search_c.best_estimator_
print(
    f"The best parameters are {grid_search_c.best_params_} with a score of {grid_search_c.best_score_:.4f}"
)

# Test accuracy
accuracy_c = best_svc_c.score(test_data, test_labels)
print(f"Test accuracy with best model: {accuracy_c:.4f}")

Output:

Fitting 10 folds for each of 154 candidates, totalling 1540 fits
The best parameters are {'C': 10.75, 'gamma': 0.04} with a score of 0.7830
Test accuracy with best model: 0.7432

부록: 학습 작업에서 데이터셋의 잠재적 양자 우위 검증

모든 데이터 세트가 PQK 사용으로 잠재적인 이점을 제공하는 것은 아닙니다. 특정 데이터 세트가 PQK의 이점을 누릴 수 있는지 확인하기 위한 예비 테스트로 사용할 수 있는 몇 가지 이론적 한계가 있습니다. 이를 정량화하기 위해 양자 머신 러닝에서 데이터의 힘 [2의] 저자는 고전적 모델과 양자 모델의 복잡도와 고전적 모델과 양자 모델의 기하학적 분리라는 수치를 정의합니다. PQK에서 잠재적인 양자 이점을 기대하려면 기존 커널과 양자 투영 커널 사이의 기하학적 분리가 대략 N\sqrt{N} 정도여야 하며, 여기서 NN 은 훈련 샘플의 수입니다. 이 조건이 충족되면 모델 복잡도를 확인하는 단계로 넘어갑니다. 고전적 모델 복잡도가 NN 정도이고 양자 투영 모델 복잡도가 NN 보다 훨씬 작다면, PQK의 잠재적 이점을 기대할 수 있습니다.

기하학적 분리는 다음과 같이 정의됩니다( [2] 의 F19 ): gcq=g(KcKq)=KqKc(Kc+λI)2KcKqg_{cq} = g(K^c \Vert K^q) = \sqrt{\Vert \sqrt{K^q} \sqrt{K^c} (K^c + \lambda I)^{-2} \sqrt{K^c} \sqrt{K^q}\Vert_{\infty}}

# Gamma values used in best models above
gamma_c = grid_search_c.best_params_["gamma"]
gamma_q = grid_search_q.best_params_["gamma"]

# Regularization parameter used in the best classical model above
C_c = grid_search_c.best_params_["C"]
l_c = 1 / C_c

# Classical and quantum kernels used above
K_c = rbf_kernel(train_data, train_data, gamma=gamma_c)
K_q = rbf_kernel(projections_train, projections_train, gamma=gamma_q)

# Intermediate matrices in the equation
K_c_sqrt = sqrtm(K_c)
K_q_sqrt = sqrtm(K_q)
K_c_inv = inv(K_c + l_c * np.eye(K_c.shape[0]))
K_multiplication = (
    K_q_sqrt @ K_c_sqrt @ K_c_inv @ K_c_inv @ K_c_sqrt @ K_q_sqrt
)

# Geometric separation
norm = np.linalg.norm(K_multiplication, ord=np.inf)
g_cq = np.sqrt(norm)
print(
    f"Geometric separation between classical and quantum kernels is {g_cq:.4f}"
)

print(np.sqrt(len(train_data)))

Output:

Geometric separation between classical and quantum kernels is 1.5440
13.114877048604

모델 복잡도는 다음과 같이 정의됩니다( [2] 의 M1 ): sK,λ(N)=λ2i=1Nj=1N(K+λI)ij2yiyjN+i=1Nj=1N((K+λI)1K(K+λI)1)ijyiyjN s_{K, \lambda}(N) = \sqrt{\frac{\lambda^2 \sum_{i=1}^N \sum_{j=1}^N (K+\lambda I)^{-2}_{ij} y_i y_j}{N}} + \sqrt{\frac{\sum_{i=1}^N \sum_{j=1}^N ((K+\lambda I)^{-1}K(K+\lambda I)^{-1})_{ij} y_i y_j}{N}}

# Model complexity of the classical kernel

# Number of training data
N = len(train_data)

# Predicted labels
pred_labels = best_svc_c.predict(train_data)
pred_matrix = np.outer(pred_labels, pred_labels)

# Intermediate terms
K_c_inv = inv(K_c + l_c * np.eye(K_c.shape[0]))

# First term
first_sum = np.sum((K_c_inv @ K_c_inv) * pred_matrix)
first_term = l_c * np.sqrt(first_sum / N)

# Second term
second_sum = np.sum((K_c_inv @ K_c @ K_c_inv) * pred_matrix)
second_term = np.sqrt(second_sum / N)

# Model complexity
s_c = first_term + second_term
print(f"Classical model complexity is {s_c:.4f}")

Output:

Classical model complexity is 1.3578
# Model complexity of the projected quantum kernel

# Number of training data
N = len(projections_train)

# Predicted labels
pred_labels = best_svc_q.predict(projections_train)
pred_matrix = np.outer(pred_labels, pred_labels)

# Regularization parameter used in the best classical model above
C_q = grid_search_q.best_params_["C"]
l_q = 1 / C_q

# Intermediate terms
K_q_inv = inv(K_q + l_q * np.eye(K_q.shape[0]))

# First term
first_sum = np.sum((K_q_inv @ K_q_inv) * pred_matrix)
first_term = l_q * np.sqrt(first_sum / N)

# Second term
second_sum = np.sum((K_q_inv @ K_q @ K_q_inv) * pred_matrix)
second_term = np.sqrt(second_sum / N)

# Model complexity
s_q = first_term + second_term
print(f"Quantum model complexity is {s_q:.4f}")

Output:

Quantum model complexity is 1.5806

다음 단계

권장사항

이 글이 흥미로웠다면, 다음 자료도 참고해 보시기 바랍니다:


참조

  1. 우트로, 필리포 외. "양자 커널 방법을 통한 CAR T세포 세포 독성 예측 향상." arXiv 사전 인쇄 arXiv:2507.22710 (2025).
  2. "양자 머신 러닝에서 데이터의 힘." 네이처 커뮤니케이션즈 12.1 (2021): 2631.
  3. 다니엘스, 카일 G., "조합 신호 모티프 라이브러리와 머신 러닝을 이용한 CAR T 세포 표현형 해독." 과학 378.6625 (2022): 1194-1200.
이 페이지가 도움이 되었습니까?
GitHub에서 버그, 오타를 보고하거나 컨텐츠를 요청하십시오.