LoginSignup
5
5

More than 3 years have passed since last update.

Estimator APIの自分用チュートリアル

Last updated at Posted at 2019-05-05

Estimator APIを使い始めたはいいもののチュートリアルにTF2.0でdeprecatedになってしまうものが多く含まれておりヽ(`Д´)ノ ウワァァァン となってしまった自分への解説メモ。

なおチュートリアルということでデータセットにはみんな大好きタイタニックを使う。
Notebook全体はこちらのColaboratoryにて公開している。

TL;DR

  • この記事はTensorFlow Estimator APIをとりあえず使いたい自分のためのチュートリアル
  • feature_columnとかtf.estimatorとかの詳しいことは書いてない(長さの都合)
  • tf.feature_columnを使って特徴量のtensorを作ろう
  • tf.dataを使ってfeatureとtargetをgenerateするinput_fnを作ろう
  • tf.estimatorを用いてモデル作成しよう
  • 予測するときのinput_fnの作り方は模索中

今回使用するデータセット

Titanicデータセットを用いる。
が、今回はチュートリアルという性格上使用するカラムを以下のように制限している。

# ライブラリインポート
import os
import itertools
from time import strftime, gmtime
import numpy as np
import pandas as pd
import tensorflow as tf

df = pd.read_csv('./data/preprocessed/train.csv')
df.head()
Survived Sex Age Fare Pclass Cabin
0 female NaN 15.2458 3 NaN
0 male 31.0 10.5000 2 NaN
0 male 31.0 37.0042 2 NaN
0 male 20.0 4.0125 3 NaN
0 male 21.0 7.2500 4 NaN

* ここで使用しているtrain.csvは元々のtrain.csvではなくカラム制限しtrain/eval用にsplitした後のcsv

特徴量の作成

使用カラムの定義

使用するcsvファイルにどういうカラムがありそれぞれのカラムのデフォルト値が何であるかを定義する。
ここのHEADER_DEFAULTSでの型は後々重要になってくるのできちんと書くこと。

HEADER = df.columns.to_list()
# ['Survived', 'Sex', 'Age', 'Fare', 'Pclass','Cabin']
HEADER_DEFAULTS = [['0'], ['female'], [30.00], [32.00], [1], ['NaN']] 
# AgeとFareのデフォルト値は平均としている

上記HEADERのうちSurvivedを目的変数(TARGET)として指定。
二値分類の場合stringとして渡す必要があるため変換してからリスト化する。

TARGET = 'Survived'
TARGET_LABEL = list(df.Survived.map(str).unique())
# ['0','1']

特徴量として使用するカラムの指定。
今回連続変数として使用するカラムとカテゴリカル変数として使用するカラムの二種類があるためカラムリストを分けて定義したものも用意。
こうしておくと後々便利になる(と自分は思っている)。

CATEGORICAL_FEATURESではそれぞれのカラムをキーとして付与している値は後ほど使用する。

NUMERICAL_FEATURE_NAMES = ['Age', 'Fare']

# カテゴリカル変数として使用するカラムたち
CATEGORICAL_FEATURES = {
    'Sex': list(df.Sex.unique()), # ['male', 'female']
    'Pclass': len(df.Parch.unique()), # [1,2,3]
    'Cabin': 30 # 一旦適当な数字をいれている
}
CATEGORICAL_FEATURE_NAMES = list(CATEGORICAL_FEATURES.keys())

# 特徴量として使用するカラム名のリスト
FEATURE_NAMES = NUMERICAL_FEATURE_NAMES + CATEGORICAL_FEATURE_NAMES

特徴量のデータ型定義

先ほどのカラム名リストを用いて特徴量のデータ型を定義していく
ここではそれぞれのfeature_columnに対する説明は割愛(長くなるため/参考リンク

1. 連続変数

数値型のカラム定義は以下のようにtf.feature_column.numeric_columnを用いて行う。

tmp_num_feature = tf.feature_column.numeric_column('Age')

tf.feature_column.numeric_columnに渡しているのはカラム名。
今回連続変数として使用するカラムは2つあるためそれぞれ作成しtensorをリスト化する。

numerical_features = []
for column in NUMERICAL_FEATURE_NAMES:
    numerical_features.append(tf.feature_column.numeric_column(column))

print(numerical_features)
>>> [NumericColumn(key='Age', shape=(1,), default_value=None, dtype=tf.float32, normalizer_fn=None),
>>> NumericColumn(key='Fare', shape=(1,), default_value=None, dtype=tf.float32, normalizer_fn=None)]

本当はこの段階で正規化できるししなくてはいけないがごちゃごちゃするので今回は割愛。

2. カテゴリカル変数

カテゴリカル変数の場合は特徴量作成の方法に応じて以下の4つのうち一つを選択する。

  • tf.feature_column.categorical_column_with_hash_bucket
  • tf.feature_column.categorical_column_with_identity
  • tf.feature_column.categorical_column_with_vocabulary_list
  • tf.feature_column.categorical_column_with_vocabulary_file

また入力するtensorはdenseである必要があるため、それぞれのカラムの定義に加え

  • tf.feature_columns.indicator_column
    もしくは
  • tf.feature_columns.embedding_column

でwrapする必要がある。

2.1 categorical_column_with_vocabulary_listを使う

カラムSexの定義にcategorical_column_with_vocabulary_listを用いる。
categorical_column_with_vocabulary_listではカテゴリカル変数のカテゴリをlistで渡しtensorを作成するメソッド。
先ほど作成したCATEGORICAL_FEATURESのdictはここで使用する。

categorical_sex_raw = tf.feature_column.categorical_column_with_vocabulary_list('Sex', CATEGORICAL_FEATURES['Sex'], num_oov_buckets=1)
# CATEGORICAL_FEATURES['Sex'] = ['male','female']

categorical_sex = tf.feature_column.indicator_column(categorical_sex_raw)

ここで例えば

male
female

という内容のファイルvocab_sex.txtが存在する場合、tf.feature_column.categorical_column_with_vocabulary_fileを用いて以下のように書くこともできる。1

categorical_sex = \
   tf.feature_column.categorical_column_with_vocabulary_file('Sex', vocabulary_file='vocab_sex.txt', vocabulary_size=2, num_oov_buckets=1)
2.2 categorical_column_with_identityを使う

カラムPclassは[1,2,3]というIDから成るカテゴリカル変数。
このようにIDを連続変数でなくカテゴリカル変数として扱いたいときにcategorical_column_with_identityを用いて以下のようにtensorを作成する。

categorical_pclass_raw = tf.feature_column.categorical_column_with_identity(key='Pclass', num_buckets=CATEGORICAL_FEATURES['Pclass'], default_value=1)
categorical_pclass = tf.feature_column.indicator_column(categorical_pclass_raw)
2.3 categorical_column_with_hash_bucketを用いる

カラムCabinは184種類の値のあるカテゴリカル変数。
今回はある程度のざっくりとしたカラムにするためtf.feature_column.categorical_column_with_hash_bucketを用いてカテゴリ化する。
またカテゴリ化したものを今度はembedding_columnでwrapする。

categorical_embedding_dim = 128

categorical_cabin_raw = tf.feature_column.categorical_column_with_hash_bucket('Cabin', CATEGORICAL_FEATURES['Cabin'])
categorical_cabin = tf.feature_column.embedding_column(categorical_column=categorical_cabin_raw, dimension=categorical_embedding_dim)

最後に作成した変数を全てリストにする。


# カテゴリカル変数はまとめておく
categorical_features.append(categorical_sex)
categorical_features.append(categorical_pclass)
categorical_features.append(categorical_cabin)

# 全ての特徴量をまとめる
input_features = numerical_features + categorical_features

入力データの処理

次にcsvから入力されたデータをバッチ分だけ持ってくるgenerator的なものを作成する。
Estimator APIにおけるgeneratorはinput_fnと呼ばれているらしくここではその名称に合わせて変数・関数を作成している。

まずはここの核となるcsv_input_fnの全体像を示す。

def csv_input_fn(config, phase, mode=tf.estimator.ModeKeys.EVAL):
    file_names = tf.matching_files(config[phase]['filename_pattern'])
    dataset = tf.data.TextLineDataset(filenames=file_names)
    dataset = dataset.skip(config[phase]['skip_header_lines'])

    shuffle = True if mode == tf.estimator.ModeKeys.TRAIN else False
    if shuffle:
        dataset = dataset.shuffle(config[phase]['batch_size'] * 2,
                                  seed=0,
                                  reshuffle_each_iteration=True)    

    dataset = dataset.batch(config[phase]['batch_size'])
    dataset = dataset.map(lambda csv_row: parse_csv_row(csv_row)) # parse_csv_rowは自作関数(後述)
    dataset = dataset.repeat(config[phase]['num_epochs'])
    iterator = dataset.make_one_shot_iterator()

    features, target = iterator.get_next()

    return features, target

以下それぞれのブロックについて順を追って説明する。
なおここでは以下のようなdictをconfigとして設定している。

# 次のセルで使用する設定
# 個人的にはconfigparserを使っているがここでは説明のため専用のdictを作成する

config = {
    "train": {
        "filename_pattern" : "./data/preprocessed/train*",
        "batch_size" : 128,
        "max_steps" : 100000,
        "num_epochs" : 2000,
        "skip_header_lines" : 1
    },
    "eval": {
        "filename_pattern" : "./data/preprocessed/eval*",
        "batch_size" : 128,
        "max_steps" : None,
        "num_epochs" : 1,
        "skip_header_lines" : 1
    },
    "test": {
        "filename_pattern" : "./data/preprocessed/test.csv",
        "batch_size" : 128,
        "max_steps" : None,
        "num_epochs" : 1,
        "skip_header_lines" : 1
    }
}

データの読み込み

まずはcsvファイルを読み込む。
train-0.csv, train-1.csvのようにファイルが分かれていてもパターンに一致すれば読み込むようtf.matching_filesを用いる。
ヘッダがある場合はその分だけスキップするように指定する。

file_names = tf.matching_files(config[phase]['filename_pattern']) # trainの場合は'/home/Titanic/data/preprocessed/train*'
dataset = tf.data.TextLineDataset(filenames=file_names)
dataset = dataset.skip(config[phase]['skip_header_lines'])

学習データのシャッフル

学習時データの取り出し方が同じにならないようシャッフルする。
ここでは学習/評価/予測かどうかがtf.estimator.ModeKeysで指定されているのでそれがtf.estimator.ModeKeys.TRAINのときのみシャッフルするよう指定

shuffle = True if mode == tf.estimator.ModeKeys.TRAIN else False
if shuffle:
    dataset = dataset.shuffle(config[phase]['batch_size'] * 2,
                              seed=0,
                              reshuffle_each_iteration=True)

データの取り出し

config内で指定したbatch_sizeの数だけデータをgenerateする。
取り出しが一回で終わらないようnum_epochs分だけ繰り返すよう引数を与える。

dataset = dataset.batch(config[phase]['batch_size'])
dataset = dataset.map(lambda csv_row: parse_csv_row(csv_row))
dataset = dataset.repeat(config[phase]['num_epochs'])
iterator = dataset.make_one_shot_iterator()

features, target = iterator.get_next()

なおここで使用しているparse_csv_rowは以下のような関数
入力されるcsv_rowとHEADER_DEFAULTSのデータ型が合ってないとコケるので注意。2

def parse_csv_row(csv_row):
    columns = tf.decode_csv(csv_row, record_defaults=HEADER_DEFAULTS)
    features = dict(zip(HEADER, columns))

    target = features.pop(TARGET)
    return features, target

TrainSpec / EvalSpecの作成

TrainSpecおよびEvalSpecは入力されたinput_fnやiterationの回数、さらにはhookまで管理してくれる便利なもの(参考リンク)。3
実際に学習を回すときはこのTrainSpecとEvalSpecを用いる。

TrainSpecの作成

まずはTrainSpecの作成。
train用のinput_fnを作成しそれをtf.estimator.TrainSpecに渡す。
Estimator全般そうだがepochという単位ではなくstep(iteration)という単位で学習の回数を指定することが多い(と自分は感じている)。

# train用のinput_fnを作成
train_input_fn = lambda: csv_input_fn(config=config, 
                                      phase='train', 
                                      mode=tf.estimator.ModeKeys.TRAIN)

train_spec = tf.estimator.TrainSpec(input_fn=train_input_fn,
                  max_steps=int(config['train']['max_steps'])
                  )

EvalSpecの作成

TrainSpecの作成方法と大まかには同じだがモデルファイル出力用のexportersという引数や評価のタイミングに関するthrottle_secsといった引数が追加されている(いずれも詳細は割愛)。

eval_input_fn = lambda: csv_input_fn(config=config,
                                     phase='eval', 
                                     mode=tf.estimator.ModeKeys.EVAL)

eval_spec = tf.estimator.EvalSpec(input_fn=eval_input_fn,
                exporters=[tf.estimator.LatestExporter(name="estimate",  
                                                       serving_input_receiver_fn=json_serving_input_fn)],
                steps=None,
                throttle_secs = 15,
                )

なおexporters内で使用しているjson_serving_input_fnという関数の中身は以下の通り。

def json_serving_input_fn():
    receiver_tensor = {}
    for feature_name in FEATURE_NAMES:
        # 変数のデータ型をカラムに応じて指定
        if feature_name in NUMERICAL_FEATURE_NAMES:
            dtype = tf.float32
        elif feature_name == 'Pclass':
            dtype = tf.int32
        else:
            dtype = tf.string
        receiver_tensor[feature_name] = tf.placeholder(shape=[None], dtype=dtype)

    return tf.estimator.export.ServingInputReceiver(receiver_tensor, receiver_tensor)

estimatorの作成

いわゆるモデル部分の作成。
モデルファイルの出力先や分散学習有無の設定用変数をあらかじめ作成する。
RunConfigも色々書くべきことあるような気がするが今回は割愛(参考リンク)。

# モデルファイルの出力先を指定し設定ファイルを作成
raw_execute_time = gmtime()
execute_time = strftime("%Y%m%d_%H%M%S", raw_execute_time )
model_dir = os.path.join('./models/', execute_time)

# 設定ファイル(RunConfig)の作成
run_config = tf.estimator.RunConfig().replace(model_dir=model_dir)

なおmodel_dirを固定してしまうと毎回同じところからモデルを呼び出し→書き込みしてしまうため実行時間ごとにディレクトリを作成するようにしている。
(雑に言うと存在するmodel_dirを指定すればそれまでの学習経過を呼び出すことができる)

tf.estimatorにはPremade Estimatorsと呼ばれる出来合いのものとCustom Estimatorsという自作のものの2つが存在している。
今回は単なるNNなのでPremade Estimatorを使用するが、複雑なモデルにする場合はCustom Estimatorを自分で作る必要がある(参考リンク)。

# estimatorの作成。今回はDropoutつきでDense2層+出力層という構成
dropout_prob = 0.3
hidden_units = [64, 32]

estimator = tf.estimator.DNNClassifier(
                        hidden_units = hidden_units,
                        feature_columns = input_features,
                        model_dir = model_dir,
                        n_classes = len(TARGET_LABEL),
                        label_vocabulary = TARGET_LABEL,
                        optimizer = tf.train.AdamOptimizer(),
                        activation_fn = tf.nn.relu,
                        dropout = dropout_prob,
                        config = run_config
                    )

学習

これまで作成したestimator, train_spec, eval_specをtf.estimator.train_and_evaluateに渡すだけ。

tf.estimator.train_and_evaluate(estimator, train_spec, eval_spec)

予測

予測の場合もこれまで同様input_fnを作る。
今回はすでにestimatorが学習を終えているためそのestimatorを用いてpredictする。4

predict_input_fn = lambda: csv_input_fn(config=config, 
                                      phase='test', 
                                      mode=tf.estimator.ModeKeys.PREDICT)
predictions = estimator.predict(input_fn=predict_input_fn)

自分の場合予測結果の出力は以下のようにitertoolsを用いて行なっている

values = list(map(lambda item: item["logistic"][0],list(itertools.islice(predictions, test_size))))

あとはSubmission用のcsvを作成して提出するだけ。
カラム適当に選んでモデルも適当なので当たり前だがスコアはよくなかった。

今回長さの都合で割愛しているがそれぞれのAPIもそこそこ設定すべき部分が多い。
それぞれのAPIについては誰か親切な方がまとめてくれるのだと思う(適当)。

お粗末。


  1. おそらく実際上はこっちの方が使いやすい。  

  2. 例えばstr型で['0']と渡したいのにデフォルトが[0]となっているとエラーになってしまう。 

  3. ここには書いていないがhookにEarlyStopping用の関数を入れることもできる。 

  4. csv_input_fn作ったからそれをそのまま使っているのだけれども、実際の予測用ファイルには目的変数の部分がないのでここをどう工夫するかはいまだ検討中(今は元データに全ての値がゼロとなるようなカラムをわざわざ追加している)。 

5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5