추천 시스템 (Recommendation System) 이란

John
10 min readMar 15, 2020

--

온라인에서 특정 물품을 구매하려고 하는 순간 ‘이런 상품은 어때요?’ 라는 알림을 받아본 적이 있을 것이다.

온라인 스토어는 많은 양의 고객과 상품 데이터를 가지고 있고, 이 데이터는 유저가 흥미를 가질 상품을 즉각적으로 추천하는데 활용되고 있다. 또한 한정된 시간안에 제품을 골라야하는 고객 입장에서 너무 많은 제품과 콘텐츠는 선택에 어려움을 주기 때문에 추천 시스템의 중요성은 점점 커지고 있는 상황이다.

1. 추천 시스템의 유형

추천 시스템은 크게 콘텐츠 기반 필터링(Content based filtering) 방식과 협업 필터링 (Collaborative Filtering) 방식으로 나뉜다. 그리고 협업 필터링 방식은 다시 최근접 이웃(Nearest Neighbor)과 잠재요인(Latent Factor) 방식으로 나뉘고 있다.

유명한 넷플릭스 추천 시스템 경연 대회에서는 행렬 분해 기법을 이용한 잠재 요인 협업 필터링 방식이 우승을 할 수 있었다고 한다.

1–1. 콘텐츠 기반 필터링 추천 시스템

간단하게 말해, 사용자가 특정 곡을 좋아하는 경우, 이와 비슷한 콘텐츠를 가진 다른 곡을 추천하는 방식이다. 예를 들어 A라는 사용자가 DPR LIVE의 ‘Jasmine’을 선호한다면 이와 비슷한 music feature (danceability , acousticness,energy,liveness) 등이 비슷한 유사한 음악을 추천하는 방식이다.

1–2. 최근접 이웃 협업 필터링

a) 사용자 기반

친구들에게 물어보는 방식과 유사하다고 보면 된다. 예를 들어 DPR LIVE의 ‘Jasmine’을 들은 유저 A,B가 있다고 치자. 그리고 그 중 유저 A는 오왼 오바도즈의 ‘City’라는 노래도 청취했다. 그러면 자신과 같은 곡을 들은 유저가 들었던 ‘City’가 유저 B에게 추천되는 방식이다.

b) 아이템 기반

청취시 O, 미청취시 공란으로 구분

유저가 기준이 되는 것이 아니라 아이템을 기준으로 아이템간의 연관성을 측정하여 유사한 아이템을 추천하는 방식이다.

위의 그림에서 Jasmine과 가장 유사도가 비슷한 곡은 청취 분포가 비슷한 City로 볼 수 있다. 즉 상호 간의 아이템 유사도가 상대적으로 높다고 볼 수 있으며, 유저 D에게 아이템 기반 협업 필터링은 City라는 곡을 추천하도록 한다.

일반적으로 사용자 기반보다는 아이템 기반의 방식이 정확도가 더 높다고 한다.

1–3. 잠재 요인 협업 필터링

사용자-아이템 평점 매트릭스 속에 숨어 있는 잠재 요인을 추출하여 추천 예측을 할 수 있게 하는 기법이다. 이러한 잠재 요인은 구체적 정의를 내리는 것이 어렵지만, 실제 추천의 근거를 마련하는 데 큰 역할을 한다.

예로, 음악의 장르를 잠재 요인으로 설정할 수 있다.

유저 A라는 유저가 발라드를 다른 장르보다 좋아한다고 가정하면, 이 유저가 곡을 청취할 때 가장 중요한 잠재 요인은 발라드냐 아니냐가 될 가능성이 크다. 그리고 이 유저에게 다른 음악을 추천해준다고 한다면, 발라드 음악을 추천하는 것이 가장 합리적일 가능성이 높다. 잠재 요인 협업 필터링은 이러한 잠재 요인을 찾아 추천에 활용하는 방식이다.

-> 해당 방법은 행렬 분해에 대한 이론적인 이해가 필요하므로 조금 더 깊게 살펴볼 예정이다.

2. 실습

캐글의 TMDB 5000 영화 데이터 세트로 콘텐츠 기반 필터링 실습을 진행해보려고 한다.

import pandas as pd
import numpy as np
import warnings; warnings.filterwarnings('ignore')
movies =pd.read_csv('tmdb_5000_movies.csv')
movies_df = movies[['id','title', 'genres', 'vote_average', 'vote_count',
'popularity', 'keywords', 'overview']]
pd.set_option('max_colwidth', 100)
movies_df[['genres','keywords']][:1]
from ast import literal_evalmovies_df['genres'] = movies_df['genres'].apply(literal_eval)
movies_df['keywords'] = movies_df['keywords'].apply(literal_eval)
movies_df['genres'] = movies_df['genres'].apply(lambda x : [ y['name'] for y in x])
movies_df['keywords'] = movies_df['keywords'].apply(lambda x : [ y['name'] for y in x])
movies_df[['genres', 'keywords']][:1]
from sklearn.feature_extraction.text import CountVectorizer# CountVectorizer를 적용하기 위해 공백문자로 word 단위가 구분되는 문자열로 변환.
movies_df['genres_literal'] = movies_df['genres'].apply(lambda x : (' ').join(x))
count_vect = CountVectorizer(min_df=0, ngram_range=(1,2))
genre_mat = count_vect.fit_transform(movies_df['genres_literal'])
from sklearn.metrics.pairwise import cosine_similaritygenre_sim = cosine_similarity(genre_mat, genre_mat)
genre_sim_sorted_ind = genre_sim.argsort()[:, ::-1]
print(genre_sim_sorted_ind[:1])
  1. 딕셔너리 형태를 리스트로 변환한 genres_literal이라는 칼럼을 생성
  2. 이를 Count 기반으로 피처 벡터화 변환
  3. genres 문자열을 피처 벡터화 행렬로 변환한 데이터 셋을 코사인 유사도로 비교, 이를 위해 데이터 셋의 레코드별로 타 레코드와 장르에서 코사인 유사도 값을 가지는 객체 생성
def find_sim_movie(df, sorted_ind, title_name, top_n=10):

# 인자로 입력된 movies_df DataFrame에서 'title' 컬럼이 입력된 title_name 값인 DataFrame추출
title_movie = df[df['title'] == title_name]

# title_named을 가진 DataFrame의 index 객체를 ndarray로 반환하고
# sorted_ind 인자로 입력된 genre_sim_sorted_ind 객체에서 유사도 순으로 top_n 개의 index 추출
title_index = title_movie.index.values
similar_indexes = sorted_ind[title_index, :(top_n)]

# 추출된 top_n index들 출력. top_n index는 2차원 데이터 임.
#dataframe에서 index로 사용하기 위해서 1차원 array로 변경
print(similar_indexes)
similar_indexes = similar_indexes.reshape(-1)

return df.iloc[similar_indexes]
similar_movies = find_sim_movie(movies_df, genre_sim_sorted_ind, 'The Godfather',10)
movies_df[['title','vote_average','vote_count']].sort_values('vote_average', ascending=False)[:10]
C = movies_df['vote_average'].mean()
m = movies_df['vote_count'].quantile(0.6)
percentile = 0.6
m = movies_df['vote_count'].quantile(percentile)
C = movies_df['vote_average'].mean()
def weighted_vote_average(record):
v = record['vote_count']
R = record['vote_average']

return ( (v/(v+m)) * R ) + ( (m/(m+v)) * C )
movies_df['weighted_vote'] = movies_df.apply(weighted_vote_average, axis=1)
movies_df[['title','vote_average','weighted_vote','vote_count']].sort_values('weighted_vote',
ascending=False)[:10]

3. 새롭게 만든 가중치 평점 ‘weighted_vote’ 생성

def find_sim_movie(df, sorted_ind, title_name, top_n=10):
title_movie = df[df['title'] == title_name]
title_index = title_movie.index.values

# top_n의 2배에 해당하는 쟝르 유사성이 높은 index 추출
similar_indexes = sorted_ind[title_index, :(top_n*2)]
similar_indexes = similar_indexes.reshape(-1)
# 기준 영화 index는 제외
similar_indexes = similar_indexes[similar_indexes != title_index]

# top_n의 2배에 해당하는 후보군에서 weighted_vote 높은 순으로 top_n 만큼 추출
return df.iloc[similar_indexes].sort_values('weighted_vote', ascending=False)[:top_n]
similar_movies = find_sim_movie(movies_df, genre_sim_sorted_ind, 'The Godfather',10)
similar_movies[['title', 'vote_average', 'weighted_vote']]

5. 장르 유사성이 높은 영화 중 ‘weighted_vote’ 가 높은 10개의 영화 추천.

참고

--

--