はじめに
テキストマイニングや自然言語処理を学んでいると、必ずと言っていいほど出てくる TF-IDF という用語。検索エンジンの仕組みや文書分類などで重要な役割を果たしているこの指標について、初心者から中級者の方に向けて、理論から実装まで詳しく解説します。
TF-IDFとは
TF-IDFは「Term Frequency-Inverse Document Frequency」の略で、文書集合の中で特定の単語がどれだけ重要かを数値化する手法です。1970年代から使われている古典的な手法でありながら、現在でも多くの場面で活用されています。
TF-IDFの基本的な考え方は以下の通りです:
- その文書でよく使われている単語:重要度が高い
- 他の文書ではあまり使われていない単語:重要度が高い
- どの文書でも使われている一般的な単語:重要度が低い
この考え方により、「が」「は」「です」のような助詞や、「する」「ある」のような一般的な動詞の重要度を下げ、その文書の特徴を表す単語の重要度を上げることができます。
TF-IDFの構成要素
TF-IDFは2つの要素の掛け算で計算されます。それぞれの要素を詳しく見てみましょう。
1. TF(Term Frequency:単語頻度)
TFは特定の文書内で、ある単語が出現する頻度を表します。計算方法はいくつかありますが、最も基本的な方法は以下の通りです:
TF = (対象単語の出現回数) / (文書内の総単語数)
例:
文書A「機械学習は面白い技術です。機械学習の勉強を続けていきましょう。」
- 「機械学習」の出現回数:2回
- 総単語数:12語
- TF = 2/12 = 0.167
TFの値は0から1の間の値を取り、値が大きいほどその文書においてその単語が重要であることを示します。
2. IDF(Inverse Document Frequency:逆文書頻度)
IDFは文書集合全体で、その単語がどれだけ珍しいかを表します。計算式は以下の通りです:
IDF = log(全文書数 / その単語を含む文書数)
例:
全10文書中、「機械学習」を含む文書が3つの場合
- IDF = log(10/3) = log(3.33) ≈ 1.20
IDFの値は0以上の値を取り、値が大きいほどその単語が希少で特徴的であることを示します。全ての文書に含まれる単語のIDFは0になります。
3. TF-IDF
最終的なTF-IDFは以下で計算されます:
TF-IDF = TF × IDF
上記の例では:
TF-IDF = 0.167 × 1.20 = 0.200
具体例で理解する
以下の5つの文書を使って、実際にTF-IDFを計算してみましょう:
- 文書1:「機械学習は人工知能の一分野です」
- 文書2:「深層学習は機械学習の手法の一つです」
- 文書3:「人工知能は様々な分野で活用されています」
- 文書4:「プログラミングは機械学習に必要なスキルです」
- 文書5:「データサイエンスと機械学習は密接な関係があります」
「機械学習」という単語のTF-IDFを各文書で計算してみます:
文書1での計算:
- TF = 1/7 ≈ 0.143(「機械学習」が1回、総7語)
- IDF = log(5/4) ≈ 0.223(5文書中4文書に出現)
- TF-IDF = 0.143 × 0.223 ≈ 0.032
文書2での計算:
- TF = 1/8 = 0.125(「機械学習」が1回、総8語)
- IDF = log(5/4) ≈ 0.223(同上)
- TF-IDF = 0.125 × 0.223 ≈ 0.028
文書3での計算:
- TF = 0(「機械学習」が出現しない)
- TF-IDF = 0
同様に、「人工知能」という単語も計算してみます:
文書1での計算:
- TF = 1/7 ≈ 0.143
- IDF = log(5/2) ≈ 0.916(5文書中2文書に出現)
- TF-IDF = 0.143 × 0.916 ≈ 0.131
「人工知能」の方が「機械学習」よりも希少な単語であるため、TF-IDFの値が高くなっています。
Pythonでの基本実装
まずは基本的な実装から始めましょう:
import math
from collections import Counter
def calculate_tf(text):
"""単語頻度(TF)を計算"""
words = text.split()
word_count = len(words)
tf_dict = {}
for word in words:
tf_dict[word] = tf_dict.get(word, 0) + 1
# 正規化
for word in tf_dict:
tf_dict[word] = tf_dict[word] / word_count
return tf_dict
def calculate_idf(documents):
"""逆文書頻度(IDF)を計算"""
N = len(documents)
idf_dict = {}
all_words = set(word for doc in documents for word in doc.split())
for word in all_words:
containing_docs = sum(1 for doc in documents if word in doc.split())
idf_dict[word] = math.log(N / containing_docs)
return idf_dict
def calculate_tfidf(documents):
"""TF-IDFを計算"""
idf_dict = calculate_idf(documents)
tfidf_documents = []
for doc in documents:
tf_dict = calculate_tf(doc)
tfidf_dict = {}
for word, tf in tf_dict.items():
tfidf_dict[word] = tf * idf_dict[word]
tfidf_documents.append(tfidf_dict)
return tfidf_documents
# 使用例
documents = [
"機械学習は人工知能の一分野です",
"深層学習は機械学習の手法の一つです",
"人工知能は様々な分野で活用されています",
"プログラミングは機械学習に必要なスキルです",
"データサイエンスと機械学習は密接な関係があります"
]
tfidf_results = calculate_tfidf(documents)
# 結果表示
for i, doc_tfidf in enumerate(tfidf_results):
print(f"文書{i+1}のTF-IDF上位3語:")
for word, score in sorted(doc_tfidf.items(), key=lambda x: x[1], reverse=True)[:3]:
print(f" {word}: {score:.4f}")
print()
より高度な実装(正規化とバリエーション)
実際のプロジェクトでは、様々な正規化手法やバリエーションが使われます:
import math
import numpy as np
from collections import Counter
class TFIDFCalculator:
def __init__(self, tf_scheme='raw', idf_scheme='standard', normalization='l2'):
"""
TF-IDF計算器
Parameters:
- tf_scheme: 'raw', 'log', 'double_norm'
- idf_scheme: 'standard', 'smooth', 'max'
- normalization: 'l1', 'l2', 'max', None
"""
self.tf_scheme = tf_scheme
self.idf_scheme = idf_scheme
self.normalization = normalization
def calculate_tf(self, text, tf_max=None):
"""異なるTF計算手法"""
words = text.split()
word_count = len(words)
word_freq = Counter(words)
if self.tf_scheme == 'raw':
# 生の頻度を文書長で正規化
tf_dict = {word: freq / word_count for word, freq in word_freq.items()}
elif self.tf_scheme == 'log':
# 対数正規化
tf_dict = {word: 1 + math.log(freq) for word, freq in word_freq.items()}
elif self.tf_scheme == 'double_norm':
# 二重正規化(最大頻度との比)
max_freq = max(word_freq.values())
tf_dict = {word: 0.5 + 0.5 * (freq / max_freq) for word, freq in word_freq.items()}
return tf_dict
def calculate_idf(self, documents):
"""異なるIDF計算手法"""
N = len(documents)
idf_dict = {}
all_words = set(word for doc in documents for word in doc.split())
for word in all_words:
df = sum(1 for doc in documents if word in doc.split())
if self.idf_scheme == 'standard':
idf_dict[word] = math.log(N / df)
elif self.idf_scheme == 'smooth':
# スムージング付き
idf_dict[word] = math.log(N / (1 + df)) + 1
elif self.idf_scheme == 'max':
# 最大値正規化
max_df = max(sum(1 for doc in documents if w in doc.split()) for w in all_words)
idf_dict[word] = math.log(max_df / df)
return idf_dict
def normalize_vector(self, tfidf_dict):
"""ベクトル正規化"""
if self.normalization is None:
return tfidf_dict
values = list(tfidf_dict.values())
if self.normalization == 'l1':
norm = sum(abs(v) for v in values)
elif self.normalization == 'l2':
norm = math.sqrt(sum(v**2 for v in values))
elif self.normalization == 'max':
norm = max(values)
if norm == 0:
return tfidf_dict
return {word: score / norm for word, score in tfidf_dict.items()}
def calculate_tfidf(self, documents):
"""TF-IDFを計算"""
idf_dict = self.calculate_idf(documents)
tfidf_documents = []
for doc in documents:
tf_dict = self.calculate_tf(doc)
tfidf_dict = {word: tf * idf_dict[word] for word, tf in tf_dict.items()}
tfidf_dict = self.normalize_vector(tfidf_dict)
tfidf_documents.append(tfidf_dict)
return tfidf_documents
# 使用例
calculator = TFIDFCalculator(tf_scheme='log', idf_scheme='smooth', normalization='l2')
tfidf_results = calculator.calculate_tfidf(documents)
# 結果比較
print("高度な実装での結果:")
for i, doc_tfidf in enumerate(tfidf_results):
print(f"文書{i+1}のTF-IDF上位3語:")
for word, score in sorted(doc_tfidf.items(), key=lambda x: x[1], reverse=True)[:3]:
print(f" {word}: {score:.4f}")
print()
scikit-learnでの実装
実際のプロジェクトでは、scikit-learnのTfidfVectorizerを使用することが多いです:
from sklearn.feature_extraction.text import TfidfVectorizer
import pandas as pd
# 文書リスト
documents = [
"機械学習は人工知能の一分野です",
"深層学習は機械学習の手法の一つです",
"人工知能は様々な分野で活用されています",
"プログラミングは機械学習に必要なスキルです",
"データサイエンスと機械学習は密接な関係があります"
]
# TF-IDFベクトライザーの作成
vectorizer = TfidfVectorizer(
max_features=1000, # 最大特徴量数
min_df=1, # 最小文書頻度
max_df=0.8, # 最大文書頻度
stop_words=None, # ストップワード
ngram_range=(1, 2) # n-gramの範囲
)
# TF-IDFマトリックス作成
tfidf_matrix = vectorizer.fit_transform(documents)
# 特徴量名の取得
feature_names = vectorizer.get_feature_names_out()
# データフレームとして表示
df = pd.DataFrame(tfidf_matrix.toarray(), columns=feature_names)
print("TF-IDFマトリックス:")
print(df.round(4))
# 各文書の重要語抽出
print("\n各文書の重要語:")
for i, doc in enumerate(documents):
doc_tfidf = tfidf_matrix[i].toarray()[0]
top_indices = doc_tfidf.argsort()[-5:][::-1] # 上位5語
print(f"文書{i+1}:")
for idx in top_indices:
if doc_tfidf[idx] > 0:
print(f" {feature_names[idx]}: {doc_tfidf[idx]:.4f}")
print()
文書類似度の計算
TF-IDFベクトルを使って文書間の類似度を計算できます:
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
def calculate_document_similarity(documents):
"""文書間の類似度を計算"""
vectorizer = TfidfVectorizer()
tfidf_matrix = vectorizer.fit_transform(documents)
# コサイン類似度計算
similarity_matrix = cosine_similarity(tfidf_matrix)
return similarity_matrix
# 使用例
similarity_matrix = calculate_document_similarity(documents)
print("文書間類似度マトリックス:")
for i in range(len(documents)):
for j in range(len(documents)):
print(f"{similarity_matrix[i][j]:.3f}", end=" ")
print()
# 最も類似した文書ペアを見つける
max_similarity = 0
best_pair = (0, 0)
for i in range(len(documents)):
for j in range(i+1, len(documents)):
if similarity_matrix[i][j] > max_similarity:
max_similarity = similarity_matrix[i][j]
best_pair = (i, j)
print(f"\n最も類似した文書ペア: 文書{best_pair[0]+1} と 文書{best_pair[1]+1}")
print(f"類似度: {max_similarity:.3f}")
TF-IDFの応用例
1. 検索エンジン
def search_documents(query, documents, top_k=3):
"""クエリに基づいて関連文書を検索"""
# 文書とクエリを結合
all_texts = documents + [query]
# TF-IDFベクトル化
vectorizer = TfidfVectorizer()
tfidf_matrix = vectorizer.fit_transform(all_texts)
# クエリベクトル(最後の要素)
query_vector = tfidf_matrix[-1]
# 文書ベクトル
doc_vectors = tfidf_matrix[:-1]
# 類似度計算
similarities = cosine_similarity(query_vector, doc_vectors)[0]
# 上位k件を返す
top_indices = similarities.argsort()[-top_k:][::-1]
results = []
for idx in top_indices:
results.append({
'document_id': idx,
'document': documents[idx],
'similarity': similarities[idx]
})
return results
# 使用例
query = "機械学習の勉強方法"
search_results = search_documents(query, documents)
print("検索結果:")
for result in search_results:
print(f"文書{result['document_id']+1} (類似度: {result['similarity']:.3f})")
print(f" {result['document']}")
print()
2. キーワード抽出
def extract_keywords(text, documents, top_k=5):
"""文書から重要キーワードを抽出"""
# 対象文書を含む文書集合を作成
all_docs = documents + [text]
# TF-IDFベクトル化
vectorizer = TfidfVectorizer()
tfidf_matrix = vectorizer.fit_transform(all_docs)
# 対象文書のTF-IDFベクトル
target_vector = tfidf_matrix[-1].toarray()[0]
feature_names = vectorizer.get_feature_names_out()
# 重要度順にソート
word_scores = [(feature_names[i], target_vector[i]) for i in range(len(feature_names))]
word_scores.sort(key=lambda x: x[1], reverse=True)
return word_scores[:top_k]
# 使用例
new_text = "深層学習ニューラルネットワークは画像認識において優れた性能を発揮します"
keywords = extract_keywords(new_text, documents)
print("抽出されたキーワード:")
for word, score in keywords:
print(f" {word}: {score:.4f}")
TF-IDFの限界と改善点
限界
- 単語の順序を考慮しない:「機械学習は難しい」と「難しい機械学習は」を同じように扱う
- 同義語・類義語を区別:「AI」と「人工知能」を別の単語として扱う
- 文書の長さに影響される:長い文書ほど有利になる場合がある
- 希少語の過大評価:タイプミスなどの希少語が高い重要度を持つ
改善手法
1. n-gramの使用
# 単語の組み合わせを考慮
vectorizer = TfidfVectorizer(ngram_range=(1, 2))
2. 前処理の改善
import re
import MeCab
def preprocess_text(text):
"""テキストの前処理"""
# 形態素解析
mecab = MeCab.Tagger('-Owakati')
words = mecab.parse(text).strip().split()
# ストップワード除去、正規化など
processed_words = []
for word in words:
if len(word) > 1 and word not in stopwords:
processed_words.append(word)
return ' '.join(processed_words)
3. 文書長正規化
# L2正規化による文書長の影響軽減
vectorizer = TfidfVectorizer(norm='l2')
まとめ
TF-IDFは以下の特徴を持つ重要な指標です:
メリット:
- 実装が比較的簡単
- 計算コストが低い
- 解釈しやすい
- 多くの場面で効果的
デメリット:
- 単語の順序を考慮しない
- 同義語を区別してしまう
- 希少語を過大評価する可能性
使用場面:
- 検索エンジン
- 文書分類
- 推薦システム
- キーワード抽出
- 文書クラスタリング
TF-IDFは自然言語処理の基礎となる重要な概念です。より高度な手法(Word2Vec、BERT等)を学ぶ前に、しっかりと理解しておくことをお勧めします。実際のプロジェクトでは、scikit-learnのTfidfVectorizerを使用することが多いですが、内部の仕組みを理解することで、適切なパラメータ設定や改善点の発見につながります。
参考文献
- Manning, C. D., Raghavan, P., & Schütze, H. (2008). Introduction to Information Retrieval
- Scikit-learn Documentation: TfidfVectorizer
- Salton, G., & Buckley, C. (1988). Term-weighting approaches in automatic text retrieval