0
0

世界各国の人の顔写真をPythonを使って画像分類してみた!

Last updated at Posted at 2024-03-13

はじめに

はじめまして!
フリーランスとしてWEBディレクターをしているNaohiroといいます。
WEBディレクターやマーケティング施策をおこなっている過程でデータサイエンスの力の必要性を感じ、Python学習を始めました。
今回はアウトプット学習の成果物として本記事を投稿させていただきます。

本記事の概要

日本人を含むアジア圏の人の顔写真をPythonの画像分類のモデルに学習させ識別させました。
今回はプログラミングの過程や結果、結果に関する考察についてお伝えできればと思います。

目次

  • 今回の目的
  • 実行環境
  • プログラム
    • 画像収集
    • (画像の前処理)
    • モデル構築
    • 結果
  • 試行錯誤
  • 考察と反省
  • おわりに

今回の目的

今回外国人の顔写真識別のモデルを構築するに思い立った経緯は、普段の日常の何気ない疑問からでした。

みなさんはテレビや映画を見ている時、「この人、どこらへの国の人だろう」と思う瞬間はないですか?
そんな時、パシャっと写真をとって、国や地域の判別ができたらスッキリしませんか?笑

そこで、Pytonを使って世界各国の人の顔写真から地域を判別できる、画像分類モデルをつくってみました。
防犯、セキュリティの観点からも使えるモデルかとも思います。

如何せんプログラミング学習自体が初学者のため、ツッコミどころがたくさんあるかと思いますがご容赦くださいませ。

実行環境

・Google Colaboratory

プログラム

画像収集

必要な画像を用意します。
まず、GoogleDrive上に画像を保管する場合はドライブをマウントしなければなりません。
以下のコードを実行することで、Google DriveとGoogle Colaboratoryとを連携できます。

from google.colab import drive
drive.mount('/content/drive')

次にいよいよ「scraiping」で画像収集をしていきます。


画像のスクレイピング

当初はもっといろんな国の人の顔写真をスクレイピングする予定でしたが、時間の兼ね合いで今回は欧米の人の顔写真と日本含むアジア圏の人の顔写真を集めました。

今回はicrawlerというWEBクローラーを使用して、Bingという検索エンジンから各国100枚の画像を集めています。

>>画像データをキーワード検索で効率的に収集する方法(Python「icrawler」のBing検索)
https://www.atmarkit.co.jp/ait/articles/2010/28/news018.html

以下コードです。

#"スペイン人 顔写真"をキーワードとする
search_word = "スペイン人 顔写真"
#ダウンロードするキーワード
crawler = BingImageCrawler(storage={'root_dir': "/content/drive/MyDrive/Aidemy_seikabutu/production/Spain"})
#ダウンロードする画像の最大枚数は100枚
crawler.crawl(keyword=search_word, max_num=100)

#"日本人 顔写真"をキーワードとする
search_word = "日本人 顔写真"
#ダウンロードするキーワード
crawler = BingImageCrawler(storage={'root_dir': "/content/drive/MyDrive/Aidemy_seikabutu/production/Japan"})
#ダウンロードする画像の最大枚数は100枚
crawler.crawl(keyword=search_word, max_num=100)

#"中国人 顔写真"をキーワードとする
search_word = "中国人 顔写真"
#ダウンロードするキーワード
crawler = BingImageCrawler(storage={'root_dir': "/content/drive/MyDrive/Aidemy_seikabutu/production/China"})
#ダウンロードする画像の最大枚数は100枚
crawler.crawl(keyword=search_word, max_num=100)

#"韓国人 顔写真"をキーワードとする
search_word = "韓国人 顔写真"
#ダウンロードするキーワード
crawler = BingImageCrawler(storage={'root_dir': "/content/drive/MyDrive/Aidemy_seikabutu/production/Korea"})
#ダウンロードする画像の最大枚数は100枚
crawler.crawl(keyword=search_word, max_num=100)

#"タイ人 顔写真"をキーワードとする
search_word = "タイ人 顔写真"
#ダウンロードするキーワード
crawler = BingImageCrawler(storage={'root_dir': "/content/drive/MyDrive/Aidemy_seikabutu/production/Taipei"})
#ダウンロードする画像の最大枚数は100枚
crawler.crawl(keyword=search_word, max_num=100)


#"ベトナム人 顔写真"をキーワードとする
search_word = "ベトナム人 顔写真"
#ダウンロードするキーワード
crawler = BingImageCrawler(storage={'root_dir': "/content/drive/MyDrive/Aidemy_seikabutu/production/Vietnam"})
#ダウンロードする画像の最大枚数は100枚
crawler.crawl(keyword=search_word, max_num=100)

#"イタリア人 顔写真"をキーワードとする
search_word = "イタリア人 顔写真"
#ダウンロードするキーワード
crawler = BingImageCrawler(storage={'root_dir': "/content/drive/MyDrive/Aidemy_seikabutu/production/Italia"})
#ダウンロードする画像の最大枚数は100枚
crawler.crawl(keyword=search_word, max_num=100)

#"フランス人 顔写真"をキーワードとする
search_word = "フランス人 顔写真"
#ダウンロードするキーワード
crawler = BingImageCrawler(storage={'root_dir': "/content/drive/MyDrive/Aidemy_seikabutu/production/France"})
#ダウンロードする画像の最大枚数は100枚
crawler.crawl(keyword=search_word, max_num=100)

#"イギリス人 顔写真"をキーワードとする
search_word = "イギリス人 顔写真"
#ダウンロードするキーワード
crawler = BingImageCrawler(storage={'root_dir': "/content/drive/MyDrive/Aidemy_seikabutu/production/British"})
#ダウンロードする画像の最大枚数は100枚
crawler.crawl(keyword=search_word, max_num=100)

#"アメリカ人 顔写真"をキーワードとする
search_word = "アメリカ人 顔写真"
#ダウンロードするキーワード
crawler = BingImageCrawler(storage={'root_dir': "/content/drive/MyDrive/Aidemy_seikabutu/production/America"})
#ダウンロードする画像の最大枚数は100枚
crawler.crawl(keyword=search_word, max_num=100)

#"オランダ人 顔写真"をキーワードとする
search_word = "オランダ人 顔写真"
#ダウンロードするキーワード
crawler = BingImageCrawler(storage={'root_dir': "/content/drive/MyDrive/Aidemy_seikabutu/production/Olanda"})
#ダウンロードする画像の最大枚数は100枚
crawler.crawl(keyword=search_word, max_num=100)


#"ブラジル人 顔写真"をキーワードとする
search_word = "ブラジル人 顔写真"
#ダウンロードするキーワード
crawler = BingImageCrawler(storage={'root_dir': "/content/drive/MyDrive/Aidemy_seikabutu/production/Brazil"})
#ダウンロードする画像の最大枚数は100枚
crawler.crawl(keyword=search_word, max_num=100)

#"ポルトガル人 顔写真"をキーワードとする
search_word = "ポルトガル人 顔写真"
#ダウンロードするキーワード
crawler = BingImageCrawler(storage={'root_dir': "/content/drive/MyDrive/Aidemy_seikabutu/production/Portugal"})
#ダウンロードする画像の最大枚数は100枚
crawler.crawl(keyword=search_word, max_num=100)


このように画像をスクレイピングすることができました!

image.png




モデル構築

画像の前処理には3つの処理を行いました。
(「試行錯誤」でも記載しますが、この精度をあげるためにもこの前処理がとても大変な作業でした)

構築したモデルをご紹介しまs。
長いため、前段と後段にわけました。

前段
・必要となるライブラリやモジュールのインポート
・画像の読み込みおよびリサイズ処理

後段
・ラベルデータの用意
・学習データと検証データの用意
・モデルの構築

以下コードになります。

(画像の読み込み)

from glob import glob
import os#osモジュール(os機能がpythonで扱えるようにする)
import cv2#画像や動画を処理するオープンライブラリ
import numpy as np#python拡張モジュール
import matplotlib.pyplot as plt#グラフ可視化
from tensorflow.keras.utils import to_categorical#正解ラベルをone-hotベクトルで求める
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#最適化関数

#各国の人間の顔写真画像の格納
drive_Spain = "/content/drive/MyDrive/Aidemy_seikabutu/production/Spain/"
drive_Japan = "/content/drive/MyDrive/Aidemy_seikabutu/production/Japan/"
drive_China = "/content/drive/MyDrive/Aidemy_seikabutu/production/China/"
drive_Korea = "/content/drive/MyDrive/Aidemy_seikabutu/production/Korea/"
drive_Taipei = "/content/drive/MyDrive/Aidemy_seikabutu/production/Taipei/"
drive_Vietnam = "/content/drive/MyDrive/Aidemy_seikabutu/production/Vietnam/"
drive_Italia = "/content/drive/MyDrive/Aidemy_seikabutu/production/Italia/"
drive_France = "/content/drive/MyDrive/Aidemy_seikabutu/production/France/"
drive_British = "/content/drive/MyDrive/Aidemy_seikabutu/production/British/"
drive_America = "/content/drive/MyDrive/Aidemy_seikabutu/production/America/"
drive_Olanda = "/content/drive/MyDrive/Aidemy_seikabutu/production/Olanda/"
drive_Brazil = "/content/drive/MyDrive/Aidemy_seikabutu/production/Brazil/"
drive_Portugal = "/content/drive/MyDrive/Aidemy_seikabutu/production/Portugal/"
image_size = 200 #200×200に変更してみた

#glob() で指定したファイルを取得

path_Spain = glob(os.path.join(drive_Spain, "*"))
path_Japan = glob(os.path.join(drive_Japan, "*"))
path_China = glob(os.path.join(drive_China, "*"))
path_Korea = glob(os.path.join(drive_Korea, "*"))
path_Taipei = glob(os.path.join(drive_Taipei, "*"))
path_Vietnam = glob(os.path.join(drive_Vietnam, "*"))
path_Italia = glob(os.path.join(drive_Italia, "*"))
path_France = glob(os.path.join(drive_France, "*"))
path_British = glob(os.path.join(drive_British, "*"))
path_America = glob(os.path.join(drive_America, "*"))
path_Olanda = glob(os.path.join(drive_Olanda, "*"))
path_Brazil = glob(os.path.join(drive_Brazil, "*"))
path_Portugal = glob(os.path.join(drive_Portugal, "*"))

#各国の人間の顔写真画像を格納するリスト作成
img_Asia = []
img_Other = []


for i in path_Spain:
    img = cv2.imread(i)#画像を読み込む
    img = cv2.resize(img,(image_size,image_size))#画像をリサイズする
    img_Other.append(img)#画像配列に画像を加える

for i in path_Japan:
    img = cv2.imread(i)#画像を読み込む
    img = cv2.resize(img,(image_size,image_size))#画像をリサイズする
    img_Asia.append(img)

for i in path_China:
  img = cv2.imread(i)#画像を読み込む
  img = cv2.resize(img,(image_size,image_size))#画像をリサイズする
  img_Asia.append(img)

for i in path_Korea:
  img = cv2.imread(i)#画像を読み込む
  img = cv2.resize(img,(image_size,image_size))#画像をリサイズする
  img_Asia.append(img)

for i in path_Taipei:
  img = cv2.imread(i)#画像を読み込む
  img = cv2.resize(img,(image_size,image_size))#画像をリサイズする
  img_Asia.append(img)

for i in path_Vietnam:
  img = cv2.imread(i)#画像を読み込む
  img = cv2.resize(img,(image_size,image_size))#画像をリサイズする
  img_Asia.append(img)

for i in path_Italia:
  img = cv2.imread(i)#画像を読み込む
  img = cv2.resize(img,(image_size,image_size))#画像をリサイズする
  img_Other.append(img)

for i in path_France:
  img = cv2.imread(i)#画像を読み込む
  img = cv2.resize(img,(image_size,image_size))#画像をリサイズする
  img_Other.append(img)

for i in path_British:
  img = cv2.imread(i)#画像を読み込む
  img = cv2.resize(img,(image_size,image_size))#画像をリサイズする
  img_Other.append(img)

for i in path_America:
  img = cv2.imread(i)#画像を読み込む
  img = cv2.resize(img,(image_size,image_size))#画像をリサイズする
  img_Other.append(img)

for i in path_Olanda:
  img = cv2.imread(i)#画像を読み込む
  img = cv2.resize(img,(image_size,image_size))#画像をリサイズする
  img_Other.append(img)

for i in path_Brazil:
  img = cv2.imread(i)#画像を読み込む
  img = cv2.resize(img,(image_size,image_size))#画像をリサイズする
  img_Other.append(img)

for i in path_Portugal:
  img = cv2.imread(i)#画像を読み込む
  img = cv2.resize(img,(image_size,image_size))#画像をリサイズする
  img_Other.append(img)

(ここからが画像分類のモデル)

#np.arrayでXに学習画像、yに正解ラベルを代入
X = np.array(img_Asia + img_Other)
#正解ラベルの作成 ラベル=データそのもの
y =  np.array([0]*len(img_Asia) + [1]*len(img_Other))
label_num = list(set(y))
#配列のラベルをシャッフルする
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):]
print(X_train.shape)
print(y_train.shape)
print(X_test.shape)
print(y_test.shape)

#正解ラベルをone-hotベクトルで求める
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)

#モデルの入力画像として用いるためのテンソールのオプション
input_tensor = Input(shape=(image_size,image_size, 3))

#転移学習のモデルとしてVGG16を使用
vgg16 = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)

#モデルの定義~活性化関数relu
#転移学習の自作モデルとして下記のコードを作成
top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(32, activation="relu"))
top_model.add(Dropout(0.5))
#top_model.add(Dense(64, activation='relu'))
#top_model.add(Dropout(0.5))
#top_model.add(Dense(512, activation='relu'))
#top_model.add(Dropout(0.5))
top_model.add(Dense(2, activation='softmax'))

#vggと自作のtop_modelを連結
model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output))

#vgg16による特徴抽出部分の重みを15層までに固定(以降に新しい層(top_model)が追加)
for layer in model.layers[:15]:
   layer.trainable = False

#訓令課程の設定
model.compile(loss='categorical_crossentropy',
             optimizer=optimizers.SGD(lr=1e-4, momentum=0.9),
             metrics=['accuracy'])

# 学習の実行
#グラフ(可視化)用コード
history = model.fit(X_train, y_train, batch_size=32, epochs=50, verbose=1, validation_data=(X_test, y_test))
#verbose に1を指定した場合は学習の進捗度合いを出力し、 0の場合は出力しません。epochsは、同じデータセットで行う学習の回数を指定します。
score = model.evaluate(X_test, y_test, batch_size=32, verbose=0)
print('validation loss:{0[0]}\nvalidation accuracy:{0[1]}'.format(score))


#acc, val_accのプロット
plt.plot(history.history["accuracy"], label="accuracy", ls="-", marker="o")
plt.plot(history.history["val_accuracy"], label="val_accuracy", ls="-", marker="x")
plt.ylabel("accuracy")
plt.xlabel("epoch")
plt.legend(loc="best")
plt.show()
#モデルを保存
model.save("my_model.h5")



⭐️ポイント①⭐️
分類数を2というバイナリ形式にして、精度をあげるよう考えました。

⭐️ポイント②⭐️
今回は画像分類を行う転移学習のモデルとしてVGG16を使用しました。

画像枚数が1300枚と大量のデータを学習する必要があったため、転移学習が最適だと判断し、VGG16を採用しています。

⭐️ポイント③⭐️
また、今回は非線形分離ができないデータだったため、活性化関数を使用して深層学習を行なっています。
使用されることが多いReLUを採用しました。


結果

それでは上記のモデルで画像を学習させた結果の発表です!

まずは以下の数値をご確認ください。

(1001, 200, 200, 3)
(1001,)
(251, 200, 200, 3)
(251,)
Downloading data from https://storage.googleapis.com/tensorflow/keras-applications/vgg16/vgg16_weights_tf_dim_ordering_tf_kernels_notop.h5
58889256/58889256 [==============================] - 0s 0us/step
WARNING:absl:`lr` is deprecated in Keras optimizer, please use `learning_rate` or use the legacy optimizer, e.g.,tf.keras.optimizers.legacy.SGD.
Epoch 1/50
32/32 [==============================] - 25s 299ms/step - loss: nan - accuracy: 0.3976 - val_loss: nan - val_accuracy: 0.3745
Epoch 2/50
32/32 [==============================] - 5s 158ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 3/50
32/32 [==============================] - 5s 158ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 4/50
32/32 [==============================] - 5s 144ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 5/50
32/32 [==============================] - 5s 147ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 6/50
32/32 [==============================] - 5s 147ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 7/50
32/32 [==============================] - 5s 146ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 8/50
32/32 [==============================] - 5s 162ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 9/50
32/32 [==============================] - 5s 148ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 10/50
32/32 [==============================] - 5s 148ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 11/50
32/32 [==============================] - 5s 152ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 12/50
32/32 [==============================] - 5s 164ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 13/50
32/32 [==============================] - 5s 164ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 14/50
32/32 [==============================] - 5s 167ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 15/50
32/32 [==============================] - 5s 155ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 16/50
32/32 [==============================] - 5s 169ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 17/50
32/32 [==============================] - 5s 171ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 18/50
32/32 [==============================] - 5s 171ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 19/50
32/32 [==============================] - 6s 175ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 20/50
32/32 [==============================] - 5s 165ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 21/50
32/32 [==============================] - 6s 176ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 22/50
32/32 [==============================] - 5s 165ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 23/50
32/32 [==============================] - 5s 172ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 24/50
32/32 [==============================] - 5s 163ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 25/50
32/32 [==============================] - 5s 170ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 26/50
32/32 [==============================] - 5s 170ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 27/50
32/32 [==============================] - 5s 170ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 28/50
32/32 [==============================] - 5s 158ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 29/50
32/32 [==============================] - 5s 171ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 30/50
32/32 [==============================] - 5s 157ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 31/50
32/32 [==============================] - 5s 169ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 32/50
32/32 [==============================] - 5s 172ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 33/50
32/32 [==============================] - 5s 171ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 34/50
32/32 [==============================] - 5s 162ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 35/50
32/32 [==============================] - 5s 171ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 36/50
32/32 [==============================] - 5s 171ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 37/50
32/32 [==============================] - 5s 164ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 38/50
32/32 [==============================] - 5s 172ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 39/50
32/32 [==============================] - 5s 163ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 40/50
32/32 [==============================] - 5s 172ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 41/50
32/32 [==============================] - 5s 172ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 42/50
32/32 [==============================] - 5s 173ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 43/50
32/32 [==============================] - 5s 160ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 44/50
32/32 [==============================] - 5s 171ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 45/50
32/32 [==============================] - 5s 171ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 46/50
32/32 [==============================] - 5s 171ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 47/50
32/32 [==============================] - 5s 161ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 48/50
32/32 [==============================] - 5s 160ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 49/50
32/32 [==============================] - 5s 161ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
Epoch 50/50
32/32 [==============================] - 5s 173ms/step - loss: nan - accuracy: 0.3816 - val_loss: nan - val_accuracy: 0.3745
validation loss:nan
validation accuracy:0.3745020031929016

image.png


はい、ご覧いただいた通り、accuracy(正答率)が0.38弱、検証データの正答率が0.37.5ということで、精度的には失敗という結果に着地しました。

ここから、精度をあげるための試行錯誤をいくつかやってみました。



試行錯誤

 1.全結合層のユニットの数を64に変更

top_model.add(Dense(64, activation='relu'))
top_model.add(Dropout(0.5))

結果は、accuracyが0.39弱、validation accuracy(検証データの正答率)が0.33と、こちらも精度的には低い結果となりました。
image.png



 2.全結合層のユニットの数を512に変更

続いて、ユニット数を512に変更して再度トライしてみました。

top_model.add(Dense(512, activation='relu'))
top_model.add(Dropout(0.5))

結果は、accuracyが0.373弱、validation accuracy(検証データの正答率)が0.33と、こちらも精度的には低い結果となりました。
image.png


ここまでで、ユニット数がモデルの精度をあげることにはならないということがわかりました。



 3.活性化関数をReLuからsigmoidに変更

続いて、活性化関数をReLuからsigmoidに変更しました。
そもそもReLuを採用していた理由としては、計算が単純で、計算コストが小さいことから一般的に使われることが多い関数という認識だったためです。

sigmoidに変更してみました。ちなみにユニット数は512のままです。

top_model.add(Dense(512, activation='sigmoid'))
top_model.add(Dropout(0.5))

image.png

びっくりしましたが、sigmoid関数に変更することで、正答率が0.55前後とReLu関数に比べて大きく上がりました。
ただしvalidationデータの正答率は大きく乱高下していることから、やはり精度が高いモデルとはいえないようです。

ここからどうすれば良いのか。。。

シグモイド関数やユニット数はこのままで、スクレイピングした画像の処理をやってみようと思います。



 4.画像の選定を再度やってみる

下は最初にスクレイピングしたポルトガル人の顔写真のフォルダ内の画像です。
ご覧の通り、日本人の顔写真がたくさん入っていたりと画像スクレイピングの精度自体に問題があると確信しました!

↓ポルトガルの顔写真フォルダ↓
image.png

13カ国分の画像の選定をしなおそう!そうすればきっと精度があがるはず!!
そう意気込み画像の選定にとりかかります。

(・・・・選定中・・・・)

さあ、いよいよ終わりました!

結果がこちら!

image.png

正答率下がるんかいっ!!と心でつぶやきました。
ただ、ReLu関数で学習させたときよりかは精度はあがったため、これも1つの結果だと自分の中では判断しました。

念のため、recallとprecisionを出してみます。
※sklearnのclassification_reportを使って表示します。

y_prob = model.predict(X_test)
y_pred = y_prob.argmax(axis=1)

from sklearn.metrics import classification_report
report = classification_report(y_test.argmax(axis=1), y_pred)
print(report)

改めて、recallとprecisionについて、復習がてらご説明できればと思います。

⭐️recall=再現率
本当に陽性であるケースの内、何%を陽性と判定できたかを示します。

つまり今回のデータみると、本当にAsiaであるデータのうち、モデルが実際にAsiaと予測できたものの割合を表します。

Asiaであろう
––––––––––––––––––––––––––
Asiaである+Asiaでない(Other)



⭐️precision=適合率
陽性であると予測した内の何%が当たっていたかを示します。
今回のデータみると、Asiaだと予測されたデータのうち、本当にAsiaであった割合を表します。

Asiaである
–––––––––––––––––––––––––––––––––––
Asiaであろう+Asiaでないだろう(Otherだろう)




それでは、表示結果をみてみましょう。

image.png

[0]データ(Asiaのデータ)のprecisionが0.41、recallが1.00となってしまっています。

これはつまり、Asiaであろうと予測されたデータのうち、本当にAsiaであった割合が41%であったことを表します。

recallが1.00ということは、実際にAsiaであるデータのうちAsiaと予測されたデータは100%であたということです。


ここまで見ると「えっすごいじゃん!」と思ってしまいましたが、[1]データ(Asiaでないデータ=Other)のprecisionとrecallを見てみるとprecisionとrecallがともに0.00となっています。

ということは、実際のOtherデータは存在しているのに、検証データに対してOtherであろう予測が正しくできていないということを表しています。

逆にいうと、全部をAsiaであろうと予測をしてしまっているということになります。

なので、このaccuracyはデータ数に偏りがあるため意味がないと判断できました。

せっかく正答率があがったのに残念です。
もう少し、がんばってみたいと思います...!



 5.データ量を減らす

本来の趣旨では世界各国の人物の顔写真からアジア系の人とそれ以外の人とを分類する目的でしたが、どうやら工夫をこらしても、精度はあがっていきませんでした...

そこで、データ量を減らし、アジア系の人の中から日本人と日本人以外で画像を分類するかたちに妥協してみました。

スクレイピングデータは変えず、画像の読み込み対象をJapan、China、Korea、Taipei、Vietnamの5カ国に絞ります。

さらに分類するカテゴリ数をAsiaとOtherからJapan、Otherに変更します。

img_Asia = [] →→→ img_Japan = []
img_Other = [] →→→ img_Other = []

結果をみてみましょう。

image.png

なんとか正答率を70%台までもっていくことができました。
validationデータの正答率については学習終盤で乱高下してしまっていますので、やはり画像によっては大きく減少してしまうのかもしれません。

precisionとrecallも見てみましょう。

image.png

先ほどの結果と違い、今回は[0]データ、[1]データともに予測がちゃんとされているようです。

[0]データ=日本人で見ると、precisionは0.72、recallは0.49とまずまずの結果となっています。

[1]データ=日本人以外で見ると、precisionは0.30、recallは0.54と適合率は低い結果となりました。


考察と反省

考察

結果としては当初予定していた13カ国の人の顔写真をアジア、それ以外でわけるモデルはVGG16を使用した画像分類モデルの場合うまくいきませんでした。

一方で以下のことを学べました。



⭐️画像のスクレイピング時に画像をうまく選定することで、多少の精度はあげることができる。

どうしてもモデルの構築に意識が向きがちでしたが、学習させるデータの質を良くすることも大事なことだということがわかりました。




⭐️分類対象が増えるほど、精度は落ちる。
今回はもともと13カ国の人の顔写真をアジアとそれ以外に分けるという分類でしたが、アジアという分け方が広かったせいか、画像選定をした後でも精度が上がりませんでした。

そこで分類対象をアジア圏の国に絞り、分類カテゴリも日本とそれ以外とすることで、精度は上がっていきました。




⭐️活性化関数はReLuが一般的だが、分類対象が増える場合はsigmoid関数も採用してみると良い。

もともとはReLuで構築していましたが、表示される学習過程を見ると全て同じ正答率が表示されていました。
分類対象が13種類あったこともあったので、sigmoid関数に変更すると学習過程の正答率もそれぞれで違う結果が出て、かつ数値面もあがっていきました。




⭐️訓練データで学習した正答率が高いからといった油断せず、precisionやrecallを見て確かめる。

活性化関数をsigmoidに変更したことで正答率があがったため、うまくいったものだと認識していましたが、アドバイザーによる指摘のもとprecisionやrecallまで見て確かめてみると、誤った学習がされた結果の数値でした。

モデルの性能評価の重要性を知ることができました。



以上が考察となります。

反省

❌ 精度がうまくいかない場合は、スクレイピング画像の確認は早くすべき

初回のコード実行で精度が悪かった時、活性化関数の見直しなどをはじめにおこなったが、まずは画像の確認をして、本当に分類できるようなデータが抽出されているかを早く確認する必要があるかと思いました。
今回はだいぶ遅れて画像確認や選定し直しをしたため、時間が無駄になってしまいました。


❌ 顔写真の中で顔部分を抽出させればよかった

後々調べたら、顔写真の中で顔部分のみを抽出できるメソッドがあることを知り、そちらを活用すれば、より精度をあげられるモデルとなり、アジア圏の国に絞った分類にせずとも、うまくいったかもしれない



大まかな反省点は以上です。
今後もこの反省を生かして様々な画像分類にトライしたいと思います。

おわりに

4月からプログラミング初心者としてPythonの学習をスタートし半年が経ちましたが、自分が書いたコードが本当に動いてくれたというプログラミングの楽しさと、いろいろ触ってみても正解に辿り着けないプログラミングの難しさ、両方を感じられる濃い半年でした。

また今回の成果物は画像分類のモデルでした。これは正直時間が足りてない中での苦肉の策としてのものでした。

これまで学んだことの中には、線形回帰のモデルや自然言語処理、時系列解析など様々なモデルがあったため、今回の成果物で終わりではなく、これからがスタートとして実際の仕事にも活かしていきたいですし、自分でオリジナルのアプリなども作ってみたいと思います。

今後も継続としてPythonの学習は続けていきたいと思います!

ご一読いただきありがとうございました!

0
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
0
0