模擬焼き鈍し法用Qubo生成pythonパッケージquboassistの使い方解説
著者:榎本 觀
連絡先:enomotokan@gmail.com
概要
本記事では、筆者が開発したQubo生成pythonパッケージquboassistの使用方法を解説する。
導入
まず、Quboとは二次制約なし二値最適化問題(Quadratic unconstrainted binary optimization)の略で、即ち
という問題である。ここで、
quboassistは、各制約条件へのある重み付けの下で
この問題は、目的函数が二次であり、各変数は有界な範囲の中で整数値をとり、かつ各制約条件は整数係数且つ線型である。
巡回セールスマン問題など、様々な計算量理論的に難しい問題がこの形式に定式化出来る。
本パッケージは、内部アルゴリズムのコア部分を、効率的なデータ構造を用いてRustで並列計算を駆使して実装し、また直感的な問題入力インタフェースをpythonで実装している為、高度な計算を簡便に行うことが可能になる。変換の理論的基礎については、github(https://github.com/enomotokan/quboassist)やPyPI(https://pypi.org/project/quboassist/)にあるREADME.mdファイルの最後の辺りを参照されたい。
使用方法
以下、本パッケージquboassistの使用方法を解説する。
まずはpythonがインストールされている環境で
pip install quboassist
してローカル環境にquboassistのビルド済みファイルをインストールする。記事執筆時点(2024/11/15)でpythonバージョンは3.13以下3.8以上、OSはubuntu、MacOS、Windowsに対応している。
早速使ってみよう。コマンドラインでpythonを起動し、
from quboassist import *
で内部で定義されている三つのクラスVariable
,Formula
,Problem
をインポートする。quboassistで変数を定義するには、Variable
クラスのインスタンスを作る。
x = Variable("x", 2, 5)
これで、
x = [Variable("x{i}".format(i), 2, 5) for i in range(10000)]
とすれば良い。次は、定義した変数を用いて式を作りたい。例えば、
に対応する式をquboassistで作るには、
f = - x[0]**2 - 3 * x[1]
とする。内部では、Variable
クラスのインスタンスである変数たちがFormula
クラスのインスタンスf
となり、その係数情報がRustベクタでメモリ上に載る。
そもそも解きたい問題が二次なので、式も二次式までのみ取り扱え、それ以上を取り扱おうとするとエラーになる。
不等式や等式を作るには、例えば
に対応する式を
g = x[0] > x[1]
としてg
に格納する。これらを用いて、問題を定義することが出来る。
P = Problem()
P.add_objective(f)
P.add_constraint(g)
P
にProblem
クラスのインスタンスを格納し、その目的函数を上で定義したf
とし、制約条件をg
とする。これで、最適化問題
を
P
に格納できたことになる。実際に逐次代入することで簡単に確かめることが出来るが、この問題の最適解はこの問題をQubo形式に変換するには、例えば制約条件
g
の重み付けをqubo = P.compile([10])
とする。今回は一つのみであるが、各制約条件の重みを表すベクトルをcompile
メソッドに代入すると、Quboが生成されてqubo
に格納される。内部では、まず全ての不等式制約に対して補助変数を加えて等式制約に変換し、次に補助変数を含め全ての変数を
後は、Quboをソルバーに代入して問題を解いてみよう。
pip install dwave-neal
で模擬焼き鈍し法(シミュレーテッドアニーリング)ソルバーをインストールしておく。
sampler = neal.SimulatedAnnealingSampler()
でソルバーをsampler
に格納する。以下、既に生成したqubo
をtodict
メソッドでsampler
の受け付ける辞書型に変換して解かせる。
result = sampler.sample_qubo(qubo.todict()).first.sample
result
には、変換した問題の二値変数の値が辞書式で格納されている。solution
メソッドで、result
から元の問題の解を構成できる。
solution = P.solution(result, "neal")
print(solution)
出力される解は、ほとんど
({'x0': 5, 'x1': 4}, [True])
注意してほしいのは、模擬焼き鈍し法は発見的手法(ヒューリスティクス)であり、必ずしも最適解を出力するわけではないことである。さらに、本パッケージの場合ラグランジュ緩和で元の問題を無理やり制約のない問題に変換しているので、必ずしも出力される解が全ての制約条件を充たすとは限らぬのである。
その情報はsolution
の二番目にあり、これはそれぞれの制約条件が充たされているか否かを表す真理値の格納されたリストである。
重み付けを如何にすべきか
重みは、quboassist
の内部では、問題P
の前回のcompile
時に既に追加されていた制約条件に対応するQuboは保存され、その繰り返しの中で同じ計算を反復することを避けている。
前章の例題では、制約条件の重みをoptuna
を用いることを薦めたい。optuna
そのものの解説については他の文献に譲るとして、ここではoptuna
を併用することで上の目的が達成されることを見る。
コマンドラインで
pip install optuna
しておく。
最適化問題を焼き鈍し法で解く時のハイパーパラメータである重み
ここで、それぞれ真理値
この目的函数を用いて、最適化問題
を解くためのサンプルコードを以下に示す。このサンプルコードでは
import neal
import quboassist
import optuna
import numpy as np
from copy import copy
x = [quboassist.Variable("x{}".format(i), 0, 3) for i in range(10)]
best_solution = []
best_val = np.inf
P = quboassist.Problem()
f = - x[0]**2 - x[1]
P.add_objective(f)
g = x[0] > x[1]
P.add_constraint(g)
h = x[0] + x[1] == x[2]
P.add_constraint(h)
sampler = neal.SimulatedAnnealingSampler()
def objective(trial):
w = [trial.suggest_float("w{}".format(i), 0, 5) for i in range(2)
qubo = P.compile(w)
result = sampler.sample_qubo(qubo.todict()).first
solution = P.solution(result.sample)
print("\n")
print(solution)
obj = w[0] + w[1] + 10 * sum(np.logical_not(solution[1]))
val = result.energy
# Note that result.energy and the value of objective function may differ by a constant which appears when expanding the product of variables!
global best_solution, best_val
if np.all(solution[1]) and val < best_val:
best_val = val
best_solution = copy(solution)
return obj
study = optuna.create_study(direction="minimize")
study.optimize(objective, n_trials=20)
print("\n\nBest Solutuion")
print(best_solution)
追記
筆者は、普段は数理最適化専門のフリーランスエンジニアとして活動しています。詳しい自己紹介は以のwantedlyページを参考にして下さい。
もし本記事やコードに関して疑問点などあれば以下のメールアドレスにご連絡下さい。
また、数理最適化及びヒューリスティクスに関する技術の相談、開発の依頼なども同メールアドレス宛に連絡下されば幸いです。
enomotokan@gmail.com
Discussion
参考になります👀👀