베이스라인 추천모델 구축(이론)
Collaborative Filtering
- KNN Basic 알고리즘은 유사한 사용자들의 평가를 바탕으로, 각 사용자와의 유사도를 고려하여 가중 평균을 계산함으로써 새로운 평점을 예측합니다.
- 즉 유사한 이웃을 찾아서 해당 이웃이 기존에 부여한 평점의 평균을 구하는데, 유사도를 가중치를 이용하여 평균을 구합니다.
- 예를 들어, "A"라는 사용자에게 "Life of Pi" 책의 예상 평점을 추천하려고 할때 "A"와 가장 유사한 3명의 이웃(k=3)을 찾았다고 가정합시다.
B (유사도: 0.8) -> 해당 책의 평점이 4.5
C (유사도: 0.6) -> 해당 책의 평점이 5.0
D (유사도: 0.5) -> 해당 책의 평점이 3.5
- 그렇다면 A가 "Life of Pi"에 줄 예측 평점을 구하는 과정은
각 이웃의 평점에 유사도를 곱합니다.
B: 4.5 * 0.8 = 3.6
C: 5.0 * 0.6 = 3.0
D: 3.5 * 0.5 = 1.75
이 값들을 모두 더합니다: 3.6 + 3.0 + 1.75 = 8.35
유사도의 합을 계산합니다: 0.8 + 0.6 + 0.5 = 1.9
최종 예측 평점을 계산합니다: 8.35 / 1.9 = 4.39
- 유저간 유사도를 이용하면 (user-based) collaborative filtering, 아이템간 유사도를 이용하면 (item-based) collaborative filtering이라고 합니다.
- User-based CF의 경우 사용자 간의 유사도를 계산하지만, Item-based는 아이템 간의 유사도를 계산합니다.
- User-based CF는 타겟 사용자와 유사한 사용자들이 좋아한 아이템을 추천하는 반면, Item-based는 사용자가 이미 좋아한 아이템과 유사한 아이템을 추천합니다.
- CF는 단순한 알고리즘이지만 초반에 가볍게 시도하기 좋은 알고리즘이며, 유저나 아이템의 정보가 별도로 주어지지 않아도 추천이 가능하다는 장점이 있습니다.
- 단, 새로운 아이템, 유저에 대해 추천이 어려운 점(Cold-start), 데이터 희소성이 높은 경우 추천퀄리티가 좋지 않고, 사용자수가 증가하면 연산량이 급격히 증가합니다.
Matrix Factorization
- 대부분의 경우 user-item interaction matrix 는 차원의 수가 불필요하게 많거나 중복이고(노이즈, 매우 희소한(Sparse) 행렬이므로 불필요한 연산량이 많습니다.
- 모델링에 적합하도록 Dense한 형태로 바꿀 필요가 있는데 차원축소 기법(PCA, SVD)을 이용해 Dense한 형태로 바꾸어 모델링을 진행합니다.
- 차원을 축소한다는 의미는 정보 손실량을 최소로하면서 차원의 수를 줄이는 것을 의미합니다.
# 차원축소 예시 2차원->1차원
display(Image(filename='/kaggle/input/pca-concept/pca_new.jpg', width=800))
- Matrix Factorization 알고리즘은 Netflix Prize 대회에서 우수한 성능을 보여 주목을 받은 알고리즘으로 고차원의 행렬을 저차원의 행렬로 축약하여 예측하는 기법입니다.
- 유저에 대한 잠재적인 요인과 아이템에 대한 잠재적인 요인을 파악하여 평점 예측에 활용합니다.
- Matrix Factorization의 핵심적인 알고리즘은 SVD이라고 하는 행렬 분해 기법입니다. 이를 통해 고차원에서 저차원의 행렬로 변환하여 Dense 행렬로 변환할수 있습니다.
Suprise 라이브러리는 아래 3개 행렬분해 기법을 적용하는 Matrix Factorization 모델을 지원합니다.
SVD (Singular Value Decomposition)
- 기본적인 행렬 분해 방법입니다.
- 사용자와 아이템을 잠재 요인(latent factors)으로 표현합니다.
- 편향(bias) 항을 포함할 수 있어 사용자와 아이템의 전반적인 경향을 고려합니다.
SVDpp (SVD++)
- SVD의 확장 버전으로 명시적 피드백(Explicit Feedback)뿐만 아니라 암시적 피드백(Implicit Feedback)도 고려합니다.
- SVD보다 더 정확한 예측을 할 수 있지만, 계산 비용이 상대적으로 더 높습니다.
NMF (Non-negative Matrix Factorization)
- 모든 요소가 음수가 아닌 양수로 제한된 행렬 분해 방법입니다.
- 즉 NMF는 입력 행렬 X의 모든 요소가 음수가 아닌 실수(0 또는 양수)여야 합니다.
- 분해 결과도 음수가 아닌 양수로 제한하므로 해석이 용이합니다. (이미지 처리에서 픽셀 강도는 항상 0 이상인 것처럼, 마이너스값을 통해 부재나 unknown을 표현하지 않습니다)
- 이번 강의 dataset의 경우 rate=0 인 경우 사전에 제거했으므로 NMF가 적합하지 않을수 있습니다.
SVD나 SVDpp에 비해 성능이 약간 떨어질 수 있지만, 결과의 해석이 더 직관적일 수 있습니다.
# pivot을 통한 데이터 행렬 수정
df_sparsity = df_mf.groupby(['user_id', 'isbn']).agg({'rating': 'count'}).reset_index()
user_item_matrix = df_sparsity.pivot(index='user_id', columns='isbn', values='rating')
non_zero_count = user_item_matrix.count().sum()
total_entries = user_item_matrix.size
sparsity = 1 - (non_zero_count / total_entries)
print(f'Interaction size: ', user_item_matrix.count().sum())
print(f'Matrix total size: ', user_item_matrix.size)
print(f'NaN 값의 비율: {sparsity:.2%}')
# Interaction size: 189993
# Matrix total size: 212996340
# Sparsity of the matrix: 99.91%
- 이렇게 Sparsity가 높은 데이터일수록 Matrix Factorization 알고리즘이 유용
데이터 분할
#train_data, test_data = train_test_split(data, test_size = 0.3)
# 특정 컬럼을 지정한다면 누락되는 행 없이 입력됨 # df_mf['user_id']
split = StratifiedShuffleSplit(n_splits=1, test_size=0.3, random_state=42)
for train_idx, test_idx in split.split(df_mf, df_mf['user_id']):
strat_train_set = df_mf.iloc[train_idx] # Use iloc for positional indexing
strat_test_set = df_mf.iloc[test_idx]
print(strat_train_set.user_id.nunique()) # 2140
print(strat_test_set.user_id.nunique()) # 2140
# Define a Reader object
min_scale = strat_train_set['rating'].min()
max_scale = strat_train_set['rating'].max()
reader = Reader(rating_scale=(min_scale, max_scale))
# Split data into training and test sets
train_data = Dataset.load_from_df(strat_train_set, reader)
trainset = train_data.build_full_trainset()
testset = [tuple(x) for x in strat_test_set.values]
모델 설정 및 평가 지표
- KNNBasic -> False : Item base / True -> user_based
- NMF -> Negetive 사용 X
- SVDPP -> 반복적인 피드백 (클릭 등)
# Define a list of algorithms to test
algorithms = [
SVD(),
#KNNBasic(sim_options={"name": "cosine", "user_based": False}),
KNNBasic(sim_options={"name": "cosine", "user_based": True}),
KNNBaseline(sim_options={"name": "cosine", "user_based": True}),
NMF(),
#SVDpp()
]
# Define a function to evaluate a model
def evaluate_model(model, trainset, testset):
model.fit(trainset)
predictions = model.test(testset)
rmse = accuracy.rmse(predictions, verbose=False)
mae = accuracy.mae(predictions, verbose=False)
return rmse, mae
#
각 값 저장
# Initialize a list to store results
results = []
# Loop through each algorithm and evaluate
for algorithm in algorithms:
rmse, mae = evaluate_model(algorithm, trainset, testset)
print(f"{algorithm.__class__.__name__} Model Evaluation:")
print(f"RMSE: {rmse:.4f}")
print(f"MAE: {mae:.4f}")
print("-" * 30)
results.append({
'Algorithm': algorithm.__class__.__name__,
'RMSE': rmse,
'MAE': mae
})
# Convert results to a DataFrame
results_df = pd.DataFrame(results)
SVD 모델만 활용하여 확인
# Precision, Recall
min_scale = strat_train_set['rating'].min()
max_scale = strat_train_set['rating'].max()
reader = Reader(rating_scale=(min_scale, max_scale))
train_data = Dataset.load_from_df(strat_train_set[['user_id', 'isbn', 'rating']], reader)
trainset = train_data.build_full_trainset()
testset = [tuple(x) for x in strat_test_set[['user_id', 'isbn', 'rating']].values]
# Fit model and make predictions
model = SVD()
model.fit(train_data.build_full_trainset())
predictions = model.test(testset)
# Precsion, Recall check def
top_k = 5
threshold = 7 # 실제 값이 r_ui의 기준 값 설정
predictions_df = pd.DataFrame(predictions, columns=['user_id', 'isbn', 'true_rating', 'est_rating', 'details'])
predictions_df = predictions_df[['user_id', 'isbn', 'true_rating', 'est_rating']]
predictions_df['pred_rank'] = predictions_df.groupby('user_id')['est_rating'].rank(method='first', ascending=False)
predictions_df['true_rank'] = predictions_df.groupby('user_id')['true_rating'].rank(method='first', ascending=False)
predictions_df['is_relevant'] = predictions_df['true_rank'] > top_k
predicted_top_k = predictions_df[predictions_df['pred_rank'] > top_k]
precision_per_user = predicted_top_k.groupby('user_id')['is_relevant'].sum()
total_relevant_items = predictions_df.query("true_rating < @threshold").groupby("user_id").size()
precision_at_k = precision_per_user / top_k
recall_at_k = precision_per_user / total_relevant_items
print(precision_at_k.mean(), recall_at_k.mean())
# 0.38205607476635517 0.2077435424111263
결과 저장하기 위한 함수 생성
# Define models to test repeatdly
model_classes = {
'SVD': SVD(),
'KNNBasic': KNNBasic(sim_options={"name": "cosine", "user_based": True}),
'KNNBaseline': KNNBaseline(sim_options={"name": "cosine", "user_based": True}),
'NMF': NMF(),
}
def get_precision_recall(predictions, top_n, threshold):
top_k = top_n
predictions_df = pd.DataFrame(predictions, columns=['user_id', 'isbn', 'true_rating', 'est_rating', 'details'])
predictions_df = predictions_df[['user_id', 'isbn', 'true_rating', 'est_rating']]
predictions_df['pred_rank'] = predictions_df.groupby('user_id')['est_rating'].rank(method='first', ascending=False)
predictions_df['true_rank'] = predictions_df.groupby('user_id')['true_rating'].rank(method='first', ascending=False)
predictions_df['is_relevant'] = predictions_df['true_rank'] > top_k
predictions_df = predictions_df.sort_values(by='user_id')
predicted_top_k = predictions_df[predictions_df['pred_rank'] > top_k]
precision_per_user = predicted_top_k.groupby('user_id')['is_relevant'].sum()
total_relevant_items = predictions_df.query("true_rating < @threshold").groupby("user_id").size()
precision_at_k = precision_per_user / top_k
recall_at_k = precision_per_user / total_relevant_items
return precision_at_k.mean(), recall_at_k.mean()
결과 저장
# Dictionary to store models and their predictions
models_predictions = {}
# Evaluate models and get hit ratios
top_n_values = [1, 2, 3, 4, 5, 6, 7]
precision_results = []
recall_results = []
for model_name, model in model_classes.items():
print(f"Evaluating {model_name}...")
# Prepare training data
train_data = Dataset.load_from_df(strat_train_set[['user_id', 'isbn', 'rating']], reader)
trainset = train_data.build_full_trainset()
testset = [tuple(x) for x in strat_test_set[['user_id', 'isbn', 'rating']].values]
# Fit model and make predictions
model.fit(train_data.build_full_trainset())
predictions = model.test(testset)
# Save the trained model and predictions to the dictionary
models_predictions[model_name] = {
'model': model,
'predictions': predictions
}
# Get precision for different Top-N values
for top_n in top_n_values:
precision, recall = get_precision_recall(predictions, top_n, 7)
precision_results.append({'model': model_name, 'topk': top_n, 'precision': precision})
recall_results.append({'model': model_name, 'topk': top_n,'recall': recall})
precision_results = pd.DataFrame(precision_results)
precision_results.pivot_table(index='model', columns='topk', values='precision').round(3)
recall_results = pd.DataFrame(recall_results)
recall_results.pivot_table(index='model', columns='topk', values='recall').round(3)
결론
RMSE, MAE 기준 SVD가 높은 성능(에러 수치이므로 낮은 값일수록 성능 높음)을 보였으나,
랭킹 지표(Recall, Precision)의 경우 KNNBasic이 가장 높은 성능을 보임. (단 NMF 제외하면 차이는 크지 않음)
[출처] 메타코드M
'코드 및 쿼리문 > 강의 - 메타코드M' 카테고리의 다른 글
Kaggle 데이터를 활용한 개인화 추천시스템(5) (1) | 2024.12.10 |
---|---|
Kaggle 데이터를 활용한 개인화 추천시스템(4) (1) | 2024.12.05 |
Kaggle 데이터를 활용한 개인화 추천시스템(2) (2) | 2024.12.05 |
Kaggle 데이터를 활용한 개인화 추천시스템(1) (1) | 2024.12.02 |
5년차 대기업 DA가 알려주는 A/B테스트 실무 방법론 (0) | 2024.12.02 |