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

[머신러닝] 추천 시스템: 아이템 기반 최근접 이웃 협업 필터링 실습

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

 

 

[머신러닝] 추천시스템

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

puppy-foot-it.tistory.com


이전 내용
 

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

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

puppy-foot-it.tistory.com


아이템 기반 최근접 이웃 협업 필터링 실습

 

Grouplens 사이트에서 만든 MovieLens 데이터 세트르 이용해 협업 필터링 기반의 영화 추천을 구현해본다.

 

MovieLens Latest Datasets

These datasets will change over time, and are not appropriate for reporting research results. We will keep the download links stable for automated downloads. We will not archive or make available p…

grouplens.org

상단의 사이트에 접속한 후 하단의 빨간 도형의 파일을 다운로드하면 되며, 해당 파일은 십만 개의 평점 정보를 가지고 있는 데이터이다.


데이터 가공 및 변환

 

다운로드 받은 파일의 압축을 해제한 후, movies.csv와 ratings.csv 파일을 DataFrame으로 불러와야 한다.

 

import numpy as np
import pandas as pd

movies = pd.read_csv("C:/Users/niceq/Documents/DataScience/Python ML Guide/Data/09. ml-latest-small/movies.csv")
ratings = pd.read_csv("C:/Users/niceq/Documents/DataScience/Python ML Guide/Data/09. ml-latest-small/ratings.csv")
print(movies.shape)
print(ratings.shape)

movies.head(2)

 

▶ movies.csv 파일은 영화에 대한 메타 정보인 title과 genres를 가지고 있다.

ratings.head(2)

▶ ratings.csv 파일은 사용자별로 영화에 대한 평점을 매긴 데이터 세트이며,

  • userId: 사용자 아이디
  • movieId: 영화 아이디
  • rating: 평점 (0.5 - 5, 0.5 단위)

협업 필터링은 ratings.csv 데이터 세트와 같이 사용자와 아이템 간의 평점에 기반해 추천하는 시스템이므로, ratings.csv의 DataFrame인 ratings를 이용해 아이템 기반의 최근접 이웃 협업 필터링을 구현해본다.

 

[로우(행) 레벨 형태의 원본 데이터 세트→ 로우-사용자 / 칼럼-영화 로 구성한 데이터 세트로 변경]

DataFrame의 pivot_table() 함수를 이용해 로우 레벨의 값을 칼럼으로 변경한다.

인자로 columns='movieId'와 같이 부여하면 movieId 칼럼의 모든 값이 새로운 칼럼 이름으로 변환된다.

ratings = ratings[['userId', 'movieId', 'rating']]
ratings_matrix = ratings.pivot_table('rating', index='userId', columns='movieId')
ratings_matrix.head()

▶ ratings.pivot_table('rating', index='userId', columns='movieId') 와 같이 호출하면 로우(행) 레벨은 'userId', 칼럼은 모두 'movieId' 칼럼에 있는 값으로 칼럼 이름이 바뀌고, 데이터는 rating 칼럼에 있는 값이 할당된다.

 

[ratings와 movies 조인]

  • NaN 값이 많은 이유는 사용자가 평점을 매기지 않은 영화가 칼럼으로 변환되면서 NaN으로 값이 할당됐기 때문이며, 이를 모두 0으로 변환한다.
  • 칼럼명이 1, 2, 3... 193609 등의 숫자로 되어 있어 사용자가 평점을 준 영화가 어떤 영화인지 알기 어려우므로, 칼럼명을 movieId가 아닌 title로 변경한다.
  • ratings와 movies를 조인해 title 칼럼을 가져온 뒤에 pivot_table() 인자로 columns 에 'title' 입력
# title 칼럼을 얻기 위해 movies와 조인
rating_movies = pd.merge(ratings, movies, on='movieId')

# colums = 'title'로 title 칼럼으로 피벗 수행
ratings_matrix = rating_movies.pivot_table('rating', index='userId', columns='title')

# NaN 값을 모두 0으로 변환
ratings_matrix = ratings_matrix.fillna(0)
ratings_matrix.head()


영화 간 유사도 산출

 

변환된 사용자-영화 평점 행렬 데이터 세트를 이용해 영화 간의 유사도를 측정한다.

영화 간의 유사도는 코사인 유사도를 기반으로 하고 사이킷런의 cosine_similarity() 를 이용해 측정한다.

하지만 cosine_similarity() 함수는 행을 기준으로 서로 다른 행을 비교해 유사도를 산출하기 때문에 (userId 기준) 이 함수를 여기에 적용하면 영화 간의 유사도가 아닌 사용자 간의 유사도가 만들어진다.

따라서 영화를 기준으로 함수를 적용하려면 ratings_matrix 데이터의 행과 열의 위치를 변경하면 되는데, 판다스는 이 같은 전치 행렬 변경을 위해 transpose() 함수를 제공한다.

ratings_matrix_T = ratings_matrix.transpose()
ratings_matrix_T.head()

 

[전치 행렬 형식으로 변경한 데이터 세트의 코사인 유사도 계산]

좀 더 직관적인 영화의 유사도 값을 표현하기 위해 cosine_similarity() 로 반환된 넘파이 행렬에 영화명을 매핑해 DataFrame으로 변환

from sklearn.metrics.pairwise import cosine_similarity

item_sim = cosine_similarity(ratings_matrix_T, ratings_matrix_T)

# cosine_similarity()로 반환된 넘파이 행렬을 영화명을 매핑해 DataFrame으로 변환
item_sim_df = pd.DataFrame(data=item_sim, index=ratings_matrix.columns, 
                           columns = ratings_matrix.columns)
print(item_sim_df.shape)
item_sim_df.head()

▶ 9719 로우인 ratings_matrix.transpose() 데이터 세트에 대해 cosine_similarity() 를 적용한 결과 9719 * 9719 Shape으로 영화의 유사도 행렬인 item_sim이 생성되었다.

 

[특정 영화와 유사도가 높은 영화 추출]

item_sim_df 를 이용해 영화 대부(Godfather)와 유사도가 높은 상위 6개 영화를 추출해 본다.

item_sim_df['Godfather, The (1972)'].sort_values(ascending=False)[:6]

▶ 기준 영화를 제외하면 '대부-2'편이 유사도가 가장 높다.

 

이번에는 '인셉션'과 유사도가 높은 영화를 찾아보는데, '인셉션'은 제외한다.

item_sim_df['Inception (2010)'].sort_values(ascending=False)[1:6]

▶ '다크나이트'가 가장 유사도가 높고, 그 뒤를 이어 주로 스릴러와 액션이 가미된 영화들이 높은 유사도를 보이고 있다.


아이템 기반 최근접 이웃 협업 필터링으로 개인화된 영화 추천

 

이번에는 위의 아이템 기반 유사도 데이터를 이용해 개인에게 특화된 영화 추천 알고리즘을 만들어 본다.

앞서 만든 유사도 데이터는 개인적인 취향을 반영하지 않고 영화 간의 유사도 만을 가지고 추천한 것이기 때문에, 최근접 이웃 협업 필터링으로 개인에게 최적화된 영화 추천을 구현해 본다.

개인화된 영화 추천의 가장 큰 특징은 개인이 아직 관람하지 않은 영화를 추천한다는 것인데, 아직 관람하지 않은 영화에 대해서 아이템 유사도와 기존에 관람한 영화의 평점 데이터를 기반으로 해 새롭게 모든 영화의 예측 평점을 계산한 후 높은 예측 평점을 가진 영화를 추천하는 방식이다.

 

[사용자별로 최적화된 평점 스코어를 예측하는 함수 생성]

앞에서 생성된 영화 간의 유사도를 가지는 DataFrame인 item_sim_df와 사용자-영화 평점 DataFrame인 ratings_matrix 변수를 계속 활용해 사용자별로 최적화된 평점 스코어를 예측하는 predict_rating() 함수를 만든다.

해당 함수는 인자로 사용자-영화 평점 넘파이 행렬(rating_matrix를 넘파이 행렬로 변환)과 영화 간의 유사도를 가지는 넘파이 행렬(item_sim_df를 넘파이 행렬로 변환)을 입력받고 개인화된 예측 평점을 계산한다.

def predict_rating(ratings_arr, item_sim_arr):
    ratings_pred = ratings_arr.dot(item_sim_arr) / np.array([np.abs(item_sim_arr).sum(axis=1)])
    return ratings_pred

 

ratings_pred = predict_rating(ratings_matrix.values, item_sim_df.values)
ratings_pred_matrix = pd.DataFrame(data=ratings_pred, index=ratings_matrix.index, columns=ratings_matrix.columns)
ratings_pred_matrix.head()

▶ 예측 평점이 사용자별 영화의 실제 평점과 영화의 코사인 유사도를 내적(dot)한 값이기 때문에 기존에 영화를 관람하지 않아 0에 해당했던 실제 영화 평점이 예측에서는 값이 부여되는 경우가 많이 발생한다.

예측 평점이 실제 평점이 비해 작을 수 있는데, 이는 내적 결과를 코사인 유사도 벡터 합으로 나누었기 때문에 생기는 현상이다.

 

[예측 결과와 실제 평점의 차이 확인]

예측 평가 지표는 MSE를 적용하는데, 실제와 예측 평점의 차이는 기존에 평점이 부여된 데이터에 대해서만 오차 정도를 측정하며 MSE를 계산하는 get_mse() 함수를 만들어 결과를 확인한다.

from sklearn.metrics import mean_squared_error

# 사용자가 평점을 부여한 영화에 대해서만 예측 성능 평가 MSE 계산
def get_mse(pred, actual):
    # 평점이 있는 실제 영화만 추출
    pred = pred[actual.nonzero()].flatten()
    actual = actual[actual.nonzero()].flatten()
    return mean_squared_error(pred, actual)

print('아이템 기반 최근접 이웃 MSE:', get_mse(ratings_pred, ratings_matrix.values))

▶ 실제 값과 예측값은 서로 스케일이 다르기 때문에 MSE가 클 수도 있으며, 중요한 것은 이 MSE를 감소시키는 방향으로 개선하는 것이다.

 

[특정 영화와 비슷한 유사도를 가지는 영화에 대해서만 유사도 예측]

기존 predict_rating() 함수는 해당 영화와 다른 모든 영화 간의 유사도 벡터를 적용하다 보니 상대적으로 평점 예측이 떨어졌다. 따라서 특정 영화와 가장 비슷한 유사도를 가지는 영화에 대해서만 유사도 벡터를 적용하는 predict_rating_topsim() 함수를 생성하여 예측 값을 계산해 본다.

해당 함수는 predict_rating() 함수와 유사하나, n 인자를 가지고 있어서 Top-N 유사도를 가지는 영화 유사도 벡터만 예측값을 계산하는데 적용한다. 다만, 이러한 계산을 위해 개별 예측값 계산 시 행, 열 별로 for 루프를 반복 수행하면서 Top-N 유사도를 계산해야 하기 때문에 수행시간이 오래 걸린다는 단점이 있다.

def predict_rating_topsim(ratings_arr, item_sim_arr, n=20):
    # 사용자-아이템 평점 행렬 크기만큼 0으로 채운 예측 행렬 초기화
    pred = np.zeros(ratings_arr.shape)

    # 사용자-아이템 평점 행렬의 열 크기만큼 루프 수행
    for col in range(ratings_arr.shape[1]):
        # 유사도 행렬에서 유사도가 큰 순으로 n개 데이터 행렬의 인덱스 반환
        top_n_items = [np.argsort(item_sim_arr[:, col])[:-n-1:-1]]
        # 개인화된 예측 평점 계산
        for row in range(ratings_arr.shape[0]):
            pred[row, col] = item_sim_arr[col, :][top_n_items].dot(ratings_arr[row,:][top_n_items].T)
            pred[row, col] /= np.sum(np.abs(item_sim_arr[col, :][top_n_items]))

    return pred

 

계산된 예측 평점 넘파이 행렬은 판다스 DataFrame으로 재생성한다.

ratings_pred = predict_rating_topsim(ratings_matrix.values, item_sim_df.values, n=20)
print('아이템 기반 최근접 Top-20 이웃 MSE:', get_mse(ratings_pred, ratings_matrix.values))

# 계산된 예측 평점 데이터는 DataFrame으로 재생성
ratings_pred_matrix = pd.DataFrame(data=ratings_pred, index=ratings_matrix.index, columns=ratings_matrix.columns)

▶ MSE가 약 3.69로 기존의 9.89 보다 많이 향상되었다.

 

[특정 사용자에 대한 영화 추천 - 1]

userId=9 인 사용자에 대해 영화를 추천해 보기 전에, 해당 사용자가 어떤 영화를 좋아하는지 확인하기 위해 해당 사용자가 평점을 준 영화를 평점이 높은 순으로 확인해 본다.

user_rating_id = ratings_matrix.loc[9, :]
user_rating_id[ user_rating_id >0].sort_values(ascending=False)[:10]

▶ '반지의 제왕', '백투더퓨처', '오스틴 파워' 등 대작 영화나 어드벤처 영화 등 전반적으로 흥행성이 좋은 영화에 높은 평점을 주고 있다.

 

[특정 사용자에 대한 영화 추천 - 2]

해당 사용자에게 아이템 기반 협업 필터링을 통해 영화를 추천하기 위해 먼저 사용자가 이미 평점을 준 영화를 제외하고 추천할 수 있도록 평점을 주지 않은 영화를 리스트 객체로 반환하는 함수인 get_unseen_movies() 함수를 생성한다.

def get_unseen_movies(ratings_matrix, userId):
    # userId로 입력받은 사용자의 모든 영화 정보를 추출해 Series로 변환
    # 반환된 user_rating은 영화명(title)을 인덱스로 가지는 Series 객체
    user_rating = ratings_matrix.loc[userId, :]

    # user_rating이 0보다 크면 기존에 관람한 영화. 대상 인덱스를 추출해 list 객체로 만듦.
    already_seen = user_rating[ user_rating > 0].index.tolist()

    # 모든 영화명을 list 객체로 만듦.
    movies_list = ratings_matrix.columns.tolist()

    # list comprehension 으로 already_seen에 해당하는 영화는 movies_list에서 제외
    unseen_list = [ movie for movie in movies_list if movie not in already_seen]

    return unseen_list

 

[특정 사용자에 대한 영화 추천 - 3]

사용자가 영화의 평점을 주지 않은 추천 대상 영화 정보와 predict_rating_topsim()에서 추출한 사용자별 아이템 유사도에 기반한 예측 평점 데이터 세트를 이용해 최종적으로 사용자에게 영화를 추천하는 함수인 recomm_movie_by_userid() 생성.

해당 함수는 예측 평점 DataFrame과 추천하려는 사용자id, 추천 후보 영화 리스트, 추천 상위 영화 개수를 인자로 받아서 사용자가 좋아할 만한 가장 높은 예측 평점을 가진 영화를 추천해 준다.

def recomm_movie_by_userid(pred_df, userId, unseen_list, top_n=10):
    # 예측 평점 DataFrame에서 사용자 id 인덱스와 unseen_list 로 들어온 영화명 칼럼을 추출해 가장 예측 평점이 높은 순으로 정렬
    recomm_movies = pred_df.loc[userId, unseen_list].sort_values(ascending=False)[:top_n]
    return recomm_movies

# 사용자가 관람하지 않은 영화명 추출
unseen_list = get_unseen_movies(ratings_matrix, 9)

# 아이템 기반의 최근접 이웃 협업 필터링으로 영화 추천
recomm_movies = recomm_movie_by_userid(ratings_pred_matrix, 9, unseen_list, top_n=10)

# 평점 데이터를 DataFrame으로 생성
recomm_movies = pd.DataFrame(data=recomm_movies.values, index=recomm_movies.index, columns=['pred_score'])
recomm_movies


★ 만약 userId = 9 가 아닌, 다른 사용자를 추천하고 싶은 상황을 대비하여 코드를 좀 수정하여 실행해 본다,

userid = 10

# 해당 유저가 보지 않은 영화 리스트
user_rating_id = ratings_matrix.loc[userid, :]
print('{0} 유저가 보지 않은 영화 리스트:'.format(userid))
user_rating_id[ user_rating_id >0].sort_values(ascending=False)[:10]

 

userid = 10

def recomm_movie_by_userid(pred_df, userId, unseen_list, top_n=10):
    # 예측 평점 DataFrame에서 사용자 id 인덱스와 unseen_list 로 들어온 영화명 칼럼을 추출해 가장 예측 평점이 높은 순으로 정렬
    recomm_movies = pred_df.loc[userId, unseen_list].sort_values(ascending=False)[:top_n]
    return recomm_movies

# 사용자가 관람하지 않은 영화명 추출
unseen_list = get_unseen_movies(ratings_matrix, userid)

# 아이템 기반의 최근접 이웃 협업 필터링으로 영화 추천
recomm_movies = recomm_movie_by_userid(ratings_pred_matrix, userid, unseen_list, top_n=10)

# 평점 데이터를 DataFrame으로 생성
recomm_movies = pd.DataFrame(data=recomm_movies.values, index=recomm_movies.index, columns=['pred_score'])

# 보지 않은 영화를 기준으로 영화 추천
print('{0} 유저를 위한 추천 영화 리스트:'.format(userid))
recomm_movies


다음 내용

 

[머신러닝] 추천 시스템: 잠재 요인 협업 필터링 실습

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

puppy-foot-it.tistory.com


[출처]

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

728x90
반응형