QSVMの基本原理
(本稿では Quantum-enhanced kernel を使うSVMをQSVMと呼びます)
QSVMは,データ分類器の一種であるSupport Vector Machine (SVM) の
特徴量空間として量子状態空間を使う量子機械学習アルゴリズムです。
上の図はSVMの概念を簡単に示したものです。
ここでは入力は2次元データであり、各データはクラス1か2というラベルを持っているとします。
我々の目的は、未知のデータが与えられたときに、それがクラス1なのか2なのかを分類することです。
SVMはそれを実現する手段の1つです。
SVMでは、既知の正解データから、分類境界を学習します。
未知データに対しては、分類境界から見てどちら側にあるのかでクラスを判別します。
複雑なデータがSVMできれいに分類できるかどうかは、この分類境界がどの程度複雑な形を取れるかが重要です。
かつ、分類境界は簡単に(効率的に)計算できなければなりません。
例えば分類境界をただの超平面(2次元の場合は直線)とする”線形SVM”は簡単でよいですが、
分類境界が単純すぎるため下図のようなデータセットは分類出来ません。
線形SVMの良さを持ちつつ非線形化する方法として、データ
そのうえで線形SVMを行うものがあります。
これは非線形SVMと呼ばれます。
飛ばす作用は特徴量マップ
非線形SVMで決めた分類境界は、特徴量空間では超平面ですが、元のデータ空間では曲面になっており、
複雑な分類に対応できます。
この特徴量空間の次元が高いほど、複雑な分類境界を生成できるのではないかと思えてきます。
量子状態ベクトルは、量子ビット数を
そこで我々は、古典データを量子状態ベクトルに埋め込み、量子状態ベクトル空間を特徴量空間として超平面で分類するような非線形SVMを考えます。
これがQSVMと呼ばれるものです。
量子状態ベクトル空間での分類平面を学習データから決定したり、未知データを分類したりするには、
量子状態ベクトル空間上でのデータ間のベクトル内積が必要となります。
つまり我々が構成すべき操作は、古典データを量子状態ベクトルへ埋め込み、そのようなベクトル同士の内積を計算し、出力させることです。
上の図は、この操作を説明したものです。
ここで量子状態ベクトル同士に内積(の絶対値の二乗)がQSVMに必要なものであり、量子カーネル
データにあったいい感じの
以下では、QSVMをblueqatで実装します。
必要なSDKのインストール
!pip install scikit-learn
Requirement already satisfied: scikit-learn in /opt/conda/lib/python3.10/site-packages (1.1.2)
Requirement already satisfied: numpy>=1.17.3 in /opt/conda/lib/python3.10/site-packages (from scikit-learn) (1.21.0)
Requirement already satisfied: joblib>=1.0.0 in /opt/conda/lib/python3.10/site-packages (from scikit-learn) (1.2.0)
Requirement already satisfied: threadpoolctl>=2.0.0 in /opt/conda/lib/python3.10/site-packages (from scikit-learn) (3.1.0)
Requirement already satisfied: scipy>=1.3.2 in /opt/conda/lib/python3.10/site-packages (from scikit-learn) (1.8.1)
from blueqat import Circuit
import numpy as np
from sklearn.svm import SVC
from sklearn.datasets import load_iris
from sklearn.datasets import make_moons
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
def print_Zbasis_expression(statevector):
nqubit = int(np.log2(np.size(statevector)))
for i in range(2**nqubit):
print('+({:.2f})*|{number:0{width}b}>'.format(statevector[i],number=i,width=nqubit),end='')
def myplot_histogram(sv,n,figsize=(20,10),fontsize=24):
probs = np.abs(sv)**2
## z方向に射影測定した時に得られる可能性があるビット列
z_basis = [format(i,"b").zfill(n) for i in range(probs.size)]
plt.figure(figsize=figsize)
plt.xlabel("states",fontsize=fontsize)
plt.ylabel("probability(%)",fontsize=fontsize)
plt.xticks(fontsize=fontsize)
plt.yticks(fontsize=fontsize)
plt.bar(z_basis, probs*100)
plt.show()
関数の定義
x の埋め込み回路 \phi(x)
データdef data_encoding(data):
circuit = Circuit(2)
for i,x in enumerate(data):
circuit.rx(x)[i]
return circuit
data = np.array([[0.1,0.2],[0.5,0.6],[0.3,0.4],[-0.2,0.6]])
data_encoding(data[0]).run(backend="draw") # show a circuit
<Figure size 1800x180 with 1 Axes>
埋め込み
|\phi(x_{0})>
状態ベクトル sv = data_encoding(data[0]).run(backend="numpy") # データが埋め込まれた状態ベクトル
print_Zbasis_expression(sv)
+(0.99+0.00j)*|00>+(0.00-0.05j)*|01>+(0.00-0.10j)*|10>+(-0.00+0.00j)*|11>```
**カーネル $k(x_{0},x_{1}) = |<\psi(x_{0})|\psi(x_{1})>|^{2}$ の計算**
-----------------------------------------------
```python
v0 = data_encoding(data[0]).run(backend="numpy") # data0
v1 = data_encoding(data[1]).run(backend="numpy") # data1
kernel_value_from_sv = np.abs(np.vdot(v0,v1))**2 # calculate innner product <psi_{0}|psi_{1}>
print(kernel_value_from_sv)
0.922618835669838
これで出来ました。
ただし、本当は状態ベクトルそのものは、測定不可能な量です。
シミュレータでのみ確認できます。
(これは量子力学の要請なので、なぜ?と言われても困ります)
つまり、ベクトル内積計算までも全て量子回路でやる必要があります。
そのような量子回路(Swap test)はありますが、やや回路が深くて実機実行が難しいので、
次に示すような特殊な浅い回路で内積を計算させることが普通です。
x_{0},x_{1} を同じ量子ビットに埋め込む回路(カーネル計算の簡素化の準備)
2データ まず、内積を取りたい2データを同じ量子ビットに埋め込みます。
ただし片方はゲートを共役にします。
def make_circuit(data0,data1):
subcircuit0 = data_encoding(data0)
subcircuit1 = data_encoding(data1).dagger()
circuit = subcircuit0 + subcircuit1
return circuit
make_circuit(data[0],data[1]).run(backend="draw")
<Figure size 1800x180 with 1 Axes>
|0...0> を測定して簡単にk(x_{0},x{1}) を得る回路
2データを埋め込んだ量子ビットの上記の回路を測定して、全量子ビットが0になる確率をメモします。
これがなんとベクトル内積(カーネル)に一致します。
def kernel(data0,data1):
circuit = make_circuit(data0,data1)
sv = circuit.run(backend="numpy")
prob = np.abs(sv)**2
kernel_value = prob[0]
return kernel_value
kernel(data[0],data[1])
0.922618835669838
個々に埋め込んで内積を取った値と比較してみると、確かに一致します。
kernel(data[0],data[1]) == kernel_value_from_sv
True
不思議ですよね。
理屈はやや複雑なので割愛しますが、手計算でも示すことができます。
\{k(x_{0},x_{1}) | (x_{0},x_{1}) \in X \times X\}
全データに対するカーネルの値(カーネル行列) def kernel_matrix(A, B):
"""Compute the matrix whose entries are the kernel
evaluated on pairwise data from sets A and B."""
return np.array([[kernel(a, b) for b in B] for a in A])
mat = kernel_matrix(data,data)
fig, ax = plt.subplots()
ax.imshow(mat)
plt.show()
<Figure size 432x288 with 1 Axes>
よくわからないが、対称行列であることはわかります。
サンプルデータセットでやってみる
Iris data set (2値分類)
np.random.seed(42)
X, y = load_iris(return_X_y=True)
# pick inputs and labels from the first two classes only,
# corresponding to the first 100 samples
X = X[0:100,0:2]
y = y[0:100]
# scaling the inputs is important since the embedding we use is periodic
scaler = StandardScaler().fit(X)
X_scaled = scaler.transform(X)
# scaling the labels to -1, 1 is important for the SVM and the
# definition of a hinge loss
y_scaled = 2 * (y - 0.5)
X_train, X_test, y_train, y_test = train_test_split(X_scaled, y_scaled)
kernel行列の計算
mat = kernel_matrix(X_train,X_train)
fig, ax = plt.subplots()
ax.imshow(mat)
plt.show()
<Figure size 432x288 with 1 Axes>
よくわからないが、対称行列であることはわかります。
量子カーネルで決まるSVMの分離平面を学習
svm = SVC(kernel=kernel_matrix).fit(X_train, y_train)
SVMによる分類と分類結果の精度
predictions = svm.predict(X_test)
print("accuracy is ", accuracy_score(predictions, y_test))
accuracy is 1.0
このデータセットは量子カーネルで正しく分類できました!
SVMによる分類境界
def plot_decision_boundaries(classifier, ax, N_gridpoints=10):
_xx, _yy = np.meshgrid(np.linspace(-2*np.pi, 2*np.pi, N_gridpoints), np.linspace(-2*np.pi, 2*np.pi, N_gridpoints))
_zz = np.zeros_like(_xx)
for idx in np.ndindex(*_xx.shape):
_zz[idx] = classifier.predict(np.array([_xx[idx], _yy[idx]])[np.newaxis, :])
plot_data = {"_xx": _xx, "_yy": _yy, "_zz": _zz}
ax.contourf(
_xx,
_yy,
_zz,
cmap=ListedColormap(["#FF0000", "#0000FF"]),
alpha=0.2,
levels=[-1, 0, 1],
)
for ii in range(len(X_test)):
if y_test[ii] > 0:
plt.plot(X_test[ii,0],X_test[ii,1], marker="o", color='black')
else:
plt.plot(X_test[ii,0],X_test[ii,1], marker="x", color='black')
return plot_data
plot_data = plot_decision_boundaries(svm, plt.gca())
<Figure size 432x288 with 1 Axes>
しかし、かなり怪しい分類平面です。。
データセットによってはダメそうな気配がします。
Moon data set でやってみる
X, y = make_moons(n_samples=200, shuffle=True)
# pick inputs and labels from the first two classes only,
# corresponding to the first 100 samples
X = X[0:100,0:2]
y = y[0:100]
# scaling the inputs is important since the embedding we use is periodic
scaler = StandardScaler().fit(X)
X_scaled = scaler.transform(X)
# scaling the labels to -1, 1 is important for the SVM and the
# definition of a hinge loss
y_scaled = 2 * (y - 0.5)
X_train, X_test, y_train, y_test = train_test_split(X_scaled, y_scaled)
svm = SVC(kernel=kernel_matrix).fit(X_train, y_train)
predictions = svm.predict(X_test)
print("accuracy is ", accuracy_score(predictions, y_test))
accuracy is 0.84
plot_data = plot_decision_boundaries(svm, plt.gca())
<Figure size 432x288 with 1 Axes>
やはりデータセットによっては、ダメでした。
古典カーネルであるRBFカーネルを用いて古典SVMをして比較
svm = SVC(kernel="rbf").fit(X_train, y_train)
predictions = svm.predict(X_test)
print("accuracy is ", accuracy_score(predictions, y_test))
plot_data = plot_decision_boundaries(svm, plt.gca())
accuracy is 1.0
<Figure size 432x288 with 1 Axes>
古典カーネルであるRBFカーネルでは一発でうまくいきます。。
このように、量子カーネルはクセが強いです。
量子はいつでも古典より良いわけではありません。つまり、
$ x + quantum \ngtr x$
です。
終わりに
いかがだったでしょうか?
思ったよりQSVMはしょぼそうに感じたかもしれません。
その通りです。うまいカーネルを見つけるのが本当に大変です。
そこで、カーネル(を構成するゲート)をパラメータ化して調整する方法もあります。
QSVM + 変分最適化 って感じですね。
しかしこれはこれで、QSVMのいいところである「簡素でノンパラメータ」という点が失われます。
ぜひ試行錯誤をしてみてください。
IBMのQiskit SDK には、qiskit-machine-learningとしてQSVMのライブラリも備わっていますので、こちらも便利です。