いい感じに区分線形近似する
こんにちは,新卒1年目でブレインパッドでデータサイエンティストをしています,H_A_ustです.
この記事は数理最適化アドベントカレンダーの20日目の記事です.
はじめに
機械学習を行った後にその予測器を混合整数計画問題として定式化する際に,区分線形近似を使う状況はよくあるかと思います.
一方で非線形関数を愚直に一定間隔で離散化してしまうと間数値の変化が緩やかな区間においても過剰に点をとってしまい無駄が生じてしまうことがしばしばあります.
例えば
今度は離散点を対数的に等間隔にとってみると以下のように「いい感じ」に区分線形近似できていることが分かります.
本記事ではある程度性質の良い非線形関数に対して,この様な効率的な離散近似点を選ぶ方法について説明します.
また,本記事はGurobiのFunction Constraintsの解説を参考にして裏側のアルゴリズムを想像して記事を書いています.より良い方法などをご存知でしたら教えていただけると幸いです.
問題設定
前述ではゆるく「いい感じ」に区分線形近似すると言いましたが,よりフォーマルにこの「いい感じ」を定義します.
まず,区分線形近似したい対象の関数を
また区分線形近似する際の区間の最小値,最大値を
さらに近似点
で表されるものとします.
この時,真の関数
と関数の差の最大値ノルムで表すことにします.
ここで最大許容誤差
この他にも
アルゴリズム
アルゴリズムを適用する上で
まず近似対象区間における
例として,近似対象区間で変曲点を1個持つような非線形関数に対する初期関数
さらに
ここで,各区間に対して
これらの仮定がなくても数値計算でこれらの値を求めることができる気もしますが,とりあえず簡単のために以下では仮定した上で話を進めます.
以上の設定のもとで初期の区分線形関数を
ここで各区間
その際の離散点の取り方として
ここでどの様にして
として与えられます.
以下に導出の概要を示します.
以下では凹関数を仮定します.(凸関数だとしても符号を入れ替えて同様の議論ができます)
すると考えるべき問題は
となります.
従って
を満たす
以上のアルゴリズムをまとめると以下のようになります.
-
,U_0 = \{[L,c_1], [c_1,c_2],...,[c_{m-1}, c_m], [c_m,R]\} とするi=0 -
の分割に対してU_i かどうかを確認する,満たすならば終了d(f, \bar f) \le \text{tol} - 許容誤差を満たさない区間
に対してA=[r,l] \in U_i を計算してp^* U_{i+1} = U_i \setminus A \cup [l,p^*] \cup [p^*,r]
として2.に戻るi\leftarrow i+1
実装と数値実験
はじめにPythonによる実装について説明します.
まず,近似対象の関数
class AbstractFunction(ABC):
@abstractmethod
def __call__(self, x):
"""
Apply the function to the input x.
:param x: The input to the function.
:return: The result of applying the function to x.
"""
pass
@abstractmethod
def df(self, x):
"""
Compute the derivative of the function at x.
:param x: The point at which to compute the derivative.
:return: The derivative of the function at x.
"""
pass
@abstractmethod
def df_inv(self, g, x):
"""
Compute the inverse derivative of the function at x.
:param g: The gradient at which to compute the inverse derivative.
:param x: The position at which to compute the inverse derivative.
:return: The inverse derivative of the function at x.
"""
pass
この関数と最大許容誤差
def make_pwl_pts(function: AbstractFunction, tol=1e-2):
f = function
x_init = list(set([f.lb, *f.infrection_pts, f.ub])) # 最初は境界と変曲点から探索を始める
def split_pts(lb, ub):
xs = []
grad = (f(ub) - f(lb)) / (ub - lb)
def f_pwl(x): return grad*(x-lb) + f(lb)
c = f.df_inv(grad, (lb+ub)/2)
err = np.abs(f(c) - f_pwl(c))
if err > tol:
xs.append(c)
xs.extend(split_pts(lb, c))
xs.extend(split_pts(c, ub))
return xs
inner_pts = []
for i in range(len(x_init)-1):
inner_pts.extend(split_pts(x_init[i], x_init[i+1])) # tolを満たすまで分割する
ret = np.array(sorted(x_init + inner_pts))
return ret
関数では区間の分割を再帰関数を用いて行っています.
もし近似する点
上の関数をもとに冒頭の
期待通り関数値の変化が激しい左端で離散点が多く取られていることが見て取れます.
おわりに
本記事ではGurobiに実装されているFunction制約のアルゴリズムについて考察しました.
Gurobiのドキュメントを参考に考えたアルゴリズムなのでもっと効率的な方法をご存知でしたら教えていただけると幸いです.
Discussion