국비지원교육/교육일지

11주차 교육일지: 추천시스템

민서타 2023. 11. 25. 13:07

1. 추천시스템

-유저와 아이템의 주변정보와 상호작용 기록을 바탕한 예측으로, 유저의 의사결정을 돕는 AI 서비스

검색시스템과의 차이

  검색시스템 추천시스템
작동방식 Push Pull
쿼리입력 있음 없음
작동시점 사용자 요청 후 사용자 요청 전
의도의 명시성 명시적 암묵적

 

2. 추천시스템 파이프라인:

후보생성: 무수히 많은 전체 상품 중 관련이 있을 것으로 예상되는 일부 후보군을 추려냄
순위매기기: 추려낸 후보군 내 유저와 아이템의 연관성 점수를 정밀하고 복잡한 모델로 선정
재정렬: 순위를 매긴 유저-아이템 쌍 중 유저와 무관한거나 비즈니스 목적에 맞지 않는 아이템 제거

 

3. 연관규칙 분석:

 ★ A를 사면 B도 산다

A를 사면(조건절) B도 산다(결과절)
a priori 알고리즘 이용

규칙 성능 지표:
 지지도: A와 B가 동시에 등장할 확률
 신뢰도: A가 장바구니에 있을 때,  B가 동시에 등장할 확률
 향상도: 둘이 등장할 확률이 독립이라고 할 때보다 얼마나 더 잘 같이 등장하는 지
 레버리지: 이 규칙에서 등장하는 상품들이 얼마나 유의미하게 같이 등장하는 지

규칙 생성 알고리즘
 1)Brute Force(무차별 탐색): 모든 경우의 서루를 무차별적으로 대입해 전부 시도, 일정 threshold를 못 넘는 규칙을 제거 응용
 2)A priori: 빈번하게 등장하는 아이템 셋에 대해서만 고려(Anti-monotone property 활용)

4. 컨텐츠, 협업 필터링:

 ★ 컨텐츠 필터링: 비슷한 유저(같은 item을 구매한)간의 필터 /

  1. 수치형태 표현

  2. 비슷한 정도 표현: 코사인 유사도, 유클리드 디스턴스, 피어슨 상관계수

절차: 1) 피처 추출, 2) 사용자 프로필 정보 생성 3) 유사도 계산 4) 랭킹 

 ★ 협업 필터링: 비슷한 유저(같은 item을 구매한)간의 필터


코드 실습: 

import pandas as pd
from mlxtend.frequent_patterns import apriori, association_rules
# Convert transaction data into a basket format
basket = (online_retail_data[online_retail_data['Country'] == "United Kingdom"]
          .groupby(['InvoiceNo', 'Description'])['Quantity']
          .sum().unstack().reset_index().fillna(0)
          .set_index('InvoiceNo'))

# Convert the quantities into 0/1 (0: not in the basket, 1: in the basket)
def encode_units(x):
    if x <= 0:
        return 0
    if x >= 1:
        return 1

basket_sets = basket.applymap(encode_units)

# Use the Apriori algorithm to find frequent itemsets
frequent_itemsets = apriori(basket_sets, min_support=0.03, use_colnames=True)

# Generate association rules
rules = association_rules(frequent_itemsets, metric="lift", min_threshold=1)
# Filter rules by a minimum lift and confidence
filtered_rules = rules[(rules['lift'] >= 3.0) & (rules['confidence'] >= 0.5)]

# Print the association rules
print(filtered_rules[['antecedents', 'consequents', 'support', 'confidence', 'lift']])
                            antecedents                        consequents
0          (ALARM CLOCK BAKELIKE GREEN)         (ALARM CLOCK BAKELIKE RED)  
1            (ALARM CLOCK BAKELIKE RED)       (ALARM CLOCK BAKELIKE GREEN)  
2     (GREEN REGENCY TEACUP AND SAUCER)   (PINK REGENCY TEACUP AND SAUCER)

5. 추천시스템 평가 지표

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())

 

반응형