[Python] 머신러닝 완벽가이드 - 09. 추천 시스템[Surprise]

Updated:

파이썬 머신러닝 완벽가이드 교재를 토대로 공부한 내용입니다.

실습과정에서 필요에 따라 내용의 누락 및 추가, 수정사항이 있습니다.


기본 세팅

import numpy as np
import pandas as pd

import matplotlib as mpl
import matplotlib.pyplot as plt
import seaborn as sns

import warnings
%matplotlib inline
%config InlineBackend.figure_format = 'retina'

mpl.rc('font', family='NanumGothic') # 폰트 설정
mpl.rc('axes', unicode_minus=False) # 유니코드에서 음수 부호 설정

# 차트 스타일 설정
sns.set(font="NanumGothic", rc={"axes.unicode_minus":False}, style='darkgrid')
plt.rc("figure", figsize=(10,8))

warnings.filterwarnings("ignore")

4. Surprise

파이썬 기반의 추천 시스템 패키지인 Surprise를 사용해보자.

Surprise는 다양한 추천 알고리즘을 쉽게 적용해 추천 시스템을 구축할 수 있다.

(사용자 또는 아이템 기반 최근접 이웃 협업 필터링, SVD, SVD++, NMF 기반 잠재 요인 협업 필터링 등)

import surprise
print(surprise.__version__)
1.1.1
from surprise import Dataset
from surprise.model_selection import train_test_split

# ml-100k: 10만 개 평점 데이터
data = Dataset.load_builtin('ml-100k')

# surprise의 train_test_split() 사용
trainset, testset = train_test_split(data, test_size=0.25, random_state=0)
  • surprise는 사이킷런 api와 사용법이 거의 유사하다.

  • load_builtin()으로 무비렌즈 사이트에서 제공하는 과거 데이터 셋 ml-100k을 불러왔다.

  • 데이터는 사용자 아이디, 아이템 아이디, 평점으로 구성되어 있다.

  • 10만 개의 데이터를 train 75,000, test 25,000으로 나누었다.

4.1 잠재 요인 협업 필터링

from surprise import SVD

# SVD를 이용한 잠재 요인 협업 필터링
algo = SVD()
algo.fit(trainset)
<surprise.prediction_algorithms.matrix_factorization.SVD at 0x1f1376d4b20>
  • surpriseSVD()객체를 생성 후 trianset을 학습시킨다.
# 사용자 아이디(uid), 아이템 아이디(iid)는 문자열로 입력
uid = str(196)
iid = str(302)

# 추천 예측 평점 (.predict)
pred = algo.predict(uid, iid)
pred
Prediction(uid='196', iid='302', r_ui=None, est=4.04440275028659, details={'was_impossible': False})
  • surprise에서 추천을 예측하는 메소드는 test()predict()가 있다.

  • predict()는 개별 사용자의 아이템에 대한 추천 평점을 예측하며 prediction 객체를 반환한다.

  • prediction 객체는 사용자 아이디(uid), 아이템 아이디(iid), 실제 평점(r_ui), 예측 평점(est)를 튜플 형태로 가진다.

  • prediction 객체의 details속성은 추천 예측을 할 수 없는 경우 True, 아닌 경우 False이다.

# 추천 예측 평점 (.test)
predictions = algo.test( testset )

print('prediction type :',type(predictions), ' size:',len(predictions))
print('prediction 결과의 최초 5개 추출')

predictions[:5]
prediction type : <class 'list'>  size: 25000
prediction 결과의 최초 5개 추출





[Prediction(uid='120', iid='282', r_ui=4.0, est=3.6545407397239638, details={'was_impossible': False}),
 Prediction(uid='882', iid='291', r_ui=4.0, est=3.986008772321205, details={'was_impossible': False}),
 Prediction(uid='535', iid='507', r_ui=5.0, est=4.079895038476315, details={'was_impossible': False}),
 Prediction(uid='697', iid='244', r_ui=5.0, est=3.7990317798414877, details={'was_impossible': False}),
 Prediction(uid='751', iid='385', r_ui=4.0, est=3.1739674591620246, details={'was_impossible': False})]
  • test()의 경우 사용자-아이템 평점 데이터 셋 전체에 대해서 추천을 예측한다.

  • 반환된 리스트 객체는 25,000개의 prediction 객체를 내부에 가지고 있다.

  • test()는 모든 사용자와 아이템 아이디에 대해서 predict()를 반복적으로 수행한 결과이다.

# 속성 확인
[ (pred.uid, pred.iid, pred.est, pred.details) for pred in predictions[:3] ]
[('120', '282', 3.6545407397239638, {'was_impossible': False}),
 ('882', '291', 3.986008772321205, {'was_impossible': False}),
 ('535', '507', 4.079895038476315, {'was_impossible': False})]
from surprise import accuracy

# 성능 평가
accuracy.rmse(predictions)
RMSE: 0.9465





0.946515001331555
  • surpriseaccuracy 모듈은 RMSE, MSE 등 추천 시스템 성능 평가 정보를 제공한다.

4.2 주요 모듈 소개

surprise는 사용자 아이디, 아이템 아이디, 평점 데이터가 로우 레벨로 된 데이터 셋만 적용 가능하다.

만약 user_id, item_id, rating, time_stamp로 구성된 데이터가 있다면 앞 3개 컬럼만 로딩한다.

일반 데이터 파일이나 판다스 데이터 프레임도 로딩 가능하나 컬럼 순서가 반드시 사용자 아이디, 아이템 아이디, 평점 순이어야 한다.

4.2.1 load_builtin

load_builtin()으로 무비렌즈 데이터를 불러올 수 있다.

디폴트로 ml-100k(10만 개 평점 데이터)를 불러오며 ml-1M(100만 개 평점 데이터)를 불러 올 수 있다.

4.2.2 load_from_file

load_from_file()은 OS 파일을 로딩할 때 사용한다.

주의할 점은 원본 파일에 컬럼명이 없어야 한다.

# index와 header를 제거한 ratings_noh.csv 파일 생성
ratings = pd.read_csv('./ml-latest-small/ratings.csv')
ratings.to_csv('./ml-latest-small/ratings_noh.csv', index=False, header=False)
  • 앞선 포스팅에서 사용한 ratings.csv에서 index와 header를 제거한 원본 파일을 생성하였다.

  • 이 데이터는 userid, movieid, rating, timestamp로 구성되어 있다.

from surprise import Reader

# Reader 객체 생성
reader = Reader(line_format='user item rating timestamp', sep=',', rating_scale=(0.5, 5))
  • surprise 데이터 셋은 기본적으로 무비렌즈 데이터 형식을 따른다.

  • 무비렌즈 데이터 형식이 아닌 다른 OS 파일은 Reader() 클래스를 먼저 설정하여야 한다.

  • line_format: 컬럼을 순서대로 나열한다(컬럼명은 공백으로 분리).

  • sep: 구분자로 디폴트는 \t이다.

  • rating_scale: 평점 값의 최소, 최대를 설정한다(디폴트는 1,5).

data = Dataset.load_from_file('./ml-latest-small/ratings_noh.csv', reader=reader)
data
<surprise.dataset.DatasetAutoFolds at 0x1f139f7d310>
  • Reader() 객체를 입력해 데이터를 불러온다.
# surprise의 train_test_split() 사용
trainset, testset = train_test_split(data, test_size=0.25, random_state=0)

# SVD를 이용한 잠재 요인 협업 필터링 (잠재 요인 크기 = 50)
algo = SVD(n_factors=50, random_state=0)
algo.fit(trainset)

# 추천 예측 평점 (.test)
predictions = algo.test( testset )

# 성능 평가
accuracy.rmse(predictions)
RMSE: 0.8682





0.8681952927143516
  • 불러온 데이터로 SVD 방법을 이용해 잠재 요인 협업 필터링을 진행하였다.

4.2.3 load_from_df

load_from_df()을 이용하여 데이터 프레임을 surprise 데이터 셋으로 로딩 가능하다.

주의할 점은 컬럼 순서가 반드시 사용자 아이디, 아이템 아이디, 평점 순이어야 한다.

# 데이터 불러오기 (데이터 프레임)
ratings = pd.read_csv('./ml-latest-small/ratings.csv') 

# Reader 객체 생성
reader = Reader(rating_scale=(0.5, 5.0))

# 사용자 아이디, 아이템 아이디, 평점 순서 (원래는 timestamp도 있으나 제외)
data = Dataset.load_from_df(ratings[['userId', 'movieId', 'rating']], reader)

# surprise의 train_test_split() 사용
trainset, testset = train_test_split(data, test_size=.25, random_state=0)

# SVD를 이용한 잠재 요인 협업 필터링 (잠재 요인 크기 = 50)
algo = SVD(n_factors=50, random_state=0)
algo.fit(trainset)

# 추천 예측 평점 (.test)
predictions = algo.test( testset )

# 성능 평가
accuracy.rmse(predictions)
RMSE: 0.8682





0.8681952927143516
  • 데이터 프레임을 suprise 데이터 셋으로 불러와 같은 과정을 수행하였다.

4.3 교차 검증

from surprise.model_selection import cross_validate 

# 데이터 불러오기 (데이터 프레임)
ratings = pd.read_csv('./ml-latest-small/ratings.csv') 

# Reader 객체 생성
reader = Reader(rating_scale=(0.5, 5.0))

# 사용자 아이디, 아이템 아이디, 평점 순서 (원래는 timestamp도 있으나 제외)
data = Dataset.load_from_df(ratings[['userId', 'movieId', 'rating']], reader)

# SVD를 이용한 잠재 요인 협업 필터링
algo = SVD(random_state=0)

# 교차 검증 수행
cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=5, verbose=True)
Evaluating RMSE, MAE of algorithm SVD on 5 split(s).

                  Fold 1  Fold 2  Fold 3  Fold 4  Fold 5  Mean    Std     
RMSE (testset)    0.8774  0.8698  0.8737  0.8766  0.8730  0.8741  0.0027  
MAE (testset)     0.6727  0.6699  0.6696  0.6748  0.6678  0.6709  0.0025  
Fit time          4.04    4.01    4.06    4.09    3.98    4.04    0.04    
Test time         0.19    0.11    0.11    0.19    0.12    0.15    0.04    





{'test_rmse': array([0.87740003, 0.86984839, 0.87372227, 0.8765873 , 0.87301413]),
 'test_mae': array([0.67272245, 0.6698664 , 0.66957162, 0.67479119, 0.66779436]),
 'fit_time': (4.039103031158447,
  4.0105016231536865,
  4.0613627433776855,
  4.088256359100342,
  3.9809882640838623),
 'test_time': (0.1872107982635498,
  0.11255478858947754,
  0.11013364791870117,
  0.19319939613342285,
  0.12269926071166992)}
  • surprise.model_selectioncross_validate()으로 교차 검증이 가능하다.

  • 전반적으로 사이킷런과 사용법이 확실히 유사하다.

4.4 하이퍼 파라미터

from surprise.model_selection import GridSearchCV

# n_epochs: SGD 수행 시 반복 횟수, n_factors: 잠재 요인 크기
param_grid = {
    'n_epochs': [20, 40, 60], 
    'n_factors': [50, 100, 200]
}

# GridSearchCV
gs = GridSearchCV(SVD, param_grid, measures=['rmse', 'mae'], cv=3) # algo가 아닌 SVD 입력하였다.
gs.fit(data)

# 최적 하이퍼 파라미터 및 그 때의 최고 성능
print(gs.best_params['rmse'])
print(gs.best_score['rmse'])
{'n_epochs': 20, 'n_factors': 50}
0.8777624423794487
  • surpriseGridSearchCV()로 하이퍼 파라미터 튜닝이 가능하다.

  • 여기선 SVD 하이퍼 파라미터에 대해 작업하였다.

4.5 실습

# 오류 코드
# data = Dataset.load_from_df(ratings[['userId', 'movieId', 'rating']], reader)
# algo = SVD(n_factors=50, random_state=0)
# algo.fit(data)
  • 실습 데이터는 ratings.csv를 train, test 분리 없이 전체로 사용한다.

  • 다만 surprise는 TrainSet 객체로 변환하지 않으면 fit()으로 학습시 오류가 발생한다.

from surprise.dataset import DatasetAutoFolds

# Reader 객체 생성
reader = Reader(line_format='user item rating timestamp', sep=',', rating_scale=(0.5, 5))

# DatasetAutoFolds 객체 생성
data_folds = DatasetAutoFolds(ratings_file='./ml-latest-small/ratings_noh.csv', reader=reader)

# 전체 데이터를 train으로 지정
trainset = data_folds.build_full_trainset()
trainset
<surprise.trainset.Trainset at 0x1f137dbd220>
  • DatasetAutoFolds() 객체를 생성 후 build_full_trainset() 메소드로 전체 데이터를 train set으로 생성 가능하다.
# SVD를 이용한 잠재 요인 협업 필터링 (잠재 요인 크기 = 50)
algo = SVD(n_epochs=20, n_factors=50, random_state=0)
algo.fit(trainset)

# 사용자 아이디, 아이템 아이디 문자열로 입력
uid = str(9)
iid = str(42)

# 추천 예측 평점 (.predict)
pred = algo.predict(uid, iid, verbose=True)
user: 9          item: 42         r_ui = None   est = 3.13   {'was_impossible': False}
  • user 9는 영화 42에 대한 평점이 없다.

  • SVD를 이용한 잠재 요인 기반 협업 필터링으로 user 9의 예측 평점을 계산하였다.

  • 이제 아직 보지 않은 영화에 대해 예측 평점 순으로 추천을 해보자.

# 영화에 대한 상세 속성 정보 DataFrame로딩
movies = pd.read_csv('./ml-latest-small/movies.csv')
# 아직 보지 않은 영화 리스트 함수
def get_unseen_surprise(ratings, movies, userId):
     # 특정 userId가 평점을 매긴 모든 영화 리스트
    seen_movies = ratings[ratings['userId']== userId]['movieId'].tolist()
    
    # 모든 영화명을 list 객체로 만듬. 
    total_movies = movies['movieId'].tolist()
      
    # 한줄 for + if문으로 안 본 영화 리스트 생성
    unseen_movies = [ movie for movie in total_movies if movie not in seen_movies]
    
    # 일부 정보 출력
    total_movie_cnt = len(total_movies)
    seen_cnt = len(seen_movies)
    unseen_cnt = len(unseen_movies)
    
    print(f"전체 영화 수: {total_movie_cnt}, 평점 매긴 영화 수: {seen_cnt}, 추천 대상 영화 수: {unseen_cnt}")
    
    return unseen_movies
unseen_movies = get_unseen_surprise(ratings, movies, 9)
전체 영화 수: 9742, 평점 매긴 영화 수: 46, 추천 대상 영화 수: 9696
  • 앞선 포스팅에서 사용한 get_unseen_movies()를 조금 변환하였다.

  • user 9가 아직 보지 않은 9,696개 중 예측 평점 순으로 추천을 진행한다.

def recomm_movie_by_surprise(algo, userId, unseen_movies, top_n=10):
    
    # 아직 보지 않은 영화의 예측 평점: prediction 객체 생성
    predictions = []    
    for movieId in unseen_movies:
        predictions.append(algo.predict(str(userId), str(movieId)))
    
    # 리스트 내의 prediction 객체의 est를 기준으로 내림차순 정렬
    def sortkey_est(pred):
        return pred.est

    predictions.sort(key=sortkey_est, reverse=True) # key에 리스트 내 객체의 정렬 기준을 입력
    
    # 상위 top_n개의 prediction 객체
    top_predictions = predictions[:top_n]
    
    # 영화 아이디, 제목, 예측 평점 출력
    print(f"Top-{top_n} 추천 영화 리스트")
    
    for pred in top_predictions:
        
        movie_id = int(pred.iid)
        movie_title = movies[movies["movieId"] == movie_id]["title"].tolist()
        movie_rating = pred.est
        
        print(f"{movie_title}: {movie_rating:.2f}")
recomm_movie_by_surprise(algo, 9, unseen_movies, top_n=10)
Top-10 추천 영화 리스트
['Godfather, The (1972)']: 4.31
['Star Wars: Episode IV - A New Hope (1977)']: 4.28
['Pulp Fiction (1994)']: 4.28
['Star Wars: Episode V - The Empire Strikes Back (1980)']: 4.23
['Usual Suspects, The (1995)']: 4.19
['Streetcar Named Desire, A (1951)']: 4.15
['Star Wars: Episode VI - Return of the Jedi (1983)']: 4.12
['Goodfellas (1990)']: 4.11
['Glory (1989)']: 4.08
['Silence of the Lambs, The (1991)']: 4.08
  • user 9에 대한 추천 예측 평점이 높은 10개의 영화를 추출하였다.

Leave a comment