본문 바로가기

인공지능/추천시스템

5. Surprise 라이브러리를 이용한 추천시스템 개발


< 목차 >

  1. 용어정의
  2. Surprise 패키지 소개
  3. Surprise 주요 모듈
    1. OS 파일데이터를 Surprise 데이터셋으로 로딩
    2. Pandas Dataframe 을 Surprise 데이터셋으로 로딩
  4. Surprise 추천 알고리즘 클래스
  5. 베이스라인 평점
  6. 교차검증과 하이퍼 파라미터 튜닝
  7. Surprise 를 이용한 개인화 영화 추천시스템 개발

1. 용어정의


2. Surprise 패키지 소개

파이썬 기반의 추천 시스템 구축을 위한 전용 패키지 중 하나인 Surprise 는 파이썬 기반에서 사이킷런과 유사한 API 와 프레임워크를 제공하여, 추천 시스템의 전반적인 알고리즘을 이해하고 사이킷런 사용경험이 있으면 쉽게 사용할 수 있다.

 

Surprise 패키지는 "pip install scikit-surprise" 혹은 "conda install -c conda-forge scikit-surprise" 를 입력하여 설치할 수 있다.


3. Surprise 주요 모듈

Surprise 는 사용자 아이디, 아이템 아이디, 평점 데이터가 로우 레벨로 된 데이터 세트만 적용할 수 있다. 그래서 데이터의 첫번째 컬럼을 사용자 아이디, 두번째 컬럼을 아이템 아이디, 세번째 컬럼을 평점으로 가정해 데이터를 로딩하고 네번째 컬럼부터는 로딩을 수행하지 않는다. 

 

예로, user_id, item_id, rating, time_stamp 필드로 구분된 데이터라면 앞 3개 필드만 로딩하고 이후 time_stamp 필드는 로딩에서 제외된다. 따라서 반드시 데이터 세트의 컬럼 순서가 사용자 아이디, 아이템 아이디, 평점 순으로 돼 있어야 한다.

3-1. OS 파일데이터를 Surprise 데이터셋으로 로딩

Surprise 패키지에 사용할 OS 파일을 로드할 때 주의할 점은 로딩되는 데이터 파일에 칼럼명을 가지는 헤어 문자열이 있어서는 안된다는 것이다. 따라서 Pandas의 DataFrame에 to_csv() 함수를 이용해 간단히 이 컬럼 헤더를 삭제하고 새로운 파일인 ratings_noh.csv 를 저장한다.

 

import pandas as pd
 
ratings = pd.read_csv('./ratings.csv')
# ratings_noh.csv 파일로 unload 시 index 와 header를 모두 제거한 새로운 파일 생성. 
ratings.to_csv('./ratings_noh.csv', index=False, header=False)​


헤더가 삭제된 ratings 파일은 Surprise 패키지에서 제공하는 Dataset 클래스의 load_from_file()을 이용해서 로드할 수 있으며, 사전에 데이터 파일의 파싱 포맷을 정의해야 한다.


이를 정의하기 위해 Surprise 패키지에서 제공하는 Reader 클래스를 이용하는데 아래의 예제처럼 Reader 객체 생성 시에 line_format 인자로 user, item, rating, timestamp 의 4개 컬럼으로 데이터가 구성돼 있음을 명시했고, 각 컬럼의 분리문자는 콤마, 평점 단위는 0.5~5 점으로 설정했다. 이렇게 Reader 설정이 완료되면 Dataset.load_from_file()은 이를 기반으로 데이터를 파싱하며, Dataset을 로딩한다.

 

from surprise import Reader
 
reader = Reader(line_format='user item rating timestamp', sep=',', rating_scale=(0.5, 5))
data=Dataset.load_from_file('./ratings_noh.csv',reader=reader)


이제 SVD 행렬 분해 기법을 이용해 추천을 예측하는 예제를 구현할 것이며, 잠재 요인 크기 K 값을 나타내는 파라미터인 n_factor 를 50으로 설정해 데이터를 학습한 뒤 테스트 데이터 세트를 적용해 예측 평점을 구한다. 그리고 구한 평점과 실제 평점의 차이를 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)

3-2. Pandas Dataframe 을 Surprise 데이터셋으로 로딩

Dataset.load_from_df() 를 이용하면 판다스의 DataFrame 에서도 Surprise 데이터 세트로 로딩할 수 있다. 이때 주의할 점은 사용자 아이디, 아이템 아이디, 평점 컬럼 순서를 지켜야 한다는 것이다.


다음 예제와 같이 ratings.csv 파일을 DataFrame으로 로딩한 ratings 에서 Surprise 데이터 세트로 로딩하려면 Dataset.load_from_df(ratings[['userId', 'movieId', 'rating']], reader) 와 같이 파라이터를 입력해줘야 한다. 그리고 상기 예제와 동일하게 SVD 행렬 분해로 평점을 예측하고 이를 실제값과 RMSE 값 비교하면 아래와 같다.

 

import pandas as pd
from surprise import Reader, Dataset
 
ratings = pd.read_csv('./ratings.csv')
reader = Reader(rating_scale=(0.5, 5.0))
 
# ratings DataFrame 에서 컬럼은 사용자 아이디, 아이템 아이디, 평점 순서를 지켜야 합니다.
data = Dataset.load_from_df(ratings[['userId', 'movieId', 'rating']], reader)
trainset, testset = train_test_split(data, test_size=.25, random_state=0)
 
algo = SVD(n_factors=50, random_state=0)
algo.fit(trainset)
predictions = algo.test( testset )
accuracy.rmse(predictions)


4. Surprise 추천알고리즘 클래스

Surrprise 에서 추천 예측을 위해 자주 사용되는 추천 알고리즘 클래스는 아래와 같으며, 이밖에도 SVD++, NMF 등 다양한 알고리즘을 제공한다. 

 

클래스명 설명
SVD 행렬 분해를 통한 잠재 요인 협업 필터링을 위한 SVD 알고리즘
KNNBasic 최근접 이웃 협업 필터링을 위한 KNN 알고리즘
BaselineOnly 사용자 Bias와 아이템 Bias를 고려한 SGD 베이스라인 알고리즘

( http://surprise.readthedocs.io/en/stable/prediction_algorithms_package.html 참고)


5. 베이스라인 평점

개개인의 다양한 평가성향을 반영해 아이템 평가에 편향성(bias) 요소를 반영하여 평점을 부과하는 것을 베이스라인 평점(Baseline Rating)이라고 한다.

보통 베이스라인 평점은 전체 평균 평점 + 사용자 편향 점수 + 아이템 편향 점수 공식으로 계산된다.

 

  • 전체 평균 평점 = 모든 사용자의 아이템에 대한 평점을 평균한 값
  • 사용자 편향 점수 = 사용자별 아이템 평점 평균 값 - 전체 평균 평점
  • 아이템 편향 점수 = 아이템별 평점 평균 값 - 전체 평균 평점

예로 들어, 모든 사용자의 평균적인 영화 평점이 3.5이고, '어벤저스 3편'을 모든 사용자가 평균적으로 평점 4.2로 평가했다면 영화 평가를 늘 까다롭게 평가하여 평균 평점이 3.0인 사용자 A가 '어벤저스 3편'을 어떻게 평가할 것인지 예측하면, 사용자 편향 점수는 3.0 - 3.5 = -0.5 이고, 아이템 편향 점수는 4.2 - 3.5 = 0.7로 계산할 수 있다. 따라서 사용자 A의 '어벤저스 3편'의 베이스라인 평점은 3.5 - 0.5 + 0.7 = 3.7 이다.


6. 교차검증과 하이퍼 파라미터 튜닝

Surprise 는 교차 검증과 하이퍼 파라미터 튜닝을 위해 사이킷런과 유사한 cross_validate() 와 GridSearchCV 클래스를 제공한다.

cross_validate()는 인자로 알고리즘 객체, 데이터, 성능 평가 방법, 폴드 데이터 세트 개수(cv)를 입력받아, 출력결과와 같이 폴드별 성능 평가 수치와 전체 폴드의 평균 성능 평가 수치를 함께 보여준다.

 

from surprise.model_selection import cross_validate
 
# Pandas DataFrame에서 Surprise Dataset으로 데이터 로딩
ratings = pd.read_csv('./ml-latest-small/ratings.csv') # reading data in pandas df
reader = Reader(rating_scale=(0.5, 5.0))
data = Dataset.load_from_df(ratings[['userId', 'movieId', 'rating']], reader)
 
algo = SVD(random_state=0)
cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=5, verbose=True)


또한, Surprise 는 GridSearchCV를 통해 교차 검증을 통한 하이퍼 파라미터 최적화를 수행할 수 있다. 하이퍼 파라미터 최적화는 알고리즘 유형에 따라 다를 수 있지만, SVD 의 경우 주로 SGD의 반복 횟수를 지정하는 n_epochs와 SVD의 잠재 요인 K의 크기를 지정하는 n_factors를 튜닝한다. 

 

아래 예제는 n_epochs:[20, 40, 60], n_factor:[50, 100, 200] 로 변경하면서 CV가 3일 때의 최적 하이퍼 파라미터를 도출한 것이다. n_epochs: 20, n_factor: 50 일 때 3개 폴드의 검증 데이터 세트에서 최적 RMSE가 0.877로 도출되었다.

 

from surprise.model_selection import GridSearchCV
 
# 최적화할 파라미터들을 딕셔너리 형태로 지정.
param_grid = {'n_epochs': [20, 40, 60], 'n_factors': [50, 100, 200] }
 
# CV를 3개 폴드 세트로 지정, 성능 평가는 rmse, mse 로 수행 하도록 GridSearchCV 구성
gs = GridSearchCV(SVD, param_grid, measures=['rmse', 'mae'], cv=3)
gs.fit(data)
 
# 최고 RMSE Evaluation 점수와 그때의 하이퍼 파라미터
print(gs.best_score['rmse'])
print(gs.best_params['rmse'])


7. Surprise 를 이용한 개인화 영화 추천시스템 개발

Surprise을 이용해 잠재 요인 협업 필터링 기반의 개인화된 영화 추천을 구현해본다. 


DatasetAutoFolds 의 build_full_trainset() 메서드를 이용해 학습데이터로 생성하고, SVD 를 이용해 학습을 수행한 후 특정사용자에 영화를 추천하기 위해 아직 보지 않은 영화 목록을 확인하여 추천한다. 이때, 특정사용자는 userId=9 인 사용자로 지정하고, userId 9가 아직 평점을 매기지 않은 영화를 movieId 42로 선정한 뒤 예측 평점을 계산한다.

 

from surprise.dataset import DatasetAutoFolds
 
reader = Reader(line_format='user item rating timestamp', sep=',', rating_scale=(0.5, 5))
# DatasetAutoFolds 클래스를 ratings_noh.csv 파일 기반으로 생성.
data_folds = DatasetAutoFolds(ratings_file='./ml-latest-small/ratings_noh.csv', reader=reader)
 
#전체 데이터를 학습데이터로 생성함.
trainset = data_folds.build_full_trainset()
 
 
algo = SVD(n_epochs=20, n_factors=50, random_state=0)
algo.fit(trainset)
 
 
# 영화에 대한 상세 속성 정보 DataFrame로딩
movies = pd.read_csv('./ml-latest-small/movies.csv')
 
# userId=9 의 movieId 데이터 추출하여 movieId=42 데이터가 있는지 확인.
movieIds = ratings[ratings['userId']==9]['movieId']
if movieIds[movieIds==42].count() == 0:
    print('사용자 아이디 9는 영화 아이디 42의 평점 없음')
 
print(movies[movies['movieId']==42])


아래 코드를 통해 추천 예측 평점은 est 값으로 3.13 인 것을 알 수 있다.

 

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


이제 사용자가 평점을 매기지 않은 전체 영화를 추출한 뒤에 예측 평점순으로 영화를 추천해볼 수 있다.

 

def get_unseen_surprise(ratings, movies, userId):
    #입력값으로 들어온 userId에 해당하는 사용자가 평점을 매긴 모든 영화를 리스트로 생성
    seen_movies = ratings[ratings['userId']== userId]['movieId'].tolist()
     
    # 모든 영화들의 movieId를 리스트로 생성.
    total_movies = movies['movieId'].tolist()
     
    # 모든 영화들의 movieId중 이미 평점을 매긴 영화의 movieId를 제외하여 리스트로 생성
    unseen_movies= [movie for movie in total_movies if movie not in seen_movies]
    print('평점 매긴 영화수:',len(seen_movies), '추천대상 영화수:',len(unseen_movies), \
          '전체 영화수:',len(total_movies))
     
    return unseen_movies
 
unseen_movies = get_unseen_surprise(ratings, movies, 9)


사용자 아이디 9번은 전체 9742개의 영화 중 46개만 평점을 매겼고, 추천 대상 영화는 9696개이며, 이중 앞에서 학습된 추천 알고리즘 클래스인 SVD 를 이용해 높은 예측 평점을 가진 순으로 Top-10 영화를 추천해보면, 다음 결과를 가진다.

 

def recomm_movie_by_surprise(algo, userId, unseen_movies, top_n=10):
    # 알고리즘 객체의 predict() 메서드를 평점이 없는 영화에 반복 수행한 후 결과를 list 객체로 저장
    predictions = [algo.predict(str(userId), str(movieId)) for movieId in unseen_movies]
     
    # predictions list 객체는 surprise의 Predictions 객체를 원소로 가지고 있음.
    # [Prediction(uid='9', iid='1', est=3.69), Prediction(uid='9', iid='2', est=2.98),,,,]
    # 이를 est 값으로 정렬하기 위해서 아래의 sortkey_est 함수를 정의함.
    # 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_movie_ids = [ int(pred.iid) for pred in top_predictions]
    top_movie_rating = [ pred.est for pred in top_predictions]
    top_movie_titles = movies[movies.movieId.isin(top_movie_ids)]['title']
    top_movie_preds = [ (id, title, rating) for id, title, rating in zip(top_movie_ids, top_movie_titles, top_movie_rating)]
     
    return top_movie_preds
 
unseen_movies = get_unseen_surprise(ratings, movies, 9)
top_movie_preds = recomm_movie_by_surprise(algo, 9, unseen_movies, top_n=10)
print('##### Top-10 추천 영화 리스트 #####')
 
for top_movie in top_movie_preds:
    print(top_movie[1], ":", top_movie[2])


 

728x90
반응형