졸업 프로젝트에서 레트로 콘텐츠 추천을 통해 2030세대의 지친 삶을 위로해주는 추억 회상 힐링 앱 'Neverland'를 기획했다. 멘토링을 통해 우리가 하고자 하는 사용자 맞춤 콘텐츠 추천이 협업 필터링(Collaborative Filtering)으로 가능하다는 것을 알게 되었다.
예를 들어, 내가 A 영화를 재미있게 봤다면, A 영화를 본 다른 사람들이 재밌다고 한 B 영화를 나에게 추천해준다는 것이다. 즉, 다른 사람들과의 교집합을 바탕으로 다른 합집합을 보여준다는 것이다.
협업 필터링이 어떤 개념인지 알게 되었지만, 여전히 많은 부분에서 해당 알고리즘에 대한 공부가 필요하다고 느꼈다. 따라서 추천 알고리즘을 주제로 포스팅 해보려 한다.
목차
추천 시스템
- 추천 시스템 정의
- 유사도
- 유사도 종류
- 추천 시스템의 종류와 장단점
추천 알고리즘 구현
- 콘텐츠 기반 필터링 Content-based Filtering
- 사용자 기반 협업 필터링 User-based Collaborative Filtering
References
다들 한 번쯤은 OTT나 SNS에서 스스로 검색하진 않았지만, 내가 봤던 콘텐츠와 비슷한 류의 콘텐츠들이 피드에 자동으로 올라와 신기했던 경험이 있을 것이다. 이는 추천 시스템에 의한 것이다. 그렇다면 추천 시스템이란 무엇일까?
추천 시스템을 이해하기 위해 먼저 수학적 개념인 유사도에 대해 알아보자. 추천 시스템에서 유사도는 다음과 같은 방식으로 적용된다.
추천 시스템에서 주로 사용되는 유사도는 다음의 4가지로 분류할 수 있다.
(1) 유클리디안 유사도
(2) 코사인 유사도
(3) 피어슨 유사도
(4) 자카드 유사도
👍🏻 장점
👎🏻 단점
👍🏻 장점
👎🏻 단점
협업 필터링은 접근 방식에 따라 다시 기억 기반과 모델 기반으로 나뉜다.
기억 기반(Memory based)
사용자 기반 협업 필터링(User-based collaborative filtering)
: 나와 성향이 비슷한 사람들이 사용한 아이템을 나에게 추천해주는 방식
{: width="50"}
아이템 기반 협업 필터링(Item-based collaborative filtering)
: 내가 구매하려는 물품과 함께 많이 구매된 아이템을 나에게 추천해주는 방식
모델 기반(Model based)
: 머신러닝이나 데이터 마이닝 방법에서 예측 모델의 context를 기반한 방법이다. 모델이 파라미터화 되어 있다면, 이 모델의 파라미터는 context 내에서 학습된다.
정리하자면!
우리 팀이 기획한 앱은 영화, 드라마, 애니메이션, 음악 등 과거를 회상할 수 있는 다양한 콘텐츠들이 포함된다. 구글 코랩 환경에서 🎬 영화 데이터셋을 활용해 콘텐츠 기반 필터링, 사용자 기반 협업 필터링을 구현해보자.
영화의 genres
속성을 이용하여 콘텐츠 기반 필터링을 수행해보자.
IMDB의 주요 5000개 영화에 대한 메타 정보를 새롭게 가공해 Kaggle에서 제공한 TMDB movie 데이터를 이용한다. 데이터셋은 다음의 링크를 통해 다운받을 수 있다.
https://www.kaggle.com/datasets/tmdb/tmdb-movie-metadata
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import linear_kernel
movies_df = pd.read_csv('/tmdb_5000_movies.csv')
케이스 수: 4803
칼럼:
budget, genres, homepage, id, keywords, original_language, original_title, overview, popularity production_companies, production_countries, release_date, revenue, runtime, spoken_languages, status, tagline, title, vote_average, vote_count
→ 총 20개
# 중요한 칼럼만 선택
selected_columns = ['id', 'title', 'genres', 'keywords']
movies_df = movies_df[selected_columns]
# 결측치 처리
movies_df = movies_df.dropna()
genres
와 keywords
칼럼만 선택하여 데이터를 간결하게 유지하고 계산 비용을 줄인다.
genres
칼럼은 TF-IDF 변환을 위한 텍스트 데이터로 사용되므로, 결측치를 빈 문자열('')로 대체하여 문제 없이 변환이 가능하도록 한다.
# TF-IDF 변환을 위한 벡터화 객체 생성
tfidf_vectorizer = TfidfVectorizer(stop_words='english', lowercase=True)
# 'genres'와 'keywords' 칼럼을 합친 새로운 칼럼 생성
movies_df['content'] = movies_df['genres'] + ' ' + movies_df['keywords']
# TF-IDF 행렬 생성
tfidf_matrix = tfidf_vectorizer.fit_transform(movies_df['content'])
movies_df를 출력해보면 *런타임 연결 끊어져서 주피터 이용 *
두 개의 칼럼(genres
, keywords
)이 content
칼럼으로 합쳐진 것을 볼 수 있다.
각 영화를 나타내는 텍스트 데이터를 TF-IDF 행렬로 표현한다. 이때 TF-IDF 변환은 각 영화의 장르가 얼마나 빈번하게 나타나는지를 고려하여 해당 장르에 중요도를 부여하기 위함이다.
# 코사인 유사도 계산
cosine_sim = linear_kernel(tfidf_matrix, tfidf_matrix)
각 영화의 장르를 벡터 형태로 표현하면, 이 벡터들 간의 코사인 유사도를 계산하여 영화 간의 유사성을 판단할 수 있다. 각 영화의 특징을 잘 반영하고, 유사한 장르를 가진 영화들끼리 묶을 수 있게 된다.
# 콘텐츠 기반 필터링
def content_based_filtering(title, cosine_sim=cosine_sim):
# 제목에 해당하는 인덱스 찾기
idx = movies_df[movies_df['title'] == title].index[0]
# 해당 영화에 대한 유사도 측정
sim_scores = list(enumerate(cosine_sim[idx]))
# 유사도에 따라 정렬
sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True)
# 상위 10개 영화 선택
sim_scores = sim_scores[1:11]
# 선택된 영화의 인덱스
movie_indices = [i[0] for i in sim_scores]
# 선택된 영화의 제목으로 반환
return movies_df['title'].iloc[movie_indices]
측정된 유사도를 기준으로 내림차순 정렬하여 유사도가 높은 상위 10편의 영화를 선택한다. 이때 자기 자신(테스트하려는 영화)은 제외한다. 선택된 영화들의 인덱스를 활용하여 해당 영화들의 제목을 리턴하는 콘텐츠 기반 필터링 함수가 완성되었다.
# 예시
result = content_based_recommendation('Avatar')
print(result)
영화 '아바타'를 이용하여 알고리즘을 시험해보자.
'아바타'와 비슷한 10개의 영화가 잘 추천된 것을 확인할 수 있다.
n번 사용자에 대해 영화 추천을 하는 사용자 기반 협업 필터링을 수행하보자.
추천 시스템 연구 분야에서 공식적으로 성능 평가 등을 측정하는데 많이 사용되는 MovieLens 데이터를 이용한다. 데이터셋은 다음의 링크를 통해 다운받을 수 있다.
https://grouplens.org/datasets/movielens/
import pandas as pd
import numpy as np
movies_df = pd.read_csv('/content/movies.csv')
ratings_df = pd.read_csv('/content/ratings.csv')
movies.csv 케이스 수: 62424
ratings.csv 케이스 수: 25000096, user 수: 162541
ratings_dict = {}
for index, row in ratings_df.iterrows():
user_id = str(row['userId'])
movie_id = str(row['movieId'])
rating = row['rating']
if user_id not in ratings_dict:
ratings_dict[user_id] = {}
ratings_dict[user_id][movie_id] = rating
사용자별로 영화와 그에 대한 평점을 딕셔너리 형태로 저장하면 이와 같은 결과를 얻는다. 사용자 '1'에 대해 '296'번 영화의 평점은 5.0, '306'번 영화의 평점은 3.5이다.
딕셔너리 형태로 저장하면 사용자별로 아이템에 대한 평가 데이터를 쉽게 조회할 수 있다. 또한 sparse한 형태를 가지기 때문에, 딕셔너리를 사용하면 0이 아닌 값이 있는 부분만을 저장하므로 메모리를 효율적으로 사용할 수 있다.
from sklearn.metrics.pairwise import cosine_similarity
def user_based_collaborative_filtering(ratings_dict, target_user, k=5):
# 1. 특정 사용자와 다른 사용자 간의 유사도 계산
similarities = {}
target_user_ratings = np.array(list(ratings_dict[target_user].values())).reshape(1, -1)
for user in ratings_dict:
if user == target_user:
continue
other_user_ratings = np.array(list(ratings_dict[user].values())).reshape(1, -1)
similarity = cosine_similarity(target_user_ratings, other_user_ratings)[0, 0]
similarities[user] = similarity
# 유사도를 기준으로 내림차순 정렬
sorted_similarities = sorted(similarities.items(), key=lambda x: x[1], reverse=True)
# 상위 k명의 이웃 선택
top_k_neighbors = sorted_similarities[:k]
# 평가하지 않은 영화 추출
not_rated_movies = set(movies_df['movieId']) - set(ratings_dict[target_user].keys())
# 2. 추천 영화 평점 예측
movie_recommendations = {}
for movie_id in not_rated_movies:
weighted_sum = 0
similarity_sum = 0
for neighbor, similarity in top_k_neighbors:
if movie_id in ratings_dict[neighbor]:
weighted_sum += ratings_dict[neighbor][movie_id] * similarity
similarity_sum += similarity
predicted_rating = weighted_sum / similarity_sum
movie_recommendations[movie_id] = predicted_rating
# 예측 평점을 기준으로 상위 영화 선택
top_movies = sorted(movie_recommendations.items(), key=lambda x: x[1], reverse=True)[:10]
return top_movies
특정 사용자와 다른 사용자 간의 유사도 계산
scikit-learn의 cosine_similarity 함수를 사용하여 두 벡터(사용자 평점 벡터) 간의 코사인 유사도 계산한다.
추천 영화 평점 예측
유사도가 높은 상위 k명(미리 정해둔 k=5)의 이웃을 선택하여 해당 사용자가 평가하지 않은 영화에 대한 평점을 예측한다.
현재 이웃 사용자가 현재 예측하려는 영화에 대해 평점을 남겼는지 확인한다. 만약 해당 영화에 대한 평가가 있다면, 가중 평균과 유사도 합을 업데이트한다.
🚨 여기서 잠깐!
유사한 사용자들이 평가한 영화 중에 현재 예측하려는 영화를 아무도 평가하지 않은 경우에는 어떡하지?
이 경우에 similarity_sum
이 0이지만 predicted_rating
이 계산될 수 있다. 즉, ZeroDivisionError가 발생한다. 따라서 if similarity_sum > 0
조건을 추가하여, 적어도 한 명 이상의 유사한 사용자가 평가한 경우에만 가중 평균을 계산하여 예측 평점을 저장하게 한다.
다음은 수정된 코드를 포함한 사용자 기반 협업 필터링 전체 코드이다.
import pandas as pd
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
# 데이터 로드
movies_df = pd.read_csv('/content/movies.csv')
ratings_df = pd.read_csv('/content/ratings.csv')
# 사용자 x 아이템 평가 데이터 생성
ratings_dict = {}
for index, row in ratings_df.iterrows():
user_id = str(row['userId'])
movie_id = str(row['movieId'])
rating = row['rating']
if user_id not in ratings_dict:
ratings_dict[user_id] = {}
ratings_dict[user_id][movie_id] = rating
# 사용자 기반 협업 필터링 함수
def user_based_collaborative_filtering(ratings_dict, target_user, k=5):
# 특정 사용자와 다른 사용자 간의 유사도 계산
similarities = {}
target_user_ratings = np.array(list(ratings_dict[target_user].values())).reshape(1, -1)
for user in ratings_dict:
if user == target_user:
continue
other_user_ratings = np.array(list(ratings_dict[user].values())).reshape(1, -1)
similarity = cosine_similarity(target_user_ratings, other_user_ratings)[0, 0]
similarities[user] = similarity
# 유사도를 기준으로 내림차순 정렬
sorted_similarities = sorted(similarities.items(), key=lambda x: x[1], reverse=True)
# 상위 k명의 이웃 선택
top_k_neighbors = sorted_similarities[:k]
# 평가하지 않은 영화 추출
not_rated_movies = set(movies_df['movieId']) - set(ratings_dict[target_user].keys())
# 추천 영화 평점 예측
movie_recommendations = {}
for movie_id in not_rated_movies:
weighted_sum = 0
similarity_sum = 0
for neighbor, similarity in top_k_neighbors:
if movie_id in ratings_dict[neighbor]:
weighted_sum += ratings_dict[neighbor][movie_id] * similarity
similarity_sum += similarity
if similarity_sum > 0:
predicted_rating = weighted_sum / similarity_sum
movie_recommendations[movie_id] = predicted_rating
# 예측 평점을 기준으로 상위 영화 선택
top_movies = sorted(movie_recommendations.items(), key=lambda x: x[1], reverse=True)[:10]
return top_movies
# 예시
target_user_id = '1'
recommendations = user_based_collaborative_filtering(ratings_dict, target_user_id)
result = pd.DataFrame(recommendations, columns=['Movie ID', 'Predicted Rating'])
print(result)
1번 사용자에 대한 추천을 한다고 가정하면, 다음과 같은 결과를 얻을 수 있다. 0번 컬럼은 movieId를, 1번 칼럼은 추천된 영화에 대한 1번 사용자의 예상 평점이다.
이번 포스팅에서는 추천 시스템을 다뤘다. 특히 콘텐츠 기반 필터링과 사용자 기반 협업 필터링은 파이썬으로 구현 및 영화 데이터셋으로 실험해보았다.
🤔 What's next ?
우리 프로젝트에서는 Spring 상에서 추천 알고리즘을 구현할 예정이다. 오늘 연습했던 추천 알고리즘을 Spring 환경에서 프로젝트 방향에 맞춰 재구성해볼 것이다.
협업 필터링
https://deepdaiv.oopy.io/articles/1
사용자 기반 협업 필터링 구현
https://data-science-hi.tistory.com/73
콘텐츠 기반 필터링 구현
https://nicola-ml.tistory.com/67