LoginSignup
11
1

More than 1 year has passed since last update.

量子コンピュータとジャンケンする

Last updated at Posted at 2021-12-18

この記事はフューチャーAdvent Calender 2021 19日目の記事です。

■ はじめに

こんにちは、突然ですが量子コンピュータってご存じでしょうか?
ニュースでたまに耳にしますが、SFチックな響きがあってロマンを感じますよね!

本記事では話題の量子コンピュータをつかってWebアプリケーションを作ってみます。
先端テクノロジーを使ってアプリを作るって考えただけでもワクワクしますね!

それでは早速本題に移ります。

■ 作成したもの

今回作成するのはジャンケンアプリです。

その名も「janQen」

量子コンピュータに乱数を生成させて、ひたすらジャンケンするというアプリです。
Herokuにデモアプリをデプロイしているため、まずは一度ジャンケンしてみてください。
(※1デモアプリではqasm_simulatorを使用しています。厳密には量子コンピュータではないです)
(※2アプリケーションの特性上レスポンスが非常に遅いです。ジョブの実行には1~2分の時間がかかる場合もあります。ボタンの連打は控えるようにお願いします。 :sob: )

リンクを踏むと以下の画面が立ちあがります。
ユーザーは「グー」、「チョキ」、「パー」のいずれかを選択します。

image.png

試しにグーを押してみましょう。

image.png

画面が切り替わって、、、

image.png

ジャンケンに勝ちました!

上にスクロールすると、、

image.png

なにやら棒グラフが表示されていますね :eyes:
このグラフが量子コンピュータの演算結果です。こちらのグラフについては後程説明します。

■ システム構成

janQenのシステム構成は以下の様にしました。

  • フロントエンド

    • Vue.js
    • Vuetify
  • バックエンド

    • Flask
    • Flask-RESTful
    • Qiskit

フロントエンドとバックエンドの通信にはaxiosを使用し、アプリケーションのホスティングはHerokuを採用しました。どの技術セットも初めて触りましたが以下参考記事のおかげで爆速でデモアプリを構築することができました。 感謝!!

基本的なアプリケーションの作成方法は上記の記事を参考にしてください。
本記事ではバックエンドの量子コンピュータによる乱数生成に注目して説明します。

■ Qiskit

QiskitとはIBM社が開発を進めている量子計算パッケージです。
Pythonで記述されており、IBM社が提供するクラウド量子コンピューティングサービスにもQikitを使用してアクセスすることができます。

今回バックエンドにFlaskを採用したのもQiskitがPythonパッケージであることが理由です。
利用にあたって基本的な線形代数の知識が必要となりますが、日本語ドキュメントが豊富にあるため、初学者の方でも比較的簡単にパッケージの使い方を習得することができると思います。

では早速量子コンピュータに乱数生成させましょう。
$$
\def\bra#1{\mathinner{\left\langle{#1}\right|}}
\def\ket#1{\mathinner{\left|{#1}\right\rangle}}
\def\braket#1#2{\mathinner{\left\langle{#1}\middle|#2\right\rangle}}
$$

■ 量子乱数生成

こちらのサイトに記載されている手法で量子乱数を生成します。

□ 量子ビット

量子乱数の生成方法を紹介する前に量子ビットについて簡単に説明します
量子コンピュータをつかった計算処理は量子ビットに行列(ユニタリー演算子)演算を行うことで実現されます。
演算対象である量子ビットは(複素)ベクトルとして量子的な状態を持っています。
古典の情報の最小単位は0と1の2値で表現される一方で、量子的な状態ではこの2つの値を重ね合わせた状態が許容されます。

例えば0という状態と1という状態をそれぞれ以下の様に表現します。

\ket{0} = 
\begin{pmatrix}
1 \\
0
\end{pmatrix}
\ket{1} = 
\begin{pmatrix}
0 \\
1
\end{pmatrix}

一般的な量子状態$\ket{\psi}$は次のように表現されます。

\ket{\psi} = 
\alpha\ket{0}+\beta\ket{1}
 =
\alpha
\begin{pmatrix}
1 \\
0
\end{pmatrix}
+
\beta
\begin{pmatrix}
0 \\
1
\end{pmatrix}
=
\begin{pmatrix}
\alpha \\
\beta
\end{pmatrix}

ここで$\alpha$と$\beta$は確率複素振幅と呼ばれます。結論から述べると確率複素振幅の絶対値の2乗でその状態が取得されます。

つまり今回のケースでは測定結果が状態$\ket{0}$になる確率は$|\alpha|^2$、測定結果が状態$\ket{1}$になる確率は$|\beta|^2$です。
この性質を利用して量子乱数を生成します。

例えば次のような状態を仮定します。

\ket{\psi_{rand}} = \frac{1}{\sqrt{2}}\ket{0}+\frac{1}{\sqrt{2}}\ket{1}

このとき状態$\ket{0}$になる確率と状態$\ket{1}$になる確率は先ほどの計算(複素振幅の絶対値の2乗)で50%であることがわかります。

つまり量子ビットを$\ket{\psi_{rand}}$のような状態にすることができれば測定を繰り返すごとに50%の確率で状態$\ket{0}$もしくは状態$\ket{1}$が取得できます。

これは純粋な物理現象(量子力学)に由来するランダム性です。
そして状態$\ket{\psi_{rand}}$はQiskitを使えば1行で生成することができます。 すごい!!

□ Qiskitで量子乱数生成を実装

まずは量子回路を作成します。以下はQiskitを使用して作成した量子回路です。

provider = IBMQ.get_provider(hub="ibm-q")
num_q = 4
device = "ibmq_qasm_simulator"
backend = provider.get_backend(device)
q = QuantumRegister(num_q, "q")
c = ClassicalRegister(num_q, "c")
circuit = QuantumCircuit(q, c)
circuit.h(q)
circuit.measure(q, c)

num_qは量子ビット数です。今回は4つの量子ビットを使用しています。
量子ビットの状態の数は量子ビット数nに対して$2^n$個存在します。
なので例えば2つの場合は$\ket{00},\ket{01},\ket{10},\ket{11}$の4状態です。ここで$\ket{nm}$という表記が現れましたが、基本法則は先ほどと同じです。状態の確率複素振幅の2乗がその状態が測定される確率となります。

deviceでは使用する実機を指定します。利用できる実機はこちらから選ぶことができます。

qは量子ビットのレジスタ、cは古典量子ビットのレジスタです。測定によって得られた値をcに格納します。
circuitで量子回路インスタンスを生成した後は、Qikitで定義されたメソッドを使用します。
上記に記載している.h()もその1つです。.h()アダマールゲートと呼ばれ、$\ket{0}$状態の量子ビットにアダマールゲートを演算すると状態

\ket{\psi_{rand}} = \frac{1}{\sqrt{2}}\ket{0}+\frac{1}{\sqrt{2}}\ket{1}

を生成することができます。
例えば、量子ビットを2つ用意し、それぞれにアダマールゲートを適用すると全体の状態を以下の様に記述することができます。

\ket{\psi_{rand}} = \frac{1}{2}\ket{00}+\frac{1}{2}\ket{01}+\frac{1}{2}\ket{10}+\frac{1}{2}\ket{11}

基本法則は同じです。状態$\ket{00}$が取得される確率は絶対値の2乗なので25%です。

1量子ビットの時と2量子ビットの時、それぞれを比較すると規則性があることがわかります。
すなわち、n量子ビットの状態は$2^n$個存在し、アダマールゲートをn量子ビットに演算することで$2^n$個存在する状態からその1つを測定する確率は$\frac{1}{2^n}$になるということです。

例えば上記では量子ビットの数4つに定義しているため、状態$\ket{0000}$を取得する確率は6.25%です。
この値は後ほど使用するので覚えておいてください。

アダマールゲートを演算したら最後は測定します。測定のメソッドは.measure()で定義されています。

量子回路が完成したらつぎは量子回路を量子コンピュータに実行させます。アクセストークンを発行することでAPIでジョブを実行することができます。

job = execute(circuit, backend, shots=1024)

shotsは測定回数です。shots=1では1回しか測定を行わないため、仮に$\ket{0000}$が測定された場合は$\ket{0000}$が確率100%として実行が完了します。shots回数を増やすことでそれぞれの状態の取得確率が6.25%に漸近していきます。(実際にはノイズなどの影響により完全に6.25%に分布することはありません。)

この分布図をプロットしたものが冒頭の棒グラフです。

counts = job.result().get_counts()
plot_histogram(counts, color='#BB2528', title="janQen Result")

image.png

6%付近のものが多いですが大きく偏りが生じていることがわかります。これはshots回数が少ないということと実機特有のノイズが影響していることが原因であると想定されます。

□ 量子乱数からグー、チョキ、パーへのマッピング

上記の方法で量子力学的に等確率な状態を測定する量子回路を作成することができました。
つまり、この測定により取得した最頻値の状態は乱数として利用することができます。
次はそれぞれの状態に対して「グー」「チョキ」「パー」を順にマッピングします。

このマッピングですが、注意して行う必要があります。
例えば、状態$\ket{0000}$から状態$\ket{1111}$まで順番にマッピングを行うと
以下の様になります。

0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111
グー チョキ パー グー チョキ パー グー チョキ パー グー チョキ パー グー チョキ パー グー

グーの確率が多くなりますね。そこで状態$\ket{1111}$を測定したときはもう一度ジョブを実行する処理を加えます。

0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111
グー チョキ パー グー チョキ パー グー チョキ パー グー チョキ パー グー チョキ パー ジョブを再実行

こうすることで量子力学的にはランダムな条件で「グー」「チョキ」「パー」を取得することができるようになりました。

上記を一般化します。
量子ビットの数によって「再実行エリア」の値は変わります。「グー」が確率的に多くなってしまう場合もあれば、「グー」と「チョキ」が確率的に多くなる場合もあります。よって量子ビット数に対してマッピングリストを作成し、最頻値を取得する際に照合を行います。
最頻値がマッピングリストにない場合(=最頻値が再実行エリアに該当)ジョブを再実行するようにします。

10進数に変換して実装したものを以下に示します。
以下の例ではdeviceをqasm_simulatorにしていますが、こちらは適宜使用するマシンに置き換えてください。
処理の流れを大まかに記載します。

  1. Postでフロントエンドよりユーザーの入力値を受け取る。
  2. Postリクエスト取得時に量子コンピュータでジョブを実行し、乱数を生成する。
  3. 最後に勝敗結果と出力画像のパスをフロントエンドにレスポンスとして戻す。
import os
import glob
import datetime
import matplotlib.pyplot as plt
from qiskit.visualization import plot_histogram
from flask import Blueprint, jsonify, request
from flask_restful import Api, Resource
from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit, execute, IBMQ

def mapping_list(num_q):
    # 0,1,2をそれぞれ「グー」「チョキ」「パー」とする。
    # 例えばnum_q=4のとき15%3=0であるがこのとき0の確率が多くなることがわかる。
    # マッピングリストには15を含めないリストを作成する
    if (2**num_q-1)%3==0:
        lst = list(range(0,2**num_q-1))
        return lst
    # 例えばnum_q=3のとき7%3=1であるがこのとき0,1の確率が多くなることがわかる。
    # マッピングリストには6,7を含めないリストを作成する
    if (2**num_q-1)%3==1:
        lst = list(range(0,2**num_q-2))
        return lst 

def generatefig(counts,date):
    plt.rcParams['figure.subplot.bottom'] = 0.15
    plot_histogram(counts, color='#BB2528', title="janQen Result").savefig("dist/"+date)

def cleanfig():
    file_names = glob.glob("dist/static/tmp/*.png")
    for file_name in file_names:
        os.remove(file_name)

def judge(i, j):
    p = (i - j + 3) % 3
    if p == 0:
        return "引き分け"
    elif p == 1:
        return "あなたの負け"
    else:
        return "あなたの勝ち"

def convObject(i):
    if i == 0:
        return "グー"
    if i == 1:
        return "チョキ"
    else:
        return "パー"

TOKEN = os.environ["TOKEN"]
IBMQ.enable_account(TOKEN)
provider = IBMQ.get_provider(hub="ibm-q")
num_q = 4
device = "ibmq_qasm_simulator"
backend = provider.get_backend(device)
q = QuantumRegister(num_q, "q")
c = ClassicalRegister(num_q, "c")
# post時に量子乱数を生成、ゲームの勝敗をレスポンスするapi(POSTメソッド)
quantum_random_generator_bp = Blueprint(
    "quantum_random_generator", __name__, url_prefix="/api/post"
)

class QuantumRandomGenerator(Resource):
    def post(self):

        cleanfig()
        now = datetime.datetime.now()
        new = "static/tmp/{0:%Y%m%d_%H%M%S}.png".format(now)
        input_data = request.json

        circuit = QuantumCircuit(q, c)
        circuit.h(q)
        circuit.measure(q, c)

        while True:
            job = execute(circuit, backend, shots=100)
            print("Executing Job...\n")

            counts = job.result().get_counts()

            print("RESULT: ", counts, "\n")
            # 最頻値を10進数で取得
            rand = int(counts.most_frequent(), 2)
            # 0は「グー」、1は「チョキ」、2は「パー」
            output_num = rand % 3
            if  rand not in mapping_list(num_q):
                continue

            input_num = input_data["input"]
            input = convObject(input_num)
            output = convObject(output_num)
            result = judge(input_num, output_num)
            generatefig(counts, new)
            result_data = {
                "input": input,
                "output": output,
                "result": result,
                "output_num": output_num,
                "fig": new,
            }
            break
        return jsonify(result_data)

これでバックエンド部分の構築は完了しました。
あとはバックエンドから取得した値に沿ってフロントエンドでゴニョゴニョすることでよい感じになります。
フロントエンドほかソースコードはこちらのリポジトリに格納しているので参考にしてください。

■ まとめ

長くなってしまいましたがここまで読んでいいただきありがとうございました。
私自身まだまだQiskitに不慣れな部分があるため誤った表現をしている箇所があるかもしれません。
その際は優しくご指摘いただけると幸いです。
また、Webアプリを構築するうえでFlaskとVue.jsを初めて触りましたがとても快適に実装することができました。
今後も使っていきたいと思います。

明日のアドベントカレンダーは@hichikawa1126 さんです。

11
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
1