-
[추천] Latent Collaborative Filtering머신러닝 & 딥러닝 2021. 12. 16. 18:22
잠재 요인 협업 필터링의 개요
- 사용자-아이템 평점 행렬 속에 숨어 있는 잠재 요인을 추출해 추천 예측을 할 수 있게 하는 기법. 대규모 다차원 행렬을 SVD와 같은 행렬 분해 (Matrix Factorization) 기법으로 분해하는 과정에서 잠재 요인을 추출하는데, 이 잠재 요인을 기반으로 사용자-아이템 평점 행렬을 재구성 하면서 추천을 구현.
- 잠재 요인 협업 필터링의 행렬 분해 목표는 희소 행렬 형태의 사용자-아이템 평점 행렬을 밀집(Dense) 행렬 형태의 사용자-잠재 요인 행렬과 잠재 요인-아이템 행렬로 분해한 뒤 이를 재결합 하여 밀집 행렬 형태의 사용자-아이템 평점 행렬을 생성하여 사용자에게 새로운 아이템을 추천하는 것.
사용자-아이템 평점 행렬 분해 이슈
- SVD는 Missing value가 없는 행렬에 적용 가능. P, Q 행렬을 일반적인 SVD 방식으로는 분해 할 수 없다. P와 q를 모르는데 어떻게 R을 예측하는가? 경사 하강법을 이용하여 P와 Q에 기반한 예측 R값이 실제 R값과 가장 최소의 오류를 가질 수 있도록 비용함수 최적화를 통해 P, Q를 최적화 유추한다.
경사 하강법 기반의 행렬 분해
- P와 Q를 임의의 값을 가진 행렬로 설정.
- p와 Q.T값을 곱해 예측 R 행렬을 계산하고 예측 R행렬과 실제 R 행렬에 해당하는 오류 값을 계산.
- 이 오류 값을 최소화할 수 있도록 P와 Q 행렬을 적절한 값으로 각각 업데이트.
- 만족할 만한 오류 값을 가질 때까지 2, 3번 작업을 반복하면서 P와 Q값을 업데이트해 근사화.
원본 행렬 R 및 R을 분해할 P와 Q를 임의의 정규분포를 가진 랜덤값으로 초기화
import numpy as np # 원본 행렬 R 생성, 분해 행렬 P와 Q 초기화, 잠재요인 차원 K는 3 설정. R = np.array([[4, np.NaN, np.NaN, 2, np.NaN ], [np.NaN, 5, np.NaN, 3, 1 ], [np.NaN, np.NaN, 3, 4, 4 ], [5, 2, 1, 2, np.NaN ]]) num_users, num_items = R.shape K=3 # P와 Q 매트릭스의 크기를 지정하고 정규분포를 가진 random한 값으로 입력합니다. np.random.seed(1) P = np.random.normal(scale=1./K, size=(num_users, K)) Q = np.random.normal(scale=1./K, size=(num_items, K)) print("P:",P) print("Q:",Q)
P: [[ 0.54144845 -0.2039188 -0.17605725] [-0.35765621 0.28846921 -0.76717957] [ 0.58160392 -0.25373563 0.10634637] [-0.08312346 0.48736931 -0.68671357]] Q: [[-0.1074724 -0.12801812 0.37792315] [-0.36663042 -0.05747607 -0.29261947] [ 0.01407125 0.19427174 -0.36687306] [ 0.38157457 0.30053024 0.16749811] [ 0.30028532 -0.22790929 -0.04096341]]
비용계산 함수를 생성. 분해된 행렬 P와 Q.T를 내적하여 예측 행렬 생성하고
실제 행렬에서 널이 아닌 값의 위치에 있는 값만 예측 행렬의 값과 비교하여 RMSE값을 계산하고 반환
from sklearn.metrics import mean_squared_error def get_rmse(R, P, Q, non_zeros): error = 0 # 두개의 분해된 행렬 P와 Q.T의 내적으로 예측 R 행렬 생성 full_pred_matrix = np.dot(P, Q.T) # 실제 R 행렬에서 널이 아닌 값의 위치 인덱스 추출하여 실제 R 행렬과 예측 행렬의 RMSE 추출 x_non_zero_ind = [non_zero[0] for non_zero in non_zeros] y_non_zero_ind = [non_zero[1] for non_zero in non_zeros] R_non_zeros = R[x_non_zero_ind, y_non_zero_ind] full_pred_matrix_non_zeros = full_pred_matrix[x_non_zero_ind, y_non_zero_ind] mse = mean_squared_error(R_non_zeros, full_pred_matrix_non_zeros) rmse = np.sqrt(mse) return rmse
경사하강법에 기반하여 P와 Q의 원소들을 업데이트 수행
# R > 0 인 행 위치, 열 위치, 값을 non_zeros 리스트에 저장. non_zeros = [ (i, j, R[i,j]) for i in range(num_users) for j in range(num_items) if R[i,j] > 0 ] steps=1000 learning_rate=0.01 r_lambda=0.01 # SGD 기법으로 P와 Q 매트릭스를 계속 업데이트. for step in range(steps): for i, j, r in non_zeros: # 실제 값과 예측 값의 차이인 오류 값 구함 eij = r - np.dot(P[i, :], Q[j, :].T) # Regularization을 반영한 SGD 업데이트 공식 적용 P[i,:] = P[i,:] + learning_rate*(eij * Q[j, :] - r_lambda*P[i,:]) Q[j,:] = Q[j,:] + learning_rate*(eij * P[i, :] - r_lambda*Q[j,:]) rmse = get_rmse(R, P, Q, non_zeros) if (step % 50) == 0 : print("### iteration step : ", step," rmse : ", rmse)
### iteration step : 0 rmse : 3.2388050277987723 ### iteration step : 50 rmse : 0.4876723101369647 ### iteration step : 100 rmse : 0.15643403848192475 ### iteration step : 150 rmse : 0.07455141311978038 ### iteration step : 200 rmse : 0.04325226798579314 ### iteration step : 250 rmse : 0.029248328780878977 ### iteration step : 300 rmse : 0.022621116143829396 ### iteration step : 350 rmse : 0.01949363619652524 ### iteration step : 400 rmse : 0.018022719092132586 ### iteration step : 450 rmse : 0.017319685953442663 ### iteration step : 500 rmse : 0.016973657887570895 ### iteration step : 550 rmse : 0.016796804595895595 ### iteration step : 600 rmse : 0.016701322901884613 ### iteration step : 650 rmse : 0.01664473691247672 ### iteration step : 700 rmse : 0.016605910068210078 ### iteration step : 750 rmse : 0.016574200475704973 ### iteration step : 800 rmse : 0.01654431582921599 ### iteration step : 850 rmse : 0.016513751774735196 ### iteration step : 900 rmse : 0.016481465738194947 ### iteration step : 950 rmse : 0.016447171683479155
pred_matrix = np.dot(P, Q.T) print('예측 행렬:\n', np.round(pred_matrix, 3))
예측 행렬: [[3.991 0.897 1.306 2.002 1.663] [6.696 4.978 0.979 2.981 1.003] [6.677 0.391 2.987 3.977 3.986] [4.968 2.005 1.006 2.017 1.14 ]]
R = np.array([[4, np.NaN, np.NaN, 2, np.NaN ], [np.NaN, 5, np.NaN, 3, 1 ], [np.NaN, np.NaN, 3, 4, 4 ], [5, 2, 1, 2, np.NaN ]])
행렬 분해 기반의 잠재 요인 협업 필터링 실습
경사하강법 기반의 행렬 분해 함수 생성
def matrix_factorization(R, K, steps=200, learning_rate=0.01, r_lambda = 0.01): num_users, num_items = R.shape # P와 Q 매트릭스의 크기를 지정하고 정규분포를 가진 랜덤한 값으로 입력합니다. np.random.seed(1) P = np.random.normal(scale=1./K, size=(num_users, K)) Q = np.random.normal(scale=1./K, size=(num_items, K)) break_count = 0 # R > 0 인 행 위치, 열 위치, 값을 non_zeros 리스트 객체에 저장. non_zeros = [ (i, j, R[i,j]) for i in range(num_users) for j in range(num_items) if R[i,j] > 0 ] # SGD기법으로 P와 Q 매트릭스를 계속 업데이트. for step in range(steps): for i, j, r in non_zeros: # 실제 값과 예측 값의 차이인 오류 값 구함 eij = r - np.dot(P[i, :], Q[j, :].T) # Regularization을 반영한 SGD 업데이트 공식 적용 P[i,:] = P[i,:] + learning_rate*(eij * Q[j, :] - r_lambda*P[i,:]) Q[j,:] = Q[j,:] + learning_rate*(eij * P[i, :] - r_lambda*Q[j,:]) rmse = get_rmse(R, P, Q, non_zeros) if (step % 10) == 0 : print("### iteration step : ", step," rmse : ", rmse) return P, Q
import pandas as pd import numpy as np movies = pd.read_csv('./ml-latest-small/movies.csv') ratings = pd.read_csv('./ml-latest-small/ratings.csv') ratings = ratings[['userId', 'movieId', 'rating']] ratings_matrix = ratings.pivot_table('rating', index='userId', columns='movieId') # title 컬럼을 얻기 이해 movies 와 조인 수행 rating_movies = pd.merge(ratings, movies, on='movieId') # columns='title' 로 title 컬럼으로 pivot 수행. ratings_matrix = rating_movies.pivot_table('rating', index='userId', columns='title')
P, Q = matrix_factorization(ratings_matrix.values, K=50, steps=200, learning_rate=0.01, r_lambda = 0.01) pred_matrix = np.dot(P, Q.T)
### iteration step : 0 rmse : 2.9023619751336867 ### iteration step : 10 rmse : 0.7335768591017927 ### iteration step : 20 rmse : 0.5115539026853442 ### iteration step : 30 rmse : 0.37261628282537446 ### iteration step : 40 rmse : 0.2960818299181014 ### iteration step : 50 rmse : 0.2520353192341642 ### iteration step : 60 rmse : 0.2248750327526985 ### iteration step : 70 rmse : 0.20685455302331537 ### iteration step : 80 rmse : 0.19413418783028683 ### iteration step : 90 rmse : 0.184700820027204 ### iteration step : 100 rmse : 0.17742927527209104 ### iteration step : 110 rmse : 0.1716522696470749 ### iteration step : 120 rmse : 0.1669518194687172 ### iteration step : 130 rmse : 0.1630529219199754 ### iteration step : 140 rmse : 0.1597669192967964 ### iteration step : 150 rmse : 0.1569598699945732 ### iteration step : 160 rmse : 0.15453398186715425 ### iteration step : 170 rmse : 0.15241618551077643 ### iteration step : 180 rmse : 0.15055080739628307 ### iteration step : 190 rmse : 0.1488947091323209
ratings_pred_matrix = pd.DataFrame(data=pred_matrix, index= ratings_matrix.index, columns = ratings_matrix.columns) ratings_pred_matrix.head(3)
title '71 (2014) 'Hellboy': The Seeds of Creation (2004) 'Round Midnight (1986) 'Salem's Lot (2004) 'Til There Was You (1997) 'Tis the Season for Love (2015) 'burbs, The (1989) 'night Mother (1986) (500) Days of Summer (2009) *batteries not included (1987) ... Zulu (2013) [REC] (2007) [REC]² (2009) [REC]³ 3 Génesis (2012) anohana: The Flower We Saw That Day - The Movie (2013) eXistenZ (1999) xXx (2002) xXx: State of the Union (2005) ¡Three Amigos! (1986) À nous la liberté (Freedom for Us) (1931) userId 1 3.055084 4.092018 3.564130 4.502167 3.981215 1.271694 3.603274 2.333266 5.091749 3.972454 ... 1.402608 4.208382 3.705957 2.720514 2.787331 3.475076 3.253458 2.161087 4.010495 0.859474 2 3.170119 3.657992 3.308707 4.166521 4.311890 1.275469 4.237972 1.900366 3.392859 3.647421 ... 0.973811 3.528264 3.361532 2.672535 2.404456 4.232789 2.911602 1.634576 4.135735 0.725684 3 2.307073 1.658853 1.443538 2.208859 2.229486 0.780760 1.997043 0.924908 2.970700 2.551446 ... 0.520354 1.709494 2.281596 1.782833 1.635173 1.323276 2.887580 1.042618 2.293890 0.396941 3 rows × 9719 columns
def get_unseen_movies(ratings_matrix, userId): # userId로 입력받은 사용자의 모든 영화정보 추출하여 Series로 반환함. # 반환된 user_rating 은 영화명(title)을 index로 가지는 Series 객체임. user_rating = ratings_matrix.loc[userId,:] # user_rating이 0보다 크면 기존에 관람한 영화임. 대상 index를 추출하여 list 객체로 만듬 already_seen = user_rating[ user_rating > 0].index.tolist() # 모든 영화명을 list 객체로 만듬. movies_list = ratings_matrix.columns.tolist() # list comprehension으로 already_seen에 해당하는 movie는 movies_list에서 제외함. unseen_list = [ movie for movie in movies_list if movie not in already_seen] return unseen_list
def recomm_movie_by_userid(pred_df, userId, unseen_list, top_n=10): # 예측 평점 DataFrame에서 사용자id index와 unseen_list로 들어온 영화명 컬럼을 추출하여 # 가장 예측 평점이 높은 순으로 정렬함. recomm_movies = pred_df.loc[userId, unseen_list].sort_values(ascending=False)[:top_n] return recomm_movies
# 사용자가 관람하지 않는 영화명 추출 unseen_list = get_unseen_movies(ratings_matrix, 9) # 잠재요인 기반 협업 필터링으로 영화 추천 recomm_movies = recomm_movie_by_userid(ratings_pred_matrix, 9, unseen_list, top_n=10) # 평점 데이타를 DataFrame으로 생성. recomm_movies = pd.DataFrame(data=recomm_movies.values,index=recomm_movies.index,columns=['pred_score']) recomm_movies
pred_score title Rear Window (1954) 5.704612 South Park: Bigger, Longer and Uncut (1999) 5.451100 Rounders (1998) 5.298393 Blade Runner (1982) 5.244951 Roger & Me (1989) 5.191962 Gattaca (1997) 5.183179 Ben-Hur (1959) 5.130463 Rosencrantz and Guildenstern Are Dead (1990) 5.087375 Big Lebowski, The (1998) 5.038690 Star Wars: Episode V - The Empire Strikes Back (1980) 4.989601 '머신러닝 & 딥러닝' 카테고리의 다른 글
[신경망] IMDB 영화 리뷰를 긍정/부정 이진 분류하기 (0) 2021.12.22 [추천] Surprise Package (0) 2021.12.16 [추천] Contents based filtering (0) 2021.12.14 [텍스트] 캐글 - Mercari Price Suggestion (0) 2021.12.06 [텍스트] KoNLPy 맥 M1 설치하기 (0) 2021.12.05