그래프를 추천시스템에 어떠게 활용할까? (심화)

2021. 2. 25. 23:13·AI/이론
그래프를 추천시스템에 어떻게 활용할까 (심화)

그래프를 추천시스템에 어떻게 활용할까? (심화)¶


✅ 추천시스템 기본 복습¶

  • 그래프를 추천시스템에 어떻게 활용할까? (기본)



✅ 넷플릭스 챌린지 소개¶

1️⃣ 넷플릭스 챌린지 데이터셋¶

  • 넷플릭스 챌린지(Netflix Challenge)에서는 사용자별 영화 평점 데이터가 사용되었다.
  • 훈련 데이터(Training Data)는 2000년부터 2005년까지 수집한 48만명 사용자의 1만 8천개의 영화에 대한 1억 개의 평점으로 구성되어 있다.
  • 평가 데이터(Test Data)는 각 사용자의 최신 평점 280만개로 구성되어 있다.

image.png


2️⃣ 넷플릭스 챌린지 대회 소개¶

📌 넷플릭스 챌린지의 목표는 추천시스템의 성능을 10%이상 향상시키는 것이다.¶

  • 평균 제곱근 오차 0.9514을 0.8563까지 낮출 경우 100만불의 상금을 받는 조건이었다.
  • 2006년부터 2009년까지 진행되었으며, 2700개의 팀이 참여하였다.
  • 넷플릭스 챌린지를 통해 추천시스템의 성능이 비약적으로 발전

image.png



✅ 잠재 인수 모형¶

1️⃣ 잠재 인수 모형 개요¶

📌 잠재 인수 모형(Latent Factor Model)의 핵심은 사용자와 상품을 벡터로 표현하는 것이다.¶

image.png


📌 사용자와 영화를 임베딩한 예시이다.¶

image.png


📌 잠재 인수 모형에서는 고정된 인수 대신 효과적인 인수를 학습하는 것을 목표로 한다.¶

  • 학습한 인수를 잠재 인수(Latent Factor)라 부른다.

image.png


2️⃣ 손실 함수¶

📌 사용자와 상품을 임베딩하는 기준은 무엇인가요?¶

  • 사용자와 상품의 임베딩의 내적(Inner Product)이 평점과 최대한 유사하도록 하는 것
  • 사용자 𝑥의 임베딩을 $p_{x}$, 상품 𝑖의 임베딩을 $q_{i}$라고 하자
  • 사용자 𝑥의 상품 𝑖에 대한 평점을 $r_{xi}$라고 하자
  • 임베딩의 목표는 $p_{x}^{T}q_{i}$이 $r_{xi}$와 유사하도록 하는 것이다.


📌 행렬 차원에서 살펴보자¶

  • 사용자 수의 열과 상품 수의 행을 가진 평점 행렬을 𝑅이라고 하자
  • 사용자들의 임베딩, 즉 벡터를 쌓아서 만든 사용자 행렬을 𝑃라고 하자
  • 영화들의 임베딩, 즉 벡터를 쌓아서 만든 상품 행렬을 𝑄라고 하자

image.png


📌 잠재 인수 모형은 다음 손실 함수를 최소화하는 $P$와 $Q$를 찾는 것이 목표이다.¶

image.png

하지만, 위 손실 함수를 사용할 경우 과적합(Overfitting)이 발생할 수 있다. 과적합이란 기계학습 모형이 훈련 데이터의 잡음(Noise)까지 학습하여, 평가 성능은 오히려 감소하는 현상을 의미한다.


📌 과적합을 방지하기 위하여 정규화 항을 손실 함수에 더해준다.¶

image.png


정규화는 극단적인, 즉 절댓값이 너무 큰 임베딩을 방지하는 효과가 있다.

image.png


3️⃣ 최적화¶

📌 손실함수를 최소화하는 $P$와 $Q$를 찾기 위해서는 (확률적) 경사하강법을 사용한다.¶

  • 경사하강법은 손실함수를 안정적으로 하지만 느리게 감소시킨다.
  • 확률적 경사하강법은 손실함수를 불안정하지만 빠르게 감소시킨다.
  • 실제로는 확률적 경사하강법이 더 많이 사용된다.

image.png

🔎 넷플릭스 챌린지에서 결과를 확인해 보면 아래와 같이 감소한 것을 볼 수 있다.

image.png



✅ 고급 잠재 인수 모형¶

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개의 별이다.


📌개선된 잠재 인수 모형에서는 평점을 전체 평균, 사용자 편향, 상품 편향, 상호작용으로 분리한다.¶

image.png

따라서 개선된 잠재 인수 모형의 손실 함수는 아래와 같다.

image.png

🔎 사용자, 상품의 편향을 고려한 결과를 확인해 보면 아래와 같다.

image.png


2️⃣ 시간적 편향을 고려한 잠재 인수 모형¶

📌넷플릭스 시스템의 변화로 평균 평점이 크게 상승하는 사건이 있었다.¶

image.png

📌영화의 평점은 출시일 이후 시간이 지남에 따라 상승하는 경향을 갖는다.¶

image.png


📌 개선된 잠재 인수 모형에서는 이러한 시간적 편향을 고려한다.¶

  • 구체적으로 사용자 편향과 상품 편향을 시간에 따른 함수로 가정한다.

image.png

🔎 시간적 편향을 고려한 결과를 확인해 보면 아래와 같다.

image.png



✅ 넷플릭스 챌린지의 결과¶

1️⃣ 앙상블 학습¶

📌BellKor 팀은 앙상블 학습을 사용하여 처음으로 목표 성능에 도달¶

image.png



✅ 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)
### Rating Dataset Format ###

userId movieId rating timestamp
0 1 1 4.0 964982703
1 1 3 4.0 964981247
2 1 6 4.0 964982224
3 1 47 5.0 964983815
4 1 50 5.0 964982931
### Rating Dataset - Timestamp Removed ###

userId movieId rating
0 1 1 4.0
1 1 3 4.0
2 1 6 4.0
3 1 47 5.0
4 1 50 5.0
... ... ... ...
100831 610 166534 4.0
100832 610 168248 5.0
100833 610 168250 5.0
100834 610 168252 5.0
100835 610 170875 3.0

100836 rows × 3 columns

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())
### Movie Dataset Format ###

Columns of Movie Dataset :  Index(['movieId', 'title', 'genres'], dtype='object')

movieId title genres
0 1 Toy Story (1995) Adventure|Animation|Children|Comedy|Fantasy
1 2 Jumanji (1995) Adventure|Children|Fantasy
2 3 Grumpier Old Men (1995) Comedy|Romance
3 4 Waiting to Exhale (1995) Comedy|Drama|Romance
4 5 Father of the Bride Part II (1995) Comedy
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))
num users: 610, num items:9724
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)
        userId  movieId  rating
0            1        1     4.0
1            1        3     4.0
2            1        6     4.0
3            1       47     5.0
4            1       50     5.0
...        ...      ...     ...
100838     900   106696     5.0
100839     900   166461     5.0
100840     900      595     5.0
100841     900   138168     1.0
100842     900     1997     1.0

[100843 rows x 3 columns]
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))
num users: 611, num items:9725

훈련 데이터와 평가 데이터를 분리¶

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))
<class 'surprise.dataset.DatasetAutoFolds'>
<class 'surprise.trainset.Trainset'>
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))
<class 'surprise.trainset.Trainset'>
<class 'surprise.dataset.DatasetAutoFolds'>

하이퍼파라미터를 탐색한다.¶

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'])
0.8783606583322707
{'n_factors': 10}
  • 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)
{'rmse': {'n_factors': 10}, 'mae': {'n_factors': 10}}
Out[10]:
<surprise.prediction_algorithms.matrix_factorization.SVD at 0x21d8e64cfc8>

모델 예측¶

In [11]:
##### algorithm prediction #####
prediction = algorithm.test(test)
for p in prediction[:5]:            # prediction 결과값 5개 미리보기
    print(p)                        # r_ui : 실제 rating 값, est: 예측된 rating 값
user: 475        item: 87232      r_ui = 3.50   est = 4.60   {'was_impossible': False}
user: 47         item: 7160       r_ui = 3.00   est = 3.11   {'was_impossible': False}
user: 414        item: 1805       r_ui = 4.00   est = 2.79   {'was_impossible': False}
user: 119        item: 131714     r_ui = 4.00   est = 3.89   {'was_impossible': False}
user: 549        item: 2571       r_ui = 5.00   est = 4.03   {'was_impossible': False}
In [12]:
#### 특정 user, 특정 item에 대한 prediction 값 ###
uid = 800
iid = 8368
prediction_user_item = algorithm.predict(uid, iid)
print(prediction_user_item)   
user: 800        item: 8368       r_ui = None   est = 4.08   {'was_impossible': False}

영화 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)
movid id: Shawshank Redemption, The (1994), movie genre: Crime|Drama,predicted rating: 4.389791615923516
movid id: Dr. Strangelove or: How I Learned to Stop Worrying and Love the Bomb (1964), movie genre: Comedy|War,predicted rating: 4.37331233374186
movid id: Lawrence of Arabia (1962), movie genre: Adventure|Drama|War,predicted rating: 4.371576287772258
movid id: Cool Hand Luke (1967), movie genre: Drama,predicted rating: 4.359722965134315
movid id: Casablanca (1942), movie genre: Drama|Romance,predicted rating: 4.319131423502716
movid id: Usual Suspects, The (1995), movie genre: Crime|Mystery|Thriller,predicted rating: 4.297686128819139
movid id: Spirited Away (Sen to Chihiro no kamikakushi) (2001), movie genre: Adventure|Animation|Fantasy,predicted rating: 4.297640790120129
movid id: Streetcar Named Desire, A (1951), movie genre: Drama,predicted rating: 4.291907457874597
movid id: Monty Python and the Holy Grail (1975), movie genre: Adventure|Comedy|Fantasy,predicted rating: 4.288184773024098
movid id: Fight Club (1999), movie genre: Action|Crime|Drama|Thriller,predicted rating: 4.285619921616426

'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
'AI/이론' 카테고리의 다른 글
  • 그래프 신경망이란 무엇일까? (심화)
  • 그래프 신경망이란 무엇일까? (기본)
  • 그래프의 정점을 어떻게 벡터로 표현할까?
  • 그래프를 추천시스템에 어떻게 활용할까?(기본)
N-analyst
N-analyst
  • N-analyst
    개발자CuCu
    N-analyst
  • 전체
    오늘
    어제
  • 공지사항

    • 티스토리에서 원하는 글 찾는 방법
    • 분류 전체보기 (140)
      • 티스토리 (4)
      • 알고리즘 (5)
        • 알고리즘 정리 (1)
        • 백준 (4)
      • 마크다운(Typora) (13)
        • 사용법 (13)
      • 에러 (1)
        • 파이썬 (1)
      • 데이터 분석 (5)
        • python_analysis (3)
        • Machine Learning (2)
      • AI (109)
        • 파이토치로 시작하는 딥러닝 기초 (2)
        • 부스트 캠프 AI tech (41)
        • 이론 (66)
      • 파이썬(python) (1)
        • 기타 (1)
      • 웹 프로그래밍 (1)
        • 설정 팁 (1)
  • 블로그 메뉴

    • 홈
    • 태그
  • 인기 글

  • 최근 글

  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.6
N-analyst
그래프를 추천시스템에 어떠게 활용할까? (심화)
상단으로

티스토리툴바