Dive into Deep Learning
Dive into Deep Learning

データの取り扱い

データを操作できなければ、何も行うことはできません。一般的に、データを処理する上で重要なことは次の2点です。(i)データを獲得すること、および(ii)コンピュータに取り込んだらそれを処理することです。データの保存方法さえわからなければ、データを獲得することに意味がありません。合成データを利用して実際に触ってみましょう。まず、NDArrayというデータを保存・変換するためのMXNetの主要なツールを紹介します。 NumPyを以前に使用したことがあれば、NDArrayは設計上、NumPyの多次元配列に似ていることがわかります。ただし、NDArrayにはいくつかの重要な利点があります。まず、NDArrayは、CPU、GPU、および分散クラウドアーキテクチャでの非同期計算をサポートしています。第二に、それらは自動微分をサポートしています。これらの特性によって、NDArrayは深層学習に必要不可欠なものとなっています。

まずはじめに

この章を通して、読者の最初のステップを支援することを目的とし、基本的な機能について話を進めていきます。Element-wiseな演算や正規分布など、基本的な数学のすべてを理解していなくても心配しないでください。以降の2つの章では、同じコンテンツについて別の見方をし、実践的な例にもとづいてその内容を解説します。一方、数学的な内容を詳しく知りたい場合は、付録の“Math” のセクションを参照してください。

MXNetのインポートおよびMXNetから ndarrayモジュールのインポートから始めます。ここで、ndndarray の短縮形です。

In [1]:
import mxnet as mx
from mxnet import nd

NDArraysは数値の (多次元の) 配列を表します。 1軸のNDArrayは(数学的には)vectorに対応します。2軸のNDArrayは行列に対応します。3つ以上の軸を持つ配列に関しては、数学者は特別な名前を与えていません - 単にそれらをテンソルと呼びます。

作成できる最も単純なオブジェクトはベクトルです。まず始めに、 arangeを使って12個の連続した整数をもつ行ベクトルを作りましょう。

In [2]:
x = nd.arange(12)
x
Out[2]:

[ 0.  1.  2.  3.  4.  5.  6.  7.  8.  9. 10. 11.]
<NDArray 12 @cpu(0)>

xを標準出力すると<NDArray 12 @cpu(0)>というプロパティを見ることができます。これはxが長さ12の1次元配列であり、それがCPUのメインメモリにあることを示します。 @cpu(0)の0は特別な意味を持たず、特定のコアを表すものでもありません。

NDArrayインスタンスの形状は shapeのプロパティを利用して確認することができます。

In [3]:
x.shape
Out[3]:
(12,)

sizeのプロパティから、NDArrayインスタンスの要素の総数を得ることもできます。これはshapeの要素の積となります。ここではベクトルを扱っているので、sizeshapeも同じ数になります。

In [4]:
x.size
Out[4]:
12

ある一つの(多次元の)配列のshapeを、同じ数の要素を含む別のものに変えるためにはreshape関数を使います。 たとえば、行ベクトルxのshapeを(3, 4)に変換できます。これは同じ値を含みますが、3行4列の行列として解釈されます。shapeは変わっていますが、xの要素は変わっていないことに注意してください。sizeは同じままです。

In [5]:
x = x.reshape((3, 4))
x
Out[5]:

[[ 0.  1.  2.  3.]
 [ 4.  5.  6.  7.]
 [ 8.  9. 10. 11.]]
<NDArray 3x4 @cpu(0)>

各次元をそれぞれ手動で指定してreshapeすることは面倒なことがあります。一方の次元がわかっていれば、もう一方の次元を決定するために、わざわざ割り算を実行する必要があるでしょうか? たとえば、上の例では、3行の行列を取得するために、4列をもつように別途指定する必要がありました(12要素を考慮して)。幸いなことに、NDArrayは自動的に一方の次元から他方の次元を決定することができます。 NDArrayに自動的に推測させたい次元に -1を配置します。さきほどの例では x.reshape((3, 4))の代わりに、 x.reshape((-1, 4))または x.reshape((3,-1))を使用することが可能です。

In [6]:
nd.empty((3, 4))
Out[6]:

[[-2.5510793e-17  4.5554812e-41 -1.8794926e+25  3.0876210e-41]
 [ 0.0000000e+00  0.0000000e+00  0.0000000e+00  0.0000000e+00]
 [ 0.0000000e+00  0.0000000e+00  0.0000000e+00  0.0000000e+00]]
<NDArray 3x4 @cpu(0)>

emptyのメソッドは、いくらかのメモリを確保して、その要素に対していずれの値も設定せずに行列を返します。これは非常に効率的ですが、各要素は非常に大きな値も含め、任意の値を取る可能性があります。通常は、行列を、1、ゼロ、既知の定数、または既知の分布から無作為に抽出された数値のいずれかで初期化しようとするでしょう。

そして、ほとんどの場合、すべてゼロの配列を必要とするでしょう。すべての要素が0、shapeが(2,3,4)であるようなテンソルを表すNDArrayを作成するには、以下を実行します。

In [7]:
nd.zeros((2, 3, 4))
Out[7]:

[[[0. 0. 0. 0.]
  [0. 0. 0. 0.]
  [0. 0. 0. 0.]]

 [[0. 0. 0. 0.]
  [0. 0. 0. 0.]
  [0. 0. 0. 0.]]]
<NDArray 2x3x4 @cpu(0)>

すべての要素が1であるようなテンソルを作成するためには以下を実行します。

In [8]:
nd.ones((2, 3, 4))
Out[8]:

[[[1. 1. 1. 1.]
  [1. 1. 1. 1.]
  [1. 1. 1. 1.]]

 [[1. 1. 1. 1.]
  [1. 1. 1. 1.]
  [1. 1. 1. 1.]]]
<NDArray 2x3x4 @cpu(0)>

数値の値を含む Python のリストを与えることで、特定の値を要素にもつNDArrayを作成することもできます。

In [9]:
y = nd.array([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
y
Out[9]:

[[2. 1. 4. 3.]
 [1. 2. 3. 4.]
 [4. 3. 2. 1.]]
<NDArray 3x4 @cpu(0)>

場合によっては、既知の確率分布に従って、NDArrayの各要素の値をランダムにサンプリングすることもあるでしょう。これは、ニューラルネットワークにおけるパラメータとして、配列を使用しようとする際に特に一般的に行われています。次のスニペットは、(3, 4)の形状をもつNDArrayを作成します。その要素は平均がゼロで分散が1の正規分布から無作為にサンプリングされた値をもちます。

In [10]:
nd.random.normal(0, 1, shape=(3, 4))
Out[10]:

[[ 2.2122064   0.7740038   1.0434405   1.1839255 ]
 [ 1.8917114  -1.2347414  -1.771029   -0.45138445]
 [ 0.57938355 -1.856082   -1.9768796  -0.20801921]]
<NDArray 3x4 @cpu(0)>

演算

配列に対して関数を適用したいときは多いと思います。最も単純かつ便利な機能として要素ごとの (element-wise)の機能が挙げられます。これらは、2つの配列の対応する要素に対して、単一のスカラー演算を実行します。スカラーからスカラーへ写像するあらゆる関数に対して、element-wiseな関数を作成することができます。数学的な記法を使うと、\(f: \mathbb{R} \rightarrow \mathbb{R}\) といった記述になります。同じshapeの2つのベクトル\(\mathbf{u}\)\(\mathbf{v}\)、関数\(f\)が与えられているとき、すべての\(i\)に対して、\(c_i \gets f(u_i, v_i)\) となるようなベクトル\(\mathbf{c} = F(\mathbf{u},\mathbf{v})\)を作成することができます。

ここで、スカラー関数をelement-wiseなベクトル演算に置き換えることで、ベクトル値関数\(F: \mathbb{R}^d \rightarrow \mathbb{R}^d\)を作成することもできます。MXNetでは、基本的な数式演算である (+,-,/,*,**) はすべて、任意のshapeに対して、shapeが同じテンソルであれば、element-wiseな演算に置き換えることが可能です。同じ shapeをもつ2つのテンソルおよび行列に対して、element-wiseな演算を行うことができます。

In [11]:
x = nd.array([1, 2, 4, 8])
y = nd.ones_like(x) * 2
print('x =', x)
print('x + y', x + y)
print('x - y', x - y)
print('x * y', x * y)
print('x / y', x / y)
x =
[1. 2. 4. 8.]
<NDArray 4 @cpu(0)>
x + y
[ 3.  4.  6. 10.]
<NDArray 4 @cpu(0)>
x - y
[-1.  0.  2.  6.]
<NDArray 4 @cpu(0)>
x * y
[ 2.  4.  8. 16.]
<NDArray 4 @cpu(0)>
x / y
[0.5 1.  2.  4. ]
<NDArray 4 @cpu(0)>

より多くの演算をelement-wiseに適用することも可能です。例えば指数関数の場合は:

In [12]:
x.exp()
Out[12]:

[2.7182817e+00 7.3890562e+00 5.4598148e+01 2.9809580e+03]
<NDArray 4 @cpu(0)>

要素ごとの計算に加えて、dot関数を使った行列の乗算のような行列演算も実行できます。以下では、xyの転置に対して、行列の演算を実行します。 xを3行4列の行列として定義し、yを4行3列の行列に転置します。 2つの行列の掛け算を計算することで3行3列の行列が得られます (これが何を意味するのか混乱していても心配しないでください。線形代数の章で行列演算についてさらに詳しく説明します。)

In [13]:
x = nd.arange(12).reshape((3,4))
y = nd.array([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
nd.dot(x, y.T)
Out[13]:

[[ 18.  20.  10.]
 [ 58.  60.  50.]
 [ 98. 100.  90.]]
<NDArray 3x3 @cpu(0)>

複数のNDArrayを結合することもできます。そのためには、どの次元で結合するかをシステムに伝える必要があります。以下の例は、次元0 (行に沿って) と次元1 (列に沿って) に沿って2つの行列をそれぞれ結合します。

In [14]:
nd.concat(x, y, dim=0)
nd.concat(x, y, dim=1)
Out[14]:

[[ 0.  1.  2.  3.  2.  1.  4.  3.]
 [ 4.  5.  6.  7.  1.  2.  3.  4.]
 [ 8.  9. 10. 11.  4.  3.  2.  1.]]
<NDArray 3x8 @cpu(0)>

ときには、論理式を使って2値のNDArrayを作成したいと思うかもしれません。例えば x == yを取り上げましょう。ある、要素に関してxyが等しい場合、新しく作成されるNDArrayにおいて、その要素と同じ位置には1の値が入ります。それ以外の場合は0です。

In [15]:
x == y
Out[15]:

[[0. 1. 0. 1.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]]
<NDArray 3x4 @cpu(0)>

NDArrayにおける全要素の和を計算すると、その和だけを唯一の要素としてもつNDArrayを生成します。

In [16]:
x.sum()
Out[16]:

[66.]
<NDArray 1 @cpu(0)>

asscalar関数を使って、結果をPythonのスカラーに変換することができます。次の例では、x\(\ell_2\)ノルムが、単一の要素をもつNDArrayを生成し、その結果はasscalarによってスカラーに変換されます。

In [17]:
x.norm().asscalar()
Out[17]:
22.494442

またプログラミングの利便性から、y.exp(), x.sum(), x.norm()と書くこともできますし、 nd.exp(y), nd.sum(x), nd.norm(x)と書くこともできます。

Broadcast の仕組み

上記の節では、同じshapeをもつ、2つのNDArrayに対する演算について説明しました。shapeが異なる場合は、NumPyと同様にBroadcastingが実行されます。まず、2つのNDArrayが同じ形状になるように要素を適切にコピーしてから、要素ごとに演算を実行します。

In [18]:
a = nd.arange(3).reshape((3, 1))
b = nd.arange(2).reshape((1, 2))
a, b
Out[18]:
(
 [[0.]
  [1.]
  [2.]]
 <NDArray 3x1 @cpu(0)>,
 [[0. 1.]]
 <NDArray 1x2 @cpu(0)>)

abはそれぞれ(3x1)と(1x2)の行列なので、これらの加算を行おうと思っても、shapeが互いに一致しません。 NDArrayは、両方の行列の要素を次のようにBroadcastすることで、より大きな(3×2)行列を生成し、これに対処します。行列aに対しては列を複製し、行列bに対しては行を複製し、最後に要素ごとに加算します。

In [19]:
a + b
Out[19]:

[[0. 1.]
 [1. 2.]
 [2. 3.]]
<NDArray 3x2 @cpu(0)>

Indexing と Slicing

他のPython配列と同じように、NDArrayの要素はそのインデックスによってアクセスできます。 Pythonでは伝統的に、最初の要素のインデックスは0で、範囲を最初の要素を含んで最後の要素は含まないように指定します。つまりは1:3で指定される範囲は、2番目と3番目の要素を選択します (インデックス1と2が選ばれ、それぞれ2番めと3番めの要素)。行列のそれぞれの行を選択して試してみましょう。

In [20]:
x[1:3]
Out[20]:

[[ 4.  5.  6.  7.]
 [ 8.  9. 10. 11.]]
<NDArray 2x4 @cpu(0)>

上記で説明したように、行列の要素に値を書き込むこともできます。

In [21]:
x[1, 2] = 9
x
Out[21]:

[[ 0.  1.  2.  3.]
 [ 4.  5.  9.  7.]
 [ 8.  9. 10. 11.]]
<NDArray 3x4 @cpu(0)>

複数の要素に同じ値を割り当てたい場合は、それらのすべてにインデックスに対して値を割り当てれば良いです。例えば、 [0:2,:]は1行目と2行目にアクセスします。以下では、それらの行に対して12を割り当てます。行列のindexingについて説明しましたが、いうまでもなくベクトルや2次元以上のテンソルに対しても同様のことが機能します。

In [22]:
x[0:2, :] = 12
x
Out[22]:

[[12. 12. 12. 12.]
 [12. 12. 12. 12.]
 [ 8.  9. 10. 11.]]
<NDArray 3x4 @cpu(0)>

メモリの節約

前に紹介した例では、演算を実行するたびに、その結​​果を格納するために新しいメモリを割り当てていました。たとえば、 y = x + yと書くと、もともとyが指していた行列を間接参照し、代わりに新しく割り当てられたメモリを指します。以下の例では、Pythonのid()関数という、メモリの参照オブジェクトの正確なアドレスを返す関数を使って実際に説明します。y = y + xを実行した後、id(y)が別の場所を指していることがわかります。これは、Pythonが最初にy + xを評価し、その結果に新しいメモリを割り当て、それからメモリ内のこの新しい位置を指すようにyをリダイレクトしているからです。

In [23]:
before = id(y)
y = y + x
id(y) == before
Out[23]:
False

これが望まれない場合として、以下の2つが挙げられます。第一に、私たちは常に不必要なメモリ割り当てを行いたくありません。 機械学習では、数百メガバイトのパラメータがあり、1秒のうちにそれらすべてを複数回更新します。 通常は、これらの更新をその場で実行(in-place)します。 第二に、同じパラメータは複数の変数が参照しているかもしれません。 適切に更新しないと、メモリリークが発生し、誤って古いパラメータを参照する可能性があります。

幸運にも、in-placeな演算は、MXNetでは簡単に行うことができます。sliceを利用して以前に確保された配列に対して、演算の結果を割り当てることができます。つまり、y[:] = とします。この挙動を示すために、0の要素ブロックを割り当てるzero_likeを利用して、行列のshapeをコピーします。

In [24]:
z = y.zeros_like()
print('id(z):', id(z))
z[:] = x + y
print('id(z):', id(z))
id(z): 139627801941648
id(z): 139627801941648

これはきれいに見えますが、ここでのx + y'はそれをy[:]にコピーする前にx + yの結果を格納するための一時バッファを割り当てます。 さらにメモリを有効利用するためには、一時的なバッファを避けるような基盤的なndarrayを直接利用することができ、この場合はelemwise_add`を利用します。

基礎となる ndarray操作、この場合はelemwise_addを直接呼び出すことができます。 そして、すべてのndarrayの演算がサポートしている、outというキーワードの引数を指定することで、以上のことを実現することができます。

In [25]:
before = id(z)
nd.elemwise_add(x, y, out=z)
id(z) == before
Out[25]:
True

もし、xの値が以降の計算において再利用されないのであれば、その演算のオーバーヘッドを削減するためにx[:] = x + y or x += y とすることも可能です。

In [26]:
before = id(x)
x += y
id(x) == before
Out[26]:
True

NDArrayとNumpyの相互変換

MXNet NDArrayとNumPyとの間の変換は容易です。変換された配列はメモリを共有しません。 これは少し不便に感じるかもしれませんが、実は非常に重要です。CPUまたは複数GPUの1つで演算を実行する際、NumPyで何か実行する場合、同じメモリ領域でMXNetがその処理を待つということは望ましくありません。変換自体はarrayasnumpyの関数によって行えます。

In [27]:
import numpy as np

a = x.asnumpy()
print(type(a))
b = nd.array(a)
print(type(b))
<class 'numpy.ndarray'>
<class 'mxnet.ndarray.ndarray.NDArray'>

練習

  1. この節のコードを実行しましょう。この節の条件文 x == yx < yまたはx > yに変更して、どのようなNDArrayを得られるか確認してください。
  2. Broadcastの仕組みで要素ごとの演算を行った2つのNDArraysを別のshapeに変えてみましょう。例えば、三次元テンソルです。結果は予想と同じでしょうか?
  3. 3つの行列a, b, cがあるとします。 コードc = nd.dot(a, b.T)+ cを、最もメモリ効率の良い方法に書き換えてください。

議論のためのQRコードをスキャン

image0