次の例は、二分法を使用して、関数x **(a + 1)-b(すべて異なるaとb)の100万コピーの根を並列に計算する方法を示しています。ここでは約12秒かかります。
import numpy
def F(x, a, b):
return numpy.power(x, a+1.0) - b
N = 1000000
a = numpy.random.rand(N)
b = numpy.random.rand(N)
x0 = numpy.zeros(N)
x1 = numpy.ones(N) * 1000.0
max_step = 100
for step in range(max_step):
x_mid = (x0 + x1)/2.0
F0 = F(x0, a, b)
F1 = F(x1, a, b)
F_mid = F(x_mid, a, b)
x0 = numpy.where( numpy.sign(F_mid) == numpy.sign(F0), x_mid, x0 )
x1 = numpy.where( numpy.sign(F_mid) == numpy.sign(F1), x_mid, x1 )
error_max = numpy.amax(numpy.abs(x1 - x0))
print "step=%d error max=%f" % (step, error_max)
if error_max < 1e-6: break
基本的な考え方は、変数のベクトルと個々のコンポーネント関数を定義するパラメーターの同等のベクトルで評価できる関数を使用して、変数のベクトルでルートファインダーの通常のすべてのステップを並列に実行することです。条件文は、マスクとnumpy.where()の組み合わせに置き換えられます。これは、すべてのルートが必要な精度で見つかるまで、または問題からそれらを削除してそれらのルートを除外する小さな問題を続行する価値がある十分なルートが見つかるまで続けることができます。
私が解決するために選択した関数は任意ですが、関数が正常に動作している場合に役立ちます。この場合、ファミリ内のすべての関数は単調であり、正の根が1つだけあります。さらに、二分法では、関数のさまざまな符号を与える変数を推測する必要があります。これらは、ここでも非常に簡単に思い付くことができます(x0とx1の初期値)。
上記のコードはおそらく最も単純な求根アルゴリズム(二分法)を使用していますが、同じ手法をニュートンラプソン法やリダー法などに簡単に適用できます。求根法の条件が少ないほど、これに適しています。ただし、必要なアルゴリズムを再実装する必要があります。既存のライブラリルートファインダー関数を直接使用する方法はありません。
上記のコードスニペットは、速度ではなく明確さを念頭に置いて書かれています。一部の計算の繰り返しを回避し、特に関数を3回ではなく、反復ごとに1回だけ評価すると、次のように最大9秒高速化されます。
...
F0 = F(x0, a, b)
F1 = F(x1, a, b)
max_step = 100
for step in range(max_step):
x_mid = (x0 + x1)/2.0
F_mid = F(x_mid, a, b)
mask0 = numpy.sign(F_mid) == numpy.sign(F0)
mask1 = numpy.sign(F_mid) == numpy.sign(F1)
x0 = numpy.where( mask0, x_mid, x0 )
x1 = numpy.where( mask1, x_mid, x1 )
F0 = numpy.where( mask0, F_mid, F0 )
F1 = numpy.where( mask1, F_mid, F1 )
...
比較のために、scipy.bisect()を使用して一度に1つのルートを見つけるには、約94秒かかります。
for i in range(N):
x_root = scipy.optimize.bisect(lambda x: F(x, a[i], b[i]), x0[i], x1[i], xtol=1e-6)