web-dev-qa-db-ja.com

スパース行列を使用したLCP

行列は大文字で、ベクトルは小文字で示します。

ベクトルvの線形不等式の次のシステムを解く必要があります。

_min(rv - (u + Av), v - s) = 0
_

ここで、_0_はゼロのベクトルです。

ここで、rはスカラー、usはベクトル、Aは行列です。

_z = v-s_、_B=rI - A_、_q=-u + Bs_を定義すると、前の問題を 線形相補性問題 として書き直すことができ、たとえばopenoptからLCPソルバーを使用したいと考えています。

_LCP(M, z): min(Bz+q, z) = 0
_

または、行列表記の場合:

_z'(Bz+q) = 0
z >= 0
Bz + q >= 0
_

問題は、私の連立方程式が巨大であるということです。 Aを作成するには、

  • _A11_を使用して、_A12_、_A21_、_A22_、_scipy.sparse.diags_の4つの行列を作成します。
  • そして、それらをA = scipy.sparse.bmat([[A11, A12], [A21, A22]])としてスタックします。
  • (これは、Aが対称ではないため、QP問題への効率的な変換が機能しないことも意味します)

_openopt.LCP_は、スパース行列を処理できないようです。これを実行すると、コンピューターがクラッシュしました。通常、A.todense()はメモリエラーにつながります。同様に、_compecon-python_は、スパース行列のLCP問題を解決できません。

この問題に適した代替のLCP実装は何ですか?


一般的な「LCPを解決するためのツール」の質問にサンプルデータが必要だとは本当に思いませんでしたが、とにかく、ここに行きます

_from numpy.random import Rand
from scipy import sparse

n = 3000
r = 0.03

A = sparse.diags([-Rand(n)], [0])
s = Rand(n,).reshape((-1, 1))
u = Rand(n,).reshape((-1, 1))

B = sparse.eye(n)*r - A
q = -u + B.dot(s)

q.shape
Out[37]: (3000, 1)
B
Out[38]: 
<3000x3000 sparse matrix of type '<class 'numpy.float64'>'
    with 3000 stored elements in Compressed Sparse Row format>
_

さらにいくつかのポインタ:

  • _openopt.LCP_が行列でクラッシュし、続行する前に行列を密に変換すると思います
  • _compecon-python_はエラーで完全に失敗します。明らかに密な行列が必要であり、スパース性のフォールバックはありません。
  • Bは正の半確定ではないため、線形相補性問題(LCP)を凸2次問題(QP)と言い換えることはできません。
  • この説明 からのすべてのQPスパースソルバーは、問題が凸である必要がありますが、私のものはそうではありません
  • Juliaでは、 PATHSolver で問題を解決できます(ライセンスがあれば)。ただし、Python with PyJulia私の問題レポートはこちら )から呼び出すと問題が発生します
  • また、Matlabにはスパース行列を処理できると思われるLCPソルバーがありますが、実装はさらに奇抜です(そして、このためにMatlabにフォールバックしたくありません)。
53
FooBar

この問題には非常に効率的な(線形時間)ソリューションがありますが、少し議論が必要です...

ゼロス:問題の明確化/ LCP

コメントの説明によると、@ FooBarは元の問題は要素ごとにminであると述べています。次のようなz(またはv)を見つける必要があります。

左の引数がゼロで右の引数が非負であるか、左の引数が非負で右の引数がゼロである

@FooBarが正しく指摘しているように、有効な再パラメーター化はLCPにつながります。ただし、以下では、LCPを必要とせずに、元の問題に対するより簡単で効率的な解決策を(元の問題の構造を利用することによって)達成できることを示します。なぜこれが簡単なのですか?さて、LCPにはz(Bz + q) 'zに二次項がありますが、元の問題にはないことに注意してください(線形項Bz + qとzのみ)。以下でその事実を利用します。

最初:単純化

この問題を大幅に簡素化する重要ですが重要な詳細があります。

  • Scipy.sparse.diagsを使用して、4つの行列A11、A12、A21、A22を作成します
  • そして、それらをA = scipy.sparse.bmat([[A11、A12]、[A21、A22]])としてスタックします。

これには大きな影響があります。具体的には、これはsinglelargeの問題ではなく、large number of really small(2D、to正確に)問題。このA行列のブロック対角構造は、後続のすべての操作を通じて保持されることに注意してください。そして、その後のすべての演算は、行列-ベクトル乗算または内積です。これは、実際にはこのプログラムがz(またはv)変数のペアでseparableであることを意味します。

具体的には、各ブロックA11,...のサイズがn×nであるとします。次に、z_1z_{n+1}が方程式と項でのみ表示され、他の場所では表示されないことに注意してください。したがって、問題はn問題に分離可能であり、各問題は2次元です。したがって、今後2D問題を解決し、疎行列や大きなoptパッケージを使用せずに、適切と思われるnを介してシリアル化または並列化できます。

2番目:2D問題のジオメトリ

2Dに要素ごとの問題があると仮定します。つまり、次のようになります。

find z such that (elementwise) min( Bz + q , z ) = 0, or declare that no such `z` exists.

セットアップではBが2x2になっているため、この問題のジオメトリは、手動で列挙できる4つのスカラー不等式に対応します(私はそれらにa1、a2、z1、z2という名前を付けました)。

“a1”: B11*z1 + B12*z2 + q1 >=0
“a2”: B21*z1 + B22*z2 + q2 >=0
“z1”: z1 >= 0
“z2:” z2 >= 0

これは、(おそらく空の)多面体、つまり2次元空間の4つの半空間の交点を表します。

番目:2D問題の解決

(編集:わかりやすくするためにこのビットを更新しました)

では、2Dの問題は具体的に何ですか?次の解決策のいずれかが達成されるzを見つけたいと思います(すべてが異なるわけではありませんが、それは問題ではありません)。

  1. a1> = 0、z1 = 0、a2> = 0、z2 = 0
  2. a1 = 0、z1> = 0、a2 = 0、z2> = 0
  3. a1> = 0、z1 = 0、a2 = 0、z2> = 0
  4. a1 = 0、z1> = 0、a2> = 0、z2 = 0

これらのいずれかが達成された場合、解決策があります。zおよびBz + qの要素ごとの最小値が0ベクトルであるz。 4つのうちどれも達成できない場合、問題は実行不可能であり、そのようなzは存在しないと宣言します。

最初のケースは、a1、a2の交点が正の象限にある場合に発生します。解z = B ^ -1qを選択し、それが要素ごとに非負であるかどうかを確認するだけです。そうである場合は、B^-1qを返します(Bはpsdではありませんが、反転可能/フルランクであると想定していることに注意してください)。そう:

if B^-1q is elementwise nonnegative, return z = B^-1q.

2番目のケース(最初のケースと完全に区別されるわけではありません)は、a1、a2の交点がどこかにあるが、原点が含まれている場合に発生します。言い換えると、z = 0に対してBz + q> 0の場合は常に。これは、qが要素的に正の場合に発生します。そう:

Elif q is elementwise nonnegative, return z = 0.

3番目のケースにはz1 = 0があり、これをa2に代入して、z2 = -q2/B22のときにa2 = 0であることを示すことができます。 z2は> = 0でなければならないため、-q2/B22> = 0です。 a1も> = 0でなければならず、これをz1とz2の値に代入すると、-B22/B12 * q2 + q1> = 0になります。そう:

Elif -q2/B22 >=0 and  -B22/B12*q2 + q1 >=0, return z1= 0, z2 = -q2/B22.

4番目のステップは3番目のステップと同じですが、1と2を入れ替えます。

Elif -q1/B11 >=0 and -B21/B11*q1 + q2 >=0, return z1 = -q1/B11, z2 =0.

最後の5番目のケースは、問題が実行不可能な場合です。これは、上記の条件のいずれも満たされない場合に発生します。そう:

else return None 

最後に:ピースをまとめる

各2D問題の解決は、いくつかの単純/効率的/自明な線形解決と比較です。それぞれが数値のペアまたはNoneを返します。次に、すべてのn 2D問題に対して同じことを行い、ベクトルを連結します。いずれかが「なし」の場合、問題は実行不可能です(すべてなし)。そうでなければ、あなたはあなたの答えを持っています。

5
muskrat

Python AMPLPYに基づく)のLCPソルバー

@denfromufaが指摘したように、AMPLソルバーへのPATHインターフェースがあります。彼はPyomoを提案しました。これはオープンソースであり、AMPLを処理できるからです。ただし、Pyomoは処理が遅く、面倒であることが判明しました。私は最終的にcythonのPATHソルバーへの独自のインターフェースを作成し、いつかそれをリリースしたいと思っていますが、現時点では入力の衛生状態がなく、迅速で汚れており、それに取り組んでいます。

今のところ、AMPLのpython拡張子を使用する答えを共有できます。PATHへの直接インターフェースほど高速ではありません。すべてのLCP解決したいのですが、(一時的な)モデルファイルを作成し、AMPLを実行して、出力を収集します。やや速くて汚いですが、少なくとも結果の一部を報告する必要があると感じました。この質問をしてからの過去数ヶ月の。

import os
# PATH license string needs to be updated
os.environ['PATH_LICENSE_STRING'] = "3413119131&Courtesy&&&USR&54784&12_1_2016&1000&PATH&GEN&31_12_2017&0_0_0&5000&0_0"


from amplpy import AMPL, Environment, dataframe
import numpy as np
from scipy import sparse
from tempfile import mkstemp
import os

import sys
import contextlib

class DummyFile(object):
    def write(self, x): pass

@contextlib.contextmanager
def nostdout():
    save_stdout = sys.stdout
    sys.stdout = DummyFile()
    yield
    sys.stdout = save_stdout


class modFile:
    # context manager: create temporary file, insert model data, and supply file name
    # apparently, amplpy coders are inable to support StringIO

    content = """
        set Rn;


        param B {Rn,Rn} default 0;
        param q {Rn} default 0;

        var x {j in Rn};

        s.t. f {i in Rn}:
                0 <= x[i]
             complements
                sum {j in Rn} B[i,j]*x[j]
                 >= -q[i];
    """

    def __init__(self):
        self.fd = None
        self.temp_path = None

    def __enter__(self):
        fd, temp_path = mkstemp()
        file = open(temp_path, 'r+')
        file.write(self.content)
        file.close()

        self.fd = fd
        self.temp_path = temp_path
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        os.close(self.fd)
        os.remove(self.temp_path)


def solveLCP(B, q, x=None, env=None, binaryDirectory=None, pathOptions={'logfile':'logpath.tmp' }, verbose=False):
    # x: initial guess
    if binaryDirectory is not None:
        env = Environment(binaryDirectory='/home/foo/amplide.linux64/')
    if verbose:
        pathOptions['output'] = 'yes'
    ampl = AMPL(environment=env)

    # read model
    with modFile() as mod:
        ampl.read(mod.temp_path)

    n = len(q)
    # prepare dataframes
    dfQ = dataframe.DataFrame('Rn', 'c')
    for i in np.arange(n):
        # shitty amplpy does not support np.float
        dfQ.addRow(int(i)+1, np.float(q[i]))

    dfB = dataframe.DataFrame(('RnRow', 'RnCol'), 'val')

    if sparse.issparse(B):
        if not isinstance(B, sparse.lil_matrix):
            B = B.tolil()
        dfB.setValues({
            (i+1, j+1): B.data[i][jPos]
            for i, row in enumerate(B.rows)
            for jPos, j in enumerate(row)
            })

    else:
        r = np.arange(n) + 1
        Rrow, Rcol = np.meshgrid(r, r, indexing='ij')
        #dfB.setColumn('RnRow', [np.float(x) for x in Rrow.reshape((-1), order='C')])
        dfB.setColumn('RnRow', list(Rrow.reshape((-1), order='C').astype(float)))
        dfB.setColumn('RnCol', list(Rcol.reshape((-1), order='C').astype(float)))
        dfB.setColumn('val', list(B.reshape((-1), order='C').astype(float)))

    # set values
    ampl.getSet('Rn').setValues([int(x) for x in np.arange(n, dtype=int)+1])
    if x is not None:
        dfX = dataframe.DataFrame('Rn', 'x')
        for i in np.arange(n):
            # shitty amplpy does not support np.float
            dfX.addRow(int(i)+1, np.float(x[i]))
        ampl.getVariable('x').setValues(dfX)

    ampl.getParameter('q').setValues(dfQ)
    ampl.getParameter('B').setValues(dfB)

    # solve
    ampl.setOption('solver', 'pathampl')

    pathOptions = ['{}={}'.format(key, val) for key, val in pathOptions.items()]
    ampl.setOption('path_options', ' '.join(pathOptions))
    if verbose:
        ampl.solve()
    else:
        with nostdout():
            ampl.solve()

    if False:
        bD = ampl.getParameter('B').getValues().toDict()
        qD = ampl.getParameter('q').getValues().toDict()
        xD = ampl.getVariable('x').getValues().toDict()
        BB = ampl.getParameter('B').getValues().toPandas().values.reshape((n, n,), order='C')
        qq = ampl.getParameter('q').getValues().toPandas().values[:, 0]
        xx = ampl.getVariable('x').getValues().toPandas().values[:, 0]
        ineq2 = BB.dot(xx) + qq
        print((xx * ineq2).min(), (xx * ineq2).max() )
    return ampl.getVariable('x').getValues().toPandas().values[:, 0]


if __== '__main__':

    # solve problem from the Julia port at https://github.com/chkwon/PATHSolver.jl
    n = 4
    B = np.array([[0, 0, -1, -1], [0, 0, 1, -2], [1, -1, 2, -2], [1, 2, -2, 4]])
    q = np.array([2, 2, -2, -6])

    BSparse = sparse.lil_matrix(B)

    env = Environment(binaryDirectory='/home/foo/amplide.linux64/')
    print(solveLCP(B, q, env=env))
    print(solveLCP(BSparse, q, env=env))

    # to test licensing
    from numpy import random
    n = 1000
    B = np.diag((random.randn(n)))
    q = np.ones((n,))
    print(solveLCP(B, q, env=env))
0
FooBar