量子アニーリングを使って魔方陣を作ってみた
はじめに
オンライン公開伴走型量子コンピューティング講座QA4U2卒業生のluna_moonlightです。この記事では、量子アニーリングを用いた魔方陣の生成について解説していきます。
量子アニーリングとは
量子アニーリングとは、量子ゆらぎと呼ばれる現象を用いた組合せ最適化問題[1]を解く手法です。
組合せ最適化問題を解く古典的な手法のひとつとして、焼きなまし法というアルゴリズムがあります。焼きなまし法では、温度というパラメータを導入し、最初は高温に設定して局所解を脱しながら解を探索していき、徐々に温度を低くしていくことで最適解へ遷移していきます。量子アニーリングは、温度の代わりに量子ゆらぎを導入して最適解を探索するアルゴリズムとして、1998年に"Quantum annealing in the transverse Ising model"という論文で門脇正史と西森秀稔により提案されました。
魔方陣
魔方陣とは、
量子アニーリングで魔方陣を解いてみる
それでは実際に量子アニーリングを用いて魔方陣生成を実装していきます。
定式化
量子アニーリングで解けるようにするには、非制約二値変数二次最適化(QUBO: Quadratic Unconstrained Binary Optimization)の形に落とし込む必要があります。QUBOでは、
を最小にする
ここで、魔方陣生成について考えてみましょう。
となります。量子アニーリングで解けるような形に落とし込むために、この組み合わせ
1.各列の総和は等しい
魔方陣は、縦横斜めのどの列についてもその列の総和が等しくなるという制約があります。この制約は以下のように表すことができます。
ただし、
ある列
この評価関数は実装が少し複雑になるため、もう少し単純にできないか考えてみます。
魔方陣の各列の総和は簡単に求めることができます。魔法陣のマスには
この先は
1-1.横の行の総和はS
横の行の総和は
1-2.縦の列の総和はS
縦の列の総和は
1-3.斜めの列の総和はS
斜めの列の総和は
1 \sim N^2 はちょうど1回しか使えない
2.魔方陣は、
ある数字をちょうど1回使う場合は、
3.各マスに1つしか数字を入れてはいけない
魔方陣は、各マスに1つしか数字を入れてはいけないという制約が暗にあります。この制約は以下のように表すことができます。
あるマスにちょうど1つの数字を入れる場合は、
魔方陣生成の評価関数
ここまでで、制約ひとつひとつに対して評価関数を作ってきました。これらを足し合わせることで、魔方陣生成に必要な評価関数を作ることができます。具体的には以下のようになります。
実装
ここまでで、魔方陣生成の評価関数をQUBOの形で定式化することができました。それでは早速実装していきましょう。
まず、D-Waveの量子アニーリングマシンを使うために、D-Wave Leapでアカウントを作成します。
次に、必要な外部ライブラリをインストールしましょう。量子アニーリングを使うためにdwave-ocean-sdkをインストールします。
pip install dwave-ocean-sdk
必要な外部ライブラリをインストールしたところで、さっそくプログラムを書いていきましょう。コードの全体像はこちらです。
コードの全体像
from collections import defaultdict
from dwave.system import DWaveSampler, EmbeddingComposite
'''-----パラメータ-----'''
N = 3 # 魔方陣の一辺の大きさ
l1 = 1 # 罰金項の強さ(各列の総和は等しい)
l2 = 10 # 罰金項の強さ(各数字は1回)
l3 = 10 # 罰金項の強さ(各マスに数字は1つ)
num_reads = 1000 # アニーリングを実行する回数
token = 'XXXX' # API token(個人のものを使用)
solver='Advantage_system6.4' # 量子アニーリングマシン
'''------------------'''
Q = defaultdict(lambda: 0) # Q_i,j,n (i, j)に入れる値n
S = N * (N ** 2 + 1) // 2 # 各列の総和
# x_i,j,nの通し番号を記録
ijn_to_idx = {}
idx = 0
for i in range(1, N + 1):
for j in range(1, N + 1):
for n in range(1, N ** 2 + 1):
ijn_to_idx[(i, j, n)] = idx
idx += 1
# 各行の総和をSに制限
for i in range(1, N + 1):
for j1 in range(1, N + 1):
for n1 in range(1, N ** 2 + 1):
for j2 in range(1, N + 1):
for n2 in range(1, N ** 2 + 1):
Q[(ijn_to_idx[(i, j1, n1)], ijn_to_idx[(i, j2, n2)])] += n1 * n2 * l1
Q[(ijn_to_idx[(i, j1, n1)], ijn_to_idx[(i, j1, n1)])] -= 2 * n1 * S * l1
# 各列の総和をSに制限
for j in range(1, N + 1):
for i1 in range(1, N + 1):
for n1 in range(1, N ** 2 + 1):
for i2 in range(1, N + 1):
for n2 in range(1, N ** 2 + 1):
Q[(ijn_to_idx[(i1, j, n1)], ijn_to_idx[(i2, j, n2)])] += n1 * n2 * l1
Q[(ijn_to_idx[(i1, j, n1)], ijn_to_idx[(i1, j, n1)])] -= 2 * n1 * S * l1
# 斜め(右下がり)の総和をSに制限
for d1 in range(1, N + 1):
for n1 in range(1, N ** 2 + 1):
for d2 in range(1, N + 1):
for n2 in range(1, N ** 2 + 1):
Q[(ijn_to_idx[(d1, d1, n1)], ijn_to_idx[(d2, d2, n2)])] += n1 * n2 * l1
Q[(ijn_to_idx[(d1, d1, n1)], ijn_to_idx[(d1, d1, n1)])] -= 2 * n1 * S * l1
# 斜め(右上がり)の総和をSに制限
for d1 in range(1, N + 1):
for n1 in range(1, N ** 2 + 1):
for d2 in range(1, N + 1):
for n2 in range(1, N ** 2 + 1):
Q[(ijn_to_idx[(d1, N - d1 + 1, n1)], ijn_to_idx[(d2, N - d2 + 1, n2)])] += n1 * n2 * l1
Q[(ijn_to_idx[(d1, N - d1 + 1, n1)], ijn_to_idx[(d1, N - d1 + 1, n1)])] -= 2 * n1 * S * l1
# 同じ数字が重複しないように制限
for n in range(1, N ** 2 + 1):
for i1 in range(1, N + 1):
for j1 in range(1, N + 1):
for i2 in range(1, N + 1):
for j2 in range(1, N + 1):
Q[(ijn_to_idx[(i1, j1, n)], ijn_to_idx[(i2, j2, n)])] += 1 * l2
Q[(ijn_to_idx[(i1, j1, n)], ijn_to_idx[(i1, j1, n)])] -= 2 * l2
# 各マスにちょうど1つの数字に制限
for i in range(1, N + 1):
for j in range(1, N + 1):
for n1 in range(1, N ** 2 + 1):
for n2 in range(1, N ** 2 + 1):
Q[(ijn_to_idx[(i, j, n1)], ijn_to_idx[(i, j, n2)])] += 1 * l3
Q[(ijn_to_idx[(i, j, n1)], ijn_to_idx[(i, j, n1)])] -= 2 * l3
# 量子アニーリングの実行
endpoint = 'https://cloud.dwavesys.com/sapi/'
dw_sampler = DWaveSampler(solver=solver, token=token, endpoint=endpoint)
sampler = EmbeddingComposite(dw_sampler)
sampleset = sampler.sample_qubo(Q, num_reads=num_reads)
# 結果に基づいて魔方陣を生成
result = [i for i in sampleset.first[0].values()]
ans = [[None] * N for _ in range(N)]
for i in range(1, N + 1):
for j in range(1, N + 1):
for n in range(1, N ** 2 + 1):
if result[ijn_to_idx[(i, j, n)]] == 1:
ans[i - 1][j - 1] = n
# 魔方陣の表示
print(*ans, sep='\n')
ここからは、このコードを分けて解説していきます。
初期化
以下のコードは、必要なライブラリのインポートや変数、定数を定義しています。パラメータと書かれている部分を変更することで、魔方陣の大きさや使う量子アニーリングマシンなどを変更することができます。
また、DWaveSamplerでは
from collections import defaultdict
from dwave.system import DWaveSampler, EmbeddingComposite
'''-----パラメータ-----'''
N = 3 # 魔方陣の一辺の大きさ
l1 = 1 # 罰金項の強さ(各列の総和は等しい)
l2 = 10 # 罰金項の強さ(各数字は1回)
l3 = 10 # 罰金項の強さ(各マスに数字は1つ)
num_reads = 1000 # アニーリングを実行する回数
token = 'XXXX' # API token(個人のものを使用)
solver='Advantage_system6.4' # 量子アニーリングマシン
'''------------------'''
Q = defaultdict(lambda: 0) # Q_i,j,n (i, j)に入れる値n
S = N * (N ** 2 + 1) // 2 # 各列の総和
# x_i,j,nの通し番号を記録
ijn_to_idx = {}
idx = 0
for i in range(1, N + 1):
for j in range(1, N + 1):
for n in range(1, N ** 2 + 1):
ijn_to_idx[(i, j, n)] = idx
idx += 1
Q の設定
QUBO行列ここでは、定式化した魔方陣生成の評価関数をプログラムに載せています。
1.各列の総和は等しい
以下のコードは、
# 各行の総和をSに制限
for i in range(1, N + 1):
for j1 in range(1, N + 1):
for n1 in range(1, N ** 2 + 1):
for j2 in range(1, N + 1):
for n2 in range(1, N ** 2 + 1):
Q[(ijn_to_idx[(i, j1, n1)], ijn_to_idx[(i, j2, n2)])] += n1 * n2 * l1
Q[(ijn_to_idx[(i, j1, n1)], ijn_to_idx[(i, j1, n1)])] -= 2 * n1 * S * l1
# 各列の総和をSに制限
for j in range(1, N + 1):
for i1 in range(1, N + 1):
for n1 in range(1, N ** 2 + 1):
for i2 in range(1, N + 1):
for n2 in range(1, N ** 2 + 1):
Q[(ijn_to_idx[(i1, j, n1)], ijn_to_idx[(i2, j, n2)])] += n1 * n2 * l1
Q[(ijn_to_idx[(i1, j, n1)], ijn_to_idx[(i1, j, n1)])] -= 2 * n1 * S * l1
# 斜め(右下がり)の総和をSに制限
for d1 in range(1, N + 1):
for n1 in range(1, N ** 2 + 1):
for d2 in range(1, N + 1):
for n2 in range(1, N ** 2 + 1):
Q[(ijn_to_idx[(d1, d1, n1)], ijn_to_idx[(d2, d2, n2)])] += n1 * n2 * l1
Q[(ijn_to_idx[(d1, d1, n1)], ijn_to_idx[(d1, d1, n1)])] -= 2 * n1 * S * l1
# 斜め(右上がり)の総和をSに制限
for d1 in range(1, N + 1):
for n1 in range(1, N ** 2 + 1):
for d2 in range(1, N + 1):
for n2 in range(1, N ** 2 + 1):
Q[(ijn_to_idx[(d1, N - d1 + 1, n1)], ijn_to_idx[(d2, N - d2 + 1, n2)])] += n1 * n2 * l1
Q[(ijn_to_idx[(d1, N - d1 + 1, n1)], ijn_to_idx[(d1, N - d1 + 1, n1)])] -= 2 * n1 * S * l1
2.1 \sim N^2 はちょうど1回しか使えない
以下のコードは、
# 同じ数字が重複しないように制限
for n in range(1, N ** 2 + 1):
for i1 in range(1, N + 1):
for j1 in range(1, N + 1):
for i2 in range(1, N + 1):
for j2 in range(1, N + 1):
Q[(ijn_to_idx[(i1, j1, n)], ijn_to_idx[(i2, j2, n)])] += 1 * l2
Q[(ijn_to_idx[(i1, j1, n)], ijn_to_idx[(i1, j1, n)])] -= 2 * l2
3.各マスに1つしか数字を入れてはいけない
以下のコードは、
# 各マスにちょうど1つの数字に制限
for i in range(1, N + 1):
for j in range(1, N + 1):
for n1 in range(1, N ** 2 + 1):
for n2 in range(1, N ** 2 + 1):
Q[(ijn_to_idx[(i, j, n1)], ijn_to_idx[(i, j, n2)])] += 1 * l3
Q[(ijn_to_idx[(i, j, n1)], ijn_to_idx[(i, j, n1)])] -= 2 * l3
量子アニーリングの実行
以下のコードは、solver(今回はAdvantage_system6.4)という量子アニーリングマシンで、QUBO行列
# 量子アニーリングの実行
endpoint = 'https://cloud.dwavesys.com/sapi/'
dw_sampler = DWaveSampler(solver=solver, token=token, endpoint=endpoint)
sampler = EmbeddingComposite(dw_sampler)
sampleset = sampler.sample_qubo(Q, num_reads=num_reads)
魔方陣の可視化
以下のコードは、num_reads回のアニーリングの中で最も上手くいった結果に基づいて魔方陣を生成し、それを可視化しています。
# 結果に基づいて魔方陣を生成
result = [i for i in sampleset.first[0].values()]
ans = [[None] * N for _ in range(N)]
for i in range(1, N + 1):
for j in range(1, N + 1):
for n in range(1, N ** 2 + 1):
if result[ijn_to_idx[(i, j, n)]] == 1:
ans[i - 1][j - 1] = n
# 魔方陣の表示
print(*ans, sep='\n')
結果
上のコードを実行すると以下のような出力が得られました。
[6, 7, 2]
[1, 5, 9]
[8, 3, 4]
これをきれいにすると以下のような魔方陣になり、この記事の最初の方で紹介した
まとめ
この記事では、量子アニーリングを用いて魔方陣を生成する方法について解説しました。
今回作成した最適化問題はかなり不安定で
-
組合せ最適化問題とは、多くの選択肢からある制約をもとに最も良い解を求める問題のこと。ナップサック問題やSAT問題などが挙げられる。 ↩︎
Discussion