Patrick's 데이터 세상

[추천 시스템 - 아이템 기반 최근접 이웃 협업 필터링 추천] Book-Crossing: 사용자 리뷰 평점 데이터 세트 본문

Machine Learning/Recommend system

[추천 시스템 - 아이템 기반 최근접 이웃 협업 필터링 추천] Book-Crossing: 사용자 리뷰 평점 데이터 세트

patrick610 2020. 9. 11. 23:11
반응형
SMALL

 

 

 

본 포스팅은 캐글(Kaggle)에서 제공하는 'Book-Crossing 사용자 리뷰 평점 데이터 세트'를 활용하여 콘텐츠 기반 필터링을 실습하기 위한 목적으로 작성하였습니다.


Git url

https://github.com/hipster4020/RecommendationSystem/blob/master/ItemNearestNeighborCollaborativeFiltering_Books.ipynb

 

hipster4020/RecommendationSystem

Contribute to hipster4020/RecommendationSystem development by creating an account on GitHub.

github.com

 

 

분석 도구 : Anaconda Jupyter Lab

 

활용 데이터 : 약 271,379 권의 도서에 대해 1,149,780 등급을 제공하는 278,858 명의 사용자를 포함하는 메타 데이터.

https://www.kaggle.com/ruchi798/bookcrossing-dataset

 

Book-Crossing: User review ratings

A collection of book ratings

www.kaggle.com

 


 

최근접 이웃 협업 필터링사용자 기반아이템 기반으로 분류하는데, 이 중 일반적으로 추천 정확도가 더 뛰어난 아이템 기반의 협업 필터링을 구현해 보겠습니다.

유저가 평점을 매긴 사용자-매체 평점 행렬 데이트 세트로 협업 필터링 기반의 추천을 할 수 있습니다.

 

매우 유명한 아이템은 취향과 관계없이 대부분의 사람이 보고 평가하는 경우가 많습니다.
하지만 대부분의 경우에는 사용자들이 도서(또는 상품)에 대해 평가를 매기지 않은 경우가 일반적이라 다른 사람과의 유사도를 비교하기가 어렵습니다.
따라서 최근접 이웃 협업 필터링은 대부분 아이템 기반의 알고리즘을 적용합니다.

 

 

 

아이템 기반 최근접 이웃 기반 협업 필터링

      아이템이 가지는 속성과는 상관없이 사용자들의 선호도에 대해 평가합니다.

      척도가 유사한 아이템을 추천하는 기준이 되는 알고리즘.

 

위 표에서 제인 오스틴의 고전작 '오만과 편견'과 에쿠니 가오리의 '냉정과 열정 사이(ROSSO)'의 독자들의 평점이 유사합니다.

아이템(도서) '오만과 편견'은 '개미'보다 '냉정과 열정 사이(ROSSO)'와 사용자들의 평점 분포가 훨씬 더 비슷하므로 '오만과 편견'과 '냉정과 열정 사이(ROSSO)'는 아이템 유사도가 상대적으로 매우 높기 때문에 사용자 D에게 '냉정과 열정 사이(ROSSO)'를 추천합니다.

 

 

 

데이터 가공 및 변환

# Library Load
import pandas as pd
import numpy as np

from sklearn.metrics.pairwise import cosine_similarity
from sklearn.metrics import mean_squared_error
path = "/content/sample_data/"

bxbooks = pd.read_csv(path+"BX-Books.csv")
bxbookratings = pd.read_csv(path+"BX-Book-Ratings.csv")

print(bxbooks.shape)
print(bxbookratings.shape)

결과

 

 

먼저 null 값이 있는지를 체크합니다.

# null 체크
bxbookratings.isnull().sum()

결과

데이터 분석 전 null 값을 체크합니다.
User-Id, ISBN, Book-Rating 등 사용할 컬럼에 null 값이 없는 것을 확인합니다.

 

 

bxbookratings.head(5)

결과

bxbooks 데이터 세트는 271,379건 레코드를 가지고 있고, bxbookratings는 1백만 건으로 데이터 양이 많아 처리에 다소 어려움이 있어 100,000건으로 데이터를 줄여 처리하겠습니다.

BX-Book-Ratings.csv 파일로 생성한 bxbookratings 데이터 세트는 사용자별로 도서에 대한 평점을 매긴 데이터 세트입니다.

사용자 아이디를 의미하는 User-ID, 국제표준도서번호 고유 값을 의미하는 ISBN, 평점인 Book-Rating 컬럼으로 이루어져 있습니다.

 

협업 필터링은 사용자와 아이템 간의 평점(또는 다른 유형의 액션)에 기반해 추천하는 시스템입니다.
먼저 DataFrame의 pivot_table() 함수를 이용하여 로우(행) 레벨 형태의 원본 데이터 세트를 모든 사용자를 로우로, 모든 도서를 칼럼으로 구성한 데이터 세트로 변경해야 합니다. 

데이터 건수가 너무 많아 pivot_table()처리에 문제가 있어 50,000으로 줄이겠습니다.

 

# 데이터 사이즈 조정
bxbookratings=bxbookratings[:50000]
print(bxbookratings.shape)

결과

 

 

 

도서 간 유사도 산출

 

bxbookratings = bxbookratings[['User-ID', 'ISBN',	'Book-Rating']]
bxbookratings_matrix = bxbookratings.pivot_table('Book-Rating', index='User-ID', columns='ISBN')

결과

거의 모든 데이터가 NaN으로 보이는 이유는 사용자가 평점을 매기지 않은 도서가 컬럼으로 변환되면서 NaN으로 할당되었기 때문입니다.

NaN 데이터를 처리하기 전에, ISBN 값을 각각 해당하는 bxbooks 데이터 세트에 있는 도서명으로 변경하겠습니다.
bxbookratings와 bxbooks를 조인해 Book-Title 컬럼을 가져고 다시 피벗(pivot)한 후에 NaN은 0으로 변환합니다.

 

# title 컬럼을 얻기 위해 movies와 조인
rating_books = pd.merge(bxbookratings, bxbooks, on='ISBN')

# columns='title'로 title 컬럼으로 피벗 수행.
ratings_matrix = rating_books.pivot_table('Book-Rating', index='User-ID', columns='Book-Title')

# NaN 값을 모두 0으로 변환
ratings_matrix = ratings_matrix.fillna(0)
ratings_matrix.head(3)

결과

변환된 사용자-아이템 테이블을 사이킷런의 cosine_similarity()를 이용하여 코사인 유사도를 기반으로 측정합니다.
cosine_similarity()는 함수는 아이템 행을 기준으로 서로 다른 행을 비교해 유사도를 산출합니다.

 

아이템을 기준으로 cosine_similarity()를 적용하려면 행 기준 : 아이템, 열 기준 : 사용자가 되어야 하는데, 현재 생성한 ratings_matrix 테이블은 사용자-아이템 기반이므로 행과 열을 변경해야 합니다.

전치 행렬 변경을 위해 pandas의 transpose() 함수를 이용하여 ratings_matrix 데이터의 행과 열의 위치를 변경합니다.

 

ratings_matrix_T = ratings_matrix.transpose()
ratings_matrix_T.head(3)

결과

생성한 전치 행렬 데이터 세트를 토대로 코사인 유사도를 측정하겠습니다.

 

item_sim = cosine_similarity(ratings_matrix_T, ratings_matrix_T)

# cosine_similarity()로 반환된 Numpy 행렬을 도서명으로 매핑해 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)

결과

cosine_similarity()를 적용한 결과 49,001 x 49,001 Shape으로 도서의 유사도 행렬인 item_sim이 생성되었습니다.
'오만과 편견'을 기준으로 유사도가 높은 상위 6건 추출해보겠습니다.

item_sim_df["Pride and Prejudice"].sort_values(ascending=False)[:6]

결과

기준 도서인 '오만과 편견'을 제외하면 'Daddies: All About the Work They Do (Little Golden Books)'라는 그림 동화책이 가장 유사도가 높고 그다음으로는 'Lion in the Valley'라는 서스펜스 소설이 유사도가 높게 나왔습니다.

item_sim_df["Romeo and Juliet"].sort_values(ascending=False)[1:6]

결과

다른 도서인 셰익스피어의 '로미오와 줄리엣'을 기준으로 확인해보면 1순위가 'She Walks These Hills'라는 문학 소설이 가장 굉장히 높은 유사도로 나왔고 다음 2순위는 처세술에 관련된 자기 계발서로 실제적으로는 유사한 도서가 나오지 않았습니다.

좀 더 개인에게 맞춰진 도서 추천 알고리즘을 만들어 보겠습니다.

 

 

개인화된 도서 추천

 

개인이 아직 읽지 않은 도서에 대해 아이템 유사도와 기존에 읽은 도서의 평점 데이터를 가지고 다른 모든 도서의 예측 평점을 계산한 후 높은 예측 평점을 가진 도서를 추천하는 방식입니다.

 

위 예측 평점 공식을 토대로 평가를 내린 예측 평점을 먼저 구해보겠습니다.

# 예측 평점 계산식 함수 구현
def predict_rating(ratings_arr, item_sim_arr):

    # dot : 내적을 이용한 가중합 계산
    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)

 

 

이렇게 구한 예측 평점과 실제 평점의 오차를 측정하기 위한 예측 평가 지표(MSE, Mean Squared Errors)가 필요합니다.

# 사용자가 평점을 부여한 도서에 대해서만 예측 성능 평가 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 값은 크게 나올수가 있습니다.

따라서 MSE 값을 감소하기 위해 특정 도서와 가장 비슷한 유사도를 가지는 도서에 대해서만 유사도 벡터를 적용하는 함수를 다음으로 구현합니다.

 

# 특정 도서와 가장 비슷한 유사도를 가지는 도서에 대해서만 유사도 벡터를 적용하는 예측 평점 계산식 함수 구현
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

 

유사도가 가장 비슷한 도서에 대해서만 유사도 벡터를 적용한 예측 평점 함수를 이용하여 예측 평점을 계산하고, 실제 평점과 MSE를 구해보겠습니다.
계산된 예측 평점 Numpy 행렬은 DataFrame으로 변경합니다.

 

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 값이  11.83로 기존 15.57보다 감소하여 향상된 것을 알 수 있습니다.
(초기에 100,000건 데이터로 진행했으나 20분 넘게까지 해당 함수가 진행되지 않아... 50,000건으로 줄여 데이터 처리하였다.
 그래도 50,000건도 10분 가량 정도 소요된 듯하다.)

 

ratings_pred_matrix.head(5)

결과

 

 

특정 사용자인 User-Id가 '277928'인 사용자에 대한 도서 추천을 진행해 보겠습니다.

사용자가 평점을 매긴 도서를 토대로 평점이 높은 순으로 내림차순 나열하겠습니다.

user_rating_id = ratings_matrix.loc[277928, :]
user_rating_id[ user_rating_id > 0 ].sort_values(ascending=False)[:10]

결과

 

사용자가 이미 평점을 매긴 도서를 제외하고 추천할 수 있도록 아직 평점을 주지 않은 도서를 리스트 객체로 반환하는 함수를 구현해 보겠습니다.

def get_unseen_movies(ratings_matrix, userId):
    # userId로 입력받은 사용자의 모든 도서 정보를 추출해 Series로 반환함.
    # 반환된 user_ratings은 도서명(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에 해당하는 도서는 books_list에서 제외함.
    unseen_list = [ movie for book in book_list if book not in already_seen ]
    
    return unseen_list

 

 

사용자가 아직 평점을 주지 않은 추천 대상 도서 정보와 predict_rating_topsim()에서 추출한 아이템 유사도에 기반한 예측 평점 데이터 세트를 이용해 최종적으로 사용자에게 도서를 추천하는 함수를 구현해 보겠습니다.

def recomm_movie_by_userid(pred_df, userId, unseen_list, top_n=10):
    # 예측 평점 DataFrame에서 사용자 id 인덱스와 unseen_list로 들어온 도서명 칼럼을 추출해 가장 예측 평점이 높은 순으로 정렬함.
    recomm_books = pred_df.loc[userId, unseen_list].sort_values(ascending=False)[:top_n]
    return recomm_books

# 사용자가 관람하지 않은 도서명 추출
unseen_list = get_unseen_books(ratings_matrix, 277928)

# 아이템 기반의 최근접 이웃 협업 필터링으로 도서 추천
recomm_books = recomm_movie_by_userid(ratings_pred_matrix, 277928, unseen_list, top_n=10)

# 평점 데이터를 DataFrame으로 생성
recomm_books = pd.DataFrame(data=recomm_books.values, index=recomm_books.index, columns=['pred_score'])

recomm_books

결과

1순위로 'Invasion of Privacy'라는 스릴러 소설, 2순위로 'The Butter Did It: A Gastronomic Tale of Love and Murder' 미스터리, 픽션 소설, 3순위로 'The Witness for the Prosecution and Other Stories' 미스터리 추리 소설 순으로 추천되었습니다.

전체적으로 상위권에 미스터리, 스릴러 장르의 소설이 추천된 것을 알 수 있습니다.

 

 

 

반응형
LIST
Comments