TensorFlow 2.0 Alpha : ガイド : Eager Execution (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 03/11/2019
* 本ページは、TensorFlow の本家サイトの TF 2.0 Alpha の以下のページを翻訳した上で
適宜、補足説明したものです:
* サンプルコードの動作確認はしておりますが、必要な場合には適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。
ガイド : Eager Execution
TensorFlow の eager execution は命令型プログラミング環境で、これはグラフを構築することなく演算を直ちに評価します : 演算は後で実行する計算グラフを構築する代わりに具体的な値を返します。これは TensorFlow を始めてモデルをデバッグすることを容易にして、そしてまたそれはボイラープレート (なコード) を削減します。このガイドに沿ってフォローするためには、対話的 python インタープリタで下のコード・サンプルを実行してください。
Eager execution は研究と実験のための柔軟な機械学習プラットフォームで、以下を提供します :
- 直感的なインターフェイス — 貴方のコードを自然に構造化して Python データ構造を使用します。小さなモデルと小さなデータ上で迅速に iterate します。
- より簡単なデバッギング — 実行中のモデルを調査して変更をテストするために ops を直接的に呼び出します。即時のエラー報告のために標準的な Python デバッギング・ツールを使用します。
- 自然な制御フロー — グラフ制御フローの代わりに Python 制御フローを使用し、動的モデルの仕様を単純化します。
Eager execution は殆どの TensorFlow 演算と GPU アクセラレーションをサポートします。
Note: 幾つかのモデルでは eager execution が有効であると増大したオーバーヘッドを経験するかもしれません。パフォーマンス改善は進行中ですが、問題が見つかる場合にはバグをファイルしてベンチマークを共有してください。
セットアップと基本的な使用方法
TensorFlow の最新版にアップグレードします :
from __future__ import absolute_import, division, print_function !pip install -q tensorflow==2.0.0-alpha0 import tensorflow as tf
TensorFlow 2.0 では、eager execution はデフォルトで有効にされています。
tf.executing_eagerly()
True
今では貴方は TensorFlow 演算を実行できて結果は直ちに返ります :
x = [[2.]] m = tf.matmul(x, x) print("hello, {}".format(m))
hello, [[4.]]
eager execution を有効にすると TensorFlow 演算がどのように挙動するかを変更します — 今ではそれらは即時に評価して値を Python に返します。tf.Tensor オブジェクトは計算グラフのノードへのシンボリックなハンドルの代わりに具体的な値を参照します。セッション内で構築して後で実行する計算グラフはありませんので、print() やデバッガーを使用して結果を調査することは容易です。tensor 値の評価、表示出力、そしてチェックは勾配を計算するためのフローを壊しません。
Eager execution は NumPy と共に素晴らしく動作します。NumPy 演算は tf.Tensor 引数を受け取ります。TensorFlow math 演算 は Python オブジェクトと NumPy 配列を tf.Tensor オブジェクトに変換します。tf.Tensor.numpy メソッドはオブジェクトの値を NumPy ndarray として返します。
a = tf.constant([[1, 2], [3, 4]]) print(a)
tf.Tensor( [[1 2] [3 4]], shape=(2, 2), dtype=int32)
# Broadcasting support b = tf.add(a, 1) print(b)
tf.Tensor( [[2 3] [4 5]], shape=(2, 2), dtype=int32)
# Operator overloading is supported print(a * b)
tf.Tensor( [[ 2 6] [12 20]], shape=(2, 2), dtype=int32)
# Use NumPy values import numpy as np c = np.multiply(a, b) print(c)
[[ 2 6] [12 20]]
# Obtain numpy value from a tensor: print(a.numpy()) # => [[1 2] # [3 4]]
[[1 2] [3 4]]
動的制御フロー
eager execution の主なメリットはモデルが実行されている間にホスト言語の総ての機能が利用可能であることです。そのため例えば、fizzbuzz を書くことも容易です :
def fizzbuzz(max_num): counter = tf.constant(0) max_num = tf.convert_to_tensor(max_num) for num in range(1, max_num.numpy()+1): num = tf.constant(num) if int(num % 3) == 0 and int(num % 5) == 0: print('FizzBuzz') elif int(num % 3) == 0: print('Fizz') elif int(num % 5) == 0: print('Buzz') else: print(num.numpy()) counter += 1
fizzbuzz(15)
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz
これは tensor 値に依拠する条件節を持ちこれらの値を実行時にプリントします。
モデルを構築する
多くの機械学習モデルは層を構成することにより表わされます。TensorFlow を eager execution で使用するとき貴方自身の層を書くか tf.keras.layers パッケージで提供される層を使用することができます。
層を表わすために任意の Python オブジェクトを使用できる一方で、TensorFlow は便利な基底クラスとして tf.keras.layers.Layer を持ちます。貴方自身の層を実装するためにそれから継承してください、そして層が命令的に実行されなければならな場合にはコンストラクタで self.dynamic=True を設定してください :
class MySimpleLayer(tf.keras.layers.Layer): def __init__(self, output_units): super(MySimpleLayer, self).__init__() self.output_units = output_units self.dynamic = True def build(self, input_shape): # The build method gets called the first time your layer is used. # Creating variables on build() allows you to make their shape depend # on the input shape and hence removes the need for the user to specify # full shapes. It is possible to create variables during __init__() if # you already know their full shapes. self.kernel = self.add_variable( "kernel", [input_shape[-1], self.output_units]) def call(self, input): # Override call() instead of __call__ so we can perform some bookkeeping. return tf.matmul(input, self.kernel)
上の MySimpleLayer の代わりに tf.keras.layers.Dense 層を使用します、何故ならばそれはその機能のスーパーセットを持つからです (それはまたバイアスを追加できます)。
層をモデルに構成するときモデルを表わすために層の線形スタックである tf.keras.Sequential を使用することができます。基本モデルのために使用することは容易です :
model = tf.keras.Sequential([ tf.keras.layers.Dense(10, input_shape=(784,)), # must declare input shape tf.keras.layers.Dense(10) ])
他の方法として、tf.keras.Model から継承することによりクラスでモデルを体系化してください。これはそれ自身が層であるような層のためのコンテナで、tf.keras.Model オブジェクトが他の tf.keras.Model オブジェクトを含むことを可能にします。
class MNISTModel(tf.keras.Model): def __init__(self): super(MNISTModel, self).__init__() self.dense1 = tf.keras.layers.Dense(units=10) self.dense2 = tf.keras.layers.Dense(units=10) def call(self, input): """Run the model.""" result = self.dense1(input) result = self.dense2(result) result = self.dense2(result) # reuse variables from dense2 layer return result model = MNISTModel()
tf.keras.Model クラスのために input shape を設定する必要はありません、何故ならば最初に入力が層に渡されるときにパラメータが設定されるからです。
tf.keras.layers クラスはそれら自身のモデル変数を作成して含み、それらは層オブジェクトのライフタイムに結び付けられます。層変数を共有するためには、それらのオブジェクトを共有します。
Eager 訓練
勾配を計算する
自動微分 はニューラルネットワークを訓練するための バックプロパゲーション のような機械学習アルゴリズムを実装するために有用です。eager execution の間は、勾配を計算するための演算を後で追跡するために tfe.GradientTape を利用します。
tfe.GradientTape は追跡しないとき最大限のパフォーマンスを提供するオプトインな特徴です。異なる演算が各呼び出しの間に発生しますので、総ての forward パス演算は「テープ」に記録されます。勾配を計算するために、テープを反対に再生してから破棄します。特定の tfe.GradientTape は 1 つの勾配を計算するだけです ; 続く呼び出しはランタイム・エラーを投げます。
w = tf.Variable([[1.0]]) with tf.GradientTape() as tape: loss = w * w grad = tape.gradient(loss, w) print(grad) # => tf.Tensor([[ 2.]], shape=(1, 1), dtype=float32)
tf.Tensor([[2.]], shape=(1, 1), dtype=float32)
モデルを訓練する
次のサンプルは標準的な MNIST 手書き数字を分類する多層モデルを作成します。eager execution 環境で訓練可能なグラフを構築する optimizer と層 API を示します。
# Fetch and format the mnist data (mnist_images, mnist_labels), _ = tf.keras.datasets.mnist.load_data() dataset = tf.data.Dataset.from_tensor_slices( (tf.cast(mnist_images[...,tf.newaxis]/255, tf.float32), tf.cast(mnist_labels,tf.int64))) dataset = dataset.shuffle(1000).batch(32)
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz 11493376/11490434 [==============================] - 0s 0us/step
# Build the model mnist_model = tf.keras.Sequential([ tf.keras.layers.Conv2D(16,[3,3], activation='relu', input_shape=(None, None, 1)), tf.keras.layers.Conv2D(16,[3,3], activation='relu'), tf.keras.layers.GlobalAveragePooling2D(), tf.keras.layers.Dense(10) ])
訓練なしでさえも、eager execution ではモデルを呼び出して出力を調査します :
for images,labels in dataset.take(1): print("Logits: ", mnist_model(images[0:1]).numpy())
Logits: [[ 0.0174868 0.04572957 0.00488297 0.0059828 0.00851009 -0.05908806 0.04110092 -0.05364231 0.00356357 0.01905422]]
keras モデルが (fit メソッドを使用した) 組み込み訓練ループを持つ一方、時には更なるカスタマイズが必要でしょう。ここに、eager で実装された訓練ループのサンプルがあります :
optimizer = tf.keras.optimizers.Adam() loss_object = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True) loss_history = []
for (batch, (images, labels)) in enumerate(dataset.take(400)): if batch % 10 == 0: print('.', end='') with tf.GradientTape() as tape: logits = mnist_model(images, training=True) loss_value = loss_object(labels, logits) loss_history.append(loss_value.numpy().mean()) grads = tape.gradient(loss_value, mnist_model.trainable_variables) optimizer.apply_gradients(zip(grads, mnist_model.trainable_variables))
........................................
import matplotlib.pyplot as plt plt.plot(loss_history) plt.xlabel('Batch #') plt.ylabel('Loss [entropy]')
Text(0, 0.5, 'Loss [entropy]')
Variable と optimizer
tfe.Variable オブジェクトは、自動微分をより容易にするために訓練の間にアクセスされるミュータブルな tf.Tensor 値をストアします。モデルのパラメータはクラス内に変数としてカプセル化できます。
モデル・パラメータは tfe.Variable を tfe.GradientTape と共に使用することでより良くカプセル化できます。例えば、上の自動微分サンプルは次のように書き換えることができます :
class Model(tf.keras.Model): def __init__(self): super(Model, self).__init__() self.W = tf.Variable(5., name='weight') self.B = tf.Variable(10., name='bias') def call(self, inputs): return inputs * self.W + self.B # A toy dataset of points around 3 * x + 2 NUM_EXAMPLES = 2000 training_inputs = tf.random.normal([NUM_EXAMPLES]) noise = tf.random.normal([NUM_EXAMPLES]) training_outputs = training_inputs * 3 + 2 + noise # The loss function to be optimized def loss(model, inputs, targets): error = model(inputs) - targets return tf.reduce_mean(tf.square(error)) def grad(model, inputs, targets): with tf.GradientTape() as tape: loss_value = loss(model, inputs, targets) return tape.gradient(loss_value, [model.W, model.B]) # Define: # 1. A model. # 2. Derivatives of a loss function with respect to model parameters. # 3. A strategy for updating the variables based on the derivatives. model = Model() optimizer = tf.keras.optimizers.SGD(learning_rate=0.01) print("Initial loss: {:.3f}".format(loss(model, training_inputs, training_outputs))) # Training loop for i in range(300): grads = grad(model, training_inputs, training_outputs) optimizer.apply_gradients(zip(grads, [model.W, model.B])) if i % 20 == 0: print("Loss at step {:03d}: {:.3f}".format(i, loss(model, training_inputs, training_outputs))) print("Final loss: {:.3f}".format(loss(model, training_inputs, training_outputs))) print("W = {}, B = {}".format(model.W.numpy(), model.B.numpy()))
Initial loss: 68.824 Loss at step 000: 66.135 Loss at step 020: 29.996 Loss at step 040: 13.906 Loss at step 060: 6.742 Loss at step 080: 3.553 Loss at step 100: 2.133 Loss at step 120: 1.500 Loss at step 140: 1.219 Loss at step 160: 1.093 Loss at step 180: 1.038 Loss at step 200: 1.013 Loss at step 220: 1.002 Loss at step 240: 0.997 Loss at step 260: 0.995 Loss at step 280: 0.994 Final loss: 0.993 W = 2.9956676959991455, B = 2.0367937088012695
eager execution の間の状態のためにオブジェクトを使用する
TF 1.x グラフ実行では、(変数のような) プログラム状態はグローバル・コレクションにストアされてそれらのライフタイムは tf.Session オブジェクトで管理されます。対照的に、eager execuction の間は状態オブジェクトのライフタイムはそれらの対応する Python オブジェクトのライフタイムにより決定されます。
Variables はオブジェクト
eager execution の間、variable はオブジェクトへの最後の参照が除去されるまで存続し、それから削除されます。
if tf.test.is_gpu_available(): with tf.device("gpu:0"): v = tf.Variable(tf.random.normal([1000, 1000])) v = None # v no longer takes up GPU memory
オブジェクト・ベースのセービング
tf.train.Checkpoint は tf.Variables をチェックポイントへ/からセーブしてリストアできます :
x = tf.Variable(10.) checkpoint = tf.train.Checkpoint(x=x)
x.assign(2.) # Assign a new value to the variables and save. checkpoint_path = './ckpt/' checkpoint.save('./ckpt/')
'./ckpt/-1'
x.assign(11.) # Change the variable after saving. # Restore values from the checkpoint checkpoint.restore(tf.train.latest_checkpoint(checkpoint_path)) print(x) # => 2.0
<tf.Variable 'Variable:0' shape=() dtype=float32, numpy=2.0>
モデルをセーブしてロードするためには、隠れ変数を必要とすることなく、tf.train.Checkpoint がオブジェクトの内部状態をストアします。モデル、optimizer そしてグローバルステップの状態を記録するためには、それらを tf.train.Checkpoint に渡します :
import os model = tf.keras.Sequential([ tf.keras.layers.Conv2D(16,[3,3], activation='relu'), tf.keras.layers.GlobalAveragePooling2D(), tf.keras.layers.Dense(10) ]) optimizer = tf.keras.optimizers.Adam(learning_rate=0.001) checkpoint_dir = 'path/to/model_dir' if not os.path.exists(checkpoint_dir): os.makedirs(checkpoint_dir) checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt") root = tf.train.Checkpoint(optimizer=optimizer, model=model) root.save(checkpoint_prefix) root.restore(tf.train.latest_checkpoint(checkpoint_dir))
<tensorflow.python.training.tracking.util.CheckpointLoadStatus at 0x7fab905e3080>
オブジェクト指向メトリクス
tf.keras.metrics はオブジェクトとしてストアされます。新しいデータを callable に渡すことでメトリクスを更新して、そして tf.keras.metrics.result メソッドを使用して結果を取得します、例えば :
m = tf.keras.metrics.Mean("loss") m(0) m(5) m.result() # => 2.5 m([8, 9]) m.result() # => 5.5
<tf.Tensor: id=84103, shape=(), dtype=float32, numpy=5.5>
上級者のための自動微分トピック
動的モデル
tf.GradientTape はまた動的モデルでも利用できます。バックトラックする直線探索 (= line search) アルゴリズムのこのサンプルは、複雑な制御フローにもかかわらず、(勾配があり微分可能であることを除けば) 普通の NumPy コードのように見えます :
def line_search_step(fn, init_x, rate=1.0): with tf.GradientTape() as tape: # Variables are automatically recorded, but manually watch a tensor tape.watch(init_x) value = fn(init_x) grad = tape.gradient(value, init_x) grad_norm = tf.reduce_sum(grad * grad) init_value = value while value > init_value - rate * grad_norm: x = init_x - rate * grad value = fn(x) rate /= 2.0 return x, value
カスタム勾配
カスタム勾配は勾配を override するための簡単な方法です。forward 関数内では、勾配は入力、出力、または中間結果に関する勾配を定義します。例えば、backward パスで勾配のノルムをクリップするための簡単な方法がここにあります :
@tf.custom_gradient def clip_gradient_by_norm(x, norm): y = tf.identity(x) def grad_fn(dresult): return [tf.clip_by_norm(dresult, norm), None] return y, grad_fn
カスタム勾配は演算のシークエンスのための数値的安定な勾配を提供するために一般に使用されます :
def log1pexp(x): return tf.math.log(1 + tf.exp(x)) def grad_log1pexp(x): with tf.GradientTape() as tape: tape.watch(x) value = log1pexp(x) return tape.gradient(value, x)
# The gradient computation works fine at x = 0. grad_log1pexp(tf.constant(0.)).numpy()
0.5
# However, x = 100 fails because of numerical instability. grad_log1pexp(tf.constant(100.)).numpy()
nan
ここで、log1pexp 関数はカスタム勾配で解析的に単純化できます。下の実装は forward パスで計算された tf.exp(x) のための値を再利用しています — 冗長な計算を除去することでそれをより効率的にしています :
@tf.custom_gradient def log1pexp(x): e = tf.exp(x) def grad(dy): return dy * (1 - 1 / (1 + e)) return tf.math.log(1 + e), grad def grad_log1pexp(x): with tf.GradientTape() as tape: tape.watch(x) value = log1pexp(x) return tape.gradient(value, x)
# As before, the gradient computation works fine at x = 0. grad_log1pexp(tf.constant(0.)).numpy()
0.5
# And the gradient computation also works at x = 100. grad_log1pexp(tf.constant(100.)).numpy()
1.0
パフォーマンス
eager execution の間は計算は GPU へと自動的にはオフロードされます。計算が動作する位置について制御することを望む場合にはそれを tf.device(‘/gpu:0’) ブロックで囲むことができます (または CPU の同値のもの) :
import time def measure(x, steps): # TensorFlow initializes a GPU the first time it's used, exclude from timing. tf.matmul(x, x) start = time.time() for i in range(steps): x = tf.matmul(x, x) # tf.matmul can return before completing the matrix multiplication # (e.g., can return after enqueing the operation on a CUDA stream). # The x.numpy() call below will ensure that all enqueued operations # have completed (and will also copy the result to host memory, # so we're including a little more than just the matmul operation # time). _ = x.numpy() end = time.time() return end - start shape = (1000, 1000) steps = 200 print("Time to multiply a {} matrix by itself {} times:".format(shape, steps)) # Run on CPU: with tf.device("/cpu:0"): print("CPU: {} secs".format(measure(tf.random.normal(shape), steps))) # Run on GPU, if available: if tf.test.is_gpu_available(): with tf.device("/gpu:0"): print("GPU: {} secs".format(measure(tf.random.normal(shape), steps))) else: print("GPU: not found")
Time to multiply a (1000, 1000) matrix by itself 200 times: CPU: 0.8538422584533691 secs GPU: not found
tf.Tensor オブジェクトはその演算を実行するために異なるデバイスへとコピーできます :
if tf.test.is_gpu_available(): x = tf.random.normal([10, 10]) x_gpu0 = x.gpu() x_cpu = x.cpu() _ = tf.matmul(x_cpu, x_cpu) # Runs on CPU _ = tf.matmul(x_gpu0, x_gpu0) # Runs on GPU:0
ベンチマーク
GPU 上の ResNet50 訓練のような計算が重いモデルについては、eager execution パフォーマンスは tf.function 実行に匹敵します。しかしこの隔たりはより少ない計算を持つモデルのためにはより大きくなり、多くの小さい演算を持つモデルのためにホットコード・パスを最適化するために行われなければならない作業があります。
Work with functions
eager execution が開発とデバッグをより対話的にする一方で、TensorFlow 1.x スタイルのグラフ execution は分散訓練、パフォーマンス最適化、そしてプロダクション配備のために優位点を持ちます。この隔たりを埋めるために、TensorFlow 2.0 は tf.function API を通した関数を導入します。より多くの情報は、Autograph ガイドを見てください。
以上