Skip to main content
IBM Quantum Platform

量子カーネル


量子カーネル入門

量子カーネル法」とは、量子コンピュータを使ってカーネルを推定する方法を指す。 この文脈では、「カーネル」はカーネル行列またはその中の個々のエントリーを指す。 特徴マッピング Φ(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つの異なる点をエンコードし、それらの内積を推定できるようにしたいことに注意。 もちろん、完全な量子カーネルワークフローには、古典的な機械学習法と同様に、マップされたデータベクトル間のこのような内積が多数含まれる。 しかし、繰り返し実行される中核ステップは、単一のカーネル行列要素の推定である。 そのために、データに依存する量子回路を選択し、2つのデータベクトルを特徴空間にマッピングする。

Classical_Review_background_kernel_circuit

カーネル行列を生成するタスクでは、 0N|0\rangle^{\otimes N} 状態を測定する確率に特に関心がある。この状態では、 NN のすべての量子ビットが 0|0\rangle 状態にある。 これを見るために、1つのデータベクトル 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)0N2.P_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: IBM Quantum プリミティブを使用して実行する

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

IBM QuantumSampler プリミティブを使用して、回路のサンプリングから得られる状態の確率分布を再構築します。 なお、これは「準確率分布」と呼ばれることもある点に留意してください。この用語は、ノイズが問題となる場合や、誤差低減などの追加の手順が導入される場合に適用されます。 このような場合、すべての確率の和が必ずしも1に等しくならないことがある。したがって、「準確率」と呼ばれる。

ステップ4:後処理を行い、結果を従来の形式で返す

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

量子回路上で 0N|0\rangle^{\otimes N} を測定する確率を計算し、使用した2つのデータベクトルに対応する位置にカーネル行列を入力する。 カーネル行列全体を埋めるには、各エントリーの量子実験を実行する必要がある。 いったんカーネル行列ができれば、 pre-calculated kernels を受け入れる多くの古典的な機械学習アルゴリズムでそれを使うことができる。 例: qml_svc = SVC(kernel="precomputed")。 その後、古典的なワークストリームを使用して、テストデータにモデルを適用し、精度スコアを得ることができる。 精度の満足度によっては、フィーチャーマップなど、計算方法を見直す必要があるかもしれない。

授業の構成

このレッスンでは、実際の量子コンピュータで時間を最適に使うために、これらのステップをいくつかの方法で実行する。 量子カーネル法を応用して

  • 特徴量が比較的少ないデータに対して、実際のバックエンドを使用した単一のカーネル行列エントリーを行うことで、各ステップで何が起こっているのかを簡単に追うことができる。
  • バックエンドをシミュレートし、特徴量が比較的少ないデータセット全体を使用することで、量子ワークストリームが古典的な機械学習手法とどのように結びつくかを確認することができる
  • 実際の量子コンピュータを使用し、多くの特徴を持つデータに対して1つのカーネル行列を入力。 IBM®、量子コンピュータでの計算時間を考慮し、大規模なデータセットのカーネル行列全体の推定は行わない。
# 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")

Output:

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)

Output:

<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)

Output:

circuit depth =  10
1

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

シミュレーター上で実行するための構文は、以下のようにコメントアウトされている。 特徴数が少ないこのデータセットの場合、シミュレータ上で実行することはまだ選択肢の一つである。 ユーティリティ・スケールの計算では、通常、シミュレーションは実行不可能である。 シミュレーターは、縮小されたコードをデバッグするためだけに使うべきだ。

# 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 the IBM Quantum 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

Output:

0.6525

2つのデータ点に対応するベクトルの内積(2乗まで)の推定値である。 測定確率(または準確率)の完全な分布を見たい場合は、以下に示すように plot_distribution 関数を使って見ることができる。 量子ビットの数が多いと、このような絵はすぐに難解になることがわかる。

from qiskit.visualization import plot_distribution

plot_distribution(counts_bit)

Output:

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:

Output of the previous code cell

高次元特徴空間における2つのデータ点間のたった1つの内積に関するこの情報から言えることは、その重なりは最大重なり( 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量子ビット深度がはるかに大きいためである。 下図はその計算の確率分布である。

zzフィーチャーマップの悪い結果。

同じカテゴリーからいくつかのデータポイントを使って、良い結果を得るためにどの程度深度を低くしなければならないかを試してみる価値はある。 以下は、例外もあるであろう大まかなアドバイスである。 一般的に、2量子ビットのトランスパイル深度が10以下であれば問題はないはずだ。 2量子ビットのトランスパイル深度50-60は最先端であり、他のツールの中でも特に高度なエラー緩和を必要とする。 この間では、データの類似性、フィーチャーマップの表現力、回路幅、その他の要因によって結果が異なる可能性があります。

通常、後処理段階には古典的な機械学習処理も含まれる。 次のセクションでは、このプロセスをデータセット全体に拡張し、古典的な機械学習のワークフローを示す。

理解度チェック

一般に、10量子ビットの量子回路では、測定可能な異なる状態はいくつあるのでしょうか?

  • 2102^{10} または1024。

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

  • それぞれの量子ビットが|0|状態で正しく発見される確率は 0.90 である。 10個の量子ビットがすべて正しい状態にある確率は 0.90100.90^{10}、約35%である。

回路の深さをモニターすることがなぜ重要なのか、自分の言葉で説明してください。 これは一般的に正しいが、量子カーネル推定の文脈で説明しよう。

  • このQKEワークフローでは、我々の推定はゼロ状態、つまり全ての量子ビットが 0|0\rangle。 非常に深い回路では、高いエラー率が発生する。 この誤差が多くの量子ビットに及ぶと、ゼロの状態を測定する確率は大幅に低下する。


完全な核行列

このセクションでは、上記のプロセスを完全なデータセットの二値分類に拡張する。 これにより、2つの重要な要素が導入される:(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]

Output:

--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は複数の反復で一緒に行われることになる。

以下のコード・セルは、1つのデータ・ポイントのペアに対して、前とまったく同じ処理を行う。 今回は単純に2つの for ループの中で実行され、最後に kernel_matrix[x_1,x_2] = ... 、各計算結果を保存するための行が追加されている。 カーネル行列の対称性を利用して計算回数を1/2に減らしていることに注意。 また、ノイズがない場合の対角要素を単純に1とした。 実装や要求される精度によっては、対角要素を用いてノイズを推定したり、エラー軽減の目的でノイズについて学んだりすることもできる。

カーネル行列が完全に入力されたら、テストデータに対してこのプロセスを繰り返し、test_matrixを入力する。 この2つを区別するために別の名前をつけただけである。

# 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")

Output:

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}")

Output:

Precomputed kernel classification test score: 1.0

学習したモデルの精度は100%であることがわかる。 これは素晴らしいことで、QKEが機能することを示している。 しかし、それは量子のアドバンテージとは大きく異なる。 古典的なカーネルであれば、この分類問題も100%の精度で解くことができただろう。 現在のユーティリティ時代において量子カーネルが最も有用となるのはどこなのか、さまざまなデータタイプやデータ関係の特徴を明らかにするためにやるべきことは多い。 このワークフローの一部を変更し、さまざまな量子特徴マップの有効性を研究することは、学習者に任されている。 以下は、考慮すべきいくつかの点である:

  • 精度はどの程度ですか? それは幅広い種類のデータに当てはまるのか、それともこの特定のトレーニングデータだけに当てはまるのか?
  • 量子フィーチャーマップが有用だと思わせるデータの構造とは?
  • トレーニングデータの量を増やしたり減らしたりすると、精度にどのような影響がありますか?
  • どのようなフィーチャーマップが使えるのか、フィーチャーマップによって結果はどのように変わるのか
  • 特徴数を増やすと、精度と実行時間はどうなりますか?
  • 実際の量子コンピューターにどのようなトレンドがあると予想しますか?

より多くの機能と量子ビットへの拡張

このセクションでは、1つの行列要素の計算を繰り返し、より多くの特徴について計算し、実用的なスケールへの道筋を描く。 行列の要素を1つに限定したのは、量子コンピュータに割り当てられた時間をあまり使わずに処理を示すためである。

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

ここでは、各データポイントが42の特徴を持つデータセットを出発点と仮定する。 最初の例と同様に、2つのデータポイントを必要とする1つのカーネル行列要素を計算する。 以下の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")

Output:

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)

Output:

<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())

Output:

circuit depth =  61
print(transpiled_depths)
print(transpiled_twoqubit_depths)

Output:

[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: IBM Quantum プリミティブを使用して実行する

実用的な規模に近づくにつれ、シミュレーターは役に立たなくなるだろう。 ここでは、実際の量子コンピューターの構文のみを示す。

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

# Import our IBM Quantum primitive
from qiskit_ibm_runtime import SamplerV2 as Sampler

# Open a 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

Output:

0.0138

単一のカーネル行列要素に対するこのプロセスを、セット内の他のデータ・ペア間で繰り返すことで、完全なカーネル行列を得ることができる。 カーネル行列の次元は、特徴の数ではなく、学習データのポイント数によって決まる。 そのため、カーネル行列を予測モデルに操作する計算コストは、特徴量や量子ビットの数のようにスケールしない。 多数の特徴を持つ比較的小さなデータセットであっても、効果的な分類をもたらす特徴マップにデータをマッチさせる必要がある。

スケーリングと今後の作業

カーネル法では、 0|0\rangle をできるだけ正確に測定する必要がある。 pp しかし、ゲート・エラーと読み出しエラーは、ある量子ビットが誤って 1|1\rangle0|0\rangle の確率は 100%100\% であるべきだ、と単純化しすぎたとしても、例えば NN ビットにエンコードされた多くの特徴では、すべてのビットが 0|0\rangle であることを正しく測定できる確率は (1p)N(1-p)^N にまで低下する。 NN が大きくなると、この方法はますます信頼できなくなる。 この困難を克服し、カーネル推定をより多くの特徴に拡張することが、現在の研究分野である。 この問題の詳細については、 Thanasilp、Wang、Cerezo、Holmes によるこの研究を参照してください。 現在の量子コンピューターで何ができるかを探求し、エラー訂正の時代に何が可能になるかを期待することをお勧めする。

レビュー

量子カーネルの計算には

  • 学習データのペアを使用して、カーネル行列のエントリを計算する
  • データをエンコードし、フィーチャーマッピングでマッピングする
  • 実際の量子コンピュータ/バックエンドで動作するように回路を最適化する

量子カーネルは、このレッスンのように、古典的な機械学習アルゴリズムで使うことができる。

量子カーネルを使用する際の留意点には、以下のようなものがある:

  • データセットに量子カーネル法は有効か?
  • さまざまなフィーチャーマップやエンタングルメントスキームを試してみてください。
  • 回路の深さは問題ないか?
  • パスマネージャーを何度も走らせ、できるだけ奥行きの小さいサーキットを使ってみる。

量子カーネル法は、量子化可能な特徴を持つデータセットと適切な量子特徴マップが適切にマッチすれば、強力なツールとなる可能性がある。 量子カーネルがどのような場面で役立つかをよりよく理解するには、 Liu, Arunachalam & Temme (2021)を読むことをお勧めする。

このページは役に立ちましたか?
バグや誤字の報告、またはコンテンツの要求はGitHubで行ってください。