量子カーネル
量子カーネルの概要
「量子カーネル法」とは、量子コンピューターを使用してカーネルを推定するあらゆる手法を指します。この文脈での「カーネル」とは、カーネル行列またはその個々の要素を意味します。特徴マッピング は、 から への写像であり、通常 が成り立ちます。この写像の目的は、データのカテゴリを超平面によって分離可能にすることです。カーネル関数は特徴写像された空間のベクトルを引数として取り、それらの内積を返します。すなわち、、 です。古典的な文脈では、カーネル関数が評価しやすい特徴マップが求められます。これはしばしば、 や を明示的に構成することなく、元のデータベクトルを用いて特徴写像された空間での内積を表現できるカーネル関数を見つけることを意味します。量子カーネル法では、特徴マッピングは量子回路によって行われ、カーネルはその回路の測定と相対的な測定確率を使用して推定されます。
このレッスンでは、大量のエンタングルメントを使用する事前定義されたエンコーディング回路の深さを調べ、手動でコーディングした回路の深さと比較します。これは一方の手法を推奨するものではありません。事前定義された回路が深すぎる場合や、カスタム構築された回路のエンタングルメントが不十分な場合もあります。これらはあくまで探索を可能にするために紹介しています。
カーネル行列推定の詳細を説明する前に、Qiskit パターンの言語を使ってワークフローの概要を説明します。
ステップ 1: 古典的な入力を量子問題にマッピングする
- 入力: トレーニングデータセット
- 出力: カーネル行列要素を計算するための抽象回路
データセットが与えられたら、最初のステップはデータを量子回路にエンコードすることです。言い換えると、データを量子コンピューターの状態のヒルベルト空間にマッピングする必要があります。これはデータに依存する回路を構築することで実現します。方法は多数あり、前のレッスンでいくつかの選択肢を紹介しました。独自の回路を構築してデータをエンコードすることもできますし、zz_feature_map のような事前定義された特徴マップを使用することもできます。このレッスンでは、その両方を行います。
1つのカーネル行列要素を計算するためには、2つの異なる点をエンコードして内積を推定する必要があります。完全な量子カーネルワークフローには、写像されたデータベクトル間の多数の内積と古典的な機械学習手法が含まれます。しかし、繰り返される核心的なステップは1つのカーネル行列要素の推定です。そのために、データに依存する量子回路を選択し、2つのデータベクトルを特徴空間にマッピングします。

カーネル行列を生成するタスクでは、すべての 量子ビットが 状態にある 状態を測定する確率に特に注目します。これを理解するために、データベクトル のエンコードと写像を担う回路を 、 のそれを と表し、写像された状態を次のように定義します:
これらの状態はデータの高次元への写像そのものであり、求めるカーネル要素は内積
です。デフォルトの初期状態 に対して両回路 と を作 用させたとき、状態 が測定される確率は
です。これはまさに求めたい値( まで)です。回路の測定層は測定確率(または特定のエラー軽減手法を使用した場合は「準確率」)を返します。注目する確率はゼロ状態 のものです。
ステップ 2: 量子実行のために問題を最適化する
- 入力: 特定のバックエンド向けに最適化されていない抽象回路
- 出力: 選択した QPU 向けに最適化されたターゲット回路とオブザーバブル
このステップでは、Qiskit の generate_preset_pass_manager 関数を使用して、実験を実行する予定の実際の量子コンピューターに対する回路の最適化ルーティンを指定します。optimization_level=3 を設定します。これは最高レベルの最適化を提供するプリセットパスマネージャーを使用することを意味します。ここでの「最適化」とは、実際の量子コンピューター上での回路実装の最適化を指します。これには、ゲート深さを最小化するための抽象量子回路の量子ビットに対応する物理量子ビットの選択や、利用可能な最低エラー率を持つ物理量子ビットの選択などが含まれます。これは機械学習問題の最適化(COBYLA などの古典的なオプティマイザーのような)とは直接関係がありません。
ステップ 2 の実装方法によっては、行列要素に関与する点のペアごとに異なる 回路が測定されるため、回路を複数回最適化する必要がある場合があります。
ステップ 3: Qiskit Runtime プリミティブを使用して実行する
- 入力: ターゲット回路
- 出力: 確率分布
Qiskit Runtime の Sampler プリミティブを使用して、回路をサンプリングして得られる状態の確率分布を再構成します。これは「準確率分布」と呼ばれることがあります。これはノイズが問題となり、エラー軽減などの追加ステップが導入された場合に適用される用語です。そのような場合、すべての確率の合計が正確に 1 にならない場合があります。そのため「準確率」と呼ばれます。
ステップ 4: 後処理を行い、結果を古典的な形式で返す
- 入力: 確率分布
- 出力: 1 つのカーネル行列要素、または繰り返す場合はカーネル行列
量子回路で を測定する確率を計算し、使用した 2 つのデータベクトルに対応する位置にカーネル行列を埋めます。カーネル行列全体を埋めるには、各要素に対して量子実験を実行する必要があります。カーネル行列が得られたら、pre-calculated kernels(事前計算済みカーネル)を受け入れる多くの古典的な機械学習アルゴリズムで使用できます。例えば:qml_svc = SVC(kernel="precomputed")。その後、古典的なワークフローを使用してモデルをテストデータに適用し、精度スコアを得ることができます。精度スコアに満足できない場合は、特徴マップなどの計算の側面を再検討する必要があるかもしれません。
レッスンの概要
このレッスンでは、実際の量子 コンピューターでの時間を最大限に活用するために、これらのステップをいくつかの方法で実行します。量子カーネル法を以下に適用します:
- 比較的少ない特徴量を持つデータの単一カーネル行列要素。実際のバックエンドを使用し、各ステップで何が起きているかを容易に追えるようにします。
- 比較的少ない特徴量を持つデータセット全体。シミュレートされたバックエンドを使用し、量子ワークフローが古典的な機械学習手法とどのように接続するかを確認します。
- 多くの特徴量を持つデータの単一カーネル行列要素。実際の量子コンピューターを使用します。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
単一カーネル行列要素
ステップ 1: 古典的な入力を量子問題にマッピングする
まず、特徴量が少ない(例えば 10 個の)データセットを考えてみましょう。カーネル行列要素を 1 つずつ計算するため、データセットは好きなだけ大きくできます。少なくとも 2 点が必要なので、そこから始めます(次の例では完全なデータセットをインポートします)。必要なパッケージをいくつかインポートします:
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])
上記の 2 つのユニタリはまさに導入部で説明した と に対応しています。unitary_overlap を使用してそれらを組み合わせることができます。常に回路の深さに注目することが重要です。
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
ステップ 2: 量子実行のために問題を最適化する
まず最も混雑していないバックエンドを選択し、そのバックエンドで実行できるように回路を最適化します。
# 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)
複雑な回路の場合、このステップでは実際の量子コンピューターのネイティブゲートにマッピングされる際に回路の深さが大幅に増加し、情報をある量子ビットから別の量子ビットに移動させる必要が生じる場合があります。この単純なケースでは、深さはほとんど変わりません。
print("circuit depth = ", overlap_ibm.decompose().depth())
overlap_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)
circuit depth = 10
1
ステップ 3: Qiskit Runtime プリミティブを使用して実行する
シミュレーターで実行するための構文は以下にコメントアウトされています。特徴量が少ないこのデータセットでは、シミュレーターでの実行がまだ選択肢の一つです。ユーティリティスケールの計算では、シミュレーションは通常実現可能ではありません。シミュレーターはスケールダウンしたコードのデバッグにのみ使用してください。
# 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()