Qiskitを使った量子最適化アルゴリズムの応用へ 〜パート2 量子もつれとアルゴリズム〜
*この記事はQiita記事の再投稿となります。
こんにちは!
株式会社アイディオットでデータサイエンティストをしています、秋田と申します。
このシリーズは、量子最適化アルゴリズムを具体的な問題に対して使うことによって、量子コンピュータのユースケースとしてどんなものがありそうかを妄想することを目的に、量子コンピュータの基礎から学ぼうというものになります。
前回は量子計算の基礎を学び、それを基にQiskitを使って量子回路を作成・実行しました。
今回は、量子もつれに焦点を当てて、特殊な量子状態を生成してみましょう!
本記事と同様の実験をGoogle Colaboratoryで簡単に出来るように、このプロジェクトのGitHubリポジトリにノートブックを作成しました。
量子もつれ
少し復習から入りましょう。
量子もつれとは、「テンソル積で表現できない状態」のことを指します。
例えば以下のベル状態
は典型的な量子もつれでした。
これをビット数を増やす方向に少し拡張させたGHZ状態というものと、次の
という、
まずはQiskitの環境を整えておきましょう。
!pip3 install qiskit[visualization] qiskit-ibm-runtime qiskit-aer
# ライブラリのインポート
import numpy as np
from qiskit import __version__, QuantumCircuit
from qiskit.visualization import plot_histogram
from qiskit_aer import AerSimulator
from qiskit_ibm_runtime import SamplerV2
# Qiskitバージョン確認
print(__version__)
GHZ状態
GHZ状態は、次のような量子もつれを表します。
GHZ状態については、あくまでこれが一般的な表現であり、
ではまずは、3量子ビットのGHZ状態から作ってみましょう。
GHZ状態はベル状態の拡張と言いました。
考え方は同じで、アダマールゲートを1つの量子ビットにかけた後、残りの2つにCNOTゲートがかかるようにすれば大丈夫です。
# 3量子ビットの量子回路を作成
ghz_3 = QuantumCircuit(3, 3)
# GHZ状態の作成
ghz_3.h(0)
ghz_3.cx(0, 1)
ghz_3.cx(1, 2) # 1番目の量子ビットは既に0番目の量子ビットともつれているので、制御量子ビットは0, 1のどちらでも良い
# 測定
for i in range(3):
ghz_3.measure(i, i)
# 量子回路を図(画像)で表示
ghz_3.draw("mpl")
ベル状態を作るときと同じように進めると、
見栄えを重視すると階段状になっているのが良いかなと思いました。
さて、これで本当にGHZ状態になっているのかを測定して確かめてみましょう!
# バックエンドにシミュレータを設定
backend = AerSimulator()
# Sampler を使用しシミュレーション
sampler = SamplerV2(backend)
result = sampler.run([ghz_3], shots=1024).result() # 1024回測定を行う
# 測定結果の出力
counts = result[0].data.c.get_counts()
print(counts)
# ヒストグラムを描画
plot_histogram(counts)
{'111': 520, '000': 504}
量子状態
それでは、今度はちょっと多くなって
やることは同じなので簡単ですね!
# 8量子ビットの量子回路を作成
ghz_8 = QuantumCircuit(8, 8)
# アダマールゲートを作用
ghz_8.h(0)
# 再帰的にCNOTゲートを作用
for i in range(7):
ghz_8.cx(i, i + 1)
# バリアを使用
ghz_8.barrier()
# 測定
for i in range(8):
ghz_8.measure(i, i)
# 量子回路を図(画像)で表示
ghz_8.draw("mpl")
途中でバリアというものを使用していますが、これは量子回路の見栄えを良くするために書いていると理解してください。
この量子回路上で何か影響があるわけではありません。
それでは測定をして確かめてみましょう!
# バックエンドにシミュレータを設定
backend = AerSimulator()
# Sampler を使用しシミュレーション
sampler = SamplerV2(backend)
result = sampler.run([ghz_8], shots=1024).result() # 1024回測定を行う
# 測定結果の出力
counts = result[0].data.c.get_counts()
print(counts)
# ヒストグラムを描画
plot_histogram(counts)
{'11111111': 521, '00000000': 503}
さて、ここで量子回路の「深さ」というものの考え方を導入しましょう。
先ほど作った ghz_8
の深さを depth()
メソッドで確認してみましょう。
# 量子回路の深さ
print(f"Depth:\t{ghz_8.depth()}")
Depth: 9
普通、アルゴリズムを扱う上で「計算量」というものを気にします。
これは、コンピュータでの負荷を抑え、高速に計算することや、メモリを確保するためにより良いアルゴリズムを考えるというものです。
量子コンピュータでのプログラミングも例外ではなく、量子回路を設計する際にその量子回路の深さをなるべく浅く(少なく)することを心がけましょう。
例えば、
それであれば、
より浅い回路が出来ないかを調べてみましょう!
# 8量子ビットの量子回路を作成
ghz_8_shallow = QuantumCircuit(8, 8)
# アダマールゲートを作用
ghz_8_shallow.h(0)
# CNOTゲートのかけ方を工夫
ghz_8_shallow.cx(0, 1)
ghz_8_shallow.cx(0, 2)
ghz_8_shallow.cx(1, 3)
ghz_8_shallow.cx(0, 4)
ghz_8_shallow.cx(1, 5)
ghz_8_shallow.cx(2, 6)
ghz_8_shallow.cx(3, 7)
# バリアを使用
ghz_8_shallow.barrier()
# 測定
for i in range(8):
ghz_8_shallow.measure(i, i)
# 量子回路を図(画像)で表示
ghz_8_shallow.draw("mpl")
# バックエンドにシミュレータを設定
backend = AerSimulator()
# Sampler を使用しシミュレーション
sampler = SamplerV2(backend)
result = sampler.run([ghz_8_shallow], shots=1024).result() # 1024回測定を行う
# 測定結果の出力
counts = result[0].data.c.get_counts()
print(counts)
# ヒストグラムを描画
plot_histogram(counts)
{'11111111': 528, '00000000': 496}
一見するとあまり変わらないように見えますが、このプログラムでは、1度もつれさせた量子ビットをなるべく満遍なく再利用することを考えています。
例えば、
ghz_8_shallow.cx(0, 4)
ghz_8_shallow.cx(1, 5)
ghz_8_shallow.cx(2, 6)
ghz_8_shallow.cx(3, 7)
ここの部分では、量子ビットのインデックス番号が1つも被っていません。
これは、それぞれのゲート作用が並行して行われていることを意味します。
一方で、先ほどの量子回路では、すべてが繋がった(線形の)ゲート作用のし方をしているため、1つ前の計算が行われてからでないと先に進めないという構造をしています。
新しい方法では、対数を用いたアルゴリズムを用いているため、線形のものより圧倒的に計算量を抑えることが出来ます!
実際に深さを確認してみましょう。
# 量子回路の深さ
print(f"Depth:\t{ghz_8_shallow.depth()}")
Depth: 5
これで浅い回路が出来たことも確認出来ました!
先ほどのプログラムだと、形が汚いので次のように関数を定めましょう。
def find_control(value: int) -> int:
"""
制御量子ビットのインデックスを取得する関数
Parameters
----------
value: int
ターゲット量子ビットのインデックス
Returns
----------
rem: int
制御量子ビットのインデックス
"""
# 2の累乗で value 以下の最大の値を求める
power_of_two = 1 << (value.bit_length() - 1)
# value を最大の2の累乗で割った余りを求める
rem = value % power_of_two
return rem
先ほどのプログラムを修正します。
# 8量子ビットの量子回路を作成
ghz_8_shallow_rec = QuantumCircuit(8, 8)
# GHZ状態の生成
ghz_8_shallow_rec.h(0)
for i in range(7):
rem = find_control(i + 1)
ghz_8_shallow_rec.cx(rem, i + 1)
# バリアを使用
ghz_8_shallow_rec.barrier()
# 測定
for i in range(8):
ghz_8_shallow_rec.measure(i, i)
# 量子回路を図(画像)で表示
ghz_8_shallow_rec.draw("mpl")
だいぶスッキリしましたね!
W状態
今度は、W状態について考えてみましょう。
こちらもGHZ状態同様に、線形の作り方と対数の作り方の2種類を学びましょう。
まずは線形のやり方から始めます。
初めに、
その後、次の式を満たす回転ゲートを用意します。
ここで
これは
実はそれほど難しくなく、
続いて、これを使って次の変換を行うための量子ゲートのブロック
これは、1つ目の量子ビットが
1つ目の量子ビットを制御量子ビットとした
しかし、残念ながら1つの
1つ目の量子ゲートは、先に定義した
ここで、
次に、今度は2つ目の量子ビットを制御量子ビット、1つ目の量子ビットをターゲット量子ビットとしたCNOTゲートを作用させることで、
となります。
よって先ほどの量子ゲート
として表せます。
この
まずは以下の初期状態
ここから、1つ目の量子ビットに
とします。
理想とするW状態は
なので、
よってここでは
とします。
続いて、
とします。
最後に
となりました!
# 量子ビット数を指定
n = 4
# W状態のエンタングルメントの係数
prob_amp = np.sqrt(1 / n)
# n量子ビットの量子回路を作成
w_n = QuantumCircuit(n, n)
# W状態の作成
w_n.x(0)
for i in range(n - 1):
comp_amp = np.sqrt(1 - i / n)
rot_ang = 2 * np.arccos(prob_amp / (comp_amp)) # 回転角の大きさを係数から判定
w_n.cry(rot_ang, i, i + 1) # cryゲートがCG(p)に相当する
w_n.cx(i + 1, i)
# バリアを使用
w_n.barrier()
# 測定
for i in range(n):
w_n.measure(i, i)
# 量子回路を図(画像)で表示
w_n.draw("mpl")
# バックエンドにシミュレータを設定
backend = AerSimulator()
# Sampler を使用しシミュレーション
sampler = SamplerV2(backend)
result = sampler.run([w_n], shots=1024).result() # 1024回測定を行う
# 測定結果の出力
counts = result[0].data.c.get_counts()
print(counts)
# ヒストグラムを描画
plot_histogram(counts)
{'0001': 253, '0100': 248, '1000': 268, '0010': 255}
W状態が確認出来ました!
では、この量子回路の深さを確認しましょう。
比較がしやすいように量子ビット数を
# W状態のエンタングルメントの係数
prob_amp = np.sqrt(1 / 8)
# 8量子ビットの量子回路を作成
w_8 = QuantumCircuit(8, 8)
# W状態の作成
w_8.x(0)
for i in range(7):
comp_amp = np.sqrt(1 - i / 8)
rot_ang = 2 * np.arccos(prob_amp / (comp_amp)) # 回転角の大きさを係数から判定
w_8.cry(rot_ang, i, i + 1) # cryゲートがCG(p)に相当する
w_8.cx(i + 1, i)
# バリアを使用
w_8.barrier()
# 測定
for i in range(8):
w_8.measure(i, i)
# 量子回路を図(画像)で表示
w_8.draw("mpl")
# 量子回路の深さ
print(f"Depth:\t{w_8.depth()}")
Depth: 16
GHZ状態のときと同様に、より浅い量子回路を作成するために対数を用いた作り方を考えてみましょう。
こちらは少しトリッキーなので順を追って見ていきましょう。
まず、対数を用いたアルゴリズムなので、W状態の量子回路の深さというのは量子ビット数
最後の
また、
そして、先ほどまでのように線形で係数の変遷を追っていけないので、それを管理するための要素数
このリストは合計が
あとは
# 8量子ビットの量子回路を作成
w_8_shallow = QuantumCircuit(8, 8)
# 対数を取った値
levels = int(np.ceil(np.log2(8)))
# 係数を管理するリスト
box = [0 for _ in range(8)]
box[0] = 8
# W状態の作成
w_8_shallow.x(0)
for level in range(levels):
tmp = 2**level
for i in range(tmp):
parent = box[i]
if parent == 1:
continue
left = parent//2
right = parent - left
prob_amp = np.sqrt(left / parent)
rot_ang = 2 * np.arccos(prob_amp)
for j in range(tmp, 8):
if box[j] == 0:
bridge = j
break
w_8_shallow.cry(rot_ang, i, bridge)
w_8_shallow.cx(bridge, i)
box[i] = left
box[bridge] = right
# バリアを使用
w_8_shallow.barrier()
# 測定
for i in range(8):
w_8_shallow.measure(i, i)
# 量子回路を図(画像)で表示
w_8_shallow.draw("mpl")
# 量子回路の深さ
print(f"Depth:\t{w_8_shallow.depth()}")
Depth: 8
だいぶ浅くなりましたね!
次回予告
次回は、変分量子アルゴリズムとはどういうものかを知ることをテーマに進めて参ります!
実際に、量子化学や量子最適化、量子機械学習などで扱われているアルゴリズムがどのようなものなのかを紹介しますので、是非また読んでみてください。
Discussion