[Python] 머신러닝 완벽가이드 - 09. 추천 시스템[콘텐츠 기반]

Updated:

파이썬 머신러닝 완벽가이드 교재를 토대로 공부한 내용입니다.

실습과정에서 필요에 따라 내용의 누락 및 추가, 수정사항이 있습니다.


기본 세팅

import numpy as np
import pandas as pd

import matplotlib as mpl
import matplotlib.pyplot as plt
import seaborn as sns

import warnings
%matplotlib inline
%config InlineBackend.figure_format = 'retina'

mpl.rc('font', family='NanumGothic') # 폰트 설정
mpl.rc('axes', unicode_minus=False) # 유니코드에서 음수 부호 설정

# 차트 스타일 설정
sns.set(font="NanumGothic", rc={"axes.unicode_minus":False}, style='darkgrid')
plt.rc("figure", figsize=(10,8))

warnings.filterwarnings("ignore")

1. 콘텐츠 기반 필터링

추천 시스템은 크게 콘텐츠 기반 필터링 방식과 협업 필터링 방식으로 나뉜다.

그리고 협업 필터링 방식은 다시 최근접 이웃 협업 필터링잠재 요인 협업 필터링으로 나뉜다.

콘텐츠 기반 필터링은 사용자가 특정 아이템을 선호하는 경우, 그와 비슷한 콘텐츠를 가진 다른 아이템을 추천하는 방식이다.

예를 들어, 특정 영화에 높은 평점을 줬다면 그 영화의 장르, 배우,감독, 키워드 등이 유사한 다른 영화를 추천한다.

1.1 데이터 로딩 및 가공

영화 데이터 정보 사이트인 IMBD는 많은 영화에 대한 정보를 제공한다.

그 중 주요 5,000개의 영화에 대한 메타 정보를 가공해서 제공하는 캐글의 TMDB 5000 데이터를 사용한다.

movies = pd.read_csv("tmdb_5000_movies.csv")
movies.head(1)
budget genres homepage id keywords original_language original_title overview popularity production_companies production_countries release_date revenue runtime spoken_languages status tagline title vote_average vote_count
0 237000000 [{"id": 28, "name": "Action"}, {"id": 12, "nam... http://www.avatarmovie.com/ 19995 [{"id": 1463, "name": "culture clash"}, {"id":... en Avatar In the 22nd century, a paraplegic Marine is di... 150.437577 [{"name": "Ingenious Film Partners", "id": 289... [{"iso_3166_1": "US", "name": "United States o... 2009-12-10 2787965087 162.0 [{"iso_639_1": "en", "name": "English"}, {"iso... Released Enter the World of Pandora. Avatar 7.2 11800
movies.shape
(4803, 20)
  • 데이터는 4,803 x 20으로 이루어져 있다.

  • 이 중 주요 컬럼만 추출해 새롭게 데이터 프레임을 만든다.

# 주요 컬럼으로 데이터 프레임 생성
col_lst = ['id', 'title', 'genres', 'vote_average', 'vote_count', 'popularity', 'keywords', 'overview']
movies_df = movies[col_lst]
  • id: 아이디

  • title: 영화 제목

  • genres: 영화가 속한 여러가지 장르

  • vote_average: 평균 평점

  • vote_count: 평점 투표 수

  • popularity: 영화의 인기 정도

  • keywords: 영화를 설명하는 주요 키워드 문구

  • overview: 영화에 대한 개요 설명

# 컬럼 길이 늘려서 출력
pd.set_option('max_colwidth', 80)
movies_df[['genres','keywords']][:1]
genres keywords
0 [{"id": 28, "name": "Action"}, {"id": 12, "name": "Adventure"}, {"id": 14, "... [{"id": 1463, "name": "culture clash"}, {"id": 2964, "name": "future"}, {"id...
  • genres, keywords 등은 리스트 내에 여러 개의 사전으로 이루어진 형태로 입력되어 있다.

  • 각 장르, 키워드 명칭은 사전의 키인 name으로 추출 가능하다.

# 옵션 초기화
pd.reset_option("max_colwidth")
movies_df['genres'][0]
'[{"id": 28, "name": "Action"}, {"id": 12, "name": "Adventure"}, {"id": 14, "name": "Fantasy"}, {"id": 878, "name": "Science Fiction"}]'
  • 다만 현재 형태가 리스트 내에 사전인 것이지 실제론 전체가 문자열로 입력되어 있다.
from ast import literal_eval

movies_df['genres'].apply(literal_eval)[0]
[{'id': 28, 'name': 'Action'},
 {'id': 12, 'name': 'Adventure'},
 {'id': 14, 'name': 'Fantasy'},
 {'id': 878, 'name': 'Science Fiction'}]
  • astliteral_eval()은 문자 그대로 evaluate 해준다.

  • 따라서 여기선 문자열이 아닌 실제 리스트 내에 사전을 가진 객체로 만든다.

  • 이를 이용해 원하는 정보만 추출해보자.

from ast import literal_eval

# 문자열을 객체로 변경: 리스트 내의 사전
movies_df['genres'] = movies_df['genres'].apply(literal_eval)
movies_df['keywords'] = movies_df['keywords'].apply(literal_eval)

# 객체에서 name만 추출: 사전 마다 name을 추출
movies_df['genres'] = movies_df['genres'].apply(lambda x : [ dic['name'] for dic in x] )
movies_df['keywords'] = movies_df['keywords'].apply(lambda x : [ dic['name'] for dic in x] )

movies_df[['genres', 'keywords']][:1]
genres keywords
0 [Action, Adventure, Fantasy, Science Fiction] [culture clash, future, space war, space colon...
  • 문자열을 객체로 만든 후 각 사전마다 name을 추출하여 리스트 객체로 변환하였다.

  • 리스트 형태의 문자열이 아닌 리스트 객체이다.

1.2 장르 콘텐츠 유사도 측정

이번엔 장르별 유사도를 측정해보자.

유사도 측정은 문서 유사도에서 사용한 코사인 유사도를 사용한다.

from sklearn.feature_extraction.text import CountVectorizer

# 리스트 객체를 문자열로 변환: 공백으로 구분
movies_df['genres_literal'] = movies_df['genres'].apply(lambda x : (' ').join(x))

# CountVectorizer
count_vect = CountVectorizer(min_df=0, ngram_range=(1,2))
genre_mat = count_vect.fit_transform(movies_df['genres_literal'])

print(genre_mat.shape)
(4803, 276)
  • 우선 리스트 객체를 문자열로 변경후 Count 피처 벡터화를 적용하였다.
from sklearn.metrics.pairwise import cosine_similarity

genre_sim = cosine_similarity(genre_mat, genre_mat)
genre_sim[0]
array([1.        , 0.59628479, 0.4472136 , ..., 0.        , 0.        ,
       0.        ])
  • cosine_similarity()를 이용해 장르 유사도 행렬을 생성하였다.

  • 결과는 첫 번째 영화와 다른 영화의 장르 유사도만 확인하였다.

def find_sim_movie(df, sim_matrix, title_name, top_n=10):
    
    # 입력한 영화의 index
    title_movie = df[df['title'] == title_name]
    title_index = title_movie.index.values
    
    # 입력한 영화의 유사도 데이터 프레임 추가
    df["similarity"] = sim_matrix[title_index, :].reshape(-1,1)
    
    # 유사도 내림차순 정렬 후 상위 index 추출
    temp = df.sort_values(by="similarity", ascending=False)
    final_index = temp.index.values[ : top_n]
    
    return df.iloc[final_index]
  • 특정 영화(movies_df의 특정 행 index)와 장르 유사도가 높은 영화의 정보를 얻기 위한 함수를 생성하였다.

  • 교재와는 다른 방식으로 함수를 작성하여 특정 영화의 유사도를 데이터 프레임에 추가하였다.

# The Godfather(대부)와 장르별 유사도가 높은 영화 10개
similar_movies = find_sim_movie(movies_df, genre_sim, 'The Godfather', 10)
similar_movies[['title', 'vote_average', "similarity"]]
title vote_average similarity
3636 Light Sleeper 5.7 1.0
892 Casino 7.8 1.0
3866 City of God 8.1 1.0
1243 Mean Streets 7.2 1.0
1370 21 6.5 1.0
4041 This Is England 7.4 1.0
1847 GoodFellas 8.2 1.0
2582 The Place Beyond the Pines 6.8 1.0
1946 The Bad Lieutenant: Port of Call - New Orleans 6.0 1.0
4217 Kids 6.8 1.0
  • 교재와 결과가 많이 다른데 정말 여러 시도를 해보았다.

  • 교재의 경우 자기 자신의 문서 유사도를 제거하지 않는다(나중에 새로운 함수로 제거한다..).

  • 만약 첫 번째 index 영화 Avatar로 함수를 실행하면 첫 번째 추천이 Avatar로 나타난다.

  • 자기 자신 index만 제거하고 정렬을 하면 또 순서가 다르다.

  • 유사도가 같은 값이 매우 많아 특정 값을 제거하니 정렬 순서가 달라지는 듯 하다.

  • 그래서 최종적으로 오히려 단순하게 특정 영화의 유사도를 추가하고 정렬하였다(자기 자신 포함).

movies_df[['title','vote_average','vote_count']].sort_values('vote_average', ascending=False)[:10]
title vote_average vote_count
3519 Stiff Upper Lips 10.0 1
4247 Me You and Five Bucks 10.0 2
4045 Dancer, Texas Pop. 81 10.0 1
4662 Little Big Top 10.0 1
3992 Sardaarji 9.5 2
2386 One Man's Hero 9.3 2
2970 There Goes My Baby 8.5 2
1881 The Shawshank Redemption 8.5 8205
2796 The Prisoner of Zenda 8.4 11
3337 The Godfather 8.4 5893
  • 이번엔 평점순으로 영화를 정렬해보았다.

  • 유명한 영화가 아님에도 평가 횟수가 적어 평점이 높은 경우가 있다.

  • 평가 횟수와 평점을 모두 고려한 가중 평점 함수를 생성하자.

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)
  • 가중 평점은 IMDB에서 사용하는 방식을 사용한다.

  • IMDB에서 사용하는 지표로, 따로 공식화된 지표는 아닌 것 같아 수식은 표기하지 않는다.

temp = movies_df[['title','vote_average','vote_count','weighted_vote']]
temp.sort_values('weighted_vote', ascending=False)[:10]
title vote_average vote_count weighted_vote
1881 The Shawshank Redemption 8.5 8205 8.396052
3337 The Godfather 8.4 5893 8.263591
662 Fight Club 8.3 9413 8.216455
3232 Pulp Fiction 8.3 8428 8.207102
65 The Dark Knight 8.2 12002 8.136930
1818 Schindler's List 8.3 4329 8.126069
3865 Whiplash 8.3 4254 8.123248
809 Forrest Gump 8.2 7927 8.105954
2294 Spirited Away 8.3 3840 8.105867
2731 The Godfather: Part II 8.3 3338 8.079586
  • 가중 평점을 기준으로 상위 10개의 영화를 출력하였다.

  • 사람마다 성향은 다르겠지만 쇼생크 탈출, 대부 등 명작 영화가 보인다.

  • 장르 유사도와 가중 평점을 모두 고려한 영화 추천 함수를 만들어 보자.

def find_sim_movie(df, sim_matrix, title_name, top_n=10):
    
    # 입력한 영화의 index
    title_movie = df[df['title'] == title_name]
    title_index = title_movie.index.values
    
    # 입력한 영화의 유사도 데이터 프레임 추가
    df["similarity"] = sim_matrix[title_index, :].reshape(-1,1)
        
    # 유사도와 가중 평점순으로 높은 상위 index 추출 (자기 자신 제거)
    temp = df.sort_values(by=["similarity", "weighted_vote"], ascending=False)
    temp = temp[temp.index.values != title_index]
    
    final_index = temp.index.values[:top_n]
    
    return df.iloc[final_index]
similar_movies = find_sim_movie(movies_df, genre_sim, 'The Godfather', 10)
similar_movies[['title', 'vote_average', "weighted_vote", "similarity"]]
title vote_average weighted_vote similarity
1881 The Shawshank Redemption 8.5 8.396052 1.0
2731 The Godfather: Part II 8.3 8.079586 1.0
1847 GoodFellas 8.2 7.976937 1.0
3866 City of God 8.1 7.759693 1.0
1663 Once Upon a Time in America 8.2 7.657811 1.0
3887 Trainspotting 7.8 7.591009 1.0
883 Catch Me If You Can 7.7 7.557097 1.0
892 Casino 7.8 7.423040 1.0
281 American Gangster 7.4 7.141396 1.0
4041 This Is England 7.4 6.739664 1.0
  • 교재랑 다르게 유사도가 높으면서 가중 평점이 높은 경우 추출되게 만들었다.

  • 그래도 이번엔 가중 평점도 고려한 덕에 교재랑 결과가 비슷하다.

  • 앞서 언급하였듯이 유사도가 1인 경우가 너무 많아 단순 정렬 문제이다.

  • 대부2, 원스 어폰 어 타임 인 아메리카 등이 추천 영화로 나타났다.

Leave a comment