Doc2Vecとk-meansで教師なしテキスト分類

More than 1 year has passed since last update.

人間が分類したデータを教師データとしてテキスト分類をしている際に人間がデータの間に介在することによる弊害が出てきたので、教師なしのテキスト分類器を作ってみました。


人間がラベル付けすることによる問題点


  • 階層構造にあるデータを並列にラベル付ける

  • 人によって大きく判断が違ってくるようなラベルをつける

  • 作業開始時点と終了時点でラベルの付け方が変わる


参考資料


やったこと


  • Doc2Vecで各文書について他の文書との類似度ベクトルを作成

  • SVDで次元圧縮

  • k-meansでクラスタリング

k-meansでなくてグラフアルゴリズムで分類した方が良かったな、と反省しています。

理由としてはDoc2Vecで計算した際に類似度が出力される文書が限られているからです。

k-meansでは一度スパースなマトリクスを構築しなければいけませんが、グラフアルゴリズムで分類を行えばわざわざ無駄なメモリを消費する必要がなかったなと。。


Doc2Vecで特徴量を作成する利点


  • 文書内の文脈を考慮出来る(BoWが入力ではない)

  • Word2Vecが元になっているので、文書に関連したwordを取得出来る


参考コード

コメントに日本語と英語が混ざっていたり、かなり汚いコードで申し訳有りません。。

# coding: utf-8

from gensim import corpora, models
import numpy as np
from numpy import random
random.seed(555)
from scipy.cluster.vq import vq, kmeans, whiten
from sklearn.decomposition import TruncatedSVD
from collections import defaultdict
from separatewords import MecabTokenize # 目的に合わせた形態素解析器を呼びだして下さい

class MyTexts:
def __init__(self, text_list):
self.text_list = text_list

def __iter__(self):
for line in self.text_list:
if line==b'未入力': continue
yield MecabTokenize.tokenize( line.rstrip().decode('utf-8') )

class LabeledLineSentence(object):
def __init__(self, texts_words):
self.texts_words = texts_words

def __iter__(self):
for uid, words in enumerate(self.texts_words):
yield models.doc2vec.LabeledSentence(words, labels=['SENT_%s' % uid])

# Doc2Vecで取得した各文章の類似度をmatrixにする
# ついでに各文章の代表ワードを取得する
def create_sim_vec(model,n_sent):
base_name = 'SENT_'
sim_matrix = []
sim_matrix_apd = sim_matrix.append
word_matrix = []
word_matrix_apd = word_matrix.append
for i_sent in xrange(n_sent):
sim_vec = np.zeros(n_sent)
word_list = []
word_list_apd = word_list.append
# sentが存在しない場合があるので、例外処理を入れておく
try:
for word, sim_val in model.most_similar(base_name+str(i_sent)):
if 'SENT_' in word:
_, s_idx = word.split('_')
sim_vec[int(s_idx)] = sim_val
else:
word_list_apd(word)
except:
pass
sim_matrix_apd(sim_vec)
word_matrix_apd(word_list)
return sim_matrix, word_matrix

# kmeansで類似の文書をまとめる
def sent_integrate(sim_matrix,n_class):
# 次元ごとの分散を均一にする
whiten(sim_matrix)

centroid, destortion = kmeans(sim_matrix, n_class, iter=100, thresh=1e-05)
labels, dist = vq(sim_matrix, centroid)
return labels

def count_class(labels):
res_dic = defaultdict(int)
for label in labels:
res_dic[label] += 1
return res_dic

def count_labeled_data(label_data, labels):
result_dict = {}
for orig_labels, label in zip(label_data, labels):
labels = np.array(orig_labels.split(), dtype=np.int64)
if label not in result_dict:
result_dict[label] = labels
else:
result_dict[label] += labels
return result_dict

if __name__=='__main__':
ifname = './out_data.csv'
model_basename = './D2V/doc2vec_result/model'
topic_result_basename = './D2V/doc2vec_result/topic'

comment_data = []
comment_data_apd = comment_data.append
label_data = []
label_data_apd = label_data.append
with open(ifname, 'r') as f:
for line in f:
single_flag, label_flags, comment = line.strip().split('\t')
comment_data_apd(comment.strip())
label_data_apd(label_flags.strip())

texts = MyTexts(comment_data)
sentences = LabeledLineSentence(texts)
model = models.Doc2Vec(alpha=0.025, min_alpha=0.025) # use fixed learning rate
model.build_vocab(sentences)

# store the model to mmap-able files
model.save(model_basename+'.d2v')

# load the model back
model_loaded = models.Doc2Vec.load(model_basename+'.d2v')

epoch = 10
for _ in xrange(epoch):
model.train(sentences)
model.alpha -= 0.002 # decrease the learning rate
model.min_alpha = model.alpha # fix the learning rate, no decay
print 'done training'

# show topic
n_sent = len(comment_data)
sent_matrix, word_matrix = create_sim_vec(model, n_sent)
print 'done get sent_matrix'

## 類似文書をまとめる
# svdによるデータ圧縮(データを密にする)
np.savetxt('./D2V/sent_matrix', np.array(sent_matrix))
dimension = 100
lsa = TruncatedSVD(dimension)
info_matrix = lsa.fit_transform(sent_matrix)
np.savetxt('./D2V/info_matrix', np.array(info_matrix))

# kmeansの実施
n_class = 7
labels = sent_integrate(np.array(info_matrix),n_class)
np.savetxt('./D2V/sent_labels.csv', labels,delimiter=',', fmt='%d')
print count_class(labels)

# 人間が分類したものとの比較
print count_labeled_data(label_data, labels)

使用したデータは、(0¥t1 0 1 0 0 0 0¥txxxx)で1行が構成されているものです。

スペース区切りで7つ01のフラグが立っているのが、人間が付けたラベルです。


コメント

実験データでの結果を付けていないので信憑性がないのですが、

2000個のデータを目視で確認して、8割程度は違和感のない分類になっていました。

逆に人間が付けたラベルを見ていて意味不明なものが3割程度ありました。

(感性の違いなのか何なのか。。。)

最近、手法の説明や実験データを載せずに記事を公開してしまっているので

今後手抜きせずに記事を書きたいな(書けたらいいな)、と思っています。

お手数ですが間違いがありましたらご指摘いただけますと助かります。