데이터 분석/머신러닝
[ML] 16. 잠재요인 협업 필터링
eunnys
2023. 11. 28. 16:23
▶ 잠재요인 협업 필터링을 통한 영화 추천
## 경사하강법을 이용한 행렬 문제 ##
- SVD(Singular Value Decomposition)는 NaN 값이 없는 행렬에만 적용할 수 있다.
- R 행렬(사용자-영화 평점 행렬)은 평점되지 않은 많은 NaN 값이 있기 때문에 P(사용자-잠재요인 행렬)와 Q(잠재요인-아이템 행렬) 행렬을 일반적인 SVD 방식으로 분해할 수 없다.
## 행렬 분해 로직 함수 ##
- R(m x n) = P(m x K) * Q.T(K x m)
- R : 사용자-영화 평점 행렬
- P : 사용자-잠재요인 행렬 (초기값은 랜덤값)
- Q : 잠재요인-아이템 행렬 (초기값은 랜덥값)
- m : 유저 수
- n : 아이템 수
- K : 잠재요인 수 (임의의 값)
- 행렬 분해 함수의 매개변수
- R : 사용자-아이템 평점 행렬 (실제 평점 값을 갖는 행렬)
- K : 잠재요인의 차원 수 (임의 지정)
- steps : 반복 학습의 횟수
- learning_rate : 학습률
- r_lambda : L2규제 강도
import numpy as np
def matrix_factorization(R, K, steps=200, learning_rate=0.01, r_lambda=0.01):
num_users, num_items = R.shape
np.random.seed(1)
# P와 Q 행렬을 임의의 값으로 채움
P = np.random.normal(scale=1/K, size=(num_users, K)) # scale : 표준편차 지정
Q = np.random.normal(scale=1/K, size=(num_items, K))
# R > 0인 행 위치, 열 위치, 값(평점)을 리스트에 저장 (평점이 있는 위치 정보 및 평점 정보 저장)
non_zeros = [(i, j, R[i,j]) for i in range(num_users) for j in range(num_items) if R[i,j] > 0]
# 출력 형식 : [(0,0,4.5), (1,3,3.0) ...]
for step in range(steps):
for i, j, r in non_zeros:
# 실제값과 예측값의 차이인 오차 계산
eij = r - np.dot(P[i,:], Q[j,:].T)
# L2 규제가 적용된 손실함수(비용함수)의 미분식
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,:])
return P, Q
## 경사하강법을 이용한 행렬분해 실습 ##
np.set_printoptions(precision=2, suppress=True)
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]])
# P와 Q에 대한 예측 값
P, Q = matrix_factorization(R, K=5, steps=1000, learning_rate=0.01, r_lambda=0.01)
pred_matrix = np.dot(P, Q.T)
print(R)
print()
print(pred_matrix)
[[ 4. nan nan 2. nan]
[nan 5. nan 3. 1.]
[nan nan 3. 4. 4.]
[ 5. 2. 1. 2. nan]]
[[3.99 1.51 1.15 2. 1.2 ]
[4.85 4.98 1.29 2.99 1. ]
[3.05 1.41 2.99 3.98 3.98]
[4.97 2. 1. 2. 0.82]]
## 데이터 로딩 ##
import numpy as np
import pandas as pd
movies = pd.read_csv('./Dataset/movies.csv')
ratings = pd.read_csv('./Dataset/ratings.csv')
ratings = ratings.drop('timestamp', axis=1)
rating_movies = pd.merge(ratings, movies, on='movieId')
ratings_matrix = rating_movies.pivot_table(index='userId', columns='title', values='rating')
ratings_matrix.fillna(0, inplace=True)
## 예측 평점 행렬 생성 ##
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)
ratings_pred_matrix = pd.DataFrame(pred_matrix, index=ratings_matrix.index, columns=ratings_matrix.columns)
ratings_pred_matrix.head(3)
## 평점을 주지 않은 영화 리스트 반환 ##
def get_unseen_movies(ratings_matrix, userId):
# 9719개의 영화에 대한 평점 정보가 넘어옴 (시리즈 형태 : 인덱스는 영화의 제목, 값은 평점)
user_rating = ratings_matrix.loc[userId]
# 관람하지 않은 영화의 목록만 넘파이의 다차원 배열로 반환
unseen_list = user_rating[user_rating == 0].index.values
return unseen_list
## 특정 시용자의 관람하지 않은 영화에 대한 예측 평점 기반 추천 ##
def recomm_movie_by_userid(pred_df, userId, unseen_list, top_n=10):
return pred_df.loc[userId, unseen_list].sort_values(ascending=False)[:top_n]
# 9번째 유저의 관람하지 않은 영화 중 예측 평점이 높은 상위 10개 영화 목록과 예측 평점
unseen_list = get_unseen_movies(ratings_matrix, 9)
recomm_movies = recomm_movie_by_userid(ratings_pred_matrix, 9, unseen_list)
recomm_movies = pd.DataFrame(recomm_movies.values, index=recomm_movies.index, columns=['pred_score'])
recomm_movies