www.kaggle.com/ibtesama/getting-started-with-a-movie-recommendation-system
필사해볼 커널입니다.
글의 목표
1. 콘텐츠 필터링이란?
2. 콘텐츠 필터링을 하기 위해서는?
3. 추천 아이템 데이터 구성해보기
4. 가장 많이 추천될 영화는 무엇일까?
1. 콘텐츠 필터링이란?
콘텐츠 필터링에 대해 설명하기전에 협업 필터링과 콘텐츠 필터링의 차이에 대해 먼저 설명해보겠습니다.
"초록색" 화살표에 집중해봅시다. 왼쪽 사진과 오른쪽 사진의 차이점이 무엇인가요?
협업 필터링은 추천을 하기전에 "유사한 다른 사용자의 정보"를 참조합니다. 반면, 콘텐츠 필터링은 추천을 하기전에 "사용 자신의 정보"를 참조합니다. 예시를 하나씩 들어보겠습니다.
예시1 - 아이템1을 구매한 C씨에게 헤어드라이기를 구매한 다른 유저들의 정보를 참조하여 아이템2를 추천
예시2 - 아이템3을 구매한 C의 정보를 참조하여 아이템3과 유사한 아이템4를 추천
둘 중에 무엇이 협업 필터링이고 무엇이 콘텐츠 필터링일까요? 예시1이 협업 필터링이고 예시2가 콘텐츠 필터링입니다. 다른 사용자들의 정보와 협업하여 추천하고 / 개인의 콘텐츠를 참조하여 추천한다. 이렇게 생각하시면 쉬울 것 같네요.
2. 콘텐츠 필터링을 하기 위해서는?
그렇다면, 콘텐츠 필터링을 하기 위해서는 무엇이 필요할까요? 앞서 예시2를 다시 살펴볼께요.
예시2 - 아이템3을 구매한 C의 정보를 참조하여 아이템3과 유사한 아이템4를 추천
두 가지가 보입니다. 하나는 "아이템3을 구매한 C의 정보" 이고 다른 하나는 아이템3과 유사한 아이템4입니다. 즉, 사용자의 기록과 아이템간의 유사성 두 가지 정보가 필요합니다. 그렇기 때문에 아이템 유사성을 계산합니다.
아이템의 유사성을 구하기 위해서는 무엇이 필요할까요? 아이템과 아이템의 유사성을 구할 수 있는 기준이 필요합니다.
3. 추천 아이템 데이터 구성해보기
3-1. 데이터 셋 살펴보기
import pandas as pd
import numpy as np
df =pd.read_csv('../input/tmdb-movie-metadata/tmdb_5000_movies.csv')
실제 커널에서는 두 가지 데이터 셋을 불러와 조인을 하는데요. 굳이 그럴 필요없이 tmdb_5000_movies.csv 하나만 사용하시면 됩니다.
df.columns
영화에 사용된 예산, 장르, 키워드, 원어, 개요, 원제목, 배급사, 수익, 상영시간, 제목, 평균평점, 투표수 등의 데이터가 보이네요.
df.columns
>> (4803, 20)
4800개 정도의 영화가 있네요.
df[['title', 'overview']].head()
여기서 title과 overview 두 가지 변수를 이용하여 영화간 유사성을 계산해 볼 것입니다. 즉, "개요"로 영화의 유사성을 측정한다고 생각하시면 됩니다.
본격적으로 들어가기에 앞서 문득 overview의 길이가 얼마나 될까? 궁금해졌습니다.
test_overview = df.copy()
test_overview.loc[:,'len'] = test_overview['overview'].map(lambda x : len(x) if pd.notnull(x) else 0)
위와 같이 개요의 길이를 계산해줍니다. 문장의 길이를 계산한 것입니다. nan값이 있는 경우 float으로 인식하기 때문에 len함수가 길이를 셀 수 없습니다. pd.notnull() 옵션을 반드시 사용해 줍시다.
test_overview.len.describe()
중앙값과 평균이 얼추 비슷한 것으로 보아 특별히 길이가 긴 개요는 많지 않습니다. 즉, 개요 길이는 대부분 비슷하다고 할 수 있겠네요.
유사성은 어떻게 계산할까요?
3-2. 코사인 유사도
자세한 설명은 해당 페이지 참조를 추천드립니다. 사용하는 이유에 대해서만 간단히 설명하겠습니다. 보통 코사인 유사도는 "크기"에 영향을 받지 않기 위해서 사용을 한다고 하는데요. "크기"에 영향을 많이 받는다는 얘기가 무엇일까요?
예를 들어서, A 문서에서 "칼퇴" 3번 B문서에서 "칼퇴" 3번이 나와서 A와 B문서가 유사하다는 판단을 했다고 가정해봅시다. 그런데, 알고 보니 A문서는 트위터의 몇 줄 안되는 글이고 B문서는 뉴스 기사 였던 것입니다. 즉, 수~많은 단어들 중 칼퇴가 끼어있었던 것이죠.
코사인 유사도는 이러한 크기의 영향력을 상쇄한다고 합니다. 하지만, 이 점이 때로는 단점으로 작용될 수도 있다고 하는데요. 아래 블로그의 설명을 한번 참조해보시길 바랍니다.
3-3. TF-IDF란?
너무 어렵게 생각하실 것 없습니다. TF-IDF는 단어를 숫자로 표현하기 위한 여~러 노력들 중 하나인데요. 근데, 왜 굳이 이런짓을 해야할까요?
towardsdatascience.com/why-do-we-use-embeddings-in-nlp-2f20e1b632d2
관련해서 재밌는 Article이 하나있어 추천드립니다.
고양이를 보고 우리는 흔히 귀엽다는 생각을 한다. 하지만, 컴퓨터한테 고양이를 보여줘도 똑같이 생각할까? 인간이 고양이를 귀엽다고 판단할 수 있는 이유는 진화에 의해 내려온 "선험적 지식" 때문이다. 하지만, 컴퓨터는 그런 배경이 없다.
-- 글의 내용중
컴퓨터는 인간과 다르다 정도로 생각하면 될 것 같네요. 그렇다면, 언어를 그대로 들려주거나 보여줄 수 없다면 어떤 방법을 선택해야할까요? "숫자" 입니다. 왜 숫자를 쓸까요? 우선, 숫자를 사용하면 정보를 요약할 수도 있습니다만, 가장 큰 특징은 상대적인 비교가 가능하다는 것인데요. 그렇기 때문에 언어를 숫자로 나타내고 이러한 작업을 워드 임베딩이라고 합니다. 글에 관련된 여러 내용이 있으니 더 궁금하신 분은 한번 읽어보시길 바랍니다.
자, 그러면 TF-IDF는 어떻게 언어를 숫자로 나타낼까요? 앞서 "숫자"로 나타내면 상대적인 비교가 가능하다고 했었는데요. 비교가 가능하다면 걔들 중 우위를 가지는 정보가 있겠죠? TF-IDF에서는 어떤 정보가 우위를 가지는지 이해하면 됩니다.
J는 문서고 I는 문서에 등장하는 단어인데요. 위공식은 J라는 문서에서 I라는 단어가 가지는 상대적인 가중치를 나타내는 과정입니다. 해당 가중치는 J라는 문서에서 I라는 단어가 등장하는 횟수에 X 전체 문서의 개수를 I라는 단어가 등장하는 문서의 개수로 나눠준 수에 로그를 씌워준 값을 곱해주는데요.
어떻게해야지 값이 커질까요? TF(i,j)가 커지고 DF(i)가 작아져야 값이 커집니다. 즉, J라는 문서에서 많이 등장하지만, 다른 문서들에서는 잘 등장하지 않는 단어에 J라는 문서는 가중치를 많이 준다는 뜻인데요. 왜 그럴까요?
생각해봅시다. I라는 놈이 여~러 문서에서 다 등장한다면? 이 녀석은 J라는 문서를 구분하는데는 큰 도움이 되지 않을 수도 있습니다. J에서만~ 그것도 많이~ 등장해야 J라는 문서를 구분하는데 도움이 되는 단어라고 할 수 있습니다!
3-4. TF-IDF 구하기
#sklearn을 이용해서 구해보기
from sklearn.feature_extraction.text import TfidfVectorizer
tfidf = TfidfVectorizer(stop_words='english')
tfidf_matrix = tfidf.fit_transform(df['overview'])
잠깐, 이런방식으로 코드를 작성하기 전에 먼저 확인하셔야 할 작업이 있습니다.
tfidf = TfidfVectorizer(stop_words='english')
tfidf_matrix = tfidf.fit_transform(df2['overview'])
tfidf_matrix.shape
>> (4800, 20967)
키로 사용할 단어에 중복이 없도록 하셔야 하는데요. 키에 사용하는 단어에 중복이 있으면 나중에 오류가 생길 수 있으니 주의하시길 바랍니다.
다시, fit_transform 함수를 이용해 tf_idf matrix를 구해주면 4800, 20967의 행렬이 나오게 되는데요. 4800개의 영화에 대한 개요에서 총 20967개의 단어가 나왔다는 뜻입니다. 그리고, 그 각각의 단어에 대해 아까 제가 말씀 드린데로 TF_IDF 수치가 매겨져 있습니다.
근데, 단어가 많긴 하죠? 단어의 수를 줄이거나, 혹은 과한 TF-IDF의 수치를 조정하는 방법도 있는데 그건 다음번에 다루어 보기로 하겠습니다.
3-5. 코사인 유사도 계산하기
from sklearn.metrics.pairwise import linear_kernel
cosine_sim = linear_kernel(tfidf_matrix, tfidf_matrix)
위와 같이 코사인 유사도를 계산해봅니다.
cosine_sim.shape
>> (4800, 4800)
행렬의 크기는 위와 같이 계산되는데요. 즉, 4800개의 영화에 각각에 대해서 코사인 유사도가 계산되어 있다는 뜻입니다.
print(cosine_sim[0])
print(cosine_sim[1])
왜 첫 번째 행에서는 1이 처음으로 나오고 왜 두 번째 행에서는 1이 두 번째로 나올까요? 바로 자기 자신이기 때문입니다. 코사인 유사도에서는 1이 가장 완벽한 유사도로 계산되는데요. 순서대로 첫 번째 영화, 두 번째 영화라고 생각하시면 됩니다.
indices = pd.Series(df2.index, index=df2['title'])
indices.head()
다음으로, 위와 같이 인덱스를 만들어주는데요. 왜 이렇게 인덱스를 구성했을가요?
def get_recommendations2(title, cosine_sim=cosine_sim):
idx = indices[title]
sim_scores = list(enumerate(cosine_sim[idx]))
sim_scores = sorted(sim_scores, key=lambda x : x[1], reverse =True)
sim_scores = sim_scores[1:6]
movie_indices = [i[0] for i in sim_scores]
score = [i[1] for i in sim_scores]
return df2['title'].iloc[movie_indices].values, score, [title]*5
아래 함수에서 그 이유를 찾을 수 있습니다. 영화 타이틀로 앞서 계산한 cosine_sim 행렬에 인덱스 정보를 줍니다. 그리고, (영화 타이틀 인덱스, 코사인 유사도 계수) 튜플로 이루어진 리스틀 생성합니다. 다음으로, 코사인 유사도를 기준으로 리스트 튜플을 정렬하는데요.
왜 이렇게 했을까요? 가장 수치가 높은! 코사인 유사도를 가진 인덱스 번호를 뽑기 위해서입니다. reverse 파라미터가 True로 된것 보이시죠? 그 다음 인덱스 정보만 뽑아서 원본 df에 전달하는 식으로 함수가 구성되어 있습니다.
이렇게되면 코사인 유사도가 가~장 높은 5개만 뽑을 수 있겠죠? 저는 여기에 다른 목저을 가지고 score와 기존 title을 score 개수만큼 받아오는 변수를 더 추가했습니다.
4. 가장 많이 추천된 영화는 무엇일까?
사실, df2['title'].iloc[movie_indices]의 값만 받아와도 코사인 유사도로 가~장 비슷한 영화 5개만 추려올 수 있습니다. 하지만, 저는 가장~ 많이 추천된 영화가 무엇일지 궁금하더라구요.
TitleVal = []
ScoreVal = []
OriginalVal = []
for title in df2.title.values:
TitleVal.extend(get_recommendations2(title)[0])
ScoreVal.extend(get_recommendations2(title)[1])
OriginalVal.extend(get_recommendations2(title)[2])
df_recommend_movie = pd.DataFrame(OriginalVal, columns=['OriginalVal'])
df_recommend_movie['TitleVal'] = TitleVal
df_recommend_movie['ScoreVal'] = ScoreVal
df_recommend_movie.head()
그래서, 위와 같이 데이터 셋을 구성해봤습니다. 원본 영화와 추천된 영화5개 그리고 추천된 영화와 원본 영화간의 코사인 유사도 값이 같이 들어 있습니다.
cond = (df_recommend_movie.ScoreVal >= 0.5)
df_recommend_movie.loc[cond]
코사인 유사도가 0.5가 넘는 경우가 4개(실질적으로는 2개 서로가 서로를 추천) 밖에 없네요.
df_recommend_count = df_recommend_movie.TitleVal.value_counts().reset_index()
df_recommend_count.nlargest(10, 'TitleVal')
가장 많이 추천된 영화 10개를 추려봤습니다. 1위가 Little Boy, 2위가 Election이네요.
print(df2.loc[(df2.title == 'Little Boy')].overview.values)
print(df2.loc[(df2.title == 'Election')].overview.values)
단순히, 개요 두 문장만 비교해 봤을 때는 어떤 특징이 있는지 정확히는 모르겠네요. 하지만, "유사성의 기준" 관점에서 생각했을 때 가장 많이 추천됐다는 것은 아마 두 영화가 다른 영화들에서도 사용할만한 일반적인 개요의 특징을 가지고 있다고 볼 수도 있겠네요.
--------------------------------------------------
이상으로 글을 마치겠습니다.
제 블로그에 와주셔서 감사합니다! 다들 오늘 하루도 좋은 일 있으시길~~
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!