技研のまつけんです。
同じデータに対して、複数の学習済みモデルで推論を実行して結果を見比べたい場合があります。そのような場合、複数のモデルを並列に接続して一度に実行することが出来ます。そうすることで、順次実行するよりも高速化できる場合があります。今回は、学習済みのKerasモデルをH5ファイルから読み込み、並列に接続して実行する方法について紹介します。
GeForce RTX 2080 Tiを搭載したマシン (先日の記事で紹介した「環境2」) で実験を行いました。
ライブラリのimportなどは、以下の通りです:
| import os |
| import time |
| import numpy as np ; na = np.array ; nm = np.matrix ; where = np.where |
| import cv2 |
| import pandas as pd |
| os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' |
| import tensorflow as tf |
| from tensorflow.keras.datasets import mnist |
| from sklearn.model_selection import train_test_split |
| from sklearn.metrics import confusion_matrix |
| from keras.models import Sequential |
| from keras.layers import Input, Conv2D, Dense, Flatten, MaxPooling2D, concatenate |
| from keras.models import Model, load_model |
高性能な学習済みモデルを作ることが目的ではないので、学習時間短縮のため、trainを1,000枚に絞ります。あとで実行時間を計測するので、testは5,000枚とします:
| N_TRAIN = 1000 |
| N_TEST = 5000 |
| (X_train, y_train), _ = mnist.load_data() |
| X_train, _ , y_train, _ = train_test_split(X_train, y_train, train_size=N_TRAIN+N_TEST, random_state=0) |
| X_train, X_test, y_train, y_test = train_test_split(X_train, y_train, train_size=N_TRAIN , random_state=0) |
| WID, HEI = 224, 224 |
| X_train = na([cv2.resize(grey2bgr(img), (WID, HEI)) / 255. for img in X_train]) |
| X_test = na([cv2.resize(grey2bgr(img), (WID, HEI)) / 255. for img in X_test ]) |
| print(X_train.shape, X_test.shape) # (1000, 224, 224, 3) (5000, 224, 224, 3) |
| print(y_train.shape, y_test.shape) # (1000,) (5000,) |
| ALL_LABELS = tuple(sorted(set(y_train) | set(y_test))) |
| N_CLASSES = len(ALL_LABELS) |
| print(N_CLASSES, ALL_LABELS) # 10 (0, 1, 2, 3, 4, 5, 6, 7, 8, 9) |
ここで使用しているgrey2bgr()は、以前の記事で紹介したグレイスケール→BGR変換です。
モデルを準備します。全く同じ内容のモデルを6つ作ります:
| N_MODELS = 6 |
| models = [] |
| for i in range(N_MODELS): |
| model = Sequential() |
| model.add(Conv2D(32, (3, 3), activation='relu', input_shape=(HEI, WID, 3))) |
| model.add(MaxPooling2D((2, 2))) |
| model.add(Conv2D(64, (3, 3), activation='relu')) |
| model.add(MaxPooling2D((2, 2))) |
| model.add(Conv2D(64, (3, 3), activation='relu')) |
| model.add(Flatten()) |
| model.add(Dense(64, activation='relu')) |
| model.add(Dense(N_CLASSES, activation='softmax')) |
| #model.summary() |
| model.compile(optimizer='adam', |
| loss='sparse_categorical_crossentropy', |
| metrics=['accuracy']) |
| models.append(model) |
学習し、その結果をH5ファイルに保存します:
| for i in range(N_MODELS): |
| print('model #' + str(i)) |
| models[i].fit(X_train, y_train, epochs=10) |
| for i in range(N_MODELS): |
| models[i].save('simple_mnist_cnn_%d.h5' % i) |
念のため、正常に学習できているかどうか混同行列をチェックします:
| Y_pred_resp = [] |
| for i in range(N_MODELS): |
| Y_pred = models[i].predict(X_test) |
| Y_pred_resp.append(Y_pred) |
| y_pred_resp = np.argmax(Y_pred, axis=1) |
| confusion_matrix_df(y_true=y_test, y_pred=y_pred_resp).to_csv('cm_%d.csv' % i) |
| Y_pred_resp = na(Y_pred_resp) |
| print(Y_pred_resp.shape) # (6, 10000, 10) |
confusion_matrix_df()は、少し丁寧に混同行列を出力する自作の関数です。縦と横(columnとrow)のどちらがtruth/predictionなのか明示し、totalも付加した状態のPandas DataFrameを返します。
| def multi_sum(mat): |
| vsum = np.sum(mat, axis=0) |
| hsum = nm(np.sum(mat, axis=1)).T |
| asum = na([np.sum(mat)]) |
| return np.vstack([ |
| np.hstack([mat , hsum]), |
| np.hstack([vsum, asum]) |
| ]) |
| def confusion_matrix_df(*args, **kwargs): |
| cm = confusion_matrix(*args, **kwargs) |
| h, w = cm.shape |
| columns = ['P=' + str(i) for i in range(w)] + ['Total'] |
| index = ['T=' + str(i) for i in range(h)] + ['Total'] |
| df = pd.DataFrame( |
| multi_sum(cm), |
| columns=columns, |
| index=index) |
| return df |
cm_0.csvファイルは、以下のようになりました:
| P=0 | P=1 | P=2 | P=3 | P=4 | P=5 | P=6 | P=7 | P=8 | P=9 | Total | |
| T=0 | 491 | 1 | 3 | 0 | 0 | 4 | 14 | 7 | 1 | 1 | 522 |
| T=1 | 0 | 577 | 2 | 0 | 0 | 1 | 3 | 3 | 5 | 0 | 591 |
| T=2 | 8 | 4 | 411 | 14 | 7 | 3 | 5 | 11 | 23 | 1 | 487 |
| T=3 | 4 | 3 | 4 | 421 | 0 | 12 | 2 | 8 | 18 | 4 | 476 |
| T=4 | 0 | 5 | 3 | 0 | 430 | 0 | 3 | 10 | 1 | 17 | 469 |
| T=5 | 4 | 2 | 1 | 12 | 1 | 398 | 5 | 0 | 9 | 7 | 439 |
| T=6 | 3 | 1 | 2 | 0 | 6 | 15 | 483 | 0 | 2 | 0 | 512 |
| T=7 | 1 | 2 | 3 | 4 | 5 | 2 | 0 | 469 | 1 | 14 | 501 |
| T=8 | 9 | 22 | 12 | 21 | 2 | 14 | 6 |
Train 1,000枚でも、それなりの性能が出るようです。
まずは、RAM上にあるmodelsを並列に接続してみます。接続後のモデルの名前はconcatenated、略して、concat’dとしましょう。
| input_shape = models[0].input_shape[1 :] |
| print(input_shape) # (224, 224, 3) |
| inputs = Input(input_shape) |
| outputs = concatenate([models[i](inputs) for i in range(N_MODELS)]) |
| concatd = Model(inputs=[inputs], outputs=[outputs]) |
これだけで並列接続は完了です (入力のshapeが同一の場合のみ)。concatd.summary()でサマリを表示させると、このようになります:
| Model: "model" |
| __________________________________________________________________________________________________ |
| Layer (type) Output Shape Param # Connected to |
| ================================================================================================== |
| input_1 (InputLayer) [(None, 224, 224, 3) 0 |
| __________________________________________________________________________________________________ |
| sequential (Sequential) (None, 10) 11132618 input_1[0][0] |
| __________________________________________________________________________________________________ |
| sequential_1 (Sequential) (None, 10) 11132618 input_1[0][0] |
| __________________________________________________________________________________________________ |
| sequential_2 (Sequential) (None, 10) 11132618 input_1[0][0] |
| __________________________________________________________________________________________________ |
| sequential_3 (Sequential) (None, 10) 11132618 input_1[0][0] |
| __________________________________________________________________________________________________ |
| sequential_4 (Sequential) (None, 10) 11132618 input_1[0][0] |
| __________________________________________________________________________________________________ |
| sequential_5 (Sequential) (None, 10) 11132618 input_1[0][0] |
| __________________________________________________________________________________________________ |
| concatenate (Concatenate) (None, 60) 0 sequential[0][0] |
| sequential_1[0][0] |
| sequential_2[0][0] |
| sequential_3[0][0] |
| sequential_4[0][0] |
| sequential_5[0][0] |
| ================================================================================================== |
| Total params: 66,795,708 |
| Trainable params: 66,795,708 |
| Non-trainable params: 0 |
| __________________________________________________________________________________________________ |
各モデルの中は分解して表示されませんが、1つの入力が6つの10出力のモデルに入力され、合計60出力になっていることがわかります。早速、時間を計測しながら実行してみます (5回実行して合計の所要時間を表示):
| start = time.perf_counter() |
| for _ in range(5): |
| Y_pred_concatd = concatd.predict(X_test) |
| print(time.perf_counter() - start) # 25.17123463691678 |
| print(Y_pred_concatd.shape) # (5000, 60) |
約25秒で25,000枚の推論が行われ、5,000×60の行列が出力されています。では、連結しないで個別に推論を行う方についても、改めて時間を計測してみます (こちらも5回実行):
| start = time.perf_counter() |
| for _ in range(5): |
| Y_pred_resp = na([models[i].predict(X_test) for i in range(N_MODELS)]) |
| print(time.perf_counter() - start) # 78.02404034498613 |
| print(Y_pred_resp.shape) # (6, 5000, 10) |
こちらは、約78秒です。並列接続して実行した方が3倍ほど高速なようです。同じ結果が得られているかどうか、Y_pred_respをY_pred_concatdと同じ形式に直して確認します:
| np.all(np.transpose(Y_pred_resp, (1, 0, 2)).reshape(len(Y_pred_concatd), -1) == Y_pred_concatd) # True |
6つのH5ファイルから学習済みモデルを読み込むには、
| models = [load_model('simple_mnist_cnn_%d.h5' % i) for i in range(N_MODELS)] |
のようにします。このmodelsを先程のやり方で並列に接続できます。その際、以下のようなエラーが出る場合があります:
| ValueError: The name "sequential" is used 6 times in the model. All layer names should be unique. |
これは、ファイルから読み込んだモデルのモデル名 (.name属性) が同一の場合に起こります。今回は6つのモデルをループで作成したので、モデル名がsequential、sequential_1、sequential_2、…、sequential_5のように末尾に番号が付加されましたが、1つのモデルを学習させるという処理だけが書かれたコードを複数回実行した場合は、同じモデル名を持ってしまう可能性があります。そのような場合は、読み込み直後に
| for i in range(len(models)): |
| models[i]._name = 'model_' + str(i) |
のようにして、モデル名がユニークになるように書き換えます。
今回は、同一のデータに対して、複数のモデルを並列に実行することで高速化する手法について紹介しました。入力のshapeが同一であれば数行で並列に接続できます。複数の学習済みモデルを個別に動かしている方は、是非、試してみていただければと思います (環境次第では高速化効果が得られない場合もあります)。


