성향기반 장소추천 MVP - 100-hours-a-week/12-marong-Wiki GitHub Wiki
📍 성향기반 장소추천
마니또(유저)와 마니띠의 MBTI 성향, 선호/비선호 음식, 장소 선호 정보를 고려하여 맞춤 장소(식당, 카페)를 추천해주는 서비스
📍 파일 구조
12-marong-AI-place/
├── main.py # 메인 스크립트 파일
├── main_tool.py # 메인 스크립트에 필요한 tool 함수 정의
├── requirements.txt
├── .env # 환경변수 (gitignore)
│
├── core/ # 추천 알고리즘 로직
│ ├── recommend_place.py # 추천 시스템 핵심 클래스
│ ├── calculate_score.py # 점수 계산 (거리, 평점, 유사도, 엔트로피)
│ ├── average_latlng.py # 평균 위치 계산
│ ├── get_week_index.py # 주차 계산 (주간 트렌드 반영 시 사용)
│ └── haversine.py # 위경도 거리 계산
│
├── models/ # MBTI 벡터 변환 및 키워드 추출
│ ├── mbti_projector.py # MBTI → 벡터 변환기
│ ├── extract_mbti_keywords.py # 키워드 임베딩 추출
│ └── best_mbti_projector.pt # 학습된 MBTI 변환 모델
│
├── db/ # DB 연결 및 ORM 정의
│ ├── db.py # SQLAlchemy 세션 유틸
│ └── db_models.py # 테이블 정의
│
├── scripts/ # 실행 전용 스크립트
│ ├── run_chroma.py # ChromaDB 실행 스크립트
│ └── sbert_down.py # SBERT 모델 다운로드 스크립트
│
└── README.md
📍 모델 선정 및 사용 이유
- 사용 모델:
snunlp/KR-SBERT-V40K-klueNLI-augSTS
- 이유: 타 모델과 비교해보았을 때, 손실이 가장 낮고 차원 수가 비교적 적은 위 모델을 설정
📍 주요 흐름
- 사용자의 MBTI, 선호/비선호 음식 정보, 좋아요 기반 장소, 메뉴 선호 정보, 거리를 MySQL DB, Chroma DB에서 조회하여 각 영역별로 점수를 정규화하여 상위 5위 점수 추천
[사용자 입력: MBTI, 선호음식, 위도/경도]
↓
MBTI → 벡터 변환 (mbti_projector.py)
↓
점수 계산 (recommend_place.py)
• 거리
• 평점
• MBTI 유사도
• 장소 선호 정보
• 엔트로피 가중치
↓
추천 결과 DB 저장
✔️ Data Crwaling 및 Chroma DB 저장
- 웹사이트에서 수집한 장소 리뷰 데이터와 메타 데이터를 Chroma DB에 저장
# 메인 함수
from chromadb.config import Settings
from typing import Optional
import torch.nn.functional as F
import pandas as pd
import numpy as np
import chromadb
import torch
import ast
import os
# 벡터 정규화 함수
def normalize_vector(vec):
return F.normalize(torch.tensor(vec, dtype=torch.float32), dim=0).tolist()
# 메인 함수
def insert_to_chroma(
df: Optional[pd.DataFrame] = None,
csv_path: Optional[str] = None,
chroma_path: str = "./chroma_db",
batch_size: int = 500,
chroma_client: Optional[chromadb.Client] = None,
review_collection=None,
menu_collection=None,
history_collection=None
):
# CSV 파일 또는 DataFrame 중 하나 사용
if df is None:
if csv_path is None:
raise ValueError("CSV 경로 또는 DataFrame 둘 중 하나는 반드시 제공되어야 합니다.")
df = pd.read_csv(csv_path)
# Chroma client 생성
if chroma_client is None:
chroma_client = chromadb.PersistentClient(
path=chroma_path,
settings=Settings(anonymized_telemetry=False)
)
# 컬렉션 연결 또는 생성
if review_collection is None:
review_collection = chroma_client.get_or_create_collection(name="review_collection")
if menu_collection is None:
menu_collection = chroma_client.get_or_create_collection(name="menu_collection")
if history_collection is None:
history_collection = chroma_client.get_or_create_collection(name="recommendation_history")
print("Chroma 컬렉션 연결 완료")
# df['리뷰_평균임베딩'] = df['리뷰_평균임베딩'].apply(parse_embedding)
# df['메뉴_임베딩'] = df['메뉴_임베딩'].apply(parse_embedding)
# 유효 데이터만 필터링
valid_documents, valid_review_embs, valid_menu_embs, valid_metadatas, valid_ids = [], [], [], [], []
for idx, row in df.iterrows():
review_emb = row['리뷰_평균임베딩']
menu_emb = row['메뉴_임베딩']
if review_emb is not None and menu_emb is not None:
valid_documents.append(row['상호명'])
valid_review_embs.append(normalize_vector(review_emb))
valid_menu_embs.append(normalize_vector(menu_emb))
metadata = {
"상호명": row.get('상호명', ''),
"대표카테고리": row.get('대표_카테고리', ''),
"메인_메뉴": row.get('메인_메뉴_형태', ''),
"주소": row.get('주소', ''),
"평균별점": row.get('평균별점', ''),
"영업시간": row.get('영업시간_요약', ''),
"위도": row.get('위도', ''),
"경도": row.get('경도', ''),
"링크": row.get('url', '')
}
valid_metadatas.append(metadata)
valid_ids.append(f"store_{idx}")
# 배치 업로드 함수
def add_to_chroma_in_batches(collection, documents, embeddings, metadatas, ids):
for i in range(0, len(documents), batch_size):
collection.add(
documents=documents[i:i+batch_size],
embeddings=embeddings[i:i+batch_size],
metadatas=metadatas[i:i+batch_size],
ids=ids[i:i+batch_size]
)
print(f" Batch {i // batch_size + 1} added to '{collection.name}'")
# 실제 업로드 실행
add_to_chroma_in_batches(review_collection, valid_documents, valid_review_embs, valid_metadatas, valid_ids)
add_to_chroma_in_batches(menu_collection, valid_documents, valid_menu_embs, valid_metadatas, valid_ids)
# 완료 출력
print("업로드 완료")
print("리뷰 컬렉션 문서 수:", review_collection.count())
print("메뉴 컬렉션 문서 수:", menu_collection.count())
print("히스토리 컬렉션 문서 수:", history_collection.count())
✔️ 장소 추천 객체 생성
RecommendPlace
클래스로 마니또와 마니띠의 맞춤 장소(식당/카페) 추천을 위한 객체 생성- 최종 정보를
.recommend
Method로 전달하면 모듈이 장소 추천 시작
food_recommender = RecommendPlace(
model=mbti_model,
embedding_model=embedding_model,
mbti_vector=mbti_avg_vector,
vibe_vector=vibe_pavg_vector,
menu_vector=menu_pavg_vector,
chroma_client=chroma_client,
review_col_name="review_collection",
menu_col_name="menu_collection",
device=device,
allow_cafe=False,
embedding_func=None
)
food_results = food_recommender.recommend(avg_lat, avg_lng, 10, 5, like_foods, dislike_foods)
✔️ 장소 추천 가중치 설명
- 사용자 MBTI: 사전에 장소 평점 기반으로 학습한 딥러닝 기반 MBTI 별 선호 분위기 Projector을 통해 선호 분위기일수록 높은 점수 부여
- 사용자 선호/비선호 음식: 사용자가 선호하는 음식과 식당 메뉴의 유사도가 크면 높은 점수, 싫어하는 음식과 식당 메뉴의 유사도가 크면 낮은 점수를 부여
- 사용자 장소 선호 정보: 사용자의 장소, 메뉴에 대한 선호 임베딩 벡터와 각각 분위기 벡터, 메뉴 벡터와의 유사도가 클수록 높은 점수 부여
- 메타데이터: 장소의 평균 별점, 장소까지의 거리 기반으로 별점이 클수록 높은 점수, 거리가 멀수록 낮은 점수 부여
📍 결과 예시(MySQL DB, Chroma DB에 저장)
"food_data": [
{
"name": "비눔",
"address": "경기 성남시 분당구 대왕판교로 660 지하1층 B106",
"rating": 5.0,
"distance": 0.76,
"link": "https://place.map.kakao.com/224825790",
"score": 0.68,
"category": "양식",
"operation_hour": ["월~금: 11:00~24:00", "토: 18:00~24:00"]
}