コスト関数
このレッスンでは、コスト関数の評価方法を学びます。
- まず、Qiskit Runtime primitives について学びます。
- コスト関数 を定義します。これはオプティマイザーが最小化(または最大化)すべき問題の目標を定義する、問題固有の関数です。
- Qiskit Runtime primitives を使った測定戦略を定義し、速度と精度のトレードオフを最適化します。
Primitives
古典・量子を問わず、あらゆる物理系はさまざまな状態を取り得ます。たとえば、道路上の車は質量・位置・速度・加速度といった物理量によってその状態が特徴付けられます。同様に、量子系も異なる配置や状態を持ちますが、測定や状態の時間発展の扱い方が古典系とは異なります。これにより、量子力学に固有の重ね合わせやエンタングルメントといった特性が生じます。車の状態を速度や加速度などの物理量で記述できるように、量子系の状態も数学的オブジェクトであるオブザーバブルを用いて記述できます。
量子力学では、状態は規格化された複素数の列ベクトル、すなわちケット()で表され、オブザーバブルはケットに作用するエルミート線形演算子()で表されます。オブザーバブルの固有ベクトル()は固有状態と呼ばれます。オブザーバブルをその固有状態のひとつ()に対して測定すると、対応する固有値()が読み出しとして得られます。
量子系をどのように測定するか、そして何を測定できるかについては、Qiskit が提供する 2 つの primitive が役立ちます。
Sampler:量子状態 が与えられた場合、この primitive は各計算基底状態が得られる確率を求めます。Estimator:量子オブザーバブル と状態 が与えられた場合、この primitive は の期待値を計算します。
Sampler primitive
Sampler primitive は、状態 を準備する量子 Circuit が与えられたとき、計算基底の各状態 が得られる確率を計算します。具体的には以下を計算します。
ここで は Qubit の数、 は任意の出力バイナリ文字列 の整数表現(すなわち 2 進数の整数)です。
Qiskit Runtime の Sampler は、量子デバイス上で Circuit を複数回実行し、各実行で測定を行い、得られたビット列から確率分布を復元します。実行回数(ショット数)が多いほど結果は正確になりますが、より多くの時間と量子リソースが必要です。
ただし、可能な出力の数は Qubit 数 に対して指数的に増加する(すなわち )ため、密な確率分布を捉えるにはショット数も指数的に増やす必要があります。そのため、Sampler が効率的に機能するのは疎な確率分布の場合に限られます。ここで、目標状態 は計算 基底状態の線形結合として表現可能であり、その項数が Qubit 数に対して高々多項式的に増加することが条件です。
また Sampler は、Circuit の一部分(全状態の部分集合)から確率を取得するように設定することもできます。
Estimator primitive
Estimator primitive は、量子状態 に対するオブザーバブル の期待値を計算します。ここで、オブザーバブルの確率は で表され、 はオブザーバブル の固有状態です。期待値は、状態 の測定で得られる全ての可能な結果 (すなわちオブザーバブルの固有値)を、対応する確率で重み付けした平均として定義されます。
しかし、オブザーバブルの期待値を計算することは常に可能とは限りません。固有基底が未知の場合が多いからです。Qiskit Runtime の Estimator は、複雑な代数的処理を用いて、実際の量子デバイス上でオブザーバブルを固有基底が既知の他のオブザーバブルの組み合わせに分解することで期待値を推定します。
簡単に言えば、Estimator は測定方法が不明なオブザーバブルを、パウリ演算子と呼ばれる、より単純で測定可能なオブザーバブルに分解します。任意の演算子は 個のパウリ演算子の組み合わせで表現できます。
このとき
ここで は Qubit 数、(、すなわち 4 進数の整数)、 です。
この分解を行った後、Estimator は元の Circuit から各オブザーバブル に対して新しい Circuit を導出し、パウリオブザーバブルを計算基底で効果的に対角化して測定します。パウリオブザーバブルは が事前に分かっているため容易に測定できますが、一般のオブザーバブルではそうではありません。
各 について、Estimator は対応する Circuit を量子デバイス上で複数回実行し、計算基底で出力状態を測定し、各可能な出力 が得られる確率 を計算します。次に各出力 に対応する の固有値 を求め、 と掛け合わせ、全ての結果を足し合わせて状態 に対するオブザーバブル の期待値を得ます。
個のパウリの期待値を計算することは(指数的に増加するため)非現実的であり、Estimator が効率的に機能するのは の大部分がゼロの場合(すなわち密な分解ではなく疎なパウリ分解)に限られます。形式的には、この計算が効率的に解けるためには、非ゼロ項の数が Qubit 数 に対して高々多項式的に増加することが必要です:
Sampler で説明したように、確率のサンプリングも効率的である必要があるという暗黙の仮定があることに読者は気づくでしょう。すなわち
期待値計算のガイド付き例
単一 Qubit の状態 と、以下のオブザーバブルを考えます。
理論的な期待値は です。
このオブザーバブルを直接測定する方法が分からないため、期待値を直接計算することはできず、 として再表現する必要があります。、 であることから、同じ結果が得られることが確認できます。
と を直接計算する方法を見てみましょう。 と は可換でない(すなわち、固有基底が共有されていない)ため、同時に測定することはできません。そのため、補助 Circuit が必要です。
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-aer qiskit-ibm-runtime rustworkx
from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
# The following code will work for any other initial single-qubit state and observable
original_circuit = QuantumCircuit(1)
original_circuit.h(0)
H = SparsePauliOp(["X", "Z"], [2, -1])
aux_circuits = []
for pauli in H.paulis:
aux_circ = original_circuit.copy()
aux_circ.barrier()
if str(pauli) == "X":
aux_circ.h(0)
elif str(pauli) == "Y":
aux_circ.sdg(0)
aux_circ.h(0)
else:
aux_circ.id(0)
aux_circ.measure_all()
aux_circuits.append(aux_circ)
original_circuit.draw("mpl")
# Auxiliary circuit for X
aux_circuits[0].draw("mpl")
# Auxiliary circuit for Z
aux_circuits[1].draw("mpl")
Sampler を使って手動で計算を行い、Estimator で結果を検証してみましょう。
from qiskit.primitives import StatevectorSampler, StatevectorEstimator
from qiskit.result import QuasiDistribution
import numpy as np
## SAMPLER
shots = 10000
sampler = StatevectorSampler()
job = sampler.run(aux_circuits, shots=shots)
# Run the sampler job and step through results
expvals = []
for index, pauli in enumerate(H.paulis):
data_pub = job.result()[index].data
bitstrings = data_pub.meas.get_bitstrings()
counts = data_pub.meas.get_counts()
quasi_dist = QuasiDistribution(
{outcome: freq / shots for outcome, freq in counts.items()}
)
# Use the probabilities and known eigenvalues of Pauli operators to estimate the expectation value.
val = 0
if str(pauli) == "X":
val += -1 * quasi_dist.get(1, 0)
val += 1 * quasi_dist.get(0, 0)
if str(pauli) == "Y":
val += -1 * quasi_dist.get(1, 0)
val += 1 * quasi_dist.get(0, 0)
if str(pauli) == "Z":
val += 1 * quasi_dist.get(0, 0)
val += -1 * quasi_dist.get(1, 0)
expvals.append(val)
# Print expectation values
print("Sampler results:")
for pauli, expval in zip(H.paulis, expvals):
print(f" >> Expected value of {str(pauli)}: {expval:.5f}")
total_expval = np.sum(H.coeffs * expvals).real
print(f" >> Total expected value: {total_expval:.5f}")
# Use estimator for comparison
observables = [
*H.paulis,
H,
] # Note: run for individual Paulis as well as full observable H
estimator = StatevectorEstimator()
job = estimator.run([(original_circuit, observables)])
estimator_expvals = job.result()[0].data.evs
# Print results
print("Estimator results:")
for obs, expval in zip(observables, estimator_expvals):
if obs is not H:
print(f" >> Expected value of {str(obs)}: {expval:.5f}")
else:
print(f" >> Total expected value: {expval:.5f}")
Sampler results:
>> Expected value of X: 1.00000
>> Expected value of Z: 0.00420
>> Total expected value: 1.99580
Estimator results:
>> Expected value of X: 1.00000
>> Expected value of Z: 0.00000
>> Total expected value: 2.00000
数学的厳密性(任意)
を の固有状態の基底で展開すると となり、以下が導かれます。
目標オブザーバブル の固有値や固有状態が未知であるため、まずその対角化を考える必要があります。 がエルミートであるため、ユニタリ変換 が存在し、 が成り立ちます。ここで は対角固有値行列であり、 のとき 、 です。
これより、期待値は次のように書き直せます。
系が状態 にある場合に を測定する確率が であることから、上記の期待値は次のように表せます。
確率が ではなく状態 から取られることに注意することが非常に重要です。これが行列 が不可欠な理由です。行列 と固有値 をどのように求めるかという疑問が生じるかもしれません。もし固有値が既に分かっているならば、変分アルゴリズムの目標がまさにこの の固有値を求めることですから、量子コンピューターを使う必要はありません。
幸い、回避策があります。任意の 行列は、 個のパウリ行列と単位行列のテンソル積の 個の線形結合として書くことができ、これらはすべてエルミートかつユニタリであり、 と が既知です。これが Runtime の Estimator が内部的 に行っていることであり、任意の Operator オブジェクトを SparsePauliOp に分解します。
使用できる演算子を以下に示します。
では、 をパウリ行列と単位行列を用いて書き直してみましょう。
ここで (、すなわち 4 進数)、 です。
ここで 、 であり、 が成り立ちます。
コスト関数
一般に、コスト関数は問題の目標と、試行状態がその目標に対してどれだけ優れているかを記述するために使われます。この定義は、化学・機械学習・金融・最適化などのさまざまな例に適用できます。
系の基底状態を求めるシンプルな例を考えましょう。目標は、エネルギーを表すオブザーバブル(ハミルトニアン )の期待値を最小化することです。
Estimator を使って期待値を評価し、その値をオプティマイザーに渡して最小化することができます。最適化が成功すれば、最適なパラメータ値 が返され、そこから提案された解の状態 を構築し、観測された期待値 を計算できます。
注意すべき点は、コスト関数を最小化できるのは、考慮している限られた状態の集合に対してのみであるということです。これにより 2 つの可能性が生じます。
- Ansatz が探索空間上の解状態を定義していない場合:この場合、オプティマイザーは 解を見つけられないため、探索空間をより正確に表現できる別の Ansatz を試す必要があります。
- オプティマイザーが有効な解を見つけられない場合:最適化はグローバルに定義される場合とローカルに定義される場合があります。これについては後のセクションで詳しく説明します。
いずれにせよ、古典的な最適化ループを実行しながら、コスト関数の評価を量子コンピューターに頼ることになります。この観点から見ると、オプティマイザーがコスト関数を評価するたびにブラックボックス量子オラクルを呼び出す純粋に古典的な取り組みと考えることもできます。
def cost_func_vqe(params, circuit, hamiltonian, estimator):
"""Return estimate of energy from estimator
Parameters:
params (ndarray): Array of ansatz parameters
ansatz (QuantumCircuit): Parameterized ansatz circuit
hamiltonian (SparsePauliOp): Operator representation of Hamiltonian
estimator (Estimator): Estimator primitive instance
Returns:
float: Energy estimate
"""
pub = (circuit, hamiltonian, params)
cost = estimator.run([pub]).result()[0].data.evs
return cost
from qiskit.circuit.library import TwoLocal
observable = SparsePauliOp.from_list([("XX", 1), ("YY", -3)])
reference_circuit = QuantumCircuit(2)
reference_circuit.x(0)
variational_form = TwoLocal(
2,
rotation_blocks=["rz", "ry"],
entanglement_blocks="cx",
entanglement="linear",
reps=1,
)
ansatz = reference_circuit.compose(variational_form)
theta_list = (2 * np.pi * np.random.rand(1, 8)).tolist()
ansatz.decompose().draw("mpl")
まずシミュレーターである StatevectorEstimator を使って実行します。デバッグには通常これが推奨されますが、デバッグ実行の直後に実際の量子ハードウェアでの計算も行います。関心のある問題の多くは、最先端のスーパーコンピューター施設なしには古典的にシミュレートできなくなってきています。
estimator = StatevectorEstimator()
cost = cost_func_vqe(theta_list, ansatz, observable, estimator)
print(cost)
[-0.58744589]
次に、実際の量子コンピューターで実行します。構文の変更に注意してください。pass_manager に関連するステップは次の例でさらに詳しく説明します。変分アルゴリズムにおいて特に重要なステップのひとつが、Qiskit Runtime セッションの使用です。セッションを開始することで、パラメーターが更新されるたびに新しいキューで待機することなく、変分アルゴリズムの複数回のイテレーションを実行できます。これは、キュー待ち時間が長い場合や多くのイテレーションが必要な場合に重要です。IBM Quantum® Network のパートナーのみが Runtime セッションを使用できます。セッションへのアクセス権がない場合は、一度に送信するイテレーション数を減らし、最新のパラメーターを保存して将来の実行 に使用することができます。イテレーションを送信しすぎたり、キュー待ち時間が長すぎる場合は、ジョブ送信間の長い遅延を示すエラーコード 1217 が発生することがあります。
# Estimated usage: < 1 min. Benchmarked at 7 seconds on an Eagle processor
# Load necessary packages:
from qiskit_ibm_runtime import (
QiskitRuntimeService,
Session,
EstimatorOptions,
EstimatorV2 as Estimator,
)
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
# Select the least busy backend:
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, min_num_qubits=ansatz.num_qubits, simulator=False
)
# Or get a specific backend:
# backend = service.backend("ibm_brisbane")
# Use a pass manager to transpile the circuit and observable for the specific backend being used:
pm = generate_preset_pass_manager(backend=backend, optimization_level=1)
isa_ansatz = pm.run(ansatz)
isa_observable = observable.apply_layout(layout=isa_ansatz.layout)
# Set estimator options
estimator_options = EstimatorOptions(resilience_level=1, default_shots=10_000)
# Open a Runtime session:
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
cost = cost_func_vqe(theta_list, isa_ansatz, isa_observable, estimator)
session.close()
print(cost)
上記 2 つの計算で得られた値は非常に近いことに注意してください。結果を改善するための手法については、以下でさらに詳しく説明します。
非物理系へのマッピングの例
最大カット(Max-Cut)問題は、グラフの頂点を2つの互いに素な集合に分割し、2つの集合の間の辺の数を最大化する組み合わせ最適化問題です。より正式には、 を頂点の集合、 を辺の集合とする無向グラフ が与えられたとき、Max-Cut 問題は頂点を2つの互いに素な部分集合 と に分割し、一方の端点が 、もう一方が にある辺の数を最大化することを求めます。
Max-Cut はクラスタリング、ネットワーク設計、相転移など、さまざまな問題を解くために応用できます。まず問題グラフを作成してみましょう:
import rustworkx as rx
from rustworkx.visualization import mpl_draw
n = 4
G = rx.PyGraph()
G.add_nodes_from(range(n))
# The edge syntax is (start, end, weight)
edges = [(0, 1, 1.0), (0, 2, 1.0), (0, 3, 1.0), (1, 2, 1.0), (2, 3, 1.0)]
G.add_edges_from(edges)
mpl_draw(
G, pos=rx.shell_layout(G), with_labels=True, edge_labels=str, node_color="#1192E8"
)
この問題は2値最適化問題として表現できます。グラフのノード数を (この場合は )として、各ノード に対して2値変数 を考えます。この変数は、ノード がグループ と呼ぶ集合に属する場合は 、グループ と呼ぶ集合に属する場合は の値をとります。また、(隣接行列 の要素 )をノード からノード へ向かう辺の重みとします。グラフが無向であるため が成り立ちます。そして、次のコスト関数を最大化する問題として定式化できます:
この問題を量子コンピューターで解くために、コスト関数を観測量の期待値として表現します。ただし、Qiskit がネイティブにサポートする観測量はパウリ演算子で構成されており、その固有値は と ではなく と です。そこで、次のような変数変換を行います:
ここで とします。隣接行列 を使って全辺の重みに容易にアクセスできます。これを利用してコスト関数を求めます:
この変換は次を意味します:
したがって、最大化したい新しいコスト関数は次のようになります:
さらに、量子コンピューターは最大値ではなく最小値(通常は最低エネルギー)を見つける性質があるため、 を最大化する代わりに、次の式を最小化します:
変数の値が と をとるコスト関数の最小化問題が得られたので、パウリ との以下のアナロジーを利用できます:
つまり、変数 は Qubit に作用する Gate と等価です。さらに:
したがって、考察する観測量は次のようになります:
そして、後からオフセット項を加える必要があります:
この演算子は、辺で接続されたノード上の Z 演算子の項の線形結合です(第0番目の Qubit が最も右に位置することに注意してください):。演算子が構築されたら、Qiskit Circuit ライブラリの QAOAAnsatz Circuit を使って QAOA アルゴリズム用のアンザッツを簡単に構築できます。
from qiskit.circuit.library import QAOAAnsatz
from qiskit.quantum_info import SparsePauliOp
hamiltonian = SparsePauliOp.from_list(
[("IIZZ", 1), ("IZIZ", 1), ("IZZI", 1), ("ZIIZ", 1), ("ZZII", 1)]
)
ansatz = QAOAAnsatz(hamiltonian, reps=2)
# Draw
ansatz.decompose(reps=3).draw("mpl")
# Sum the weights, and divide by 2
offset = -sum(edge[2] for edge in edges) / 2
print(f"""Offset: {offset}""")
Offset: -2.5
Runtime Estimator はハミルトニアンとパラメーター化されたアンザッツを直接受け取り、必要なエネルギーを返します。そのため、QAOA インスタンスのコスト関数は非常にシンプルです:
def cost_func(params, ansatz, hamiltonian, estimator):
"""Return estimate of energy from estimator
Parameters:
params (ndarray): Array of ansatz parameters
ansatz (QuantumCircuit): Parameterized ansatz circuit
hamiltonian (SparsePauliOp): Operator representation of Hamiltonian
estimator (Estimator): Estimator primitive instance
Returns:
float: Energy estimate
"""
pub = (ansatz, hamiltonian, params)
cost = estimator.run([pub]).result()[0].data.evs
# cost = estimator.run(ansatz, hamiltonian, parameter_values=params).result().values[0]
return cost
import numpy as np
x0 = 2 * np.pi * np.random.rand(ansatz.num_parameters)
estimator = StatevectorEstimator()
cost = cost_func_vqe(x0, ansatz, hamiltonian, estimator)
print(cost)
1.473098768180865
# Estimated usage: < 1 min, benchmarked at 6 seconds on ibm_osaka, 5-23-24
# Load some necessary packages:
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import Session, EstimatorV2 as Estimator
# Select the least busy backend:
backend = service.least_busy(
operational=True, min_num_qubits=ansatz.num_qubits, simulator=False
)
# Or get a specific backend:
# backend = service.backend("ibm_brisbane")
# Use a pass manager to transpile the circuit and observable for the specific backend being used:
pm = generate_preset_pass_manager(backend=backend, optimization_level=1)
isa_ansatz = pm.run(ansatz)
isa_hamiltonian = hamiltonian.apply_layout(layout=isa_ansatz.layout)
# Set estimator options
estimator_options = EstimatorOptions(resilience_level=1, default_shots=10_000)
# Open a Runtime session:
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
cost = cost_func_vqe(x0, isa_ansatz, isa_hamiltonian, estimator)
# Close session after done
session.close()
print(cost)
1.1120776913677988
このサンプルは「Applications」のセクションで改めて取り上げ、オプティマイザーを活用して探索空間を反復する方法を学びます。一般的に、このプロセスには次のステップが含まれます:
- オプティマイザーを活用して最適なパラメーターを見つける
- 最適パラメーターをアンザッツにバインドして固有値を求める
- 固有値を問題の定義に変換する
測定戦略:速度と精度のトレードオフ
前述のとおり、私たちはノイズのある量子コンピューターをブラックボックスオラクルとして使用しています。ノイズにより取得される値が非決定的になり、ランダムな揺らぎが生じます。この揺らぎは、特定のオプティマイザーが提案された解に収束することを妨げたり、完全に防いだりする可能性があります。これは、量子ユーティリティの探索を段階的に進め、量子優位性に向けて前進する上で対処すべき一般的な課題です:
Qiskit Runtime Primitive のエラー抑制およびエラー軽減オプションを使用することで、ノイズに対処し、現在の量子コンピューターの有用性を最大化できます。
エラー抑制
エラー抑制とは、エラーを最小化するためにコンパイル時に Circuit を最適化・変換する技術を指します。これは基本的なエラー処理技術であり、通常は全体の実行時間に対して古典的な前処理のオーバーヘッドが生じます。このオーバーヘッドには、量子ハードウェア上で実行するための Circuit の Transpile が含まれます:
- 量子システムで利用可能なネイティブゲートを使って Circuit を表現する
- 仮想 Qubit を物理 Qubit にマッピングする
- 接続性の要件に基づいて SWAP を追加する
- 1Q ゲートおよび 2Q ゲートを最適化する
- アイドル状態の Qubit にダイナミカルデカップリングを追加してデコヒーレンスの影響を防ぐ
Primitive では optimization_level オプションの設定と高度なトランスパイルオプションの選択により、エラー抑制技術を利用できます。後続のコースでは、結果を改善するためのさまざまな Circuit 構築方法を詳しく説明しますが、ほとんどの場合は optimization_level=3 に設定することを推奨します。
ここでは、単純な理想的な動作を持つサンプル Circuit を用いて、トランスパイルプロセスにおける最適化レベルを上げることの価値を可視化してみましょう。
from qiskit.circuit import Parameter, QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
theta = Parameter("theta")
qc = QuantumCircuit(2)
qc.x(1)
qc.h(0)
qc.cp(theta, 0, 1)
qc.h(0)
observables = SparsePauliOp.from_list([("ZZ", 1)])
qc.draw("mpl")
上記の Circuit は、 などの適切な区間にわたる位相を挿入することで、与えられた観測量の正弦波状の期待値を算出できます。
## Setup phases
import numpy as np
phases = np.linspace(0, 2 * np.pi, 50)
# phases need to be expressed as a list of lists in order to work
individual_phases = [[phase] for phase in phases]
シミュレーターを使って最適化されたトランスパイルの有用性を示します。エラー軽減の有用性を示すために実際のハードウェアを使う場合については後ほど説明します。ここでは QiskitRuntimeService を用いて実際の Backend(この例では ibm_brisbane)を取得し、AerSimulator でその Backend のノイズ挙動を含めてシミュレーションします。
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_aer import AerSimulator
# get a real backend from the runtime service
service = QiskitRuntimeService()
backend = service.backend("ibm_brisbane")
# generate a simulator that mimics the real quantum system with the latest calibration results
backend_sim = AerSimulator.from_backend(backend)
次に、パスマネージャーを使って Circuit を Backend の「命令セットアーキテクチャ(ISA)」にトランスパイルします。これは Qiskit Runtime の新しい要件です。Backend に送信されるすべての Circuit は、Backend のターゲットの制約に準拠していなければなりません。つまり、Backend の ISA(デバイスが理解・実行できる命令セット)に基づいて記述される必要があります。これらのターゲット制約は、デバイスのネイティブな基底ゲート、Qubit の接続性、そして必要に応じてパルスやその他の命令タイミング仕様といった要素によって決まります。
なお、今回のケースではこれを2回行います。1回目は optimization_level = 0、2回目は 3 に設定します。それぞれで Estimator Primitive を使い、さまざまな位相の値における観測量の期待値を推定します。
# Import estimator and specify that we are using the simulated backend:
from qiskit_ibm_runtime import EstimatorV2 as Estimator
estimator = Estimator(mode=backend_sim)
circuit = qc
# Use a pass manager to transpile the circuit and observable for the backend being simulated.
# Start with no optimization:
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
pm = generate_preset_pass_manager(backend=backend_sim, optimization_level=0)
isa_circuit = pm.run(circuit)
isa_observables = observables.apply_layout(layout=isa_circuit.layout)
noisy_exp_values = []
pub = (isa_circuit, isa_observables, [individual_phases])
cost = estimator.run([pub]).result()[0].data.evs
noisy_exp_values = cost[0]
# Repeat above steps, but now with optimization = 3:
exp_values_with_opt_es = []
pm = generate_preset_pass_manager(backend=backend_sim, optimization_level=3)
isa_circuit = pm.run(circuit)
isa_observables = observables.apply_layout(layout=isa_circuit.layout)
pub = (isa_circuit, isa_observables, [individual_phases])
cost = estimator.run([pub]).result()[0].data.evs
exp_values_with_opt_es = cost[0]
最後に結果をプロットすると、最適化なしでも計算の精度はかなり良好でしたが、最適化レベルを 3 に上げることで確実に向上したことがわかります。より深く複雑な Circuit では、最適化レベル 0 と 3 の差はより顕著になる可能性があります。ここで使用したのは、toy モデルとして用いたごくシンプルな Circuit です。
import matplotlib.pyplot as plt
plt.plot(phases, noisy_exp_values, "o", label="opt=0")
plt.plot(phases, exp_values_with_opt_es, "o", label="opt=3")
plt.plot(phases, 2 * np.sin(phases / 2) ** 2 - 1, label="ideal")
plt.ylabel("Expectation")
plt.legend()
plt.show()
エラー緩和
エラー緩和とは、実行時にデバイスのノイズをモデル化することで、Circuit のエラーを低減できる技術を指します。一般的に、モデルの学習に関連する量子的な前処理オーバーヘッドと、生成したモデルを用いて生の結果のエラーを緩和するための古典的な後処理オーバーヘッドが発生します。
Qiskit Runtime プリミティブの resilience_level オプションは、エラーに対してどの程度の耐性を持たせるかを指定します。レベルが高いほど、量子サンプリングのオーバーヘッドによる処理時間の増加と引き換えに、より正確な結果が得られます。レジリエンスレベルを使用することで、プリミティブクエリにエラー緩和を適用する際のコストと精度のトレードオフを調整できます。
エラー緩和技術を実装する場合、緩和前のバイアスと比較して、結果のバイアスが低減されることが期待されます。場合によっては、バイアスが完全に消えることもあります。ただし、これにはコストが伴います。推定量のバイアスを低減するにつれて、統計的なばらつき(すなわち分散)が増加します。これはサンプリングプロセスにおける Circuit あたりのショット数をさらに増やすことで対処できます。ただし、 バイアスを低減するために必要な量を超えたオーバーヘッドが発生するため、デフォルトでは行われません。以下の例に示すように、options.executions.shots で Circuit あたりのショット数を調整することで、この動作を簡単に有効にできます。
このコースでは、Qiskit Runtime プリミティブが実行できるエラー緩和について、完全な実装の詳細を必要とせずに概要レベルで説明するために、これらのエラー緩和モデルを取り上げます。
Twirled readout error extinction(T-REx)
Twirled readout error extinction(T-REx)は、Pauli twirling と呼ばれる技術を使用して、量子測定プロセス中に発生するノイズを低減します。この技術はノイズの特定の形式を前提としないため、非常に汎用的かつ効果的です。
全体的なワークフロー:
- ゼロ状態のデータをランダムなビットフリップ(測定前の Pauli X)付きで取得する
- 目的の(ノイズのある)状態のデータをランダムなビットフリップ(測定前の Pauli X)付きで取得する
- 各データセットに対して特殊関数を計算し、除算する
これは options.resilience_level = 1 で設定でき、以下の例で示します。
ゼロノイズ外挿法
ゼロノイズ外挿法(ZNE)は、目的の量子状態を準備する Circuit のノイズを最初に増幅し、複数の異なるノイズレベルで測定値を取得したうえで、それらの測定値を用いてノイズのない結果を推定するという手法です。
全体的なワークフロー:
- 複数のノイズ係数に対して Circuit ノイズを増幅する
- ノイズを増幅したすべての Circuit を実行する
- ゼロノイズの限界へ外挿する
これは options.resilience_level = 2 で設定できます。さらに、noise_factors、noise_amplifiers、extrapolators などさまざまなオプションを探索することで最適化できますが、それはこのコースの範囲外です。こちらに記載されているオプションを試してみることをお勧めします。
各手法にはそれぞれ固有のオーバーヘッドがあり、必要な量子計算の回数(時間)と結果の精度とのトレードオフが生じます。
Qiskit Runtime の緩和・抑制オプションの使い方
以下に、Qiskit Runtime でエラー緩和と抑制を使用しながら期待値を計算する方法を示します。先ほどとまったく同じ Circuit とオブザーバブルを利用できますが、今回は最適化レベルをレベル 2 に固 定したまま、使用する_レジリエンス_(エラー緩和技術)を調整します。このエラー緩和プロセスは、最適化ループ全体を通じて複数回実行されます。
エラー緩和はシミュレーターでは利用できないため、この部分は実際のハードウェア上で実行します。
# Estimated usage: 8 minutes, benchmarked on an Eagle processor, 5-23-24
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import (
Session,
EstimatorOptions,
EstimatorV2 as Estimator,
)
# We select the least busy backend
# Select the least busy backend
# backend = service.least_busy(
# operational=True, min_num_qubits=ansatz.num_qubits, simulator=False
# )
# Or use a specific backend
backend = service.backend("ibm_brisbane")
# Initialize some variables to save the results from different runs:
exp_values_with_em0_es = []
exp_values_with_em1_es = []
exp_values_with_em2_es = []
# Use a pass manager to optimize the circuit and observables for the backend chosen:
pm = generate_preset_pass_manager(backend=backend, optimization_level=2)
isa_circuit = pm.run(circuit)
isa_observables = observables.apply_layout(layout=isa_circuit.layout)
# Open a session and run with no error mitigation:
estimator_options = EstimatorOptions(resilience_level=0, default_shots=10_000)
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
pub = (isa_circuit, isa_observables, [individual_phases])
cost = estimator.run([pub]).result()[0].data.evs
session.close()
exp_values_with_em0_es = cost[0]
# Open a session and run with resilience = 1:
estimator_options = EstimatorOptions(resilience_level=1, default_shots=10_000)
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
pub = (isa_circuit, isa_observables, [individual_phases])
cost = estimator.run([pub]).result()[0].data.evs
session.close()
exp_values_with_em1_es = cost[0]
# Open a session and run with resilience = 2:
estimator_options = EstimatorOptions(resilience_level=2, default_shots=10_000)
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
pub = (isa_circuit, isa_observables, [individual_phases])
cost = estimator.run([pub]).result()[0].data.evs
session.close()
exp_values_with_em2_es = cost[0]
先ほどと同様に、使用した 3 つのエラー緩和レベルについて、位相角の関数として期待値をプロットできます。注意深く見ると、エラー緩和によって結果がわずかに改善されていることが分かります。この効果は、より深く複雑な Circuit においてはるかに顕著に現れます。
import matplotlib.pyplot as plt
plt.plot(phases, exp_values_with_em0_es, "o", label="unmitigated")
plt.plot(phases, exp_values_with_em1_es, "o", label="resil = 1")
plt.plot(phases, exp_values_with_em2_es, "o", label="resil = 2")
plt.plot(phases, 2 * np.sin(phases / 2) ** 2 - 1, label="ideal")
plt.ylabel("Expectation")
plt.legend()
plt.show()
まとめ
このレッスンでは、コスト関数の作成方法を学びました。
- コスト関数の作成方法
- Qiskit Runtime プリミティブを活用してノイズを緩和・抑制する方法
- 速度と精度を最適化するための測定戦略の定義方法
変分ワークロードの全体像は以下のとおりです。
コスト関数は最適化ループの反復ごとに実行されます。次のレッスンでは、古典的なオプティマイザーがコスト関数の評価を使用して新しいパラメーターを選択する方法について説明します。
import qiskit
import qiskit_ibm_runtime
print(qiskit.version.get_version_info())
print(qiskit_ibm_runtime.version.get_version_info())
1.1.0
0.23.0