PromleeBlog
sitemap
aboutMe

posting thumbnail
약속 장소 추천을 ML 문제로 재정의하는 방법 - XGBoost 추천시스템 1편
Redefining Meetup Recommendation as ML Problem - XGBoost Recommender System Part 1

📅

들어가기 전에 🔗

안녕하세요
어느 날 문득 서로 떨어져 있는 친구들과 약속 장소를 정하는 일이 생각보다 어렵다는 것을 깨달았습니다.
각자 출발하는 위치도 다르고, 대중교통을 타는지 차를 타는지에 따라 걸리는 시간도 제각각이기 때문입니다.
그래서 이런 불편함을 직접 해결해보고자 meet-in-the-middle 이라는 약속 장소 추천 서비스를 개발하게 되었습니다.


처음에는 단순히 지도에서 핀을 찍어 중간 지점을 찾으면 될 줄 알았습니다.
하지만 실제로는 참여자 모두의 이동 시간과 목적, 취향을 동시에 만족시켜야 하는 복잡한 문제였습니다.
이번 시리즈에는 이 서비스 개발 경험을 바탕으로, 약속 장소 추천을 머신러닝 문제로 재정의하는 방법을 공유해 보려고 합니다.
특히 실무에서 강력한 성능을 내는
XGBoost
알고리즘을 도입하기 전, 목표 변수와 평가 지표를 어떻게 세워야 하는지 차근차근 설명해 드리겠습니다.

참고 코드 및 데이터셋은 GitHub 저장소에서 확인하실 수 있습니다.
약속장소 쉽게 정하기
약속장소 쉽게 정하기

기존 규칙 기반 추천의 한계 🔗

서비스 초기에는 지도 API를 활용해 사용자들의 출발지 좌표를 모아 평균을 내고, 그 중심에서 가까운 장소를 보여주는 방식을 사용했습니다.
이 방식은 코드를 짜기 쉽고 결과도 아주 빠르게 나옵니다.
하지만 서비스를 운영하며 사용자들(지인)의 피드백을 받아보니 명확한 한계들이 보이기 시작했습니다.

이동 시간과 좌표 거리의 불일치 🔗

지도상의 직선거리는 가깝지만, 막상 대중교통을 타면 빙 돌아가야 하는 경우가 많습니다.
환승 횟수나 배차 간격에 따라 실제 도착 예상 시간은 거리와 완전히 다르게 나타납니다.
이런 교통 상황을 무시하고 거리만 평균을 내어 추천하면, 누군가는 10분 만에 도착하고 누군가는 1시간이 걸리는 불공평한 상황이 발생합니다.

사용자 의도 반영의 어려움 🔗

오랜만에 만나는 친구들과의 저녁 식사와, 스터디 모임을 위한 카페 탐색은 장소를 고르는 기준이 다릅니다.
스터디를 하는데 홍대를 추천하고, 친구들과 저녁 식사에 삼성역을 추천하면 만족하기 어렵겠죠..
규칙 기반 방식은 이렇게 상황마다 달라지는 사용자의 숨은 의도를 점수에 반영하기가 매우 까다롭습니다.
결국 화면에 장소가 추천되더라도 사용자의 마음에 들지 않아 선택받지 못하는 경우가 늘어났습니다.

추천 문제를 랭킹(Ranking) 문제로 재정의하기 🔗

이러한 문제를 근본적으로 해결하기 위해 우리는 머신러닝의 랭킹 모델을 도입해야 합니다.
👨‍💻
랭킹 모델은 컴퓨터 공학이나 알고리즘 시간에 배우는 정렬 과정과 원리가 비슷합니다.
다만 단순히 1부터 10까지 숫자의 크기를 비교하여 오름차순으로 나열하는 것이 아닙니다.
장소의 혼잡도, 참여자들의 평균 이동 시간, 카테고리 선호도 등 수많은 가중치를 종합적으로 계산하여 가장 최적의 순서를 결정하는 아주 똑똑한 정렬 알고리즘입니다.

사용자에게는 완벽한 정답 하나를 강요하는 것보다, 상위에 노출된 5개에서 10개의 장소가 얼마나 매력적인 순서로 잘 배치되어 있는지가 훨씬 중요합니다.

라벨(Label) 설계와 데이터셋 구성 🔗

머신러닝 모델이 좋은 장소를 똑똑하게 골라내려면, 무엇이 정답인지 알려주는 라벨이 필요합니다.
사용자가 meet-in-the-middle 웹사이트에 접속해서 최종 모임 장소를 확정하기까지의 흐름을 단계별로 나누어 라벨을 설계할 수 있습니다.

사용자 행동 퍼널에 맞춘 점수화 🔗

이러한 신호들을 모아 하나의 타깃 점수로 만드는 파이썬 코드를 작성해 보았습니다.
practice/
├── ml/
│   ├── label_builder.py
│   ├── requirements-ml.txt
│   ├── sample_actions.jsonl
│   └── sample_actions_with_labels.jsonl
|── scripts/
│   ├── setup_mac_linux.sh
│   ├── setup_windows.ps1
│   ├── build_labels.sh.sh
│   └── build_labels_windows.ps1
|── requirements.txt
├── README.md
우선, requirements-ml.txt 파일에는 머신러닝 모델 학습에 필요한 라이브러리들이 명시되어 있습니다.
ml/requirements-ml.txt
numpy>=2.1.0
pandas>=2.2.0
scikit-learn>=1.5.0
xgboost>=2.1.0
psycopg2-binary>=2.9.9
추가적으로, 실제 라벨을 계산하는 로직은 ml/label_builder.py 파일에 구현을 진행합니다.
ml/label_builder.py
 # 사용자 행동 데이터를 종합하여 머신러닝 학습용 타깃 점수를 계산합니다.
def build_target_score(user_action):
    # 화면에 아예 노출되지 않은 장소는 학습할 의미가 없으므로 제외합니다.
    if user_action["is_exposed"] == 0:
        return None
    
    # 행동의 깊이에 따라 가중치를 다르게 부여합니다. (확정에 가까울수록 높은 점수)
    click_score = 0.2 if user_action["is_clicked"] == 1 else 0.0
    vote_score = 0.3 if user_action["is_voted"] == 1 else 0.0
    final_score = 0.4 if user_action["is_final_selected"] == 1 else 0.0
    
    # 5점 만점의 만족도 별점을 0에서 0.1 사이의 추가 보너스 점수로 변환합니다.
    rating = user_action.get("rating", 0)
    feedback_score = (rating / 5.0) * 0.1
    
 # 모든 점수를 합산하여 최종 점수를 반환합니다.
    return click_score + vote_score + final_score + feedback_score
위 함수는 한 줄의 사용자 행동 로그에서 target_score를 만들어냅니다.
예를 들어 클릭과 투표가 모두 있었던 후보는 0.2 + 0.3으로 0.5점이 됩니다.
최종 확정과 별점 5점까지 받은 후보는 0.2 + 0.4 + 0.1로 0.7점이 됩니다.
👨‍💻
이렇게 만들어진 target_score는 0에서 1 사이의 연속적인 값으로, 랭킹 모델이 학습할 수 있는 훌륭한 라벨이 됩니다.
우선, 샘플 입력 데이터는 JSONL 형식으로 준비해 두었습니다.
JSONL은 한 줄에 JSON 하나가 들어있는 형식이라서 로그 데이터를 다룰 때 많이 씁니다.
/ml/sample_actions.jsonl
{"room_id":"room_001","place_id":"place_a","is_exposed":1,"is_clicked":1,"is_voted":1,"is_final_selected":0,"rating":0}
{"room_id":"room_001","place_id":"place_b","is_exposed":1,"is_clicked":1,"is_voted":0,"is_final_selected":1,"rating":5}
{"room_id":"room_001","place_id":"place_c","is_exposed":1,"is_clicked":0,"is_voted":0,"is_final_selected":0,"rating":0}
{"room_id":"room_002","place_id":"place_d","is_exposed":0,"is_clicked":0,"is_voted":0,"is_final_selected":0,"rating":0}

개발 환경별 실행 가이드 🔗

이러한 파이썬 스크립트나 머신러닝 모델을 로컬 환경에서 실행할 때는 가상환경을 분리하는 것이 가장 안전합니다.
Mac OS와 Linux 환경을 사용하신다면 터미널을 열고 아래와 같이 명령어를 입력하여 가상환경을 설정합니다.
/ml/scripts/scripts/setup_mac_linux.sh
#!/usr/bin/env bash
set -euo pipefail
 
python3 -m venv .venv-ml
source .venv-ml/bin/activate
 
python -m pip install --upgrade pip
pip install -r requirements-ml.txt
 
echo "Environment ready."
가상환경 설정이 완료되면, 라벨을 계산하는 스크립트를 실행하여 학습용 데이터셋을 만들어 보겠습니다.
아래 명령어를 입력하면 sample_actions_with_labels.jsonl 파일이 생성됩니다
/ml/scripts/build_labels.sh.sh
#!/usr/bin/env bash
set -euo pipefail
 
source .venv-ml/bin/activate
 
python ml/label_builder.py \
  --input ml/sample_actions.jsonl \
  --output ml/sample_actions_with_labels.jsonl
build_labels 실행 결과
build_labels 실행 결과
Windows 사용자분들은 Command Prompt 대신 주로 PowerShell을 사용하시게 됩니다.
보안 정책상 스크립트 실행 명령어가 Mac과 조금 다르니 프로젝트에 있는 스크립트 코드를 참고해 주시기 바랍니다.
/ml/scripts/setup_windows.ps1
/ml/scripts/build_labels_windows.ps1
실행이 완료되면, sample_actions_with_labels.jsonl 파일이 생성되어 각 장소에 대한 target_score가 포함된 것을 확인할 수 있습니다.
/ml/sample_actions_with_labels.jsonl
{"room_id": "room_001", "place_id": "place_a", "is_exposed": 1, "is_clicked": 1, "is_voted": 1, "is_final_selected": 0, "rating": 0, "target_score": 0.5}
{"room_id": "room_001", "place_id": "place_b", "is_exposed": 1, "is_clicked": 1, "is_voted": 0, "is_final_selected": 1, "rating": 5, "target_score": 0.7}
{"room_id": "room_001", "place_id": "place_c", "is_exposed": 1, "is_clicked": 0, "is_voted": 0, "is_final_selected": 0, "rating": 0, "target_score": 0.0}

결과 비교 🔗

sample_actions.jsonl(원본 데이터)과 sample_actions_with_labels.jsonl(라벨링된 결과)을 비교해 보겠습니다.

결론 🔗

직접 서비스를 만들어보며 느낀 점은, 추천 시스템을 도입할 때 무작정 좋은 모델을 쓰는 것보다 문제를 정확하게 정의하는 것이 훨씬 중요하다는 사실이었습니다.
단순한 좌표 계산이나 분류 문제가 아닌 랭킹 문제로 접근하고, 사용자의 실제 행동 데이터를 바탕으로 정교한 라벨을 만들어야 합니다.
이러한 기반 공사를 튼튼히 해두면, 추후 모델이 서비스에 투입되었을 때 훨씬 더 안정적이고 만족스러운 약속 장소를 추천해 줍니다.

다음 2편에서는 오늘 정의한 목표를 달성하기 위해, 실제 추천 데이터를 수집하고 데이터베이스 스키마를 설계했던 구축 과정을 자세히 알아보겠습니다.
감사합니다.

참고 🔗