はじめに
本記事は、2023年に公開した旧記事をPennyLane 0.45.1向けに全面的に書き直した改訂版です。API更新だけでなく、データ分割、評価方法、回路出力、古典モデルとの比較も見直しています。
本記事では、PennyLaneを使って、Irisデータセットの3クラス分類を行う簡単な量子機械学習モデルを作ります。
扱う内容は、次の流れです。
- 4個の特徴量を4量子ビットへ埋め込む
- 学習可能な回転ゲートを作用させる
- CNOTゲートで量子ビット間に相関を導入する
- 3個の期待値を分類スコアとして読み出す
- 古典的な最適化アルゴリズムで量子回路のパラメータを更新する
- より深い回路として
StronglyEntanglingLayersを試す - 古典的なロジスティック回帰とも比較する
この記事の目的は、Iris分類で量子優位性を示すことではありません。
Irisデータは150サンプル、4特徴量、3クラスという非常に小さな古典データセットです。この問題では、古典的な機械学習でも高い精度を容易に得られます。
それでもIrisデータを使う理由は、データの埋め込み、変分量子回路、量子測定、損失関数、勾配に基づく最適化という、量子機械学習の基本要素を小さなコードで確認できるためです。
本記事のコードは、2026年7月15日にPennyLane 0.45.1で動作を確認しています。
PennyLane 0.45.1ではPython 3.11以上が必要です。
古いコードで使われていた
qml.draw(
quantum_circuit,
expansion_strategy="device",
)
は現在は動きません。代わりに、
qml.draw(
quantum_circuit,
level="device",
)
を使います。
Google Colab版はこちらです。
https://colab.research.google.com/drive/115pxiPV1kZ-ahl0qeqFoVrB_XX_NQhFr?usp=sharing
関連記事
https://qiita.com/yamadasuzaku/items/30f2d8e17fcd9f673686
公式ドキュメント
量子機械学習を使うと何がうれしいのか
Irisデータを量子機械学習で分類して、何がうれしいのでしょうか。
まず明確にしておくと、このサンプルだけから、
量子機械学習は古典機械学習より高性能である
とは主張できません。
量子機械学習で重要なのは、単に古典データを量子回路へ入力することではなく、
- データを量子状態へどのように埋め込むか
- 量子状態空間のどの構造を利用するか
- 量子ビット間の相関をどのように設計するか
- どの物理量を測定して出力とするか
- 有限回測定やノイズの影響をどのように扱うか
を考えることです。
量子機械学習の利点が現れやすい候補としては、量子化学、量子多体系、量子センサーなどから得られる量子的なデータや、古典計算では扱いにくい構造を持つ問題が考えられます。
一方、今回扱うIrisデータは完全に古典的で、小さく、分類もしやすいデータです。
したがって、本記事は量子優位性の実証ではなく、
量子回路を微分可能なモデルとして扱うとは、具体的に何をしているのか
を理解するための入門例です。
今回のモデルの全体像
Irisデータの各サンプルには、次の4個の特徴量があります。
- がく片の長さ
- がく片の幅
- 花びらの長さ
- 花びらの幅
そこで、4個の特徴量を4量子ビットへ一つずつ対応させます。
今回の量子回路は、次の4段階から構成します。
1. データの埋め込み
各特徴量を、量子ビットの$Y$軸回転角として使います。
|0\rangle
\longrightarrow
R_Y(x_i)|0\rangle
特徴量$x_i$の値によって、量子ビットがブロッホ球上の異なる位置へ回転します。
2. 学習可能な回転
各量子ビットへ、3個の学習パラメータを持つqml.Rotゲートを作用させます。
R(\phi,\theta,\omega)
=
R_Z(\omega)
R_Y(\theta)
R_Z(\phi)
状態に作用する順番としては、最初に$R_Z(\phi)$、次に$R_Y(\theta)$、最後に$R_Z(\omega)$です。
3. 量子ビット間の結合
CNOTゲートを使い、量子ビット間に相関を導入します。
基礎編では、
0 → 1 → 2 → 3
という隣接量子ビット間のCNOTチェーンを使います。
4. 測定と分類
3クラス分類なので、3個の量子ビットについてPauli-$Z$期待値を測定します。
z_k
=
\langle Z_k\rangle
各期待値は$-1$から$1$の範囲を取ります。
これらを3クラスに対応するロジットとして使い、softmax関数で3クラスの確率へ変換します。
p_k
=
\frac{\exp(z_k)}
{\displaystyle\sum_{j=0}^{2}\exp(z_j)}
最も確率が高いクラスを予測結果とします。
旧版の記事では、クラスごとに独立した量子回路を3個用意し、それぞれを別々に評価していました。
今回の修正版では、一つの量子回路から3個の期待値を同時に返します。
Pauli-$Z$測定同士は可換なので、この構成の方が自然であり、同じ入力に対して量子回路を3回繰り返す必要もありません。
ソースコード全体
Google Colabへのインストール
Google Colabでは、最初のセルでPennyLaneをインストールします。
再現性を優先する場合は、動作確認したバージョンを明示的に指定します。
%pip install -q "pennylane==0.45.1"
完全なコード
import matplotlib.pyplot as plt
import numpy as np
import pennylane as qml
# PennyLaneの自動微分に対応したNumPy
from pennylane import numpy as pnp
from sklearn import datasets
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
# ============================================================
# 0. 再現性のための乱数シード
# ============================================================
SEED = 42
rng = np.random.default_rng(SEED)
print("PennyLane version:", qml.__version__)
# ============================================================
# 1. Irisデータセットを読み込む
# ============================================================
X, y = datasets.load_iris(
return_X_y=True,
)
n_classes = len(np.unique(y))
print("X[:3] =")
print(X[:3])
print("y[:3] =")
print(y[:3])
# ============================================================
# 2. 訓練データとテストデータへ分割する
# ============================================================
X_train, X_test, y_train, y_test = train_test_split(
X,
y,
test_size=0.3,
random_state=SEED,
stratify=y,
)
# ============================================================
# 3. 訓練データだけを使って標準化する
# ============================================================
scaler = StandardScaler()
X_train = scaler.fit_transform(
X_train
)
X_test = scaler.transform(
X_test
)
print("X_train[:3] =")
print(X_train[:3])
# ============================================================
# 4. 量子デバイスを用意する
# ============================================================
# Irisデータの特徴量数は4個
n_wires = X_train.shape[1]
# 解析的な量子ビットシミュレータ
dev = qml.device(
"default.qubit",
wires=n_wires,
)
# ============================================================
# 5. 基礎編の量子回路
# ============================================================
@qml.qnode(
dev,
interface="autograd",
)
def basic_circuit(X_batch, weights):
"""
基礎的な変分量子回路。
X_batch:
shape = (batch_size, n_wires)
または
shape = (n_wires,)
weights:
shape = (n_wires, 3)
"""
# 4個の特徴量をRY回転角として埋め込む
qml.AngleEmbedding(
features=X_batch,
wires=range(n_wires),
rotation="Y",
)
# 各量子ビットへ学習可能なRotゲートを作用させる
for wire in range(n_wires):
qml.Rot(
*weights[wire],
wires=wire,
)
# 隣接量子ビットをCNOTで結ぶ
for control in range(n_wires - 1):
qml.CNOT(
wires=[
control,
control + 1,
]
)
# 3クラスに対応する3個の期待値を返す
return tuple(
qml.expval(qml.Z(wire))
for wire in range(n_classes)
)
# ============================================================
# 6. StronglyEntanglingLayersを使った量子回路
# ============================================================
n_layers = 3
@qml.qnode(
dev,
interface="autograd",
)
def strong_circuit(X_batch, weights):
"""
StronglyEntanglingLayersを使った変分量子回路。
weights:
shape = (n_layers, n_wires, 3)
"""
qml.AngleEmbedding(
features=X_batch,
wires=range(n_wires),
rotation="Y",
)
qml.StronglyEntanglingLayers(
weights=weights,
wires=range(n_wires),
)
return tuple(
qml.expval(qml.Z(wire))
for wire in range(n_classes)
)
# ============================================================
# 7. QNodeの出力をロジット行列へ変換する
# ============================================================
def circuit_logits(
circuit,
X_batch,
weights,
):
"""
QNodeが返す期待値のtupleを、
shape = (batch_size, n_classes)
の行列へ変換する。
"""
return pnp.stack(
circuit(
X_batch,
weights,
),
axis=1,
)
# ============================================================
# 8. softmax関数
# ============================================================
def softmax(logits):
"""
各サンプルの3個のロジットを、
総和が1となるクラス確率へ変換する。
"""
# 数値的なオーバーフローを避けるため、
# 各行の最大値を先に引く
shifted = logits - pnp.max(
logits,
axis=1,
keepdims=True,
)
exp_logits = pnp.exp(
shifted
)
return exp_logits / pnp.sum(
exp_logits,
axis=1,
keepdims=True,
)
# ============================================================
# 9. 交差エントロピー損失
# ============================================================
def cross_entropy_loss(
circuit,
weights,
X_batch,
y_batch,
):
logits = circuit_logits(
circuit,
X_batch,
weights,
)
probabilities = softmax(
logits
)
# 正解クラスに割り当てられた確率を取り出す
correct_probabilities = probabilities[
pnp.arange(len(y_batch)),
y_batch,
]
return -pnp.mean(
pnp.log(
correct_probabilities
+ 1.0e-12
)
)
# ============================================================
# 10. 予測と精度
# ============================================================
def predict(
circuit,
weights,
X,
):
logits = circuit_logits(
circuit,
X,
weights,
)
logits = np.asarray(
logits,
dtype=float,
)
return np.argmax(
logits,
axis=1,
)
def accuracy(
circuit,
weights,
X,
y,
):
predictions = predict(
circuit,
weights,
X,
)
return np.mean(
predictions == y
)
# ============================================================
# 11. 学習関数
# ============================================================
def train_model(
circuit,
initial_weights,
X_train,
y_train,
*,
num_epochs,
batch_size,
stepsize,
rng,
):
"""
Adam法を使って量子回路のパラメータを学習する。
"""
weights = pnp.array(
initial_weights,
requires_grad=True,
)
optimizer = qml.AdamOptimizer(
stepsize=stepsize
)
n_samples = len(X_train)
for epoch in range(num_epochs):
# 各epochでデータの順番を入れ替える
indices = rng.permutation(
n_samples
)
epoch_losses = []
for start in range(
0,
n_samples,
batch_size,
):
batch_indices = indices[
start:start + batch_size
]
X_batch = X_train[
batch_indices
]
y_batch = y_train[
batch_indices
]
weights, batch_loss = (
optimizer.step_and_cost(
lambda current_weights:
cross_entropy_loss(
circuit,
current_weights,
X_batch,
y_batch,
),
weights,
)
)
epoch_losses.append(
float(batch_loss)
)
train_accuracy = accuracy(
circuit,
weights,
X_train,
y_train,
)
# 毎epoch出すと表示が長くなるため、
# 最初と5epochごとに表示する
if (
epoch == 0
or (epoch + 1) % 5 == 0
or epoch == num_epochs - 1
):
print(
f"Epoch {epoch + 1:2d}, "
f"Loss: {np.mean(epoch_losses):.6f}, "
f"Train accuracy: "
f"{train_accuracy:.4f}"
)
return weights
# ============================================================
# 12. 基礎編の回路を描画する
# ============================================================
basic_initial_weights = rng.normal(
loc=0.0,
scale=0.1,
size=(
n_wires,
3,
),
)
x_example = pnp.array(
X_train[0],
requires_grad=False,
)
w_example = pnp.array(
basic_initial_weights,
requires_grad=False,
)
fig, ax = qml.draw_mpl(
basic_circuit,
level="device",
decimals=2,
)(
x_example,
w_example,
)
plt.show()
print(
qml.draw(
basic_circuit,
level="device",
decimals=2,
)(
x_example,
w_example,
)
)
# ============================================================
# 13. 基礎編を学習する
# ============================================================
print()
print("=== Basic circuit ===")
basic_weights = train_model(
basic_circuit,
basic_initial_weights,
X_train,
y_train,
num_epochs=40,
batch_size=15,
stepsize=0.05,
rng=rng,
)
basic_test_accuracy = accuracy(
basic_circuit,
basic_weights,
X_test,
y_test,
)
print(
"Basic circuit test accuracy:",
f"{basic_test_accuracy:.4f}",
)
# ============================================================
# 14. StronglyEntanglingLayersの回路を描画する
# ============================================================
strong_shape = (
qml.StronglyEntanglingLayers.shape(
n_layers=n_layers,
n_wires=n_wires,
)
)
strong_initial_weights = rng.normal(
loc=0.0,
scale=0.1,
size=strong_shape,
)
strong_w_example = pnp.array(
strong_initial_weights,
requires_grad=False,
)
fig, ax = qml.draw_mpl(
strong_circuit,
level="device",
decimals=2,
)(
x_example,
strong_w_example,
)
plt.show()
print(
qml.draw(
strong_circuit,
level="device",
decimals=2,
max_length=120,
)(
x_example,
strong_w_example,
)
)
# ============================================================
# 15. StronglyEntanglingLayersを学習する
# ============================================================
print()
print("=== StronglyEntanglingLayers ===")
print("Weight shape:", strong_shape)
print(
"Number of trainable parameters:",
np.prod(strong_shape),
)
strong_weights = train_model(
strong_circuit,
strong_initial_weights,
X_train,
y_train,
num_epochs=30,
batch_size=15,
stepsize=0.03,
rng=rng,
)
strong_test_accuracy = accuracy(
strong_circuit,
strong_weights,
X_test,
y_test,
)
print(
"Strong circuit test accuracy:",
f"{strong_test_accuracy:.4f}",
)
# ============================================================
# 16. 古典的なロジスティック回帰との比較
# ============================================================
classical_model = LogisticRegression(
max_iter=1000,
)
classical_model.fit(
X_train,
y_train,
)
classical_test_accuracy = (
classical_model.score(
X_test,
y_test,
)
)
print()
print("=== Comparison ===")
print(
"Basic quantum circuit:",
f"{basic_test_accuracy:.4f}",
)
print(
"Strong quantum circuit:",
f"{strong_test_accuracy:.4f}",
)
print(
"Logistic regression:",
f"{classical_test_accuracy:.4f}",
)
# ============================================================
# 17. 最初の5サンプルの予測結果
# ============================================================
strong_predictions = predict(
strong_circuit,
strong_weights,
X_test,
)
print()
print(
"Predicted classes:",
strong_predictions[:5],
)
print(
"True classes: ",
y_test[:5],
)
コードの解説
なぜ標準化より先にデータを分割するのか
古いコードでは、次の順序で処理していました。
scaler = StandardScaler().fit(X)
X_scaled = scaler.transform(X)
X_train, X_test, y_train, y_test = train_test_split(
X_scaled,
y,
)
これは機械学習として望ましくありません。
なぜでしょうか。
StandardScaler.fit(X)は、全データの平均値と標準偏差を計算します。
\mu_j
=
\frac{1}{N}
\sum_{i=1}^{N}
x_{ij}
\sigma_j
=
\sqrt{
\frac{1}{N}
\sum_{i=1}^{N}
\left(
x_{ij}-\mu_j
\right)^2
}
全データを使って$\mu_j$と$\sigma_j$を求めると、テストデータの情報が訓練時の前処理へ混入します。
これはデータリークと呼ばれます。
そこで、先に訓練データとテストデータを分けます。
X_train, X_test, y_train, y_test = train_test_split(
X,
y,
test_size=0.3,
random_state=42,
stratify=y,
)
その後、訓練データだけで標準化の変換を決めます。
scaler = StandardScaler()
X_train = scaler.fit_transform(
X_train
)
X_test = scaler.transform(
X_test
)
テストデータにはfit_transformではなく、訓練データから決めた変換をtransformで適用するだけです。
stratify=yも指定しています。
Irisデータには3クラスがそれぞれ50サンプルずつ含まれています。stratify=yを指定することで、訓練データとテストデータのクラス比率をほぼ同じに保てます。
なぜ通常のNumPyとPennyLane NumPyを分けるのか
コードでは、二つのNumPyを使っています。
import numpy as np
from pennylane import numpy as pnp
通常のNumPyは、データ処理や結果表示に使います。
X_train
y_train
np.argmax(...)
一方、PennyLane NumPyは、勾配を計算する学習パラメータに使います。
weights = pnp.array(
initial_weights,
requires_grad=True,
)
PennyLane NumPyでは、requires_grad=Trueが付いた配列を自動微分の対象として扱えます。
入力データや教師ラベルまで学習対象にする必要はないため、学習パラメータだけをPennyLane NumPyで管理する方が、何を微分しているのかが明確になります。
データを量子状態へ埋め込む
基礎編では、次のコードを使っています。
qml.AngleEmbedding(
features=X_batch,
wires=range(n_wires),
rotation="Y",
)
これは、4個の特徴量を4個の$R_Y$ゲートの角度として使います。
U_{\mathrm{embed}}(\boldsymbol{x})
=
R_Y(x_0)
\otimes
R_Y(x_1)
\otimes
R_Y(x_2)
\otimes
R_Y(x_3)
初期状態は、
|0000\rangle
です。
各量子ビットに$R_Y(x_i)$を作用させると、
R_Y(x_i)|0\rangle
=
\cos\left(\frac{x_i}{2}\right)|0\rangle
+
\sin\left(\frac{x_i}{2}\right)|1\rangle
となります。
ここで大切なのは、特徴量そのものを量子ビットへ保存しているわけではないことです。
特徴量は回転角として使われ、三角関数を通して量子状態の振幅へ反映されます。
したがって、データをどの範囲へスケーリングするかは、単なる前処理ではなく、量子モデルの構造の一部です。
回転ゲートには周期性があります。
R_Y(x+2\pi)
=
-R_Y(x)
全体位相を無視すれば、$2\pi$周期で同じ物理状態を表します。
そのため、非常に大きな値をそのまま角度へ入れると、異なる特徴量が量子状態上で似た位置へ折り返される可能性があります。
今回のIrisデータでは標準化後の値が比較的小さいため、そのまま角度として使っています。
より一般には、MinMaxScalerなどを使い、特徴量を$[-\pi,\pi]$などへ明示的に写像する方法も考えられます。
学習可能な回転ゲート
基礎編では、各量子ビットへqml.Rotを作用させています。
for wire in range(n_wires):
qml.Rot(
*weights[wire],
wires=wire,
)
qml.Rotは3個のパラメータを持つ、一般的な1量子ビット回転です。
R(\phi,\theta,\omega)
=
R_Z(\omega)
R_Y(\theta)
R_Z(\phi)
量子状態には、右側の演算子から順番に作用します。
したがって、
- $R_Z(\phi)$
- $R_Y(\theta)$
- $R_Z(\omega)$
の順番で回転します。
なぜ3個のパラメータが必要なのでしょうか。
純粋な1量子ビット状態をブロッホ球上の一点として指定するだけなら、極角と方位角の2個で足ります。
一方、任意の1量子ビットユニタリ変換を表すには、全体位相を除いて3個の自由度が必要です。
qml.Rotは、この3自由度を$Z$-$Y$-$Z$のEuler角として表現しています。
基礎編では4量子ビットに一つずつRotを置くため、学習パラメータ数は、
4
\times
3
=
12
です。
旧版の記事では、3クラスごとに独立した回路パラメータを用意していたため、
3
\times
4
\times
3
=
36
パラメータでした。
今回の修正版では、一つの回路から3個の測定値を返すため、クラス数をパラメータ配列の次元に含める必要はありません。
CNOTゲートはいつエンタングルメントを作るのか
CNOTゲートについて、
CNOTを入れれば必ずエンタングルメントが生成される
と理解するのは正確ではありません。
CNOTの作用は、制御量子ビットと標的量子ビットの入力状態に依存します。
制御量子ビットが基底状態の場合
例えば、
|1\rangle|0\rangle
へCNOTを作用させると、
|1\rangle|0\rangle
\longrightarrow
|1\rangle|1\rangle
となります。
しかし、
|1\rangle|1\rangle
=
|1\rangle
\otimes
|1\rangle
なので、これは依然として積状態です。
制御量子ビットが厳密に$|0\rangle$または$|1\rangle$であるだけなら、CNOTがビットを反転させても、それだけでエンタングルメントが生じるとは限りません。
制御量子ビットが重ね合わせ状態の場合
制御量子ビットが、
|+\rangle
=
\frac{|0\rangle+|1\rangle}{\sqrt{2}}
で、標的量子ビットが$|0\rangle$なら、入力状態は、
\frac{|00\rangle+|10\rangle}{\sqrt{2}}
です。
ここへCNOTを作用させると、
\frac{|00\rangle+|10\rangle}{\sqrt{2}}
\longrightarrow
\frac{|00\rangle+|11\rangle}{\sqrt{2}}
となります。
右辺はBell状態であり、一つの量子ビットだけの状態へ分解できません。
したがって、CNOTの役割は、
制御量子ビットに存在する重ね合わせを、量子ビット間の非古典的な相関へ変換する
と理解すると、本質が見えやすくなります。
今回の回路では、AngleEmbeddingとRotによって各量子ビットが一般に重ね合わせ状態になっています。
その後にCNOTを作用させるため、入力特徴量と学習パラメータに依存した量子ビット間相関を導入できます。
ただし、任意のパラメータで常に強いエンタングルメントが存在するわけではありません。
特定の入力やパラメータでは、CNOTを含む回路であっても最終状態が積状態に近くなる可能性があります。
なぜ3個の期待値を出力するのか
3クラス分類では、最終的に3個のスコアが必要です。
そこで、
return tuple(
qml.expval(qml.Z(wire))
for wire in range(n_classes)
)
として、量子ビット0、1、2のPauli-$Z$期待値を返しています。
それぞれ、
z_k
=
\langle\psi|
Z_k
|\psi\rangle
です。
$Z$の固有値は$+1$と$-1$なので、期待値は、
-1
\leq
z_k
\leq
1
の範囲を取ります。
この3個の値を、そのまま確率と呼ぶことはできません。
例えば、
[0.8, 0.4, -0.1]
という期待値の総和は1ではありません。
そこで、softmax関数を使います。
def softmax(logits):
shifted = logits - pnp.max(
logits,
axis=1,
keepdims=True,
)
exp_logits = pnp.exp(shifted)
return exp_logits / pnp.sum(
exp_logits,
axis=1,
keepdims=True,
)
softmaxを通した後は、
0
<
p_k
<
1
かつ、
\sum_{k=0}^{2}
p_k
=
1
となります。
ただし、ここで得られる値は、量子力学のBorn則から直接得られた3クラス確率ではありません。
Pauli-$Z$期待値を古典的なsoftmax関数へ入力して得た、分類モデル上の確率です。
量子回路と古典的な出力層を組み合わせた、ハイブリッドモデルだと考える方が正確です。
損失関数
教師ラベル$y_i$に対して、正解クラスへモデルが割り当てた確率を$p_{i,y_i}$とします。
交差エントロピー損失は、
\mathcal{L}
=
-\frac{1}{N}
\sum_{i=1}^{N}
\log p_{i,y_i}
です。
正解クラスの確率が1に近いほど損失は小さくなります。
反対に、正解クラスへ非常に小さな確率しか与えなければ、$-\log p$が大きくなります。
コードでは、
correct_probabilities = probabilities[
pnp.arange(len(y_batch)),
y_batch,
]
loss = -pnp.mean(
pnp.log(
correct_probabilities
+ 1.0e-12
)
)
としています。
1.0e-12は、数値誤差によって確率が0に極端に近づいたとき、log(0)が発生するのを避けるためです。
一つずつ回路を呼ばず、バッチとして入力する
旧版では、次のようにサンプルを一つずつ量子回路へ渡していました。
predictions = np.array(
[
quantum_neural_net(
var,
x,
)
for x in X
]
)
この方法は分かりやすい一方、Pythonループの回数が多くなり、シミュレータではかなり遅くなります。
修正版では、
basic_circuit(
X_batch,
weights,
)
のように、複数サンプルをまとめてQNodeへ渡しています。
X_batchの形状は、
(batch_size, n_wires)
です。
AngleEmbeddingがバッチ次元を扱い、各サンプルに同じ学習パラメータを適用します。
QNodeの戻り値は、3個の期待値からなるtupleです。
各期待値は、
(batch_size,)
の配列になるため、
pnp.stack(
circuit(
X_batch,
weights,
),
axis=1,
)
によって、
(batch_size, n_classes)
へ並べ替えています。
この変更によって、コードが短くなるだけでなく、特にシミュレータ上の実行時間を大きく削減できます。
なぜテストデータを毎epoch見ないのか
旧版では、各epochの最後にテスト精度を表示していました。
for epoch in range(num_epochs):
...
accuracy = evaluate_on_test_data(...)
一見すると問題なさそうですが、テスト精度を毎回見ながら、
- epoch数
- 学習率
- 回路の深さ
- 初期値
- バッチサイズ
を調整すると、実質的にテストデータをモデル選択へ使ってしまいます。
テストデータは、最終的な汎化性能を評価するために残しておくべきです。
今回のコードでは、学習中は訓練精度だけを表示します。
train_accuracy = accuracy(
circuit,
weights,
X_train,
y_train,
)
そして、学習が終わった後に一度だけテスト精度を計算します。
test_accuracy = accuracy(
circuit,
weights,
X_test,
y_test,
)
本格的なモデル選択を行う場合は、訓練データをさらに、
- training set
- validation set
へ分けるか、交差検証を使う必要があります。
基礎編のパラメータ数
基礎編では、4量子ビットにRotゲートを一つずつ置きます。
各Rotゲートは3パラメータなので、
N_{\mathrm{parameter}}
=
4
\times
3
=
12
です。
CNOTゲートには学習パラメータがありません。
したがって、基礎編の学習パラメータ配列は、
basic_initial_weights.shape
が、
(4, 3)
となります。
StronglyEntanglingLayersを使う
基礎編では、1層のRotゲートと、隣接量子ビット間のCNOTチェーンだけを使いました。
より深い回路を手作業で記述すると、コードが長くなります。
PennyLaneには、変分量子回路の典型的な構造をまとめたテンプレートが用意されています。
その一つが、
qml.StronglyEntanglingLayers
です。
qml.StronglyEntanglingLayers(
weights=weights,
wires=range(n_wires),
)
と書くだけで、複数層の1量子ビット回転と2量子ビットゲートを配置できます。
重みの形状
StronglyEntanglingLayersが要求する重みの形状は、
(n_layers, n_wires, 3)
です。
手作業で形状を記述するよりも、公式のshape()を使う方が安全です。
strong_shape = (
qml.StronglyEntanglingLayers.shape(
n_layers=n_layers,
n_wires=n_wires,
)
)
今回の場合は、
(3, 4, 3)
となります。
したがって、学習パラメータ数は、
3
\times
4
\times
3
=
36
です。
旧版の記事では、クラスごとに別々のパラメータを持たせていたため、
3
\times
4
\times
4
\times
3
=
144
パラメータとしていました。
修正版では、一つの回路から3クラス分の出力を返すため、クラス数の次元は不要です。
3層、4量子ビットの場合は36パラメータです。
1レイヤーですべての量子ビット対を結ぶわけではない
StronglyEntanglingLayersについて、
各レイヤーですべての量子ビット対を結合する
と説明するのは正確ではありません。
量子ビット数を$M$とし、レイヤーごとのrangeを$r$とすると、量子ビット$i$から、
(i+r)
\bmod
M
番目の量子ビットへ2量子ビットゲートを作用させます。
例えば、4量子ビットで$r=1$なら、
0 → 1
1 → 2
2 → 3
3 → 0
です。
$r=2$なら、
0 → 2
1 → 3
2 → 0
3 → 1
です。
rangeをレイヤーごとに変えることで、近い量子ビットだけでなく、異なる距離の量子ビットを結びます。
rangesを明示しなければ、PennyLaneがレイヤー番号に応じてrangeを決めます。
また、デフォルトの2量子ビットゲートはCNOTです。
CNOTゲート数
量子ビット数を$M>1$とすると、一つのレイヤーでは、各量子ビットから一つずつ2量子ビットゲートが配置されます。
したがって、基本的には1レイヤー当たり$M$個のエンタングラーがあります。
4量子ビットなら、1レイヤー当たり4個です。
旧版にあった、
1レイヤー当たり$n_{\mathrm{wires}}-1$個
という説明は、手作業で作った直線状CNOTチェーンには当てはまりますが、StronglyEntanglingLayersには当てはまりません。
実行結果の例
乱数シード、PennyLaneのバージョン、実行環境、最適化条件によって結果は変化します。
PennyLane 0.45.1、random_state=42を使った実行例では、おおむね次のような結果になります。
=== Basic circuit ===
Epoch 1, Loss: 1.187755, Train accuracy: 0.3714
Epoch 5, Loss: 0.917..., Train accuracy: 0.5...
...
Epoch 40, Loss: 0.86..., Train accuracy: 0.46...
Basic circuit test accuracy: 0.5778
=== StronglyEntanglingLayers ===
Weight shape: (3, 4, 3)
Number of trainable parameters: 36
Epoch 1, Loss: 1.17..., Train accuracy: 0.50...
Epoch 5, Loss: 0.70..., Train accuracy: 0.85...
...
Epoch 30, Loss: 0.60..., Train accuracy: 0.89...
Strong circuit test accuracy: 0.8667
=== Comparison ===
Basic quantum circuit: 0.5778
Strong quantum circuit: 0.8667
Logistic regression: 0.9111
ここで重要なのは、StronglyEntanglingLayersを使ったモデルが基礎回路より高精度になることではありません。
同じデータ分割では、古典的なロジスティック回帰が量子回路より高い精度を示しています。
この結果は失敗ではありません。
むしろ、
小さな古典データセットに対して、回路を深くしただけで量子モデルが有利になるわけではない
ことを確認できています。
100%精度が出ても、量子機械学習が優れているとは限らない
Irisデータは小さいため、乱数シードやデータ分割によっては、テスト精度が非常に高くなることがあります。
場合によっては100%になることもあります。
しかし、一度の分割で100%になっただけでは、モデルが本質的に優れているとは判断できません。
考えるべき点は複数あります。
テストサンプル数が少ない
テストデータは全体の30%なので、45サンプルです。
1サンプル正解数が変わるだけで、精度は、
\frac{1}{45}
\simeq
0.022
すなわち約2.2%変化します。
データ分割への依存
random_stateを変えると、訓練データとテストデータの組み合わせが変わります。
一つの分割で得られた高い精度が、別の分割でも再現するとは限りません。
初期値への依存
変分量子回路の損失関数は一般に非凸です。
パラメータの初期値を変えると、異なる局所解へ到達する可能性があります。
モデル選択の影響
テスト精度を確認しながら回路構造やepoch数を調整すると、テストデータへ間接的に過学習します。
より慎重に評価するには、
- 複数の乱数初期値
- 複数のtrain/test分割
- 交差検証
- 精度の平均値と標準偏差
- 混同行列
- クラス別precision、recall
- 古典モデルとの比較
が必要です。
回路が深ければ強いとは限らない
StronglyEntanglingLayersは、基礎回路より多くのパラメータと2量子ビットゲートを持ちます。
そのため、より複雑な関数を表現できる可能性があります。
しかし、回路を深くすれば必ず学習が改善するわけではありません。
最適化が難しくなる
パラメータ数が増えると、損失関数の地形も複雑になります。
局所解や平坦な領域に入り、学習が進みにくくなる可能性があります。
量子回路が深くなると、勾配が非常に小さくなるbarren plateauも問題になり得ます。
ただし、すべての回路で一律に発生するわけではなく、初期化方法、回路の局所性、損失関数、入力状態などに依存します。
過学習しやすくなる
モデルの自由度が高くなると、訓練データへ強く適合する一方、新しいデータに対する性能が下がる可能性があります。
実機ではノイズの影響が増える
2量子ビットゲートは、一般に1量子ビットゲートより誤差が大きくなりやすい部分です。
深い回路では、
- ゲート誤差
- デコヒーレンス
- 読み出し誤差
- 有限回測定による統計揺らぎ
が蓄積します。
シミュレータ上で高精度な深い回路が、そのまま実機で有利とは限りません。
default.qubitと実際の量子コンピュータの違い
今回使用している、
dev = qml.device(
"default.qubit",
wires=n_wires,
)
は、古典コンピュータ上で量子状態を計算するシミュレータです。
shotsを指定していないため、shots=Noneの解析モードで動作します。
この場合、期待値は量子状態ベクトルからほぼ厳密に計算されます。
実際の量子コンピュータでは、期待値を一度の測定で知ることはできません。
同じ回路を何度も実行し、測定結果の平均から期待値を推定します。
例えば、有限回測定を試すには、現在のPennyLaneではqml.set_shotsを使えます。
@qml.set_shots(shots=1000)
@qml.qnode(
dev,
interface="autograd",
)
def finite_shot_circuit(X_batch, weights):
...
shots=1000なら、同じ回路を1000回測定した統計から期待値を推定します。
有限shotsでは、同じ入力と同じパラメータを使っても、出力に統計揺らぎが生じます。
さらに実機では、理想的な有限shotsシミュレーションに加えて、ハードウェアノイズも存在します。
したがって、
default.qubitの解析モードで得られた精度
と、
実際の量子デバイス上で得られる精度
は、区別して考える必要があります。
また、有限shotsで学習する場合は、解析モードと同じ微分方式が常に使えるわけではありません。
実機や有限shotsでは、parameter-shift法などの量子勾配計算法が必要になります。
古典モデルとの比較が必要な理由
量子機械学習モデルを作ったとき、量子モデル単体の精度だけを示しても、その値が良いのか判断できません。
少なくとも、単純な古典モデルと同じデータ分割で比較する必要があります。
本記事では、ロジスティック回帰を使っています。
classical_model = LogisticRegression(
max_iter=1000,
)
classical_model.fit(
X_train,
y_train,
)
classical_test_accuracy = (
classical_model.score(
X_test,
y_test,
)
)
Irisデータに対しては、ロジスティック回帰でも高い精度を得られます。
量子モデルが古典モデルを上回らなかったとしても、量子機械学習のコードに意味がないわけではありません。
この比較によって、
- 量子回路を使うだけでは優位性にならない
- 回路構造だけでなく、問題設定が重要である
- データの埋め込み方法がモデル性能を左右する
- 古典的な基準モデルを用意する必要がある
ことが分かります。
厳密には、まだ考えるべきことが多い
このサンプルは量子機械学習の入口ですが、厳密に考えると、非自明な問題が多く残っています。
なぜ$R_Y$埋め込みなのか
今回は元の記事に合わせて$R_Y$を使いました。
しかし、
- $R_X$
- $R_Y$
- $R_Z$
- 振幅埋め込み
- 基底埋め込み
- データ再アップロード
- 問題固有のHamiltonianを使った埋め込み
など、選択肢は多数あります。
どの埋め込みがよいかは、入力データの対称性や、分類したい境界の構造に依存します。
なぜPauli-$Z$を測るのか
今回の出力は、
\langle Z_0\rangle
,\quad
\langle Z_1\rangle
,\quad
\langle Z_2\rangle
です。
しかし、
- $X$や$Y$の期待値
- 複数量子ビットの相関
- $\langle Z_i Z_j\rangle$
- 計算基底での確率
- Hamiltonianの期待値
を使う方法もあります。
どの観測量が分類に適しているかは自明ではありません。
量子エンタングルメントは本当に必要なのか
CNOTを増やして精度が上がったとしても、それだけでエンタングルメントが性能向上の原因だとは言えません。
パラメータ数、回路の深さ、非線形性、最適化のしやすさも同時に変わるためです。
エンタングルメントの効果を調べるには、
- CNOTなし
- CNOTあり
- 同じパラメータ数
- 同程度の回路深さ
という制御実験が必要です。
量子回路の表現力と汎化性能は同じではない
複雑な回路は多くの関数を表現できます。
しかし、表現力が高いことと、未知データへよく汎化することは同じではありません。
表現力を増やしすぎれば、訓練データへ過適合する可能性があります。
シミュレータで速い構造と実機でよい構造は異なる
シミュレータでは、複雑なゲートも行列演算として扱えます。
実機では、使用する量子デバイスが直接実装できるnative gateへ分解されます。
したがって、抽象的な回路図のゲート数だけでなく、
- native gateへの分解後の深さ
- 量子ビット接続性
- SWAPゲート数
- 2量子ビットゲート誤差
- 測定回数
を考える必要があります。
まとめ
本記事では、PennyLane 0.45.1を使い、Irisデータを3クラス分類する量子機械学習モデルを作りました。
基礎編では、
AngleEmbeddingRot- CNOTチェーン
- Pauli-$Z$期待値
- softmax
- 交差エントロピー損失
を手作業で組み合わせました。
応用編では、StronglyEntanglingLayersを使い、複数層の回転ゲートとエンタングラーを持つ回路へ拡張しました。
今回の修正版で特に重要な点は、次のとおりです。
- 標準化より先にtrain/test分割を行う
- テストデータを標準化の
fitへ含めない - テスト精度を毎epoch見ない
-
expansion_strategyではなくlevel="device"を使う -
qml.templates.を付けず、qml.AngleEmbeddingなどを直接使う - 一つの量子回路から3個の期待値を返す
- 期待値をそのまま確率と呼ばず、softmaxを通す
-
StronglyEntanglingLayers.shape()で重み形状を決める - 1レイヤーですべての量子ビット対を結ぶわけではない
-
StronglyEntanglingLayersのパラメータ数を正しく数える - 古典的な基準モデルと比較する
- 一度の高精度だけで量子優位性を主張しない
- 解析的シミュレータと有限shots・実機を区別する
Iris分類で量子優位性が示されるわけではありません。
しかし、この小さな例を通して、
データを量子状態へ変換し、測定結果から損失を作り、古典最適化によって量子回路を学習する
という変分量子機械学習の基本的な流れを、一通り確認できます。
ここから先は、埋め込み方法、観測量、回路構造、ノイズ、有限shots、古典モデルとの比較を変えながら、
量子回路を使うことで、本当にどのような構造を表現できているのか
を検証することが重要です。
StronglyEntanglingLayersの重み形状は公式に(L, M, 3)で、各レイヤーの結合先は$(i+r)\bmod M$です。ranges=Noneではレイヤー番号に応じてrangeが選ばれ、デフォルトのエンタングラーはCNOTです。(PennyLane)
また、全データを標準化してから分割する処理はデータリークになるため、記事では分割後に訓練データだけでStandardScaler.fit_transform()する構成へ変更しています。(scikit-learn.org)

