본문 바로가기
머신러닝과 딥러닝/실습

장르 유사도 기반 영화 추천 시스템 / Scikit-Learn의 문제점

by Bentist 2020. 1. 28.

* 코사인 유사도 개념을 이용하여, 장르가 유사한 영화를 추천해주는 시스템을 만들어보았습니다.

 

[ 데이터 전처리와 벡터화 ]

1) 영화 데이터셋은 https://www.kaggle.com/tmdb/tmdb-movie-metadata 사용

2) 각 장르를 feature 벡터화 한 후, 형렬로 만든 다음 행렬 데이터로 코사인 유사도로 계산

   -> 코사인 유사도가 1에 가까울수록 두 영화 간의 장르가 비슷하다고 할 수 있습니다.

3) 데이터 전처리 과정 중 Scikit-Learn의 문서 전처리용 클래스인 CountVectorizer 문제점 발견

   -> 이를 순수 파이썬 코드를 작성하여 비교해보았습니다.

 

import pandas as pd
import numpy as np

from sklearn.metrics.pairwise import cosine_similarity

movies = pd.read_csv('tmdb_5000_movies.csv')
movies.columns

칼럼 인덱스명에서 genres(장르)를 벡터화시킬 예정

# 컬럼 일부만 가져오기
movies_df = movies[['id','title','genres', 'vote_average', 'vote_count',
                    'popularity','keywords','overview']]

# 'genres'컬럼은 str 형태로 리스트 안에 딕셔너리로 장르 키워드들이 들어가있다.
movies_df['genres'][0]

-> AST모듈의 literal_eval을 사용하여 str타입을 list타입으로 변경해줍니다.

from ast import literal_eval
movies_df['genres'] = movies_df['genres'].apply(literal_eval)
movies_df['genres']

-> Count 기반으로 벡터화하기 위해 리스트를 공백 문자를 기준으로 구분되는 문자열로 변환

movies_df['genres_literal'] = movies_df['genres'].apply(lambda x : (' ').join(x))
movies_df['genres_literal']

리스트가 문자열로 바뀐 것을 확인

# CountVectorizer()함수는 문서 집합에서 단어 토큰을 생성하고,
# 각 단어의 수를 세어 BOW(bag of words) 인코딩한 벡터를 만드는 것
# 4803개의 영화의 문서에서 해당 단어가 있으면 1, 없으면 0 행렬을 만들게 된다.

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

> (4803, 22)

결과를 보면 22개의 영화 장르가 생성되었지만, 영화 장르의 개수는 총 20개입니다.

그 이유는 리스트를 공백을 기준으로 나누어서 count하였기 때문에 'TV Movie'와 'Science Fiction' 이 한번씩 더 나눠져 'TV', 'Movie', 'Science', 'Fiction'으로 장르가 2번 더 분리되었습니다.

이 문제는 아래에서 해결하도록 하고, CountVectorizer의 ngram_range에 대해서 간단히 알아보고 넘어가겠습니다.

 

* ngram_range=(1,1)은 feature 값으로 단일 장르만 벡터화 시킨 것입니다.

아래 예제를 통해 ngram_range를 더 살펴보겠습니다.

corpus = [
    'This is the first document.',
    'This is the second second document.',
    'And the third one.',
    'Is this the first document?',
    'The last document?',
]
vect = CountVectorizer()
vect.fit(corpus)
vect.vocabulary_

-> ngram_range=(1,1)은 토큰 하나만 단어로 사용하여 벡터화 합니다.

-> ngram_range=(1,2)은 1개의 토큰 단어와 2개가 연결된 토큰을 하나의 단어처럼 사용하여 벡터화 합니다.

-> 아래의 벡터를 보면, 1개로 된 단어와 2개가 조합된 단어의 수가 합쳐져 벡터화가 되었습니다.

[문제 해결 과정]

CountVectorizer로 벡터화된 장르 매트릭스는 공백을 기준으로 문자열 변환되어, 'science fiction'과 같은 하나의 장르를 'science', 'fiction' 두 개의 장르로 나누어버리는 문제점이 발생하였습니다.

이것을 해결하기 위해, 파이썬 문법을 사용하여 직접 코사인 유사도 행렬을 만들어보겠습니다.

 

CountVectorizer을 사용하지 않고 장르의 name에 0 ~ 20까지 새로운 id를 부여해서 영행렬을 정의합니다.

그리고나서 해당 영화의 장르에 1을 할당해주는 작업을 진행해서 (4803, 20)의 행렬을 만들 것입니다.

import pandas as pd
import numpy as np

from ast import literal_eval
from sklearn.metrics.pairwise import cosine_similarity

movies = pd.read_csv('tmdb_5000_movies.csv')
movies_df = movies[['id','title','genres', 'vote_average', 'vote_count',
                    'popularity','keywords','overview']]

movies_df['genres'] = movies_df['genres'].apply(literal_eval)
movies_df['genres'] = movies_df['genres'].apply(lambda x: [y['name'] for y in x])

box = []
for i in movies_df['genres']:
    for v in i: 
        box.append(v)  

new_genres = pd.Series(box)
# 판다스의 .unique()메소드를 사용하여 중복되는 장르값을 제거한다.
new_genres = new_genres.unique()
new_genres

총 20개의 장르가 제대로 나온 것을 확인할 수 있다.

genres_num = dict(zip(range(len(new_genres)), list(new_genres)))
# >> {0: 'Action', 1: 'Adventure'..} 

# key와 values를 바꿔주는 작업 진행
new_genres_id = {v: k for k, v in genres_num.items()}
new_genres_id

box = []
for i in movies_df['genres']:
    mini_box = []
    for v in i: 
        if v in new_genres_id.keys():
            mini_box.append(new_genres_id[v])
    box.append(mini_box)
    
print(box)    

-> 4803개의 영화가 갖고 있는 장르를 숫자 0~19로 할당하고, 열방향 행렬을 만들기 위한 작업을 진행

 

# 영화 수와 영화 장르 수에 맞는 영행렬 생성

score = np.zeros((movies_df['id'].nunique(), movies_df['genres_literal'].nunique()))
score.shape
>> (4803, 20)

for i, v in enumerate(box):
    for w in v:
        score[i,w] = 1.0

print(score.shape, end='\n\n')
print(score)

>> (4803, 20)
 
 [[1. 1. 1. ... 0. 0. 0.]
 [1. 1. 1. ... 0. 0. 0.]
 [1. 1. 0. ... 0. 0. 0.]
 ...
 [0. 0. 0. ... 0. 0. 1.]
 [0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 1. 0. 0.]]

-> 위의 코드를 통해 4803개의 영화가 어떤 장르(0~19)를 포함하고 있는지에 대한 정보를 0과 1로 나타내고 있음

 

cosine_similar = cosine_similarity(score, score)

# 자기 자신에 대한 유사도는 가끔 0.99999997 이 있기 때문에 사전 처리
for i in range(movies_df['id'].nunique()):
    cosine_similar[i,i] = 1.0

cosine_similar_data = pd.DataFrame(cosine_similar)
cosine_similar_data.head(10)

print(cosine_similar_data)

이로써 영화별 장르 유사도가 계산된 행렬(4803, 4803)이 무사히 생성되었습니다.

댓글