Qiskit で遊んでみる (13) — 量子位相推定
目的
量子フーリエ変換を眺める で量子フーリエ変換に触れたので、その応用として量子位相推定についてまとめておきたい。
簡単な近似計算を CPU 上で行い、より精度を高めた近似を GPU 上で行ってみたい。
量子位相推定とは
量子位相推定 に細かい説明があるので、二番煎じになるのだが、「与えられたユニタリ行列
量子フーリエ変換おさらい
量子フーリエ変換をモジュールとして利用するのでここでおさらいをする。
量子フーリエ変換を眺める では省いたのだが、Qiskit textbook の 量子フーリエ変換 を見ていくと、「4. 量子フーリエ変換」において、QFT の具体的なテンソル積分解の式が掲載されている。なお、右端が 0 版目の量子ビットで、左端が
ここで
やりたいこと (量子位相推定のアルゴリズム概略)
量子位相推定のキーとなるテクニックの 1 つは位相キックバックである。量子ビット
として現れる。これが要点のほとんどすべてである。
(2) 式を (1) 式と見比べると、QFT で現れる一部分になっていることが分かる。よって、
まとめると:
- 位相キックバックを用いて
を出力する回路を作り出す。P_{n-k} \left( \frac{2\pi}{2^k} \theta \right) \ket{+} - この回路を並列に
個並べてテンソル積の形でn を出力する回路にする。\bigotimes_{k=1}^n P_{n-k} \left( \frac{2\pi}{2^k} \theta \right) \ket{+}^{\otimes n} - 逆量子フーリエ変換
を作用させて\operatorname{QFT}^\dagger を測定する。\ket{\theta}
となる。
実際には少々これは正しくなくて、
なお、量子ビット数に対して必要なゲート数は
やってみる (数式)
まずは数式の形で追いかける。唐突だが、
となって、位相ゲートを
これを
となる。ここで (1) 式と比較すると、
ここで、
この変数変換を踏まえて (3) 式を書き直すと
となる。ほぼ QFT の形が得られた。
(1) 式と (3') 式を見比べると、量子ビットを
である。
(4) 式に逆 QFT を適用して計算基底で測定することで、
であり、両辺を
という、
以上が数式を用いた量子位相推定の解説となる。
やってみる (実装)
(4) 式を実装して、逆 QFT の回路を接続すれば実装は完了である。つまり、以下のようにすれば良い:
from qiskit import QuantumCircuit
import qiskit.quantum_info as qi
from qiskit.circuit.library import QFT
from qiskit.extensions import UnitaryGate
from qiskit_aer import AerSimulator
# 数式でいうユニタリ行列 U をランダムに作る。
unitary = qi.random_unitary(2, seed=1234)
unitary
Operator([[-0.65182701+0.35104045j, -0.06872086+0.66870741j],
[ 0.30111103-0.60101938j, 0.3613751 +0.64615469j]],
input_dims=(2,), output_dims=(2,))
再現性のために、seed
を固定してランダムなユニタリ行列を作った。続けて、
# U の固有値と固有ベクトルを得る。
w, vec = np.linalg.eig(unitary.data)
w = w[0]
vec = vec[:, 0]
# np.allclose(unitary.data @ vec, w * vec) # => True
arg = np.log(w)
arg = arg.imag * 1.j
print(arg)
theta = (arg / (2*np.pi) * (-1j)).real
assert 0 <= theta < 1
print(theta)
2.878968619668155j
0.4582020868266377
最終的には量子位相推定では後者の 0.4582020868266377
の近似値が求まることになる。
# ユニタリ行列をゲートにする。
unitary_gate = UnitaryGate(unitary, label='random_unitary')
unitary_gate.name = 'random_unitary'
%%time
n_qubits = 4
qc = QuantumCircuit(n_qubits + 1, n_qubits)
for i in range(n_qubits):
qc.h(i)
# 固有ベクトルを補助量子ビットの部分に用意する。
qc.initialize(vec, n_qubits)
for i in range(n_qubits):
for _ in range(2**i):
# ユニタリゲートを制御ユニタリゲートとして回路に追加する。
qc.append(unitary_gate.control(), [i, n_qubits])
qc.barrier()
iqft_circuit = QFT(n_qubits).inverse()
qc = qc.compose(iqft_circuit, list(range(n_qubits)))
for i in range(n_qubits):
qc.measure(i, i)
qc.draw(scale=0.4, fold=75)
以上で量子回路の実装は完了である。先に触れた通り、回路の深さは大凡
測定を行って
まず、シミュレータが解釈できるゲートになるまで .decompose
を適用する。何回適用すれば良いかよく分からないが、今回のケースでは 6 回だった。
%%time
qc1 = qc.decompose().decompose().decompose().decompose().decompose().decompose()
“測定” を行い、最も多く観測された結果を取得する:
%%time
sim = AerSimulator()
counts = sim.run(qc1).result().get_counts()
counts_items = sorted(counts.items(), key=lambda k_v: -k_v[1])
print(counts_items)
[('0111', 710), ('1000', 169), ('0110', 48), ('1001', 32), ('0101', 14), ('1010', 12), ('1011', 10), ('0011', 9), ('0100', 5), ('1110', 3), ('0010', 2), ('0000', 2), ('1101', 2), ('1111', 2), ('0001', 2), ('1100', 2)]
まぁまぁの頻度で答えが得られているが、ノイズがあると少々怖い。
state, count = counts_items[0]
estimated_theta = int(state, 2) / (2**n_qubits)
print(f'estimated={estimated_theta}, real theta={theta}')
estimated=0.4375, real theta=0.4582020868266377
まぁまぁの精度で
もう少し精度を上げてやってみる
ここまでは CPU 上での計算で、わりとすぐに完了する。が、精度面では恐らく不満も残るものであると思う。そこで精度をあげて、12 量子ビットでシミュレーションを行ってみたい。この規模になると CPU だとしんどいと思われるが、まずは試してみる。
適当にセルを抜粋して実行時間を掲載すると以下のような結果であった:
...
%%time
n_qubits = 12
qc = QuantumCircuit(n_qubits + 1)
for i in range(n_qubits):
qc.h(i)
qc.initialize(vec, n_qubits)
for i in range(n_qubits):
for _ in range(2**i):
qc.append(unitary_gate.control(), [i, n_qubits])
qc.barrier()
print(i)
iqft_circuit = QFT(n_qubits).inverse()
qc = qc.compose(iqft_circuit, list(range(n_qubits)))
for i in range(n_qubits):
qc.measure(i, i)
print(len(qc))
4133
CPU times: user 54.5 s, sys: 463 ms, total: 55 s
Wall time: 55.3 s
%%time
qc1 = qc.decompose().decompose().decompose().decompose().decompose().decompose()
print(len(qc1))
57729
CPU times: user 1min 30s, sys: 434 ms, total: 1min 30s
Wall time: 1min 31s
.decompose
前は 4233 だった深さが、57729 にまで増えているのもつらいところである。つまり理論上は
さて、まずは CPU で試してみよう:
%%time
sim = AerSimulator(method='statevector')
counts = sim.run(qc1).result().get_counts()
counts_items = sorted(counts.items(), key=lambda k_v: -k_v[1])
print(counts_items[:5])
これを実行したところ、16 分を経過しても完了しなかったので諦めた。
少し書き換えて以下のようにしてみる。GPU が効率的に使われるかはよく分からないが、試してみる。GPU の使用については、Qiskit で遊んでみる (7) — Qiskit Aer GPU に書いたような手順で Colab 上で GPU 対応 Qiskit Aer を実行できる。なお、この記事は現時点では古くなっているので、適宜読み替える必要がある。
%%time
sim = AerSimulator(method='statevector', device='GPU')
counts = sim.run(qc1).result().get_counts()
counts_items = sorted(counts.items(), key=lambda k_v: -k_v[1])
print(counts_items[:5])
これも、20 分超えても完了しなかったので諦めた。あまり理由は分からないが以下は結構時間がかかるなりに、そこそこ早く終わった:
%%time
sim = AerSimulator(method='statevector', device='GPU', cuStateVec_enable=True)
counts = sim.run(qc1).result().get_counts()
counts_items = sorted(counts.items(), key=lambda k_v: -k_v[1])
print(counts_items[:5])
[('011101010101', 882), ('011101010100', 53), ('011101010110', 30), ('011101010011', 14), ('011101010111', 9)]
CPU times: user 11min 8s, sys: 3.03 s, total: 11min 11s
Wall time: 11min
state, count = counts_items[0]
estimated_theta = int(state, 2) / (2**n_qubits)
print(f'estimated={estimated_theta}, real theta={theta}')
estimated=0.458251953125, real theta=0.4582020868266377
4 量子ビットの時よりはかなりマシな精度で推定できた。但し、量子ビット数を増やしても 2 進小数の桁の精度しか上がらないので、10 進小数と比較すると精度の改善が緩やかであることには注意したい。
大雑把に 12 量子ビットでの計算で 15 分程度かかったわけだが、13 量子ビットだと 30 分、14 量子ビットだと 1 時間くらいかかることになるのだろうか?
別の方法でやってみる (GPU 計算)
大量の量子ビット数だと難しくなる方法だが、状態ベクトルを直接求めてしまうという手もある。この場合、内部的に巨大なユニタリ行列を状態ベクトルにかけまくることになるので通常はメモリも大量に消費して重いのだが、GPU の得意分野でもあるので速度が出せるかもしれない。
測定はエルミート演算であってユニタリではないからという理由かもしれないが、とにかく非対応なので、測定演算
for i in range(n_qubits):
qc.measure(i, i)
を除去する。そうして transpile した後に、
from qiskit_aer.quantum_info import AerStatevector as Statevector
して以下を実行する。
%%time
sv = Statevector(qc1, device='GPU', cuStateVec_enable=True)
CPU times: user 2.03 s, sys: 33 ms, total: 2.07 s
Wall time: 2.06 s
やはり GPU の得意分野だけあって劇的に速い。ほぼ一瞬だった。
state, _ = sorted(sv.probabilities_dict().items(), key=lambda k_v: -k_v[1])[0]
estimated_theta = int(state, 2) / (2**n_qubits)
print(f'estimated={estimated_theta}, real theta={theta}')
estimated=0.458251953125, real theta=0.4582020868266377
測定で求めた場合と同じ結果になるし、そもそも状態ベクトルを求めているので理論上の結果が出る。
まとめ
量子フーリエ変換を応用するアルゴリズムとして量子位相推定を見てみた。量子計算としての計算量には古典計算に対する優位性があると思うが、一方で計算を担う量子回路の構築にかなりのリソースを食うので、この部分は後々には Python のままだと厳しいかなという感触がある。そのうち Mojo みたいな言語で書けるようになるとこの辺も改善するのだろうか?
-
なので、U \ket{\psi} = \lambda \ket{\psi} である。積をとると\bra{\psi} U^\dagger = \lambda^* \bra{\psi} となるので、1 = \braket{\psi | U^\dagger U | \psi} = |\lambda|^2 \braket{\psi | \psi} である。 ↩︎|\lambda| = 1
Discussion