web-dev-qa-db-ja.com

スパースCSR配列のアウトオブコア処理

Pythonを使用してディスクに保存されたスパースCSR配列のチャンクにいくつかの関数を並列に適用するにはどうすればよいですか?続いて、これを行うことができます。 CSR配列を_joblib.dump_で保存し、joblib.load(.., mmap_mode="r")で開き、行のチャンクを1つずつ処理します。これは dask でより効率的に行うことができますか?

特に、スパース配列で可能なすべてのコア外操作が必要ではなく、行チャンクを並列にロードし(各チャンクはCSR配列です)、それらに関数を適用する機能(私の場合はたとえば、scikit-learnのestimator.predict(X))。

また、このタスクに適したファイル形式はディスク上にありますか? Joblibは機能しますが、メモリマップとしてロードされたCSR配列の(並列)パフォーマンスについてはよくわかりません。 _spark.mllib_は、カスタムのスパースストレージ形式(純粋なPythonパーサー)ではないようです)またはLIBSVM形式(scikit-learnのパーサーは私の経験、_joblib.dump_)よりもはるかに遅い...

注:私は ドキュメントhttps://github.com/dask/dask/でそれに関するさまざまな問題 を読みましたが、これに最善のアプローチをする方法がまだわかりません問題。

編集:より実用的な例を示すために、以下は密な配列の場合はdaskで機能するが、 thisエラー

_import numpy as np
import scipy.sparse

import joblib
import dask.array as da
from sklearn.utils import gen_batches

np.random.seed(42)
joblib.dump(np.random.Rand(100000, 1000), 'X_dense.pkl')
joblib.dump(scipy.sparse.random(10000, 1000000, format='csr'), 'X_csr.pkl')

fh = joblib.load('X_dense.pkl', mmap_mode='r')

# computing the results without dask
results = np.vstack((fh[sl, :].sum(axis=1)) for sl in gen_batches(fh.shape[0], batch_size))

# computing the results with dask
x = da.from_array(fh, chunks=(2000))
results = x.sum(axis=1).compute()
_

Edit2:以下の説明に従って、以下の例は前のエラーを克服しますが、_IndexError: Tuple index out of range_の_dask/array/core.py:L3413_に関するエラーを取得します。

_import dask
# +imports from the example above
dask.set_options(get=dask.get)  # disable multiprocessing

fh = joblib.load('X_csr.pkl', mmap_mode='r')

def func(x):
    if x.ndim == 0:
        # dask does some heuristics with dummy data, if the x is a 0d array
        # the sum command would fail
        return x
    res = np.asarray(x.sum(axis=1, keepdims=True))
    return res

Xd = da.from_array(fh, chunks=(2000))
results_new = Xd.map_blocks(func).compute()
_
40
rth

したがって、アプリケーション固有のデータ形式は言うまでもなく、joblibやdaskについては何も知りません。ただし、実際には、スパースデータ構造を保持しながら、ディスクからスパース行列をチャンクで読み取ることができます。

CSR形式に関するウィキペディアの記事 は、それがどのように機能するかを説明する素晴らしい仕事をしていますが、簡単に要約します。

いくつかの疎行列、例:

_1 0 2
0 0 3
4 5 6
_

ゼロ以外の各値とそれが存在する列を記憶することによって格納されます。

_sparse.data    = 1 2 3 4 5 6  # acutal value
sparse.indices = 0 2 2 0 1 2  # number of column (0-indexed)
_

今でも行がありません。圧縮形式では、すべての値の行を格納するのではなく、各行にゼロ以外の値がいくつあるかを格納するだけです。

ゼロ以外のカウントも累積されるため、次の配列には、この行までのゼロ以外の値の数が含まれていることに注意してください。さらに複雑なことに、配列は常に_0_で始まり、したがって_num_rows+1_エントリが含まれます。

_sparse.indptr = 0 2 3 6
_

したがって、2行目までは、ゼロ以外の3つの値、つまり_1_、_2_、および_3_があります。

これを整理したので、マトリックスの「スライス」を開始できます。目標は、いくつかのチャンクに対してdataindices、およびindptr配列を構築することです。元の巨大なマトリックスが3つのバイナリファイルに保存されていると仮定します。これを段階的に読み取ります。ジェネレーターを使用して、チャンクを繰り返しyieldします。

このためには、各チャンクにゼロ以外の値がいくつあるかを知り、それに応じた値と列インデックスの量を読み取る必要があります。ゼロ以外のカウントは、indptr配列から簡単に読み取ることができます。これは、必要なチャンクサイズに対応する巨大なindptrファイルからいくらかのエントリを読み取ることによって実現されます。 indptrファイルのその部分の最後のエントリから前の非ゼロ値の数を引いたものが、そのチャンク内の非ゼロの数を示します。したがって、チャンクdataおよびindices配列は、大きなdataおよびindicesファイルからスライスされます。 indptr配列には、人為的にゼロを付加する必要があります(これは、フォーマットで必要なことです。私に聞かないでください:D)。

次に、チャンクdataindices、およびindptrを使用してスパース行列を作成し、新しいスパース行列を取得できます。

実際のマトリックスサイズは、3つのアレイだけから直接再構築できないことに注意する必要があります。これは、マトリックスの最大列インデックスであるか、運が悪く、チャンクに未決定のデータがない場合です。したがって、列数も渡す必要があります。

私はおそらくかなり複雑な方法で物事を説明したので、このようなジェネレーターを実装する不透明なコードとしてこれを読んでください。

_import numpy as np
import scipy.sparse


def gen_batches(batch_size, sparse_data_path, sparse_indices_path, 
                sparse_indptr_path, dtype=np.float32, column_size=None):
    data_item_size = dtype().itemsize

    with open(sparse_data_path, 'rb') as data_file, \
            open(sparse_indices_path, 'rb') as indices_file, \
            open(sparse_indptr_path, 'rb') as indptr_file:
        nnz_before = np.fromstring(indptr_file.read(4), dtype=np.int32)

        while True:
            indptr_batch = np.frombuffer(nnz_before.tobytes() +
                              indptr_file.read(4*batch_size), dtype=np.int32)

            if len(indptr_batch) == 1:
                break

            batch_indptr = indptr_batch - nnz_before
            nnz_before = indptr_batch[-1]
            batch_nnz = np.asscalar(batch_indptr[-1])

            batch_data = np.frombuffer(data_file.read(
                                       data_item_size * batch_nnz), dtype=dtype)
            batch_indices = np.frombuffer(indices_file.read(
                                          4 * batch_nnz), dtype=np.int32)

            dimensions = (len(indptr_batch)-1, column_size)

            matrix = scipy.sparse.csr_matrix((batch_data, 
                           batch_indices, batch_indptr), shape=dimensions)

            yield matrix


if __name__ == '__main__':
    sparse = scipy.sparse.random(5, 4, density=0.1, format='csr', dtype=np.float32)

    sparse.data.tofile('sparse.data')        # dtype as specified above  ^^^^^^^^^^
    sparse.indices.tofile('sparse.indices')  # dtype=int32
    sparse.indptr.tofile('sparse.indptr')    # dtype=int32

    print(sparse.toarray())
    print('========')

    for batch in gen_batches(2, 'sparse.data', 'sparse.indices', 
                             'sparse.indptr', column_size=4):
        print(batch.toarray())
_

numpy.ndarray.tofile()はバイナリ配列を格納するだけなので、データ形式を覚えておく必要があります。 _scipy.sparse_はindicesindptrを_int32_として表すため、これが合計行列サイズの制限になります。

また、コードのベンチマークを行ったところ、scipycsrマトリックスコンストラクターが小さなマトリックスのボトルネックであることがわかりました。マイレージは異なる場合がありますが、これは単なる「原則の証明」です。

より洗練された実装が必要な場合、または何かが難しすぎる場合は、私に連絡してください:)

4
Obay