← 전체 해설 목록  ·  week-03 · 3강

3강 — 키워드 추출과 토픽 분석

🎯 학습 목표

📖 해설

지난 시간(19차시) ABSA로 "graphics: 긍정, performance: 부정"처럼 분석했죠. 그런데 에스펙트는 우리가 미리 정의한 것입니다. 만약 유저 100명이 "로딩 시간이 너무 느리다"고 말하는데 우리 에스펙트 목록에 없다면? 놓칩니다. 오늘은 리뷰에서 키워드를 직접 발견하는 바텀업 접근을 배웁니다.

1. 탑다운의 한계, 바텀업의 등장

오늘 배울 세 기법은 모두 바텀업입니다: ① 키워드 추출 → ② 토픽 클러스터링 → ③ 빈도 분석. 최종 목표는 이런 인사이트예요 — "리뷰 1,000개에서 'bugs'가 234회 언급됨. Technical 토픽이 전체의 37%."

2. 키워드 추출이란 — 그리고 왜 LLM인가

독후감 요약으로 비유해 봅시다. 책 한 권을 읽고 핵심 단어 다섯 개(마법, 우정, 성장, 호그와트…)로 요약하면 그 책이 뭔지 대강 압니다. 리뷰도 마찬가지예요. 버그·DLC·가격 같은 키워드가 많이 나오면, 리뷰 1,000개를 다 읽지 않고도 유저들이 뭘 말하는지 파악할 수 있습니다.

키워드 추출 방법은 두 갈래입니다.

방식 장점 단점
전통 NLP (TF-IDF·RAKE·TextRank 등) 빠르고 비용 거의 없음(수학적 계산) 맥락을 이해 못 함
LLM 기반 맥락 이해 + 의미적으로 중요한 것 식별, 정확 API 비용, 상대적으로 느림

전통 NLP는 단어 빈도(TF-IDF)나 규칙·그래프 기반 알고리즘(RAKE, TextRank)으로 키워드를 뽑습니다. 단점은 치명적이에요 — not bad를 not과 bad로 따로 떼어내 "부정"으로 처리할 수 있습니다. 의미가 정반대로 뒤집히죠. 반면 LLM은 맥락을 이해하니 not bad가 긍정 표현임을 알고, 무엇이 의미적으로 중요한지까지 가려냅니다. 정형화된 단순 문제는 NLP로도 되지만, 실무의 리뷰는 그렇게 단순하지 않기 때문에 우리 과정은 LLM 기반으로 진행합니다. 아직 시작 단계인 첨단 영역이라는 점도 기억해 두세요.

3. 호출·정제를 헬퍼 함수로 묶기

1·2강에서 매번 client.chat.completions.create(...)를 길게 쓰고, 응답에서 ```json을 떼어냈죠. 같은 코드가 반복되니 이제 헬퍼 함수로 묶어 정리합니다. 더불어 정제 함수도 정규식으로 더 튼튼하게 만듭니다(코드블록뿐 아니라 앞뒤에 설명이 붙는 경우까지 처리).

import re, json

# ① 모델을 한 곳에서 관리 — 바꾸려면 이 줄만 수정
MODEL = "mistralai/devstral-2512:free"

def clean_json_response(text):
    """LLM 응답에서 순수 JSON 문자열만 추출한다(코드블록·앞뒤 설명 제거)."""
    text = text.strip()
    # ```json ... ``` 코드블록
    if "```json" in text:
        m = re.search(r'```json\s*([\s\S]*?)\s*```', text)
        if m: return m.group(1).strip()
    # 그냥 ``` ... ``` 코드블록
    if "```" in text:
        m = re.search(r'```\s*([\s\S]*?)\s*```', text)
        if m: return m.group(1).strip()
    # 코드블록이 없으면 첫 { 또는 [ 부터 마지막 } 또는 ]까지 통째로
    m = re.search(r'(\{[\s\S]*\}|\[[\s\S]*\])', text)
    if m: return m.group(1).strip()
    return text

def call_llm(system_prompt, user_message):
    """시스템/유저 프롬프트를 받아 LLM 응답 텍스트를 돌려준다."""
    response = client.chat.completions.create(
        model=MODEL,
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_message},
        ],
    )
    return response.choices[0].message.content

def call_llm_json(system_prompt, user_message):
    """LLM을 호출하고 정제까지 거쳐 파이썬 객체(dict/list)로 돌려준다."""
    raw = call_llm(system_prompt, user_message)
    return json.loads(clean_json_response(raw))

re(정규식)는 "이런 패턴의 글자"를 찾아내는 도구입니다. \{[\s\S]*\}는 "여는 중괄호부터 닫는 중괄호까지 전부"라는 뜻이에요. 이제 어떤 작업이든 call_llm_json(시스템프롬프트, 유저메시지) 한 줄로 끝납니다. 반복을 줄이면 정작 바뀌는 부분(프롬프트)에 집중할 수 있죠.

오늘 실습의 전체 로드맵은 네 단계입니다 — ① 키워드 추출 프롬프트 작성 → ② 토픽 분류 기능 추가 → ③ JSON 구조화 → ④ 빈도 분석 코드 작성. 사실 이건 17차시의 퓨샷(예시 제공)과 19차시의 ABSA 구조(정의·규칙·예시·형식)를 조합하는 것이라, 새 기법이라기보다 배운 걸 새 목적에 끼워 맞추는 연습에 가깝습니다.

4. 키워드 추출 프롬프트

설계는 늘 같은 네 단계입니다 — 역할 부여 → 규칙 정의 → 예시 제공 → 출력 형식. 이제 이 틀이 눈에 익으셨을 거예요. 그러면 우리가 바꿔야 할 핵심 부분(규칙)만 손보면 됩니다.

KEYWORD_EXTRACT_PROMPT = """너는 게임 리뷰 분석 전문가야.
리뷰에서 핵심 키워드를 추출해.

### 규칙 ###
- 게임과 관련된 키워드만 추출 (일반적인 단어 제외)
- 명사 또는 명사구 형태로
- 최대 5개까지
- 중요도 순으로 정렬

### 예시 ###
리뷰: "Graphics are stunning but the game crashes every hour"
키워드: ["graphics", "crashes", "stability issues"]
### 예시 끝 ###

JSON 배열 형식으로만 답해: ["키워드1", "키워드2", ...]"""

def extract_keywords(review):
    """리뷰에서 핵심 키워드 리스트를 뽑는다. 실패 시 빈 리스트."""
    try:
        return call_llm_json(KEYWORD_EXTRACT_PROMPT, f"리뷰: {review}")
    except json.JSONDecodeError:
        return []

for r in [
    "Graphics are beautiful but the loading times are terrible. Too many bugs!",
    "Amazing story and characters. Worth every penny. Highly recommend!",
]:
    print(r[:45], "→", extract_keywords(r))
# 예시: ['graphics', 'loading times', 'bugs'] / ['story', 'characters', 'value for money']

"게임 관련 키워드만"이라고 못 박으면 the·is·very 같은 일반 단어는 빠집니다. "명사/명사구"로 지정하면 graphics 같은 깔끔한 형태로 나와 더 쓸모 있죠. 프롬프트를 별도 변수(KEYWORD_EXTRACT_PROMPT)로 뺀 이유는, 여기가 계속 바뀌는 부분이라 따로 관리하면 편하기 때문입니다.

5. 토픽 클러스터링 — 흩어진 키워드를 묶기

bugs, crashes, lag, optimization, frame drops — 이 키워드들의 공통점은? 모두 기술 문제(technical)죠. 이렇게 비슷한 키워드를 하나의 주제로 묶는 게 토픽 클러스터링입니다. 파편적인 키워드도 묶으면 의미가 생깁니다.

토픽 포함 키워드 예시
technical bugs, crashes, lag, optimization
content story, characters, ending, DLC
value price, sale, worth, playtime
community multiplayer, server, matchmaking

에스펙트와 토픽이 비슷해 보이지만, 핵심 차이가 있습니다.

구분 에스펙트 (19차시) 토픽 (이번 시간)
접근 탑다운 (미리 정의) 바텀업 (데이터에서 발견)
유연성 고정됨 유연함
일관성 높음 낮을 수 있음
발견 정의한 것만 예상 못한 것도

둘은 우열이 아니라 상호 보완입니다. 에스펙트로 큰 틀을 잡고, 토픽으로 세부를 발견하는 하이브리드가 가장 강력해요. 키워드 추출 프롬프트에 토픽 정의를 더해, 키워드마다 토픽을 함께 붙여 봅시다.

KEYWORD_TOPIC_PROMPT = """너는 게임 리뷰 분석 전문가야.
리뷰에서 키워드를 추출하고 토픽으로 분류해.

### 토픽 정의 ###
- technical: 버그, 최적화, 성능, 크래시 관련
- content: 스토리, 캐릭터, 게임플레이, DLC 관련
- value: 가격, 플레이타임, 가성비 관련
- community: 멀티플레이, 서버, 매칭 관련
- other: 위에 해당하지 않는 것

### 예시 ###
리뷰: "Great story but crashes often. Worth the $20."
{
  "keywords": [
    {"word": "story", "topic": "content"},
    {"word": "crashes", "topic": "technical"},
    {"word": "price", "topic": "value"}
  ]
}
### 예시 끝 ###

JSON 형식으로만 답해."""

def extract_keywords_with_topics(review):
    """키워드와 토픽을 함께 추출한다."""
    try:
        return call_llm_json(KEYWORD_TOPIC_PROMPT, f"리뷰: {review}")
    except json.JSONDecodeError:
        return {"keywords": []}

토픽을 미리 정의(technical/content/value/community/other)하면 일관된 분류가 가능합니다. 이건 에스펙트(탑다운)와 키워드(바텀업)를 섞은 하이브리드인 셈이죠.

6. 빈도 분석 — 숫자가 우선순위를 만든다

키워드를 모았으면 몇 번 나왔는지 셉니다. 여기엔 LLM이 필요 없어요. 이미 LLM이 뽑아준 키워드를, 파이썬 기본 라이브러리 collections.Counter로 집계만 하면 됩니다. (LLM 분석 단계와 집계 단계를 머릿속에서 구분해 두면 개발이 쉬워집니다.)

from collections import Counter
import time

sample_reviews = [
    "Amazing graphics but terrible bugs. Crashes constantly.",
    "Story is masterpiece. Worth every penny despite some lag.",
    "Multiplayer servers are broken. Too expensive for what you get.",
    "Beautiful visuals and engaging gameplay. Minor optimization issues.",
    "Great characters but loading times are frustrating. Good value on sale.",
]

# 1) 여러 리뷰에서 키워드를 모은다
all_keywords = []
for i, review in enumerate(sample_reviews, 1):
    result = extract_keywords_with_topics(review)
    all_keywords.extend(result.get("keywords", []))   # extend: 리스트를 펼쳐서 합치기
    if i < len(sample_reviews):
        time.sleep(1)

# 2) 키워드 빈도 Top 10 (Counter가 세는 일을 대신해 준다)
word_counts = Counter(k["word"].lower() for k in all_keywords)
print("=== 키워드 빈도 Top 10 ===")
for rank, (word, count) in enumerate(word_counts.most_common(10), 1):
    print(f"{rank:2}. {word:20} {count}회")

# 3) 토픽별 분포 (%)
topic_counts = Counter(k["topic"] for k in all_keywords)
total = sum(topic_counts.values())
print("\n=== 토픽별 분포 ===")
for topic, count in topic_counts.most_common():
    print(f"{topic:12} {count:2}회 ({count/total*100:4.1f}%)")

extend와 append의 차이: append는 리스트를 통째로 하나의 요소로 넣고, extend는 펼쳐서 이어 붙입니다. 키워드 리스트끼리 합칠 땐 extend가 맞아요.

빈도가 왜 중요할까요? 빈도가 곧 우선순위이기 때문입니다. "버그가 좀 있는 것 같아"는 감(感)이고 근거가 없습니다. "버그 언급 234회, 전체의 23%"는 데이터 기반 의사결정이고, 누가 반박해도 설득력이 있죠.

데이터가 충분할 때 어떤 그림이 나오는지 봅시다. 리뷰 50개를 분석하면 예컨대 이런 결과가 나옵니다.

순위 키워드 빈도 토픽 분포
1 bugs 23회 technical 37%
2 graphics 18회 content 33%
3 story 15회 value 16%
4 crashes 14회 community 9%
5 price 12회 other 5%

bugs와 crashes가 나란히 상위권이고 둘 다 technical 토픽이죠. 토픽 분포에서도 technical이 37%로 가장 큽니다. 그러면 결론은 명확합니다 — 기술 문제(버그·크래시) 해결이 급선무. 이렇게 빈도와 토픽 분포가 곧장 우선순위와 행동으로 이어집니다.

주의: 위 샘플은 리뷰가 5개뿐이라 키워드가 대부분 1회씩 나옵니다(데이터가 적어서). 실제 리뷰 수백~수천 개를 넣으면 bugs 234회처럼 빈도 격차가 뚜렷해지고, 그제야 우선순위가 의미를 가집니다.

🔑 핵심 용어

용어 쉬운 설명
탑다운(top-down) 카테고리를 미리 정해놓고 분석. 일관적이나 정의 밖은 놓침 (에스펙트)
바텀업(bottom-up) 데이터에서 키워드를 먼저 발견해 묶음. 예상 밖 주제도 포착 (토픽)
키워드 추출 리뷰에서 핵심 단어/구를 자동으로 뽑기
전통 NLP vs LLM TF-IDF 등은 빠르나 맥락을 못 읽음(not bad→오해), LLM은 맥락 이해
토픽 클러스터링 비슷한 키워드를 하나의 주제로 묶기 (technical, content, value…)
빈도 분석 키워드가 몇 번 나왔는지 세기. 빈도 = 우선순위
Counter / most_common() 빈도를 세고 상위 N개를 뽑는 파이썬 기본 도구
헬퍼 함수 call_llm_json()처럼 반복 코드를 묶어 재사용
re(정규식) "이런 패턴의 글자"를 찾는 도구. 응답 정제에 사용

🎮 게임 기획자 시점

에스펙트(2강)가 기획자가 보기로 한 축이라면, 키워드/토픽은 유저가 실제로 말한 축입니다. 패치 후 "로딩이 느려졌다"가 갑자기 키워드 상위로 치고 올라오면, 우리가 정의하지 않았어도 데이터가 먼저 알려 주죠 — 바텀업의 진짜 가치는 모니터링하지 않던 문제의 조기 경보입니다. 그리고 빈도표는 회의의 무기가 됩니다. "체감상 버그 불만이 좀…"이 아니라 "버그 언급 234회(23%), technical 토픽 37%"라고 말하는 순간, 우선순위 논쟁이 근거 싸움으로 바뀝니다. 탑다운(에스펙트)으로 KPI를 고정하고, 바텀업(토픽)으로 신호를 줍는 — 두 축을 함께 돌리는 게 실전 운영입니다.

⚠️ 흔한 오해 / 짚고 갈 점

✅ 30초 정리

  1. 에스펙트(탑다운)는 정의 밖을 놓칩니다. 키워드 추출(바텀업)로 예상 못 한 이슈까지 발견하세요.
  2. 키워드는 맥락을 읽는 LLM으로 뽑고, 비슷한 것끼리 토픽으로 묶어 의미를 만듭니다.
  3. 빈도가 곧 우선순위. Counter로 집계해 "무엇을 먼저 고칠지"를 데이터로 답하세요. 반복 호출은 헬퍼 함수로 깔끔하게.

▶️ 다음 강 연결

다음 21차시는 프로젝트 시간(영상 없음)입니다 — 4차원 통합 분석. 1~3강에서 배운 감성(다차원)·에스펙트(ABSA)·키워드/토픽을 하나의 멀티태스크 프롬프트로 합쳐, 리뷰 한 건에서 모든 결과를 JSON으로 한 번에 받아 봅니다. 그리고 22차시(배치 처리)부터는 시야가 바뀝니다 — 리뷰 5개가 아니라 1,000개, 10만 개를 효율적이고 안전하게 처리하는 법으로 넘어갑니다.

전체📚 강의 목록