fastTextで小説の著者分類を行う

## 概要

青空文庫からとってきた5人の著者['太宰治', '森鴎外', '坂口安吾', '夏目漱石', '宮沢賢治']から、
誰が著者なのかを予測する分類器を作成した。
（他にも[fastTextでTwitterのラベリングを行う](https://qiita.com/analytics-hiro/items/7f27cdb889cfe78283ee)みたいなのもやってみました）

## インストール

cloneしてmakeして、（Cython入っていなかったら）事前に入れて、インストール。かんたん


```bash
git clone https://github.com/facebookresearch/fastText.git
cd fastText/
sudo make
cd ../
pip install Cython
pip install fastText
```

## やったこと

- 各著者に対するファイル作成（1〜3繰返し）。コーパス数：30〜100万
	1.テキストファイル読み込み
	2.前処理（整形,わかちがき,label情報付与）
	3.ファイル作成
- モデル作成
- 各著者に対するテストデータ生成と予測値追加（1〜4繰返し）。テストサンプル数：1000件
	1.テキストファイル読み込み
	2.前処理（整形,label情報付与）
	3.データ生成
	4.予測値生成しデータに追加
- 各著者の正解率を算出

## データ

前にクローズドで行ったイベントで提供してもらったものなので、公開していいか確認してなかったので、公開は控えますが、下記の感じのテキストデータです。
(例は0001.txtの「太宰治」から一部抜粋）


```text



思いは、ひとつ、窓前花。




十三日。　なし。

十四日。　なし。

十五日。　かくまで深き、

十六日。　なし。

十七日。　なし。

十八日。
　　ものかいて扇ひき裂くなごり哉

　ふたみにわかれ

十九日。
　十月十三日より、板橋区のとある病院にいる。来て、三日間、歯ぎしりして泣いてばかりいた。銅貨のふくしゅうだ。ここは、気ちがい病院なのだ。となりの部屋の若旦那は、ふすまをあけたら、浴衣がかかっていて、どうも工合いがわるかった、など言って、みんな私よりからだが丈夫で、大河内昇とか、星武太郎などの重すぎる名を有し、帝大、立大を卒業して、しかも帝王の如く尊厳の風貌をしている。惜しいことには、諸氏ひとしく自らの身の丈よりも五寸ほどずつ恐縮していた。母を殴った人たちである。
　四日目、私は遊説に出た。鉄格子と、金網と、それから、重い扉、開閉のたびごとに、がちん、がちん、と鍵の音。寝ずの番の看守、うろ、うろ。この人間倉庫の中の、二十余名の患者すべてに、私のからだを投げ捨てて、話かけた。まるまると白く太った美男の、肩を力一杯ゆすってやって、なまけもの！　と罵った。眼のさめて在る限り、枕頭の商法の教科書を百人一首を読むような、あんなふしをつけて大声で読みわめきつづけている一受験狂に、勉強やめよ、試験全廃だ、と教えてやったら、一瞬ぱっと愁眉をひらいた。うしろ姿のおせん様というあだ名の、セル着たる二十五歳の一青年、日がな一日、部屋の隅、壁にむかってしょんぼり横坐りに居崩れて坐って、だしぬけに私に頭を殴られても、僕はたった二十五歳だ、捨てろ、捨てろ、と低く呟きつづけるばかりで私の顔を見ようとさえせぬ故、こんどは私、めそめそするな、と叱って、力いっぱいうしろから抱いてやって激しくせきにむせかえったら、青年いささか得意げに、放せ、放せ、肺病がうつると軽蔑して、私は有難くて泣いてしまった。元気を出せ。みんな、青草原をほしがっていた。私は、部屋へかえって、「花をかえせ。」という帝王の呟きに似た調子の張った詩を書いて、廻診しに来た若い一医師にお見せして、しんみに話合った。午睡という題の、「人間は人間のとおりに生きて行くものだ。」という詩を書いてみせて、ふたりとも、顔を赤くして笑った。五六百万人のひとたちが、五六百万回、六七十年つづけて囁き合っている言葉、「気の持ち様。」というこのなぐさめを信じよう。僕は、きょうから涙、一滴、見せないつもりだ。ここに七夜あそんだならば、少しは人が変ります。豚箱などは、のどかであった。越中富山の万金丹でも、熊の胃でも、三光丸でも五光丸でも、ぐっと奥歯に噛みしめて苦いが男、微笑、うたを唄えよ。私の私のスウィートピイちゃん。
```

## コードサンプル

```python
import re
import pandas as pd
import json
import MeCab
import tqdm
from time import sleep
import fasttext as ft
from requests_oauthlib import OAuth1Session

# その他
N = 1000
FILEPATH = '../../data/'
FILE_NAME_TRAIN = ['0001.txt','0002.txt','0003.txt','0004.txt','0005.txt']
FILE_NAME_TEST = ['0001_test.txt','0002_test.txt','0003_test.txt','0004_test.txt','0005_test.txt']
TRAIN_FILENAME = 'train.txt'
MODEL_NAME = 'model_ft'
AUTHOR = ['太宰治', '森鴎外', '坂口安吾', '夏目漱石', '宮沢賢治'] # 著者
CLASS_LABEL = ['__label__1','__label__2','__label__3','__label__4','__label__5'] # ラベル

# ファイル読み込み
def read_file(file_path, file_name):
    fr = open(file_path + file_name, 'r')
    texts = fr.read()
    fr.close()
    ptn = re.compile(r'\n|。')
    sp = ptn.split(texts)
    contents = [i for i in sp if (i != '') & (len(i) >= 5)]
    return contents

# わかち書き
def get_wakati(content):
    tagger = MeCab.Tagger('')
    tagger.parse('')
    surf = []
    node = tagger.parseToNode(content)
    while node:
        surf.append(node.surface)
        node = node.next
    return surf

# わかち書きを行ったlist作成
def get_surfaces(contents):
    results = []
    for row in contents:
        content = format_text(row)
        surf = get_wakati(content)
        results.append(surf)
    return results

# fasttext学習実行用ファイル作成
def write_txt(contents, class_label):
    try:
        if(len(contents) > 0):
            filename = class_label + '.txt'
            labelText = class_label + ','
            fw = open(FILEPATH + filename, 'w') # 各ラベル
            fa = open(FILEPATH + TRAIN_FILENAME, 'a') # 全ラベル
            for row in contents:
                spaceTokens = ' '.join(row)
                result = labelText + spaceTokens + '\n'
                fw.write(result)
                fa.write(result)
            fw.close()
            fa.close() 
        print(class_label+'：'+str(len(contents))+'行出力')
    except Exception as e:
        print('書き込み失敗')
        print(e)

# 整形
def format_text(text):
    text=re.sub(r'https?://[\w/:%#\$&\?\(\)~\.=\+\-…]+', "", text)
    text=re.sub(r'@[\w/:%#\$&\?\(\)~\.=\+\-…]+', "", text)
    text=re.sub(r'&[\w/:%#\$&\?\(\)~\.=\+\-…]+', "", text)
    text=re.sub(';', "", text)
    text=re.sub('RT', "", text)
    text=re.sub('\n', " ", text)
    text=re.sub('\u3000','',text)
    return text

def create_file():
    for i in range(5):
        # get
        novel = read_file(FILEPATH,FILE_NAME_TRAIN[i])
        # foramt
        for j in range(len(novel)):
            novel[j] = format_text(novel[j])
        # wakati
        surfaces = get_surfaces(novel)
        # create file
        write_txt(surfaces, CLASS_LABEL[i]) 
        sleep(3)
    print('ファイル作成完了')

# 予測値を取得
def get_predict(word, model_path):
    classifier = ft.load_model(model_path + '.bin')
    estimate = classifier.predict_proba([word], k=1)[0]
    return estimate

# テスト用データ作成
def create_testdata(class_label, file_name_test):
    test_contents = []
    labelText = class_label + ','
    # get
    novel = read_file(FILEPATH, file_name_test)
    for text in novel:
        text = format_text(text)
        test_contents.append([labelText, text])
    return test_contents

# スコアを取得
def get_predict_score(class_label, file_name_test):
    test_contents = create_testdata(class_label, file_name_test)
    for i in range(N):
        word = " ".join(get_wakati(test_contents[i][1]))
        estimate = get_predict(word, FILEPATH + MODEL_NAME)
        test_contents[i].extend([estimate[0][0],estimate[0][1]])
    df = pd.DataFrame(test_contents[:N],columns=['fac','text','pred','pro'])
    df['score_flg'] = 0
    df.loc[df['fac'] == df['pred'] , 'ans_flg'] = 1
    cnt = len(df)
    ans = df['ans_flg'].sum(axis=0)
    return cnt, ans

def main():
    create_file()
    classifier = ft.supervised(FILEPATH + TRAIN_FILENAME, FILEPATH + MODEL_NAME)
    for i in range(5):
        cnt, ans = get_predict_score(CLASS_LABEL[i], FILE_NAME_TEST[i])
        print('{}（{}）  正解率:{}%  テストサンプル数:{}個'.format(AUTHOR[i],CLASS_LABEL[i], ans/cnt * 100, cnt))

#FILEPATH = '../temp/' 

if __name__ == "__main__":
    main()
```


## 結果

- 正解率は著者によってかなりばらつきがある（全体の正解率は57%程度）
	- そもそも分類が難しい著者がいるらしい（だれだか忘れてしまった）
	- ハッカソンの1位は70%くらいだった（気がする）ので、著者の取捨選択を適切に行えば、同じくらいの正解率は出せるかも
- 今回はコーパス数が大きいが、モデル生成自体は数十秒で完了した
- fastText単体で使用するのではなく、（ベクトル表現生成がとにかく早いので）fasttextベースの文書ベクトル作成＋DL or MLなどで速度を担保しつつ精度を上げることができそう（な気がする）
- subwordのアルゴリズムをしっかり把握する必要ありそう


```
太宰治（__label__1）  正解率:83.39999999999999%  テストサンプル数:1000個
森鴎外（__label__2）  正解率:20.5%  テストサンプル数:1000個
坂口安吾（__label__3）  正解率:87.6%  テストサンプル数:1000個
夏目漱石（__label__4）  正解率:85.6%  テストサンプル数:1000個
宮沢賢治（__label__5）  正解率:11.700000000000001%  テストサンプル数:1000個
```