動画タイトルからYouTuberを当てる

# はじめに
※[昨日の記事](https://qiita.com/_rio_/items/03416500705200208c8a)に引き続きポップなテーマになります！

- 最近自然言語処理が必要な場面があり、基本的な知識の獲得がしたかった
- 最近の休日は特定のYouTuberを見るためにYouTube漬け  

上記の背景がありまして、「動画タイトルからYouTuberを予測するモデル」を作ってみたいと思います。
ちなみに今回は、YouTubeの光と闇と言われている「東海オンエア」と「水溜りボンド」というYouTuberが分類できるか試してみます。

# Ajenda
1. YouTuberのチャンネルからタイトルをスクレイピング
2. 文字列の前処理
3. 文書のベクトル化（Bag of Words）／次元圧縮（LSI）
4. データの可視化 
5. 予測（RandomForest）
6. 評価
7. まとめ

# YouTuberのチャンネルからタイトルをスクレイピング
各YouTuberのメインチャンネルとサブチャンネルからそれぞれタイトルを取得します。

```python3:get_title.py
import json
import time
import requests
from unicodedata import normalize

def get_title_list(channelId):
    """動画タイトルを取得"""
    list_title = []
    nextPageToken = ''
    for i in range(0, 100):
        url = 'https://www.googleapis.com/youtube/v3/search?part=snippet'
        payload = {'key':API_KEY, 'channelId': channelId, 'maxResults':50, 'order':'relevance', 'pageToken':nextPageToken}
        r = requests.get(url, params=payload)
        json_data = r.json()
        if not 'nextPageToken' in json_data:
            break;
        for q in range(0,len(json_data['items'])):
            list_title.append(json_data['items'][q]['snippet']['title'])
        nextPageToken = json_data['nextPageToken']

        time.sleep(2)
    
    # タイトルに重複があれば除外
    list_title_set = set(list_title)
    print('全取得数:{0} 重複除外後:{1}'.format(len(list_title), len(list_title_set)))
    return list_title


if __name__ == "__main__":
    f = open('./data/youtuber_channelId.json', 'r') # YouTuber名とチャンネルIDの辞書を用意しておく
    dict_youtuber_channelId = json.load(f) 

    API_KEY = ''

    dict_youtuber_titles = {}
    for name, channelId in dict_youtuber_channelId.items():
        print(name)
        list_title = get_title_list(channelId)
        dict_youtuber_titles['list_title_'+name] = list_title    
    
    # dump
    file = open("./outputs/youtuber_titles.json", 'w')
    json.dump(dict_youtuber_titles, file)
    file.close()
```

これで、各Youtuberの動画タイトルリストが取得できました。

```
for name, titles in dict_youtuber_titles.items():
    print(name, len(titles))

>> tokaionair 1012
>> mizutamaribond 972

print(dict_youtuber_titles['tokaionair'][0:3])
print(dict_youtuber_titles['mizutamaribond'][0:3])

>> ['【応援は力なり】メンバー各自の応援歌つくってみた', '「人間ローションボウリング」', '【おやこでみてね】たのしいえほんのよみきかせ']
>> ['【リベンジ】トミー1.5リットルコーラ早飲みで今度は吐血...', '【カンタと@小豆】ロケットサイダー踊ってみた', '【逃げ恥】恋ダンスを踊ってみた【家族?】']
```

# 文字列前処理
自然言語処理の最も重要なところといっても過言ではないであろう前処理ですが、今回は最低限の処理しか行っていません。~~（時間が無かった）~~

- 正規化
- 記号の除去

```
def clean_title(title):
    title_cleaned = normalize('NFKC', title)

    kigou = re.compile(u'[ \-/:@\[\]\{\}・\(\)#\&\'\,\.【】。『』～、×=＝「」\!\?<>★☆■□◆◇▲▼△▽♪※◎＊]')
    title_cleaned = kigou.sub('', title_cleaned)
    
    return title_cleaned

if __name__ == "__main__":
    list_titles_all = [] # 全Youtuberのタイトルリスト
    for list_titles in dict_youtuber_titles.values():
        list_titles_all += list_titles
    list_titles_all_cleaned = [clean_title(title) for title in list_titles_all]
```

# 文書のベクトル化（Bag of Words）／次元圧縮（LSI）
文書のベクトル表現方法は色々ありますが、今回はBoWを使います。また、作成したBoWに対して、TF-IDFを用いて重み付けを行い、LSIで次元削減をしました。LSIは、特異値分解を用いた次元圧縮の方法です。
次元をいくつまで落とすかは人が決めないといけないのですが、今回は色々試してAccuracyが高かった100次元まで落としました。

```
mecab = MeCab.Tagger('-Owakati -d /usr/lib64/mecab/dic/mecab-ipadic-neologd')

def word_tokenize(title):
    """タイトルを分かち書きしてリストとして返す"""
    result = mecab.parse(title)
    list_words = result.split(" ")
    list_words.pop() # 分かち書きの結果の最後の要素が'\n'なので削除
    return list_words

def title2bow(title):
    """タイトルをBoW化する"""
    title_words = word_tokenize(title)
    word_cnt = dictionary.doc2bow(title_words)
    dense = list(matutils.corpus2dense([word_cnt], num_terms=len(dictionary)).T[0])
    return dense

if __name__ == "__main__":
    # 全タイトルから単語リストを作成
    documents = [word_tokenize(title) for title in list_titles_all_cleaned] # 全タイトルの単語リスト
    # 辞書を定義
    dictionary = corpora.Dictionary(documents)
    print('単語数:',len(dictionary.keys()))
    # dictionary.filter_extremes(no_above=0.5) # 低頻度、高頻度の単語を消す
    # print(dictionary.token2id)
    print('単語数:',len(dictionary.keys()))

    dictionary.save_as_text('./data/titles_dic.txt')
    # dictionary = corpora.Dictionary.load_from_text('./data/titles_dic.txt')
    
    # BoWベクトルの作成
    bow_corpus = [dictionary.doc2bow(d) for d in documents] 

    # TF-IDFによる重み付け
    tfidf_model = models.TfidfModel(bow_corpus)
    tfidf_corpus = tfidf_model[bow_corpus]

    # LSIによる次元削減
    lsi_model = models.LsiModel(tfidf_corpus, id2word=dictionary, num_topics=100)
    lsi_corpus = lsi_model[tfidf_corpus] # あるYoutuberの全タイトルのベクトル化を次元圧縮したもの
```

# データの可視化
LSIで2次元まで落として散布図書いてみました。
※黄緑が東海オンエア、緑が水溜りボンド
![tokai_mizutamari.png](https://qiita-image-store.s3.amazonaws.com/0/177134/bb442e24-6cd9-a4bf-58b2-a4ba93932d83.png)
・・・（´・ω・｀）分けられるのか不安になってきますね。
まぁ約4000次元を無理やり2次元にしているのでこんなもんでしょうか。
とりあえず学習させてみます。


# 学習・予測
今回はRandomForestを使いました。（SVMでも試したけど精度でなかった）
RFのパラメータは、木の深さだけGridSearchをかけています。また、cv数3でCrossValidationをしています。

```
# calc class weight
class_weight = {}
i = 0
for name, list_titles in dict_youtuber_titles.items():
    class_weight[i] = (len(list_titles_all)/len(list_titles))
    i += 1
    
print(class_weight)
>> {0: 1.9604743083003953, 1: 2.0411522633744856}
```

```
from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import RandomForestClassifier

rf = RandomForestClassifier()
parameters = {'max_depth': [2,4,6,8], 'n_estimators':[100], 'class_weight':[class_weight], 'random_state':[3655]}
clf = GridSearchCV(rf, parameters)
clf.fit(X_train, ý_train)
y_pred = clf.predict(X_test)
```

# 評価
次元数をいくつか試してみた結果、100次元くらいが良さそうでした。
<img width="150" alt="dimentions.JPG" src="https://qiita-image-store.s3.amazonaws.com/0/177134/8cf3a324-c1e1-9504-9a6d-80f95cb63598.jpeg">

|  |東海オンエア(True)|水溜りボンド(True)|
|---|---|---|
|東海オンエア(Pred)|155|48|
|水溜りボンド(Pred)|32|162|

- 特に東海オンエアっぽいと予測されたタイトル達(クラス所属確率0.85以上)

```
>> オンエアの日常【オムライス】
>> 寝顔に水
>> てつやのお料理日記【13日目】
>> てつやの買った透明なコート、値段当てたらあげます
>> てつやのおしおき日記【1日目】
>> 第1回 口ゲンカトーナメント!
>> 【ポケモンSM】素人てつやの四天王挑戦日記 vsカヒリ
>> てつやの高級ドライバーでてつやを起こします
>> てつやにもう耐性ができてる寝顔に水25
>> 第一回新しい漢字作り選手権!!!
>> てつやのお料理日記【14日目】
>> てつやのお料理日記【3日目】
>> 【スマブラ】対戦実況Part3『vsはじめしゃちょー』
>> てつやvsゆめまる 深夜のアート対決
>> てつやのお料理日記【8日目】
```

- 特に水溜りボンドっぽいと予測されたタイトル達(クラス所属確率0.85以上)

```
>> 【トミー怪我】1対1でガチ野球したら本当に面白すぎたwww
>> ディズニーで日が暮れるまでトミーに見つからなかったら賞金ゲット
>> くまカレーが絶品すぎた!!
>> カンタのナンパ術が怖すぎてネットニュースにまとめられたwww
>> トミーの超暴力的スーパープレイ集 が酷すぎたwww
>> 世界一高いハンドスピナーの回し心地が予想外だったww
>> カンタ復活
>> まさかのカンタ健康診断がトミーより悪かった?
>> 超ピカピカ光るお菓子作ったら味が不思議だった
```
てつや/ゆめまる、トミー/カンタ等、各グループのメンバーの名前が入ってくると分類しやすいようですね。
（当たり前・・・）

# まとめ
精度がいまいちだったので、文字の前処理やベクトル化の方法、学習器の工夫等してみたいです。（個人的には、光と闇の彼らなら何となく分類できそうな気がしていたのです・・・）

# 参考にさせていただいたリンク
http://resola.ai/dev/2016/06/20/bowsvm%E3%81%A7%E6%96%87%E6%9B%B8%E5%88%86%E9%A1%9E%EF%BC%88%EF%BC%91%EF%BC%89/