아이템 기반 최근접 이웃 협업 필터링 실습¶
최근접 이웃 협업 필터링은 사용자 기반과 아이템 기반으로 분류합니다. 이 중 추천 장확도가 더 뛰어난 아이템 기반의 협업 필터링을 구현해 보겠습니다. 사용자가 영화 평점을 매긴 사용자-영화 평점 행렬 데이터 세트를 다운로드하겠습니다. https://grouplens.org/datasets/movielens/latest/ 에서 내려받을 수 있습니다.
데이터 가공 및 변환¶
import numpy as np
import pandas as pd
movies = pd.read_csv('/content/drive/MyDrive/military/grouplens/movies.csv')
ratings = pd.read_csv('/content/drive/MyDrive/military/grouplens/ratings.csv')
print(movies.shape)
print(ratings.shape)
movies.head(2)
movies는 영화에 대한 메타 정보인 title, genres를 가지고 있습니다.
ratings.head(2)
ratings에는 userId, movieId, rating이 있고 timestamp는 현재로서는 큰 의미가 없는 칼럼입니다. 평점은 0.5점 단위로 5점까지 줄 수 있습니다. 영화는 아이템 기반 필터링에서 아이템을 담당합니다. 현재 데이터는 로우 형태로 돼있으므로, 사용자-영화 데이터 세트로 변경해야합니다.
DataFrame.pivot_table() 함수를 이용하면 사용자-아이템 데이터 세트로 변경하기 쉽습니다. DataFrame.pivot_table('rating', index='userId', columns='movieId')로 입력하면 칼럼은 movieId의 값들로 입력되고, 레코드는 'rating'값이 들어갑니다.
ratings = ratings[['userId', 'movieId', 'rating']]
ratings_matrix = ratings.pivot_table('rating', index='userId', columns='movieId')
ratings_matrix.head(3)
movies와 ratings를 join해서 movieId에 맞는 'title'로 바꾸겠습니다. 그리고 NaN값을 0으로 바꾸겠습니다.
# title 칼럼을 얻기 위해 movies와 조인
rating_movies = pd.merge(ratings, movies, on='movieId')
# columns='title'로 title 칼럼으로 피벗 수행.
ratings_matrix = rating_movies.pivot_table('rating', index='userId', columns='title')
# NaN 값을 모두 0으로 변환
ratings_matrix = ratings_matrix.fillna(0)
ratings_matrix.head(3)
영화 간 유사도 산출¶
코사인 유사도를 이용해 영화 간 유사도를 추정하겠습니다. sklearn의 cosine_similarity() 함수를 적용하면 행을 기준으로 서로 다른 행을 비교해 유사도를 산출합니다. 영화를 기준으로 적용하려면 ratings_matrix의 전치 행렬을 cosine_similarity()에 넣어야 합니다. 이를 위해 DataFrame.transpose() 함수를 이용합니다.
ratings_matrix_T = ratings_matrix.transpose()
ratings_matrix_T.head(3)
from sklearn.metrics.pairwise import cosine_similarity
item_sim = cosine_similarity(ratings_matrix_T, ratings_matrix_T)
# cosine_similarity()로 반환된 넘파이 행렬을 영화명을 매핑해 DataFrame으로 변환
item_sim_df = pd.DataFrame(data=item_sim, index=ratings_matrix.columns,\
columns=ratings_matrix.columns)
print(item_sim_df.shape)
item_sim_df.head(3)
item_sim_df를 이용해 'Godfather, The (1972)'와 유사도가 높은 상위 6개 영화를 추출해보겠습니다.
item_sim_df["Godfather, The (1972)"].sort_values(ascending=False)[:6]
대부와 완전히 장르가 다른 영화도 포함돼있습니다. 이번엔 'Inception (2010)"과 유사도가 높은 순으로 자신을 제외한 상위 5개 영화를 추출하겠습니다.
item_sim_df["Inception (2010)"].sort_values(ascending=False)[1:6]
'다크 나이트'가 가장 유사도가 높고, 나머지는 스릴러와 액션이 가미된 영화가 높은 유사도를 나타내고 있습니다. 아이템 기반 유사도 데이터는 사용자의 평점 정보를 모두 취합해 영화에 따라 유사한 다른 영화를 추천할 수 있게 추천해줍니다. 이번엔 개인에게 특화된(Personalized) 영화 추천 알고리즘을 만들어 보겠습니다.
아이템 기반 최근접 이웃 협업 필터링으로 개인화된 영화 추천¶
아이템 기반 영화 유사도 데이터는 모든 사용자의 평점을 기준으로 영화의 유사도를 생성했습니다. 개인화된 영화 추천은 아직 개인이 관람하지 않은 영화에 대해 기존에 관람한 영화의 평점 데이터를 기반으로 모든 영화의 예측 평점을 계산해 높은 순으로 추천하는 방식입니다.
아이템 기반의 협업 필터링에서 개인화된 예측 평점식은 아래와 같습니다.
$$\hat{R}_{u, i} = \frac{\sum_{}^{N}(S_{i, N}*R_{u, N})}{\sum_{}^{N}(\left | S_{i, N} \right |)}$$
위 변수들은 rating_matrix, item_sim_df를 numpy 행렬로 변환해서 구할 수 있습니다. 위 식을 구현하는 함수 predict_rating()을 만들어 보겠습니다.
def predict_rating(ratings_arr, item_sim_arr):
ratings_pred = ratings_arr.dot(item_sim_arr)/ np.array([np.abs(item_sim_arr).sum(axis=1)])
return ratings_pred
ratings_pred = predict_rating(ratings_matrix.values, item_sim_df.values)
ratings_pred_matrix = pd.DataFrame(data=ratings_pred, index=ratings_matrix.index, \
columns = ratings_matrix.columns)
ratings_pred_matrix.head(3)
실제 영화 평점이 0인 부분의 대다수가 예측값으로 채워졌습니다. 이는 R[u, N]과 S[i, N]의 모든 요소들이 내적되어 더해진 값이 보통 0이 아니기 때문입니다.
이 예측 결과와 실제 평점에 얼마나 차이가 있는지 MSE를 측정하겠습니다. 실제 데이터에서 0인 부분은 계산에서 제외하겠습니다.
from sklearn.metrics import mean_squared_error
# 사용자가 평점을 부여한 영화에 대해서만 예측 성능 평가 MSE를 구함.
def get_mse(pred, actual):
# 평점이 있는 실제 영화만 추출
pred = pred[actual.nonzero()].flatten()
actual = actual[actual.nonzero()].flatten()
return mean_squared_error(pred, actual)
print('아이템 기반 모든 최근접 이웃 MSE:', get_mse(ratings_pred, ratings_matrix.values))
실제값과 예측값은 스케일이 달라서 MSE는 감소시키는 방향으로 개선해야한다고 생각하면 좋습니다.
예측 평점을 계산하는데 개별 영화와 모든 영화 간의 유사도 벡터를 이용하는 것이 아니라, 개별 영화와 가장 비슷한 유사도를 가지는 영화의 유사도 벡터만 예측값을 계산하는 데 적용합니다. 단점은 개별 예측값을 구하기 위해 행, 열 별로 for 루프를 반복하면서 Top-N 유사도 벡터를 구한다는 점입니다. 이는 데이터의 크기가 커지면 매우 오래 걸리는 로직입니다.
def predict_rating_topsim(ratings_arr, item_sim_arr, n=20):
# 사용자-아이템 평점 행렬 크기만큼 0으로 채운 예측 행렬 초기화
pred = np.zeros(ratings_arr.shape)
# 사용자-아이템 평점 행렬의 열 크기만큼 루프 수행.
for col in range(ratings_arr.shape[1]):
# 유사도 행렬에서 유사도가 큰 순으로 n개 데이터 행렬의 인덱스 반환
top_n_items = [np.argsort(item_sim_arr[:, col])[:-n-1:-1]]
# 개인화된 예측 평점을 계산
for row in range(ratings_arr.shape[0]):
pred[row, col] = item_sim_arr[col, :][top_n_items].dot(ratings_arr[row, :][top_n_items].T)
pred[row, col] /= np.sum(np.abs(item_sim_arr[col, :][top_n_items]))
return pred
predict_rating_topsim() 함수를 이용해 예측 평점을 계산하고, 실제 평점과의 MSE를 구해보겠습니다.
ratings_pred = predict_rating_topsim(ratings_matrix.values, item_sim_df.values, n=20)
print('아이템 기반 최근접 Top-20 이웃 MSE: ', get_mse(ratings_pred, ratings_matrix.values))
# 계산된 예측 평점 데이터는 DataFrame으로 재생성
ratings_pred_matrix = pd.DataFrame(data=ratings_pred, index=ratings_matrix.index, \
columns=ratings_matrix.columns)
MSE가 3.69로 기존 9.89보다 많이 향상됐습니다. userId=9인 사용자에 대해 영화를 추천해보겠습니다. 먼저 9번 사용자가 어떤 영화를 좋아하는지 확인해보겠습니다.
user_rating_id = ratings_matrix.loc[9, :]
user_rating_id[user_rating_id > 0].sort_values(ascending=False)[:10]
'오스틴 파워', '반지의 제왕'등 대작 영화, 어드벤처, 코미디 영화 등 흥행성이 좋은 영화에 좋은 평점을 주고 있습니다. 사용자가 이미 평점을 준 영화를 제외하고 추천할 수 있도록 평점을 주지 않은 영화를 리스트 객체로 반환하는 함수인 get_unseen_movies()를 생성합니다.
def get_unseen_movies(ratings_matrix, userId):
# userId로 입력받은 사용자의 모든 영화 정보를 추출해 Series로 반환함.
# 반환된 user_rating은 영화명(title)을 인덱스로 가지는 Series 객체임.
user_rating = ratings_matrix.loc[userId, :]
# user_rating이 0보다 크면 기존에 관람한 영화임. 대상 인덱스를 추출해 list 객체로 만듦.
already_seen = user_rating[user_rating > 0].index.tolist()
# 모든 영화명을 list 객체로 만듦.
movies_list = ratings_matrix.columns.tolist()
# list comprehension으로 already_seen에 해당하는 영화는 movie_list에서 제외함.
unseen_list = [movie for movie in movies_list if movie not in already_seen]
return unseen_list
사용자가 평점을 주지 않은 영화 리스트와 predict_rating_topsim()을 이용해 사용자에게 영화를 추천하는 recomm_movie_by_userid()를 만들겠습니다.
def recomm_movie_by_userid(pred_df, userId, unseen_list, top_n=10):
# 예측 평점 DataFrame에서 사용자 id인덱스와 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
'슈렉', '스파이더맨', '인디아나 존스' 등 다양하지만 높은 흥행성을 가진 작품이 추천됐습니다.
궁금한 점¶
- DataFrame.pivot_table()
- pd.merge()
- pred[actual.nonzero()].flatten()
- Item 기반에서 유사도를 구할 때 cosine_similarity는 행 벡터들의 값(평점)들이 유사한 것을 통해 cosine 값을 추정한다. 같은 사람이 각각 영화에 준 평점이 비슷할 수록 유사도값이 유사해지는 것 같다.
np.argsort(arr)[s:e:step] # s에서 e까지 step 만큼 더한 인덱스의 ndarray 반환
A /= B # same as A=A/B
출처: 파이썬 머신러닝 완벽가이드(권철민)
사진 출처:
'파이썬 머신 러닝 완벽 가이드' 카테고리의 다른 글
행렬 분해를 이용한 잠재 요인 협업 필터링 실습 (0) | 2021.12.15 |
---|---|
콘텐츠 기반 필터링 실습 - TMDB 5000 영화 데이터 세트 (0) | 2021.12.13 |
추천 시스템 (0) | 2021.12.11 |
텍스트 분석 실습 - 캐글 Mercari Price Suggestion Challenge (0) | 2021.12.10 |
텍스트 분류 실습 - 20 뉴스그룹 분류 (0) | 2021.12.05 |