メインコンテンツへスキップ

量子カーネル

量子カーネルの概要

「量子カーネル法」とは、量子コンピューターを使用してカーネルを推定するあらゆる手法を指します。この文脈での「カーネル」とは、カーネル行列またはその個々の要素を意味します。特徴マッピング Φ(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) を明示的に構成することなく、元のデータベクトルを用いて特徴写像された空間での内積を表現できるカーネル関数を見つけることを意味します。量子カーネル法では、特徴マッピングは量子回路によって行われ、カーネルはその回路の測定と相対的な測定確率を使用して推定されます。

このレッスンでは、大量のエンタングルメントを使用する事前定義されたエンコーディング回路の深さを調べ、手動でコーディングした回路の深さと比較します。これは一方の手法を推奨するものではありません。事前定義された回路が深すぎる場合や、カスタム構築された回路のエンタングルメントが不十分な場合もあります。これらはあくまで探索を可能にするために紹介しています。

カーネル行列推定の詳細を説明する前に、Qiskit パターンの言語を使ってワークフローの概要を説明します。

ステップ 1: 古典的な入力を量子問題にマッピングする

  • 入力: トレーニングデータセット
  • 出力: カーネル行列要素を計算するための抽象回路

データセットが与えられたら、最初のステップはデータを量子回路にエンコードすることです。言い換えると、データを量子コンピューターの状態のヒルベルト空間にマッピングする必要があります。これはデータに依存する回路を構築することで実現します。方法は多数あり、前のレッスンでいくつかの選択肢を紹介しました。独自の回路を構築してデータをエンコードすることもできますし、zz_feature_map のような事前定義された特徴マップを使用することもできます。このレッスンでは、その両方を行います。

1つのカーネル行列要素を計算するためには、2つの異なる点をエンコードして内積を推定する必要があります。完全な量子カーネルワークフローには、写像されたデータベクトル間の多数の内積と古典的な機械学習手法が含まれます。しかし、繰り返される核心的なステップは1つのカーネル行列要素の推定です。そのために、データに依存する量子回路を選択し、2つのデータベクトルを特徴空間にマッピングします。

Classical_Review_background_kernel_circuit

カーネル行列を生成するタスクでは、すべての NN 量子ビットが 0|0\rangle 状態にある 0N|0\rangle^{\otimes N} 状態を測定する確率に特に注目します。これを理解するために、データベクトル xi\vec{x}_i のエンコードと写像を担う回路を Φ(xi)\Phi(\vec{x}_i)xj\vec{x}_j のそれを Φ(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} に対して両回路 Φ(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 まで)です。回路の測定層は測定確率(または特定のエラー軽減手法を使用した場合は「準確率」)を返します。注目する確率はゼロ状態 0N|0\rangle^{\otimes N} のものです。

ステップ 2: 量子実行のために問題を最適化する

  • 入力: 特定のバックエンド向けに最適化されていない抽象回路
  • 出力: 選択した QPU 向けに最適化されたターゲット回路とオブザーバブル

このステップでは、Qiskit の generate_preset_pass_manager 関数を使用して、実験を実行する予定の実際の量子コンピューターに対する回路の最適化ルーティンを指定します。optimization_level=3 を設定します。これは最高レベルの最適化を提供するプリセットパスマネージャーを使用することを意味します。ここでの「最適化」とは、実際の量子コンピューター上での回路実装の最適化を指します。これには、ゲート深さを最小化するための抽象量子回路の量子ビットに対応する物理量子ビットの選択や、利用可能な最低エラー率を持つ物理量子ビットの選択などが含まれます。これは機械学習問題の最適化(COBYLA などの古典的なオプティマイザーのような)とは直接関係がありません。

ステップ 2 の実装方法によっては、行列要素に関与する点のペアごとに異なる回路が測定されるため、回路を複数回最適化する必要がある場合があります。

ステップ 3: Qiskit Runtime プリミティブを使用して実行する

  • 入力: ターゲット回路
  • 出力: 確率分布

Qiskit Runtime の Sampler プリミティブを使用して、回路をサンプリングして得られる状態の確率分布を再構成します。これは「準確率分布」と呼ばれることがあります。これはノイズが問題となり、エラー軽減などの追加ステップが導入された場合に適用される用語です。そのような場合、すべての確率の合計が正確に 1 にならない場合があります。そのため「準確率」と呼ばれます。

ステップ 4: 後処理を行い、結果を古典的な形式で返す

  • 入力: 確率分布
  • 出力: 1 つのカーネル行列要素、または繰り返す場合はカーネル行列

量子回路で 0N|0\rangle^{\otimes N} を測定する確率を計算し、使用した 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 つのユニタリはまさに導入部で説明した U1U_1U2U_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

Output of the previous code cell

ステップ 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()

ステップ 4: 後処理を行い、結果を古典的な形式で返す

導入部で説明したように、ここで最も有用な測定はゼロ状態 00000|00000\rangle を測定する確率です。

counts.get(0, 0.0) / num_shots
0.6525

これが求めていた結果です。高次元特徴空間における 2 つのデータ点のベクトルの内積(絶対値の 2 乗まで)の推定値です。測定確率(または準確率)の完全な分布を確認したい場合は、以下に示す plot_distribution 関数を使って行うことができます。量子ビット数が多い場合、このような図はすぐに扱いにくくなります。

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

高次元特徴空間における 2 つのデータ点間の内積に関するこの情報だけからわかることは、それらの重なりが最大の重なり(1.0 となります)と比較してかなり大きいということです。これは、2 つのデータ点が本質的に何らかの意味で類似しており、同じクラスに分類されることを示す指標かもしれません。あるいは、特徴マップが類似したデータの重なりを大きくし、異なるデータの重なりを小さくする空間への写像が効果的でないことを示している可能性もあります。どちらが正しいかを知るには、特徴マップをデータセット全体に適用し、結果のカーネル行列を操作してクラスを高精度に分離できるかどうかを確認する必要があります。

z_feature_map を使用した場合、トランスパイル後の 2 量子ビット深さが低く(実際には深さ 1)なったことは注目に値します。回路が深くなりすぎると、大量のノイズが発生し、特徴マップがデータに適切に対応していても、ゼロ状態を測定する確率が非常に低くなります。例えば、同じデータ点を使用して zz_feature_map, entanglement='linear', reps=1 を用いて上記のプロセスを繰り返すと、dist.get(0,0.0) = 0.0015 となりました。これは zz_feature_map による回路深さと 2 量子ビット深さが大幅に増加するためです。下の図はその計算の確率分布を示しています。

Bad results from a zz feature map.

いくつかの同じカテゴリのデータ点で試してみて、良い結果を得るために深さをどれくらい低く抑える必要があるかを確認することをお勧めします。以下は例外が確実にある大まかなアドバイスです。一般的に、トランスパイル後の 2 量子ビット深さが 10 以下であれば問題ないはずです。トランスパイル後の 2 量子ビット深さが 50〜60 は最先端であり、高度なエラー軽減などのツールが必要です。その中間では、データの類似性、特徴マップの表現力、回路幅、その他の要因によって結果が異なる場合があります。 通常、後処理ステップには古典的な機械学習プロセスも含まれます。次のセクションでは、このプロセスをデータセット全体に拡張し、古典的な機械学習のワークフローを示します。

理解度チェック

以下の質問を読み、答えを考えてから、三角形をクリックして解答を確認してください。

10 量子ビットの量子回路では、一般的に測定される可能性のある状態は何通りありますか?

解答:

2102^{10} または 1024 通りです。

量子コンピューティングに不慣れな人が、2 量子ビット深さが非常に高い量子回路を使用し、エラー軽減を使用しないとします。さらに、その結果として各量子ビットのエラー率が 10% になるとします。この回路に対応する真の(エラーなしの)カーネル行列要素が非常に大きい(例えば 1.0)場合、10 個すべての量子ビットが |0> 状態にある状態を測定する確率はどのくらいですか?

解答:

各量子ビットが |0> 状態で正しく測定される確率は 0.90 です。10 個すべての量子ビットが正しい状態で測定される確率は 0.90100.90^{10}、つまり約 35% です。

回路の深さを監視することがなぜそれほど重要なのか、自分の言葉で説明してください。これは一般的に当てはまりますが、量子カーネル推定の文脈で説明してください。

解答:

この QKE ワークフローでは、推定はゼロ状態、つまりすべての量子ビットが 0|0\rangle 状態で見つかる状態の測定に基づいています。非常に深い回路は高いエラー率をもたらします。そのエラー率が多数の量子ビットにわたって累積されると、ゼロ状態を測定する確率が大幅に低下します。

完全なカーネル行列

このセクションでは、上記のプロセスを完全なデータセットの二値分類に拡張します。これにより 2 つの重要なコンポーネントが導入されます:(1) 後処理で古典的な機械学習を実装できるようになること、(2) トレーニングの精度スコアを取得できること、です。

ステップ1:古典的な入力を量子問題にマッピングする

次に、分類に使用する既存のデータセットをインポートします。このデータセットは128行(データポイント)から構成され、各ポイントには14個の特徴量があります。また、各ポイントの二値カテゴリ(±1\pm 1)を示す15番目の要素があります。データセットは以下でインポートされますが、データセットとその構造はこちらでも確認できます。

訓練には最初の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)

次に、古典データを量子回路にエンコードしてマッピングするための特徴マップを作成します。独自の特徴マップを構築することも、あらかじめ用意されたものを使用することも自由です。以下の特徴マップを自由に変更したり、ZFeatureMapに切り替えたりしてください。ただし、常に回路の深さに注意してください。前の6量子ビットの例では、zz_feature_mapを使用した際にトランスパイル後の回路深さが処理しきれないほど大きくなったことを思い出してください。回路の規模と複雑さが増すにつれて、深さはノイズが結果を圧倒するほど急速に増大する可能性があります。どのような特徴マップ構造が最も有用かを示すデータ構造についての知識があれば、その知識を活かした独自のカスタム特徴マップを作成することをお勧めします。

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:問題を最適化し、プリミティブを使って実行する

オーバーラップ回路を構築し、もし実際の量子コンピュータで実行する場合は、前述の手順と同様に実行用に最適化します。ただし今回は、すべてのデータポイントをステップオーバーして完全なカーネル行列を計算する予定です。データベクトル xi\vec{x}_ixj\vec{x}_j の各ペアに対して、異なるオーバーラップ回路を作成します。そのため、データポイントのペアごとに回路を最適化する必要があります。つまり、ステップ2と3は複数のイテレーションの中で一緒に行われます。

以下のコードセルは、単一のデータポイントペアに対して前述のプロセスとまったく同じ処理を行います。今回は2つのforループの中で実行されるだけで、各計算の結果を格納するための行末のkernel_matrix[x_1,x_2] = ...という行が追加されています。計算回数を1/2に削減するためにカーネル行列の対称性を活用していることに注目してください。また、ノイズがない場合にはそうあるべきであることから、対角要素を単純に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を使用して、事前計算済みのカーネルを使用するようにSVCを指定する必要があります。

# 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%の精度で解けた可能性が高いです。現在のユーティリティ時代において、量子カーネルが最も有用となる場面を見極めるため、さまざまなデータの種類やデータの関係を特徴づける研究がまだ多く残されています。 このワークフローの各部分を変更して、さまざまな量子特徴マップの有効性を調べることを学習者に委ねます。以下に考察すべき点をいくつか挙げます:

  • 精度はどれほど頑健ですか?広範なデータの種類に対して有効ですか、それともこの特定の訓練データに限定されますか?
  • データのどのような構造が、量子特徴マップが有用であると示唆していますか?
  • 訓練データの量を増減させると、精度はどのように影響を受けますか?
  • どのような特徴マップが使用できますか?また、特徴マップによって結果はどのように異なりますか?
  • 特徴量の数を増やすと、精度と実行時間はどのように影響を受けますか?
  • 実際の量子コンピュータでも成り立つと予想されるトレンドはありますか?

より多くの特徴量と量子ビットへのスケーリング

このセクションでは、ユーティリティに向けたスケールアップへの道筋を描きながら、はるかに多くの特徴量に対して単一の行列要素の計算を繰り返します。単一の行列要素に限定しているのは、量子コンピュータの割り当て時間を過度に消費せずにプロセスを示すためです。

ステップ1:古典的な入力を量子問題にマッピングする

各データポイントに42個の特徴量があるデータセットを出発点とします。最初の例と同様に、単一のカーネル行列要素を計算するために2つのデータポイントが必要です。以下の2つのポイントには42個の特徴量と1つのカテゴリ変数(±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個)でもかなり深い回路を生成したことを思い出してください。特徴量の数が増えると、回路の深さを注意深く監視する必要があります。これを示すために、まずzz_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
)

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量子ビット深さが100を超えていることは、そもそも出発点として成立しません。これが、このレッスン全体を通じてカスタム特徴マップを強調してきた理由です。データセット全体の構造について知識があれば、その構造を考慮したエンタングルメントマップを設計すべきです。ここでは、2つのデータポイント間の内積のみを計算しているため、データ構造の詳細な考慮よりも回路深さを低く保つことを優先しています。

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量子ビット深さこそが本当に重要であるため、ここでは深さをまだ確認しません。

ステップ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')>

小規模なジョブでは、プリセットパスマネージャーは同じ深さの同じ回路を安定して返すことが多いです。しかし、非常に大規模で複雑な回路では、パスマネージャーは実行のたびに異なるトランスパイル済み回路を返すことがあります。これは発見的手法(ヒューリスティクス)を使用しており、非常に大規模な回路では最適化の可能性が複雑に絡み合っているためです。数回トランスパイルを行い、最も浅い回路を採用することがよく有効です。これはクラシカルなオーバーヘッドを生じさせるだけですが、量子コンピューターからの結果を大幅に改善できる場合があります。

ここでは、ユニタリ・オーバーラップ回路を20回トランスパイルし、得られた回路の深さを確認します。

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

ここでは、トランスパイルのパスが異なると全体のゲート深さにある程度のばらつきが生じることが分かります。この回路はまだ2量子ビットトランスパイル深さにばらつきが見られるほど深く・広くはありません。ここでは、深さ60の transpiled_qcs[1] を使用します。これは、得られた最も深い回路の深さ77よりわずかに浅い値です。

overlap_ibm = transpiled_qcs[1]

ステップ3:Qiskit Runtimeプリミティブを使った実行

ユーティリティスケールに近づくにつれて、シミュレーターは有用ではなくなります。ここでは実際の量子コンピューターのための構文のみを示します。

# 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

1つのカーネル行列要素を求めるこのプロセスを、データセット内の他のデータペアに対して繰り返すことで、完全なカーネル行列を得ることができます。カーネル行列の次元は、特徴量の数やキュービット数ではなく、訓練データの点数によって決まります。そのため、カーネル行列を予測モデルに変換する計算コストは、特徴量の数やキュービット数に応じてスケールしません。特徴量の数が多い比較的小規模なデータセットであっても、有効な分類を行うためには、データを適切な特徴マップに対応させる必要があります。

スケーリングと今後の研究

カーネル法では、0|0\rangle をできる限り正確に測定する必要があります。しかし、ゲートエラーや読み出しエラーにより、任意のキュービットが誤って 1|1\rangle 状態として測定される確率 pp がゼロではありません。0|0\rangle の確率が 100%100\% であるべきという単純化した前提でも、たとえば NN ビットにエンコードされた多数の特徴量に対して、すべてのビットが正しく 0|0\rangle と測定される確率は (1p)N(1-p)^N に低下します。NN が大きくなるにつれて、この方法はますます信頼性が低下します。この困難を克服し、カーネル推定をより多くの特徴量にスケールさせることは、現在も研究が進められている分野です。この問題の詳細については、Thanasilp, Wang, Cerezo, Holmes による研究をご参照ください。現在の量子コンピューターで何ができるかを探求しつつ、誤り訂正の時代に何が可能になるかにも期待してください。

まとめ

量子カーネルの計算には以下が含まれます:

  • 訓練データの点のペアを使ってカーネル行列の要素を計算すること
  • データをエンコードし、特徴マッピングによってマッピングすること
  • 実際の量子コンピューター / バックエンドで実行するために回路を最適化すること

量子カーネルは、このレッスンで示したように、クラシカルな機械学習アルゴリズムで使用できます。

量子カーネルを使用する際に心がけるべき重要な点として、以下が挙げられます:

  • データセットは量子カーネル法から恩恵を受けられそうか?
  • さまざまな特徴マップやエンタングルメント方式を試してみましょう。
  • 回路の深さは許容範囲内か?
  • パスマネージャーを複数回実行し、得られる最も浅い深さの回路を使用しましょう。

量子カーネル法は、量子に適した特徴を持つデータセットと適切な量子特徴マップが適切に組み合わさることで、強力なツールになり得ます。量子カーネルが有効と考えられる場面をより深く理解するために、Liu, Arunachalam & Temme (2021) を読むことをお勧めします。