宝塚男役トップスターの舞台化粧顔と素化粧顔は、どれぐらい違う!?

# 目次
* はじめに
* 宝塚歌劇団について
* テーマ
* 実行環境
* 学習の流れ
* 実践！
    * スクレイピング
    * 学習&テスト用データセットの作成
    * モデル学習
    * 精度の検証
* まとめと考察
* アプリはこちら

# はじめに

記事を目に留めて下さり有難うございます。人生で初めてブログなるものを書きました。


 **自己紹介**
高校生の時に好きになった初めての贔屓が退団して抜け殻となり、
昨年の月組公演「グレート・ギャツビー」で月城沼に足を取られてヅカオタに返り咲いた宝塚ファンです。


以前から機械学習に興味があり、AidemyさんのAIアプリ講座でpythonの基本から勉強した成果をまとめてみました。
プログラミング自体が未経験で、最初は簡単なif文すら怪しい状態でしたが、3か月間でここまでできるようになった！という軌跡を見て頂けたら嬉しいです。

# 宝塚歌劇団（来年は創立110周年！！）について
宝塚歌劇団は1914年に結成された、未婚の女性のみで構成される歌劇団

花・月・雪・星・宙の5組と、スペシャリストで構成される専科に分かれている

兵庫県宝塚市にある宝塚大劇場と、東京都の日比谷にある東京宝塚劇場の専用劇場を持つ


宝塚歌劇団公式サイト↓
https://kageki.hankyu.co.jp/

# テーマ
どんなテーマでアプリを作るのか正直悩みました。仕事絡みで、「AlphaFold2」という蛋白質の立体構造を計算してくれるツールに興味があって、いつか使いこなせたらな～というのも勉強を始めた動機の1つだったので、仕事に少し寄せた内容にしようかなとも思いました。
…が、アルゴリズムを見て眩暈がしたので、思いっきり趣味に走りました。

やったこととしては、
**花・月・雪・星・宙の5組に所属する男役トップスター5名の舞台化粧顔を、機械学習モデルに学習させる
⇒素化粧顔を正しく判別できるか？** 
というものです。

舞台化粧は、同一人物でも洋物や日本物で目の描き方なども全然違うし、日本物だと見慣れているはずの顔も「あれ誰？」（~~単純に私の顔判別能力が低いだけかもしれない~~）と混乱することがあるので、
和洋混合の舞台化粧顔を使って学習したモデルで素化粧顔を判別させるとなると、一体どうなることやらと先行きが既に不安ですが、初めて作った機械学習モデルでどれぐらいの精度を出せるのか楽しみです。

# 学習の流れ

**１．スクレイピング**
web自動化ツールの**selenium**を使って、googleから5組の男役トップスター

花組：柚香光
月組：月城かなと
雪組：彩風咲奈
星組：礼真琴
宙組：芹香斗亜

の写真を集めてきました。

10分で理解するSelenium
https://qiita.com/Chanmoro/items/9a3c86bb465c1cce738a

柚香光
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3517930/4f9a280e-3057-6b95-b1c5-5c2bd5e435a8.png)

月城かなと
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3517930/080436c4-e05d-d531-63bc-24e22e87d127.png)

彩風咲奈
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3517930/0c6329e5-0b66-68a6-ae1e-83de7edb9a1c.png)

礼真琴
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3517930/ed641bb7-6d56-539c-0381-53a9880e72c0.png)

芹香斗亜
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3517930/3d067aef-4239-8440-0225-23801c5b95fc.png)

皆さん素敵ですね( *´艸｀)

宝塚歌劇団公式HP スタープロフィール
https://kageki.hankyu.co.jp/star/index.html







**２．学習＆テスト用データセットの作成**
顔の部分だけをトリミングした加工写真を作り、トップスター5名の舞台化粧顔と素化粧顔に分けてローカル環境に保存しました。

**３．モデル学習**
 学習用データ数が少なく、**VGG16** を使って転移学習を行いモデルを構築しました。
CNN（Convolutional Neural Network）とは、人間の脳の視覚野と似た構造を持つ 「畳み込み層」 という層を使って特徴抽出を行うニューラルネットワークです。

以下引用
>VGG16は、オックスフォード大学の研究グループが2014年に発表したCNNモデルです。
シンプルな構造ながらImagenetの画像認識コンペで2位を取った高精度なモデルで、今もKerasやPytorchに学習済みモデルが用意されています。

https://qiita.com/ps010/items/dee9413d3de28de7d2f9

**４．精度の検証**
構築したモデルの完成度はいかに？



では、実際に書いてみたコードを順に載せていきます。
もしここはこう書くといいよ、みたいなコメントを頂けたらとても喜びます…！

# 実践!
## スクレイピング
**1. 前準備**
初めに環境構築を行います。
```python
%%shell

sudo apt -y update

# ダウンロードのために必要なパッケージをインストール
sudo apt install -y wget curl unzip
# 以下はChromeの依存パッケージ

wget http://archive.ubuntu.com/ubuntu/pool/main/libu/libu2f-host/libu2f-udev_1.1.4-1_all.deb
dpkg -i libu2f-udev_1.1.4-1_all.deb

# Chromeのインストール
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
dpkg -i google-chrome-stable_current_amd64.deb

# Chrome Driverのインストール
CHROME_DRIVER_VERSION=`curl -sS chromedriver.storage.googleapis.com/LATEST_RELEASE`
wget -N https://chromedriver.storage.googleapis.com/$CHROME_DRIVER_VERSION/chromedriver_linux64.zip -P /tmp/
unzip -o /tmp/chromedriver_linux64.zip -d /tmp/
chmod +x /tmp/chromedriver
mv /tmp/chromedriver /usr/local/bin/chromedriver
```

Google ChromeとChromeDeiverのバージョンを確認。

```python
!google-chrome --version
!chromedriver --version
```
Google Chromeのバージョンは114.0.5735.198
ChromeDriverのバージョンは114.0.5735.90
でした。
ChromeDriverのバージョンは、Google Chromeよりも数値が小さくなっているので問題なさそうです。
https://prtn-life.com/blog/chromedriver



最後にseleniumをインストール。
```python
!pip install selenium
```


環境構築ができましたので、スクレイピング開始です。

**2. Googleから画像を取ってくる**
まずは必要なモジュールをインポート。
```python
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time
import requests
import os
from selenium.common.exceptions import ElementNotInteractableException


options = webdriver.ChromeOptions()
options.add_argument("--headless")
options.add_argument('--disable-dev-shm-usage')
options.add_argument("--no-sandbox")

wd = webdriver.Chrome(
    options=options
)
```
GoogleのURLを取得し、ページの最下部までスクロールして要素を全て取得。
```python
url = 'https://www.google.co.jp/search?q=%E8%88%9E%E7%A9%BA+%E7%9E%B3&tbm=isch' #各スターのURL
wd.get(url)
print(wd.current_url)

for _ in range(10):
    wd.execute_script("window.scrollTo(0,document.body.scrollHeight);")
    time.sleep(1)
    next_btn = wd.find_element(By.XPATH, '//*[@id="islmp"]//input[@type="button"]')
    try:
        next_btn.click()
        time.sleep(1)
    except ElementNotInteractableException:
        pass

img_elems = wd.find_elements(By.XPATH,"//img[contains(@class,'Q4LuWd')]")
print(len(img_elems))
```

要素のURLを保存。
```python
img_urls = []
MAX = 700 # 取れてくる写真の枚数が700枚を超えることはなかったので固定
for e in img_elems[:MAX]:
    try:
        e.click() # click to activate the src attribute
        url = WebDriverWait(wd, 20).until(EC.visibility_of_element_located((By.XPATH, f'//*[@id="Sva75c"]/div[2]/div[2]/div[2]/div[2]/c-wiz/div/div/div/div[3]/div[1]/a/img[1]'))).get_attribute("src")
        print(f'url: {url}')
        if url.startswith('http'):
            print(f'filtered url: {url}')
            img_urls.append(url)
    except Exception as e:
        # print(e)
        print('Error!')
        pass
```
これで、Googleの画像検索で出てきた画像のURLを取ってくることができました。
続いてディレクトリを作って、URLから画像を取得し保存していきます。

```python
import random
import re
import string

## 保存先
DESTINATION_FOLDER = 'images/maisora'

# ディレクトリを作成
if not os.path.exists(DESTINATION_FOLDER):
    os.makedirs(DESTINATION_FOLDER)

def get_extension(url):
    url_lower = url.lower()
    extension = re.search(r'\.jpg|\.jpeg|\.png', url_lower)
    if extension:
        return extension.group()
    elif 'encrypted-tbn0.gstatic.com' in url_lower:
        return '.jpg'
    else:
        return '.jpg'

def randomname(n):
   randlst = [random.choice(string.ascii_letters + string.digits) for i in range(n)]
   return ''.join(randlst)

def down_load_image(url, save_dir, loop, http_header):
    result = False
    for i in range(loop):
        try:
            r = requests.get(url, headers=http_header, stream=True, timeout=10)
            r.raise_for_status()

            extension = get_extension(url)
            file_name = randomname(12)
            file_path = save_dir + '/' + file_name + str(extension)

            with open(file_path, 'wb') as f:
                f.write(r.content)

            print(f'{url}の保存に成功')
            print(f'{file_path}に保存しました')

        except requests.exceptions.SSLError:
            print('*****SSLエラー*****')
            break

        except requests.exceptions.RequestException as e:
            print(f'***** requests エラー ({e}): {i+1} 回目')
            time.sleep(1)
        else:
            result = True
            break
    return result


HTTP_HEADERS = {'User-Agent': wd.execute_script('return navigator.userAgent;')}


for image_url in img_urls:
    down_load_image(image_url, DESTINATION_FOLDER, 3, HTTP_HEADERS)
```

写真は、舞台化粧顔と素化粧顔の写真（＋その他雑多なもの）が各トップスターのディレクトリに大体600-700枚ぐらい取得できました。


## 2. 学習&テスト用データセットの作成
続いて、モデルに学習させるデータセットとテスト用データセットの作成です。

**1. 顔検出器によるトリミング**
今回は顔の判別をしたいのですが、取得した写真には複数人が写っていたり、背景の割合が大きかったりノイズが大きい写真が殆どだったので、
openCVライブラリにパッケージングされているカスケード分類器 CascadeClassifierを使って、顔の部分だけをトリミング加工しました。

```python
import os
import cv2

# 顔検出器の初期化
face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')


FACES_DESTINATION_FOLDER = os.path.join(DESTINATION_FOLDER, 'faces')

# ディレクトリを作成
if not os.path.exists(FACES_DESTINATION_FOLDER):
    os.makedirs(FACES_DESTINATION_FOLDER)

# フォルダ内の画像ファイルを取得
image_files = [f for f in os.listdir(DESTINATION_FOLDER) if f.endswith(('.jpg', '.jpeg', '.png', '.gstatic.com'))]

# 顔の部分を切り出し、保存
for image_file in image_files:
    # 画像のパス
    image_path = os.path.join(DESTINATION_FOLDER, image_file)

    # 画像の読み込み
    image = cv2.imread(image_path)

    # グレースケールに変換
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

    # 顔検出
    faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=6, minSize=(40, 40))

    # 顔の部分を切り出し、保存
    for i, (x, y, w, h) in enumerate(faces):
        face_image = image[y:y+h, x:x+w]
        save_path = os.path.join(FACES_DESTINATION_FOLDER, f'{image_file}_face_{i}.jpg')
        cv2.imwrite(save_path, face_image)
```
ここで顔だけトリミングされた、月組トップスターの月城かなとさんを見てみましょう。

[トリミング前]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3517930/e52726a4-6244-c626-75c8-86bf793b8c25.png)

[トリミング後]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3517930/b65e04cc-cc69-8630-7d20-2300f51e4832.png)

…顔が良い（尊）。

トリミング処理をすることで顔ではない部分が切り出されることもありますが、そういった写真は手動で省いていきます。
今回のテーマは、**「舞台化粧顔で学習したモデルは、素化粧顔を判別できるか？」** という内容なので、
トップスター5名ずつ、舞台化粧顔と素化粧顔に分けて、ローカル環境に1枚ずつ保存していきました。
（ここが一番地道な作業でした。）

学習用データとしてはかなり枚数が少ないのは重々承知ですが、

柚香：61枚
月城：107枚
彩風：123枚
礼：156枚
芹香：89枚

のトリミングされた舞台化粧顔の写真を取得することができました。
柚香さんと芹香さんは、顔検出器を追加でやってみたのですが、うまく写真が集まりませんでした。。。

**2. データセットの作成**
続いて、上で集めてきた写真を使ってモデル学習に使用するデータセットを作成していきます。
学習用もテスト用も作り方は同じなので、ここでは学習用データセットの作り方を下記に記します。

Google Driveにローカル環境に保存していたフォルダをアップロードし、
Google colabolatoryとGoogle Driveを接続しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3517930/66d0ca54-a0b6-5cd3-8e9f-86c195b1fbb4.png)
セルの横に出てくるアイコンの、右から2番目を押すとマウントされます。

必要なモジュールをインポート。（osとcv2は上でインポート済み）
```python
import numpy as np
```

各makeupディレクトリに保存した5組トップスターの写真を、各人の名前の変数に格納。
```python
path_yuzuka = os.listdir('/content/drive/MyDrive/blog_make/face_data/makeup_yuzuka/')
path_tsukishiro = os.listdir('/content/drive/MyDrive/blog_make/face_data/makeup_tsukishiro/')
path_ayakaze = os.listdir('/content/drive/MyDrive/blog_make/face_data/makeup_ayakaze/')
path_rei = os.listdir('/content/drive/MyDrive/blog_make/face_data/makeup_rei/')
path_serika = os.listdir('/content/drive/MyDrive/blog_make/face_data/makeup_serika/')
```
写真を50×50ピクセルにリサイズし、色調を調整してリストに格納。
```python
# trainデータを作成するための空リストを変数にセット
train_yuzuka = []
train_tsukishiro = []
train_ayakaze = []
train_rei = []
train_serika = []

# 柚香光
for i in range(len(path_yuzuka)):
    img = cv2.imread('/content/drive/MyDrive/blog_make/face_data/makeup_yuzuka/' + path_yuzuka[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (50,50))
    train_yuzuka.append(img)

# 月城かなと
for i in range(len(path_tsukishiro)):
    img = cv2.imread('/content/drive/MyDrive/blog_make/face_data/makeup_tsukishiro/' + path_tsukishiro[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (50,50))
    train_tsukishiro.append(img)

# 彩風咲奈
for i in range(len(path_ayakaze)):
    img = cv2.imread('/content/drive/MyDrive/blog_make/face_data/makeup_ayakaze/' + path_ayakaze[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (50,50))
    train_ayakaze.append(img)

# 礼真琴
for i in range(len(path_rei)):
    img = cv2.imread('/content/drive/MyDrive/blog_make/face_data/makeup_rei/' + path_rei[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (50,50))
    train_rei.append(img)

# 芹香斗亜
for i in range(len(path_serika)):
    img = cv2.imread('/content/drive/MyDrive/blog_make/face_data/makeup_serika/' + path_serika[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (50,50))
    train_serika.append(img)
```

0:柚香光 1:月城かなと 2:彩風咲奈 3:礼真琴 4:芹香斗亜 と教師ラベルを付けた学習用データセットを作り、データをシャッフル。
```python
#データセットの作成
X_train = np.array(train_yuzuka + train_tsukishiro + train_ayakaze + train_rei + train_serika )
y_train = np.array([0]*len(train_yuzuka) + [1]*len(train_tsukishiro) + [2]*len(train_ayakaze) + [3]*len(train_rei) + [4]*len(train_serika))

#シャッフル
rand_index = np.random.permutation(np.arange(len(X_train)))
X_train = X_train[rand_index]
y_train = y_train[rand_index]
```


これで、学習用データセットが完成しました。同じようにテスト用データセットも作成しておきます。
完成したデータセットのディレクトリ構造はこちら。

学習用データセットのディレクトリ構造
```
content/
├drive/
    └
    Mydrive/
        └
        blog_make/
            └
            face_data/
            ├ makeup_yuzuka
            ├ makeup_tsukishiro
            ├ makeup_ayakaze
            ├ makeup_rei
            ├ makeup_serika
    
```
テスト用データセットは、同じ構造でフォルダ名をnatural_スター名としました。


最後に、教師ラベルのone-hotエンコーディング処理。
```python
from tensorflow.keras.utils import plot_model, to_categorical

y_train = to_categorical(y_train, 5)
y_test = to_categorical(y_test, 5)
```
男役トップスターは5名いるので、クラス分けは5つです。
後ほど作成するモデルを使って素化粧顔を判定させるときに、

[1, 0, 0, 0, 0]なら 柚香光
[0, 1, 0, 0, 0]なら 月城かなと
[0, 0, 1, 0, 0]なら 彩風咲奈
[0, 0, 0, 1, 0]なら 礼真琴
[0, 0, 0, 0, 1]なら 芹香斗亜

という風に判定できるようにしておきます。


## 3. モデル学習
データセットが作成できたところで、モデル学習に移っていきます。

参考文献（最初にこちらで型を作りました）
https://qiita.com/ykoji/items/e9c2c5d7288c6290d21b

必要なモジュールをインポート。
```python
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Conv2D
from tensorflow.keras.layers import MaxPool2D
from tensorflow.keras.optimizers 

from tensorflow.keras.layers import Dense, Activation, Dropout, Flatten
from tensorflow.keras.applications.vgg16 import VGG16
from keras.callbacks import TensorBoard
```
モデルオブジェクトを定義し、.add()メソッドでレイヤーを追加。
```python
model = Sequential()

model.add(Flatten(input_shape=vgg16.output_shape[1:]))
model.add(Dense(1024))
model.add(Activation('relu'))
model.add(Dropout(0.4))

model.add(Dense(5, activation='softmax')) # 5名いるので5クラス分類
```
vgg16と構築したmodelを連結。
VGG16の重みが更新されないようにしておきます。

```python
model = Model(inputs=vgg16.input, outputs=model(vgg16.output))

for layer in model.layers[:19]:
    layer.trainable = False
```




学習のためのモデルを設定。

```python
# 最適化手法はSGDを採用
model.compile(loss='categorical_crossentropy',
              optimizer=optimizers.SGD(lr=1e-4, momentum=0.9),
              metrics=['accuracy'])
```


## 4. 精度の検証
3.で作ったモデルを使って、いよいよ顔の学習を実行していきます。
さて結果は…
```python
history = model.fit(X_train, y_train, validation_data=(X_test, y_test), batch_size=32, epochs=20)
```

どーん！！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3517930/2b57958a-c241-9089-ac8d-ea5c65f8e2ff.png)



…
…

accuracyは学習データに対する精度、val_accuracyはテスト用データに対する精度を表しています。
Epochが進むとaccuracyはいったん上がり、あとはずっと横ばいです。
val_accuracyに至っては学習前が一番精度が高く出ています。

結果として素化粧の顔に対する正解率は2割弱という、お察しクオリティのものが出来上がってしまいました。
これでは贔屓の顔もろくに判別できない状態です。
れいこちゃん（月城さん）、ごめん…💧


グラフとしてプロットするとこんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3517930/09185925-3fe9-dded-9620-bd54836b674f.png)


ちなみに、転移学習をせずに普通のCNNで学習をしたときはこのようになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3517930/da69a4cd-0462-2b45-5d06-010101f774e9.png)

Epochが進むに従ってaccuracyは上がっていますが、val_accuracyはずっと横ばいです。
つまり、舞台化粧顔に特化したモデルになってしまい、明らかに過学習状態であることが見て取れます。

この後精度を改善すべくmobilenetを使った転移学習も試みましたが、
精度は特に変わらず。。。機械学習は難しいです。

mobilenetを使った転移学習の結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3517930/562c0189-01a0-de89-bebf-272420425d70.png)



最後に、顔を判定する関数。
```python
labels_dict = {0:'柚香光', 1:'月城かなと', 2:'彩風咲奈', 3:'礼真琴', 4:'芹香斗亜'}

def pred_faces(img):
    img = cv2.resize(img, (50, 50))
    pred_idx = np.argmax(model.predict(np.array([img])))
    return label_dict.get(pred_idx)
```

# まとめと考察
初めて作ったモデルの精度はイマイチだったものの、
ゴールデンウィークの頃はpythonの基本構文もまともに書けなかった状態から、3か月で何とか機械学習をするところまで進むことができました！
モデルを作るにあたって色々調べたところ、機械学習界隈は進化が早く、次々と新しいモデルが出てくることを知りました。勉強のし甲斐がありそうです。
今回の学習用のデータとテスト用のデータは、同一人物だけどちょっと毛色の異なるデータを使用していたので、
距離学習なんかも面白そうだなと思いました。

距離学習
https://qiita.com/tancoro/items/8d3438cab574a02319cc

2つの物が同じなのか、異なるものなのかを判定できるみたいです。

最後まで読んで頂き有難うございました！


# アプリはこちら





**トップスターの素化粧の顔を入れてみてください↓**
https://blog-app2-4xfd.onrender.com/


ちなみにこの写真を入れてみたら、
https://www.sankei.com/smp/west/news/180613/wst1806130059-s1.html
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3517930/df075bc3-ee85-3074-3f72-34a50ae8ddc2.png)
↓
↓
↓

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3517930/baf375ba-1e66-3a26-32d8-0c32aa33c0b2.png)

一応正解しました！ただ、正解率は20%を切っているのでまぐれですね。
これからも勉強を続けていきます。