Re:ゼロから始めるML生活

Standing on the shoulders of Giants

トピックモデルとlivedoor ニュースコーパスで遊ぶ

この前は全然分からないなりに、トピックモデルの雰囲気だけ勉強をしました。

www.nogawanogawa.com

今回は、理論の復習しながら、実装してみます。 参考にしたのは今回もこちら。

トピックモデル (機械学習プロフェッショナルシリーズ)

トピックモデル (機械学習プロフェッショナルシリーズ)

  • 作者:岩田 具治
  • 出版社/メーカー: 講談社
  • 発売日: 2015/04/08
  • メディア: 単行本(ソフトカバー)

こちらの本には実装は書いてないので、他人の記事とか見ながらやってみたいと思います。

理屈のおさらい

もう1ヶ月近く前に読んだ内容なので、さらっと内容の復習をしていきます。

トピックモデルは、文書が生成される過程を、確率を用いてモデル化した確率モデルです。 これは、文書にはトピックの分布が存在し、各トピックごとに出現しやすい単語は確率的に表現できると考えます。 これを仮定したとき、各文書は、文書が持つトピック分布とトピックごとの単語の出現確率をもとに単語が生成され、その生成された単語の集合で構成されていると考えるモデルがトピックモデルです。

f:id:nogawanogawa:20200211164311j:plain:w500

これには、トピックごとの単語の出現確率に関する分布を求め、与られた文書についてトピックの分布を算出することが必要です。 これらの分布のパラメータを設定していくことが、トピックモデルにおいて学習するということになります。

準備

実装に移ります。まずは簡単に、文書集合にトピックモデルを適用して色々眺めて見たいと思います。

データセット

もう何度も使っていますがlivedoor ニュースコーパスです。

後でデータセットについて、考えるのでここで一応概要だけ紹介します。 livedoor ニュースコーパスは、複数のwebメディアの記事を集めた日本語テキストのデータセットです。 中には

の9種類のメディアの記事から構成されており、合計7367の記事から成っています。

実装

まずは一巡り

とりあえず他の方の記事や実装を参考に、書いてみます。

データの取得 ~ 形態素解析

まずは文書データを取得して、形態素解析(テキストから単語のリストに直す処理)してしまいます。

class SudachiAnalizer():
    
    def get_token(self, source) :
        
        tokenizer_obj = dictionary.Dictionary().create()

        mode = tokenizer.Tokenizer.SplitMode.C
        result = tokenizer_obj.tokenize(source, mode)

        word_list = []
        for mrph in result:
            if not (mrph == ""):
                norm_word = mrph.normalized_form()
                hinsi = mrph.part_of_speech()[0] 

                # 単語の正規表現が特定の品詞の場合のみ採用する
                if hinsi in  ["名詞", "動詞", "形容詞"]:
                    word = tokenizer_obj.tokenize(norm_word, mode)[0].dictionary_form()
                    word_list.append(word)

        return word_list

corpus = []
PATH = "text/"

sudachi = SudachiAnalizer()

#  pathの中のdir(txt以外)をlistにして返す
def corpus_subdirs(path):
    subdirs = []
    for x in listdir(path):
        if not x.endswith('.txt'):
            subdirs.append(x)
    return subdirs

# pathの中のファイルをlistにして返す
def corpus_filenames(path):
    labels = [] # *.txt
    for y in listdir(path):
        if not y.startswith('LICENSE'):
            labels.append(y)
    return labels

for dir in corpus_subdirs(PATH):
    for file in corpus_filenames(PATH+dir):
        corpus_data = open(path.join(PATH + dir + "/" + file), "r")
        source = corpus_data.read()
        token = sudachi.get_token(source)
        corpus_data = {"name" : file, "tag" : dir, "token" : token}
        print(file)
        corpus.append(corpus_data)

text_list = []

for item in corpus:
    text_list.append(item["token"]) 

これによって、各文書をBoW形式で保持しておきます。

形態素解析には、こちらも何度も使用していますが、Sudachiを使用します。

github.com

SudachiPyは非常に遅いので、もうちょっと早いほうが良いという場合には、別の形態素解析器を使うと良いかと思います。 日本語の形態素解析器の比較についてはこちらがわかりやすかったです。

qiita.com

辞書の作成

次に文書データを使って辞書を作成します。

dictionary =corpora.Dictionary(text_list)
dictionary.filter_extremes(no_below=2,no_above=0.2)

corpus_=[dictionary.doc2bow(tokens) for tokens in text_list]

学習

トピックモデルは、LDA (Latent Dirichlet Allocation) を仮定して考えられていたので、そのとおり学習します。

f:id:nogawanogawa:20200102102139j:plain:w500

num_topics=20

model = models.LdaModel(corpus_,
                               num_topics=num_topics,
                               id2word=dictionary,
                               random_state=5
                              )
model.save('lda.model')

中身を見てみる

とりあえず、中身を見てみます。

livedoor news corpusでは、webメディアごとに文書が格納されています。 webメディアごとに、取り扱う対象に偏りがあることが想像できるので、おそらく各Webメディアごとのトピックには一定の傾向が見られることが予想できます。 そこで、webマガジンごとのトピックの分布を見てみます。

f:id:nogawanogawa:20200211212602p:plain:w200f:id:nogawanogawa:20200211212615p:plain:w200f:id:nogawanogawa:20200211212627p:plain:w200f:id:nogawanogawa:20200211212641p:plain:w200f:id:nogawanogawa:20200211212652p:plain:w200f:id:nogawanogawa:20200211212703p:plain:w200f:id:nogawanogawa:20200211212715p:plain:w200f:id:nogawanogawa:20200211212731p:plain:w200f:id:nogawanogawa:20200211212743p:plain:w200
各メディアごとのトピック分布
まあ、バラけてるっちゃあバラけてますかね?

トピックごとの単語やトピック間の相関はpyLDAvisを使ってこんな感じになってました。

f:id:nogawanogawa:20200211213646p:plain:w600

細かいところはgithubのコードを最後に付けといたので、自分で動かして直接グリグリいじっていただければと思います。

モデルの良し悪しの評価

まあ、ここまでだと人様のコード使ってなんとなく動かしてみたって感じになりますので、もうちょっとだけ深堀りします。

ここまでやってみて、なんとなくモデルは構築できました。 ここまで、マジックナンバーとしてトピック数を決めていました。 トピックは単語の集合なわけなので、トピック数を適切に決めて上げないと上で考えたモデルの評価にも影響を与えると考えられます。

作ったモデルが良いのか悪いのかについては、ここまでワードクラウド等によって定性的にしか判断していません。 定量的に判断するために、次のような指標を考えます。

Perplexity

こちらの資料がわかりやすいです。

www.slideshare.net

早い話が「モデルに従って正解を出す際の困難さ」を表します。 指標の意味から、値が小さいほうが優れたモデルということになります。

Coherence

www.slideshare.net

こちらは各トピックで人間の解釈のしやすさを表します。 人間の解釈のしやすさを表しているので、値が大きいほうが優れているということになります。

トピック数の決め方

上で紹介した2つの指標を使うと、

  • Perplexityは低いほうがモデルとして優れている
  • Coherenceは高いほうが人間が解釈しやすい

ということになります。

  • 人間の解釈性を抜きにしてモデル化する
    • Coherenceは無視
  • モデルの中身まで考慮したい
    • Coherenceが大きくPerplexityが低くなるトピック数を選択する

って感じになるでしょうか。どちらのケースもあると思いますので、必要に応じて人間が選択してあげないとだめですね。

f:id:nogawanogawa:20200211224543p:plain

確認してみた感じ、主観ですがtopic数は4~9あたりがちょうど良さそうですね。 もとのメディアが9個でなんかかぶってそうなメディアがあるので、当たり前っちゃ当たり前ですかね。

トピック数を直してもう一度やってみる

トピック数を9に調整してもう一回やってみます。

f:id:nogawanogawa:20200211230354p:plain:w200f:id:nogawanogawa:20200211230416p:plain:w200f:id:nogawanogawa:20200211230429p:plain:w200f:id:nogawanogawa:20200211230439p:plain:w200f:id:nogawanogawa:20200211230448p:plain:w200f:id:nogawanogawa:20200211230502p:plain:w200f:id:nogawanogawa:20200211230512p:plain:w200f:id:nogawanogawa:20200211230541p:plain:w200f:id:nogawanogawa:20200211230550p:plain:w200
トピック数を調整した後のメディアごとのトピック分布

まあまあバラけてますね。

f:id:nogawanogawa:20200211231735p:plain:w600

スポーツとか芸能とかトピックを言い表せそうな感じはします。

使ったコード

今回使ったコードはこちら。

GitHub - nogawanogawa/topic_model_test

データセットの準備等はREADMEをご参照ください。

参考にさせていただいた記事

こちらの記事を大いに参考にさせていただきました。 ありがとうございます。

pira-nino.hatenablog.com

qiita.com

感想

この前は理論をやってみたので、今回は実装してみました。 gensimを使うだけで非常に簡単に簡単にできて良いですね。