5
0

More than 1 year has passed since last update.

”ジョジョの奇妙な冒険” 機械学習による画像識別

Last updated at Posted at 2022-09-13

はじめに

概要
プログラミングスクールAidemyの"データ分析講座"の最終成果物として優れた画像認識アルゴリズムを持つCNN(Conbolutional Neural Network) を使用しジョジョの奇妙な冒険のキャラクターを識別を実施しました。

私は
元々ITベンダーの営業で去年からAI/MLプロジェクトを扱うPM兼エンジニアに転職しましたが、お客様の為により良いSolutionを提供する為、また案件を進める為にもより理解を深めたく本コースを受講しました。
畑が全く違うのでコーディングの勉強が苦痛でしかなく最終成果物は好きな物でモチベが上がるものにしようとチューターの方にオススメ頂き大好きなジョジョの奇妙な冒険のキャラクターの識別を機械学習での識別を実施しました。
結果学習過程で一番楽しかったです。目に見える成果がすぐに出ないと頑張れない自分の特性も知れエンジニアには程遠い属性だなとも思いました。
異業種からの挑戦のため、とんでもない並びのコードや手順かもしれませんがご了承ください。
では本題に移ります。

その前に、、、、ジョジョの奇妙な冒険とは

『ジョジョの奇妙な冒険』(ジョジョのきみょうなぼうけん、英語: JOJO'S BIZARRE ADVENTURE, イタリア語: LE BIZZARRE AVVENTURE DI GIOGIO)は、荒木飛呂彦による日本の漫画作品。略称は「ジョジョ」[1]
作品全体のテーマは「人間讃歌」[注 1]。仲間たちとの絆・強敵との死闘など少年漫画の基本を押さえながらも、個性的な表現方法とホラーサスペンス的な不気味さで独自の世界観を築き上げており、その作風は「王道を行きながら実験的」と評されている[5][6]。
Wikipedia より引用

各キャラにスタンドという守護霊的なものがキャラごとにありその点もこの作品の大きな特徴。

「スタンド」とは「パワーを持った像(ヴィジョン)」であり、持ち主の傍に出現してさまざまな超常的能力を発揮し、他人を攻撃したり持ち主を守ったりする守護霊のような存在である。漢字では「幽波紋」と表記される。
作者の荒木飛呂彦によれば、スタンドとは超能力を目に見える形で表現したものであり超能力そのものに姿を持たせて絵に描くことができるようにしたもの[1]
Wikipedia より引用

ちなみに、各シリーズの主人公は名前が全て略称で”ジョジョ”となるように命名されております。
例)
シリーズ第1部 : ジョナサン・ジョースター = ジョジョ
シリーズ第3部 : 空条承太郎 = ジョジョ
シリーズ第4部 : 東方仗助 = 呼び名は”じょうすけ”だが、訓読みで”ジョジョ”

ジョジョと私
幼少期から連載をずっと見ていた為、私の精神はこの漫画から成り立っております。初恋の人が作者の荒木飛呂彦先生で、表紙にある作者からの一言と写真を見る為にコミック本をお小遣いを貯めて購入していました。
色んな意味での青春と想いが詰まっており私を育ててくれた作品の為、営業からPM/エンジニアの挑戦という中年の新しい扉を開く一歩でキャラクター達に応援されながら(妄想)成果物が作れて本当によかったです。


使用するキャラ選定
1986年からの連載でシリーズも8部あり登場人物も多く今回は下記で選出しました。
機械学習が識別時に露頭に迷わないようにそれぞれがユニークでなるべく識別がしやすそうなキャラクターを選出しました。

1. 空条承太郎(くうじょう じょうたろう):第3部スターダストクルセイダースの主人公。全シリーズでも人気が高いキャラクターで3部以外のシリーズにも出ている為、画像が多そうという理由で選出。

2. 空条徐倫(くうじょう じょりーん):第6部”スターダストクルセイダース”の主人公。空条承太郎の娘。女性版のジョジョで特徴的なウサギの耳のような髪型が識別がされ易いかなという理由で選出。

3. 広瀬康一(ひろせ こういち):第4部「ダイヤモンドは砕けない」から登場。主人公の東方仗助の友人。普通の少年がスタンド使いになり成長過程も楽しく、冷静で勇気と決断力がある為、個人的に一番”男らしぃぃぃー”と思う好きなキャラで選出。

4. 山岸由花子(やまぎし ゆかこ):同じく第4部「ダイヤモンドは砕けない」の登場人物。康一君が居るのであれば、由花子さんが居ないと嫉妬で家を髪の毛で潰されそうなので(4部参照)選出。徐倫と同じ女性だが、由花子さんは髪の毛がスタンドという事もあり長い美しい髪の毛が特徴的なのでモデルが迷わないだろうという期待も込めて。

5. 荒木飛呂彦(あらき ひろひこ):ジョジョシリーズの作者。前述通り初恋の人で素晴らしい作品を創出し続ける大先生。1人だけ3次元なのでモデルがどう選別するかが興味あり。
スクリーンショット 2022-08-21 15.52.16(2).png

では、本題に移ります。


目次
1.開発環境
2.画像の処理
 2-1.画像収集
 2-2.画像選別と加工
 2-3.加工画像のアップロード
3.モデルの為の画像処理
 3-1.訓練とテストデータに画像を分類
 3-2.訓練データの拡張
4.モデル定義と学習
5.動作確認
6.まとめと考察

1. 開発環境

Google Colaboratory/Safari/MacOS
処理装置:GPU
GPUとは

GPUは、Graphics Processing Unitの略で、画像処理装置を意味します。その名の通り、画像を描写するために必要な計算を処理するものです。近年では、画像や映像を利用する機会も増えており、より速く、よりきれいに画像・映像を映すには、GPUが欠かせません。また画像を処理するということは膨大なデータを瞬時に計算する必要があるため、ビッグデータを処理するのにも適しています。
基礎からわかるGPU 性能・特徴や選び方のポイント

GPUが無料で使えるなんてありがたくてたまりません。
初め気付かずデフォルトでモデル学習していたら100エポックで処理に2時間ほどかかったので忘れないうちに設定。

GPU選択方法 on Google Colaboratory:下記
GPU画像.png

2. 画像の処理

ここからは学習/識別させる画像処理の方法となります。項目毎に分けて記載しております。では、始めましょう。

2-1. 画像収集

ScrapingでWebからモデルのテスト、トレーニング用の画像を自動調達。画像は多い方がモデル精度が高まるが時間の制約もあり今回は各々300枚収集を設定。検索エンジンは"Google"と"Bing"で画像収集数/検索精度を比較し"Google"はコスプレ画像なども多くヒットし選別に時間がかかると判断し今回は"Bing"を採用。

全体手順としては下記
 Step1: 必要機能icrawlerインストール
 Step2: pythonライブラリの「icrawler」でBing用モジュールをインポート
 Step3: 各キャラ画像をGoogle Colaboratory上のフォルダにDL
 Step4: 画像を編集するためにローカルにDLする為に取得した画像を全てZip化

Scraipingとは(一連の流れをコード付きで丁寧かつ短い時間で説明されていてわかりやすい/Youtube)
【だれでもできる】プログラミングが未経験でも大丈夫。Webから大量画像を収集する方法をわかりやすく解説します。

icrawelerとは(一連の操作流れとGoogleからの画像検索方法)
【Python】たった3行のコードで画像をまとめてダウンロードするライブラリicrawler

下記コーディングでStep1~4を実施
処理時間:11分26秒

#Step1: icrawlerインストール
!pip install icrawler

#Step2: pythonライブラリの「icrawler」でBing用モジュールをインポート
from icrawler.builtin import BingImageCrawler

# Step3: 各キャラ画像をGoogle Colaboratory上のフォルダにDL
# 検索ワードを定義し各キャラ画像をGoogle Colaboratory上のフォルダに最大300枚ずつDLされるよう定義
# keyword = 検索ワード
# crawler = BingImageCrawler(storage = {'root_dir' : "対象キャラ名のフォルダ名"})
#  ---> icrawlerをインスタンス化、ここで取得した画像の保存先ディレクトリを指定
# crawler.crawl(keyword, max_num = "取得したい枚数")
#  ---> 必要数の画像DLの処理実行

keyword = "空条承太郎"
crawler = BingImageCrawler(storage = {'root_dir' : keyword})
crawler.crawl(keyword, max_num = 300)

keyword = "空条徐倫"
crawler = BingImageCrawler(storage = {'root_dir' : keyword})
crawler.crawl(keyword, max_num = 300)

keyword = "広瀬康一"
crawler = BingImageCrawler(storage = {'root_dir' : keyword})
crawler.crawl(keyword, max_num = 300)

keyword = "山岸由花子"
crawler = BingImageCrawler(storage = {'root_dir' : keyword})
crawler.crawl(keyword, max_num = 300)

keyword = "荒木飛呂彦"
crawler = BingImageCrawler(storage = {'root_dir' : keyword})
crawler.crawl(keyword, max_num = 300)

# Step4:取得した画像を全てZip化
!zip -r /content/download.zip /content

上記コーディングの補足
補足1) フォルダの作成と画像格納
Step3: "各キャラ画像をGoogle Colaboratory上のフォルダにDL"に対して。
→ファイルが作成されると下記の画像のように"keyword"毎のフォルダが作成され検索された画像が格納されます。
Colaboratoryディレクトリ:"Content"-"sample_data"-"空条承太郎"

スクリーンショット 2022-08-07 19.58.09.png

今回は300枚指定しましたがあくまでこれは検索数の最大数のため、実際拾ってきた数は下記でした。思ったより少なかったです。

取得枚数(枚)
1.空条承太郎 →162
2.空条徐倫  →138
3.広瀬康一  →131
4.山岸由花子 →138
5.荒木飛呂彦 →162

補足2) zipファイル作成とDL
Step4: "画像を編集するためにローカルにDLする為に取得した画像を全てZip化"に対して。
→Step4コマンドでzipが作成下記に格納されます。
ディレクトリ:"Content"ー"sample_data"ー"download.zip"
download.zipの右側にある3つの縦の点をクリックすると"ダウンロード"が選択可能となり、ローカルにDLが可能となります
ダウンロード.png***


2-2. 画像選別と加工

ローカルにDLした画像を1枚ずつ選別、マニュアルで加工しました。
実施内容

  • 複数人写っているものは対象キャラのみが表示されるよう加工
  • 顔のみを抽出
  • 3次元のコスプレ画像の削除
  • 個人作成のイラストでオリジナルから程遠いものは削除
  • 同じ画像の削除
  • 後ろ向き画像削除

苦労した点

  • スタンドがワンセットのため、作者以外はスタンドとセット画像が多く識別と精度の為に1画像1個人にする為トリミングが多くかかった。ジョジョにしたことを少し後悔した瞬間
  • コスプレイヤーウケするのかコスプレ画像も多く削除
  • 意外に画像の枚数と種類の幅が少なかった(同じ画像が多い)

考慮する点

  • 後に反転処理をするため、胴体が入った画像だと反転+Reshapeで胴体のみが表示された画像になるため顔のみの抽出が必要

結果:選別結果による画像数削減
選別、加工の結果取得した画像枚数が下記に減少しました (枚)

1.空条承太郎 162→119
2.空条徐倫  138→102
3.広瀬康一  131→117
4.山岸由花子 138→89
5.荒木飛呂彦 162→97


2-3. 加工画像のアップロード

ローカルに落として加工した画像をGoogle Colabotoryにアップロード
全体手順としては下記
 Step1: GoogleDriveに加工画像ファイルをアップロード
 Step2: GoogleColaboratoryにGoogleDriveをインポート

Step1: GoogleDriveに加工画像ファイルをアップロード
ローカルのフォルダをマイドライブにドラッグし加工した画像フォルダをアップロード(下記図参照)今回は"jojolist"と銘打ちULを実施
スクリーンショット 2022-08-21 17.36.30(2).png

Step2: GoogleColaboratoryにGoogleDriveをインポート
ColaboratoryとDriveを連結させる事で、DriveのファイルやフォルダがColaboratory上に表示、操作が可能となります
---> 処理時間:17秒

# Step2: GoogleColaboratoryの"Content"フォルダにGoogleDriveをインポート
from google.colab import drive
drive.mount('/content/drive')

#ディレクトリをColaboratory-Content-mydrive移動
%cd /content/drive/MyDrive/jojolist

いったんまとめ:ここまでで、画像取得→加工→ULの流れが終わりました。次は機械学習モデル実装のための画像処理の手順に進みます

3. モデルの為の画像処理

ここからは機械学習モデルのための画像の処理となります。こちらも項目毎に記載しております。

3-1. 訓練とテストデータに画像を分類

加工した画像の中で各キャラクター毎30枚をテストデータにし他を訓練データに分割
test_imagesのディレクトリを作成し、キャラクター毎にランダムに30枚ずつ選択しtest_imagesに保存
--->処理時間:0秒

import os, glob
import random
import shutil

# 各キャラクターをリスト化
members =  ["空条承太郎","空条徐倫","広瀬康一","山岸由花子","荒木飛呂彦"]

IMAGE_DIR = "/content/drive/MyDrive/jojolist"
os.makedirs("test_images/".format(members), exist_ok=True)

#  加工した画像から各キャラクターの画像30枚をランダムにtest_imagesに移行
for member in members:
    files = glob.glob(os.path.join(IMAGE_DIR, member + "/*.jpg"))
    os.makedirs("test_images/" + member, exist_ok=True)

    for i in range(30):
      shutil.move(str(files[i]), "test_images/" + member)

3-2. 訓練データの拡張

今回取得画像が少数のため、訓練データが(各キャラ毎の加工画像)30枚しか用意が出来ず、
モデル精度の向上へOpenCVデータの水増しを実施

OpenCVによるデータの水増し(lambda)

上記リンクを参照し、今回は下記を実施

  • 上下反転
  • ぼかし
  • 画像の回転(30°)

---> 処理時間:4分

import os
import cv2
import numpy as np

def scratch_image(img, flip=True, blur=True, rotate=True):
    methods = [flip, blur, rotate]
        # filp は画像上下反転
        # blur はぼかし
        # rotate は画像回転

    # 画像のサイズ(x, y)
    size = np.array([img.shape[1], img.shape[0]])
    # 画像の中心位置(x, y)
    center = tuple([int(size[0]/2), int(size[1]/2)])
    # 回転させる角度
    angle = 30
    # 拡大倍率
    scale = 1.0

    mat = cv2.getRotationMatrix2D(center, angle, scale)

    # 画像処理をする手法をNumpy配列に格納
    scratch = np.array([
        lambda x: cv2.flip(x, 0),                               # flip
        lambda x: cv2.GaussianBlur(x, (15, 15), 0),             # blur
        lambda x: cv2.warpAffine(x, mat, img.shape[::-1][1:3])  # rotate
    ])

    # imagesにオリジナルの画像を配列として格納
    images = [img]

    # 関数と画像を引数に、加工した画像を元と合わせて水増しする関数
    def doubling_images(func, images):
        return images + [func(i) for i in images]

    for func in scratch[methods]:
        images = doubling_images(func, images)

    return images


# jojolistディレクトリにある各キャラの画像を拡張する
IMAGE_DIR = "/content/drive/MyDrive/jojolist"
members =  ["空条承太郎","空条徐倫","広瀬康一","山岸由花子","荒木飛呂彦"]

for member in members:
    files = glob.glob(os.path.join(IMAGE_DIR, member + "/*.jpg"))

    for index, file in enumerate(files):
        member_image = cv2.imread(file)
        data_aug_list = scratch_image(member_image)

        # 拡張した画像を出力するディレクトリを作成
        os.makedirs("train_images/{}".format(member), exist_ok=True)
        output_dir = "train_images/{}".format(member)

        # 保存
        for j, img in enumerate(data_aug_list):
            cv2.imwrite("{}/{}_{}.jpg".format(output_dir, str(index).zfill(3), str(j).zfill(2)), img)

下記のように反転や傾き、ぼかされた画像ができました
反転とか.png

いったんまとめ:モデルの学習に必要なデータが揃いました。次からは、この画像データを利用してキャラ識別が実施できるようにモデルの学習に進みます

4. モデル定義と学習

今回モデルはCNNモデルから"VGG16"というImageNetという大規模画像データセットで学習された16層からなるモデルで学習を実施

全体工程としては下記
 Step1:必要ライブラリをインポート
 Step2:訓練/テストデータの読み込みリスト代入
 Step3:データの正規化とOne-hot表現
 Step4:モデル定義(VGG16)と作成
 Step5:モデルの学習

Step1: 必要ライブラリをインポート
---> 処理時間:2秒

import os, glob
import random
import cv2
import numpy as np
import matplotlib.pyplot as plt
from tensorflow.keras.utils import to_categorical
from tensorflow.keras import Input, Sequential, Model
from tensorflow.keras.models import load_model, save_model
from tensorflow.keras.layers import Dense, Conv2D, Flatten, Dropout, MaxPooling2D
from tensorflow.keras.preprocessing.image import ImageDataGenerator, img_to_array, load_img
from tensorflow.keras.applications.vgg16 import VGG16
from tensorflow.keras.optimizers import SGD, Adam
from keras.datasets import mnist
from keras.utils.vis_utils import plot_model

Step2: 訓練/テストデータの読み込みリスト代入
前提:

  1. Image_sizeは100に設定
  2. ラベルデータは5(選定キャラ人数)
    ---> 処理時間:60秒
IMAGE_DIR_TRAIN = "train_images"
IMAGE_DIR_TEST = "test_images"
image_size = 100

# 訓練データとテストデータをわける
X_train = []
X_test  = []
y_train = []
y_test  = []

# 訓練データをリストに代入
for index, member in enumerate(members):
    files = glob.glob(os.path.join(IMAGE_DIR_TRAIN, member + "/*.jpg"))
    for file in files:
        image = load_img(file)
        image = image.resize((image_size, image_size))
        image = img_to_array(image)
        X_train.append(image)
        y_train.append(index)

# テストデータをリストに代入
for index, member in enumerate(members):
    files = glob.glob(os.path.join(IMAGE_DIR_TEST, member + "/*.jpg"))
    for file in files:
        image = load_img(file)
        image = image.resize((image_size, image_size))
        image = img_to_array(image)
        X_test.append(image)
        y_test.append(index)

# テストデータと訓練データをシャッフル
p = list(zip(X_train, y_train))
random.shuffle(p)
X_train, y_train = zip(*p)

q = list(zip(X_test, y_test))
random.shuffle(q)
X_test, y_test = zip(*q)
q = list(zip(X_test, y_test))
random.shuffle(q)

# Numpy配列に変換
X_train = np.array(X_train)
X_test = np.array(X_test)
y_train = np.array(y_train)
y_test = np.array(y_test)

Step3: データの正規化とOne-hot表現
---> 処理時間:0秒

# データの正規化
X_train = X_train / 255.0
X_test = X_test / 255.0

# One-hot表現:ラベルが5人のため5を指定
y_train = to_categorical(y_train, 5)
y_test = to_categorical(y_test, 5)

補足:正規化とOne-hot表現にする目的
1)正規化

正規化とは:最小値0~最大値1にスケーリングする「Min-Max normalization」
データの最小値からの偏差(=最小値を中心0にした場合の値)をデータ範囲(=最大値-最小値)で割ることでこれにより、データの最小値は0、最大値は1に変換される。
正規化を実施する理由:主に機械学習モデルに入力する前のデータに対して行われる前処理で、例えばデータの列(特徴量)によっては、その範囲が0~1の場合もあれば、-50~+5000の場合もあり得るが、このように列によって数値の範囲が違いすぎると機械学習がうまくできなかったり、学習により多くの時間がかかったりする可能性があるため実施 正規化(Normalization)/標準化(Standardization)とは?

下記で255.0が指定されている理由

X_train = X_train / 255.0
X_test = X_test / 255.0

黒から白までの変化を0〜255で表現しているので、255で割ることでデータを0〜1の範囲に正規化。float型にすることで実数値で表現。
正規化前「0〜255」 正規化実施「0.0〜1.0」(実数値)

2)One-hot表現
One-hot表現とは:文字表記で数字でない値を取る説明変数を扱いたい場合、3次元数値で表現すること。イメージとしてはラベルデータをOne-hotベクトルに変更しラベルデータの整数値を2値クラスの行列に変換
今回ラベルデータは5のため、下記設定

y_train = to_categorical(y_train, 5)

One-hot表現を実施する理由:全ての値を平等に扱えることがメリット
例)"猫"、"うさぎ"、"犬" を数値化
猫:  0
うさぎ:1
犬:  2
上記だとうさぎが猫と犬の中間値として処理されてしまい分析が狂うため、全てを平等に扱うために下記のように変換する
猫:  (1,0,0)
うさぎ:(0,1,0)
犬:  (0,0,1)
参照リンク
One-hotベクトル(ワンホット表現)の意味とメリット・デメリット

Step4:モデル定義(VGG16)と作成
---> 処理時間:3秒

# VGG16のインスタンスの生成
input_tensor = Input(shape=(100, 100, 3))
vgg16 = VGG16(include_top=False, weights="imagenet", input_tensor=input_tensor)

# モデルの生成
top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(256, activation="relu"))
top_model.add(Dropout(0.5))
top_model.add(Dense(128, activation="relu"))
top_model.add(Dropout(0.5))
top_model.add(Dense(5, activation="softmax")) # num_classesを5に設定

# モデルの結合
model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output))

# model.summary()

# 15層目までのパラメータを固定
for layer in model.layers[:15]:
    layer.trainable = False


# モデルのコンパイル
optimizer = SGD(lr=1e-4, momentum=0.9)
model.compile(optimizer=optimizer, loss="categorical_crossentropy", metrics=["accuracy"])

補足VGG16とは
VGG16理解用:畳み込みネットワークの「基礎の基礎」を理解する ~ディープラーニング入門
実行理解用:VGG16モデルを使用してオリジナル写真の画像認識を行ってみる

Step5:モデルの学習
前提:ハイパーパラメータ値

  1. バッチサイズ=32
  2. エポック=100

ハイパーパラメータの決定方法
1)バッチサイズ:ミニバッチ勾配降下法と呼ばれる手法を使い、適切なサブセットに分ける必要があります。バッチサイズは、機械学習において、慣習的に2のn乗が利用されることが多いです。したがって、まずは「16、32、64、128、256、512、1024、2048」の中から試すのが良い。
機械学習におけるバッチサイズとは?決め方や注意点を解説

2)エポック:過学習が出ない程度に最大値で試す
機械学習/ディープラーニングにおけるバッチサイズ、イテレーション数、エポック数の決め方

また、今回は過学習防止のためにEalryStoppingを設定
モデルの改善が止まった時点で学習を止めれるため時間節約にも良い
kerasでモデルの学習が進まなくなったら学習を止める方法

--->処理時間:8分


#EarlyStoppingインポート
from keras.callbacks import EarlyStopping

#モデルの保存
import os
from google.colab import files

# モデルの学習
batch_size = 32
epochs = 100

# EaelyStoppingの設定
early_stopping =  EarlyStopping(
                            monitor='val_loss',
                            min_delta=0.0,
                            patience=100,
)

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

scores = model.evaluate(X_test, y_test, verbose=1)

# 可視化
fig = plt.figure(figsize=(15,5))
plt.subplots_adjust(wspace=0.4, hspace=0.6)

ax1 = fig.add_subplot(1, 2, 1)
ax1.plot(history.history["accuracy"], c="b", label="acc")
ax1.plot(history.history["val_accuracy"], c="r", label="val_acc")
ax1.set_xlabel("epochs")
ax1.set_ylabel("accuracy")
plt.legend(loc="best")

ax2 = fig.add_subplot(1, 2, 2)
ax2.plot(history.history["loss"], c="b", label="loss")
ax2.plot(history.history["val_loss"], c="r", label="val_loss")
ax2.set_xlabel("epochs")
ax2.set_ylabel("loss")
plt.legend(loc="best")

fig.show()
学習経過はこちら
Epoch 1/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0852 - accuracy: 0.9764 - val_loss: 0.7447 - val_accuracy: 0.8067
Epoch 2/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0867 - accuracy: 0.9731 - val_loss: 0.7931 - val_accuracy: 0.8267
Epoch 3/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0742 - accuracy: 0.9789 - val_loss: 0.8183 - val_accuracy: 0.8267
Epoch 4/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0683 - accuracy: 0.9818 - val_loss: 0.8692 - val_accuracy: 0.8133
Epoch 5/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0772 - accuracy: 0.9757 - val_loss: 0.8499 - val_accuracy: 0.8267
Epoch 6/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0648 - accuracy: 0.9836 - val_loss: 0.8456 - val_accuracy: 0.8200
Epoch 7/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0595 - accuracy: 0.9822 - val_loss: 0.8980 - val_accuracy: 0.8200
Epoch 8/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0534 - accuracy: 0.9862 - val_loss: 0.8791 - val_accuracy: 0.8267
Epoch 9/100
86/86 [==============================] - 4s 50ms/step - loss: 0.0533 - accuracy: 0.9815 - val_loss: 0.8260 - val_accuracy: 0.8133
Epoch 10/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0491 - accuracy: 0.9873 - val_loss: 0.9071 - val_accuracy: 0.8200
Epoch 11/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0473 - accuracy: 0.9844 - val_loss: 0.8708 - val_accuracy: 0.8133
Epoch 12/100
86/86 [==============================] - 4s 50ms/step - loss: 0.0466 - accuracy: 0.9869 - val_loss: 0.8776 - val_accuracy: 0.8200
Epoch 13/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0389 - accuracy: 0.9906 - val_loss: 0.8734 - val_accuracy: 0.8267
Epoch 14/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0453 - accuracy: 0.9884 - val_loss: 0.8851 - val_accuracy: 0.8200
Epoch 15/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0406 - accuracy: 0.9902 - val_loss: 0.8914 - val_accuracy: 0.8400
Epoch 16/100
86/86 [==============================] - 4s 50ms/step - loss: 0.0330 - accuracy: 0.9913 - val_loss: 0.9459 - val_accuracy: 0.8467
Epoch 17/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0370 - accuracy: 0.9906 - val_loss: 1.0014 - val_accuracy: 0.8267
Epoch 18/100
86/86 [==============================] - 5s 52ms/step - loss: 0.0326 - accuracy: 0.9909 - val_loss: 0.9149 - val_accuracy: 0.8267
Epoch 19/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0346 - accuracy: 0.9895 - val_loss: 1.0499 - val_accuracy: 0.8133
Epoch 20/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0250 - accuracy: 0.9935 - val_loss: 0.9276 - val_accuracy: 0.8200
Epoch 21/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0293 - accuracy: 0.9920 - val_loss: 0.9729 - val_accuracy: 0.8333
Epoch 22/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0257 - accuracy: 0.9927 - val_loss: 1.0272 - val_accuracy: 0.8333
Epoch 23/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0307 - accuracy: 0.9902 - val_loss: 1.0400 - val_accuracy: 0.8200
Epoch 24/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0237 - accuracy: 0.9942 - val_loss: 1.0269 - val_accuracy: 0.8133
Epoch 25/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0309 - accuracy: 0.9891 - val_loss: 1.1494 - val_accuracy: 0.8267
Epoch 26/100
86/86 [==============================] - 4s 52ms/step - loss: 0.0210 - accuracy: 0.9949 - val_loss: 1.0369 - val_accuracy: 0.8200
Epoch 27/100
86/86 [==============================] - 4s 52ms/step - loss: 0.0235 - accuracy: 0.9949 - val_loss: 1.0055 - val_accuracy: 0.8200
Epoch 28/100
86/86 [==============================] - 4s 52ms/step - loss: 0.0228 - accuracy: 0.9931 - val_loss: 1.0466 - val_accuracy: 0.8133
Epoch 29/100
86/86 [==============================] - 5s 53ms/step - loss: 0.0185 - accuracy: 0.9964 - val_loss: 1.0416 - val_accuracy: 0.8200
Epoch 30/100
86/86 [==============================] - 4s 52ms/step - loss: 0.0230 - accuracy: 0.9931 - val_loss: 0.9968 - val_accuracy: 0.8333
Epoch 31/100
86/86 [==============================] - 4s 52ms/step - loss: 0.0212 - accuracy: 0.9938 - val_loss: 1.0821 - val_accuracy: 0.8400
Epoch 32/100
86/86 [==============================] - 4s 52ms/step - loss: 0.0168 - accuracy: 0.9949 - val_loss: 1.0420 - val_accuracy: 0.8467
Epoch 33/100
86/86 [==============================] - 4s 52ms/step - loss: 0.0246 - accuracy: 0.9931 - val_loss: 1.0097 - val_accuracy: 0.8400
Epoch 34/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0153 - accuracy: 0.9975 - val_loss: 1.0302 - val_accuracy: 0.8400
Epoch 35/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0176 - accuracy: 0.9949 - val_loss: 0.9956 - val_accuracy: 0.8267
Epoch 36/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0205 - accuracy: 0.9945 - val_loss: 1.0806 - val_accuracy: 0.8267
Epoch 37/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0185 - accuracy: 0.9949 - val_loss: 1.1441 - val_accuracy: 0.8333
Epoch 38/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0161 - accuracy: 0.9960 - val_loss: 1.1157 - val_accuracy: 0.8267
Epoch 39/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0139 - accuracy: 0.9975 - val_loss: 1.0497 - val_accuracy: 0.8267
Epoch 40/100
86/86 [==============================] - 5s 53ms/step - loss: 0.0165 - accuracy: 0.9953 - val_loss: 1.0526 - val_accuracy: 0.8333
Epoch 41/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0136 - accuracy: 0.9975 - val_loss: 1.0846 - val_accuracy: 0.8333
Epoch 42/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0109 - accuracy: 0.9982 - val_loss: 1.1398 - val_accuracy: 0.8200
Epoch 43/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0128 - accuracy: 0.9967 - val_loss: 1.1188 - val_accuracy: 0.8200
Epoch 44/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0146 - accuracy: 0.9967 - val_loss: 1.1153 - val_accuracy: 0.8400
Epoch 45/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0128 - accuracy: 0.9967 - val_loss: 1.1627 - val_accuracy: 0.8400
Epoch 46/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0105 - accuracy: 0.9978 - val_loss: 1.1043 - val_accuracy: 0.8267
Epoch 47/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0110 - accuracy: 0.9978 - val_loss: 1.1402 - val_accuracy: 0.8333
Epoch 48/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0145 - accuracy: 0.9971 - val_loss: 1.1428 - val_accuracy: 0.8267
Epoch 49/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0121 - accuracy: 0.9975 - val_loss: 1.1437 - val_accuracy: 0.8400
Epoch 50/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0099 - accuracy: 0.9989 - val_loss: 1.1438 - val_accuracy: 0.8333
Epoch 51/100
86/86 [==============================] - 4s 52ms/step - loss: 0.0101 - accuracy: 0.9975 - val_loss: 1.1521 - val_accuracy: 0.8333
Epoch 52/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0102 - accuracy: 0.9975 - val_loss: 1.1921 - val_accuracy: 0.8333
Epoch 53/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0109 - accuracy: 0.9975 - val_loss: 1.2109 - val_accuracy: 0.8400
Epoch 54/100
86/86 [==============================] - 4s 52ms/step - loss: 0.0089 - accuracy: 0.9978 - val_loss: 1.1550 - val_accuracy: 0.8400
Epoch 55/100
86/86 [==============================] - 5s 53ms/step - loss: 0.0115 - accuracy: 0.9971 - val_loss: 1.2402 - val_accuracy: 0.8333
Epoch 56/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0128 - accuracy: 0.9964 - val_loss: 1.1411 - val_accuracy: 0.8333
Epoch 57/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0100 - accuracy: 0.9978 - val_loss: 1.2081 - val_accuracy: 0.8333
Epoch 58/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0107 - accuracy: 0.9971 - val_loss: 1.1854 - val_accuracy: 0.8333
Epoch 59/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0063 - accuracy: 0.9993 - val_loss: 1.2097 - val_accuracy: 0.8400
Epoch 60/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0075 - accuracy: 0.9985 - val_loss: 1.1771 - val_accuracy: 0.8400
Epoch 61/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0106 - accuracy: 0.9971 - val_loss: 1.2425 - val_accuracy: 0.8333
Epoch 62/100
86/86 [==============================] - 5s 53ms/step - loss: 0.0102 - accuracy: 0.9975 - val_loss: 1.1825 - val_accuracy: 0.8267
Epoch 63/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0106 - accuracy: 0.9978 - val_loss: 1.2748 - val_accuracy: 0.8400
Epoch 64/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0060 - accuracy: 0.9985 - val_loss: 1.2690 - val_accuracy: 0.8333
Epoch 65/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0098 - accuracy: 0.9982 - val_loss: 1.2646 - val_accuracy: 0.8267
Epoch 66/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0084 - accuracy: 0.9985 - val_loss: 1.1802 - val_accuracy: 0.8400
Epoch 67/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0082 - accuracy: 0.9989 - val_loss: 1.1602 - val_accuracy: 0.8267
Epoch 68/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0096 - accuracy: 0.9975 - val_loss: 1.1882 - val_accuracy: 0.8400
Epoch 69/100
86/86 [==============================] - 4s 52ms/step - loss: 0.0077 - accuracy: 0.9978 - val_loss: 1.2631 - val_accuracy: 0.8400
Epoch 70/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0125 - accuracy: 0.9953 - val_loss: 1.1898 - val_accuracy: 0.8400
Epoch 71/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0077 - accuracy: 0.9978 - val_loss: 1.2319 - val_accuracy: 0.8400
Epoch 72/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0081 - accuracy: 0.9978 - val_loss: 1.2066 - val_accuracy: 0.8333
Epoch 73/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0086 - accuracy: 0.9975 - val_loss: 1.1613 - val_accuracy: 0.8467
Epoch 74/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0078 - accuracy: 0.9975 - val_loss: 1.2379 - val_accuracy: 0.8400
Epoch 75/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0059 - accuracy: 0.9996 - val_loss: 1.1931 - val_accuracy: 0.8400
Epoch 76/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0072 - accuracy: 0.9982 - val_loss: 1.1885 - val_accuracy: 0.8400
Epoch 77/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0083 - accuracy: 0.9982 - val_loss: 1.1935 - val_accuracy: 0.8400
Epoch 78/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0056 - accuracy: 0.9993 - val_loss: 1.1708 - val_accuracy: 0.8600
Epoch 79/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0098 - accuracy: 0.9975 - val_loss: 1.2341 - val_accuracy: 0.8333
Epoch 80/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0071 - accuracy: 0.9982 - val_loss: 1.2435 - val_accuracy: 0.8333
Epoch 81/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0086 - accuracy: 0.9978 - val_loss: 1.2952 - val_accuracy: 0.8333
Epoch 82/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0085 - accuracy: 0.9978 - val_loss: 1.3061 - val_accuracy: 0.8400
Epoch 83/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0065 - accuracy: 0.9993 - val_loss: 1.2427 - val_accuracy: 0.8333
Epoch 84/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0077 - accuracy: 0.9985 - val_loss: 1.2860 - val_accuracy: 0.8400
Epoch 85/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0049 - accuracy: 0.9993 - val_loss: 1.3283 - val_accuracy: 0.8400
Epoch 86/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0062 - accuracy: 0.9989 - val_loss: 1.2673 - val_accuracy: 0.8400
Epoch 87/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0070 - accuracy: 0.9989 - val_loss: 1.2400 - val_accuracy: 0.8333
Epoch 88/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0054 - accuracy: 0.9985 - val_loss: 1.2405 - val_accuracy: 0.8400
Epoch 89/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0067 - accuracy: 0.9971 - val_loss: 1.2524 - val_accuracy: 0.8333
Epoch 90/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0055 - accuracy: 0.9982 - val_loss: 1.2506 - val_accuracy: 0.8467
Epoch 91/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0063 - accuracy: 0.9985 - val_loss: 1.2573 - val_accuracy: 0.8333
Epoch 92/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0064 - accuracy: 0.9985 - val_loss: 1.4230 - val_accuracy: 0.8267
Epoch 93/100
86/86 [==============================] - 4s 52ms/step - loss: 0.0057 - accuracy: 0.9989 - val_loss: 1.2354 - val_accuracy: 0.8533
Epoch 94/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0066 - accuracy: 0.9985 - val_loss: 1.2519 - val_accuracy: 0.8467
Epoch 95/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0069 - accuracy: 0.9985 - val_loss: 1.3576 - val_accuracy: 0.8400
Epoch 96/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0076 - accuracy: 0.9985 - val_loss: 1.2990 - val_accuracy: 0.8467
Epoch 97/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0075 - accuracy: 0.9975 - val_loss: 1.3484 - val_accuracy: 0.8400
Epoch 98/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0047 - accuracy: 0.9996 - val_loss: 1.2541 - val_accuracy: 0.8400
Epoch 99/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0045 - accuracy: 0.9996 - val_loss: 1.2180 - val_accuracy: 0.8467
Epoch 100/100
86/86 [==============================] - 4s 51ms/step - loss: 0.0074 - accuracy: 0.9978 - val_loss: 1.2680 - val_accuracy: 0.8400
5/5 [==============================] - 0s 39ms/step - loss: 1.2680 - accuracy: 0.8400

実際の結果
正解立.png

各数値と解説
正解率が82.67%でした。画像数が少なかったのが原因かと思います。
今回時間の制限があり難しかったですが今後画像収集の数量を増やして精度向上に再トライしたいと思います。

いったんまとめ:これでモデルは完成です。次にモデルの正解度を実際の画像を使用して確認してみましょう


5. 動作確認

学習したモデルの動作確認
テスト用の画像を別に用意し1枚ずつ回答の正誤を確認しました。
学習用画像データはBingから取得したため、今回の確認用データはGoogleから取得しテストを実施しました
Step1:確認用データをGoogleから取得
Step2:確認用画像からモデルのラベリングの正誤を確認


Step1:確認用データをGoogleから取得
取得後のLocalへの保存、加工修正方法は前述と同様

!pip install icrawler
from icrawler.builtin import GoogleImageCrawler
crawler = GoogleImageCrawler(storage={"root_dir": "jotaro-test"})
crawler.crawl(keyword='空条承太郎', max_num=20)

crawler = GoogleImageCrawler(storage={"root_dir": "jorin-test"})
crawler.crawl(keyword='空条徐倫', max_num=20)

crawler = GoogleImageCrawler(storage={"root_dir": "koichi-test"})
crawler.crawl(keyword='広瀬康一', max_num=20)

crawler = GoogleImageCrawler(storage={"root_dir": "yukako-test"})
crawler.crawl(keyword='山岸由花子', max_num=20)

crawler = GoogleImageCrawler(storage={"root_dir": "sakusya-test"})
crawler.crawl(keyword='荒木飛呂彦', max_num=20)

Step2:確認用画像からモデルのラベリングの正誤を確認

前提

  1. ImageSize=(100,100)で前述のShapeと合わせました
# 画像を一枚渡して、キャラ識別
def pred_gender(img):
  img = cv2.resize(img, (100, 100,))
  pred = np.argmax(model.predict(np.array([img])))
  if pred == 0:
    return "空条承太郎"
  elif pred == 1:
    return "空条徐倫"
  elif pred == 2:
    return "広瀬康一"
  elif pred == 3:
    return "山岸由花子"
  elif pred == 4:
    return "荒木飛呂彦"

# pred_gender関数に顔写真を渡して性別を予測します
for i in range(1):
    # pred_gender関数に顔写真を渡して性別を予測します
    img = cv2.imread('/content/drive/MyDrive/Google test /000001.png')
    b,g,r = cv2.split(img) 
    img = cv2.merge([r,g,b])
    plt.imshow(img)
    plt.show()
    print(pred_gender(img))

実際の識別結果
テスト.png
正解率
正解率修正.png

ランダムに拾ったGoogle画像を30枚識別させたので、これがモデルの正解率にはイコールにはならないですが、山岸由花子がGoogle画像だとことごとく空条承太郎と認識されたので食わせている画像が不鮮明なのかと思い再度Bingで拾ったThe由花子な画像を食わせると識別精度が上がりました。娘の徐倫より由花子の方が造形が承太郎に似てるんですかね。
実際トレーニング用に食わせた画像も荒木飛呂彦と山岸由花子が圧倒的に少なかったので画像数が圧倒的に多い空条承太郎に全振りされたように見えますしこの中での正解率も承太郎だけ100%だった為、画像の取り込み数も全種均等にあった方が良いのかなと思いました。

いったんまとめ:これで全行程が終了しました。次のまとめと考察を持って終了となります。


6. まとめと考察

まず、物作りは苦しいですが、楽しかったです。
講座の中でポイントポイントで理解していた気になっていた事をフロー化してみて今まで気付かなかったコードの組み合わせによるエラー発見やデバック作業は期限が迫る中では非常にストレスフルでしたが、1行ずつ見て理解して作業を進めていくと。。。講座で学習していた時よりもきちんと理解が深まり、新たに調べ、読み解き、もっと知りたい!という事を実施できたように思え、継続して分析や機械学習の勉強を進めたいと思いました。

今回自分のコース外の画像識別-CNNに挑みましたが、付け焼き刃の知識で進めた所もあり下記が反省点でした。

  • キャラ毎の加工後の画像枚数がばらついていた:山岸由花子と荒木飛呂彦が加工後、数量が他よりも少なくてもそのまま突っ切ってしまいキャラ毎の精度のバランスが出た
  • OpenCV未使用(画像加工の手作業):時間がなく顔抽出の作業をOpenCVを使わず手作業で実施してしまった。機械学習で稼働を削減しているにも関わらず自動化出来ていない
  • 画像の処理が初めは雑だった:当初顔だけを切り取らず、面倒なのでキャラが個体で写っていれば体全体が入っていてもOKとしていました → 水増しの際に、トリミングし反転した際に胴体だけの画像がありモデル精度劣化につながった
  • 精度向上が出来なかった:最後に山岸由花子を荒木飛呂彦と識別してしまう誤認識が乱立したが、モデルの作成し直しより、識別に読み込ませる画像を分かり易いものにして強引にモデルが由花子と識別したという既成事実を作ってしまった

今後に向けて
上記の通り、まだ改善する点は多くあり、まずはOpenCVを理解し画像加工過程の稼働を削減したいです。その上で学習させる画像数を増やしてモデル自体の精度向上を実施し深みを磨きたいです。

次に、キャラを増やしたり識別が難しいであろうキャラの識別をモデル化し(*別漫画ですが、ドラゴンボールの孫悟空とターレス)精度を見ることで横の幅を増やし結果の考察。結局CNNが何を持って識別しているのか、何の識別が苦手なのかを理解しケースに応じてあらゆる観点で精度向上を実施できる視点を持ちたいなと夢を抱きました。
このように体感しないと仕組みを理解できないタイプなのですが、どなたか良い参考文献などあればお教えください。本当は入学当初は壮大なやりたい事の夢を持っていたので、将来はそれが実現できるようにしたいです。今回の成果物がその夢に関連のある最初の1歩の内容でしたので。
次に進む前に、今回の物をUpGradeしていきたいです!

余談。。。。
山岸由花子さんを空条承太郎と識別したり、誤認識が一番多かったりと恐怖しかよぎりませんでした。
由花子さんすみません。今後はもっとお美しい姿を収集取得し、他が認識できてなくても由花子さんだけは1000%識別ができるよう努めてまいります!
しょうもないことを書いて以上です。勉強続けるぞー。

image.png
https://bibi-star.jp/posts/6432

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