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

2021. 2. 24. 22:21·AI/이론
그래프를 추천시스템에 어떻게 활용할까(기본)

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


✅ 내용 기반 추천시스템¶

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 ###

'AI > 이론' 카테고리의 다른 글

그래프 신경망이란 무엇일까? (심화)  (0) 2021.02.26
그래프 신경망이란 무엇일까? (기본)  (1) 2021.02.26
그래프를 추천시스템에 어떠게 활용할까? (심화)  (0) 2021.02.25
그래프의 정점을 어떻게 벡터로 표현할까?  (0) 2021.02.25
그래프의 구조를 어떻게 분석할까?  (0) 2021.02.24
그래프를 바이럴 마케팅에 어떻게 활용할까?  (0) 2021.02.23
검색 엔진에서는 그래프를 어떻게 활용할까?  (0) 2021.02.23
실제 그래프는 어떻게 생겼을까?  (0) 2021.02.22
'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
그래프를 추천시스템에 어떻게 활용할까?(기본)
상단으로

티스토리툴바