微分可能な回路・VQE・QAOA

シミュレータを通した勾配

任意のゲートパラメータに requires_grad=True の torch.Tensor を渡せます。ゲート行列・状態の時間発展(両実行モード)・確率・期待値まで、 パイプライン全体が微分可能なtorch演算で構成されています:

import torch
from blueqat import Circuit
from blueqat.utils import Z

theta = torch.tensor(0.4, dtype=torch.float64, requires_grad=True)
energy = Circuit(1).rx(theta)[0].expect(1.0 * Z[0])
energy.backward()
theta.grad        # -sin(0.4)、厳密な解析勾配

このため変分アルゴリズムにパラメータシフト則は不要で、通常の torch.optim のオプティマイザがそのまま使えます。

パウリ演算子とハミルトニアン

blueqat.utils がパウリ代数を提供します:

from blueqat.utils import X, Y, Z, I, from_qubo, qubo_bit

h = 0.5 * Z[0] * Z[1] + 1.2 * X[0] - 3.0
h = h.simplify()
h.to_matrix(2)                   # 密/疎のtorch行列
term = (X[0] * Y[1]).to_term()
evo = term.get_time_evolution()  # exp(-i t P) を回路に追加する関数

from_qubo はQUBOのコスト行列をIsingハミルトニアンに変換します。

VQE

import torch
from blueqat import Circuit
from blueqat.utils import AnsatzBase, Vqe, Z, X

class MyAnsatz(AnsatzBase):
    def get_circuit(self, params):
        return Circuit(2).rx(params[0])[0].ry(params[1])[1].cx[0, 1]

hamiltonian = (1.0 * Z[0] * Z[1] + 0.5 * X[0]).simplify()
ansatz = MyAnsatz(hamiltonian, n_params=2)
result = Vqe(ansatz).run()
result.most_common(4)

Vqe は任意の torch.optim オプティマイザクラス、オプションの サンプラ(ショットベース推定の get_measurement_sampler(n) 、厳密で 勾配を保つ non_sampling_sampler )、 initial_params を受け取れます。

実行の再現と収束の確認

initial_params を渡さない場合、 Vqe.run() はランダムなパラメータから 始まるため、同じ問題でも実行ごとに異なる局所最適解に落ちます。QAOAでは 「最適解が見つかる確率」が実行ごとに大きく変わることもあります。 seed= を渡すと実行全体が決定的になります:

Vqe(ansatz, seed=42).run()          # または: Vqe(ansatz).run(seed=42)

1つのシードが両方の乱数を決めます。初期パラメータと、 get_measurement_sampler(n, seed=...) で作ったシード対応サンプラの サンプリングです。 Circuit.run(seed=...) と同じく専用の generator を 使うので、グローバルな乱数には影響しません。

各実行は反復ごとの目的関数値を記録するので、別のオプティマイザで回し直さ なくても収束の様子を確認できます:

result = Vqe(ansatz, seed=42).run()
len(result.loss_history)      # 実際に回った反復数
result.loss_history[-1]       # 最後に記録された目的関数値

ショット雑音とパラメータシフト則

ショットから期待値を推定すると autograd のグラフが失われます(カウントは 数値であって、ゲート角の微分可能な関数ではありません)。そのため ショットベースの目的関数には逆伝播できる勾配がありません。 Vqe は それを検知してパラメータシフト則に切り替えます。同じ推定器を、 ずらしたパラメータで評価することで勾配を得る方法です:

from blueqat.utils import get_measurement_sampler

vqe = Vqe(ansatz, sampler=get_measurement_sampler(2000, seed=3), seed=42)
result = vqe.run()          # 動く。逆伝播だけでは動かない

各ゲートの寄与は (E(theta + pi/2) - E(theta - pi/2)) / 2 で、有限差分の 近似ではなく厳密です。これを autograd でアンザッツのパラメータへ連鎖 させるので、QAOAの角のように1つのパラメータが多数のゲートを動かす場合も 寄与が正しく合算されます。

gradient= で選択を上書きできます。 'backprop' は常に逆伝播 (ショットサンプラでは失敗します)、 'parameter_shift' は常にシフト則、 既定の 'auto' は目的関数が微分可能かどうかで選びます。シフト則は パラメータ付きゲート1適用あたり2回の追加実行を要するので、厳密経路での 逆伝播の代わりに無条件で使うものではありません。

厳密に成り立つのは、生成子の固有値が2つでその差が1のゲートに限られます: rx ・ ry ・ rz ・ p / phase ・ rxx ・ ryy ・ rzz ・ cp ・ exch 。制御回転( crx ・ cry ・ crz )は 固有値が4つで4項の規則が必要なため、黙って誤った勾配を返さずエラーに します。

parameter_shift_gradient() は同じ仕組みを直接公開して おり、任意のアンザッツとエネルギー推定器に対してエネルギーと勾配を返します。

ショット数はどれくらい要るか

思ったより少なくて足ります。パラメータ27個のQAOA(K6のMax-Cut、 step=1 、 120反復)を、1回の推定あたり 100・1000・8000 ショットで最適化したところ、 どれも収束し、最適カットが得られる確率は 0.66〜0.72 でした。少ない側で 劣化していないので、閾値があるとしても 100 より下です。

実行時間もこの3水準でほとんど変わりませんでした。ここが実用上の要点です。 ショットベース実行のコストを支配するのは、シフト則が要求する回路実行の回数 (パラメータ付きゲート1適用あたり反復ごとに2回)であって、その中のショット数 ではありません。ショット数を削って速くしようとしてもほとんど効きません。 効くのはパラメータ数か反復数を減らすことです。

ショット雑音は、確率的勾配降下法の雑音と似た振る舞いも見せます。上と同じ実験 では、厳密勾配の参照が悪い局所解に落ちた一方、ショットベースの実行はすべて 良い解に到達しました。ただしこれは1つの構成での事例で一般則ではありません (同じ問題は多くの初期値から良い解に到達します)。とはいえ、雑音のある勾配を 単に劣化したものと決めてかからない理由にはなります。

QAOA

QaoaAnsatz は、項が互いに可換なハミルトニアン (自動チェックされます)から標準的なQAOAアンザッツを構築します:

from blueqat.utils import QaoaAnsatz, Vqe, from_qubo

qubo = [[1, 1], [1, 0]]
h = from_qubo(qubo)
ansatz = QaoaAnsatz(h.simplify(), step=2)
result = Vqe(ansatz).run()
print(result.most_common(2))

完全な自己検証付きプログラムはリポジトリの examples/maxcut_qaoa.py と examples/vqe_ground_state.py を参照してください。