본문 바로가기

블로그/머신러닝

[추천 시스템] 협업 필터링 Collaborative Filtering - 유사도 순위 반영

이전 포스팅에서는 단순히 영화를 시청한 유저가 영화를 보고 평가한 Rating 성향을 코사인 유사도를 바탕으로 구하고, 유사도가 높은 사람을 추천하는 CF를 구현해보았다.

 

이번에는 유저의 영화 Rating 성향이 더욱 가까운 사람 K명을 선정하여 추천에 이용하는 트릭을 활용해보자.

추천 모델 RMSE
CF - 사용자 유사도 1.017
(본 포스팅) CF - 사용자 유사도 + 유사도 순위 1.010

 

이전 포스팅 - CF - user

https://fenzhan.tistory.com/30

 

[추천 시스템] 협업 필터링 Collaborative Filtering - User based

추천시스템 개요 추천시스템은 아이템에 대한 사용자의 예상 선호도를 추정하는 것이다. 따라서 어떻게 하면 아이템에 대한 사용자의 선호도를 정확히 예측할 것인지에 대한 알고리즘임 그렇

fenzhan.tistory.com

 

본 포스팅에서 활용한 데이터셋 : Movie lens 100K

https://www.kaggle.com/datasets/imkushwaha/movielens-100k-dataset

 

본 포스팅에서 활용한 자료 : 

연세대학교 인공지능대학원 개인화추천시스템 강의 - 임일 교수님

Python을 이용한 개인화 추천시스템 - 임일 교수님

 

모든 이웃을 고려하는 대신에 유사도가 높은 사람만 이웃으로 선정해서 추천을 하는 방법을 활용하여 성능을 조금 높여 볼 것이다.

 

단순하게 앞 포스팅에서 진행한 cf_simple에서 이웃 숫자만큼 짤라서 아이템에 대한 평균 평점을 예측하는 것이다.

본 포스팅에서는 하나의 유저에 이웃 30명을 고려하여, 아이템에 대한 평균 평점을 예측하는 모델이다.

import numpy as np
import pandas as pd

r_cols = ['user_id', 'movie_id', 'rating', 'timestamp']
ratings = pd.read_csv('archive/u.data', names=r_cols,  sep='\t',encoding='latin-1')
ratings = ratings.drop('timestamp', axis=1)

# Rating 데이터를 test, train으로 나누고 train을 full matrix로 변환
from sklearn.model_selection import train_test_split
x = ratings.copy()
y = ratings['user_id']
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.25, stratify=y, random_state=12)
rating_matrix = x_train.pivot(values='rating', index='user_id', columns='movie_id')

# Train set의 모든 사용자 pair의 Cosine similarities 계산
from sklearn.metrics.pairwise import cosine_similarity
matrix_dummy = rating_matrix.copy().fillna(0)
user_similarity = cosine_similarity(matrix_dummy, matrix_dummy)
user_similarity = pd.DataFrame(user_similarity, index=rating_matrix.index, columns=rating_matrix.index)

# RMSE 계산을 위한 함수
def RMSE(y_true, y_pred):
    import numpy as np
    return np.sqrt(np.mean((np.array(y_true) - np.array(y_pred))**2))

def score(model, k=20):
    id_pairs = zip(x_test['user_id'], x_test['movie_id'])
    y_pred = np.array([model(user, movie, k) for (user, movie) in id_pairs])
    y_true = np.array(x_test['rating'])
    return RMSE(y_true, y_pred)

# Neighbor size를 고려하는 추천
def cf_knn(user_id, movie_id, neighbor_size=20):
    import numpy as np
    if movie_id in rating_matrix:
        # 현재 사용자와 다른 사용자 간의 similarity 가져오기
        sim_scores = user_similarity[user_id]
        # 현재 영화에 대한 모든 사용자의 rating값 가져오기
        movie_ratings = rating_matrix[movie_id]
        # 현재 영화를 평가하지 않은 사용자의 index 가져오기
        none_rating_idx = movie_ratings[movie_ratings.isnull()].index
        # 현재 영화를 평가하지 않은 사용자의 rating (null) 제거
        movie_ratings = movie_ratings.drop(none_rating_idx)
        # 현재 영화를 평가하지 않은 사용자의 similarity값 제거
        sim_scores = sim_scores.drop(none_rating_idx)
        if neighbor_size == 0:               # Neighbor size가 지정되지 않은 경우
            # 현재 영화를 평가한 모든 사용자의 가중평균값 구하기
            mean_rating = np.dot(sim_scores, movie_ratings) / sim_scores.sum()
        else:                                # Neighbor size가 지정된 경우
            # 지정된 neighbor size 값과 해당 영화를 평가한 총사용자 수 중 작은 것으로 결정
            neighbor_size = min(neighbor_size, len(sim_scores))
            # array로 바꾸기 (argsort를 사용하기 위함)
            sim_scores = np.array(sim_scores)
            movie_ratings = np.array(movie_ratings)
            # 유사도를 순서대로 정렬
            user_idx = np.argsort(sim_scores)
            # 유사도를 neighbor size만큼 받기
            sim_scores = sim_scores[user_idx][-neighbor_size:]
            # 영화 rating을 neighbor size만큼 받기
            movie_ratings = movie_ratings[user_idx][-neighbor_size:]
            # 최종 예측값 계산 
            mean_rating = np.dot(sim_scores, movie_ratings) / sim_scores.sum()
    else:
        mean_rating = 3.0
    return mean_rating

print(score(cf_knn, 30))

Output >> 1.0108864923478802

 

위에서 구축한 추천시스템을 바탕으로 추천을 실행해보자.

 

아래 코드를 보면 다시 코사인 유사도와 Rating 정보를 불러 오는 것을 알 수 있다.

 

이는 Collaborative Filtering(CF)의 단점이기도 한데, CF는 memory-based 모델이기 때문에  평가 정보와 Cosin Similarity를 메모리에 저장시켜둬야 한다.  따라서, 추천을 진행할 때 다시 Rating 정보와 코사인 유사도를 구하는 것이다.

 

이후에 포스팅할 Matrix Factorization(MF)와 같은 Model-based apporaches는 해당 문제가 어느 정도 해소된다.

 

잡담은 뒤로하고, 

1번 사용자에게 예상평점이 가장 높을 것으로 보이는 영화 10개를 추천해주자.

 

###################### 추천하기 ######################
# 추천을 위한 데이터 읽기 (추천을 위해서는 전체 데이터를 읽어야 함)
import pandas as pd
r_cols = ['user_id', 'movie_id', 'rating', 'timestamp']
ratings = pd.read_csv('archive/u.data', names=r_cols,  sep='\t',encoding='latin-1')
ratings = ratings.drop('timestamp', axis=1)
rating_matrix = ratings.pivot(values='rating', index='user_id', columns='movie_id')

# 영화 제목 가져오기
i_cols = ['movie_id', 'title', 'release date', 'video release date', 'IMDB URL', 
          'unknown', 'Action', 'Adventure', 'Animation', 'Children\'s', 'Comedy', 
          'Crime', 'Documentary', 'Drama', 'Fantasy', 'Film-Noir', 'Horror', 
          'Musical', 'Mystery', 'Romance', 'Sci-Fi', 'Thriller', 'War', 'Western']
movies = pd.read_csv('C:/RecoSys/Data/u.item', sep='|', names=i_cols, encoding='latin-1')
movies = movies[['movie_id', 'title']]
movies = movies.set_index('movie_id')

# Cosine similarity 계산
from sklearn.metrics.pairwise import cosine_similarity
matrix_dummy = rating_matrix.copy().fillna(0)
user_similarity = cosine_similarity(matrix_dummy, matrix_dummy)
user_similarity = pd.DataFrame(user_similarity, index=rating_matrix.index, columns=rating_matrix.index)

# 추천하기
def recommender(user, n_items=10, neighbor_size=20):
    # 현재 사용자의 모든 아이템에 대한 예상 평점 계산
    predictions = []
    rated_index = rating_matrix.loc[user][rating_matrix.loc[user] > 0].index    # 이미 평가한 영화 확인
    items = rating_matrix.loc[user].drop(rated_index)
    for item in items.index:
        predictions.append(cf_knn(user, item, neighbor_size))                   # 예상평점 계산
    recommendations = pd.Series(data=predictions, index=items.index, dtype=float)
    recommendations = recommendations.sort_values(ascending=False)[:n_items]    # 예상평점이 가장 높은 영화 선택
    recommended_items = movies.loc[recommendations.index]['title']
    return recommended_items

# 영화 추천 함수 부르기
recommender(1, 10, 35)

CF_KINN 모델이 만들어낸 1번 사용자에 대한 추천 아이템리스트 10개

 

 

시청한 모든 사람들의 평점을 유사도를 바탕으로 가중평균한 앞 포스팅의 모델보다 성능이 어느정도 개선이 되었다.

(1.017 ->1.010)

 

여기서 추가적인 문제가 발생하는데, 과연 몇 K를 최적의 이웃수로 설정할 것인가이다.

 

 

해당 이웃수는 도메인마다 다르기 때문에, 최적의 K를 찾기 위해  K를 조금 씩 늘려가며 RMSE 성능을 트래킹하자

RMSE_by_K = []
Neighbor_size = []
for K in range(20, 60, 5):
    RMSE_by_K.append(score(cf_knn, K))
    Neighbor_size.append(K)
print(RMSE_by_K)

# Plot RMSE
import matplotlib.pyplot as plt
plt.plot(Neighbor_size, RMSE_by_K)
plt.ylim(1.005, 1.015)
plt.xlabel('Neighbor Size')
plt.ylabel('RMSE')
plt.show()