NumPyの配列要素を複製した際のオブジェクトはコピー(copy)とビュー(view)に分類することができます。この2つについての理解が深まると、メモリ効率や速度を意識したコードが書けるようになります。
本記事を通して、copyとviewについての理解を深めましょう。
そもそもcopyとviewとは何なのか
copy
とview
というのはオブジェクトであり、元となる配列との関係性で定まります。両者の特徴をまとめると以下のようになります。
- copy:元の配列と違うメモリを使用しているが要素が同一の意味を持つもの
- view:元の配列と同じメモリを参照しているもの
と呼びます。view
は同じメモリを参照しているためview
にあたる配列の要素が変更されると元の配列にも変更が反映されることになります。
In [1]: import numpy as np
In [2]: a = np.array([1,2,3])
In [3]: d = a.view() # viewを作成する
In [4]: d[0] = 100 # viewの値を1つ変更する。
In [5]: d
Out[5]: array([100, 2, 3])
In [6]: a # 元の配列であるaにも変更が反映される。
Out[6]: array([100, 2, 3])
copy
だと変更は反映されません。
In [7]: c = a.copy()
In [8]: c[1] = 25
In [9]: c
Out[9]: array([100, 25, 3])
In [10]: a
Out[10]: array([100, 2, 3])
メモリ効率という観点では大きな配列になればなるほど、view
をなるべく使った方が良いことになります。しかし、元の配列のデータを変えてしまう可能性が非常に高く、元のデータを保持しておきたいという場合にはcopy
を使いましょう。
操作による違い
NumPyの操作ではcopy
とview
が使い分けられています。内部操作がcopy
となるのかview
を生成するのかを知っていることで、さらに効率を意識したコードを書くことができるのでまとめます。
代入
まずは変数に配列そのものを格納する場合についてです。
=
を使って代入していくわけですが、Pythonにおける変数への代入というのはオブジェクトへの参照を格納するということになります。つまり、代入された変数がオブジェクトになるのではなく、変数はあくまでオブジェクトがある場所を示すものとなっています。
In [11]: a = np.array([1, 2, 3])
In [12]: b = a # bにaを代入
In [13]: id(a) == id(b) # 参照元が同じかどうかを確かめる
Out[13]: True
id
関数はオブジェクトの’識別値’を返す関数です。
ここで、変数へ代入するときにa
をa[:]
と表記を変えてみます。PythonのlistだとcopyになりますがNumPyのndarrayではviewとなります。
In [20]: a = np.array([1,2,3])
In [21]: c = a[:] # 表記を変えてみる
In [22]: id(a) == id(c) # 違うオブジェクトを参照しているのがわかる。
Out[22]: False
In [23]: c[1] = 22 # cの一部を変更
In [24]: a # 変更がaにも反映されている。
Out[24]: array([ 1, 22, 3])
このように、スライス表記ですと元の配列と異なるオブジェクトが生成されますがそのオブジェクトの要素は元の配列のviewとなります。もちろん、スライス表記なので一部の配列を抜き出すようにしてもviewが生成されます。
In [25]: d = a[:1]
In [26]: id(d) == id(a)
Out[26]: False
In [27]: d[0] = 11
In [28]: a
Out[28]: array([11, 22, 3])
In [29]: d
Out[29]: array([11])
代入でコピーを作成するには元の配列のコピーを代入します。
In [36]: e = a.copy() # aのコピーを代入
In [37]: e.base is a
Out[37]: False
In [38]: e[2] = 234
In [39]: e
Out[39]: array([ 11, 22, 234])
In [40]: a
Out[40]: array([ 11, 22, 3])
base
属性(attribute)はベースとなるオブジェクトが存在した場合そのベースとなるオブジェクトを表示するものです。オブジェクトがviewでない限りこれの値はNone
となります。
In [41]: print(a.base)
None
コピーを作成したい場合は、copy
関数を使いましょう。
In [42]: f = a.copy()
In [43]: f.base is a
Out[43]: False
演算
演算結果について、表記の仕方でコピーが作られる場合と作られない場合が存在します。
例えば、足し算をする場合を考えてみます。a = a + 1
とするとaのcopyが生成され、各要素に1を加算した新たな配列がaに代入されます。
In [45]: a = np.array([1,2,3])
In [46]: c = a # viewとなるcをつくる。
In [47]: a = a + 1 # 1を加算する
In [48]: c # cに変更が反映されない。
Out[48]: array([1, 2, 3])
In [49]: a
Out[49]: array([2, 3, 4])
a
のviewとなるc
に変更が反映されていません。a
の参照しているオブジェクトが新しく生成されたため、a
とc
のviewの参照先が異なることを意味します。
a += 1
の形にするとどうなるでしょうか。この場合はa
のコピーが作られることなく、配列の値が1ずつ加算される形で更新されます。
In [50]: a = np.array([1,2,3])
In [51]: c = a
In [52]: a += 1
In [53]: c
Out[53]: array([2, 3, 4])
In [54]: a
Out[54]: array([2, 3, 4])
これは四則演算子である+
,-
,*
,/
や累乗を計算する**
においても同様のことが言えます。
コピーを作らずに演算を行う方法として演算を行う関数を使うこともできます。加算ならadd
関数、減算ならsubtract
関数が使えます。
数学的関数については以下の記事で色々紹介しているので参考にしてみてください。
NumPyの数学関数・定数まとめ - DeepAge /features/numpy-math.html
これらの演算関数の引数にはout
というものがあり、ここに元となる配列を指定すると配列の値を上書きすることができ、メモリ効率をあげることができます。コピーを作らないだけで計算パフォーマンスも向上します。
In [66]: a = np.array([1,2,3])
In [67]: c = a
In [68]: np.add(a,1, out=a)
Out[68]: array([2, 3, 4])
In [69]: a
Out[69]: array([2, 3, 4])
In [70]: c
Out[70]: array([2, 3, 4])
では、計算速度を比較してみましょう。2つの配列X
、Y
を作り以下のように表されるA
の値を計算するとします。
A = X*4 + Y*3
様々なやり方で計算してみましょう。以下のような関数test()を使って計測します。
def test():
import time
X = np.ones(100000000, dtype='int8')
Y = np.ones(100000000, dtype='int8')
a = time.time()
for _ in range(100): # 方法1
X = X*4 + Y*3
X = np.ones(100000000, dtype='int8')
b = time.time()
print('X = X*4 + Y*3: {} sec'.format((b-a)/100))
a = time.time()
for _ in range(100): # 方法2
X *= 4
X += Y*3
X = np.ones(100000000, dtype='int8')
b = time.time()
print('X *= 4; X += Y*3: {} sec'.format((b-a)/100))
a = time.time()
for _ in range(100): # 方法3
np.multiply(X, 4, out=X)
np.multiply(Y, 3, out=Y)
np.add(X, Y, out=X)
X = np.ones(100000000, dtype='int8')
b = time.time()
print("using functions: {} sec".format((b-a)/100))
これを実行します。
In [111]: test()
X = X*4 + Y*3: 0.15649971961975098 sec
X *= 4; X += Y*3: 0.11750322103500366 sec
using functions: 0.08618597984313965 sec
関数を使って計算した方が早く計算ができることがわかりますね。
配列の一次元化
配列を一次元化する関数としてflatten
関数とravel
関数が存在します。flatten
関数では配列の一次元化したcopyを作成してそれへの参照が返されます。一方、ravel
関数では読み込むメモリーの場所は元の配列と同じビュー(view)を返します。
ravel
関数はコピーを作成しないため、コピーを作成するflatten
関数より処理速度が速い特徴があります。詳しい解説は以下の記事で行なっているので使い方などが気になる方は参照してみてください。
配列を1次元に変換するNumPyのflatten関数の使い方 - DeepAge /features/numpy-flatten.html
flattenよりも高速に配列を一次元化するnumpy.ravel関数の使い方 - DeepAge /features/numpy-ravel.html
In [1]: import numpy as np
In [2]: a = np.random.randn(2,3,9)
In [3]: b = a.ravel()
In [4]: c = a.flatten()
In [5]: a
Out[5]:
array([[[-0.08174386, 0.26572468, 0.567573 , -1.38605284, 0.38670771,
1.18548385, 0.60952909, -0.73427919, 0.1827606 ],
[ 1.56150254, 0.78448663, 0.79470278, -1.68160908, 1.65006098,
0.30753166, 0.26845956, -0.61851187, 0.43305801],
[ 0.46361119, -1.8501969 , 0.67521371, 1.5689528 , 0.76623727,
0.61117491, 2.10937054, -0.11104958, 0.0083282 ]],
[[ 0.20375639, 1.95139355, -0.68546597, 1.08584989, 1.69015733,
-0.48769387, 2.2704154 , -0.70044442, -0.3769095 ],
[ 0.12435112, -0.58086285, 1.35984034, 0.38650113, -0.86224261,
0.21651296, 1.46182258, -1.05273576, -0.65269428],
[-0.82453928, -0.47707194, 1.57473802, -0.40136323, 2.25313411,
0.31413172, -0.37410255, -0.38898 , -0.99284404]]])
In [6]: a[0,0,0] = 129
In [7]: a[0,0,0], b[0], c[0]
Out[7]: (129.0, 129.0, -0.081743863020672075)
fancy indexing
特殊なインデックス指定を用いると基本的にコピーが生成されます。
In [1]: import numpy as np
In [3]: a = np.random.randint(10,size=100)
In [4]: a
Out[4]:
array([2, 5, 8, 4, 2, 3, 8, 6, 7, 4, 2, 5, 7, 7, 5, 1, 9, 5, 8, 7, 7, 4, 9,
4, 8, 4, 4, 4, 2, 0, 9, 6, 9, 2, 9, 5, 6, 8, 9, 6, 0, 9, 2, 6, 4, 9,
8, 7, 5, 7, 1, 0, 2, 0, 1, 6, 5, 9, 9, 2, 4, 0, 3, 2, 1, 7, 6, 4, 6,
3, 0, 7, 2, 6, 0, 0, 2, 8, 8, 3, 3, 5, 8, 8, 4, 7, 9, 7, 7, 8, 1, 2,
3, 0, 2, 7, 8, 2, 3, 1])
In [5]: n = a%3==0 # 3の倍数であるもののところがTrueになる。
In [6]: n
Out[6]:
array([False, False, False, False, False, True, False, True, False,
False, False, False, False, False, False, False, True, False,
False, False, False, False, True, False, False, False, False,
False, False, True, True, True, True, False, True, False,
True, False, True, True, True, True, False, True, False,
True, False, False, False, False, False, True, False, True,
False, True, False, True, True, False, False, True, True,
False, False, False, True, False, True, True, True, False,
False, True, True, True, False, False, False, True, True,
False, False, False, False, False, True, False, False, False,
False, False, True, True, False, False, False, False, True, False], dtype=bool)
In [7]: a[n] # 3の倍数の要素を抽出
Out[7]:
array([3, 6, 9, 9, 0, 9, 6, 9, 9, 6, 9, 6, 0, 9, 6, 9, 0, 0, 6, 9, 9, 0, 3,
6, 6, 3, 0, 6, 0, 0, 3, 3, 9, 3, 0, 3])
In [8]: k = a[n]
In [9]: np.may_share_memory(a,k) # メモリを共有しているかどうかを確かめる関数。後ほど解説。
Out[9]: False
In [10]: f = a[np.arange(0,10,2)]
In [11]: np.may_share_memory(a,f)
Out[11]: False
copyかviewか確かめる方法
値を毎回代入してcopyかどうかを確かめるのは大変です。そうしなくても確かめる方法がいくつかあります。
may_share_memory
関数を使う方法
一番簡単な方法として、NumPyにあるmay_share_memory
関数で確かめる方法があります。
この関数は、引数として指定された2つの配列が同じメモリーを参照しているかどうかを確かめるものです。この判定は実は厳密なものではなく、True
が返ってきても必ずしもメモリを共有しているとは限りませんが基本的に信用して大丈夫です。
False
が出た場合、その結果は信頼できます(間違ったTrue
判定を出すときはあるが間違ったFalse
判定を出すことがない)。このようになっているのは恐らく、本来ビューのものをコピーと間違って扱う方が致命的なミスにつながりかねないが、逆にビューだと間違って扱う方が元のデータ構造を破壊するなどのミスをすることはないからだと思われます。
より厳密に知りたいかたはshare_memory
関数が実装されているのでそちらを使うことをオススメします。デメリットとしてmay_share_memory
関数より処理時間がかかるというものがありますが。
In [1]: import numpy as np
In [2]: a = np.array([1,2,3])
In [4]: b = a
In [5]: c = b
In [6]: d = a.copy()
In [8]: np.may_share_memory(a,b) # Trueならbはaのビューだということになる。
Out[8]: True
In [9]: np.may_share_memory(a,c) # こちらもTrue
Out[9]: True
In [10]: np.may_share_memory(a, d) # dはaのコピーなのでFalse
Out[10]: False
In [12]: np.shares_memory(a,b) # より厳密な測定にはshare_memory関数を使う
Out[12]: True
In [13]: np.shares_memory(a,d)
Out[13]: False
base属性(attribute)を使う方法
他にも、その配列がviewであるなら参照元の配列を返すbase
属性(attribute)というものがあります。これは1つ上の配列を返すだけなのでviewのviewのbase
はオリジナルの配列と一致せず、1つ前のビューを返すことになってしまいます。
そのため、若干使い勝手が悪くなります。
copyかviewかを確かめるためだけであったらbase
を使って確かめる方法はあまりオススメできません。