그래프를 추천시스템에 어떻게 활용할까? (심화)¶
✅ 추천시스템 기본 복습¶
✅ 넷플릭스 챌린지 소개¶
1️⃣ 넷플릭스 챌린지 데이터셋¶
- 넷플릭스 챌린지(Netflix Challenge)에서는 사용자별 영화 평점 데이터가 사용되었다.
- 훈련 데이터(Training Data)는 2000년부터 2005년까지 수집한 48만명 사용자의 1만 8천개의 영화에 대한 1억 개의 평점으로 구성되어 있다.
- 평가 데이터(Test Data)는 각 사용자의 최신 평점 280만개로 구성되어 있다.
2️⃣ 넷플릭스 챌린지 대회 소개¶
📌 넷플릭스 챌린지의 목표는 추천시스템의 성능을 10%이상 향상시키는 것이다.¶
- 평균 제곱근 오차 0.9514을 0.8563까지 낮출 경우 100만불의 상금을 받는 조건이었다.
- 2006년부터 2009년까지 진행되었으며, 2700개의 팀이 참여하였다.
- 넷플릭스 챌린지를 통해 추천시스템의 성능이 비약적으로 발전
✅ 잠재 인수 모형¶
1️⃣ 잠재 인수 모형 개요¶
📌 잠재 인수 모형(Latent Factor Model)의 핵심은 사용자와 상품을 벡터로 표현하는 것이다.¶
📌 사용자와 영화를 임베딩한 예시이다.¶
2️⃣ 손실 함수¶
📌 사용자와 상품을 임베딩하는 기준은 무엇인가요?¶
- 사용자와 상품의 임베딩의 내적(Inner Product)이 평점과 최대한 유사하도록 하는 것
- 사용자 𝑥의 임베딩을 $p_{x}$, 상품 𝑖의 임베딩을 $q_{i}$라고 하자
- 사용자 𝑥의 상품 𝑖에 대한 평점을 $r_{xi}$라고 하자
- 임베딩의 목표는 $p_{x}^{T}q_{i}$이 $r_{xi}$와 유사하도록 하는 것이다.
📌 행렬 차원에서 살펴보자¶
- 사용자 수의 열과 상품 수의 행을 가진 평점 행렬을 𝑅이라고 하자
- 사용자들의 임베딩, 즉 벡터를 쌓아서 만든 사용자 행렬을 𝑃라고 하자
- 영화들의 임베딩, 즉 벡터를 쌓아서 만든 상품 행렬을 𝑄라고 하자
📌 잠재 인수 모형은 다음 손실 함수를 최소화하는 $P$와 $Q$를 찾는 것이 목표이다.¶
하지만, 위 손실 함수를 사용할 경우 과적합(Overfitting)이 발생할 수 있다. 과적합이란 기계학습 모형이 훈련 데이터의 잡음(Noise)까지 학습하여, 평가 성능은 오히려 감소하는 현상을 의미한다.
📌 과적합을 방지하기 위하여 정규화 항을 손실 함수에 더해준다.¶
정규화는 극단적인, 즉 절댓값이 너무 큰 임베딩을 방지하는 효과가 있다.
3️⃣ 최적화¶
📌 손실함수를 최소화하는 $P$와 $Q$를 찾기 위해서는 (확률적) 경사하강법을 사용한다.¶
- 경사하강법은 손실함수를 안정적으로 하지만 느리게 감소시킨다.
- 확률적 경사하강법은 손실함수를 불안정하지만 빠르게 감소시킨다.
- 실제로는 확률적 경사하강법이 더 많이 사용된다.
🔎 넷플릭스 챌린지에서 결과를 확인해 보면 아래와 같이 감소한 것을 볼 수 있다.
✅ 고급 잠재 인수 모형¶
1️⃣ 사용자와 상품의 편향을 고려한 잠재 인수 모형¶
📌 각 사용자의 편향은 해당 사용자의 평점 평균과 전체 평점 평균의 차이다.¶
- 나연이 매긴 평점의 평균이 4.0개의 별,
- 다현이 매긴 평점의 평균이 3.5개의 별이라고 하자.
전체 평점 평균이 3.7개의 별인 경우,
- 나연의 사용자 편향은 4.0 - 3.7 = 0.3개의 별이고
- 다현의 사용자 편향은 3.5 - 3.7 = -0.2개의 별이다.
📌 각 상품의 편향은 해당 상품에 대한 평점 평균과 전체 평점 평균의 차이다.¶
- 영화 식스센스에 대한 평점의 평균이 4.5개의 별,
- 영화 클레멘타인이 매긴 평점의 평균이 3.0개의 별이라고 하자.
전체 평점 평균이 3.7개의 별인 경우,
- 식스센스의 상품 편향은 4.5 – 3.7 = 0.8개의 별이고
- 클레멘타인의 상품 편향은 3.0 - 3.7 = -0.7개의 별이다.
📌개선된 잠재 인수 모형에서는 평점을 전체 평균, 사용자 편향, 상품 편향, 상호작용으로 분리한다.¶
따라서 개선된 잠재 인수 모형의 손실 함수는 아래와 같다.
🔎 사용자, 상품의 편향을 고려한 결과를 확인해 보면 아래와 같다.
2️⃣ 시간적 편향을 고려한 잠재 인수 모형¶
📌넷플릭스 시스템의 변화로 평균 평점이 크게 상승하는 사건이 있었다.¶
📌영화의 평점은 출시일 이후 시간이 지남에 따라 상승하는 경향을 갖는다.¶
📌 개선된 잠재 인수 모형에서는 이러한 시간적 편향을 고려한다.¶
- 구체적으로 사용자 편향과 상품 편향을 시간에 따른 함수로 가정한다.
🔎 시간적 편향을 고려한 결과를 확인해 보면 아래와 같다.
✅ 넷플릭스 챌린지의 결과¶
1️⃣ 앙상블 학습¶
📌BellKor 팀은 앙상블 학습을 사용하여 처음으로 목표 성능에 도달¶
✅ Surprise 라이브러리와 잠재 인수모형의 활용¶
라이브러리 로드¶
In [1]:
import numpy as np
import pandas as pd
from surprise import SVD
from surprise.model_selection import train_test_split
from surprise.dataset import DatasetAutoFolds
from surprise.model_selection import cross_validate
from surprise import Dataset, Reader
from surprise import accuracy
데이터 로드(100,000개의 평점으로 구성)¶
In [2]:
#### 데이터셋 불러오기(MovieLens 10k) ####
df_ratings = pd.read_csv('../실습코드/data/others/ratings.csv')
#### 평점 데이터셋 형태 확인####
# surprise library의 Reader 사용 시 반드시 사용자-아이템-평점 순으로 정보가 들어가 있어야 함
print("### Rating Dataset Format ###", end='\n\n')
display(df_ratings.head())
df_ratings.drop(['timestamp'], axis=1, inplace=True)
print("### Rating Dataset - Timestamp Removed ###", end='\n\n')
display(df_ratings)
In [3]:
df_movies = pd.read_csv('../실습코드/data/others/movies.csv')
#### 영화 데이터셋 형태 확인 ####
print("### Movie Dataset Format ###", end = '\n\n')
print("Columns of Movie Dataset : ",df_movies.columns, end = '\n\n')
display(df_movies.head())
In [4]:
#### Dataset의 User, Movie 수 확인 ####
n_users = df_ratings.userId.unique().shape[0]
n_items = df_ratings.movieId.unique().shape[0]
print("num users: {}, num items:{}".format(n_users, n_items))
In [5]:
### Add Your Own Data ###
###################################### Example 1#################################################
# User 800 is a HUGE fan of Musical Movies
rows = [] # row = [user_id, movie_id, rating]
user_id = 800
rows.append([user_id, 73, 5]) # movie 73: Miserables, Les (1995)
rows.append([user_id, 107780, 5]) # movie 107780: Cats(1998)
rows.append([user_id, 588, 5]) # movie 588: Aladin(1992)
rows.append([user_id, 60397, 5]) # movie 69397: Mamma Mia!(2008)
rows.append([user_id, 99149, 5]) # movie 99149: Miserables, Les (2012)
rows.append([user_id, 138186, 1]) # movie 138186: Sorrow(2015)
rows.append([user_id, 1997, 1]) # movie 1997: Scream 2 (1991)
##################################################################################################
###################################### Example 2#################################################
# User 900 is a HUGE fan of Animation Movies
rows = [] # row = [user_id, movie_id, rating]
user_id = 900
rows.append([user_id, 1022, 5]) # movie 1022: Cinderella(1950)
rows.append([user_id, 594, 5]) # movie 594: Snow White and the Seven Dwarfs(1937)
rows.append([user_id, 106696, 5]) # movie 106696: Frozen(2013)
rows.append([user_id, 166461, 5]) # movie 166461: Moana(2016)
rows.append([user_id, 595, 5]) # movie 595: Beauty and the Beast (1991)
rows.append([user_id, 138168, 1]) # movie 138168: Sorrow(2015) 위 데이터에 없는 영화
rows.append([user_id, 1997, 1]) # movie 1997: Scream 2 (1991)
##################################################################################################
########################### Add Your Own Ratings using 'movie.csv' data #########################
# my_rows = []
# my_id = 2021
# rows.append([user_id, ,]) # Fill your movie id and rating
# rows.append([user_id, ,]) # 자신이 평가할 영화의 id와 점수를 입력한다..
# rows.append([user_id, ,])
# rows.append([user_id, ,])
# rows.append([user_id, ,])
##################################################################################################
for row in rows:
df_ratings = df_ratings.append(pd.Series(row, index=df_ratings.columns), ignore_index=True)
print(df_ratings)
In [6]:
#### Dataset의 User, Movie 수 확인 ####
n_users = df_ratings.userId.unique().shape[0]
n_items = df_ratings.movieId.unique().shape[0]
print("num users: {}, num items:{}".format(n_users, n_items))
훈련 데이터와 평가 데이터를 분리¶
In [7]:
#### pandas dataframe을 surprise dataset 형태로 바꿔준 후, train set과 test set을 split 해준다 ####
reader = Reader(rating_scale=(0, 5))
data = Dataset.load_from_df(df_ratings[['userId','movieId','rating']], reader=reader)
train, test = train_test_split(data, test_size=0.2, shuffle=True)
print(type(data))
print(type(train))
In [8]:
## Grid Search를 위해 surprise.trainset 형태의 데이터를 surprise.dataset으로 변경해준다
iterator = train.all_ratings()
train_df = pd.DataFrame(columns=['userId', 'movieId', 'rating'])
i = 0
for (uid, iid, rating) in iterator:
train_df.loc[i] = [train.to_raw_uid(int(uid)), train.to_raw_iid(iid), rating]
i = i+1
train_data = Dataset.load_from_df(train_df, reader=reader)
print(type(train))
print(type(train_data))
하이퍼파라미터를 탐색한다.¶
In [9]:
### Hyperparameter Grid Search ###
from surprise.model_selection import GridSearchCV
param_grid = {'n_factors': [10,15,20,30,50,100]}
grid = GridSearchCV(SVD, param_grid, measures = ['rmse', 'mae'], cv=4)
grid.fit(train_data)
print(grid.best_score['rmse'])
print(grid.best_params['rmse'])
n_factors
: 임베딩 공간의 차원을 설정해준다.
잠재 인수 모형 학습¶
In [10]:
### Use the Hyperparameter with best performance ###
print(grid.best_params)
algorithm = SVD(grid.best_params['rmse']['n_factors'])
algorithm.fit(train)
Out[10]:
모델 예측¶
In [11]:
##### algorithm prediction #####
prediction = algorithm.test(test)
for p in prediction[:5]: # prediction 결과값 5개 미리보기
print(p) # r_ui : 실제 rating 값, est: 예측된 rating 값
In [12]:
#### 특정 user, 특정 item에 대한 prediction 값 ###
uid = 800
iid = 8368
prediction_user_item = algorithm.predict(uid, iid)
print(prediction_user_item)
영화 ID로 부터 영화 제목을 얻기 위한 변수 설정¶
In [13]:
#### Get Movid Name from Movie ID - 영화 ID로부터 영화 제목 얻기 ###
movie_set = set()
ratings = np.zeros((n_users, n_items))
for (_, movie_id, _) in df_ratings.itertuples(index=False):
movie_set.add(movie_id)
movie_id_to_name=dict()
movie_id_to_genre=dict()
for (movie_id, movie_name, movie_genre) in df_movies.itertuples(index=False):
if movie_id not in movie_set: # 어떤 영화가 rating data에 없는 경우 skip
continue
movie_id_to_name[movie_id] = movie_name
movie_id_to_genre[movie_id] = movie_genre
In [14]:
#해당 user가 아직 보지 않은 영화를 return해주는 함수
def get_unseen_movies(data, user_id):
watched_movies = set()
total_movies = set()
for (uid, iid, rating) in data.all_ratings():
total_movies.add(iid)
if uid == user_id:
watched_movies.add(iid)
unseen_movies = total_movies - watched_movies
return unseen_movies
# return total_movies
In [15]:
# 특정 user에게 top k개의 영상을 추천해주는 함수
def recommend(train, algorithm, user_id, top_k=10):
unseen_movies = get_unseen_movies(train, user_id)
prediction = [algorithm.predict(user_id, movie_id) for movie_id in unseen_movies]
prediction.sort(key=lambda x:x.est, reverse=True)
for _, movie, _, pred, _ in prediction[:top_k]:
print("movid id: {}, movie genre: {},predicted rating: {}".format(movie_id_to_name[movie], movie_id_to_genre[movie], pred))
In [16]:
# 800번 유저 추천 결과
recommend(train, algorithm, user_id=800, top_k=10)
'AI > 이론' 카테고리의 다른 글
Annotation data efficient learning (2) | 2021.03.08 |
---|---|
Image classification I (0) | 2021.03.08 |
그래프 신경망이란 무엇일까? (심화) (0) | 2021.02.26 |
그래프 신경망이란 무엇일까? (기본) (1) | 2021.02.26 |
그래프의 정점을 어떻게 벡터로 표현할까? (0) | 2021.02.25 |
그래프를 추천시스템에 어떻게 활용할까?(기본) (0) | 2021.02.24 |
그래프의 구조를 어떻게 분석할까? (0) | 2021.02.24 |
그래프를 바이럴 마케팅에 어떻게 활용할까? (0) | 2021.02.23 |