이미지 로딩 중...
AI Generated
2025. 11. 17. · 7 Views
추천 시스템 설계 완벽 가이드
유튜브, 넷플릭스처럼 사용자에게 딱 맞는 콘텐츠를 추천하는 시스템을 어떻게 만들까요? 추천 시스템의 핵심 알고리즘부터 실제 구현까지, 초급 개발자도 쉽게 따라할 수 있도록 친절하게 안내합니다.
목차
- 협업_필터링_기본_개념
- 아이템_기반_협업_필터링
- 콘텐츠_기반_필터링
- 하이브리드_추천_시스템
- 행렬_분해_Matrix_Factorization
- 딥러닝_기반_추천_신경망_협업_필터링
- 컨텍스트_인식_추천_시스템
- 평가_지표와_A_B_테스트
1. 협업_필터링_기본_개념
시작하며
여러분이 넷플릭스에서 영화를 보려고 할 때, "당신을 위한 추천"이라는 섹션을 본 적 있나요? 신기하게도 여러분이 좋아할 만한 영화들이 정확하게 나타납니다.
이런 마법 같은 추천은 어떻게 가능할까요? 비밀은 바로 "비슷한 취향을 가진 다른 사람들"에게 있습니다.
만약 여러분과 똑같은 영화 10개를 좋아하는 사람이 있다면, 그 사람이 좋아한 11번째 영화도 여러분이 좋아할 가능성이 높겠죠? 바로 이것이 협업 필터링의 핵심 아이디어입니다.
마치 친구에게 "이거 나랑 취향 비슷한데, 이 영화 진짜 재밌더라!"라고 추천받는 것과 같은 원리입니다.
개요
간단히 말해서, 협업 필터링은 "비슷한 사람들은 비슷한 것을 좋아한다"는 아이디어를 기반으로 합니다. 실제 서비스에서 이게 왜 중요할까요?
쇼핑몰을 운영한다고 생각해보세요. 1만 개의 상품 중에서 고객이 관심 있을 만한 상품을 찾아주지 못하면, 고객은 그냥 떠나버립니다.
협업 필터링을 사용하면 수백만 명의 고객 데이터를 분석해서 각 고객에게 딱 맞는 상품을 추천할 수 있습니다. 전통적인 방법에서는 개발자가 직접 "운동화를 산 사람에게는 운동복을 추천하자"는 식의 규칙을 수백 개 만들어야 했습니다.
하지만 협업 필터링을 사용하면 시스템이 자동으로 고객들의 패턴을 학습합니다. 협업 필터링의 핵심 특징은 세 가지입니다.
첫째, 아이템 자체의 특성을 몰라도 됩니다(영화 장르나 배우를 몰라도 추천 가능). 둘째, 사용자가 많을수록 정확도가 높아집니다.
셋째, 새로운 패턴을 자동으로 발견합니다. 이러한 특징들이 중요한 이유는 규모가 큰 서비스일수록 수동으로 관리하기 불가능하기 때문입니다.
코드 예제
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
# 사용자-아이템 평점 행렬 (5명의 사용자, 4개의 영화)
# 0은 아직 평가하지 않은 영화
ratings = np.array([
[5, 3, 0, 1], # 사용자 0의 평점
[4, 0, 0, 1], # 사용자 1의 평점
[1, 1, 0, 5], # 사용자 2의 평점
[1, 0, 0, 4], # 사용자 3의 평점
[0, 1, 5, 4], # 사용자 4의 평점
])
# 사용자 간 유사도 계산 (코사인 유사도 사용)
user_similarity = cosine_similarity(ratings)
# 특정 사용자(0번)에게 추천할 영화 예측
def predict_rating(user_id, item_id):
# 해당 아이템을 평가한 다른 사용자들의 유사도와 평점을 가중 평균
sim_scores = user_similarity[user_id]
item_ratings = ratings[:, item_id]
# 평점을 준 사용자들만 고려 (0이 아닌 평점)
rated_mask = item_ratings > 0
if not rated_mask.any():
return 0
weighted_sum = np.sum(sim_scores[rated_mask] * item_ratings[rated_mask])
similarity_sum = np.sum(np.abs(sim_scores[rated_mask]))
return weighted_sum / similarity_sum if similarity_sum > 0 else 0
# 사용자 0이 아직 보지 않은 영화 2에 대한 예측 평점
predicted = predict_rating(0, 2)
print(f"예측 평점: {predicted:.2f}")
설명
이것이 하는 일: 위 코드는 사용자들의 과거 평점 데이터를 바탕으로 아직 평가하지 않은 아이템에 대한 예측 평점을 계산합니다. 첫 번째 단계로, ratings 행렬을 만듭니다.
이것은 마치 엑셀 표와 같습니다. 각 행은 한 명의 사용자를, 각 열은 하나의 영화를 나타냅니다.
예를 들어 사용자 0은 영화 0에 5점을 줬지만, 영화 2는 아직 보지 않았습니다(0으로 표시). 이렇게 행렬로 만드는 이유는 수학적 계산을 빠르게 하기 위해서입니다.
두 번째 단계에서는 cosine_similarity를 사용해 사용자들 간의 유사도를 계산합니다. 코사인 유사도는 두 벡터(여기서는 사용자의 평점 패턴)가 얼마나 비슷한 방향을 가리키는지 측정합니다.
값이 1에 가까울수록 취향이 매우 비슷하고, 0에 가까우면 전혀 다른 취향입니다. 예를 들어 사용자 0과 사용자 1은 둘 다 영화 0을 높게 평가하고 영화 3을 낮게 평가했으므로 유사도가 높게 나옵니다.
세 번째 단계인 predict_rating 함수에서는 실제 예측이 일어납니다. 사용자 0이 영화 2를 몇 점이나 줄지 예측하려면, 영화 2를 이미 본 다른 사용자들의 평점을 참고합니다.
단, 모든 사용자의 의견을 똑같이 반영하면 안 됩니다. 사용자 0과 취향이 비슷한 사람의 의견은 많이 반영하고, 취향이 다른 사람의 의견은 적게 반영해야겠죠?
그래서 유사도를 가중치로 사용해 가중 평균을 계산합니다. 여러분이 이 코드를 사용하면 수천, 수만 명의 사용자 데이터에서도 동일한 방식으로 추천을 생성할 수 있습니다.
실무에서는 이 기본 알고리즘을 확장해서 사용합니다. 예를 들어 넷플릭스는 1억 명 이상의 사용자 데이터를 처리하지만, 핵심 원리는 동일합니다.
또한 실시간으로 사용자의 새로운 평점이 추가될 때마다 추천 결과를 업데이트할 수 있어, 사용자 경험이 계속 개선됩니다.
실전 팁
💡 희소성 문제(Sparsity)를 조심하세요. 대부분의 사용자는 전체 아이템 중 극히 일부만 평가합니다. 예를 들어 넷플릭스에 1만 개의 영화가 있는데 사용자가 10개만 봤다면 99.9%가 빈 값입니다. 이럴 때는 행렬 분해(Matrix Factorization) 기법을 함께 사용하면 좋습니다.
💡 콜드 스타트 문제에 대비하세요. 새로운 사용자나 새로운 아이템은 평점 데이터가 없어 추천이 불가능합니다. 이럴 때는 인기 아이템을 먼저 추천하거나, 사용자에게 몇 가지 선호도를 물어보는 온보딩 과정을 추가하세요.
💡 유사도 계산 시 피어슨 상관계수도 고려해보세요. 어떤 사용자는 모든 영화에 후하게 점수를 주고, 어떤 사용자는 짜게 줍니다. 피어슨 상관계수는 이런 개인별 평점 기준 차이를 보정해줍니다.
💡 성능 최적화를 위해 모든 사용자 쌍의 유사도를 미리 계산하지 마세요. 사용자가 100만 명이라면 유사도 행렬은 1조 개의 값을 가지게 됩니다. 대신 KNN(K-Nearest Neighbors) 알고리즘을 사용해서 가장 유사한 상위 K명의 사용자만 찾아 사용하세요.
💡 주기적으로 모델을 재학습하세요. 사용자의 취향은 시간이 지나면서 변합니다. 작년에 로맨스 영화를 좋아했던 사람이 올해는 액션 영화를 좋아할 수 있습니다. 실무에서는 보통 하루나 일주일 단위로 모델을 업데이트합니다.
2. 아이템_기반_협업_필터링
시작하며
여러분이 아마존에서 책을 하나 샀을 때, "이 상품을 구매한 고객이 함께 구매한 상품"이라는 섹션을 본 적 있나요? 이건 사용자 기반 협업 필터링과는 조금 다른 접근입니다.
사용자는 매일 바뀌고 늘어나지만, 아이템(상품, 영화 등)은 상대적으로 안정적입니다. 새 영화가 나오는 속도보다 새 사용자가 가입하는 속도가 훨씬 빠르죠.
그렇다면 "비슷한 사용자"를 찾는 대신 "비슷한 아이템"을 찾으면 어떨까요? 바로 이것이 아이템 기반 협업 필터링입니다.
"어벤져스를 좋아하는 사람들이 아이언맨도 좋아한다"는 패턴을 찾아내는 거죠. 한 번 계산해두면 오래 사용할 수 있어서 실무에서 매우 인기가 높습니다.
개요
간단히 말해서, 아이템 기반 협업 필터링은 "이 아이템을 좋아한 사람들이 함께 좋아한 다른 아이템"을 찾는 방식입니다. 왜 이게 필요할까요?
실제 서비스에서는 사용자가 수백만 명이지만 아이템은 수만 개 정도인 경우가 많습니다. 유튜브에는 수십억 명의 사용자가 있지만, 추천해야 할 비디오는 상대적으로 적습니다(물론 절대적으로는 많지만).
아이템 간 유사도는 자주 변하지 않으므로 미리 계산해두고 재사용할 수 있어 훨씬 효율적입니다. 기존 사용자 기반 방식에서는 새 사용자가 가입할 때마다 유사도를 다시 계산해야 했습니다.
하지만 아이템 기반 방식에서는 아이템 간 유사도를 하루에 한 번만 계산하고, 실시간 추천 요청에는 이미 계산된 값을 빠르게 조회만 하면 됩니다. 아이템 기반 협업 필터링의 핵심 특징은 세 가지입니다.
첫째, 확장성이 뛰어납니다(사용자가 아무리 많아도 문제없음). 둘째, 추천 이유를 설명하기 쉽습니다("이 영화와 비슷해서 추천했어요").
셋째, 안정적입니다(아이템 유사도가 갑자기 크게 변하지 않음). 이러한 특징들 덕분에 아마존, 넷플릭스 같은 대규모 서비스에서 실제로 사용됩니다.
코드 예제
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
# 사용자-아이템 평점 행렬 (전치해서 아이템 관점으로 봄)
ratings = np.array([
[5, 3, 0, 1],
[4, 0, 0, 1],
[1, 1, 0, 5],
[1, 0, 0, 4],
[0, 1, 5, 4],
])
# 행렬을 전치하여 아이템-사용자 관점으로 변환
# 이제 각 행이 아이템, 각 열이 사용자
item_ratings = ratings.T
# 아이템 간 유사도 계산
item_similarity = cosine_similarity(item_ratings)
print("아이템 유사도 행렬:")
print(item_similarity)
# 특정 사용자가 좋아한 아이템을 기반으로 추천
def recommend_items(user_id, top_n=2):
# 사용자가 평가한 아이템들
user_ratings = ratings[user_id]
# 각 아이템에 대한 예측 점수 계산
scores = np.zeros(len(user_ratings))
for item_id in range(len(user_ratings)):
if user_ratings[item_id] == 0: # 아직 평가하지 않은 아이템
# 사용자가 평가한 아이템들과의 유사도를 가중치로 사용
rated_items = user_ratings > 0
similarities = item_similarity[item_id][rated_items]
ratings_values = user_ratings[rated_items]
# 가중 평균으로 점수 계산
if np.sum(np.abs(similarities)) > 0:
scores[item_id] = np.sum(similarities * ratings_values) / np.sum(np.abs(similarities))
# 점수가 높은 상위 N개 아이템 추천
recommended_items = np.argsort(scores)[::-1][:top_n]
return [(item_id, scores[item_id]) for item_id in recommended_items if scores[item_id] > 0]
# 사용자 0에게 추천
recommendations = recommend_items(0, top_n=2)
print(f"\n사용자 0에게 추천: {recommendations}")
설명
이것이 하는 일: 위 코드는 아이템들 간의 유사도를 계산하고, 사용자가 좋아한 아이템과 비슷한 다른 아이템을 추천합니다. 첫 번째로, ratings.T를 사용해 행렬을 전치합니다.
원래는 각 행이 사용자였는데, 이제는 각 행이 아이템이 됩니다. 왜 이렇게 할까요?
우리는 "이 아이템을 좋아한 사람들의 패턴"을 보고 싶기 때문입니다. 예를 들어 영화 0번 행을 보면 [5, 4, 1, 1, 0]인데, 이는 "5명의 사용자가 이 영화에 준 평점들"을 의미합니다.
그 다음으로, 아이템 간 유사도를 계산합니다. 영화 0과 영화 1의 유사도가 높다는 것은 "두 영화에 비슷한 평점 패턴을 준 사용자들이 많다"는 뜻입니다.
예를 들어 둘 다 높은 점수를 받았거나, 둘 다 낮은 점수를 받았다면 유사도가 높습니다. 이 유사도는 한 번만 계산하면 되므로, 실제 서비스에서는 밤에 배치 작업으로 계산해서 데이터베이스에 저장해둡니다.
recommend_items 함수는 실제 추천 로직입니다. 사용자 0이 영화 0에 5점을 줬다고 가정해봅시다.
그러면 영화 0과 유사한 영화들(예: 영화 1)을 찾아서 추천합니다. 단, 여러 영화를 고려해야 하므로 가중 평균을 사용합니다.
사용자가 5점을 준 영화와 매우 유사한 영화는 높은 점수를, 3점을 준 영화와 유사한 영화는 중간 점수를 받게 됩니다. 여러분이 이 코드를 실제 서비스에 적용하면 여러 이점이 있습니다.
첫째, 응답 속도가 매우 빠릅니다. 아이템 유사도는 이미 계산되어 있으므로, 추천 요청이 오면 단순히 조회만 하면 됩니다.
둘째, 사용자에게 "왜 이걸 추천했나요?"라고 물어보면 "당신이 좋아한 A 상품과 비슷해서요"라고 명확하게 설명할 수 있습니다. 셋째, 새로운 사용자가 100만 명 추가되어도 시스템이 느려지지 않습니다.
넷째, A/B 테스트가 쉽습니다. 유사도 계산 방식을 조금씩 바꿔가며 어떤 방식이 실제 구매나 시청으로 이어지는지 측정할 수 있습니다.
실전 팁
💡 아이템 유사도를 저장할 때 모든 쌍을 저장하지 마세요. 아이템이 1만 개라면 1억 개의 유사도 값이 생깁니다. 대신 각 아이템마다 가장 유사한 상위 50~100개만 저장하세요. 대부분의 경우 이것만으로도 충분하고, 저장 공간과 조회 속도가 크게 개선됩니다.
💡 시간 가중치를 적용하세요. 1년 전 평점보다 최근 평점이 더 중요합니다. 유사도 계산 시 최근 평점에 더 높은 가중치를 주면 트렌드를 반영할 수 있습니다. 예를 들어 지난 달 평점은 1.0, 3개월 전 평점은 0.7의 가중치를 적용하는 식입니다.
💡 인기도 페널티를 고려하세요. 모든 사람이 좋아하는 아이템(예: 어벤져스)은 모든 아이템과 유사도가 높게 나옵니다. 이렇게 되면 추천이 다양하지 못하고 항상 같은 인기 아이템만 추천됩니다. 인기도에 반비례하는 가중치를 적용하면 더 다양하고 개인화된 추천이 가능합니다.
💡 정규화를 적용하세요. 어떤 아이템은 평점 100개, 어떤 아이템은 평점 10,000개를 가질 수 있습니다. 평점이 적은 아이템은 우연히 유사도가 높게 나올 수 있으므로, 최소 평점 개수(예: 50개) 이상인 아이템만 유사도 계산에 포함시키세요.
💡 여러 유사도 지표를 조합하세요. 코사인 유사도 외에도 자카드 유사도, 피어슨 상관계수 등 다양한 지표가 있습니다. 실무에서는 이들을 앙상블(조합)해서 사용하면 더 강건한 추천 시스템을 만들 수 있습니다.
3. 콘텐츠_기반_필터링
시작하며
여러분이 스포티파이에서 좋아하는 노래를 듣고 있다고 상상해보세요. 그런데 이 노래는 너무 새로워서 아직 아무도 듣지 않았습니다.
협업 필터링은 "다른 사람들의 평가"가 필요한데, 평가가 하나도 없으면 어떡하죠? 바로 이럴 때 필요한 것이 콘텐츠 기반 필터링입니다.
다른 사람의 의견 대신 "노래 자체의 특성"을 봅니다. 이 노래가 록 장르이고, 템포가 빠르고, 기타 소리가 많다면, 여러분이 과거에 좋아했던 비슷한 특성의 노래를 찾아서 추천하는 거죠.
콘텐츠 기반 필터링은 마치 여러분의 개인 취향 프로필을 만드는 것과 같습니다. "이 사람은 액션 영화, 특히 마블 시리즈를 좋아하네"라는 프로필을 바탕으로 새로운 마블 영화를 추천하는 방식입니다.
개요
간단히 말해서, 콘텐츠 기반 필터링은 아이템의 특성(장르, 키워드, 속성 등)을 분석해서 사용자가 과거에 좋아한 아이템과 비슷한 것을 추천합니다. 실무에서 이게 왜 중요할까요?
뉴스 추천 서비스를 만든다고 생각해보세요. 새로운 뉴스 기사는 매 시간 수십 개씩 올라오는데, 아직 아무도 읽지 않았습니다.
협업 필터링으로는 추천이 불가능하지만, 기사의 제목, 본문, 키워드를 분석하면 즉시 추천할 수 있습니다. "이 사용자는 IT와 AI 관련 기사를 많이 읽었으니, 이 새로운 ChatGPT 기사를 추천하자"는 식으로요.
전통적인 협업 필터링에서는 콜드 스타트(새 아이템) 문제가 심각했습니다. 하지만 콘텐츠 기반 필터링은 아이템이 만들어진 즉시 추천이 가능합니다.
심지어 사용자가 단 한 명뿐인 서비스 초기에도 작동합니다. 콘텐츠 기반 필터링의 핵심 특징은 세 가지입니다.
첫째, 새로운 아이템도 즉시 추천 가능합니다. 둘째, 사용자 독립적입니다(다른 사용자 데이터가 필요 없음).
셋째, 투명성이 높습니다(추천 이유를 명확히 설명 가능). 이러한 특징들이 중요한 이유는 빠르게 변하는 콘텐츠(뉴스, 동영상, 블로그 등)를 다루는 서비스에서는 필수적이기 때문입니다.
코드 예제
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
# 영화 설명 데이터 (실제로는 DB에서 가져옴)
movies = [
"액션 히어로 마블 슈퍼맨 전투 특수효과",
"로맨스 사랑 감동 드라마 연애",
"액션 스파이 첩보 전투 미션",
"코미디 웃음 유머 가족 일상",
"공포 좀비 호러 서바이벌 스릴러"
]
# 영화 제목
movie_titles = ["어벤져스", "노트북", "007", "극한직업", "부산행"]
# TF-IDF 벡터화 (텍스트를 숫자 벡터로 변환)
tfidf = TfidfVectorizer()
tfidf_matrix = tfidf.fit_transform(movies)
# 영화 간 유사도 계산
movie_similarity = cosine_similarity(tfidf_matrix)
# 사용자가 좋아한 영화를 기반으로 추천
def recommend_by_content(liked_movie_idx, top_n=3):
# 좋아한 영화와 다른 영화들의 유사도
similarity_scores = list(enumerate(movie_similarity[liked_movie_idx]))
# 유사도 기준으로 정렬 (자기 자신 제외)
similarity_scores = sorted(similarity_scores, key=lambda x: x[1], reverse=True)[1:]
# 상위 N개 추천
top_movies = similarity_scores[:top_n]
print(f"\n'{movie_titles[liked_movie_idx]}'를 좋아하는 사용자에게 추천:")
for idx, score in top_movies:
print(f" - {movie_titles[idx]} (유사도: {score:.3f})")
return top_movies
# 사용자가 "어벤져스"를 좋아한다면
recommend_by_content(0, top_n=3)
설명
이것이 하는 일: 위 코드는 영화의 설명 텍스트를 분석하여 내용이 비슷한 영화를 찾아 추천합니다. 첫 번째로, movies 리스트에 각 영화를 설명하는 키워드들을 저장합니다.
실제 서비스에서는 영화 줄거리, 장르, 감독, 배우 등 훨씬 많은 정보를 사용하지만, 원리는 같습니다. 예를 들어 "어벤져스"는 "액션 히어로 마블"로 설명되고, "노트북"은 "로맨스 사랑 감동"으로 설명됩니다.
두 번째로, TfidfVectorizer를 사용해 텍스트를 숫자로 변환합니다. TF-IDF는 "Term Frequency-Inverse Document Frequency"의 약자로, 각 단어가 얼마나 중요한지 계산합니다.
예를 들어 "액션"이라는 단어가 모든 영화에 등장하면 중요도가 낮지만, "마블"이 한두 영화에만 등장하면 중요도가 높습니다. 이렇게 하면 "액션 영화"보다 "마블 액션 영화"처럼 더 구체적인 특징을 잡아낼 수 있습니다.
변환 결과는 각 영화가 하나의 벡터(숫자 배열)가 됩니다. 세 번째 단계에서는 코사인 유사도로 영화 간 유사도를 계산합니다.
벡터로 표현된 두 영화가 비슷한 방향을 가리키면 내용이 비슷한 것입니다. "어벤져스"와 "007"은 둘 다 "액션"과 "전투" 벡터 성분이 크므로 유사도가 높게 나옵니다.
반면 "어벤져스"와 "노트북"은 완전히 다른 방향이므로 유사도가 낮습니다. recommend_by_content 함수는 실제 추천 엔진입니다.
사용자가 "어벤져스"를 좋아한다고 표시하면, 어벤져스와 유사도가 높은 영화들을 순서대로 보여줍니다. 실제 서비스에서는 사용자가 좋아한 영화가 여러 개일 텐데, 그럴 때는 각 영화의 벡터를 평균내서 "사용자 프로필 벡터"를 만듭니다.
그리고 이 프로필 벡터와 가장 유사한 영화들을 추천하면 됩니다. 여러분이 이 코드를 활용하면 실시간 추천이 가능합니다.
새로운 영화가 추가되면 즉시 TF-IDF 벡터를 계산하고 유사도를 구할 수 있습니다. 협업 필터링처럼 "다른 사용자들이 이 영화를 볼 때까지 기다릴" 필요가 없습니다.
또한 사용자에게 "왜 이 영화를 추천했나요?"라고 물어보면 "액션과 히어로 요소가 당신이 좋아한 영화들과 비슷해서요"라고 구체적으로 설명할 수 있어, 신뢰도가 높아집니다. 실무에서는 이미지, 비디오, 오디오 같은 비정형 데이터도 딥러닝으로 벡터화해서 동일한 방식으로 추천합니다.
실전 팁
💡 단순 키워드 매칭을 넘어 임베딩을 사용하세요. TF-IDF는 단어가 정확히 일치해야 유사도가 높게 나옵니다. 하지만 "자동차"와 "차량"은 같은 의미인데 다른 단어로 취급됩니다. Word2Vec, BERT 같은 임베딩 모델을 사용하면 의미적으로 비슷한 단어들을 같은 벡터 공간에 배치할 수 있습니다.
💡 다양한 특성을 조합하세요. 영화 추천이라면 텍스트(줄거리) 외에도 장르(카테고리), 개봉연도(숫자), 평점(숫자), 감독/배우(카테고리) 등 여러 특성이 있습니다. 이들을 모두 벡터로 변환한 후 연결(concatenate)하면 더 풍부한 표현이 가능합니다. 각 특성의 중요도에 따라 가중치를 다르게 줄 수도 있습니다.
💡 사용자 피드백을 반영하세요. 사용자가 추천받은 영화를 실제로 봤는지, 얼마나 좋아했는지 추적하세요. 이 데이터로 사용자 프로필을 업데이트하면 추천이 점점 정확해집니다. 예를 들어 처음에는 "액션"을 좋아한다고 생각했는데, 계속 로맨스 영화를 보면 프로필을 조정해야겠죠.
💡 필터 버블을 조심하세요. 콘텐츠 기반 필터링은 사용자가 이미 좋아하는 것과 비슷한 것만 추천합니다. 이렇게 되면 사용자가 새로운 영역을 탐험하기 어렵습니다. 추천 결과의 10~20%는 의도적으로 다양성을 위해 조금 다른 콘텐츠를 포함시키세요.
💡 성능 최적화를 위해 벡터 인덱싱을 사용하세요. 아이템이 수백만 개라면 모든 아이템과 유사도를 계산하는 것은 너무 느립니다. Faiss, Annoy 같은 근사 최근접 이웃(ANN) 라이브러리를 사용하면 정확도를 약간 희생하는 대신 속도를 수백 배 개선할 수 있습니다.
4. 하이브리드_추천_시스템
시작하며
여러분은 넷플릭스를 사용할 때 추천이 정말 정확하다고 느낀 적 있나요? 사실 넷플릭스는 단 하나의 추천 알고리즘만 사용하지 않습니다.
여러 알고리즘을 조합해서 사용하죠. 협업 필터링은 비슷한 취향의 사람들을 찾아주지만, 새로운 영화에는 약합니다.
콘텐츠 기반 필터링은 새로운 영화도 추천할 수 있지만, 의외의 발견은 어렵습니다. 그렇다면 둘의 장점만 합치면 어떨까요?
바로 이것이 하이브리드 추천 시스템입니다. 마치 의사가 진단할 때 혈액검사, X-ray, 문진을 모두 종합하는 것처럼, 여러 추천 방법의 결과를 합쳐서 최종 추천을 만드는 거죠.
실무에서 가장 많이 사용되는 방식입니다.
개요
간단히 말해서, 하이브리드 추천 시스템은 여러 추천 알고리즘을 조합하여 각각의 약점을 보완하고 장점을 극대화하는 방식입니다. 실제 서비스에서 왜 이게 중요할까요?
단일 알고리즘으로는 모든 상황을 완벽하게 처리할 수 없습니다. 예를 들어 새 사용자에게는 인기 기반 추천을, 충분한 데이터가 쌓인 사용자에게는 협업 필터링을, 새로운 상품에는 콘텐츠 기반 필터링을 사용하는 식으로 상황에 맞게 전략을 바꿔야 합니다.
하이브리드 시스템은 이를 자동으로 처리합니다. 전통적으로는 개발자가 "이런 경우에는 이 알고리즘을 쓰자"는 규칙을 수동으로 만들어야 했습니다.
하지만 하이브리드 시스템에서는 머신러닝이 각 알고리즘의 결과에 자동으로 가중치를 매깁니다. 하이브리드 시스템의 핵심 특징은 네 가지입니다.
첫째, 강건성이 높습니다(한 알고리즘이 실패해도 다른 것이 보완). 둘째, 정확도가 높습니다(여러 신호를 종합).
셋째, 다양성이 좋습니다(여러 소스에서 추천). 넷째, 상황 적응력이 뛰어납니다(콜드 스타트, 데이터 희소성 등 다양한 문제 해결).
이러한 특징들 덕분에 아마존, 넷플릭스, 유튜브 같은 글로벌 서비스들이 모두 하이브리드 방식을 채택하고 있습니다.
코드 예제
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
# 예제 데이터
user_ratings = np.array([
[5, 3, 0, 1],
[4, 0, 0, 1],
[1, 1, 0, 5],
])
item_features = np.array([
[1, 0, 1], # 아이템 0: 액션, 마블
[0, 1, 0], # 아이템 1: 로맨스
[1, 1, 0], # 아이템 2: 액션, 로맨스
[0, 0, 1], # 아이템 3: 마블
])
# 1. 협업 필터링 점수
def collaborative_score(user_id, item_id):
user_sim = cosine_similarity(user_ratings)
sim_scores = user_sim[user_id]
item_ratings = user_ratings[:, item_id]
rated_mask = item_ratings > 0
if not rated_mask.any():
return 0
weighted = np.sum(sim_scores[rated_mask] * item_ratings[rated_mask])
sim_sum = np.sum(np.abs(sim_scores[rated_mask]))
return weighted / sim_sum if sim_sum > 0 else 0
# 2. 콘텐츠 기반 점수
def content_based_score(user_id, item_id):
# 사용자가 좋아한 아이템들의 특성 평균
liked_items = user_ratings[user_id] > 3
if not liked_items.any():
return 0
user_profile = np.mean(item_features[liked_items], axis=0)
item_vector = item_features[item_id]
# 코사인 유사도
similarity = np.dot(user_profile, item_vector) / (
np.linalg.norm(user_profile) * np.linalg.norm(item_vector) + 1e-10
)
return similarity
# 3. 하이브리드 추천 (가중 평균)
def hybrid_recommend(user_id, cf_weight=0.6, cb_weight=0.4, top_n=2):
scores = []
for item_id in range(len(item_features)):
# 이미 평가한 아이템은 제외
if user_ratings[user_id][item_id] > 0:
continue
cf_score = collaborative_score(user_id, item_id)
cb_score = content_based_score(user_id, item_id)
# 가중 평균
final_score = cf_weight * cf_score + cb_weight * cb_score
scores.append((item_id, final_score, cf_score, cb_score))
# 점수 기준 정렬
scores.sort(key=lambda x: x[1], reverse=True)
print(f"\n사용자 {user_id}에게 추천:")
for item_id, final, cf, cb in scores[:top_n]:
print(f" 아이템 {item_id}: 최종={final:.3f} (협업={cf:.3f}, 콘텐츠={cb:.3f})")
return scores[:top_n]
# 사용자 0에게 추천
hybrid_recommend(0, cf_weight=0.6, cb_weight=0.4, top_n=2)
설명
이것이 하는 일: 위 코드는 협업 필터링과 콘텐츠 기반 필터링의 결과를 가중 평균하여 최종 추천을 생성합니다. 첫 번째로, collaborative_score 함수는 협업 필터링 점수를 계산합니다.
이전에 배운 방식과 동일하게 비슷한 사용자들의 평점을 가중 평균합니다. 이 점수는 "다른 사람들의 집단 지성"을 반영하므로, 의외의 좋은 추천을 발견하는 데 강점이 있습니다.
하지만 평점 데이터가 부족하면 0을 반환합니다. 두 번째로, content_based_score 함수는 콘텐츠 기반 점수를 계산합니다.
먼저 사용자가 과거에 좋아한 아이템들(평점 3점 초과)의 특성들을 평균내서 "사용자 프로필"을 만듭니다. 예를 들어 사용자가 액션 영화 2개, 로맨스 영화 1개를 좋아했다면 프로필은 [0.67, 0.33, 0.67] 같은 형태가 됩니다.
그리고 새로운 아이템의 특성 벡터와 유사도를 계산합니다. 이 점수는 "사용자의 일관된 취향"을 반영하므로, 새로운 아이템에도 추천이 가능합니다.
세 번째 단계인 hybrid_recommend 함수에서 마법이 일어납니다. 각 아이템마다 협업 필터링 점수와 콘텐츠 기반 점수를 모두 계산한 후, 가중 평균을 냅니다.
기본값으로 협업 필터링에 60%, 콘텐츠 기반에 40% 가중치를 줍니다. 왜 60:40일까요?
실무에서는 A/B 테스트를 통해 최적의 비율을 찾습니다. 어떤 서비스는 70:30이 좋고, 어떤 서비스는 50:50이 좋을 수 있습니다.
이 코드의 장점은 상황에 따라 자동으로 적응한다는 것입니다. 예를 들어 새로운 아이템이라 협업 필터링 점수가 0이라도, 콘텐츠 기반 점수가 있으므로 추천이 가능합니다.
반대로 아이템 특성 정보가 부족해도 협업 필터링 점수로 추천할 수 있습니다. 결과를 출력할 때 각 알고리즘의 개별 점수도 함께 보여주므로, "왜 이 아이템이 추천되었는지" 분석하기 쉽습니다.
여러분이 실무에서 이 코드를 확장하면 더 많은 것을 할 수 있습니다. 예를 들어 시간대별로 가중치를 바꿀 수 있습니다(밤에는 협업 필터링 비중을 높이고, 새로운 콘텐츠가 많이 올라오는 오전에는 콘텐츠 기반 비중을 높임).
또는 사용자별로 다른 가중치를 사용할 수 있습니다(새 사용자에게는 콘텐츠 기반 비중을 높이고, 오래된 사용자에게는 협업 필터링 비중을 높임). 또한 인기도, 최신성, 다양성 같은 추가 요소들도 점수에 반영할 수 있습니다.
넷플릭스는 실제로 100개 이상의 서로 다른 추천 알고리즘을 조합한다고 알려져 있습니다.
실전 팁
💡 고정 가중치 대신 학습 가중치를 사용하세요. 위 코드는 60:40 같은 고정 비율을 사용하지만, 실무에서는 로지스틱 회귀나 그래디언트 부스팅 같은 머신러닝 모델로 최적 가중치를 학습시킵니다. 각 알고리즘의 점수를 특성으로 입력하고, 실제 사용자 클릭/구매 여부를 레이블로 사용해 학습하면 됩니다.
💡 메타 레벨 하이브리드를 고려하세요. 단순 가중 평균 대신, 한 알고리즘의 결과를 다른 알고리즘의 입력으로 사용하는 방법도 있습니다. 예를 들어 콘텐츠 기반으로 후보 아이템 100개를 뽑고, 그중에서 협업 필터링으로 최종 10개를 선택하는 식입니다. 이렇게 하면 계산량도 줄고 정확도도 높아집니다.
💡 스위칭 하이브리드도 유용합니다. 상황에 따라 아예 다른 알고리즘을 사용하는 방법입니다. 예를 들어 사용자 평점이 10개 미만이면 인기도 기반, 10~100개면 콘텐츠 기반, 100개 이상이면 협업 필터링을 사용하는 식입니다. 각 상황에 가장 적합한 알고리즘을 선택하므로 효율적입니다.
💡 앙상블 다양성을 확보하세요. 비슷한 알고리즘들을 조합하면 효과가 적습니다. 협업 필터링(사용자 기반)과 협업 필터링(아이템 기반)을 합치는 것보다, 협업 필터링과 콘텐츠 기반을 합치는 것이 훨씬 효과적입니다. 서로 다른 관점의 알고리즘을 조합하세요.
💡 실시간 가중치 조정을 구현하세요. A/B 테스트로 발견한 최적 가중치가 시간이 지나면서 변할 수 있습니다. 예를 들어 크리스마스 시즌에는 인기도 가중치를 높이고, 평소에는 개인화 가중치를 높이는 식으로 자동 조정하는 시스템을 만들 수 있습니다.
5. 행렬_분해_Matrix_Factorization
시작하며
여러분이 넷플릭스의 1억 명 사용자와 1만 개 영화의 평점 데이터를 다룬다고 상상해보세요. 1억 x 1만 = 1조 개의 칸이 있는 엄청난 표입니다.
컴퓨터 메모리에 저장조차 어렵죠. 게다가 대부분의 칸은 비어있습니다.
한 사람이 봤을 영화는 기껏해야 100개 정도니까요. 이런 "구멍 뚫린 치즈" 같은 데이터를 효율적으로 다루려면 어떻게 해야 할까요?
놀랍게도 이 거대한 행렬을 두 개의 작은 행렬로 쪼갤 수 있습니다. 마치 "사용자는 10가지 취향 요소를 가지고 있고, 영화도 10가지 특성 요소를 가지고 있다"고 가정하는 거죠.
이것이 행렬 분해의 핵심 아이디어입니다.
개요
간단히 말해서, 행렬 분해는 큰 사용자-아이템 평점 행렬을 두 개의 작은 행렬(사용자 특성 행렬과 아이템 특성 행렬)의 곱으로 표현하는 방법입니다. 실무에서 이게 왜 중요할까요?
앞서 본 협업 필터링 방법들은 메모리와 계산 비용이 너무 큽니다. 사용자 100만 명의 유사도 행렬은 1조 개의 값을 저장해야 합니다.
하지만 행렬 분해를 사용하면 각 사용자를 50차원 벡터로, 각 아이템을 50차원 벡터로 표현할 수 있습니다. 100만 x 50 + 1만 x 50 = 5천만 개의 값만 저장하면 되죠.
2만 배 이상 효율적입니다! 전통적인 협업 필터링에서는 "사용자 A와 사용자 B가 비슷하다"는 것을 명시적으로 계산했습니다.
하지만 행렬 분해에서는 숨겨진 특성(latent factor)을 자동으로 학습합니다. 예를 들어 "액션을 좋아하는 정도", "로맨스를 좋아하는 정도" 같은 특성들이 자동으로 발견됩니다.
행렬 분해의 핵심 특징은 네 가지입니다. 첫째, 메모리 효율이 매우 높습니다.
둘째, 희소한 데이터에서도 잘 작동합니다. 셋째, 숨겨진 패턴을 자동으로 발견합니다.
넷째, 확장성이 뛰어납니다(수억 명 사용자도 처리 가능). 이러한 특징들 덕분에 넷플릭스 프라이즈 대회에서 우승한 알고리즘의 핵심 기술이 되었고, 지금도 대규모 추천 시스템의 표준으로 사용됩니다.
코드 예제
import numpy as np
# 사용자-아이템 평점 행렬 (0은 평가 안 함)
R = np.array([
[5, 3, 0, 1, 0],
[4, 0, 0, 1, 2],
[1, 1, 0, 5, 4],
[0, 0, 5, 4, 0],
[0, 1, 4, 0, 3],
])
# 행렬 분해 파라미터
n_users, n_items = R.shape
n_factors = 3 # 숨겨진 특성 개수
learning_rate = 0.01
regularization = 0.01
n_epochs = 1000
# 사용자 행렬 P와 아이템 행렬 Q 초기화 (랜덤)
P = np.random.rand(n_users, n_factors)
Q = np.random.rand(n_items, n_factors)
# 경사하강법으로 학습
for epoch in range(n_epochs):
for i in range(n_users):
for j in range(n_items):
if R[i][j] > 0: # 평가한 아이템만 학습
# 예측 오차 계산
error = R[i][j] - np.dot(P[i], Q[j])
# 그래디언트 업데이트
P[i] += learning_rate * (error * Q[j] - regularization * P[i])
Q[j] += learning_rate * (error * P[i] - regularization * Q[j])
# 전체 평점 행렬 예측
predicted_R = np.dot(P, Q.T)
print("원본 평점 행렬:")
print(R)
print("\n예측 평점 행렬:")
print(predicted_R.round(2))
# 사용자 0에게 추천 (아직 평가 안 한 아이템 중 높은 점수)
user_id = 0
unrated_items = np.where(R[user_id] == 0)[0]
predictions = [(item, predicted_R[user_id][item]) for item in unrated_items]
predictions.sort(key=lambda x: x[1], reverse=True)
print(f"\n사용자 {user_id}에게 추천:")
for item, score in predictions[:2]:
print(f" 아이템 {item}: 예측 평점 {score:.2f}")
설명
이것이 하는 일: 위 코드는 경사하강법을 사용해 사용자-아이템 평점 행렬을 두 개의 작은 행렬로 분해하고, 빈 평점을 예측합니다. 첫 번째로, 행렬 R은 실제 사용자들의 평점입니다.
5명의 사용자와 5개의 아이템이 있고, 0은 "아직 평가하지 않음"을 의미합니다. 우리의 목표는 이 0인 부분을 정확하게 예측하는 것입니다.
핵심 아이디어는 "각 사용자는 n_factors개의 숨겨진 선호도를 가지고, 각 아이템은 n_factors개의 숨겨진 특성을 가진다"는 것입니다. 여기서는 3개로 설정했습니다.
두 번째로, P와 Q 행렬을 랜덤하게 초기화합니다. P는 (사용자 수 x 3) 크기이고, Q는 (아이템 수 x 3) 크기입니다.
예를 들어 P의 첫 번째 행 [0.7, 0.2, 0.9]는 "사용자 0은 특성 0을 0.7만큼, 특성 1을 0.2만큼, 특성 2를 0.9만큼 선호한다"는 의미입니다. Q의 첫 번째 행은 "아이템 0은 특성 0을 얼마나 가지고 있는지"를 나타냅니다.
이 두 벡터의 내적(dot product)이 예측 평점이 됩니다. 세 번째 단계는 학습입니다.
1000번 반복하면서 P와 Q를 조금씩 조정합니다. 각 반복마다 모든 (사용자, 아이템) 쌍을 확인합니다.
만약 사용자 0이 아이템 0에 5점을 줬는데 현재 예측이 3점이라면, 오차는 2입니다. 이 오차를 줄이는 방향으로 P[0]과 Q[0]을 업데이트합니다.
regularization 항은 과적합을 방지합니다. 너무 극단적인 값을 가지지 않도록 페널티를 주는 거죠.
학습이 끝나면 predicted_R = P @ Q.T로 전체 행렬을 예측할 수 있습니다. 이제 원래 0이었던 칸들도 예측값으로 채워집니다.
사용자 0이 아이템 2를 평가하지 않았다면, P[0]과 Q[2]의 내적으로 예측 평점을 계산합니다. 이 예측 평점이 높은 아이템을 추천하면 됩니다.
여러분이 이 코드를 실전에서 사용하면 놀라운 효과를 볼 수 있습니다. 첫째, 메모리 사용량이 획기적으로 줄어듭니다.
사용자 100만 명, 아이템 10만 개라도 n_factors=100이면 충분히 처리 가능합니다. 둘째, 추천 속도가 매우 빠릅니다.
각 사용자의 P 벡터와 각 아이템의 Q 벡터를 내적만 하면 되니까요. 셋째, 숨겨진 특성을 분석하면 재미있는 인사이트를 얻을 수 있습니다.
예를 들어 특성 0이 높은 영화들을 모아보니 모두 SF 영화더라, 같은 발견이 가능합니다. 넷째, 다른 추가 정보(사용자 나이, 아이템 가격 등)를 쉽게 통합할 수 있습니다.
실전 팁
💡 적절한 n_factors를 선택하세요. 너무 작으면 복잡한 패턴을 잡아내지 못하고, 너무 크면 과적합됩니다. 일반적으로 50~200 사이가 좋습니다. 교차 검증으로 최적값을 찾으세요.
💡 Bias 항을 추가하세요. 어떤 사용자는 모든 영화에 후하게 점수를 주고, 어떤 영화는 모두에게 높은 점수를 받습니다. 예측 공식을 rating = global_mean + user_bias + item_bias + dot(P, Q)로 확장하면 정확도가 크게 개선됩니다.
💡 SGD 대신 ALS를 고려하세요. 위 코드는 확률적 경사하강법(SGD)을 사용하지만, 교대 최소 제곱법(ALS)도 많이 사용됩니다. ALS는 P를 고정하고 Q를 최적화, Q를 고정하고 P를 최적화를 반복합니다. 병렬화가 쉬워서 대규모 데이터에 유리합니다. Spark MLlib에 구현되어 있습니다.
💡 암묵적 피드백도 활용하세요. 평점 데이터가 없어도 "이 영화를 봤다/안 봤다", "이 상품을 클릭했다/안 했다" 같은 암묵적 피드백으로도 행렬 분해가 가능합니다. 이럴 때는 손실 함수를 조금 다르게 정의합니다(Bayesian Personalized Ranking 같은 방법).
💡 시간 역학을 고려하세요. 사용자 취향은 시간에 따라 변합니다. 시간 정보를 모델에 통합하는 Time-SVD++ 같은 확장 버전도 있습니다. 예를 들어 P를 고정된 벡터가 아니라 시간의 함수로 만드는 거죠.
6. 딥러닝_기반_추천_신경망_협업_필터링
시작하며
여러분이 유튜브에서 영상을 볼 때, 다음에 볼 영상이 놀라울 정도로 정확하게 추천된다고 느낀 적 있나요? 이건 단순한 수학 공식으로는 설명하기 어려운 복잡한 패턴입니다.
사용자가 영상을 보는 시간대, 이전에 본 영상들의 순서, 같은 채널의 다른 영상, 영상 길이, 썸네일 스타일 등 수십 가지 요소가 복잡하게 얽혀있죠. 이런 복잡한 비선형 관계를 잡아내려면 어떻게 해야 할까요?
바로 딥러닝의 등장입니다. 신경망은 수백만 개의 파라미터로 아주 복잡한 패턴도 학습할 수 있습니다.
마치 사람의 뇌처럼 경험을 통해 "이런 사용자는 이런 영상을 좋아하더라"는 패턴을 자동으로 발견하는 거죠.
개요
간단히 말해서, 딥러닝 기반 추천은 신경망(Neural Network)을 사용해 사용자와 아이템의 복잡한 상호작용을 학습하는 방법입니다. 실무에서 이게 왜 중요할까요?
전통적인 행렬 분해는 선형 관계만 잡아냅니다. "사용자 특성 벡터와 아이템 특성 벡터의 내적"이라는 단순한 공식이죠.
하지만 실제 세상은 훨씬 복잡합니다. 예를 들어 "평소에는 액션을 좋아하지만 주말 밤에는 로맨스를 본다", "이 감독의 영화는 좋아하는데 이 배우와 조합되면 싫어한다" 같은 복잡한 조건부 선호가 있습니다.
딥러닝은 이런 비선형 패턴을 학습할 수 있습니다. 전통적인 방법에서는 특성 엔지니어링(feature engineering)에 많은 시간을 썼습니다.
"이 특성과 저 특성을 곱하면 좋을 것 같은데"라고 개발자가 수동으로 만들어야 했죠. 하지만 딥러닝은 자동으로 유용한 특성 조합을 찾아냅니다.
딥러닝 기반 추천의 핵심 특징은 네 가지입니다. 첫째, 비선형 패턴을 학습할 수 있습니다.
둘째, 다양한 종류의 데이터(텍스트, 이미지, 시퀀스 등)를 자연스럽게 통합할 수 있습니다. 셋째, 표현 학습(representation learning)으로 풍부한 임베딩을 자동으로 생성합니다.
넷째, 대규모 데이터에서 뛰어난 성능을 발휘합니다. 이러한 특징들 덕분에 유튜브, 틱톡, 인스타그램 같은 최신 플랫폼들이 딥러닝 기반 추천을 적극 사용합니다.
코드 예제
import numpy as np
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
# 예제 데이터 (실제로는 훨씬 많음)
user_ids = np.array([0, 0, 1, 1, 2, 2, 3, 3])
item_ids = np.array([0, 1, 0, 2, 1, 3, 2, 3])
ratings = np.array([5.0, 3.0, 4.0, 1.0, 2.0, 5.0, 4.0, 2.0])
n_users = 4
n_items = 4
embedding_dim = 8
# 신경망 협업 필터링 모델
user_input = layers.Input(shape=(1,), name='user_input')
item_input = layers.Input(shape=(1,), name='item_input')
# 임베딩 레이어
user_embedding = layers.Embedding(n_users, embedding_dim, name='user_embedding')(user_input)
item_embedding = layers.Embedding(n_items, embedding_dim, name='item_embedding')(item_input)
# Flatten
user_vec = layers.Flatten()(user_embedding)
item_vec = layers.Flatten()(item_embedding)
# 두 임베딩을 연결
concat = layers.Concatenate()([user_vec, item_vec])
# 깊은 신경망 레이어 (비선형 상호작용 학습)
dense1 = layers.Dense(64, activation='relu')(concat)
dense2 = layers.Dense(32, activation='relu')(dense1)
dense3 = layers.Dense(16, activation='relu')(dense2)
# 출력 레이어 (평점 예측)
output = layers.Dense(1, activation='linear', name='output')(dense3)
# 모델 컴파일
model = keras.Model(inputs=[user_input, item_input], outputs=output)
model.compile(optimizer='adam', loss='mse', metrics=['mae'])
# 학습
model.fit(
[user_ids, item_ids],
ratings,
epochs=100,
batch_size=4,
verbose=0
)
# 예측
user_id = np.array([0])
item_id = np.array([2])
predicted_rating = model.predict([user_id, item_id], verbose=0)
print(f"사용자 {user_id[0]}의 아이템 {item_id[0]}에 대한 예측 평점: {predicted_rating[0][0]:.2f}")
# 사용자 0에게 모든 아이템 추천
user_array = np.array([0] * n_items)
items_array = np.array(range(n_items))
all_predictions = model.predict([user_array, items_array], verbose=0).flatten()
print(f"\n사용자 0에게 모든 아이템 예측 평점:")
for item, pred in enumerate(all_predictions):
print(f" 아이템 {item}: {pred:.2f}")
설명
이것이 하는 일: 위 코드는 TensorFlow/Keras를 사용해 딥러닝 기반 협업 필터링 모델을 구축하고 학습시킵니다. 첫 번째로, 데이터를 준비합니다.
user_ids, item_ids, ratings는 "사용자 0이 아이템 0에 5점을 줬다" 같은 정보를 담고 있습니다. 실제 서비스에서는 수백만 개의 평점 데이터가 있겠지만, 원리는 동일합니다.
중요한 점은 사용자 ID와 아이템 ID를 숫자로 인코딩한다는 것입니다(0, 1, 2, 3...). 두 번째로, 임베딩 레이어를 만듭니다.
이것이 딥러닝 추천의 핵심입니다. Embedding 레이어는 각 사용자를 8차원 벡터로, 각 아이템을 8차원 벡터로 변환합니다.
행렬 분해의 P, Q와 비슷하지만, 여기서는 신경망의 일부로 학습됩니다. 처음에는 랜덤 벡터지만, 학습을 거치면서 "비슷한 취향의 사용자는 비슷한 벡터"를 가지게 됩니다.
세 번째 단계가 가장 중요합니다. 사용자 벡터와 아이템 벡터를 단순히 내적하는 대신, Concatenate로 연결한 후 여러 Dense 레이어를 통과시킵니다.
64개 뉴런 레이어, 32개 뉴런 레이어, 16개 뉴런 레이어를 거치면서 복잡한 패턴을 학습합니다. 각 레이어의 ReLU 활성화 함수가 비선형성을 제공하므로, "사용자 특성 0과 아이템 특성 1이 동시에 높으면 평점이 높다" 같은 복잡한 조건도 학습할 수 있습니다.
model.fit()으로 학습이 진행됩니다. 100번의 에포크 동안 모델은 실제 평점과 예측 평점의 차이(MSE)를 최소화하도록 모든 가중치를 조정합니다.
Adam 옵티마이저가 효율적으로 최적의 가중치를 찾아줍니다. 학습이 끝나면 model.predict()로 새로운 (사용자, 아이템) 쌍의 평점을 예측할 수 있습니다.
여러분이 이 코드를 실무에 적용하면 많은 확장이 가능합니다. 첫째, 추가 특성을 쉽게 넣을 수 있습니다.
사용자 나이, 성별, 지역, 아이템 가격, 카테고리 등을 별도의 입력으로 추가하고 concat에 연결하면 됩니다. 둘째, 시퀀스 데이터를 다룰 수 있습니다.
LSTM이나 Transformer를 사용하면 "사용자가 최근에 본 10개 영화의 순서"를 고려할 수 있습니다. 셋째, 멀티태스크 학습이 가능합니다.
평점 예측과 동시에 "클릭할 것인가"도 예측하도록 출력을 두 개 만들 수 있습니다. 넷째, 사전 학습된 임베딩을 사용할 수 있습니다.
예를 들어 영화 포스터 이미지를 CNN으로 임베딩하고, 이를 아이템 임베딩과 결합할 수 있습니다.
실전 팁
💡 과적합을 조심하세요. 신경망은 파라미터가 많아 쉽게 과적합됩니다. Dropout 레이어를 추가하거나, L2 정규화를 사용하거나, Early Stopping으로 검증 손실이 증가하면 학습을 중단하세요. 실무에서는 학습 데이터의 10~20%를 검증용으로 분리합니다.
💡 배치 정규화를 사용하세요. 각 Dense 레이어 뒤에 BatchNormalization을 추가하면 학습이 안정되고 빨라집니다. 특히 네트워크가 깊을 때 필수적입니다.
💡 임베딩 차원을 신중하게 선택하세요. 너무 작으면 정보 손실이 크고, 너무 크면 과적합되고 느립니다. 일반적으로 사용자/아이템 수의 제곱근 정도가 좋은 출발점입니다. 예를 들어 아이템이 10,000개면 100차원 정도.
💡 네거티브 샘플링을 고려하세요. 위 코드는 평점이 있는 데이터만 학습합니다. 하지만 "사용자가 이 아이템을 선택하지 않았다"는 정보도 중요합니다. 랜덤하게 (사용자, 아이템) 쌍을 샘플링해서 낮은 평점으로 학습시키면 모델이 더 잘 구별합니다.
💡 하이퍼파라미터 튜닝에 시간을 투자하세요. 레이어 개수, 뉴런 수, 학습률, 배치 크기 등이 성능에 큰 영향을 줍니다. Optuna나 Keras Tuner 같은 자동 튜닝 도구를 사용하면 최적 조합을 효율적으로 찾을 수 있습니다.
7. 컨텍스트_인식_추천_시스템
시작하며
여러분이 아침에 출근길에서 스포티파이를 켠다고 상상해보세요. 활기찬 팝송이 나오면 좋겠죠?
하지만 밤 11시에 집에서 같은 앱을 켜면 차분한 재즈가 나오는 게 더 좋을 겁니다. 똑같은 사용자, 똑같은 앱인데 추천이 달라져야 합니다.
왜냐하면 "상황(컨텍스트)"이 다르니까요. 시간대, 위치, 날씨, 요일, 함께 있는 사람, 사용 중인 기기 등 수많은 상황 정보가 사용자의 선호에 영향을 줍니다.
바로 이것이 컨텍스트 인식 추천입니다. 단순히 "이 사람은 이걸 좋아한다"가 아니라 "이 사람은 이런 상황에서 이걸 좋아한다"를 학습하는 거죠.
같은 음식도 점심과 저녁에 다르고, 혼자 있을 때와 친구와 함께 있을 때 다르듯이요.
개요
간단히 말해서, 컨텍스트 인식 추천은 사용자와 아이템뿐만 아니라 시간, 장소, 상황 등의 맥락 정보를 함께 고려하여 추천하는 방법입니다. 실무에서 이게 왜 중요할까요?
모바일 시대에는 컨텍스트가 엄청나게 중요합니다. 사용자가 집에서 와이파이로 접속했는지, 지하철에서 LTE로 접속했는지에 따라 추천해야 할 콘텐츠가 다릅니다.
와이파이면 긴 영화를, LTE면 짧은 클립을 추천하는 게 좋겠죠. 또 레스토랑 추천 앱이라면 현재 위치에서 가까운 곳을 추천해야 하고, 점심시간이면 빠른 식사를, 저녁시간이면 분위기 좋은 곳을 추천해야 합니다.
전통적인 추천 시스템에서는 "사용자 x 아이템" 2차원 행렬만 다뤘습니다. 하지만 컨텍스트 인식 추천은 "사용자 x 아이템 x 시간 x 위치 x ..." 다차원 텐서를 다룹니다.
이렇게 하면 훨씬 정교한 추천이 가능합니다. 컨텍스트 인식 추천의 핵심 특징은 네 가지입니다.
첫째, 상황 적응적입니다(같은 사용자에게도 상황에 따라 다른 추천). 둘째, 모바일 환경에 최적화되어 있습니다(GPS, 시간 등 활용).
셋째, 개인화 수준이 매우 높습니다(더 세밀한 선호 파악). 넷째, 실시간성이 강합니다(현재 상황에 즉각 반응).
이러한 특징들 덕분에 우버(위치 기반), 스포티파이(시간/활동 기반), 배달앱(위치/시간 기반) 등에서 필수적으로 사용됩니다.
코드 예제
import numpy as np
from sklearn.ensemble import GradientBoostingRegressor
# 컨텍스트를 포함한 학습 데이터
# [사용자ID, 아이템ID, 시간(0~23), 요일(0~6), 위치(0=집, 1=회사, 2=기타)]
X_train = np.array([
[0, 0, 8, 1, 1], # 사용자0, 아이템0, 아침8시, 월요일, 회사
[0, 1, 20, 1, 0], # 사용자0, 아이템1, 저녁8시, 월요일, 집
[0, 0, 21, 5, 0], # 사용자0, 아이템0, 밤9시, 토요일, 집
[1, 2, 12, 2, 1], # 사용자1, 아이템2, 점심12시, 화요일, 회사
[1, 1, 22, 6, 0], # 사용자1, 아이템1, 밤10시, 일요일, 집
[2, 0, 9, 3, 1], # 사용자2, 아이템0, 아침9시, 수요일, 회사
])
# 평점 (컨텍스트에 따라 같은 아이템도 평점이 다름)
y_train = np.array([3.0, 5.0, 2.0, 4.0, 5.0, 4.0])
# 그래디언트 부스팅으로 학습 (트리 기반이라 컨텍스트 상호작용 잘 잡음)
model = GradientBoostingRegressor(n_estimators=100, max_depth=5, random_state=42)
model.fit(X_train, y_train)
# 예측 함수
def predict_with_context(user_id, item_id, hour, weekday, location):
features = np.array([[user_id, item_id, hour, weekday, location]])
prediction = model.predict(features)[0]
return prediction
# 같은 사용자-아이템 쌍이 컨텍스트에 따라 다른 평점
print("사용자 0, 아이템 0 예측:")
print(f" 아침 회사: {predict_with_context(0, 0, 8, 1, 1):.2f}")
print(f" 저녁 집: {predict_with_context(0, 0, 20, 1, 0):.2f}")
print(f" 주말 집: {predict_with_context(0, 0, 14, 6, 0):.2f}")
# 현재 컨텍스트에서 최적 아이템 추천
def recommend_with_context(user_id, hour, weekday, location, n_items=3, top_n=2):
predictions = []
for item_id in range(n_items):
score = predict_with_context(user_id, item_id, hour, weekday, location)
predictions.append((item_id, score))
predictions.sort(key=lambda x: x[1], reverse=True)
return predictions[:top_n]
print(f"\n사용자 0에게 월요일 저녁 집에서 추천:")
recommendations = recommend_with_context(0, hour=20, weekday=1, location=0, n_items=3, top_n=2)
for item_id, score in recommendations:
print(f" 아이템 {item_id}: {score:.2f}")
설명
이것이 하는 일: 위 코드는 컨텍스트 변수(시간, 요일, 위치)를 포함하여 추천 모델을 학습하고, 같은 아이템도 상황에 따라 다른 평점을 예측합니다. 첫 번째로, 학습 데이터 X_train을 만듭니다.
기존 추천 시스템은 [사용자ID, 아이템ID] 두 개만 사용했지만, 여기서는 [시간, 요일, 위치]를 추가합니다. 예를 들어 첫 번째 행은 "사용자 0이 월요일 아침 8시에 회사에서 아이템 0을 사용했다"는 의미입니다.
실제 서비스에서는 날씨(맑음/비), 기기(모바일/데스크톱), 동반자(혼자/친구와), 이전 활동 등 훨씬 많은 컨텍스트를 사용할 수 있습니다. 두 번째로, y_train은 각 상황에서의 평점입니다.
중요한 점은 사용자 0이 아이템 0에 대해 여러 평점을 줄 수 있다는 것입니다. 아침 회사에서는 3점, 저녁 집에서는 5점처럼요.
이것이 컨텍스트 인식의 핵심입니다. "이 사람은 이 아이템을 좋아한다/싫어한다"는 이분법이 아니라 "이 사람은 이런 상황에서는 좋아하고 저런 상황에서는 싫어한다"를 학습합니다.
세 번째로, GradientBoostingRegressor를 사용합니다. 왜 이 모델을 선택했을까요?
트리 기반 모델은 자동으로 특성 간 상호작용을 잡아냅니다. 예를 들어 "시간이 20시 이상이고 위치가 집이면" 같은 조건을 자동으로 학습합니다.
선형 모델로는 이런 복잡한 조건을 다루기 어렵지만, 트리는 자연스럽게 처리합니다. 또한 100개의 트리를 앙상블하므로 과적합도 적고 정확도도 높습니다.
predict_with_context 함수는 실시간 추천의 핵심입니다. 사용자가 앱을 열 때마다 현재 시간, 요일, GPS 위치를 읽어서 이 함수에 넣으면 즉시 예측값을 얻을 수 있습니다.
예제에서 보듯이 사용자 0이 아이템 0에 대해 아침 회사에서는 낮은 점수를, 저녁 집에서는 높은 점수를 받습니다. 아마도 아이템 0은 "긴 영화"나 "여유 있는 활동"인 것 같네요.
recommend_with_context 함수는 전체 워크플로우를 보여줍니다. 현재 컨텍스트를 받아서 모든 아이템의 점수를 예측하고, 가장 높은 점수의 아이템들을 추천합니다.
실제 서비스에서는 수천 개 아이템을 모두 계산하면 느리므로, 먼저 협업 필터링으로 후보 100개를 뽑고, 그중에서 컨텍스트를 고려해 최종 10개를 선택하는 식으로 2단계로 처리합니다. 여러분이 이 코드를 실전에 적용하면 사용자 경험이 크게 개선됩니다.
예를 들어 음악 스트리밍 앱이라면 출근 시간에는 신나는 음악, 점심시간에는 편안한 음악, 운동 시간에는 빠른 템포의 음악을 자동으로 추천할 수 있습니다. 배달 앱이라면 비 오는 날에는 배달 시간이 짧은 음식점을, 주말 저녁에는 인기 맛집을 추천할 수 있습니다.
뉴스 앱이라면 출근 시간에는 짧은 헤드라인을, 저녁에는 긴 분석 기사를 추천할 수 있습니다. 컨텍스트를 활용하면 추천의 정확도가 20~30% 향상된다는 연구 결과들이 많습니다.
실전 팁
💡 컨텍스트 변수를 신중하게 선택하세요. 모든 정보가 유용한 건 아닙니다. 도메인 지식을 활용해서 실제로 사용자 선호에 영향을 주는 변수만 포함하세요. 너무 많은 변수를 넣으면 차원의 저주와 과적합 문제가 생깁니다. A/B 테스트로 각 컨텍스트의 효과를 측정하세요.
💡 컨텍스트를 올바르게 인코딩하세요. 위 코드에서 시간은 0~23 숫자로 표현했지만, 실제로는 순환적입니다(23시 다음이 0시). sin/cos 변환을 사용하면 이 순환성을 반영할 수 있습니다. 또한 요일은 원-핫 인코딩하는 것이 더 좋을 수 있습니다.
💡 Pre-filtering과 Post-filtering을 구분하세요. Pre-filtering은 컨텍스트로 데이터를 먼저 필터링한 후 추천 모델을 적용하는 방식입니다(예: "저녁 시간대 데이터만으로 모델 학습"). Post-filtering은 일반 추천 결과를 컨텍스트로 필터링하는 방식입니다. 위 코드는 Contextual Modeling 방식으로 가장 강력하지만 데이터가 많이 필요합니다.
💡 시간 윈도우를 활용하세요. 단순히 "20시"보다는 "지난 1시간 동안의 활동"이 더 유용할 수 있습니다. "최근 1시간 동안 액션 영화 3개를 봤다"면 지금도 액션 영화를 추천하는 게 좋겠죠. 슬라이딩 윈도우로 최근 N개의 상호작용을 특성으로 만드세요.
💡 개인정보 보호를 고려하세요. GPS 위치, 시간대 같은 민감한 정보를 사용할 때는 사용자 동의를 받고, 데이터를 익명화하세요. 정확한 위치 대신 "집/회사/기타" 같은 카테고리로 일반화하거나, 정확한 시간 대신 "아침/점심/저녁" 같은 구간으로 묶는 것도 방법입니다.
8. 평가_지표와_A_B_테스트
시작하며
여러분이 추천 시스템을 만들었다고 칩시다. 협업 필터링도 적용하고, 딥러닝도 사용하고, 컨텍스트도 고려했습니다.
그런데 이게 정말 잘 작동하는지 어떻게 알 수 있을까요? "내 눈에는 좋아 보이는데"로는 부족합니다.
실제 사용자들이 추천을 클릭하는지, 구매하는지, 만족하는지를 객관적으로 측정해야 합니다. 또 새로운 알고리즘이 기존보다 나은지 과학적으로 비교해야 하죠.
바로 이럴 때 필요한 것이 평가 지표와 A/B 테스트입니다. 마치 의사가 환자의 혈압, 혈당, 체온을 측정하듯이, 추천 시스템의 건강 상태를 다양한 지표로 진단하고, 실험을 통해 개선하는 거죠.
개요
간단히 말해서, 평가 지표는 추천 시스템의 성능을 수치화하는 방법이고, A/B 테스트는 두 가지 버전을 실제 사용자에게 동시에 제공하여 어느 것이 더 나은지 비교하는 실험 방법입니다. 실무에서 이게 왜 중요할까요?
추천 시스템은 비즈니스에 직접적인 영향을 줍니다. 넷플릭스는 추천 시스템 덕분에 연간 10억 달러를 절약한다고 합니다.
하지만 잘못된 추천은 사용자를 떠나게 만듭니다. 그래서 모든 변경사항을 신중하게 측정하고 검증해야 합니다.
"이 새로운 알고리즘이 클릭률을 5% 높였다"는 명확한 증거가 있어야 배포할 수 있습니다. 전통적으로는 정확도(Accuracy)만 측정했습니다.
"추천한 것 중 몇 %를 사용자가 좋아했나". 하지만 현대 추천 시스템은 훨씬 다양한 목표를 가집니다.
다양성(같은 것만 추천하면 지루함), 새로움(새로운 발견 제공), 커버리지(전체 아이템을 골고루 추천), 수익성(비싼 상품 추천) 등을 모두 고려해야 합니다. 평가의 핵심 특징은 네 가지입니다.
첫째, 다차원적입니다(여러 지표를 함께 봐야 함). 둘째, 온라인/오프라인 평가가 다릅니다(과거 데이터 분석 vs 실제 사용자 실험).
셋째, 비즈니스 지표와 연결되어야 합니다(CTR, 매출, 체류 시간 등). 넷째, 지속적입니다(한 번 측정하고 끝이 아니라 계속 모니터링).
이러한 특징들 덕분에 데이터 기반 의사결정이 가능하고, 추천 시스템을 지속적으로 개선할 수 있습니다.
코드 예제
import numpy as np
from sklearn.metrics import mean_squared_error, mean_absolute_error
# 실제 평점과 예측 평점
y_true = np.array([5, 4, 3, 5, 2, 4, 1, 5])
y_pred = np.array([4.5, 4.2, 3.1, 4.8, 2.5, 3.8, 1.2, 4.9])
# 1. 평점 예측 정확도 지표
rmse = np.sqrt(mean_squared_error(y_true, y_pred))
mae = mean_absolute_error(y_true, y_pred)
print("=== 평점 예측 정확도 ===")
print(f"RMSE (Root Mean Squared Error): {rmse:.3f}")
print(f"MAE (Mean Absolute Error): {mae:.3f}")
# 2. 랭킹 지표 (Top-K 추천의 정확도)
def precision_at_k(recommended, relevant, k):
"""상위 K개 추천 중 실제로 관련 있는 비율"""
recommended_k = recommended[:k]
relevant_set = set(relevant)
hits = len([item for item in recommended_k if item in relevant_set])
return hits / k if k > 0 else 0
def recall_at_k(recommended, relevant, k):
"""실제 관련 있는 것 중 상위 K개에 포함된 비율"""
recommended_k = recommended[:k]
relevant_set = set(relevant)
hits = len([item for item in recommended_k if item in relevant_set])
return hits / len(relevant_set) if len(relevant_set) > 0 else 0
def ndcg_at_k(recommended, relevant, k):
"""정규화된 할인 누적 이득 (순서도 고려)"""
dcg = 0
for i, item in enumerate(recommended[:k]):
if item in relevant:
# 순위가 높을수록 높은 가중치
dcg += 1 / np.log2(i + 2)
# 이상적인 DCG (관련 아이템이 모두 앞에 있을 때)
idcg = sum([1 / np.log2(i + 2) for i in range(min(len(relevant), k))])
return dcg / idcg if idcg > 0 else 0
# 예제: 사용자에게 추천한 아이템과 실제로 좋아한 아이템
recommended_items = [3, 7, 1, 9, 2, 5, 8] # 추천 순서
relevant_items = [3, 1, 5, 12] # 실제로 좋아한 아이템
print("\n=== 랭킹 지표 (Top-5 추천) ===")
print(f"Precision@5: {precision_at_k(recommended_items, relevant_items, 5):.3f}")
print(f"Recall@5: {recall_at_k(recommended_items, relevant_items, 5):.3f}")
print(f"NDCG@5: {ndcg_at_k(recommended_items, relevant_items, 5):.3f}")
# 3. 다양성 지표
def diversity(recommended_items, item_categories):
"""추천 아이템의 카테고리 다양성"""
categories = [item_categories.get(item, 'unknown') for item in recommended_items]
unique_categories = len(set(categories))
return unique_categories / len(categories) if len(categories) > 0 else 0
item_categories = {3: 'action', 7: 'action', 1: 'romance', 9: 'comedy', 2: 'action'}
div_score = diversity(recommended_items[:5], item_categories)
print(f"\n=== 다양성 지표 ===")
print(f"카테고리 다양성: {div_score:.3f}")
# 4. A/B 테스트 시뮬레이션
def ab_test_simulation():
"""A/B 테스트 결과 분석"""
# A 그룹: 기존 알고리즘 (클릭률 10%)
# B 그룹: 새로운 알고리즘 (클릭률 12%)
np.random.seed(42)
n_users = 1000
group_a_clicks = np.random.binomial(1, 0.10, n_users)
group_b_clicks = np.random.binomial(1, 0.12, n_users)
ctr_a = group_a_clicks.mean()
ctr_b = group_b_clicks.mean()
# 통계적 유의성 검정 (간단한 z-test)
p_pool = (group_a_clicks.sum() + group_b_clicks.sum()) / (2 * n_users)
se = np.sqrt(p_pool * (1 - p_pool) * (2 / n_users))
z_score = (ctr_b - ctr_a) / se
print(f"\n=== A/B 테스트 결과 ===")
print(f"그룹 A (기존) CTR: {ctr_a:.3f}")
print(f"그룹 B (신규) CTR: {ctr_b:.3f}")
print(f"개선율: {((ctr_b - ctr_a) / ctr_a * 100):.1f}%")
print(f"Z-score: {z_score:.2f} (>1.96이면 유의미)")
ab_test_simulation()
설명
이것이 하는 일: 위 코드는 추천 시스템의 성능을 측정하는 다양한 지표를 계산하고, A/B 테스트 결과를 분석합니다. 첫 번째로, RMSE와 MAE는 평점 예측 정확도를 측정합니다.
RMSE는 예측 오차의 제곱 평균의 제곱근입니다. 큰 오차에 더 큰 페널티를 주므로, "5점짜리 영화를 1점으로 예측"하는 치명적 실수를 더 강하게 반영합니다.
MAE는 절대 오차의 평균으로, 모든 오차를 동등하게 취급합니다. 둘 다 값이 작을수록 좋으며, 보통 RMSE < 1.0이면 꽤 좋은 성능입니다.
하지만 평점 예측 정확도가 높다고 해서 실제 사용자 만족도가 높은 건 아닙니다. 사용자는 정확한 평점보다 "내가 좋아할 만한 걸 추천해줬는가"에 더 관심이 있으니까요.
두 번째로, Precision@K와 Recall@K는 Top-K 추천의 품질을 측정합니다. Precision@5는 "추천한 5개 중 실제로 좋아한 것의 비율"입니다.
예제에서는 5개 추천 중 3개(3, 1, 5)가 적중했으므로 3/5 = 0.6입니다. Recall@5는 "실제로 좋아한 것 중 상위 5개에 포함된 비율"입니다.
실제로 좋아한 것이 4개인데 그중 3개를 포함했으므로 3/4 = 0.75입니다. 실무에서는 이 둘의 조화평균인 F1-Score도 많이 사용합니다.
세 번째로, NDCG@K는 순서까지 고려합니다. 같은 3개를 추천해도 1등으로 추천한 것과 5등으로 추천한 것은 다르니까요.
NDCG는 높은 순위에 관련 아이템이 있을수록 높은 점수를 줍니다. 1/log2(i+2) 공식 때문에 1위는 1.0, 2위는 0.63, 3위는 0.5의 가중치를 받습니다.
이상적인 순서(IDCG)와 비교하여 정규화하므로, 1.0에 가까울수록 완벽한 순서입니다. 넷플릭스, 유튜브 같은 서비스에서는 NDCG가 매우 중요합니다.
사용자는 추천 목록의 상위 몇 개만 보니까요. 네 번째로, 다양성 지표는 추천이 얼마나 다양한지 측정합니다.
위 예제는 단순하게 카테고리 개수로 계산했지만, 실무에서는 더 정교한 방법을 사용합니다. 예를 들어 아이템 간 유사도를 계산하여 "추천 목록 내 아이템들이 서로 얼마나 다른가"를 측정합니다.
다양성이 너무 낮으면 "추천이 다 비슷하네"라는 불만이 나오고, 너무 높으면 "내 취향과 안 맞는 것도 많네"라는 불만이 나옵니다. 균형이 중요합니다.
마지막으로, A/B 테스트 시뮬레이션은 실제 비즈니스에서 가장 중요한 부분입니다. 사용자를 무작위로 A그룹(기존 알고리즘)과 B그룹(새 알고리즘)으로 나누고, 각 그룹의 클릭률(CTR)을 비교합니다.
예제에서 B그룹이 12% CTR로 A그룹의 10%보다 높지만, 이게 우연일 수도 있습니다. 그래서 z-test로 통계적 유의성을 검정합니다.
Z-score가 1.96보다 크면 95% 신뢰도로 "진짜 차이가 있다"고 말할 수 있습니다. 여러분이 실무에서 평가할 때는 여러 지표를 종합적으로 봐야 합니다.
RMSE는 낮은데 Precision이 낮을 수 있고, Precision은 높은데 다양성이 낮을 수 있습니다. 또한 오프라인 지표(과거 데이터)와 온라인 지표(실제 사용자)가 다를 수 있습니다.
오프라인에서 NDCG가 높아도 실제 CTR이 낮으면 의미가 없습니다. 그래서 항상 A/B 테스트로 최종 검증해야 합니다.
큰 기업들은 한 번에 수백 개의 A/B 테스트를 돌리며 추천 시스템을 계속 개선합니다.
실전 팁
💡 여러 지표를 균형 있게 모니터링하세요. Precision만 최적화하면 인기 아이템만 추천하게 됩니다(안전하지만 지루함). 다양성과 새로움도 함께 추적하세요. 실무에서는 "가중 평균 점수 = 0.5NDCG + 0.3다양성 + 0.2*새로움" 같은 복합 지표를 만들어 사용합니다.
💡 오프라인 평가 시 시간 분할을 올바르게 하세요. 미래 데이터로 과거를 예측하면 안 됩니다. 예를 들어 2024년 1~11월 데이터로 학습하고, 12월 데이터로 평가하세요. 또한 사용자별로 분할하지 말고, 시간별로 분할하세요. 그래야 실제 서비스 환경과 비슷합니다.
💡 A/B 테스트 시 충분한 샘플 크기를 확보하세요. 사용자가 너무 적으면 통계적 유의성을 얻기 어렵습니다. 온라인 계산기로 필요한 샘플 크기를 미리 계산하세요. 일반적으로 그룹당 최소 1,000명 이상은 필요합니다.
💡 비즈니스 지표를 최우선으로 하세요. 기술 지표(RMSE, NDCG)는 중간 지표일 뿐입니다. 최종 목표는 비즈니스 성과(매출, 체류 시간, 재방문율 등)입니다. A/B 테스트에서 NDCG는 올랐는데 매출이 떨어졌다면, 그 알고리즘은 배포하면 안 됩니다.
💡 장기 효과를 고려하세요. 어떤 추천은 단기적으로는 CTR이 높지만 장기적으로 사용자를 지치게 만듭니다. 예를 들어 자극적인 제목의 낚시성 콘텐츠는 클릭률은 높지만 만족도는 낮습니다. 1주일, 1개월 후 사용자 리텐션도 함께 추적하세요.