파이썬을 사용해 LLM으로 추천 시스템 정확도 향상
Source: Towards Data Science
미국 문화에서 흔히 쓰이는 말은 다음과 같습니다:
“You can’t have your cake and eat it too.”
“케이크를 가지고 있으면서 동시에 먹을 수는 없다.”
이 문장은 매우 시적이면서도 실용적이고 유용하다고 생각합니다. 이 말의 메시지는 간단합니다: 우리가 이루는 모든 일은 트레이드오프를 통해 달성되며, 모든 것에는 대가가 있다는 것이죠.
철학적인 논의는 이 글의 범위를 벗어나지만, 이러한 고려사항의 실질적인 결과는 데이터 과학과 소프트웨어 엔지니어링 전반과 매우 일치합니다. 설명해 보겠습니다.
소프트웨어 엔지니어링과 데이터 과학에서는 “완벽한 설계”라는 것이 그 자체로 존재하지 않습니다. 특정 애플리케이션에 환상적인 알고리즘도 다른 상황에서는 형편없이 실패할 수 있습니다.
다음 사례들을 통해 계산량 vs 메모리 트레이드오프를 생각해 보세요.
-
두 도시 사이의 거리를 미리 계산해 데이터셋에 저장하는 것은 매우 합리적이며, 비행 중에 매번 거리를 계산하는 것은 의미가 없습니다. 이는 도시가 자주 이동하지 않으므로 데이터셋을 낮은 유지보수 비용으로 유지할 수 있기 때문이며, 뉴욕과 샌프란시스코 사이의 거리를 매 순간 계산하는 것은 어리석은 일입니다. [Case A]
-
반면 챗봇이 인간이 할 수 있는 모든 질문을 외워두고 질문이 들어올 때마다 답을 꺼내는 것은(아마도) 불가능에 가깝습니다. 문제의 성격이 훨씬 더 동적이며 “실시간” 계산이 필요하기 때문입니다. [Case B]
Case A에서는 메모리를 희생하고 매우 빠른 계산을 얻습니다. Case B에서는 더 많은 계산 시간을 쓰지만 “쿼리” 메모리를 전혀 사용하지 않습니다.
계산 시간도 없고 메모리도 없을 수 있을까요?
실제로는 불가능합니다. 케이크를 가지고 있으면서 동시에 먹을 수는 없으니까요 😊
그럼 좀 더 덜 직관적이고 “트렌디”한 예시를 들어보겠습니다. **대형 언어 모델(LLM)**에 대해 이야기해 보죠.
LLM은 현재 우리가 보유한 가장 강력한 AI 모델이며, 전 세계에 존재하는 모든 지식으로 학습되었습니다. 또한 거대합니다. 실제로 사내에 두고 운영하기는 드물고, 보통 API를 통해 호출합니다. 그런데 API 호출 = 토큰 = 비용이라는 점을 기억하세요.
예를 들어, 오늘 저녁에 갈 최고의 레스토랑을 찾고 싶다고 가정해 봅시다. ChatGPT에게 다음과 같이 물을 수 있겠죠:
“너무 비싸지 않으면서 로맨틱하고, 위치도 좋은 이탈리안 레스토랑을 추천해 줄래?”
만약 GPT 모델이 전 우주의 모든 레스토랑을 탐색해 이탈리안인지, 비싼지, 위치가 좋은지, 그리고 내 위치와 가까운지를 판단해야 한다면 어떨까요? 최악의 경우 수백만 토큰이 소모되고, 계산이 끝날 때쯤 이미 잠자리에 들어 있을 겁니다.
하지만 우리는 LLM이 가진 풍부한 자연어 해석·정보 검색 능력을 완전히 포기하고 싶지는 않습니다. 핵심은 LLM을 언제, 어떻게 사용할지 전략을 세우는 것입니다. 가장 똑똑한 파이프라인 부분을 항상 사용하는 것은 “케이크를 가지고 있으면서 동시에 먹는” 상황과 동일합니다.
이 글에서는 레스토랑 추천이라는 사용 사례를 바탕으로, LLM을 활용한 스마트한 추천 시스템을 만드는 레시피를 소개하겠습니다.
시스템의 입력은 사용자가 특정 도시에서 원하는 이상적인 레스토랑에 대한 설명이며, 출력은 추천 레스토랑 리스트가 됩니다.
시작해 볼까요!
1. 시스템 설계
앞서 언급한 케이크 속담은 엔지니어링에서는 정확도‑규모‑시간 삼각형이라고도 불립니다:
- 정확하면서도 방대한 데이터셋을 사용할 수 있지만, 속도는 느리다.
- 정확하면서도 빠를 수 있지만, 대규모 데이터셋에선 확장성이 떨어진다.
- 빠르고 확장성은 좋지만, 정확도가 낮다.
Image made by author
우리는 최종적으로 정확한 결과를 원하므로, 옵션 3만으로는 부족합니다. 대신 옵션 3에 더 정확한 모델을 겹쳐서 보완할 수 있습니다. 다시 말해, 옵션 3은 작은 계산 시간에 후보 리스트를 빠르게 생성하고, 그 리스트 위에 대형 언어 모델을 사용해 가장 정확한 추천을 선택하는 방식입니다.
즉, 설계는 다음과 같습니다:
- 빠르고 간단한 검색을 통해 상위 K개의 가장 가까운 레스토랑을 찾는다. (규칙 기반, 높은 Recall, 낮은 Precision)
- 느리고 매우 지능적인 LLM이 상위 K 후보 중 사용자의 질의에 가장 부합하는 레스토랑을 선택한다. (AI 기반, 높은 Precision)
이렇게 하면 느린 LLM에 대한 비용과 시간을 절감하면서도, 후보 리스트에만 적용함으로써 LLM의 스마트함을 충분히 활용할 수 있습니다.
그럼 이제 코딩을 시작해 보겠습니다!
2. 스크립트
2.1 설정
뒤에서 깔끔히 작업을 해두었습니다 🙂
전체 과정은 객체지향 프로그래밍(OOP) 방식으로 구성된 스크립트와 파이프라인으로 구현됩니다. GitHub 폴더는 이곳이며, 코드를 그대로 실행하려면 해당 레포를 클론하고 아래 import 블록을 사용하면 됩니다:
# 예시 import 블록
import os, sys, json, pandas as pd
# ...
2.2 데이터 생성
추천을 하려면 먼저 추천할 데이터가 필요합니다. 실제 시스템이라면 S3에 저장된 레스토랑 데이터베이스를 사용할 텐데, 여기서는 재현 가능하고 무료로 실행할 수 있는 합성 데이터를 생성합니다.
datagenerator.py 안의 RestaurantDataGenerator 클래스가 담당합니다. 이 클래스는 약 10,000개의 레스토랑을 8개 도시(뉴욕, 샌프란시스코, 시카고, 오스틴, 시애틀, 보스턴, 마이애미, 덴버) 전역에 흩뿌립니다. 각 레스토랑은 다음 정보를 가집니다:
- 무작위로 조합된 이름
- 해당 도시와 위도/경도(도시 중심에서 약 13km 이내)
- 음식 종류(이탈리안, 일본식, 멕시칸, 타이, 프렌치 등)
- 식단 프로필(잡식/채식/비건)
- 평균 평점
- 투표 수
- 가격대(10 / 100 / 1000, 1인당 평균 티켓 규모)
이 제너레이터는 한 번만 실행하면 됩니다. 데이터 생성은 다음 한 줄 호출로 끝납니다:
RestaurantDataGenerator().generate()
위 호출은 data/restaurants.csv 파일에 테이블을 저장합니다. 파일 예시는 다음과 같습니다:
name,city,latitude,longitude,cuisine,diet,score,votes,price_range
...
이제 레스토랑 데이터를 확보했으니, 추천 로직을 살펴보겠습니다.
2.3 후보 생성 (Stage 1)
이 단계는 저비용·고속·규칙 기반 후보 리스트를 만드는 단계입니다. 사용자가 어느 도시에 있는지 알려주면, 해당 도시와 가장 가까운 레스토랑만 남깁니다. 코드 흐름은:
- 테이블을 사용자가 지정한 도시로 필터링
- 사용자 위치와 각 레스토랑 간의 구면거리(great‑circle distance) 계산
N_DISTANCE_CANDIDATES(기본값 50) 만큼 가장 가까운 레스토랑을 선택
이 단계는 높은 Recall, 낮은 Precision을 목표로 합니다. 전체 10k 레스토랑을 한 번에 훑어도 API 호출이나 토큰 비용이 전혀 발생하지 않으니, 비용 효율적입니다. 물론 여기서는 특별히 똑똑하거나 화려한 로직을 쓰지는 않지만, 사용자가 실제로 고려할 수 없는 후보들을 미리 걸러내는 큰 효과가 있습니다.
예시 질의:
“저렴한 비건 타코와 활기찬 분위기의 식당” (여러 도시에서)
출력 예시:
city, name