静かなる名辞

pythonとプログラミングのこと


【python】sklearnのfetch_20newsgroupsで文書分類を試す(2)

 前回の続きをやっていく。とりあえず今回は簡単な方法で分類してみて、ベースラインを作ることにする。

 目次

 何はともあれ、文書から特徴抽出してベクトル化しないと話にならない。ベースラインなのでBag of Wordsを使うことにする。pure pythonで愚直に書くと激遅なのが目に見えてるので、sklearnのCountVectorizerを使う。

sklearn.feature_extraction.text.CountVectorizer — scikit-learn 0.20.1 documentation

特徴を捨てる

 とりあえず、どれくらいの大きさの特徴量になるのか見てみよう。

>>> from sklearn.datasets import fetch_20newsgroups
>>> from sklearn.feature_extraction.text import CountVectorizer as CV
>>> cv = CV()
>>> news20 = fetch_20newsgroups()
>>> cv.fit_transform(news20.data).shape
(11314, 130107)

 1.1万件はまあいいとして、13万次元! ( ;゚д゚)アワワワワ

 これでは何もできない。でも、どうせ大半の次元はノイズなのでそれほど心配する必要はない。

 ノイズを消すには、CountVectorizerのmin_dfとmax_dfを指定する。df(document frequency:ある単語について、全文書中でその単語が出現した文書の割合)に基いて要らない特徴を消し、min_df < df < max_dfの条件を満たす特徴だけ残す。つまり上と下を切る。下の方はどうせ多くの文書には出現しないから分類には役立たないし、上の方はどうせどの文書にも出現するからやっぱり分類には役立たないだろう・・・という考え方。

 どれくらいに設定すると良いのか確認するため、次のコードを走らせた。

# coding: UTF-8

from itertools import product

import numpy as np

from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import CountVectorizer as CV

def main():
    news20 = fetch_20newsgroups()    

    print("min_df max_df dim")
    for min_df, max_df in product([0.005, 0.01, 0.02, 0.03, 0.05], 
                                  [0.5, 0.6,0.7, 0.8]):
        cv = CV(min_df=min_df, max_df=max_df)
        matrix = cv.fit_transform(news20.data)
        print(min_df, max_df, matrix.shape[1])

if __name__ == "__main__":
    main()

 実行結果は、

min_df max_df dim
0.005 0.5 3949
0.005 0.6 3959
0.005 0.7 3963
0.005 0.8 3967
0.01 0.5 2141
0.01 0.6 2151
0.01 0.7 2155
0.01 0.8 2159
0.02 0.5 1071
0.02 0.6 1081
0.02 0.7 1085
0.02 0.8 1089
0.03 0.5 684
0.03 0.6 694
0.03 0.7 698
0.03 0.8 702
0.05 0.5 370
0.05 0.6 380
0.05 0.7 384
0.05 0.8 388

 なんとなくだが1000次元くらいになってほしい気持ちなので、min_df=0.02, max_df=0.5で行くことにする。1000次元ならなんとか分類できるだろう、という気持ち。

分類する

 次元を落とすめどがたったので、後は愚直に分類してみる。分類器にはRandomForestを使う。経験的にBag of Words系の特徴と相性が良く、この手の文書分類には向いている。

 ベンチマーク用にtrainとtestで分けてくれてるデータもあるらしいが、今回はそんなものは使わない。交差検証(stratified k-fold, k=3)を10回やり、precision, recall, f1を計算する。

# coding: UTF-8

from itertools import product

import numpy as np

from sklearn.datasets import fetch_20newsgroups
from sklearn.ensemble import RandomForestClassifier as RFC
from sklearn.feature_extraction.text import CountVectorizer as CV

from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import precision_recall_fscore_support as prf

def main():
    news20 = fetch_20newsgroups()    
    
    cv = CV(min_df=0.02, max_df=0.5)
    matrix = cv.fit_transform(news20.data)

    rfc = RFC(n_estimators=100, n_jobs=-1)

    scores = []
    for i in range(10):
        print(i)
        trues = []
        preds = []
        for train_index, test_index in StratifiedKFold().split(matrix, news20.target):
            rfc.fit(matrix[train_index], news20.target[train_index])
            trues.append(news20.target[test_index])
            preds.append(rfc.predict(matrix[test_index]))
        scores.append(prf(np.hstack(trues), np.hstack(preds), average="macro")[:3])

    score_mean = np.array(scores).mean(axis=0)
    print("p:{0:.6f} r:{1:.6f} f1:{2:.6f}".format(score_mean[0],
                                                  score_mean[1],
                                                  score_mean[2]))

if __name__ == "__main__":
    main()

 結果は、

p:0.723259 r:0.708348 f1:0.709696

 悪いと言えば悪いような気もするし、20クラス分類で0.7ならまあまあじゃない? という気もする、なんとも微妙な水準になった。でもまあベースラインだし、逆にここで0.97とかはじき出されたらこれ以上書くことがなくなってしまうので、いい塩梅になってくれたという解釈もある。RandomForestのn_estimatorsを増やすともうちょっと改善するだろうが(1000まで増やすと+0.05~0.1程度は改善が得られるはず)、このプログラムでも回すのに数分かかってるので、これ以上遅くなられたら困るので、やらない。

まとめ

 とりあえず、愚直に分類するとどれくらいのスコアになるかはわかった。次回以降改善していこう。