[Recommender System] Bayesian Personalized Ranking from Implicit Feedback (BPR)
2025.06.01 - [Data & Research] - [Recommender System] Table of Contents
관찰되지 않은 User-Item 쌍은 실제 부정적인 피드백인지 아니면 단순히 아직 상호작용이 없었던 경우인지 구별하기 어렵다는 특징을 가지고 있습니다.
또, 많은 추천시스템에서 사용자의 평점 자체를 예측하는 것보다는 상대적인 선호(순위)를 예측하는 것이라는 점도 주목해주시기 바랍니다. 그런 관점에서는 우리가 달성해야 하는 목표가 "각 User별 personalized total ranking( \(>_{u} \subset I^2\) )을 구하는 것"이라고 생각해도 좋겠죠? (pairwise preference)
https://arxiv.org/pdf/1205.2618
1. 모델의 가정
\(>_{u}\)는 아래와 같은 특성을 가진다고 할 수 있겠습니다. (Item에 대한 상대적인 선호라고 한다면 직관적으로 이해가능합니다)
- \( \forall i, j \in I : i \neq j \Rightarrow i >_u j \lor j >_u i \quad \) (totality)
- \( \forall i, j \in I : i >_u j \land j >_u i \Rightarrow i = j \quad \) (antisymmetry)
- \( \forall i, j, k \in I : i >_u j \land j >_u k \Rightarrow i >_u k \quad \) (transitivity)
여기에
User가 보지 못한 item에 비해 관측한 item을 더 선호한다
유저 (\(i\), \(j\)) 모두 관측했거나 관측하지 않았다면 어떤 선호도도 추론할 수 없다
는 가정을 추가해서 논지를 전개해보겠습니다.
2. 목적 함수의 도출
BPR는 모델 파라미터 \(\Theta\)가 주어질 때 아래와 같은 사후확률(posterior)를 최대화하는 것을 목적으로 합니다.
\( p(\Theta | >_u) \propto p(>_u |\Theta) p(\Theta) \)
BPR은 모든 사용자의 행동은 서로 독립적이며, 특정 사용자에 대한 각 아이템 쌍의 순위는 다른 아이템 쌍의 순위와 독립적이라고 가정합니다. 따라서 사용자 \(u\)에 대한 likelihood 함수는 다음과 같이 표현될 수 있습니다.
\( p(>_u |\Theta) = \prod_{(u,i,j) \in D_S} p(i >_u j|\Theta) \)
여기서 \( D_S := \{(u, i, j)|i \in I^+_u \land j \in I \setminus I^+_u \} \) : \(i\)는 \(I_{𝑢}^+\) 에 속해있고 \(j\)는 속해있지 않다
사용자 \(u\)가 아이템 \(i\)를 아이템 \(j\)보다 선호할 확률 \( p(i >_u j|\Theta) \)는 아래 함수를 사용해서 정의됩니다.
\( \sigma(x) = 1 / (1 + e^{-x}) \)
\[ p(i >_u j|\Theta) := \sigma(\hat{x}_{uij}(\Theta)) \]
\( \hat{x}_{uij} \)는 모델 parameter에 의해 결정되는 사용자(\(u\)), 아이템(\(i\)), 아이템(\(j\))의 관계를 나타내는 함수이고 기반모델에 따라 달라집니다.
사전확률(prior)는 아래와 같이 가정해보겠습니다. 무난한 prior꼴이죠?
\[ p(\Theta) \sim \mathcal{N}(0, \Sigma_\Theta) \]
이 때 연산의 결과가 최종적으로 L2-regularization꼴이 되도록 하려면 \( \Sigma_\Theta = \lambda_\Theta I \) 와 같이 prior를 설정해주면 됩니다.
\( \ln p(\Theta| >_u) = \sum_{(u,i,j) \in D_S} \ln \sigma(\hat{x}_{uij}) + \ln p(\Theta) \) 이 posterior라고 할 수 있구요(물론 연산 상의 편의를 위해 log를 붙인 꼴).
방금 가정한것처럼 prior꼴을 잡아주면 아래와 같은 최적화 목적함수가 나옵니다.
\[ \sum_{(u,i,j) \in D_S} \ln \sigma(\hat{x}_{uij}) - \lambda_\Theta ||\Theta||^2 \]
3. 목적함수의 최적화(parameter estimation) 방법
위에서 도출한 목적함수는 미분가능하기 때문에 SGD(Stochastic Gradient Descent) 기반의 Learn BPR알고리즘으로 학습하게 됩니다. 데이터셋은 (User, positive item, negative item)의 triples 로 구성되며 이를 균등한 확률로 무작위 추출하는 Bootstrap 방식이 활용됩니다. 사실 implicit feedback은 긍정적(positive) feedback의 수가 압도적으로 많고 사용자별 상호작용이나 아이템별 인기도도 크게 다를 수 있지만 이런 접근을 통해 모델이 소수의 빈번한 상호작용에만 의존하지 않도록 조정해줍니다.
\[ \Theta \leftarrow \Theta + \alpha \left( \frac{\partial}{\partial \Theta} \ln \sigma(\hat{x}_{uij}) - \lambda_\Theta \Theta \right) \]
\[ \frac{\partial}{\partial \Theta} \ln \sigma(\hat{x}_{uij}) = \frac{1}{1 + e^{\hat{x}_{uij}}} \cdot \frac{\partial}{\partial \Theta} \hat{x}_{uij} \]
이 일반론을 대표적인 추천시스템인 MF에 한번 적용해보면 어떨까요?
BPR-MF에서는 사용자 \(u\)가 아이템 \(i\)를 아이템 \(j\)보다 선호하는 정도를 아래와 같이 정의합니다. 선호도는 latent vector 2개의 내적으로 표현된다는 점을 체크해보시면 좋습니다.
\[ \hat{x}_{uij} = \hat{x}_{ui} - \hat{x}_{uj} = w_u^T h_i - w_u^T h_j \]
그러면 이 관점에서 gradient (사용자, 긍정아이템, 부정아이템 에 따라)는 아래와 같이 계산해볼 수 있습니다.
\[ \frac{\partial}{\partial w_u} \ln \sigma(\hat{x}_{uij}) = \frac{1}{1 + e^{\hat{x}_{uij}}} (h_i - h_j) - \lambda_\Theta w_u \]
\[ \frac{\partial}{\partial h_i} \ln \sigma(\hat{x}_{uij}) = \frac{1}{1 + e^{\hat{x}_{uij}}} w_u - \lambda_\Theta h_i \]
\[ \frac{\partial}{\partial h_j} \ln \sigma(\hat{x}_{uij}) = -\frac{1}{1 + e^{\hat{x}_{uij}}} w_u - \lambda_\Theta h_j \]
4. 프로그래밍 예시
BPR을 지원하는 파이썬 패키지가 있어서 어렵지 않게 코딩해볼 수 있습니다.
import numpy as np
import pandas as pd
from spotlight.interactions import Interactions
from spotlight.factorization.bpr import BPR
from sklearn.model_selection import train_test_split
# 0. 데이터 생성 (가상 데이터)
# 실제 시나리오에서는 MovieLens와 같은 데이터셋을 사용합니다.
# (user_id, item_id) 쌍으로 구성된 데이터입니다.
data = {
'user_id': [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3],
'item_id': [0, 1, 2, 0, 3, 4, 1, 2, 5, 3, 5, 6],
'rating': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] # 암묵적 피드백이므로 모두 1로 설정 (시청했다는 의미)
}
df = pd.DataFrame(data)
# spotlight 라이브러리를 위한 Interactions 객체 생성
# rating은 암묵적 피드백이므로 1로 채워진 더미 값입니다.
# num_users와 num_items는 데이터셋의 고유 사용자/아이템 수입니다.
unique_users = df['user_id'].unique()
unique_items = df['item_id'].unique()
num_users = len(unique_users)
num_items = len(unique_items)
# Interactions 객체는 학습 데이터에 필요한 형식입니다.
interactions = Interactions(
user_ids=df['user_id'].values,
item_ids=df['item_id'].values,
ratings=df['rating'].values, # 암묵적 피드백이므로, 단순히 존재 유무를 나타냅니다.
num_users=num_users,
num_items=num_items
)
print(f"총 사용자 수: {num_users}")
print(f"총 아이템 수: {num_items}")
print(f"인터랙션(시청 기록) 수: {len(interactions)}")
# 1. 학습 및 평가 데이터 분할
# BPR은 훈련 데이터에서 긍정적인 쌍(사용자-선호 아이템)과 부정적인 쌍(사용자-비선호 아이템)을 샘플링하여 학습합니다.
train_interactions, test_interactions = train_test_split(
interactions, test_size=0.2, random_state=RANDOM_SEED
)
print(f"훈련 데이터 인터랙션 수: {len(train_interactions)}")
print(f"테스트 데이터 인터랙션 수: {len(test_interactions)}")
# 2. BPR 모델 정의 및 학습
# embedding_dim: 사용자 및 아이템 특징 벡터의 차원
# n_iter: 학습 에포크 수
# batch_size: 배치 크기
# l2: L2 정규화 강도 (과적합 방지)
model = BPR(
loss='bpr', # BPR 손실 함수 사용
embedding_dim=32,
n_iter=20,
batch_size=128,
l2=0.01,
random_state=RANDOM_SEED
)
print("\nBPR 모델 학습 시작...")
model.fit(train_interactions)
print("BPR 모델 학습 완료!")
# 3. 추천 생성 및 평가 (간단한 예시)
# 특정 사용자에 대한 추천
user_id_to_recommend = 0
print(f"\n사용자 {user_id_to_recommend}에게 추천할 아이템:")
# 사용자가 이미 본 아이템
watched_items = df[df['user_id'] == user_id_to_recommend]['item_id'].values
print(f" 사용자가 이미 본 아이템: {watched_items}")
# 모든 아이템에 대한 예측 점수 얻기
# predict 함수는 사용자 ID와 아이템 ID 배열을 받아서 예측 점수를 반환합니다.
# 이 점수는 높을수록 선호도가 높을 것으로 예측됨을 의미합니다.
all_item_ids = np.arange(num_items)
predictions = model.predict(user_id_to_recommend, all_item_ids)
# 예측 점수가 높은 순서로 아이템 정렬 (내림차순)
top_n = 5 # 상위 N개 추천
recommended_items_with_scores = sorted(
zip(all_item_ids, predictions), key=lambda x: x[1], reverse=True
)
# 이미 본 아이템을 제외하고 상위 N개 추천
count = 0
print(f" 추천 아이템 (점수 포함, 본 아이템 제외):")
for item_id, score in recommended_items_with_scores:
if item_id not in watched_items:
print(f" 아이템 {item_id}: 예측 점수 {score:.4f}")
count += 1
if count >= top_n:
break
# 4. 모델 성능 평가 (Train AUC)
# BPR 모델의 성능을 평가하는 한 가지 방법은 AUC (Area Under the ROC Curve)입니다.
# AUC는 임의의 긍정적 쌍이 임의의 부정적 쌍보다 높은 순위를 가질 확률을 나타냅니다.
# 훈련 데이터셋에 대한 AUC 계산
train_auc = model.predict_auc(train_interactions)
print(f"\n훈련 데이터셋 AUC: {train_auc:.4f}")
# 테스트 데이터셋에 대한 AUC 계산
# 실제 추천 시스템에서는 테스트 데이터셋으로 모델의 일반화 성능을 평가하는 것이 중요합니다.
test_auc = model.predict_auc(test_interactions)
print(f"테스트 데이터셋 AUC: {test_auc:.4f}")
# 참고: spotlight 라이브러리는 내부적으로 Negative Sampling을 처리합니다.
# BPR의 핵심은 긍정적 샘플 (user, item_i)과 부정적 샘플 (user, item_j) 쌍을 생성하여
# user가 item_i를 item_j보다 선호한다는 것을 학습하는 것입니다.
# spotlight.factorization.bpr.BPR 클래스는 이 샘플링 과정을 자동으로 수행합니다.