LoginSignup
1
0

More than 1 year has passed since last update.

バスに子供が「居る」、「居ない」、判別アプリ

Last updated at Posted at 2023-04-29

目次

1.はじめに
2.テーマ選択の動機
3.動作環境
4.画像の収集
5.画像認識モデルの選択
- マルチクラス分類
- マルチラベル分類
6.「人」分類精度の改善
- (1) 学習用画像データの追加
- (2) 結合画像の作成
- (3) 転移学習
- ResNet152
- (4) 画像の水増し
7.バスに子供が居るいない、判定アプリ
8.おわりに

1 はじめに

AidemyのAIアプリ開発講座の最終課題として、画像に人が「居る」か、「居ない」か判別するアプリの制作をします。本記事では制作するアプリについてまとめます。

2 テーマ選択の動機

2022年9月に、園児が通園バスに置き去りになる悲惨な事故が起きました。通園バスに、置き去りを防止する安全装置の設置を義務化することが検討されています。すでに安全装置に、AI搭載カメラを使いアラートを送る装置が販売され始めています。ここでは、これまで学習した、深層学習(ディープラーニング)の画像分類技術を使って、バスに子供が居るか居ないか、識別するアプリを自分でも作ってみたいと思います。

3 動作環境

[Google Colab 使用時]

  • os: Ubuntu
  • version: 18.04.6 LTS (Bionic Beaver)
  • Python: version/3.7.15

[ ローカルの環境]

  • os:Debian GNU/Linux
  • version: 11 (bullseye)
  • Python: version/3.9.2

4 画像の収集

  • 画像収集は、画像を集めるのに便利なpythonのライブラリ-「iscrawler」を使ったコードを使ってネットから検索ダウンロードしたものと、公開されているAI学習用のデータセットを使いました。
画像収集用の「iscrawler」を使ったコードです。

(iscrawlerで画像収集)

"""
iscrawlerを使って、画像を集めます。
1)使い方は、変数 keyword に検索したいテキストを設定。
2)変数 imagedir に保存用のディレクトリー名
3)変数 max_gazo にダウンロードしたい画像枚数
を指定してください。
スクリプトを走らせると、GoogleとBing から画像をダウンロードし、
Googleは、prefixに'g'の連番、Bingは'b'の連番ファイル名で画像が保存されます。
"""

from icrawler.builtin import BaiduImageCrawler, BingImageCrawler, GoogleImageCrawler
import os

#keywd ='2 years old person'
#keywd ='3歳児'
#keywd ='園児 バス 車内'
#keywd ='5歳時 バス車内'
keywd ='4yo child in a bus'
#keywd ='child'
#imagedir = '5saibus'
imagedir = '4yobus'
#imagedir = 'enji'

gazo_max = 300

filters = dict(
    type='photo'
    )

def rename_all(target_dir, prefix):
    for fn in os.listdir(target_dir):
        if fn[0].isdigit(): #ファイル名の1文字目が数字だったら
            org_file = os.path.join(target_dir, fn)
            changed_file = os.path.join(target_dir, prefix + fn)
            os.rename(org_file, changed_file)

google_crawler = GoogleImageCrawler(
    storage={'root_dir': imagedir },
    feeder_threads=1,
    parser_threads=1,
    downloader_threads=4,
    )
google_crawler.crawl(keyword=keywd, filters = filters, max_num=gazo_max)
rename_all(imagedir, 'g')

bing_crawler = BingImageCrawler(
    storage={'root_dir': imagedir },
    feeder_threads=1,
    parser_threads=1,
    downloader_threads=4,
        )
bing_crawler.crawl(keyword= keywd, filters = filters, max_num=gazo_max)
rename_all(imagedir, 'b')

  • 集めた画像は:
  1. 訓練・検証用データ「車内に人がいる画像(車内の人画像)」
    車内に人がいる画像(車内の人画像)を、およそ300枚収集しました。「訓練・検証用データ」は、モデルの学習前に、訓練用と検証用に分けて(80%, 20% など)、モデルの学習と検証に使います。
    [車内に人が居る画像]

  2. 訓練・検証用データ「車内に人が居ない画像(車内の画像)」
    車内に人が居ない画像(車内の画像)を、およそ500枚収集しました。
    [車内に人が居ない画像]
    inai_bus.png

  3. 訓練・検証用データ「人の画像]
    Pedestrian Detection Data set(kaggle)のデータセットから,およそ300枚の画像を取り出して準備しました。
    https://www.kaggle.com/datasets/karthika95/pedestrian-detection
    出典:Pedestrian Detection Data set
    作者:N J Karthika, Chandran Saravanan
    タイトル:International Conference on Electronic Systems and Intelligent Computing (ESIC 2020)
    [人の画像]
    person.png

  4. テスト用データ「子供が居るバス車内の画像」
    子供が居るバス車内の画像(バスの子供画像)を、30枚準備しました。テスト用に使います。
    [バスに子供が居る画像]

  5. テスト用データ「人が居ないバス車内画像」
    人が居ないバス車内の画像(バス車内画像)を、30枚準備しました。次の人が居るバス車内の画像とともに、これから作成する学習済みの画像識別モデルが、最終的にどの程度判別できるかテストするのに使います。
    [バスに人が居ない画像]
    noone_in_a_bus.png

  6. 収集画像の調整
    集めた画像に重複画像ができていましたので、次のコードで重複画像を削除しました。

重複画像は、次のコードで削除しました。

(ライブラリーimagehashを使って重複画像の削除)

import os
from PIL import Image
import imagehash
import cv2

userpath = './inai_bus'  # 検索するパス

image_files = []
f = [os.path.join(userpath, path) for path in os.listdir(userpath)]
for i in f:
    if i.endswith('.jpg') or i.endswith('.png'):
        image_files.append(i)

imgs = {}
removes = []
for img in sorted(image_files):
    hash = imagehash.average_hash(Image.open(img))
    if hash in imgs:
        print('Similar image :', img, imgs[hash])
        removes.append(imgs[hash])
    else:
        imgs[hash] = img

for fn in removes:
    os.remove(fn)

5 画像認識モデルの選択

マルチクラス分類

[マルチクラス分類 multi-calss classification]
 「マルチクラス分類」によって人のいる画像の分類をしてみます。マルチクラス分類では、1つの画像に複数のラベルを付けることはできません。まずは、「車内に人の居る」クラスと「車内に人の居ない」クラスで識別するモデルを考えました。

(1) 「車内に人の居る」クラスと「車内に人の居ない」クラスで識別

300セットの「車内に人の居る画像」と「車内に人の居ない画像」をCNN画像認識ニューラルネットワークで学習・検証してみます。epoch 数を80, batch size を 64 で行いました。CNN画像認識は、出力層の活性化関数にsoftmax, 損失関数にcategorical_crossentropy を使いました。その学習済みモデルに、30セットのテスト用の「バスに子供が居る画像」と「バスに子供が居ない画像」を判定させました。

コードはこちら

(Multi-class calssification モデルを使って検証)

# -*- coding: utf-8 -*-

"""
マルチクラス分類(multi-class classification)で、車内に人が居る画像と、
車内に人が居ない画像の2つのクラスで学習し、バスに子供が居る画像を
認識できるか、CNN画像認識(活性化関数softmax, 損失関数categorical_crossentropy
を使って検証する。
"""
# Python標準ライブラリ
import os
import numpy as np

# サードパーティライブラリ(tensorflow)
from tensorflow import keras
from tensorflow.keras.layers import Activation, Conv2D, Dense, Dropout, Flatten, MaxPooling2D
from tensorflow.keras.layers import BatchNormalization
from tensorflow.keras.models import Sequential, load_model
from tensorflow.keras.utils import to_categorical

# サードパーティライブラリ(Scikit-learn)
from sklearn.metrics import confusion_matrix
from sklearn.metrics import classification_report

# サードパーティライブラリ(その他)
import matplotlib.pyplot as plt
import cv2
import seaborn as sns
import pandas as pd
import japanize_matplotlib


#基本pathの設定
#base_path = '.' #ローカルの場合
base_path = '/content/multi-class' #Google colab の場合

#保存用pathの設定
#result_path = './result' #ローカルの場合
result_path = '/content/drive/MyDrive/multi-class_1' #Google colab の場合

#車内に人が居る居ないの画像から学習用評価用データセットを作成
inai_path = os.path.join(base_path, 'images/inai_bus') #車内に人が居ない画像
iru_path = os.path.join(base_path, 'images/iru_bus') #車内に人が居る画像

#フォルダー上にある画像をnum数だけリサイズして取り出す関数
def make_set(folder, num):
    images = []
    filenames = os.listdir(folder)
    for i in range(len(filenames)):
        img = cv2.imread(os.path.join(folder, filenames[i]))
        img = img[:, :, ::-1] # bgr→rgbチャネル入れ替え
        #多次元ndarrayの各次元のスライスをカンマで区切って指定できる。
        #スライスのstep(sart:stop:step)は、step個毎に取り出しだが、マイナスは逆順  
        img = cv2.resize(img, (64,64))
        images.append(img)

    return images[:num]

#NUM枚の画像でデータセットを作成
NUM=300
#データセットの作成
s_inai = make_set(inai_path, NUM)
s_iru = make_set(iru_path, NUM)

X = np.array(s_inai + s_iru)
y =  np.array([0]*len(s_inai) + [1]*len(s_iru))

rand_index = np.random.permutation(np.arange(len(X)))
X = X[rand_index]
y = y[rand_index]

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

# モデルの定義
#CNN画像認識モデル
model = Sequential()
model.add(Conv2D(64,(3,3),activation="relu",input_shape=(64,64,3), padding='same', kernel_initializer='he_normal'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(BatchNormalization())
model.add(Dropout(0.25))
model.add(Conv2D(128,(3,3),activation="relu",padding='same', kernel_initializer='he_normal'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(BatchNormalization())
model.add(Dropout(0.25))
model.add(Conv2D(256,(3,3),activation="relu",padding='same', kernel_initializer='he_normal'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(BatchNormalization())
model.add(Dropout(0.25))
model.add(Conv2D(256,(3,3),activation="relu",padding='same', kernel_initializer='he_normal'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(BatchNormalization())
model.add(Dropout(0.25))
model.add(Conv2D(128,(3,3),activation="relu",padding='same', kernel_initializer='he_normal'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(BatchNormalization())
model.add(Dropout(0.25))

model.add(Flatten())
model.add(Dense(256, activation='relu', kernel_initializer='he_normal'))
model.add(Dropout(0.5))
model.add(Dense(256, activation='relu', kernel_initializer='he_normal'))
model.add(Dropout(0.5))
model.add(Dense(2))  #出力層のユニットはいるいないの2
model.add(Activation('softmax'))

#モデル構造の確認
#model.summary()

# コンパイル
opt = keras.optimizers.RMSprop(lr=0.0001, decay=1e-6)
model.compile(loss='categorical_crossentropy',
              optimizer=opt,
              metrics=['accuracy'])

# 重みデータ を読み込みます
#model.load_weights(os.path.join(result_path, 'param_multi-class_1.hdf5'))

# 学習
history = model.fit(X_train, 
                    y_train, 
                    epochs=80, 
                    batch_size=64, 
                    verbose=1, 
                    validation_data=(X_test, y_test)
                    )

#----history 描画用変数代入-------
loss=history.history['loss']
val_loss=history.history['val_loss']
acc=history.history['accuracy']
val_acc=history.history['val_accuracy']
#----------------------------------

# 学習によって得た重みを保存する場合は、 save_weights() メソッドを使います。
model.save_weights(os.path.join(result_path, 'param_multi-class_1.hdf5'))

#検証結果保存用テキストファイル
with open(os.path.join(result_path, 'eval_multi-class_1.txt'), 'w') as f:
  # 精度の評価
  scores = model.evaluate(X_test, y_test, verbose=1)
  f.write('Test loss: {}\n'.format(scores[0]))
  f.write('Test accuracy: {}\n'.format(scores[1]))
  f.write('\n')

#----history 描画用matPlotlib-------
epochs=len(loss)
fig = plt.figure()
# figure全体のタイトル
fig.suptitle("マルチクラスモデルで、人が居るバス/居ないバス")

plt.subplot(121)
plt.plot(range(epochs), loss, marker = 'o', color='b', label = 'loss')
plt.plot(range(epochs), val_loss,  color='b', label = 'val_loss')
plt.legend(loc = 'best')
plt.grid()
plt.xlabel('epoch')
plt.ylabel('loss')

plt.subplot(122)
plt.plot(range(epochs), acc, marker = 'o', color='b',  label = 'acc')
plt.plot(range(epochs), val_acc, color='b', label = 'val_acc')
plt.legend(loc = 'best')
plt.grid()
plt.xlabel('epoch')
plt.ylabel('acc')

plt.tight_layout(rect=[0,0,1,0.96])  #suptitleが重なるので、
plt.savefig(os.path.join(result_path, "history_multi-class_1.png"))
plt.show()
plt.close()
#----history 描画用matPlotlib 終わり-------

#テストデータを学習済みモデルで予測した結果をグラフに描画する関数
#テスト画像のフォルダから、指定された数のリスト
#(pred=予測値, org_img=オリジナル画像)を作成
def mk_data(path, num):
    files = os.listdir(path)
    files = files[:num]
    img_list = []

    for fn in files:
        org_img = cv2.imread(os.path.join(path, fn))
        img = org_img[:, :, ::-1] # bgr→rgbチャネル入れ替え
        img = cv2.resize(img, (64,64))
        pred = model.predict(img.reshape(1,64,64,3), verbose = 0)
        #print('pred:', pred)
        img_list.append([pred, org_img])

    return img_list

def make_kekka(img_list, text, ax):
    x_0 = []  #クラス0(車内) の検証No.
    y_0 = []  #クラス0(車内) の予測値
    x_1 = []  #クラス1(子供) の検証No.
    y_1 = []  #クラス1(子供) の予測値
    for i, res in enumerate(img_list):
        x_0.append(i+1)
        y_0.append(res[0][0][0])
        x_1.append(i+1)
        y_1.append(res[0][0][1])

    ax.set_title(text)
    ax.plot(x_0, y_0, label='バス')
    ax.plot(x_1, y_1, label='子供')
    ax.set_ylabel('predict')
    ax.set_xticks(range(len(x_0)))
    ax.legend()

no_path = os.path.join(base_path, 'images/noone_in_a_bus') #車内に子供が居ない画像
child_path = os.path.join(base_path, 'images/child_in_a_bus') #車内に子供が居る画像

fig = plt.figure(figsize=(14,6))

#子供が居る画像のテスト
child_img_list = mk_data(child_path, 30)
text = '子供が居るバス'
ax = fig.add_subplot(1,2,1)
make_kekka(child_img_list, text, ax)

#子供が居ない画像のテスト
no_img_list = mk_data(no_path, 30)
text = '子供が居ないバス'
ax = fig.add_subplot(1,2,2)
make_kekka(no_img_list, text, ax)

sfn = os.path.join(result_path, 'multi-class_1_graf.png')
plt.savefig(sfn)
plt.show()
plt.close()

#----ここから画像表示-----
#子供の予測がしきい値(=Svalue)
def save_gazo(img_list, savefn, shiki):
  Svalue = shiki  

  plt.figure(figsize=(32, 32))
  #plt.subplots_adjust(wspace=0.801, hspace=0.063)

  image_pos = 0
  cnt_disp = 30
  col = 5
  row = int(cnt_disp / col) + 1

  j=0
  for pred, img in img_list:
    image = cv2.resize(img, (256, 256))
    plt.subplot(row, col, image_pos + 1)
    plt.imshow(image)
    image_pos += 1

    y_0 = pred[0][0]
    y_1 = pred[0][1]

    # 子供予測確率がしきい値より高ければ Blue
    if y_1 > Svalue:
      color = 'blue'
    else:
      color = 'red'

    plt.xlabel("[{}] C:{:.3f}, B:{:.3f}".format(j+1,y_1, y_0), color=color, fontsize=12)
    j+=1

  plt.tight_layout()
  plt.savefig(savefn)
  plt.show()
  plt.close()

"""
savefn = os.path.join(result_path, 'multi-class_1_gazos_0.png')
save_gazo(mk_data(no_path, 30), savefn, 0.5)
savefn = os.path.join(result_path, 'multi-class_1_gazos_1.png')
save_gazo(mk_data(child_path, 30), savefn, 0.5)
"""

#-------------
#confusion matrix の実装
pred=[] #予測結果のリスト
for data in child_img_list:
  pred.append(int(data[0][0][1] > 0.5))

for data in no_img_list:
  pred.append(int(data[0][0][1] > 0.5))

ty=[] #ラベル
ty = [1]*len(child_img_list) + [0] *len(no_img_list)

labels = [0,1]  #子供のラベルを後にしています。
cm = confusion_matrix(ty, pred, labels = labels)
cm = pd.DataFrame(data=cm, index=["バス", "子供"], 
                           columns=["バス", "子供"])
sns.heatmap(cm, square=True, cbar=True, annot=True, cmap='Blues')
plt.yticks(rotation=0)
plt.xlabel("予測値", fontsize=13, rotation=0)
plt.ylabel("真の値", fontsize=13)
sfn = os.path.join(result_path, 'multi-class_1_confusion_matrix.png')
plt.savefig(sfn)

print('混同行列:\n',cm)
#label_names = ['BUS', 'Child']
label_names = ['バス', '子供']
#検証結果保存用テキストファイル
with open(os.path.join(result_path, 'eval_multi-class_1.txt'), 'a') as f:
  f.write(classification_report(ty, pred,target_names=label_names))

学習履歴です。
history_multi-class_1.png

評価 損失関数(loss): 0.6416658759117126
評価 正解率(accuracy): 0.6333333253860474

学習正解率にバラツキがあり、損失関数(loss) 0.64 正解率(accuracy) 0.63 ともに、あまり良い数字ではありません。

次のグラフは、テストデータを予測した結果です。

multi-class_1_graf.png

子供の予測値が0.5以上を、子供が居ると予測したと判定すると、次のマトリックス(混同行列)になります。

multi-class_1_confusion_matrix.png

30枚の子供が居る画像のうち、9枚だけ正解ですので、低い正解率になってしまいました。

今回のモデルで学習後の正解率が低いのは、「車内」と「人」の両方の要素のある「車内に人がいる画像」を教師として学習させたため、学習がうまくできなかったためと考えました。

(2) 「人」クラスと人の居ない「車内」クラスで識別

次に、同じマルチクラス識別モデルで、学習クラスを「人」と人の居ない「車内」の画像300セットで学習し、同じ30セットのテスト用の「バスに子供が居る画像」と「バスに子供が居ない画像」を判定させました。こんどは、教師データの「人」と人の居ない「車内」の画像に重複した要素はないため、学習はうまく行くと期待できます。

コードは、先程のマルチクラス識別モデル(1)のコードと同じコードを使い、学習用画像のパスのみを変更して検証しました。こちらが、その結果です。

history_multi-class_2.png

評価 損失関数(loss): 0.4065142273902893
評価 正解率(accuracy): 0.824999988079071

損失関数 0.40、正解率 0.82 と、ともに、先程の(1)モデルよりも改善しました。
学習は、先程よりも上手くいったようです。

次は、テストデータを予測した結果です。

multi-class_2_graf.png

マトリックス(混同行列)は、次のようになります。

multi-class_2_confusion_matrix.png

学習履歴は改善しましたが、テスト結果は、30枚の子供の画像うち8枚しか正解できていませんので、改善していないことがわかります。

学習は改善しているのに、テスト結果が伸びないのはなぜでしょう。
マルチクラス分類モデルでは、子供がバスに居る画像を判別する際、「人」か、「車内」かの、どちらかに識別しようとします。マルチクラス分類では、prediction(予測確率)は、「人」の確率と「車内」の確率の合計が必ず1になります。
画像に子供とバスのどちらのクラスもある画像を見せたとき、子供が居ても、バスの要素が強ければ、バスと判定してしまうため、子供の予測確率が高くならないことが考えられます。

これに対して、マルチラベル識別モデルでは、1つの画像に2つのラベルが付けられます。また、「車内」と「人」のprediction(予測確率)は、合計が1にはならず、それぞれ最大1までの予測確率で表されます。したがって、「車内」の「人」の画像でも、「人」のprediction(予測確率)に高い数字がでることが期待できます。

マルチラベル分類

[マルチラベル分類 multi-label classification]
それでは、1つの画像に複数のラベルが設定できる、「マルチラベル分類」(multi-label classification)を使って、人のいる画像の分類をしてみます。
クラスは、「車内」のクラスと、「ひと(人)」のクラスの2つのクラスを考えます。
学習/テスト用画像は、「車内」ラベルの(車内の画像)、「ひと(人)」ラベルの(人の画像), そして、「ひと(人)」+「車内」の複合ラベルの(車内の人画像)に分けて、それぞれ300セットを準備しました。epocks数80と、batch size 64は、これまでの検証と同じです。これを、同じくCNN画像認識ニューラルネットワークで学習します。学習済みモデルに、30セットのテスト用の「バスに子供が居る画像」と「バスに子供が居ない画像」を判定させました。
「マルチラベル分類」には、出力層の活性化関数にsigmoid, 損失関数にbinary_crossentropyを使いました。

コードはこちら

(Multi-label classification モデルを使って検証)

# -*- coding: utf-8 -*-

"""
マルチラベル分類(multi-label classification)で、人の画像と、
車内に人が居ない画像の2つのクラスを設定し、人ラベル、車内ラベル、人+車内
複合ラベルの3つの画像グループで学習する。
バスに子供が居る画像を認識できるか、CNN画像認識(活性化関数sigmoid, 損失関数binary_crossentropy
を使って検証する。
"""
# Python標準ライブラリ
import os
import numpy as np

# サードパーティライブラリ(tensorflow)
from tensorflow import keras
from tensorflow.keras.layers import Activation, Conv2D, Dense, Dropout, Flatten, MaxPooling2D
from tensorflow.keras.layers import BatchNormalization
from tensorflow.keras.models import Sequential, load_model
from tensorflow.keras.utils import to_categorical

# サードパーティライブラリ(Scikit-learn)
from sklearn.metrics import confusion_matrix
from sklearn.metrics import classification_report

# サードパーティライブラリ(その他)
import matplotlib.pyplot as plt
import cv2
import seaborn as sns
import pandas as pd
import japanize_matplotlib


#基本pathの設定
#base_path = '.' #ローカルの場合
base_path = '/content/multi-class' #Google colab の場合

#result_path = './result' #ローカルの場合
result_path = '/content/drive/MyDrive/multi-label_1' #Google colab の場合

#車内に人が居る居ないの画像から学習用評価用データセットを作成
inai_path = os.path.join(base_path, 'images/inai_bus') #車内に人が居ない画像
iru_path = os.path.join(base_path, 'images/iru_bus') #車内に人が居る画像
hito_path = os.path.join(base_path, 'images/person') #人の画像

#フォルダー上にある画像をnum数だけリサイズして取り出す関数
def make_set(folder, num):
    images = []
    filenames = os.listdir(folder)
    for i in range(len(filenames)):
        img = cv2.imread(os.path.join(folder, filenames[i]))
        img = img[:, :, ::-1] # bgr→rgbチャネル入れ替え
        #多次元ndarrayの各次元のスライスをカンマで区切って指定できる。
        #スライスのstep(sart:stop:step)は、step個毎に取り出しだが、マイナスは逆順  
        img = cv2.resize(img, (64,64))
        images.append(img)

    return images[:num]

#NUM枚の画像でデータセットを作成
NUM=300
#データセットの作成
s_inai = make_set(inai_path, NUM)
s_hito = make_set(hito_path, NUM)
s_iru = make_set(iru_path, NUM)

X = np.array(s_inai + s_hito + s_iru)
#マルチラベル分類用ラベルの作成(マルチホットエンコーディング)
#車内のみ(バス)の画像=ラベル[1,0]
#人の画像=ラベル[0,1]
#車内に人が居る画像 = ラベル[1,1]
y =  np.array([[1,0]]*len(s_inai) + [[0,1]]*len(s_hito) + [[1,1]]*len(s_iru))

rand_index = np.random.permutation(np.arange(len(X)))
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):]

# モデルの定義
#CNN画像認識モデル
model = Sequential()
model.add(Conv2D(64,(3,3),activation="relu",input_shape=(64,64,3), padding='same', kernel_initializer='he_normal'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(BatchNormalization())
model.add(Dropout(0.25))
model.add(Conv2D(128,(3,3),activation="relu",padding='same', kernel_initializer='he_normal'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(BatchNormalization())
model.add(Dropout(0.25))
model.add(Conv2D(256,(3,3),activation="relu",padding='same', kernel_initializer='he_normal'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(BatchNormalization())
model.add(Dropout(0.25))
model.add(Conv2D(256,(3,3),activation="relu",padding='same', kernel_initializer='he_normal'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(BatchNormalization())
model.add(Dropout(0.25))
model.add(Conv2D(128,(3,3),activation="relu",padding='same', kernel_initializer='he_normal'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(BatchNormalization())
model.add(Dropout(0.25))

model.add(Flatten())
model.add(Dense(256, activation='relu', kernel_initializer='he_normal'))
model.add(Dropout(0.5))
model.add(Dense(256, activation='relu', kernel_initializer='he_normal'))
model.add(Dropout(0.5))
model.add(Dense(2))  #出力層のユニットはいるいないの2
model.add(Activation('sigmoid'))

#モデル構造の確認
#model.summary()

# コンパイル
opt = keras.optimizers.RMSprop(lr=0.0001, decay=1e-6)
model.compile(loss='binary_crossentropy',
              optimizer=opt,
              metrics=['accuracy'])

# 重みデータ を読み込みます
#model.load_weights(os.path.join(result_path, 'param_multi-label_1_1.hdf5'))

# 学習
history = model.fit(X_train, 
                    y_train, 
                    epochs=80, 
                    batch_size=64, 
                    verbose=1, 
                    validation_data=(X_test, y_test)
                    )

#----history 描画用変数代入-------
loss=history.history['loss']
val_loss=history.history['val_loss']
acc=history.history['accuracy']
val_acc=history.history['val_accuracy']
#----------------------------------

# 学習によって得た重みを保存する場合は、 save_weights() メソッドを使います。
model.save_weights(os.path.join(result_path, 'param_multi-label_1_1.hdf5'))

#検証結果保存用テキストファイル
with open(os.path.join(result_path, 'eval_multi-label_1_1.txt'), 'w') as f:
  # 精度の評価
  scores = model.evaluate(X_test, y_test, verbose=1)
  f.write('Test loss: {}\n'.format(scores[0]))
  f.write('Test accuracy: {}\n'.format(scores[1]))
  f.write('\n')

#----history 描画用matPlotlib-------
epochs=len(loss)
fig = plt.figure()
# figure全体のタイトル
fig.suptitle("マルチラベルモデルで、人/人の居ないバス/人のいるバス")
#plt.title("マルチクラスモデルで、人が居るバス/居ないバス")

plt.subplot(121)
plt.plot(range(epochs), loss, marker = 'o', color='b', label = 'loss')
plt.plot(range(epochs), val_loss,  color='b', label = 'val_loss')
plt.legend(loc = 'best')
plt.grid()
plt.xlabel('epoch')
plt.ylabel('loss')

plt.subplot(122)
plt.plot(range(epochs), acc, marker = 'o', color='b',  label = 'acc')
plt.plot(range(epochs), val_acc, color='b', label = 'val_acc')
plt.legend(loc = 'best')
plt.grid()
plt.xlabel('epoch')
plt.ylabel('acc')

plt.tight_layout(rect=[0,0,1,0.96])  #suptitleが重なるので、
plt.savefig(os.path.join(result_path, "history_multi-label_1_1.png"))
plt.show()
plt.close()
#----history 描画用matPlotlib 終わり-------

#テストデータを学習済みモデルで予測した結果をグラフに描画する関数
#テスト画像のフォルダから、指定された数のリスト
#(pred=予測値, org_img=オリジナル画像)を作成
def mk_data(path, num):
    files = os.listdir(path)
    files = files[:num]
    img_list = []

    for fn in files:
        org_img = cv2.imread(os.path.join(path, fn))
        img = org_img[:, :, ::-1] # bgr→rgbチャネル入れ替え
        img = cv2.resize(img, (64,64))
        pred = model.predict(img.reshape(1,64,64,3), verbose = 0)
        #print('pred:', pred)
        img_list.append([pred, org_img])

    return img_list

def make_kekka(img_list, text, ax):
    x_0 = []  #クラス0(車内) の検証No.
    y_0 = []  #クラス0(車内) の予測値
    x_1 = []  #クラス1(子供) の検証No.
    y_1 = []  #クラス1(子供) の予測値
    for i, res in enumerate(img_list):
        x_0.append(i+1)
        y_0.append(res[0][0][0])
        x_1.append(i+1)
        y_1.append(res[0][0][1])

    ax.set_title(text)
    ax.plot(x_0, y_0, label='バス')
    ax.plot(x_1, y_1, label='子供')
    ax.set_ylabel('predict')
    ax.set_xticks(range(len(x_0)))
    ax.legend()

no_path = os.path.join(base_path, 'images/noone_in_a_bus') #車内に子供が居ない画像
child_path = os.path.join(base_path, 'images/child_in_a_bus') #車内に子供が居る画像

fig = plt.figure(figsize=(14,6))

#子供が居る画像のテスト
child_img_list = mk_data(child_path, 30)
text = '子供が居るバス'
ax = fig.add_subplot(1,2,1)
make_kekka(child_img_list, text, ax)

#子供が居ない画像のテスト
no_img_list = mk_data(no_path, 30)
text = '子供が居ないバス'
ax = fig.add_subplot(1,2,2)
make_kekka(no_img_list, text, ax)

sfn = os.path.join(result_path, 'multi-label_1_1_graf.png')
plt.savefig(sfn)
plt.show()
plt.close()

#----ここから画像表示-----
#子供の予測がしきい値(=Svalue)
def save_gazo(img_list, savefn, shiki):
  Svalue = shiki  

  plt.figure(figsize=(32, 32))
  #plt.subplots_adjust(wspace=0.801, hspace=0.063)

  image_pos = 0
  cnt_disp = 30
  col = 5
  row = int(cnt_disp / col) + 1

  j=0
  for pred, img in img_list:
    image = cv2.resize(img, (256, 256))
    plt.subplot(row, col, image_pos + 1)
    plt.imshow(image)
    image_pos += 1

    y_0 = pred[0][0]
    y_1 = pred[0][1]

    # 子供予測確率がしきい値より高ければ Blue
    if y_1 > Svalue:
      color = 'blue'
    else:
      color = 'red'

    plt.xlabel("[{}] C:{:.3f}, B:{:.3f}".format(j+1,y_1, y_0), color=color, fontsize=12)
    j+=1

  plt.tight_layout()
  plt.savefig(savefn)
  plt.show()
  plt.close()

savefn = os.path.join(result_path, 'multi-label_1_1_gazos_0.png')
save_gazo(mk_data(no_path, 30), savefn, 0.5)
savefn = os.path.join(result_path, 'multi-label_1_1_gazos_1.png')
save_gazo(mk_data(child_path, 30), savefn, 0.5)

#-------------
#confusion matrix の実装
pred=[] #予測結果のリスト
for data in child_img_list:
  pred.append(int(data[0][0][1] > 0.5))

for data in no_img_list:
  pred.append(int(data[0][0][1] > 0.5))

ty=[] #ラベル
ty = [1]*len(child_img_list) + [0] *len(no_img_list)

labels = [0,1]  #子供のラベルを後にしています。
cm = confusion_matrix(ty, pred, labels = labels)
cm = pd.DataFrame(data=cm, index=["バス", "子供"], 
                           columns=["バス", "子供"])
sns.heatmap(cm, square=True, cbar=True, annot=True, cmap='Blues')
plt.yticks(rotation=0)
plt.xlabel("予測値", fontsize=13, rotation=0)
plt.ylabel("真の値", fontsize=13)
ax.set_ylim(len(cm), 0)
sfn = os.path.join(result_path, 'multi-label_1_1_confusion_matrix.png')
plt.savefig(sfn)

print('混同行列:\n',cm)
#label_names = ['BUS', 'Child']
label_names = ['バス', '子供']
#検証結果保存用テキストファイル
with open(os.path.join(result_path, 'eval_multi-label_1_1.txt'), 'a') as f:
  f.write(classification_report(ty, pred,target_names=label_names))


学習履歴です。

history_multi-label_1_1.png

評価 損失関数(loss): 0.4377872943878174
評価 正解率(accuracy): 0.7277777791023254

検証正解率(accuracy) は、0.72 とマルチクラスモデル(2)の正解率0.82から少し下がりました。次にテスト結果を見てみます。

multi-label_1_1_graf.png

multi-label_1_1_confusion_matrix.png

テスト結果を見ると、クラスモデル(2)では、30枚の子供の画像うち8枚しか正解できていなかったところ、30枚中28枚正解していますので、再現率(recall)は、0.93となり、しっかりと子供を検出できていることがわかります。

次項より、マルチラベル分類モデルを使って、さらに子供の検知精度を高める工夫をしてみます。

6 「子供」検知精度の改善

(1) 学習用画像データの追加

  1. 子供の画像データ収集
    子供の画像データを1000枚準備しました。

  2. 車内の画像データの収集
    人の居ない「車内」の画像データを1000枚準備しました。

(2) 結合画像の作成

子供の画像と、バスの画像から、子供+バスの結合画像を作成しました。
それぞれの画像を32x64の縦長と横長の画像にリサイズし、opencvのhconcat
とvconcatで上下、左右に順番を変えながら結合して、1000枚の結合画像を作成しました。

このコードで結合画像を作りました

(opencv を使って合成画像を作成)

"""
2つのフォルダ名を指定して、それぞれのフォルダ内の画像セットから
ランダムに重複なしに画像を選ぶ。
画像グループを4つに分ける。
分けたグループそれぞれに、
(1)選んだそれぞれの画像を縦長の32x64にリサイズして、横結合して
結合した64x64の画像ファイルを作成する。
(2)横結合の順番を逆にする
(3)選んだそれぞれの画像を64x32にリサイズして、縦結合する
(4)その逆順
作成するファイル数量は、NUMに指定する。
"""

import cv2
import numpy as np
import os

#2つのフォルダ名を指定します。
path_1 = './kodomo'
path_2 = './bus'

#保存用フォルダ名を指定します。
save_dir = './kodomo_x_bus'

#作成する画像の数量を指定します。
NUM=900

path1s = os.listdir(path_1) #フォルダの画像ファイル名リストを取得
path2s = os.listdir(path_2)

#NUM数のランダムで重複しない整数のリスト
a = np.random.permutation(np.arange(NUM))
#ランダムリストを4つの配列に適当に均等に分ける
a1,a2,a3,a4 =np.array_split(a, 4, 0)

for i in a1:
    #path1, path2 のindex=iのイメージ読み込むを
    im1 = cv2.imread(os.path.join(path_1, path1s[i]))
    im2 = cv2.imread(os.path.join(path_2, path2s[i]))

    rim1 = cv2.resize(im1, (32,64)) #縦長にリサイズする場合
    rim2 = cv2.resize(im2, (32,64))
    gcon = cv2.hconcat([rim1, rim2]) #2つを横に結合
    cv2.imwrite(os.path.join(save_dir, '{}.jpg'.format('con'+str(i))), gcon)

for i in a2:
    #path1, path2 のindex=iのイメージ読み込むを
    im1 = cv2.imread(os.path.join(path_1, path1s[i]))
    im2 = cv2.imread(os.path.join(path_2, path2s[i]))

    rim1 = cv2.resize(im1, (32,64)) #縦長にリサイズする場合
    rim2 = cv2.resize(im2, (32,64))
    gcon = cv2.hconcat([rim2, rim1]) #2つを逆順で横に結合
    cv2.imwrite(os.path.join(save_dir, '{}.jpg'.format('con'+str(i))), gcon)

for i in a3:
    #path1, path2 のindex=iのイメージ読み込むを
    im1 = cv2.imread(os.path.join(path_1, path1s[i]))
    im2 = cv2.imread(os.path.join(path_2, path2s[i]))

    rim1 = cv2.resize(im1, (64,32)) #横長にリサイズする場合
    rim2 = cv2.resize(im2, (64,32))
    gcon = cv2.vconcat([rim1, rim2]) #2つを縦にに結合
    cv2.imwrite(os.path.join(save_dir, '{}.jpg'.format('con'+str(i))), gcon)

for i in a4:
    #path1, path2 のindex=iのイメージ読み込むを
    im1 = cv2.imread(os.path.join(path_1, path1s[i]))
    im2 = cv2.imread(os.path.join(path_2, path2s[i]))

    rim1 = cv2.resize(im1, (64,32)) #横長にリサイズする場合
    rim2 = cv2.resize(im2, (64,32))
    gcon = cv2.vconcat([rim2, rim1]) #2つを縦に逆順で結合
    cv2.imwrite(os.path.join(save_dir, '{}.jpg'.format('con'+str(i))), gcon)

追加収集した1000枚の画像セットを、マルチラベルCNNモデルで学習・検証・テスト

1000枚の追加した収集画像を使って、マルチラベル分類CNNモデルで、学習・検証・テストした結果は、次のようになりました。

history_multi-lael_2.png

評価 損失関数(loss): 0.24989499151706696
評価 正解率(accuracy): 0.8399999737739563

損失関数、正解率ともに、300枚データで検証したときよりも、向上しました。

以下、テスト結果です。

multi-lael_2_graf (1).png

multi-lael_2_confusion_matrix.png

         precision    recall  f1-score   support

      バス       0.79      0.90      0.84        30
      子供       0.88      0.77      0.82        30

accuracy                           0.83        60

子供の再現率(recall)0.77 正解率(accuracy) 0.83 と良い数字になってはいますが、子供の正解率(再現率)がもう少し高い数字になってほしいところです。

(3) 転移学習

学習済みモデルのVGG16, VGG19, RegNet50, ResNet152, MobileNetV2, RegNetY320 を epochs 数100で、どのような正解率(accuracy)がでるか、試してみました。結果をみると、ResNet152 の検証正解率(val_accuracy) が、学習正解率(accuracy) からの乖離が少なく、一番適しているモデルのように考えられます。

histories.png

ResNet152

ResNet152を使った転移学習モデルを作成して、検証データ30セットをテストしました。
最適化関数にAdamを採用しました。また、上位レイヤーの 'conv5_block3_1_conv'の学習をしないよう、ファインチューニングしています。EarlyStoppingを設定することで、val_loss の値に一定の改善が見られなかった場合に、学習を打ち切るようにしました。

history_multi-label_resnet152.png

評価 損失関数(loss): 0.216453418135643
評価 正解率(accuracy): 0.8166666626930237

転移学習モデルの学習結果は、先程のCNNモデルの学習結果に近い数字になりました。

以下は、30セットの子供が居るいないバスのテストデータでテストした結果です。

multi-label_resnet152_graf.png

multi-label_resnet152_confusion_matrix.png

          precision    recall  f1-score   support

      バス       0.93      0.83      0.88        30
      子供       0.85      0.93      0.89        30

accuracy                           0.88        60

テスト結果は、再現率(recall)が0.93,正解率(accuracy)が0.88 となっており、CNNモデルより精度が増した結果になりました。

転移学習はこちらのコードで検証しました

(転移学習のマルチラベル識別モデルで画像識別)

# -*- coding: utf-8 -*-

"""
転移学習を使用した、マルチラベル分類(multi-label classification)で、人の画像と、
車内に人が居ない画像の2つのクラスを設定し、人ラベル、車内ラベル、人+車内
複合ラベルの3つの画像グループで学習する。
バスに子供が居る画像を認識できるか、CNN画像認識(活性化関数sigmoid, 損失関数binary_crossentropy
を使って検証する。
"""
# Python標準ライブラリ
import os

# サードパーティライブラリ(tensorflow)
from tensorflow.keras.applications.resnet import ResNet152
from tensorflow.keras.callbacks import EarlyStopping
from tensorflow.keras.layers import Dense,Dropout,Input,BatchNormalization, Flatten
from tensorflow import keras
from tensorflow.keras.models import Model, Sequential, load_model
from tensorflow.keras import optimizers

# サードパーティライブラリ(Scikit-learn)
from sklearn.metrics import classification_report
from sklearn.metrics import confusion_matrix

# サードパーティライブラリ(その他)
import numpy as np
import matplotlib.pyplot as plt
import cv2
import pandas as pd
import seaborn as sns
import japanize_matplotlib


#基本pathの設定
#base_path = '.' #ローカルの場合
base_path = '/content/multi-label' #Google colab の場合

#保存用pathの設定
#result_path = './result' #ローカルの場合
result_path = '/content/drive/MyDrive/multi-label_2' #Google colab の場合

#子供の画像と人の居ない車内の画像、子供+車内の画像から学習用評価用データセットを作成
inai_path = os.path.join(base_path, 'images/bus') #人が居ない車内の画像(車内)
iru_path = os.path.join(base_path, 'images/kodomo_x_bus') #車内に子供が居る画像(車内+子供)
hito_path = os.path.join(base_path, 'images/kodomo') #子供の画像

#フォルダー上にある画像をnum数だけリサイズして取り出す関数
def make_set(folder, num):
    images = []
    filenames = os.listdir(folder)
    for i in range(len(filenames)):
        img = cv2.imread(os.path.join(folder, filenames[i]))
        img = img[:, :, ::-1] # bgr→rgbチャネル入れ替え
        #多次元ndarrayの各次元のスライスをカンマで区切って指定できる。
        #スライスのstep(sart:stop:step)は、step個毎に取り出しだが、マイナスは逆順  
        img = cv2.resize(img, (64,64))
        images.append(img)

    return images[:num]

#NUM枚の画像でデータセットを作成
NUM=1000
#データセットの作成
s_inai = make_set(inai_path, NUM)
s_hito = make_set(hito_path, NUM)
s_iru = make_set(iru_path, NUM)

X = np.array(s_inai + s_hito + s_iru)
#マルチラベル分類用ラベルの作成(マルチホットエンコーディング)
#車内のみ(バス)の画像=ラベル[1,0]
#人の画像=ラベル[0,1]
#車内に人が居る画像 = ラベル[1,1]
y =  np.array([[1,0]]*len(s_inai) + [[0,1]]*len(s_hito) + [[1,1]]*len(s_iru))

rand_index = np.random.permutation(np.arange(len(X)))
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):]


# モデルの定義
# 転移学習用練習済みモデルのインスタンスの生成
#---------------------------
#インプット shapeは画像のピクセルに合わせる
conv_base =ResNet152 (weights='imagenet',
                  include_top=False,
                  input_shape=(64, 64, 3))
#---------------------------

#モデル構造の確認
#conv_base.summary()

#転移学習
model = Sequential()
model.add(conv_base)
model.add(Flatten())
#relu活性化関数のkernel_initializerは、he_normalを指定します。
model.add(Dense(256, activation='relu', kernel_initializer='he_normal' ))
model.add(Dense(2, activation='sigmoid')) #出力層は2 ユニット

conv_base.trainable = True

set_trainable = False
for layer in conv_base.layers:
    if layer.name == 'conv5_block3_1_conv': #上位2-3レイヤー
        set_trainable = True
    if set_trainable:
        layer.trainable = True
    else:
        layer.trainable = False

# コンパイル
model.compile(loss='binary_crossentropy',
              #最適化関数をRMDpropから、Adamに変更してみる。
              optimizer = optimizers.Adam(lr=0.001, beta_1=0.9, beta_2=0.999, epsilon=None, decay=0.0, amsgrad=False), 
              metrics=['accuracy'])


# 重みデータ を読み込みます
#model.load_weights(os.path.join(result_path, 'param_multi-label_3.hdf5'))

# 学習
#early_stoppingでval_lossに改善が見られなかったら学習打ち切り
early_stopping = EarlyStopping(
                              patience=10,
                              min_delta=0.0001,                               
                              monitor="val_loss",
                              restore_best_weights=True
                              )

# 学習
history = model.fit(X_train, y_train, 
                    epochs=80, 
                    batch_size=64, 
                    verbose=1, 
                    validation_data=(X_test, y_test),
                    callbacks=[early_stopping],
                    )

#----history 描画用変数代入-------
loss=history.history['loss']
val_loss=history.history['val_loss']
acc=history.history['accuracy']
val_acc=history.history['val_accuracy']
#----------------------------------

# 学習によって得た重みを保存する場合は、 save_weights() メソッドを使います。
model.save_weights(os.path.join(result_path, 'param_multi-label_resnet152.hdf5'))


#検証結果保存用テキストファイル
with open(os.path.join(result_path, 'eval_multi-label_resnet152.txt'), 'w') as f:
  # 精度の評価
  scores = model.evaluate(X_test, y_test, verbose=1)
  f.write('Test loss: {}\n'.format(scores[0]))
  f.write('Test accuracy: {}\n'.format(scores[1]))
  f.write('\n')


#----history 描画用matPlotlib-------
epochs=len(loss)
fig = plt.figure()
# figure全体のタイトル
fig.suptitle("マルチラベル ResNet152 転移学習")

plt.subplot(121)
plt.plot(range(epochs), loss, marker = 'o', color='b', label = 'loss')
plt.plot(range(epochs), val_loss,  color='b', label = 'val_loss')
plt.legend(loc = 'best')
plt.grid()
plt.xlabel('epoch')
plt.ylabel('loss')

plt.subplot(122)
plt.plot(range(epochs), acc, marker = 'o', color='b',  label = 'acc')
plt.plot(range(epochs), val_acc, color='b', label = 'val_acc')
plt.legend(loc = 'best')
plt.grid()
plt.xlabel('epoch')
plt.ylabel('acc')

plt.tight_layout(rect=[0,0,1,0.96])  #suptitleが重なるので、
plt.savefig(os.path.join(result_path, "history_multi-label_resnet152.png"))
plt.show()
plt.close()
#----history 描画用matPlotlib 終わり-------

#テストデータを学習済みモデルで予測した結果をグラフに描画する関数
#テスト画像のフォルダから、指定された数のリスト
#(pred=予測値, org_img=オリジナル画像)を作成
def mk_data(path, num):
    files = os.listdir(path)
    files = files[:num]
    img_list = []

    for fn in files:
        org_img = cv2.imread(os.path.join(path, fn))
        img = org_img[:, :, ::-1] # bgr→rgbチャネル入れ替え
        img = cv2.resize(img, (64,64))
        pred = model.predict(img.reshape(1,64,64,3), verbose = 0)
        #print('pred:', pred)
        img_list.append([pred, org_img])

    return img_list

def make_kekka(img_list, text, ax):
    x_0 = []  #クラス0(車内) の検証No.
    y_0 = []  #クラス0(車内) の予測値
    x_1 = []  #クラス1(子供) の検証No.
    y_1 = []  #クラス1(子供) の予測値
    for i, res in enumerate(img_list):
        x_0.append(i+1)
        y_0.append(res[0][0][0])
        x_1.append(i+1)
        y_1.append(res[0][0][1])

    ax.set_title(text)
    ax.plot(x_0, y_0, label='bus')
    ax.plot(x_1, y_1, label='child')
    ax.set_ylabel('predict')
    ax.set_xticks(range(len(x_0)))
    ax.legend()

no_path = os.path.join(base_path, './images/noone_in_a_bus') #車内に子供が居ない画像
child_path = os.path.join(base_path, './images/child_in_a_bus') #車内に子供が居る画像

fig = plt.figure(figsize=(14,6))

#子供が居る画像のテスト
child_img_list = mk_data(child_path, 30)
text = '子供が居るバス'
ax = fig.add_subplot(1,2,1)
make_kekka(child_img_list, text, ax)

#子供が居ない画像のテスト
no_img_list = mk_data(no_path, 30)
text = '子供が居ないバス'
ax = fig.add_subplot(1,2,2)
make_kekka(no_img_list, text, ax)

sfn = os.path.join(result_path, 'multi-label_resnet152_graf.png')
plt.savefig(sfn)
plt.show()
plt.close()

#----ここから画像表示-----
#子供の予測がしきい値(=Svalue)
def save_gazo(img_list, savefn, shiki):
  Svalue = shiki  

  plt.figure(figsize=(32, 32))
  #plt.subplots_adjust(wspace=0.801, hspace=0.063)

  image_pos = 0
  cnt_disp = 30
  col = 5
  row = int(cnt_disp / col) + 1

  j=0
  for pred, img in img_list:
    image = cv2.resize(img, (256, 256))
    plt.subplot(row, col, image_pos + 1)
    plt.imshow(image)
    image_pos += 1

    y_0 = pred[0][0]
    y_1 = pred[0][1]

    # 子供予測確率がしきい値より高ければ Blue
    if y_1 > Svalue:
      color = 'blue'
    else:
      color = 'red'

    plt.xlabel("[{}] C:{:.3f}, B:{:.3f}".format(j+1,y_1, y_0), color=color, fontsize=12)
    j+=1

  plt.tight_layout()
  plt.savefig(savefn)
  plt.show()
  plt.close()

"""
savefn = os.path.join(result_path, 'multi-label_vg19_gazos_0.png')
save_gazo(mk_data(no_path, 30), savefn, 0.5)
savefn = os.path.join(result_path, 'multi-label_resnet152_gazos_1.png')
save_gazo(mk_data(child_path, 30), savefn, 0.5)
"""

#-------------
#confusion matrix の実装
pred=[] #予測結果のリスト
for data in child_img_list:
  pred.append(int(data[0][0][1] > 0.5)) #2つ目の「人」の予測値が0.5より大きかったら1

for data in no_img_list:
  pred.append(int(data[0][0][1] > 0.5)) #2つ目の「人」の予測値が0.5より大きかったら1

ty=[] #ラベル 子供を含む画像を1とする
ty = [1]*len(child_img_list) + [0] *len(no_img_list)  

cm = confusion_matrix(ty, pred)
cm = pd.DataFrame(data=cm, index=["バス", "子供"], 
                           columns=["バス", "子供"])
sns.heatmap(cm, square=True, cbar=True, annot=True, cmap='Blues')
plt.yticks(rotation=0)
plt.xlabel("予測値", fontsize=13, rotation=0)
plt.ylabel("真の値", fontsize=13)
ax.set_ylim(len(cm), 0)
sfn = os.path.join(result_path, 'multi-label_resnet152_confusion_matrix.png')
plt.savefig(sfn)

print('混同行列:\n',cm)
label_names = ['バス', '子供']
#検証結果保存用テキストファイル
with open(os.path.join(result_path, 'eval_multi-label_resnet152.txt'), 'a') as f:
  f.write(classification_report(ty, pred,target_names=label_names))

(4) 画像の水増し

さらに正解率が向上することを期待して、マルチラベルクラスの転移学習モデルに、学習する画像を水増しして渡してみます。

学習する画像の水増しは、ImageDataGenerator クラスを使用しました。このクラスでは、変数を設定することで、画像の回転や、水平・垂直のシフト・反転、画像のズームなど、様々な変化をつけて画像を水増しすることができます。このクラスの、flow_from_dataframeメソッドにデータフレーム形式の画像の情報を渡すことで、モデルが学習する直前に、水増しした画像を作成してモデルに提供するように設定しました。

前項で検証・テストした、ResNet152を使用したマルチラベルクラスの転移学習モデルに水増し画像を学習させたところ、次のような学習結果となり評価正解率が0.7と、水増しなしデータのときの0.81よりも悪い結果となってしまいました。

history_multi-label_resnet152.png

一方、学習済みモデルVGG19を使用したマルチラベルクラスの転移学習モデルに水増し画像を学習させたところ、次のように、水増しなしデータのときの評価正解率が0.79だったところ、水増しデータでは、0.85に改善しました。

history_multi-label_vgg19.png

そこで、水増しデータを使ったモデルの改善は、マルチラベルクラスのVGG19転移学習モデルのハイパーパラメーターを調整して、子供の検知精度が更に向上するようにしてみます。

今回ハイパーパラメーターで、正解率に影響が大きいと思われるパラメーターは、 epochs 数, learning_rate および、ImageDataGenerator の rotation_range, shear_range でした。
shear_rangeは設定せず、rotation_range は20に設定、また、epoch数を80にして、epoch数60のところで、learning_rateを1/2に調整したところ、次のように、水増し画像を使用しない転移モデルに比べ、検証正解率 0.81-> 0.86 再現率(recall) 0.93 -> 0.97 と、精度の向上したモデルができました。

history_multi-label_vgg1950f4.png

評価 損失関数(loss): 0.10100848972797394
評価 正解率(accuracy): 0.8613861203193665

multi-label_vgg1950f4_graf.png

multi-label_vggf1950f4_confusion_matrix.png

precision recall f1-score support

      バス       0.96      0.80      0.87        30
      子供       0.83      0.97      0.89        30

accuracy                           0.88        60
水増し転移学習モデルのコードです

(水増しデータ転移学習のマルチラベル識別モデルで画像識別)

# -*- coding: utf-8 -*-

"""
転移学習を使用した、マルチラベル分類(multi-label classification)で、人の画像と、
車内に人が居ない画像の2つのクラスを設定し、人ラベル、車内ラベル、人+車内
複合ラベルの3つの画像グループで学習する。
バスに子供が居る画像を認識できるか、CNN画像認識(活性化関数sigmoid, 損失関数binary_crossentropy
を使って検証する。
"""

#Python標準ライブラリ
import os

# サードパーティライブラリ(tensorflow)
from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.applications.vgg19 import VGG19
from tensorflow.keras.callbacks import EarlyStopping
from tensorflow.keras.layers import Dense,Dropout,Input,BatchNormalization, Flatten
from tensorflow import keras
from tensorflow.keras.models import Model, Sequential, load_model
from tensorflow.keras import optimizers
from tensorflow.keras.callbacks import LearningRateScheduler

# サードパーティライブラリ(Scikit-learn)
from sklearn.metrics import classification_report
from sklearn.metrics import confusion_matrix

# サードパーティライブラリ(その他)
import numpy as np
import matplotlib.pyplot as plt
import cv2
import pandas as pd
import seaborn as sns
import japanize_matplotlib

#基本pathの設定
#base_path = '.' #ローカルの場合
base_path = '/content/img_gen' #Googl colab の場合
#base_path = '/home/studio-lab-user/img_gen' #SageMaker Studio Lab の場合

#子供の画像と人の居ない車内の画像、子供+車内の画像を1つのフォルダーに集約
img_path = os.path.join(base_path, 'images') #画像フォルダのパス

#保存用pathの設定
#result_path = './result' #ローカルの場合
#result_path = '/home/studio-lab-user/vgg19' #SageMaker Studio Lab の場合
result_path = '/content/drive/MyDrive/vgg1950f' #Google colab の場合

#画像とラベルインデックスのデータフレームに読み込む
df=pd.read_csv(os.path.join(base_path, "gazo_labels.csv"))
df = df.sample(frac=1, ignore_index=True)
columns=["bus", "child"]


# 学習用・検証用
train_datagen = ImageDataGenerator(
    rotation_range=20,
    height_shift_range=0.2,
    channel_shift_range = 100, 
    #shear_range=0.2,
    fill_mode="nearest",
    rescale=1./255,
    validation_split=0.2,
)
19
# テスト用
test_datagen = ImageDataGenerator(
    rescale=1./255,
    validation_split=0.2,
)


# 学習用
train_generator = train_datagen.flow_from_dataframe(
    dataframe=df[:2500],
    directory=img_path,
    x_col='Filenames',
    y_col=columns,
    class_mode='raw',
    batch_size=64,
    target_size=(64,64),
    shuffle=True,
    seed=4,
    subset='training'
)

# 検証用
valid_generator = test_datagen.flow_from_dataframe(
    dataframe=df[2500:] ,
    directory=img_path,
    x_col='Filenames',
    y_col=columns,
    class_mode='raw',
    batch_size=64,
    target_size=(64,64),
    shuffle=True, 
    seed=4,
    subset='validation'
)


# モデルの定義
# 転移学習用練習済みモデルのインスタンスの生成
#---------------------------
#インプット shapeは画像のピクセルに合わせる
conv_base = VGG19(weights='imagenet',
                  include_top=False,
                  input_shape=(64, 64, 3))
#---------------------------


#転移学習
model = Sequential()
model.add(conv_base)
model.add(Flatten())
#relu活性化関数のkernel_initializerは、he_normalを指定します。
model.add(Dense(256, activation='relu', kernel_initializer='he_normal' ))
model.add(Dense(2, activation='sigmoid')) #出力層は2 ユニット

#指定したレイヤー名以前の層は、学習を凍結する。
layer_names = [l.name for l in conv_base.layers]
idx = layer_names.index('block5_conv1')
print('idx', idx)

conv_base.trainable = True

for layer in conv_base.layers[:idx]:
    layer.trainable = False

#モデル構造の確認
conv_base.summary()

# コンパイル
model.compile(loss='binary_crossentropy',
              #最適化関数を、Adamに。
              #optimizer = optimizers.Adam(lr=0.00001, beta_1=0.9, beta_2=0.999, decay=0.0, epsilon=None, amsgrad=False), 
              optimizer = optimizers.Adam(lr=0.0001, amsgrad=False), 
              metrics=['accuracy'])


# 重みデータ を読み込みます
#model.load_weights(os.path.join(result_path, 'param_multi-label_3.hdf5'))

# 学習
#early_stoppingでval_lossに改善が見られなかったら学習打ち切り
#early_stopping = EarlyStopping(
#                              patience=10,
#                              min_delta=0.0001,                               
#                              monitor="val_loss",
#                              restore_best_weights=True
#                              )

def step_decay(epoch):
    lr = 0.0001
    if(epoch >= 60):
        lr/=2
#    if(epoch>=25):
#        lr/=20
#    if(epoch>=30):
#        lr/=20
    return lr

lr_decay = LearningRateScheduler(step_decay)

history = model.fit(
    train_generator,
    epochs=80,
    steps_per_epoch=len(train_generator),
    validation_data=valid_generator,
    validation_steps=len(valid_generator),
    #callbacks=[early_stopping],
    callbacks = [lr_decay]
    #callbacks=[early_stopping, lr_decay],
)

#----history 描画用変数代入-------
loss=history.history['loss']
val_loss=history.history['val_loss']
acc=history.history['accuracy']
val_acc=history.history['val_accuracy']
#----------------------------------

# 学習によって得た重みを保存する場合は、 save_weights() メソッドを使います。
#model.save_weights(os.path.join(result_path, 'param_multi-label_3.hdf5'))
#モデルを保存 model.save() メソッドを使います。
model.save(os.path.join(result_path, 'model_multi-label_vgg1950f4.h5'))


#検証結果保存用テキストファイル
with open(os.path.join(result_path, 'eval_multi-label_vgg1950f4.txt'), 'w') as f:
  # 精度の評価
  #scores = model.evaluate(X_test, y_tesut, verbose=1)
  scores = model.evaluate(valid_generator)
  print('Test loss: {}\n'.format(scores[0]))
  print('Test accuracy: {}\n'.format(scores[1]))
  f.write('Test loss: {}\n'.format(scores[0]))
  f.write('Test accuracy: {}\n'.format(scores[1]))
  f.write('\n')


#----history 描画用matPlotlib-------
epochs=len(loss)
fig = plt.figure()
# figure全体のタイトル
fig.suptitle("マルチラベル VGG19 転移学習 水増しrot.25 epochs.80")

plt.subplot(121)
plt.plot(range(epochs), loss, marker = 'o', color='b', label = 'loss')
plt.plot(range(epochs), val_loss,  color='b', label = 'val_loss')
plt.legend(loc = 'best')
plt.grid()
plt.xlabel('epoch')
plt.ylabel('loss')

plt.subplot(122)
plt.plot(range(epochs), acc, marker = 'o', color='b',  label = 'acc')
plt.plot(range(epochs), val_acc, color='b', label = 'val_acc')
plt.legend(loc = 'best')
plt.grid()
plt.xlabel('epoch')
plt.ylabel('acc')

plt.tight_layout(rect=[0,0,1,0.96])  #suptitleが重なるので、
plt.savefig(os.path.join(result_path, "history_multi-label_vgg1950f4.png"))
plt.show()
plt.close()
#----history 描画用matPlotlib 終わり-------

#テストデータを学習済みモデルで予測した結果をグラフに描画する関数
#テスト画像のフォルダから、指定された数のリスト
#(pred=予測値, org_img=オリジナル画像)を作成
def mk_data(path, num):
    files = os.listdir(path)
    files = files[:num]
    img_list = []

    for fn in files:
        org_img = cv2.imread(os.path.join(path, fn))
        img = org_img[:, :, ::-1] # bgr→rgbチャネル入れ替え
        img = cv2.resize(img, (64,64))
        pred = model.predict(img.reshape(1,64,64,3), verbose = 0)
        img_list.append([pred, org_img])

    return img_list

def make_kekka(img_list, text, ax):
    x_0 = []  #クラス0(車内) の検証No.
    y_0 = []  #クラス0(車内) の予測値
    x_1 = []  #クラス1(子供) の検証No.
    y_1 = []  #クラス1(子供) の予測値
    for i, res in enumerate(img_list):
        x_0.append(i+1)
        y_0.append(res[0][0][0])
        x_1.append(i+1)
        y_1.append(res[0][0][1])

    ax.set_title(text)
    ax.plot(x_0, y_0, label='バス')
    ax.plot(x_1, y_1, label='子供')
    ax.set_ylabel('予測値')
    ax.set_xticks(range(len(x_0)))
    ax.legend()

no_path = os.path.join(base_path, 'noone_in_a_bus') #車内に子供が居ない画像
child_path = os.path.join(base_path, 'child_in_a_bus') #車内に子供が居る画像

fig = plt.figure(figsize=(14,6))

#子供が居る画像のテスト
child_img_list = mk_data(child_path, 30)
text = '子供が居るバス'
ax = fig.add_subplot(1,2,1)
make_kekka(child_img_list, text, ax)

#子供が居ない画像のテスト
no_img_list = mk_data(no_path, 30)
text = '子供の居ないバス'
ax = fig.add_subplot(1,2,2)
make_kekka(no_img_list, text, ax)

sfn = os.path.join(result_path, 'multi-label_vgg1950f4_graf.png')
plt.savefig(sfn)
plt.show()
plt.close()

#----ここから画像表示-----
#子供の予測がしきい値(=Svalue)
def save_gazo(img_list, savefn, shiki):
  Svalue = shiki  

  plt.figure(figsize=(32, 32))
  #plt.subplots_adjust(wspace=0.801, hspace=0.063)

  image_pos = 0
  cnt_disp = 30
  col = 5
  row = int(cnt_disp / col) + 1

  j=0
  for pred, img in img_list:
    image = cv2.resize(img, (256, 256))
    plt.subplot(row, col, image_pos + 1)
    plt.imshow(image)
    image_pos += 1

    y_0 = pred[0][0]
    y_1 = pred[0][1]

    # 子供予測確率がしきい値より高ければ Blue
    if y_1 > Svalue:
      color = 'blue'
    else:
      color = 'red'

    plt.xlabel("[{}] C:{:.3f}, B:{:.3f}".format(j+1,y_1, y_0), color=color, fontsize=12)
    j+=1

  plt.tight_layout()
  plt.savefig(savefn)
  plt.show()
  plt.close()

savefn = os.path.join(result_path, 'multi-label_vgg1950f4_gazos_0.png')
save_gazo(mk_data(no_path, 30), savefn, 0.5)
savefn = os.path.join(result_path, 'multi-label_vgg1950f4_1.png')
save_gazo(mk_data(child_path, 30), savefn, 0.5)

#-------------
#confusion matrix の実装
pred=[] #予測結果のリスト
for data in child_img_list:
  pred.append(int(data[0][0][1] > 0.5)) #2つ目の「人」の予測値が0.5より大きかったら1

for data in no_img_list:
  pred.append(int(data[0][0][1] > 0.5)) #2つ目の「人」の予測値が0.5より大きかったら1

ty=[] #ラベル 子供を含む画像を1とする
ty = [1]*len(child_img_list) + [0] *len(no_img_list)  

cm = confusion_matrix(ty, pred)
cm = pd.DataFrame(data=cm, index=["バス", "子供"], 
                           columns=["バス", "子供"])
sns.heatmap(cm, square=True, cbar=True, annot=True, cmap='Blues')
plt.yticks(rotation=0)
plt.xlabel("予測値", fontsize=13, rotation=0)
plt.ylabel("真の値", fontsize=13)
ax.set_ylim(len(cm), 0)
sfn = os.path.join(result_path, 'multi-label_vggf1950_confusion_matrix.png')
plt.savefig(sfn)

print('混同行列:\n',cm)
label_names = ['バス', '子供']
#検証結果保存用テキストファイル
with open(os.path.join(result_path, 'eval_multi-label_vgg1950f4_1.txt'), 'a') as f:
  f.write(classification_report(ty, pred,target_names=label_names))

7 バスに子供が居るいない、判定アプリ

完成したモデルを使った「バスに子供が居るいない、判定アプリは、こちらになります。

8 おわりに

園児の置き去り防止用としては、精度が高ければ高いほうがよいと思います。
もう少し、子供の居ない画像の誤認を減らせれば良いと思いましたが、今回はこれ以上精度を上げるのは、難しかったです。

今後は、トライできていない学習済みモデル、EfficientNet なども試してみたいです。

また、収集した画像をもっと紹介したかったのですが、出典の不明なものは控えることにしました。

ここでは、画像識別モデルを作成しましたが、次の機会には、物体検知も勉強したいです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0