CB ( Content-Based filtering ) - 내가 재밌게 본 영화와 비슷한 영화 찾기
https://github.com/mkk4726/CB-movie
GitHub - mkk4726/CB-movie: Content-based filtering about movie
Content-based filtering about movie . Contribute to mkk4726/CB-movie development by creating an account on GitHub.
github.com
해당 깃헙에서 관련 코드들을 찾아볼 수 있습니다.
내용 기반 필터링 ( CB )는, 비슷한 아이템을 추천한다는 기본적인 아이디어를 가지고 추천을 합니다.
주로 텍스트 정보가 많은 제품 ( ex: 뉴스 , 책 ) 등을 분석하여 추천할 때 많이 이용되는 기술입니다.
이를 통해 유저가 좋게 평가한 제품과 비슷한 제품들을 추천해줄 수 있습니다.
이를 위한 과정을 다음과 같습니다.
1. 각 아이템 간의 유사도 계산
2. 추천 대상인 사용자가 선호하는 아이템 선정
3. 2번에서 선정한 아이템과 유사도가 높은 N개의 아이템 선정
4. 이를 사용자에게 추천
이를 통해 저는 제가 재밌게 본 영화와 비슷한 영화를 찾아보겠습니다.
1. 유사도 계산
정리: 유사도는 coinse similarity를 사용했고 TF-IDF를 이용해 벡터화 시켰습니다.
1.1 유사도란?
유사도란 두 개의 벡터가 얼마나 비슷한지를 의미합니다.
유사도를 계산하는데는 여러 지표가 사용될 수 있습니다.
그 중 코사인 유사도(cosine similarity)를 사용하려 합니다.

index | column1 | column2 | column3 | column4 |
1 | value1_1 | value1_2 | value1_3 | value1_4 |
2 | value2_1 | value2_2 | value2_3 | value2_4 |
첫 번째 인덱스의 값을 item1, 두 번째 인덱스의 값을 item2라고 하면
두 개의 row들은 하나의 벡터로 생각할 수 있고 좌표평면 상에 그릴 수 있습니다.
즉 그림1처럼 두 개의 벡터의 모습을 띄고, 이 두 개의 벡터 사이의 각을 이용해 유사도를 정의하는 것입니다.
$ \theta $가 0이 될 수록 두 벡터는 비슷하고, 180이 될수록 두 벡터는 서로 다르다는 것을 직관적으로 유추해볼 수 있습니다.
$cos(\theta)$ 의 값은 -1에서 1의 값을 가지게 되고 , $\theta$가 0일 때 1, 180일 때 -1의 값을 가집니다.
이를 통해 값이 클수록 비슷하고 작을수록 다르다라고 생각할 수 있습니다.
이를 수식으로 표현하면 다음과 같습니다.
$cosinse \ similarity = S_c(A, B) := cos(\theta) = \frac{AB}{||A||||B||}=\frac{\sum_{i=1}^nA_iB_i}{\sqrt{\sum_{i=1}^nA_i^2}\sqrt{\sum_{i=1}^nB_i^2}}$
1.2 벡터를 어떻게 정의할 것인가 , TF-IDF
유사도를 어떻게 계산할지 정했으면 벡터를 어떻게 정의할 것인지 정해야합니다.
즉, column을 어떻게 정할 것인지를 정해야합니다.
저는 tf-idf를 이용해 영화에 대한 리뷰 또는 개요와 같은 글들을 벡터화시킬 것입니다.
여기에는 한 가지 가정이 존재합니다.
비슷한 단어 분포를 가진 것들은 비슷하다.
즉 '요리'라는 단어가 많이 쓰인 글은 요리와 관련된 글일 것이고, '총'이 많이 쓰인 글은 전쟁과 관련된 글일 것이라고 가정하는 것입니다.
이 컨셉을 통해 글을 가장 단순하게 벡터화 시킬 수 있는 방법은, row별로 단어가 사용된 횟수만큼을 column에 표시하는 것입니다.
index | 투자 | 수익률 | 건강 | 회복 | 그리고 | 끝나다 | 좋다 |
1 | 10 | 15 | 0 | 8 | 20 | 8 | 7 |
2 | 11 | 0 | 12 | 14 | 21 | 5 | 8 |
예를 들면, 글에 사용된 단어의 횟수를 이용해 위와 같은 두 개의 벡터를 생성할 수 있습니다.
직관적으로 1번은 투자와 관련된 글임을, 2번은 건강과 관련된 글임을 유추해볼 수 있습니다.
여기에는 한 가지 문제가 있습니다. '그리고'와 같이 공통적으로 사용되는 단어들이 존재해, 두 개의 벡터가 비슷하다고 판단하도록 만든다는 것입니다.
이를 보완하기 위해 TF-IDF라는 개념이 생겨났습니다.
TF-IDF(Term Frequency - Inverse Documnet Frequency)는 이름에서 그 의미를 쉽게 유추해볼 수 있습니다.
글 전체에 많이 나오는 단어에는 패널티를 부여하겠다는 것입니다.

이를 식으로 표현하면 다음과 같습니다.
$TF(t,d) = \frac{document \ d에서\ term \ t의 빈도}{document \ d에서\ 전체 term \ 의 개수}$
$IDF(t,d) = log\frac{전체 \ document의 \ 개수}{term \ t를 가지고 있는 document \ 의 \ 개수}$
$TF-IDF(t, d, D) = TF(t, d) \times IDF(t, D)$
이번 글에서는 이를 이용해 영화에 관련한 글들을 벡터화 시켜 사용하겠습니다.
2. 사용한 데이터
IMDb에서 review data를 직접 긁어와 사용했습니다.
2023.07.02 - [DataScience/Crawling & Scraping] - Scraping IMDb review data
Scraping IMDb review data
https://github.com/mkk4726/CB-movie GitHub - mkk4726/CB-movie: Content-based filtering about movie Content-based filtering about movie . Contribute to mkk4726/CB-movie development by creating an account on GitHub. github.com 이번 글의 코드를 crawlin
mkk4726.tistory.com
위 글을 참고 하시면 됩니다.
수집한 결과는 다음과 같습니다.
3. 벡터화 시키기
관련코드는 algorithms의 vectorizer.ipynb 파일에서 찾아볼 수 있습니다.
이제 이걸 벡터화 시키겠습니다.
위에서 언급한 것처럼 TF-IDF를 이용했습니다.
from sklearn.feature_extraction.text import TfidfVectorizer
from nltk import word_tokenize
import re
# 정규식 이용해서 영어만 남기자
def tokenizer(text):
# Tokenize the text using NLTK's word_tokenizer
words = word_tokenize(text)
# Pattern to match non-English words
pattern = re.compile(r'\b[^\W\d_]+\b', re.IGNORECASE)
# Filter out non-English words
english_words = [word for word in words if re.match(pattern, word)]
return english_words
tfidf_vectorizer_kwargs = {
'ngram_range': (1, 3), # 단어 연결해서 생성
'stop_words': 'english', # 불용어 제거
'lowercase': 'True', # 다 소문자로 바꿈
'tokenizer': tokenizer,
# 너무 많이 나오는 단어, 코퍼스 특이적 불용어(corpus-specific stopwords) 제거
'max_df': 0.95, # 전체 문서 중 95%이상 나온 단어를 무시 ( 많이 나오는 단어 제거 )
'min_df': 0.01 # 전체 문서 중 1%이하로 나온 단어는 무시 ( 희소하게 나오는 단어 제거 )
}
tfidf_vectorizer = TfidfVectorizer(**tfidf_vectorizer_kwargs)
tfidf_matrix = tfidf_vectorizer.fit_transform(reviews['review'])
feature_names = tfidf_vectorizer.get_feature_names()
# Create a dataframe from the TF-IDF matrix
vectorized_df = pd.DataFrame(tfidf_matrix.toarray(), columns=feature_names, index=reviews.index)
vectorized_df.head()
정규식을 이용해 영단어만을 남겼고, 너무 많이 나오는 단어나 너무 적게 나오는 단어는 제거했습니다.
4. 유사도 계산하기 & CB 적용
관련코드는 algorithms의 basic_CB.ipynb 파일에서 찾아볼 수 있습니다.
먼저 벡터화 시킨 벡터는 희소행렬이기에 메모리 문제를 예방하기 위해 scipy.sparse의 csr_matrix 함수를 이용해 csr matrix로 바꿔줬습니다.
from scipy.sparse import csr_matrix
from sklearn.metrics.pairwise import cosine_similarity
# 데이터가 워낙 많고 희소행렬이기 때문에 csr로 바꿔서 유사도를 계산
csr_vec = csr_matrix(vectorized_df.values)
cosine_sim = cosine_similarity(csr_vec)
cosine_sim_df = pd.DataFrame(cosine_sim, index=vectorized_df.index, columns=vectorized_df.index)
cosine_sim_df
결과는 다음과 같습니다.

이제 이를 이용해 CB를 구현하면 됩니다
CB(Content-Based filtering)은 비슷한 아이템을 추천해주는 것을 뜻합니다.
유사도를 구했으니 비슷한 애들을 필터링해서 사용자에게 알려주기만 하면 끝납니다.
함수는 다음과 같습니다.
def get_CB_recomm(df1:"pd.DataFrame", df2:"pd.DataFrame", title_id:str):
"""CB 기반 추천 알고리즘
Args:
df1 (pd.DataFrame): title_basic_df
df2 (pd.DataFrame): cosine_df
title_id (str): title id
"""
print(f"about : {m.find_title(df1, title_id)}")
candidate_series = df2[title_id].sort_values(ascending=False)[1:10]
for title_id, sim in zip(candidate_series.index, candidate_series):
title = m.find_title(df1, title_id)
url = f'https://www.imdb.com/title/{title_id}/?ref_=fn_al_tt_1'
print(f"title: {title} / sim: {sim:.4f} / url: {url}")
df1은 title_id를 title로 바꿔주기 위해 필요한 것이며df2은 앞서 구한 유사도 matrix입니다.
결과를 살펴보면,
제가 정말 좋아하는 D.P을 입력했을 때 추천받은 것은 위의 사진과 같습니다.
대부분의 작품들이 청춘물에 속하고, 남자 2명이 주인공인 영화들이 존재해 나름 잘 골라졌다고 판단할 수 있을 것 같습니다.
가장 아쉬운 점은 결과를 주관적으로 판단할 수 밖에 없다는 점이다.
MF나 FM으로 가면 평점을 예측하는 회귀 문제로 접근할 수 있고 RMSE를 통해 결과를 확인해볼 수 있겠지만,
결국 어떤 것을 추천했을 때 이를 어떻게 평가할 것인지에 대한 의문은 계속 남아있을 것 같다.
이에 대해 관련 블로그나 논문들을 찾아보며 정리해봐야겠다.
출처 :
그림2: https://betterprogramming.pub/a-friendly-guide-to-nlp-tf-idf-with-python-example-5fcb26286a33
MovieLens 100K