TOP
class="layout-aside-left paging-number">
본문 바로가기
[파이썬 Projects]/<파이썬 머신러닝>

[머신러닝] 추천 시스템: 콘텐츠 기반 필터링

by 기록자_Recordian 2024. 11. 1.
728x90
반응형
추천 시스템이란?
 

[머신러닝] 추천시스템

이전 내용 [머신러닝] 텍스트 분석이전 내용 [머신러닝] 군집화 (Clustering)군집화(Clustering) [군집]군집은 비슷한 샘플을 클러스터 또는 비슷한 샘플의 그룹으로 할당하는 작업으로, 데이터 분석,

puppy-foot-it.tistory.com


콘텐츠 기반 필터링 추천 시스템

 

◆ 콘텐츠 기반 필터링:

사용자가 특정한 아이템을 매우 선호하는 경우, 그 아이템과 비슷한 콘텐츠를 가진 다른 아이템을 추천하는 방식이다.

콘텐츠 기반 필터링 추천 시스템은 사용자가 높게 평가한 콘텐츠를 감안해 이와 적절하게 매칭되는 콘텐츠를 추천해 준다.

 

출처: https://brunch.co.kr/@cysstory/159


콘텐츠 기반 필터링 실습 - TMDB 5000 영화 데이터 세트

 

TMDB 5000 영화 데이터 세트는 유명한 영화 데이터 정보 사이트인 IMDB의 많은 영화 중 주요 5000개 영화에 대한 메타 정보를 새롭게 가공해 캐글(Kaggle)에서 제공하는 데이터 세트이다.

 

TMDB 5000 Movie Dataset

Metadata on ~5,000 movies from TMDb

www.kaggle.com

상단 링크에 접속하여 'tmdb_5000_movies.csv' 파일을 다운로드 하면 된다. (로그인 필수)

 

콘텐츠 기반 필터링 추천 시스템을 영화를 선택하는 데 중요한 요소인 영화 장르 속성을 기반으로 만들어 본다. 장르 칼럼 값의 유사도를 비교한 뒤 그중 높은 평점을 가지는 영화를 추천하는 방식이다.


데이터 로딩 및 가공

 

다운받은 파일을 DataFrame으로 로딩하고 개력적으로 데이터를 확인해 본다.

import pandas as pd
import numpy as np
import warnings; warnings.filterwarnings('ignore')

movies = pd.read_csv("C:/Users/niceq/Documents/DataScience/Python ML Guide/Data/09. tmdb_5000_movies.csv")
print(movies.shape)
movies.head()

tmdb_5000_movies.csv 는 4803개의 레코드와 20개의 피처로 구성돼 있다.

 

[콘텐츠 기반 필터링 추천 분석에 사용할 주요 칼럼만 추출]

추출할 주요 칼럼은

  • id
  • 영화제목 title
  • 영화가 속한 여러 가지 장르 genres
  • 평균 평점 vote_average
  • 평점 투표수 vote_count
  • 영화 인기 popularity
  • 영화를 설명하는 주요 키워드 문구 keywords
  • 영화에 대한 개요 설명 overview

해당 주요 칼럼만 추출해 새롭게 DataFrame으로 만든다.

movies_df = movies[['id', 'title', 'genres', 'vote_average', 'vote_count', 'popularity',
                    'keywords', 'overview']]

 

단, 'genres', 'keywords' 등과 같은 칼럼을 보면 파이썬 리스트(list) 내부에 여러 개의 딕셔너리(dict)가 있는 형태의 문자열로 표기돼 있는데, 이는 한꺼번에 여러 개의 값을 표현하기 위한 표현 방식이다.

그러나 이 칼럼이 DataFrame으로 만들어질 때는 단순히 문자열 형태로 로딩되므로 이 칼럼을 가공하지 않고는 필요한 정보를 추출할 수가 없다.

pd.set_option('max_colwidth', 100)
movies_df[['genres', 'keywords']][:1]

 

파이썬 ast 모듈의 literal_eval() 함수를 이용하여 genres 칼럼의 문자열을 분해해서 개별 장르를 파이썬 리스트 객체로 추출한다. 이 함수를 이용하면 이 문자열을 문자열이 의미하는 list[ dict1, dict2 ] 객체로 만들 수 있다. Series 객체의 apply()에 literal_eval() 함수를 적용해 문자열을 객체로 변환한다.

(keywords 칼럼도 동일한 작업 진행)

from ast import literal_eval
movies_df['genres'] = movies_df['genres'].apply(literal_eval)
movies_df['keywords'] = movies_df['keywords'].apply(literal_eval)

▶ 이제 genres 칼럼은 문자열이 아니라 실제 리스트 내부에 여러 장르 딕셔너리로 구성된 객체를 가진다.

 

genres 칼럼에서 장르명 (예. ['Action', 'Adventure'] 와 같은 장르명만 리스트 객체로 추출한다.

genres 칼럼에서 'name' 키에 해당하는 값을 추출하기 위해 apply lambda 식을 이용한다.

movies_df['genres'] = movies_df['genres'].apply(lambda x : [y['name'] for y in x])
movies_df['keywords'] = movies_df['keywords'].apply(lambda x : [y['name'] for y in x])
movies_df[['genres', 'keywords']][:1]

▶ apply(lambda x : y['name'] for y in x])와 같이 변환하면 리스트 내 여러 개의 딕셔너리의 'name' 키에 해당하는 값을 찾아 이를 리스트 객체로 변환한다.


장르 콘텐츠 유사도 측정

 

genres 칼럼은 여러 개의 개별 장르가 리스트로 구성돼 있기 때문에, genres를 문자열로 변경한 뒤 이를 CountVectorizer로 피처 벡터화한 행렬 데이터 값을 코사인 유사도로 비교한다.

 

[genres 칼럼을 기반으로 하는 콘텐츠 기반 필터링 프로세스]

  • 문자열로 변환된 genres 칼럼을 Count 기반으로 피처 벡터화 변환
  • genres 문자열을 피처 벡터화 행렬로 변환한 데이터 세트를 코사인 유사도를 통해 비교. (데이터 세트의 레코드별로 타 레코드와 장르에서 코사인 유사도 값을 가지는 객체 생성)
  • 장르 유사도가 높은 영화 중에 평점이 높은 순으로 영화 추천

[문자열로 변환된 genres 칼럼을 Count 기반으로 피처 벡터화 변환]

리스트 객체 값으로 구성된 genres 칼럼을 apply(lambda x : (' ').join(x))를 적용해 개별 요소를 공백 문자로 구분하는 문자열로 변환해 별도의 칼럼인 'genres_literal' 칼럼으로 저장한다.

※ 리스트 객체 내의 개별 값을 연속된 문자열로 변환하려면 일반적으로 ('구분 문자').join(리스트 객체)를 사용하면 된다.

from sklearn.feature_extraction.text import CountVectorizer

# CountVectorizer 적용위해 공백 문자로 word 단위가 구분되는 문자열로 변환
movies_df['genres_literal'] = movies_df['genres'].apply(lambda x : (' ').join(x))
count_vect = CountVectorizer(min_df=0.0, ngram_range=(1,2))
genre_mat = count_vect.fit_transform(movies_df['genres_literal'])
print(genre_mat.shape)

▶ CountVectorizer로 변환해 4803개의 레코드와 276개의 개별 단어 피처로 구성된 피처 벡터 행렬이 생성되었다.

 

[cosine_similarity() 함수를 이용해 코사인 유사도 계산]

from sklearn.metrics.pairwise import cosine_similarity

genre_sim = cosine_similarity(genre_mat, genre_mat)
print(genre_sim.shape)
print(genre_sim[:2])

▶ cosine_similarity() 호출로 생성된 genre_sim 객체는 movies_df의 genre_literal 칼럼을 피처 벡터화한 행렬(genre_mat) 데이터의 행(레코드)별 유사도 정보를 가지고 있다. (movies_df DataFrame의 행별 장르 유사도 값)

 

movies_df 를 장르 기준으로 콘텐츠 기반 필터링을 수행하려면 movies_df의 개별 레코드에 대해서 가장 장르 유사도가 높은 순으로 다른 레코드를 추출해야 하는데, 이를 위해 앞서 생성한 genre_sim 객체를 이용한다.

 

genre_sim 객체의 기준 행별로 비교 대상이 되는 행의 유사도 값이 높은 순으로 정렬된 행렬의 위치 인덱스 값을 추출하기 위해 numpy의 argsort() 함수를 이용한다.

argsort()[:, ::-1]을 이용하면 유사도가 높은 순으로 정리된 genre_sim 객체의 비교 행 위치 인덱스 값을 편리하게 얻을 수 있다.

genre_sim_sorted_ind = genre_sim.argsort()[:, ::-1]
print(genre_sim_sorted_ind[:1])

▶ 반횐된 [[   0 3494  813 ... 3038 3037 2401]]이 의미하는 것은 0번 레코드의 경우 자신이 0번 레코드를 제외하면 3494번 레코드가 가장 유사도가 높고, 그 다음이 813번.. 가장 유사도가 낮은 레코드가 2401이라는 의미이다.


 장르 콘텐츠 필터링을 이용한 영화 추천

 

장르 유사도에 따라 영화를 추천하는 함수 find_sim_moive() 를 생성한다.

인자로 기반 데이터인 movies_df DataFrame, 레코드별 장르 코사인 유사도 인덱스를 가지고 있는 genre_sim_sorted_ind, 고객이 선정한 추천 기준이 되는 영화 제목, 추천할 영화 건수를 입력하면 추천 영화 정보를 가지는 DataFrame을 반환한다.

def find_sim_movie(df, sorted_ind, title_name, top_n=10):
    # 인자로 입력된 mvoide_df DataFrame에서 'title' 칼럼이 입력된 title_name 값인 DataFrame 추출
    title_movie = df[df['title'] == title_name]

    # title_name을 가진 DataFrame의 index 객체를 ndarray로 반환하고
    # sorted_ind 인자로 입력된 genre_sim_sorted_ind 객체에서 유사도 순으로 top_n 개의 index 추출
    title_index = title_movie.index.values
    similar_indexes = sorted_ind[title_index, :(top_n)]

    # 추출된 top_n index 출력. top_n_index는 2차원 데이터임.
    # DataFrame에서 index로 사용하기 위해서는 1차원 array로 변경
    print(similar_indexes)
    similar_indexes = similar_indexes.reshape(-1)

    return df.iloc[similar_indexes]

 

앞서 생성한 함수를 이용해 영화 '대부(The Godfather)'와 유사한 영화 10개를 추천해 본다.

similar_movies = find_sim_movie(movies_df, genre_sim_sorted_ind, 'The Godfather', 10)
similar_movies[['title', 'vote_average']]

▶ 대부 2편(The Godfather: Part II)가 가장 먼저 추천됐다.

그러나 평점이 낮거나, 생소한 영화가 추천되기도 하여 ('Light Sleeper', 'Mi America' 등) 개선이 필요해 보인다.

 

[영화 평점에 따라 필터링하는 방식으로 변경]

좀 더 많은 후보군을 선정한 뒤에 영화의 평점('vote_average')에 따라 필터링해서 최종 추천하는 방식으로 변경해 본다.

단, 평점을 적용할 때에 1명, 2명의 소수의 관객이 특정 영화에 매우 높은 평점(또는 만점)을 부여해 왜곡된 데이터를 가지고 있을 수 있다.

우선 이를 확인해 본다.

movies_df[['title', 'vote_average', 'vote_count']].sort_values('vote_average', ascending=False)[:10]

▶ sort_values를 이용해 평점 오름차순으로 정렬해서 10개만 출력해보니, 평가 횟수가 적은 영화들이 대부분이다.

 

[가중치가 부여된 평점 방식을 이용한 평점 부여]

따라서 이와 같은 왜곡된 평점 데이터를 회피할 수 있도록 평가 횟수에 대한 가중치가 부여된 평점(Weighted Rating) 방식을 이용해 새롭게 평점을 부여해 본다.

가중 평점(Weighted Rating) = (v/(v+m) * R + (m/(v+m)) * C
  • v: 개별 영화에 평점을 투표한 횟수 (vote_count)
  • R: 개별 영화에 대한 평균 평점 (vote_average)
  • m: 평점을 부여하기 위한 최소 투표 횟수 (투표 횟수에 따른 가중치를 직접 조절하는 역할)
  • C: 전체 영화에 대한 평균 평점

▶ m 값을 높이면 평점 투표 횟수가 많은 영화에 더 많은 가중 평점을 부여하는데, m 값은 전체 투표 횟수에서 상위 60%에 해당하는 횟수를 기준으로 정하며, Series 객체의 quantile()을 이용해 추출한다.

C = movies_df['vote_average'].mean()
m = movies_df['vote_count'].quantile(0.6)
print('C:', round(C, 3), 'm:', round(m, 3))

 

기존 평점을 새로운 가중 평점으로 변경하는 함수 weighted_vote_average()를 생성하고 이를 이용해 새로운 평점 정보인 'vote_weighted' 값을 만든다.

 weighted_vote_average() 함수는 DataFrame의 레코드를 인자로 받아 이 레코드의 vote_count와 vote_average 칼럼, 그리고 미리 추출된 m과 C 값을 적용해 레코드별 가중 평균을 반환한다.

해당 함수를 movide_df의 apply() 함수의 인자로 입력해 가중 평균을 계산한다.

percentile = 0.6
m = movies_df['vote_count'].quantile(percentile)
C = movies_df['vote_average'].mean()

def weighted_vote_average(record):
    v = record['vote_count']
    R = record['vote_average']

    return ( (v/(v+m)) * R ) + ( (m/(v+m)) * C)

movies_df['weighted_vote'] = movies.apply(weighted_vote_average, axis=1)

 

새롭게 부여된 weighted_vote를 기준으로 평점이 높은 순으로 상위 10개의 영화를 추출해 본다.

movies_df[['title', 'vote_average', 'weighted_vote', 'vote_count']].sort_values('weighted_vote', ascending=False)[:10]

▶ 대부분 재미로도, 작품성으로도 유명한 영화(애니메이션) 이다. 참고로, 'Sprinted Away'는 센과 치히로의 행방불명 영어 제목이다.

 

[새롭게 정의된 평점 기준에 따른 영화 추천]

장르 유사성이 높은 영화를 top_n의 2배수만큼 후보군으로 선정한 뒤에 weighted_vote 칼럼 값이 높은 순으로 top_n 만큼 추출하는 방식으로 find_sim_moive() 함수를 변경한다. 그리고 다시 '대부'와 유사한 영화를 콘텐츠 기반 필터링 방식으로 추천해 본다.

def find_sim_movie(df, sorted_ind, title_name, top_n=10):
    title_movie = df[df['title'] == title_name]
    title_index = title_movie.index.values

    # top_n의 2배에 해당하는 장르 유사성이 높은 인덱스 추출
    similar_indexes = sorted_ind[title_index, :(top_n*2)]
    similar_indexes = similar_indexes.reshape(-1)

    # 기준 영화 인덱스는 제외
    similar_indexes = similar_indexes[similar_indexes != title_index]

    # top_n의 2배에 해당하는 후보군에서 weighted_vote가 높은 순으로 top_n 만큼 추출
    return df.iloc[similar_indexes].sort_values('weighted_vote', ascending=False)[:top_n]

similar_movies = find_sim_movie(movies_df, genre_sim_sorted_ind, 'The Godfather', 10)
similar_movies[['title', 'vote_average', 'weighted_vote']]

▶ 이전에 추천된 영화보다 훨씬 나은 영화가 추천되었다.

장르만으로 영화가 전달하는 많은 요소와 분위기, 그리고 개인이 좋아하는 성향을 반영하기에는 부족할 수 있고, 좋아하는 배우나 영화감독을 보고 영화를 선택하는 경우도 많을 것이기 때문에 장르를 기반으로 한 콘텐츠 필터링 예제를 좀 더 다양한 콘텐츠 기반으로 확장할 수 있다.


다음 내용

 

[머신러닝] 추천 시스템: 협업 필터링

추천 시스템이란? [머신러닝] 추천시스템이전 내용 [머신러닝] 텍스트 분석이전 내용 [머신러닝] 군집화 (Clustering)군집화(Clustering) [군집]군집은 비슷한 샘플을 클러스터 또는 비슷한 샘플의

puppy-foot-it.tistory.com


[출처]

파이썬 머신러닝 완벽 가이드

https://brunch.co.kr/@cysstory/159

728x90
반응형