note.nkmk.me

NumPy配列ndarrayを一次元化(平坦化)するravelとflatten

Date: 2019-12-02 / tags: Python, NumPy

NumPy配列ndarrayを一次元化(平坦化、flatten)するには、numpy.ravel()関数、または、numpy.ndarrayravel()メソッド、flatten()メソッドを使う。

ここでは以下の内容について説明する。

  • numpy.ravel()関数による一次元化
  • ndarray.ravel()メソッドによる一次元化
  • ndarray.flatten()メソッドによる一次元化
  • reshape(-1)による一次元化
  • ravel()flatten()の違い(ビューとコピー)
  • ravel()flatten()の速度比較
  • 引数order
  • 3次元以上の多次元配列の場合
スポンサーリンク

numpy.ravel()関数による一次元化

numpy.ravel()の第一引数に対象とするnumpy.ndarrayを指定すると、一次元化されたnumpy.ndarrayが返される。

import numpy as np

a = np.arange(12).reshape(3, 4)
print(a)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

print(np.ravel(a))
# [ 0  1  2  3  4  5  6  7  8  9 10 11]

print(type(np.ravel(a)))
# <class 'numpy.ndarray'>

引数にはPython組み込み型のリストなど、いわゆるarray-likeオブジェクトも指定可能。その場合も返り値はnumpy.ndarray

print(np.ravel([[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]]))
# [ 0  1  2  3  4  5  6  7  8  9 10 11]

print(type(np.ravel([[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]])))
# <class 'numpy.ndarray'>

中のリストの要素数が異なる二次元リスト(リストのリスト)の場合、リストを要素とするnumpy.ndarrayが返されるので注意。

print(np.ravel([[0, 1, 2, 3], [4, 5, 6, 7], [8, 9]]))
# [list([0, 1, 2, 3]) list([4, 5, 6, 7]) list([8, 9])]

print(type(np.ravel([[0, 1, 2, 3], [4, 5, 6, 7], [8, 9]])))
# <class 'numpy.ndarray'>

このようなリストを平坦化したい場合は以下の記事を参照。

ndarray.ravel()メソッドによる一次元化

ravel()numpy.ndarrayのメソッドとしても提供されている。

print(a.ravel())
# [ 0  1  2  3  4  5  6  7  8  9 10 11]

ndarray.flatten()メソッドによる一次元化

numpy.ndarrayのメソッドにはflatten()もある。

print(a.flatten())
# [ 0  1  2  3  4  5  6  7  8  9 10 11]

ravel()は可能な限りビューを返すが、flatten()は常にコピーを返すという違いがある。flatten()はメモリを新たに確保する必要があるため、ravel()よりも遅い。詳細は後述。

なお、バージョン1.17時点では、flatten()numpy.ndarrayのメソッドのみで、numpy.flatten()のような関数としては提供されていない。

reshape(-1)による一次元化

numpy.ndarrayの形状shapeを変換するreshape()を利用して一次元化することもできる。-1を使うとサイズが自動的に算出されるので、reshape(-1)で一次元に変換できる。

numpy.ndarrayのメソッド。

print(a.reshape(-1))
# [ 0  1  2  3  4  5  6  7  8  9 10 11]

numpy.reshape()関数。関数の場合は上述のreval()と同様、リストなどのarray-likeオブジェクトも処理できる。

print(np.reshape(a, -1))
# [ 0  1  2  3  4  5  6  7  8  9 10 11]

print(np.reshape([[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]], -1))
# [ 0  1  2  3  4  5  6  7  8  9 10 11]

print(np.reshape([[0, 1, 2, 3], [4, 5, 6, 7], [8, 9]], -1))
# [list([0, 1, 2, 3]) list([4, 5, 6, 7]) list([8, 9])]

ravel()とflatten()の違い(ビューとコピー)

ravel()は可能な限りビューを返すが、flatten()は常にコピーを返す。reshape()reval()と同様、可能な限りビューを返す。

numpy.ndarrayにおけるビューとコピーについての詳細は以下の記事を参照。

print(np.shares_memory(a, a.ravel()))
# True

print(np.shares_memory(a, np.ravel(a)))
# True

print(np.shares_memory(a, a.flatten()))
# False

print(np.shares_memory(a, a.reshape(-1)))
# True

print(np.shares_memory(a, np.reshape(a, -1)))
# True

ビューの場合、元のnumpy.ndarrayとメモリを共有しているため、一方の値を変更すると他方の値も変わる。

a_ravel = a.ravel()
print(a_ravel)
# [ 0  1  2  3  4  5  6  7  8  9 10 11]

a_ravel[0] = 100
print(a_ravel)
# [100   1   2   3   4   5   6   7   8   9  10  11]

print(a)
# [[100   1   2   3]
#  [  4   5   6   7]
#  [  8   9  10  11]]

コピーの場合はそれぞれメモリが確保されるので、お互い別々に処理される。

「可能な限り」と書いたように、ravel(), reshape()が常にビューを返すとは限らない。

例えばステップを指定したスライスを一次元化した結果、メモリ上のstrideが一定にならない場合などはravel(), reshape()もコピーを返す(メモリを共有しない)。

a = np.arange(12).reshape(3, 4)
print(a)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

print(a[:, ::3])
# [[ 0  3]
#  [ 4  7]
#  [ 8 11]]

print(np.shares_memory(a[:, ::3], np.ravel(a[:, ::3])))
# False

print(np.shares_memory(a[:, ::3], np.reshape(a[:, ::3], -1)))
# False

ravel(), reshape()で異なる場合もある。ステップを指定したスライスでもstrideが一定だとreshape()はビューを返すが、ravel()はコピーを返す。

print(a[:, ::2])
# [[ 0  2]
#  [ 4  6]
#  [ 8 10]]

print(np.shares_memory(a[:, ::2], np.ravel(a[:, ::2])))
# False

print(np.shares_memory(a[:, ::2], np.reshape(a[:, ::2], -1)))
# True

公式ドキュメントのNotesにあるように、できる限りビューを返してほしい場合はravel()よりreshape(-1)のほうが適している。

When a view is desired in as many cases as possible, arr.reshape(-1) may be preferable.
numpy.ravel — NumPy v1.17 Manual

ravel()とflatten()の速度比較

コピーを返すflatten()はメモリを新たに確保する必要があるため、ravel()よりも遅い。

簡単なテスト結果を示す。

以下の例はJupyter Notebookのマジックコマンド%%timeitを利用しており、Pythonスクリプトとして実行しても計測されない。

a = np.arange(12).reshape(3, 4)
print(a)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

%%timeit
a.ravel()
# 242 ns ± 2.78 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

%%timeit
a.flatten()
# 725 ns ± 45.2 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

%%timeit
a.reshape(-1)
# 851 ns ± 13.5 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

上の例ぐらいの小さなnumpy.ndarrayであれば特に気にしなくてもよいが、サイズが大きくなるとflatten()は文字通り桁違いに時間がかかる。当然ながらメモリ使用量もflatten()のほうが多くなる。

ビューでも問題ないのであればravel()を使うほうが良いだろう。

a_large = np.arange(1000000).reshape(100, 100, 100)

%%timeit
a_large.ravel()
# 242 ns ± 3.6 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

%%timeit
a_large.flatten()
# 2.03 ms ± 8.63 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

%%timeit
a_large.reshape(-1)
# 899 ns ± 52 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

引数order

ravel(), flatten(), reshape()には引数orderを指定することができる。

デフォルトはorder='C'で、これまでの例のようにC言語ライクなrow-major(行優先)で平坦化されるが、order='F'とすると、Fortranライクなcolumn-major(列優先)となる。

a = np.arange(12).reshape(3, 4)
print(a)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

print(a.ravel())
# [ 0  1  2  3  4  5  6  7  8  9 10 11]

print(a.ravel('F'))
# [ 0  4  8  1  5  9  2  6 10  3  7 11]

print(np.ravel(a, 'F'))
# [ 0  4  8  1  5  9  2  6 10  3  7 11]

print(a.flatten('F'))
# [ 0  4  8  1  5  9  2  6 10  3  7 11]

print(a.reshape(-1, order='F'))
# [ 0  4  8  1  5  9  2  6 10  3  7 11]

print(np.reshape(a, -1, order='F'))
# [ 0  4  8  1  5  9  2  6 10  3  7 11]

引数orderには、reshape()では'C', 'F', 'A'ravel(), flatten()では'C', 'F', 'A', 'K'が指定可能。

'C', 'F'はメモリレイアウトは考慮されないが、'A', 'K'ではメモリレイアウトが考慮される。

Note that the ‘C’ and ‘F’ options take no account of the memory layout of the underlying array, and only refer to the order of axis indexing. ‘A’ means to read the elements in Fortran-like index order if a is Fortran contiguous in memory, C-like order otherwise. ‘K’ means to read the elements in the order they occur in memory, except for reversing the data when strides are negative. By default, ‘C’ index order is used.
numpy.ravel — NumPy v1.17 Manual

np.info()で確認できるnumpy.ndarrayの情報とともにorderによる結果の違いを示す。

例えばfortranTrueの場合は'A''F'と等しく、fortranFalseの場合は'A''C'と等しくなる。

np.info(a)
# class:  ndarray
# shape:  (3, 4)
# strides:  (32, 8)
# itemsize:  8
# aligned:  True
# contiguous:  True
# fortran:  False
# data pointer: 0x7fe081640f90
# byteorder:  little
# byteswap:  False
# type: int64

print(a.ravel('C'))
# [ 0  1  2  3  4  5  6  7  8  9 10 11]

print(a.ravel('F'))
# [ 0  4  8  1  5  9  2  6 10  3  7 11]

print(a.ravel('A'))
# [ 0  1  2  3  4  5  6  7  8  9 10 11]

print(a.ravel('K'))
# [ 0  1  2  3  4  5  6  7  8  9 10 11]

T属性による転置。

print(a.T)
# [[ 0  4  8]
#  [ 1  5  9]
#  [ 2  6 10]
#  [ 3  7 11]]

np.info(a.T)
# class:  ndarray
# shape:  (4, 3)
# strides:  (8, 32)
# itemsize:  8
# aligned:  True
# contiguous:  False
# fortran:  True
# data pointer: 0x7fe081640f90
# byteorder:  little
# byteswap:  False
# type: int64

print(a.T.ravel('C'))
# [ 0  4  8  1  5  9  2  6 10  3  7 11]

print(a.T.ravel('F'))
# [ 0  1  2  3  4  5  6  7  8  9 10 11]

print(a.T.ravel('A'))
# [ 0  1  2  3  4  5  6  7  8  9 10 11]

print(a.T.ravel('K'))
# [ 0  1  2  3  4  5  6  7  8  9 10 11]

負のステップのスライスによる反転。

print(a.T[::-1])
# [[ 3  7 11]
#  [ 2  6 10]
#  [ 1  5  9]
#  [ 0  4  8]]

np.info(a.T[::-1])
# class:  ndarray
# shape:  (4, 3)
# strides:  (-8, 32)
# itemsize:  8
# aligned:  True
# contiguous:  False
# fortran:  False
# data pointer: 0x7fe081640fa8
# byteorder:  little
# byteswap:  False
# type: int64

print(a.T[::-1].ravel('C'))
# [ 3  7 11  2  6 10  1  5  9  0  4  8]

print(a.T[::-1].ravel('F'))
# [ 3  2  1  0  7  6  5  4 11 10  9  8]

print(a.T[::-1].ravel('A'))
# [ 3  7 11  2  6 10  1  5  9  0  4  8]

print(a.T[::-1].ravel('K'))
# [ 3  2  1  0  7  6  5  4 11 10  9  8]

3次元以上の多次元配列の場合

これまでの例は2次元配列だが、3次元以上の多次元配列の場合も同様に一次元化(平坦化)できる。

a_3d = np.arange(24).reshape(2, 3, 4)
print(a_3d)
# [[[ 0  1  2  3]
#   [ 4  5  6  7]
#   [ 8  9 10 11]]
# 
#  [[12 13 14 15]
#   [16 17 18 19]
#   [20 21 22 23]]]

print(a_3d.ravel())
# [ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23]

print(a_3d.ravel('F'))
# [ 0 12  4 16  8 20  1 13  5 17  9 21  2 14  6 18 10 22  3 15  7 19 11 23]

上の例のように、多次元配列の場合は引数orderの影響がややこしいので注意。シンプルな例で確認してみてから使うのをオススメする。

スポンサーリンク
シェア
このエントリーをはてなブックマークに追加

関連カテゴリー

関連記事