1. K-means 문서 군집화란?
데이터를 $k$개의 그룹(클러스터)으로 나누는 대표적인 비지도 학습 알고리즘입니다. 텍스트 분석에서는 주로 TF-IDF 등으로 벡터화된 문서들이 주어졌을 때, 내용이 비슷한 문서끼리 같은 그룹으로 묶는 데 사용됩니다.
주요 과정
- 임의의 중심(centroid) k개를 설정합니다.
- 모든 데이터를 가장 가까운 중심에 배정합니다.
- 군집 내 데이터들의 평균을 구해 중심을 새롭게 이동합니다.
- 중심의 위치가 변하지 않을 때까지 위 과정을 반복합니다.
활용 예시
- 배송 불만 리뷰, 가격 만족 리뷰 등을 자동으로 분류할 때 사용합니다.
- sklearn 라이브러리의 KMeans 클래스를 사용하여 구현하며, n_clusters (군집 개수) 설정이 필수적입니다.
kmeans = KMeans(
n_clusters=k,
random_state=42,
n_init="auto" # 최신 sklearn에서 권장 )
2. 최적의 군집 개수(K) 결정 방법
A. 엘보우 기법 (Elbow Method)
군집 내 오차 제곱 합(SSE, inertia)이 줄어드는 추세를 보고 결정하는 방법입니다.
특징
- k가 늘어날수록 SSE는 무조건 작아집니다.
- 하지만 특정 지점부터는 감소 폭이 확 줄어드는데, 그 꺾이는 지점(팔꿈치 모양)을 최적의 k 후보로 봅니다.

B. 실루엣 계수 (Silhouette Score)
군집화가 얼마나 잘 되었는지를 -1에서 1 사이의 점수로 정량화하는 방법입니다.
특징
- 점수 해석: +1에 가까울수록 자기 군집에 잘 속해 있고 다른 군집과는 잘 분리된 상태입니다. 0은 경계, 음수는 잘못 분류된 상태를 의미합니다.
- 수식: a(i)는 내 군집 내 평균 거리, b(i)는 가장 가까운 타 군집과의 평균 거리일 때 다음과 같습니다.

3. 엘보우 vs 실루엣 비교 및 선택 가이드
두 방법은 상호 보완적으로 사용되며, 상황에 따라 적절한 것을 선택해야 합니다.
엘보우 (Elbow) 추천 상황
- 데이터와 차원이 매우 커서 계산 속도가 중요할 때 유리합니다.
- $k$의 대략적인 범위(예: 3~8 사이)를 빠르게 필터링하고 싶을 때 사용합니다.
- 계산이 가볍지만, 최적점이 눈으로 보기에 애매할 수 있습니다.
실루엣 (Silhouette) 추천 상황
- 데이터 크기가 적당하고, 군집 품질을 숫자로 명확히 증명해야 할 때(보고서, 논문 등) 유리합니다.
- 엘보우 기법으로 k 후보를 좁힌 뒤, 최종적으로 어떤 k가 더 나은지 비교할 때(예: 4, 5, 6 중 선택) 사용합니다.
- 해석이 직관적이지만 계산량이 많습니다
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.cluster import KMeans
import numpy as np
import pandas as pd
# --- 1. 데이터 준비 및 설정 ---
# 1) 예제 리뷰 데이터
docs = [
"배송이 빠르고 포장이 꼼꼼해서 만족합니다",
"배송이 너무 느리고 포장도 엉망이에요",
"가격은 저렴한데 품질이 좋아요",
"가격이 비싼 편이지만 디자인이 예쁩니다",
"배송도 빠르고 품질도 좋아서 또 구매하고 싶어요",
]
# 2) TF-IDF 벡터화 객체 초기화
# max_df=0.8: 전체 문서의 80% 이상에 나타나는 단어는 제거 (너무 흔한 단어)
# min_df=1: 최소 1개 문서에 나타나는 단어만 사용
# token_pattern: 한글 포함 모든 단어(토큰)를 인식하도록 설정
tfidf = TfidfVectorizer(
max_df=0.8, min_df=1, token_pattern=r"(?u)\b\w+\b" # 한글 포함 토큰
)
# 데이터를 학습하고 TF-IDF 행렬 X를 생성 (희소 행렬 형태)
X = tfidf.fit_transform(docs) # shape: (문서 수, 단어 수)
# --- 2. K-means 모델 학습 ---
k = 2 # 군집 개수 설정
# n_init="auto": 중심 초기화를 여러 번 시도하여 가장 좋은 결과를 선택 (최신 sklearn 권장)
kmeans = KMeans(n_clusters=k, random_state=42, n_init="auto")
# TF-IDF 행렬 X를 이용하여 K-means 모델 학습
kmeans.fit(X)
# 각 문서가 할당된 클러스터 레이블 (0 또는 1)
labels = kmeans.labels_
print("문서별 클러스터 라벨:", labels)
# --- 3. 클러스터링 결과 확인 ---
# 문서와 클러스터 정보를 같이 보기 위해 DataFrame 생성
df_cluster = pd.DataFrame({"doc_id": range(len(docs)), "text": docs, "cluster": labels})
print("\n[문서별 클러스터 할당 결과]")
print(df_cluster)
# 클러스터별 문서 묶어서 출력하여 클러스터링 결과 시각화
for c in range(k):
print(f"\n=== 클러스터 {c} ===")
# 현재 클러스터 c에 속하는 문서만 필터링하여 출력
for text in df_cluster[df_cluster["cluster"] == c]["text"]:
print("-", text)
# --- 4. 문서 및 클러스터별 대표 키워드 추출 함수 ---
# 단어집 (TF-IDF 벡터라이저의 피처 이름) 먼저 가져오기
feature_names = tfidf.get_feature_names_out()
# 개별 문서의 상위 키워드를 추출하는 함수
def get_top_keywords_for_doc(tfidf_ventor, top_n=5):
# print(tfidf_ventor.toarray()) # 문서의 전체 TF-IDF 벡터 출력 (주석 처리)
# 희소 행렬을 넘파이 배열로 변환하고 1차원으로 평탄화
vec = tfidf_ventor.toarray().flatten()
# 가중치가 큰 순서대로 인덱스 top_n개를 추출 (음수(-vec)로 변환 후 argsort는 내림차순 효과)
top_idx = np.argsort(-vec)[:top_n]
# 키워드 이름과 가중치를 묶어 반환
return list(zip(feature_names[top_idx], vec[top_idx]))
# 0번 문서의 상위 3개 키워드 추출 및 출력 예시
top_keyword_doc0 = get_top_keywords_for_doc(X[0], top_n=3)
print("\n[0번 문서 대표 키워드]")
for word, score in top_keyword_doc0:
print(f"{word} : {score:.3f}")
# 클러스터별 대표 키워드를 추출하는 함수 (평균 벡터 사용)
def get_top_keywords_for_cluster(X, labels, cluster_id, top_n=5):
# 현재 클러스터 ID와 일치하는 문서들을 찾는 마스크 생성
mask = labels == cluster_id
# 1. 마스킹된 희소 행렬을 밀집 배열(toarray())로 변환 후
# 2. 단어 축(axis=0)을 따라 평균을 계산하여 클러스터 대표 벡터를 생성 (안정성 확보)
cluster_vec = X[mask].toarray().mean(axis=0)
# 3. 평균 벡터 처리 (넘파이 배열이므로 flatten 해도 무방)
vec = cluster_vec.flatten()
# 4. 가중치가 큰 순서대로 인덱스 top_n개를 추출
top_idx = np.argsort(-vec)[:top_n]
# 5. 키워드와 평균 가중치를 묶어 반환
return list(zip(feature_names[top_idx], vec[top_idx]))
# 모든 클러스터에 대해 대표 키워드 추출 및 출력
for c in range(k):
# 현재 클러스터 c의 상위 3개 대표 키워드를 추출
top_keywords = get_top_keywords_for_cluster(X, labels, cluster_id=c, top_n=3)
print(f"\n==== 클러스터 {c} 대표 키워드 ====")
for word, score in top_keywords:
print(f"{word} : {score:.3f}")
'''
문서별 클러스터 라벨: [0 0 1 1 1]
[문서별 클러스터 할당 결과]
doc_id text cluster
0 0 배송이 빠르고 포장이 꼼꼼해서 만족합니다 0
1 1 배송이 너무 느리고 포장도 엉망이에요 0
2 2 가격은 저렴한데 품질이 좋아요 1
3 3 가격이 비싼 편이지만 디자인이 예쁩니다 1
4 4 배송도 빠르고 품질도 좋아서 또 구매하고 싶어요 1
=== 클러스터 0 ===
- 배송이 빠르고 포장이 꼼꼼해서 만족합니다
- 배송이 너무 느리고 포장도 엉망이에요
=== 클러스터 1 ===
- 가격은 저렴한데 품질이 좋아요
- 가격이 비싼 편이지만 디자인이 예쁩니다
- 배송도 빠르고 품질도 좋아서 또 구매하고 싶어요
[0번 문서 대표 키워드]
포장이 : 0.482
꼼꼼해서 : 0.482
만족합니다 : 0.482
==== 클러스터 0 대표 키워드 ====
배송이 : 0.382
포장이 : 0.241
꼼꼼해서 : 0.241
==== 클러스터 1 대표 키워드 ====
가격은 : 0.167
좋아요 : 0.167
저렴한데 : 0.167'''
import re
import numpy as np
import pandas as pd
from konlpy.tag import Okt
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt
# 코드 상단에 추가되어야 할 import 구문
from sklearn.metrics import silhouette_score
df = pd.read_csv(
r"/Users/choimanseon/Documents/multicampus/example/nlp/stat_nlp/movie_reviews.csv",
encoding="utf-8",
)
df = df.dropna()
_, df_sample = train_test_split(
df, test_size=10000, stratify=df["label"], shuffle=True, random_state=42
)
df_sample = df_sample.reset_index(drop=True)
okt = Okt()
with open(
r"/Users/choimanseon/Documents/multicampus/example/nlp/stat_nlp/stopwords-ko.txt",
encoding="utf-8",
) as f:
stopwords = set(w.strip() for w in f if w.strip())
add_stopwords = set(
[
"그리고",
"그래서",
"하지만",
"그러나",
"너무",
"정말",
"진짜",
"조금",
"또한",
"그냥",
"매우",
"에서",
"에게",
"미만",
"이상",
"같은",
"하다",
"되다",
]
)
stopwords.update(add_stopwords)
def preprocess_text(text: str) -> list:
text = text.lower()
text = re.sub(r"[^0-9a-zA-Z가-힣\s]", " ", text)
morphs = okt.pos(text, stem=True)
tokens = []
for word, tag in morphs:
if tag in ["Noun", "Verb", "Adjective"]:
if word not in stopwords and len(word) > 1:
tokens.append(word)
return tokens
TEXT_COL = "document"
tfidf = TfidfVectorizer(
max_df=0.8, min_df=1, token_pattern=None, tokenizer=preprocess_text
)
X_tfidf = tfidf.fit_transform(df_sample[TEXT_COL])
inertias = []
#! 엘보우
for k in range(1, 11):
kmeans = KMeans(n_clusters=k, random_state=42, n_init="auto")
kmeans.fit(X_tfidf)
inertias.append(kmeans.inertia_)
plt.plot(range(1, 11), inertias, marker="o")
plt.xlabel("k 클러스터 수")
plt.ylabel("Inertia (SSE)")
plt.title("Elbow Method")
plt.show()
#! 실루엣
k_range = range(2, 11)
sil_scores = []
for k in k_range:
kmeans = KMeans(n_clusters=k, random_state=42, n_init="auto")
labels = kmeans.fit_predict(X_tfidf)
score = silhouette_score(X_tfidf, labels)
sil_scores.append(score)
print(f"k={k}, silhouette_score = {score:.3f}")
plt.plot(k_range, sil_scores, marker="o")
plt.xlabel("k cluster count")
plt.ylabel("Silhouette Score")
plt.title("Silhouette Method")
plt.show()


'데이터 분석 > 머신러닝, 딥러닝' 카테고리의 다른 글
| 단어 임베딩 (0) | 2025.12.03 |
|---|---|
| 딥러닝 텍스트 전처리 (0) | 2025.12.01 |
| Cosine 유사도 (0) | 2025.11.28 |
| RNN(Recurrent Neural Networks) (0) | 2025.11.27 |
| CNN(Convolutional neural network) (0) | 2025.11.26 |