LDA(潜在的ディリクレ配分法)は、文書集合から潜在的なトピック構造を抽出するための確率的生成モデルであり、直接的な意味解析ではなく、統計的手法によって文書内の重要な語彙を特定します。この手法は、文書が複数のトピックの混合で構成され、各トピックが単語の確率分布を持つという仮定に基づいています。
トピック数の選定
LDAでは、事前にトピック数Kを設定する必要があります。この値はモデルの解釈性と精度に大きく影響します。トピック数が少なすぎると情報が過度に抽象化され、多すぎると冗長で重複したトピックが生成される可能性があります。最適なKを見つけるために、以下の指標を用いることが一般的です:
- Coherence Score:トピック内単語の意味的一貫性を評価
- Perplexity:モデルの汎化性能を測定
これらの指標をプロットし、スコアの変化が安定する点やピークを最適なトピック数として採用します。
コード実装例
以下は、gensimライブラリを用いてLDAモデルを構築し、トピックごとのキーワードを抽出する例です。まず、最適なトピック数を余弦類似度に基づいて探索し、その後、その数で最終モデルを訓練します。
import pandas as pd
import numpy as np
import re
import itertools
import matplotlib.pyplot as plt
from gensim import corpora, models
# 日本語フォント設定
plt.rcParams['font.sans-serif'] = ['IPAexGothic']
plt.rcParams['axes.unicode_minus'] = False
# 分かち書き済みデータの読み込み
df = pd.read_csv("tokenized_data.csv", encoding='utf-8')
token_lists = df['tokens'].apply(eval).tolist() # 文字列→リスト変換
# 辞書とコーパスの作成
dictionary = corpora.Dictionary(token_lists)
corpus = [dictionary.doc2bow(tokens) for tokens in token_lists]
# 余弦類似度計算関数
def cosine_sim(vec1, vec2):
dot = sum(a * b for a, b in zip(vec1, vec2))
norm1 = sum(a * a for a in vec1) ** 0.5
norm2 = sum(b * b for b in vec2) ** 0.5
return dot / (norm1 * norm2) if norm1 and norm2 else 0.0
# 最適トピック数探索
def find_optimal_topics(corpus, dictionary, max_k=10):
scores = [1.0] # k=1の類似度は1とする
for k in range(2, max_k + 1):
lda = models.LdaModel(corpus, num_topics=k, id2word=dictionary, passes=10)
topics = [lda.show_topic(tid, topn=30) for tid in range(k)]
# 各トピックの単語頻度ベクトルを作成
all_words = set(word for topic in topics for word, _ in topic)
vectors = []
for topic in topics:
freq_map = dict(topic)
vectors.append([freq_map.get(w, 0) for w in all_words])
# 全ペアの平均類似度を計算
pairs = list(itertools.combinations(range(k), 2))
similarities = [cosine_sim(vectors[i], vectors[j]) for i, j in pairs]
scores.append(np.mean(similarities) if similarities else 0.0)
return scores
# スコアプロット
optimal_scores = find_optimal_topics(corpus, dictionary)
plt.figure(figsize=(8, 5))
plt.plot(range(1, len(optimal_scores)+1), optimal_scores, marker='o')
plt.title('トピック数 vs 平均類似度')
plt.xlabel('トピック数 K')
plt.ylabel('平均余弦類似度')
plt.grid(True)
plt.show()
# 最適Kで再学習(例: K=4)
final_lda = models.LdaModel(corpus, num_topics=4, id2word=dictionary, passes=20)
# 各トピックの上位10語を抽出
for idx, topic in enumerate(final_lda.print_topics(num_words=10)):
keywords = re.findall(r'"(.*?)"', topic[1])
print(f"トピック {idx+1}: {', '.join(keywords)}")
また、トピック数を固定せず簡易的に使用する場合も可能です:
import jieba
from gensim import corpora, models
import re
text = "6月19日、中国愛心都市プロジェクトの発表会が北京で開催されました。..." # 長文省略
sentences = [s.strip() for s in text.split('。') if s.strip()]
tokenized = [list(jieba.cut(s)) for s in sentences]
flattened_tokens = [word for sent in tokenized for word in sent]
# 辞書・コーパス構築
vocab = corpora.Dictionary([[w] for w in flattened_tokens])
bow_corpus = [vocab.doc2bow([w]) for w in flattened_tokens]
# LDA実行(トピック数=3で固定)
simple_lda = models.LdaModel(bow_corpus, num_topics=3, id2word=vocab)
topics = simple_lda.print_topics(num_words=8)
for tid, desc in topics:
words = re.findall(r'"(.*?)"', desc)
print(f"トピック{tid}: {', '.join(words)}")