高本彩花と松本潤を機械学習で判別してみた！

# はじめに
去年の2月、「日向坂で会いましょう」で日向坂46のメンバーが男装して、朗読劇をするというバレンタイン企画が放送されました。番組を観てて思ったのが、日向坂46のメンバーの「おたけ」こと高本彩花ちゃんの男装が、嵐のメンバーの「松潤」こと松本潤さんにそっくりであるということです。役名は「ジュンヤ」していました（笑）。
![test4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2810794/c00bda96-3d93-0fd9-e23f-fdf7bb97db5f.jpeg)
そこで今回は、「高本彩花と松本潤を機械学習で判別してみた！」と題して、画像分類問題にチャレンジしてみました。

# 概要
今回は以下の手順で画像分類問題に取り組みました。
* 画像収集
* データの加工
* モデルの実装
* 結果の確認

**環境：JupyterNotebook, macOS Monterey (version 12.4.1)**

# 画像収集
[こちらの記事](https://atmarkit.itmedia.co.jp/ait/articles/2010/28/news018.html)を参考に画像を収集しました。
下記のコードで実装しました。
```Python
def scraping(path, keyword, num):
    
    bing_crawler=BingImageCrawler(
    downloader_threads=4,
    storage={'root_dir': path}
    )

    #検索ワードにkeywordを入れたときに得られる画像をnum枚収集
    bing_crawler.crawl(
        keyword=keyword,
        max_num=num
    )
    print(f'{keyword}: scraping completed!')

    
#ファイルの形式はjpegなので、ファイル名には拡張子.jpgがつく
#高本彩花の画像はフォルダ名'ayaka_takamoto', 松本潤の画像はフォルダ名'jun_matsumoto'
ak_path='ayaka_takamoto/*.jpg'
mj_path='jun_matsumoto/*.jpg'

keywords=['高本彩花', '松本潤']
num=300

scraping('ayaka_takamoto/', keywords[0], num)
scraping('jun_matsumoto/', keywords[1], num)
```

実際やってみると300枚も収集できませんでしたが、用意できた枚数の画像でモデルの実装をしました。画像の大きさがバラバラなので、同じ大きさに統一(リサイズ)させました。下記のコードで実装しました。

```Python
"""
画像をリサイズするメソッド
引数は保存したいパスpath=フォルダ名+フォーマット名、変更後のサイズの幅と高さw,h

*リサイズしたい画像はパスで指定される
"""

def resize_image(path, w, h):
    img_paths=glob.glob(path)
    
    for img_path in img_paths:
        #画像ファイルに変換
        img=Image.open(img_path)
        #指定したサイズでリサイズをする
        img_resized=img.resize((w,h))
        
        #リサイズした画像を上書き保存、同じパスを指定
        img_resized.save(img_path)
    print(f'{path}: resized!')

#サイズは300x300で指定
width=300
height=300

resize_image(ak_path, width, height)
resize_image(mj_path, width, height)
resize_image(test_path, width, height)
```
# データの加工
画像データを扱う際、数字に変換しないとコンピュータ側では処理ができません。そこで画像データを配列に変換して、ラベルをつけてデータセットを作成しました。下記のコードで実装しました。

```Python
"""
画像を配列に変換したデータとして、画像ごとにラベルを割り当てる

引数はファイルのパス
戻り値はnp.array型に変換された画像データ
"""

def make_dataset(img_paths):
    dataset=[]
    
    for img_path in img_paths:
        img=Image.open(img_path)
        img_array=np.asarray(img)
        size=img_array.shape[0]*img_array.shape[1]*img_array.shape[2]
        img_data=img_array.reshape(1,size)
        
        dataset.append(img_data[0])
    dataset=np.array(dataset)
    
    return dataset

ak_df=make_dataset(img_paths=glob.glob(ak_path))
mj_df=make_dataset(img_paths=glob.glob(mj_path))
test_df=make_dataset(img_paths=glob.glob(test_path))

#データフレーム型に変換して結合
ak_df=pd.DataFrame(ak_df)
mj_df=pd.DataFrame(mj_df)
ak_df['cluster']='0'
mj_df['cluster']='1'
df=pd.concat([ak_df, mj_df], axis=0)

test_df=pd.DataFrame(test_df)

#目的変数と説明変数に分割
from sklearn.model_selection import train_test_split

X=df.drop(['cluster'],axis=1)
y=df['cluster']

test=test_df

#訓練データと検証データに分割
train_X, val_X, train_y, val_y=train_test_split(X, y, test_size=0.3, random_state=0)
```
ラベルが0のデータは高本彩花の画像、ラベル1のデータは松本潤の画像としてモデルの学習を行いました。

# ランダムフォレストで実装
ハイパーパラメータの調整はおこなわず、デフォルトの設定で実装しました。
計算時間は約30秒でした。

```Python
from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import confusion_matrix

rfr = RandomForestClassifier()
rfr.fit(train_X, train_y)
score = rfr.score(val_X, val_y)
predict = rfr.predict(test_X)
print("")
print("スコア:", score)
```
出力結果
```
スコア: 0.8560606060606061
```

どれだけうまく分類できているかは混同行列で確認しました。
```
#訓練用
confusion_matrix(y_true=train_y, y_pred=rfr.predict(train_X))
```
出力結果(訓練用)
```
array([[160,   0],
       [  0, 146]])
```

```
#検証用
confusion_matrix(y_true=val_y, y_pred=rfr.predict(val_X))
```
出力結果（検証用）
```
array([[56, 11],
       [ 8, 57]])
```

# lightGBMで実装
こちらもハイパーパラメータの調整はおこなわず、デフォルトの設定で実装しました。
計算時間は約10分でした。
```Python
import lightgbm as lgb

lgb_clf=lgb.LGBMClassifier()
lgb_clf.fit(train_X, train_y)

score = lgb_clf.score(val_X, val_y)
predict = lgb_clf.predict(val_X)
print("")
print("スコア:", score)
```
出力結果
```
スコア: 0.7703703703703704
```
こちらもどれだけうまく分類できているかは混同行列で確認しました。
```
#学習用
confusion_matrix(y_true=train_y, y_pred=lgb_clf.predict(train_X))
```
出力結果(学習用)
```
array([[157,   0],
       [  0, 158]])
```
```
#訓練用
confusion_matrix(y_true=val_y, y_pred=lgb_clf.predict(val_X))
```
出力結果（訓練用）
```
array([[56, 14],
       [17, 48]])
```


**ランダムフォレストでもlightGBMでも誤判別された検証用画像の一部です。**
![スクリーンショット 2022-09-11 17.35.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2810794/c5603b02-09cf-2fe5-7cd5-64194b8a73da.png)
![スクリーンショット 2022-09-11 17.36.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2810794/19a046df-476c-94a8-69e3-485a094edb34.png)
♪ソンナコトナイヨ〜　僕はそう思わない
って言いたいところだが、目元は似てると思う

# 結果の確認
果たして男装したおたけの画像4枚は判別されるのか？
![スクリーンショット 2022-09-11 18.05.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2810794/81607c3a-b28f-0c80-32b6-4ff3cbcbcc4e.png)
![スクリーンショット 2022-09-11 18.05.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2810794/d0038a21-b75e-6e68-b7dc-69fb562bdcf9.png)

ランダムフォレストでもlightGBMでも正しく判別できてました！
結果、おたけの部分しか出てきてなかったのか（笑）

# まとめ
気に入った画像を、これまではいちいち検索して収集していたが数行のコードを書くだけであっという間にできてしまうことに驚きでした。機械学習コンペでよく用いられているlightGBMは処理に時間がかかった上に、さほど精度は高くなかった。松潤がジュンヤのような髪型をして、おたけと入れ替わってもバレないなんてことが起きてしまったら収録どころではなくなる（現実ではあり得ない）。改めて誤判別が発生しないシステムを開発するのは難しいと感じました。OpenCVを用いたシステム開発にもチャレンジしていきたいです。

# 感想
嵐の松潤、日向坂46のおたけと自分の推し（担当）の画像をたくさん拝みながら勉強しから、とても楽しかったです。
アイドルだけでなく、先日の東京ガールズコレクションなどのファッションショーに出演するモデルとして活躍するおたけは男装してもかっこいい。おたけ含め、今後の日向坂46の活躍から目が離せない。8thシングルでは一番の推しメンのきょんこ（齊藤京子ちゃん）がセンターなので楽しみです♪

# 参考元
* [唐揚げ?orトイプードル？機械学習で判別してみた](https://qiita.com/katsujitakeda/items/f1842f5e831bb6475ba8)
* [【Python】icrawlerを使って画像データを簡単に収集する](https://zenn.dev/robes/articles/fb617fc0144e80)
* [sklearnドキュメント](https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestClassifier.html)