日本語テキストのカテゴリをtf-idfとランダムフォレストで学習する 〜 【チューニング編】

[日本語テキストのカテゴリをtf-idfとランダムフォレストで学習する〜livedoor ニュースを題材に](http://qiita.com/kotaroito/items/bd5f5760a45152281b54) はチューニングの余地がありそうなので、頑張ってみます。


## 使うコード

前回記事と同様のものです。

```py3
import glob
import random
import numpy as np
from natto import MeCab
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.ensemble import RandomForestClassifier

def load_livedoor_news_corpus():
    category = {
        'dokujo-tsushin': 1,
        'it-life-hack':2,
        'kaden-channel': 3,
        'livedoor-homme': 4,
        'movie-enter': 5,
        'peachy': 6,
        'smax': 7,
        'sports-watch': 8,
        'topic-news':9
    }
    docs  = []
    labels = []

    for c_name, c_id in category.items():
        files = glob.glob("./text/{c_name}/{c_name}*.txt".format(c_name=c_name))

        text = ''
        for file in files:
            with open(file, 'r') as f:
                lines = f.read().splitlines() 

                url = lines[0]
                datetime = lines[1]
                subject = lines[2]
                body = "\n".join(lines[3:])
                text = subject + "\n" + body

            docs.append(text)
            labels.append(c_id)
    
    return docs, labels

docs, labels = load_livedoor_news_corpus()


random.seed()
indices = list(range(len(docs)))
random.shuffle(indices)

split_size = 7000

train_data = [docs[i] for i in indices[0:split_size]]
train_labels = [labels[i] for i in indices[0:split_size]]
test_data = [docs[i] for i in indices[split_size:]]
test_labels = [labels[i] for i in indices[split_size:]]



def tokenize(text):
    tokens = []
    with MeCab('-F%f[0],%f[6]') as nm:
        for n in nm.parse(text, as_nodes=True):
            # ignore any end-of-sentence nodes
            if not n.is_eos() and n.is_nor():
                klass, word = n.feature.split(',', 1)
                if klass in ['名詞']:
                    tokens.append(word)

    return tokens


vectorizer = TfidfVectorizer(tokenizer=tokenize)
train_matrix = vectorizer.fit_transform(train_data)
test_matrix = vectorizer.transform(test_data)


clf2 = RandomForestClassifier(n_estimators=100, max_features=3000, oob_score=True)
clf2.fit(train_matrix, train_labels)

print(clf2.score(train_matrix, train_labels))
print(clf2.score(test_matrix, test_labels))
```

#### 結果

| Dataset | Score | 
|:-----------|------------:|
| Training  | 1.0 |
| Test     | 0.901 |



Trainingデータの識別率が100%なので、品詞を削ってfeatureを減らしても良さそうです。

## 品詞を名詞だけに限定する

```py3
def tokenize(text):
    tokens = []
    with MeCab('-F%f[0],%f[6]') as nm:
        for n in nm.parse(text, as_nodes=True):
            # ignore any end-of-sentence nodes
            if not n.is_eos() and n.is_nor():
                klass, word = n.feature.split(',', 1)
                if klass in ['名詞']:
                    tokens.append(word)

    return tokens
```

#### 結果

| Dataset | Score | 
|:-----------|------------:|
| Training  | 1.0 |
| Test     | 0.918 |


Testデータの識別率は91.8%まで向上しました。


## ランダムフォレストのmax_featuresを増やす

TfidfVectorizer の get_feature_names メソッドで測ったところ、feature は 31258 ありました。

RandomForestClassifierのmax_featuresはデフォルトでsqrt、今回のケースで言うと 176 でした。これは少なすぎる気がするので、少し増やしてみます。


```py3
clf2 = RandomForestClassifier(n_estimators=100, max_features=num_features)
clf2.fit(train_matrix, train_labels)
```

#### 結果
num_features = 1000

| Dataset | Score | 
|:-----------|------------:|
| Training  | 1.0 |
| Test     | 0.931 |

num_features = 3000

| Dataset | Score | 
|:-----------|------------:|
| Training  | 1.0 |
| Test     | 0.937|

Testデータの識別率は93.7%まで向上しました。


## oob_score を使う

sklearn の API document には "Whether to use out-of-bag samples to estimate the generalization error." と書かれており、汎化性能が向上するならそれに越したことはないわけですが...

イマイチどういう振る舞いをするのかドキュメントからでは読み解けない。。


```py3
clf2 = RandomForestClassifier(n_estimators=100, max_features=3000, oob_score=True)
clf2.fit(train_matrix, train_labels)
```

#### 結果

| Dataset | Score | 
|:-----------|------------:|
| Training  | 1.0 |
| Test     | 0.948 |


今回のケースでは oob_score=True としてチューニングしたほうがよい結果が出ました。


## まとめ

チューニング前は 90.1% でしたが、最終的には 94.8% まで識別率が向上しました。