聖刻の里

趣味や日常について自由に語る日記です。

【自然言語処理】TF-IDFの概要とPythonでの実装方法について

f:id:scarlet09Libra:20210224140542j:plain

どうもLibraです。

 
 今回は自言語処理(Natural Language Processing: NLP)でよく使われるTF-IDFと呼ばれる技術について解説していきます。Bag of Wordsと同じく文書を分析して特徴ベクトルに変換する特徴抽出手法の一つですが、ここではTF-IDFの概要とPythonによる実装方法についてみていきます。Bag of Wordsや特徴ベクトルについては過去の記事でまとめていますので、興味のある方は合わせてご覧になってみてください(´∀`*)

scarlet09libra.hatenablog.com



【目次】



実行環境

OS: MacOS Catalina 10.15.7

Python: 3.7.2

ライブラリ: scikit-learn 0.24.1

辞書: IPAdic

TF-IDFとは

 そもそもTF-IDFとは何ぞや?という疑問に対して一言で答えるなら「文書に含まれる単語の重要度を評価する手法のひとつ」です。

 もう少し具体的に書くと、 文書を特徴付けない単語(例えば「私」「僕」「こんにちは」「こんばんは」のような広く一般的に使われる言葉)に対して重要度を小さく補正し、文書を特徴付ける単語(例えば食レポの記事なら「ラーメン」「たこ焼き」「お寿司」など)に対して重要度を大きく補正します。このようにTF-IDFで文書分析を行い、その結果が検索エンジン、レコメンド、テキストマイニングといった技術に用いられています。
 上記の内容を下にまとめました。TF-IDFは主にこの2つの観点に基づいて特徴ベクトルとして文書の特徴を表現します。

  • ある文書の中で、他の文書と比較して頻繁に登場する単語は、その文書を特徴付けるための重要な単語である
  • 多くの文書で登場する単語は、個々の文書を特徴付ける単語として重要ではない



TF-IDFの意味

 ここではTF-IDFの意味について、もう少し深く掘り下げていきます。まずTF-IDFというのはTF(Term Frequency:単語の出現頻度)IDF(Inverse Document Frequency:DFの逆数)の積で表されます。詳しい計算方法はいったん置いといて、まずはそれぞれの意味からみていきましょう。(t: 単語 d: 文書)
 ここではゴリゴリ数式が登場するので数式に慣れてない方には「何言ってるか全然わかんねーですよ!」と思われるかもしれませんが、日本語でも意味を書いておいたので、数式が分からなくても式の意味さえ理解できれば問題なしです。

TF\verb|-|IDF(t, d) = TF(t, d) \cdot IDF(t)


TF(Term Frequency:単語の出現頻度)

 まずは定義を書いておきます。繰り返しになりますが、詳しい計算方法はいったん置いといてまず意味から確認していきましょう。

\begin{align}
TF(t, d) &= \frac{\verb| 文書dでの単語tの出現回数 |}{\verb| 文書dでのすべての単語の出現回数の和 |}\\
              &= \displaystyle{\frac{f _ {t, d}}{\sum_{t^{\prime}\in{d}} f _ {t^{\prime}, d}}}

\end{align}


 数式の解説をしていきます。まず登場人物の紹介からすると、fというのはfrequencyの略で頻度、 t^{\prime}は文書dに出現するすべての単語を表しています。分子の f _ {t, d}というのは、文書dの中で単語tの出現頻度を示しており、分母は文書dの全ての単語の出現回数の総和を示しています。要約するとこの式は「文書dの中で単語tが出現する割合」を表しています。

IDF(Inverse Document Frequency:DFの逆数)

 上と同じようにまずは定義をみていきましょう。

\begin{align}

IDF(t) &= \log{\frac{\verb| 文書の総数 |}{\verb| 単語tを含む文書の数 |}}\\
           &=\log{\frac{D}{d _ {t}}}\\
           &=\log{\frac{1}{P(X)}}

\end{align}


 まず上の式の登場人物ですが、D: 文書の総数、 d _ {t}: 単語tを含む文書の数、P(X): 確率を表しています。TFの式と比べるとシンプルな見た目をしていますが、この式の意味を理解するためには、IDFの名前にもある「Inverse: 逆数」と式の頭にくっついている「log」が重要なポイントになってきます。
 この式を見て勘の良い方なら気づいたかもしれませんが、一行目の日本語の式に注目してみてください。「あれ、これ確率っぽいな」と思った方はとてもセンスがいいです(私の専門は数学ではないのでとても尊敬します)。より正確に言うと「全文書Dからランダムに文書を選び、単語tを含む文書dを引き当てる確率」であるので、この確率をP(X)とすると三行目の式のようにP(X)の逆数の形で表すことができます。
 そしてまたここでセンスの良さが問われます。情報理論を学んだことのある方なら見覚えのある形だと思いますが、確率の逆数に対数を取ったこの式は情報量(エントロピー)そのものです。要約するとIDFというのは「単語tの重要度(出現する情報量)」を表しています。少し言葉にしづらいですが直感的には「出現頻度の高い単語は重要ではない、出現頻度の低い単語は重要であると計算している」といった理解で良いと思います。


【補足1: Dとdについて】
 Dというのは文書全体の集合です。なので単語tを含む文書dというのはDの要素になります。上の説明では「文書の総数」「単語tを含む文書の数」と説明していて(間違ってはないですが)、定数と勘違いされる方がいらっしゃるかもしれないので一応補足。

【補足2: 情報量(エントロピー)とは】
 IDFの説明でいきなり「これは情報量そのものです(ドヤァ)」と言われても情報理論を学んだことのない方にとっては「??」という感じになると思うので情報量について簡単に解説しておきます。
 例えば、友人のEさんが3回コイントスを行って、そのうち1回分の結果のみ教えてくれたとします。一方、別の友人のFさんも同じく3回コイントスを行い、そのうち2回分の結果を教えてくれたとします。この場合、Fさんが行ったコイントスの結果の方が情報量が多いということがなんとなく分かると思います。
 この情報量を定量的に計算する方法が \displaystyle{\log{\frac{1}{P(E)}}}です(P(E)は確率)。実際に計算してみると、Eさんが教えてくれた結果の情報量は、 \displaystyle{\log{\frac{1}{1/2}}} \fallingdotseq 0.69 、Fさんが教えてくれた結果の情報量は \displaystyle{\log{\frac{1}{1/2^{2}}}} \fallingdotseq 1.39となり、Fさんが教えてくれた情報量はEさんのそれよりも2倍多い(約0.69)ということが定量的にわかります(コイントスで表or裏が出る確率は1/2とします)。このように情報量は確率P(E)でおこる事象の「めずらしさ(のようなもの)」を定量化することができます。
 詳しく知りたい方は情報量(エントロピー)で調べてみましょう。

TF-IDFの計算方法

 実際にTF-IDFの計算をしてみましょう。TFとIDFをそれぞれ計算しそれらの積を求めるだけのシンプルなお仕事です。以下3つの文書の特徴量をTF-IDFを用いて計算してみましょう。

文書1「今日は雨が降っています。」
文書2「雨はコーラが飲めない」
文書3「私は雨の降る音が好きです。」


ついでにこれらの文書を単語ごとに区切ると以下のようになります。

文書1「今日 / は / 雨 / が / 降っ / て / い / ます / 。」
文書2「雨 / は / コーラ / が / 飲め / ない」
文書3「私 / は / 雨 / の / 降る / 音 / が / 好き / です / 。」



TF(Term Frequency:単語の出現頻度)

 文書2の「雨」と「コーラ」について考えていきましょう。文書2で登場する単語は全部で6個で、この中で「雨」と「コーラ」の登場回数はそれぞれ1回です。よってTFは、

\begin{align}

TF(\verb|雨, 文書|2) &= \frac{1}{6} \fallingdotseq 0.167\\
TF(\verb|コーラ, 文書|2) &= \frac{1}{6} \fallingdotseq 0.167

\end{align}



IDF(Inverse Document Frequency:DFの逆数))

 引き続き文書2の「雨」と「コーラ」について考えていきます。今回分析している文書の総数は3つです。そして「雨」を含む文書の数は3つ、「コーラ」を含む文書の数は1つです。よってIDFは、

\begin{align}

IDF(雨) &= \log{\frac{3}{3}} = 0\\
IDF(コーラ) &= \log{\frac{3}{1}} \fallingdotseq 1.099

\end{align}



TF-IDFの計算結果

 TFとIDFの結果が揃ったのであとは掛けるだけです。

\begin{align}

TF\verb|-|IDF(\verb|雨, 文書|2) &= 0.167 \times 0 = 0\\ 
TF\verb|-|IDF(\verb|コーラ, 文書|2) &= 0.167 \times 1.099 \fallingdotseq 0.184

\end{align}


 「雨」のTF-IDFは0、「コーラ」のTF-IDFは0.184という結果になりました。これは全ての文書で登場する「雨」は特徴量が低く、文書2でしか登場しない「コーラ」は特徴量が高く計算されているということを示しています。もう少し抽象度の高い表現をすると、「多くの文書で登場する単語であるほど特徴量が低くなり、登場頻度の少ない単語であるほど特徴量が高くなる」と言えます。

実装方法

import MeCab

from sklearn.feature_extraction.text import TfidfVectorizer


# 分かち書きを行う関数
def wakachi(text):

    mecab = MeCab.Tagger("-Owakati")

    return mecab.parse(text).strip().split(" ")

# サンプルテキスト
sentence = [
    "今日は雨が降っています。", 
    "雨はコーラが飲めない",
    "私は雨の降る音が好きです。"
]

# TF-IDFを計算する
vectrizer = TfidfVectorizer(tokenizer = wakachi, smooth_idf = False)
vectrizer.fit(sentence)
tfidf = vectrizer.transform(sentence)

print(tfidf.toarray())
print(vectrizer.get_feature_names())

"""
TF-IDFで計算した特徴ベクトル
[[0.27050092 0.40390655 0.19246363 0.40390655 0.         0.
  0.         0.19246363 0.40390655 0.         0.40390655 0.
  0.         0.40390655 0.         0.19246363 0.         0.        ]
 [0.         0.         0.24835604 0.         0.         0.52120304
  0.         0.24835604 0.         0.52120304 0.         0.
  0.         0.         0.         0.24835604 0.         0.52120304]
 [0.25081451 0.         0.17845659 0.         0.3745112  0.
  0.3745112  0.17845659 0.         0.         0.         0.3745112
  0.3745112  0.         0.3745112  0.17845659 0.3745112  0.        ]]
"""

"""
単語ベクトル
['。', 'い', 'が', 'て', 'です', 'ない', 'の', 'は', 'ます', 'コーラ', '今日', '好き', '私', '降っ', '降る', '雨', '音', '飲め']
"""



scikit-learnについて補足

「あれ?計算結果ちがくね?」と思われた方、その通りです。TF-IDFの計算方法は上で紹介したものだけではなく、様々なバリエーションがあります。この記事では分かりやすさを重視し、なるべくシンプルな形の式を紹介しました。TF-IDFの計算方法は色々ありますが、いずれも考え方と計算方法の大筋は同じですので、この記事の内容が理解できれば、他の方法で計算する場合でも理解しやすくなると思います。
 scikit-learnではIDFを以下のように計算しています。TFは文書dに登場する単語tの数、TF-IDFの計算方法は上と同じです。

\begin{align}

IDF(t) &= \log{\frac{\verb| 文書の総数 |}{\verb| 単語tを含む文書の数 |} + 1}

\end{align}


 そしてTF-IDFによって得られた特徴ベクトルを正規化すれば計算完了です(デフォルト設定ではユークリッドノルムを用いて正規化するようになっています)。
 実際にこの方法で文書2のTF-IDFを計算してみましょう(文書2で登場しない単語の計算は省きます。TF = 0なのでTF-IDFも0となり、これからの計算に影響しないためです)。文書2の単語の登場回数は全て1回なので、いずれの単語もTF(t, 文書2) = 1となります。IDFは、

\begin{align}

IDF(\verb|雨|) &= \log{\frac{3}{3} + 1} = 1\\
IDF(\verb|は|) &= \log{\frac{3}{3} + 1} = 1\\
IDF(\verb|コーラ|) &= \log{\frac{3}{1} + 1} \fallingdotseq 2.099\\
IDF(\verb|が|) &= \log{\frac{3}{3} + 1} = 1\\
IDF(\verb|飲め|) &= \log{\frac{3}{1} + 1} \fallingdotseq 2.099\\
IDF(\verb|ない|) &= \log{\frac{3}{1} + 1} \fallingdotseq 2.099

\end{align}


 ですので、TFとIDFの結果を掛けて、得られるTF-IDFの特徴ベクトルは以下のようになります(文書2で登場しない単語の分はここでは書いてません)。

\begin{align}

TF\verb|-|IDF _ {raw} &= (1, 1, 2.099, 1, 2.099, 2.099)

\end{align}


 さらにこの結果を正規化します。

\begin{align}

TF\verb|-|IDF _ {normalized} &= \frac{(1, 1, 2.099, 1, 2.099, 2.099)}{\sqrt{1 + 1 + 2.099^{2} + 1 + 2.099^{2} + 2.099^{2}}} \fallingdotseq (0.248, 0.248, 0.521, 0.248, 0.521, 0.521)

\end{align}


 上述したpythonコードの一番下にある単語ベクトルを見てください。今回scikit-learnから得られた単語ベクトルは、['。', 'い', 'が', 'て', 'です', 'ない', 'の', 'は', 'ます', 'コーラ', '今日', '好き', '私', '降っ', '降る', '雨', '音', '飲め']となっていますので、これに対応するように先ほど計算した文書2の特徴ベクトルを当てはめていきます。

# 手計算して得られた文書2の特徴ベクトル
 [0,         0,         0.248,     0,         0,         0.521, 
  0,         0.248,     0,         0.521,     0,         0, 
  0,         0,         0,         0.248,     0,         0.521]

# scikit-learnから得られた文書2の特徴ベクトル
 [0.         0.         0.24835604 0.         0.         0.52120304
  0.         0.24835604 0.         0.52120304 0.         0.
  0.         0.         0.         0.24835604 0.         0.52120304]


 上記のプログラムから得られた結果とほぼ一致していますね(文書1と文書3の特徴ベクトルについても同様に計算できます)。

TF-IDFとBoWとの比較

 同じ特徴量抽出の手法であるBoWとTF-IDFの結果を比較してみます。BoWでは単語を含むor含まないでしか文書の特徴を抽出できていないのに対して、TF-IDFではより詳細な文書の特徴を抽出できていることが分かるかと思います。(単語ベクトルは上述のソースコードに記載したものと同じです)

# BoWで得られた特徴ベクトル
[[1 1 1 1 0 0 0 1 1 0 1 0 0 1 0 1 0 0]
 [0 0 1 0 0 1 0 1 0 1 0 0 0 0 0 1 0 1]
 [1 0 1 0 1 0 1 1 0 0 0 1 1 0 1 1 1 0]]

# TF-IDFで得られた特徴ベクトル
[[0.27050092 0.40390655 0.19246363 0.40390655 0.         0.
  0.         0.19246363 0.40390655 0.         0.40390655 0.
  0.         0.40390655 0.         0.19246363 0.         0.        ]
 [0.         0.         0.24835604 0.         0.         0.52120304
  0.         0.24835604 0.         0.52120304 0.         0.
  0.         0.         0.         0.24835604 0.         0.52120304]
 [0.25081451 0.         0.17845659 0.         0.3745112  0.
  0.3745112  0.17845659 0.         0.         0.         0.3745112
  0.3745112  0.         0.3745112  0.17845659 0.3745112  0.        ]]



TF-IDFの欠点

 シンプルな特徴抽出手法であるBoWを改良したものがTF-IDFでした。しかしTF-IDFにも弱点があります。それはTFの計算過程で文書の長さを考慮できていないことです。この記事で紹介した方法もそうですが、TFを計算する際に分母が「文書dでのすべての単語の出現回数の和」になっていました。ということは、単語が10個含まれる文書と単語が10000個含まれる文書ではTFの値が変わってきます(例えばどちらの文書にも単語tが1つしか含まれていなかった場合、TFはそれぞれ1/10と1/10000ということになり、1000倍の差が生じてしまう)。このようにTF-IDFでは文書の長さによって計算結果に影響が出てきてしまいます
 この欠点を改良した特徴抽出手法としてBM25というものがあります。BM25はなかなか難しい内容で、TF-IDF以上に説明が長くなってしまうので、今回は名前だけの紹介とさせていただきます。
 

まとめ

 いかがでしたでしょうか。BoWと比較するとけっこう複雑な内容なので説明が長くなってしまいました(BM25に比べればまだマシですが(^_^;))。NLPの記事がなかなか好評のようなので、これからもNLPを始め様々な技術関連の記事を書いていこうと思っていますので、そのときはどうぞよろしくお願いします。


f:id:scarlet09Libra:20210224210514j:plain