AI/이론
그래프를 추천시스템에 어떻게 활용할까?(기본)
N-analyst
2021. 2. 24. 22:21
그래프를 추천시스템에 어떻게 활용할까? (기본)¶
✅ 내용 기반 추천시스템¶
1️⃣ 내용 기반 추천시스템의 원리¶
📌 내용 기반 추천은 각 사용자가 구매/만족했던 상품과 유사한 것을 추천하는 방법이다.¶
- 동일한 장르의 영화를 추천하는 것
- 동일한 감독의 영화 혹은 동일 배우가 출현한 영화를 추천하는 것
- 동일한 카테고리의 상품을 추천하는 것
- 동갑의 같은 학교를 졸업한 사람을 친구로 추천하는 것
📌 내용 기반 추천은 다음 네 가지 단계로 이루어 진다.¶
1. 첫 단계는 사용자가 선호했던 상품들의 상품 프로필(Item Profile)을 수집하는 단계
- 어떤 상품의 상품 프로필이란 해당 상품의 특성을 나열한 벡터이다.
- 영화의 경우 감독, 장르, 배우 등의 원-핫 인코딩이 상품 프로필이 될 수 있다.
2. 사용자 프로필(User Profile)을 구성하는 단계
- 사용자 프로필은 선호한 상품들의 상품 프로필을 선호도를 사용하여 가중 평균하여 계산한다.
- 즉, 사용자 프로필 역시 벡터이다.
- 앞선 영화 프로필 예시에서는 담음과 같은 형태의 사용자 프로필을 얻을 수 있다.
3. 사용자 프로필과 다른 상품들의 상품 프로필을 매칭하는 단계
- 사용자 프로필 벡터 $\vec{u}$와 상품 프로필 벡터 $\vec{v}$ 코사인 유사도 ${\vec{u}\cdot\vec{v} \over \parallel\vec{u}\parallel\parallel\vec{v}\parallel}$를 계산한다.
- 즉, 두 벡터의 사이각의 코상인 값을 계산한다.
- 코사인 유사도가 높을 수록, 해당 사용자가 과거 선호했던 상품들과 해당 삼품이 유사함을 의미한다.
4. 사용자에게 상품을 추천하는 단계
- 계산한 코사인 유사도가 높은 상품을 추천해 준다.
📌 내용 기반 추천시스템의 장점¶
- 다른 사용자의 구매 기록이 필요하지 않는다.
- 독특한 취향의 사용자에게도 추천이 가능하다.
- 새 상품에 대해서도 추천이 가능하다.
- 추천의 이유를 제공할 수 있다.
- 예시: 당신은 로맨스 영화를 선호했기 때문에, 새로운 로맨스 영화를 추천합니다.
📌 내용 기반 추천시스템의 단점¶
- 상품에 대한 부가 정보가 없는 경우에는 사용할 수 없다.
- 구매 기록이 없는 사용자에게는 사용할 수 없다.
- 과적합으로 지나치게 협소한 추천을 할 위험이 있다.
✅ 협업 필터링¶
1️⃣ 협업 필터링의 원리¶
📌 사용자-사용자 협업 필터링은 다음 3단계로 이루어 진다.¶
추천의 대상 사용자를 Mr.A 라고 하자
- 우선 Mr.A와 유사한 취향의 사용자들을 찾는다.
- 유사한 취향의 사용자들이 선호한 상품을 찾는다.
- 이 상품들을 Mr.A에게 추천한다.
📌 사용자-사용자 협업 필터링의 핵심은 유사한 취향의 사용자를 찾는 것이다.¶
위 예시는 사용자 별 영화 평점이다.
"?"는 평점이 입력되지 않은 경우를 의미한다.
지수와 제니의 취향이 유사하고, 로제는 둘과 다른 취향을 가진 것을 알 수 있다.
📌 취향의 유사성은 상과 계수(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)를 사용해 취향의 유사도를 계산한다.
🔎 예시에서 취향의 유사도를 확인해 보자¶
- 지수와 제니가 같이 본 반지의 제왕, 겨울왕국, 해리포터에 대해서 계산이 진행 된다.
가장 먼저 지수와 제니의 평균 점수를 알아야 한다.
- 지수의 평균 평점 = (4+1+2+5)/4 = 3
- 제니의 평균 평점 = (5+1+4+2)/4 = 3
이후 위 식대로 계산하면 지수와 로제의 취향의 유사도는 0.88로 둘의 취향은 매우 유사하다.
- 지수와 로제의 취향의 유사도는 -0.94이고 둘의 취향은 매우 상이하다.
- 따라서, 지수의 취향을 추정할 때는 제니의 취향을 참고하게 된다.
- 예를 들명, 지수는 미녀와 야수를 좋아할 확률이 낮다.
- 지수는 제니와 취향이 유사하고, 제니는 미녀와 야수를 좋아하지 않았기 때문이다.
📌 구체적으로 취향의 유사도를 가중치로 사용한 평점의 가중 평균을 통해 평점을 추정한다.¶
🔎 사용자 $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️⃣ 데이터 분리¶
📌 추천 시스템의 정확도는 어떻게 평가할까?¶
- 가장 먼저 훈련 데이터와 평가 데이터로 분리한다.
- 이때 행은 사용자를 의미하고 열은 각각의 상품를 의미한다.
- 해당 원소는 사용자가 해당 상품에 준 점수를 의미한다.
- 평가 데이터는 따로 주어지지 않았다고 가정한다.
- 훈련 데이터를 이용해서 가리워진 평가 데이터의 평점을 추정한다.
- 이를 토대로 원래의 실제 평가 데이터와 추정한 평점을 비교하여 오차를 측정한다.
2️⃣ 평가 지표¶
📌 추정한 평점과 실제 평가 데이터를 비교하여 오차를 측정¶
- 오차를 측정하는 지표로는 평균 제곱 오차(Mean Squared Error, MSE)가 많이 사용된다.
- 평가 데이터 내의 평점들을 집함 $T$라고 하자
- 평균 제곱 오차는 아래 수식으로 계산된다.
- 평균 제곱근 오차(Root Mean Squared Error,RMSE)도 많이 사용된다.
📌 이 밖에도 다양한 지표가 사용된다.¶
- 추정한 평점으로 순위를 매긴 후, 실제 평점으로 매긴 순위와의 상관 계수를 계산하기도 한다.
- 추천한 상품 중 실제 구매로 이루어진 것의 비율을 측정하기도 한다.
- 추천의 순서 혹은 다양성까지 고려하는 지표들도 사용된다.
✅ 협업 필터링 구현¶
라이브러리 로드¶
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())
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))
- 유저는 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
유저 별 평점에서 평균 평점을 빼주는 함수를 만든다.¶
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
이제 전체 유사도를 계산하는 함수¶
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
유사도를 사용한 가중 평균을 통해 사용자 - 영화 쌍 각각에 대해 점수를 추정¶
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)
In [14]:
### 사용자 - 영화 쌍 각각에 대해 점수를 추정 ###
predicted_ratings = predict(train_ratings, similarity)
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)