< 목차 >
- 용어정의
- Surprise 패키지 소개
- Surprise 주요 모듈
- OS 파일데이터를 Surprise 데이터셋으로 로딩
- Pandas Dataframe 을 Surprise 데이터셋으로 로딩
- Surprise 추천 알고리즘 클래스
- 베이스라인 평점
- 교차검증과 하이퍼 파라미터 튜닝
- Surprise 를 이용한 개인화 영화 추천시스템 개발
1. 용어정의
- 피드백 후 작성예정입니다.
- MovieLens 데이터셋은 데이터셋 자료실 에 있습니다.
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])
'인공지능 > 추천시스템' 카테고리의 다른 글
4. 행렬 분해를 이용한 잠재요인 협업 필터링 (4) | 2021.04.07 |
---|---|
3. 아이템 기반 최근집 이웃 협업 필터링 (10) | 2021.04.06 |
2. 콘텐츠 기반 필터링 (0) | 2021.04.06 |
1. 추천시스템 개요 (2) | 2021.04.06 |