動画:QUBO++ 入門(Python)

動画「QUBO++ 入門(Python)」の手引きです。 章ごとに、要点と動画に出てくるプログラム・出力、詳しい説明のページへのリンクをまとめています。 「▶ 動画の … から再生」をクリックすると、その場面から再生します。 動画では一部を省略して見せているプログラムも、ここではそのまま実行できる形で載せています。

QUBO++ 入門(Python)(45:39・YouTube)

この動画で扱うこと:

  • 組合せ最適化問題と QUBO、QUBO++ で数式をそのまま書いて解く流れ
  • 変数と式、simplify_as_binary での整理、ソルバーで解いて解を読む方法
  • 変数の配列(qbpp.sum、one-hot、否定リテラル ~x、配列演算)と分割問題
  • 制約の書き方(ペナルティ式と qbpp.cons())、整数変数と不等式の制約
  • 3 つのソルバーの使い分け、非線形関数と高次の項、すべてを 1 つのモデルにまとめる例

動画(チュートリアル)の一覧に戻る

QUBO++ とは

組合せ最適化

▶ 動画の 0:45 から再生

たくさんの選択肢の中から、いちばんよい組合せを選ぶ問題を、組合せ最適化問題といいます。 0 か 1 を選ぶ変数が 1 つ増えるたびに、組合せの数は 2 倍になります。 変数が 100 個あるだけで、およそ $10^{30}$ 通りになり、すべてを調べることはできません。

QUBO

▶ 動画の 1:38 から再生

QUBO は、0 か 1 の値をとる変数の 2 次の多項式を最小にする問題です。 たとえば $f = (a + 2b + 3c + 4d - 5)^2$ を展開すると、次の式になります(0/1 の変数では $a^2 = a$)。

\[f = 25 - 9a - 16b - 21c - 24d + 4ab + 6ac + 8ad + 12bc + 16bd + 24cd\]

1 次の係数は対角に、2 次の係数は変数の組の場所に並べると、行列 $Q$ になります(定数 25 は行列の外)。

  a b c d
a −9 4 6 8
b   −16 12 16
c     −21 24
d       −24

実際の問題では、制約をペナルティの式に直して展開し、不等式には補助の変数も加えて、行列を作ります。手でやるのは大変です。

詳しくはQUBO問題をご覧ください。

QUBO++

▶ 動画の 3:03 から再生

QUBO++ では、行列を作る代わりに、数式をそのまま書きます。 さきほどの $f$ は、Python では f = qbpp.sqr(a + 2 * b + 3 * c + 4 * d - 5)、 C++ では auto f = qbpp::sqr(a + 2 * b + 3 * c + 4 * d - 5); の 1 行です。 展開して行列を作る作業は QUBO++ が自動で行い、書いた式は内蔵のソルバーにそのまま渡して解けます。

準備

▶ 動画の 4:45 から再生

試すだけなら、ドキュメントのサイトの Playground で、プログラムを書いてその場で実行できます。 手元で使うときは、ターミナルで pip でインストールします(Linux、Windows では WSL の上で使います)。

pip install pyqbpp

プログラムは、import pyqbpp as qbpp で始めます。

詳しくはインストールとPlaygroundをご覧ください。

変数と式

変数と式

▶ 動画の 5:26 から再生

qbpp.var で、0 か 1 の値をとる変数を作ります。かっこの中の名前は、式を表示するときに使われます。 変数をたし算、ひき算、かけ算で組み合わせると式になり、式は自動的に展開されます。

import pyqbpp as qbpp

a = qbpp.var("a")
b = qbpp.var("b")
c = qbpp.var("c")
f = (a + b - 1) * (b + c - 1)
print("f =", f)

出力:

f = 1 -b -c -a -b +a*b +a*c +b*b +b*c

式の整理

▶ 動画の 6:42 から再生

simplify() は同じ項をまとめます。simplify_as_binary() は、0/1 の変数では $b^2 = b$ になる性質も使って、いちばん簡単な形にします。 ソルバーに渡す前には、必ず simplify_as_binary() を呼んでおきます。 f.simplify_as_binary() は f 自身を書き換え、qbpp.simplify_as_binary(f) は新しい式を返します。

import pyqbpp as qbpp

a = qbpp.var("a")
b = qbpp.var("b")
c = qbpp.var("c")
f = (a + b - 1) * (b + c - 1)
print("f =", f)
print("f =", f.simplify())
print("f =", f.simplify_as_binary())

出力:

f = 1 -b -c -a -b +a*b +a*c +b*b +b*c
f = 1 -a -2*b -c +a*b +a*c +b*b +b*c
f = 1 -a -b -c +a*b +a*c +b*c

詳しくは変数と式をご覧ください。

ソルバーで解く

問題を式にする

▶ 動画の 7:52 から再生

0/1 の変数 a, b, c, d で、$a + 2b + 3c + 4d = 5$ になる組合せを探します。 $f = (a + 2b + 3c + 4d - 5)^2$ は 0 以上で、条件を満たすときだけ 0 になります。 16 通りの割り当てのうち、$f = 0$ になるのは (a, b, c, d) = (0, 1, 1, 0) と (1, 0, 0, 1) の 2 通りです。 $f$ を最小にする値が答えです。

EasySolver で解く

▶ 動画の 9:01 から再生

qbpp.EasySolver(f) でソルバーを作り、search で探索します。 time_limit は探索する最長の時間(秒)、target_energy=0 は $f$ の値が 0 以下の解が見つかった時点で探索を止める指定です。

import pyqbpp as qbpp

a = qbpp.var("a")
b = qbpp.var("b")
c = qbpp.var("c")
d = qbpp.var("d")
f = qbpp.sqr(a + 2 * b + 3 * c + 4 * d - 5)
f.simplify_as_binary()
solver = qbpp.EasySolver(f)
sol = solver.search(time_limit=10, target_energy=0)
print(sol)

動画での出力(最適解は 2 つあるので、実行するたびにもう一方の解が返ることもあります):

Sol(energy=0, {a: 0, b: 1, c: 1, d: 0})

解を読む

▶ 動画の 10:18 から再生

sol.energy は、見つかった解での $f$ の値で、エネルギーと呼びます(f(sol) も同じ値です)。 sol(a) のように変数を渡すと、その変数の値が分かります。

import pyqbpp as qbpp

a = qbpp.var("a")
b = qbpp.var("b")
c = qbpp.var("c")
d = qbpp.var("d")
f = qbpp.sqr(a + 2 * b + 3 * c + 4 * d - 5)
f.simplify_as_binary()
solver = qbpp.EasySolver(f)
sol = solver.search(time_limit=10, target_energy=0)
print(sol)
print("energy =", sol.energy)
print("a, b, c, d =", sol(a), sol(b), sol(c), sol(d))

動画での出力(最適解は 2 つあるので、実行するたびにもう一方の解が返ることもあります):

Sol(energy=0, {a: 0, b: 1, c: 1, d: 0})
energy = 0
a, b, c, d = 0 1 1 0

すべての最適解

▶ 動画の 11:18 から再生

ExhaustiveSolver は、すべての割り当てを調べて、最適解であることを保証します。 best_energy_sols=0 にすると最適解をすべて集め、sol.sols に入ります。

import pyqbpp as qbpp

a = qbpp.var("a")
b = qbpp.var("b")
c = qbpp.var("c")
d = qbpp.var("d")
f = qbpp.sqr(a + 2 * b + 3 * c + 4 * d - 5)
f.simplify_as_binary()
solver = qbpp.ExhaustiveSolver(f)
sol = solver.search(best_energy_sols=0)
for s in sol.sols:
    print(s)

出力:

Sol(energy=0, {a: 0, b: 1, c: 1, d: 0})
Sol(energy=0, {a: 1, b: 0, c: 0, d: 1})

詳しくは式の求解をご覧ください。

変数の配列

配列を作る

▶ 動画の 12:28 から再生

qbpp.var に shape を指定すると、変数の配列ができます。x[i] で i 番目の変数を取り出します(番号は 0 から)。

import pyqbpp as qbpp

x = qbpp.var("x", shape=5)
print(x)

出力:

[x[0], x[1], x[2], x[3], x[4]]

詳しくは配列をご覧ください。

合計

▶ 動画の 13:11 から再生

0 から始めて += で足していく形はよく使われます。qbpp.sum を使えば、同じ式を 1 行で作れます。

import pyqbpp as qbpp

x = qbpp.var("x", shape=5)
f = 0
for i in range(5):
    f += x[i]
print("f =", f.simplify_as_binary())
g = qbpp.sum(x)
print("g =", g.simplify_as_binary())

出力:

f = x[0] +x[1] +x[2] +x[3] +x[4]
g = x[0] +x[1] +x[2] +x[3] +x[4]

詳しくは総和関数をご覧ください。

one-hot

▶ 動画の 13:53 から再生

合計から 1 を引いて 2 乗した式は、ちょうど 1 つの変数が 1 のときだけ 0 になります。この条件を one-hot と呼びます。

import pyqbpp as qbpp

x = qbpp.var("x", shape=5)
f = qbpp.sqr(qbpp.sum(x) - 1)
f.simplify_as_binary()

solver = qbpp.ExhaustiveSolver(f)
sol = solver.search(best_energy_sols=0)
for s in sol.sols:
    print(s)

出力:

Sol(energy=0, {x[0]: 1, x[1]: 0, x[2]: 0, x[3]: 0, x[4]: 0})
Sol(energy=0, {x[0]: 0, x[1]: 1, x[2]: 0, x[3]: 0, x[4]: 0})
Sol(energy=0, {x[0]: 0, x[1]: 0, x[2]: 1, x[3]: 0, x[4]: 0})
Sol(energy=0, {x[0]: 0, x[1]: 0, x[2]: 0, x[3]: 1, x[4]: 0})
Sol(energy=0, {x[0]: 0, x[1]: 0, x[2]: 0, x[3]: 0, x[4]: 1})

分割問題

問題

▶ 動画の 15:03 から再生

8 個の数 64, 27, 47, 74, 12, 83, 63, 40 を、P と Q の 2 つのグループに分け、2 つの合計をできるだけ近づけます。 数ごとに変数 x[i] を用意し、1 なら P、0 なら Q に入れます。 2 つの合計の差を 2 乗した $f$ を最小にすればよいわけです。

否定リテラル

▶ 動画の 15:52 から再生

~x は、x が 0 のとき 1、x が 1 のとき 0 になる否定リテラルで、値は $1 - x$ と同じです。 Q の合計は、w[i] と ~x[i] の積の和で書けます(ループの中では q += w[i] * ~x[i])。 QUBO++ は式を作るとき ~x を $1 - x$ に展開せず、そのまま扱います(simplify_as_binary() で簡約すると、2 次以下の項の ~x は展開されます)。変数の配列 x も、~x と書くと否定の配列になります。

詳しくは否定リテラルをご覧ください。

ループで書く

▶ 動画の 16:49 から再生

変数は 8 個で、割り当ては $2^8 = 256$ 通りなので、ExhaustiveSolver で全部調べられます。

import pyqbpp as qbpp

w = [64, 27, 47, 74, 12, 83, 63, 40]
x = qbpp.var("x", shape=len(w))
p = 0
q = 0
for i in range(len(w)):
    p += w[i] * x[i]
    q += w[i] * ~x[i]
f = qbpp.sqr(p - q)
f.simplify_as_binary()

sol = qbpp.ExhaustiveSolver(f).search()
print("f =", sol(f), " p =", sol(p), " q =", sol(q))
print("P:", [w[i] for i in range(len(w)) if sol(x[i]) == 1])
print("Q:", [w[i] for i in range(len(w)) if sol(x[i]) == 0])

出力:

f = 0  p = 205  q = 205
P: [47, 12, 83, 63]
Q: [64, 27, 74, 40]

配列演算

▶ 動画の 17:54 から再生

w * x は要素ごとのかけ算です。qbpp.sum で足し合わせれば、ループの 5 行が 2 行になります。

import pyqbpp as qbpp

w = [64, 27, 47, 74, 12, 83, 63, 40]
x = qbpp.var("x", shape=len(w))
p = qbpp.sum(w * x)
q = qbpp.sum(w * ~x)
f = qbpp.sqr(p - q)
f.simplify_as_binary()

sol = qbpp.ExhaustiveSolver(f).search()
print("f =", sol(f), " p =", sol(p), " q =", sol(q))
print("P:", [w[i] for i in range(len(w)) if sol(x[i]) == 1])
print("Q:", [w[i] for i in range(len(w)) if sol(x[i]) == 0])

出力:

f = 0  p = 205  q = 205
P: [47, 12, 83, 63]
Q: [64, 27, 74, 40]

詳しくは配列演算をご覧ください。

結果

▶ 動画の 18:41 から再生

P は 47, 12, 83, 63、Q は 64, 27, 74, 40 で、どちらの合計も 205 です。差は 0 なので、これ以上よい分け方はありません。 すべての変数を反転しても、P と Q が入れ替わるだけの同じ分け方です。

詳しくは分割問題をご覧ください。

制約:割当問題

割当問題

▶ 動画の 19:18 から再生

4 人に 4 つの仕事を 1 つずつ割り当て、コストの合計を最小にします。表の数は、人 i が仕事 j をしたときのコストです。

  仕事 0 仕事 1 仕事 2 仕事 3
人 0 58 73 91 44
人 1 62 15 87 39
人 2 78 56 23 94
人 3 11 85 68 72

変数 x[i][j] が 1 なら、人 i に仕事 j を割り当てます(16 個)。 制約は、各行と各列の和がどれも 1 になることです。

2 次元の配列

▶ 動画の 20:17 から再生

shape に行の数と列の数を並べると、2 次元の配列になります。 qbpp.vector_sum は、axis=1 で各行の和、axis=0 で各列の和を並べた配列を作ります。

import pyqbpp as qbpp

x = qbpp.var("x", shape=(2, 3))
print(x)
print(qbpp.vector_sum(x, axis=1))
print(qbpp.vector_sum(x, axis=0))

出力:

[[x[0][0], x[0][1], x[0][2]], [x[1][0], x[1][1], x[1][2]]]
[x[0][0] +x[0][1] +x[0][2], x[1][0] +x[1][1] +x[1][2]]
[x[0][0] +x[1][0], x[0][1] +x[1][1], x[0][2] +x[1][2]]

詳しくは多次元変数をご覧ください。

ペナルティ式

▶ 動画の 21:29 から再生

コストの合計 $g$ は、コストの表 $c$ と変数 $x$ の要素ごとの積の和です。 各行・各列の和から 1 を引いて 2 乗したペナルティは、制約を満たすと 0、破ると正になります。 全体は $h = g + P \times (\text{すべての行と列のペナルティの和})$ で、重み $P$ が十分大きければ、制約を満たす解が $h$ の最小になります。

ペナルティ式で解く

▶ 動画の 22:21 から再生

重み $P$ を 1000 と 10 にして、16 個の変数のすべての組合せを調べます。

import pyqbpp as qbpp

c = qbpp.array([[58, 73, 91, 44],  # コストの表(4×4)
                [62, 15, 87, 39],
                [78, 56, 23, 94],
                [11, 85, 68, 72]])
x = qbpp.var("x", shape=(4, 4))
g = qbpp.sum(c * x)
f = (qbpp.sum(qbpp.sqr(qbpp.vector_sum(x, axis=1) - 1)) +
     qbpp.sum(qbpp.sqr(qbpp.vector_sum(x, axis=0) - 1)))

for P in [1000, 10]:
    h = g + P * f
    h.simplify_as_binary()
    sol = qbpp.ExhaustiveSolver(h).search()
    print(f"P = {P}: g = {sol(g)}, f = {sol(f)}")
    print(sol(x))

出力:

P = 1000: g = 93, f = 0
[[0, 0, 0, 1], [0, 1, 0, 0], [0, 0, 1, 0], [1, 0, 0, 0]]
P = 10: g = 26, f = 4
[[0, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 0], [1, 0, 0, 0]]

重みの大きさ

▶ 動画の 23:27 から再生

重みが 10 のときは 2 人にしか仕事が割り当てられず、空いた行と列が 4 つあるので、ペナルティは $4 \times 10 = 40$ です。 コストの 26 と合わせても 66 で、正しい割り当ての 93 より小さいので、ソルバーは制約を破った解を選びます。 重みは、制約を破ると損になる大きさにする必要があります。次の cons でも同じです。

詳しくは比較制約をご覧ください。

cons() で制約を宣言

cons()

▶ 動画の 24:17 から再生

制約の部分を qbpp.cons() で囲むと、制約として宣言されます。 配列の比較を囲むと、行ごと・列ごとに 1 本ずつ、この例では 8 本の制約になります。 値は 重み × 違反量$^2$ で、等式ではペナルティ式と同じです。ソルバーは、制約を満たす解を効率よく探します。

import pyqbpp as qbpp

c = qbpp.array([[58, 73, 91, 44],  # コストの表(4×4)
                [62, 15, 87, 39],
                [78, 56, 23, 94],
                [11, 85, 68, 72]])
x = qbpp.var("x", shape=(4, 4))
g = qbpp.sum(c * x)
h = g + 1000 * (qbpp.cons(qbpp.vector_sum(x, axis=1) == 1) +
                qbpp.cons(qbpp.vector_sum(x, axis=0) == 1))
h.simplify_as_binary()

sol = qbpp.EasySolver(h).search(time_limit=1)
print("result =", qbpp.onehot_to_int(sol(x), axis=1))
print("cost =", sol(g))
print("violated constraints =", h.cons(sol))

解を確かめる

▶ 動画の 25:34 から再生

動画での出力(EasySolver の結果は実行ごとに変わることがあります):

result = [3, 1, 2, 0]
cost = 93
violated constraints = 0

コストは 44 + 15 + 23 + 11 = 93 です。h.cons(sol) は違反している制約の本数で、0 なのですべての制約を満たしています。 qbpp.onehot_to_int(sol(x), axis=1) は、各行で 1 になっている列の番号を並べます。人 0 は仕事 3、人 1 は仕事 1 という割り当てです。

詳しくはワンホットから整数への変換をご覧ください。

ペナルティ式と cons() の違い

▶ 動画の 26:25 から再生

  ペナルティ式 cons()
書き方 sqr で 2 乗する 比較を cons() で囲む
等式の値 違反量$^2$ 違反量$^2$(同じ)
不等式 補助の変数が必要 補助の変数は要らない
違反の確認   h.cons(sol)

ソルバーは、cons() の制約を制約として特別に扱います。

詳しくは非線形関数とネイティブ制約をご覧ください。

整数変数

integer= と between=

▶ 動画の 27:03 から再生

qbpp.var に integer= で範囲を指定すると、整数の変数になります。 between= で作った a は 0/1 の変数の重み付きの和に展開され、integer= で作った b は展開されずに整数のまま扱われます。 範囲を 0〜100 に広げると a は 0/1 の変数 7 個になりますが、b は変数 1 つのままなので、個数や時刻などの量には integer= が向いています。

import pyqbpp as qbpp

a = qbpp.var("a", between=(0, 10))
b = qbpp.var("b", integer=(0, 10))
print("a =", a)
print("b =", b)

出力:

a = a[0] +2*a[1] +4*a[2] +3*a[3]
b = b

連立方程式

▶ 動画の 28:26 から再生

$x + 2y = 700$ と $x - y = 100$ を解きます。 それぞれの左辺と右辺の差を 2 乗して足した $f$ は、両方が成り立つときだけ 0 になります。 整数の変数では、simplify_as_binary を呼んでも $x^2$ は $x$ にならず、2 乗のまま扱われます。

import pyqbpp as qbpp

x = qbpp.var("x", integer=(0, 1000))
y = qbpp.var("y", integer=(-1000, 1000))
f = qbpp.sqr(x + 2 * y - 700) + qbpp.sqr(x - y - 100)
f.simplify_as_binary()

sol = qbpp.EasySolver(f).search(time_limit=1, target_energy=0)
print(f"x = {sol(x)}, y = {sol(y)}, f = {sol.energy}")

動画での出力(EasySolver の結果は実行ごとに変わることがあります):

x = 300, y = 200, f = 0

詳しくは整数変数をご覧ください。

不等式の制約

cons() で不等式

▶ 動画の 30:00 から再生

$2x + 3y \le 24$、$7x + 5y \le 54$ のもとで、$5x + 4y$ を最大にします。 ソルバーは最小化するので、符号を反転した $-f$ から始めます。不等式も、比較の式を qbpp.cons() で囲むだけです。

import pyqbpp as qbpp

x = qbpp.var("x", integer=(0, 10))
y = qbpp.var("y", integer=(0, 10))
f = 5 * x + 4 * y
g = -f + 100 * (qbpp.cons(2 * x + 3 * y <= 24) +
                qbpp.cons(7 * x + 5 * y <= 54))
g.simplify_as_binary()

sol = qbpp.EasySolver(g).search(time_limit=1)
print(f"x = {sol(x)}, y = {sol(y)}, f = {sol(f)}")
print("violated constraints =", g.cons(sol))

動画での出力(EasySolver の結果は実行ごとに変わることがあります):

x = 4, y = 5, f = 40
violated constraints = 0

詳しくは整数線形計画をご覧ください。

ペナルティ式との比較

▶ 動画の 31:35 から再生

同じ問題を、between= で作った x, y(0/1 の変数 4 個ずつ)で、ペナルティ式(qbpp.constrain)と qbpp.cons の両方で書いて比べます。 ペナルティ式では補助の変数が 9 個増えて変数 17 個・133 項になり、cons では変数 8 個・8 項のままです。 組合せの数は 256 通りから $2^{17} = 131072$ 通りに増え、探索する範囲が広いほど、よい解を見つけにくくなります。

import pyqbpp as qbpp

x = qbpp.var("x", between=(0, 10))  # 0/1 の変数 4 個
y = qbpp.var("y", between=(0, 10))  # 0/1 の変数 4 個
f = 5 * x + 4 * y

gp = -f + 100 * qbpp.constrain(2 * x + 3 * y, between=(0, 24))  # ペナルティ式
gp += 100 * qbpp.constrain(7 * x + 5 * y, between=(0, 54))
gp.simplify_as_binary()
sp = qbpp.EasySolver(gp).search(time_limit=1)

gc = -f + 100 * qbpp.cons(2 * x + 3 * y, between=(0, 24))       # 制約の宣言
gc += 100 * qbpp.cons(7 * x + 5 * y, between=(0, 54))
gc.simplify_as_binary()
sc = qbpp.EasySolver(gc).search(time_limit=1)

print(f"penalty: var_count = {sp.info['var_count']}, term_count = {sp.info['term_count']}, f = {sp(f)}")
print(f"cons:    var_count = {sc.info['var_count']}, term_count = {sc.info['term_count']}, f = {sc(f)}")

出力(f は EasySolver の結果で、実行ごとに変わることがあります):

penalty: var_count = 17, term_count = 133, f = 40
cons:    var_count = 8, term_count = 8, f = 40

ソルバーの使い分け

3 つのソルバー

▶ 動画の 32:51 から再生

ソルバー 説明
ExhaustiveSolver すべての組合せを調べ、最適解を保証(小規模向け)
EasySolver マルチコアの CPU で探索。まず試す
ABS3Solver GPU と CPU を使って並列に探索

3 つとも、solver = qbpp.EasySolver(f) のように $f$ を渡してソルバーを作り、sol = solver.search(target_energy=0) のように探索します。 クラス名を変えるだけで、ほかのソルバーも同じように使えます。target_energy などのパラメータも共通です。

大きな問題

▶ 動画の 33:44 から再生

200 個の数の分割問題です。組合せは $2^{200}$、およそ $10^{60}$ 通りで、すべてを調べることはできません。 sol.tts は、返された解が見つかるまでの時間です。

import random
import pyqbpp as qbpp

random.seed(1)
w = [random.randint(1, 100) for _ in range(200)]
x = qbpp.var("x", shape=len(w))
f = qbpp.sqr(qbpp.sum(w * x) - qbpp.sum(w * ~x))
f.simplify_as_binary()

solver = qbpp.EasySolver(f)     # または qbpp.ABS3Solver(f)
sol = solver.search(time_limit=10, target_energy=0)
print(f"f = {sol.energy}, TTS = {sol.tts:.3f} s")

動画での出力(TTS は実行ごとに変わります):

f = 0, TTS = 0.002 s

qbpp.ABS3Solver(f) に変えると GPU も使った探索になり、動画(RTX 3090 × 2)では f = 0, TTS = 0.001 s でした。 EasySolver と ABS3Solver は、よい解を短い時間で探しますが、最適であることの保証はありません。

詳しくはEasy SolverとABS3 Solverをご覧ください。

複数の解

▶ 動画の 35:16 から再生

topk_sols=4 にすると、エネルギーの小さい順に 4 つの解が得られます。s.energy は解のエネルギー、s(x) は x の値です。 エネルギー 0 の 2 つの解は、0 と 1 を入れ替えた同じ分け方です。

import pyqbpp as qbpp

w = [64, 27, 47, 74, 12, 83, 63, 40]
x = qbpp.var("x", shape=len(w))
f = qbpp.sqr(qbpp.sum(w * x) - qbpp.sum(w * ~x))
f.simplify_as_binary()

sol = qbpp.ExhaustiveSolver(f).search(topk_sols=4)
for s in sol.sols:
    print(s.energy, s(x))

出力:

0 [0, 0, 1, 0, 1, 1, 1, 0]
0 [1, 1, 0, 1, 0, 0, 0, 1]
4 [0, 0, 1, 1, 0, 1, 0, 0]
4 [1, 0, 1, 0, 1, 1, 0, 0]

詳しくは探索パラメータをご覧ください。

ソルバーの選び方

▶ 動画の 36:22 から再生

ソルバー 動くところ 向いている問題 最適の保証
ExhaustiveSolver CPU(GPU があれば GPU も) 変数が少ない問題 あり(すべて調べる)
EasySolver マルチコア CPU まず試す なし
ABS3Solver GPU + CPU(GPU がなければ CPU だけ) 難しい問題 なし

使い方は同じなので、クラス名を変えるだけで切り替えられます。

非線形関数

abs と max

▶ 動画の 37:13 から再生

qbpp.abs(f) は絶対値、qbpp.relu(f) は $\max(0, f)$、qbpp.max(f, g)・qbpp.min(f, g) は大きいほう・小さいほうです。 分割問題を、差の絶対値の最小化と、大きいほうの合計の最小化で解き直します。補助の変数や場合分けを、自分で書く必要はありません。

import pyqbpp as qbpp

w = [64, 27, 47, 74, 12, 83, 63, 40]
x = qbpp.var("x", shape=len(w))
p = qbpp.sum(w * x)
q = qbpp.sum(w * ~x)

f = qbpp.abs(p - q)    # 差の絶対値
f.simplify_as_binary()
sol = qbpp.ExhaustiveSolver(f).search()
print(f"abs: f = {sol.energy}, p = {sol(p)}, q = {sol(q)}")

g = qbpp.max(p, q)     # 大きいほうの合計
g.simplify_as_binary()
sol = qbpp.ExhaustiveSolver(g).search()
print(f"max: g = {sol.energy}, p = {sol(p)}, q = {sol(q)}")

出力:

abs: f = 0, p = 205, q = 205
max: g = 205, p = 205, q = 205

relu

▶ 動画の 38:36 から再生

利益 $4x + 7y$ を最大にしながら、作業量 $2x + 3y$ が 36 を超えた分にコストをかけます。 relu の 2 番目の引数を 2 にすると、超えた分の 2 乗になります。$x + y \le 12$ の制約は cons で加えます。

import pyqbpp as qbpp

x = qbpp.var("x", integer=(0, 20))
y = qbpp.var("y", integer=(0, 20))
profit = 4 * x + 7 * y
overtime = qbpp.relu(2 * x + 3 * y - 36, 2)
f = -profit + overtime + 100 * qbpp.cons(x + y <= 12)
f.simplify_as_binary()

sol = qbpp.ExhaustiveSolver(f).search()
print(f"x = {sol(x)}, y = {sol(y)}")
print(f"profit = {sol(profit)}, overtime = {sol(overtime)}")

出力:

x = 0, y = 12
profit = 84, overtime = 0

作業量はちょうど 36 で、しきい値を超えていません。

詳しくは非線形関数とネイティブ制約をご覧ください。

高次の項

3 次の項

▶ 動画の 39:44 から再生

QUBO は 2 次の式ですが、QUBO++ では 3 次以上の項もそのまま書け、内蔵のソルバーはそのまま解きます。 2 次の式しか受け取らないほかのソルバーに渡すときは、reduce で 2 次の式に直せます。

import pyqbpp as qbpp

y = qbpp.var("y", shape=4)
f = 2 * y[0] * y[1] * y[2] - 3 * y[1] * y[2] * y[3] + y[1]
f.simplify_as_binary()
print("f =", f)
sol = qbpp.ExhaustiveSolver(f).search()
print(sol)

出力:

f = y[1] +2*y[0]*y[1]*y[2] -3*y[1]*y[2]*y[3]
Sol(energy=-2, {y[0]: 0, y[1]: 1, y[2]: 1, y[3]: 1})

詳しくはHUBO の QUBO への変換をご覧ください。

否定リテラルの積

▶ 動画の 40:46 から再生

否定リテラルも、高次の項の中でそのまま使えます。 ~x のまま書けば 1 項ですが、$1 - x$ の形で展開すると、否定が 1 つ増えるたびに項の数が 2 倍になり、4 つでは 16 項です。

import pyqbpp as qbpp

x = qbpp.var("x", shape=4)
neg = ~x[0] * ~x[1] * ~x[2] * ~x[3]
expanded = qbpp.simplify((1 - x[0]) * (1 - x[1]) * (1 - x[2]) * (1 - x[3]))
print("neg      =", neg)
print("expanded =", expanded)

出力:

neg      = ~x[0]*~x[1]*~x[2]*~x[3]
expanded = 1 -x[0] -x[1] -x[2] -x[3] +x[0]*x[1] +x[0]*x[2] +x[0]*x[3] +x[1]*x[2] +x[1]*x[3] +x[2]*x[3] -x[0]*x[1]*x[2] -x[0]*x[1]*x[3] -x[0]*x[2]*x[3] -x[1]*x[2]*x[3] +x[0]*x[1]*x[2]*x[3]

詳しくは否定リテラルをご覧ください。

すべてを 1 つのモデルに

3 本の生産ライン

▶ 動画の 41:41 から再生

3 本の生産ラインで、合計 20 個を作ります。

条件 書き方
ライン i を使うか(使うと段取り費 5) use[i]:0/1 の変数
ライン i の生産量(0〜10 個) q[i]:integer=(0, 10)
6 個を超えた分に、2 倍の超過費 2 * qbpp.relu(q[i] - 6)
ライン 0 と 1 の生産量の偏り qbpp.abs(q[0] - q[1])
止めたラインでは作れない qbpp.cons(q[i] - 10 * use[i] <= 0)
合計はちょうど 20 個 qbpp.cons(qbpp.sum(q) == 20)
3 本とも止めると違約金 50 50 * ~use[0] * ~use[1] * ~use[2]

プログラムと結果

▶ 動画の 42:42 から再生

どの条件も、式の中にそのまま書いています。このモデルには、補助の変数が 1 つもありません。

import pyqbpp as qbpp

use = qbpp.var("use", shape=3)                 # 0/1 の変数
q = qbpp.var("q", shape=3, integer=(0, 10))    # 整数の変数
f = 0
for i in range(3):
    f += 5 * use[i]
    f += 2 * qbpp.relu(q[i] - 6)
    f += 100 * qbpp.cons(q[i] - 10 * use[i] <= 0)
f += qbpp.abs(q[0] - q[1])
f += 50 * ~use[0] * ~use[1] * ~use[2]
f += 100 * qbpp.cons(qbpp.sum(q) == 20)
f.simplify_as_binary()

sol = qbpp.ExhaustiveSolver(f).search()
print("use =", sol(use))
print("q   =", sol(q))
print("energy =", sol.energy)

出力:

use = [1, 1, 1]
q   = [7, 7, 6]
energy = 19

3 本とも使って 7 個、7 個、6 個を作ります。段取り費 15 と、ライン 0 と 1 が 1 個ずつ超える超過費 2 + 2 = 4 で、合計 19 です。

詳しくはPyQBPP の表現力をご覧ください。

ケーススタディ

▶ 動画の 43:58 から再生

ドキュメントには、QUBO++ で解いた例が、プログラムと解説つきで 40 以上載っています。 N クイーン、巡回セールスマン、配送計画、機械スケジューリング、ハーフトーン、シフトスケジューリングなど、どれもこの動画で紹介した書き方で作られています。

詳しくは活用事例をご覧ください。

まとめ

▶ 動画の 44:34 から再生

  • qbpp.var で変数を作ります。shape= で配列に、integer= で整数の変数になります。
  • たし算、ひき算、かけ算や qbpp.sqr、qbpp.sum で式を書き、simplify_as_binary で整理して、ソルバーで解きます。
  • ソルバーは ExhaustiveSolver・EasySolver・ABS3Solver の 3 種類で、どれも使い方は同じです。
  • 解は sol(x) で変数の値を読み、sol.energy で $f$ の値が分かります。f.cons(sol) で違反している制約の本数が分かります。
  • 制約は cons で、整数は integer= で、絶対値や最大値は関数で、そのまま書けます。

Back to top

Page last modified: 2026.10.30.

© 2026 中野浩嗣, 広島大学