技研のまつけんです。

 

同じデータに対して、複数の学習済みモデルで推論を実行して結果を見比べたい場合があります。そのような場合、複数のモデルを並列に接続して一度に実行することが出来ます。そうすることで、順次実行するよりも高速化できる場合があります。今回は、学習済みの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

H5ファイルから読み込んで結合

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が同一であれば数行で並列に接続できます。複数の学習済みモデルを個別に動かしている方は、是非、試してみていただければと思います (環境次第では高速化効果が得られない場合もあります)。