데이터 분석/추천시스템

RS(2): 추천시스템 평가 지표 정리

민서타 2023. 11. 24. 00:19

1. 추천시스템 평가 지표

MRR(Mean Reciprocal Rank): 관련 있는게 언제 처음 나왔는지, 이진적으로 좋은 추천인지 나쁜 추천인지 평가

MAP(Mean Average Precision): 리스트 내 유관상품이 등장할 때 마다 정밀도를 구하고 평균. 이진적으로 좋은지 나쁜지 평가

NDCG(Normalized Discounted Cumulative Gain): DCG를 IDCG로 나누어 표준화해준 값
 -CG: i번째 포지션의 평가된 유관도의 누적합(추천된 총 p번까지의 추천 상품들의)
 -DCG: CG의 각 항을 log(i+1)로 나누어줌(덜 중요한 뒤쪽 추천 리스트에 패널티 부여)
 -IDCG: 코퍼스 내 p번 자리까지의 유관한 문서들의 목록에 대한 총 누적 이득(도달할 수 있는 최고값)
 -Average NDCG Across User: 유저 간 NDCG의 총합

Hit rate: 유저에게 추천한 것 중 마음에 드는게 있었는지 1 / 0 표현

Diversity: 다양한 것을 추천

Novelty: 참신성

Serendipity: 의도적으로 찾지 않았음에도 뭔가 새로운 좋은 것을 발견하는 일

XAI(eXplainable AI): 머신러닝 모델이 어떻게 의사결정을 내리는지 인간이 이해할 수 있도록 설명하는 알고리즘

블랙박스 해석 모델(LIME, SHAP)

LIME

Local: observation specific, 개인 혹은 한 샘플에 내려진 판단이 어떻게 내려진 것인지 분석
Interpretable Explanation: 각 예측을 내림에 있어 어떤 피처가 사용되었는지
Model-agnostic: 어떤 모델을 사용하든지 간에 무관하게 사용
Explanations

메커니즘
1) 데이터 뒤섞기
2) 뒤섞은 데이터와 기존 관측치 사이의 거리 측정
3) blackbox model을 사용해 새로운 데이터를 대상으로  예측수행
4) 뒤섞은 데이터로부터 복잡한 모델의 출력을 가장 잘 설명하는 m개의 피처 선택

5) 여기서 뽑은 m개의 피처로 뒤섞은 데이터를 대상으로 단순한 모델 적합, 유사도 점수를 가중치로 사용
6) 단순한 모델의 가중치는 복잡한 모델의 local한 행동을 설명하는데 사용

SHAP

피처의 기여도를 기반으로 전체모델, 개별 데이터의 변수 중요도 설명

ALS

ALS(Alternating Least Squares): 주어진 사용자-아이템 평가 행렬을 사용자의 특성 벡터와 아이템의 특성 벡터로 분해

 -User Factor Update: 사용자 요인 행렬을 고정, 아이템 요인 행렬 업데이트
 -Item Factor Update: 아이템 요인 행렬을 고정시키고 사용자 요인 행렬을 업데이트

두 단계를 번갈아 수행하며 각 특성을 업데이트 및 최적화 -> 잠재 요인을 고려한 사용자의 평가를 예측하고 추천

EBM

EBM(Explainable Boosting Machine): 글래스 박스 해석 모델

코드 구현

 

from lime.lime_tabular import LimeTabularExplainer

model = LGBMClassifier(random_state=42)
# instance 선택
instance = X_test[X_test.index==sample_idx].values[0]

# explainer 초기화
explainer = LimeTabularExplainer(X_train_sample.values,
                                 feature_names=X_train_sample.columns,
                                 class_names=['0', '1'],
                                 verbose=True,
                                 mode='classification')

# 예측 결과 설명 생성
exp = explainer.explain_instance(instance, model.predict_proba)

# 설명 시각화
exp.show_in_notebook(show_table=True)

# 피처 리스트 및 그에 해당하는 가중치
features = exp.as_list()

# 가중치 절댓값 크기에 따라 피처 정렬
# features.sort(key=lambda x: np.abs(x[1]), reverse=True)

# 가중치 값에 따라 피처 정렬
features.sort(key=lambda x: x[1], reverse=True)

# 두 리스트로 나누기
feature_names, feature_weights = zip(*features)

# 바 플랏 그리기
plt.figure(figsize=(10, 6))
plt.barh(feature_names, feature_weights, color='skyblue')
plt.xlabel('Feature Weights')
plt.gca().invert_yaxis()
plt.show()

%%time
import shap

# shap tree explainer 인스턴스 생성
shap_explainer = shap.TreeExplainer(model)

# test set 대상으로 shap value 생성
shap_values = shap_explainer.shap_values(X_test)

# 테스트 셋 내의 한 인스턴스에 대해 시각화
# 기본 값으로부터(모델이 셋팅한 베이스라인, shap_explainer.expected_value) 얼마나 각 피처가 출력 값을 밀어냈는지 계싼

# 입력 값
# shap_explainer.expected_value[1]: 모델 출력 값의 기본 값, 기댓값
# shap_values[1][X_test.index.get_loc(sample_idx)]: 테스트 셋 내 내가 지정한, 확인하고 싶은 인덱스에 해당하는 shap value
# X_test.iloc[1,:]: 실제 피처 값

shap.initjs()
shap.force_plot(shap_explainer.expected_value[1],
                shap_values[1][X_test.index.get_loc(sample_idx)],
                X_test.iloc[1,:])

def recommend_top_n(user_id, model, n=5):
    # 아직 유저가 평가하지 않은 애니 가져오기
    user_data = data[data['user_id'] == user_id]
    rated_animes = user_data['anime_id'].unique()
    unrated_animes = item_features[~item_features.index.isin(rated_animes)]

    # 유저 정보 붙여주기
    user_features_df = user_features.loc[user_id]
    unrated_animes = unrated_animes.assign(**user_features_df)

    # 해당 유저 대상으로 모델 예측
    unrated_animes['predicted_rating'] = model.predict_proba(unrated_animes)[:, 1]

    # 상위 N개의 평점 예측
    top_n_animes = unrated_animes.sort_values('predicted_rating', ascending=False).head(n)

    return top_n_animes
# 특정 유저에 대한 상위 n개 예측 생성
user_id = 26
top_n = recommend_top_n(user_id, models['LGBM'], n=5)[['predicted_rating']]

# top n개 예측의 상세 정보
top_n_details = top_n.merge(animes, how='left', left_index=True, right_on='anime_id')

print(f'Top 5 anime recommendations for user {user_id}:')
display(top_n_details)

# 좋은 평점을 준 애니 검색
user_ratings = ratings[ratings['user_id'] == user_id]
well_rated_animes = user_ratings[user_ratings['rating'] >= user_ratings['rating'].mean()]

# 상세 정보 조회
well_rated_details = animes[animes['anime_id'].isin(well_rated_animes['anime_id'])]

# 평점 정보를 포함한 좋은 평점 정보 출력
well_rated_details = well_rated_details.merge(well_rated_animes[['anime_id', 'rating']], on='anime_id', how='inner')

ALS 구현

# ALS class 정의

class ALS:
    # 하이퍼 파라미터 지정
    def __init__(self, factors=10, iterations=20, reg=0.01):
        self.factors = factors
        self.iterations = iterations
        self.reg = reg
    # 모델 적합 -> 평점 행렬 입력
    def fit(self, ratings):
        # 랜덤으로 user 수 * latent factor 형태의 행렬 생성
        self.user_factors = np.random.random((ratings.shape[0], self.factors))
        # 랜덤으로 item 수 * latent factor 형태의 행렬 생성
        self.item_factors = np.random.random((ratings.shape[1], self.factors))
       
        # 사전에 지정한 iteration 수에 걸쳐서, 교차로 als_step 진행
        for _ in range(self.iterations):
            # user_factors 먼저 업데이트
            self.user_factors = self.als_step(ratings, self.user_factors, self.item_factors)
            # 이어서 item_factors 업데이트
            self.item_factors = self.als_step(ratings.T, self.item_factors, self.user_factors)
   
    # 교차로 업데이트하는 스텝 메서드
    def als_step(self, ratings, solve_vecs, fixed_vecs):
        # normal equation - 업데이트 되지 않을 user/item feature의 공분산 matrix
        # feature가 주어진(고정된) 상태에서 최적의 해를 찾아 그 행렬을 새로운 factors로 사용
        # 가령, user_factors가 고정되어 있을 때는 최적의 item_factors를 구하고, 반대도 마찬가지
        A = fixed_vecs.T.dot(fixed_vecs) + np.eye(self.factors) * self.reg
        b = ratings.dot(fixed_vecs)
        A_inv = np.linalg.inv(A)
        solve_vecs = b.dot(A_inv)
        return solve_vecs

    def predict(self):
        pred = self.user_factors.dot(self.item_factors.T)
        return pred
test_ratio = 0.2
train = pivot.copy()
test = np.zeros(pivot.shape)

for user in range(pivot.shape[0]):
    test_interactions = np.random.choice(pivot.values[user, :].nonzero()[0],
                                         size=int(test_ratio*np.sum(pivot.values[user, :])),
                                         replace=False)
    train.values[user, test_interactions] = 0.
    test[user, test_interactions] = pivot.values[user, test_interactions]
train_csr = coo_matrix(train.values)
test_csr = coo_matrix(test)
# train ALS model
als = ALS(factors = n_latent_factors, iterations=100, reg=0.01)
als.fit(train_csr)

# predict
als_pred = als.predict()
predicted_als = (als_pred - als_pred.min()) / (als_pred.max() - als_pred.min())

SVD, NMF 구현

# SVD(using svds from scipy)
u, sigma, vt = svds(train_csr.astype(float), n_latent_factors)
svd_pred = np.dot(u, np.dot(np.diag(sigma), vt))
predicted_svd = (svd_pred - svd_pred.min()) / (svd_pred.max() - svd_pred.min())
# NMF
model = NMF(n_components=n_latent_factors, init='random', random_state=0)

W = model.fit_transform(train_csr)
H = model.components_
nmf_pred = np.dot(W, H)
predicted_nmf = (nmf_pred - nmf_pred.min()) / (nmf_pred.max() - nmf_pred.min())

 

반응형