モデルサイズ制約に沿った機械学習アプリ作成

「このブログはAidemy Premiumのカリキュラムの一環で、受講修了条件を満たすために公開しています。」

動作環境
・デプロイ:GitHub→Render
・モデル学習:Google Colab
機械学習の手法
・CNN(vgg16ファインチューニング)


①mnistアプリ及び2クラス分類プログラムからのステップアップ

DAGM2007分類アプリの作成

Aidemy Premiumの講座を通し、2クラス分類プログラムの作成及びmnist判定アプリのデプロイを体験した。
次のステップアップとして、製造業で有用なアプリの開発を考えた。
具体的なタスクとして、表面キズ判定データセットのDAGM2007について12クラス分類する事とした。

DAGM2007データセットの紹介

出典
公式サイトトップページ:DAGM 2007 (mpg.de)
ダウンロードページ:DAGM 2007 (mpg.de)

DAGM2007データセットはドイツで開催された表面キズ検出コンペの事前学習用データセットである。
512*512pixの画像が6クラス、それぞれにキズなし・キズありが用意されている。

以下に各クラスのキズあり画像一例を掲載する。

Class1_def
Class2_def
Class3_def
Class4_def
Class5_def
Class6_def

各画像の枚数はキズなし1000枚、キズあり150枚となっている。
今回は6クラス*キズあり・なしの計12クラスを一括分類するため、
キズなし画像はアンダーサンプリングし、またテスト画像分15枚を確保し
各クラス135枚をデータセットとして読み込んだ

また、今回は活用しなかったが、各キズの座標及び範囲が記載された欠陥ラベルも同梱されている。

検証したこと

  • プログラムの改良

  • 学習結果の可視化・向上

  • デプロイ試行

目的

タスクの設定・学習結果の向上・改良したプログラムのデプロイを実施できるよう習得する。

検証内容

改良したプログラムでの学習を行い、精度向上に関しては
vgg16重み固定について、block2まで固定する場合とblock3まで固定する場合とblock4まで固定する場合の比較
を行った。

結果

デプロイについては後述のファイルサイズ容量の問題があったため
次の②モデル容量の削減にて成功となった。

学習の精度向上に関して
vgg16重み固定について、block2までとblock3までとblock4までの比較は以下の通りとなる。

block2まで重み固定 val_acc:0.8117


block3まで重み固定 val_acc:0.8426


block4まで重み固定 val_acc:0.8302

block3まで固定する場合が最も高い精度を得られたため
一旦block3まで重みを固定する事とした。

結果の考察としては、今回のタスクは転移学習元の1000クラス分類とは相違する点があったため、block4以降は再学習したほうが適しているが
block2までは基本的な形状が学習されているため、学習済みの重みを用いても問題ない
と捉えた。

学習用プログラム

以下にコードを掲載する。

インポート文

import os
import cv2
import numpy as np
import matplotlib.pyplot as plt
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.layers import Dense, Dropout, Flatten, Input
from tensorflow.keras.applications.vgg16 import VGG16
from tensorflow.keras.models import Model, Sequential
from tensorflow.keras import optimizers
from google.colab import drive
import random
import tensorflow
print(tensorflow.__version__)

goole colabのtensorflowは実施時、2.15.0であった。

データセットのマウント

drive.mount('/content/gdrive')
%cd ./gdrive/'MyDrive'/"Colab Notebooks"

Colab Notebooksのフォルダには各クラス135枚、計1620枚のDAGM2007データセットをアップロードした。
136以降のデータはテスト用として未使用で残している。

シード固定

random.seed(42)
np.random.seed(42)
tensorflow.random.set_seed(42)

データ下準備

path_Class1 = os.listdir('./DAGM_135/Class1/')
path_Class1_def = os.listdir('./DAGM_135/Class1_def/')
path_Class2 = os.listdir('./DAGM_135/Class2/')
path_Class2_def = os.listdir('./DAGM_135/Class2_def/')
path_Class3 = os.listdir('./DAGM_135/Class3/')
path_Class3_def = os.listdir('./DAGM_135/Class3_def/')
path_Class4 = os.listdir('./DAGM_135/Class4/')
path_Class4_def = os.listdir('./DAGM_135/Class4_def/')
path_Class5 = os.listdir('./DAGM_135/Class5/')
path_Class5_def = os.listdir('./DAGM_135/Class5_def/')
path_Class6 = os.listdir('./DAGM_135/Class6/')
path_Class6_def = os.listdir('./DAGM_135/Class6_def/')

img_Class1 = []
img_Class1_def = []
img_Class2 = []
img_Class2_def = []
img_Class3 = []
img_Class3_def = []
img_Class4 = []
img_Class4_def = []
img_Class5 = []
img_Class5_def = []
img_Class6 = []
img_Class6_def = []

for i in range(len(path_Class1)):
    img = cv2.imread('./DAGM_135/Class1/' + path_Class1[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (224,224))
    img_Class1.append(img)

for i in range(len(path_Class1_def)):
    img = cv2.imread('./DAGM_135/Class1_def/' + path_Class1_def[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (224,224))
    img_Class1_def.append(img)

for i in range(len(path_Class2)):
    img = cv2.imread('./DAGM_135/Class2/' + path_Class2[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (224,224))
    img_Class2.append(img)

for i in range(len(path_Class2_def)):
    img = cv2.imread('./DAGM_135/Class2_def/' + path_Class2_def[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (224,224))
    img_Class2_def.append(img)

for i in range(len(path_Class3)):
    img = cv2.imread('./DAGM_135/Class3/' + path_Class3[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (224,224))
    img_Class3.append(img)

for i in range(len(path_Class3_def)):
    img = cv2.imread('./DAGM_135/Class3_def/' + path_Class3_def[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (224,224))
    img_Class3_def.append(img)

for i in range(len(path_Class4)):
    img = cv2.imread('./DAGM_135/Class4/' + path_Class4[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (224,224))
    img_Class4.append(img)

for i in range(len(path_Class4_def)):
    img = cv2.imread('./DAGM_135/Class4_def/' + path_Class4_def[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (224,224))
    img_Class4_def.append(img)

for i in range(len(path_Class5)):
    img = cv2.imread('./DAGM_135/Class5/' + path_Class5[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (224,224))
    img_Class5.append(img)

for i in range(len(path_Class5_def)):
    img = cv2.imread('./DAGM_135/Class5_def/' + path_Class5_def[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (224,224))
    img_Class5_def.append(img)

for i in range(len(path_Class6)):
    img = cv2.imread('./DAGM_135/Class6/' + path_Class6[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (224,224))
    img_Class6.append(img)

for i in range(len(path_Class6_def)):
    img = cv2.imread('./DAGM_135/Class6_def/' + path_Class6_def[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (224,224))
    img_Class6_def.append(img)

X = np.array(img_Class1 + img_Class1_def + img_Class2 + img_Class2_def + img_Class3 + img_Class3_def + img_Class4 + img_Class4_def + img_Class5 + img_Class5_def + img_Class6 + img_Class6_def)
y = np.array([0]*len(img_Class1) + [1]*len(img_Class1_def) + [2]*len(img_Class2) + [3]*len(img_Class2_def) + [4]*len(img_Class3) + [5]*len(img_Class3_def) + [6]*len(img_Class4) + [7]*len(img_Class4_def) + [8]*len(img_Class5) + [9]*len(img_Class5_def) + [10]*len(img_Class6) + [11]*len(img_Class6_def))

rand_index = np.random.permutation(np.arange(len(X)))

print(X)
print(rand_index)
X = X[rand_index]
y = y[rand_index]

# データの分割
X_train = X[:int(len(X)*0.8)]
y_train = y[:int(len(y)*0.8)]
X_test = X[int(len(X)*0.8):]
y_test = y[int(len(y)*0.8):]

y_test = to_categorical(y_test, 12)
y_train = to_categorical(y_train, 12)

DAGM2007の分類手法には
・6クラス分類してからキズの有無を判定
等いろいろな手順が考えられるが、今回はmnistアプリからのステップアップのため、
・12クラスとみなし一括で分類
というタスクを解くこととした。

またDAGM2007はモノクロデータセットだが、vgg16の学習済みモデルを利用するためには3チャンネルカラー画像に変換する必要がある。

モデル

input_tensor = Input(shape=(224,224,3))
vgg16 = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)

top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(256, activation='relu'))
top_model.add(Dropout(rate =0.5))
top_model.add(Dense(12, activation='softmax'))

model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output))

for layer in model.layers[:15]:
    layer.trainable = False

model.compile(loss='categorical_crossentropy',optimizer=optimizers.SGD(learning_rate=1e-4, momentum=0.9),metrics=['accuracy'])

vgg16の重みを固定する際、
block2まで固定する場合はfor layer in model.layers[:7]:
block3まで固定する場合はfor layer in model.layers[:11]:
block4まで固定する場合はfor layer in model.layers[:15]:
を指定する。

学習開始用コード

history = model.fit(X_train, y_train, batch_size=36, epochs=100, validation_data=(X_test, y_test))

バッチサイズは学習データ数1296の平方根より36と仮決めし、今回はバッチサイズの違いによる影響までは検証していない。

学習結果の可視化

 #acc , val_accのプロット
plt.plot(history.history["accuracy"], label="acc", ls="-", marker="o")
plt.plot(history.history["val_accuracy"], label="val_acc", ls="-", marker="x")
plt.ylabel("accuracy")
plt.xlabel("epoch")
plt.legend(loc="best")
plt.show()
 #loss , val_lossのプロット
plt.plot(history.history["loss"], label="loss", ls="-", marker="o")
plt.plot(history.history["val_loss"], label="val_loss", ls="-", marker="x")
plt.ylabel("loss")
plt.xlabel("epoch")
plt.legend(loc="best")
plt.show()

# 精度の評価
scores = model.evaluate(X_test, y_test, verbose=1)
print('Test loss:', scores[0])
print('Test accuracy:', scores[1])

実施することで以下のようにepoch数に対するaccuracy及びlossを出力する。

block3まで重み固定・dense層幅256・optimizer:SGD(momentum=0.9) val_accuracy: 0.8426
block3まで重み固定・dense層幅256・optimizer:SGD(momentum=0.9) val_loss: 0.5472

②モデル容量の削減

GitHubの容量制限及びLFSの通信容量制限

GitHubは1ファイルあたり100MBのサイズ制限がある。
LFSを利用することで100MB以上のファイルも扱えるようになるが、無料の場合1GBまでの制限がある。
この制限はファイル総量だけでなく、1月当たりの通信量にも課せられる。
デプロイ試行中に既に今月の制限に達しているためLFSは使用できず、モデルサイズを100MB以下に抑える必要があった。

検証したこと

vgg16の学習済みモデル利用時の、固定するブロックの違いによるモデル容量変化を確認した。

目的

モデルサイズが100MB以下となる学習条件を決める

検証内容

  • vgg16block3まで重み固定した場合と、block4まで重み固定した場合のモデルサイズ比較

  • 100Mbを超えない範囲でのdense層幅の調整

結果

モデルサイズ(MB)
  • block3までしか重み固定しない場合、dense層幅を1まで減らしてもモデルサイズは100MBを下回らなかった。

  • block4まで重み固定する場合、dense層幅は128まで広げると100MBを超え、64では100MB以内に収まった。

学習結果

block4まで重み固定・dense層幅64・optimizer:SGD(momentum=0.9) val_accuracy: 0.7870
block4まで重み固定・dense層幅64・optimizer:SGD(momentum=0.9) val_loss: 0.8217

モデルサイズの制約により、精度は5.6%程低下したと考えられる。

この悪影響を回避する方法としてはモデルデータではなく重みデータのみでデプロイし容量を抑え、アプリ側にモデル構造を記載する等の方法が考えられるが
学習時のモデル構造とアプリ側のモデル構造の合わせ忘れ等、ヒューマンエラーの余地が増えるリスクが挙げられる。
(block3まで重み固定・dense層幅256の場合、重みデータのみでは80.7MB)

デプロイしたアプリ

準備物としてDAGM2007データセットをダウンロードし、任意のクラスの136番以降の画像を用意してください。
ダウンロードページ:DAGM 2007 (mpg.de)

アプリ画面

ファイルの選択ボタンをクリックし、用意したDAGM2007画像を指定し、submit!ボタンを押すと下部に判定結果が表示されます。

表示されるクラス名はDAGM2007のフォルダ名に沿ったものとなり
ClassNo.の後ろに_defが付く場合がキズありとなります。

検討余地

今回はoptimizerにモメンタム法(SGDを指定しmomentum値を入力)を用いた。
ここで、モデルサイズは用いるoptimizerにも依存する。

モデルサイズが100MB以下となるoptimizerには、SGDの他にAdagrad,Lion,RMSprop,Adafactorがあった。
これらのoptimizerに変更し検討することで、モデルサイズを維持したまま精度が向上する可能性がある。

この記事が気に入ったらサポートをしてみませんか?