# 24.11.28_영화추천 사이트 제작_협업 기반 필터링
영화추천 사이트 제작이 80% 정도 완료되어서 세부 내용들 및 css 정리를 진행하고 있는데, 주변 사람들의 추천을 받아서 협업 기반 필터링을 넣은 추천 시스템을 넣으면 어떨까라는 추천을 받았고 이에 해당 내용을 서치해 공부해보았습니다.
협업 필터링이란?
협업 필터링(Collaborative Filtering, CF)은 다른 사용자들의 행동이나 선호도를 기반으로 추천을 제공하는 기법으로. 이 방법은 사용자와 아이템 간의 상호작용 데이터를 분석하여 새로운 추천을 생성하게 됩니다. 예를 들어, 사용자가 찜한 영화 데이터를 기반으로, 비슷한 취향을 가진 사용자들의 선호도를 참고해 추천을 제공하는 방식입니다.
왜 협업 필터링을 사용하는가?
1. 데이터 기반 개인화
- 협업 필터링은 개별 사용자의 과거 데이터 없이도, 전체 사용자들의 행동을 기반으로 개인화된 추천을 제공합니다.
- 예: "나와 비슷한 취향의 사람들이 좋아한 영화라면, 나도 좋아할 가능성이 크다."
2. 도메인 지식 불필요
- 협업 필터링은 아이템(영화, 음악, 상품 등)에 대한 세부 정보(예: 장르, 감독, 배우 등)를 몰라도 작동합니다.
- 예: 사용자가 단순히 영화 ID만 입력하더라도 추천을 생성할 수 있음.
3. 유연성
- 영화, 음악, 쇼핑 등 사용자-아이템 간 상호작용이 존재하는 다양한 분야에서 적용 가능합니다.
4. 추천의 다양성
- 사용자가 직접 탐색하지 못했던 새로운 아이템을 발견하게 도와줍니다.
- 예: 같은 장르 외의 영화를 추천받을 수 있음.
협업 필터링 작동 방식
협업 필터링에는 크게 두 가지 접근 방식이 있습니다:
1. 사용자 기반 협업 필터링(User-Based Collaborative Filtering)
- 아이디어: 나와 비슷한 선호도를 가진 사용자가 좋아하는 아이템을 추천.
- 예: A와 B가 비슷한 영화를 좋아하면, B가 찜한 영화 중 A가 보지 않은 영화를 추천.
- 계산 방법:
- 사용자의 아이템 선호도를 행렬로 표현.
- 유사도 계산(Cosine Similarity, Pearson Correlation): 사용자 간의 선호도 유사도를 계산.
2. 아이템 기반 협업 필터링(Item-Based Collaborative Filtering)
- 아이디어: 비슷한 특징을 가진 아이템을 추천.
- 예: 특정 사용자가 영화 X를 좋아했다면, 영화 X와 비슷한 영화를 추천.
- 계산 방법:
- 아이템 간 유사도를 행렬로 표현.
- 아이템 간의 유사도를 기반으로 추천 리스트 생성.
기업들이 협업 필터링(Collaborative Filtering)을 사용하는 이유는 고객 데이터를 기반으로 효과적이고 개인화된 추천 시스템을 제공하여 비즈니스 성과를 극대화할 수 있기 때문입니다. 아래에 주요 이유를 정리해 보았습니다.
1. 고객 경험 향상
- 개인화된 추천:
- 고객의 선호도와 취향을 분석하여 사용자 맞춤형 콘텐츠나 상품을 추천할 수 있습니다.
- 예: 넷플릭스는 사용자가 좋아할 가능성이 높은 콘텐츠를 추천해 고객 만족도를 높입니다.
- 새로운 아이템 발견:
- 고객이 스스로 탐색하지 않았을 아이템을 추천하여, 서비스에 대한 흥미를 유지할 수 있습니다.
- 예: 스포티파이는 유사한 취향의 사용자가 좋아하는 곡을 추천함으로써 새로운 음악을 발견하도록 돕습니다.
2. 매출 증대
- 업셀링(Upselling)과 크로스셀링(Cross-Selling):
- 사용자가 구매한 상품과 유사한 상품 또는 관련 상품을 추천하여 추가 구매를 유도합니다.
- 예: 아마존은 "이 상품을 구매한 고객은 이 상품도 구매했습니다"를 통해 크로스셀링을 강화합니다.
- 재구매율 증가:
- 고객이 서비스나 플랫폼을 다시 이용하도록 유도하여 반복 구매를 늘립니다.
- 예: 쇼핑몰에서 고객이 관심 가질만한 상품을 추천해 재방문을 유도.
3. 데이터 활용 극대화
- 빅데이터 활용:
- 고객의 행동 데이터를 분석하여 가치 있는 정보를 얻고, 이를 추천에 반영할 수 있습니다.
- 기업은 고객 데이터를 기반으로 미래 트렌드를 예측하거나 상품 개발 전략을 세울 수 있습니다.
- 도메인 독립성:
- 협업 필터링은 상품(아이템)의 구체적인 세부 정보를 몰라도 작동합니다. 데이터만 있다면 어떤 도메인에서도 활용 가능.
4. 경쟁력 강화
- 고객 이탈 방지:
- 개인화된 추천은 고객에게 "내가 원하는 것을 잘 알고 있다"는 느낌을 주어 충성도를 강화합니다.
- 예: 넷플릭스는 개인화 추천 시스템을 통해 사용자가 플랫폼을 떠나지 않도록 유도합니다.
- 차별화된 서비스:
- 단순히 인기 아이템을 나열하는 대신, 사용자마다 다른 경험을 제공함으로써 차별화된 브랜드 이미지를 구축할 수 있습니다.
5. 확장성과 유연성
- 다양한 산업에서 활용 가능:
- 협업 필터링은 영화, 음악, 쇼핑, 교육 등 사용자-아이템 간의 상호작용이 있는 모든 분야에 적용 가능.
- 예:
- 음악 스트리밍: 스포티파이, 애플뮤직
- 전자상거래: 아마존, 이베이
- 교육 플랫폼: Coursera, Udemy
- 새로운 사용자와 아이템 처리:
- 기존 데이터를 바탕으로 새로 추가된 사용자나 아이템에 대해서도 유사도를 계산해 빠르게 추천 제공 가능.
6. 비용 절감
- 효율적 마케팅:
- 전통적인 마케팅 방식(광고 캠페인)보다 고객의 관심사에 맞춘 추천이 ROI(Return on Investment)를 높이는 데 효과적.
- 예: 고객의 행동 데이터를 기반으로 개인화 이메일을 보내거나, 특정 상품에 관심을 가진 고객에게만 프로모션 제공.
- 자동화된 고객 관리:
- 협업 필터링은 데이터와 알고리즘만으로 동작하므로, 인건비나 추가 리소스 없이도 고객에게 개인화된 서비스를 제공 가능.
7. 사용자 행동 분석
- 사용자 행동 패턴 이해:
- 협업 필터링 데이터를 통해 고객이 선호하는 제품, 서비스, 콘텐츠의 유형을 이해할 수 있습니다.
- 예: 넷플릭스는 사용자가 즐겨보는 장르나 테마를 분석해 콘텐츠 제작 및 구매 전략을 세웁니다.
- 장기적인 전략 수립:
- 기업은 협업 필터링 데이터를 통해 고객 세그먼트를 나누고, 특정 그룹의 니즈를 반영한 상품이나 서비스를 개발할 수 있습니다.
8. 성공적인 사례
- 넷플릭스:
- 협업 필터링 기반 추천 시스템은 사용자가 콘텐츠를 쉽게 찾도록 도와줌으로써 이탈률을 낮추고 구독률을 높이는 데 기여.
- 자체 데이터에 따르면 추천 시스템 덕분에 75% 이상의 콘텐츠 시청이 이루어짐.
- 아마존:
- 협업 필터링을 사용한 추천 시스템으로 평균 매출의 **35%**가 추천 아이템을 통해 발생.
- 스포티파이:
- 비슷한 취향의 사용자 데이터를 활용해 'Discover Weekly' 추천 플레이리스트 제공, 사용자 경험 개선.
- 유튜브:
개인화된 동영상 추천으로 전체 트래픽의 70% 이상이 추천 알고리즘에서 발생.
내가 구현한 필터링 방식
구현된 방식
- **사용자 기반 협업 필터링(User-Based Collaborative Filtering)**이 적용하였습니다.
- 단계:
- 사용자-아이템 행렬(User-Item Matrix)을 생성.
- 데이터프레임을 pivot_table로 변환하여 사용자별 찜 데이터를 이진 행렬로 구성.
- 영화가 찜되었으면 1, 그렇지 않으면 0으로 표시.
- 사용자 간 유사도 계산.
- Cosine Similarity를 사용해 유사도를 계산.
- 유사도가 높은 사용자일수록 선호가 비슷하다고 간주.
- 추천 점수 계산.
- 유사도가 높은 사용자의 데이터를 가중치로 사용해 현재 사용자가 보지 않은 아이템에 점수를 부여.
- 최종 점수가 높은 아이템을 추천.
- 사용자-아이템 행렬(User-Item Matrix)을 생성.
사용 이유
- 찜 데이터가 사용자-아이템 행렬로 변환되기 적합하며, 이를 기반으로 빠르게 유사도를 계산할 수 있음.
- Cosine Similarity는 데이터가 희소할 경우에도 유사도 계산에 유리.
아래는 구현한 코드입니다.
1. 데이터 준비
- 찜 데이터 가져오기:
- Jjim.objects를 사용하여 사용자의 찜 목록을 포함한 전체 데이터를 가져옴
- 이 데이터를 Pandas 데이터프레임으로 변환
data = Jjim.objects.values('user_id', 'movie_id') df = pd.DataFrame(data) |
사용자-아이템 행렬(User-Item Matrix) 생성:
- pivot_table을 사용해 사용자-영화 찜 여부를 나타내는 행렬을 생성
user_item_matrix = df.pivot_table(index='user_id', columns='movie_id', aggfunc='size', fill_value=0) |
2. 사용자 유사도 계산
- Cosine Similarity를 사용해 사용자 간의 유사도를 계산
similarity_matrix = cosine_similarity(user_item_matrix) similarity_df = pd.DataFrame(similarity_matrix, index=user_item_matrix.index, columns=user_item_matrix.index) |
해석:
- 사용자 1과 2의 유사도는 0.5
- 사용자가 스스로와의 유사도는 항상 1
. 추천 계산
- 유사 사용자 탐색:
- 현재 사용자의 ID(user_id)에 대해 다른 사용자와의 유사도를 정렬하여 가장 유사한 사용자부터 순서대로 나열함
similar_users = similarity_df[user_id].sort_values(ascending=False) |
예를 들어, 사용자가 1이라면 similar_users는 다음과 같을 수 있다.
사용자가 보지 않은 영화 찾기:
- 현재 사용자가 찜하지 않은 영화 목록(unseen_movies)을 추출
user_movies = user_item_matrix.loc[user_id] unseen_movies = user_item_matrix.columns[user_movies == 0] |
추천 점수 계산:
- 다른 사용자(특히 유사도가 높은 사용자)의 영화 선호도를 기반으로 사용자가 보지 않은 영화의 점수를 계산
movie_scores = user_item_matrix.loc[similar_users.index, unseen_movies].T.dot(similar_users) |
- user_item_matrix.loc[similar_users.index, unseen_movies]: 유사 사용자의 찜 데이터를 필터링.
- .T.dot(similar_users): 유사 사용자 가중치를 적용해 영화 점수를 계산.
최종 추천:
- 점수가 높은 영화부터 순서대로 추천
recommended_movie_ids = movie_scores.sort_values(ascending=False).head(20).index |
찜 목록에 이미 있는 영화는 제외(찜 목록에 있는 영화는 삭제해야 추천 기능으로서 올바르게 작동)
filtered_movie_ids = [movie_id for movie_id in recommended_movie_ids if movie_id not in user_jjims] |
요약
- 모든 사용자의 찜 데이터를 기반으로 사용자 간 유사도를 계산.
- 유사도가 높은 사용자의 찜 데이터를 참고하여, 현재 사용자가 보지 않은 영화를 추천.
- 사용자가 보지 않은 영화 중에서, 유사도가 높은 사용자들이 찜한 영화를 추천
장점
- 사용자가 많은 데이터를 기반으로 협업적 추천이 가능함
- 유사도를 활용해 개인화된 추천 결과를 제공한다.
한계
- Cold Start 문제: 새로운 사용자는 찜 데이터가 없으므로 추천이 불가능함
- Sparse Data: 사용자의 찜 데이터가 적거나 특정 영화만 찜한 경우, 추천 품질이 떨어질 수 있음.
- 개선책: 장르 기반 추천, 인기 영화 보충 등을 추가적으로 적용.
전체 코드
from django.shortcuts import render from django.views import View import requests import pandas as pd from sklearn.metrics.pairwise import cosine_similarity from jjim.models import Jjim from collections import Counter from django.contrib.auth.mixins import LoginRequiredMixin class MovieSearchView(LoginRequiredMixin, View): template_name = 'moodiecinema/search_results.html' api_key = '5f0eb3027f1b131897e4dcbe057e0931' def get(self, request): query = request.GET.get('q') page = request.GET.get('page', 1) selected_genres = request.GET.getlist('genre') actor_name = request.GET.get('actor') director_name = request.GET.get('director') genres = self.get_genre_list() movies = [] total_pages = 1 # "추천" 기능: 하드코딩된 영화 리스트 반환 if query == "추천": predefined_movie_ids = [299536, 597, 137113, 155, 300, 603, 24428] # TMDb 영화 ID 예시 movies = self.get_movie_details(predefined_movie_ids, request.user.pk) # 일반 검색 기능 elif query: response = requests.get( 'https://api.themoviedb.org/3/search/movie', params={ 'api_key': self.api_key, 'query': query, 'language': 'ko-KR', 'page': page } ) data = response.json() movies = data.get('results', []) total_pages = data.get('total_pages', 1) # 영화 검색 결과가 없으면 배우/감독으로 검색 if not movies: movies = self.search_by_person(query, page) # 필터 기반 검색 (장르, 배우, 감독) elif selected_genres or actor_name or director_name: movies, total_pages = self.search_by_filters(selected_genres, actor_name, director_name, page) # 캐싱 데이터로 중복 방지 및 로테이션 cached_movies = request.session.get('cached_movies', []) movies = self.rotate_recommendations(movies, cached_movies) request.session['cached_movies'] = [movie['id'] for movie in movies] return render(request, self.template_name, { 'movies': movies, 'query': query, 'genres': genres, 'selected_genres': selected_genres, 'actor_name': actor_name, 'director_name': director_name, 'current_page': int(page), 'total_pages': total_pages }) def get_genre_list(self): """TMDb API에서 장르 목록 가져오기""" response = requests.get( 'https://api.themoviedb.org/3/genre/movie/list', params={'api_key': self.api_key, 'language': 'ko-KR'} ) data = response.json() return data.get('genres', []) def search_by_person(self, query, page): """배우 또는 감독으로 영화 검색""" person_response = requests.get( 'https://api.themoviedb.org/3/search/person', params={ 'api_key': self.api_key, 'query': query, 'language': 'ko-KR', 'page': page } ) person_data = person_response.json() people = person_data.get('results', []) movies = [] if people: for person in people: known_for_movies = person.get('known_for', []) full_movie_credits = self.get_movie_credits(person['id']) for movie in known_for_movies + full_movie_credits: if movie not in movies: movies.append(movie) return movies def search_by_filters(self, selected_genres, actor_name, director_name, page): """장르, 배우, 감독 필터 기반 영화 검색""" actor_id = self.get_person_id(actor_name) if actor_name else None director_id = self.get_person_id(director_name) if director_name else None params = { 'api_key': self.api_key, 'with_genres': ','.join(selected_genres) if selected_genres else None, 'with_cast': actor_id if actor_id else None, 'with_crew': director_id if director_id else None, 'language': 'ko-KR', 'page': page } response = requests.get( 'https://api.themoviedb.org/3/discover/movie', params=params ) data = response.json() return data.get('results', []), data.get('total_pages', 1) def rotate_recommendations(self, movies, cached_movies): """중복 방지를 위한 캐싱 처리""" unique_movies = [movie for movie in movies if movie['id'] not in cached_movies] if not unique_movies: return movies[:10] # 캐싱된 영화만 있으면 일부 반환 return unique_movies[:10] def get_person_id(self, name): """배우 또는 감독 이름으로 ID 가져오기""" if not name: return None response = requests.get( 'https://api.themoviedb.org/3/search/person', params={ 'api_key': self.api_key, 'query': name, 'language': 'ko-KR' } ) data = response.json() person = data.get('results', []) if not person: return None return person[0]['id'] def get_movie_credits(self, person_id): """특정 배우나 감독의 영화 목록 가져오기""" response = requests.get( f'https://api.themoviedb.org/3/person/{person_id}/movie_credits', params={ 'api_key': self.api_key, 'language': 'ko-KR' } ) data = response.json() return data.get('cast', []) def get_recommendations(self, user_id): """찜 목록 기반 협업 필터링 추천""" user_jjims = list(Jjihttp://m.objects.filter(user_id=user_id).values_list('movie_id', flat=True)) if not user_jjims: return [] data = Jjihttp://m.objects.values('user_id', 'movie_id') df = pd.DataFrame(data) if df.empty: return [] user_item_matrix = df.pivot_table(index='user_id', columns='movie_id', aggfunc='size', fill_value=0) similarity_matrix = cosine_similarity(user_item_matrix) similarity_df = pd.DataFrame(similarity_matrix, index=user_item_matrix.index, columns=user_item_matrix.index) similar_users = similarity_df[user_id].sort_values(ascending=False) user_movies = user_item_matrix.loc[user_id] unseen_movies = user_item_matrix.columns[user_movies == 0] movie_scores = user_item_matrix.loc[similar_users.index, unseen_movies].T.dot(similar_users) genre_weights = pd.Series({movie_id: sum(1 for g in self.get_movie_genres(movie_id)) for movie_id in unseen_movies}) movie_scores += genre_weights recommended_movie_ids = movie_scores.sort_values(ascending=False).head(20).index return [movie_id for movie_id in recommended_movie_ids if movie_id not in user_jjims][:10] def get_movie_genres(self, movie_id): """특정 영화의 장르 ID 가져오기""" response = requests.get( f'https://api.themoviedb.org/3/movie/{movie_id}', params={'api_key': self.api_key, 'language': 'ko-KR'} ) if response.status_code == 200: movie_data = response.json() return [genre['id'] for genre in movie_data.get('genres', [])] return [] def get_movie_details(self, movie_ids, user_id): """TMDb API에서 영화 상세 정보 가져오기""" movie_details = [] all_genres = [] for movie_id in movie_ids: response = requests.get( f'https://api.themoviedb.org/3/movie/{movie_id}', params={'api_key': self.api_key, 'language': 'ko-KR'} ) if response.status_code == 200: movie_data = response.json() movie_details.append(movie_data) genres = movie_data.get('genres', []) all_genres.extend([genre['id'] for genre in genres]) if len(movie_details) < 10: genre_counter = Counter(all_genres) most_common_genres = [genre[0] for genre in genre_counter.most_common(3)] additional_movies = self.get_movies_by_genres(most_common_genres, 10 - len(movie_details), movie_ids) movie_details.extend(additional_movies) return movie_details def get_movies_by_genres(self, genre_ids, count, exclude_movie_ids): """특정 장르의 영화 가져오기""" genre_ids_str = ','.join(map(str, genre_ids)) response = requests.get( 'https://api.themoviedb.org/3/discover/movie', params={ 'api_key': self.api_key, 'language': 'ko-KR', 'with_genres': genre_ids_str, 'sort_by': 'popularity.desc', 'page': 1 } ) data = response.json() movies = data.get('results', []) return [movie for movie in movies if movie['id'] not in exclude_movie_ids][:count] |
- 기존 검색부분 포함된 코드