【python cvxpy】モデリング言語cvxpyを利用して数理最適化問題を解く
【はじめに】
これまで「数理最適化問題」を解く際に「ソルバーに渡すモデリング言語」として「python-MIP」や「PuLP」の使い方を取り上げてきた。
今回は「モデリング言語:cvxpy」を取り上げる
【cvxpyの特徴について】
「モデリング言語:cvxpy」の特徴を簡単に言うと、
というもの。
早速、例題を通して使い方をざっと見ていく。
【例題】:制約条件付きの最小二乗法(least squares problem with box constraints)
例題には「公式サイトトップにある例」を「もう少し簡単にしたもの」を使ってみる。
■やりたいことのイメージ図
つまりは、
※今回作りたい近似式
$${input_0*x_0 + input_1*x_1 + input_2*x_2=output}$$
▲この$${x_0}$$,$${x_1}$$,$${x_2}$$をいい感じに決めたい。
【解答例】
実行環境は「Google Colab」。
【1】cvxpyのインストール
!pip install cvxpy
!pip install --upgrade cvxpy
■バージョンや対応ソルバ情報を確認してみる
import cvxpy as cp
# バージョン確認
print(cp.__version__)
# 定義されているソルバーを出力してみる(settings.pyより)
print(cp.settings.SOLVERS)
■インストール済みのソルバーを確認する
cvxpyと一緒にインストールされるソルバーを確認してみる
print( cp.installed_solvers() )
【2】サンプルデータの用意
例題の連立方程式は、次のような行列式で表現することができる。
これをふまえて、A,x,bそれぞれにわけて、必要なサンプルデータを用意する。
■行列Aと行列bのデータを用意
import numpy as np
A = np.array( [
[ 1.62434536, -0.61175641, -0.52817175],
[-1.07296862, 0.86540763, -2.3015387 ],
[ 1.74481176, -0.7612069 , 0.3190391 ],
[-0.24937038, 1.46210794, -2.06014071],
[-0.3224172 , -0.38405435, 1.13376944],
[-1.09989127, -0.17242821, -0.87785842]
])
b = np.array(
[ 0.04221375, 0.58281521, -1.10061918, 1.14472371, 0.90159072, 0.50249434 ]
)
【3】変数xの定義
「cvxpy」では「Variable()」をつかって変数を定義する。
n = len(A[0]) # 用意する変数の数
x = cp.Variable(shape=(n,))
print(x)
【4】目的関数
繰り返しになるが今回の問題は
というもの。
これを実現するために
※残差平方和(RSS:residual sum of squares)のイメージ
▲なお、この考え方の場合、「全体として0に近づけるので、どこかの式に大きく乖離が出てしまう可能性はある」が、今回は気にしない。
■Minimize()とsum_squares()
「cvxpy」では「Minimize()」と「sum_squares()」を組み合わせることで「残差平方和:RSSの最小化」を表現できる。
※Minimize()
※sum_squares()
■目的関数を作成
#objective = cp.Minimize(cp.sum_squares(A @ x - b)) #「@」オペレータ(ver3.5以降)の場合
objective = cp.Minimize(cp.sum_squares(np.matmul(A,x) - b)) # 行列演算
print(objective)
※cvxpyの特徴
この「目的関数」の記述の仕方のように、cvxpyでは数式の記述に近い表現のままプログラムを書けることが特徴の一つである。
※補足:行列の掛け算について
行列の掛け算については
等を使うことができる。
【5】制約条件
今回は
を制約条件として用意してみる。
■制約条件の用意
constraints = [0 <= x, x <= 1]
print(constraints)
▲先ほど「定義した変数」に対して、「設定したい制約条件」を記述し「list化」すればよい。
なお、公式ドキュメントにもある通り、
【6】「Problemオブジェクト」に「目的関数」と「制約条件」を設定
「cvxpy」では「Problemオブジェクト」に「目的関数」と「制約条件」を設定してsolverに渡すことで、問題を解かせることができる。
※Problemオブジェクト
■Problemオブジェクトに目的関数と制約条件を設定する
prob = cp.Problem(objective, constraints)
print(prob)
【7】解を求める
「Problem.solve()」をコールして解を求める。
なお、「cvxpy」はソルバー指定がなければ、数式から裏でいい感じにソルバーを選んで計算をする。
■ソルバーを起動して問題を解く
今回は「verbose=True」として詳細な情報を出力しながら問題を解いている。
prob.solve(verbose=True)
※使用したソルバーや計算結果だけを知りたい場合
# 計算に使用したソルバーを確認
print( prob.solver_stats.solver_name )
# 計算結果を確認
print( prob.status )
▲ status として「optimal」が出力されているので解が計算できている
※「solve()」後に保持している「status」について
「solve()」コール後、計算できたかどうかの結果は「Problem.status」に格納されている。
用意されている「Status」は次の通り。(ver. 1.2.0時点)
【8】結果の確認
定義した変数の計算結果は「Variable.value」に格納されている。
■目的関数の最小値:ソルバーが計算した「RSS(残差平方和)」の最小値
print(prob.value)
■定義した変数に対して計算した最適値を出力する
print(x.value)
▲問題に対する答えとしては
(x1,x2,x3)=(-1.12948270e-21, 6.36487459e-01, 3.26185025e-22)
となる。
つまり、以下のような感じに近似式が見積もられた、ということ。
※おまけ1:近似式に6つの方程式の係数(input相当)の値を入れてみる
▲見積もられた近似式の計算では、方程式1はもともとの値にも近い結果だが、方程式4は乖離が一番大きい。
しかし、6つの方程式全体としては、乖離(差の合計)が小さくされている。
※特定の方程式だけの差が大きいなど、「偏り」が気になる場合、例えば、『「目的関数」に「分散」を組み込んで調整する』といった方法もある。(より良い手法はその界隈の専門家に聞いてみるのがよい)
※おまけ2:制約条件の充足具合について
今回の制約条件は次の通りであった。
これをふまえると、
「1つ目の変数の計算結果:-1.12948270e-21」は「負の値」なので、計算間違いではないのか?
と疑問に思うかもしれない。
これは「cvxpy」の「制約条件」の「誤差の許容範囲の精度」をデフォルトで「1e-08(10のマイナス8乗:0.00000001)」としているためである。
※イメージ図
■制約条件の充足具合を確認する
print(prob.constraints[0].value(tolerance= 1e-8)) # 0≦xの制約
print(prob.constraints[1].value()) # x≦1の制約
【9】全体コード
最後に全体コードを示しておく。
・実行環境:Google Colab
■インストール
!pip install cvxpy
!pip install --upgrade cvxpy
■全体コード
import numpy as np
import cvxpy as cp
# バージョン確認
print(cp.__version__)
# 定義されているソルバーを出力してみる(settings.pyより)
print(cp.settings.SOLVERS)
# インストール済みのソルバの確認
print( cp.installed_solvers() )
A = np.array( [
[ 1.62434536, -0.61175641, -0.52817175],
[-1.07296862, 0.86540763, -2.3015387 ],
[ 1.74481176, -0.7612069 , 0.3190391 ],
[-0.24937038, 1.46210794, -2.06014071],
[-0.3224172 , -0.38405435, 1.13376944],
[-1.09989127, -0.17242821, -0.87785842]
])
b = np.array(
[ 0.04221375, 0.58281521, -1.10061918, 1.14472371, 0.90159072, 0.50249434 ]
)
n = len(A[0]) # 用意する変数の数
x = cp.Variable(shape=(n,))
print(x)
#objective = cp.Minimize(cp.sum_squares(A @ x - b)) #「@」オペレータ(ver3.5以降)の場合
objective = cp.Minimize(cp.sum_squares(np.matmul(A,x) - b)) # 行列演算
print(objective)
constraints = [0 <= x, x <= 1]
print(constraints)
prob = cp.Problem(objective, constraints)
print(prob)
prob.solve(verbose=True)
# 計算に使用したソルバーを確認
print( prob.solver_stats.solver_name )
# 計算結果を確認
print( prob.status ) # status
print(prob.value) # 目的関数の最小値
print(x.value) # 計算により決定された変数の値
# 制約条件の充足状況
print(prob.constraints[0].value(tolerance= 1e-8)) # 0≦xの制約
print(prob.constraints[1].value()) # x≦1の制約
■見積もった近似式に6つの係数(input相当)を入れて可視化するコード
%matplotlib inline
import matplotlib.pyplot as plt
def calc_output(in1, in2, in3):
return x.value[0]*in1+ x.value[1]*in2+ x.value[2]*in3
fig, ax = plt.subplots()
axis_x = np.arange(len(A))
calc_y = [calc_output(*data) for data in A]
ax.scatter(x=axis_x, y = b)
ax.scatter(x=axis_x, y = calc_y)
【実行結果例】