2) 学習済みモデルの書き出し
学習済みモデルをTensorFlow Servingに載せるには、SavedModel形式で書き出す必要があります。
第3回で作成したPythonコードに対して必要な変更は以下の2点です:
- Estimator作成時に書き出し用の情報を付加するようにする
- 実際に書き出すコードを追加する
(1) Estimator作成部分の変更
Estimator作成のための関数のうち最後に値を返す部分は、学習を行う場合と学習はせず利用のみする場合で条件分岐していました。
今回行いたい「書き出し」操作にも学習は必要ないので、学習はせず利用のみする場合の分岐を利用し、書き出し用の情報を付加するような変更を加えます。
< return tf.estimator.EstimatorSpec(mode=mode, predictions=probabilities) --- > output = tf.estimator.export.ClassificationOutput(scores=probabilities) > return tf.estimator.EstimatorSpec(mode=mode, predictions=probabilities, > export_outputs={'result': output})
上記の通り、EstimatorSpecオブジェクトの作成時にClassificationOutputを渡すように変更しています。これにより、このモデルがデータの分類(Classification)タスクであり、出力としてprobabilities
変数の指すもの、つまり犬猫それぞれの確率を返すということを定義しています。
この定義はTensorFlow Servingで学習済みモデルを動作させる際に利用されます。
上記の変更を適用すると、mydnn_tf.pyは以下のようになります。
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): input_layer = tf.reshape(features["img"], [-1] + list(input_shape)) 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) # for prediction if mode == tf.estimator.ModeKeys.PREDICT: probabilities = tf.nn.softmax(output_layer) output = tf.estimator.export.ClassificationOutput(scores=probabilities) return tf.estimator.EstimatorSpec(mode=mode, predictions=probabilities, export_outputs={'result': output}) # for training 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()) # for evaluation 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
(2) 実際に書き出すコードの追加
モデルを書き出すには、Estimatorオブジェクトのexport_savedmodel
メソッドを呼び出します。
第1引数は保存先ディレクトリ、第2引数は入力データの受け取り方を定義する関数です。
def input_receiver_fn(): # ここでtf.estimator.export.ServingInputReceiverを作成して返す model.export_savedmodel(export_dir, input_receiver_fn)
このコードでは第2引数にわたす関数としてinput_receiver_fn
を定義しています。
最終的にはtf.estimator.export.ServingInputReceiver
オブジェクトを生成して返すのがこの関数の役目です。
ServingInputReceiverは名前の通りTensorFlow Servingに送られてきた入力データの処理の仕方を定義するためのものです。
では、input_receiver_fn
の中身を見てみましょう。
def input_receiver_fn(): input_example = tf.placeholder(dtype=tf.string) input_size_xyc = input_size_x * input_size_y * input_size_c feature_spec = {'img' : tf.FixedLenFeature(dtype=tf.float32, shape=(input_size_xyc))} features = tf.parse_example(input_example, feature_spec) receiver_tensors = {'examples': input_example} return tf.estimator.export.ServingInputReceiver(features, receiver_tensors)
ServingInputReceiverのイニシャライザの第1引数features
は、クライアントから送られてくるデータを受け止めてEstimatorで利用可能な形に加工したものです
(※ 正確には、そのような加工処理を行うTensorFlowの計算工程を示すものです)。
クライアントからは文字列の形にシリアライズされたデータが送られてきますので、それをうまく構造化データにもどしてやる必要があるわけです。
受け取った文字列はまず文字列形式のplaceholderであるinput_example
で受け止めることとします。
そしてその文字列をtf.parse_example
関数に渡してパースします(ちなみにexampleという名前は、TensorFlow Servingのサーバ/クライアント間でデータの受け渡しに使われるクラスtf.train.Example
に由来しています)。
parse_exampleの第2引数として与えているfeature_spec
は、受け取るデータは固定長のfloatデータであり、Estimatorに渡すためにimg
というキーを添えることを指定しています。
parse_exampleの返り値としてfeatures
が得られます。
ただし、このままでは、input_example
は数多くあるplaceholderのひとつに過ぎませんので、クライアントから受け取ったデータをinput_example
で受け止めたいということを明示的に伝える必要があります。
それを行っているのがServingInputReceiverのイニシャライザの第2引数receiver_tensors
です。
全体として、cat_dog_dnn_tf.py
のコードは以下のようになります。
既存の「学習モード」「判定モード」に加えて「書き出しモード」を選択できるようにし、上記の書き出し部分のコードを追加しています。
import argparse import tensorflow as tf import os, sys import numpy as np from datetime import datetime from mydnn_tf_layers import MyDNN 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 = 'ckpt/tf/' export_dir = 'tf_savedmodels/' parser = argparse.ArgumentParser() parser.add_argument("--export", action="store_true", default=False, help="学習済みモデルを本番向けに書き出す") 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) train = not (args.infer or args.export) if train: 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 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) elif args.infer: 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 print(data_infer.shape) # 判定処理の実行 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]) else: print("書き出しモード") def input_receiver_fn(): input_example = tf.placeholder(dtype=tf.string) input_size_xyc = input_size_x * input_size_y * input_size_c feature_spec = {'img' : tf.FixedLenFeature(dtype=tf.float32, shape=(input_size_xyc))} features = tf.parse_example(input_example, feature_spec) receiver_tensors = {'examples': input_example} return tf.estimator.export.ServingInputReceiver(features, receiver_tensors) model.export_savedmodel(export_dir, input_receiver_fn)
※TensorFlow, the TensorFlow logo and any related marks are trademarks of Google Inc.
その他、本コンテンツ内で利用させて頂いた各プロダクト名やサービス名などは、各社もしくは各団体の商標または登録商標です。