はじめに
livedoorのニュースコーパスを、gensimライブラリを使ったLDAによるトピックモデルで分類するということを前編・後編に分けてやっていこうと思います!
ということで今回の前編では、トピックモデルの簡単な説明と、最終的にトピック分類するために必要な前処理をしていきます。
livedoorニュースコーパス
トピックニュース、Sports Watch、ITライフハック、家電チャンネル 、MOVIE ENTER、独女通信、エスマックス、livedoor HOMME、Peachy という9つのニュースサイトからの記事をそれぞれ集めたものです。
データをダウンロードしてくると、このそれぞれのクラスごとにディレクトリが別れており、各ディレクトリに記事が一つずつテキスト形式で入っています。
livedoorニュースコーパスのダウンロードはこちら
トピックモデルとは
トピックモデルとは、一つの文章が複数のトピックを持つと仮定するモデルです。
アルゴリズムは以下のようになります。
- for トピック $k = 1, ..., K$
- 単語分布を生成 $φ_k$ ~ $Dirichlet(β)$
- for 文書 $d = 1, ..., D$
- (a) トピック分布を生成 $θ_d$ ~ $Dirichlet(α)$
- (b) for 単語 $n = 1, ..., N_d$
- ⅰ. トピックを生成 $z_{dn}$ ~ $Categorical(θ_d)$
- ⅱ. 単語を生成 $w_{dn}$ ~ $Categorical(φ_{z_{dn}})$
トピックモデルでは文書ごとにトピック分布 $θ_d$ があります。
このトピック分布 $θ_d$ に従って、文書$d$のそれぞれの単語にトピックが割り当てられます。そして、割り当てられたトピックの単語分布に従って単語が生成される、という流れです。
理論的な面は、MLPシリーズの「トピックモデル」が大変参考になりました。
実装
環境はMac OS X です。
python3.5を使用しています。
今回はgensimというライブラリを使うので、上のようなアルゴリズムを意識することはとくにありません。便利ですね。
gensimの詳しい使い方に関しては gensimチュートリアル が大変わかりやすいです。
前処理
今回は"TextTransform"という前処理用のクラスを作成しました。
インスタンス化の際に引数として、"これから処理したい文書集合(リスト)", "無視する単語が記されたテキストファイルのpath(後で説明)", "ユーザー辞書(後で説明)"をとります。
import urllib.request as req
import MeCab
from gensim import corpora
import os
class TextTransform(object):
def __init__(self, texts, ignores_path, userdic=None):
self.texts = texts
self.ignores_path = ignores_path
self.userdic = userdic
# 下に続く
中の関数を一つずつ説明していきます。
def _get_stoplist(self):
# nltkは日本語用のストップワード一覧がないため、SlothLibを使う
slothlib_path = 'http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt'
res = req.urlopen(slothlib_path)
stoplist = [line.decode('utf-8').strip() for line in res]
if self.ignores_path:
with open(self.ignores_path, 'r') as f:
ignores = f.readlines()
ignores = list(map(lambda word: word.strip(), ignores))
stoplist.extend(ignores)
return stoplist
NLTKのコーパスには日本語のストップワードが存在しないので、SlothLib を利用して日本語のストップワードを取得しています。また、文書集合を形態素解析してみた結果、SlothLibには無いが明らかにいらなそうな単語を目視で確認し、ignores.txtに1行ずつ書き込んでいきました。インスタンス化の際に指定していたignores_pathはこのようなファイルへのパスを指定し、そのファイル内で指定した単語をストップワードに加えるというものです。(ストップワード除去)
def _tokenize(self, text):
words = []
stoplist = self._get_stoplist()
if self.userdic:
mecab = MeCab.Tagger('-Ochasen -u ' + self.userdic)
else:
if os.path.exists('/usr/local/lib/mecab/dic/mecab-ipadic-neologd/'):
mecab = MeCab.Tagger('-Ochasen -d /usr/local/lib/mecab/dic/mecab-ipadic-neologd/')
else:
mecab = MeCab.Tagger('-Ochasen -d /usr/local/lib/mecab/dic/ipadic/')
mecab.parseToNode('')
node = mecab.parseToNode(text)
while node:
feature = node.feature.split(',')
word = feature[-3] # 基本形
if word == '*':
word = node.surface
if feature[0] in ['名詞', '動詞', '形容詞'] and feature[1] != '数' and feature[-1] != 'ignore':
if word not in stoplist:
words.append(word)
node = node.next
return words
ここでは一つの文書を引数にとり、それをMeCabで形態素解析した結果、名詞・動詞・形容詞以外の単語、また数詞を取り除き、残った単語をリストに入れて返しています。このリスト内の各単語は形態素に分解した結果そのままではなく、各単語を基本形に直す処理を行っています。(ステミング)
各単語の基本形はMeCabの解析結果から得ることができます。
またここではMeCabのシステム辞書として**"mecab-ipadic-NEologd"**というものを使っています。これは従来のipadicなどよりも新語・固有表現に強い辞書で、かなり性能がいいです。neologdはLINEのData Labsの方が作ってくれているもののようで、詳しくはこの記事に書かれています。
インストールはこちらを参照。
ここでさらにもう一つ重要なものが**"ユーザー辞書"**です。
ユーザー辞書の登録方法はこちらの記事がとてもわかりやすいです。
MeCabでは、"Mac"という単語も"4月1日"という単語も同じ名詞であり区別する方法はありませんが、"〜月〜日"という単語はニュース記事中にはいくつも出てきますし、トピック分類には効きそうにないので除きたいですよね。"1800円"や"20歳"なども同様です。
最初は、
・1円、2円、... 、10000円
・0歳、1歳、... 、100歳
・1月1日、1月2日、... 、12月31日
などの記事に出てきそうなこれらの単語を列挙して全て先ほどのignores.txtに記し、ストップワードリストに追加していました。しかし全文書の全単語ごとに、その単語がストップワードリスト内の単語とマッチしていないか調べるのは効率が悪いです。
これに対処するために、上記のような単語はまとめてユーザー辞書に登録しました。ここでは解析結果の末尾に'ignore'という属性を入れることで、解析時にこれにマッチした単語は除くようにしています。
インスタンス化の際に引数でユーザー辞書のパスを指定しておくことで、自分で組んだユーザー辞書を使えるようにしました。
def _make_words_list(self):
words_list = []
for text in self.texts:
words = self._tokenize(text)
words_list.append(words)
return words_list
何も処理されていない文書集合を、各文書ごとに今までの処理を行いながら単語に分けた"単語リストのリスト"を作ります。
def make_dict(self, no_below, no_above, output=None):
words_list = self._make_words_list()
dictionary = corpora.Dictionary(words_list)
dictionary.filter_extremes(no_below, no_above)
dictionary.compactify()
if output:
dictionary.save(output)
return dictionary
def make_corpus(self, no_below, no_above, output=None):
words_list = self._make_words_list()
dictionary = self.make_dict(no_below, no_above)
corpus = [dictionary.doc2bow(words) for words in words_list]
if output:
corpora.MmCorpus.serialize(output, corpus)
return corpus
ここではgensimのcorporaモジュールを使って辞書とコーパスを作り、適宜保存できるようにしています。辞書とコーパスについてざっくり言うと、
辞書:文書集合から各単語の出現回数を計算したもの
コーパス:作成した辞書を参考にして、各文書にどの単語が何回出現するのかを記録したもの
になります。
またコーパスの中で出現頻度の低すぎる単語と高すぎる単語は、文書間の違いを表せないので特徴語には不適切と考えて除去します。これは、gensimのfilter_extremesを使うことで解決します。引数のno_belowとno_aboveですが、それぞれ"〜語以下しか出現しない単語"と"全体の〜割以上出現する単語"を指定して除去するためのものです。
gensimの辞書やコーパスについて知りたい方は、こちらの記事を参照してみてください。gensimチュートリアル自体は英語ですが、そのチュートリアルを適宜日本語に訳しながら実践してくれているのでとても理解しやすいかと思います。
少し長くなりましたが、これで前編改め前処理編を終わりにしようかと思います。
次回は、前処理を通して得たコーパスをgensimのLDAモデルにつっこんでトピック分類の結果を見ていきたいと思います!