Word2Vec+RandomForestで笑点の回答者ともらえる座布団の枚数を予測する

#動機
笑点がものすごく好きで、実家に帰省すると録画した笑点を延々と見ていたりする。
一時自然言語処理の研究から開放され、息抜きとして笑点を・・・

ちゃっちゃら～ちゃらら
**チャッ**
**チャッ！！！！！**

```shell:inside-head
答えをどうぞ：(笑点っぽい答えを入力）
山田くん！三遊亭円楽さんの１枚持ってって！
```

パフっ！（ひらめきのおと）

#笑点って？
・昔からやっている演芸番組
・プロの落語家がお題に対して洒落た回答をする大喜利が有名
・面白い回答をすると座布団がもらえる。滑ったり失礼な回答をすると座布団を取られる
・座布団を10枚集めるとものすごい商品がもらえる

#目的
笑点っぽい答えを入力したら、
・誰の答えに一番近いか
・座布団を何枚もらえるのか
を予測して表示する。



#手順①　文章収集、前処理
日本テレビが公開している過去の放送内容[http://www.ntv.co.jp/sho-ten/02_week/kako_2011.html](http://www.ntv.co.jp/sho-ten/02_week/kako_2011.html)から2011年分の
・回答
・回答者
・座布団の増減
を記録。主要6人以外の回答（アナウンサー大喜利、若手大喜利等）は対象外とした。

収集した回答数は1773回答。
1人あたり大体330回答くらいで、あまり差がなかったのは驚き。

この文章から記号や変な空白の削除、emojiで絵文字の排除、mojimojiで大文字小文字等の統一をした。

#手順②　Word2Vec
Word2Vecで文章を200次元のベクトルに変換した。
[日本語Wikipediaエンティティベクトル](http://www.cl.ecei.tohoku.ac.jp/~m-suzuki/jawiki_vector/)を利用して文章中の単語をベクトル化する。
（口語的表現が多い笑点の回答でWikipedia使うのはどうかと思ったけど、気軽に使える学習済みモデルには勝てなかった）
回答中からとれた単語ベクトルの加算平均をその回答のベクトルとした。

#手順③　RandomForestで学習
ランダムフォレストで分類器を作成した。
ランダムフォレストは計算が軽いのがとても良いですね。
GridsearchCVでパラメータの最適化も行いました。
パラメータ探索範囲は
最大深さ:1~10
決定木数:1~1000
です。

パラメータ探索後に最も正解率(accuracy)が高いやつを抜き出して、Pickleを使って分類器を保存します。

```python:gridsearch.py
grid_mori_speaker = GridSearchCV(RandomForestClassifier() , grid_param_mori() , cv=10 ,scoring = 'accuracy', verbose = 3,n_jobs=-1)
grid_mori_speaker.fit(kotae_vector,shoten.speaker)
grid_mori_speaker_best = grid_mori_speaker.best_estimator_
with open('shoten_speaker_RF.pickle',mode = 'wb') as fp :
    pickle.dump(grid_mori_speaker_best,fp)
```

これを回答者判別ともらえる座布団の枚数でそれぞれ計算して、pickleファイルで保存します。

ちなみに回答者判別での最高正解率は0.25、座布団枚数では0.50でした。
まだだいぶ低いので手順②～③を改良して精度よくしていきたいですね。

#手順④　文章を入力させて分類するプログラムを作る
文章を手入力させて分類結果を表示させるプログラムを作ります。
やってることはpickleファイル化した分類機を解凍して文章ベクトルを突っ込んで分類結果を出力といった感じです。

```python:shoten.py
#usr/bin/env python
#coding:utf-8

import numpy as np
import re
import emoji
import mojimoji
import MeCab
from gensim.models import KeyedVectors
import pickle

mecab = MeCab.Tagger("")#Neologd辞書を使う場合はパスを記載してください
model_entity = KeyedVectors.load_word2vec_format("entity_vector.model.bin",binary = True)

with open('shoten_speaker_RF.pickle', mode='rb') as f:
    speaker_clf = pickle.load(f)
with open('shoten_zabuton_RF.pickle', mode='rb') as f:
    zabuton_clf = pickle.load(f)
    
def text_to_vector(text , w2vmodel,num_features):
    kotae = text
    kotae = kotae.replace(',','、')
    kotae = kotae.replace('/n','')
    kotae = kotae.replace('\t','')
    kotae = re.sub(r'\s','',kotae)
    kotae = re.sub(r'^@.[\w]+','',kotae)
    kotae = re.sub(r'https?://[\w/:%#\$&\?\(\)~\.=\+\-]+','',kotae)
    kotae = re.sub(r'[!-/:-@[-`{-~ ]+','',kotae)
    kotae = re.sub(r'[：-＠，【】★☆「」。、・]+','',kotae)
    kotae = mojimoji.zen_to_han(kotae,kana = False)
    kotae = kotae.lower()
    kotae = ''.join(['' if character in emoji.UNICODE_EMOJI else character for character in kotae])
    kotae_node = mecab.parseToNode(kotae)
    kotae_line = []
    while kotae_node:
        surface = kotae_node.surface
        meta = kotae_node.feature.split(",")
        if not meta[0] == '記号' and not meta[0] == 'BOS/EOS':
            kotae_line.append(kotae_node.surface)
        kotae_node = kotae_node.next
    feature_vec = np.zeros((num_features), dtype = "float32")
    word_count = 0
    for word in kotae_line:
        try:
            feature_vec = np.add(feature_vec,w2vmodel[word])
            word_count += 1
        except KeyError :
            pass
        if len(word) > 0:
            if word_count == 0:
                feature_vec = np.divide(feature_vec,1)
            else:
                feature_vec = np.divide(feature_vec,word_count)
        feature_vec = feature_vec.tolist()
    return feature_vec

def zabuton_challenge(insert_text):
    vector = np.array(text_to_vector(insert_text,model_entity,200)).reshape(1,-1)
    if(zabuton_clf.predict(vector)[0] == 0):
        print(str(speaker_clf.predict(vector)[0])+"さんに座布団は差し上げません")
    elif(zabuton_clf.predict(vector)[0] < 0):
        print("山田くん！"+str(speaker_clf.predict(vector)[0])+"さんに"+str(zabuton_clf.predict(vector)[0])+"枚差し上げて！")
    elif(zabuton_clf.predict(vector)[0] > 0):
        print("山田くん！"+str(speaker_clf.predict(vector)[0])+"さんの"+str(zabuton_clf.predict(vector)[0] * -1)+"枚持ってって！")
    else:
        print("山田くん！エラー出す分類器作った開発者の座布団全部持ってけ！")
        
if __name__ == "__main__":
    while True:
        text = input("答えをどうぞ:")
        zabuton_challenge(text)
```

コメントをほとんど書いていないのは今はお許しいただきたい。
関数text_to_vector()の内容はある方のブログ記事（ソース紛失しました。ごめんなさい）で書かれていたコードを改造したものです。

#動かす
shoten.pyを起動すると文章を入力することができる。（はじめにPickleファイルを読み込みのでちょっと時間かかるけど・・・）

テストデータとして、[2012年12月29日放送の第2395回1問目の回答](http://www.ntv.co.jp/sho-ten/02_week/131229/week_01.html)を入力してみる。

![コメント 2019-12-01 161726.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269845/ee5b3a45-96e4-5e7f-72f3-28f76736db27.png)

小遊三さんと円楽さんと木久扇さんしか出力されていないが、答えによっては他の3人も出力される。
正答率がよろしくないのは作成した分類器の精度が悪いのが原因ですね。
あと誰一人として座布団をもらえていないのは収集したデータの半数以上が座布団0枚だったことが足を引っ張っているのではないかと思ってます。

#改良案
・もっとデータ集める（収集元サイトでは2011年～2014年4月放送分までの回答が記載されている。もっとデータ集めてデータ数で殴って行きたい ~~ものっすごく面倒だけど~~）
・口語的表現に強いコーパスを使う（Wikipediaのコーパス以外見つけられなかったので口語的表現に強いものを知ってる方は教えていただきたい）
・分類アルゴリズムを変える（研究でBERT使う雰囲気になってきたのでBERTでやってみようかと考えている）