(2020/03/19) 以下のオリジナルの内容はTensorFlow 1.xを対象としています。TensorFlow 2.x系で異なる点が出てきましたので、加筆しました。
はじめに
TensorFlowのTensor
って何だっけ?名前が難しそうで関わりたくない?
というときには、numpyと対比しながら押さえていくと分かりやすいです。
自分なりの理解をまとめてみます。
Tensorって何?
行列、ベクトル、数値などを一般化したデータです。多次元配列のイメージでよいでしょう(0次元の配列というのは考えにくいですが)。
これだけならnumpyのndarray
と変わりませんが、Tensor
に対する操作は記号的な操作であって、実際の計算はモデルを学習する時など必要になったときに初めて行われます。(*)
そのため、
-
Tensor
オブジェクトに具体的な値は入っていない。データの形状(行列のサイズとか)や型の情報はある。- $x, 2x, x+y$ といった数式をイメージするとよい。実際に変数に値が代入されないと式の値は決まらない。
-
Tensor
オブジェクトは、基本的にはイミュータブル(変更不可能)。 - サイズが不定なデータも扱える。
という特徴があります。
Pythonからは単なる記号的な操作だけを定義しておくことで、演算の最適化ができたり、処理の遅いPython側での演算処理を避けたりできるのだろうと推測します。
*(2020/03/19追記) TensorFlow 2.x系ではEager Executionがデフォルトとなり、即座に式の値が返されるようになりました。
どんなところに出てくる?
素のTensorFlowを使う上では、Tensorを使ってネットワークを表現していきます。
一方、Kerasを併用するとTensorを意識することは減りますが、こういうところには出てきます。
# 1件のTFRecordをデコード
def parse_example(example):
features = tf.parse_single_example(
example,
features={
# リストを読み込む場合は次元数を指定する
"x": tf.FixedLenFeature([feature_dim], dtype=tf.float32),
"y": tf.FixedLenFeature([], dtype=tf.float32)
})
x = features["x"]
y = features["y"]
return x, y
# === TFRecordファイルのデータを学習・評価用に準備 ===
# イテレータはエポック間で使い回されるので、
# バッチ化した後に repeat(-1) をつける。
# 何ステップ実行すれば1エポックになるかは事前に計算する必要がある。
dataset_train = tf.data.TFRecordDataset(["train.tfrecords"]) \
.map(parse_example) \
.shuffle(batch_size * 100) \
.batch(batch_size).repeat(-1)
これは以前の記事「TensorFlow & Keras で TFRecord & DataSetを使って大量のデータを学習させる方法 - Qiita」に出てきたコードの抜粋ですが、
x = features["x"]
y = features["y"]
これらがTensor
になっています。
また、KerasのモデルのFunctional APIでも、Tensor
を変換していくことでネットワークを表現します。
以下はFunctional APIのドキュメントに書いてあるサンプルコードの抜粋です。コメントにあるように、inputs
やx
、それにpredictions
はTensor
です。
# This returns a tensor
inputs = Input(shape=(784,))
# a layer instance is callable on a tensor, and returns a tensor
x = Dense(64, activation='relu')(inputs)
x = Dense(64, activation='relu')(x)
predictions = Dense(10, activation='softmax')(x)
Tensorの第一歩
本ページのコードを実行する際には、事前に
import numpy as np
import tensorflow as tf
が書かれているものとします。
最初のサンプル
Tensor
については、以下にドキュメントがあります。
tf.Tensor | TensorFlow Core v2.1.0
TensorFlow 1.x
このページの最初の例を引用します(注:TensorFlow 1.13時代の内容で、現在はドキュメントのコードが一部異なっています)。
# Build a dataflow graph.
c = tf.constant([[1.0, 2.0], [3.0, 4.0]])
d = tf.constant([[1.0, 1.0], [0.0, 1.0]])
e = tf.matmul(c, d)
# Construct a `Session` to execute the graph.
sess = tf.Session()
# Execute the graph and store the value that `e` represents in `result`.
result = sess.run(e)
最初のc
, d
, e
がTensor
になります。
行列c
とd
の積(行列積)をe
に代入していますが、これはあくまでe
の定義であり、まだ実際の値は計算されていません。
tf.Session()
で作成したSession
オブジェクトの下でrun(e)
を呼び出すことによって、初めてe
の値が計算されます。
c
, d
, e
のオブジェクトをprint
してみます。
c = tf.constant([[1.0, 2.0], [3.0, 4.0]])
d = tf.constant([[1.0, 1.0], [0.0, 1.0]])
e = tf.matmul(c, d)
print(c) # Tensor("Const:0", shape=(2, 2), dtype=float32)
print(d) # Tensor("Const_1:0", shape=(2, 2), dtype=float32)
print(e) # Tensor("MatMul:0", shape=(2, 2), dtype=float32)
ご覧のように、具体的な値が出てくるわけではありませんが、c
とd
は成分がfloat32
型の2×2行列ですから、それらの行列積であるe
がfloat32
型の2×2行列であることは、実際の値を計算しなくても確定しています。
TensorFlow 2.x (2020/03/19追記)
TensorFlow 2.xでも基本的に使い方は同じなのですが、sess.run()
を呼び出さなくても具体的な値が確認できるようになりました。デバッグが捗ります。
c = tf.constant([[1.0, 2.0], [3.0, 4.0]])
d = tf.constant([[1.0, 1.0], [0.0, 1.0]])
e = tf.matmul(c, d)
print(c)
# tf.Tensor(
# [[1. 2.]
# [3. 4.]], shape=(2, 2), dtype=float32)
print(d)
# tf.Tensor(
# [[1. 1.]
# [0. 1.]], shape=(2, 2), dtype=float32)
print(e)
# tf.Tensor(
# [[1. 3.]
# [3. 7.]], shape=(2, 2), dtype=float32)
Tensor
の値を numpy.ndarray
として取り出すことができます。
print(e.numpy())
# array([[1., 3.],
# [3., 7.]], dtype=float32)
print(type(e.numpy()))
# <class 'numpy.ndarray'>
スライス
numpy.ndarray
のように、インデックスを指定して特定の成分や特定の行・列などを取り出すことができます。
print(c[1]) # Tensor("strided_slice_0:0", shape=(2,), dtype=float32)
print(d[:, 0]) # Tensor("strided_slice_1:0", shape=(2,), dtype=float32)
c
やd
の行・列は、1次元でかつサイズが2のオブジェクト(リスト)であることが分かります。c
もd
も2行2列ですから当たり前ですね。
(2020/03/19追記) 例によってTensorFlow 2.xでは実際の値が見えます。(これ以降のコードも同様です)
print(c[1]) # tf.Tensor([3. 4.], shape=(2,), dtype=float32)
print(d[:, 0]) # tf.Tensor([1. 0.], shape=(2,), dtype=float32)
ndarray
は、もっと高度なことができました。
a = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
print(a[[0, 1], [2, 1]]) # (0, 2)成分と(1, 1)成分を抽出。array([3., 5.])
print(a[a >= 2.0]) # array([2., 3., 4., 5., 6.])
Tensor
の場合にはこうします。
c = tf.constant([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
tf.gather_nd(c, [(0, 2), (1, 1)]) # インデックスの指定順序が違う!
tf.boolean_mask(c, c >= 2.0)
numpy.ndarrayの操作との関係
オブジェクトの生成などの操作は、numpy.ndarray
と似た感覚でTensor
についても行うことができます。
以下に両者の対応を示します。メソッドの名前が違うところは太字で示しています。細かい処理やオプション引数の違いがある場合もありますが、詳細は割愛します。
処理内容 | ndarray | Tensor |
---|---|---|
数値・リストaからオブジェクトを生成 | np.array(a) | tf.constant(a) |
連続した数値からなるオブジェクトを生成 | np.arange([start, ]stop, [step]) | tf.range([start, ]stop, [step]) |
零行列を作成 | np.zeros(shape) | tf.zeros(shape) |
単位行列を作成 | np.identity(size) np.eye(size) |
tf.eye(size) |
全ての成分が1の行列を作成 | np.ones(shape) | tf.ones(shape) |
全ての成分が特定の値の行列を作成 | np.full(shape, value) | tf.fill(shape, value) |
True である成分のインデックスを取得 |
np.where(condition) | tf.where(condition) |
バイナリx (bytes/tf.string) からオブジェクトを作成 | np.fromstring(x, dtype) | (※1) tf.decode_raw(x, dtype) |
オブジェクトxを転置 | np.transpose(x) x.T |
tf.transpose(x) |
xをパディング(前後に定数や両端の値を追加) | np.pad(array, pad_width, mode) | tf.pad(array, pad_width, mode) (※2) |
オブジェクトxの形状を取得 | x.shape | (※3) tf.shape(x) / x.get_shape() |
オブジェクトxの形状を変換 | np.reshape(x, shape) x.reshape(shape) |
tf.reshape(x, shape) |
オブジェクトxのデータ型を変換 | x.astype(dtype) | tf.cast(x, dtype) |
- (※1) 不定サイズのオブジェクトを返します(後述)。
-
(※2)
Tensor
では、mode
に指定できるのは"constant"
,"reflect"
,"symmetric"
の3種類のみ。 - (※3) 不定サイズのオブジェクトに対する挙動が違います(後述)。
Pythonの数値型やnumpy.ndarrayとの演算
Tensor
は、Pythonの数値型やnumpy.ndarray
との演算も定義されています。ndarray
の感覚でブロードキャストされるので、行列(2次元配列)にベクトル(1次元配列)や数値(スカラー)の足し算、とかもできます。
c = tf.constant([[1.0, 2.0], [3.0, 4.0]])
print(c+1) # Tensor("add:0", shape=(2, 2), dtype=float32)
f = tf.constant([[1.0, 2.0]])
print(f) # Tensor("Const_2:0", shape=(1, 2), dtype=float32)
print(c+f) # Tensor("add_1:0", shape=(2, 2), dtype=float32)
a = np.array([[1.0, 2.0]])
print(c+a) # Tensor("add_2:0", shape=(2, 2), dtype=float32)
print(tf.matmul(c, f))
# ValueError: Dimensions must be equal, but are 2 and 1 for 'MatMul_1' (op: 'MatMul') with input shapes: [2,2], [1,2].
行列の次元数やサイズは分かっているので、最後の例のように演算が定義できない場合は即座にエラーとなります。
演算子としては、以下のようなものがあります。(Python 3.x系を仮定します)
使いやすそうなものを表にしてみました。numpy.ndarray
との対比が分かりやすいと思うので、numpy.ndarray
の演算と並べています。メソッドの名前が違うところは太字で示しています。
(2020/03/19追記) 2.x系の Tensor
に対する演算を表に追加しました。TensorFlow 2.xでは、算術演算に関する機能は tf.math
、行列積などの線形代数関係は tf.linalg
パッケージに入れられていますが、TensorFlow 2.1.0時点では1.x系の tf.add()
なども引き続き利用できるようです。(一部例外あり。TensorFlow 2.1.0時点で利用できなくなっているものには取消線を付しています)
演算 | ndarray | Tensor (1.x) | Tensor (2.x) |
---|---|---|---|
xとyの和 | x + y np.add(x, y) |
x + y tf.add(x, y) |
x + y tf.math.add(x, y) |
xとyの差 | x - y np.subtract(x, y) |
x - y tf.subtract(x, y) |
x - y tf.math.subtract(x, y) |
xとyの要素ごとの積 | x * y np.multiply(x, y) |
x * y tf.multiply(x, y) |
x * y tf.math.multiply(x, y) |
xとyの要素ごとの商(実数) | x / y np.true_divide(x, y) |
x / y tf.divide(x, y) |
x / y tf.math.divide(x, y) |
xとyの要素ごとの商(整数) | x // y np.floor_divide(x, y) |
x // y |
x // y tf.math.floordiv(x, y) |
xとyの要素ごとのべき乗 | x ** y np.power(x, y) |
x ** y tf.pow(x, y) |
x ** y tf.math.pow(x, y) |
xとyの行列積 | np.dot(x, y) x.dot(y) |
tf.matmul(x, y) | tf.linalg.matmul(x, y) |
xとyの要素ごと比較(演算子) |
x == y x != y x > y x < y x >= y x <= y |
なし なし x > y x < y x >= y x <= y |
x == y x != y x > y x < y x >= y x <= y |
xとyの要素ごと比較(メソッド) | np.equal(x, y) np.not_equal(x, y) np.greater(x, y) np.less(x, y) np.greater_equal(x, y) np.less_equal(x, y) |
tf.equal(x, y) tf.not_equal(x, y) tf.greater(x, y) tf.less(x, y) tf.greater_equal(x, y) tf.less_equal(x, y) |
tf.math.equal(x, y) tf.math.not_equal(x, y) tf.math.greater(x, y) tf.math.less(x, y) tf.math.greater_equal(x, y) tf.math.less_equal(x, y) |
xの全成分または特定の軸に沿って総和 | np.sum(x[, axis]) x.sum([axis]) |
tf.reduce_sum(x[, axis]) | tf.math.reduce_sum(x[, axis]) |
xの全成分または特定の軸に沿って平均 | np.mean(x[, axis]) x.mean([axis]) |
tf.reduce_mean(x[, axis]) | tf.math.reduce_mean(x[, axis]) |
xの全成分または特定の軸に沿って最大値 | np.max(x[, axis]) x.max([axis]) |
tf.reduce_max(x[, axis]) | tf.math.reduce_max(x[, axis]) |
xの全成分または特定の軸に沿って最小値 | np.min(x[, axis]) x.min([axis]) |
tf.reduce_min(x[, axis]) | tf.math.reduce_min(x[, axis]) |
不定サイズのTensor
今まではサイズが決まったオブジェクトだけを見ていたのですが、前述のdecode_raw
関数などを使うと、不定サイズのオブジェクトができてしまいます。具体例として、先ほどのparse_example
を少し変更したものを見てみましょう。
def parse_example(example):
features = tf.parse_single_example( # 2.xでは tf.io.parse_single_example
example,
features={
"x": tf.FixedLenFeature([], dtype=tf.string), # 2.xでは tf.io.FixedLenFeature
"y": tf.FixedLenFeature([], dtype=tf.float32)
})
x = tf.decode_raw(features["x"], tf.float32) # 2.xでは tf.io.decode_raw
y = features["y"]
print(x) # Tensor("DecodeRaw:0", shape=(?,), dtype=float32)
return x, y
これは、TFRecordに書き込んだ可変長のデータ(単語列などの系列データ)を使って学習を行う際によく起こると思われるパターンです。書き込み時には ndarray.tostring()
を使ってバイナリ化しておき、読み込み時にはバイナリデータから tf.decode_raw()
(1.x) / tf.io.decode_raw()
(2.x) で Tensor
を作成します。
具体的なデータが入力されていない状態では、x
の形状を決定することができません。このような場合には、オブジェクトの形状は shape=(?,)
のように表示され、この ?
が不定(任意)サイズを表します。(ただし、表示から分かるようにx
が1次元のオブジェクトであることだけは決まっています)
reshape
ndarray.tostring()
では値の並びが保存されるだけで形状は保存されませんので、2次元以上のデータを保存した場合は、読み込み側で形状を復元してから使用しなければなりません。
例えば、x
が元々 (データ数, 3) という形状をしていたとすると、
x = tf.reshape(x, (-1, 3)) # Tensor("Reshape:0", shape=(?, 3), dtype=float32)
print(x)
のような感じで形状を復元します。numpy.ndarray
と似た感覚でよいですね。
変換後の形状は shape=(?, 3)
となりました。行数が不定、列数が3の行列(2次元配列)ということを表しています。
実際には、元々の x
の要素の数は3の倍数でなければなりませんが、TensorFlow 1.xの場合、**実行するまで実際の要素の数を知ることはできませんので、この時点でのエラーチェックは行われません。**もちろん、実際に入力されたデータの要素の数が3の倍数でないときは、実行時(学習時など)にエラーが発生します。
不定サイズのオブジェクトに対する演算
たとえ x
の全貌が分からなくても、正しく reshape
できれば列数は3になることが保証されますので、行数が3である行列との積は定義することができます。
weight = np.arange(6, dtype="float32").reshape((3, 2))
print(tf.matmul(x, weight)) # Tensor("MatMul:0", shape=(?, 2), dtype=float32)
データ処理の過程で、例えば x
の行数(データ数)と同じ長さのオブジェクトを作成したいということもあると思います。
print(tf.shape(x)) # Tensor("Shape:0", shape=(2,), dtype=int32)
print(x.get_shape()) # (?, 3)
print(tf.shape(x)[0]) # Tensor("strided_slice:0", shape=(), dtype=int32)
print(repr(x.get_shape()[0])) # Dimension(None)
ご覧のように、tf.shape(x)
の結果が Tensor
である一方、get_shape()
についてはDimension(None)
という「サイズが決まっていない」という情報だけが返ってきます(前述**(※2)**)。
後者のやり方では、不定サイズの行に対して具体的な操作を定義することができません。
tf.shape(x)[0]
は具体的な値こそ確定していませんが、意味的には「x
の0番目の軸のサイズ(行数)」を定義していることになります。形状は shape=()
とあり、0次のテンソル(何らかの単一の数値を表す)になっていることが分かります(繰り返しになりますが、(1.xでは)具体的な値はデータが入力されたときに初めて確定します)。
このテンソルを使って
rng = tf.range(tf.shape(x)[0])
print(rng) # Tensor("range:0", shape=(?,), dtype=int32)
のようにすれば、0から(行数-1)までのリストを作成できたりします。
x
の行数とrng
のサイズは同じになるはずですので、rng
を縦ベクトルとして使うことで
print(x + tf.cast(tf.reshape(rng, (-1, 1)), "float32")) # Tensor("add:0", shape=(?, 3), dtype=float32)
などを定義することも可能です。
なお、ndarray
であればcast
しなくてもfloat32
とint
の和を計算できますが、Tensor
の場合は明示的に型変換する必要がある点に要注意です。
最後に
numpy
と関連付けておくと少しは見通しがよくなるかと思って、自分の理解を整理するために書いてみました。