Wizard Notes

Python, JavaScript を使った音楽信号分析の技術録、作曲活動に関する雑記

numpy.savez_compressed:複数のNumPy配列を圧縮&バイナリ保存

Numpy で配列をバイナリで保存する方法としては、

numpy.save(npy_filepath, arr)

が最も単純な方法です。 しかし、

  • 非圧縮であるため、ファイルサイズが大きくなりがち
  • 単一の配列オブジェクトのみ保存

であるため、大量の配列を保存する場合や、配列サイズが大きい場合に悩むことがあると思います。

そう言った場合に有効なのがnumpy.savez_compressedであり、

  • 1つのファイル(xxxx.npz)に、複数の配列を保存
  • 圧縮により、ファイルサイズを削減

ができます。

この記事では、numpy.savez_compressedの実用時に、知っておくと利用が捗るポイントを紹介します。

使い方/拡張子/保存形式について

numpy.savez_compressedでは、.npzという拡張子のファイルに1つ以上のNumpy配列を保存します。

numpy.savez_compressedで保存したファイルをnp.load()すると、NpzFile オブジェクトが得られます。 NpzFile オブジェクトは辞書型であり、numpy.savez_compressedで指定したキーワード引数がキーとなっており、キーを与えると保存した配列を取り出すことができます。

>>> test_array = np.random.rand(3, 2)
>>> test_vector = np.random.rand(4)
>>> np.savez_compressed('/tmp/123', a=test_array, b=test_vector)
>>> loaded = np.load('/tmp/123.npz')
>>> print(np.array_equal(test_array, loaded['a']))
True
>>> print(np.array_equal(test_vector, loaded['b']))
True

numpy.savez_compressed — NumPy v1.17 Manual より

numpy.saveznumpy.savez_compressed

実は、どちらも _savez をラップしてます。

@array_function_dispatch(_savez_compressed_dispatcher)
def savez_compressed(file, *args, **kwds):
    ...
    _savez(file, args, kwds, True)
@array_function_dispatch(_savez_dispatcher)
def savez(file, *args, **kwds):
    ...
    _savez(file, args, kwds, False)

_savez()は以下のようになっており、真値型の第4引数で圧縮をどうするか制御しています。

def _savez(file, args, kwds, compress, allow_pickle=True, pickle_kwargs=None):
    import zipfile

    ...
    if compress:
        compression = zipfile.ZIP_DEFLATED
    else:
        compression = zipfile.ZIP_STORED
    ...

zipfile --- ZIP アーカイブの処理 — Python 3.8.2 ドキュメント

numpy/npyio.py at v1.17.0 · numpy/numpy · GitHub

1つの配列だけを圧縮する/複数ファイルをキーワード引数なしで保存する

numpy.savez_compressedで1つの配列だけを圧縮したい場合もあると思います。

そういったときに、

>>> test_array = np.random.rand(3, 2)
>>> np.savez_compressed('/tmp/123', a=test_array)
>>> loaded = np.load('/tmp/123.npz')
>>>  loaded['a']

とするのはちょっとだけ面倒です。

実は、キーワード引数を指定しない場合、NpzFile オブジェクトのキーワード引数は以下のように設定されます。

def _savez(file, args, kwds, compress, allow_pickle=True, pickle_kwargs=None):
    ...
    namedict = kwds
    for i, val in enumerate(args):
        key = 'arr_%d' % i
        if key in namedict.keys():
            raise ValueError(
                "Cannot use un-named variables and keyword %s" % key)
        namedict[key] = val
    ...

つまり、引数が1つの場合は、"arr_0"がキーとなります

同様に、二つ以上の可変長引数では、それぞれのキーは "arr_0", "arr_1", "arr_2"... となります

>>> test_array = np.random.rand(3, 2)
>>> np.savez_compressed('/tmp/123', test_array)
>>> loaded = np.load('/tmp/123.npz')
>>> test_array = loaded['arr_0']

圧縮性能

先ほどのコードを見ると、ZIPでの圧縮となっていることが分かります(ランレングス圧縮+ハフマン符号化)。

従って、値のバリエーションが少ない+同じ値が連続するほど、圧縮性能が高くなります

簡易検証: ※全てnp.float32で保存

  • np.random.ramdom(10000) => 75,638 バイト
  • np.ones(10000) => 278 バイト
  • np.random.randint(10, size=10000) => 7,552 バイト
  • np.random.randint(100, size=10000) => 13,742 バイト

もちろん一様乱数は最悪です。

出現する値のバリエーションが少ない、出現頻度に偏りがある、値が連続して出現する、配列がスパースといった場合に効いてきます