名古屋出身ソフトウェアエンジニアのブログ

PuLP での論理和と論理積の線形化

公開:
更新:

前回の記事では補助変数を利用することで、論理包含のある一見すると非線形な制約を線形制約へ変形し、PuLP で整数制約問題を解くことができました。

現実の問題を扱う際には、「ある特定の状況が望ましくないので、この条件を満たす場合にのみペナルティを足そう」というモデリングを行いたくなることが容易に想像できます。

そこで、論理積の線形表現を PuLP で実現する抽象を導出してみました。論理和もついでにやっておきました。

二項論理和 (OR) の線形化

論理和の方が簡単なので、先にやります。考え方は真理値表をそのまま実現することです。

y=ab y = a \lor b を実現するために、次のように制約を置きます。

a,b,y{0,1} a, b, y \in \{0,1\} yaybya+b y \geq a \qquad y \geq b \qquad y \leq a + b

ya y \geq a yb y \geq b により a,ba,b どちらかが 11 であれば yy11 になります。 ya+b y \leq a + b a,ba,b がともに 00 のときに、 yy00 にするための制約です。

ya+b y \leq a + b の右辺の最大値は 22 ですが、 yy0011 なので 11 で止まります。

aabb制約から許される yy
000000=00 = 0 \lor 0
110011=10 = 1 \lor 0
001111=01 = 0 \lor 1
111111=11 = 1 \lor 1

このように制約付けることで、上真理値表のとおり唯一の yy が決まり y=ab y = a \lor b となります。

二項論理積 (AND) の線形化

OR より難しいですが、考え方は同じです。

a,b,z{0,1} a, b, z \in \{0,1\} zazbza+b1 z \leq a \qquad z \leq b \qquad z \geq a + b - 1

za z \leq a zb z \leq b で偽値があるときに 00 となるように上から抑え、そうでないときは za+b1 z \geq a + b - 1 によって 11 にします。

aabb制約から許される zz
000000=00 = 0 \land 0
110000=10 = 1 \land 0
001100=01 = 0 \land 1
111111=11 = 1 \land 1

これで z=ab z = a \land b となります。

定数 pp を決めて、目的関数に p×z p \times z を足してやれば、「 aabb両方 11 ならペナルティ pp 」を表現できます。

一般化

n 項の OR

ai{1,,n},y{0,1}i.yaiyi=1nai a_{i \in \{1,\dots,n\}}, y \in \{0,1\} \qquad \forall i .\: y \ge a_i \qquad y \le \sum_{i=1}^n a_i

n 項の AND

ai{1,,n},z{0,1}i.zaizi=1nai(n1) a_{i \in \{1,\dots,n\}}, z \in \{0,1\} \qquad \forall i .\: z \le a_i \qquad z \ge \sum_{i=1}^n a_i - (n - 1)

PuLP 実装サンプル

適当な最小化問題を作成してみました。 1 をとる変数を増やすとコストが減りますが、全部 1 だと大きなペナルティを食らう目的関数になっています。

import pulp as pl

prob = pl.LpProblem("DEMO", pl.LpMinimize)

# 既存の決定変数
a = pl.LpVariable("a", lowBound=0, upBound=1, cat="Binary")
b = pl.LpVariable("b", lowBound=0, upBound=1, cat="Binary")
c = pl.LpVariable("c", lowBound=0, upBound=1, cat="Binary")


# OR に条件付けするユーティリティ
def choreograph_or(prob, dest_var, *in_vars):
    for v in in_vars:
        prob += dest_var >= v
    prob += dest_var <= pl.lpSum(in_vars)

# AND に条件付けするユーティリティ
def choreograph_and(prob, dest_var, *in_vars):
    n = len(in_vars)
    for v in in_vars:
        prob += dest_var <= v
    prob += dest_var >= pl.lpSum(in_vars) - (n - 1)

# OR(a,b,c) を表す補助変数
y = pl.LpVariable("y", lowBound=0, upBound=1, cat="Binary")
choreograph_or(prob, y, a, b, c)

# AND(a,b,c) を表す補助変数
z = pl.LpVariable("z", lowBound=0, upBound=1, cat="Binary")
choreograph_and(prob, z, a, b, c)

# 目的関数(真なる変数を増やすとコストが減るが、全部真だと大きなペナルティを食らう)
prob += 5 * z - 2 * y - a - b - c


prob.solve()

print(
    f"{pl.value(a)=}",
    f"{pl.value(b)=}",
    f"{pl.value(c)=}",
    f"{pl.value(y)=}",
    f"{pl.value(z)=}",
    f"{pl.LpStatus[prob.status]=}",
    f"{pl.value(prob.objective)=}",
    sep="\n",
)

出力

pl.value(a)=0.0
pl.value(b)=1.0
pl.value(c)=1.0
pl.value(y)=1.0
pl.value(z)=0.0
pl.LpStatus[prob.status]='Optimal'
pl.value(prob.objective)=-4.0

AND が機能し a, b, c すべてを 1 にしないよう最適化されたことが分かります。

SCIP の場合

ところで SCIP では、AND/OR/XOR 制約などをそのまま表現でき、これらの制約に対応する効率的な最適化メソッドが用意されています1

model.addConsAnd([a, b, c], z)

しかし、目的関数を非線形にすることは SCIP でもできない2ので、補助変数にコンディションを表現させて係数をかけるというやり方は SCIP においても使います。


  1. https://pyscipopt.readthedocs.io/en/latest/tutorials/constypes.html#constraint-types ↩︎

  2. https://pyscipopt.readthedocs.io/en/latest/faq.html#why-can-i-not-add-a-non-linear-objective

    Why can I not add a non-linear objective?

    SCIP does not support non-linear objectives. However, an equivalent optimization problem can easily be constructed by introducing a single new variable and a constraint. Please see this page for a guide.

     ↩︎