본문으로 건너뛰기

064 클러스터링 실전 - 문서 군집화

키워드: 문서, 군집화

개요

문서 군집화는 유사한 주제의 문서들을 자동으로 그룹화하는 기법입니다. 뉴스 분류, 검색 결과 정리, 고객 피드백 분석 등에 활용됩니다.

실습 환경

  • Python 버전: 3.11 권장
  • 필요 패키지: pycaret[full]>=3.0, scikit-learn

비즈니스 문제

목표: 대량의 텍스트 문서를 주제별로 자동 분류 활용: 뉴스 카테고리화, FAQ 그룹화, 리뷰 토픽 분석

데이터 준비

import pandas as pd
import numpy as np
from sklearn.datasets import fetch_20newsgroups

# 20 Newsgroups 데이터 (일부 카테고리만)
categories = [
'rec.sport.baseball',
'rec.sport.hockey',
'comp.graphics',
'comp.sys.mac.hardware',
'sci.med',
'sci.space'
]

# 064 데이터 로드
newsgroups = fetch_20newsgroups(
subset='all',
categories=categories,
remove=('headers', 'footers', 'quotes'), # 노이즈 제거
random_state=42
)

# 064 데이터프레임 생성
data = pd.DataFrame({
'text': newsgroups.data,
'category': [newsgroups.target_names[i] for i in newsgroups.target]
})

print(f"문서 수: {len(data)}")
print(f"\n카테고리별 문서 수:")
print(data['category'].value_counts())

텍스트 전처리

import re
from sklearn.feature_extraction.text import TfidfVectorizer

def preprocess_text(text):
"""텍스트 전처리"""
# 소문자 변환
text = text.lower()
# 특수 문자 제거
text = re.sub(r'[^a-zA-Z\s]', '', text)
# 여러 공백을 하나로
text = re.sub(r'\s+', ' ', text)
return text.strip()

# 064 전처리 적용
data['clean_text'] = data['text'].apply(preprocess_text)

# 064 빈 문서 제거
data = data[data['clean_text'].str.len() > 50]

print(f"전처리 후 문서 수: {len(data)}")
print(f"\n샘플 텍스트:")
print(data['clean_text'].iloc[0][:200])

TF-IDF 벡터화

from sklearn.feature_extraction.text import TfidfVectorizer

# 064 TF-IDF 벡터화
tfidf = TfidfVectorizer(
max_features=1000, # 상위 1000개 단어만
min_df=5, # 최소 5개 문서에 등장
max_df=0.7, # 70% 이상 문서에 등장하면 제외
stop_words='english', # 불용어 제거
ngram_range=(1, 2) # 유니그램 + 바이그램
)

tfidf_matrix = tfidf.fit_transform(data['clean_text'])

print(f"TF-IDF 행렬 크기: {tfidf_matrix.shape}")
print(f"(문서 수 × 특성 수)")

# 064 중요 단어 확인
feature_names = tfidf.get_feature_names_out()
print(f"\n상위 20개 단어: {feature_names[:20].tolist()}")

PyCaret 클러스터링

from pycaret.clustering import *
import pandas as pd

# 064 TF-IDF 결과를 데이터프레임으로
tfidf_df = pd.DataFrame(
tfidf_matrix.toarray(),
columns=tfidf.get_feature_names_out()
)

# 064 PyCaret 설정
clust = setup(
data=tfidf_df,
normalize=True,
session_id=42,
verbose=False
)

print("설정 완료!")

최적 클러스터 수 탐색

from sklearn.metrics import silhouette_score
import pandas as pd

X = get_config('X')

# 064 실루엣 분석
results = []
for k in range(2, 10):
kmeans = create_model('kmeans', num_clusters=k)
clustered = assign_model(kmeans)
labels = clustered['Cluster'].values

score = silhouette_score(X, labels)
results.append({'K': k, 'Silhouette': score})

df_results = pd.DataFrame(results)
print("K별 실루엣 점수:")
print(df_results)

# 064 실제 카테고리 수가 6이므로 K=6 선택

클러스터링 수행

# 064 K-Means (K=6)
kmeans = create_model('kmeans', num_clusters=6)
clustered = assign_model(kmeans)

# 064 결과 병합
data['Cluster'] = clustered['Cluster'].values

print("\n클러스터별 문서 수:")
print(data['Cluster'].value_counts().sort_index())

클러스터 품질 평가

from sklearn.metrics import (
silhouette_score,
adjusted_rand_score,
normalized_mutual_info_score
)

X = get_config('X')
pred_labels = data['Cluster'].values

# 064 내부 평가 (정답 없이)
sil_score = silhouette_score(X, pred_labels)
print(f"실루엣 점수: {sil_score:.4f}")

# 064 외부 평가 (정답 있으므로)
true_labels = data['category'].values
ari = adjusted_rand_score(true_labels, pred_labels)
nmi = normalized_mutual_info_score(true_labels, pred_labels)

print(f"ARI: {ari:.4f}")
print(f"NMI: {nmi:.4f}")

클러스터-카테고리 매핑

import pandas as pd

# 064 혼동 행렬 (클러스터 vs 실제 카테고리)
confusion = pd.crosstab(data['Cluster'], data['category'])
print("\n클러스터 vs 실제 카테고리:")
print(confusion)

# 064 각 클러스터의 다수 카테고리
print("\n클러스터별 주요 카테고리:")
for cluster in sorted(data['Cluster'].unique()):
cluster_data = data[data['Cluster'] == cluster]
majority = cluster_data['category'].mode()[0]
purity = (cluster_data['category'] == majority).mean()
print(f" 클러스터 {cluster}: {majority} (순도: {purity:.2%})")

클러스터별 핵심 키워드

import numpy as np

def get_top_keywords(tfidf_matrix, labels, feature_names, n_top=10):
"""클러스터별 상위 키워드 추출"""
clusters = {}

for cluster in np.unique(labels):
# 클러스터에 속한 문서의 TF-IDF 평균
cluster_docs = tfidf_matrix[labels == cluster]
mean_tfidf = np.asarray(cluster_docs.mean(axis=0)).flatten()

# 상위 키워드
top_indices = mean_tfidf.argsort()[-n_top:][::-1]
top_keywords = [feature_names[i] for i in top_indices]
clusters[cluster] = top_keywords

return clusters

# 064 키워드 추출
top_keywords = get_top_keywords(
tfidf_matrix,
data['Cluster'].values,
tfidf.get_feature_names_out()
)

print("\n=== 클러스터별 핵심 키워드 ===\n")
for cluster, keywords in top_keywords.items():
print(f"클러스터 {cluster}: {', '.join(keywords)}")

워드 클라우드

from wordcloud import WordCloud
import matplotlib.pyplot as plt

fig, axes = plt.subplots(2, 3, figsize=(15, 10))

for idx, cluster in enumerate(sorted(data['Cluster'].unique())):
ax = axes[idx // 3, idx % 3]

# 클러스터 문서 텍스트 결합
cluster_text = ' '.join(data[data['Cluster'] == cluster]['clean_text'])

# 워드 클라우드
wordcloud = WordCloud(
width=400, height=300,
background_color='white',
max_words=50
).generate(cluster_text)

ax.imshow(wordcloud, interpolation='bilinear')
ax.set_title(f'Cluster {cluster}')
ax.axis('off')

plt.tight_layout()
plt.savefig('document_wordclouds.png', dpi=150)

차원 축소 시각화

from sklearn.manifold import TSNE
import matplotlib.pyplot as plt

# 064 t-SNE (고차원 → 2D)
# 064 샘플링하여 속도 향상
sample_size = min(1000, len(data))
sample_idx = np.random.choice(len(data), sample_size, replace=False)

X_sample = tfidf_matrix[sample_idx].toarray()
labels_sample = data['Cluster'].values[sample_idx]
categories_sample = data['category'].values[sample_idx]

tsne = TSNE(n_components=2, random_state=42, perplexity=30)
X_tsne = tsne.fit_transform(X_sample)

# 064 클러스터별 시각화
fig, axes = plt.subplots(1, 2, figsize=(15, 6))

# 064 클러스터 레이블
scatter1 = axes[0].scatter(X_tsne[:, 0], X_tsne[:, 1],
c=labels_sample, cmap='viridis', alpha=0.6)
axes[0].set_title('Clusters (Predicted)')
plt.colorbar(scatter1, ax=axes[0])

# 064 실제 카테고리
category_codes = pd.Categorical(categories_sample).codes
scatter2 = axes[1].scatter(X_tsne[:, 0], X_tsne[:, 1],
c=category_codes, cmap='tab10', alpha=0.6)
axes[1].set_title('Categories (Actual)')
plt.colorbar(scatter2, ax=axes[1])

plt.tight_layout()
plt.savefig('document_tsne.png', dpi=150)

새 문서 분류

# 064 새 문서의 클러스터 예측
new_docs = [
"The baseball game was exciting with many home runs scored.",
"My computer graphics card needs an upgrade for gaming.",
"The space shuttle mission was successful.",
"The patient showed symptoms of the flu.",
]

# 064 전처리 및 벡터화
new_clean = [preprocess_text(doc) for doc in new_docs]
new_tfidf = tfidf.transform(new_clean)
new_df = pd.DataFrame(new_tfidf.toarray(), columns=tfidf.get_feature_names_out())

# 064 예측
predictions = predict_model(kmeans, data=new_df)

print("\n새 문서 클러스터 예측:")
for i, (doc, pred) in enumerate(zip(new_docs[:50], predictions['Cluster'])):
print(f" 문서: '{doc[:50]}...'")
print(f" → 클러스터 {pred}")
print()

토픽 라벨링

# 064 클러스터에 의미 있는 이름 부여
cluster_labels = {}

for cluster in sorted(data['Cluster'].unique()):
keywords = top_keywords[cluster][:3]
cluster_labels[cluster] = ' / '.join(keywords)

print("\n클러스터 라벨:")
for cluster, label in cluster_labels.items():
print(f" 클러스터 {cluster}: {label}")

# 064 데이터에 라벨 추가
data['Topic'] = data['Cluster'].map(cluster_labels)

계층적 클러스터링과 비교

from pycaret.clustering import *
from sklearn.metrics import silhouette_score, adjusted_rand_score

# 064 계층적 클러스터링
hclust = create_model('hclust', num_clusters=6)
hclust_result = assign_model(hclust)

# 064 비교
print("\n=== 알고리즘 비교 ===\n")

algorithms = {
'K-Means': data['Cluster'].values,
'Hierarchical': hclust_result['Cluster'].values
}

X = get_config('X')
true_labels = data['category'].values

for name, labels in algorithms.items():
sil = silhouette_score(X, labels)
ari = adjusted_rand_score(true_labels, labels)
print(f"{name}:")
print(f" 실루엣: {sil:.4f}")
print(f" ARI: {ari:.4f}")
print()

모델 저장

import pickle

# 064 클러스터링 모델 저장
save_model(kmeans, 'document_clustering_model')

# 064 TF-IDF 벡터라이저도 저장 (새 문서 변환용)
with open('tfidf_vectorizer.pkl', 'wb') as f:
pickle.dump(tfidf, f)

print("모델 저장 완료!")

실무 팁

  1. 전처리 중요: 불용어 제거, 어간 추출(stemming), 표제어 추출(lemmatization)
  2. TF-IDF 파라미터: max_features, min_df, max_df 조절
  3. 차원 축소: LSA/SVD, LDA 활용 가능
  4. 평가: 정답이 있으면 ARI/NMI, 없으면 실루엣
  5. 해석: 키워드 추출로 클러스터 의미 파악

정리

  • 문서 군집화는 TF-IDF + 클러스터링 조합
  • 키워드 추출로 클러스터 해석
  • t-SNE로 고차원 텍스트 시각화
  • 새 문서 분류에 활용 가능
  • 전처리가 결과에 큰 영향

다음 글 예고

다음 글에서는 클러스터링 결과 활용하기를 다룹니다.


PyCaret 머신러닝 마스터 시리즈 #064