日本語テキストのカテゴリをtf-idfとランダムフォレストで学習する〜livedoor ニュースを題材に はチューニングの余地がありそうなので、頑張ってみます。
使うコード
前回記事と同様のものです。
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を減らしても良さそうです。
品詞を名詞だけに限定する
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 でした。これは少なすぎる気がするので、少し増やしてみます。
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." と書かれており、汎化性能が向上するならそれに越したことはないわけですが...
イマイチどういう振る舞いをするのかドキュメントからでは読み解けない。。
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% まで識別率が向上しました。