web-dev-qa-db-ja.com

Python:ループしているnumpy数学関数を書き換えてGPUで実行する

誰かがこの1つの関数doTheMath関数)を書き換えてGPUで計算を行うのを手伝ってくれませんか?私は数日を使って頭を動かそうとしましたが、結果はありませんでした。私が最後に同じ結果を出すので、誰かがこの関数をログとして適合しているように見える方法で書き直すのを手伝ってくれるかもしれません。 numbaから_@jit_を使用しようとしましたが、何らかの理由で、通常のようにコードを実行するよりも実際にはかなり遅くなります。巨大なサンプルサイズの場合、目標は実行時間を大幅に短縮することなので、当然、GPUがそれを実行する最速の方法だと思います。

実際に起こっていることを少し説明します。実際のデータは、以下のコードで作成されたサンプルデータとほぼ同じに見えますが、サンプルごとに約5.000.000行、またはファイルごとに約150MBのサンプルサイズに分割されます。合計で約600.000.000行または20GBのデータがあります。このデータをサンプルごとにループし、各サンプルの行ごとにループし、各行の最後の2000(または別の)行を取得して、結果を返すdoTheMath関数を実行する必要があります。その後、その結果はハードドライブに保存され、別のプログラムで他のことを実行できます。以下に示すように、すべての行のすべての結果は必要ありません。特定の量よりも大きい結果のみが必要です。 python=で現在のように関数を実行すると、1.000.000行あたり約62秒になります。これは、すべてのデータと処理速度を考慮すると非常に長い時間です。

実際のデータファイルをdata = joblib.load(file)を使用してファイルごとにRAM=)にアップロードするので、約0.29秒しかかからないため、データのアップロードは問題ではありません。ファイルごとにアップロードした後、以下のコード全体を実行します。最も時間がかかるのはdoTheMath関数です。私は、stackoverflowで持っている500のレピュテーションポイントをすべて、喜んで誰かに報酬として与えますこの単純なコードを書き直してGPUで実行します。特にGPUに関心があります。この問題に対してどのように実行されるかを実際に確認したいと思っています。

編集/更新1:以下は、実際のデータの小さなサンプルへのリンクです。 data_csv.Zip 約102000行実データ2aおよびデータ2bの実データ1および2000行。実際のサンプルデータで_minimumLimit = 400_を使用する

編集/更新2:この投稿をフォローしている人のために、ここに以下の回答の短い要約があります。これまで、元のソリューションに対する4つの答えがあります。 @Divakarが提供するものは、元のコードを調整しただけです。 2つの調整のうち、最初の調整のみがこの問題に実際に適用できます。2番目は適切な調整ですが、ここでは適用されません。他の3つの答えのうち、そのうちの2つはCPUベースのソリューションで、1つはtensorflow-GPUの試みです。 Paul PanzerによるTensorflow-GPUは有望なようですが、実際にGPUで実行すると、元のGPUよりも速度が遅いため、コードの改善が必要です。

他の2つのCPUベースのソリューションは、@ PaulPanzer(純粋なnumpyソリューション)および@MSeifert(numbaソリューション)によって提出されます。どちらのソリューションでも非常に優れた結果が得られ、元のコードと比較して非常に高速にデータを処理します。 2つのうち、Paul Panzerによって提出された方が高速です。約3秒で約1.000.000行を処理します。唯一の問題は、batchSizesが小さいことです。これは、MSeifertが提供するnumbaソリューションに切り替えるか、以下で説明するすべての調整を行った後で元のコードに切り替えることで解決できます。

私は@PaulPanzerと@MSeifertが彼らの回答に対して行った仕事に対してとても幸せで感謝しています。それでも、これはGPUベースのソリューションに関する質問なので、誰かがGPUバージョンを試してみてくれるかどうかを確認し、現在のCPUと比較して、GPUでデータをどれだけ高速に処理できるかを確認するのを待っています。ソリューション。 @PaulPanzerの純粋で大胆なソリューションよりも優れた回答が他にない場合、私は彼の回答を正しいものとして受け入れ、賞金を獲得します:)

編集/更新3:@DivakarがGPUのソリューションを含む新しい回答を投稿しました。実際のデータでテストした後、速度はCPUの対応するソリューションにさえ匹敵しません。 GPUは約1.5秒で約5.000.000を処理します。これはすごいです:)私はGPUソリューションに非常に興奮しています。それを投稿してくれた@Divakarに感謝します。 CPUソリューションについて@PaulPanzerと@MSeifertに感謝します:)今、私の研究はGPUにより信じられないほどの速度で続けています:)

_import pandas as pd
import numpy as np
import time

def doTheMath(tmpData1, data2a, data2b):
    A = tmpData1[:, 0]
    B = tmpData1[:,1]
    C = tmpData1[:,2]
    D = tmpData1[:,3]
    Bmax = B.max()
    Cmin  = C.min()
    dif = (Bmax - Cmin)
    abcd = ((((A - Cmin) / dif) + ((B - Cmin) / dif) + ((C - Cmin) / dif) + ((D - Cmin) / dif)) / 4)
    return np.where(((abcd <= data2a) & (abcd >= data2b)), 1, 0).sum()

#Declare variables
batchSize = 2000
sampleSize = 5000000
resultArray = []
minimumLimit = 490 #use 400 on the real sample data 

#Create Random Sample Data
data1 = np.matrix(np.random.uniform(1, 100, (sampleSize + batchSize, 4)))
data2a = np.matrix(np.random.uniform(0, 1, (batchSize, 1))) #upper limit
data2b = np.matrix(np.random.uniform(0, 1, (batchSize, 1))) #lower limit
#approx. half of data2a will be smaller than data2b, but that is only in the sample data because it is randomly generated, NOT the real data. The real data2a is always higher than data2b.


#Loop through the data
t0 = time.time()
for rowNr in  range(data1.shape[0]):
    tmp_df = data1[rowNr:rowNr + batchSize] #rolling window
    if(tmp_df.shape[0] == batchSize):
        result = doTheMath(tmp_df, data2a, data2b)
        if (result >= minimumLimit):
            resultArray.append([rowNr , result])
print('Runtime:', time.time() - t0)

#Save data results
resultArray = np.array(resultArray)
print(resultArray[:,1].sum())
resultArray = pd.DataFrame({'index':resultArray[:,0], 'result':resultArray[:,1]})
resultArray.to_csv("Result Array.csv", sep=';')
_

私が取り組んでいるPCの仕様:

_GTX970(4gb) video card; 
i7-4790K CPU 4.00Ghz; 
16GB RAM;
a SSD drive 
running Windows 7; 
_

副次的な質問として、SLIの2番目のビデオカードがこの問題の解決に役立ちますか?

20
RaduS

はじめにとソリューションコード

さて、あなたはそれを求めました!したがって、この投稿には、_ PyCUDA を使用した実装があり、CUDAの機能のほとんどをPython環境。 Python環境にとどまるCUDAカーネルを記述およびコンパイルできるSourceModule機能を使用します。

関係する計算の中で、当面の問題に到達すると、最大値と最小値、わずかな差、除算と比較がスライドします。最大および最小の部品については、(各スライディングウィンドウの)ブロック最大検出が含まれます。詳細は here で説明されているように、削減手法を使用します。これはブロックレベルで行われます。スライディングウィンドウにわたる上位レベルの反復では、グリッドレベルのインデックスを使用してCUDAリソースにインデックスを付けます。このブロックおよびグリッド形式の詳細については、 page-18 を参照してください。 PyCUDAは、maxやminなどの計算削減のためのビルトインもサポートしますが、制御を失います。具体的には、共有メモリや定数メモリなどの専用メモリを使用して、GPUを最適レベルに近づけるつもりです。

PyCUDA-NumPyソリューションコードのリスト-

1] PyCUDAパーツ-

import pycuda.autoinit
import pycuda.driver as drv
import numpy as np
from pycuda.compiler import SourceModule

mod = SourceModule("""
#define TBP 1024 // THREADS_PER_BLOCK

__device__ void get_Bmax_Cmin(float* out, float *d1, float *d2, int L, int offset)
{
    int tid = threadIdx.x;
    int inv = TBP;
    __shared__ float dS[TBP][2];

    dS[tid][0] = d1[tid+offset];  
    dS[tid][1] = d2[tid+offset];         
    __syncthreads();

    if(tid<L-TBP)  
    {
        dS[tid][0] = fmaxf(dS[tid][0] , d1[tid+inv+offset]);
        dS[tid][1] = fminf(dS[tid][1] , d2[tid+inv+offset]);
    }
    __syncthreads();
    inv = inv/2;

    while(inv!=0)   
    {
        if(tid<inv)
        {
            dS[tid][0] = fmaxf(dS[tid][0] , dS[tid+inv][0]);
            dS[tid][1] = fminf(dS[tid][1] , dS[tid+inv][1]);
        }
        __syncthreads();
        inv = inv/2;
    }
    __syncthreads();

    if(tid==0)
    {
        out[0] = dS[0][0];
        out[1] = dS[0][1];
    }   
    __syncthreads();
}

__global__ void main1(float* out, float *d0, float *d1, float *d2, float *d3, float *lowL, float *highL, int *BLOCKLEN)
{
    int L = BLOCKLEN[0];
    int tid = threadIdx.x;
    int iterID = blockIdx.x;
    float Bmax_Cmin[2];
    int inv;
    float Cmin, dif;   
    __shared__ float dS[TBP*2];   

    get_Bmax_Cmin(Bmax_Cmin, d1, d2, L, iterID);  
    Cmin = Bmax_Cmin[1];
    dif = (Bmax_Cmin[0] - Cmin);

    inv = TBP;

    dS[tid] = (d0[tid+iterID] + d1[tid+iterID] + d2[tid+iterID] + d3[tid+iterID] - 4.0*Cmin) / (4.0*dif);
    __syncthreads();

    if(tid<L-TBP)  
        dS[tid+inv] = (d0[tid+inv+iterID] + d1[tid+inv+iterID] + d2[tid+inv+iterID] + d3[tid+inv+iterID] - 4.0*Cmin) / (4.0*dif);                   

     dS[tid] = ((dS[tid] >= lowL[tid]) & (dS[tid] <= highL[tid])) ? 1 : 0;
     __syncthreads();

     if(tid<L-TBP)
         dS[tid] += ((dS[tid+inv] >= lowL[tid+inv]) & (dS[tid+inv] <= highL[tid+inv])) ? 1 : 0;
     __syncthreads();

    inv = inv/2;
    while(inv!=0)   
    {
        if(tid<inv)
            dS[tid] += dS[tid+inv];
        __syncthreads();
        inv = inv/2;
    }

    if(tid==0)
        out[iterID] = dS[0];
    __syncthreads();

}
""")

THREADS_PER_BLOCK, TBPは、batchSizeに基づいて設定されることに注意してください。ここでの経験則は、2の累乗値をTBPbatchSizeよりもわずかに割り当てることです。したがって、batchSize = 2000の場合、1024としてTBPが必要でした。

2] NumPyパーツ-

def gpu_app_v1(A, B, C, D, batchSize, minimumLimit):
    func1 = mod.get_function("main1")
    outlen = len(A)-batchSize+1

    # Set block and grid sizes
    BSZ = (1024,1,1)
    GSZ = (outlen,1)

    dest = np.zeros(outlen).astype(np.float32)
    N = np.int32(batchSize)
    func1(drv.Out(dest), drv.In(A), drv.In(B), drv.In(C), drv.In(D), \
                     drv.In(data2b), drv.In(data2a),\
                     drv.In(N), block=BSZ, grid=GSZ)
    idx = np.flatnonzero(dest >= minimumLimit)
    return idx, dest[idx]

ベンチマーク

GTX 960Mでテストしました。 PyCUDAは配列が連続した順序であると想定していることに注意してください。したがって、列をスライスしてコピーを作成する必要があります。列ではなく行に沿ってデータが分散されるように、ファイルからデータを読み取ることができることを期待/想定しています。したがって、現時点ではベンチマーク機能の対象外にしてください。

オリジナルのアプローチ-

def org_app(data1, batchSize, minimumLimit):
    resultArray = []
    for rowNr in  range(data1.shape[0]-batchSize+1):
        tmp_df = data1[rowNr:rowNr + batchSize] #rolling window
        result = doTheMath(tmp_df, data2a, data2b)
        if (result >= minimumLimit):
            resultArray.append([rowNr , result]) 
    return resultArray

タイミングと検証-

In [2]: #Declare variables
   ...: batchSize = 2000
   ...: sampleSize = 50000
   ...: resultArray = []
   ...: minimumLimit = 490 #use 400 on the real sample data
   ...: 
   ...: #Create Random Sample Data
   ...: data1 = np.random.uniform(1, 100000, (sampleSize + batchSize, 4)).astype(np.float32)
   ...: data2b = np.random.uniform(0, 1, (batchSize)).astype(np.float32)
   ...: data2a = data2b + np.random.uniform(0, 1, (batchSize)).astype(np.float32)
   ...: 
   ...: # Make column copies
   ...: A = data1[:,0].copy()
   ...: B = data1[:,1].copy()
   ...: C = data1[:,2].copy()
   ...: D = data1[:,3].copy()
   ...: 
   ...: gpu_out1,gpu_out2 = gpu_app_v1(A, B, C, D, batchSize, minimumLimit)
   ...: cpu_out1,cpu_out2 = np.array(org_app(data1, batchSize, minimumLimit)).T
   ...: print(np.allclose(gpu_out1, cpu_out1))
   ...: print(np.allclose(gpu_out2, cpu_out2))
   ...: 
True
False

したがって、CPUとGPUのカウントにはいくつかの違いがあります。それらを調査しましょう-

In [7]: idx = np.flatnonzero(~np.isclose(gpu_out2, cpu_out2))

In [8]: idx
Out[8]: array([12776, 15208, 17620, 18326])

In [9]: gpu_out2[idx] - cpu_out2[idx]
Out[9]: array([-1., -1.,  1.,  1.])

一致しないカウントの4つのインスタンスがあります。これらは1までに最大でオフになっています。研究の際、私はこれに関するいくつかの情報に出くわしました。基本的に、最大値と最小値の計算に数学の組み込み関数を使用しているため、浮動小数点表現の最後のバイナリビットがCPUの対応するものと異なっていると思います。これはULPエラーと呼ばれ、詳細には here および here は使用されていません。

最後に、問題を別にして、最も重要なビットであるパフォーマンスについて説明しましょう。

In [10]: %timeit org_app(data1, batchSize, minimumLimit)
1 loops, best of 3: 2.18 s per loop

In [11]: %timeit gpu_app_v1(A, B, C, D, batchSize, minimumLimit)
10 loops, best of 3: 82.5 ms per loop

In [12]: 2180.0/82.5
Out[12]: 26.424242424242426

より大きなデータセットで試してみましょう。 sampleSize = 500000を使用すると、

In [14]: %timeit org_app(data1, batchSize, minimumLimit)
1 loops, best of 3: 23.2 s per loop

In [15]: %timeit gpu_app_v1(A, B, C, D, batchSize, minimumLimit)
1 loops, best of 3: 821 ms per loop

In [16]: 23200.0/821
Out[16]: 28.25822168087698

したがって、スピードアップは27で一定に保たれます。

制限:

1)float32の数値を使用しています。GPUはこれらの数値で最適に機能するためです。特にサーバーGPU以外の倍精度は、パフォーマンスに関しては一般的ではありません。このようなGPUを使用しているため、float32でテストしました。

さらに改善:

1)constant memoryを使用する代わりに、より高速なdata2aを使用してdata2bglobal memoryをフィードできます。

8
Divakar

微調整#1

通常、NumPy配列を使用する場合はベクトル化することをお勧めします。しかし、非常に大きな配列では、選択肢がないと思います。したがって、パフォーマンスを向上させるために、マイナーツイークを合計の最後のステップで最適化できます。

1s0sの配列を作成し、合計するステップを置き換えることができます。

np.where(((abcd <= data2a) & (abcd >= data2b)), 1, 0).sum()

np.count_nonzeroを使用すると、1sおよび0sに変換する代わりに、ブール配列内のTrue値を効率的にカウントします-

np.count_nonzero((abcd <= data2a) & (abcd >= data2b))

ランタイムテスト-

In [45]: abcd = np.random.randint(11,99,(10000))

In [46]: data2a = np.random.randint(11,99,(10000))

In [47]: data2b = np.random.randint(11,99,(10000))

In [48]: %timeit np.where(((abcd <= data2a) & (abcd >= data2b)), 1, 0).sum()
10000 loops, best of 3: 81.8 µs per loop

In [49]: %timeit np.count_nonzero((abcd <= data2a) & (abcd >= data2b))
10000 loops, best of 3: 28.8 µs per loop

微調整#2

暗黙のブロードキャストを受けるケースを処理する場合は、事前計算された逆数を使用します。もう少し情報 here 。したがって、difの逆数を格納し、代わりにそれをステップで使用します:

((((A  - Cmin) / dif) + ((B  - Cmin) / dif) + ...

サンプルテスト-

In [52]: A = np.random.Rand(10000)

In [53]: dif = 0.5

In [54]: %timeit A/dif
10000 loops, best of 3: 25.8 µs per loop

In [55]: %timeit A*(1.0/dif)
100000 loops, best of 3: 7.94 µs per loop

difによる除算を使用している場所が4つあります。だから、うまくいけば、これもそこに顕著なブーストをもたらすでしょう!

10
Divakar

ターゲット(GPU)の微調整を開始する前、または他のもの(つまり、並列実行)を使用する前に、既存のコードを改善する方法を検討することをお勧めします。 numba -tagを使用したので、これを使用してコードを改善します。最初に、行列ではなく配列を操作します。

_data1 = np.array(np.random.uniform(1, 100, (sampleSize + batchSize, 4)))
data2a = np.array(np.random.uniform(0, 1, batchSize)) #upper limit
data2b = np.array(np.random.uniform(0, 1, batchSize)) #lower limit
_

doTheMathを呼び出すたびに整数が返されることが期待されますが、多くの配列を使用し、多くの中間配列を作成します。

_abcd = ((((A  - Cmin) / dif) + ((B  - Cmin) / dif) + ((C   - Cmin) / dif) + ((D - Cmin) / dif)) / 4)
return np.where(((abcd <= data2a) & (abcd >= data2b)), 1, 0).sum()
_

これにより、各ステップで中間配列が作成されます。

  • _tmp1 = A-Cmin_、
  • _tmp2 = tmp1 / dif_、
  • _tmp3 = B - Cmin_、
  • _tmp4 = tmp3 / dif_
  • ...あなたは要点を取得します。

ただし、これはレデュース関数(配列->整数)であるため、中間配列を多く持つ必要はなく、「フライ」の値を計算するだけです。

_import numba as nb

@nb.njit
def doTheMathNumba(tmpData, data2a, data2b):
    Bmax = np.max(tmpData[:, 1])
    Cmin = np.min(tmpData[:, 2])
    diff = (Bmax - Cmin)
    idiff = 1 / diff
    sum_ = 0
    for i in range(tmpData.shape[0]):
        val = (tmpData[i, 0] + tmpData[i, 1] + tmpData[i, 2] + tmpData[i, 3]) / 4 * idiff - Cmin * idiff
        if val <= data2a[i] and val >= data2b[i]:
            sum_ += 1
    return sum_
_

複数の操作を回避するために、ここで別のことをしました。

_(((A - Cmin) / dif) + ((B - Cmin) / dif) + ((C - Cmin) / dif) + ((D - Cmin) / dif)) / 4
= ((A - Cmin + B - Cmin + C - Cmin + D - Cmin) / dif) / 4
= (A + B + C + D - 4 * Cmin) / (4 * dif)
= (A + B + C + D) / (4 * dif) - (Cmin / dif)
_

これにより、実際には私のコンピューターでの実行時間がほぼ10倍に短縮されます。

_%timeit doTheMath(tmp_df, data2a, data2b)       # 1000 loops, best of 3: 446 µs per loop
%timeit doTheMathNumba(tmp_df, data2a, data2b)  # 10000 loops, best of 3: 59 µs per loop
_

確かに他の改善点もあります。たとえば、ローリングの最小/最大値を使用してBmaxCminを計算すると、計算の少なくとも一部がO(sampleSize)ではなくO(samplesize * batchsize)で実行されます。また、CminBmaxが次のサンプルで変更されない場合、これらの値は変わらないため、一部の_(A + B + C + D) / (4 * dif) - (Cmin / dif)_計算を再利用することもできます。比較が異なるため、実行は少し複雑です。しかし、間違いなく可能です!こちらをご覧ください:

_import time
import numpy as np
import numba as nb

@nb.njit
def doTheMathNumba(abcd, data2a, data2b, Bmax, Cmin):
    diff = (Bmax - Cmin)
    idiff = 1 / diff
    quarter_idiff = 0.25 * idiff
    sum_ = 0
    for i in range(abcd.shape[0]):
        val = abcd[i] * quarter_idiff - Cmin * idiff
        if val <= data2a[i] and val >= data2b[i]:
            sum_ += 1
    return sum_

@nb.njit
def doloop(data1, data2a, data2b, abcd, Bmaxs, Cmins, batchSize, sampleSize, minimumLimit, resultArray):
    found = 0
    for rowNr in range(data1.shape[0]):
        if(abcd[rowNr:rowNr + batchSize].shape[0] == batchSize):
            result = doTheMathNumba(abcd[rowNr:rowNr + batchSize], 
                                    data2a, data2b, Bmaxs[rowNr], Cmins[rowNr])
            if (result >= minimumLimit):
                resultArray[found, 0] = rowNr
                resultArray[found, 1] = result
                found += 1
    return resultArray[:found]

#Declare variables
batchSize = 2000
sampleSize = 50000
resultArray = []
minimumLimit = 490 #use 400 on the real sample data 

data1 = np.array(np.random.uniform(1, 100, (sampleSize + batchSize, 4)))
data2a = np.array(np.random.uniform(0, 1, batchSize)) #upper limit
data2b = np.array(np.random.uniform(0, 1, batchSize)) #lower limit

from scipy import ndimage
t0 = time.time()
abcd = np.sum(data1, axis=1)
Bmaxs = ndimage.maximum_filter1d(data1[:, 1], 
                                 size=batchSize, 
                                 Origin=-((batchSize-1)//2-1))  # correction for even shapes
Cmins = ndimage.minimum_filter1d(data1[:, 2], 
                                 size=batchSize, 
                                 Origin=-((batchSize-1)//2-1))

result = np.zeros((sampleSize, 2), dtype=np.int64)
doloop(data1, data2a, data2b, abcd, Bmaxs, Cmins, batchSize, sampleSize, minimumLimit, result)
print('Runtime:', time.time() - t0)
_

これは私に_Runtime: 0.759593152999878_(numbaが関数をコンパイルした後!)を与えますが、元のテイクは_Runtime: 24.68975639343262_を持っていました。今では30倍速くなりました!

あなたのサンプルサイズではそれでも_Runtime: 60.187848806381226_がかかりますが、それは悪くないですよね?

そして、私が自分でこれを実行していなくても、 numba"Numba for CUDA GPUs" と書くことが可能であり、複雑に思われないと言います。

5
MSeifert

アルゴリズムを調整するだけで何ができるかを示すコードを次に示します。それは純粋な派手なものですが、投稿したサンプルデータでは、元のバージョンよりも約35倍高速化されています(私の控えめなマシンでは〜2.5秒で〜1,000,000サンプル)。

>>> result_dict = master('run')
[('load', 0.82578349113464355), ('precomp', 0.028138399124145508), ('max/min', 0.24333405494689941), ('ABCD', 0.015314102172851562), ('main', 1.3356468677520752)]
TOTAL 2.44821691513

使用された微調整:

  • A + B + C + D、私の他の答えを見てください
  • 同じCmin/difで(A + B + C + D-4Cmin)/(4dif)を複数回計算することを避けることを含む、min/maxの実行。

これらは多かれ少なかれ日常的なものです。これは高価なdata2a/bとの比較を残しますO(NK)ここで、Nはサンプルの数、Kはウィンドウのサイズです。ここでは、比較的よく利用できます-動作中のデータ。実行中の最小/最大値を使用して、一度にウィンドウオフセットの範囲をテストするために使用できるdata2a/bのバリアントを作成できます。テストが失敗した場合、これらのすべてのオフセットはすぐに除外できます。そうでない場合、範囲は二分されます。 。

import numpy as np
import time

# global variables; they will hold the precomputed pre-screening filters
preA, preB = {}, {}
CHUNK_SIZES = None

def sliding_argmax(data, K=2000):
    """compute the argmax of data over a sliding window of width K

    returns:
        indices  -- indices into data
        switches -- window offsets at which the maximum changes
                    (strictly speaking: where the index of the maximum changes)
                    excludes 0 but includes maximum offset (len(data)-K+1)

    see last line of compute_pre_screening_filter for a recipe to convert
    this representation to the vector of maxima
    """
    N = len(data)
    last = np.argmax(data[:K])
    indices = [last]
    while indices[-1] <= N - 1:
        ge = np.where(data[last + 1 : last + K + 1] > data[last])[0]
        if len(ge) == 0:
            if last + K >= N:
                break
            last += 1 + np.argmax(data[last + 1 : last + K + 1])
            indices.append(last)
        else:
            last += 1 + ge[0]
            indices.append(last)
    indices = np.array(indices)
    switches = np.where(data[indices[1:]] > data[indices[:-1]],
                        indices[1:] + (1-K), indices[:-1] + 1)
    return indices, np.r_[switches, [len(data)-K+1]]


def compute_pre_screening_filter(bound, n_offs):
    """compute pre-screening filter for point-wise upper bound

    given a K-vector of upper bounds B and K+n_offs-1-vector data
    compute K+n_offs-1-vector filter such that for each index j
    if for any offset 0 <= o < n_offs and index 0 <= i < K such that
    o + i = j, the inequality B_i >= data_j holds then filter_j >= data_j

    therefore the number of data points below filter is an upper bound for
    the maximum number of points below bound in any K-window in data
    """
    pad_l, pad_r = np.min(bound[:n_offs-1]), np.min(bound[1-n_offs:])
    padded = np.r_[pad_l+np.zeros(n_offs-1,), bound, pad_r+np.zeros(n_offs-1,)]
    indices, switches = sliding_argmax(padded, n_offs)
    return padded[indices].repeat(np.diff(np.r_[[0], switches]))


def compute_all_pre_screening_filters(upper, lower, min_chnk=5, dyads=6):
    """compute upper and lower pre-screening filters for data blocks of
    sizes K+n_offs-1 where
    n_offs = min_chnk, 2min_chnk, ..., 2^(dyads-1)min_chnk

    the result is stored in global variables preA and preB
    """
    global CHUNK_SIZES

    CHUNK_SIZES = min_chnk * 2**np.arange(dyads)
    preA[1] = upper
    preB[1] = lower
    for n in CHUNK_SIZES:
        preA[n] = compute_pre_screening_filter(upper, n)
        preB[n] = -compute_pre_screening_filter(-lower, n)


def test_bounds(block, counts, threshold=400):
    """test whether the windows fitting in the data block 'block' fall
    within the bounds using pre-screening for efficient bulk rejection

    array 'counts' will be overwritten with the counts of compliant samples
    note that accurate counts will only be returned for above threshold
    windows, because the analysis of bulk rejected windows is short-circuited

    also note that bulk rejection only works for 'well behaved' data and
    for example not on random numbers
    """
    N = len(counts)
    K = len(preA[1])
    r = N % CHUNK_SIZES[0]
    # chop up N into as large as possible chunks with matching pre computed
    # filters
    # start with small and work upwards
    counts[:r] = [np.count_nonzero((block[l:l+K] <= preA[1]) &
                                   (block[l:l+K] >= preB[1]))
                  for l in range(r)]

    def bisect(block, counts):
        M = len(counts)
        cnts = np.count_nonzero((block <= preA[M]) & (block >= preB[M]))
        if cnts < threshold:
            counts[:] = cnts
            return
        Elif M == CHUNK_SIZES[0]:
            counts[:] = [np.count_nonzero((block[l:l+K] <= preA[1]) &
                                          (block[l:l+K] >= preB[1]))
                         for l in range(M)]
        else:
            M //= 2
            bisect(block[:-M], counts[:M])
            bisect(block[M:], counts[M:])

    N = N // CHUNK_SIZES[0]
    for M in CHUNK_SIZES:
        if N % 2:
            bisect(block[r:r+M+K-1], counts[r:r+M])
            r += M
        Elif N == 0:
            return
        N //= 2
    else:
        for j in range(2*N):
            bisect(block[r:r+M+K-1], counts[r:r+M])
            r += M


def analyse(data, use_pre_screening=True, min_chnk=5, dyads=6,
            threshold=400):
    samples, upper, lower = data
    N, K = samples.shape[0], upper.shape[0]
    times = [time.time()]
    if use_pre_screening:
        compute_all_pre_screening_filters(upper, lower, min_chnk, dyads)
    times.append(time.time())
    # compute switching points of max and min for running normalisation
    upper_inds, upper_swp = sliding_argmax(samples[:, 1], K)
    lower_inds, lower_swp = sliding_argmax(-samples[:, 2], K)
    times.append(time.time())
    # sum columns
    ABCD = samples.sum(axis=-1)
    times.append(time.time())
    counts = np.empty((N-K+1,), dtype=int)
    # main loop
    # loop variables:
    offs = 0
    u_ind, u_scale, u_swp = 0, samples[upper_inds[0], 1], upper_swp[0]
    l_ind, l_scale, l_swp = 0, samples[lower_inds[0], 2], lower_swp[0]
    while True:
        # check which is switching next, min(C) or max(B)
        if u_swp > l_swp:
            # greedily take the largest block possible such that dif and Cmin
            # do not change
            block = (ABCD[offs:l_swp+K-1] - 4*l_scale) \
                    * (0.25 / (u_scale-l_scale))
            if use_pre_screening:
                test_bounds(block, counts[offs:l_swp], threshold=threshold)
            else:
                counts[offs:l_swp] = [
                    np.count_nonzero((block[l:l+K] <= upper) &
                                     (block[l:l+K] >= lower))
                    for l in range(l_swp - offs)]
            # book keeping
            l_ind += 1
            offs = l_swp
            l_swp = lower_swp[l_ind]
            l_scale = samples[lower_inds[l_ind], 2]
        else:
            block = (ABCD[offs:u_swp+K-1] - 4*l_scale) \
                    * (0.25 / (u_scale-l_scale))
            if use_pre_screening:
                test_bounds(block, counts[offs:u_swp], threshold=threshold)
            else:
                counts[offs:u_swp] = [
                    np.count_nonzero((block[l:l+K] <= upper) &
                                     (block[l:l+K] >= lower))
                    for l in range(u_swp - offs)]
            u_ind += 1
            if u_ind == len(upper_inds):
                assert u_swp == N-K+1
                break
            offs = u_swp
            u_swp = upper_swp[u_ind]
            u_scale = samples[upper_inds[u_ind], 1]
    times.append(time.time())
    return {'counts': counts, 'valid': np.where(counts >= 400)[0],
            'timings': np.diff(times)}


def master(mode='calibrate', data='fake', use_pre_screening=True, nrep=3,
           min_chnk=None, dyads=None):
    t = time.time()
    if data in ('fake', 'load'):
        data1 = np.loadtxt('data1.csv', delimiter=';', skiprows=1,
                           usecols=[1,2,3,4])
        data2a = np.loadtxt('data2a.csv', delimiter=';', skiprows=1,
                            usecols=[1])
        data2b = np.loadtxt('data2b.csv', delimiter=';', skiprows=1,
                            usecols=[1])
        if data == 'fake':
            data1 = np.tile(data1, (10, 1))
        threshold = 400
    Elif data == 'random':
        data1 = np.random.random((102000, 4))
        data2b = np.random.random(2000)
        data2a = np.random.random(2000)
        threshold = 490
        if use_pre_screening or mode == 'calibrate':
            print('WARNING: pre-screening not efficient on artificial data')
    else:
        raise ValueError("data mode {} not recognised".format(data))
    data = data1, data2a, data2b
    t_load = time.time() - t
    if mode == 'calibrate':
        min_chnk = (2, 3, 4, 5, 6) if min_chnk is None else min_chnk
        dyads = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10) if dyads is None else dyads
        timings = np.zeros((len(min_chnk), len(dyads)))
        print('max bisect  ' + ' '.join([
            '   n.a.' if dy == 0 else '{:7d}'.format(dy) for dy in dyads]),
              end='')
        for i, mc in enumerate(min_chnk):
            print('\nmin chunk {}'.format(mc), end=' ')
            for j, dy in enumerate(dyads):
                for k in range(nrep):
                    if dy == 0: # no pre-screening
                        timings[i, j] += analyse(
                            data, False, mc, dy, threshold)['timings'][3]
                    else:
                        timings[i, j] += analyse(
                            data, True, mc, dy, threshold)['timings'][3]
                timings[i, j] /= nrep
                print('{:7.3f}'.format(timings[i, j]), end=' ', flush=True)
        best_mc, best_dy = np.unravel_index(np.argmin(timings.ravel()),
                                            timings.shape)
        print('\nbest', min_chnk[best_mc], dyads[best_dy])
        return timings, min_chnk[best_mc], dyads[best_dy]
    if mode == 'run':
        min_chnk = 2 if min_chnk is None else min_chnk
        dyads = 5 if dyads is None else dyads
        res = analyse(data, use_pre_screening, min_chnk, dyads, threshold)
        times = np.r_[[t_load], res['timings']]
        print(list(Zip(('load', 'precomp', 'max/min', 'ABCD', 'main'), times)))
        print('TOTAL', times.sum())
        return res
4
Paul Panzer

これは技術的にはトピックから外れていますが(GPUではありません)、きっとあなたは興味を持つことでしょう。

明らかでかなり大きな節約が1つあります。

_A + B + C + D_を事前計算します(ループ内ではなく、データ全体:data1.sum(axis=-1))。なぜなら、abcd = ((A+B+C+D) - 4Cmin) / (4dif)だからです。これはかなりの数の操作を節約します。

驚いたことに誰も以前にそれを発見しませんでした;-)

編集:

別のこともありますが、それはあなたの例にすぎず、実際のデータにはないのではないかと思います。

現状では、_data2a_の約半分は_data2b_よりも小さくなります。これらの場所では、abcdの条件を両方ともTrueにすることはできないため、abcdを計算する必要もありません。

編集:

以下で使用したもう1つのTweakですが、言及するのを忘れていました。移動ウィンドウで最大(または最小)を計算する場合。たとえば、1ポイントを右に移動すると、最大値が変更される可能性はどのくらいありますか?変更できるのは2つだけです。右側の新しいポイントが大きい(windowlengthの時間でほぼ1回発生します。発生した場合でも、新しい最大値がすぐにわかります)、または古い最大値が左側のウィンドウから外れます。 (これもwindowlength回に約1回発生します)。この最後の場合のみ、ウィンドウ全体で新しい最大値を検索する必要があります。

編集:

Tensorflowで試してみるのに抵抗できませんでした。私はGPUを持っていないので、速度をテストする必要があります。マークされた行に「cpu」の代わりに「gpu」を入れます。

CPUでは、元の実装の約半分の速度です(つまり、Divakarの調整なし)。入力をマトリックスからプレーン配列に自由に変更できることに注意してください。現在、テンソルフローは少し動くターゲットなので、正しいバージョンを使用していることを確認してください。私はPython3.6とtf 0.12.1を使用しました。pip3をインストールした場合、今日tensorflow-gpuをインストールします。 すべき うまくいくかもしれません。

_import numpy as np
import time
import tensorflow as tf

# currently the max/min code is sequential
# thus
parallel_iterations = 1
# but you can put this in a separate loop, precompute and then try and run
# the remainder of doTheMathTF with a larger parallel_iterations

# tensorflow is quite capricious about its data types
ddf = tf.float64
ddi = tf.int32

def worker(data1, data2a, data2b):
    ###################################
    # CHANGE cpu to gpu in next line! #
    ###################################
    with tf.device('/cpu:0'):
        g = tf.Graph ()
        with g.as_default():
            ABCD = tf.constant(data1.sum(axis=-1), dtype=ddf)
            B = tf.constant(data1[:, 1], dtype=ddf)
            C = tf.constant(data1[:, 2], dtype=ddf)
            window = tf.constant(len(data2a))
            N = tf.constant(data1.shape[0] - len(data2a) + 1, dtype=ddi)
            data2a = tf.constant(data2a, dtype=ddf)
            data2b = tf.constant(data2b, dtype=ddf)
            def doTheMathTF(i, Bmax, Bmaxind, Cmin, Cminind, out):
                # most of the time we can keep the old max/min
                Bmaxind = tf.cond(Bmaxind<i,
                                  lambda: i + tf.to_int32(
                                      tf.argmax(B[i:i+window], axis=0)),
                                  lambda: tf.cond(Bmax>B[i+window-1], 
                                                  lambda: Bmaxind, 
                                                  lambda: i+window-1))
                Cminind = tf.cond(Cminind<i,
                                  lambda: i + tf.to_int32(
                                      tf.argmin(C[i:i+window], axis=0)),
                                  lambda: tf.cond(Cmin<C[i+window-1],
                                                  lambda: Cminind,
                                                  lambda: i+window-1))
                Bmax = B[Bmaxind]
                Cmin = C[Cminind]
                abcd = (ABCD[i:i+window] - 4 * Cmin) * (1 / (4 * (Bmax-Cmin)))
                out = out.write(i, tf.to_int32(
                    tf.count_nonzero(tf.logical_and(abcd <= data2a,
                                                    abcd >= data2b))))
                return i + 1, Bmax, Bmaxind, Cmin, Cminind, out
            with tf.Session(graph=g) as sess:
                i, Bmaxind, Bmax, Cminind, Cmin, out = tf.while_loop(
                    lambda i, _1, _2, _3, _4, _5: i<N, doTheMathTF,
                    (tf.Variable(0, dtype=ddi), tf.Variable(0.0, dtype=ddf),
                     tf.Variable(-1, dtype=ddi),
                     tf.Variable(0.0, dtype=ddf), tf.Variable(-1, dtype=ddi),
                     tf.TensorArray(ddi, size=N)),
                    shape_invariants=None,
                    parallel_iterations=parallel_iterations,
                    back_prop=False)
                out = out.pack()
                sess.run(tf.initialize_all_variables())
                out, = sess.run((out,))
    return out

#Declare variables
batchSize = 2000
sampleSize = 50000#00
resultArray = []

#Create Sample Data
data1 = np.random.uniform(1, 100, (sampleSize + batchSize, 4))
data2a = np.random.uniform(0, 1, (batchSize,))
data2b = np.random.uniform(0, 1, (batchSize,))

t0 = time.time()
out = worker(data1, data2a, data2b)
print('Runtime (tensorflow):', time.time() - t0)


good_indices, = np.where(out >= 490)
res_tf = np.c_[good_indices, out[good_indices]]

def doTheMath(tmpData1, data2a, data2b):
    A = tmpData1[:, 0]
    B  = tmpData1[:,1]
    C   = tmpData1[:,2]
    D = tmpData1[:,3]
    Bmax = B.max()
    Cmin  = C.min()
    dif = (Bmax - Cmin)
    abcd = ((((A  - Cmin) / dif) + ((B  - Cmin) / dif) + ((C   - Cmin) / dif) + ((D - Cmin) / dif)) / 4)
    return np.where(((abcd <= data2a) & (abcd >= data2b)), 1, 0).sum()

#Loop through the data
t0 = time.time()
for rowNr in  range(sampleSize+1):
    tmp_df = data1[rowNr:rowNr + batchSize] #rolling window
    result = doTheMath(tmp_df, data2a, data2b)
    if (result >= 490):
        resultArray.append([rowNr , result])
print('Runtime (original):', time.time() - t0)
print(np.alltrue(np.array(resultArray)==res_tf))
_
3
Paul Panzer