2.1. データ操作¶
何らかの実用的な処理を行うには、データを保存し、操作するための方法が必要になる。一般的に、データに対して行うべき重要なことは2つある。(i)
データを取得すること、そして (ii)
取得したデータをコンピュータに取り込んだ後に処理することである。データを保存する方法がなければ、データを取得しても意味がない。そこでまずは、テンソルとも呼ばれる
\(n\) 次元配列の操作に慣れていきよう。NumPy
のような科学技術計算パッケージをすでに知っているなら、これは簡単だろう。現代のあらゆる深層学習フレームワークにおいて、テンソルクラス(MXNet
では ndarray、PyTorch と TensorFlow では Tensor)は NumPy の
ndarray
に似ているが、いくつかの強力な機能が追加されている。第一に、テンソルクラスは自動微分をサポートしている。第二に、NumPy
が CPU 上でしか動作しないのに対し、テンソルは GPU
を活用して数値計算を高速化できる。これらの特性により、ニューラルネットワークは実装しやすく、かつ高速に実行できるようになる。
2.1.1. はじめに¶
まず、PyTorch ライブラリをインポートする。パッケージ名は torch
であることに注意しよ。
import torch
from mxnet import np, npx
npx.set_np()
import jax
from jax import numpy as jnp
import tensorflow as tf
PyTorch
には、値をあらかじめ入れた新しいテンソルを作成するためのさまざまな関数がある。たとえば
arange(n) を呼び出すと、0(含む)から
n(含まない)までの等間隔の値を持つベクトルを作成できる。デフォルトでは、間隔の大きさは
\(1\)
である。特に指定しない限り、新しいテンソルは主記憶に保存され、CPU
ベースの計算に割り当てられる。
x = torch.arange(12, dtype=torch.float32)
x
tensor([ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11.])
x = np.arange(12)
x
[07:03:50] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU
array([ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11.])
x = jnp.arange(12)
x
No GPU/TPU found, falling back to CPU. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)
Array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], dtype=int32)
x = tf.range(12, dtype=tf.float32)
x
<tf.Tensor: shape=(12,), dtype=float32, numpy=
array([ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11.],
dtype=float32)>
これらの各値はテンソルの 要素 と呼ばれる。テンソル x には 12
個の要素がある。テンソル内の要素数の総数は numel
メソッドで確認できる。
x.numel()
12
x.size
12
x.size
12
tf.size(x)
<tf.Tensor: shape=(), dtype=int32, numpy=12>
(テンソルの shape(各軸に沿った長さ))は shape
属性を調べることで取得できる。ここではベクトルを扱っているので、shape
には1つの要素しか含まれず、サイズと同じである。
x.shape
torch.Size([12])
reshape
を呼び出すことで、サイズや値を変えずにテンソルの形状を変更できる。たとえば、形状が
(12,) のベクトル x を、形状が (3, 4) の行列 X
に変換できる。この新しいテンソルはすべての要素を保持したまま、それらを行列として再配置する。ベクトルの要素は1行ずつ並んでいるので、x[3] == X[0, 3]
となることに注意しよ。
X = x.reshape(3, 4)
X
tensor([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.]])
X = x.reshape(3, 4)
X
array([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.]])
X = x.reshape(3, 4)
X
Array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]], dtype=int32)
X = tf.reshape(x, (3, 4))
X
<tf.Tensor: shape=(3, 4), dtype=float32, numpy=
array([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.]], dtype=float32)>
reshape に対して各 shape
成分をすべて明示するのは冗長であることに注意しよ。テンソルのサイズはすでに分かっているので、他の成分が分かれば
shape の1成分を求められる。たとえば、サイズが \(n\) で目標の shape
が (\(h\), \(w\)) のテンソルでは、\(w = n/h\)
であることが分かりる。shape
の成分を自動的に推定させるには、自動推定されるべき成分に -1
を指定する。ここでは、x.reshape(3, 4) の代わりに
x.reshape(-1, 4) や x.reshape(3, -1) と書いても同じである。
実務では、すべて 0 や 1
で初期化されたテンソルを扱うことがよくある。すべての要素が 0
に設定されたテンソルを構築する([STRIKEOUT:または
1])には、zeros 関数を使って shape を (2, 3, 4) にする。
torch.zeros((2, 3, 4))
tensor([[[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]],
[[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]]])
np.zeros((2, 3, 4))
array([[[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]],
[[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]]])
jnp.zeros((2, 3, 4))
Array([[[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]],
[[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]]], dtype=float32)
tf.zeros((2, 3, 4))
<tf.Tensor: shape=(2, 3, 4), dtype=float32, numpy=
array([[[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]],
[[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]]], dtype=float32)>
同様に、ones を呼び出すことで、すべて 1 のテンソルを作成できる。
torch.ones((2, 3, 4))
tensor([[[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]],
[[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]]])
np.ones((2, 3, 4))
array([[[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]],
[[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]]])
jnp.ones((2, 3, 4))
Array([[[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]],
[[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]]], dtype=float32)
tf.ones((2, 3, 4))
<tf.Tensor: shape=(2, 3, 4), dtype=float32, numpy=
array([[[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]],
[[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]]], dtype=float32)>
しばしば、与えられた確率分布から各要素をランダムに(かつ独立に)サンプリングする必要がある。たとえば、ニューラルネットワークのパラメータはしばしばランダムに初期化される。次のコード片は、平均 0、標準偏差 1 の標準ガウス(正規)分布から要素を抽出したテンソルを作成する。
torch.randn(3, 4)
tensor([[ 1.5124, 0.2588, 0.5924, 0.2041],
[-0.9516, 1.5309, 0.7024, 0.0892],
[ 1.4337, -0.3017, -1.4316, 0.5385]])
np.random.normal(0, 1, size=(3, 4))
array([[ 2.2122064 , 1.1630787 , 0.7740038 , 0.4838046 ],
[ 1.0434403 , 0.29956347, 1.1839255 , 0.15302546],
[ 1.8917114 , -1.1688148 , -1.2347414 , 1.5580711 ]])
# Any call of a random function in JAX requires a key to be
# specified, feeding the same key to a random function will
# always result in the same sample being generated
jax.random.normal(jax.random.PRNGKey(0), (3, 4))
Array([[ 1.1901639 , -1.0996888 , 0.44367844, 0.5984697 ],
[-0.39189556, 0.69261974, 0.46018356, -2.068578 ],
[-0.21438177, -0.9898306 , -0.6789304 , 0.27362573]], dtype=float32)
tf.random.normal(shape=[3, 4])
<tf.Tensor: shape=(3, 4), dtype=float32, numpy=
array([[ 0.01757281, -1.1219722 , 0.82192016, -0.35135165],
[-0.21407793, -0.0028088 , -1.709675 , -1.2871331 ],
[ 0.56736195, 1.1376742 , -0.1567428 , 1.146607 ]],
dtype=float32)>
最後に、数値リテラルを含む(場合によっては入れ子になった)Python のリストを与えることで、各要素の正確な値を指定してテンソルを構築できる。ここでは、リストのリストを使って行列を構築している。最外側のリストが軸 0 に対応し、内側のリストが軸 1 に対応する。
torch.tensor([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
tensor([[2, 1, 4, 3],
[1, 2, 3, 4],
[4, 3, 2, 1]])
np.array([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
array([[2., 1., 4., 3.],
[1., 2., 3., 4.],
[4., 3., 2., 1.]])
jnp.array([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
Array([[2, 1, 4, 3],
[1, 2, 3, 4],
[4, 3, 2, 1]], dtype=int32)
tf.constant([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
<tf.Tensor: shape=(3, 4), dtype=int32, numpy=
array([[2, 1, 4, 3],
[1, 2, 3, 4],
[4, 3, 2, 1]], dtype=int32)>
2.1.2. インデックス付けとスライシング¶
Python のリストと同様に、インデックス付け(0
から始まる)によってテンソルの要素にアクセスできる。リストの末尾からの位置に基づいて要素にアクセスするには、負のインデックスを使う。さらに、スライシング(たとえば
X[start:stop])によってインデックスの範囲全体にアクセスできる。このとき返される値には最初のインデックス(start)は含まれるが、最後のインデックス(stop)は含まれない。最後に、\(k\)-階テンソル
に対して1つのインデックス(またはスライス)だけを指定した場合、それは軸
0 に沿って適用される。したがって、次のコードでは、[-1]
は最後の行を選択し、[1:3] は2行目と3行目を選択する。
X[-1], X[1:3]
(tensor([ 8., 9., 10., 11.]),
tensor([[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.]]))
読み取るだけでなく、(インデックスを指定して行列の要素を 書き込む こともできる。)
X[1, 2] = 17
X
tensor([[ 0., 1., 2., 3.],
[ 4., 5., 17., 7.],
[ 8., 9., 10., 11.]])
X[1, 2] = 17
X
array([[ 0., 1., 2., 3.],
[ 4., 5., 17., 7.],
[ 8., 9., 10., 11.]])
# JAX arrays are immutable. jax.numpy.ndarray.at index
# update operators create a new array with the corresponding
# modifications made
X_new_1 = X.at[1, 2].set(17)
X_new_1
Array([[ 0, 1, 2, 3],
[ 4, 5, 17, 7],
[ 8, 9, 10, 11]], dtype=int32)
X_var = tf.Variable(X)
X_var[1, 2].assign(9)
X_var
<tf.Variable 'Variable:0' shape=(3, 4) dtype=float32, numpy=
array([[ 0., 1., 2., 3.],
[ 4., 5., 9., 7.],
[ 8., 9., 10., 11.]], dtype=float32)>
複数の要素に同じ値を代入したい場合は、代入操作の左辺でインデックス付けを行う。
たとえば、[:2, :] は1行目と2行目にアクセスし、: は軸
1(列)に沿ったすべての要素を取りる。ここでは行列についてインデックス付けを説明したが、これはベクトルや2次元より高いテンソルにも同様に使える。
X[:2, :] = 12
X
tensor([[12., 12., 12., 12.],
[12., 12., 12., 12.],
[ 8., 9., 10., 11.]])
X[:2, :] = 12
X
array([[12., 12., 12., 12.],
[12., 12., 12., 12.],
[ 8., 9., 10., 11.]])
X_new_2 = X_new_1.at[:2, :].set(12)
X_new_2
Array([[12, 12, 12, 12],
[12, 12, 12, 12],
[ 8, 9, 10, 11]], dtype=int32)
X_var = tf.Variable(X)
X_var[:2, :].assign(tf.ones(X_var[:2,:].shape, dtype=tf.float32) * 12)
X_var
<tf.Variable 'Variable:0' shape=(3, 4) dtype=float32, numpy=
array([[12., 12., 12., 12.],
[12., 12., 12., 12.],
[ 8., 9., 10., 11.]], dtype=float32)>
2.1.3. 演算¶
テンソルの構築方法と、その要素の読み書き方法が分かったので、さまざまな数学演算を使ってテンソルを操作できるようになる。中でも最も有用なのは 要素ごとの 演算である。これは、標準的なスカラー演算をテンソルの各要素に適用する。2つのテンソルを入力に取る関数では、要素ごとの演算は対応する要素の各組に標準的な二項演算子を適用する。スカラーをスカラーへ写す任意の関数から、要素ごとの関数を作ることができる。
数学記法では、このような 単項 スカラー演算子(1つの入力を取る)を、シグネチャ \(f: \mathbb{R} \rightarrow \mathbb{R}\) で表す。これは、関数が任意の実数を別の実数へ写すことを意味する。\(e^x\) のような単項演算子を含む、ほとんどの標準的な演算子は要素ごとに適用できる。
torch.exp(x)
tensor([162754.7969, 162754.7969, 162754.7969, 162754.7969, 162754.7969,
162754.7969, 162754.7969, 162754.7969, 2980.9580, 8103.0840,
22026.4648, 59874.1406])
np.exp(x)
array([1.0000000e+00, 2.7182817e+00, 7.3890562e+00, 2.0085537e+01,
5.4598148e+01, 1.4841316e+02, 4.0342880e+02, 1.0966332e+03,
2.9809580e+03, 8.1030840e+03, 2.2026465e+04, 5.9874141e+04])
jnp.exp(x)
Array([1.0000000e+00, 2.7182817e+00, 7.3890562e+00, 2.0085537e+01,
5.4598152e+01, 1.4841316e+02, 4.0342880e+02, 1.0966332e+03,
2.9809580e+03, 8.1030840e+03, 2.2026465e+04, 5.9874141e+04], dtype=float32)
tf.exp(x)
<tf.Tensor: shape=(12,), dtype=float32, numpy=
array([1.0000000e+00, 2.7182817e+00, 7.3890562e+00, 2.0085537e+01,
5.4598148e+01, 1.4841316e+02, 4.0342877e+02, 1.0966332e+03,
2.9809580e+03, 8.1030835e+03, 2.2026465e+04, 5.9874141e+04],
dtype=float32)>
同様に、実数の組を1つの実数へ写す 二項 スカラー演算子は、シグネチャ
\(f: \mathbb{R}, \mathbb{R} \rightarrow \mathbb{R}\)
で表す。任意の2つのベクトル \(\mathbf{u}\) と \(\mathbf{v}\) が
同じ shape を持ち、二項演算子 \(f\) が与えられたとき、各 \(i\)
について \(c_i \gets f(u_i, v_i)\) と置くことで、ベクトル
\(\mathbf{c} = F(\mathbf{u},\mathbf{v})\)
を生成できる。ここで、\(c_i, u_i, v_i\) はそれぞれベクトル
\(\mathbf{c}, \mathbf{u}, \mathbf{v}\) の \(i\)-番目
の要素である。ここでは、スカラー関数を要素ごとのベクトル演算へ
持ち上げる ことで、ベクトル値の
\(F: \mathbb{R}^d, \mathbb{R}^d \rightarrow \mathbb{R}^d\)
を得ている。加算(+)、減算(-)、乗算(*)、除算(/)、べき乗(**)といった一般的な標準算術演算子は、任意の
shape を持つ同じ shape のテンソルに対して、すべて
持ち上げられ、要素ごとの演算として使えるようになっている。
x = torch.tensor([1.0, 2, 4, 8])
y = torch.tensor([2, 2, 2, 2])
x + y, x - y, x * y, x / y, x ** y
(tensor([ 3., 4., 6., 10.]),
tensor([-1., 0., 2., 6.]),
tensor([ 2., 4., 8., 16.]),
tensor([0.5000, 1.0000, 2.0000, 4.0000]),
tensor([ 1., 4., 16., 64.]))
x = np.array([1, 2, 4, 8])
y = np.array([2, 2, 2, 2])
x + y, x - y, x * y, x / y, x ** y
(array([ 3., 4., 6., 10.]),
array([-1., 0., 2., 6.]),
array([ 2., 4., 8., 16.]),
array([0.5, 1. , 2. , 4. ]),
array([ 1., 4., 16., 64.]))
x = jnp.array([1.0, 2, 4, 8])
y = jnp.array([2, 2, 2, 2])
x + y, x - y, x * y, x / y, x ** y
(Array([ 3., 4., 6., 10.], dtype=float32),
Array([-1., 0., 2., 6.], dtype=float32),
Array([ 2., 4., 8., 16.], dtype=float32),
Array([0.5, 1. , 2. , 4. ], dtype=float32),
Array([ 1., 4., 16., 64.], dtype=float32))
x = tf.constant([1.0, 2, 4, 8])
y = tf.constant([2.0, 2, 2, 2])
x + y, x - y, x * y, x / y, x ** y
(<tf.Tensor: shape=(4,), dtype=float32, numpy=array([ 3., 4., 6., 10.], dtype=float32)>,
<tf.Tensor: shape=(4,), dtype=float32, numpy=array([-1., 0., 2., 6.], dtype=float32)>,
<tf.Tensor: shape=(4,), dtype=float32, numpy=array([ 2., 4., 8., 16.], dtype=float32)>,
<tf.Tensor: shape=(4,), dtype=float32, numpy=array([0.5, 1. , 2. , 4. ], dtype=float32)>,
<tf.Tensor: shape=(4,), dtype=float32, numpy=array([ 1., 4., 16., 64.], dtype=float32)>)
要素ごとの計算に加えて、内積や行列積のような線形代数演算も実行できる。これらについては 2.3 章 で詳しく説明する。
また、複数のテンソルを連結 することもでき、端から端へつなげてより大きなテンソルを作れる。そのためには、テンソルのリストを与え、どの軸に沿って連結するかをシステムに指定するだけである。以下の例では、2つの行列を列(軸 1)ではなく行(軸 0)に沿って連結したときに何が起こるかを示している。最初の出力の軸 0 の長さ(\(6\))は、2つの入力テンソルの軸 0 の長さ(\(3 + 3\))の和であり、2つ目の出力の軸 1 の長さ(\(8\))は、2つの入力テンソルの軸 1 の長さ(\(4 + 4\))の和であることが分かりる。
X = torch.arange(12, dtype=torch.float32).reshape((3,4))
Y = torch.tensor([[2.0, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
torch.cat((X, Y), dim=0), torch.cat((X, Y), dim=1)
(tensor([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[ 2., 1., 4., 3.],
[ 1., 2., 3., 4.],
[ 4., 3., 2., 1.]]),
tensor([[ 0., 1., 2., 3., 2., 1., 4., 3.],
[ 4., 5., 6., 7., 1., 2., 3., 4.],
[ 8., 9., 10., 11., 4., 3., 2., 1.]]))
X = np.arange(12).reshape(3, 4)
Y = np.array([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
np.concatenate([X, Y], axis=0), np.concatenate([X, Y], axis=1)
(array([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[ 2., 1., 4., 3.],
[ 1., 2., 3., 4.],
[ 4., 3., 2., 1.]]),
array([[ 0., 1., 2., 3., 2., 1., 4., 3.],
[ 4., 5., 6., 7., 1., 2., 3., 4.],
[ 8., 9., 10., 11., 4., 3., 2., 1.]]))
X = jnp.arange(12, dtype=jnp.float32).reshape((3, 4))
Y = jnp.array([[2.0, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
jnp.concatenate((X, Y), axis=0), jnp.concatenate((X, Y), axis=1)
(Array([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[ 2., 1., 4., 3.],
[ 1., 2., 3., 4.],
[ 4., 3., 2., 1.]], dtype=float32),
Array([[ 0., 1., 2., 3., 2., 1., 4., 3.],
[ 4., 5., 6., 7., 1., 2., 3., 4.],
[ 8., 9., 10., 11., 4., 3., 2., 1.]], dtype=float32))
X = tf.reshape(tf.range(12, dtype=tf.float32), (3, 4))
Y = tf.constant([[2.0, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
tf.concat([X, Y], axis=0), tf.concat([X, Y], axis=1)
(<tf.Tensor: shape=(6, 4), dtype=float32, numpy=
array([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[ 2., 1., 4., 3.],
[ 1., 2., 3., 4.],
[ 4., 3., 2., 1.]], dtype=float32)>,
<tf.Tensor: shape=(3, 8), dtype=float32, numpy=
array([[ 0., 1., 2., 3., 2., 1., 4., 3.],
[ 4., 5., 6., 7., 1., 2., 3., 4.],
[ 8., 9., 10., 11., 4., 3., 2., 1.]], dtype=float32)>)
ときには、論理式を通じて二値テンソルを構築したいことがある。X == Y
を例にしよう。各位置 \(i, j\) について、X[i, j] と Y[i, j]
が等しければ、結果の対応する要素は 1 になり、そうでなければ 0 になる。
X == Y
tensor([[False, True, False, True],
[False, False, False, False],
[False, False, False, False]])
テンソルのすべての要素を合計すると、要素が1つだけのテンソルが得られる。
X.sum()
tensor(66.)
X.sum()
array(66.)
X.sum()
Array(66., dtype=float32)
tf.reduce_sum(X)
<tf.Tensor: shape=(), dtype=float32, numpy=66.0>
2.1.4. ブロードキャスティング¶
ここまでで、同じ shape を持つ2つのテンソルに対して要素ごとの二項演算を行う方法を学んだ。ある条件下では、shape が異なっていても、ブロードキャスティング機構を呼び出すことで要素ごとの二項演算を実行できる。 ブロードキャスティングは次の2段階の手順に従いる。(i) 長さ 1 の軸に沿って要素をコピーすることで、一方または両方の配列を拡張し、この変換後に2つのテンソルが同じ shape になるようにする。(ii) 得られた配列に対して要素ごとの演算を行う。
a = torch.arange(3).reshape((3, 1))
b = torch.arange(2).reshape((1, 2))
a, b
(tensor([[0],
[1],
[2]]),
tensor([[0, 1]]))
a = np.arange(3).reshape(3, 1)
b = np.arange(2).reshape(1, 2)
a, b
(array([[0.],
[1.],
[2.]]),
array([[0., 1.]]))
a = jnp.arange(3).reshape((3, 1))
b = jnp.arange(2).reshape((1, 2))
a, b
(Array([[0],
[1],
[2]], dtype=int32),
Array([[0, 1]], dtype=int32))
a = tf.reshape(tf.range(3), (3, 1))
b = tf.reshape(tf.range(2), (1, 2))
a, b
(<tf.Tensor: shape=(3, 1), dtype=int32, numpy=
array([[0],
[1],
[2]], dtype=int32)>,
<tf.Tensor: shape=(1, 2), dtype=int32, numpy=array([[0, 1]], dtype=int32)>)
a と b はそれぞれ \(3\times1\) と \(1\times2\)
の行列なので、shape は一致しない。ブロードキャスティングでは、行列 a
を列方向に、行列 b
を行方向に複製してから要素ごとに加算することで、より大きな
\(3\times2\) 行列を生成する。
a + b
tensor([[0, 1],
[1, 2],
[2, 3]])
2.1.5. メモリの節約¶
演算を実行すると、結果を保持するために新しいメモリが割り当てられることがある。
たとえば Y = X + Y と書くと、Y
が以前指していたテンソルへの参照を外し、代わりに Y
を新しく割り当てられたメモリに向ける。この問題は Python の id()
関数で確認できる。これは参照先オブジェクトのメモリアドレスを正確に返す。Y = Y + X
を実行した後、id(Y)
が別の場所を指していることに注意しよ。これは、Python がまず Y + X
を評価して結果用の新しいメモリを割り当て、その後 Y
をこの新しいメモリ位置に向けるからである。
before = id(Y)
Y = Y + X
id(Y) == before
False
これは2つの理由で望ましくない場合がある。第一に、不要なメモリ割り当てを何度も行いたくないからである。機械学習では、しばしば数百メガバイト規模のパラメータを持ち、それらすべてを毎秒何度も更新する。可能な限り、これらの更新は インプレース で行いたいものである。第二に、同じパラメータを複数の変数から参照していることがある。インプレース更新をしない場合、メモリリークを起こしたり、古いパラメータを誤って参照したりしないよう、これらすべての参照を注意深く更新しなければならない。
幸いなことに、インプレース演算の実行は簡単である。スライス記法
Y[:] = <expression> を使うことで、演算結果をすでに割り当て済みの配列
Y に代入できる。この概念を示すために、zeros_like を使って
Y と同じ shape を持つように初期化したテンソル Z
の値を上書きする。
Z = torch.zeros_like(Y)
print('id(Z):', id(Z))
Z[:] = X + Y
print('id(Z):', id(Z))
id(Z): 139859127252688
id(Z): 139859127252688
Z = np.zeros_like(Y)
print('id(Z):', id(Z))
Z[:] = X + Y
print('id(Z):', id(Z))
id(Z): 140413152328000
id(Z): 140413152328000
# JAX arrays do not allow in-place operations
Z = tf.Variable(tf.zeros_like(Y))
print('id(Z):', id(Z))
Z.assign(X + Y)
print('id(Z):', id(Z))
id(Z): 140582589136560
id(Z): 140582589136560
X の値が後続の計算で再利用されないなら、X[:] = X + Y や
X += Y を使って演算のメモリオーバーヘッドを減らすこともできる。
before = id(X)
X += Y
id(X) == before
True
before = id(X)
X += Y
id(X) == before
True
A = jax.device_get(X)
B = jax.device_put(A)
type(A), type(B)
(numpy.ndarray, jaxlib.xla_extension.ArrayImpl)
@tf.function
def computation(X, Y):
Z = tf.zeros_like(Y) # This unused value will be pruned out
A = X + Y # Allocations will be reused when no longer needed
B = A + Y
C = B + Y
return C + Y
computation(X, Y)
<tf.Tensor: shape=(3, 4), dtype=float32, numpy=
array([[ 8., 9., 26., 27.],
[24., 33., 42., 51.],
[56., 57., 58., 59.]], dtype=float32)>
2.1.6. 他の Python オブジェクトへの変換¶
NumPy
テンソル(ndarray)への変換、またはその逆は簡単である。変換結果はメモリを共有しない。この小さな不便さは実は非常に重要である。CPU
や GPU 上で演算を行うとき、Python の NumPy
パッケージが同じメモリ領域を使って別のことをしたいかどうかを待つために、計算を止めたくはないからである。
NumPy
テンソル(ndarray)への変換、またはその逆は簡単である。torch
テンソルと NumPy
配列は基盤となるメモリを共有し、一方をインプレース演算で変更すると、もう一方も変更される。
A = X.numpy()
B = torch.from_numpy(A)
type(A), type(B)
(numpy.ndarray, torch.Tensor)
A = X.asnumpy()
B = np.array(A)
type(A), type(B)
(numpy.ndarray, mxnet.numpy.ndarray)
a = jnp.array([3.5])
a, a.item(), float(a), int(a)
(Array([3.5], dtype=float32), 3.5, 3.5, 3)
A = X.numpy()
B = tf.constant(A)
type(A), type(B)
(numpy.ndarray, tensorflow.python.framework.ops.EagerTensor)
サイズ 1 のテンソルを Python のスカラーに変換するには、item 関数や
Python の組み込み関数を呼び出せる。
a = torch.tensor([3.5])
a, a.item(), float(a), int(a)
(tensor([3.5000]), 3.5, 3.5, 3)
a = np.array([3.5])
a, a.item(), float(a), int(a)
(array([3.5]), 3.5, 3.5, 3)
a = tf.constant([3.5]).numpy()
a, a.item(), float(a), int(a)
(array([3.5], dtype=float32), 3.5, 3.5, 3)
2.1.7. 要約¶
テンソルクラスは、深層学習ライブラリにおいてデータを保存し操作するための主要なインターフェースである。テンソルは、構築ルーチン、インデックス付けとスライシング、基本的な数学演算、ブロードキャスティング、メモリ効率のよい代入、そして他の Python オブジェクトとの相互変換など、さまざまな機能を提供する。
2.1.8. 演習¶
この節のコードを実行しよ。条件式
X == YをX < YまたはX > Yに変えて、どのようなテンソルが得られるか確認しよ。ブロードキャスティング機構で要素ごとに作用する2つのテンソルを、3次元テンソルなど別の shape に置き換えよ。結果は期待どおりになるか?