見出し画像

【VGG16を用いた画像認識】サッカー日本代表選手の画像を認識する

昨今、AI(Artificial Intelligence: 人工知能)による革新的な技術が様々な分野
で実装されており、私たちの身近でも以前では考えられないようなサービスが利用可能になってきております。

筆者が身を置いている製薬業界においてもそれは例外ではなく、例えば「AI創薬」という従来とは違うアプローチで新薬を生み出そうとする研究が各製薬企業で進められております。

「AI創薬」とは、端的に「機械学習等のAI技術を活用して新たな薬をこの世に生み出す研究活動(創薬研究)」と言えます。筆者はこれまで、製薬企業の実験化学者として創薬研究に携わってきましたが、門外漢でありながらも昨今のAI創薬ブームには以前から興味を引き付けられておりました。その主な理由としては以下が挙げられます。

【AI創薬に興味がわいた理由】
(1) AIで本当に創薬ができるのか実験化学者としてはやや半信半疑
(2) 一方AI技術には興味あり(創薬の学会でもAI絡みの演題が増えてきた)
(3) AI創薬に関する知識を習得すればセカンドキャリアに活かせるかも?

上記理由の詳細について今回は割愛しますが、(1)については特に否定的な意味合いではなく、純粋にAI創薬の真偽を自らの目で確かめてみたいという思いが強くありました。そういった動機もあり、まずはAIや機械学習の初歩的な知識から学んでみようとこの度一念発起するに至りました。

関連する知識を習得する上で、未経験のプログラミングの学習が大きなハードルでした。独学は厳しそう…と思うに至り、AIやプログラミングに関する学習機会を提供するオンラインスクールを活用しようと様々調べた結果、株式会社Aidemyの「データ分析講座」を選択し、約3か月に渡り受講しました(スクール選びにはこちらのブログを参考にしました)。

約3か月にわたる講座の集大成として、「学習成果の実践」があります。自らが選んだテーマで機械学習を実装し、ブログ形式で手順、結果、考察をレポートするというものです。

今回その集大成の場として、試行錯誤と苦労の結果を以下まとめていきたいと思います。

■ テーマ選び


成果物を作成するにあたり、タイトルにもあるように「サッカー日本代表の選手の画像を認識する」をテーマとして選択しました。

「長々と語ってたくせに全然創薬と関係ないやん…」

さっそくそう思われてしまっても仕方ないかもしれませんが、AI創薬は大手製薬企業各社が一大プロジェクトとしてしのぎを削っているほどの、competitiveなテーマでもあります。ドシロートの筆者がいきなり取り組むにはハードルがあまりに高いと考え、まずは身近でとっつきやすいテーマから手を付けることにしました。

そんな折、本ブログを執筆時点はちょうど2022年FIFAワールドカップ直前であり、にわかなサッカーファンでもある筆者にはもってこいのタイミングと重なり、関連するテーマとしてこれを選択することしました。

本テーマの課題と解決案は、イメージとしては以下になります。

【課題】
ワールドカップの時期に発生するサッカーにわかファンは、日本代表の希少なゴールシーンを見た時、盛り上がりながらも「ところであの選手誰?」となる場合がある

【解決案】
ゴールした選手の写真を撮ることで、〇〇%の確率でその選手の名前を当ててくれるモデルを提供する

「ゴールで盛り上がっているときに写真をわざわざ用意するん?」
「隣の人に聞いた方が早いんやないか?」

そんな疑問がわくテーマでもありますが、画像認識は機械学習の基本の一つであることを講座を通して学んだので、基本に立ち返るべく自信をもって実装を目指していきます。

■ 事前準備


<目的と方法>

今回の取り組みの具体的な目的および実装の方法は以下になります。

【目的】
筆者が独断と偏見と好みで選んだ、ワールドカップ本大会でゴールが期待できる or 期待したい選手を3人ピックアップし、それぞれの選手の画像を自動で認識する

【方法】
VGG16を用いた3クラス分類

【対象選手】
・久保建英選手(スペイン/レアル・ソシエダ)
・鎌田大地選手(ドイツ/アイントラハト・フランクフルト)
・南野拓実選手(フランス/ASモナコ)

<使用環境>

以下の環境下で行いました。

・Google Colaboratory
・Google Drive

Google Colaboratory (Colab)の使い方については様々なサイトで紹介されているので、今回は割愛します。

Google Driveは、学習させる画像(train data)、検証用の画像(test data)、実際の予想に使う未知の画像(sample data)、および学習したモデルの保存のために使用します。

保存データをわかりやすく分類するためGoogle Drive内は以下のようなフォルダ構成としました(事前準備からモデルの実装まで、主にこちらのブログを参考にさせていただきました)。

【Google Drive内のフォルダ構成】
Google Colab
-----img
----------deep_learning #学習用の画像を保存するフォルダ
---------------鎌田大地
---------------久保建英
---------------南野拓実

-----sample #実際の予想に使う未知の画像を保存するフォルダ

■ GPU環境の確認とドライブの準備


事前準備が完了したらここからが本番です。画像分類するためのモデルの実装に向けpythonでコード書いていきます。Aidemyの講座を断片的に復習したり、ネット上の情報を様々見ながら試行錯誤しました。

今回は成果物のレポートだけではなく、備忘録も兼ねているので、わかる範囲でコードの意味を説明していきたいと思います。

<GPU環境の確認>

まず下記のコードを入力します。

import tensorflow as tf
tf.test.gpu_device_name()

今回、Google Colab上で使用可能なGPU環境下にてモデルの実装を行います。GPUがちゃんとアサインされていると以下が出力されます(GPUの設定の方法はこちら)。

'/device:GPU:0'

<Google Driveを紐づける>

次に、「事前準備」で整えたGoogle Driveを今回使用するColabと紐づけます。

#Google Driveと紐づける
from google.colab import drive
drive.mount('/content/drive')

上記を入力すると、Googleアカウントへのサインイン等の過程を経て以下のように出力され、Google Driveとの紐づけが完了します。

Drive already mounted at /content/drive; to attempt to forcibly remount, call drive.mount("/content/drive", force_remount=True).

次に、以下のコードによりGoogle Drive内のカレントディレクトリを指定します。

#カレントディレクトリに移動
%cd "/content/drive/My Drive/Google Colab"

今後画像の読み込みや学習モデルの保存を行いますが、この操作によりGoogle Drive内にある「Google Colabフォルダ」より下層でそれらを行うという指定が事前になされると理解しました。実際にディレクトリを指定する場合は、Google Colabフォルダより下層のディレクトリだけ記述すればよくなります。出力内容は以下です。

/content/drive/My Drive/Google Colab

■ 画像の収集と処理


<icrawlerを用いた画像収集>

次に、学習用の画像(train data)、検証用の画像(test data)および実際の予想に使う未知の画像(sample data)を収集していきます。

画像を収集する方法は、大きく分けて以下3つの方法があります。

(1) 手動でキーワード検索し、ヒットした画像を手動でダウンロードをする
(2) 既存のデータセットを用いる
(3) プログラミングにより目的の画像を自動でダウンロードする

今回はせっかくなので、(3)の方法にチャレンジます。
方法としては、Pythonライブラリに搭載されているicrawlerを用います。これにより、比較的簡単なコードで目的の画像をウェブサイトから検索、ダウンロードすることができます(icrawlerについてはこちら)。

icrawlerをColab上で使用可能にするために、以下のコードを実行することでicrawlerパッケージをインストールします。これにより、icrawler使用の準備が整います。

!pip install icrawler

ここから、いよいよ画像を収集していきます。
以下のコードにより、久保選手、南野選手、鎌田選手の画像をそれぞれ最大250個ダウンロードします。

from icrawler.builtin import BingImageCrawler

# 久保建英選手の画像を250枚取得
crawler = BingImageCrawler(storage={"root_dir": "img/deep_learning/久保建英"})
crawler.crawl(keyword="久保建英 takefusa kubo", max_num=250)

# 南野拓実選手の画像を250枚取得
crawler = BingImageCrawler(storage={"root_dir": "img/deep_learning/堂安律"})
crawler.crawl(keyword="南野拓実 takumi minamino", max_num=250)

# 鎌田大地選手の画像を250枚取得
crawler = BingImageCrawler(storage={"root_dir": "img/deep_learning/鎌田大地"})
crawler.crawl(keyword="鎌田大地 daichi kamata", max_num=250)

BingImageCrawlerの引数「img/deep_learning/〇〇〇〇」は、先ほど指定したカレントデレクトリより下層のフォルダ内にヒットした画像を保存するという意味です。

上記を実行することにより、検索でヒットした画像が指定したフォルダに自動で保存されました。以下は「久保建英」のフォルダ内の様子です。

久保建英フォルダ内

<画像の処理①:手作業による仕分け>

収集した画像は、学習用の画像(train data)、検証用の画像(test data)および実際の予想に使う未知の画像(sample data)として使用しますが、適切な学習モデルを構築するためには、このままの状態では使用できません。

というのも、結構な割合で以下のような「適切でない画像(ノイズ)」が紛れ込んでいるからです。

(1) 本人の顔が明確に写っていない画像
(2) 本人以外の人物も写っている画像
(3) 重複した画像
(4) そもそも別人の画像
(5) 全く関係のない画像

まずは手作業で今回の用途にそぐわないノイズを削除していきます。

ここまでの作業で整理した画像から、各選手それぞれ10枚を後に実際の予想に使う未知の画像(sample data)として抜き取り、「sample」フォルダの中に移しておきます。

sampleフォルダ内

<画像の処理②:画像の水増し>

ここまでで収集・整理した画像は、後述の理由と手動による削除により、思ったほど多くはありません。一方で、機械学習において高いパフォーマンスを発揮するためには、膨大な量のラベル付き画像データが求められます。

しかしながら、我々が自前で用意できるデータセットのボリュームには限界があります。そこで活用されるのが、Data Augmentation(データ拡張)です。データ拡張では、用意した学習用の画像データに対して「変換」を行うことで元データをベースに画像データ数の水増しをします。

今回、画像の水増しとして「画像の反転」と「画像の白黒化」を行いました。

まずは、以下のコードにて「画像の反転」を実行します。

import cv2
import os
import time
import datetime

#久保選手の反転画像
tm_start = time.time()            #処理時間計測用
dt_now = datetime.datetime.now()  # 現在日時
dt_date_str = dt_now.strftime('%Y/%m/%d %H:%M')
print(dt_date_str)

path = 'img/deep_learning/久保建英'

files = os.listdir(path)

files_file =  [f for f in files if os.path.isfile(os.path.join(path, f))]

for y_pic in files_file:

  img = cv2.imread(path + "/" + y_pic)

  y = cv2.flip(img,1)

  cv2.imwrite(path + "/y_" + y_pic,y)

tm_end = time.time()
print('処理完了')
print('------------------------------------')
total = tm_end - tm_start
total_str = f'トータル時間: {total:.1f}s({total/60:.2f}min)'
print(total_str)


#南野選手の反転画像
tm_start = time.time()            #処理時間計測用
dt_now = datetime.datetime.now()  # 現在日時
dt_date_str = dt_now.strftime('%Y/%m/%d %H:%M')
print(dt_date_str)

path = 'img/deep_learning/南野拓実'

files = os.listdir(path)

files_file =  [f for f in files if os.path.isfile(os.path.join(path, f))]

for y_pic in files_file:

  img = cv2.imread(path + "/" + y_pic)

  y = cv2.flip(img,1)

  cv2.imwrite(path + "/y_" + y_pic,y)

tm_end = time.time()
print('処理完了')
print('------------------------------------')
total = tm_end - tm_start
total_str = f'トータル時間: {total:.1f}s({total/60:.2f}min)'
print(total_str)

#鎌田選手の反転画像
tm_start = time.time()            #処理時間計測用
dt_now = datetime.datetime.now()  # 現在日時
dt_date_str = dt_now.strftime('%Y/%m/%d %H:%M')
print(dt_date_str)

path = 'img/deep_learning/鎌田大地'

files = os.listdir(path)

files_file =  [f for f in files if os.path.isfile(os.path.join(path, f))]

for y_pic in files_file:

  img = cv2.imread(path + "/" + y_pic)

  y = cv2.flip(img,1)

  cv2.imwrite(path + "/y_" + y_pic,y)

tm_end = time.time()
print('処理完了')
print('------------------------------------')
total = tm_end - tm_start
total_str = f'トータル時間: {total:.1f}s({total/60:.2f}min)'
print(total_str)

上記を実行することで、先ほど画像を保存したフォルダ内に元データ数と同数の反転画像が選手ごとに保存されます。

久保選手の反転画像

次に、以下のコードにより「画像の白黒化」を行います。

#久保選手の白黒画像を保存する処理
tm_start = time.time()            #処理時間計測用
dt_now = datetime.datetime.now()  # 現在日時
dt_date_str = dt_now.strftime('%Y/%m/%d %H:%M')
print(dt_date_str)

path ='img/deep_learning/久保建英'

files = os.listdir(path)

files_file =  [f for f in files if os.path.isfile(os.path.join(path, f))]

print(files_file)


for y_pic in files_file:

  img = cv2.imread(path + '/' + y_pic)

  img_gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)

  cv2.imwrite(path + '/gray_' + y_pic,img_gray)

tm_end = time.time()
print('処理完了')
print('------------------------------------')
total = tm_end - tm_start
total_str = f'トータル時間: {total:.1f}s({total/60:.2f}min)'
print(total_str)

#南野選手の白黒画像を保存する処理
tm_start = time.time()            #処理時間計測用
dt_now = datetime.datetime.now()  # 現在日時
dt_date_str = dt_now.strftime('%Y/%m/%d %H:%M')
print(dt_date_str)

path ='img/deep_learning/南野拓実'

files = os.listdir(path)

files_file =  [f for f in files if os.path.isfile(os.path.join(path, f))]

print(files_file)


for y_pic in files_file:

  img = cv2.imread(path + '/' + y_pic)

  img_gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)

  cv2.imwrite(path + '/gray_' + y_pic,img_gray)

tm_end = time.time()
print('処理完了')
print('------------------------------------')
total = tm_end - tm_start
total_str = f'トータル時間: {total:.1f}s({total/60:.2f}min)'
print(total_str)

#鎌田選手の白黒画像を保存する処理
tm_start = time.time()            #処理時間計測用
dt_now = datetime.datetime.now()  # 現在日時
dt_date_str = dt_now.strftime('%Y/%m/%d %H:%M')
print(dt_date_str)

path ='img/deep_learning/鎌田大地'

files = os.listdir(path)

files_file =  [f for f in files if os.path.isfile(os.path.join(path, f))]

print(files_file)


for y_pic in files_file:

  img = cv2.imread(path + '/' + y_pic)

  img_gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)

  cv2.imwrite(path + '/gray_' + y_pic,img_gray)

tm_end = time.time()
print('処理完了')
print('------------------------------------')
total = tm_end - tm_start
total_str = f'トータル時間: {total:.1f}s({total/60:.2f}min)'
print(total_str)

今回の場合、「画像の白黒化」は「元画像+反転画像」に対し処理が行われます。

そのため、例えば元画像が100枚あった場合、反転画像は100枚生成しているため、白黒化した画像は元画像と反転画像に対応し200枚出力され、トータルでは400枚の画像が得られる計算になります。

久保選手の白黒画像

以上のような作業により、元々のデータ数の4倍の数まで画像データの水増しを行うことができました。

<画像の処理③:画像サイズの統一化、配列化、分割>

ここから、学習用画像データの最終的な処理をしていきます。以下に記載されたコードを実行します。

import os
import cv2
import numpy as np
import glob as glob
from sklearn.model_selection import train_test_split
from keras.utils import np_utils


#フォルダをクラス名にする
path = "img/deep_learning"
folders = os.listdir(path)

#フォルダ名を抽出
classes = [f for f in folders if os.path.isdir(os.path.join(path, f))]
n_classes = len(classes)


#画像とラベルの格納
X = []
Y = []


#画像を読み込みリサイズする
for label,class_name in enumerate(classes):
  files = glob.glob(path + "/" +  class_name + "/*.jpg")
  for file in files:
    img = cv2.imread(file)
    img = cv2.resize(img,dsize=(224,224))
    X.append(img)
    Y.append(label)

#精度を上げるために正規化
X = np.array(X)
X = X.astype('float32')
X /= 255.0

#ラベルの変換
Y = np.array(Y)
Y = np_utils.to_categorical(Y,n_classes)
Y[:5]

#train dataとtest dataに分ける(train data 8割、test data 2割)
X_train,X_test,Y_train,Y_test = train_test_split(X,Y,test_size=0.2)
#train data(8割)
print(X_train.shape)
#test data(2割)
print(X_test.shape)
#train data(8割)
print(Y_train.shape)
#test data(2割)
print(Y_test.shape)

ここで実行されていることを簡潔にまとめると以下になります。

(1) 画像サイズを224×224に統一化
(2) 画像データはXに格納し配列化
(3) ラベル(正解、つまり選手名)の情報はYに格納し配列化
(4) 全画像データのうち8割をtrain dataに、2割をtest dataに分割

(4)で示したように、今回は用意した画像のうちの8割を学習用のデータ(train data)、2割を学習結果の精度を確認するためのデータ(train data)として使用します。

上記コードにより出力された情報は以下となり、しっかりと分割でできていることがわかります。

(1065, 224, 224, 3) #train dataが1065ある
(267, 224, 224, 3) #test dataが267ある
(1065, 3) #1065のtrain dataは3クラス(3選手分)ある
(267, 3) #267のtest dataは3クラス(3選手分)ある

■ 学習モデルの実装


<VGG16による画像認識>

用意した画像の前処理が完了しましたので、ここから画像を認識するためのモデルを実装していきます。

画像認識は、VGG16という学習済みのモデルを利用することで行います。VGG16とは、大規模な画像データセットですでに学習済みのモデルであり、畳み込みニューラルネットワーク(Convolutional Neural Network: CNN)の一つに分類されています(VGG16の詳細はこちら)。

VGG16は、本来は出力層(学習済みのクラス)が1000あり、入力された画像が1000のクラスのうちどれに分類されるかを予想します。今回はVGG16を実装するコードを改変し、筆者が用意した3クラスを分類できるようにしていきます。

最終的なコードは以下のようになりました。

from keras.applications.vgg16 import VGG16
from keras.models import Sequential
from keras.models import model_from_json
from keras.models import Model
from keras.layers import Input, Activation, Dense, Flatten, Dropout
from tensorflow.keras.optimizers import Adam


#vgg16
input_tensor = Input(shape=(224,224,3))
#最後の1000の層を省く
base_model = VGG16(weights='imagenet', input_tensor=input_tensor,include_top=False)


#後付けで入れたい層の作成
top_model = Sequential()
top_model.add(Flatten(input_shape=base_model.output_shape[1:]))
top_model.add(Dropout(0.5))
top_model.add(Dense(256, activation='relu'))
top_model.add(Dense(n_classes, activation='softmax'))


#結合
model = Model(inputs=base_model.input, outputs=top_model(base_model.output))


#学習させない層
for layer in model.layers[:19]:
  layer.trainable = False

print('# layers=', len(model.layers))

model.compile(loss='categorical_crossentropy',optimizer='adam',metrics=['accuracy'])

model.summary()

※コメント
ここ以降が今回の成果物作成の一番の山場でした。後述する学習結果のaccuracyが全く上がらず、かなり四苦八苦しました…。

最終的にはweb上の様々な情報やAidemyの講師の先生方からのアドバイスを
もとに何とか適正化しました。残念ながら、筆者の力不足もありコードを的確に説明できるには至っておりません。拙い内容にはなりますが、わかる範囲で説明していきます。

上記のコードを実行することで、今回構築したモデルの構造が以下のように示されました。

# layers= 20
Model: "model_6"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_7 (InputLayer)        [(None, 224, 224, 3)]     0         
                                                                 
 block1_conv1 (Conv2D)       (None, 224, 224, 64)      1792      
                                                                 
 block1_conv2 (Conv2D)       (None, 224, 224, 64)      36928     
                                                                 
 block1_pool (MaxPooling2D)  (None, 112, 112, 64)      0         
                                                                 
 block2_conv1 (Conv2D)       (None, 112, 112, 128)     73856     
                                                                 
 block2_conv2 (Conv2D)       (None, 112, 112, 128)     147584    
                                                                 
 block2_pool (MaxPooling2D)  (None, 56, 56, 128)       0         
                                                                 
 block3_conv1 (Conv2D)       (None, 56, 56, 256)       295168    
                                                                 
 block3_conv2 (Conv2D)       (None, 56, 56, 256)       590080    
                                                                 
 block3_conv3 (Conv2D)       (None, 56, 56, 256)       590080    
                                                                 
 block3_pool (MaxPooling2D)  (None, 28, 28, 256)       0         
                                                                 
 block4_conv1 (Conv2D)       (None, 28, 28, 512)       1180160   
                                                                 
 block4_conv2 (Conv2D)       (None, 28, 28, 512)       2359808   
                                                                 
 block4_conv3 (Conv2D)       (None, 28, 28, 512)       2359808   
                                                                 
 block4_pool (MaxPooling2D)  (None, 14, 14, 512)       0         
                                                                 
 block5_conv1 (Conv2D)       (None, 14, 14, 512)       2359808   
                                                                 
 block5_conv2 (Conv2D)       (None, 14, 14, 512)       2359808   
                                                                 
 block5_conv3 (Conv2D)       (None, 14, 14, 512)       2359808   
                                                                 
 block5_pool (MaxPooling2D)  (None, 7, 7, 512)         0         
                                                                 
 sequential_6 (Sequential)   (None, 3)                 6423555   
                                                                 
=================================================================
Total params: 21,138,243
Trainable params: 6,423,555
Non-trainable params: 14,714,688
_________________________________________________________________

<train dataを用いた学習>

以上のような構成のモデルを用いて、学習を実行していきます。
以下のコードにより、用意したtrain dataを用いてtrain dataの学習、その後test dataを用いて学習モデルの精度(accuracy)を確認します。

#train dataで学習
model.fit(X_train, Y_train, epochs=70, batch_size=16)

#test dataで精度確認
score = model.evaluate(X_test, Y_test, batch_size=16)

以下が学習結果になります。

Epoch 1/70
67/67 [==============================] - 6s 80ms/step - loss: 1.4726 - accuracy: 0.4685
Epoch 2/70
67/67 [==============================] - 5s 80ms/step - loss: 0.5861 - accuracy: 0.7700
Epoch 3/70
67/67 [==============================] - 5s 81ms/step - loss: 0.3047 - accuracy: 0.9080
Epoch 4/70
67/67 [==============================] - 5s 81ms/step - loss: 0.1887 - accuracy: 0.9531
Epoch 5/70
67/67 [==============================] - 5s 82ms/step - loss: 0.1042 - accuracy: 0.9812
Epoch 6/70
67/67 [==============================] - 6s 82ms/step - loss: 0.0693 - accuracy: 0.9906
Epoch 7/70
67/67 [==============================] - 5s 81ms/step - loss: 0.0532 - accuracy: 0.9906
Epoch 8/70
67/67 [==============================] - 5s 82ms/step - loss: 0.0497 - accuracy: 0.9934
Epoch 9/70
67/67 [==============================] - 5s 82ms/step - loss: 0.0293 - accuracy: 0.9962
Epoch 10/70
67/67 [==============================] - 5s 82ms/step - loss: 0.0222 - accuracy: 0.9972
Epoch 11/70
67/67 [==============================] - 6s 83ms/step - loss: 0.0157 - accuracy: 0.9991
Epoch 12/70
67/67 [==============================] - 6s 84ms/step - loss: 0.0236 - accuracy: 0.9962
Epoch 13/70
67/67 [==============================] - 6s 86ms/step - loss: 0.0186 - accuracy: 0.9981
Epoch 14/70
67/67 [==============================] - 6s 84ms/step - loss: 0.0198 - accuracy: 0.9962
Epoch 15/70
67/67 [==============================] - 6s 84ms/step - loss: 0.0230 - accuracy: 0.9934
Epoch 16/70
67/67 [==============================] - 5s 82ms/step - loss: 0.0084 - accuracy: 1.0000
Epoch 17/70
67/67 [==============================] - 6s 82ms/step - loss: 0.0070 - accuracy: 1.0000
Epoch 18/70
67/67 [==============================] - 6s 82ms/step - loss: 0.0107 - accuracy: 0.9962
Epoch 19/70
67/67 [==============================] - 5s 82ms/step - loss: 0.0155 - accuracy: 0.9972
Epoch 20/70
67/67 [==============================] - 5s 82ms/step - loss: 0.0110 - accuracy: 0.9972
Epoch 21/70
67/67 [==============================] - 5s 82ms/step - loss: 0.0103 - accuracy: 0.9981
Epoch 22/70
67/67 [==============================] - 5s 82ms/step - loss: 0.0142 - accuracy: 0.9944
Epoch 23/70
67/67 [==============================] - 5s 82ms/step - loss: 0.0290 - accuracy: 0.9878
Epoch 24/70
67/67 [==============================] - 6s 82ms/step - loss: 0.0646 - accuracy: 0.9793
Epoch 25/70
67/67 [==============================] - 6s 84ms/step - loss: 0.1259 - accuracy: 0.9681
Epoch 26/70
67/67 [==============================] - 6s 82ms/step - loss: 0.0492 - accuracy: 0.9812
Epoch 27/70
67/67 [==============================] - 5s 82ms/step - loss: 0.0290 - accuracy: 0.9897
Epoch 28/70
67/67 [==============================] - 6s 82ms/step - loss: 0.0515 - accuracy: 0.9831
Epoch 29/70
67/67 [==============================] - 6s 83ms/step - loss: 0.0525 - accuracy: 0.9850
Epoch 30/70
67/67 [==============================] - 6s 82ms/step - loss: 0.0349 - accuracy: 0.9859
Epoch 31/70
67/67 [==============================] - 6s 82ms/step - loss: 0.0466 - accuracy: 0.9822
Epoch 32/70
67/67 [==============================] - 6s 82ms/step - loss: 0.0448 - accuracy: 0.9840
Epoch 33/70
67/67 [==============================] - 5s 82ms/step - loss: 0.1123 - accuracy: 0.9587
Epoch 34/70
67/67 [==============================] - 5s 82ms/step - loss: 0.0241 - accuracy: 0.9915
Epoch 35/70
67/67 [==============================] - 6s 82ms/step - loss: 0.0198 - accuracy: 0.9925
Epoch 36/70
67/67 [==============================] - 6s 82ms/step - loss: 0.0118 - accuracy: 0.9972
Epoch 37/70
67/67 [==============================] - 6s 83ms/step - loss: 0.0713 - accuracy: 0.9718
Epoch 38/70
67/67 [==============================] - 6s 85ms/step - loss: 0.0322 - accuracy: 0.9878
Epoch 39/70
67/67 [==============================] - 6s 83ms/step - loss: 0.0199 - accuracy: 0.9944
Epoch 40/70
67/67 [==============================] - 6s 83ms/step - loss: 0.0156 - accuracy: 0.9944
Epoch 41/70
67/67 [==============================] - 6s 83ms/step - loss: 0.0137 - accuracy: 0.9953
Epoch 42/70
67/67 [==============================] - 6s 83ms/step - loss: 0.0143 - accuracy: 0.9962
Epoch 43/70
67/67 [==============================] - 6s 83ms/step - loss: 0.0439 - accuracy: 0.9850
Epoch 44/70
67/67 [==============================] - 6s 83ms/step - loss: 0.0077 - accuracy: 0.9962
Epoch 45/70
67/67 [==============================] - 6s 83ms/step - loss: 0.0380 - accuracy: 0.9869
Epoch 46/70
67/67 [==============================] - 6s 83ms/step - loss: 0.0232 - accuracy: 0.9925
Epoch 47/70
67/67 [==============================] - 6s 83ms/step - loss: 0.0137 - accuracy: 0.9934
Epoch 48/70
67/67 [==============================] - 6s 83ms/step - loss: 0.0075 - accuracy: 0.9962
Epoch 49/70
67/67 [==============================] - 6s 83ms/step - loss: 0.0142 - accuracy: 0.9953
Epoch 50/70
67/67 [==============================] - 6s 85ms/step - loss: 0.0133 - accuracy: 0.9934
Epoch 51/70
67/67 [==============================] - 6s 83ms/step - loss: 0.0107 - accuracy: 0.9962
Epoch 52/70
67/67 [==============================] - 6s 83ms/step - loss: 0.0149 - accuracy: 0.9953
Epoch 53/70
67/67 [==============================] - 6s 83ms/step - loss: 0.0137 - accuracy: 0.9962
Epoch 54/70
67/67 [==============================] - 6s 83ms/step - loss: 0.0102 - accuracy: 0.9953
Epoch 55/70
67/67 [==============================] - 6s 83ms/step - loss: 0.0032 - accuracy: 0.9991
Epoch 56/70
67/67 [==============================] - 6s 83ms/step - loss: 0.0130 - accuracy: 0.9944
Epoch 57/70
67/67 [==============================] - 6s 83ms/step - loss: 0.0046 - accuracy: 0.9991
Epoch 58/70
67/67 [==============================] - 6s 83ms/step - loss: 0.0087 - accuracy: 0.9981
Epoch 59/70
67/67 [==============================] - 6s 83ms/step - loss: 0.0051 - accuracy: 0.9981
Epoch 60/70
67/67 [==============================] - 6s 83ms/step - loss: 0.0163 - accuracy: 0.9953
Epoch 61/70
67/67 [==============================] - 6s 83ms/step - loss: 0.0225 - accuracy: 0.9944
Epoch 62/70
67/67 [==============================] - 6s 84ms/step - loss: 0.0323 - accuracy: 0.9878
Epoch 63/70
67/67 [==============================] - 6s 84ms/step - loss: 0.0271 - accuracy: 0.9915
Epoch 64/70
67/67 [==============================] - 6s 83ms/step - loss: 0.0348 - accuracy: 0.9869
Epoch 65/70
67/67 [==============================] - 6s 84ms/step - loss: 0.0034 - accuracy: 0.9981
Epoch 66/70
67/67 [==============================] - 6s 84ms/step - loss: 0.0085 - accuracy: 0.9962
Epoch 67/70
67/67 [==============================] - 6s 83ms/step - loss: 0.0564 - accuracy: 0.9850
Epoch 68/70
67/67 [==============================] - 6s 83ms/step - loss: 0.0264 - accuracy: 0.9878
Epoch 69/70
67/67 [==============================] - 6s 83ms/step - loss: 0.0074 - accuracy: 0.9972
Epoch 70/70
67/67 [==============================] - 6s 83ms/step - loss: 0.0101 - accuracy: 0.9962
17/17 [==============================] - 2s 82ms/step - loss: 0.3235 - accuracy: 0.9064

Epoch数(train dataの学習サイクルの数)を重ねるごとにaccuracyが上昇し、ある程度のEpoch数からaccuracyが頭打ちになっていることがわかります。

最後の行では、学習させたモデルを用いてtest dataを予測させた結果が示されております。この場合、accuracyは0.9064となり、高い精度で予測ができる学習モデルが実装できたことがわかります。

このように実装できた学習モデルをファイルとして保存します。以下のコードを実行することで、Google Drive内に学習モデルが保存され、以後sample dataを予測する際に呼び出して使用します。

import pickle
#クラス名の保存
pickle.dump(classes, open('classes.sav', 'wb'))
#モデルの保存
model.save('cnn.h5')

■ sample detaの予測とその結果


では最終段階として、用意しておいた30枚の画像(久保選手、鎌田選手、南野選手それぞれ10枚ずつ)が、今回構築した学習モデルで的確に予測できるか見てみましょう。

以下のコードにより、sampleフォルダに格納されていた画像を前処理したうえで、先ほど構築した学習モデルにかけることで予測が実行されます。

from keras.models import load_model
import pickle
import cv2
import glob

#モデルとクラス名の読み込み
model = load_model('cnn.h5')
classes = pickle.load(open('classes.sav', 'rb'))

#sample dataの前処理
files = glob.glob('sample/*')
box = []
for file in files:
  img = cv2.imread(file)
  img = cv2.resize(img,dsize=(224,224))
  img = img.astype('float32')
  img /= 255.0
  img = img[None, ...]
  result = model.predict(img)

  #確率が一番大きいクラス
  pred = result.argmax()

  img = cv2.imread(file)
  cv2.imwrite('output/' + str(classes[pred]) + '/' + file,img)

  print(result)
  print(str(file) + 'は' + str(classes[pred]))

少し見にくいですが、以下が結果になります。

1/1 [==============================] - 0s 122ms/step
[[1.2391562e-02 9.8757035e-01 3.8120364e-05]]
sample/kubo1.jpgは鎌田大地
1/1 [==============================] - 0s 18ms/step
[[9.8888725e-01 1.0521170e-02 5.9155311e-04]]
sample/kubo2.jpgは久保建英
1/1 [==============================] - 0s 16ms/step
[[9.9266410e-01 1.3047419e-05 7.3229261e-03]]
sample/kubo3.jpgは久保建英
1/1 [==============================] - 0s 18ms/step
[[1.7840461e-01 8.2157898e-01 1.6461534e-05]]
sample/kubo4.jpgは鎌田大地
1/1 [==============================] - 0s 16ms/step
[[9.9998569e-01 1.2017159e-05 2.2981960e-06]]
sample/kubo5.jpgは久保建英
1/1 [==============================] - 0s 16ms/step
[[0.71086586 0.01276    0.27637416]]
sample/kubo6.jpgは久保建英
1/1 [==============================] - 0s 15ms/step
[[9.9937743e-01 6.2262738e-04 3.6395683e-08]]
sample/kubo7.jpgは久保建英
1/1 [==============================] - 0s 16ms/step
[[9.980621e-01 1.923380e-03 1.452592e-05]]
sample/kubo8.jpgは久保建英
1/1 [==============================] - 0s 17ms/step
[[9.9469924e-01 5.2196928e-03 8.1152306e-05]]
sample/kubo9.jpgは久保建英
1/1 [==============================] - 0s 17ms/step
[[0.05092108 0.01130175 0.93777716]]
sample/kubo10.jpgは南野拓実
1/1 [==============================] - 0s 17ms/step
[[1.8719113e-03 9.9812812e-01 1.2268883e-10]]
sample/kamata1.jpgは鎌田大地
1/1 [==============================] - 0s 17ms/step
[[0.00117886 0.9908172  0.00800395]]
sample/kamata2.jpgは鎌田大地
1/1 [==============================] - 0s 16ms/step
[[0.779199   0.21924734 0.00155368]]
sample/kamata3.jpgは久保建英
1/1 [==============================] - 0s 16ms/step
[[9.9994457e-01 5.5440953e-05 3.4997860e-09]]
sample/kamata4.jpgは久保建英
1/1 [==============================] - 0s 19ms/step
[[2.4061189e-04 9.9975938e-01 8.7493546e-10]]
sample/kamata5.jpgは鎌田大地
1/1 [==============================] - 0s 19ms/step
[[1.0353546e-08 9.9999988e-01 1.1301980e-07]]
sample/kamata6.jpgは鎌田大地
1/1 [==============================] - 0s 19ms/step
[[8.505556e-04 9.985684e-01 5.809788e-04]]
sample/kamata7.jpgは鎌田大地
1/1 [==============================] - 0s 17ms/step
[[2.3501589e-06 9.9999762e-01 8.2509919e-12]]
sample/kamata8.jpgは鎌田大地
1/1 [==============================] - 0s 19ms/step
[[1.3903771e-03 9.9860954e-01 1.2026290e-08]]
sample/kamata9.jpgは鎌田大地
1/1 [==============================] - 0s 20ms/step
[[9.9998224e-01 1.8530093e-06 1.5841038e-05]]
sample/kamata10.jpgは久保建英
1/1 [==============================] - 0s 17ms/step
[[9.7631756e-04 6.9136827e-06 9.9901676e-01]]
sample/minamino1.jpgは南野拓実
1/1 [==============================] - 0s 19ms/step
[[7.8878444e-01 2.0779559e-04 2.1100777e-01]]
sample/minamino2.jpgは久保建英
1/1 [==============================] - 0s 25ms/step
[[0.2196649  0.66050076 0.11983429]]
sample/minamino3.jpgは鎌田大地
1/1 [==============================] - 0s 16ms/step
[[8.7713438e-01 2.4317138e-04 1.2262238e-01]]
sample/minamino4.jpgは久保建英
1/1 [==============================] - 0s 18ms/step
[[0.28230467 0.25431585 0.4633795 ]]
sample/minamino5.jpgは南野拓実
1/1 [==============================] - 0s 16ms/step
[[5.4143472e-03 9.2360671e-05 9.9449337e-01]]
sample/minamino6.jpgは南野拓実
1/1 [==============================] - 0s 17ms/step
[[2.3811184e-01 8.0463624e-06 7.6188010e-01]]
sample/minamino7.jpgは南野拓実
1/1 [==============================] - 0s 16ms/step
[[8.758480e-02 9.124098e-01 5.475617e-06]]
sample/minamino8.jpgは鎌田大地
1/1 [==============================] - 0s 19ms/step
[[4.436459e-07 2.336208e-08 9.999995e-01]]
sample/minamino9.jpgは南野拓実
1/1 [==============================] - 0s 17ms/step
[[0.00194647 0.6634822  0.3345713 ]]
sample/minamino10.jpgは鎌田大地

画像のファイル名は、事前に選手に対応した形にしてありましたので、例えば、

「sample/kubo1.jpgは久保建英」

といった形で名前が一致していれば正解、そうでなければ不正解となります。

結果をわかりやすく集計すると、以下のようになりました。

・久保選手の画像の正解数 → 10枚中7枚 = 正解率70%
・鎌田選手の画像の正解数 → 10枚中7枚 = 正解率70%
・南野画像の正解数 → 10枚中5枚 = 正解率50%

トータルの正解数 → 30枚中19枚 = 正解率63.3%

先ほどtest dataを用いて学習モデルの精度を確認した際はaccuracyが0.9064でしたので、それよりは低い正解率となりました。

以上、冒頭でお示しした課題に対し今回提供可能となった解決案は以下になります。

【課題】
ワールドカップの時期に発生するサッカーにわかファンは、日本代表の希少なゴールシーンを見た時、盛り上がりながらも「ところであの選手誰?」となる場合がある

【提供可能な解決案】
ゴールした選手の写真を撮ることで、63.3%の確率でその選手の名前を当ててくれるモデルが提供できた

■ 考察と反省


以上、初めての機械学習がとりあえず完了しました。
モデルの実装は高い精度でできたと思ったものの、sample dataを用いた検証ではやや低い精度となりました。

しかしながら、今回は3クラスの分類でしたので適当に分類した場合の正解率が33.3%になることを考慮すると、特に久保選手、鎌田選手の画像はそこそこの精度で予測ができているな、と素人ながら前向きに思うことにしました。

一方で、作業を進めながら「おそらくここがまずいかもな…」と思う部分もありました。そう思いながらも現時点で筆者にはこれ以上改善ができなかったというのが正直なところですので、以下考察と反省として記録し今後の学習課題にしていきたいと思います。

<画像の質と数>

  • ノイズは手動で削除したつもりだが、背景等その他の要素がノイズとなってしまった画像が依然として残されていた可能性がある。顔の特徴が明確な画像をもっとたくさん収集する必要があったかもしれない。

  • 重複画像があったかもしれない。重複画像を除去するコードにチャレンジしたものの、うまく実装できなかった。そのため目視で確認したが抜け漏れがあった可能性がある。

  • icrawlerを用いて画像を収集したが、指定した画像数が集まらず想定よりデータ数が少なくなってしまった。検索キーワードの工夫や、画像データの質を高めるため手動での画像収集がbetterかもしれない。

<モデルの実装やプログラミング全体に関して>

  • VGG16の構成について理解が深くなく、精度を高めるための編集ができなかった。さらなる自己学習が必要であることを痛感した。

  • もっと効率よくコードを書ける個所が多々あったと感じた。また、Matplotlibを用いて、Epoch数とaccuracyの関係等わかりやすく可視化すべき個所があったが、復習が追い付かずそこまで手が付けられなかった。pythonに関する基本的な学習も継続して行う必要がある。

以上、挙げればきりがないほど反省点や考察すべき点がありました。
わからないことも依然として多々あり、特にtest dataで高いaccuracyが出せたものの、sample dataがそれに及ばなかった理由は色々調べましたが結局未解決のままとなってしまいました。

さらなる学習に励みたいと思います。

■ まとめ


以上、初めての機械学習の実装を完了しました。

Aidemyでプログラミングや機械学習を学んだ3か月間は、私生活での多忙もありモチベーションの維持に苦労した面もありましたが、概ね楽しんで学習することができました。

成果物の作成についても、着手する前はそれまでの講座を全く復習できていなかったので不安だらけでしたが、着手してみると意外にもこれまた楽しんで進めることができました。

今後はさらなる学習をすすめ、本来の目的でもあるAI創薬の理解を深めていきたいと思います。






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