QAP.02:イジング変数と「数の2分割問題」【量子コンピュータ/アニーリング@Python/Fixstars Amplify】
【はじめに】
前回は「Fixstars Amplify」を使って、ざっと量子アニーリングのプログラムをやってみた。簡単にまとめると次のような感じになる。
【再掲】
量子アニーリングによるプログラムの挙動をざっくりいうと、
というもの。
実際にプログラムをするときは
といった流れになる。
引き続き、簡単な例題を使って、量子アニーリングのプログラムをしてみる。今回は「イジング変数」を使ってみる。
【再掲】
「Fixstars Amplify」では組み合わせを見つけるための変数として「イジング変数」と「QUBO変数」を用意している。
【例題】:イジング変数+数の2分割
「数の分割問題」は
というもの。
今回は、次の「10個の数字を2つのグループに分ける」場合の組み合わせを考えてみる
【考え方】
「数の総和ができるだけ同じになるように2分割する」ということは、
と考えることもできる。
ということは、各数字の符号「±」について、「プラスマイナスがいい感じになる組み合わせ」を見つけられたら「数の2分割」が達成できることになる。
【実行環境について】
今回も引き続き「Fixstars Amplify (無料枠)」+「Google Colab (無料枠)」でプログラミングしてみる。
【1】ライブラリのインストール(Colab上にインストール)
! pip install amplify
! pip install --upgrade amplify
【2】クライアントオブジェクトの生成
from amplify.client import FixstarsClient
client = FixstarsClient()
client.parameters.timeout = 1000 # タイムアウトは1000ミリ秒(1秒)
client.parameters.outputs.duplicate = True # みつかった解が複数あってもOK
client.parameters.outputs.num_outputs = 0 # 0: 見つかった解を全て出力
# トークン設定
client.token = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" # 無料登録して取得したトークンを指定する
※トークンは掲載上「XXX... ...XXX」としているが、実際は「Fixstars Amplify」に無料登録して取得したトークンを設定する。
【3】イジング変数を用意する
QUBO変数と同じく、「SymbolGeneratorオブジェクト」経由で「IsingPolyオブジェクト」を生成することで「イジング変数」を用意することができる。
from amplify import IsingPoly
from amplify import SymbolGenerator
# サンプルデータ
my_sample = [2, 10, 3, 8, 5, 7, 9, 5, 3, 2]
# イジング変数の生成
gen = SymbolGenerator(IsingPoly)
q = gen.array(len(my_sample))
print(q)
▲ s_0 ~ s_9 まで10個のイジング変数が生成された
【4】目的関数を作る
数式通り作ると次のような感じになる。
■目的関数を数式通りにプログラムした場合(未完)
# 目的関数
f = my_sample[0]*q[0] + my_sample[1]*q[1] + my_sample[2]*q[2] \
+ my_sample[3]*q[3] + my_sample[4]*q[4] + my_sample[5]*q[5] \
+ my_sample[6]*q[6] + my_sample[7]*q[7] + my_sample[8]*q[8] \
+ my_sample[9]*q[9]
# for文でまとめる場合
#f = sum(my_sample[i]*q[i] for i in range(len(my_sample)))
# アインシュタイン縮約表記の場合
#from amplify import einsum
#f = einsum("i,i",q,my_sample)
print(f)
■目的関数を量子アニーリング計算に合わせる
ここで注意したいのが、量子アニーリングは与えられた数式(+制約条件)の中で、
ということ。
そのため、そのままの目的関数(数式)では「全てマイナスをとって最小値」を取ろうとする
そこで、全体を2乗して0が最小値となるような式にして適切な組み合わせをつくる
■最終的な量子アニーリング計算用の目的関数(完成版)
# 目的関数
f = my_sample[0]*q[0] + my_sample[1]*q[1] + my_sample[2]*q[2] \
+ my_sample[3]*q[3] + my_sample[4]*q[4] + my_sample[5]*q[5] \
+ my_sample[6]*q[6] + my_sample[7]*q[7] + my_sample[8]*q[8] \
+ my_sample[9]*q[9]
# for文でまとめる場合
#f = sum(my_sample[i]*q[i] for i in range(len(my_sample)))
# アインシュタイン縮約表記の場合
#from amplify import einsum
#f = einsum("i,i",q,my_sample)
# 量子アニーリングの計算用に目的関数を2乗する
f = f ** 2
print(f)
【5】Solverオブジェクトを生成して量子アニーリング計算をする
※再掲:
「Fixstars Amplify」では「Solverオブジェクト(Client設定仕込み済み)」経由で「目的関数」を解かせる。
# Solverオブジェクト生成
from amplify import Solver
solver = Solver(client)
# 目的関数をわたして量子アニーリング計算をする
result = solver.solve(f)
【6】計算結果を確認する
答えが求められているかは「len()」で確認する
# 解があるかを確認
if len(result) == 0:
print("no answer")
# 得られた解の数を出力してみる
print(f"{len(result)} answers !")
イテレータをまわして求められた解を出力してみる
from amplify import decode_solution
for sol in result:
energy = sol.energy
values = sol.values
res = decode_solution(q, values)
print(f"energy = {energy}, {values} \nResult is {q} = {res}")
print("-----------")
もう少しわかりやすく、サンプルデータの数字を使って分割結果を出力してみる。
for sol in result:
#energy = sol.energy
values = sol.values
res = decode_solution(q, values)
A0 = []
A1 = []
# enumerateで インデックスと実データを取り出す
for idx, val in enumerate(res):
if val > 0: # イジング変数が1.0となったもの
A1.append(my_sample[idx])
else: # イジング変数が- 1.0となったもの
A0.append(my_sample[idx])
print(f"{A0}:{sum(A0)} - {A1}:{sum(A1)}")
▲各グループの総和が同じになるような組み合わせが求められている。
本来は、「見やすくソートする、1つ目と2つ目のグループが入れ替わっただけの同じ組み合わせを除外する」等のような「後処理」も必要だが省略。
なお、しれっと使った「enumerate」は、pythonの組み込み関数。イテレータでループを回すときに「インデックス と 値」を同時に取り出してくれるものである。
※ enumerate関数について
【7】全体コード
最後にコード全体を示しておく。
【例題】
次の10個の数字を2つのグループに分け、各グループ内の総和ができるだけ同じになるようなの組み合わせを探す
■ サンプルデータ
[2, 10, 3, 8, 5, 7, 9, 5, 3, 2]
【実行環境】
・Fixstars Amplify + Google Colab
■事前準備:ライブラリのインストール
! pip install amplify
! pip install --upgrade amplify
■ここからが全体コード
from amplify.client import FixstarsClient
from amplify import IsingPoly
from amplify import SymbolGenerator
from amplify import Solver
from amplify import decode_solution
client = FixstarsClient()
client.parameters.timeout = 1000 # タイムアウトは1000ミリ秒(1秒)
client.parameters.outputs.duplicate = True # みつかった解が複数あってもOK
client.parameters.outputs.num_outputs = 0 # 0: 見つかった解を全て出力
# トークン設定
client.token = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" # 無料登録して取得したトークンを指定する
# サンプルデータ
my_sample = [2, 10, 3, 8, 5, 7, 9, 5, 3, 2]
# イジング変数の生成
gen = SymbolGenerator(IsingPoly)
q = gen.array(len(my_sample))
print(q) # [s_0, s_1, s_2, s_3, s_4, s_5, s_6, s_7, s_8, s_9]
# 目的関数の作成
#f = my_sample[0]*q[0] + my_sample[1]*q[1] + my_sample[2]*q[2] \
# + my_sample[3]*q[3] + my_sample[4]*q[4] + my_sample[5]*q[5] \
# + my_sample[6]*q[6] + my_sample[7]*q[7] + my_sample[8]*q[8] \
# + my_sample[9]*q[9]
f = sum(my_sample[i]*q[i] for i in range(len(my_sample)))
# アインシュタイン縮約表記版
#from amplify import einsum
#f = einsum("i,i",q,my_sample)
# 2乗して量子アニーリング計算できるようにする
f = f ** 2
# Solverを生成
solver = Solver(client)
# 目的関数を渡して計算させる
result = solver.solve(f)
# 結果を確認
if len(result) == 0:
print("no answer")
print(f"{len(result)} answers !")
# 答えを出力して確認する
#for sol in result:
# energy = sol.energy
# values = sol.values
# res = decode_solution(q, values)
#
# print(f"energy = {energy}, {values} \nResult is {q} = {res}")
# print("-----------")
# サンプルデータを使って分割結果を出力する
for sol in result:
#energy = sol.energy
values = sol.values
res = decode_solution(q, values)
A0 = []
A1 = []
# enumerateで インデックスと実データを取り出す
for idx, val in enumerate(res):
if val > 0: # イジング変数が1.0となったもの
A1.append(my_sample[idx])
else: # イジング変数が- 1.0となったもの
A0.append(my_sample[idx])
print(f"{A0}:{sum(A0)} - {A1}:{sum(A1)}")
【おまけ】:energy=0にならない場合の挙動
今回は、2分割したら各グループの総和がちょうど同じになるような都合の良い例(元のサンプルデータの総和が偶数となっている例)を挙げた。
そうではない場合(サンプルデータの総和が奇数の場合)はどうなるかをやってみる。
from amplify.client import FixstarsClient
from amplify import IsingPoly
from amplify import SymbolGenerator
from amplify import Solver
from amplify import decode_solution
client = FixstarsClient()
client.parameters.timeout = 1000 # タイムアウトは1000ミリ秒(1秒)
client.parameters.outputs.duplicate = True # みつかった解が複数あってもOK
client.parameters.outputs.num_outputs = 0 # 0: 見つかった解を全て出力
# トークン設定
client.token = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" # 無料登録して取得したトークンを指定する
# サンプルデータ
#my_sample = [2, 10, 3, 8, 5, 7, 9, 5, 3, 2]
my_sample = [2, 10, 4, 8, 5, 7, 9, 5, 3, 2]
# イジング変数の生成
gen = SymbolGenerator(IsingPoly)
q = gen.array(len(my_sample))
print(q) # [s_0, s_1, s_2, s_3, s_4, s_5, s_6, s_7, s_8, s_9]
# 目的関数の作成
f = sum(my_sample[i]*q[i] for i in range(len(my_sample)))
# アインシュタイン縮約表記版
#from amplify import einsum
#f = einsum("i,i",q,my_sample)
# 2乗して量子アニーリング計算できるようにする
f = f ** 2
# Solverを生成
solver = Solver(client)
# 目的関数を渡して計算させる
result = solver.solve(f)
# 結果を確認
if len(result) == 0:
print("no answer")
print(f"{len(result)} answers !") # 例:78 answers !
# 答えを出力して確認する
for sol in result:
energy = sol.energy
values = sol.values
res = decode_solution(q, values)
print(f"energy = {energy}, {values} \nResult is {q} = {res}")
print("-----------")
# サンプルデータを使って分割結果を出力する
for sol in result:
#energy = sol.energy
values = sol.values
res = decode_solution(q, values)
A0 = []
A1 = []
# enumerateで インデックスと実データを取り出す
for idx, val in enumerate(res):
if val > 0: # イジング変数が1.0となったもの
A1.append(my_sample[idx])
else: # イジング変数が- 1.0となったもの
A0.append(my_sample[idx])
print(f"{A0}:{sum(A0)} - {A1}:{sum(A1)}")
▲ 量子アニーリングの計算の性質通り、数式の答えをできるだけ小さくしようして、energy = 1.0となる組み合わせが算出される。