勾配ブースティングを使って文書分類モデルを作成してみた

文書ベクトルの各要素を説明変数に、文書のカテゴリを目的変数として、勾配ブースティングで分類器が作れるか試しました。

以下の前提で実施しました。

- データはlivedoorニュースコーパス
- livedoorニュースコーパスをインプットとしてWord2Vecで単語分散表現を作る（200次元)
- 形態素解析エンジンはMeCabを使用(辞書やstopwords、品詞のフィルタリングなどは特に指定なし)
- 文書ベクトルは単語の和の平均ベクトル

今回は勾配ブースティングの手法としてXGBoostとLightGBMを使ってみました。
双方ともインストールで躓くケースがあるので、KaggleのPython環境が使えるdocker上で実行しています。
両者のインストールに困ったときは以下をご参考にどうぞ

- [LightGBMをインストール](https://qiita.com/m__k/items/5f905cf5d20e875961b5)

## データ準備①

```python
import os
from glob import glob
import pandas as pd
from tqdm import tqdm_notebook as tqdm

# カテゴリ（ディレクトリ名）をリスト化
categories = [name for name in os.listdir('text') if os.path.isdir("text/" +name)]
print(categories)

# カテゴリをID化
category2id = {}
for i, cat in enumerate(categories):
    category2id[cat] = i

# DataFrame作成
datasets = pd.DataFrame(columns=["document", "category"])

for category in tqdm(categories):
    path = "text/" + category + "/*.txt"
    files = glob(path)
    for text_name in files:
        with open(text_name, 'r', encoding='utf-8') as f:
            document = f.read()
            row = pd.Series([document, category], index=datasets.columns)
            datasets = datasets.append(row, ignore_index=True)
print("doc num", len(datasets))

# シャッフルして表示
# datasets = datasets.sample(frac=1).reset_index(drop=True)
# datasets.head()
```

## 形態素解析の定義
KaggleのPython環境ではMeCabが使えないので、pipでインストールしています。

```python
!pip install mecab-python3
```

```python

import MeCab
import re

tagger = MeCab.Tagger("-Owakati")

def make_wakati(sentence):
    sentence = tagger.parse(sentence)
    sentence = re.sub(r'[0-9０-９a-zA-Zａ-ｚＡ-Ｚ]+', " ", sentence)
    sentence = re.sub(r'[\．_－―─！＠＃＄％＾＆\-‐|\\＊\“（）＿■×+α※÷⇒—●★☆〇◎◆▼◇△□(：〜～＋=＝)／*&^%$#@!~`){}［］…\[\]\"\'\”\’:;<>?＜＞〔〕〈〉？、。・,\./『』【】「」→←○《》≪≫\n\u3000]+', "", sentence)
    wakati = sentence.split(" ")
    wakati = list(filter(("").__ne__, wakati))
    return wakati

# test
# print(make_wakati(datasets["document"][0]))
```
## Word2Vecで単語分散表現作成

```python
from gensim.models import Word2Vec
import numpy as np
import logging

# word2vec parameters
num_features    = 200
min_word_count  = 5
num_workers     = 40
context         = 10
downsampling    = 1e-3
model_name = "livedoor_corpus_feature200.model"

# コーパス読み込み
corpus = []
for doc in tqdm(datasets["document"]):
    corpus.append(make_wakati(doc))


# word2vecモデルの作成＆モデルの保存
print("cleating word2vec model ...")
logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
model = Word2Vec(corpus, workers=num_workers, hs = 0, sg = 1, negative = 10, iter = 25, 
                 size=num_features, min_count = min_word_count,  window = context, sample = downsampling, seed=1)
model.save(model_name)
print("Done.")

# word2vecモデルの読み込み
# model = Word2Vec.load(model_name)

# test
# print(model.wv.most_similar("男性"))

```

## 文書ベクトル作成用関数の定義
単語の和の平均で文書ベクトルを作成しています。

```python

def wordvec2docvec(sentence):
    # 文章ベクトルの初期値（0ベクトルを初期値とする）
    docvecs = np.zeros(num_features, dtype="float32")

    # 文章に現れる単語のうち、モデルに存在しない単語をカウントする
    denomenator = len(sentence)

    # 文章内の各単語ベクトルを足し合わせる
    for word in sentence:
        try:
            temp = model[word]
        except:
            denomenator -= 1
            continue
        docvecs += temp

    # 文章に現れる単語のうち、モデルに存在した単語の数で割る
    if denomenator > 0:
        docvecs =  docvecs / denomenator

    return docvecs
```

## データ準備②
- `data_X` が横がベクトルの要素（２００個）、縦が文書のDataFrame（説明変数）
- `data_Y` が正解ラベル（目的変数）

これらを7:3に分けて、７割を学習データとしています。

```python
from sklearn.model_selection import train_test_split

print(len(datasets["document"]))
X, Y = [], []
for doc, category in tqdm(zip(datasets["document"], datasets["category"])):
    wakati = make_wakati(doc)
    docvec = wordvec2docvec(wakati)
    X.append(list(docvec))
    Y.append(category2id[category])
data_X = pd.DataFrame(X, columns=["X" + str(i + 1) for i in range(num_features)])
data_Y = pd.DataFrame(Y, columns=["category_id"])

train_x, test_x, train_y, test_y = train_test_split(data_X, data_Y, train_size= 0.7)
```

## XGBoostで分類器を作成＆予測

- XGBoostのパラメータとかは何もいじらず（いじり方知らないだけ...）、プレーンな状態で実行しています。
- XGBoostのFスコア平均は0.89

```python
import xgboost as xgb
from sklearn.metrics import classification_report

print("Fitting XGboost model ...")
xgb_model = xgb.XGBClassifier()
xgb_model.fit(train_x, train_y)
print("Done.")

# 予測
pred = xgb_model.predict(test_x)
print(classification_report(pred, test_y["category_id"], target_names=categories))
#           precision    recall  f1-score   support
#
#   movie-enter       0.93      0.90      0.92       282
#  it-life-hack       0.88      0.88      0.88       253
# kaden-channel       0.87      0.92      0.90       259
#    topic-news       0.88      0.91      0.90       226
#livedoor-homme       0.76      0.76      0.76       146
#        peachy       0.82      0.85      0.83       239
#  sports-watch       0.95      0.94      0.95       285
#dokujo-tsushin       0.88      0.83      0.86       277
#          smax       0.93      0.93      0.93       246
#
#   avg / total       0.89      0.89      0.89      2213
```

## LightGBMで分類器の作成＆予測

- LightGBMもプレーンな状態で実行
- LightGBMのFスコア平均は0.90
- 大体のカテゴリでXGBoostよりかはギリ精度良さげ

```python
import lightgbm as lgbm

print("Fitting LightGBM model ...")
lgbm_model = lgbm.LGBMClassifier()
lgbm_model.fit(train_x, train_y)
print("Done.")

# 予測
pred = lgbm_model.predict(test_x)
print(classification_report(pred, test_y["category_id"], target_names=categories))
#                precision    recall  f1-score   support
#
#   movie-enter       0.95      0.92      0.94       279
#  it-life-hack       0.88      0.90      0.89       249
# kaden-channel       0.90      0.91      0.90       271
#    topic-news       0.88      0.91      0.89       224
#livedoor-homme       0.77      0.80      0.78       142
#        peachy       0.85      0.88      0.86       240
#  sports-watch       0.96      0.94      0.95       287
#dokujo-tsushin       0.89      0.84      0.86       275
#          smax       0.93      0.93      0.93       246
#
#   avg / total       0.90      0.90      0.90      2213
```

## 念の為Random Forestでもやってみる

```python
from sklearn.ensemble import RandomForestClassifier

rfc = RandomForestClassifier()
rfc.fit(train_x, train_y)
pred = rfc.predict(test_x)
print(classification_report(pred, test_y["category_id"], target_names=categories))
#                precision    recall  f1-score   support
#
#   movie-enter       0.96      0.85      0.90       306
#  it-life-hack       0.77      0.79      0.78       247
# kaden-channel       0.84      0.81      0.82       284
#    topic-news       0.84      0.80      0.82       242
#livedoor-homme       0.58      0.64      0.60       132
#        peachy       0.71      0.77      0.74       226
#  sports-watch       0.90      0.92      0.91       274
#dokujo-tsushin       0.77      0.79      0.78       256
#          smax       0.91      0.90      0.90       246
#
#   avg / total       0.82      0.82      0.82      2213
```
そこそこボロ負け...

## TF-IDFによる文書ベクトルでもやってみる
- NMFとかで次元圧縮したほうがいいような気がしますが、一旦なにも考えずそのままLightGBMで学習させてみる
- 文書ベクトルの次元数が79673次元とかなり高次元なため、学習に結構時間かかります。
- Fスコア平均が0.94となり、Word2Vecによる文書ベクトルよりも良い結果になりました。

```python
from sklearn.feature_extraction.text import TfidfVectorizer

corpus = datasets["document"]
tfidf_vectorizer = TfidfVectorizer(analyzer=make_wakati)
tfidfs = tfidf_vectorizer.fit_transform(corpus)
print(tfidfs.shape)
# (7376, 79673)

tfidf_data_X = pd.DataFrame(tfidfs.toarray(),columns=["X"+str(i) for i in range(tfidfs.shape[1])])
train_x, test_x, train_y, test_y = train_test_split(data_X, data_Y, train_size=0.7)
lgbm_model = lgbm.LGBMClassifier()
lgbm_model.fit(train_x, train_y)
pred = lgbm_model.predict(test_x)
print(classification_report(pred, test_y["category_id"], target_names=categories))
#                precision    recall  f1-score   support
#
#   movie-enter       0.98      0.94      0.96       279
#  it-life-hack       0.96      0.95      0.95       256
# kaden-channel       0.96      0.98      0.97       270
#    topic-news       0.90      0.98      0.94       228
#livedoor-homme       0.85      0.86      0.85       130
#        peachy       0.90      0.90      0.90       279
#  sports-watch       0.97      0.90      0.93       279
#dokujo-tsushin       0.93      0.95      0.94       254
#          smax       0.98      0.99      0.99       238
#
#     micro avg       0.94      0.94      0.94      2213
#     macro avg       0.94      0.94      0.94      2213
#  weighted avg       0.94      0.94      0.94      2213
```


## 終わりに
文書ベクトルの要素を説明変数にしても勾配ブースティングでそこそこの精度がでる分類器ができそうなことがわかった。
いろんなところでチューニング要素があると思うので、精度向上はまだまだできそう

おわり