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

技術特集

1_tensorflow_keras1_tit

tit_tensorflow_keras

3) ディープラーニング実行部の実装

次に、MyDNNを呼び出す側のコードを示します。ほぼ前回のKerasの例を踏襲した構造になっています。

再度仕様をご説明しますと、
コマンドライン引数なしで実行すると特定のディレクトリから学習用画像を読み込んでディープラーニングを行い、
コマンドライン引数として–inferオプションとともに判定したい画像のファイルパスを指定すると、学習済みモデルをもとにその画像に写っているのが犬か猫かを判定するという想定です。

import argparse
import tensorflow as tf
import os, sys
import numpy as np
from datetime import datetime
from mydnn_tf_layers import MyDNN
import tensorflow as tf
 
ImageDataGenerator = tf.keras.preprocessing.image.ImageDataGenerator
load_img = tf.keras.preprocessing.image.load_img
img_to_array = tf.keras.preprocessing.image.img_to_array
 
input_size_x = 224
input_size_y = 224
batch_size = 20
input_size_c = 3
output_size = 2
model_dir = 'tfmodel/'
 
parser = argparse.ArgumentParser()
parser.add_argument("--infer", action="store", nargs="?", type=str,
                    help="学習は行わず、このオプションで指定した画像ファイルの判定処理を行う")
parser.add_argument("--epochs", action="store", nargs="?", default=10, type=int,
                    help="学習データ全体を何周するか")
args = parser.parse_args()
 
epochs = args.epochs
 
model = MyDNN(input_shape=(input_size_x, input_size_y, input_size_c),
              output_size=output_size, model_dir=model_dir)
 
if not args.infer:
    print("学習モード")
    # 学習データの読み込み
    keras_idg = ImageDataGenerator(rescale=1.0 / 255)
    train_generator = keras_idg.flow_from_directory('data/train',
                              target_size=(input_size_x, input_size_y),
                          batch_size=1,
                          class_mode='categorical',
                          shuffle=True)
    valid_generator = keras_idg.flow_from_directory('data/valid',
                          target_size=(input_size_x, input_size_y),
                          batch_size=1,
                          class_mode='categorical')
 
    # 学習の実行
    num_data_train_dog = len(os.listdir('data/train/dog'))
    num_data_train_cat = len(os.listdir('data/train/cat'))
    num_data_train = num_data_train_dog + num_data_train_cat
 
    num_data_valid_dog = len(os.listdir('data/valid/dog'))
    num_data_valid_cat = len(os.listdir('data/valid/cat'))
    num_data_valid = num_data_valid_dog + num_data_valid_cat
 
    steps_per_epoch = num_data_train / batch_size
    validation_steps = num_data_valid / batch_size
    
    # ImageDataGeneratorをDatasetに変換
    def my_input_fn(generator):
        gen_fn = lambda: generator
        dataset = tf.data.Dataset.from_generator(gen_fn, (tf.float32, tf.float32))
        dataset = dataset.map(lambda f, l: ({"img": f}, l)).batch(batch_size)
        return dataset.make_one_shot_iterator().get_next()
 
    for epoch in range(epochs):
        print("epoch ",  epoch)
        print("training...")
        model.train(input_fn=lambda: my_input_fn(train_generator),
                    steps=steps_per_epoch)
        print("evaluation:")
        eval_results = model.evaluate(input_fn=lambda: my_input_fn(valid_generator),
                                      steps=validation_steps)
        print(eval_results)
 
else:
    print("判定モード")
    # 判定する画像の読み込み
    image_infer = load_img(args.infer, target_size=(input_size_x, input_size_y))
    data_infer = img_to_array(image_infer)
    data_infer = np.expand_dims(data_infer, axis=0)
    data_infer = data_infer / 255.0
 
    # 判定処理の実行
    predict_input_fn = tf.estimator.inputs.numpy_input_fn(x={"img": data_infer},
                                                          batch_size=1,
                                                          shuffle=False)
    result_generator = model.predict(predict_input_fn)
    result = next(result_generator) * 100
 
    # 判定結果の出力
    if result[0] > result[1]:
      print('Cat (%.1f%%)' % result[0])
    else:
      print('Dog (%.1f%%)' % result[1])

ImageDataGeneratorに関して

一見して「keras」という文字列が紛れ込んでいることに気づかれたアナタ、鋭いですね。
実は2017年11月3日にリリースされたばかりのTensorFlow 1.4では、Kerasの機能がTensorFlowの正式なAPIの中に取り込まれています。
前回の記事で画像を読み込む際に使用したImageDataGeneratorは非常に強力な機能でありながら、TensorFlowのKerasモジュール以外の部分には同等の機能が見当たりません。

そこで今回はImageDataGeneratorもTensorFlowの一部ということでご容赦いただき、前回同様ImageDataGeneratorを使用して画像を読み込むコードになっています。
このコードはkerasを別途インストールしなくても動作しますし、一応コードの冒頭でtf.kerasモジュールから(つまりTensorFlowの一部として)ImageDataGeneratorと関連関数を読み込んでいることがご確認いただけます。

これにより、「学習モード」のブロックは「ImageDataGeneratorをDatasetに変換」というコメントの前までは前回のコードと共通となっています。

Dataset: 効率的にデータを投入する

「TensorFlowの基礎」でご紹介した通り、TensorFlowで大量のデータを処理する場合、
「データを用意→placeholderとして指定し計算を実行→結果を処理」という流れを繰り返し行うことになり、またデータの集合に対して分割やシャッフルをはじめとした下処理を行うことも頻繁にあります。
そこでその一連の操作をまとめたうえで効率的な実装を提供するのがDatasetです。

今回のコードではデータの読み出しにImageDataGeneratorを使用しているため、Datasetを最大限活用しているというわけではないのですが、基本的な使い方の例としてご参照ください。

Datasetクラスにはfrom_〇〇というメソッドが複数あり、手元のデータからDatasetを作成するにはこれらのメソッドを使用します。
また、テキストファイルを直接読み込むことのできるTextLineDatasetをはじめとした、ファイルシステム上のデータを簡単に参照できるDatasetのサブクラスも存在します。

Datasetの作成

今回はImageDataGeneratorのオブジェクトをもとにDatasetを作成しますので、from_generatorというメソッドを使います。

gen_fn = lambda: generator
dataset = tf.data.Dataset.from_generator(gen_fn, (tf.float32, tf.float32))

tf.data.Dataset.from_generatorの第1引数はデータのジェネレータ関数を指定することが期待されています。
しかし今回は、ImageDataGeneratorの機能よりすでにイテレータを取得していますので、そのイテレータを返す擬似的なジェネレータ関数を作ってお茶を濁しています。

第2引数にはジェネレータから得られるデータの型を指定します。今回は、学習データとして画像をfloatで数値化したものと、教師データとしてラベルをfloatで表したもののがそれぞれ得られますので、(tf.float32, tf.float32)を指定します。

DatasetとEstimatorの連携

Estimatorで学習・評価・結果取得を行う際には、入力データはEstimatorが指定する仕様を持つ関数input_fnという形で与えます。
その仕様とは「入力データfeaturesと教師データlabelsTensorオブジェクトとして返すこと」です。
(正確には、TensorオブジェクトもしくはTensorオブジェクトを値として持つ辞書という2つの選択肢があります)

このfeaturesとlabelsは、EstimatorSpecの作成に使用したmodel_fnにそのまま与えられるイメージです。

ちょうどDatasetにはデータをTensorオブジェクトとして出力する機能がありますので、そちらを使って実装を行っています。

# ImageDataGeneratorをDatasetに変換
def my_input_fn(generator):
    gen_fn = lambda: generator
    dataset = tf.data.Dataset.from_generator(gen_fn, (tf.float32, tf.float32))
    dataset = dataset.map(lambda f, l: ({"img": f}, l))
    dataset = dataset.batch(batch_size)
    iterator = dataset.make_one_shot_iterator()
    features, labels = iterator.get_next()
    return features, labels
    
...
 
model.train(input_fn=lambda: my_input_fn(train_generator),
            steps=steps_per_epoch)

from_generatorメソッドによりDatasetオブジェクトを作成した続きから見ていきましょう。

dataset.mapは、Datasetが参照する各データに加工をほどこすメソッドです。
今回はfeaturesの値を、imgというキーをともなう辞書の中に格納しています。
Estimatorは基本的に辞書の形でデータを授受することを想定して作られているようなので、それに倣っています。

dataset.batchは、Datasetが参照する各データを指定数分まとめて入力するようにするメソッドです。

dataset.make_one_shot_iteratorは、Datasetから値を読み出すIteratorオブジェクトを作成するメソッドです。
Iteratorオブジェクトは、その名前に反して通常のPythonのイテレータとしては使用できません。
get_nextメソッドで得たTensorオブジェクトをSession.runで読み込むと、Datasetが参照する各データを順に計算対象としてくれるという仕組みになっています。

従ってここではiterator.get_next()の返り値がEstimatorのinput_fnが返すべきTensorであるということになります。

さらに、このTensorはあらかじめ取得してEstimatorに渡すという手順ではうまくいかず、あくまでもinput_fnで、つまりEstimatorの中で取得される必要があるようです。
そのためmy_input_fnをラムダ関数で包み、Estimatorの中でmy_input_fnが実行されるようにしています。

モデルの評価についても、使用するメソッドがevaluateである以外はほぼ同様です。重複となりますので今一度の説明は割愛いたします。

判定モード:1件のデータを入れる

判定モードでは学習済みモデルを利用して判定を行います。
といってもEstimatorの利用法は、メソッドがpredictになるだけでinput_fnを渡すことに代わりはありません。

ただしinput_fnの生成方法について、このプログラムの判定モードでは1件だけデータが入ればよいので、Datasetなしでできる軽量なやりかたで実装しています。

predict_input_fn = tf.estimator.inputs.numpy_input_fn(x={"img": data_infer},
                                                      batch_size=1,
                                                      shuffle=False)
result_generator = model.predict(predict_input_fn)

データが入ったNumPy Arrayが手元にあれば、それをtf.estimator.inputs.numpy_input_fnという関数でinput_fnとして渡せるものに変換できます。
また入力データは引数xに指定しますが、これは入力データそのものではなく、入力データを値とする辞書である必要があります。

さらに、predictメソッドの返り値は各出力のイテレータであることに注意してください。