動画サムネイルからYoutuber当てるAIを作った

#きっかけ
![hikakin.png](https://qiita-image-store.s3.amazonaws.com/0/104983/4141058e-93eb-2709-30ca-4fe9beb75f98.png)

ヒカキンさんの動画のサムネイルって、自分の変顔のアップばっかりやな…
↓
これは機械学習できる

#対象のYoutuber
今回はUUUM所属で、登録者数の多い5名を選ばせていただきました。

- [はじめしゃちょー](https://www.youtube.com/channel/UCgMPP6RRjktV7krOfyUewqw)
- [ヒカキン](https://www.youtube.com/channel/UCZf__ehlCEBPop-_sldpBUQ)
- [フィッシャーズ](https://www.youtube.com/channel/UCibEhpu5HP45-w7Bq1ZIulw)
- [水溜りボンド](https://www.youtube.com/channel/UCpOjLndjOqMoffA-fr8cbKA)
- [東海オンエア](https://www.youtube.com/channel/UCutJqz56653xV2wwSvut_hQ)

全員男性Youtuberとなりますが、メンバーの人数が違っていて、
はじめしゃちょー、ヒカキンは1名、
水溜りボンドは2名、
東海オンエアは6名、
フィッシャーズは7名、
となります。

サムネイルにはメンバーの画像が写っている場合が多いので、人数の違いは一つのカギになりそうです。

#手順
1. Youtube Data APIでサムネイル画像を取得
2. 学習用データの作成
3. モデルの構築・訓練・評価

##1. Youtube Data APIでサムネイル画像を取得
まずはAPIにアクセスするために、APIキーを取得する必要があります。
Googleの管理画面からすぐに払い出せますので、簡単です。
手順は以下の記事が参考になると思います。
https://www.plusdesign.co.jp/blog/?p=7752

Youtube Data APIにはいろいろなリソースがありますが、Searchリソースのlistメソッドを使うと、特定のチャンネルに紐づく動画の情報を取得することができます。
[公式のリファレンス](https://developers.google.com/youtube/v3/docs/search/list?hl=ja)がありますので、こちらをもとに実装します。

```python:get_video_list.py
from apiclient.discovery import build
import urllib.request as req

DEVELOPER_KEY = ""
YOUTUBE_API_SERVICE_NAME = "youtube"
YOUTUBE_API_VERSION = "v3"

channel_id = ""
youtuber_name = ""
next_page_token = ""

def youtube_saerch_video():
  youtube = build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION, developerKey=DEVELOPER_KEY)

  videos_response = youtube.search().list(
    part="snippet",
    channelId=channel_id,
    maxResults=50,
    order="date",
    pageToken= next_page_token
  ).execute()
  print(videos_response["nextPageToken"])
  thumbnail_list = []

  for result in videos_response.get("items", []):
    thumbnail_url = result["snippet"]["thumbnails"]["default"]["url"]
    try:
      video_id = result["id"]["videoId"]
    except:
      print("エラー発生:" + thumbnail_url)
      pass
    else:
      req.urlretrieve(thumbnail_url, "./image/" + youtuber_name + "/" + result["id"]["videoId"] + ".jpg")
    print("Saved:" + thumbnail_url)

if __name__ == "__main__":
  youtube_saerch_video()

```

一回に取得できる件数はmaxResultsというパラメータで指定していますが、これは最大で50件までしか設定できません。それ以上取得する場合は、レスポンスの中にnext_page_tokenという値があり、6文字の英字が格納されているので、それをリクエストの際のpageTokenパラメータに指定します。そうすることで次の50件の動画の情報を取得することができます。

今回はYoutuberごとに200枚ずつ、計1000枚の画像をダウンロードします。

↓ダウンロードした画像
![folder.png](https://qiita-image-store.s3.amazonaws.com/0/104983/55187680-772f-27c0-f3de-afd4f3512e99.png)


##2.学習用データの作成
サムネイルはすべて同じサイズで取得されますので、トリミングは不要です。画像サイズは横120px*縦90pxで上下に黒い帯がついているので、やや小さいかなという気がしますが、許容範囲かと思います。
特に画像処理はせず、そのままNumPy配列化します。

```python
from PIL import Image
import os, glob
import numpy as np
import random, math

#設定
root_dir = "./image/"
categories = ["はじめしゃちょー","ヒカキン","フィッシャーズ","水溜りボンド","東海オンエア"]
nb_classes = len(categories)

# 画像データの読み込み
X = [] # 画像データ
Y = [] # ラベルデータ
def add_sample(cat, fname, is_train):
    img = Image.open(fname)
    img = img.convert("RGB")
    data = np.asarray(img)
    X.append(data)
    Y.append(cat)

    #テスト用データであれば、水増しせずに終了
    if not is_train: return

    # 3度ずつ角度を変えて、10枚データを追加
    for ang in range(-5, 5):
        img2 = img.rotate(ang*3)
        data = np.asarray(img2)
        X.append(data)
        Y.append(cat)

def make_sample(files, is_train):
    global X, Y
    X = []; Y = []
    for cat, fname in files:
        add_sample(cat, fname, is_train)
    return np.array(X), np.array(Y)

# ディレクトリごとに分けられたファイルを収集する
allfiles = []
for idx, cat in enumerate(categories):
    image_dir = root_dir + "/" + cat
    files = glob.glob(image_dir + "/*.jpg")
    for f in files:
        allfiles.append((idx, f))

# シャッフルして学習データとテストデータに分ける
random.shuffle(allfiles)
th = math.floor(len(allfiles) * 0.7)
train = allfiles[0:th]
test  = allfiles[th:]
X_train, y_train = make_sample(train, True)
X_test, y_test = make_sample(test, False)
xy = (X_train, X_test, y_train, y_test)
np.save("./image/youtuber.npy", xy)
rint("ok,", len(y_train))

```

##3.モデルの構築・訓練・評価
一般的なCNNのモデルを構築・構築します。

```python:
from keras.models import Sequential
from keras.layers import Convolution2D, MaxPooling2D
from keras.layers import Activation, Dropout, Flatten, Dense
from keras.utils import np_utils
from keras.metrics import categorical_accuracy
import numpy as np

# 分類対象のカテゴリ
root_dir = "./image/"
categories = ["はじめしゃちょー","ヒカキン","フィッシャーズ","水溜りボンド","東海オンエア"]
nb_classes = len(categories)

# データをロード
def main():
    X_train, X_test, y_train, y_test = np.load(root_dir + "youtuber.npy")
    # データを正規化する
    X_train = X_train.astype("float") / 256
    X_test  = X_test.astype("float")  / 256
    y_train = np_utils.to_categorical(y_train, nb_classes)
    y_test  = np_utils.to_categorical(y_test, nb_classes)
    # モデルを訓練し評価する
    model = model_train(X_train, y_train, X_test, y_test)
    model_eval(model, X_test, y_test)

# モデルを構築
def build_model(in_shape):
    model = Sequential()
    model.add(Convolution2D(32, 3, 3,
	border_mode='same',
	input_shape=in_shape))
    model.add(Activation('relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Dropout(0.25))
    model.add(Convolution2D(64, 3, 3, border_mode='same'))
    model.add(Activation('relu'))
    model.add(Convolution2D(64, 3, 3))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Dropout(0.25))
    model.add(Flatten())
    model.add(Dense(512))
    model.add(Activation('relu'))
    model.add(Dropout(0.5))
    model.add(Dense(nb_classes))
    model.add(Activation('softmax'))
    model.compile(loss='categorical_crossentropy',
	optimizer='adam',
	metrics=['accuracy'])
    return model

# モデルを訓練する
def model_train(X, y, X_test, y_test):
    model = build_model(X.shape[1:])
    model.fit(X, y, batch_size=32, epochs=10, validation_data=(X_test, y_test))
    # モデルを保存する
    hdf5_file = root_dir + "youtuber.hdf5"
    model.save_weights(hdf5_file)
    return model

# モデルを評価する
def model_eval(model, X, y):
    score = model.evaluate(X, y)
    print('loss=', score[0])
    print('accuracy=', score[1])

if __name__ == "__main__":
    main()

```

実行結果：

```python
Epoch 1/5
7623/7623 [==============================] - 147s 19ms/step - loss: 1.3953 - acc: 0.4392 - val_loss: 1.3240 - val_acc: 0.5604
Epoch 2/5
7623/7623 [==============================] - 148s 19ms/step - loss: 0.5473 - acc: 0.8019 - val_loss: 2.0669 - val_acc: 0.5235
Epoch 3/5
7623/7623 [==============================] - 149s 20ms/step - loss: 0.1940 - acc: 0.9319 - val_loss: 2.4804 - val_acc: 0.5369
Epoch 4/5
7623/7623 [==============================] - 147s 19ms/step - loss: 0.0910 - acc: 0.9698 - val_loss: 2.7755 - val_acc: 0.5403
Epoch 5/5
7623/7623 [==============================] - 151s 20ms/step - loss: 0.0940 - acc: 0.9700 - val_loss: 2.9549 - val_acc: 0.5503
298/298 [==============================] - 2s 5ms/step
loss= 2.9549226264825603
accuracy= 0.550335571269861
```
正直言ってあまり上手く行きませんでした。
学習が進むたびにテストデータのlossが大きくなっているので、過学習の可能性があります。
とはいえ、それでも5種類の分類で**55%**の精度を出す事ができました。

##4.考察
どのYoutuberが上手く予測できていないのかを把握するために、表(Confusion matrix)を作成しました。
行が予測で、列が正解になります。太文字が予測が当たっているケースで、青文字が10件以上間違った予測をしているケースとなります。

|                  | はじめしゃちょー | ヒカキン | フィッシャーズ | 水溜りボンド | 東海オンエア |
|------------------|------------------|----------|----------------|--------------|--------------|
| はじめしゃちょー | **47**               | 1        | 3              | 4            | 2            |
| ヒカキン         | 2                | **37**       | 4              | 6            | 8            |
| フィッシャーズ   | 4                | 8        | **35**             | <font color="Blue">18</font>           | <font color="Blue">11</font>            |
| 水溜りボンド     | 4                | 1        | 7              | **17**           | <font color="Blue">15</font>           |
| 東海オンエア     | 5                | <font color="Blue">10</font>        | <font color="Blue">13</font>             | 8            | **28**           |

基本的にどのYoutuberも予測と正解が合っているケースが多いですが、水溜りボンドがフィッシャーズと予測される誤判定が目立っています。水溜りボンドのサムネイルはいろいろなバリエーションがあることから、予測が難しいように見受けられます。
青文字の分布を見ると、やはり**複数人いるグループ系Youtuberの予測が難しい**ようです。1人でやっているYoutuberであれば、サムネイル内の顔の特徴を学習しやすいですが、全員が写り込んだサムネイルになるとどうしても一人一人が小さくなってしまうため、そのあたりが影響しているものと思われます。
次回は画像を増やした上で、大きめのサムネイル画像で学習してみたいと思います。

#5.おまけ
![hqdefault.jpg](https://qiita-image-store.s3.amazonaws.com/0/104983/83d03b8e-f3d5-3e06-b0e5-e78dbc98a031.jpeg)
正解＝ヒカキン
予測＝水溜りボンド
↓
ヒカキン＋セイキン＝水溜りボンド

機械学習うんぬんより、ヒカキンさんのサムネ芸がすごいという感想しかない。

#引用
Youtubeよりサムネイル画像を引用しております。
https://www.youtube.com/