OpenJijで最適化問題を解く#
jijmodeling
の使い方を理解するために基本的な例を見ていきましょう。以下では、シンプルな数理モデルを作成し、それを変換し、ソルバーで実行する手順を説明します。最初の2つのセクションでは、jijmodeling
だけで十分ですが、LaTeX出力を簡単に確認するためにJupyterノートブックを使用することをお勧めします。
3つ目のセクションでは、JijModelingTranspilerとOpenJijを使用します。JijModelingTranspilerとOpenJijはpip
を使用してインストールできます。
pip install jijmodeling-transpiler openjij
概要#
jijmodeling
を記述方法は数理最適化に精通している人にとって自然に感じられるはずです。
jijmodeling
のクラス(BinaryVar
やPlaceholder
など)を基本的な演算を使用して組み合わせることで数式が表現できます。___Var
クラスはさまざまな種類の決定変数を指します。Placeholder
は後で指定される定数や値を表します。つまり、問題を抽象化し、インスタンスデータとしてマークしたいものを表しています。もちろん、jijmodeling
による数理モデルの構築には数値リテラルも使用できます。
import jijmodeling as jm
x = jm.BinaryVar("x")
y = jm.IntegerVar("y", lower_bound=1, upper_bound=10)
n = jm.Placeholder("n")
exp = x * (y ** 2) * n
上記のプレースホルダーと変数は0次元(スカラー)ですが、これらのクラスを使用して配列や多次元変数および定数を表現することもできます(後で詳しく説明します)。
jijmodeling
では、上記のexp
のような式をProblem
オブジェクトに加えることで数理モデルを構築し、数理モデル全体を表現します。制約は、比較式をラップするConstraint
クラスによって定義されます(Constraint
では<=
、==
、および>=
のみがサポートされています)。
a = jm.IntegerVar("a", lower_bound=5, upper_bound=20)
b = jm.IntegerVar("b", lower_bound=1, upper_bound=20)
c = jm.IntegerVar("c", lower_bound=20, upper_bound=30)
n = jm.Placeholder("n")
problem = jm.Problem("my problem")
problem += n * (a + b + c)
problem += jm.Constraint("c1", 2 * (b + c) <= 75)
problem += jm.Constraint("c2", a + b <= 40)
problem
モデルの作成#
一般的なナップサック問題をどのようにモデル化するか見てみましょう。
ナップサック問題とは、それぞれ価値と重量が設定された\(N\)個のアイテムがあるとき、重量制限\(W\)の範囲内で持ち帰れるアイテムの価値を最大化する問題です。以下では、アイテム\(i\)を持ち帰るかどうかをバイナリ変数\(x_{i}\)で表現し、アイテム\(i\)の重量を\(w_i\)、アイテム\(i\)の価値を\(v_i\)で表現するものとします。
まず、これらの値をjijmodeling
で定義してみましょう。
# W: 重量制限(持ち帰れる重量の上限値)
W = jm.Placeholder("W")
# v_i: アイテムiの価値
values = jm.Placeholder("v", ndim=1)
# w_i: アイテムiの重量
weights = jm.Placeholder("w", ndim=1)
# Nはプレースホルダーの0次元方向のサイズに基づいて自動的に決まるため、
# 以下のように`len_at`メソッドを使って定義することができる。
# また、LaTeX出力のために、ここでは`latex`パラメータを指定しておく。
N = values.len_at(0, latex="N")
# x_i: アイテムiを持ち帰るかどうかを表すバイナリ変数
# ここでは、`shape`パラメータを使って、N個の要素を持つベクトルを定義している。
x = jm.BinaryVar("x", shape=(N,))
# 総和を取るためにインデックスを定義しておく。
i = jm.Element("i", (0, N))
上記のコードの最後に定義したElement
を利用してjijmodeling.sum
を使用すると、シグマ記法に似たスタイルで総和を記述できます。上記のコードでは、インデックスi
を0を含み\(N\)を含まないの区間として定義しています。これを事前に記述するのは奇妙に感じるかもしれませんが、そうすることで再利用が可能になり利便性が向上します。
ナップサック問題では、重量制限を満たしながら持ち帰るアイテムの価値を最大化する必要があります。これをjijmodeling
で表現すると以下のようになります。
problem = jm.Problem("knapsack", sense = jm.ProblemSense.MAXIMIZE)
problem += jm.sum(i, values[i] * x[i])
problem += jm.Constraint("weight limit", jm.sum(i, weights[i] * x[i]) <= W)
problem
jijmodeling
の式は通常のPythonオブジェクトのように変数に格納できます。これにより、大規模な問題に取り組む際には複雑な式を小さなものから構築することができ、後で理解・修正しやすくなります。ナップサック問題のような小さい問題ではあまり役に立ちませんが、例として、上記のコードを次のように書き直すことができます。
chosen_v = values[i] * x[i]
chosen_w = weights[i] * x[i]
sum_of_values = jm.sum(i, chosen_v)
sum_of_weights = jm.sum(i, chosen_w)
weight_below_limit = sum_of_weights <= W
problem = jm.Problem("knapsack", sense = jm.ProblemSense.MAXIMIZE)
problem += sum_of_values
problem += jm.Constraint("weight limit", weight_below_limit)
problem
これらの2つのコードが表すモデルは同等です。あなたの好みや利便に合わせてモデルを記述してみてください。
モデルを解く#
前述のコードによりモデルを構築することができましたが、jijmodeling
だけではLaTeX出力する以外にはあまりできることはありません。インスタンスデータと組み合わせて、ソルバーの入力を生成すること必要になります。例として、無料で利用できるJijModelingTranspilerを使用して数理モデルを変換し、OpenJijを使用してモデルを解く方法を見てみましょう。なお、JijZeptで提供されているツールもjijmodeling
によって構築された数理モデルを入力として受け入れるようになっています。
本説は簡易的なデモンストレーションを目的としているため、JijModelingTranspilerやOpenJijの説明は行いません。詳細については、それぞれのドキュメントを参照してください。
以下では、JijModelingTranspilerを使用して、jijmodeling
で構築した数理モデルとインスタンスデータを渡してQUBOに変換することとします。
from jijmodeling_transpiler.core import compile_model
from jijmodeling_transpiler.core.pubo import transpile_to_pubo
data = {
"W": 100,
"v": [100, 90, 80, 70, 60, 50, 40, 30],
"w": [1, 5, 10, 20, 30, 40, 50, 60, 70]
}
compiled = compile_model(problem, data)
qubo, _ = transpile_to_pubo(compiled).get_qubo_dict()
これでOpenJijが入力として受け入れることができるQUBOを取得できました。QUBOを手作業で書き出すことはミスが非常に発生しやすいのですが、jijmodeling
およびjijmodeling_transpiler
を利用すれば、そのようなミスを無くすことができ、検証が容易になります。では、実際にQUBOをopenjij
に入力して問題を解いてみましょう。
import openjij as oj
sampler = oj.SASampler()
response = sampler.sample_qubo(qubo, num_reads=1)
response.first
Sample(sample={0: 1, 1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 0, 7: 0}, energy=-5.501111111111113, num_occurrences=1)
上記のコードはopenjij
のシミュレーテッドアニーリングを使用しており、num_reads=1
は1回だけサンプリングすることを示しています。num_reads
の値を増やすことで複数回サンプリングし、レスポンスオブジェクトを使用してさまざまな結果を探索することができます。しかし、今回の問題では、すべてのサンプルが最適解に到達するため、ここでは1回のサンプルを行い、見つかった「最良」の解を見てみることとします。
Sample(sample={0: 1, 1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 0, 7: 0}, energy=-5.501111111111113, num_occurrences=1)
上記のレスポンスオブジェクトは各決定変数に対してopenjij
が求めた値を示しています。JijModelingTranspilerやOpenJijを使用して結果をより良く処理および視覚化したり、同じ数理モデルを異なる目的で再利用したりするために、さらに多くのことができますが、それについてはそれぞれのドキュメントページを参照してください。