tf-idfを出す用事があったので、scikit-learnで実行してみる。
例として宮沢賢治の作品から8作品ほどを青空文庫より取得し、それぞれの作品に対してtf-idf上位10件のワードを抽出する。
Pythonは3.5を利用。mecab-python3が入っていること。
「オツベルと象」「風の又三郎」「銀河鉄道の夜」「グスコーブドリの伝記」「セロ弾きのゴーシュ」「注文の多い料理店」「よだかの星」「シグナルとシグナレス」の8作品を使う。
落としてきたファイルは事前に解凍してUTF-8に変換しておく。下記とかで。
$ find . -name '*.zip' -exec unzip {} ";"
$ nkf -w --overwrite *
生成したファイルは aozora という名前の1つのディレクトリに放り込んでおく。
次に各ファイルを読み込んで、分かち書きした結果ファイルを生成する。ここでは名詞のみを対象とする。
import os, MeCab
# 分かち書きしたファイル格納用のディレクトリ
if not os.path.exists('wakati'):
os.mkdir('wakati')
tagger = MeCab.Tagger()
tagger.parseToNode('') # おまじない
def lineWakatiWriter(line, writer):
node = tagger.parseToNode(line)
while node:
if node.feature.startswith('名詞'):
writer.write(node.surface + '\n')
node = node.next
for file in os.listdir('aozora'):
with open('aozora/' + file, 'rt') as reader, open('wakati/' + file, 'wt') as writer:
for line in reader:
lineWakatiWriter(line, writer)
これで登場した名詞が羅列されたファイルが生成された。
tf-idfを取る上で役に立つ、sklearn.feature_extraction.text.CountVectorizerの動きを確認する。これはdocumentごとの単語のfrequencyを取ってくれる。
from sklearn.feature_extraction.text import CountVectorizer
# ファイル名で渡すので、inputにfilename指定して初期化
# max_dfは0.5(半分以上の文書に出現する言葉はいらん)を設定
count_vectorizer = CountVectorizer(input='filename', max_df=0.5, min_df=1, max_features=3000)
# 全ファイルパスを入れた変数でfit_transform
files = ['wakati/' + path for path in os.listdir('wakati')]
tf = count_vectorizer.fit_transform(files)
# shapeは8(doc count) * 3000(max_features)になっていた
tf.shape
#=> (8, 3000)
# featue_name一覧を取得
features = count_vectorizer.get_feature_names()
features[0:5]
#=> ['0213', '10月25日', '13', '1980', '1989']
# index:100〜104までの5つの単語について、各ドキュメントの出現数を出す。
# 8つの文書を読ませたので、8文書*5文字のmatrixになっている
tf.toarray()[:, 100:105]
#=> array([[0, 0, 0, 0, 0],
#=> [0, 0, 0, 0, 0],
#=> [0, 0, 0, 1, 0],
#=> [0, 1, 1, 0, 0],
#=> [0, 0, 0, 0, 0],
#=> [0, 0, 0, 0, 0],
#=> [0, 1, 0, 3, 1],
#=> [1, 0, 0, 1, 0]], dtype=int64)
軽く調べてみたところ、1428番目の言葉が一番出現数が多いらしい。その中身はというと、ジョバンニでした。対するカムパネルラは101回。
tf.toarray()[:, 1428]
#=> array([ 0, 0, 0, 0, 0, 0, 190, 0], dtype=int64)
features[1428]
#=> 'ジョバンニ'
tf.toarray()[:, features.index('カムパネルラ')]
#=> array([ 0, 0, 0, 0, 0, 0, 101, 0], dtype=int64)
ちなみにここで出てくる変数tfはsparse matrixになっているので、toarrayしない限りは多少サイズが大きくなってもメモリ使用量はそこまで大きくならないと思われる思いたい。
TfidfTransformerは名前の通り、tf-idfを計算してくれる。l1/l2のnormalizerもついている。
from sklearn.feature_extraction.text import TfidfTransformer
# normalizeはl2で、sublinear_tfも使う設定で実行してみる
tfidf_transformer = TfidfTransformer(norm='l2', sublinear_tf=True)
# fit_transformにCountVectorizerで生成したmatrixを渡せばtfidfが出せる。
tfidf = tfidf_transformer.fit_transform(tf)
# shapeはtfと変わらず
tfidf.shape
#=> (8, 3000)
# 最大値は0.269...になっている
tfidf.toarray().max()
#=> 0.2694441892392106
# このfeatureは何かというと、正解はオツベルでした
features[1335]
#=> 'オツベル'
# 「風の音」(2ドキュメントに出ている)はこんなくらいのスコア
tfidf.toarray()[:, features.index('風の音')]
#=> array([ 0. , 0.02869741 , 0. , 0. , 0. , 0. , 0.01849087, 0.])
# 「公開」(4ドキュメントに出ている)はこのくらいのスコア
tfidf.toarray()[:, features.index('公開')]
#=> array([ 0.03606189, 0. , 0. , 0. , 0. , 0.04528741, 0.01398999, 0.02719194])
上記ではCountVectorizerとTfidfTransformerの二段階で値を出していたが、TfidfVectorizerを使えば1回でいける。
from sklearn.feature_extraction.text import TfidfVectorizer
# 引数として、CountVectorizerとTfidfTransformerで使った双方の引数が指定できるようになっている
tfidf_vectorizer = TfidfVectorizer(input='filename', max_df=0.5, min_df=1, max_features=3000, norm='l2')
# 全ファイルパスを入れた変数でfit_transform
files = ['wakati/' + path for path in os.listdir('wakati')]
tfidf = tfidf_vectorizer.fit_transform(files)
# feature_name一覧
feature_names = tfidf_vectorizer.get_feature_names()
# カムパネルラの値
tfidf.toarray()[:, features.index('カムパネルラ')]
#=> array([ 0. , 0. , 0. , 0. , 0. , 0. , 0.43341484, 0. ])
# ジョバンニの値
tfidf.toarray()[:, features.index('ジョバンニ')]
#=> array([ 0. , 0. , 0. , 0. , 0. , 0. , 0.81533485,, 0. ])