\( \def\vector#1{\boldsymbol{#1}} \)

TF-IDF

Takami Torao #TF-IDF
  • このエントリーをはてなブックマークに追加

定義と特性

TF-IDF (term frequency - inverse document frequency) はある単語がコーパス内の文書に対してどれほど重要であるかを示す統計的数値。TF-IDF 値は文書内に単語が出現する回数に比例して増加するが、コーパス内での単語の出現頻度によって相殺されることが多く、一般に頻繁に出現する単語を調整する利点をもつ。TF-IDF は、文書の特徴ベクトルを算出してベクトル空間モデルでの文書検索や文書分類、索引付けなどに利用されている。

文書集合内の文書 \(d\) に対する単語 \(w\) の \({\rm tf\mbox{-}idf}_{d,w}\) は以下の式で表される。\[ \begin{equation} {\rm tf\mbox{-}idf}_{d,w} = {\rm tf}_{d,w} \times {\rm idf}_w = \frac{n_{w,d}}{\sum_i n_{i,d}} \times \log \frac{N}{N_w} \label{tfidf} \end{equation} \] ここで \(n_{w,d}\) は文書 \(d\) での単語 \(w\) の出現回数、\(\sum_i n_{i,d}\) は \(d\) に含まれる全ての単語の出現数の総和である。

「ある文書の特徴を示す単語はその文書内に頻出する」と仮定すると \({\rm tf}_{d,w}\) は文書 \(d\) に含まれる単語の中で \(w\) が占める割合を示している。\({\rm tf}_{d,w}\) で単語の頻度を定義する方法はいくつかある。代表的なものは単語 \(w\) の相対頻度を表す式 (\(\ref{tf}\)) である。\[ \begin{equation} {\rm tf}_{d,w} = \frac{n_{w,d}}{\sum_w n_{w,d}} = f_{d,w} \label{tf} \end{equation} \] また \({\rm tf}_{d,w}\) は必ずしも線形である必要はなく、Gerard Salton (1927-1995) の後期の研究では式 (\(\ref{tf2}\)) の表記が多く使用されている。\[ \begin{equation} {\rm tf}_{d,w} = \begin{cases} 1 + \log f_{d,w} & f_{d,w} \gt 0 \\ 0 & \mbox{Otherwise} \end{cases} \label{tf2} \end{equation} \]

ただし \({\rm tf}_{d,w}\) だけでは他の文書でも頻出している単語 (例えば "the" や "it" など) も高く評価してしまうため目的にそぐわない。この調整のため \({\rm idf}_w\) を乗算して多くの文書に現れる頻度を相殺している。

文書数 \(N\) の文書集合で単語 \(w\) を含む文書数を \(N_w\) とすると、ある文書に単語 \(w\) が含まれる確率は \(P_w=\frac{N_w}{N}\) である。このとき、\({\rm idf}_w\) は単語 \(w\) の選択情報量 \(I_w\) を表していることが分かる。\[ \begin{equation} I_w = \log \frac{1}{P_w} = \log \frac{N}{N_w} = {\rm idf}_w \label{idf} \end{equation} \]

なお形態素解析などの処理で複数の文書に頻出する単語 (stop word) をフィルタリングする場合は以下の \(0 \leq I_w \leq 1\) に正規化された選択情報量を使用すると良い。\[ \begin{align*} I_w & \propto \log_N \frac{N}{N_w} = \log_N N - \log_N N_w \\ & = 1 - \log_N N_w = 1 - \frac{\log N_w}{\log N}, \ \ N_w \gt 0 \end{align*} \] ただし、\({\rm tf}\) と同様に \({\rm idf}\) もいくつかの表記がある。TF-IDF を重み付けなどに使用する場合、TF 関数と IDF 関数を選択可能な設計にしておくことが推奨される。F

プログラミング

文書のベクトル化

TF-IDF は「特定の文書に偏在する単語は特徴が高い」と「すべての文書に一様に出現する単語の特徴は低い」という特徴を併せ持つ量である。従って任意のテキスト要素 (形態素や Ngram など) に対する TF-IDF 値はその文書を特徴づけている特徴ベクトル (feature vector) と考える事ができる。

例として文書内に果物の絵文字 (単語などのテキスト要素の代替) を含む文書セット \(d_1\), \(d_2\), \(d_3\) について考える。\[ \begin{align*} d_1 & = \{🍌, 🍌, 🍎, 🍊\} \\ d_2 & = \{🍌, 🍎, 🍊, 🍒, 🍒\} \\ d_3 & = \{🍎, 🍇, 🍇\} \end{align*} \]

それぞれの絵文字と文書に対して TF (\(\ref{tf}\))、IDF (\(\ref{idf}\)) は以下の値をとる。

🍌 🍎 🍊 🍒 🍇
\({\rm tf}_{d_1}\) 0.500 0.250 0.250 0.000 0.000
\({\rm tf}_{d_2}\) 0.200 0.200 0.200 0.400 0.000
\({\rm tf}_{d_3}\) 0.000 0.333 0.000 0.000 0.667
\({\rm idf}\) 0.405 0.000 0.405 1.098 1.098

すべての文書に含まれている🍎は何の特徴も表していないため IDF が 0 となっていることが分かるだろう。

以上より各絵文字の文書ごとの TF-IDF (\(\ref{tfidf}\)) は以下のように算出される。文書内で最も高い値を強調して示している。

🍌 🍎 🍊 🍒 🍇
\({\rm tfidf}_{d_1}\) 0.202 0.000 0.101 0.000 0.000
\({\rm tfidf}_{d_2}\) 0.081 0.000 0.081 0.162 0.000
\({\rm tfidf}_{d_3}\) 0.000 0.000 0.000 0.000 0.732

文書 \(d_3\) にのみ含まれている🍇は文書 \(d_3\) の特徴を強く表す単語である。つまり、未知の文書の TF-IDF 値が🍇に高い量を示していれば \(d_3\) と類似した文書である可能性が高いことを意味している。

各文書に対するテキスト要素ごとの TF-IDF を使用して、文書の特徴ベクトル \(\vector{v}_{d_i}\) は以下のように表すことができる。\[ \begin{align*} \vector{v}_{d_1} & = (0.202, 0.000, 0.101, 0.000, 0.000) \\ \vector{v}_{d_2} & = (0.081, 0.000, 0.081, 0.162, 0.000) \\ \vector{v}_{d_3} & = (0.000, 0.000, 0.000, 0.000, 0.732) \end{align*} \]

TF-IDF を使用した文書のベクトル化はベクトル空間モデル上で類似文書検索やインデックス付けを行うための典型的な方法である。

以下のコードはこの例に基づいて各文書の TF-IDF による特徴ベクトルを作成する。

def tf(doc:Seq[Int], term:Int):Double = doc(term) / doc.sum.toDouble

def idf(docs:Seq[Seq[Int]], term:Int):Double = math.log(docs.length.toDouble / docs.count(_(term) > 0))

def tfidf(docs:Seq[Seq[Int]], doc:Int, term:Int):Double = tf(docs(doc), term) * idf(docs, term)

val terms = Seq("banana", "apple", "orange", "cherry", "grape")
val doc1 = Seq("banana", "banana", "apple", "orange")
val doc2 = Seq("banana", "apple", "orange", "cherry", "cherry")
val doc3 = Seq("apple", "grape", "grape")

val docs = Seq(doc1, doc2, doc3).map{ doc =>
  terms.map(term => doc.count(_ == term))
}
// List(List(2, 1, 1, 0, 0), List(1, 1, 1, 2, 0), List(0, 1, 0, 0, 2))

docs.indices.map{ doc =>
  terms.map{ term =>
    tfidf(docs, doc, terms.indexOf(term))
  }
}.foreach{ fs =>
  println(fs.map(x=>f"$x%.3f").mkString(", "))
}
// 0.203, 0.000, 0.101, 0.000, 0.000
// 0.081, 0.000, 0.081, 0.439, 0.000
// 0.000, 0.000, 0.000, 0.000, 0.732

参考文献

  1. Stefan Buttcher, Charles L. A. Clarke, Gordon V. Cormack. 情報検索 :検索エンジンの実装と評価. 森北出版 (2020)