본문 바로가기

Data Science/추천시스템과 NLP

[NLP] Contents-based Recommendation(컨텐츠 기반 추천)

반응형

이번에 다룰 주제는 2개의 포스팅으로 분리해서 작성해 볼 예정이다. 바로 자연어 처리(NLP)를 활용한 추천 시스템 구현이다. 추천 시스템은 우리의 일상에 매우 가까이에서 적용되고 있다. 대표적으로 YouTube나 Netflix와 같은 영상 컨텐츠를 담은 기업뿐만 아니라 국내외 E-commerce 기업에서도 추천 알고리즘을 상당히 효과적으로 활용하고 있다. 

 

YouTube에서의 '맞춤 동영상' 이나 E-commerce 홈페이지의 "이 A라는 상품을 구매한 고객들은 B라는 상품도 함께 구매합니다" 라는 문구를 보았을 것이다. 이런 것들이 바로 추천 알고리즘을 적용한 예시이다.

 

https://nfinity8.com/youtube-recommendation-algorithm/

 

추천 알고리즘에도 종류가 몇 가지 존재한다. 크게 '컨텐츠 기반 필터링' '협업 필터링'으로 나뉘어 진다. '협업 필터링'에서는 또 '최근접 이웃 기반' 으로 하는 것과 '잠재요인 기반'으로 세부적으로 분류된다. 이번 포스팅에서는 '컨텐츠 기반 필터링'에 대해 개념적으로 다루어보고 이를 실제 데이터에 적용해 코드로 구현해보는 과정까지 해보려 한다. 목차는 다음과 같다.

 

1. 컨텐츠 기반 필터링

2. 컨텐츠 기반 필터링 프로세스

3. 컨텐츠 기반 필터링 구현하기

1. 컨텐츠 기반 필터링

여기서 '필터링'이란 쉽게 말해서 '추천'이라고 생각하면 되겠다. 컨텐츠 기반 필터링은 기본적으로 컨텐츠를 구성하는 내용 즉, 텍스트에 기반하여 문서 유사도를 측정해 비슷한 다른 컨텐츠를 추천하는 것을 말한다. 여기서 유사도란, 텍스트를 벡터화 시킨 후 벡터들간의 거리를 측정하는 것이다. 벡터간의 거리를 측정하는 여러가지 방식이 있지만 저번 문서 유사도 측정 포스팅에서와 마찬가지로 '코사인 유사도'를 사용하려 한다.

 

https://corpling.hypotheses.org/495(Cosine Similarity)

 

2. 컨텐츠 기반 필터링 프로세스

그렇다면 컨텐츠 기반으로 추천하는 알고리즘의 프로세스는 어떻게 될까? 기존에 우리가 배워왔던 문서 유사도 군집화 프로세스와 비슷하다.

 

  1. 컨텐츠에 대한 내용의 '텍스트를' BOW(Bag Of Words) 또는 Word Embedding 방식으로 Feature Vecotrization 시킨다.
  2. 컨텐츠들의 Feature 벡터들 간에 Distance function(여기서는 Cosine Similarity를 사용)을 사용해서 유사도 행렬을 구해준다. 
  3. 유사도 행렬과는 별개로 또 다른 파생변수로 컨텐츠에 대한 고객들의 평점갯수와 평점을 이용해 가중 평점을 계산한다. 이 파생 변수는 컨텐츠 성격에 맞게 유동적으로 변경할 수 있다.
  4. 특정 컨텐츠를 기준으로 그 컨텐츠와 유사도, 가중평점이 가장 높은 순으로 정렬한 후 컨텐츠를 추천해 준다.

위 4단계의 프로세스에서 한 가지 짚고 넘어가야 할 개념이 있다. 바로 3단계의 '가중 평점'에 대한 공식이다. 우선 이 '가중 평점'이 왜 필요한지부터 알아보자. 

 

예를 들어, 볼려는 영화를 검색하기 위해 네이버에 'A'라는 영화를 검색했다. 리뷰 개수가 1000개이고 평점이 6.0이다.(사람의 기준에 따라 다르겠지만 6.0이 그렇게 높은 수치는 아니라고 가정하자.) 그리고 다른 영화 'B'를 검색했다. 평점이 9.5이다. 매우 높은 수치이다! 하지만 리뷰개수가 20개 밖에 되지 않는다. 그렇다면 이 둘 중 어떤 영화가 더 재밌다고 평가를 받는 걸까? 'A' 영화일 것이다. 왜냐하면 리뷰개수가 1000개나 된다는 것은 1000명의 사람들이 평균적으로 평가하는 점수가 6점이라는 것이기 때문이다. 반면에 'B' 영화는 평점이 9.5이지만 리뷰개수가 20개 밖에 되지 않으며 'A' 영화에 대한 평가보다 훨씬 더 주관적인 견해가 개입된 것이라고 볼 수 있다.(마치 댓글알바 처럼..?)

 

따라서 위와 같은 상황이 발생할 수 있기 때문에 컨텐츠간의 유사도 값 변수 이외에 가중 평점이라는 추가적인 변수를 고려하는 것이 고객에게 더 적절한 컨텐츠를 추천해줄 수 있을 것이다. 가중 평점에 대한 공식은 다음과 같다. 

 

가중 평점 공식(출처 : 파이썬 머신러닝 완벽 가이드)

 

3. 컨텐츠 기반 필터링 구현하기

이제 컨텐츠에 기반해 추천하는 것이 무엇인지, 또 프로세스가 어떻게 되는지 알아보았으니 실제 데이터에 적용해서 코드로 구현해보자.

데이터는 여러가지 영화에 대한 평점 정보가 들어 있는 Kaggle의 TMDB 5000 Movie Dataset을 사용했다. 우선 데이터를 로드하고 약간의 데이터를 전처리 해보자.

 

from ast import literal_eval
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings(action='ignore')
movies = pd.read_csv('tmdb_5000_movies.csv', encoding='utf-8')
# 컨텐츠를 추천하는 시스템을 만들기 위해서
# 해당 영화들의 유사도 비교할 항목들만 추출
movies_df = movies[['id','title', 'genres', 'vote_average', 'vote_count',
                 'popularity', 'keywords', 'overview']]

movies_df['genres'] = movies_df['genres'].apply(literal_eval)
movies_df['keywords'] = movies_df['keywords'].apply(literal_eval)
# genres라는 dict형태에서 'name' key의 value들만 가져오기
movies_df['genres'] = movies_df['genres'].apply(lambda x: [y['name'] for y in x])
# keywords도 동일하게 수행
movies_df['keywords'] = movies_df['keywords'].apply(lambda x: [y['name'] for y in x])
# 리스트에 담겨있는 genres, keywords를 공백 기준으로 문자열로 붙이기
movies_df['genres_literal'] = movies_df['genres'].apply(lambda x : (' ').join(x))
movies_df['keywords_literal'] = movies_df['keywords'].apply(lambda x: (' ').join(x))

 

 

이제 텍스트 데이터를 Feature Vectorization을 시켜주자. 해당 실습에서는 진행하지 않았지만 벡터화시키면서 토큰화 함수를 넣어주어 텍스트를 토큰화시키면서 어근을 추출(Lemmatization)해줄 수 도 있다.

 

# 모든 변수가 텍스트로 이루어졌으니 Countvectorizer로 feature 벡터화시키기
# 경우에 따라 추가적으로 Lemmatization 추가해 토큰화 전용 함수 추가도 가능!

from sklearn.metrics.pairwise import cosine_similarity
from sklearn.feature_extraction.text import CountVectorizer

cnt_vect = CountVectorizer(min_df=0, ngram_range=(1,2))
# fit_transform안에 데이터프레임형태로 넣어주면 안됨. 하나의 변수씩만 넣어주자!
genres_vect = cnt_vect.fit_transform(movies_df['genres_literal'])
keywords_vect = cnt_vect.fit_transform(movies_df['keywords_literal'])

# 장르에 따른 영화별 코사인 유사도 추출
genre_sim = cosine_similarity(genres_vect, genres_vect)
# 3개만 유사도행렬값 추출해보기
print(genre_sim[:3])

 

유사도 행렬값이 다음과 같이 나왔다.

 

영화 컨텐츠들간의 유사도 행렬

 

이제 유사도 행렬을 기준으로 어벤져스영화와 유사한 10개의 영화가 무엇인지 살펴보자.

 

# argsort를 이용해서 유사도가 높은 영화들의 index 추출
genre_sim_idx = genre_sim.argsort()[::-1]

# 특정 영화를 기준으로 선정해서 유사도가 높은 영화를 반환하는 함수 생성
def find_sim_movie(df, sorted_idx, title_name, top_n=10):
    # 비교기준으로 선정할 특정 영화 선정
    title_movie = df[df['title'] == title_name]
    
    # 비교기준 영화의 index.values를 할당해 유사도 행렬에서 비교기준 영화에 해당하는 유사도 행렬값을 찾자!
    title_movie_idx = title_movie.index.values
    # 모든 영화끼리의 유사도 행렬에서 비교기준 영화에 대한 유사도행렬 찾아서 할당
    top_sim_idx = sorted_idx[title_movie_idx, :top_n]
    print(top_sim_idx)
    
    # top_sim_idx는 2차원 array이기 때문에 1차원 array로 변경
    # 왜냐하면 top_sim_idx로 movies_df의 index값으로 넣어서 유사한 영화추출할 것이기 때문
    top_sim_idx = top_sim_idx.reshape(-1,)
    similar_movie = df.iloc[top_sim_idx]
    
    return similar_movie
    
similar_movies = find_sim_movie(movies_df, genre_sim_idx, 'The Avengers')
print(similar_movies[['title','vote_average','vote_count']])

 

텍스트들의 단어 Count에만 기반하여 어벤져스 영화와 가장 유사한 10개의 영화들은 다음과 같다.

 

어벤져스와 유사한 영화

 

하지만 문제점이 하나 생겼다. Light it Up의 영화는 영화 구성 내용만으로는 어벤져스영화와 매우 유사하긴 하지만 vote_average(평점)에 비해 vote_count(리뷰 개수)가 너무 적다는 것이다. 만약 이 영화를 어벤져스를 본 고객에게 "당신이 좋아할만한 다른 영화 입니다." 라고 추천해준다면 고객이 만족스러워할까? 시나리오 측면에서만 두 개의 영화가 비슷할지라도 흥미를 느낀다는 점에서는 매우 상이한 느낌을 받을 것이다. 따라서 우리는 추가적인 변수로 앞서 배웠던 '가중 평점' 변수를 추가해서 다시 유사한 영화들을 재정렬시켜보자.

 

C = movies_df['vote_average'].mean()

# 투표횟수 중 60%이상의 횟수에 달하는 숫자
# 예를들어 총 투표횟수가 100과 1일때 m값은 매우 달라진다.
m = movies_df['vote_count'].quantile(0.6)

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)
 
# 가중 평점 변수를 추가해서 유사한 영화 찾아주는 함수 재정의
def find_sim_movie(df, sorted_idx, title_name, top_n=10):
    title_movie = df[df['title'] == title_name]
    title_idx = title_movie.index.values
    
    similar_idx = sorted_idx[title_idx, :(top_n*2)]
    similar_idx = similar_idx.reshape(-1,)
    
    #자기 자신 영화 제외, boolean index기법 사용!
    similar_idx = similar_idx[similar_idx != title_idx]
    return df.iloc[similar_idx].sort_values(by=['weighted_vote'], ascending=False)[:top_n]

similar_movies = find_sim_movie(movies_df, genre_sim_idx,
                               'The Avengers')
print(similar_movies[['title','vote_average','weighted_vote']])

 

아래를 보시다시피 단순히 유사도값만을 가지고 어벤져스와 유사한 상위 10개의 영화를 출력했을 때의 결과와 달라졌음을 알 수가 있다.

 

가중 평점을 추가한 어벤져스와 유사한 영화

 

참고자료 : 파이썬 머신러닝 완벽 가이드

 

반응형