TF-IDFとPythonを用いたテキスト類似度計算による関連情報抽出
はじめに
業務でテキストデータを扱う際、特定のキーワードを含む情報を抽出するだけでなく、「似ている内容」や「関連性の高い情報」を効率的に見つけたいというニーズは多く存在します。例えば、顧客からの問い合わせ内容に類似する過去の対応履歴を検索したり、製品レビューの中から特定の意見に賛同または反対する他の意見を探したりする場合などが考えられます。
このような課題を解決するための手法の一つに、テキストを数値ベクトルに変換し、そのベクトル間の類似度を計算する方法があります。本記事では、このアプローチの基本となる「TF-IDF」という手法と、それを用いたテキストの「類似度計算」について、Pythonでの具体的な実装コードを交えながら解説します。NLPライブラリの経験が少ない方でも理解できるよう、基本的な概念から丁寧に説明を進めます。
TF-IDFとは何か?
TF-IDF(Term Frequency - Inverse Document Frequency)は、文書集合(コーパス)における各単語の重要度を評価するための統計的な手法です。単語の重要度が高いほど、その単語はその文書の特徴をよく表していると考えられます。
TF-IDFは、以下の2つの要素を組み合わせて計算されます。
-
TF(Term Frequency:単語頻度): ある単語が個々の文書にどのくらいの頻度で出現するかを示します。文書内で多く出現する単語ほど、その文書にとって重要であると考えられます。単純な出現回数だけでなく、文書の長さに応じて正規化することもあります。 $TF(t, d) = (\text{文書 } d \text{ における単語 } t \text{ の出現回数}) / (\text{文書 } d \text{ の全単語数})$
-
IDF(Inverse Document Frequency:逆文書頻度): ある単語が文書集合全体でどのくらいの文書に出現するかを示します。多くの文書に出現する単語(例:「てにをは」のような助詞や冠詞)は、特定の文書を識別する上ではあまり重要ではないと考えられます。逆に、特定の少ない文書にしか出現しない単語ほど、その単語が出現する文書を際立たせるという意味で重要であると考えられます。 $IDF(t, D) = \log_e(\text{文書集合 } D \text{ の総文書数} / (\text{単語 } t \text{ を含む文書数} + 1))$ ここで、$D$は文書集合全体を示し、分母に1を足すのは、ある単語がどの文書にも出現しない場合にゼロ除算を防ぐためです。
そして、TF-IDF値は、これらTFとIDFの積として計算されます。
$TFIDF(t, d, D) = TF(t, d) \times IDF(t, D)$
このように計算されたTF-IDF値は、ある単語が「特定の文書にはよく出現するが、他の多くの文書にはあまり出現しない」という、その文書の特徴を表す単語に対して高い値を示します。
テキストのベクトル表現と類似度計算
TF-IDFを計算することで、各文書は、文書集合に存在する全ての単語(語彙)を次元とするベクトルとして表現できるようになります。このベクトル空間における各次元の値が、その単語の文書におけるTF-IDF値となります。
例えば、文書集合に「りんご」「バナナ」「オレンジ」「美味しい」「甘い」という語彙があるとします。
ある文書Aが「りんごが美味しい」という内容であれば、そのTF-IDFベクトルは (りんごのTFIDF, バナナのTFIDF, オレンジのTFIDF, 美味しいのTFIDF, 甘いのTFIDF)
のようになります。
別の文書Bが「バナナは甘くて美味しい」という内容であれば、そのTF-IDFベクトルは (りんごのTFIDF, バナナのTFIDF, オレンジのTFIDF, 美味しいのTFIDF, 甘いのTFIDF)
となります。
これらの文書ベクトル間の「距離」や「角度」を計算することで、文書同士の類似度を測ることができます。様々な類似度計算手法がありますが、テキスト分析でよく用いられるのはコサイン類似度です。
コサイン類似度は、2つのベクトルがなす角度のコサイン値を計算します。2つのベクトルが全く同じ方向を向いている場合(類似度が高い)、角度は0度となりコサイン値は1になります。完全に直交している場合(関連性がない)、角度は90度となりコサイン値は0になります。全く逆の方向を向いている場合(例えば一方のベクトル成分が全て負の場合)、角度は180度となりコサイン値は-1になります(TF-IDFベクトルは非負なので0から1の間の値を取ります)。
文書Aのベクトルを $\vec{a}$、文書Bのベクトルを $\vec{b}$ とすると、コサイン類似度は以下の式で計算されます。
$\text{Cosine Similarity}(\vec{a}, \vec{b}) = \frac{\vec{a} \cdot \vec{b}}{||\vec{a}|| \cdot ||\vec{b}||}$
これは、2つのベクトルの内積を、それぞれのベクトルのノルム(長さ)の積で割ったものです。この計算により、ベクトルの長さ(単語数が多い文書ほどベクトル長が大きくなる傾向がある)の影響を受けずに、ベクトルの「向き」、つまり単語の相対的な重要度の分布の類似性を測ることができます。
Pythonによる実装例
PythonでTF-IDF計算とコサイン類似度計算を行う場合、scikit-learn
ライブラリのTfidfVectorizer
クラスを利用するのが一般的で効率的です。
まず、必要なライブラリをインポートします。
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import pandas as pd
次に、対象となる文書集合を準備します。ここでは、簡単なレビューテキストをサンプルとして使用します。
documents = [
"この製品は使いやすく、非常に便利です。",
"使い方は簡単ですが、機能が少し物足りません。",
"機能は豊富ですが、操作が少し複雑です。",
"カスタマーサポートの対応が丁寧で助かりました。",
"このサービスはおすすめです。とても便利に使えます。",
"以前のバージョンより使いやすくなりました。",
]
# DataFrameとして扱うと便利
df = pd.DataFrame({'text': documents})
print("--- 元のドキュメント ---")
print(df)
print("-" * 20)
TF-IDFベクトルの生成
TfidfVectorizer
を使用して、文書集合をTF-IDFベクトルに変換します。
# TfidfVectorizerのインスタンスを作成
# stop_words='english'などを指定することも可能ですが、ここでは日本語なので指定しません。
# 日本語の場合は別途、形態素解析などによる前処理が必要になることが多いですが、
# ここでは簡単のためスペース区切りとします。
vectorizer = TfidfVectorizer()
# 文書集合に対してfit_transformを実行
# fitで語彙を学習し、transformでTF-IDFベクトルに変換します
tfidf_matrix = vectorizer.fit_transform(df['text'])
print("--- TF-IDF行列の形状 ---")
print(tfidf_matrix.shape) # (文書数, 語彙数)
print("-" * 20)
# 語彙リストを取得
feature_names = vectorizer.get_feature_names_out()
print("--- 抽出された語彙の一部 ---")
print(feature_names[:10]) # 最初の10個を表示
print("-" * 20)
# TF-IDF行列(疎行列形式)を表示(全て表示すると長くなるため一部)
# print("--- TF-IDF行列 ---")
# print(tfidf_matrix)
# print("-" * 20)
TfidfVectorizer
は内部で以下の処理を行います。
1. 文書を単語に分割します(デフォルトでは空白区切り)。
2. 出現した単語のリスト(語彙)を作成します。
3. 各文書に対して、学習した語彙に基づいてTF-IDF値を計算し、疎行列(多くの要素がゼロである行列)として表現します。
tfidf_matrix.shape
が (6, 22)
となっているのは、6つの文書があり、抽出された語彙(ユニークな単語)が22種類あったことを示しています。
コサイン類似度の計算
生成されたTF-IDF行列を用いて、文書間のコサイン類似度を計算します。sklearn.metrics.pairwise.cosine_similarity
関数が便利です。
# TF-IDF行列からコサイン類似度行列を計算
# 戻り値は(文書数, 文書数)の行列
cosine_sim_matrix = cosine_similarity(tfidf_matrix, tfidf_matrix)
print("--- コサイン類似度行列 ---")
print(cosine_sim_matrix)
print("-" * 20)
# 結果をDataFrameで見やすく表示
cosine_sim_df = pd.DataFrame(cosine_sim_matrix, index=df.index, columns=df.index)
print("--- コサイン類似度行列 (DataFrame) ---")
print(cosine_sim_df)
print("-" * 20)
コサイン類似度行列は、i
行j
列目の要素がi
番目の文書とj
番目の文書の類似度を示します。対角線上の値は常に1になります(同じ文書同士の類似度)。対称行列になります(文書iとjの類似度 = 文書jとiの類似度)。
特定の文書に類似する文書の検索
この類似度行列を利用して、特定の文書(例:0番目の文書)に最も類似する他の文書を検索してみます。
# 0番目の文書 (インデックス0) に類似する文書を検索
target_document_index = 0
target_document = df.iloc[target_document_index]['text']
print(f"--- 基準となる文書 (インデックス {target_document_index}) ---")
print(target_document)
print("-" * 20)
# 基準文書と他の全ての文書との類似度を取得
# 基準文書自身との類似度(1.0)も含まれる
similarity_scores = cosine_sim_matrix[target_document_index]
# 類似度スコアと元のドキュメントを組み合わせてDataFrameを作成
similarity_df = pd.DataFrame({
'index': df.index,
'text': df['text'],
'similarity': similarity_scores
})
# 基準文書自身を除外し、類似度が高い順にソート
sorted_similarity_df = similarity_df[similarity_df['index'] != target_document_index].sort_values(by='similarity', ascending=False)
print("--- 類似度が高い順の文書 ---")
print(sorted_similarity_df)
print("-" * 20)
この結果を見ると、0番目の文書「この製品は使いやすく、非常に便利です。」に対して、類似度が最も高いのは5番目の文書「以前のバージョンより使いやすくなりました。」や4番目の文書「このサービスはおすすめです。とても便利に使えます。」であることがわかります。これは、「使いやすさ」や「便利さ」といったキーワードやそれに関連する表現が共通しているためと考えられます。
実務への応用例
TF-IDFとコサイン類似度を用いたテキスト抽出は、様々な実務課題に応用できます。
- 顧客レビュー/問い合わせの分類・ルーティング: 新規の問い合わせやレビューが来た際に、過去の対応履歴や既知のFAQ文書との類似度を計算し、関連する情報や担当者をサジェストするシステム。
- 社内文書検索: 特定のドキュメントやクエリ(検索語)に対して、最も関連性の高い社内文書を探し出すシステム。
- コンテンツレコメンデーション: ユーザーが閲覧した記事や商品レビューに類似する他のコンテンツを推薦する機能。
- 重複文書の検出: 文書集合の中から、内容が酷似している文書やコピーコンテンツを検出する。
これらの応用では、単にキーワードが含まれているかだけでなく、文書全体の内容の類似性を評価することで、より高品質な情報提供や業務効率化が期待できます。
パフォーマンスと考慮事項
TF-IDFとコサイン類似度は比較的シンプルで理解しやすい手法ですが、実務で大規模なテキストデータに適用する際にはいくつかの考慮事項があります。
- 計算量とメモリ使用量: TF-IDFベクトルは、文書集合の語彙数を次元とするベクトルになります。語彙数が非常に多い場合(特に日本語のように分かち書きがされていない言語や、ドメイン固有の専門用語が多い場合)、ベクトルの次元数が大きくなり、
TfidfVectorizer
が生成する行列や、その後の類似度計算に多くのメモリと計算時間を要する可能性があります。TfidfVectorizer
は疎行列を扱うことでメモリ使用量を抑える工夫がされていますが、それでも限界はあります。 - 前処理の重要性: テキストの前処理(正規化、ノイズ除去、ストップワード除去、ステミングやレンマ化など)は、TF-IDFの精度に大きく影響します。特に日本語の場合は、形態素解析を行って適切に単語に分割し、不要な品詞や記号を除去することが必須となる場合が多いです。
- TF-IDFの限界: TF-IDFは単語の出現頻度に基づいた手法であり、単語の並び順や文脈、単語間の意味的な関連性(例:「犬」と「猫」は意味的に近いが、TF-IDFでは直接的な関連性が捉えにくい)を捉えることができません。このため、「犬が猫を追いかけた」と「猫が犬を追いかけた」のような、単語は同じだが意味が異なる文を区別することは困難です。
これらの限界を超えるためには、Word Embeddings(Word2Vec, GloVeなど)や、さらに発展したSentence Embeddings、あるいはTransformersベースのモデル(BERTなど)を用いた手法を検討する必要があります。これらの手法は単語や文脈の意味をより深く捉えることができますが、計算コストが高く、利用するライブラリやモデルもより専門的になります。
実務においては、まずはTF-IDFのような比較的シンプルな手法から始めて、必要に応じてより高度な手法を検討していくのが現実的なアプローチとなるでしょう。
まとめ
本記事では、テキストデータから関連情報を抽出するための基本的な手法として、TF-IDFを用いたテキストのベクトル化と、コサイン類似度による類似度計算について解説しました。Pythonのscikit-learn
ライブラリを用いることで、これらの処理を比較的容易に実装できることを示しました。
TF-IDFとコサイン類似度は、そのシンプルさにも関わらず、文書検索、分類、推薦など、多くのテキスト分析タスクにおいて有効なベースライン手法となります。実務でテキストデータからの情報抽出に取り組む際の、強力なツールとしてご活用いただければ幸いです。より高度なタスクや大規模データへの対応が必要になった際には、本記事で触れた発展的な手法についても学習を進めてみることをお勧めいたします。