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

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


✅ 내용 기반 추천시스템

1️⃣ 내용 기반 추천시스템의 원리

📌 내용 기반 추천은 각 사용자가 구매/만족했던 상품과 유사한 것을 추천하는 방법이다.

  • 동일한 장르의 영화를 추천하는 것
  • 동일한 감독의 영화 혹은 동일 배우가 출현한 영화를 추천하는 것
  • 동일한 카테고리의 상품을 추천하는 것
  • 동갑의 같은 학교를 졸업한 사람을 친구로 추천하는 것


📌 내용 기반 추천은 다음 네 가지 단계로 이루어 진다.

image.png


1. 첫 단계는 사용자가 선호했던 상품들의 상품 프로필(Item Profile)을 수집하는 단계

image.png

  • 어떤 상품의 상품 프로필이란 해당 상품의 특성을 나열한 벡터이다.
  • 영화의 경우 감독, 장르, 배우 등의 원-핫 인코딩이 상품 프로필이 될 수 있다. image.png



2. 사용자 프로필(User Profile)을 구성하는 단계

image.png

  • 사용자 프로필은 선호한 상품들의 상품 프로필을 선호도를 사용하여 가중 평균하여 계산한다.
  • 즉, 사용자 프로필 역시 벡터이다.
  • 앞선 영화 프로필 예시에서는 담음과 같은 형태의 사용자 프로필을 얻을 수 있다. image.png



3. 사용자 프로필과 다른 상품들의 상품 프로필을 매칭하는 단계

image.png

  • 사용자 프로필 벡터 $\vec{u}$와 상품 프로필 벡터 $\vec{v}$ 코사인 유사도 ${\vec{u}\cdot\vec{v} \over \parallel\vec{u}\parallel\parallel\vec{v}\parallel}$를 계산한다.
  • 즉, 두 벡터의 사이각의 코상인 값을 계산한다.
  • 코사인 유사도가 높을 수록, 해당 사용자가 과거 선호했던 상품들과 해당 삼품이 유사함을 의미한다. image.png



4. 사용자에게 상품을 추천하는 단계

image.png

  • 계산한 코사인 유사도가 높은 상품을 추천해 준다.


📌 내용 기반 추천시스템의 장점

  • 다른 사용자의 구매 기록이 필요하지 않는다.
  • 독특한 취향의 사용자에게도 추천이 가능하다.
  • 새 상품에 대해서도 추천이 가능하다.
  • 추천의 이유를 제공할 수 있다.
    • 예시: 당신은 로맨스 영화를 선호했기 때문에, 새로운 로맨스 영화를 추천합니다.

📌 내용 기반 추천시스템의 단점

  • 상품에 대한 부가 정보가 없는 경우에는 사용할 수 없다.
  • 구매 기록이 없는 사용자에게는 사용할 수 없다.
  • 과적합으로 지나치게 협소한 추천을 할 위험이 있다.



✅ 협업 필터링

1️⃣ 협업 필터링의 원리

📌 사용자-사용자 협업 필터링은 다음 3단계로 이루어 진다.

image.png

추천의 대상 사용자를 Mr.A 라고 하자

  1. 우선 Mr.A와 유사한 취향의 사용자들을 찾는다.
  2. 유사한 취향의 사용자들이 선호한 상품을 찾는다.
  3. 이 상품들을 Mr.A에게 추천한다.


📌 사용자-사용자 협업 필터링의 핵심은 유사한 취향의 사용자를 찾는 것이다.

image.png

위 예시는 사용자 별 영화 평점이다.
"?"는 평점이 입력되지 않은 경우를 의미한다.
지수와 제니의 취향이 유사하고, 로제는 둘과 다른 취향을 가진 것을 알 수 있다.


📌 취향의 유사성은 상과 계수(CorrelationCoefficient)를 통해 측정한다.

  • 사용자 $x$의 상품 $s$에 대한 평점을 $r_{xs}$라고 하자
  • 사용자 $x$가 매긴 평균 평점을 $\bar{r_{x}}$라고 하자
  • 사용자 $x$와 $y$가 공동 구매한 상품들을 $S_{xy}$라 하자

사용자 $x$와 $y$의 취향의 유사도는 아래 수식으로 계산된다.

$$sim(x,y) = {\sum_{s∈S_{xy}}(r_{xs}-\bar{r_{x}})(r_{ys}-\bar{r_{y}}) \over \sqrt{\sum_{s∈S_{xy}}(r_{xs}-\bar{r_{x}})^{2}}\sqrt{\sum_{s∈S_{xy}}(r_{ys}-\bar{r_{y}})^{2}}}$$

즉, 통계에서의 상관 계수(Correlation Coefficient)를 사용해 취향의 유사도를 계산한다.


🔎 예시에서 취향의 유사도를 확인해 보자

image.png

  • 지수와 제니가 같이 본 반지의 제왕, 겨울왕국, 해리포터에 대해서 계산이 진행 된다.

image.png

가장 먼저 지수와 제니의 평균 점수를 알아야 한다.

  • 지수의 평균 평점 = (4+1+2+5)/4 = 3
  • 제니의 평균 평점 = (5+1+4+2)/4 = 3

이후 위 식대로 계산하면 지수와 로제의 취향의 유사도는 0.88로 둘의 취향은 매우 유사하다.



image.png

image.png

  • 지수와 로제의 취향의 유사도는 -0.94이고 둘의 취향은 매우 상이하다.



image.png

  • 따라서, 지수의 취향을 추정할 때는 제니의 취향을 참고하게 된다.
  • 예를 들명, 지수는 미녀와 야수를 좋아할 확률이 낮다.
  • 지수는 제니와 취향이 유사하고, 제니는 미녀와 야수를 좋아하지 않았기 때문이다.


📌 구체적으로 취향의 유사도를 가중치로 사용한 평점의 가중 평균을 통해 평점을 추정한다.

🔎 사용자 $x$의 상품 $s$에 대한 평점 $r_{xs}$를 추정하는 경우를 생각해 보자!!

  • 앞서 설명한 상관 계수를 이용하여 상품 $s$를 구매한 사용자 중에 $x$와 취향이 가장 유사한 k명의 사용자 $N(x;s)$를 뽑는다.
  • 평점 $r_{xs}$는 아래의 수식을 이용해 추정한다.

$$\hat{r_{xs}} = {\sum_{y∈N(x;s)}sim(x,y)\cdot{r_{ys}} \over \sum_{y∈N(x;s)}sim(x,y)}$$

즉, 취향의 유사도를 가중치로 사용한 평점의 가중 평균을 계산한다.


📌 추정한 평점이 가장 높은 상품을 추천하는 단계

  • 추천의 대상 사용자를 x라고 하자
  • 앞서 설명한 방법을 통해, x가 아직 구매하지 않은 상품 각각에 대해 평점을 추정한다.
  • 추정한 펴점이 가장 높은 상품들을 x에게 추천한다.


📌 협업 필터링의 장단점

$\color{blue}{(+)}$ 상품에 대한 부가 정보가 없는 경우에도 사용할 수 있다.

$\color{red}{(-)}$ 충분한 수의 펴점 데이터가 누적되어야 효과적이다.
$\color{red}{(-)}$ 새 상품, 새로운 사용자에 대한 추천이 불가능하다.
$\color{red}{(-)}$ 독특한 취향의 사용자에게 추천이 어렵다.



✅ 추천 시스템의 평가

1️⃣ 데이터 분리

📌 추천 시스템의 정확도는 어떻게 평가할까?

image.png

  • 가장 먼저 훈련 데이터와 평가 데이터로 분리한다.
  • 이때 행은 사용자를 의미하고 열은 각각의 상품를 의미한다.
  • 해당 원소는 사용자가 해당 상품에 준 점수를 의미한다.


image.png

  • 평가 데이터는 따로 주어지지 않았다고 가정한다.
  • 훈련 데이터를 이용해서 가리워진 평가 데이터의 평점을 추정한다.
  • 이를 토대로 원래의 실제 평가 데이터와 추정한 평점을 비교하여 오차를 측정한다.


2️⃣ 평가 지표

📌 추정한 평점과 실제 평가 데이터를 비교하여 오차를 측정

  • 오차를 측정하는 지표로는 평균 제곱 오차(Mean Squared Error, MSE)가 많이 사용된다.
  • 평가 데이터 내의 평점들을 집함 $T$라고 하자
  • 평균 제곱 오차는 아래 수식으로 계산된다. image.png
  • 평균 제곱근 오차(Root Mean Squared Error,RMSE)도 많이 사용된다. image.png


📌 이 밖에도 다양한 지표가 사용된다.

  • 추정한 평점으로 순위를 매긴 후, 실제 평점으로 매긴 순위와의 상관 계수를 계산하기도 한다.
  • 추천한 상품 중 실제 구매로 이루어진 것의 비율을 측정하기도 한다.
  • 추천의 순서 혹은 다양성까지 고려하는 지표들도 사용된다.



✅ 협업 필터링 구현

라이브러리 로드

In [1]:
import numpy as np 
import pandas as pd
from math import sqrt
from sklearn.metrics import mean_squared_error

데이터 불러오기

In [2]:
# 데이터셋 불러오기(MovieLens 100k)
df_ratings = pd.read_csv('../실습코드/data/others/ratings.csv')

# 평점 데이터셋 형태 확인
print("### Rating Dataset Format ###")
display(df_ratings.head())
df_ratings.drop(['timestamp'], axis=1, inplace=True)


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())
### 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
### 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 [3]:
# 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
  • 유저는 610명, 영화는 9724개가 있다.

데이터 전처리

In [4]:
# 데이터 전처리 
# user id, movie id의 범위를 (0 ~ 사용자 수 -1), (0 ~ 영화 수 -1) 사이로 맞춰줌.

user_dict = dict()      # {user_id : user_idx}, user_id : original data에서 부여된 user의 id, user_idx : 새로 부여할 user의 id
movie_dict = dict()     # {movie_id: movie_idx}, movie_id : original data에서 부여된 movie의 id, movie_idx: 새로 부여할 movie의 id
user_idx = 0
movie_idx = 0
ratings = np.zeros((n_users, n_items))
for row in df_ratings.itertuples(index=False):
    user_id, movie_id, _ = row
    if user_id not in user_dict:
        user_dict[user_id] = user_idx
        user_idx += 1
    if movie_id not in movie_dict:
        movie_dict[movie_id] = movie_idx
        movie_idx += 1
    ratings[user_dict[user_id], movie_dict[movie_id]] = row[2]
user_idx_to_id = {v: k for k, v in user_dict.items()}

movie_idx_to_name=dict()
movie_idx_to_genre=dict()
for row in df_movies.itertuples(index=False):
    movie_id, movie_name, movie_genre = row
    if movie_id not in movie_dict:              # 어떤 영화가 rating data에 없는 경우 skip
        continue
    movie_idx_to_name[movie_dict[movie_id]] = movie_name 
    movie_idx_to_genre[movie_dict[movie_id]] = movie_genre

학습 데이터와 평가 데이터를 분리

In [5]:
######################################################################################################################################
# Training Set과 Test Set을 분리해 주는 함수
######################################################################################################################################
def train_test_split(ratings):
    test = np.zeros_like(ratings)
    train = ratings.copy()
    for x in range(ratings.shape[0]):
        nonzero_idx = ratings[x, :].nonzero()[0]
        test_ratings = np.random.choice(nonzero_idx, 
                                        size=int(len(nonzero_idx)/5),  
                                        replace=False)
        train[x, test_ratings] = 0.
        test[x, test_ratings] = ratings[x, test_ratings]
        
    assert(np.all((train * test) == 0))     # train set과 test set이 완전히 분리되었는지 확인

    return train, test

유저 별 평점에서 평균 평점을 빼주는 함수를 만든다.

image.png

In [6]:
######################################################################################################################################
# Pearson 상관계수를 계산하기 위해 평균 값을 빼줌.
# (유저별로 평점을 주는 기준이 다를 수 있으므로, 유저 별 평균 평점 값을 실제 평점 값에서 빼준다)
######################################################################################################################################
def subtract_mean(ratings):

    mean_subtracted_ratings = np.zeros_like(ratings)
    
    for i in range(ratings.shape[0]):
        nonzero_idx = ratings[i].nonzero()[0]                       # rating 값이 존재하는(0이 아닌) index 추출
        sum_ratings = np.sum(ratings[i])          
        num_nonzero = len(nonzero_idx)
        avg_rating = sum_ratings / num_nonzero                      # rating 값들의 평균값 계산
        if num_nonzero == 0: 
            print("No Rating: ", i)                    
            avg_rating = 0
        mean_subtracted_ratings[i, nonzero_idx] = ratings[i, nonzero_idx] - avg_rating 
                                                                    # 원 rating matrix에서 평균 값을 빼줌

    return mean_subtracted_ratings

이제 전체 유사도를 계산하는 함수

image.png

In [7]:
from tqdm import tqdm

######################################################################################################################################
# 두 rating의 Pearson Correlation을 값으로 갖는 similarity matrix를 생성하여 return해주는 함수
######################################################################################################################################
def collaborative_filtering(ratings):
    similarity = np.zeros((ratings.shape[0], ratings.shape[0]))                             # user-user collaborative filtering : (num_user, num_user)
    num_r, num_c = ratings.shape
    ############################ Fill in Your Code ###############################
    for i in tqdm(range(num_r)):
        for j in range(i+1, num_r):
            sum_i = 0
            sum_j = 0
            dot_product = 0
            for k in range(num_c):
                if ratings[i,k] !=0 and ratings[j,k] != 0:
                    sum_i += ratings[i,k]**2
                    sum_j += ratings[j,k]**2
                    dot_product += ratings[i,k] * ratings[j,k]
                
            if dot_product!=0 : 
                similarity[i,j] = dot_product / sqrt(sum_i) / sqrt(sum_j)
                similarity[j,i] = similarity[i,j]
            #print("i:{}, j:{}".format(i,j))
    ################################################################################
    return similarity

유사도를 사용한 가중 평균을 통해 사용자 - 영화 쌍 각각에 대해 점수를 추정

image.png

In [8]:
from tqdm import tqdm

######################################################################################################################################
# collaborative filtering을 통해 구한 similarity matrix와 주어진 rating matrix를 사용하여 rating을 예측하는 함수
# 주어진 유저(영화)와의 Pearson Correlation이 양수인 유저(영화) 중, 
# 본인을 제외한 top k개의 rating을 similarity에 따라 weighted sum해주어 점수를 예측
######################################################################################################################################
def predict(ratings, similarity, k=10):

    pred = np.zeros(ratings.shape)

    ############################ Fill in Your Code ###############################

    for u in tqdm(range(ratings.shape[0])):
        for i in range(ratings.shape[1]):
            watched_i = ratings[:,i].nonzero()[0]                                  # 영화 i를 본 user들을 추출
            if u in watched_i:
                watched_i = np.setdiff1d(watched_i, u)                             # 본인은 제외
            
            similarity_u = similarity[u, watched_i]                                # 영화 i를 본 user들의 유사도를
            similar_idx = np.argsort(similarity_u)[::-1]                           # 높은 순으로 정렬
            similar_idx = similar_idx[:k]                                          # 유사도가 가장 높은 k개의 index만 추출     
            similar_idx = np.where(similarity_u[similar_idx] > 0)[0]               # 양수값을 갖는 유사도만 사용

            sum_similarity = np.sum(similarity[u, similar_idx])                    # 0/0 = nan 문제 피하기 위해
            if sum_similarity == 0:
                sum_similarity = 1

            pred[u, i] = np.sum(similarity[u, similar_idx].reshape([-1, 1]) * ratings[similar_idx, i]) / sum_similarity

  
    return pred

평가 데이터와 추정한 점수를 비교, 평균 제곱 오차(MSE)를 계산

In [9]:
######################################################################################################################################
# Test Score와 Predicted Score의 Mean Squared Error를 계산
######################################################################################################################################
def get_mse(pred, actual):
    pred = pred[actual.nonzero()].flatten()
    actual = actual[actual.nonzero()].flatten()
    return mean_squared_error(pred, actual)

영화 추천

In [10]:
######################################################################################################################################
# 특정 user와 유사한 영화를 추천
######################################################################################################################################
def recommend(watched_rating, pred, user_id, user_dict, movie_idx_to_name, movie_idx_to_genre):
    movies_in_order = np.argsort(pred[user_dict[user_id]])[::-1]
    watched_movie = watched_rating[user_dict[user_id]].nonzero()[0]
    cnt = 0
    ##################################### Fill in Your Code ##########################################################################
    for movie in movies_in_order:
        if pred[user_dict[user_id], movie] == 0:
            if cnt== 0:
                print("### Cannot Recommend a Movie : All Input Ratings Have Same Value ###")
            break
        if movie in watched_movie: continue
        cnt += 1 
        print("### Top {} Movie for User {} : {} \t Genre: {} ###".format(cnt, user_id, movie_idx_to_name[movie], movie_idx_to_genre[movie]))
        if cnt == 5: break
    #####################################################################################################################################

✅ 이제 하나씩 수행해 보자

In [11]:
# 데이터 쪼개기
train_ratings, test_ratings = train_test_split(ratings)
In [12]:
# 유저별로 평점을 주는 기준이 다를 수 있으므로, 유저 별 평균 평점 값을 실제 평점 값에서 빼준다
mean_subtracted_ratings = subtract_mean(train_ratings)
In [13]:
### 유사도 계산 ### 
similarity = collaborative_filtering(mean_subtracted_ratings) 
100%|████████████████████████████████████████████████████████████████████████████████| 610/610 [15:13<00:00,  1.50s/it]
In [14]:
### 사용자 - 영화 쌍 각각에 대해 점수를 추정 ### 
predicted_ratings = predict(train_ratings, similarity)
100%|████████████████████████████████████████████████████████████████████████████████| 610/610 [04:44<00:00,  2.15it/s]
In [15]:
### Fill your own user id and test your result! ### 
user_id = 600
recommend(train_ratings, predicted_ratings, user_id, user_dict, movie_idx_to_name, movie_idx_to_genre)
### Top 1 Movie for User 600 : Forrest Gump (1994) 	 Genre: Comedy|Drama|Romance|War ###
### Top 2 Movie for User 600 : Batman (1989) 	 Genre: Action|Crime|Thriller ###
### Top 3 Movie for User 600 : Star Wars: Episode IV - A New Hope (1977) 	 Genre: Action|Adventure|Sci-Fi ###
### Top 4 Movie for User 600 : Usual Suspects, The (1995) 	 Genre: Crime|Mystery|Thriller ###
### Top 5 Movie for User 600 : In the Name of the Father (1993) 	 Genre: Drama ###

+ Recent posts