グローバーのアルゴリズム基礎(4):まとめと簡単な応用例
すみません、忙しくて更新が遅くなりました。
このシリーズ最後はGroverのアルゴリズムの簡単な応用を紹介します。
(自分で考えたトイプロブレムです。)
シリーズ予定:
- オラクル回路
- 位相反転増幅回路
- 3量子ビット以上の場合
- まとめと簡単な応用例 <<今回
前回までで入力の量子ビットについて、特定の状態について振幅を増幅できるようになりました。
ただこれだけだと入力Nビットの何番目のビットが立った状態を増幅すればいいというだけなので、実行する前から答えがわかっている計算しかできません。
より実用に近づけるには、「入力Nビットについてある条件を満たすビットを知りたいのだけど、予めわからないのでGroverのアルゴリズムで探索・特定したい」という課題にアプローチできるようになる必要があります。
その一例はQuantumChallenge2020の出題の通りですが、これはQRAMとかやや複雑な数理があるので、今回は単純な問題設定にして解説します。
今回の問題設定は「a + b = 0もしくは2
の解となるa
, b
をGroverのアルゴリズムで見つける」です。
(= 1
となる解は増幅されないのでやりません。
理由はわからないのですが僕の理解が追いついていないからだと思うので、わかったらまたポストします。
もしくはご存じの方がいたらコメントください、、。)
対象とする読者
- 量子プログラミング・量子計算に興味を持って勉強し始めたエンジニア・高校生・大学生
- Groverのアルゴリズムについて改めて知りたい方 など
前提とする知識
- 簡単な論理演算(AND, OR, NOT)が分かること
- 基本的な量子ゲート(H, X, CX, CCX, Z, CZ)が分かること
- 線形代数(行列・ベクトルの積、直積、あとできれば線形変換)が分かること
- ベクトルのブラケット記法(など、もしくはDirac記法ともいう)が分かること
- 加算器の仕組みがわかること(わからなくとも入出力の結果を納得できること)
バージョン情報
- Python 3.7.3
- Qiskit 0.23.1
目次
問題設定
問題は「a + b = 0もしくは2
の解となるa
, b
をGroverのアルゴリズムで見つける」です。
この場合の答えはa = 0, b = 0
、a = 1, b = 1
で人間にとってはすぐわかりますが、これをGroverのアルゴリズムで解きます。
なお、a + b = 1
の解はa = 0, b = 1
もしくはa = 1, b = 0
でこちらも人間にとってはすぐわかりますが、Gorverのアルゴリズムで増幅されないので今回は対象外とします。
理由はわかってないのですが、複数の解が含まれる場合は増幅されないのかもしれません。
数理的に理解ができたら改めて解説しようと思います。
オラクル回路の構築
オラクル回路の演算子の確認
オラクル回路は見つけ出したい量子状態をマークして、その状態の位相を反転する回路でした。 一般に量子オラクルはマーキングしたい状態をとし、その個数をとすると下記のように表します。
例えばa + b = 0
を満たす入力はa = 0, b = 0
なので状態としてはを、
またa + b = 2
を満たす入力はa = 1, b = 1
なので状態としてはをマーキング(位相を反転)するような回路を構成できれば良いということです。
条件を満たす量子状態の振幅を反転
a + b = 0を満たす解
これを満たす入力を検出する回路は下記です。
最終的にMCTゲートで検出しています。
共通関数
def XOR(qc, control1, control2, target): qc.cx(control2, target) qc.cx(control1, target) return qc def AND(qc, control1, control2, target): qc.ccx(control1, control2, target) return qc def adder(qc, input1, input2, carry, summ): """半加算器 """ qc = XOR(qc, input1, input2, summ) qc = AND(qc, input1, input2, carry) return qc
inputs = QuantumRegister(2, 'input') calcs = QuantumRegister(2, 'calculation') oracle = QuantumRegister(1, 'oracle') c = ClassicalRegister(3) grover = QuantumCircuit(inputs, calcs, oracle, c) grover.h(inputs) grover.barrier() grover = adder(grover, inputs[0], inputs[1], calcs[1], calcs[0]) # calcs[1]がcarry calcs[0]がsum grover.x(calcs) grover.mct(calcs, oracle) grover.barrier() grover.measure(inputs, c[:2]) grover.measure(oracle, c[2])
この測定結果は下記です。
この時点ではすべて等確率ですが、MCTでoracle
ビットが反転しているのは入力が00
の場合(図2の一番右)のみです。
a + b = 2を満たす解
同様にこれを満たす回路は下記です。
inputs = QuantumRegister(2, 'input') calcs = QuantumRegister(2, 'calculation') oracle = QuantumRegister(1, 'oracle') c = ClassicalRegister(3) grover = QuantumCircuit(inputs, calcs, oracle, c) grover.h(inputs) grover.barrier() grover = adder(grover, inputs[0], inputs[1], calcs[1], calcs[0]) # calcs[1]がcarry calcs[0]がsum grover.x(calcs[0]) grover.mct(calcs, oracle) grover.barrier() grover.measure(inputs, c[:2]) grover.measure(oracle, c[2])
この測定結果は下記です。
この時点ではすべて等確率ですが、MCTでoracle
ビットが反転しているのは入力が11
の場合(図4の一番右)のみです。
逆演算(Uncomputation)
満たす解を確認するだけだったらここまでで良いのですが、Groverのアルゴリズムを実行するには、逆演算(Uncomputation)という処理が必要です。
測定したい入力のビット(inputs
)とオラクルのビット(oracle
)以外のancilaとして用いたビット(ここではcaluculation
)へ掛けたゲートの効果を「無かったことにする」処理です。
掛けた時と逆順にゲートを掛ければよいです。
inputs = QuantumRegister(2, 'input') calcs = QuantumRegister(2, 'calculation') oracle = QuantumRegister(1, 'oracle') c = ClassicalRegister(2, 'output') grover = QuantumCircuit(inputs, calcs, oracle, c) grover.h(inputs) grover.barrier() grover = adder(grover, inputs[0], inputs[1], calcs[1], calcs[0]) # calcs[1]がcarry calcs[0]がsum grover.x(calcs[0]) grover.mct(calcs, oracle) grover.x(calcs[0]) grover = adder_r(grover, inputs[0], inputs[1], calcs[1], calcs[0])
逆演算そのものの数理的な解説は「量子コンピューティング 量子アルゴリズムから機械学習まで(嶋田義皓著)」の2.5節を参照してください。 ここでは実際に逆演算を仮にしないまま位相反転増幅した場合にうまくいかないことを示します。
計算する回路全体の構成は下記です。
inputs = QuantumRegister(2, 'input') calcs = QuantumRegister(2, 'calculation') oracle = QuantumRegister(1, 'oracle') outputs = ClassicalRegister(2, 'output') grover = QuantumCircuit(inputs, calcs, oracle, outputs) grover.h(inputs) grover.x(oracle) grover.h(oracle) grover.barrier() grover = adder(grover, inputs[0], inputs[1], calcs[1], calcs[0]) # calcs[1]がcarry calcs[0]がsum grover.x(calcs[0]) grover.mct(calcs, oracle) grover.barrier() grover.h(oracle) grover.x(oracle) grover.h(inputs) grover.x(inputs) grover.barrier(inputs) grover.h(inputs[1]) grover.cx(inputs[0], inputs[1]) grover.h(inputs[1]) grover.barrier(inputs) grover.x(inputs) grover.h(inputs) grover.measure(inputs, outputs)
図6の回路でマーキングした直後の量子状態は下記です。(計算はご自身でやってみてください。)
ここで、はinput0, はinput1, はsum(calculation0), はcarry(calculation1), そしてはoracleの各ビットを示します。 求めたい入力である
の状態がマーキングされています。
そして、この後位相反転増幅した後の最終状態は下記です。
sumとcarryとのエンタングルメントを解除していないので、各項が分かれたままきれいにまとまらなくなってしまっています。 これをを中心に整理しなおすと
となり、[tex: |00 \gt{ab}, |11 \gt{ab}]の確率振幅の大きさが[tex: |01 \gt{ab}, |10 \gt{ab}]のものより少し大きくなるということになります。
実際に測定すると下図となり、推測した通りの傾向になり、そして解を得ることができません。
逆演算の必要性を理解していただけると幸いです。
計算結果
逆演算と位相反転増幅も含めた全体の回路は下記です。
a + b = 0を満たす解を求める
inputs = QuantumRegister(2, 'input') calcs = QuantumRegister(2, 'calculation') oracle = QuantumRegister(1, 'oracle') outputs = ClassicalRegister(2, 'output') grover = QuantumCircuit(inputs, calcs, oracle, outputs) grover.h(inputs) grover.x(oracle) grover.h(oracle) grover.barrier() grover = adder(grover, inputs[0], inputs[1], calcs[1], calcs[0]) # calcs[1]がcarry calcs[0]がsum grover.x(calcs) grover.mct(calcs, oracle) grover.x(calcs) grover = adder_r(grover, inputs[0], inputs[1], calcs[1], calcs[0]) grover.barrier() grover.h(oracle) grover.x(oracle) grover.h(inputs) grover.x(inputs) grover.barrier(inputs) grover.h(inputs[1]) grover.cx(inputs[0], inputs[1]) grover.h(inputs[1]) grover.barrier(inputs) grover.x(inputs) grover.h(inputs) grover.measure(inputs, outputs)
この測定結果は下図で、確かにが増幅されています。
a + b = 2を満たす解を求める
inputs = QuantumRegister(2, 'input') calcs = QuantumRegister(2, 'calculation') oracle = QuantumRegister(1, 'oracle') outputs = ClassicalRegister(2, 'output') grover = QuantumCircuit(inputs, calcs, oracle, outputs) grover.h(inputs) grover.x(oracle) grover.h(oracle) grover.barrier() grover = adder(grover, inputs[0], inputs[1], calcs[1], calcs[0]) # calcs[1]がcarry calcs[0]がsum grover.x(calcs[0]) grover.mct(calcs, oracle) grover.x(calcs[0]) grover = adder_r(grover, inputs[0], inputs[1], calcs[1], calcs[0]) grover.barrier() grover.h(oracle) grover.x(oracle) grover.h(inputs) grover.x(inputs) grover.barrier(inputs) grover.h(inputs[1]) grover.cx(inputs[0], inputs[1]) grover.h(inputs[1]) grover.barrier(inputs) grover.x(inputs) grover.h(inputs) grover.measure(inputs, outputs)
この測定結果は下図で、確かにが増幅されています。
まとめ
これでひとまずグローバーのアルゴリズムの一連の解説を終わります。 オラクル回路を問題に合わせて埋め込めことができれば、解ける問題の数・種類が広がります。 良い量子プログラミングライフを!