静かなる名辞

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


【python】TF-IDFで重要語を抽出してみる

概要

 すでに語り尽くされた感のあるネタですが、TF-IDFで文書の重要な単語(重要語、あるいは特徴語)を抽出してみます。

 numpyとsklearnを使うと、10行程度のコードで実現できるので簡単です。

スポンサーリンク



コードの書き方

 とりあえず、対象データとしては20newsgroupsを使います。関数一つで読み込めて便利だからです。

 sklearn.datasets.fetch_20newsgroups — scikit-learn 0.20.1 documentation

 自然言語処理の技術紹介などの記事で、Webスクレイピングなどをしてデータを作っているケースをよく見かけますが、こちらの方が手間がかからなくて、再現性も高いです*1。使えるデータは使いましょう。

 関連記事:【python】sklearnのfetch_20newsgroupsで文書分類を試す(1) - 静かなる名辞

 あとはTfidfVectorizerに入れて、いきなりTF-IDFのベクトルに変換します。

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

 詳しい使い方は、ドキュメントやCountVectorizerの記事を読んでいただければ良いです(CountVectorizerと使い方はほぼ同じ)。

 使い方のコツとして

  • min_dfオプションを適当に指定してゴミ単語を削った方が良いこと
  • 基本的にtransformした返り値がsparse matrix型なのでtoarray()メソッドで密行列に変換して取り扱ってやる必要があること

 が挙げられます。それ以外は、とりあえず使うだけならそれほど気は配らなくても良いはず。

 ここまでの記述をコードにすると、こんな感じです。

import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.datasets import fetch_20newsgroups

news20 = fetch_20newsgroups()
vectorizer = TfidfVectorizer(min_df=0.03)
tfidf_X = vectorizer.fit_transform(news20.data[:1000]).toarray()  # ぜんぶで1万データくらいあるけど、そんなに要らないので1000件取っている

 ここからどうするんじゃい、ということですが、スマートに書くためには、ちょっとしたnumpy芸が要求されます。

index = tfidf_X.argsort(axis=1)[:,::-1]

 tfidf_X.argsort(axis=1)でソートした結果のindexを返します。[:,::-1]はreverseです。これによって、各文書のTF-IDF値にもとづいて降順ソートされたindexが得られます。

 次に、このindexに基づいて単語を復元することを考えます。TfidfVectorizer.get_feature_names()で、特徴抽出時に使ったindexの順に並んだ単語のリストが得られるのですが*2、リストだとnumpy芸が使えないのでnumpy配列にしておきます。あとは、一気に変換します。

feature_names = np.array(vectorizer.get_feature_names())
feature_words = feature_names[index]

 numpyのこの機能を使っているコードはあまり見かけないのですが、実は

>>> import numpy as np
>>> a = np.array(["hoge","fuga","piyo"])
>>> b = np.array([[0,0,0],[2,1,0],[0,2,0]])
>>> a[b]
array([['hoge', 'hoge', 'hoge'],
       ['piyo', 'fuga', 'hoge'],
       ['hoge', 'piyo', 'hoge']], dtype='<U4')

 こういう仕様になっておりまして、意図した通りの変換が一発でできています。知らないと戸惑いますね。

 あとは配列から適当に取り出せばオッケーです。各文書ベクトル(というか単語の順列)の先頭n次元を取ると、それがそのままn個目までの重要語になっています。

やってみた

 コード全文を以下に示します。

import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.datasets import fetch_20newsgroups

news20 = fetch_20newsgroups()
vectorizer = TfidfVectorizer(min_df=0.03)
tfidf_X = vectorizer.fit_transform(news20.data[:1000]).toarray()

index = tfidf_X.argsort(axis=1)[:,::-1]
feature_names = np.array(vectorizer.get_feature_names())
feature_words = feature_names[index]

n = 5  # top何単語取るか
m = 15  # 何記事サンプルとして抽出するか
for fwords, target in zip(feature_words[:m,:n], news20.target):
    # 各文書ごとにtarget(ラベル)とtop nの重要語を表示
    print(news20.target_names[target])
    print(fwords)

 結果は、

rec.autos
['car' 'was' 'this' 'the' 'where']
comp.sys.mac.hardware
['washington' 'add' 'guy' 'speed' 'call']
comp.sys.mac.hardware
['the' 'display' 'anybody' 'heard' 'disk']
comp.graphics
['division' 'chip' 'systems' 'computer' 'four']
sci.space
['error' 'known' 'tom' 'memory' 'the']
talk.politics.guns
['of' 'the' 'com' 'to' 'says']
sci.med
['thanks' 'couldn' 'instead' 'file' 'everyone']
comp.sys.ibm.pc.hardware
['chip' 'is' 'fast' 'ibm' 'bit']
comp.os.ms-windows.misc
['win' 'help' 'please' 'appreciated' 'figure']
comp.sys.mac.hardware
['the' 'file' 'lost' 've' 'it']
rec.motorcycles
['00' 'org' 'the' 'out' 'and']
talk.religion.misc
['the' 'that' 'may' 'to' 'is']
comp.sys.mac.hardware
['hp' 'co' 'com' 'tin' 'newsreader']
sci.space
['the' 'power' 'and' 'space' 'nasa']
misc.forsale
['10' 'very' 'and' 'reasonable' 'sale']

 まあ、それなりにうまくいってるんじゃね? という結果が得られました*3

 車のカテゴリやコンピュータのカテゴリ、宇宙のカテゴリなんかは割とわかりやすいですが、talk.religion.misc(宗教に関する話題?)だと['the' 'that' 'may' 'to' 'is']になっていたりするのは面白いです。この文書だけがたまたまとても抽象的だったのか、このカテゴリ自体こんな感じなのかはよくわかりません。

 ということで、文書ごとにやってうまく結果が出るのはわかったので、次は各カテゴリ(ラベル)ごとに特徴的な単語を出してみようと思ったのですが、これはちょっとめんどいのでとりあえずパス。そのうち気が向いたら追記します。

まとめ

 特徴抽出とTF-IDFの計算を自分で書いて、重要語への変換も自分で書いてという感じでやるとかなり手間がかかるのですが、sklearnとnumpyのちからに頼ると簡潔に書けて嬉しいですね。

 TF-IDFの上位数件くらいは、それなりに文書の特徴を反映するような単語と言って良いと思うので、ざっくり内容を把握したいとか、ざっくり特徴抽出したいというときはこういう方法も良いと思います。

*1:sklearnが仕様変更しない限り再現できる

*2:つまりindexと特徴ベクトルの次元が対応

*3:それなりに「まとも」な結果になっているのはTfidfVectorizerのオプションでmin_df=0.03を指定しているからで、これをやらないと見事にdfが低すぎるゴミ単語ばっかり引っかかる結果になる。注意しましょう