日本語テキストのカテゴリをtf-idfとランダムフォレストで学習する〜livedoor ニュースを題材に

## はじめに

世間的には既にやり尽くされた感のあるネタではありますが、日本語テキストの扱いに慣れるにはよい題材だなと思ったので、Qiitaに書いてみます。

今回使うものは下記の通りです。

* python(anaconda3-2.5.0)
* sklearn
* mecab

## livedoor ニュースコーパス

> NHN Japan株式会社が運営する「livedoor ニュース」のうち、下記のクリエイティブ・コモンズライセンスが適用されるニュース記事を収集し、可能な限りHTMLタグを取り除いて作成したものです。

http://www.rondhuit.com/download.html を利用させていただきます。

機会学習をやる上でデータの準備は最も時間が掛かる仕事のうちの一つですが、こうして整形したデータを用意していただけるのは本当にありがたいことです。


tarの中身を展開すると、下記のようになります。
```
livedoor-news-data
├── CHANGES.txt
├── README.txt
├── dokujo-tsushin
│   ├── LICENSE.txt
│   ├── dokujo-tsushin-4782522.txt
│   ├── dokujo-tsushin-4788357.txt
...skipping...
├── it-life-hack
│   ├── LICENSE.txt
│   ├── it-life-hack-6292880.txt
│   ├── it-life-hack-6294340.txt
...skipping...
    ├── topic-news-6841012.txt
    └── topic-news-6918105.txt
```

次に、テキストデータを後工程で使いやすいよう取り込む必要があります。

```py3
import glob

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()
```

テキストデータは3行目に件名、4行目以降が本文となっているので、2つを結合してdocs配列に格納しています。また、カテゴリは整数に変換し、labels配列に格納しています。

```
print(len(docs))   # 7367
print(len(labels)) # 7367
```


## 学習データとテストデータをつくる

7367件のデータがあるので、シンプルに7000件の学習データと367件のテストデータに分割します。
docs と labels はカテゴリでソートされてるので、シャッフルをする必要があります。

```py3
import random

## indices は 0〜7366 の整数をランダムに並べ替えた配列
random.seed()
indices = list(range(len(docs)))
random.shuffle(indices)

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


## 文書をtf-idfベクトルに変換する

文書のカテゴリ分類を行うためには、文書を何らかの多次元ベクトルに変換する必要があります。今回は tf-idf で文書をベクトル表現してみます。

```py3
from natto import MeCab
from sklearn.feature_extraction.text import TfidfVectorizer

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)
```

tokenizeで ['名詞', '形容詞', '形容動詞', '動詞'] のみを取り出しています。

## ナイーブベイズで学習する

Multinomial Naive Bayes を用いて、学習を行います。

> MultinomialNB implements the naive Bayes algorithm for multinomially distributed data, and is one of the two classic naive Bayes variants used in text classification (where the data are typically represented as word vector counts, although tf-idf vectors are also known to work well in practice). 

http://scikit-learn.org/stable/modules/naive_bayes.html#multinomial-naive-bayes

```py3
clf = MultinomialNB()
clf.fit(train_matrix, train_labels)

test_matrix = vectorizer.transform(test_data)
```

```py3
print(clf.score(train_matrix, train_labels)) # 0.881
print(clf.score(test_matrix, test_labels)) # 0.825613079019
```

ランダムに並べているので毎回結果は異なりますが、82%はまずまずの結果かなと思います。


## ランダムフォレストで学習する

ついでにランダムフォレストでもやってみます。

```py3
from sklearn.ensemble import RandomForestClassifier

clf2 = RandomForestClassifier(n_estimators=100)
clf2.fit(train_matrix, train_labels)

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

学習データの分類精度は100%にもかかわらず、テストデータが89%なので over-fitting 気味かなぁという感じ。

tf-idfマトリックスのfeatureを減らす等の工夫が必要かもしれません。


#### 追記

チューニングにより精度は 94.8% まで上げることができた。
http://qiita.com/kotaroito/items/0f7898c036304d5d5ae0


## まとめ

記事にしてみると「なんだそれほど難しくないじゃないか。」と思うわけですが、取り組み中は「うーむ..」と唸ったり、しょうもないことでつまづいたりを繰り返しました。

日頃から如何に要素技術（今回で言えば、形態素解析やtf-idf）に慣れ親しんでるかが、迅速にミスなく実装できる鍵になることを改めて実感した次第です。


## 参考

- http://scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestClassifier.html#sklearn.ensemble.RandomForestClassifier

- http://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfVectorizer.html#sklearn.feature_extraction.text.TfidfVectorizer.get_feature_names

- http://www.analyticsvidhya.com/blog/2015/06/tuning-random-forest-model/