BoWを使った文書分類を試す(GiNZA)

BoW(Bag of Words)で文書分類を行うまでの一連の操作を試します。

# 予備知識
- BoWとは「[文書中に出現する単語を数え、その数を特徴とする手法](https://medium.com/chakki/シンプルだけど奥が深い-単語で表す文章の特徴-5e12123e8a60)」
- GiNZAとは「[Python向け日本語自然言語処理オープンソースライブラリ](https://www.recruit.co.jp/newsroom/2019/0402_18331.html)」
- スクレイピングとは「[ウェブサイトから情報を抽出する技術](https://ja.wikipedia.org/wiki/ウェブスクレイピング)」

# 実践
参考：[はじめての自然言語処理 第4回 spaCy/GiNZA を用いた自然言語処理](https://www.ogis-ri.co.jp/otc/hiroba/technical/similar-document-search/part4.html)
この内、「5.3 bow による分類」を試します。

スクレイピング→ラベリング→前処理→文書分類
の順になります。

## スクレイピング
訓練のためにtsvというタブ区切りのテキストファイルを用意する必要があるようです。
私は下記の感じの処理で用意していますが、ソースは割愛します。

```Python:scrapeWiki.py
from bs4 import BeautifulSoup
import csv
import os.path
import requests
import time

def read_csv(path):
    with open(path, 'r') as read_file:
        reader = csv.reader(read_file)
        lines = list(reader)
        return lines

def write_csv(path, data): 
    with open(path, 'w') as write_file:
        writer = csv.writer(write_file, delimiter='\t', quoting=csv.QUOTE_ALL)
        writer.writerows(data)

if __name__ == '__main__':
    en_lines = [['text']] # set header
    ja_lines = [['text']] # set header

    if os.path.exists(EN_LINE_PATH):
        en_lines = read_csv(EN_LINE_PATH)
    
    if os.path.exists(JA_LINE_PATH):
        ja_lines = read_csv(JA_LINE_PATH)
    
    crawl_titles = get_titles()

    for title in crawl_titles:
        print(title + " Ready")
        time.sleep(10)
        quotes = scrape(title)
        
        # dic to list
        en_lines += extract_lang(quotes, suffix_en)
        ja_lines += extract_lang(quotes, suffix_ja)

        write_csv(EN_LINE_PATH, en_lines)
        write_csv(JA_LINE_PATH, ja_lines)

        interval = 290
        print(title + " Done. Next request'll be sent after " + str(interval) + " s.")
        time.sleep(interval)
```

## ラベリング
[doccano](https://github.com/chakki-works/doccano)を使うと良さそうです。
私は知らずに下記のコードを参考にしましたが・・・

参考：[Creating a Custom Classifier for Text Cleaning](https://towardsdatascience.com/creating-a-custom-classifier-for-text-cleaning-a2a1fc818935)

```Python:labelManually.py
import numpy as np
import pandas as pd

def manually_label(input_file):
    if input_file.endswith(".csv"):
        df = pd.read_csv(input_file, delimiter='\t')
    elif input_file.endswith(".pickle"):
        df = pd.read_pickle(input_file)
    else:
        raise ValueError("Impropper argument")

    # Add label column 
    if 'label' not in df:
        df.insert(loc=0, column='label', value=np.nan)

    for index, row in df.iterrows():
        print("index: " + str(index))

        if pd.isnull(row.text):
            # Nothing to label
            print("pass: " + str(row.text))
            df.drop(index, inplace=True)
            df.to_csv(OUTPUT_FILE, sep='\t', index=False)
            continue

        if row.label.isspace():
            # Label not found
            print("raw: " + row.text)
            
            label = input()
            df.loc[index, 'label'] = label
            df.to_csv(OUTPUT_FILE, sep='\t', index=False)
        else:
            # Label found
            print(str(row.label) + ": " + row.text)
            
    print('No more labels to classify!')

if __name__ == '__main__':
    manually_label(OUTPUT_FILE)
```

## 前処理と訓練データの分割

前処理する理由について
- [自然言語処理における前処理の種類とその威力](https://qiita.com/Hironsan/items/2466fe0f344115aff177)

前処理の処理例
- [自然言語のpythonでの前処理のかんたん早見表（テキストクリーニング、分割、ストップワード、辞書の作成、数値化](https://qiita.com/kakiuchis/items/9149c7249f748668bb9b)
- [形態素解析前の日本語文書の前処理 (Python)](https://ohke.hateblo.jp/entry/2019/02/09/141500)

データの分割
- [How to split data into 3 sets (train, validation and test)?](https://stackoverflow.com/questions/38250710/how-to-split-data-into-3-sets-train-validation-and-test)

(前述の処理でラベルが数値になっているので文字に置き換えます)
- [Replacing column values in a pandas DataFrame
](https://stackoverflow.com/questions/23307301/replacing-column-values-in-a-pandas-dataframe)

```Python:normalizeAndSplit.py
import numpy as np
import pandas as pd
import neologdn
import re
import sys

LABEL_NAME = {
    1	:"category name1",
    2	:"category name2"
}

# 参考：https://stackoverflow.com/questions/38250710/how-to-split-data-into-3-sets-train-validation-and-test
def train_validate_test_split(df, train_percent=.6, validate_percent=.2, seed=None):
    np.random.seed(seed)
    perm = np.random.permutation(df.index)
    m = len(df.index)
    train_end = int(train_percent * m)
    validate_end = int(validate_percent * m) + train_end
    train = df.iloc[perm[:train_end]]
    validate = df.iloc[perm[train_end:validate_end]]
    test = df.iloc[perm[validate_end:]]
    return train, validate, test

def main():
    df = pd.read_csv(INPUT_FILE, delimiter='\t')
    df['label'] = df['label'].map(LABEL_NAME) #参考：https://stackoverflow.com/questions/23307301/replacing-column-values-in-a-pandas-dataframe

    for index, row in df.iterrows():
        normlized = neologdn.normalize(row.text, repeat=1)
        normlized = re.sub(r'(\d)([,.])(\d+)', r'\1\3', normlized)
        normlized = re.sub(r'[!-/:-@[-`{-~]', r' ', normlized)

    train, validate, test = train_validate_test_split(df)
    train.to_csv("train.tsv", sep='\t', index=False)
    validate.to_csv("dev.tsv", sep='\t', index=False)
    test.to_csv("test.tsv", sep='\t', index=False)

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

## 文書分類

Google Colaboratoryで動かす場合には
[pkg_resourcesを下記の処理でリロードする必要がある](https://qiita.com/kyamamoto9120/items/cd36017be13529366767)ようですが、
私の環境ではなぜか解決しないのでランタイムを再起動させています。

```Python
import pkg_resources, imp
imp.reload(pkg_resources)
```

下記の記事を参考に訓練を始めます。
参考元のソースと変わらないですが、[私のノートも載せておきます。](https://gist.github.com/dychw/b539c6089a8e8e3c236c3c00ebc3993b)
> 参考：[はじめての自然言語処理 第4回 spaCy/GiNZA を用いた自然言語処理](https://www.ogis-ri.co.jp/otc/hiroba/technical/similar-document-search/part4.html)


# 躓いた点1
訓練の終わったモデルを[to_disk](https://spacy.io/api/language#to_disk)でファイルに出力すると、
エラーメッセージ「Maximum recursion level reached」が返ってきます。
原因はlabelの値が 'numpy.int64'だったからなのですが、[エラーメッセージがわかり辛いです。](https://github.com/esnme/ultrajson/issues/339)

# 躓いた点2
モデルの読み込み方がわからなかった。

```Python
import spacy
#After from_disk('/content/drive/My Drive')
nlp = spacy.load('/content/drive/My Drive')
doc = nlp("ありがとうございます")
doc.cats
max(doc.cats, key=doc.cats.get)
```

# おまけ
マルチラベル分類（Multi-label classification）を試したいなら、[fasttextのチュートリアル](https://fasttext.cc/docs/en/supervised-tutorial.html#multi-label-classification)が良さそう。