[ML] 14. 콘텐츠 기반 필터링
▶ 장르 속성을 이용한 영화 콘텐츠 기반 필터링
## 데이터 로딩 및 가공 ##
- TMDB 5000 데이터셋 : 영화 데이터 정보 사이트인 imdb.com의 영화 중 주요 영화 5,000개에 대한 메타 정보를 가공해서 kaggle에서 제공하는 데이터 세트
- https://www.kaggle.com/tmdb/tmdb-movie-metadata
TMDB 5000 Movie Dataset
Metadata on ~5,000 movies from TMDb
www.kaggle.com
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings(action='ignore')
movies = pd.read_csv('./Dataset/tmdb_5000_movies.csv')
print(movies.shape) # (4803, 20)
display(movies.head())
## 분석에 사용할 주요 컬럼 추출 ##
- id, title, genres, vote_average(평균 평점), vote_count(펑점 투표수), popularity(영화 인기도), keywords, overview(영화 개요)
movies_df = movies[['id', 'title', 'genres', 'vote_average', 'vote_count', 'popularity', 'keywords', 'overview']]
movies_df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4803 entries, 0 to 4802
Data columns (total 8 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 id 4803 non-null int64
1 title 4803 non-null object
2 genres 4803 non-null object
3 vote_average 4803 non-null float64
4 vote_count 4803 non-null int64
5 popularity 4803 non-null float64
6 keywords 4803 non-null object
7 overview 4800 non-null object
dtypes: float64(2), int64(2), object(4)
memory usage: 300.3+ KB
# 출력 범위를 키워줌
pd.set_option('max_colwidth', 100)
# 장르와 키워드 컬럼은 리스트 안에 딕셔너리 형태로 되어있음
movies_df[['genres', 'keywords']][:1]
# 장르 컬럼의 값 목록을 가져옴 (문자열 형태)
movies_df['genres'].values[0]
'[{"id": 28, "name": "Action"}, {"id": 12, "name": "Adventure"}, {"id": 14, "name": "Fantasy"}, {"id": 878, "name": "Science Fiction"}]'
## eval()과 literal_eval() ##
- eval()은 문자 형태로 되어있는 표현식을 실행하는 함수로, 함수나 객체도 가능
- literal_eval()은 eval()과는 다르게 파이썬에서 제공하는 기본 데이터 타입정도만 변환해주는 용도로 사용 가능
## literal_eval() 함수를 통해 genres, keywords 컬럼의 값을 list 객체로 변환 ##
from ast import literal_eval
movies_df['genres'] = movies_df['genres'].apply(literal_eval)
movies_df['keywords'] = movies_df['keywords'].apply(literal_eval)
# 문자열이 아닌 파이썬 list 객체로 변환된 것을 확인
movies_df['genres'].values[0]
[{'id': 28, 'name': 'Action'},
{'id': 12, 'name': 'Adventure'},
{'id': 14, 'name': 'Fantasy'},
{'id': 878, 'name': 'Science Fiction'}]
# 장르와 키워드 컬럼의 name 키의 값만 원소로 추출해서 리스트로 생성
# ['Action', 'Adventure', ...] 장르 리스트 만들기
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])
# y는 딕셔너리 객체
# 첫 번째 영화가 갖는 장르 값만 리스트 형태로 반환
movies_df['genres'].values[0]
['Action', 'Adventure', 'Fantasy', 'Science Fiction']
## 장르 콘텐츠 유사도 측정 ##
- 리스트로 변환된 장르 컬럼은 카운트 기반으로 피처 벡터화 변환 (sklearn의 CountVectorizer 이용)
- 장르 문자열을 피처 벡터화 행렬로 변환한 데이터셋을 코사인 유사도를 통해 비교
- 장르 유사도가 높은 영화 중에 평점이 높은 순으로 영화를 추천
[참고] CountVectorizer
- 텍스트에서 단위(단어)별 출현 횟수를 카운팅하여 수치 벡터화 한다.
from sklearn.feature_extraction.text import CountVectorizer
# ngram_range : 모델의 단어 순서를 어느정도 보강하기 위한 범위 (범위 최소값, 범위 최대값)
# (1,1) : 단어를 1개씩 피처로 추출
# (1,2) : 토큰화된 단어를 1개씩, 그리고 순서대로 2개씩 묶어서 피처로 추출 (피처의 수가 늘어남)
vectorizer = CountVectorizer(ngram_range=(1,1))
# 4개의 어휘를 학습한 CountVectorizer를 생성
vectorizer.fit(['첫번째 문서 테스트', '두번째 문서 테스트'])
print(vectorizer.vocabulary_) # 고유한 단어가 각각의 인덱스를 가지게 된다
# 새로운 문서에 대해 미리 학습해놓은 사전을 기반으로 단어의 빈도수를 세어준다
counts = vectorizer.transform(['직접 첫번째 테스트 두번째 테스트'])
print(counts) # 밀집행렬의 형태
print(counts.toarray()) # 희소행렬의 형태
{'첫번째': 2, '문서': 1, '테스트': 3, '두번째': 0}
(0, 0) 1
(0, 2) 1
(0, 3) 2
[[1 0 1 2]]
- ['Action', 'Adventure', 'Fantasy', 'Science Fiction'] --> 'Action Adventure Fantasy Science Fiction' 형태로 변환하기
# CounterVectorizer를 적용하기 위해 공백문자로 word 단위가 구분되는 문자열로 변환하여 새로운 컬럼에 추가
movies_df['genres_literal'] = movies_df['genres'].apply(lambda x: ' '.join(x))
# min_df : 전체 문서에서 낮은 빈도수를 갖는 단어 피처를 제외하기 위한 파라미터
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) 영화 4803개, 장르가 276개
print(genre_mat.toarray()[:1]) # 첫번째 영화 장르에 맞는 값은 1로 반환
[[1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]]
## 장르간의 코사인 유사도 ##
from sklearn.metrics.pairwise import cosine_similarity
genre_sim = cosine_similarity(genre_mat, genre_mat)
print(genre_sim.shape) # (4803, 4803)
print(genre_sim[:3]) # 대각행렬 기준으로 대칭행렬
[[1. 0.59628479 0.4472136 ... 0. 0. 0. ]
[0.59628479 1. 0.4 ... 0. 0. 0. ]
[0.4472136 0.4 1. ... 0. 0. 0. ]]
# 장르 유사도에 대헤 정렬이 된 인덱스 값 반환
genre_sim_sorted_idx = genre_sim.argsort(axis=1)[:,::-1] # 행기준으로 유사도가 높은 순서대로 내림차순 정렬
print(genre_sim_sorted_idx[:1]) # 첫 번째 영화와 장르간 유사도가 높은 영화의 인덱스 값
[[ 0 3494 813 ... 3038 3037 2401]]
print(genre_sim_sorted_idx[0]) # 1번째 영화와 장르간 유사도가 높은 영화의 인덱스 값
print(genre_sim_sorted_idx[1]) # 2번째 영화와 장르간 유사도가 높은 영화의 인덱스 값
print(genre_sim_sorted_idx[2]) # 3번째 영화와 장르간 유사도가 높은 영화의 인덱스 값
print(genre_sim_sorted_idx[3]) # 4번째 영화와 장르간 유사도가 높은 영화의 인덱스 값
[ 0 3494 813 ... 3038 3037 2401]
[ 262 1 129 ... 3069 3067 2401]
[ 2 1740 1542 ... 3000 2999 2401]
[2195 1850 3316 ... 887 2544 4802]
# 장르가 같은 영화는 유사도가 모두 1로 계산됨
print(movies_df.loc[262, 'genres']) # 262번째 영화의 장르
print(movies_df.loc[1, 'genres']) # 1번째 영화의 장르
print(movies_df.loc[129, 'genres']) # 129번째 영화의 장르
# 모두 동일한 장르인 것을 확인
['Adventure', 'Fantasy', 'Action']
['Adventure', 'Fantasy', 'Action']
['Adventure', 'Fantasy', 'Action']
## 장르 콘텐츠 필터링을 이용한 영화 추천 ##
# 찾고자 하는 영화를 데이터프레임으로 가져옴
movies_df[movies_df['title'] == 'The Dark Knight Rises']
def find_sim_movie(df, sorted_idx, title_name, top_n=10):
# 대상이 되는 영화를 찾아옴
target_movie = df[df['title'] == title_name]
# 대상이 되는 영화의 인덱스 값을 가져옴
title_index = target_movie.index.values
# 대상이 되는 영화와 유사도가 높은 상위 n개의 영화의 인덱스 값을 가져옴
similar_index = sorted_idx[title_index, :top_n]
# print(similar_index)
# 추출된 top_n 인덱스는 2차원 데이터이기 때문에 1차원 배열로 변환
similar_index = similar_index.reshape(-1)
return df.iloc[similar_index]
## 조금 더 많은 후보군을 선정한 뒤에 **영화의 평점에 따라 필터링**하는 방식으로 변경 필요 ##
similar_movies = find_sim_movie(movies_df, genre_sim_sorted_idx, 'The Godfather', 10)
similar_movies[['title', 'vote_average']] # 추천 받은 영화의 제목과 평점만 확인
# 평점이 낮은 영화도 추천해주는 것이 문제
## 영화 평점이 높은 순으로 정렬하여 상위 10개 영화 확인 ##
# 제목, 평점, 평점 투표애 참여한 인원수 컬럼을 평점 기준으로 내림차순 정렬
movies_df[['title', 'vote_average', 'vote_count']].sort_values('vote_average', ascending=False) [:10]
# 평점이 높아도 참여율이 낮으면 의미 없음
## 평점에 평가 횟수를 반영한 가중 평점 방식 적용 필요 ##
- 가중 평점 = (v/(v+m)) * R + (m/(v+m)) * C
- v : 개별 영화에 평점을 투표한 횟수 (vote_count)
- m : 평점을 부여하기 위한 최소 투표 횟수 (임의의 값)
- R : 개별 영화에 대한 평균 평점 (vote_average)
- C : 전체 영화에 대한 평균 평점
# 전체 평균
C = movies_df['vote_average'].mean()
# 전체 투표 횟수에서 상위 60%에 해당하는 횟수를 기준으로 정함
m = movies_df['vote_count'].quantile(0.6)
print(f'C:{C:.3f}, m:{m:.3f}')
C:6.092, m:370.200
# 가중 평점 계산 함수
def weighted_vote_average(movie):
v = movie['vote_count']
R = movie['vote_average']
return (v/(v+m)) * R + (m/(v+m)) * 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]
# 더 많은 후보군을 만들어냄
def find_sim_movie(df, sorted_idx, title_name, top_n=10):
# 대상이 되는 영화를 찾아옴
target_movie = df[df['title'] == title_name]
# 대상이 되는 영화의 인덱스 값을 가져옴
title_index = target_movie.index.values
# 장르 유사도가 높은 인덱스를 top_n의 2배수로 추출
similar_index = sorted_idx[title_index, :top_n*2]
similar_index = similar_index.reshape(-1)
# 대상이 되는 영화 인덱스는 제외
similar_index = similar_index[similar_index != title_index]
return df.iloc[similar_index].sort_values('weighted_vote', ascending=False)[:top_n]
similar_movies = find_sim_movie(movies_df, genre_sim_sorted_idx, 'The Godfather', 10)
similar_movies[['title', 'vote_average', 'weighted_vote']]