見出し画像

ディープラーニングでディズニーとサンリオのキャラクター識別を作る(修正)


1.背景

AidemyのE資格対策講座とAIアプリ開発講座が終わった。

人間が見たらディズニーとサンリオのキャラクターの違いは認識できるが、機械学習で見分けることができるか興味があったので、習ったことを生かしてディズニーとサンリオのキャラクターを識別できるWebアプリケーションを作成することにした。
今回は「ディズニー」と「サンリオ」の二値分類だが、多項分類も少しソースを変更すればできるようにした。

2.開発の流れ

2.1 開発の流れ

下記の流れで開発を行う。
・「スクレイピング」で教師用画像データを取得
・取得した教師用画像データの「水増し」を行う
・「転移学習」を利用して「画像識別モデル」を作成
・「Webアプリケーション」に作成した画像認識モデルを組み込む

2.2 開発環境

開発環境は下記の通り。
・iMac2019(Intel) macOS Ventura 13.2.1
・python 3.10.9
・Visual Studio Code 1.76.1

2.3 使用ライブラリのバージョン

beautifulsoup4               4.11.2
requests                          2.28.1
lxml                                 4.9.2
chromedriver-binary-auto     0.2.3
tensorflow                   2.11.0
keras                           2.11.0
scikit-learn                 1.2.2
Flask                            2.2.3

3.開発

3.1 スクレイピング

まずはディープラーニングの学習に使う教師用学習画像を取得するためにスクレイピングのスクリプトを作成する。
今回は、教師データとして、Googleで検索したディズニーとサンリオの検索結果を100枚ずつ取り込むことにした。

検索ワードは「ディズニー キャラ」と「サンリオ キャラクター 公式」で行った

「サンリオ キャラクター 公式」の検索結果

Googleの画像検索は無限スクロールになっているのでスクレイピングする際にスクロールをする必要があるため、ChromeDriverManagerやseleniumのモジュールを使用した。

 #利用するライブラリ (モジュール)をインポート
import requests
from time import sleep
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager
from bs4 import BeautifulSoup
import os
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome import service as fs
import base64



def my_scraping(actor_name, get_limit, save_dir):
    """画像のスクレイピングを行い、指定フォルダに保存する

    Args:
        actor_name (string): 検索ワード
        get_limit (int): 最大取得画像数
        save_dir (string): 取得した画像を保存するフォルダ
    """

    # ChromeDriverを使うための設定
    options = webdriver.ChromeOptions()
    options.add_argument("--headless")

    # ChromeDriverを確実に破棄するためにwithで宣言
    with webdriver.Chrome(ChromeDriverManager().install(), options=options) as browser:
        # 画像検索ページを開く
        url = "https://www.google.co.jp/imghp?hl=ja"
        browser.get(url)

        # 検索欄のinput要素を取得
        search = browser.find_element(By.NAME, 'q') 

        # 検索ワードを入力し、エンターキーを押下、検索
        search.send_keys(actor_name)
        search.send_keys(Keys.ENTER)

        # imgタグのsrc情報を取得する
        img_urls = []
        img_count = 0

        # 事前にスクロールして画像を表示しておく スクロール1回あたり二十枚だが、余分にスクロールしておく
        scloll_cnt = get_limit//5
        print("スクロール回数:", scloll_cnt)
        for s in range(scloll_cnt):
            browser.execute_script("window.scrollTo(0, document.body.scrollHeight);")
            sleep(1)

        #BeautifulSoupで画像検索したページの画像を取得する
        soup = BeautifulSoup(browser.page_source, "html.parser")

        # imgタグを取得
        img_tags = soup.select("img")

        # imgタグのsrcの中を取得し、配列にする
        for img_tag in img_tags:
            # 画像の取得上限を超えたら終了
            if img_count >= get_limit:
                print("上限を超えたので修了")
                break
            
            # src属性の中身を取得
            url = img_tag.get("src")
            # src属性の中身が空の場合は処理をスキップ
            if url is None:
                continue
            # ファビコンやpng、svgなどの場合はスキップ
            if ('favicon' in url) or ('.png' in url) or ('.svg' in url) or ('data:image/' in url):
                continue
            
            # 一覧に追加
            img_urls.append(url)
            img_count += 1


        # 一覧にしたURLから画像を取得して保存する

        # 保存フォルダの確認 なければ作成する
        if not os.path.exists(save_dir):
            os.mkdir(save_dir)

        #取得した画像のデータを保存する
        i=1
        for url in img_urls:
            # print(i)
            try:
                # base64形式の場合、デコードして保存
                if 'data:image/' in url:
                    with open(save_dir + "image_" + str(i) + "_0.jpg", "wb") as fp:
                        fp.write(base64.b64decode(url.split('base64,')[1]))
                # URLの場合
                elif "http" in url:
                    # 画像のURLを取得
                    r = requests.get(url)
                    # 取得したファイルの保存
                    with open(save_dir + "image_" + str(i) + "_0.jpg", "wb") as fp:
                        fp.write(r.content)
                else:
                    continue

                # インクリメント
                i += 1
                # 連続取得を避けるため、スリープ
                sleep(0.1)
            except:
                # エラーを握りつぶす
                pass

        browser.quit()


if __name__ == "__main__":
    # ディズニーの画像を取得
    print("ディズニー")
    actor_name = "ディズニー キャラクター イラスト"       # 検索ワード
    get_limit = 100                # 最大取得画像数
    save_dir = "./img/desny/"          # 取得した画像を保存するフォルダ
    my_scraping(actor_name, get_limit, save_dir)

    # サンリオの画像を取得
    print("サンリオ")
    actor_name = "サンリオ キャラクター 公式"       # 検索ワード
    get_limit = 100                # 最大取得画像数
    save_dir = "./img/sanrio/"          # 取得した画像を保存するフォルダ
    my_scraping(actor_name, get_limit, save_dir)

画像を100枚ずつスクレイピングし、あとは水増しを行うのだが、
水増しの前に教師用データと精度測定用データに手動で分けた。
(leak防止のため)

取得した画像を、教師用とテスト用に手動で分けた(八:二の割合)

3.2 水増し

取得した画像について、ディープラーニングの教師データとしては足りないので、水増しを行う。
今回は下記の水増しを行った
・画像の左右反転
・閾値処理(二値化)
・ぼかし
・モザイク処理
・収縮

import numpy as np
import matplotlib.pyplot as plt
import cv2
import glob as glob

def scratch_image(img, flip=True, thr=True, blur=True, resize=True, erode=True):
    # 水増しの手法を配列にまとめる
    methods = [flip, thr, blur, resize, erode]
    
    # 画像のサイズを習得、収縮処理に使うフィルターの作成
    img_size = img.shape
    filter1 = np.ones((3, 3))
    # オリジナルの画像データを配列に格納
    images = [img]

    # 手法に用いる関数
    scratch = np.array([
       
        # 画像の左右反転
        lambda x: cv2.flip(x, 1),
        
        # 閾値処理
        lambda x: cv2.threshold(x, 100, 255, cv2.THRESH_TOZERO)[1],
        
        # ぼかし
        lambda x: cv2.GaussianBlur(x, (5, 5), 0),
        
        # モザイク処理
        lambda x: cv2.resize(x, (img_size[1] // 5, img_size[0] // 5)),
        
        # 収縮
        lambda x: cv2.erode(x, filter1)
    ])
    
    # 関数と画像を引数に、加工した画像を元と合わせて水増しする
    doubling_images = lambda f, imag: (imag + [f(i) for i in imag])
    
    # doubling_imagesを用いてmethodsがTrueの関数で画像データ(images)を水増
    for func in scratch[methods]:
        images = doubling_images(func, images)
    
    return images



def all_acratch_image(folder_path):
    """ 指定したフォルダの全画像を水増しする

    Args:
        folder_path (str): フォルダパス 例)./img/sanrio
    """

    # フォルダ配下のファイルを全て読み込む
    files = glob.glob(folder_path + "/*")
    # 取得したファイルのループ
    for i, file in enumerate(files):
        # 画像の読み込み
        cat_img = cv2.imread(file)

        # 画像の水増し
        scratch_cat_images = scratch_image(cat_img)

        for num, im in enumerate(scratch_cat_images):
            # まず保存先のディレクトリ"scratch_images/"を指定、番号を付けて保存
            cv2.imwrite(folder_path + "/image_" + str(i) + "_" + str(num + 1) + ".jpg" , im) 


if __name__ == "__main__":
    # ディズニー(教師用)
    all_acratch_image("./img/disney")
    # サンリオ(教師用)
    all_acratch_image("./img/sanrio")

    # ディズニー(テスト用)
    all_acratch_image("./img/disney_test")
    # サンリオ(テスト用)
    all_acratch_image("./img/sanrio_test")

上記を実行した結果、ディズニー、サンリオの画像が3,300枚ずつに水増しされた(教師用データ、テスト用データ含む)。

3.3 転移学習を利用して画像識別モデルを作る

今回は転移学習としてVGG16モデルを利用する。
VGGの特徴抽出部分のみを用いてそれ以降のモデルは自分で作成したモデルと結合させる。
精度計測に使うテスト用データについては、前述の通り水増し前に分けておいたデータを使った。

import cv2
import numpy as np
import glob as glob
from sklearn.model_selection import train_test_split
from keras.utils import np_utils
from keras.applications.vgg16 import VGG16
from keras.models import Sequential, Model
from keras.layers import Input, Dense, Flatten
import pickle
from tensorflow.keras import optimizers
import matplotlib.pyplot as plt


# 設定
classes = ["disney", "sanrio"]   # ラベル(分類名)
num_classes = len(classes)      # ラベル数
image_size = 50                 # イメージをリサイズするサイズ


# 教師用画像を読み込む
X_train = []
y_train = []
# フォルダ(クラス名)のループ
for index, classlabel in enumerate(classes):
    # フォルダ配下のファイルを全て読み込む
    photos_dir = "./img/" + classlabel
    files = glob.glob(photos_dir + "/*")
    # 取得したファイルのループ
    for i, file in enumerate(files):
        img = cv2.imread(file)                                  # ファイルの読み込み
        img = cv2.resize(img,dsize=(image_size, image_size))    # リサイズ
        X_train.append(img)                                     # 画像をappend
        y_train.append(index)                                   # ラベル(クラス名)をappend

# 正規化
X_train = np.array(X_train)
X_train = X_train.astype('float32')
X_train /= 255.0

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


# テスト用画像を読み込む
X_test = []
y_test = []
# フォルダ(テスト用)のループ
for index, classlabel in enumerate(classes):
    # フォルダ配下のファイルを全て読み込む
    photos_dir = "./img/" + classlabel + "_test"
    files = glob.glob(photos_dir + "/*")
    # 取得したファイルのループ
    for i, file in enumerate(files):
        img = cv2.imread(file)                                  # ファイルの読み込み
        img = cv2.resize(img,dsize=(image_size, image_size))    # リサイズ
        X_test.append(img)                                     # 画像をappend
        y_test.append(index)                                   # ラベル(クラス名)をappend

# 正規化
X_test = np.array(X_test)
X_test = X_test.astype('float32')
X_test /= 255.0

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


# VGG16をロード
input_tensor = Input(shape=(image_size, image_size, 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(Dense(256, activation='relu'))
top_model.add(Dense(num_classes, activation='softmax'))


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


# 学習させない層(呼び出したVGG16は19層だが、下位4層+追加した層を学習させる設定)
for layer in model.layers[:15]:
   layer.trainable = False

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

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

model.summary()

print("学習データで学習==================================================")

# 学習データで学習
# 5, 32
epochs = 5
batch_size = 32
history = model.fit(X_train, y_train, epochs=epochs, batch_size=batch_size)



print("テストデータで精度確認==================================================")

# テストデータで精度確認
score = model.evaluate(X_test, y_test, batch_size=batch_size)


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


# 学習曲線を表示
x = range(epochs)
plt.plot(x, history.history['accuracy'], label="accuracy", color="b")
plt.plot(x, history.history['loss'], label="loss", color="r")
plt.title("accuracy")
plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))
plt.show()

ハイパーパラメータとして、バッチサイズ、エポック数の値を色々と試行錯誤した。
結果は以下の表の通り。

汎化性能評価(val_accuracy)

epoch=5, batch_size=32 が一番精度が良い結果になった。

epoch=10, batch_size=32も精度が良かったのでepoch=20も試してみたが、
こちらは逆に精度が下がる結果になった。
おそらく、過学習が起こったのだと思われる。


3.4 Webアプリケーション作成

3.3 で作成したモデルファイル(cnn.h5)を利用して画像の判定行うWebアプリケーションを作成する
今回は簡易的なWebを作成するので、Flaskを利用する

フォルダ構成は以下の通り。

フォルダ構成

templates/index.html
必要最低限、ファイルアップロードができるようにする

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>ディズニー、サンリオ画像判別</title>
    <link rel="stylesheet" href="./static/stylesheet.css">
</head>
<body>
    <header>   
        <a class="header-logo" href="#">ディズニー、サンリオ画像判別</a>
    </header>

    <div class="main">    
        <h2> AIが送信された画像がディズニーかサンリオなのか識別します</h2>
        <p>画像を送信してください</p>
        <form method="POST" enctype="multipart/form-data">
            <input class="file_choose" type="file" name="file">
            <input class="btn" value="submit!" type="submit">
        </form>
        <div class="answer">{{answer}}</div>
    </div>

    <footer>
        
    </footer>
</body>
</html>

mnist.py
アップロードされた画像に対して、cnn.h5のモデルを呼び出し、画像認識を行う。

import os
from flask import Flask, request, redirect, render_template, flash
from werkzeug.utils import secure_filename
from tensorflow.keras.models import Sequential, load_model
from tensorflow.keras.preprocessing import image
import secrets

import numpy as np
import cv2


classes = ["disney", "sanrio"]
image_size = 50

UPLOAD_FOLDER = "uploads"
ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif'])

app = Flask(__name__)
secret = secrets.token_urlsafe(32)
app.secret_key = secret

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

model = load_model('./cnn.h5')    #学習済みモデルをロード


@app.route('/', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        if 'file' not in request.files:
            flash('ファイルがありません')
            return redirect(request.url)
        file = request.files['file']
        if file.filename == '':
            flash('ファイルがありません')
            return redirect(request.url)
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            file.save(os.path.join(UPLOAD_FOLDER, filename))
            filepath = os.path.join(UPLOAD_FOLDER, filename)

            #受け取った画像を読み込み、np形式に変換
            img = cv2.imread(filepath)                                  # ファイルの読み込み
            img = cv2.resize(img,dsize=(image_size, image_size))    # リサイズ
            data = np.array([img])
            #変換したデータをモデルに渡して予測する
            result = model.predict(data)[0]
            predicted = result.argmax()
            pred_answer = "これは " + classes[predicted] + " です"

            return render_template("index.html",answer=pred_answer)

    return render_template("index.html",answer="")


if __name__ == "__main__":
    port = int(os.environ.get('PORT', 8080))
    app.run(host ='0.0.0.0',port = port)

サンリオの画像が認識できるか実行してみた。


正しく認識することができている

ディズニーの認識も行ってみたが、正しく認識できている

逆に、このような複数キャラクターがいるような写真の場合は正しく認識されず、「sanrio」となるべきところ、「disney」として認識されてしまった。

4.総括

今回は「ディズニー」か「サンリオ」かという識別にしたが、ソースを少し変更するだけで、「犬種の識別」、「芸能人の名前識別」など色々応用ができそうなものが作成できた。

また、おおざっぱな分類の後で詳細な分類モデルを動かすのもいいかもしれない。
例えば「犬か犬以外か」というモデルで最初に識別を行い、犬の場合は、2つ目のモデル「犬種の識別」を活用する、といった活用方法だ。

反省点としては、テストデータの精度が約70%程度だったのが悔しいところ。
実用的なモデルは90%以上と言われているので次回はそこを目指したい。
学習用データのクレンジングを行っていないので、事前のデータクレンジングをすればもう少し上がるものと思われる。

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