본문 바로가기
국비 교육/머신러닝, 딥러닝

[머신러닝] 추천 시스템 알고리즘 - 잠재요인 협업 필터링

by 육츠 2024. 8. 1.
Contents 접기

경사하강법을 이용한 행렬 분해

SVD(Singular Value Decomposition)는 NaN 값이 없는 행렬에만 적용할 수 있다.
R 행렬(사용자-영화 평점 행렬)은 평점되지 않은 많은 NaN 값이 있기 때문에 P(사용자-잠재요인 행렬)와 Q(잠재요인-아이템 행렬) 행렬을 일반적인 SVD 방식으로 분해할 수 없다.

 

행렬 분해 로직 함수

R(m x n) = P(m x K) * Q.T(K x n) [ m: user 수, n: 아이템 수, K: 잠재요인 수 ]
R : 사용자-영화 평점 행렬
P : 사용자-잠재요인 행렬 (초기 값은 랜덤 값으로)
Q : 잠재요인-아이템 행렬 (초기 값은 랜덤 값으로)

행렬 분해 함수 매개변수

R: 사용자 - 아이템 평점 행렬 (실제 평점 값을 갖는 행렬)
K: 잠재요인 차원 수 (임의 지정)
step : 반복 학습 횟수
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 = np.random.normal(scale= 1/K, size= (num_users,K))     # randn: 표준정규 분포 내에서 <> noraml : 표준편차 지정 가능
    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 ] # 사용자 개수만큼 <아이템 개수 만큼>
    # 실제로 평점값이 존재하는 위치와 그 값 

    for step in range(steps):
        for i, j, r in non_zeros:
            # 손실함수의 미분식을 가지고 P와 Q를 만듦
            # 실제값돠 예측값의 차이인 오차 계산
            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

L2 규제가 적용된 손실함수의 미분식 P[i,:] = P[i,:] + learning_rate * ( eij * Q[j,:] - r_lambda * P[i,:]) 과 유사하다.

 

경사 하강법 이용한 행렬 분해 실습

전처리

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]])  # 4x5 아이템-평점 행렬

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.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)
# ratings_matrix.values : numpy 다차원 배열의 값을 넣어야함
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):   # ratings_matrix : 정답을 가진 행렬
    user_rating = ratings_matrix.loc[userId]  # Series타입. 인덱스: 영화의 제목, 값: 평점
    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]

unseen_list = get_unseen_movies(ratings_matrix, 9)
recomm_movies = recomm_movie_by_userid(ratings_pred_matrix, 9, unseen_list)
#print(recomm_movies)

recomm_movies = pd.DataFrame(recomm_movies.values, index= recomm_movies.index, columns= ['pred_score'])
recomm_movies

영화 평점 기반의 사용자 - 아이템 행렬 데이터라면 영화가 가지는 장르별 특성 선호도를 가정할 수 있다.

 

"16. (추천) 아이템 기반 최근접 이웃 협업필터링.ipynb" 의 결과와 비교

16. (추천) 아이템 기반 최근접 이웃 협업필터링 / 17. 잠재요인 협업 필터링 결과

영화 평점 기반의 사용자 - 아이템 행렬 데이터라면 영화가 가지는 장르별 특성 선호도를 가정할 수 있다.