Patrick's 데이터 세상

[추천 시스템 - Surprise를 이용한 잠재 요인 협업 필터링 추천] Book-Crossing: 사용자 리뷰 평점 데이터 세트 본문

Machine Learning/Recommend system

[추천 시스템 - Surprise를 이용한 잠재 요인 협업 필터링 추천] Book-Crossing: 사용자 리뷰 평점 데이터 세트

patrick610 2020. 9. 23. 21:18
반응형
SMALL

 

 

 

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


Git url

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

 

hipster4020/RecommendationSystem

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

github.com

 

 

분석 도구 : Google Colaboratory

 

활용 데이터 : 약 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

 


 

파이썬 기반의 추천 시스템 구축을 위한 전용 패키지 중 Surprise를 활용하여 추천 시스템을 구현해보겠습니다.

사용자 리뷰 평점 데이터 세트를 잠재 요인 협업 필터링하기 전에 간략하게 '잠재 요인 협업 필터링'에 대해 알아보겠습니다.

 

 

잠재 요인 협업 필터링(Latent Factor Collaborative Filtering)

 

   사용자-아이템 평점 매트릭스 속에 숨어 있는 잠재 요인을 추출해 추천 예측을 할 수 있게 하는 기법입니다.
   대규모 다차원 행렬을 SVD와 같은 차원 감소 기법으로 분해하는 과정에서 잠재 요인을 추출하는데, 이러한 기법을 행렬 분해(Matrix Factorization)라고 합니다.



   위 그림은 행렬 분해 기법을 이용해 사용자-잠재 요인 행렬과 아이템-잠재 요인 행렬의 전치 행렬(즉, 잠재 요인-아이템 행렬)로 분해된 데이터 세트를 다시 내적 곱으로 결합하면서 사용자가 예측하지 않은 아이템에 대한 평점을 도출하는 방식을 대략적으로 나타낸 것입니다.
   행렬 분해에 의해 추출되는 '잠재 요인'이 정확히 어떤 것인지는 알 수 없지만, 가령 도서 평점 기반의 사용자-아이템 평점 행렬 데이터라면 도서가 가지는 장르별 특성 선호도로 가정할 수 있습니다.
   즉, 사용자-잠재 요인 행렬은 사용자의 도서 장르에 대한 선호도로, 아이템-잠재 요인 행렬은 도서의 장르별 특성 값으로 정의할 수 있습니다.

 

 

 

초반에는 콘텐츠 기반 필터링을 많이 사용했으나 넷플릭스(Nexflix) 추천 시스템 경연 대회에서 행렬 분해(Matrix Factorization) 기법을 이용한 잠재요인 협업 필터링 방식이 우승한 사례 이후 대부분의 온라인 스토어에서 잠재 요인 협업 필터링을 많이 사용하게 되었다고 합니다.
그러나, 서비스하는 아이템의 특성에 따라 콘텐츠 기반 필터링이나 최근접 이웃 기반 협업 필터링 방식을 유지하는 사이트도 많습니다.
특히, 아마존의 경우는 아직도 아이템 기반의 최근접 이웃 협업 필터링 방식을 추천 엔진으로 사용합니다.

 

 

 

 

 

 

데이터 가공 및 변환

# Surprise 설치
!pip install scikit-surprise
# Library import
from surprise import SVD
from surprise import Dataset
from surprise import accuracy
from surprise.model_selection import train_test_split
from surprise.dataset import DatasetAutoFolds

import os

# 맥os의 경우 라이브러리를 중복 사용해 오류가 발생할 수 있으므로 환경변수를 아래와 같이 설정한다.
os.environ['KMP_DUPLICATE_LIB_OK']='True'


import pandas as pd
from surprise import Reader

 

Surprise는 파이썬 기반에서 사이킷런과 유사한 API와 프레임워크를 제공합니다.

Surprise를 이용한 추천 시스템은 데이터를 학습용과 테스트용 데이터 세트로 분리한 뒤 SVD 행렬 분해를 통한 잠재 요인 협업 필터링을 수행합니다.

 

 

Dataset.load_from_file API를 이용해 지정된 디렉터리에 있는 사용자-아이템 평점 데이터를 로딩합니다.

데이터 로딩 시에 ISBN 컬럼은 Reader 객체에서 line_format을 사용할 수 없으므로 도서명으로 바꾸는 작업 후 item으로 컬럼 명칭을 변경합니다.

ratings = pd.read_csv('/content/sample_data/Book-Ratings.csv')
books = pd.read_csv('/content/sample_data/BX-Books.csv')
bookratings = pd.merge(ratings, books, on='ISBN')
bookratings.rename(columns={"ISBN":"item"}, inplace=True)
books.rename(columns={"Book-Title":"title", "ISBN":"item"}, inplace=True)
# ratings_noh.csv 파일로 언로드 시 인덱스와 헤더를 모두 제거한 새로운 파일 생성.
bookratings.to_csv('/content/sample_data/Book-Ratings_noh.csv', index=False, header=False)

'Book-Ratings_noh.csv'로 헤더를 제거한 csv 파일을 재생성하여 load_from_file()을 이용해 DataSet로 로드합니다.

먼저 Dataset.load_from_file()을 적용하기 전에 Reader 클래스를 이용해 데이터 파일의 파싱 포맷을 정의해야 합니다.

 

col = 'user item rating'
reader = Reader(line_format=col, sep=',', rating_scale=(1, 10))
data=Dataset.load_from_file('/content/sample_data/Book-Ratings_noh.csv', reader=reader)

 

   · line_format(string) : 컬럼을 순서대로 나열. 입력된 문자열을 공백으로 분리해 컬럼으로 인식.

   · sep(char) : 컬럼을 분리하는 분리자, 디폴트 '\t'. Pandas DataFrame에서 입력받을 경우에는 기재할 필요가 없다.

   · rating_scale(tuple, optional) : 평점 값의 최소 ~ 최대 평점을 설정. 디폴트는 (1, 5)이지만  Book-Ratings.csv 파일의 경우는 최소 평점이 1, 최대 평점이 10이므로 (0, 10)로 설정.

 

이렇게 Reader 설정이 완료되면 Dataset.load_from_file()은 이를 기반으로 데이터를 파싱하면서 Dataset을 로딩합니다.

 

 

다음으로 SVD 행렬 분해 기법을 통해 추천 기법을 구현해보겠습니다.

먼저, 잠재 요인 크기 K 값을 나타내는 파라미터인 n_factor를 50으로 설정해 데이터를 학습합니다.

그 후 학습 데이터로 fit()을 호출해 학습한 뒤 테스트 데이터로 test()를 호출해 예측 평점을 계산하고 MSE/RMSE로 성능을 평가합니다.

trainset, testset = train_test_split(data, test_size=.25, random_state=0)

# 수행 시마다 동일한 결과를 도출하기 위해 random_state 설정
algo = SVD(n_factors=50, random_state=0)

# 학습 데이터 세트로 학습하고 나서 테스트 데이터 세트로 평점 예측 후 RMSE 평가
algo.fit(trainset)
predictions = algo.test(testset)
accuracy.rmse(predictions)

결과

 

 

 

Surprise를 이용한 개인화 도서 추천 시스템 구축

잠재 요인 협업 필터링 기반의 개인화된 도서 추천을 구현하기 위해 학습된 추천 알고리즘을 기반으로 특정 사용자가 아직 평점을 매기지 않은 도서 중에서 개인 취향에 가장 적절한 도서를 추천을 구현하겠습니다.

 

ratings.csv 데이터를 학습 데이터와 테스트 데이터로 분리하지 않고 전체를 학습 데이터로 사용한다.

# 다음 코드는 train_test_split()으로 분리되지 않은 데이터 세트에 fit()을 호출해 오류가 발생한다.
data = Dataset.load_from_df(bookratings[['user','item','rating']], reader)
algo = SVD(n_factors=50, random_state=0)
algo.fit(data)

결과

fit()은 데이터 세트를 train_test_split()을 이용해 trainset 클래스 객체로 변환한 데이터로 학습할 수 있는데 위에서는 데이터 세트를 그대로 사용했기 때문에 오류를 유발합니다.

 

 

데이터 세트 전체를 학습 데이터로 사용하려면 DatasetAutoFolds 객체를 생성하고 build_full_trainset() 메서드를 호출하여 전체 데이터를 학습 데이터 세트로 만듭니다.

col = 'user item rating'
reader = Reader(line_format=col, sep=',', rating_scale=(1, 10))
# DatasetAutoFolds 클래스를 ratings_noh.csv 파일 기반으로 생성.
data_folds = DatasetAutoFolds(ratings_file='/content/sample_data/Book-Ratings_noh.csv', reader=reader)

# 전체 데이터를 학습 데이터로 생성함.
trainset = data_folds.build_full_trainset()

 

 

먼저 SVD를 이용해 학습을 수행하고 특정 사용자를 임의로 userId=9로 지정하여 도서를 추천하기 위해 아직 보지 않은 도서 목록을 확인해 보겠습니다.

algo = SVD(n_epochs=20, n_factors=50, random_state=0)
algo.fit(trainset)
# userId=9 ISBN 데이터를 추출해 ISBN '034545104X' 데이터가 있는지 확인.
ISBNs = bookratings[bookratings['user']==9]['item']

if ISBNs[ISBNs=='034545104X'].count() ==0 :
  print('사용자 아이디 2313는 ISBN ''034545104X''의 평점 없음')

# 도서에 대한 상세 속성 정보 DataFrame 로딩
print(books[books['item']=='034545104X'])

결과

이 ISBN '034545104X' 도서에 대해서 userId 9 사용자의 추천 예상 평점은 predict() 메서드를 이용하여 구할 수 있습니다.
학습된 SVD 객체에서 predict() 메서드 내에 userId와 ISBN 값을 입력합니다.
단, 이 값은 모두 문자열 값이어야 합니다.

uid = str(9)
iid = str('034545104X')

pred = algo.predict(uid, iid, verbose=True)

결과

추천 예측 평점은 est 값으로 2.31이다.

이러한 방식으로 사용자가 평점을 매기지 않은 아이템에 대해 간단한 추천 예측 평점을 구할 수 있습니다.

 

 


이제 사용자가 평점을 매기지 않은 전체 도서를 추출한 후 예측 평점 순으로 도서를 추천해 보겠습니다.

먼저 임의의 userId=276847의 추천 대상이 되는 도서를 추출합니다.

def get_unread_surprise(bookratings, books, userId):
  # 입력값으로 들어온 userId에 해당하는 사용자가 평점을 매긴 모든 도서를 리스트로 생성
  read_books = bookratings[bookratings['user']==userId]['item'].tolist()

  # 모든 도셔의 ISBN를 리스트로 생성.
  total_books = books['item'].tolist()

  # 모든 도서의 ISBN 중 이미 평점을 매긴 도서의 ISBN를 제외한 후 리스트로 생성
  unread_books = [book for book in total_books if book not in read_books]
  print('평점 매긴 도서 수 : ', len(read_books), '추천 대상 도서 수 : ', len(unread_books),
        '전체 도서 수 : ', len(total_books))
  
  return unread_books

unread_books = get_unread_surprise(bookratings, books, 276847)

결과

get_unread_surprise() 함수를 만들고 이를 이용해 아이디 276847인 사용자가 아직 평점을 매기지 않은 도서의 정보를 반환합니다.

 

사용자 아이디 276847번은 전체 도서 271,379개의 도서 중에서 47개 도서에 대해 평점을 매겼습니다.
추천 대상 영화는 271,332개 입니다.

 

 

 

학습된 추천 알고리즘 클래스인 SVD를 이용해 높은 예측 평점을 가진 순으로 영화를 추천해 보겠습니다.

이를 위해 recomm_book_by_surprise() 함수를 새롭게 생성하여 인자로 학습이 완료된 추천 알고리즘 객체, 추천 대상 사용자 아이디, 추천 대상 도서의 리스트 객채, 그리고 추천 상위 N개 개수를 받습니다.

 

recomm_book_by_surprise()는 추천 대상 도서 모두를 대상으로 추천 알고리즘 객체의 predict() 메서드를 호출하고 그 결과인 Prediction 객체를 리스트 객체로 저장합니다.

이렇게 저장된 리스트 내부의 Prediction 객체를 예측 평점이 높은 순으로 다시 정렬한 뒤 TOP-N개의 Prediction 객체에서 ISBN, 도서 제목, 예측 평점 정보를 추출해 반환합니다.

def recomm_book_by_surprise(algo, userId, unread_books, top_n=10):

  # 알고리즘 객체의 predict() 메서드를 평점이 없는 영화에 반복 수행한 후 결과를 list 객체로 저장
  predictions = [algo.predict(str(userId), str(item)) for item in unread_books]
  
  # predictions list 객체는 surprise의 Prediction 객체를 원소로 가지고 있음.
  # [Prediction(uid='276847', iid='1', est=3.69), Prediction(uid='276847', iid='2', est=2.98),,,,]

  # 이를 est 값으로 정렬하기 위해서 아래의 sortkey_eat 함수를 정의함.
  # sortkey_est 함수는 list 객체의 sort() 함수의 키 값으로 사용되어 정렬 수행.
  def sortkey_est(pred):
    return pred.est
    
  # sortkey_est() 반환값의 내림 차순으로 정렬 수행하고 top_n개의 최상위 값 추출.
  predictions.sort(key=sortkey_est, reverse=True)
  top_predictions = predictions[:top_n]

  # top_n으로 추출된 영화의 정보 추출, 영화 아이디, 추천 예상 평점, 제목 추출
  top_book_ids = [ int(pred.iid) for pred in top_predictions]
  top_book_rating = [ pred.est for pred in top_predictions]
  top_book_titles = bookratings[bookratings.item.isin(top_book_ids)]['Book-Title']

  top_book_preds = [ (id, rating) for id, rating in 
                      zip(top_book_ids, top_book_rating)]
  return top_book_preds

unread_books = get_unread_surprise(bookratings, books, 276847)
top_book_preds = recomm_book_by_surprise(algo, 276847, unread_books, top_n=10)

print('  ')
print('  ')
print('***************************')
print('*** 추천 영화 리스트 Top 10 ***')
print('  ')
for top_book in top_book_preds :
  print(top_book[0], ":", top_book[1])

결과

list = ['440234743', '440225701', '671042858', '60976845', '451526341', '345335465', '385503857', '60932759', '312195516', '156027321']

bookstitle=books[books.item.isin(list)]
bookstitle

결과

 

'276847'번 아이디 사용자는 법정 스릴러의 대가 존 그리샴 작가의 'ISBN : 440234743, 서명 : The Testament'가 1위, 'ISBN : 440225701, 서명 : The Street Lawyer'가 2위를 차지하였습니다.

그 뒤는 추리, 스릴러 'ISBN : 671042858, 서명 : The Girl Who Loved Tom Gordon',

일반 소설 'ISBN : 60976845, 서명 : Little Altars Everywhere: A Novel', 

풍자 소설 'ISBN : 451526341, 서명 : Animal Farm'등이 뒤를 이었습니다.

 

 

 

반응형
LIST
Comments