クロの制作日記

初心者が頑張るTensorflow1.13.1 eager execution入門(mnist分類)

はじめに

Tensorflow1.Xでは以下の記事で解説したようにDefine and Runで実行するのがデフォルトでした。
www.kuroshum.com

Define and Runとは以下の画像のように、最初に処理の金型(グラフ)を作成してから実行する際にデータを入力することで、処理を行う形式のことです。

f:id:kurora-shumpei:20191126151751p:plain

ただ、Tensorflow2.0からはPyTorchと同じようにDefine by Run(上の画像でのPythonの処理の仕方)で実行する「Eager Execution」がデフォルトになりました。また、Tensorflow1.xでも1.5以上ならばEager Executionを呼び出すことで使用することができます。

なので、今回は以下の記事で作成したコードをTensorflow1.13.1でのEager Executionモードで実行できるように書き換えようと思います。
www.kuroshum.com



コード

Eager Executionを用いたコードは以下のURLから確認してください。
github.com

ここからはEager Executionで実行するために書き換えた部分の解説をしていきます。

GPUのメモリの設定

GPUを用いてTensorflowのコードを動かすときに何も設定しないとGPUのメモリを全て使ってしまいます。

そのGPUのメモリを抑制する設定を標準のTensorflow(Define and Run)の場合ではこんな感じに書きますが、

config = tf.ConfigProto(gpu_options=tf.GPUOptions(allow_growth=True))
sess = tf.Session(config=config)

Eager Executionではこんな感じです。

config = tf.ConfigProto(allow_soft_placement=True)
config.gpu_options.allow_growth = True

このconfigを次のEagerを使うように設定するときに引数として渡します。

Eager Executionモードを設定

Tensorflow1.xではEagerがデフォルトではないので、最初の方でeager使いますよーと以下のように明示してあげる必要があります。

tfe.enable_eager_execution(config=config)

ここでconfigを渡すことで上のgpuのメモリ設定も反映されます。

subclassing APIを用いたモデル定義

subclassing APIはeagerの機能というよりはkerasのモデル定義の機能です。eagerではkeras APIの使用が推奨されている(Tensorflow2.xではkerasが標準APIになっている)ので、今回はkerasでモデルの定義などを行っていきます。

kerasのモデル定義のAPIはsubclassing API以外にも「function API」、「Sequential API」などがありますが、今回は使いません。詳細が知りたい人は以下の記事を参考にしてください。
note.nkmk.me


subclassing APIでは以下のようにモデルを定義します。

#--------------------------------------------------------------------------------
# 分類モデル
class ClassifierModel(tf.keras.Model):
    
    # モデルのレイヤー定義
    def __init__(self, output_dim, **kwargs):
        super(ClassifierModel, self).__init__(**kwargs)
        # wx + bを出力
        # 活性化関数はsoftmax
        self.fc1 = tf.keras.layers.Dense(output_dim, activation='softmax')
    
    # モデルの実行
    def call(self, x_t):
        pred = self.fc1(x_t)
    
        return pred
#--------------------------------------------------------------------------------

tf.keras.Modelを継承したclassを作成することでモデルを定義します。コンストラクタ__init__()ではモデルのレイヤーを定義し、call()ではそのレイヤーを呼び出し、計算した結果を返します。

subclassing APIで作成されたモデルは以下のようにして呼び出すことができます。

# モデルの定義
# output_dim : 出力結果の次元
classifier_model = ClassifierModel(output_dim)
# x_t : 入力データ
classifier_model(x_t)

クロスエントロピー損失関数

クロスエントロピー損失関数はtf.keras.losses.categorical_crossentropy()を用います。tf.keras.lossesでのクロスエントロピー損失にはbinary_crossentropyもありますが、これは二値分類に使う関数ですので、今回のような10クラス分類ではcategoricalを使用します。

ちなみに、categorical_crossentropyに渡す引数(真値と予測値)にはどちらもone-hot表現のデータ(shapeが[データ数, クラス数])を渡します。

コードは以下のようになります。

#--------------------------------------------------------------------------------
# クロスエントロピー損失関数
# args : 
#   model : 定義したモデル(Classifier model)
#   x_t : 学習 or テスト データ
#   y_t : 教師データ
def cross_entropy_loss(model, x_t, y_t):
    # categorical_crossentropyの入力データはone-hot表現なので
    # y_t, model(x_t)はどちらも(データ数, 10)
    return tf.reduce_mean(tf.keras.losses.categorical_crossentropy(tf.to_float(y_t), tf.to_float(model(x_t))))
#--------------------------------------------------------------------------------

引数をそのまま渡すと
「tensorflow.python.framework.errors_impl.InvalidArgumentError: cannot compute Mul as input #0(zero-based) was expected to be a float tensor but is a double tensor [Op:Mul] name: mul/」
っていうエラーがでましたが、引数をfloat型に変換してやると消えました。

学習を行う関数

subclassing APIを使用するとclassifier_model.fit()と書くだけで学習してくれるらしいのですが、以下のように自分で記述することができます。

#--------------------------------------------------------------------------------
# 1エポックで行う学習(ミニバッチ)
# args : 
#   model : 定義したモデル(Classifier model)
#   dataset : 学習データと教師データをtf.data.Datasetのコレクションに変換したやつ
#   loss_list : lossを記録する
#   optimizer : 最適化関数
def fit(model, dataset, loss_list, optimizer=tf.train.AdamOptimizer(), training=False):

    # 損失関数から勾配を計算する
    loss_and_grads = tfe.implicit_value_and_gradients(cross_entropy_loss)
    
    # ミニバッチ
    for _, (x, y) in enumerate(dataset):
        
        # 学習時は損失と勾配を計算, テスト時は損失のみ計算
        if training:
            # 損失と勾配を計算
            loss, grads = loss_and_grads(model, x, y)

            # 勾配を更新
            optimizer.apply_gradients(grads)
        else:
            loss = cross_entropy_loss(model, x, y)

        # 損失を記録
        loss_list.update_state(loss)
    
    # ミニバッチで記録した損失の平均を返す
    return loss_list.result()
#--------------------------------------------------------------------------------

ここで、 loss_and_gradsは返り値として、loss(損失の値), grads(勾配)としていますが、gradsの中には、勾配のほかにも重みが保存されています。その勾配と重みをapply_gradients()に渡すことで勾配を更新してくれます。

mnistのnumpyデータをtensorに変換

define by Runになったとはいえ、tensorflowやkerasの関数を使うときは以下のようにデータをtensorに変換する必要があります。

# numpyデータをtensorに
xData_train = tf.convert_to_tensor(xData_train)
xData_test = tf.convert_to_tensor(xData_test)
yData_train = tf.convert_to_tensor(yData_train)
yData_test = tf.convert_to_tensor(yData_test)

また、eagerでは逆にtensorからnumpyに変換することができます。方法は上で変換したxData_trainを例にすると、xData_train.numpy()という感じです。

ちなみに、pdbを使ってデバッグすることもできちゃうのですが、以下のようにnumpyに変換していないxData_trainを確認してみると、tensorの中にnumpyという項目があり、そこに値が保存されていました。

(Pdb) xData_train
<tf.Tensor: id=1, shape=(56000, 784), dtype=float32, numpy=
array([[0., 0., 0., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.],
       ...,
       [0., 0., 0., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.]], dtype=float32)>

そこで、xData_trainのデータ型を調べてみると、「」というようにただのtensorではなくEagerTensorになっており、eagerモードではeager専用のtensorオブジェクトを使って処理を行っているようです。

このEagerTensorのおかげでデバッグがすごい楽になりました(define and runではsess.run()しないとtensorの値を確認することができなかった)。

tf.data.Dataset

これもeagerモードの機能ではないのですが、とても使いやすかったので軽く紹介します。

詳しい説明は以下のサイトがわかりやすいので参考にしてください。
qiita.com

以下のように、データをDatasetに登録?することでデータをシャッフルしたり、指定したバッチサイズにデータを簡単に分割することができます。

# 入力データをdatasetに
# シャッフルとバッチを作成
dataset_train = tf.data.Dataset.from_tensor_slices((xData_train, yData_train))
dataset_train = dataset_train.shuffle(buffer_size=num_data_train)
dataset_train = dataset_train.batch(BATCH_SIZE, drop_remainder=True)

dataset_test = tf.data.Dataset.from_tensor_slices((xData_test, yData_test))
dataset_test = dataset_train.shuffle(buffer_size=num_data_test)
dataset_test = dataset_train.batch(BATCH_SIZE, drop_remainder=True)

そして以下のように、for文でバッチデータをx, yに渡すことができます。

for _, (x, y) in enumerate(dataset):




最後に

Eager Executionを用いてmnistのコードを改良しました。

Define by Runが圧倒的に使いやすかったのとkerasが低レベルの実装もできるようになっていた(元から?)ので、今後はeager + kerasでやっていこうかなと思います。

今回はtensorflow1.13.1でeagerモードに触れましたが、今後はtensorflow2.xでの実装も検討しようかなと思います。