본문 바로가기

공부/AI

Recommender System 공부했다.



 대뜸 광고하는 것도 아니고, 쇼핑몰 사이트 '쿠팡'의 일부를 왜 올린거지??? 싶을까 싶다. 그런데 이는 오늘 배운 내용 Recommender System과 관련이 깊다. 글자 그대로 해석하면 '추천 시스템'이 되는데 쇼핑몰 사이트를 이용하다 보면, 내가 구입하거나 조회한 상품과 유사도가 높은 다른 제품들을 엮어서 추천을 해주는 경우도 있고, 페이스북 등을 하다 보면 내 친구들이 좋아한 페이지가 뜨면서 나에게도 구독을 요청하는 경우를 심심찮게 볼 수 있다. 이 작업들을 어떤 알바생이 고생하면서 엮어주는 것이 아니고, AI가 해주는 것인데, 오늘은 그 방법에 대해서 공부를 하였다. 


Copyright © 2018. Alina Inc. All Rights Reserved.

출처 : ai-pm alina



 이렇게 추천 시스템의 종류들을 분류할 수 있는데 그 중에서도 크게 다음 3 가지를 소개한다.


 내용 기반 추천

 협업 필터링

 모델 기반 협업 필터링


하나씩 살펴본다.


Copyright © 2018. Alina Inc. All Rights Reserved.



우선 설명은 이렇지만 예시를 들어 보면 좋을 것 같다. 영화의 취향에 대해서 조사할 때 사람들은 주로 어떤 장르를 좋아하는가에 큰 영향을 받는다. 


판타지 : 신비한 동물사전, 해리포터, 나니야 연대기, 호빗 ...

액션 : 트랜스포터, 테이큰, 미션 임파서블, 007 ...

애니메이션 : 주먹왕 랄프, 인크레더블, 주토피아 ...

로맨스 : 러브 로지, 이터널 선샤인, 노딩 힐, 이프 온리...

음악 : 보헤미안 렙소디, 드럼 라인, 라라랜드, 원스 ....


 이렇게 비슷한 장르의 영화들을 분류하고 이를 데이터화 한다. 이때 사용하는 개념이 BoF(Bag of Words)라고 하는데,


Copyright © 2018. Alina Inc. All Rights Reserved.



 위와 같이 속성들을 순서대로 나열하고 영화가 그 속성을 가지면 1, 아니면 0으로 각각의 영화들을 벡터화한다. 다만, 이렇게 하면 단순한 1과 0일 뿐 그것이 나타내는 의미는 사라지게 된다. 그래서 다른 방법을 사용한다. 


Copyright © 2018. Alina Inc. All Rights Reserved.



TF-IDF란 글이 단어들로 이루어져 있음에 기반해서 각 단어들을 수치화하는 기법이다 위의 예시에서 the나 of와 같은 전치사들은 글 안에서 자주 나올 것이다. 그런데 그렇다고 해서 전치사가 중요하지는 않다. 그래서 다른 글들과의 비교를 통해 모든 글에서 자주 나오는 단어라는 것을 발견하면 중요도를 낮추게 되는 것이다.


 이렇게 영화가 가진 내용을 수치화 하였다면 이제 영화들 사이의 유사도를 측정해서 비슷한 부류를 사용자에게 추천할 수 있다. 다만 무엇을 가지고 두 영화 사이의 유사도를 측정할 것인가?!!! 


Copyright © 2018. Alina Inc. All Rights Reserved.


 Cosine 유사도라는 개념을 소개한다. 좌표 상에서 A와 B가 있다고 하자. 위 그림에서 C라는 점이 생겼다고 하면, 좌표거리 상으로 B와 더 가깝다고 할 수 있다. 그런데 저 위에 D라는 점이 있다고 하자. A의 입장에서 좌표거리 상으로는 C가 D보다 더 가까워 보인다.


 하지만, 원점와 A을 이어서 만들어진 연장선을 A구역, 마찬가지로 B와 원점을 이은 점을 B구역이라고 하면 C보다는 D가 A 구역에 속한다고 말할 수 있다. 이를 수학적으로 어떻게 표현하는가 하면 두 점(벡터)이 이루는 각도의 cosine값이 크면 비슷한 속성을 가진다고 말할 수 있다. 모두 알고 있듯이 cos(0) = 1, cos(90 - degree) = 0이기 때문이다. 더욱이 앞서 데이터들을 벡터화 하였기 때문에 cos을 계산하기에도 용이해졌다. (참고로 자기 자신과의 유사도는 항상 1이 된다.)


 그럼 즐거운 코딩 시간이다~


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import linear_kernel
import urllib.request
 
def get_recommendations(similarities, idx, n = 10):
    scores = list(enumerate(similarities[idx]))
    scores = sorted(scores, key=lambda x: x[1], reverse=True)
    movie_indices = [i[0for i in scores[:n+1if i[0!= idx]
    return movies.iloc[movie_indices]
 
print('downloading data...')
url = 'https://raw.githubusercontent.com/salopge/datasets/master/ml-latest-small/movies.csv'
urllib.request.urlretrieve(url, './movies.csv')
print ('done')
 
pd.set_option('display.max_columns', None)
movies = pd.read_csv('movies.csv')
movies['genres'].fillna('', inplace=True)
movies['genres'= movies['genres'].str.split('|')
 
vectorizer = TfidfVectorizer(analyzer='word', ngram_range=(1,2),
                            min_df=0, stop_words='english', token_pattern=r'(?u)\b\w[\w-]*\w\b')
tfidf_matrix = vectorizer.fit_transform(movies['genres'].astype('str'))
#print(tfidf_matrix)
 
#bag_of_words = vectorizer.get_feature_names()
#print(bag_of_words[:10])
 
cosine_sim = linear_kernel(tfidf_matrix, tfidf_matrix)
 
idx = 9708
print(movies.iloc[idx].title)
print('similar movies')
for i, r in get_recommendations(cosine_sim, idx).iterrows():
    print(r.title)
cs


출처 : code_on_web


 위 코드는 아직 주석 처리를 하지는 않았다. 사실 포스트를 올리는 것도 내 공부 정리가 목적인데, 혹시 댓글로 요청이 있다면 주석을 달도록 하겠다.


 이번에는 유사도를 측정하는 방법으로 cosine 유사도 말고 다른 것을 사용해보자. 이번에 들게 될 예시는 다음과 같다. 


Copyright © 2018. Alina Inc. All Rights Reserved.



Adam, Brian, Claire, David 4 사람에게 5편의 영화를 보게 하고 별점을 주게 하였다. 그런데 모든 영화에 별점을 준 것이 아니라 1,2개씩 점수를 주지 않은 상황이다. Adam을 예로 본다면, 이 친구는 액션을 별로 좋아하지 않고, 로맨스를 좋아하는 경향이 있다. 그럼 5번 로맨스 영화도 좋아할 것 같다. Brian은 종잡을 수 없고, Claire은 로맨스보다는 액션이 취향인가 보다.


 잠깐, 그럼 액션을 좋아하는 사람을 대체로 로맨스를 기피하고 반대로 로맨스를 좋아하는 사람은 액션을 싫어하는 경향이 있군!!!!! 


 이를 통해서 액션에 높은 점수를 준 David가 나머지 영화들에 어떤 점수를 주었을 지 추측을 해 보자!!!!! 그런데 이를 코드로 표현하기 위해서 수학적으로 어떻게 표현하지??? ㅠㅠ


 

Copyright © 2018. Alina Inc. All Rights Reserved.


짜잔~!! Pearson 상관 계수라는 것이 있다.(pearson... 어디서 많이 들어 봤다. 맞다 출판사!) 어떠한 상황들에 대해서 두 변수가 변하는 정도, 경향을 갖는지 분석하는 것이다. 우선 선형인지 비선형인지, 어떠한 값을 갖는지를 보아야하는데 다음과 같다.


선형 & 1이다 : 둘은 비슷한 성향을 가진다.

선형 & -1이다. : 둘은 반대의 성향을 가진다.

비선형 & 1이다 : 둘은 비슷한 성향을 가진다.

비선형 & -1이다 : 둘은 반대의 성향을 가진다.

0이다 : 상관이 거의 없다.


  peason 상관 계수도 cosine 유사도와 같이 자기 자신에 대해서는 1의 값을 가진다. 그리고 앞에서는 말을 못하였는데, n명의 사람들이 있고 이들이 각각 다른 사람들과 어떠한 상관 관계를 갖는지 보이는 similarity행렬이 있다고 하면 이는 n*n 형식이고 대각선 값은 1, 대각선에 대해서 대칭(diagonal)할 것이라는 것을 알 수 있다.


 이제 필요한 작업들은 끝!!! David의 빈칸을 매꿔 보자!!! - 원래 우리가 하려던 것이 무엇이었는지 나도 살짝 잊어버려서 다시 앞을 보고 왔다. 

 David와 나머지 3 사람의 similarity를 가지고 있으므로, 그 similarity와 각각 사람이 준 별점을 곱해서 합하고 normalize하면 끝!!! - 뭔 소린지 모르겠지?? ㅋㅋ 풀어본다.

2번 액션 영화에 대해서

(David와 Adam의 similarity) * (Adam이 준 평점)
(David와 Brian의 similarity) * (Brian이 준 평점)
(David와 Claire의 similarity) * (Claire이 준 평점)

을 다 더한다, 그런데 이 값은 5넘을 수 있다. 그래서 비율을 다시 설정하는데 이것이 normalizing이다. 이는 

(앞서 구한 값) / (similarity들의 합)


가 되겠다. 여기서 주의할 점이 있다. 실제로 이 상황에서 David와 Adam의 similarity는 -1이 되는데 이러한 값들은 제외를 해 주어야 한다고 한다. 뒤에 나오는 코드에도 이 처리가 되어 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import linear_kernel
import urllib.request
 
print('downloading data...')
url = 'https://raw.githubusercontent.com/salopge/datasets/master/movie_romance_action_ratings.csv'
urllib.request.urlretrieve(url, './movie_romance_action_ratings.csv')
print ('done')
 
pd.set_option('display.max_columns', None)
data = pd.read_csv('movie_romance_action_ratings.csv')
#print(data)
 
movie_ratings = pd.pivot(columns=data.user, index=data.title, values=data.rating)
users = list(movie_ratings.columns.values)
movie_ratings.reset_index(inplace=True)
print(movie_ratings)
 
user_sim = movie_ratings.corr(method='pearson').reset_index()
print('user_sim')
print(user_sim)
 
no_david_ratings = movie_ratings.loc[movie_ratings.David.isnull()]
print(no_david_ratings)
 
no_david_ratings = pd.melt(no_david_ratings, id_vars=['title'], value_vars=users,
                          value_name='rating')
no_david_ratings = no_david_ratings.dropna()
print(no_david_ratings)
 
ratings = pd.merge(no_david_ratings, user_sim[['user''David']], on='user')
ratings.rename(columns={'David''similarity'}, inplace=True)
print(ratings)
 
ratings = ratings.drop(ratings[ratings.similarity < 0].index)
print(ratings)
 
ratings['rating_similarity'= ratings.rating * ratings.similarity
print(ratings)
 
ratings = ratings.groupby(['title'])['similarity''rating_similarity'].sum()
print(ratings)
 
ratings['predicted_rating'= ratings.rating_similarity / ratings.similarity
print(ratings[['predicted_rating']])
 
cs

 아래의 코드에서는 앞선 예제와 같이 cosine 유사도를 사용자에 적용함과 더불어, pearson 상관 계수로 아이템 - 아이템 유사도를 계산해서 별점을 예측해 보고, 더불어 어떤 영화를 좋다고 하면 비슷한 영화를 추천해 주는 코드가 되겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import pandas as pd
import numpy as np
from sklearn.metrics import pairwise_distances
 
#import urllib.request
#print('downloading data...')
#url = 'https://raw.githubusercontent.com/salopge/datasets/master/ml-latest-small/movies.csv'
#urllib.request.urlretrieve(url, './movies.csv')
#url = 'https://raw.githubusercontent.com/salopge/datasets/master/ml-latest-small/ratings.csv'
#urllib.request.urlretrieve(url, './ratings.csv')
#print ('done')
 
pd.set_option('display.max_columns', None)
 
def get_sparsity(data):
    sparsity = float((data != 0).sum(axis=1).sum())
    sparsity /= (data.shape[0* data.shape[1])
    return sparsity * 100
 
ratings = pd.read_csv('ratings.csv')
n_users = ratings.userId.unique().shape[0]
n_movies = ratings.movieId.unique().shape[0]
#print(ratings.shape)
#print(ratings.head())
print('# of users: ', n_users)
print('# of movies: ', n_movies)
 
users_movies = ratings.pivot(index='userId', columns='movieId', values='rating').reset_index(drop=True)
users_movies.fillna(0, inplace=True)
#print(users_movies.shape)
#print(users_movies.head())
#print('sparsity: ', get_sparsity(users_movies), '%')
 
user_sim = 1 - pairwise_distances(users_movies, metric='cosine')
#print(user_sim.shape)
#print(user_sim[:4, :4])
 
np.fill_diagonal(user_sim, 0)
#print(user_sim.shape)
weighted_ratings = user_sim.dot(users_movies)
sum_ratings = np.abs(user_sim).dot((users_movies > 0).astype(int))
pred_users_movies = weighted_ratings / sum_ratings
np.nan_to_num(pred_users_movies, copy=False)
#print(pred_users_movies.shape)
#print(pred_users_movies[:5, ])
#print('sparsity: ', get_sparsity(users_movies), '%')
 
movie_sim = 1 - pairwise_distances(users_movies.T, metric='cosine')
np.fill_diagonal(movie_sim, 0)
#print(movie_sim.shape)
#print(movie_sim[:5, :5])
 
movies = pd.read_csv('movies.csv')
movie_ids = users_movies.columns.values
def get_recommendations(idx, k=10):
    return [movie_ids[x] for x in np.argsort(movie_sim[idx, :])[:-k-1:-1]]
#print('done')
 
idx = 0
movie_id = movie_ids[idx]
title = movies.loc[movies.movieId == movie_id].title.values[0]
print(title)
 
for movie_id in get_recommendations(idx):
    title = movies.loc[movies.movieId == movie_id].title.values[0]
    print(title)
cs



 자 이제 마지막, 모델기반 협업 필터링 공부해 보겠다! 매일 한 챕터씩 하면 금방 하겠거니 했는데 뒤에 나오는 CNN같은 경우 상당히 양이 많아서 그건 힘들 것 같다. 


 

Copyright © 2018. Alina Inc. All Rights Reserved.


 위 사진은 아래 링크에서 가져왔다고 하는데, 드라마를 예로 든다면, 어떤 사람이 태양의 후예와 성균관 스캔들을 좋아한다면, 그 이유가 단순히 드라마를 좋아하는 것이 아니라, 이 두개의 드라마에 송중기가 출연해서 그럴 것이라는 추측을 할 수가 있다. 그래서 어떤 사람이 좋아하는 드라마들을 알고 있다면, 이 사람이 좋아하게 되는 그 매개체(모델)를 알아내는 것이 모델 기반 협업 필터링이다. 그 중에서도 오늘 배운 방법은 Matrix Factorization이다. 



Copyright © 2018. Alina Inc. All Rights Reserved.



 가장 기본적인 방법으로 선형 대수 시간에 배웠던 SVD를 사용하는 것이 있다. A를 각각 사용자가 드라마에 매긴 점수라고 한다면, U는 사용자, V는 아이템 matrix이고 중간에 위치한 행렬을 알아내는 것이 우리의 목표이다. 원래 정확한 Factorization을 위해서는 각 행렬의 차원이 파란색으로 꽉 채워져 있어야 A와 동일 하지만, 분홍색으로 차원을 줄여서 시행을 해 본다. 그리고 이 방법의 핵심은 가운데 r*r의 차원에 달려 있다. 이 가운데 행렬을 찾아내는 방법으로 두 가지를 소개한다.


Copyright © 2018. Alina Inc. All Rights Reserved.



 첫 번째 ALS는 SVD를 한 결과의 가운데 행렬이 없다고 생각을 하고 이를 두 개의 행렬의 곱으로 가정하는 방법이다. 이 두 행렬 U,V를 임의의 값으로 정해주고, 처음에는 U를 고정하고 V의 값을 최적화한 다음, V의 값들이 최적화가 되었으면 이번에는 V를 고정하고 U의 값들을 최적화한다. 이 두 과정을 반복해서 특정한 에러값의 조건을 충족하면 종료하는 방법이다. 이렇게 구한 U,V로 A의 비어 있는 값들을 구해내는 것이다.


 SGD는 Stochastic이라는 이름에서 알 수 있듯이, A에서 한 값을 가져와서 이 값에 영향을 주는 U,V값들을 최적화시키고, 이 과정을 반복하는 것이다.


 하지만, 이 두 방법들이 실제로 어떻게 최적화를 하는지, 에러값의 조건은 무엇인지는 알려주지 않았다. 아니면 너무 Heuristic해서 실제로는 잘 쓰이지 않는 것일 수도 있겠다고 생각을 하였다. 


Copyright © 2018. Alina Inc. All Rights Reserved.



 이렇게 만들어지 추천 시스템, 이제는 어떻게 그 성능을 평가할 것인가에 대한 문제이다. 제일 위에 것들은 사실상 Cost를 구하는 것과 비슷하게 에러 값들을 측정하는 것인데, 사실상 추천 시스템이기에 원본과의 차이를 줄이기보다 사용자의 만족도를 높여주는 것이 중요하다고 생각된다.


 그래서 아래와 같이 선호도를 비교하거나, 얼마나 다양한 아이템을 추천하는지, 뻔한 추천 말고 정말 취향을 저격해서, 의외의 추천을 해주는지를 측정하곤 한다.



 추천 시스템은 우리 주변에서 자주 보인다. 

유투브에서 노래를 들었는데 옆에서 비슷한 장르의 음악을 추천하거나, 동일한 가수의 노래를 추천해주는 것을 자주 보곤 하고,

 티스토리 광고들을 보더라도, 내가 개발 블로그들을 자주 가다 보면 각종, 컴퓨터 강좌들의 광고가 뜨는 것을 알 수 있다. 


다만, 이들을 위해서는 수많은 데이터셋이 필요했을 텐데, 이들이 다 어디에서 왔을까? 우리의 개인 정보들을 사용하였을 것이다. 동의를 구하고 사용했을 수도 있지만, 뭔가 찝찝한 기분이 드는 것은 어쩔 수 없었다. 다음 시간에는 머신 러닝의 방법론과 그 결과를 해석하는 것에 대해 공부하겠다.