第3回 TensorFlow 基礎から最新APIまで (2/4)

技術特集

1_tensorflow_keras1_tit

tit_tensorflow_keras

2) 高レベルAPIでディープラーニングを実装する

さて、ここまでTensorFlowの数値計算機能の基礎についてご説明してきましたが、
placeholderがこうで、Variableがこうで、Sessionがこうなっているから……というような話はあくまでTensorFlow側の事情であって、
とにかくディープラーニングの処理を組んで流したいんだ!という目的のためには、率直に言って少し遠回りをさせられている感もないではありません。

そのような人々のためにTensorFlowには、そういった泥臭い部分をある程度スキップできる高レベルのAPIも提供されています。
ここからは、前回作成した犬と猫の画像を分類するDNNを題材とし、TensorFlowの高レベルAPIを用いて実装する例をご紹介していきます。

ちなみに前回までにご紹介したKerasは、思い切った抽象化によるスリムな書き方を実現した一方で、もう一歩踏み込んでカスタマイズしたいというときのハードルが高くなっています。
実際のところとしては、まずはKerasを使ってみてその枠からはみ出したくなったらTensorFlowに移行するというのがセオリーといえそうです。

ディープラーニングモデルの定義

前回と同様に、ディープラーニングモデルを別ファイルとして定義するところからはじめましょう。
前回と全く同じ構造のDNNをTensorFlowを用いて定義するとコードは下記のとおりになります;

import tensorflow as tf
 
def MyDNN(input_shape=(32, 32, 1), output_size=10, learning_rate=0.001,
          keep_prob=0.5, model_dir='tfmodel'):
    def mydnn_fn(features, labels, mode):
        # tf.LayersによるDNN構造の定義
        input_layer = tf.reshape(features["img"], [-1] + list(input_shape))
        labels = tf.reshape(labels, [-1, output_size])
 
        layer = tf.layers.conv2d(filters=20, kernel_size=5, strides=2,
                                 activation=tf.nn.relu,
                                 inputs=input_layer)
        layer = tf.layers.max_pooling2d(pool_size=3, strides=2,
                                        inputs=layer)
        layer = tf.layers.batch_normalization(inputs=layer)
 
        layer = tf.layers.conv2d(filters=50, kernel_size=5, strides=2,
                                 activation=tf.nn.relu,
                                 inputs=layer)
        layer = tf.layers.max_pooling2d(pool_size=3, strides=2,
                                        inputs=layer)
        layer = tf.layers.batch_normalization(inputs=layer)
 
        layer = tf.contrib.layers.flatten(layer)
        layer = tf.layers.dense(units=100, activation=tf.nn.relu,
                                inputs=layer)
        layer = tf.layers.dropout(rate=(1 - keep_prob),
                                  training=(mode == tf.estimator.ModeKeys.TRAIN),
                                  inputs=layer)
        output_layer = tf.layers.dense(units=output_size,
                                       inputs=layer)
 
        # tf.Estimatorを用いる準備
        # 学習済みモデルを利用した出力の取得に必要な値
        if mode == tf.estimator.ModeKeys.PREDICT:
            probabilities = tf.nn.softmax(output_layer)
            return tf.estimator.EstimatorSpec(mode=mode, predictions=probabilities)
 
        # 学習に必要な値
        labels = tf.reshape(labels, [-1, output_size])
        loss = tf.losses.softmax_cross_entropy(onehot_labels=labels,
                                               logits=output_layer)
        optimizer = tf.train.AdamOptimizer(learning_rate, epsilon=1e-1)
        train_op = optimizer.minimize(loss=loss, global_step=tf.train.get_global_step())
 
        # 評価に必要な値
        classes = tf.argmax(input=output_layer, axis=1)
        eval_metric_ops = {
          "accuracy": tf.metrics.accuracy(labels=tf.argmax(labels, axis=1),
                                          predictions=classes)
        }
 
        return tf.estimator.EstimatorSpec(mode=mode, loss=loss,
                                          train_op=train_op,
                                          eval_metric_ops=eval_metric_ops)
 
 
 
    estimator = tf.estimator.Estimator(model_fn=mydnn_fn, model_dir=model_dir)
 
    return estimator

Layers: DNNの構造を定義

DNNの各層を実装するにあたっては、LayersというTensorFlowのAPIが使えます。
上記コードの「LayersによるDNN構造の定義」というコメントの部分です。

Layersを用いると、Kerasを用いた時とかなり近い感覚でDNNの構造を書くことができます。
比較のため、畳み込み層→プーリング層→正規化層というまとまりの部分を双方から引用してみます:

layer = tf.layers.conv2d(filters=20, kernel_size=5, strides=2,
         activation=tf.nn.relu,
         inputs=input_layer)
layer = tf.layers.max_pooling2d(pool_size=3, strides=2,
            inputs=layer)
layer = tf.layers.batch_normalization(inputs=layer)
model.add(Conv2D(20, kernel_size=5, strides=2,
                 activation='relu',
                 input_shape=input_shape))
model.add(MaxPooling2D(3, strides=2))
model.add(BatchNormalization())

Layersを用いずにTensorFlowで実装すると……という話はやめておくことにします。 世の中には知らない方がいい苦労というものがあるものです。

1点だけ注意すべきなのは、Layersでドロップアウトを行う場合、引数で指定するドロップアウト率の意味がKerasと逆になっているところです。
Layersでは引数rateはデータがドロップアウトの対象と なる割合 を意味しますので、「全くドロップアウトしない」場合は0.0を指定します。
一方KerasではDropoutの引数はデータがロップアウトの対象と ならない割合 を意味しますので、「全くドロップアウトしない」場合は1.0を指定します。

Estimator: モデルの学習・評価・利用

MyDNN関数の実装においてKeras版と大きく違うのは、学習の基準値や評価値の定義の方法です。
KerasのSequentialオブジェクトに相当するものとして、TensorFlowにはEstimatorが用意されています。

def mydnn_fn(features, labels, mode):
    ...
 
estimator = tf.estimator.Estimator(model_fn=mydnn_fn, model_dir=model_dir)

Estimatorオブジェクトはtrainevaluatepredictといったメソッドを持ち、それらを正しく呼び出すことで学習・評価・学習済みモデルを利用した出力の取得が可能です。
また学習中は学習結果を自動的に保存し、保存された学習結果がある場合は学習開始時にそれを自動でロードしてくれます。
Estimatorを用いない場合は、学習の計算や評価値の計算などをSession.runを通じて直接実行し、結果を自分で保存して……おっと、 知らないでいい苦労については話さない ことにしたのでしたね。

Estimatorの作成に必要なのは、基本的にmodel_fnというパラメータのみです。このパラメータには、処理に必要な設定を保持するEstimatorSpecというオブジェクトを返す関数を渡します。
従って、DNNの実装はmodel_fnの内部で行うことになります。

また、model_dirというパラメータには学習済みモデルや学習途中の各種評価値の記録を保存するディレクトリ名を指定することができます。
API上は指定しなくてもよいパラメータですが、実用上は明示的に指定することになるでしょう。

model_fn内でのEstimatorSpecの作成

model_fnとして渡す関数は最低でもfeatureslabelsmodeという3つの引数を持っている必要があります。
featuresは入力データ、labelsは教師データに相当します。modeには、これから行うのが学習・評価・出力取得のいずれであるかを示す定数が渡されます。

EstimatorSpecのコンストラクタにおいて必要な引数は、modeによって違います:

mode 必要な引数
学習 mode, loss, train_op
評価 mode, loss
出力取得 mode, predictions

modeは渡されたmodeそのままです。
lossは、学習データと教師データの差異を表す値、いわゆる損失値です。
train_opは、損失値に基いてパラメータを最適化する処理のオブジェクトを渡します。
predictionは、出力取得処理で取得したい値を指定します。

使わない引数をセットしても支障はないようですので、上記に掲載したコードでは学習時と評価時のEstimatorSpecは共通としています。
しかし出力取得のみを行う場合については、教師データlabelsが空であることによるエラーが発生したため、labelsを扱う前にmodeをチェックし、EstimatorSpecを作成してリターンする実装としています。