【Tensorflow・Keras】Tensorflow・Kerasの重みを見つけたなら

この記事はTensorFlow Advent Calendar 2020の12日目の記事です。

代表的な深層学習のフレームワークはTensorFlow, Keras, PyTorch, Chainerが挙げられます。今回はその中でも、TensorFlow(以下TF)・Kerasのバージョン2の畳み込みニューラルネットワークワーク(以下CNN)のレイヤーごとの重み・出力の取得方法と計算方法について考えたいと思います。

タイトルは映画「小説家を見つけたら」を捩っています。

この記事を読んだら理解できること

  • CNNの計算方法やCNNでの画像サイズ・インプットチャネル・アウトプットチャネル(フィルターの数)・フィルターサイズの計算イメージ
  • TF・KerasでのCNNのレイヤーごとの重み・出力の取得方法と計算方法
  • 自作推論でのCNNの実装感覚が掴める
  • 名著「ゼロから作るDeepLearning①」のCNNのページ(200ページ辺り)が実装から理解できる

どういうときに使用するのか

  • TF・KerasでのCNNのレイヤーごとの重み・出力を確認するケース
  • TF・Kerasの学習済み重みを利用してCやJavaなど他言語で自作推論に適用するケース

環境

OSなど特に指定ありません。

  • Python 3.7.6(バージョン制約ありません。TF2.3が比較的最新のバージョンなので、できるだけPythonも新しいバージョンをおすすめします。)
  • Windows・Linux(Ubuntu)(WindowsならAnaconda・Docker、LinuxならDockerが一般的だと思います。もちろんGoogleColabなどでも大丈夫です。)

ライブラリ

以下のPython Package(pip)は事前にインストールしていてください。GoogleColabなどでは既にインストール済みかと思います。

  • tensorflow-gpu 2.3.0(自分の環境ではGPUを使用してますが、GPUなしでもTFのバージョンが同じなら問題ありません。)
  • numpy 1.19.4(バージョン制約ありませんが、できるだけ新しい方が良いです。)

概略

はじめにまとめを書きます。今回は以下のフローで本記事のテーマを実現します。

  1. 重みとCNNについて理解
  2. TF・KerasでMNISTデータを用いて簡単なモデルを学習
  3. TF・Kerasでのレイヤーごとの重みと出力について理解
  4. 自作推論を実装するための理解

1. 重みとCNNについて理解

重みとCNNについて紹介します。ここでは概略だけに留めたいと思います。

詳しくは別記事で記載予定です。

重みについて

TF・Kerasで保存されるCNNにおける重みとはフィルターの数値であり、学習により最適化されバイアスと共に保存されます。

KerasのAPIであるmodel.save()などで保存されるパラメータ値が上の数値です。(正確にはTF・Keras用にエンコードされているとは思いますが、理解のためにフィルターのパラメータ値と言ってます。)

CNNの計算について

CNNにおいて混乱しやすい部分がチャネルなので、まずは4つのパターンでCNN計算の概略を掴んでもらいます。

  1. インプットのチャネル1、フィルターの数1=>アウトプットのチャネル1
  2. インプットのチャネル3、フィルターの数1=>アウトプットのチャネル1
  3. インプットのチャネル1、フィルターの数3=>アウトプットのチャネル3
  4. インプットのチャネル3、フィルターの数3=>アウトプットのチャネル3

4つのパターン全てでフィルターの数がアウトプットのチャネルであることがわかります。フィルター1つ1つが画像加工アプリなどの加工と思えば想像しやすいかと思います。ここでは、入ってくる画像のチャネルは関係なく、様々なフィルター(加工)を適用し、アウトプットの画像を取得できるといった想像ができれば十分です!

この辺りは、かの有名な「ゼロから作るDeepLearning①」の
200ページ辺りにも記載されているので、気になる方はそちらの
書籍もチェックしてみてください!

CNNの畳み込みのパターンの1-3までが下の図です。自作図です。

「1. インプットのチャネル1、フィルターの数1」通常の畳み込みです。3*3の行列を2つ計算しています。

「2. インプットのチャネル3、フィルターの数1」はインプットチャネルを1チャネルずつフィルターと畳み込みを行い合計してます。

「 3. インプットのチャネル1、フィルターの数3」はフィルターの数だけアウトプットのチャネルが導かれることがわかると思います。

1. インプットのチャネル1、フィルターの数1

2. インプットのチャネル3、フィルターの数1

3. インプットのチャネル1、フィルターの数3

2. TF・KerasでMNISTデータを用いて簡単なモデルを学習

MNISTのデータを用いて重み・バイアス・出力を得るために2つのCNNと2つの全結合を持つ簡単なモデルで学習します。

スクリプトになっているので、手元で学習できる方はそのままコピペで学習し、GoogleColabやJupyterの場合、mainから下のインデントを削除してコピペして使用してください。

5エポックなのですぐに学習終了すると思います。学習後、base_model.hdf5sample.npyが作成されます。今後はbase_model.hdf5をロードしモデルを使用、sample.npyをロードし出力を確認します。

import numpy as np

import tensorflow as tf
from tensorflow.keras import datasets, layers, models
from tensorflow.keras.models import Sequential, Model, load_model
from tensorflow.keras.layers import Dense, Conv2D, Flatten, Dropout, MaxPooling2D, Convolution2D, Activation


def base_model():
    model = models.Sequential()
    model.add(layers.Conv2D(16, (3, 3), activation='relu', padding="same", input_shape=(28, 28, 1), name='conv1'))
    model.add(layers.MaxPooling2D((2, 2), name='maxpool1'))
    model.add(layers.Conv2D(32, (3, 3), activation='relu', padding="same", name='conv2'))
    model.add(layers.MaxPooling2D((2, 2), name='maxpool2'))
    model.add(Flatten(name='flatten1')) 

    model.add(Dense(1024, activation='relu', name='dense1'))
    model.add(layers.Dense(10, activation='softmax', name='dense2'))
    model.compile(optimizer='adam',loss='sparse_categorical_crossentropy',metrics=['accuracy'])

    return model

if __name__ == "__main__":
    (train_images, train_labels), (test_images, test_labels) = datasets.mnist.load_data()

    train_images = train_images.reshape((60000, 28, 28, 1))
    test_images = test_images.reshape((10000, 28, 28, 1))

    # ピクセルの値を 0~1 の間に正規化
    train_images_regularized, test_images_regularized = train_images / 255.0, test_images / 255.0

    model = base_model()
    model.fit(train_images_regularized, train_labels, epochs=5)
    test_loss, test_acc = model.evaluate(test_images,  test_labels, verbose=1)
    print(test_acc)

    model.save("base_model.hdf5")

    # 出力確認のためにNumpyデータを用意
    sample = train_images_regularized[0]
    sample_np = sample.reshape((1, 28, 28, 1))
    np.save('sample', sample_np)

3. TF・Kerasでのレイヤーごとの重みと出力について理解

CNNのレイヤーごとの重み・バイアスを取得するためにmodel.get_weights()メソッドを使用します。

今回のモデルではCNNが2つと全結合が2つなので、重み・バイアスを持つのは4層です。model.get_weights()メソッドでは、重み・バイアスを交互にlistに格納するため、偶数のインデックスが重みであり、奇数がバイアスの数値を表します。

早速実行してみましょう!

import numpy as np

import tensorflow as tf
from tensorflow.keras import datasets, layers, models
from tensorflow.keras.models import Sequential, Model, load_model
from tensorflow.keras.layers import Dense, Conv2D, Flatten, Dropout, MaxPooling2D, Convolution2D, Activation

def get_output(model, layer_name, sample):
    model = Model(inputs=model.input,outputs=model.get_layer(layer_name).output)
    return model.predict(sample)

if __name__ == "__main__":

    model = load_model("base_model.hdf5")

    # 重み
    w0 = np.array(model.get_weights()[0])
    w1 = np.array(model.get_weights()[2])
    w2 = np.array(model.get_weights()[4])
    w3 = np.array(model.get_weights()[6])

    # バイアス
    b0 = np.array(model.get_weights()[1])
    b1 = np.array(model.get_weights()[3])
    b2 = np.array(model.get_weights()[5])
    b3 = np.array(model.get_weights()[7])

    print("conv1 shape:",w0.shape)
    print("conv2 shape:",w1.shape)
    print("dense1 shape:",w2.shape)
    print("dense2 shape:",w3.shape)

    print("bias1 shape:",b0.shape)
    print("bias2 shape:",b1.shape)
    print("bias3 shape:",b2.shape)
    print("bias4 shape:",b3.shape)

    # Numpy load
    loaded_sample = np.load('sample.npy')

    # 出力確認
    pool1_output = get_output(model, 'maxpool1', loaded_sample)
    print("maxpool1 output shape:",pool1_output.shape)

出力は以下のようになります。ここで注目して頂きたいのはTF・KerasでのCNNの次元の順番は(フィルターサイズ、フィルターサイズ、インプットチャネル、アウトプットのチャネル)であり、マックスプーリング後の次元の順番は(インプットチャネル、画像サイズ、画像サイズ、アウトプットのチャネル)です。

全結合の次元の順番は(インプット、アウトプット)であり、バイアスはアウトプットと同じになるので1次元です。

conv1 shape: (3, 3, 1, 16)
conv2 shape: (3, 3, 16, 32)
dense1 shape: (1568, 1024)
dense2 shape: (1024, 10)
bias1 shape: (16,)
bias2 shape: (32,)
bias3 shape: (1024,)
bias4 shape: (10,)
maxpool1 output shape: (1, 14, 14, 16)

4. 自作推論を実装するための理解

すでに紹介した「1. 重みとCNNについて理解」の「CNNの計算について」では、フィルターの数からアウトプットのチャネル数が導かれると紹介しました(3. インプットのチャネル1、フィルターの数3)。

また、インプットのチャネル数で1つのフィルターで計算し、合計することも既に紹介してます(2. インプットのチャネル3、フィルターの数1)。では、どのように実装すればよいでしょうか?

こういうときは結果から順に考えていけば簡単です。

  • すべてのアウトプットのチャネルの計算結果
  • アウトプットの1チャネルの計算結果
  • すべてのインプットのチャネルの幅・高さでストライド(移動)した計算結果
  • インプットの1チャネルの幅・高さでストライド(移動)した計算結果
  • インプットの1チャネルでフィルターと畳み込み計算をした計算結果

何を言っているかわかりにくいと思うので、for文のイメージを追加します。

for フィルターの数(アウトプットのチャネル数)
    アウトプットのリスト = []
    for 画像高さ
        for 画像幅
            画像のインプットチャネルの合計 = 0
            for 画像のインプットのチャネル数
                for フィルターサイズ高さ
                    for フィルターサイズ高さ
               画像のインプットチャネルの合計 += 計算結果
            アウトプットのリスト[i] = 画像のインプットチャネルの合計
            アウトプットのリスト[i] += バイアス[i]

「3. TF・Kerasでのレイヤーごとの重みと出力について理解」で重みと出力の幅・高さ・インプットチャネル・アウトプットチャネルの次元の順番をすでに確認しているので、Numpyのnumpy.transpose()メソッドを使用し、次元を変更した上で重み・バイアスをテキストなどの形式で保存し、自作推論で重み・バイアスをロードすれば再現可能です。

Numpyの重みをテキスト保存する記事は【Numpy】テキスト形式でNumpy形式データを保存の記事を参考にしてください。

まとめ

CNN計算とTF・KerasのCNNの重み・バイアスについての理解深まったでしょうか?とりあえずTFのアドベントカレンダーで4日分書いてるPINTさんはめちゃすごいということがわかりました。