はじめに
TensorFlow2.0から Eager Execution と Keras API が標準になる見込みです。すでにブログではこのことを何度か取り上げています。
今回は、TF2.0から最も標準的になると思われるコードの書き方を見ておきましょうというテーマになります。 特にディープラーニングのテクニックや手法の考察などは行わないので、あくまで書き方の参考という程度に御覧ください。
コードはgoogle colabで書いていったので、基本的にはjupyter notebookなどで動作させることを想定していますが、特に普通にpythonスクリプトとして書いても問題ないでしょう。
Eager Executionの書き方
インポート
まずは基本的に使うであろうモジュールたちをインポートしておきます。 ここは必要に応じて適宜用意してください。
import numpy as np import pandas as pd import tensorflow as tf import matplotlib.pyplot as plt %matplotlib inline
また、下記の宣言を行っておきます。この書き方が定着するかは定かではありませんが、tf.keras
を標準に使っていくのであれば何かしら略称を当たり前のように使っていきたいですね。
また、tf.enable_eager_execution()
はEagerを利用しますという宣言になるのですが、TF2.0からは此方が標準になるので、おそらくこの宣言を唱える必要はなくなっているはずです。
tfk = tf.keras tf.enable_eager_execution()
データの準備
TFにはtf.data.Dataset
なるAPIが準備されており、このAPIがあるおかげでデータの前処理に相当する部分をTFの計算グラフの中に投げることができました。Eagerを使う場合には無理して使う必要はないと思われます。しかしEagerでフレキシブルな開発を、Graphで最適化されたデータ処理基盤の運用を、一貫して行えるというのがTFの触れ込みでもあるので、TFをガッツリ使うのであれば慣れる必要があるでしょう。
ひとまず今回はcifar10のデータを使うことにしましょう。
(x_train, y_train), (x_test, y_test) = tfk.datasets.cifar10.load_data()
x_train
とx_test
は (num_data, height, width, channels)
というデータの形をしています。y_train
と y_test
は label
データであり、 one-hot表現にはなっていません。このデータを下記のコードでTFで使われるデータセットの形式に変えます。
train_dataset = ( tf.data.Dataset.from_tensor_slices((x_train, y_train)) .batch(128) .shuffle(10000) )
tf.data.Dataset.from_tensor_slices((x_train, y_train))
によって、データセットがnumpy配列から作ることが可能です。こうして作られたDataset
オブジェクトのbatch(128)
メソッドによって128のミニバッチを設定することができます。続いてshuffle(10000)
というメソッドを使うと、10000
個のデータを一塊としてシャッフルを行います(理想的にはデータ全体のサイズでシャッフルしておく方が良いかもしれません)。
続いて、下記のコードで、データに対する前処理を記述していきます。
train_dataset = ( train_dataset.map(lambda x, y: (tf.div(tf.cast( tf.transpose(x, [0, 3, 1, 2]), tf.float32), 255.0), tf.reshape(tf.one_hot(y, 10), (-1, 10)))) ) train_dataset = ( train_dataset.map(lambda x, y: (tf.image.random_flip_left_right(x), y)) ) train_dataset = train_dataset.repeat()
基本的にはmap()
メソッドを使って、tf
の任意の関数を当てていくことができます。lambda
式で訓練入力データであるx
に対する処理と、ラベルデータであるy
に対する処理を書きます。一つ目のコードはそれぞれ、channels_first
に変換し、tf.float32
に型を変換し、255.0
で除算して値の範囲を0~1
にする(画像処理ではよく行われる)処理を実装しています。
二つ目のコードは、x
の方にだけtf.image.random_flip_left_right()
という処理を当てています。こいつも画像処理でよく行われる処理です。TFのDataset
オブジェクトでは前処理を簡単に記述できます。
最後のコードは、データが一巡したら終わりではなく、呼び出され続ける限り繰り返すことを宣言しています。これがない場合は、データが一巡したらDataset
は役目を終えてしまいます。学習済モデルで大規模なデータに対する予測を1回だけ行いたい場合は、宣言不要というわけです。
検証データセットを作る場合には下記のコードになります。
valid_dataset = ( tf.data.Dataset.from_tensor_slices((x_test, y_test)) .batch(1000) .shuffle(10000) ) valid_dataset = ( valid_dataset.map(lambda x, y: (tf.div(tf.cast( tf.transpose(x, [0, 3, 1, 2]), tf.float32),255.0), tf.reshape(tf.one_hot(y, 10), (-1, 10)))) ) valid_dataset = valid_dataset.repeat()
モデルの書き方
コードを一気に載せてしまいます。 書き方はPyTorchやChainerに極めて類似しています。どちらかと言うとPyTorchに似ており、ChainerのようにTrainingする層(あるいはパラメータ)を明示的に宣言しなくても全てがTrainingする層として認識されます(したがって、転移学習を行う際には、学習しないパラメータをちゃんと指定する必要あり)。
class Cifar10Model(tfk.Model): def __init__(self): super(Cifar10Model, self).__init__(name='cifar_cnn') self.conv_block1 = tfk.Sequential([ tfk.layers.Conv2D( 8, 5, padding='same', activation=tf.nn.relu, kernel_initializer=tf.initializers.variance_scaling, kernel_regularizer=tfk.regularizers.l2(l=0.001), data_format="channels_first" ), tfk.layers.MaxPooling2D( (3, 3), (2, 2), padding='same', data_format="channels_first" ), tfk.layers.BatchNormalization(), ]) self.conv_block2 = tfk.Sequential([ tfk.layers.Conv2D( 16, 5, padding='same', activation=tf.nn.relu, kernel_initializer=tf.initializers.variance_scaling, kernel_regularizer=tfk.regularizers.l2(l=0.001), data_format="channels_first" ), tfk.layers.MaxPooling2D( (3, 3), (2, 2), padding='same', data_format="channels_first" ), tfk.layers.BatchNormalization(), ]) self.flatten = tfk.layers.Flatten() self.fc1 = tfk.layers.Dense( 256, activation=tf.nn.relu, kernel_initializer=tf.initializers.variance_scaling, kernel_regularizer=tfk.regularizers.l2(l=0.001)) self.dropout = tfk.layers.Dropout(0.5) self.fc2 = tfk.layers.Dense(10) self.softmax = tfk.layers.Softmax() def call(self, x): x = self.conv_block1(x) x = self.conv_block2(x) x = self.flatten(x) x = self.dropout(self.fc1(x)) x = self.fc2(x) return self.softmax(x)
ポイントとしては、tfk.Sequential()
というクラスに、tfk.layers
のリストを与えてやれば、リストに入れた順番に層を積み上げた計算グラフを構築してくれる点です。これは model = tfk.Sequential()
としておいて model.add(some_layers )
を繰り返し書くよりも簡単です。また、tfk.Sequenrial
を上手に使うことで、後々のcall
での呼び出しを手短く書くことができます。
各層のコンストラクタの引数に関しては特に普通の使い方と代わりはありません。今回はGPUを使うことを想定してdata_format="channels_first"
を指定しています(GPUはこっちのデータ形式のほうが速いであってましたっけ…?要確認)。
学習コード
本ブログのEager Executionの紹介では、これまでずっと、地道にloss
関数を実装し、順伝播、損失の計算、逆伝播、勾配の更新のコードを真面目に書く実装を見せてきました。
いつからかわかりませんが、いつの間にか、EagerでもKerasの高レベルAPIが普通に使えるようになっていたのでその書き方を明示しておきます。ここらへんはKerasに慣れていれば困らないところでしょう。というか、ある意味EagerとKerasが融合した待望の状態になっていると思われます。
model = Cifar10Model() model.compile(optimizer=tf.train.AdamOptimizer(), loss='categorical_crossentropy', metrics=['accuracy']) callbacks = [ tfk.callbacks.TensorBoard(log_dir='./log/') ] model.fit(train_dataset, epochs=10, steps_per_epoch=int(x_train.shape[0]/128), validation_data=valid_dataset, validation_steps=3, callbacks=callbacks)
TensorBoardも使うことができますね。素晴らしい。
steps_per_epoch
は1epochで何回更新を行うか(何回ミニバッチのサンプリングを行うか)を指定しており、ここではミニバッチの数である「全データ / ミニバッチサイズ(の切り下げ)」としております。
モデルの評価
普通にmodel.predict()
やmodel.evaluate()
を利用することが可能です。
特にEagerに依存したものではないため割愛します。
補足 Google colabでのTensorBoard
google colabではTensorBoardを見るのが面倒なようです。 下記のおまじないによってアクセスすることができました(何が行われているのかはよくわからん)。
ngrokを落としてきます。
!wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip !unzip ngrok-stable-linux-amd64.zip
ngrok
が何者かを知る必要がありそう。
LOG_DIR = './log' get_ipython().system_raw( 'tensorboard --logdir {} --host 0.0.0.0 --port 6006 &' .format(LOG_DIR) ) get_ipython().system_raw('./ngrok http 6006 &')
あとは下記で、TensorBoardのアクセス先が表示されます。
!curl -s http://localhost:4040/api/tunnels | python3 -c \ "import sys, json; print(json.load(sys.stdin)['tunnels'][0]['public_url'])"
最後に
だいぶPyTorchに近い書き方になっています。
もっと複雑なモデルを書こうと思うと、自然と学習のコードも自前で準備しなければならなくなるケースが多いですが、
大抵のモデルの場合はmodel.fit
のAPIを流用できるはずです。
コードは以下に置いておきます。今のところ、TensorBoardでモデルのGraphがEagerでは出てこないのですが、2.0では改善されたりするのでしょうか。
乞うご期待。