성향기반 장소추천 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"]
    }