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()
_
したがって、アプリケーション固有のデータ形式は言うまでもなく、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
_があります。
これを整理したので、マトリックスの「スライス」を開始できます。目標は、いくつかのチャンクに対してdata
、indices
、およびindptr
配列を構築することです。元の巨大なマトリックスが3つのバイナリファイルに保存されていると仮定します。これを段階的に読み取ります。ジェネレーターを使用して、チャンクを繰り返しyield
します。
このためには、各チャンクにゼロ以外の値がいくつあるかを知り、それに応じた値と列インデックスの量を読み取る必要があります。ゼロ以外のカウントは、indptr配列から簡単に読み取ることができます。これは、必要なチャンクサイズに対応する巨大なindptr
ファイルからいくらかのエントリを読み取ることによって実現されます。 indptr
ファイルのその部分の最後のエントリから前の非ゼロ値の数を引いたものが、そのチャンク内の非ゼロの数を示します。したがって、チャンクdata
およびindices
配列は、大きなdata
およびindices
ファイルからスライスされます。 indptr
配列には、人為的にゼロを付加する必要があります(これは、フォーマットで必要なことです。私に聞かないでください:D)。
次に、チャンクdata
、indices
、および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
_はindices
とindptr
を_int32
_として表すため、これが合計行列サイズの制限になります。
また、コードのベンチマークを行ったところ、scipycsrマトリックスコンストラクターが小さなマトリックスのボトルネックであることがわかりました。マイレージは異なる場合がありますが、これは単なる「原則の証明」です。
より洗練された実装が必要な場合、または何かが難しすぎる場合は、私に連絡してください:)