蛇ノ目の記

技術のことも。そうでないことも。

アイドルで理解するTF-IDF TF-IDF編

ども

そろそろPyCon JP 2019が始まりますね

始まると一気にブログを書かなくなるので今のうちに書きます

PyCon JPのお祭り感が好きなんですよね

そのあとはPyCon TWに遊びに行きます

人生二度目の台湾です

TL; DR

  • TF は文書頻度。

    • 文書中の単語の重要度を表す指標
  • IDFは逆文書頻度

    • 各単語がどれだけの文書に出現するかを表す指標
  • TF-IDFはTFとIDFの積。それぞれの文書の中で重要な単語ほど大きい値になる

TFの計算式

文書d_iにおける単語t_jtfは以下のように表される。

tf_i=\frac{n_i,j}{\sum_k n_{k,j}}

n_{i,j}は文書d_iにおける単語t_jの出現回数。

{\sum_k n_{k,j}}は文書d_iにおけるすべての単語の出現回数の和。

tf-idf - Wikipedia

式で表しても(自分が)今ひとつわからないので実際の文書で考えてみる。

神﨑風花はかわいい

寺口夏花はかわいい

山崎愛はかわいい

みんなかわいい

という文書があって、単語は

  • 神﨑風花

  • 寺口夏花

  • 山崎愛

  • みんな

  • かわいい

と分けるとする。

文書中の単語はベクトルで表す。ここで単語は6つなのでベクトルの成分は6つになる。

神﨑風花はかわいい

の各単語のTFを求めてみる。

単語のベクトルはこのようになる。

神﨑風花 寺口夏花 山崎愛 みんな かわいい
1 0 0 0 1 1

「神﨑風花」という単語のtfは以下のように求められる。

{\sum_k n_{k,j}}は文書d_iにおけるすべての単語の出現回数の和なので、3になる。

tf_i=\frac{n_i,j}{\sum_k n_{k,j}}=\frac{1}{3}=0.3

同様に他の単語についてもtfを求めると、

神﨑風花はかわいい

の各単語のTFは以下のようになる。

神﨑風花 寺口夏花 山崎愛 みんな かわいい
0.3 0 0 0 0.3 0.3

IDF

文書d_iにおける単語t_jidf_iは以下のように表される。

|D|は文書の総数。

|\lbrace d:d \ni t_i \rbrace |は単語t_iを含む文書数。

tdf_i=\log\frac{|D|}{|\lbrace d:d \ni t_i \rbrace |}

tf-idf - Wikipedia

実際には単語tが出現しない文書があるとゼロ除算が発生するので、分子と分母にそれぞれ1を加える。

やっぱり式で表しても(自分が)今ひとつわからないので実際の文書で考えてみる。

神﨑風花はかわいい

寺口夏花はかわいい

山崎愛はかわいい

みんなかわいい

があって、単語のベクトルはそれぞれ以下のようになる。

神﨑風花 寺口夏花 山崎愛 みんな かわいい
1 0 0 0 1 1
0 1 0 0 1 1
0 0 1 0 1 1
0 0 0 1 0 1

単語それぞれのIDFを求めてみる。

  • 神﨑風花

idf_{神﨑風花}=\log_2\frac{4}{1}=\log_2 4=2.0

  • 寺口夏花

idf_{寺口夏花}=\log_2\frac{4}{1}=\log_2 4=2.0

  • 山崎愛

idf_{山崎愛}=\log_2\frac{4}{1}=\log_2 4=2.0

  • みんな

idf_{みんな}=\log_2\frac{4}{1}=\log_2 4=2.0

idf_{は}=\log_2\frac{4}{3}=0.4

※ 少数第二位で四捨五入してる

  • かわいい

idf_{かわいい}=\log_2\frac{4}{4}=0

神﨑風花 寺口夏花 山崎愛 みんな かわいい
2.0 2.0 2.0 2.0 0.4 0

TF-IDF

TF-IDFはtf_i,jidf_iの積。

「神﨑風花はかわいい」の場合

tf_{i,j}

神﨑風花 寺口夏花 山崎愛 みんな かわいい
0.3 0 0 0 0.3 0.3

idf_i

神﨑風花 寺口夏花 山崎愛 みんな かわいい
2.0 2.0 2.0 2.0 0.4 0

ifidf_{i,j}

神﨑風花 寺口夏花 山崎愛 みんな かわいい
0.6 0 0 0 0.12 0

となる。最も値が大きいのは「神﨑風花」なので、文書「神﨑風花はかわいい」の中で重要度が最も高いのは「神﨑風花」という単語ということになる。

Pythonでやってみる

scikit-learnを使えば簡単にできると「Pythonによるあたらしいデータ分析の教科書」という本に書いてあるけど、今回はNumPyを使って自力で実装してみる。

なお、TF-IDFのL2正規化はNumPyの力を借りた。

import typing

import numpy as np

def get_all_docs(in_csv: str) -> typing.List:
    all_docs = []
    with open(in_csv, encoding='utf-8') as fin:
        for line in fin:
            words = [word.strip() for word in line.split(',')[3:]]
            all_docs.append(words)
    return all_docs


def create_words_list(all_words: typing.List) -> typing.List:
    flatten_words = []
    for words in all_words:
        for word in words:
            flatten_words.append(word)
    words_list = []
    for word in flatten_words:
        if word not in words_list:
            words_list.append(word)
    return words_list


def get_word2int(words_list: typing.List) -> typing.Dict[str, int]:
    word2int = {}
    for idx, word in enumerate(words_list):
        word2int[word] = idx
    return word2int


def calc_tf(all_docs: typing.List, word2int: typing.Dict[str, int]) -> np.ndarray:
    tf_array = np.zeros((len(all_docs), len(word2int)))
    for doc_idx, doc in enumerate(all_docs):
        temp_array = np.zeros((1, len(word2int)))
        for word_idx, word in enumerate(word2int.keys()):
            if word in doc:
                temp_array[0, word_idx] += 1
        tf_array[doc_idx:] = temp_array / np.sum(temp_array)
    return tf_array


def calc_idf(all_docs: typing.List, word2int: typing.Dict[str, int]) -> np.ndarray:
    temp_array = np.zeros((1, len(word2int)))
    all_docs = all_docs
    for idx, word in enumerate(word2int.keys()):
        word_count = 0
        for doc in all_docs:
            if word in doc:
                word_count += 1
        temp_array[0, idx] = word_count
    idf_array = np.log2((len(all_docs) + 1 / temp_array + 1))
    return idf_array


def l2_normalize(tf_idf: np.ndarray) -> np.ndarray:
    l2_norm = np.linalg.norm(tf_idf, ord=2)
    return tf_idf / l2_norm

def tf_idf(in_csv: str):
    all_docs = get_all_docs(in_csv)   # in_csvは形態素解析して取り出した名詞のCSVファイル
    words_list = create_words_list(all_docs)
    word2int = get_word2int(words_list)
    tf_array = calc_tf(all_docs, word2int)
    idf_array = calc_idf(all_docs, word2int)
    tf_idf = tf_array * idf_array
    normalized_tf_idf = l2_normalize(tf_idf)

tf_idf_with_idol/tf_idf.py at master · NaoY-2501/tf_idf_with_idol · GitHubから抜粋。

次回は山崎愛ちゃんのブログに対して計算したTF-IDFと実際の記事を見比べて、特徴語が取り出せているかを検証したりする予定。