Numpyのsavezとdictの組み合わせが沼

/

Numpyでとあるデータを永続化したときにNumpyのnp.savez()がとんでもないことになっていた。

  • Python 3.8.2
  • Numpy 1.18.2+mkl

結果

結果は次のようになる。dictをアンパックせずにnp.savez()のargsに指定してはいけない。

records = {
    'dimens': (2, 5, 2, 5, 3,),
    'matrices': np.random.randn(10, 10, 3)
}

np.savez('filename', records) # ココ!

with np.load('filename.npz', allow_pickle=True) as npz:
  loaded_records = npz['arr_0'].flat[0]
  dimens = loaded_records['dimens']
  matrices = loaded_records['matrices']

この場合の正解は以下のようになる。

records = {
    'dimens': (2, 5, 2, 5, 3,),
    'matrices': np.random.randn(10, 10, 3)
}

np.savez('filename', **records) # ココ!

with np.load('filename.npz', allow_pickle=True) as loaded_records:
  dimens = loaded_records['dimens']
  matrices = loaded_records['matrices']

Pythonではアステリスク2つでdictをアンパックしてキーワード引数に出来る。こうすると非常に直観的にnp.load()で構造化したデータを永続化して読み出せる。構造化したデータの読出しにはallow_pickle=Trueが必要だがこれはエラーメッセージが教えてくれるのでハマりどころではない。

問題

既にミスしたケースで保存してしまっていたデータがあったのでなんとしてもこのデータを取り出す必要があった。問題はハマったケースの

loaded_records = npz['arr_0'].flat[0]

で何が起こっているのか。このケース、まずargs=recordsをキーワードを省略してnp.savez()に指定したことになる。この場合は順番にarr_0,arr_1,arr_2というキーが割り当てられていくのでそこからndarrayを取り出せる。あくまでargsにはndarrayが指定されることが想定されているのだ。

ところが今指定したのはndarrayではなくdictを指定した。従ってこのnpz['arr_0']はdtype=objectでdictの要素を1つもつ配列になっている。実際このことはこのオブジェクトをprintすれば表示される。問題はここからで、[0]でそのdictを取り出すことが出来ると思うだろう。ところが

In [2]: npz['arr_0'][0]
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-53-770d95b9ee81> in <module>
----> 1 records[0]

IndexError: too many indices for array

どういうこと?となった。調べるとこういうことだった。

In [3]: npz['arr_0'].ndim, npz['arr_0'].shape, npz['arr_0'].size
Out[3]: (0, (), 1)

ndimとshapeは初期化されてないのにsizeは1でしっかりデータが入っている。なんやかんやした結果.flatを経由してそのあるはずのデータにアクセスできることが分かったとさ。

感想

アンパックしなかったこっちのミスなんだけどもこの現象はひどすぎるよぉ。

https://docs.scipy.org/doc/numpy/reference/generated/numpy.savez.html

https://docs.scipy.org/doc/numpy/reference/generated/numpy.generic.html