HELLO CYBERNETICS

深層学習、機械学習、強化学習、信号処理、制御工学、量子計算などをテーマに扱っていきます

TensorFlow Eager Execution + Keras API の基本

 

 

follow us in feedly

https://cdn.blog.st-hatena.com/images/theme/og-image-1500.png

はじめに

TensorFlow2.0から Eager Execution と Keras API が標準になる見込みです。すでにブログではこのことを何度か取り上げています。

www.hellocybernetics.tech

www.hellocybernetics.tech

今回は、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_trainx_test(num_data, height, width, channels) というデータの形をしています。y_trainy_testlabelデータであり、 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では改善されたりするのでしょうか。

github.com

乞うご期待。