4/15(水)~17(金) NexTech Week@ビッグサイトに出展します☆彡
量子アニーリング

New!! 量子アニーリング(QUBO)でライツアウト(lights out)を解く

AI要約

パズル「ライツアウト」を量子アニーリング(QUBO)で攻略します。各マスの「反転回数の偶奇」に着目してQUBO形式に落とし込み、TYTANを用いてサンプリングします。補助量子ビットの数を削減する工夫にも注目です。

やること

ライツアウトはすでに「遺伝的アルゴリズム」でも「理論」でも解いたのですが、

QUBOの練習にもピッタリなので、ちゃっかり量子アニーリングでも解いてみましょう。

例題

以前も使用したこちらの例題。

赤丸のマスをクリックするとすべてのライトが消えます。解は4種類ありますがこれが最小手数の解です。

おさらい

こちらの3パターンの条件設定を使います。久しぶりの降順制約ですね。忘れたときにやってくる。

基本「n個の量子ビットからm個を1にする」
C1「2個の量子ビットを降順にする」
D2 解が0と1だけの方程式(右辺が複数)

定式化

各マスに量子ビットを割り当て、クリックするかどうかを0/1で表すこととします。(25個)

ここで制約条件を考えます。消灯マスが消灯のままでいるためには「偶数回」反転する必要があります。つまり、自身を中心とする十字のマス(場所によって3~5マス)の合計が [0 or 2 or 4] と等しくなればOKです。逆に、点灯マスが消灯するためには、十字マスの合計が [1 or 3 or 5] と等しくなる必要があります。

この方程式制約をシンプルに作るなら、各マスに3個の補助量子ビットを割り当てて [1 or 3 or 5] を表現することになるでしょう。

#補助量子ビットをワンホット
H += (s0 + s1 + s2 - 1)**2

#十字マス(5マス)の合計が [1 or 3 or 5] と等しくなる
H += ((q0 + q1 + q2 + q3 + q4) - (1*s0 + 3*s1 + 5*s2))**2

しかし、もっと量子ビットを節約する空前絶後のアイデアがあります。各マスの補助量子ビットを2個にすると、そのパターンは [0, 0]/[0, 1]/[1, 0]/[1, 1] の4通りで合計を取ると [0 or 1 or 2] となり、2倍して1を足せば [1 or 3 or 5] を表現できるのです。

ただしこのままでは一つの解に対して補助量子ビットの[0, 1]/[1, 0]のバリエーションが無数に出てしまって探索に無駄が発生するので、[0, 1]のパターンをブロックするために降順制約を与えます。つまり、補助量子ビットが [0, 0]/[1, 0]/[1, 1] の3パターンしか出ないように制限します。

#補助量子ビットを降順
H += (1 - s0) * s1

#十字マス(5マス)の合計が [1 or 3 or 5] と等しくなる
H += ((q0 + q1 + q2 + q3 + q4) - (1 + 2*(s0 + s1)))**2

その他に、最小手数の解を出すために全体に対して0になりやすい圧力もかけます。

import numpy as np
from copy import deepcopy
from tytan import *

#パズルの記述
lights = ['---#-',
          '---##',
          '--##-',
          '#-###',
          '##-##']


#パズルをプレイする関数
def play(lights, ans):
    #盤面をコピー
    lights_copy = deepcopy(lights)
    
    #盤面に対して答えを十字型に足し込む
    lights_copy[:, :] += ans[:, :]
    lights_copy[1:, :] += ans[:-1, :]
    lights_copy[:-1, :] += ans[1:, :]
    lights_copy[:, 1:] += ans[:, :-1]
    lights_copy[:, :-1] += ans[:, 1:]
    
    #偶数回返されたマスは0、奇数回は1とする
    lights_copy = lights_copy % 2
    
    return lights_copy

#変換
lights_tmp = []
for line in lights:
    line = line.replace('-', '0')
    line = line.replace('#', '1')
    lights_tmp.append(list(line))
lights = np.array(lights_tmp, dtype=int)
print(f'lights = \n{lights}')

#サイズ
h, w = lights.shape
print(f'(h, w) = ({h}, {w})')

#量子ビット
q = symbols_list([h, w], 'q{}_{}')

#補助量子ビット
s = symbols_list([h, w, 2], 's{}_{}_{}')

#補助量子ビットを降順
Hconst1 = 0
for i in range(h):
    for j in range(w):
        Hconst1 += (1 - s[i, j, 0]) * s[i, j, 1]

#全マスを消灯させる
Hconst2 = 0
for i in range(h):
    for j in range(w):
        #ここを中心とする十字マスの量子ビットを集める
        jujimasu = q[i, j]
        if i-1 >= 0:
            jujimasu += q[i-1, j]
        if i+1 < h:
            jujimasu += q[i+1, j]
        if j-1 >= 0:
            jujimasu += q[i, j-1]
        if j+1 < w:
            jujimasu += q[i, j+1]
        
        #ここが消灯マスなら十字マスの合計が [0 or 2 or 4]個
        if lights[i, j] == 0:
            Hconst2 += (np.sum(jujimasu) - (0 + 2*np.sum(s[i, j])))**2
        else:
            #ここが点灯マスなら十字マスの合計が [1 or 3 or 5]個
            Hconst2 += (np.sum(jujimasu) - (1 + 2*np.sum(s[i, j])))**2

#最小手数にする
Hcost = 0
for i in range(h):
    for j in range(w):
        Hcost += (q[i, j] - 0)**2

#合体
H = Hconst1 + Hconst2 + 0.01*Hcost

#コンパイル
qubo, offset = Compile(H).get_qubo()
print(f'offset = {offset}')

#サンプラー選択
solver = sampler.ArminSampler()

#サンプリング
result = solver.run(qubo, shots=5000, T_num=20000)

#上位3件
for r in result[:3]:
    print(f'Energy {r[1]}, Occurrence {r[2]}')
    
    #配列
    ans, _ = Auto_array(r[0]).get_ndarray('q{}_{}')
    print(f'ans = \n{ans}')
    
    #プレイ結果
    result = play(lights, ans)
    print(f'result = \n{result}')
lights = 
[[0 0 0 1 0]
 [0 0 0 1 1]
 [0 0 1 1 0]
 [1 0 1 1 1]
 [1 1 0 1 1]]
(h, w) = (5, 5)
offset = 13.0
MODE: GPU
DEVICE: cuda:0
Energy -12.920001983642578, Occurrence 3
ans = 
[[1 0 0 0 0]
 [1 1 0 1 0]
 [1 0 0 0 0]
 [0 0 1 0 0]
 [0 1 0 0 1]]
result = 
[[0 0 0 0 0]
 [0 0 0 0 0]
 [0 0 0 0 0]
 [0 0 0 0 0]
 [0 0 0 0 0]]
Energy -12.879999160766602, Occurrence 1
ans = 
[[0 1 0 1 1]
 [1 1 0 1 0]
 [0 1 0 1 1]
 [0 0 1 0 0]
 [1 0 0 1 0]]
result = 
[[0 0 0 0 0]
 [0 0 0 0 0]
 [0 0 0 0 0]
 [0 0 0 0 0]
 [0 0 0 0 0]]
Energy -12.879997253417969, Occurrence 1
ans = 
[[0 0 1 0 1]
 [0 1 1 1 1]
 [1 0 0 0 0]
 [1 0 0 0 1]
 [1 1 1 0 0]]
result = 
[[0 0 0 0 0]
 [0 0 0 0 0]
 [0 0 0 0 0]
 [0 0 0 0 0]
 [0 0 0 0 0]]

結果、第1位の解が模範解答と一致しています。

ans = 
[[1 0 0 0 0]
 [1 1 0 1 0]
 [1 0 0 0 0]
 [0 0 1 0 0]
 [0 1 0 0 1]]

それに続く解は最小手数ではない別解たちです。(理論記事の方と見比べると一致しています)

プレイ結果もきちんとオール消灯になっていますね。完全勝利 (✌ ・ω・ ✌)

おわりに

こいついつもパズル解いてるな ( ˘ω˘ ; )

リアクションのお願い

「参考になった!」「刺激された!」と思ったらぜひリアクションをしましょう。エンジニアの世界はGive and Takeによって成り立っています。これからも無料で良質な情報にアクセスできるよう、Giveする人への感謝をリアクションで示しましょう!

この記事をシェアする

自身のブログ等で使用する場合は引用を忘れずに!

また、寄付も受け付けています。コーヒー1杯でとても喜びます(*˘︶˘*)

 Amazonでギフト券(アマギフ)を贈る

こちらのリンク から金額を指定してお贈りください。(デフォルトで10000円になっているのでご変更ください)

配送:Eメール
受取人:staffあっとvigne-cla.com
贈り主:あなたのお名前やニックネーム
メッセージ:◯◯の記事が参考になりました。など

のようにご入力ください。見返りはありませんのでご了承ください。

 Amazonで食事券(すかいらーく優待券)を贈る

500円 1000円 2000円 5000円 からお贈りください。

配送:Eメール
受取人:staffあっとvigne-cla.com
贈り主:あなたのお名前やニックネーム
メッセージ:◯◯の記事が参考になりました。など

のようにご入力ください。見返りはありませんのでご了承ください。

 その他、ギフト券やクーポン券をメールで贈る

デジタルのギフト券/クーポン券はメールアドレス(staffあっとvigne-cla.com)までお送りください。受領の返信をいたします。
紙のギフト券/クーポン券は 「郵便物はこちらへ」の住所 まで送付してください。名刺やメールアドレスを同封していただければ受領の連絡をいたします。
余った株主優待券等の処理におすすめです。
いずれも見返りはありませんのでご了承ください。

不明点はSNSでお気軽にご連絡ください

ビネクラのTwitter・Youtubeでコメントをください!


Slack・Discordの場合はこちらの公開グループに参加してShoya YasudaまでDMをください!


※当ブログに関することは何でもご相談・ご依頼可能です。

この記事を書いた人
Yasuda

博士(理学)。専門は免疫細胞、数理モデル、シミュレーション。米国、中国で研究に携わった。遺伝的アルゴリズム信者。物価上昇のため半額弁当とともに絶滅寸前。

タイトルとURLをコピーしました