量子アルゴリズム:変分量子アルゴリズム
Takashi Imamichi(2024年5月24日)
元の講義資料はPDFでダウンロードできます。なお、コードスニペットは静的な画像のため、一部が非推奨となっている場合があります。
この実験を実行するおおよそのQPU時間は9分です(Eagleプロセッサーでテスト済み)。
(このノートブックは、Open Planの制限時間内に評価が完了しない場合があります。量子コンピューティングのリソースを大切にご利用ください。)
1. はじめに
このチュートリアルでは、ハイブリッド量子古典アル ゴリズムの概要を解説します。特に、変分量子固有値ソルバー(VQE)と量子近似最適化アルゴリズム(QAOA)に焦点を当てます。これらのアルゴリズムの主な目的は、パラメーター化された量子ゲートを持つ量子Circuitを活用して、最適化問題を解決することです。
量子コンピューティングの進歩にもかかわらず、現在の量子デバイスにはノイズが存在するため、深い量子Circuitから有意義な結果を得ることは困難です。この課題を克服するために、VQEとQAOAはハイブリッド量子古典アプローチを採用しています。このアプローチでは、比較的浅い量子Circuitを量子計算で繰り返し実行し、目標とするパラメーター化された量子Circuitのパラメーターを古典計算で最適化します。
QAOAは、さまざまなエラー緩和・抑制技術の適用により、ユーティリティ規模で対象問題の最適解を提供できる可能性があります。VQEは量子化学など多くの応用がありますが、スケーラビリティには課題があります。しかし、Krylov部分空間対角化やサンプリングベースの量子対角化(SQD)など、VQEを補完・拡張する固有値関連のアプローチが数多く登場しています。VQEを理解することは、これまでに登場した幅広い古典量子ハイブリッドアルゴリズムを理解するための重要な第一歩です。
このモジュールでは、VQEとQAOAの基本概念と実装について説明します。より高度なトピックやこれらのアルゴリズムのスケールアップ手法については、今後のチュートリアルで取り上げます。
このノートブックを実行するには、以下のライブラリが環境にインストールされている必要があります。 まだインストールしていない場合は、以下のセルのコメントを外して実行することでインストールできます。
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-ibm-runtime rustworkx scipy
# % pip install 'qiskit[visualization]' qiskit-ibm-runtime
2. 単純なハミルトニアンの最小固有値の計算
まず、VQEがどのように機能するかを理解するために、非常に単純なケースに適用してみます。VQEを使ってパウリ 行列の最小固有値を計算します。最初に、いくつかの一般的なパッケージをインポートします。
import numpy as np
from qiskit.circuit import ParameterVector, QuantumCircuit
from qiskit.primitives import StatevectorEstimator, StatevectorSampler
from qiskit.quantum_info import SparsePauliOp
from scipy.optimize import minimize
次に、対象の演算子を定義し、行列形式で確認します。
op = SparsePauliOp("Z")
op.to_matrix()
array([[ 1.+0.j, 0.+0.j],
[ 0.+0.j, -1.+0.j]])
固有値は古典的な方法で容易に求めることができるので、結果を検証できます。ユーティリティ規模にスケールアップするにつれて、これは困難になる可能性があります。ここではnumpyを使用します。
# compute eigenvalues with numpy
result = np.linalg.eigh(op.to_matrix())
print("Eigenvalues:", result.eigenvalues)
Eigenvalues: [-1. 1.]
変分量子アルゴリズムを使って固有値を求めるために、変分パラメーターを持つGateで構成されたCircuitを構築します。
# define a variational form
param = ParameterVector("a", 3)
qc = QuantumCircuit(1, 1)
qc.u(param[0], param[1], param[2], 0)
qc_estimator = qc.copy()
qc.measure(0, 0)
qc.draw("mpl")
演算子( など)の期待値を推定したい場合はEstimatorを使用します。システムの状態を確認したい場合はSamplerを使用します。
sampler = StatevectorSampler()
estimator = StatevectorEstimator()
Samplerを使って、ランダムなパラメーター値 [1, 2, 3] でビット列0と1のカウントを計算できます。
# compute counts of bitstrings with random parameter values by Sampler
result = sampler.run([(qc, [1, 2, 3])]).result()
counts = result[0].data.c.get_counts()
counts
{'0': 783, '1': 241}
確率 を用いて によりZの期待値を計算できます。
# compute the expectation value of Z based on the counts
(counts.get("0", 0) - counts.get("1", 0)) / sum(counts.values())
0.529296875
このCircuitは機能しましたが、選択したパラメーター値は非常に低エネルギー(または低固有値)の状態に対応していませんでした。得られた固有値は最小値よりもかなり高い値です。Estimatorを使用した場合も同様の結果が得られます。
EstimatorはBitの測定を含まない量子Circuitを受け取ることに注意してください。
result = estimator.run([(qc_estimator, op, [1, 2, 3])]).result()
result[0].data.evs
array(0.54030231)
パラメーターを探索して、最低の固有値をもたらすものを見つける必要があります。 変分形式のパラメーター値を受け取り、期待値 を返す関数を作成します。
# define a cost function to look for the minimum eigenvalue of Z
def cost(x):
result = sampler.run([(qc, x)]).result()
counts = result[0].data.c.get_counts()
expval = (counts.get("0", 0) - counts.get("1", 0)) / sum(counts.values())
# the following line shows the trajectory of the optimization
print(expval, counts)
return expval
SciPyの minimize 関数を適用して、Zの最小固有値を求めましょう。
# minimize the cost function with scipy's minimize
min_result = minimize(cost, [0, 0, 0], method="COBYLA", tol=1e-8)
min_result
1.0 {'0': 1024}
0.494140625 {'0': 765, '1': 259}
0.466796875 {'0': 751, '1': 273}
0.564453125 {'0': 801, '1': 223}
-0.4296875 {'1': 732, '0': 292}
-0.984375 {'1': 1016, '0': 8}
-0.8984375 {'1': 972, '0': 52}
-0.990234375 {'1': 1019, '0': 5}
-0.892578125 {'1': 969, '0': 55}
-0.986328125 {'1': 1017, '0': 7}
-0.861328125 {'1': 953, '0': 71}
-1.0 {'1': 1024}
-0.982421875 {'1': 1015, '0': 9}
-0.99609375 {'1': 1022, '0': 2}
-0.986328125 {'1': 1017, '0': 7}
-1.0 {'1': 1024}
-0.990234375 {'1': 1019, '0': 5}
-0.998046875 {'1': 1023, '0': 1}
-0.99609375 {'1': 1022, '0': 2}
-1.0 {'1': 1024}
-1.0 {'1': 1024}
-1.0 {'1': 1024}
-1.0 {'1': 1024}
-0.998046875 {'1': 1023, '0': 1}
-1.0 {'1': 1024}
-1.0 {'1': 1024}
-0.998046875 {'1': 1023, '0': 1}
-0.998046875 {'1': 1023, '0': 1}
-0.998046875 {'1': 1023, '0': 1}
-1.0 {'1': 1024}
-0.99609375 {'1': 1022, '0': 2}
-1.0 {'1': 1024}
-0.99609375 {'1': 1022, '0': 2}
-0.998046875 {'1': 1023, '0': 1}
-0.998046875 {'1': 1023, '0': 1}
-0.99609375 {'1': 1022, '0': 2}
-0.998046875 {'1': 1023, '0': 1}
-1.0 {'1': 1024}
-0.998046875 {'1': 1023, '0': 1}
-0.998046875 {'1': 1023, '0': 1}
-0.99609375 {'1': 1022, '0': 2}
-1.0 {'1': 1024}
-0.998046875 {'1': 1023, '0': 1}
-1.0 {'1': 1024}
-0.998046875 {'1': 1023, '0': 1}
-0.998046875 {'1': 1023, '0': 1}
-1.0 {'1': 1024}
-0.998046875 {'1': 1023, '0': 1}
-0.998046875 {'1': 1023, '0': 1}
-1.0 {'1': 1024}
-1.0 {'1': 1024}
-1.0 {'1': 1024}
-1.0 {'1': 1024}
-1.0 {'1': 1024}
-1.0 {'1': 1024}
-0.998046875 {'1': 1023, '0': 1}
-0.994140625 {'1': 1021, '0': 3}
-1.0 {'1': 1024}
-1.0 {'1': 1024}
-1.0 {'1': 1024}
-1.0 {'1': 1024}
-1.0 {'1': 1024}
-1.0 {'1': 1024}
message: Optimization terminated successfully.
success: True
status: 1
fun: -1.0
x: [ 3.182e+00 1.338e+00 1.664e-01]
nfev: 63
maxcv: 0.0
# check counts of bitstrings with the optimal parameters
result = sampler.run([(qc, min_result.x)]).result()
result[0].data.c.get_counts()
{'0': 1, '1': 1023}
2.1 演習
VQEを使って の最小固有値を計算してください。
z2 = SparsePauliOp("ZZ")
print(z2)
print(z2.to_matrix())
SparsePauliOp(['ZZ'],
coeffs=[1.+0.j])
[[ 1.+0.j 0.+0.j 0.+0.j 0.+0.j]
[ 0.+0.j -1.+0.j 0.+0.j 0.+0.j]
[ 0.+0.j 0.+0.j -1.+0.j 0.+0.j]
[ 0.+0.j 0.+0.j 0.+0.j 1.+0.j]]
# compute eigenvalues with numpy
# define a variational form
# qc = ...
# compute counts of bitstrings with a random parameter values by Sampler
# result = sampler.run(...)
# result
# compute the expectation value of ZZ based on the counts
# verify the expectation value of ZZ with Estimator
# define a cost function to look for the minimum eigenvalue of ZZ
# def cost(x):
# expval = ...
# return expval
# minimize the cost function with scipy's minimize
# min_result = minimize(cost, [...], method="COBYLA", tol=1e-8)
# min_result
# check counts of bitstrings with the optimal parameter values
# result = sampler.run(qc, min_result.x).result()
# result
演習の解答
対象の演算子を定義し、行列形式で確認します。
z2 = SparsePauliOp("ZZ")
print(z2)
print(z2.to_matrix())
SparsePauliOp(['ZZ'],
coeffs=[1.+0.j])
[[ 1.+0.j 0.+0.j 0.+0.j 0.+0.j]
[ 0.+0.j -1.+0.j 0.+0.j 0.+0.j]
[ 0.+0.j 0.+0.j -1.+0.j 0.+0.j]
[ 0.+0.j 0.+0.j 0.+0.j 1.+0.j]]
変分量子アルゴリズムを使って固有値を求めるために、変分パラメーターを持つGateで構成されたCircuitを構築します。
# define a variational form
param = ParameterVector("a", 6)
qc = QuantumCircuit(2, 2)
qc.u(param[0], param[1], param[2], 0)
qc.u(param[3], param[4], param[5], 1)
qc_estimator = qc.copy()
qc.measure([0, 1], [0, 1])
qc.draw("mpl")
演算子( など)の期待値を推定したい場合はEstimatorを使用します。システムの状態を確認したい場合はSamplerを使用します。
sampler = StatevectorSampler()
estimator = StatevectorEstimator()
# compute counts of bitstrings with random parameter values by Sampler
result = sampler.run([(qc, [1, 2, 3, 4, 5, 6])]).result()
counts = result[0].data.c.get_counts()
counts
{'10': 661, '11': 203, '01': 47, '00': 113}
# compute the expectation value of ZZ based on the counts
(
counts.get("00", 0)
- counts.get("01", 0)
- counts.get("10", 0)
+ counts.get("11", 0)
) / sum(counts.values())
-0.3828125
このCircuitは機能しましたが、選択したパラメーター値は非常に低エネルギー(または低固有値)の状態に対応していませんでした。得られた固有値は最小値よりもかなり高い値です。Estimatorを使用した場合も同様の結果が得られます。
# verify the expectation value of ZZ with Estimator
result = estimator.run([(qc_estimator, z2, [1, 2, 3, 4, 5, 6])]).result()
result[0].data.evs
array(-0.35316516)
パラメーターを探索して、最低の固有値をもたらすものを見つける必要があります。
# define a cost function to look for the minimum eigenvalue of ZZ
def cost(x):
result = sampler.run([(qc, x)]).result()
counts = result[0].data.c.get_counts()
expval = (
counts.get("00", 0)
- counts.get("01", 0)
- counts.get("10", 0)
+ counts.get("11", 0)
) / sum(counts.values())
print(expval, counts)
return expval
# minimize the cost function with scipy's minimize
min_result = minimize(cost, [0, 0, 0, 0, 0, 0], method="COBYLA", tol=1e-8)
min_result
1.0 {'00': 1024}
0.578125 {'00': 808, '01': 216}
0.5234375 {'00': 780, '01': 244}
0.548828125 {'00': 793, '01': 231}
0.3515625 {'00': 637, '10': 164, '11': 55, '01': 168}
0.3359375 {'00': 638, '11': 46, '10': 174, '01': 166}
0.283203125 {'00': 602, '10': 181, '01': 186, '11': 55}
-0.087890625 {'01': 414, '00': 184, '10': 143, '11': 283}
0.236328125 {'10': 27, '11': 623, '01': 364, '00': 10}
-0.0625 {'11': 261, '01': 403, '00': 219, '10': 141}
0.248046875 {'01': 366, '11': 628, '00': 11, '10': 19}
-0.0625 {'10': 145, '11': 254, '01': 399, '00': 226}
0.228515625 {'01': 373, '11': 609, '00': 20, '10': 22}
0.0546875 {'11': 376, '10': 273, '01': 211, '00': 164}
-0.447265625 {'01': 731, '10': 10, '11': 267, '00': 16}
-0.71484375 {'01': 871, '11': 99, '00': 47, '10': 7}
-0.46484375 {'01': 741, '00': 253, '10': 9, '11': 21}
-0.87890625 {'01': 962, '00': 39, '11': 23}
-0.640625 {'00': 176, '01': 837, '11': 8, '10': 3}
-0.88671875 {'01': 966, '00': 41, '11': 17}
-0.994140625 {'01': 1021, '11': 3}
-0.91796875 {'01': 982, '11': 35, '00': 7}
-0.994140625 {'01': 1021, '11': 2, '00': 1}
-0.939453125 {'01': 993, '00': 31}
-0.990234375 {'01': 1019, '11': 5}
-0.90234375 {'01': 974, '00': 21, '11': 29}
-0.98046875 {'01': 1014, '11': 10}
-0.994140625 {'01': 1021, '00': 3}
-0.990234375 {'01': 1019, '11': 4, '00': 1}
-0.98828125 {'01': 1018, '11': 6}
-0.990234375 {'01': 1019, '11': 4, '00': 1}
-0.994140625 {'01': 1021, '11': 2, '00': 1}
-0.99609375 {'01': 1022, '11': 2}
-0.998046875 {'01': 1023, '00': 1}
-0.99609375 {'01': 1022, '00': 2}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '11': 1}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '00': 1}
-0.998046875 {'01': 1023, '11': 1}
-0.998046875 {'01': 1023, '00': 1}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '11': 1}
-0.998046875 {'01': 1023, '11': 1}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '11': 1}
-0.998046875 {'01': 1023, '11': 1}
-0.998046875 {'01': 1023, '00': 1}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '00': 1}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '11': 1}
-0.998046875 {'01': 1023, '11': 1}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '11': 1}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '11': 1}
-0.998046875 {'01': 1023, '11': 1}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '11': 1}
-0.998046875 {'01': 1023, '11': 1}
-0.998046875 {'01': 1023, '00': 1}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '11': 1}
-1.0 {'01': 1024}
-0.99609375 {'01': 1022, '00': 1, '11': 1}
-0.998046875 {'01': 1023, '11': 1}
-0.998046875 {'01': 1023, '00': 1}
-0.998046875 {'01': 1023, '11': 1}
-1.0 {'01': 1024}
-0.99609375 {'01': 1022, '11': 1, '00': 1}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '00': 1}
-0.994140625 {'01': 1021, '00': 3}
-0.998046875 {'01': 1023, '00': 1}
-0.99609375 {'01': 1022, '11': 2}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '11': 1}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '11': 1}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '11': 1}
-0.998046875 {'01': 1023, '11': 1}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '00': 1}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '11': 1}
-0.998046875 {'01': 1023, '11': 1}
-0.998046875 {'01': 1023, '11': 1}
-0.99609375 {'01': 1022, '11': 2}
-1.0 {'01': 1024}
-0.998046875 {'01': 1023, '11': 1}
message: Optimization terminated successfully.
success: True
status: 1
fun: -0.998046875
x: [ 3.167e+00 6.940e-01 1.033e+00 -2.894e-02 8.933e-01
1.885e+00]
nfev: 128
maxcv: 0.0
message: Optimization terminated successfully.
success: True
status: 1
fun: -0.99609375
x: [ 3.098e+00 -5.402e-01 1.091e+00 -1.004e-02 3.615e-01
6.913e-01]
nfev: 115
maxcv: 0.0
numpyから得られた最小値に非常に近い固有値が得られました。
# check counts of bitstrings with the optimal parameters
result = sampler.run([(qc, min_result.x)]).result()
result[0].data.c.get_counts()
{'01': 1024}
3. Qiskitパターンによる量子最適化
このハウツーでは、Qiskitパターンと量子近似最適化について学びます。Qiskitパターンとは、量子コンピューティングのワークフローを実装するための直感的で再現可能な一連の手順です。
このパターンを組合せ最適化のコンテキストに適用し、ハイブリッド(量子古典)反復手法である**量子近似最適化アルゴリズム(QAOA)を使って最大カット(Max-Cut)**問題を解く方法を示します。
このQAOA部分は、量子近似最適化アルゴリズムチュートリアルの「パート1:小規模QAOA」に基づいています。スケールアップの方法については、そのチュートリアルを参照してください。
3.1 (小規模)最適化問題のQiskitパターン
このパートでは、小規模なMax-Cut問題を使って、量子コンピュータで最適化問題を解くための手順を説明します。 Max-Cut問題は解くことが困難(より具体的にはNP困難)な最適化問題であり、クラスタリング、ネットワーク科学、統計物理学などさまざまな応用があります。このチュートリアルでは、エッジで結ばれたノードのグラフを考え、エッジを「切断」することでノードを2つの集合に分割し、切断されるエッジの数を最大化することを目指します。

この問題を量子アルゴリズムにマッピングする前に背景を理解するため、まず最小化関数 を考えることでMax-Cut問題が古典的な組み合わせ最適化問題になることを確認します。
ここで入力 はグラフの各ノードに対応する成分を持つベクトルです。各成分は または (カットに含まれるか否か)に制約されます。この小規模な例では ノードのグラフを使用します。
ノードの対 に対して、対応するエッジ がカットに含まれるかどうかを示す関数を定義できます。たとえば は、 または のどちらか一方が のとき(つまりエッジがカットに含まれるとき)のみ となり、それ以外では です。カット内のエッジ数を最大化する問題は次のように定式化できます。
これは最小化問題として書き直すことができます。
この場合の の最小値は、カットで横断されるエッジ数が最大のときです。ご覧の通り、ここではまだ量子コンピューティングに関連する要素はありません。量子コンピュータが理解できる形にこの問題を再定式化する必要があります。
ノードのグラフを作成して問題を初期化します。
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import rustworkx as rx
from rustworkx.visualization import mpl_draw
n = 5
graph = rx.PyGraph()
graph.add_nodes_from(range(1, n + 1))
edge_list = [
(0, 1, 1.0),
(0, 2, 1.0),
(1, 2, 1.0),
(1, 3, 1.0),
(2, 4, 1.0),
(3, 4, 1.0),
]
graph.add_edges_from(edge_list)
pos = rx.spring_layout(graph, seed=2)
mpl_draw(graph, node_size=600, pos=pos, with_labels=True, labels=str)
3.2 ステップ1:古典的入力を量子問題にマッピングする
パターンの最初のステップは、古典的な問題(グラフ)を量子 Circuit および 演算子 にマッピングすることです。これには主に3つの手順があります。
- 一連の数学的再定式化を利用して、この問題を二次制約なし二値最適化(QUBO)問題の表記法で表現します。
- 最適化問題をハミルトニアンとして書き直します。このハミルトニアンの基底状態がコスト関数を最小化する解に対応します。
- 量子アニーリングに類似した処理によって、このハミルトニアンの基底状態を準備する量子 Circuit を作成します。
注意: QAOAの手法では、最終的にハイブリッドアルゴリズムのコスト関数を表す演算子(ハミルトニアン)と、問題の候補解を表す量子状態を表すパラメータ化された Circuit(Ansatz)が必要です。これらの候補状態からサンプリングし、コスト関数を使って評価することができます。