概要
TF-IDF(Term Frequency-Inverse Document Frequency)は、文書集合の中で各単語がどれだけ重要かを数値化する手法である。「この文書でよく出る単語」(TF)と「多くの文書に出ない珍しい単語」(IDF)を掛け合わせることで、ありふれた単語を自動的に低く評価し、その文書を特徴づける単語を高く評価する。情報検索・文書分類・キーワード抽出の古典的な基盤手法である。
直感・モチベーション
「機械学習の論文」を検索するとき、「の」「は」「ある」といった単語は全文書に登場するため、文書の識別には役立たない。一方、「勾配降下法」は機械学習関連文書にしか現れず、文書の内容を強く示す。
単純な頻度(TF)だけでは高頻度の助詞・接続詞が上位に来てしまう。IDFはこれを補正する仕組みである。
トレードオフ:
- 利点: 実装がシンプル、解釈しやすい、語彙全体に対して疎なベクトルとして表現できる
- 限界: 語順・文脈を無視する(Bag of Words)。「銀行(金融)」と「銀行(川岸)」を区別できない
数学的定式化
文書集合 と単語 、文書 に対して:
TF(単語頻度):文書 中で単語 が出現する割合
ここで は文書 中の単語 の出現回数。
IDF(逆文書頻度):単語 を含む文書数の少なさを対数スケールで表す
TF-IDF:両者の積
導出を見る
なぜ対数を使うか。文書数が10倍になっても単語の希少さが10倍重要になるわけではない。対数を使うことで、スコアの差を抑えつつ「多くの文書に出るほどペナルティ」という方向性を保つ。
IDF のゼロ除算を防ぐためにスムージングを加えることが多い:
文書ベクトルの正規化:文書の長さが異なっても比較できるよう、TF-IDFベクトルをL2正規化する:
正規化後のベクトル同士のコサイン類似度は内積で計算できる()。
重要な性質・注意点
- 疎ベクトル: 語彙サイズ 次元のベクトルで、1文書に現れる単語数は より遥かに少ないため大半がゼロ
- 文書長の正規化: TFを生頻度ではなく割合にする理由は、長い文書ほど絶対頻度が高くなるバイアスを除去するため
- IDFの対数: を取ることで全文書に出現する単語()の重みが完全にゼロになる
まとめ
- TFで「この文書での出現率」、IDFで「珍しさ」を測り、その積がTF-IDF
- 高頻度だが全文書共通の単語(ストップワード)を自然に低く評価できる
- Bag of Wordsベースのため語順・文脈は無視される
- 検索エンジンのランキングや文書分類の特徴量として今も広く使われる
実装例
pythonimport math from collections import Counter def tf(term: str, doc: list[str]) -> float: counts = Counter(doc) # 文書内で各単語が何回出るかを数える return counts[term] / len(doc) # TF: 単語termの出現回数 / 文書中の総単語数 def idf(term: str, corpus: list[list[str]]) -> float: n_docs = len(corpus) # コーパス内の総文書数N df = sum(1 for doc in corpus if term in doc) # termを含む文書数df return ( math.log((n_docs + 1) / (df + 1)) + 1 ) # IDF: log((N+1)/(df+1))+1(0割防止の平滑化) def tfidf(term: str, doc: list[str], corpus: list[list[str]]) -> float: return tf(term, doc) * idf(term, corpus) # TF-IDF: tf × idf def tfidf_vector(doc: list[str], corpus: list[list[str]]) -> dict[str, float]: vocab = set(w for d in corpus for w in d) # コーパス全体の語彙集合(特徴次元) vec = { term: tfidf(term, doc, corpus) for term in vocab } # 各語のTF-IDF値でベクトル化 norm = math.sqrt(sum(v**2 for v in vec.values())) # L2ノルム: sqrt(Σ v_i^2) return ( {term: v / norm for term, v in vec.items()} if norm > 0 else vec ) # 正規化後ベクトル(長さ1) def cosine_similarity(v1: dict, v2: dict) -> float: keys = set(v1) & set(v2) # 共通次元(共通語) return sum( v1[k] * v2[k] for k in keys ) # コサイン類似度: 正規化済みベクトル同士の内積 # 使用例 corpus = [ ["機械", "学習", "で", "モデル", "を", "学習", "する"], ["深層", "学習", "は", "機械", "学習", "の", "一種"], ["自然", "言語", "処理", "で", "テキスト", "を", "分析"], ] v0 = tfidf_vector(corpus[0], corpus) v1 = tfidf_vector(corpus[1], corpus) v2 = tfidf_vector(corpus[2], corpus) print(cosine_similarity(v0, v1)) # 文書0と1の類似度(高いはず) print(cosine_similarity(v0, v2)) # 文書0と2の類似度(低いはず)