[Basic] 추천 시스템 - 영화 추천

고보·2024년 3월 14일

1 장르나 키워드 유사한 영화들 추천

import pandas as pd
movies = pd.read_csv('../data/tmdb_5000_movies.csv')

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

pd.set_option('max_colwidth',100)
  • 데이터 불러오고 => 필요한 열만 추출
  • pd.set_option: DataFrame의 출력 옵션 제어.
    • max_colwidth표시되는 텍스트 열의 최대 너비를 지정. 그 이상은 줄임표로 표시.
    • max_rows: 표시되는 최대 행
    • max_columns: 표시되는 최대 열
    • precision: 실수 표시할 때 소수점 이하 자릿수 지정
### 텍스트 문자 1차 가공. 파이썬 딕셔너리 변환 후 리스트 형태로 변환
from ast import literal_eval
movies_df['genres'] = movies_df['genres'].apply(literal_eval)
movies_df['keywords'] = movies_df['keywords'].apply(literal_eval)
  • literal_eval: 문자열을 파이썬 객체로 안전하게 변환해주는 함수.
    예를 들면 "[1, 2, 3, 4, 5]"가 있으면 => 리스트 [1, 2, 3, 4, 5]로 바꿔준다.
    eval은 보안상 위험하지만, literal_eval은 안전하게 파이썬 리터럴만 평가.
  • 이를 통해 장르와 키워드를 문자열에서 리스트로 바꾼다.
  • 위와 같은 데이터라서, 안에 있는 장르와 키워드만 추출한다.
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]

from sklearn.feature_extraction.text import CountVectorizer
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'])
  • 먼저 genre에 있는 키워드들을 하나로 모두 join한다.
  • CountVectorizer: 텍스트 데이터를 단어의 빈도수 기반으로 벡터 형태로 변환해주는 객체. => 이렇게 벡터화해서 머신 러닝 알고리즘에 입력할 수 있게 된다.
    • stop_words: 불용어로 지정
    • tokenizer: 토큰화 수행하는 함수 지정
    • ngram_range: 기본값은 (1, 1), 단어의 묶음을 나타내는 n그램 범위 지정. The apple is yummy이면 (1, 1)이면 The, apple, is, yummy 로 나누고 (1, 2)면 The, apple, is, yummy, The apple, apple is, is yummy로 나눈다. 일반적으로(최소, 최대)의 형태로 지정한다. => 작은 ngram 범위는 더 많은 특성을 생성하지만, 훈련 데이터 부족할 떈 과적합 유발.
    • min_df: 단어의 최소 빈도 지정. 0으로 지정되면 모든 단어 고려, 2이면 적어도 2개 이상의 문서에서 나온 단어.
    • max_df: 단어의 최대 문서 빈도 지정. 0.5면 단어가 문서 전체 집합에서 50% 이상이면 무시.
  • .fit_transform(): fit과 transform을 한 번에 하는 메서드. => 위의 CountVectorizer 객체에 위에 만든 genres_literal을 넣어서 모두 벡터화한 모델을 => genre_mat으로 만든다.
from sklearn.metrics.pairwise import cosine_similarity
genre_sim = cosine_similarity(genre_mat, genre_mat)
genre_sim.argsort()
genre_sim_sorted_ind = genre_sim.argsort()[:, ::-1]
  • cosine_similarity(X, Y=None, dense_output): 두 개의 배열 X와 Y의 입력을 받아서, 각 배열의 벡터 간 코사인 유사도 계산. Y가 제공 안되면 X와 X의 유사도 계산. => 여기서은 genre_mat은 위에서 CountVectorizer로 장르 특성을 벡터화한 결과. 각 행은 하나의 영화, 각 열은 단어(장르)를 나타내는 희소 행렬(sparse matrix)다. 각 행렬 요소의 값은 해당 영화에 특정 단어(장르)를 나타내는 횟수다.
    따라서, genre_sim은 영화 간의 장르 유사도를 나타내는 행렬이고, 이 행렬은 각 요소의 두 영화 간의 장르 유사성을 나타낸다.
  • genre_sim.argsort(): 코사인 유사도 행렬에 대해, 각 영화에 대해 코사인 유사도가 가장 낮은 것부터 가장 높은 것까지의 인덱스를 반환한다.
    즉, genre_sim.argsort()[0][0]은, 첫 번째 영화에서, 가장 유사도가 낮은 영화의 인덱스가 나온다.
  • genre_sim.argsort()[:, ::-1] => 유사도가 가장 높은 순부터 정렬한다.
def find_sim_movies(df, sorted_ind, title_name, top_n = 10):
    title_movie = df[df['title'] == title_name]
    title_index = title_movie.index.values
    similar_indexes = sorted_ind[title_index, :(top_n)]
    print(similar_indexes)
    similar_indexes = similar_indexes.reshape(-1)
    return df.iloc[similar_indexes]
  • 비슷한 영화를 출력하는 함수.
  • 먼저 title_movie에 지정한 영화의 행을 꺼낸다. => title_index에 그 행의 인덱스도 꺼낸다
  • similar_indexes에 sorted_ind는 기존에 genre_sim.argsort()[:, ::-1]로 해놓은 것을 넣을 예정. 거기서 원하는 영화와, top 몇인지 => top 몇으로 가까운 영화들 인덱스가 나온다.
  • 2차원 배열을 => 1차원 배열로 바꿔서 => DF에서 그 행들만 추출

1-1 전체 흐름 정리

  • 영화들의 장르와 키워드만 추출해서 하나의 행으로(리스트로 꺼낸 후, join)
  • CountVectorizer 객체 생성 후 거기에 fit하고 transform => 텍스트 데이터를 단어의 빈도수 기반으로 벡터 형태로 변환
  • cosine_similarity를 이용해, 각 벡터의 유사도 배열을 만들고 => argsort()[:, ::-1]로 가장 가까운 것들의 인덱스를 추출
  • 그 인덱스를 이용해서 가장 가까운 영화 추출하는 함수 만들기

2 영화 평가한 사람 적을수록 높은 점수 몰리는 문제 해결

movies_df.sort_values('vote_average',ascending=False)
  • 평점 높은 영화들 나열하면, 위에는 다 평점 매긴 사람이 1명 처럼 엄청 적다.
C = movies_df['vote_average'].mean()
m = movies_df['vote_count'].quantile(0.6) # 상위 60% - 370회 정도의 투표
print('C:', round(C, 3), 'm:', round(m,3))

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/(m+v)) * C) # 가중 평점

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

movies_df[['title', 'vote_average', 'weighted_vote', 'vote_count']]\
.sort_values('weighted_vote', ascending=False)[:10]
  • 위는, 평균 평점과, 상위 40%가 몇 번 평점 받았는지. 시리즈.quantile: 0.6이면 하위 60%에서 40%.
  • 가중평정식에서 v는 평가수, R은 평균 평점, m은 평가수 상위 40%가 몇 개인지(여기선 370개), C는 평균 평점.
    그래서 평가수가 많으면 점수를 높게 주고 적으면 낮게 주고, 평점이 높으면 더 높게 주는 식으로 가중 평점.
  • 그 가중치를 열로 만든다.
profile
일본에서 일하는 게임 기획자. 시시해서 죽어버리지 않게, 재밌고 의미 있는 컨텐츠에 관심 있습니다. 그 도구로 데이터, AI도 찝적댑니다.

0개의 댓글