[머신러닝] 추천 시스템 알고리즘 - 콘텐츠 기반 필터링
추천시스템
추천 시스템은 정보 필터링 (IF) 기술의 일종으로, 특정 사용자가 관심을 가질만한 정보 (영화, 음악, 책, 뉴스, 이미지, 웹 페이지 등)를 추천하는 것이다. 추천 시스템에는 협업 필터링 기법을 주로 사용한다.
소셜 북마크 사이트에서 링크를 사람들에게 추천하고 무비렌즈 데이터 세트에서 영화를 추천하는 방법등이 이에 속한다.
크게 콘텐츠 기반 필터링(content based filtering) 방식과 협업 필터링(collaboration filtering) 방식으로 나뉜다.
추천 시스템 종류
1. 콘텐츠 기반 필터링
사용자가 한 아이템을 선호하는 경우 그 아이템과 비슷한 콘텐츠를 가진 다른 아이템을 추천하는 방식이다.
예를 들어, 한 사용자가 특정 영화에 높은 평점을 주었다면 그 영화의 장르, 감독, 출연배우 등의 콘텐츠와 유사한 다른 영화를 추천해주는 방식이다. (rating값이 없어도 가능하다)
2. 협업 필터링
- 최근접 이웃 협업 필터링(Nearst Neighbor) 과 잠재요인 (Latent Factor) 협업 필터링 으로 나뉜다.
- 최근접 이웃 협업 필터링 Nearst Neighbor
사용자가 아이템에 매긴 평점정보나 구매이력과 같은 사용자 행동 양식만을 기반으로 추천을 수행하는 방법이다.
즉, 사용자가 평가하지 않은 아이템을 평가한 아이템에 기반하여 예측 평가하는 알고리즘을 말한다.
평점과 같은 반응점수 rating이 필요하다.
희소행렬의 특징을 가지고 있다. (하지만 개수에 따라 너무 큰 희소행렬을 가지게 된다는 단점이 존재한다.)
[실습] 장르 속성을 이용한 영화 콘텐츠 기반 필터링
데이터 로딩 및 가공
TMDB 5000 데이터 셋
: 영화데이터 정보 사이트인 imdb.com 영화 중 주요 영화 5000 개에 대한 메타정보를 가공해서 kaggle 에서 제공하는 데이터 세트
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)
분석에 사용할 주요 컬럼 추출
id, title, genres, vote_average(평균 평점), vote_count(평점 투표수), popularity(영화 인기도), keywords, overview(영화개요)
movies_df = movies[['id', 'title', 'genres', 'vote_average',
'vote_count', 'popularity', 'keywords', 'overview']]
장르와 키워드 컬럼이 리스트 안에 딕셔너리 형태로 구성되어 있는 것을 head()를 통해 확인 할 수 있다.
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)
값을 확인해보면
movies_df['genres'].values[0]
[{'id': 28, 'name': 'Action'},
{'id': 12, 'name': 'Adventure'},
{'id': 14, 'name': 'Fantasy'},
{'id': 878, 'name': 'Science Fiction'}]
리스트 안에 딕셔너리 형태로 있는 것을 알 수 있다.
장르와 키워드 컬럼의 name 키의 값만 원소로 추출하여 리스트로 생성
lambda x: [y ['name'] for y in x]
: movies_df['genres'] 에서 딕셔너리 형태로 된 값들을 하나씩 받아와 그 중 'name' 의 값만을 리스트로 저장한다.
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'].values[0]
['Action', 'Adventure', 'Fantasy', 'Science Fiction']
장르 콘텐츠 유사도 측정
- 리스트로 변환된 장르 컬럼은 카운트 기반으로 피처 벡터화 변환 (사이킷런의 CountVectorizer 이용)
- 장르에 대한 문자열을 피처 벡터화 행렬로 변환한 데이터 세트를 코사인 유사도를 통해 비교한다.
- 장르 유사도가 높은 영화중에 평점이 높은 순으로 영화를 추천한다.
[참고] CountVectorizer
텍스트에서 단위(단어)별 출현 횟수를 카운팅하여 수치 벡터화 한다.
from sklearn.feature_extraction.text import CountVectorizer
# ngram_range: 모델의 단어 순서를 어느정도 보강하기 위한 범위(범위최소값, 범위 최대값)
# (1,1): 단어를 한 개씩 피처로 추출
# (1,2): 토큰화 된 단어를 1개씩 뽑아내고, 순서대로 두 개씩 묶어 피처로 추출
vectorizer = CountVectorizer(ngram_range=(1,1)) # ngram_range: 단어 수 보강 목적, (1,1): default
vectorizer.fit(['첫번째 문서 테스트','두번째 문서 테스트']) # 4개의 어휘를 학습한 CountVectorizer
print(vectorizer.vocabulary_) # 고유한 단어가 각각의 인덱스를 가지게 된다.
counts = vectorizer.transform(['직접 첫번째 테스트 두번째 테스트']) # 새로운 문서에 대해 미리 학습해놓은 사전을 기반으로 단어의 빈도수를 세어준다.
# print(counts) # 밀집행렬의 형태로 보여준다
print(counts.toarray()) # 희소행렬
{'첫번째': 2, '문서': 1, '테스트': 3, '두번째': 0}
[[1 0 1 2]]
['Action', 'Adventure', 'Fantasy', 'Science Fiction'] --> ['Action Adventure Fantasy Science Fiction'] 만들기 위해서는..
# CounterVectorize 를 적용하기 위해 공백 문자로 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)
# 피처 벡터화 변환
print(genre_mat.toarray()[:1]) # 4803 : 영화개수, 276 : 장르개수 (단어를 보강했기 때문에 개수가 276개이다.)
(4803, 276)
[[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]]
코사인 유사도는 벡터간의 유사도를 비교할때 벡터의 상호방향이 얼마나 유사한가에 기반한다. ( -1 <= cos(a,b) <= 1 )
코사인 유사도가 높다는 말은 벡터가 비슷하다는 의미이기 때문에 유사관계를 알아보기 위해서는 코사인 유사도를 사용한다.
# 코사인 유사도
from sklearn.metrics.pairwise import cosine_similarity
genre_sim = cosine_similarity(genre_mat, genre_mat)
print(genre_sim.shape)
print(genre_sim[:3]) # 대각행렬 - 대칭행렬
# 0행 : 0 번째 영화 ~ 4802개 영화의 장르간 코사인 유사도
(4803, 4803)
[[1. 0.59628479 0.4472136 ... 0. 0. 0. ]
[0.59628479 1. 0.4 ... 0. 0. 0. ]
[0.4472136 0.4 1. ... 0. 0. 0. ]]
영화간의 유사관계를 알아보는 코사인 유사도이므로 4803 x 4803 의 대각행렬을 만들게 된다. (자기자신 = 대각선 = 1)
genre_sim_sorted_idx = genre_sim.argsort()[:,::-1] # 유사도가 높은순으로 내림차순
# argsort() : 데이터를 오름차순으로 정렬 후 원 데이터의 인덱스 값을 반환
print(genre_sim_sorted_idx[:1]) # 인덱스값 // 맨 처음은 자기 자신
[[ 0 3494 813 ... 3038 3037 2401]]
영화 정보에 대해 장르 기준으로 콘텐츠기반 필터링을 하려면 영화 데이터의 개별 레코드에 대해 가장 장르 유사도가 높은 순으로 다른 레코드를 추출해야한다.
이를 위해 코사인 유사도 행렬의 기준 행별로 비교대상이 되는 행의 유사도 값이 높은 순으로 정렬된 행렬의 위치인덱스 값을 추출한다(유사관계를 정렬하여 본 데이터에 대입하기 위하여 argsort를 사용)
장르 컨텐츠 필터링을 이용한 영화 추천을 위한 함수 find_sim_movie()
def find_sim_movie(df, sorted_idx, title_name, top_n = 10): # top_n = 추천 영화의 개수 (default : 10)
target_movie = df[df['title'] == title_name] # 추천 기준과 같은 영화를 가져온다.
title_index = target_movie.index.values # 인덱스 객체의 실제 값을 가져옴
similar_index = sorted_idx[title_index, :top_n] # sorted_idx = 영화이름 x 유사도가 높은 영화이름
print(similar_index)
# 추출된 top_n은 2차원 데이터로 되어있기 때문에 1차원 배열로 변환한다.
similar_index = similar_index.reshape(-1)
return df.iloc(similar_index)