クロの制作日記

初心者が頑張るTensorflow2入門(NN + CNNでのmnist分類)

最初に

前回の記事でTensorflow1.13.1のEager Executionでmnistの分類モデルを作成しました。
www.kuroshum.com

上の記事はほんの何時間前かに作成した記事なのですが、tensorflow1.15でないと使えない関数があり、色々と面倒になったのでTensorflow2に鞍替えしました。

なので、今回はTensorflow2.0.0を使ったmnist分類モデル作成の方法を解説していこうかなと思います。また、今回はNNとCNNの2つのバージョンのコードを作成しました。



コード

Tensorflow2を用いたコードは以下を確認してください。
github.com

基本的にtensorflow1.x系のEager Executionと同じなのですが、ちょこちょこ使えない関数があったので、それもまとめておきました。

gpuのメモリの設定

Tensorflow1.x系ではこんな感じでしたが、

# gpuのメモリを全て使わないように設定
config = tf.ConfigProto(allow_soft_placement=True)
config.gpu_options.allow_growth = True
# eagerモードを設定
tfe.enable_eager_execution(config=config)

Tensorflow2.x系では以下の記事によるとこんな感じらしいです。
qiita.com

physical_devices = tf.config.experimental.list_physical_devices('GPU')
if len(physical_devices) > 0:
    for k in range(len(physical_devices)):
        tf.config.experimental.set_memory_growth(physical_devices[k], True)
        print('memory growth:', tf.config.experimental.get_memory_growth(physical_devices[k]))
else:
    print("Not enough GPU hardware devices available")

mnistのダウンロード

以前のコードでは、scikit-learnのfetch_openmlを使用していましたが、kerasに同じような機能があり、しかも最初からtrainとtestにデータを分割してくれているので、こちらを使用しました。
keras.io
ただ、validationデータはないので、validate(検証)を行いたいときはtrainとtestのデータをくっつけてからtrain・test・validationデータに分割するという作業をしないといけないですね。

load_mnist_data(NNバージョン)

NNでは入力データを[28.28]の画像ではなく[784]のようにshapeを変換する必要があります。

#--------------------------------------------------------------------------------
# mnistデータをロードし、学習データとテストデータを返す
def load_mnist_data():
    # mnistデータをロード
    (xData_train, yData_train), (xData_test, yData_test) = mnist.load_data()

    # 画像データ データ数*784
    xData_train = xData_train.reshape([-1, 28*28]).astype(np.float32)
    xData_test = xData_test.reshape([-1, 28*28]).astype(np.float32)
    
    # 0-1に正規化する
    xData_train /= 255 
    xData_test /= 255 

    # ラベルデータ70000
    yData_train = yData_train.astype(np.int32) 
    yData_test = yData_test.astype(np.int32) 

    return (xData_train, yData_train), (xData_test, yData_test)
#--------------------------------------------------------------------------------
load_mnist_image_data(CNNバージョン)

CNNでは入力データを[28.28]の画像ではあるのですが、チャネルを追加する必要がありますので、[28,28,1]という風にshapeを変換する必要があります。

#--------------------------------------------------------------------------------
#mnistデータをロードし、学習データとテストデータを返す
def load_mnist_image_data():
    # mnistデータをロード
    (xData_train, yData_train), (xData_test, yData_test) = mnist.load_data()

    # 画像データ データ数*28*28
    xData_train = xData_train.astype(np.float32)[:,:,:,np.newaxis]
    xData_test = xData_test.astype(np.float32)[:,:,:,np.newaxis]

    # 0-1に正規化する
    xData_train /= 255 
    xData_test /= 255 

    # ラベルデータ70000
    yData_train = yData_train.astype(np.int32) 
    yData_test = yData_test.astype(np.int32) 

    return (xData_train, yData_train), (xData_test, yData_test)
#--------------------------------------------------------------------------------

分類モデル

CNNのモデルを新しく作成したのと、NNはbatch normalizationを追加 & 層をふやしました。別に層を増やしたりバッチ正規化はなくても良かったのですが、以下の公式のモデルとほとんど同じになって面白くなかったので、色々追加してみました。

CNNの方も、コメントアウトしていますが、層を増やしたりバッチ正規化をしているので、動かしてみてください 。
www.tensorflow.org

batch normalizationの詳しい仕様はこちらを参考にしてください。
note.nkmk.me

ClassifierModel(NNバージョン)
#--------------------------------------------------------------------------------
# 分類モデル
class ClassifierModel(tf.keras.Model):
    
    # モデルのレイヤー定義
    # args : 
    #   output_dim : 出力結果の次元  
    def __init__(self, **kwargs):
        super(ClassifierModel, self).__init__(**kwargs)
        
        # 1層目        
        # wx + bを出力
        # 活性化関数はsoftmax
        self.fc1 = tf.keras.layers.Dense(HIDDEN_DIM, activation='relu')
        #self.dropout1 = tf.keras.layers.Dropout(keep_prob)
        self.bn1 = tf.keras.layers.BatchNormalization()

        # 2層目
        self.fc2 = tf.keras.layers.Dense(HIDDEN_DIM, activation='relu')
        #self.dropout2 = tf.keras.layers.Dropout(keep_prob)
        self.bn2 = tf.keras.layers.BatchNormalization()

        # 3層目
        self.fc3= tf.keras.layers.Dense(CLASS_NUM, activation='softmax')
    
    # モデルの実行
    def call(self, x_t, training=False):
        
        # 1層目
        x = self.fc1(x_t)
        x = self.bn1(x, training=training)

        # 2層目
        x = self.fc2(x)
        x = self.bn2(x, training=training)

        # 3層目
        x = self.fc3(x)
    
        return x
#--------------------------------------------------------------------------------
ClassifierModel(CNNバージョン)

tf.keras.layers.Conv2Dで注意すべきなのは、data_formatに「channels_last」を指定する必要がある点です。

以下の公式サイトのinput shapeの箇所に書いていますが、データ形式

  • [データ数、チャネル数、高さ、幅]なら「channels_first」
  • [データ数、高さ、幅、チャネル数]なら「channels_last」

という風に設定する必要があります。
www.tensorflow.org

#--------------------------------------------------------------------------------
# 分類モデル
class ClassifierCnnModel(tf.keras.Model):
    
    # モデルのレイヤー定義
    # args : 
    #   output_dim : 出力結果の次元  
    def __init__(self, data_format='channels_last', **kwargs):
        super(ClassifierCnnModel, self).__init__(**kwargs)

        # 1層目        
        self.conv1 = tf.keras.layers.Conv2D(filters=32, kernel_size=3, data_format=data_format, activation='relu')
        #self.bn1 = tf.keras.layers.BatchNormalization()

        #self.conv2 = tf.keras.layers.Conv2D(filters=32, kernel_size=3, data_format=data_format, activation='relu')
        #self.bn2 = tf.keras.layers.BatchNormalization()

        self.flatten = tf.keras.layers.Flatten()

        # 3層目
        self.fc1 = tf.keras.layers.Dense(HIDDEN_DIM, activation='relu')
        #self.bn3 = tf.keras.layers.BatchNormalization()

        self.fc2 = tf.keras.layers.Dense(CLASS_NUM, activation='softmax')
        #self.bn4 = tf.keras.layers.BatchNormalization()
    
    # モデルの実行
    def call(self, x_t, training=tf.cast(False, tf.bool)):

        # 1層目
        x = self.conv1(x_t)
        #x = self.bn1(x, training=training)

        #x = self.conv2(x)
        #x = self.bn2(x, training=training)

        x = self.flatten(x)

        # 3層目
        x = self.fc1(x)
        #x = self.bn2(x, training=training)

        x = self.fc2(x)
    
        return x
#--------------------------------------------------------------------------------

accuracyの計算

kerasには以下のように真値と予測値を渡すとaccuracyを計算してくれる関数があります。
www.tensorflow.org

acc_list = tf.keras.metrics.CategoricalAccuracy()

acc_list.update_state(y, model(x))

学習とテスト

以前のコードでは、学習とテストを同じ関数fit)に実装していたのですが、以下のように学習とテストで別々の関数を実装した方が良いみたいです。

関数名の上に書いてある「@tf.function」は以下の記事を参考にしてください(ダイレクトマーケティング)。
www.kuroshum.com

学習
#--------------------------------------------------------------------------------
# 1エポックで行う学習(ミニバッチ)
# args : 
#   model : 定義したモデル(Classifier model)
#   dataset : 学習データと教師データをtf.data.Datasetのコレクションに変換したやつ
#   loss_list : lossを記録する
#   optimizer : 最適化関数
@tf.function
def train_step(model, dataset, loss_list, acc_list, optimizer=tf.keras.optimizers.Adam()):
    # 損失関数から勾配を計算する
    #loss_and_grads = tfe.implicit_value_and_gradients(cross_entropy_loss)
    
    # ミニバッチ
    for x, y in dataset:
            
        # 自動微分のAPI
        with tf.GradientTape() as tape:
            # cross-entropy-lossの計算
            loss = cross_entropy_loss(model(x, training=tf.cast(True, tf.bool)), y)
        
        # 損失から勾配を計算
        grad = tape.gradient(loss, sources=model.trainable_variables)
        
        # 損失と勾配を計算
        #loss, grads = loss_and_grads(model, x, y)

        # 勾配を更新
        optimizer.apply_gradients(zip(grad, model.trainable_variables))

        # 損失を記録
        loss_list.update_state(loss)

        # accuracyを記録
        acc_list.update_state(y, model(x))
    
    # ミニバッチで記録した損失の平均を返す
    return loss_list.result(), acc_list.result()
#--------------------------------------------------------------------------------
テスト
#--------------------------------------------------------------------------------
# 1エポックで行うテスト(ミニバッチ)
# args : 
#   model : 定義したモデル(Classifier model)
#   dataset : 学習データと教師データをtf.data.Datasetのコレクションに変換したやつ
#   loss_list : lossを記録する
@tf.function
def test_step(model, dataset, loss_list, acc_list):

    # ミニバッチ
    for x, y in dataset:
        
        # テストの損失を計算
        loss = cross_entropy_loss(model(x), y)

        # テストの損失を記録
        loss_list.update_state(loss)

        # テストaccuracyを記録
        acc_list.update_state(y, model(x))
    
    return loss_list.result(), acc_list.result()

tf.to_float()

tf.to_float()はtensorflow2.x系ではなくなっているので、tf.cast()に変更しました。

optimizer

tensorflow1.x系でOptimizer(最適化関数)を使うためにはtf.trainを用いるのですが、

tf.train.AdamOptimizer()

Tensorflow2.xではtf.keras.optimizerを用います。
www.tensorflow.org

optimizer=tf.keras.optimizers.Adam()

ここではAdamを用いていますが、もちろんそれ以外の最適化関数も使えます。

勾配計算

Tensorflow1.x系では、tensorflow.contrib.eagerのimplicit_value_and_gradientsを用いるのですが、

loss_and_grads = tfe.implicit_value_and_gradients(cross_entropy_loss)
loss, grads = loss_and_grads(model, x, y)

Tensorflow2.x系ではtf.GradientTape()を使用します。
www.tensorflow.org

with tf.GradientTape() as tape:
    # cross-entropy-lossの計算
    loss = cross_entropy_loss(model, x, y)
            
# 損失から勾配を計算
grad = tape.gradient(loss, sources=model.trainable_variables)

# 勾配を更新
optimizer.apply_gradients(zip(grad, model.trainable_variables))

個人的にwith文で定義したtapeをwith文の外で使うのは奇妙な感じがしますが、公式でもそうしているのでそういうものなのでしょう。

tf.function(おまけ)

tf.functioinを使えば処理速度を早くすることができます。詳細は以下の記事で
www.kuroshum.com

実際にどんなもんかと思い、速度を計った結果が以下です。

tf.functionなし

処理速度:597.3312249183655

tf.functionあり

処理速度:143.28664112091064

tf.functionを使うと4倍以上の速度で処理を行うことができました...。すごい...。




最後に

Tensorflow1.x系で俺は頑張るんだと息巻いていたのですが、色々面倒なことが起きたのでtensorflow2に鞍替えしました。

使ってみた感じ、Tensorflow1.x系のeagerモードとそこまで変わらないので、今後はTensorflow2.x系で頑張ってみようかと思います。

ただ、tensorflow2.0.0だと勾配計算でやばいバグがあるらしいので、ちょっと怖い...。詳細がわからないし...。
qiita.com

tensorflow2.1.0だと修正されているらしいのでアップグレードしたいのですが、cuda,cudnnのバージョンもアップグレードしないといけないので...。めんどくさい...。