쩝쩝LAB 리서치
리뷰 문장을 '의미 단위'로 묶어 만든 음식점 추천 — 유저→식당 · 유사 식당 · 유사 유저
- 2,110만
- 정제한 리뷰 mention
- 96% → 48%
- 군집 노이즈 (UMAP 도입 후)
- 3종
- semantic_id 기반 추천 구현
- 0.950
- 유사 식당 1위 score (같은 브랜드 다른 지점)
한 줄로
“맛있어요” 같은 평균적인 평가가 아니라, 리뷰어가 실제로 하려던 말을 의미 단위로 모아 취향 벡터를 만들고, 그 벡터로 유저→식당 · 유사 식당 · 유사 유저 추천을 구현했습니다. 대상은 서울·수도권, 동해권(고성·속초·양양·강릉), 제주, 오사카·도쿄입니다.
데이터 파이프라인
- 수집 — 지도 서비스의 공개 리뷰 데이터를 비상업적 연구 목적으로 수집
- mention 단위 정규화 — 사전 추출 → ko-sroberta + HDBSCAN → Qwen3-8B 분류기
- 카테고리 / 서브카테고리 매핑 — 분류 모델 + 저신뢰 구간은 에이전트가 재분류
- 개인화 평가 체계 + Semantic ID 기반 추천 아키텍처 설계
문제 — 짧은 표현으로는 취향이 잘린다
처음에는 리뷰에서 짧은 표현(mention span, 문장당 평균 6.6개)을 뽑아 카테고리를 붙이는 방식으로 데이터 적재까지 끝냈습니다. 그런데 두 가지 벽에 부딪혔습니다.
- 표현이 너무 짧다 — “친절해요”, “웨이팅 김” 같은 어구로는 리뷰어의 의사 전체가 담기지 않습니다.
- 미분류 잔여가 크다 — 개념 클러스터 245,889건 중 56.8%가 미분류(ETC), mention의 44.3%(511만 건)가 군집 노이즈로 남았습니다.
그리고 2,110만 건을 직접 조회하던 중, subcategory 361개 중 42개(11.6%, 약 30만 건)가 집계 파이프라인에서 통째로 빠져 있는 것을 발견했습니다. 명명 규칙을 flat → 계층형으로 바꾸면서 재집계가 누락된 것이 원인이었습니다.
접근 — 문장 전체를 의미 단위로
리뷰 길이 중앙값이 1문장이라는 실측을 근거로, 문단이 아니라 문장을 의미 단위로 택했습니다.
| 단계 | 내용 | 판단 근거 |
|---|---|---|
| 문장 분할 | kss | 정규식 분할기가 “1. 친절함 2. 맛있음” 같은 번호 리스트를 문장 경계로 오인식해 교체 |
| 임베딩 | Qwen3-Embedding-4B (768d) | mention 파이프라인과 같은 벡터 공간을 공유해야 교차검증이 가능 |
| 군집화 | UMAP 768→15d 후 HDBSCAN | 원본 차원에서는 파라미터와 무관하게 노이즈 94.5~96.9% — 원인은 파라미터가 아니라 차원의 저주. 축소 후 약 48% |
| 극성 분리 | 같은 토픽도 긍·부정이 다르면 분리 | 기존 mention 극성 라벨(99.9997% 채워짐)을 다수결로 재사용 |
| 라벨링 · 병합 | LLM 쌍 단위 판단 + Connected Components | 아래 참고 |
병합은 LLM에게 한 가지만 묻습니다. “이 두 클러스터가 같은 의미인가?” 전이적 묶음(A~B, B~C ⇒ A~B~C)은 LLM이 아니라 그래프 알고리즘이 처리합니다. 후보는 같은 극성 그룹 안에서만 centroid 코사인 0.85 이상으로 제한했습니다. cluster_id와 semantic_id를 별도 ID 공간으로 둬서, 대표 클러스터가 나중에 재라벨링돼도 깨지지 않게 했습니다.
추천 — semantic_id를 피처로
리뷰어×음식점 sparse 행렬의 피처 키를 (COALESCE(semantic_id, cluster_id), polarity)로 구성하고, 행 단위 L2 정규화 후 코사인 유사도로 세 갈래를 계산합니다.
- 유저 → 식당:
reviewer_vec · diner_matrix.T - 유사 식당:
diner_matrix · diner_vec.T - 유사 유저:
reviewer_matrix · reviewer_vec.T
COALESCE 폴백 덕분에 병합이 일부에만 적용된 지금도 전체가 동작하고, 병합 범위가 넓어질수록 벡터가 덜 희소해지는 방향으로만 개선됩니다. split_date 이전 리뷰의 문장만 벡터에 넣어 시점 리키지를 차단했습니다.
다음 단계 — 실측 분포로 나눈 Phase A / B / C
식당 벡터를 TIGER/GRID 계열 Residual K-Means로 (32, 61, 55) 같은 이산 토큰으로 바꾸는 모듈이 있었는데, 토큰만 만들고 추천으로 이어지지 않는 gap을 발견해 연결 구조를 설계했습니다.
- 리뷰어 554,884명 중 63.3%가 리뷰 1건(콜드스타트), 반면 n≥3 리뷰어는 22.6%지만 리뷰량의 73%를 차지
- 그래서 Phase A 학습 없는 Prefix 근사검색(전원 커버) → Phase B 방문 시퀀스 기반 생성형 추천(반복 방문자 전용, constrained beam search) → Phase C RRF 결합 + Bayesian 점수 보정으로 나눴습니다.
검증과 한계
- 기준 식당의 유사 식당 1·2위가 같은 브랜드의 다른 지점(score 0.950, 0.936)으로 나와 방향성을 확인했습니다.
- 11,561개 클러스터 dry-run에서 한 컴포넌트가 600개 이상을 흡수하는 체이닝 현상을 발견해, 전체 규모 DB 반영은 보류했습니다(50개 파일럿에서 결정론적·멱등 동작은 검증 완료).
- 정답 라벨이 없어 NDCG/Recall 같은 정량 지표는 아직 없습니다. 리뷰 시점 이전 리뷰어별 시간가중 평균을 baseline으로 만족/불만족 라벨을 만드는 오프라인 평가 프레임워크는 설계해 두었고, 미방문 식당은 관측되지 않는(MNAR) 한계도 함께 적어 두었습니다.
- 유사 유저 추천은 리뷰 1건 리뷰어가 63%인 구조적 희소성 때문에 신호가 약합니다(score 0.3~0.5).
이 프로젝트에서 보여주고 싶은 것
- 이전 방법론을 버리지 않고 다음 세대의 입력 신호로 재배치하는 설계 (mention 극성 → 문장 파이프라인)
- 실데이터에서 결함을 스스로 발견하고 원인까지 규명하는 습관
- 12명 모임을 운영하며 논문 리뷰 → 구현 → 발표(우아콘·모두콘)로 이어지는 사이클을 1년 6개월간 유지한 경험