[kaggle] 영화 추천 시스템 2편

2018. 8. 9. 17:49kaggle

영화 추천 시스템 2편 



1편은 단순 장르만으로 추천을 해주었다면 이번에는 내용, 배우, 감독 등을 고려하여 평가하는 시스템을 만들어 보겠다. 필요한 데이터 파일들은 1편에 올렸습니다. 

우리가 1편에서 만들었던 시스템은 사람들의 성향과 영화의 내용을 생각하지 않고 추천을 해주는 시스템이었습니다. 그래서 영화간의 유사도를 조사해서 특정 매트릭스를 만들어서 줄거리, 배우, 감독 등을 고려해서 추천영화를 만들어 보겠습니다. 

1. 미니 데이터 만들기

원본 데이터를 그대로 사용하기에는 필자의 컴퓨터에는 무리가 가서 원본보다는 small사이즈의 데이터를 사용하겠습니다. 4


link_small = pd.read_csv('input/links_small.csv')
link_small = link_small[link_small['tmdbId'].notnull()]['tmdbId'].astype('int') md = md.drop([19730, 29503, 35587]) md['id'] = md['id'].astype('int') smd = md[md['id'].isin(link_small)]

smd라는 작은 데이터를 만들었습니다. md의 id와 smd의 tmdbid는 같은 것 같습니다. 




2. 영화의 설명을 벡터화 시키기 



smd['tagline'] = smd['tagline'].fillna('')
smd['description'] = smd['overview'] + smd['tagline']
smd['description'].fillna('')

np.nan 

tf = TfidfVectorizer(analyzer='word', ngram_range=(1, 2), min_df=0, stop_words='english')
tfidf_matrix = tf.fit_transform(smd['description'])

'word'를 기준으로 vector화 시킨다.


min_df, max_df: 문서에서 토큰이 나타난 횟수를 기준으로 단어장을 구성할 수도 있다. 토큰의 빈도가 max_df로 지정한 값을 초과 하거나 min_df로 지정한 값보다 작은 경우에 무시한다. 


stop_words='english' 문서에 단어장을 생성할 때 무시할 수 있는 단어를 말한다. 


ngram_range(1, 2) 단어장 생성에 사용할 토큰의 크기를 결정한다. 


analyzer='word' 문자열 또는 함수


이런다음 TfidfVectorizer 인코딩에 대해 알아보자!


단어를 갯수 그대로 카운트하지 않고 모든 문서에 공통적으로 들어있는 단어의 경우 

문서 구별 능력이 떨어진다고 보아 가중치를 축소하는 방법이다.

tf-idf(d,t)=tf(d,t)idf(t)


tf: 특정한 단어의 빈도수

idf: 특정한 단어가 들어 있는 문서의 수에 반비례하는 수




3. cosineSimilarty를 사용하여 두영화 사이에서 유사성을 계산하기


cosine Similarity를 사용해서 두 영화 사이의 유사성을 계산합니다. 


cosine(x, y) =  (x.y⊺) / (|| x ||. || y ||)

TF-IDF Vectorizer를 사용했기 때문 Dot Product를 계산하면 Cosine Similarity Score가 바로 제공됩니다.




cosine_sim = linear_kernel(tfidf_matrix, tfidf_matrix)
cosine_sim[0]

array([1.        , 0.00680476, 0.        , ..., 0.        , 0.00344913,

       0.        ])


데이터 정제 중 

smd = smd.reset_index()
titles = smd['title']
indces = pd.Series(smd.index, index=titles)


smd에 인덱스를 포함하고 타이틀을 만든다. pd.Series를 통해서 타이틀을 인덱스로 하고 indces를 만듭니다. 선형 커널로 만든 consine_sim의 값을 

def getrecommandations(title):
index = indces[title]
sim_scores = list(enumerate(cosine_sim[index]))
sim_scores = sorted(sim_scores, key=lambda x:x[1], reverse=True)
sim_scores = sim_scores[1:31]
movie_indices = [i[0] for i in sim_scores]
return titles.iloc[movie_indices]


list(enumerate(cosine_sim[0]))

[(0, 1.0000000000000018), (1, 0.0068047556717484225), (2, 0.0), (3, 0.0), (4, 0.0), (5, 0.0), (6, 0.0), (7, 0.0), (8, 0.0), (9, 0.0), (10, 0.0), (11, 0.0), (12, 0.0), (13, 0.0), (14, 0.0), (15, 0.0), (16, 0.006787806467693803)] 인덱스 값과 입려된 인덱스와 다른 인덱스 영화들이 얼마나 비슷한지 점수가 나온다. 


이런 점수를 기준으로 정렬 후 30개의 후보를 빱아서 타이틀을 출력시켜준다. 


getrecommandations('The Dark Knight')



4. 감독, 배우등을 고려해서 추천해주기 



1. credit과 keywords파일 읽어오기 


credits = pd.read_csv('movieRecommaned/input/credits.csv')
keywords = pd.read_csv('movieRecommaned/input/keywords.csv')

1편에 등록된 파일 들 중 하나이다. 


keywords['id'] = keywords['id'].astype(int)
credits['id'] = credits['id'].astype(int)
md['id'] = md['id'].astype(int)

int로 형태 변환을 시켜준다. 


md = md.merge(credits, on='id')
md = md.merge(keywords, on='id')

id 값을 기준으로 크레딧과 키워드를 md에 합쳐준다. 


smd = md[md['id'].isin(link_small)]

동일한 id가 link_small에 존재하는 데이터만 smd에 넣어준다. 


2. 데이터 정재하기


2-1. 배우와 감독 데이터 정제하기 



smd['cast'] = smd['cast'].apply(literal_eval) #cast데이터를 ''문자열안에 있는 document를
smd['crew'] = smd['crew'].apply(literal_eval) #진짜 document로 만든다.
smd['keywords'] = smd['keywords'].apply(literal_eval)
smd['cast_size'] = smd['cast'].apply(lambda x :len(x)) #배우 수를 적는다.
smd['crew_size'] = smd['crew'].apply(lambda x: len(x)) #스태프 수를 적는다.

스태프들 중에서 감독만 가져온다. 왜냐면 감독도 영화 선택에 영향을 끼치니깐 

def get_director(x):
for i in x:
if i['job'] == 'Director':
return i['name']
return np.nan


smd에 감독 데이터를 집어넣는다. 

smd['director'] = smd['crew'].apply(get_director)


배우들 이름을 넣는다.

smd['cast'] = smd['cast'].apply(lambda x : [i['name'] for i in x] if isinstance(x, list) else [])


가장 영향력있는 배우 세명을 넣는다.

smd['cast'] = smd['cast'].apply(lambda x: x[:3] if len(x) >3 else x)


키워드도 넣어준다.

smd['keywords'] = smd['keywords'].apply(lambda x: [i['name'] for i in x] if isinstance(x, list) else [])



일단 우리가 원하는 것은 장르, 감독, 주인공, 및 키워드로 이루어진 모든 영화에 대한 메타 데이터 덤프를 만드는 것 입니다. 

그런 다음 CountVectorizer를 사용하여 다른 영화들과 얼마나 비슷한지 비교하고 점수를 기준으로 정렬합니다. 


smd['cast'] = smd['cast'].apply(lambda x : [str.lower(i.replace(" ", "")) for i in x])
smd['director'] = smd['director'].astype(str).apply(lambda x: str.lower(x.replace(" ", "")))
smd['director'] = smd['director'].apply(lambda x: [x, x, x])

배우와 감독 데이터들을 소문자화 시켜주고 띄어쓰기를 없애줍니다. 그리고 감독은 강조시키기 위해 세번 반복해서 적어줬습니다. 


s = smd.apply(lambda x: pd.Series(x['keywords']), axis=1).stack().reset_index(level=1, drop=True)
s.name = 'keywords'

keywords라는 인덱스를 기준으로 각 영화마다의 장르를 동일한 인덱스로 표현할 수 있다. 


s = s.value_counts()

independent film        610

woman director          550

murder                  399

duringcreditsstinger    327

based on novel          318

Name: keywords, dtype: int64

s = s[s>1]

하나밖에 없는 키워드는 영향력이 적으므로 없애준다. 


stemmer = SnowballStemmer('english')
stemmer.stem('dogs')

dogs, dogsss 같은 원형에서 변형된 단어를 원형으로 바꾸어준다. 위와 같은 경우에는 dog가 된다. 


def filter_keywords(x):
words = []
for i in x:
if i in s:
words.append(i)
return words

키워드를 골라내서 인덱스에 담는다. 그후 리턴해준다. 

위에서 구했던 s는 우리가 직접적으로 사용할 수 있는 데이터인지 아닌지 구분해주기 위한 것 입니다. 


smd['keywords'] = smd['keywords'].apply(filter_keywords)

0       [jealousi, toy, boy, friendship, friend, rival...

1       [boardgam, disappear, basedonchildren'sbook, n...

2                  [fish, bestfriend, duringcreditssting]

3       [basedonnovel, interracialrelationship, single...

4       [babi, midlifecrisi, confid, age, daughter, mo...


단어들을 원형으로 만들어주고 띄어쓰기를 없애줍니다. 

smd['keywords'] = smd['keywords'].apply(lambda x :[stemmer.stem(i) for i in x])
smd['keywords'] = smd['keywords'].apply(lambda x: [str.lower(i.replace(" ", "")) for i in x])


전체적인 데이터들을 합쳐서 soup로 만들어줍니다. 각 soup 사이 데이터마다 띄어쓰기를 추가합니다.  

smd['soup'] = smd['keywords']+smd['cast']+smd['director']+smd['genres']
smd['soup'] = smd['soup'].apply(lambda x: ' '.join(x))


단어별로 벡터화 시켜주기

count = CountVectorizer(analyzer='word', ngram_range=(1, 2), min_df=0, stop_words='english')
count_matrix = count.fit_transform(smd['soup'])

CountVectorizer를 이용해서 벡터화 시켜줍니다. 각각의 옵션이 뭘의미하는지는 위의 옵션 설명이 있습니다. 


CounterVectorizer는 문서 집합에서 단어 토큰을 생성하고 각 단어의 수를 세어 BOW 인코딩한 벡터를 만든다.


진행하는 순서는 아래와 같습니다. 


  1. 문서를 토큰 리스트로 변환한다.
  2. 각 문서에서 토큰의 출현 빈도를 센다.
  3. 각 문서를 BOW 인코딩 벡터로 변환한다.
cosine_sim = cosine_similarity(count_matrix, count_matrix)
이렇다고 하네요... 
코사인 유사도(― 類似度, 영어cosine similarity)는 내적공간의 두 벡터간 각도의 코사인값을 이용하여 측정된 벡터간의 유사한 정도를 의미한다.
평가 척도가 만들어 졌습니다. 



4. 영화 추천 받기 


smd = smd.reset_index()
titles = smd['title']
indces = pd.Series(smd.index, index=smd['title'])

인덱스를 영화이름으로 만들어줘서 위에서 만들었던 영화 타이틀을 입력하면 영화 리스트를 반환해주는 함수를 이용합니다. 


getrecommandations('The Dark Knight')

8031                 The Dark Knight Rises

6218                         Batman Begins

6623                          The Prestige

2085                             Following

7648                             Inception

4145                              Insomnia

3381                               Memento

8613                          Interstellar

7659            Batman: Under the Red Hood

1134                        Batman Returns

8927               Kidnapping Mr. Heineken

5943                              Thursday

1260                        Batman & Robin

9024    Batman v Superman: Dawn of Justice

4021                  The Long Good Friday

.....


이번에는 무조건 배트맨 보다는 크리스토퍼 놀란이 연출한 영화들이 몇몇이 상위권임을 알 수 있습니다. 


추천함수 


def getrecommandations(title):
index = indces[title]
sim_scores = list(enumerate(cosine_sim[index]))
sim_scores = sorted(sim_scores, key=lambda x:x[1], reverse=True)
sim_scores = sim_scores[1:31]
movie_indices = [i[0] for i in sim_scores] return titles.iloc[movie_indices] 


'kaggle' 카테고리의 다른 글

[kaggle] 영화 추천 시스템  (3) 2018.08.08