본문 바로가기
Data analysis

도서 추천시스템 - 아이템 기반 협업 필터링

by jhpaik 2023. 10. 7.

추천시스템 개요

추천시스템은 유튜브부터 이커머스 등의 플랫폼까지 우리가 접하는 다양한 콘텐츠, 생활 각각 요소에서 보편적으로 활용되고 있다. 하지만 현재 기업이 추천시스템의 활용하는 목적은 고객을 서비스에 계속 활동할 수 있도록 하는 것이 목적이고 이는 곧 수익창출을 의미한다. 그러므로 현재 추천시스템은 고객에게 다양한 질 좋은 정보를 제공하기보다는 고객이 서비스를 이용하는 데 시간을 소모하도록 하는 것이 목적이기 때문에 사용자 입장에선 자신의 관심사 또는 한 쪽의 편향된 정보만을 주입받아 편향된 생각과 정보에만 노출되는 우려가 있다. 이를 에코챔버(Echo Chamber) 효과라고 한다.

 

추천시스템의 이로움, 해로움을 떠나 우리는 추천시스템의 많은 영향을 받는 것은 사실이다. 해당 추천시스템에 대해서 좀 더 구체적으로 알아보기 위해 해당 포스트를 게재하게 되었다.

 

추천시스템 이론

추천시스템은 콘텐츠 기반 필터링과 협업 기반 필터링으로 나뉘며, 여기서 협업 기반 필터링은 최근접 이웃 방식과 잠재 요인 방식으로 나뉜다. 두 방식 모두 사용자-아이템 평점 행렬 데이터에 의해 추천된다.

 

이번 포스트 목적은 아이템 기반의 협업 필터링과 잠재요인 협업 필터링을 활용하는 것이 주목적이기 때문에 두 가지를 중점으로 설명하겠다. 사용자 기반의 협업 필터링과 아이템 기반의 협업 필터링의 차이는 간단하다.

사용자 기반 협업 필터링이 사용자의 행동양식을 통해, ‘예측 평가’를 수행한다면,

ex) 영화 리뷰를 쓴 사람에게 다른 영화를 추천하는 것

아이템 기반의 협업 필터링은 사용자들의 아이템에 대한 호/불호 평점이 유사한 아이템을 추천하는 기준이 되는 알고리즘이다.

 

사용자 기반: “사용자 a와 비슷한 사용자 b가 이 아이템을 사용함”

아이템 기반: “이 아이템을 구매한 다른 고객은 저 아이템도 구매함”

 

아이템 기반 협업 필터링 실습

데이터셋을 통해 아이템 기반 협업 필터링을 진행해보자

데이터셋은 캐글의 데이터셋을 활용했으며 약 271,379 권의 도서에 대해 1,149,780 등급을 제공하는 278,858 명의 사용자를 포함하는 데이터셋이다.

Book-Crossing: User review ratings

 

Book-Crossing: User review ratings

A collection of book ratings

www.kaggle.com

 

데이터 불러오기 및 데이터셋 확인

먼저 필요한 라이브러리를 불러오고 데이터셋을 가져온 뒤 간단히 NULL 값을 확인하였다.

데이터셋은 도서정보에 대한 데이터셋인 BX_Books.csv와 유저가 매긴 도서 평점 정보가 담긴 BX_Book_Ratings.csv를 활용하였다.

import csv
import pandas as pd
import numpy as np

from sklearn.metrics.pairwise import cosine_similarity
from sklearn.metrics import mean_squared_error
bxbooks = pd.read_csv('data/kaggle_book/BX_Books.csv', sep=';')
bxbookratings = pd.read_csv('data/kaggle_book/BX_Book_Ratings.csv', sep=';', encoding="cp949")

# 도서 null 체크 1
bxbooks.isnull().sum()

ISBN                    0

Book-Title              0

Book-Author             1

Year-Of-Publication    0

Publisher               2

Image-URL-S             0

Image-URL-M             0

Image-URL-L             0

dtype: int64

 

# 평점 null 체크 2
bxbookratings.isnull().sum()

User-ID         0

ISBN            0

Book-Rating    8

dtype: int64

 

아래와 같이 도서데이터셋은 ISBN, 책 제목, 저자 등의 도서에 관한 컬럼을 확인할 수 있다.

bxbooks.head()

 

코사인 유사도를 적용하여 데이터프레임으로 변환한 뒤 추천알고리즘을 적용하였다.

*cosine_similarity(): 행을 기준으로 서로 다른 행을 비교해 유사도를 측정하는 함수

# 변경한 데이터셋을 기반으로 책의 코사인 유사도 구함

from sklearn.metrics.pairwise import cosine_similarity

item_sim = cosine_similarity(ratings_matrix_T, ratings_matrix_T)

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

 

# 1. 오만과 편견
item_sim_df['Pride and Prejudice'].sort_values(ascending=False)[:6]

 

# 2. 로미오와 줄리엣
item_sim_df['Romeo and Juliet'].sort_values(ascending=False)[1:6]

item_sim_df.head(3)

 

아이템 기반 최근접 이웃 협업 필터링으로 개인화된 도서 추천

아이템 기반 최근접 이웃 협업 필터링으로 개인화된 도서 추천을 진행해보자. 개인화된 도서 추천의 가장 큰 특징은 개인이 아직 읽지 않은 도서를 추천하는 것이 핵심이다. 아직 읽지 않은 도서에 대해 아이템 유사도와 기존 읽은 책 평점 데이터 기반으로 새롭게 모든 책의 예측 평점 계산한 후 높은 예측 평점을 가진 도서를 추천하는 방식이다.

# 아이템 기반 협업필터링에서 개인화된 예측 평점계산식 함수 설계
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

# 도서간 유사도를 갖는 df인 item_sim_df와 사용자-도서 평점 df인 rating_matrix 변수를 통해 사용자별로 최적화된 평점 스코어 예측
ratings_pred = predict_rating(ratings_matrix.values, item_sim_df.values)

# 예측된 rating_pred에 rating_matrix을 index, item_sim_df을 컬럼으로 하여 df 생성
ratings_pred_matrix = pd.DataFrame(data=ratings_pred, index= ratings_matrix.index, \\
                                  columns = ratings_matrix.columns)
ratings_pred_matrix.head(3)

 

예측결과가 실제 평점과 얼마나 차이가 나는지 확인하기 위해 사용자가 부여한 도서에 대해서만 예측 성능 평가 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:  15.572293628480582

 

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

 

predict_rating_topsim() 함수를 이용해 예측 평점을 계산하고 실제 평점과 MSE 구해보자

계산된 예측 평점 넘파이 행렬은 df로 재생성하였다.

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))

# 계산된 예측 평점 데이터를 df로 변경
ratings_pred_matrix = pd.DataFrame(data=ratings_pred, index=ratings_matrix.index,
                                  columns=ratings_matrix.columns)

 

Example: 특정 사용자에게 도서 추천

특정 사용자 277928로 도서추천을 수행해보자

먼저 277928 사용자가 높게 평점을 준 도서 확인하였다.

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

# 사용자가 평점을 주지 않은 책을 리스트객체로 반환하는 함수 생성
def get_unread_books(ratings_matrix, userId):

# userId로 입력받은 사용자의 모든 도서정보를 추출해 Series로 반환 반환된 user_ratings은 도서명(title)을 index로 갖는 Series 객체
user_rating = ratings_matrix.loc[userId, :]

# user_rating이 0보다 크면 기존에 읽은 도서. 대상 index를 추출해 list객체 생성
already_seen = user_rating [ user_rating > 0 ].index.tolist()

# 모든 도서명을 list 객체로 만듦
book_list = ratings_matrix.columns.tolist()

# list comprehension 으로 already_seen에 해당되는 도서는 books_list에서 제외
unread_list = [ book for book in book_list if book not in already_seen ]

return unread_list

 

사용자가 책의 평점을 주지 않은 추천대상 도서정보와 predict_rating_topsim()에서 추출한 사용자별 아이템 유사도에 기반한 예측 평점 데이터셋을 이용해 최종적으로 사용자에게 책을 추천하는 함수를 생성하였다.

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

# 사용자가 읽지 않은 도서명 추출
unread_list = get_unread_books(ratings_matrix, 277928)

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

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

recomm_books
# 사용자가 좋아할 만한 가장 높은 예측 평점을 가진 도서 리스트가 추천됨

 

Reference

파이썬 머신러닝 완벽가이드 - 권철민