intentfinder-api / clusterer.py
youngryong's picture
deploy: IntentFinder API (HF Docker Space)
21bdc64
Raw
History Blame Contribute Delete
3.6 kB
# S-BERT ์ž„๋ฒ ๋”ฉ โ†’ UMAP ์ฐจ์›์ถ•์†Œ โ†’ HDBSCAN ๋ฐ€๋„ ๊ตฐ์ง‘ํ™”๋กœ ํ‚ค์›Œ๋“œ๋ฅผ ์˜๋ฏธ ํด๋Ÿฌ์Šคํ„ฐ๋กœ ๋ฌถ๋Š” ๋ชจ๋“ˆ
import warnings
from functools import lru_cache
import hdbscan
import pandas as pd
import umap
from sentence_transformers import SentenceTransformer
warnings.filterwarnings("ignore") # UMAP/HDBSCAN์˜ ๋น„์น˜๋ช… ๊ฒฝ๊ณ  ์–ต์ œ
# ๋‹ค๊ตญ์–ด ๊ฒฝ๋Ÿ‰ ๋ชจ๋ธ(ํ•œ๊ตญ์–ด ํฌํ•จ, ~120MB). ํ’ˆ์งˆ ์šฐ์„  ์‹œ jhgan/ko-sroberta-multitask๋กœ ๊ต์ฒด ๊ฐ€๋Šฅ
MODEL_NAME = "paraphrase-multilingual-MiniLM-L12-v2"
MIN_KEYWORDS = 20 # ์ด๋ณด๋‹ค ์ ์œผ๋ฉด ๊ตฐ์ง‘์ด ๋ฌด์˜๋ฏธ
RELEVANCE_THRESHOLD = 0.35 # ํ‚ค์›Œ๋“œ๋ณ„ ์‹œ๋“œ ์œ ์‚ฌ๋„ ํ•˜ํ•œ (๊ณ„์‚ฐ๊ธฐยทํ‚ค๋ณด๋“œ ๋“ฑ ๋ฌด๊ด€์–ด ์ œ๊ฑฐ)
CLUSTER_RELEVANCE_THRESHOLD = 0.45 # ํด๋Ÿฌ์Šคํ„ฐ ํ‰๊ท  ์‹œ๋“œ ๊ด€๋ จ์„ฑ ํ•˜ํ•œ (๋ฐฑํŒฉยท์ƒŒ๋“ค ๋“ฑ ํŒจ์…˜ ์žกํ™” ๊ตฐ์ง‘ ์ œ๊ฑฐ)
@lru_cache(maxsize=1)
def _model() -> SentenceTransformer:
# ๋ชจ๋ธ์€ ํ•œ ๋ฒˆ๋งŒ ๋กœ๋“œํ•ด ์žฌ์‚ฌ์šฉ (์ฝœ๋“œ ์Šคํƒ€ํŠธ ๋น„์šฉ ์ ˆ๊ฐ)
return SentenceTransformer(MODEL_NAME)
def filter_by_relevance(df: pd.DataFrame, seed: str, threshold: float = RELEVANCE_THRESHOLD):
# ์‹œ๋“œ์™€ ์œ ์‚ฌ๋„๊ฐ€ ๋‚ฎ์€ ๋ฌด๊ด€ ํ‚ค์›Œ๋“œ ์ œ๊ฑฐ. (ํ•„ํ„ฐ๋œ df, ์ •๊ทœํ™” ์ž„๋ฒ ๋”ฉ) ๋ฐ˜ํ™˜
model = _model()
seed_emb = model.encode([seed], normalize_embeddings=True)[0]
kw_emb = model.encode(df["keyword"].tolist(), normalize_embeddings=True)
sims = kw_emb @ seed_emb # ์ •๊ทœํ™”๋ผ ์žˆ์–ด ๋‚ด์  = ์ฝ”์‚ฌ์ธ ์œ ์‚ฌ๋„
df = df.copy()
df["relevance"] = sims
mask = sims >= threshold
return df[mask].reset_index(drop=True), kw_emb[mask]
def cluster_keywords(df: pd.DataFrame, embeddings=None, min_cluster_size: int = 10) -> list[dict]:
# ํ‚ค์›Œ๋“œ df๋ฅผ ์˜๋ฏธ ๊ตฐ์ง‘์œผ๋กœ ๋ฌถ์–ด ๊ฒ€์ƒ‰๋Ÿ‰์ˆœ ํด๋Ÿฌ์Šคํ„ฐ ์š”์•ฝ ๋ฐ˜ํ™˜ (๋…ธ์ด์ฆˆ -1 ์ œ์™ธ)
# embeddings: filter_by_relevance๊ฐ€ ๋งŒ๋“  ์ž„๋ฒ ๋”ฉ์„ ๋„˜๊ธฐ๋ฉด ์žฌ๊ณ„์‚ฐ์„ ์ƒ๋žตํ•œ๋‹ค
if len(df) < MIN_KEYWORDS:
return []
df = df.copy()
df["total_volume"] = df["search_volume_pc"].fillna(0) + df["search_volume_mobile"].fillna(0)
# 1) ์ •๊ทœํ™” ์ž„๋ฒ ๋”ฉ (์—†์œผ๋ฉด ์ƒˆ๋กœ ๊ณ„์‚ฐ)
emb = embeddings if embeddings is not None else _model().encode(
df["keyword"].tolist(), normalize_embeddings=True, show_progress_bar=False
)
# 2) UMAP์œผ๋กœ 5์ฐจ์› ์ถ•์†Œ (๊ณ ์ฐจ์› ๋ฉ”๊ฐ€ ํด๋Ÿฌ์Šคํ„ฐ ๋ฐฉ์ง€, ์ฝ”์‚ฌ์ธ ๊ธฐ๋ฐ˜)
n_neighbors = min(10, len(df) - 1)
reduced = umap.UMAP(
n_neighbors=n_neighbors, n_components=5, min_dist=0.0,
metric="cosine", random_state=42,
).fit_transform(emb)
# 3) HDBSCAN leaf ๋ฐฉ์‹์œผ๋กœ ์„ธ๋ถ„ํ™”๋œ ๋™์งˆ ๊ตฐ์ง‘ ์ถ”์ถœ
df["cluster_id"] = hdbscan.HDBSCAN(
min_cluster_size=min_cluster_size, cluster_selection_method="leaf"
).fit_predict(reduced)
clusters = []
for cid, g in df[df["cluster_id"] != -1].groupby("cluster_id"):
# ํด๋Ÿฌ์Šคํ„ฐ ์ „์ฒด๊ฐ€ ์‹œ๋“œ์™€ ๋А์Šจํ•œ ํŒจ์…˜ ์žกํ™”๋ฉด ๊ตฐ์ง‘์งธ ์ œ์™ธ
if "relevance" in g.columns and g["relevance"].mean() < CLUSTER_RELEVANCE_THRESHOLD:
continue
g = g.sort_values("total_volume", ascending=False)
clusters.append({
"cluster_id": int(cid),
"cluster_label": g.iloc[0]["keyword"], # ๋Œ€ํ‘œ ํ‚ค์›Œ๋“œ(์ตœ๊ณ  ๊ฒ€์ƒ‰๋Ÿ‰)
"keyword_count": int(len(g)),
"total_search_volume": int(g["total_volume"].sum()),
"top_keywords": g["keyword"].head(4).tolist(), # ์ฃผ์š” ํฌํ•จ ํ‚ค์›Œ๋“œ
})
clusters.sort(key=lambda c: c["total_search_volume"], reverse=True)
return clusters