TensorFlow Eager Executionを試してみた

Python TensorFlow/Keras ディープラーニング

今年2月頃、TensorFlow 1.5が公開され、TensorFlowをDefine by Runで実行できる「Eager Execution for TensorFlow」が追加されました。

TensorFlowといえば、Define and Runが特徴的ですが、その特性上、デバッグがかなりやり辛い印象でした。

ちなみにDefine and/by Runについて整理しておきますと、

  • Define by Run: 計算グラフ(ニューラルネットの構造)の構築をデータを流しながら行う
  • Define and Run: 計算グラフを構築してから、そこにデータを流していく

といったプログラミングスタイルで、ChainerやPyTorchがDefine by Run型のライブラリとなります。

今回はTensorFlowにDefine by Runモードが追加されると聞いて少し気になっていましたので、実際に動かしてみて操作感を確認してみました。

TensorFlow Eager Executionについて

2018年1月26日(米国時間)、Googleがオープンソース機械学習ライブラリの最新版「TensorFlow 1.5」を公開しました。

その時に特に注目された変更点としては、以下の機能が挙げられています。

  • Eager Execution for TensorFlow
  • TensorFlow Lite
  • GPUアクセラレーション対応の強化

今回の本題であるEager Execution for TensorFlowは、Define by Run型のプログラミングスタイルを可能にするインタフェースで、これを有効にすると、PythonからTensorFlow演算を呼び出してすぐに実行できるようになります。

ついでに他の項目も見ていきますと、TensorFlow Liteは、モバイルや組み込みデバイス向けのTensorFlowの軽量版で、学習済みのTensorFlowモデルを「.tflite」ファイルに変換しモバイルデバイスを使って低レイテンシで実行できるようになります。

なんかこれはこれで、気になりますね笑

GPUアクセラレーション対応の強化に関しては、新たに「CUDA 9」と「cuDNN 7」をサポートといったところです。

TensorFlow Eager Executionですが、GoogleはEager Execution for TensorFlowのメリットとして、下記を挙げています。

  1. 実行時エラーの即時確認と, Pythonツールと統合された迅速なデバッグ
  2. 使いやすいPython制御フローを利用した動的モデルのサポート
  3. カスタムおよび高次勾配の強力なサポート
  4. ほとんどのTensorFlow演算が利用可能

1は冒頭でも述べたように、デバッグをする際にも、デバッグするためのグラフを作って値を流すみたいなことをわざわざやらないといけません。

動的に動かすことができるようになるため、2はPythonのif分などで計算のグラフを制御できることを意味します。

3についてはよく分かりません。

ただし、今回触ってみたところ、勾配計算の部分が通常モードとだいぶ異なっているようで、これについてはあまり深掘り出来ていないのですが、便利になった点が何かしらあるのかもしれません。

4は tf.matmul などがすぐ実行できるよってことですかね。

通常モードのTensorFlow

アイリスのデータで分類問題を学習させてみます。

比較のため、まずは通常モードでアイリスを解いてみます。

import sys, os
import numpy as np
from tqdm import tqdm
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
iris = load_iris()
train_x, valid_x, train_y, valid_y = train_test_split(iris.data, iris.target)
train_x.shape, train_y.shape, valid_x.shape, valid_y.shape
"""
((112, 4), (112,), (38, 4), (38,))
"""
import tensorflow as tf

特に詳しいコメントはしませんので、一気にコードを載せます。

# フローの定義

input_size = 4
output_size = 3
hidden_size = 20

x_ph = tf.placeholder(tf.float32, shape=[None, input_size])
y_ph = tf.placeholder(tf.int32, [None])
y_oh = tf.one_hot(y_ph, depth=output_size, dtype=tf.float32)

fc1_w = tf.Variable(tf.truncated_normal([input_size, hidden_size], stddev=0.1), dtype=tf.float32)
fc1_b = tf.Variable(tf.constant(0.1, shape=[hidden_size]), dtype=tf.float32)
fc1 = tf.nn.relu(tf.matmul(x_ph, fc1_w) + fc1_b)

fc2_w = tf.Variable(tf.truncated_normal([hidden_size, hidden_size], stddev=0.1), dtype=tf.float32)
fc2_b = tf.Variable(tf.constant(0.1, shape=[hidden_size]), dtype=tf.float32)
fc2 = tf.nn.relu(tf.matmul(fc1, fc2_w) + fc2_b)

fc3_w = tf.Variable(tf.truncated_normal([hidden_size, output_size], stddev=0.1), dtype=tf.float32)
fc3_b = tf.Variable(tf.constant(0.1, shape=[output_size]), dtype=tf.float32)
y_pre = tf.matmul(fc2, fc3_w) + fc3_b

cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=y_oh, logits=y_pre))
train_step = tf.train.AdamOptimizer().minimize(cross_entropy)
correct_prediction = tf.equal(tf.argmax(y_pre, 1), tf.argmax(y_oh, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

# 学習

epoch_num = 30
batch_size = 16

init = tf.global_variables_initializer()

with tf.Session() as sess:
    
    sess.run(init)

    for epoch in tqdm(range(epoch_num), file=sys.stdout):
        
        perm = np.random.permutation(len(train_x))
        
        for i in range(0, len(train_x), batch_size):
            
            batch_x = train_x[perm[i:i+batch_size]]
            batch_y = train_y[perm[i:i+batch_size]]
            train_step.run(feed_dict={x_ph: batch_x, y_ph: batch_y})
            
        train_loss = cross_entropy.eval(feed_dict={x_ph: train_x, y_ph: train_y})
        valid_loss = cross_entropy.eval(feed_dict={x_ph: valid_x, y_ph: valid_y})
        train_acc = accuracy.eval(feed_dict={x_ph: train_x, y_ph: train_y})
        valid_acc = accuracy.eval(feed_dict={x_ph: valid_x, y_ph: valid_y})
        
        if (epoch+1)%5 == 0:
            tqdm.write('epoch:\t{}\ttrain/loss:\t{:.5f}\tvalid/loss:\t{:.5f}\ttrain/accuracy:\t{:.5f}\tvalid/accuracy:\t{:.5f}'.format(epoch+1, train_loss, valid_loss, train_acc, valid_acc))

TensorFlowは機械学習を書く時は、低レベルAPI(生TensorFlow)の書き方と高レベルAPI(keras、layersなどを使う)の書き方があります。

使い分けとしては、

  • 低レベルAPI: 機械学習や深層学習のアルゴリズムを自分で実装したい人向け
  • 高レベルAPI: 機械学習や深層学習を使ってみたい人向け

に分かれると思います。

私の場合は生TensorFlowの書き方に慣れていましたので、上記のコードは低レベルAPIの書き方になると思います。

EagerモードのTensorFlow

次にEagerモードでアイリスを学習させてみます。

Eagerモードは、実行する時に下記のコマンドで初期化します。

import tensorflow as tf
import tensorflow.contrib.eager as tfe
tf.enable_eager_execution()

print("TensorFlow version: {}".format(tf.VERSION))
print("Eager execution: {}".format(tf.executing_eagerly()))

ちなみに、これを先程のような通常モードのTensorFlowを実行した後などに行おうとすると、

といった感じにエラーとなってしまいます。

どうやら実行カーネル内での通常モードとEagerモードの共存は出来ないようで、Jupyterの場合は、この場合は再起動しなければなりません。

ちなみに逆も然りで、上記初期化が行われた後に、先程の通常モードのTensorFlowのコードを実行しようとすると、


とエラーを吐きます。

同じTensorFlowなのに面白いですね。

さて、Eagerモードで学習させてみます。

実は公式のEagerモードのチュートリアルはアイリスの問題で書かれていますので、まずはサンプルコードをまるまる実行してみます。

- https://www.tensorflow.org/get_started/eager

train_dataset_url = "http://download.tensorflow.org/data/iris_training.csv"

train_dataset_fp = tf.keras.utils.get_file(fname=os.path.basename(train_dataset_url), origin=train_dataset_url)

def parse_csv(line):
    example_defaults = [[0.], [0.], [0.], [0.], [0]]  # sets field types
    parsed_line = tf.decode_csv(line, example_defaults)
    # First 4 fields are features, combine into single tensor
    features = tf.reshape(parsed_line[:-1], shape=(4,))
    # Last field is the label
    label = tf.reshape(parsed_line[-1], shape=())
    return features, label

train_dataset = tf.data.TextLineDataset(train_dataset_fp)
train_dataset = train_dataset.skip(1)             # skip the first header row
train_dataset = train_dataset.map(parse_csv)      # parse each row
train_dataset = train_dataset.shuffle(buffer_size=1000)  # randomize
train_dataset = train_dataset.batch(32)

model = tf.keras.Sequential([
    tf.keras.layers.Dense(10, activation="relu", input_shape=(4,)),  # input shape required
    tf.keras.layers.Dense(10, activation="relu"),
    tf.keras.layers.Dense(3)
])

def loss(model, x, y):
    y_ = model(x)
    return tf.losses.sparse_softmax_cross_entropy(labels=y, logits=y_)

def grad(model, inputs, targets):
    with tf.GradientTape() as tape:
        loss_value = loss(model, inputs, targets)
    return tape.gradient(loss_value, model.variables)

optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.01)

train_loss_results = []
train_accuracy_results = []

num_epochs = 201

for epoch in range(num_epochs):
    epoch_loss_avg = tfe.metrics.Mean()
    epoch_accuracy = tfe.metrics.Accuracy()

    # Training loop - using batches of 32
    for x, y in train_dataset:
        # Optimize the model
        grads = grad(model, x, y)
        optimizer.apply_gradients(zip(grads, model.variables), global_step=tf.train.get_or_create_global_step())

        # Track progress
        epoch_loss_avg(loss(model, x, y))  # add current batch loss
        # compare predicted label to actual label
        epoch_accuracy(tf.argmax(model(x), axis=1, output_type=tf.int32), y)

    # end epoch
    train_loss_results.append(epoch_loss_avg.result())
    train_accuracy_results.append(epoch_accuracy.result())
    
    if epoch % 50 == 0:
        print("Epoch {:03d}: Loss: {:.3f}, Accuracy: {:.3%}".format(epoch, epoch_loss_avg.result(), epoch_accuracy.result()))

TensorFlowの便利な関数がめっちゃ使われていて、少し分かりづらいです。

少し中身を見てみますが、

train_dataset_fp = tf.keras.utils.get_file(fname=os.path.basename(train_dataset_url), origin=train_dataset_url)

は直接TensorFlowからファイルポイントを取得できるようです。

これについて、

train_dataset = tf.data.TextLineDataset(train_dataset_fp)
train_dataset = train_dataset.skip(1)             # skip the first header row
train_dataset = train_dataset.map(parse_csv)      # parse each row
train_dataset = train_dataset.shuffle(buffer_size=1000)  # randomize
train_dataset = train_dataset.batch(32)

で直接CSVファイルの加工も行って、バッチサイズまで決めているようです。

Chainerのイテレータクラスあたりまでの機能を保持しているということでしょうか。

model = tf.keras.Sequential([
    tf.keras.layers.Dense(10, activation="relu", input_shape=(4,)),  # input shape required
    tf.keras.layers.Dense(10, activation="relu"),
    tf.keras.layers.Dense(3)
])

def loss(model, x, y):
    y_ = model(x)
    return tf.losses.sparse_softmax_cross_entropy(labels=y, logits=y_)

def grad(model, inputs, targets):
    with tf.GradientTape() as tape:
        loss_value = loss(model, inputs, targets)
    return tape.gradient(loss_value, model.variables)

optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.01)

で、モデル、loss関数、最適化を定義しています。

モデルは高レベルAPIの書き方をしていて、loss関数は特に通常モードと変わりありませんが、勾配計算がちょっと見慣れない形になっています。

そして、以下のように学習となっています。

train_loss_results = []
train_accuracy_results = []

num_epochs = 201

for epoch in range(num_epochs):
    epoch_loss_avg = tfe.metrics.Mean()
    epoch_accuracy = tfe.metrics.Accuracy()

    # Training loop - using batches of 32
    for x, y in train_dataset:
        # Optimize the model
        grads = grad(model, x, y)
        optimizer.apply_gradients(zip(grads, model.variables), global_step=tf.train.get_or_create_global_step())

        # Track progress
        epoch_loss_avg(loss(model, x, y))  # add current batch loss
        # compare predicted label to actual label
        epoch_accuracy(tf.argmax(model(x), axis=1, output_type=tf.int32), y)

    # end epoch
    train_loss_results.append(epoch_loss_avg.result())
    train_accuracy_results.append(epoch_accuracy.result())
    
    if epoch % 50 == 0:
        print("Epoch {:03d}: Loss: {:.3f}, Accuracy: {:.3%}".format(epoch, epoch_loss_avg.result(), epoch_accuracy.result()))

特に微分計算のところがまた特殊な形であることと、あと評価の結果を保持するためのクラスが用意されているようです。

train_dataset でループすると、そのままバッチサイズごとに取り出せる模様。

tf.data は色々なファイルやデータベース等、接続先が多種多様に用意されていれば、Pythonへのデータの入力からTensorFlowの世界に閉じることができそうですので、これは別途調べてみたい要素でした。

何はともあれ、このサンプルコードのままじゃ、処理がよく分かりません。

なので、一旦、通常モードの時と同様に、scikit-learnから落としてきたデータ(numpy)からスタートするように書き直してみました。

train_x_tf = tf.convert_to_tensor(train_x, dtype=tf.float32)
train_y_tf = tf.convert_to_tensor(train_y, dtype=tf.int32)
valid_x_tf = tf.convert_to_tensor(valid_x, dtype=tf.float32)
valid_y_tf = tf.convert_to_tensor(valid_y, dtype=tf.int32)

model = tf.keras.Sequential([
    tf.keras.layers.Dense(20, activation='relu', input_shape=(4,)),
    tf.keras.layers.Dense(20, activation='relu'),
    tf.keras.layers.Dense(3)
])

def lossfun(model, x, y):
    y_pre = model(x)
    y_oh = tf.one_hot(y, depth=output_size, dtype=tf.float32)
    cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=y_oh, logits=y_pre))
    return cross_entropy

def grad(model, x, y):
    with tf.GradientTape() as tape:
        loss = lossfun(model, x, y)
    return tape.gradient(loss, model.variables)

epoch_num = 30
batch_size = 16

optimizer = tf.train.AdamOptimizer()

for epoch in tqdm(range(epoch_num), file=sys.stdout):
    
    n, _ = train_x_tf.shape
    n = n.value
    perm = np.random.permutation(n)
    
    for i in range(0, n, batch_size):

        batch_x = tf.gather(train_x_tf, perm[i:i+batch_size])
        batch_y = tf.gather(train_y_tf, perm[i:i+batch_size])

        grads = grad(model, batch_x, batch_y)
        optimizer.apply_gradients(zip(grads, model.variables), global_step=tf.train.get_or_create_global_step())

    train_loss = lossfun(model, train_x_tf, train_y_tf)
    correct_prediction = tf.equal(tf.argmax(model(train_x_tf), axis=1, output_type=tf.int32), train_y_tf)
    train_acc = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
    valid_loss = lossfun(model, valid_x_tf, valid_y_tf)
    correct_prediction = tf.equal(tf.argmax(model(valid_x_tf), axis=1, output_type=tf.int32), valid_y_tf)
    valid_acc = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
        
    if (epoch+1)%5 == 0:
        tqdm.write('epoch:\t{}\ttrain/loss:\t{:.5f}\tvalid/loss:\t{:.5f}\ttrain/accuracy:\t{:.5f}\tvalid/accuracy:\t{:.5f}'.format(
            epoch+1, train_loss, valid_loss, train_acc, valid_acc)
                  )

だいぶ理解できてきました。

train_x_tf = tf.convert_to_tensor(train_x, dtype=tf.float32)
train_y_tf = tf.convert_to_tensor(train_y, dtype=tf.int32)
valid_x_tf = tf.convert_to_tensor(valid_x, dtype=tf.float32)
valid_y_tf = tf.convert_to_tensor(valid_y, dtype=tf.int32)

確かに、Eagerモードとなればnumpyを触る必要もあまりありませんので、最初にnumpyをTensor型にしています。

PyTorchでいえばtorch型になると考えれば良さそうです。

モデル、微分計算は変わりありませんが、loss関数は、

def lossfun(model, x, y):
    y_pre = model(x)
    y_oh = tf.one_hot(y, depth=output_size, dtype=tf.float32)
    cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=y_oh, logits=y_pre))
    return cross_entropy

と、以前のようにone-hot型で評価するようにしました。

結局どちらでも良いのですが、tf.nn.softmax_cross_entropy_with_logits_v2 は以前のようにラベルはone-hot型の tf.float32tf.losses.sparse_softmax_cross_entropy はChainerなどのように、ラベルをそのまま tf.int32 で評価できるようです。

_with_logits は、softmaxはこちらで取るので、順伝播で出力されたラベル次元数のベクトルをそのまま渡してねという意味です。

学習ループに関しては、

    n, _ = train_x_tf.shape
    n = n.value
    perm = np.random.permutation(n)

    for i in range(0, n, batch_size):

        batch_x = tf.gather(train_x_tf, perm[i:i+batch_size])
        batch_y = tf.gather(train_y_tf, perm[i:i+batch_size])

でバッチを回しています。

Tensor型はnumpy型のようには扱えませんので、そのために変更しています。

numpy型だと、取得したいインデックスを配列にして複数同時にアクセスすることができますが、Tensor型ではそれを tf.gater 関数で行います。

    train_loss = lossfun(model, train_x_tf, train_y_tf)
    correct_prediction = tf.equal(tf.argmax(model(train_x_tf), axis=1, output_type=tf.int32), train_y_tf)
    train_acc = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
    valid_loss = lossfun(model, valid_x_tf, valid_y_tf)
    correct_prediction = tf.equal(tf.argmax(model(valid_x_tf), axis=1, output_type=tf.int32), valid_y_tf)
    valid_acc = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

一旦、評価は以前と同じものにしました。

metrics というクラスが用意されていることは分かりましたが、慣れてから勉強します。

と、このようにして、学習させることができましたし、なんとなく理解できてきました。

やはり通常モードと大きく違う点としては、微分計算のところがだいぶ様子が異なっています。(ひょっとしたら、これも同じように書き直せるのかもしれないですが)

どうやらGradientTapeクラスが、dloss/dw の微分計算を担当するようで、tape = tfe.GradientTape() として tape(loss, w) とするようです。

なるほど、ここは慣れは必要ですね。

また、上記は高レベルAPIの書き方なので、ちょっと生TensorFlowの書き方に寄せてみました。

"""
model = tf.keras.Sequential([
    tf.keras.layers.Dense(20, activation='relu', input_shape=(4,)),
    tf.keras.layers.Dense(20, activation='relu'),
    tf.keras.layers.Dense(3)
])
"""
class Model():
    
    def __init__(self):
        input_size = 4
        output_size = 3
        hidden_size = 20
        self.fc1_w = tfe.Variable(tf.truncated_normal([input_size, hidden_size], stddev=0.1), dtype=tf.float32)
        self.fc1_b = tfe.Variable(tf.constant(0.1, shape=[hidden_size]), dtype=tf.float32)
        self.fc2_w = tfe.Variable(tf.truncated_normal([hidden_size, hidden_size], stddev=0.1), dtype=tf.float32)
        self.fc2_b = tfe.Variable(tf.constant(0.1, shape=[hidden_size]), dtype=tf.float32)
        self.fc3_w = tfe.Variable(tf.truncated_normal([hidden_size, output_size], stddev=0.1), dtype=tf.float32)
        self.fc3_b = tfe.Variable(tf.constant(0.1, shape=[output_size]), dtype=tf.float32)
        self.variables = [
            self.fc1_w, self.fc1_b,
            self.fc2_w, self.fc2_b,
            self.fc3_w, self.fc3_b
        ]
        
    def __call__(self, x):
        h = tf.nn.relu(tf.matmul(x, self.fc1_w) + self.fc1_b)
        h = tf.nn.relu(tf.matmul(h, self.fc2_w) + self.fc2_b)
        y_pre = tf.matmul(h, self.fc3_w) + self.fc3_b
        return y_pre
    
model = Model()

def lossfun(model, x, y):
    y_pre = model(x)
    y_oh = tf.one_hot(y, depth=output_size, dtype=tf.float32)
    cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=y_oh, logits=y_pre))
    return cross_entropy

def grad(model, x, y):
    with tf.GradientTape() as tape:
        loss = lossfun(model, x, y)
    return tape.gradient(loss, model.variables)
    
train_x_tf = tf.convert_to_tensor(train_x, dtype=tf.float32)
train_y_tf = tf.convert_to_tensor(train_y, dtype=tf.int32)
valid_x_tf = tf.convert_to_tensor(valid_x, dtype=tf.float32)
valid_y_tf = tf.convert_to_tensor(valid_y, dtype=tf.int32)

epoch_num = 30
batch_size = 16

optimizer = tf.train.AdamOptimizer()

for epoch in tqdm(range(epoch_num), file=sys.stdout):
    
    n, _ = train_x_tf.shape
    n = n.value
    perm = np.random.permutation(n)
    
    for i in range(0, n, batch_size):

        batch_x = tf.gather(train_x_tf, perm[i:i+batch_size])
        batch_y = tf.gather(train_y_tf, perm[i:i+batch_size])

        grads = grad(model, batch_x, batch_y)
        optimizer.apply_gradients(zip(grads, model.variables), global_step=tf.train.get_or_create_global_step())

    train_loss = lossfun(model, train_x_tf, train_y_tf)
    correct_prediction = tf.equal(tf.argmax(model(train_x_tf), axis=1, output_type=tf.int32), train_y_tf)
    train_acc = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
    valid_loss = lossfun(model, valid_x_tf, valid_y_tf)
    correct_prediction = tf.equal(tf.argmax(model(valid_x_tf), axis=1, output_type=tf.int32), valid_y_tf)
    valid_acc = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
        
    if (epoch+1)%5 == 0:
        tqdm.write('epoch:\t{}\ttrain/loss:\t{:.5f}\tvalid/loss:\t{:.5f}\ttrain/accuracy:\t{:.5f}\tvalid/accuracy:\t{:.5f}'.format(
            epoch+1, train_loss, valid_loss, train_acc, valid_acc)
                  )

こんな書き方をして良いのか分かりませんが、動いたからいいや。

変数は tf.Variable ではなく tfe.Variable を使います。

とりあえず、これで自分で変数までも定義させた上で、Eagerモードで実行することができました。

ちなみに、上記の高レベルAPIの書き方はkerasですが、kerasじゃない書き方( tf.layers )をさせる場合は、下記のように tfe.Network クラスが便利そうです。

class Model(tfe.Network):
    
    def __init__(self):
        super(Model, self).__init__()
        input_size = 4
        output_size = 3
        hidden_size = 20
        self.fc1 = self.track_layer(tf.layers.Dense(hidden_size, input_shape=(input_size, )))
        self.fc2 = self.track_layer(tf.layers.Dense(hidden_size, input_shape=(hidden_size, )))
        self.fc3 = self.track_layer(tf.layers.Dense(output_size, input_shape=(hidden_size, )))
    
    def __call__(self, x):
        h = tf.nn.relu(self.fc1(x))
        h = tf.nn.relu(self.fc2(h))
        y = self.fc3(h)
        return y
    
model = Model()

def lossfun(model, x, y):
    y_pre = model(x)
    y_oh = tf.one_hot(y, depth=output_size, dtype=tf.float32)
    cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=y_oh, logits=y_pre))
    return cross_entropy

def grad(model, x, y):
    with tf.GradientTape() as tape:
        loss = lossfun(model, x, y)
    return tape.gradient(loss, model.variables)
    
train_x_tf = tf.convert_to_tensor(train_x, dtype=tf.float32)
train_y_tf = tf.convert_to_tensor(train_y, dtype=tf.int32)
valid_x_tf = tf.convert_to_tensor(valid_x, dtype=tf.float32)
valid_y_tf = tf.convert_to_tensor(valid_y, dtype=tf.int32)

epoch_num = 30
batch_size = 16

optimizer = tf.train.AdamOptimizer()

for epoch in tqdm(range(epoch_num), file=sys.stdout):
    
    n, _ = train_x_tf.shape
    n = n.value
    perm = np.random.permutation(n)
    
    for i in range(0, n, batch_size):

        batch_x = tf.gather(train_x_tf, perm[i:i+batch_size])
        batch_y = tf.gather(train_y_tf, perm[i:i+batch_size])

        grads = grad(model, batch_x, batch_y)
        optimizer.apply_gradients(zip(grads, model.variables), global_step=tf.train.get_or_create_global_step())

    train_loss = lossfun(model, train_x_tf, train_y_tf)
    correct_prediction = tf.equal(tf.argmax(model(train_x_tf), axis=1, output_type=tf.int32), train_y_tf)
    train_acc = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
    valid_loss = lossfun(model, valid_x_tf, valid_y_tf)
    correct_prediction = tf.equal(tf.argmax(model(valid_x_tf), axis=1, output_type=tf.int32), valid_y_tf)
    valid_acc = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
        
    if (epoch+1)%5 == 0:
        tqdm.write('epoch:\t{}\ttrain/loss:\t{:.5f}\tvalid/loss:\t{:.5f}\ttrain/accuracy:\t{:.5f}\tvalid/accuracy:\t{:.5f}'.format(
            epoch+1, train_loss, valid_loss, train_acc, valid_acc)
                  )

このように、高レベルAPIの使い方+Eagerで書いていると、やはり高レベルなライブラリでDefine by RunのChainerやPyTorchになんとなくコードの構成が似てきます。

とりあえず、これでEagerモードの使い方が分かりましたし、動的なグラフをTensorFlowで学習させることができるようになりました。

例えば、何の意味もないですが、以下のように無駄にもう一つ順伝播を通ったり通らなかったりみたいなネットワークも、Pythonのif文で学習させることができます。

class Model(tfe.Network):
    
    def __init__(self):
        super(Model, self).__init__()
        input_size = 4
        output_size = 3
        hidden_size = 20
        self.fc1 = self.track_layer(tf.layers.Dense(hidden_size, input_shape=(input_size, )))
        self.fc2 = self.track_layer(tf.layers.Dense(hidden_size, input_shape=(hidden_size, )))
        
        self.fc2_2 = self.track_layer(tf.layers.Dense(hidden_size, input_shape=(hidden_size, ))) # もう一つ無駄に順伝播作って
        
        self.fc3 = self.track_layer(tf.layers.Dense(output_size, input_shape=(hidden_size, )))
    
    def __call__(self, x):
        
        h = tf.nn.relu(self.fc1(x))
        h = tf.nn.relu(self.fc2(h))
        
        # ランダムにもう一つ無駄に通すという意味のない分岐をするネットワーク
        prob = np.random.randn()
        if prob > 0:
            h = tf.nn.relu(self.fc2_2(h))
            
        y = self.fc3(h)
        return y
    
model = Model()

def lossfun(model, x, y):
    y_pre = model(x)
    y_oh = tf.one_hot(y, depth=output_size, dtype=tf.float32)
    cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=y_oh, logits=y_pre))
    return cross_entropy

def grad(model, x, y):
    with tf.GradientTape() as tape:
        loss = lossfun(model, x, y)
    return tape.gradient(loss, model.variables)
    
train_x_tf = tf.convert_to_tensor(train_x, dtype=tf.float32)
train_y_tf = tf.convert_to_tensor(train_y, dtype=tf.int32)
valid_x_tf = tf.convert_to_tensor(valid_x, dtype=tf.float32)
valid_y_tf = tf.convert_to_tensor(valid_y, dtype=tf.int32)

epoch_num = 30
batch_size = 16

optimizer = tf.train.AdamOptimizer()

for epoch in tqdm(range(epoch_num), file=sys.stdout):
    
    n, _ = train_x_tf.shape
    n = n.value
    perm = np.random.permutation(n)
    
    for i in range(0, n, batch_size):

        batch_x = tf.gather(train_x_tf, perm[i:i+batch_size])
        batch_y = tf.gather(train_y_tf, perm[i:i+batch_size])

        grads = grad(model, batch_x, batch_y)
        optimizer.apply_gradients(zip(grads, model.variables), global_step=tf.train.get_or_create_global_step())

    train_loss = lossfun(model, train_x_tf, train_y_tf)
    correct_prediction = tf.equal(tf.argmax(model(train_x_tf), axis=1, output_type=tf.int32), train_y_tf)
    train_acc = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
    valid_loss = lossfun(model, valid_x_tf, valid_y_tf)
    correct_prediction = tf.equal(tf.argmax(model(valid_x_tf), axis=1, output_type=tf.int32), valid_y_tf)
    valid_acc = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
        
    if (epoch+1)%5 == 0:
        tqdm.write('epoch:\t{}\ttrain/loss:\t{:.5f}\tvalid/loss:\t{:.5f}\ttrain/accuracy:\t{:.5f}\tvalid/accuracy:\t{:.5f}'.format(
            epoch+1, train_loss, valid_loss, train_acc, valid_acc)
                  )

あっちいったりこっちいったりするネットワークなので、学習が安定しません。面白い。

この程度なら別にプレースホルダーに確率値を供給することで通常モードでも可能ですが、データによってとか、バッチごとに異なるネットワークを通したい時には使えるということになりそうです。

感想

TensorFlow Eager Execution使ってみました。

といっても今回はCPUのみで、GPUにする場合はどうなるかなどは、また別の調べてみます。

TensorFlowのことなので、おそらくCPUとGPUの行き来は楽にできているのではないかと思います。

そう考えると、Eagerモードにすることで、結構numpyライクに数値計算が書けるようになりましたので、これをGPUで行えるようになると考えると、機械学習に限らず、一般的な計算でも便利になる場面があるのかもしれません。

深層学習のライブラリとしては、やっぱり他と突出しているような要素は見つかりませんでしたし、結局ChainerでもPyTorchでもTensorFlowでもTensorFlow Eagerでも、使いやすいものを選べばいいやって感じです笑

あと、本題とはあまり関係ないですが、TensorFlowは低レベルAPI、高レベルAPI、そしてEagerも追加されてと、TensorFlowの中だけでも複雑になってきましたので、高レベルAPIで使うか、低レベルAPIで使うかを先に決めておかないと、TensorFlow死ねますね。

追記(2018-10-06)

TensorFlow Eagerで簡単なCNNの書き方をGitHubにあげました。

こんな感じでしょうか?

GitHub: https://github.com/Gin04gh/datascience/blob/master/samples_deeplearning_python/cnn_tensorflow_eager.ipynb

コメント