또르르's 개발 Story

[23-3] Collaborative Filtering 구현 본문

부스트캠프 AI 테크 U stage/실습

[23-3] Collaborative Filtering 구현

또르르21 2021. 2. 25. 02:35

1️⃣ 설정

 

필요한 모듈을 import 합니다.

import numpy as np 

import pandas as pd

from sklearn.metrics import mean_squared_error     # MSE를 계산하기 위하여

 

2️⃣ 데이터셋 확인하기

 

데이터셋은 다음과 같은 형태로 구성되어 있습니다.

 

  • ratings.csv : 평점
### Rating Dataset Format ###

   userId  movieId  rating  timestamp
0       1        1     4.0  964982703
1       1        3     4.0  964981247
2       1        6     4.0  964982224
3       1       47     5.0  964983815
4       1       50     5.0  964982931
  • movies.csv : 영화
### Movie Dataset Format ###

Columns of Movie Dataset :  Index(['movieId', 'title', 'genres'], dtype='object')

   movieId  ...                                       genres
0        1  ...  Adventure|Animation|Children|Comedy|Fantasy
1        2  ...                   Adventure|Children|Fantasy
2        3  ...                               Comedy|Romance
3        4  ...                         Comedy|Drama|Romance
4        5  ...                                       Comedy

 

Dataset의 User, Movie 수를 확인하면 다음과 같습니다.

n_users = df_ratings.userId.unique().shape[0]   # user의 유일한 값의 개수

n_items = df_ratings.movieId.unique().shape[0]


>>> print("num users: {}, num items:{}".format(n_users, n_items))

num users: 611, num items:9724

 

3️⃣ 데이터 전처리

 

user id, movie id의 범위를 (0 ~ 사용자 수 -1), (0 ~ 영화 수 -1) 사이로 맞춰줍니다.

user_dict = dict()      # {user_id : user_idx}, user_id : original data에서 부여된 user의 id, user_idx : 새로 부여할 user의 id

movie_dict = dict()     # {movie_id: movie_idx}, movie_id : original data에서 부여된 movie의 id, movie_idx: 새로 부여할 movie의 id

user_idx = 0

movie_idx = 0

user_dict과 movie_dict에 넣어줍니다.

ratings = np.zeros((n_users, n_items))      # row : 사용자수, col : 영화수

for row in df_ratings.itertuples(index=False):

    user_id, movie_id, _ = row
    
    if user_id not in user_dict:
    
        user_dict[user_id] = user_idx     # 새로운 index 맵핑
        
        user_idx += 1
        
    if movie_id not in movie_dict:
    
        movie_dict[movie_id] = movie_idx
        
        movie_idx += 1
        
    ratings[user_dict[user_id], movie_dict[movie_id]] = row[2]      # ratings에 평점값을 넣어줌

user_idx_to_id = {v: k for k, v in user_dict.items()}
movie_idx_to_name=dict()

movie_idx_to_genre=dict()

for row in df_movies.itertuples(index=False):

    movie_id, movie_name, movie_genre = row
    
    if movie_id not in movie_dict:              # 어떤 영화가 rating data에 없는 경우 skip
    
        continue
        
    movie_idx_to_name[movie_dict[movie_id]] = movie_name 
    
    movie_idx_to_genre[movie_dict[movie_id]] = movie_genre

 

4️⃣ Collaborative Filtering 사용 함수

1) train_test_split 

 

Training Set과 Test Set을 분리해 주는 함수입니다.

def train_test_split(ratings):

    test = np.zeros_like(ratings)
    
    train = ratings.copy()
    
    for x in range(ratings.shape[0]):     # x는 사용자
    
        nonzero_idx = ratings[x, :].nonzero()[0]    # 사용자 x가 몇개의 영화에 평점을 매겼는지
        
        test_ratings = np.random.choice(nonzero_idx, 
        
                                        size=int(len(nonzero_idx)/5),   # 1/5은 랜덤으로 뽑음
                                        
                                        replace=False)
                                        
        train[x, test_ratings] = 0.     # test에서 뽑힌 데이터는 train에서 0으로 만들어서 예측하게함
        
        test[x, test_ratings] = ratings[x, test_ratings]    # 평점 데이터에서 가지고와서 test에 넣어줌
        
        
    assert(np.all((train * test) == 0))     # train set과 test set이 완전히 분리되었는지 확인
    

    return train, test

 

2) subtract_mean

 

Pearson 상관계수를 계산하기 위해 평균 값을 빼줍니다.

(유저별로 평점을 주는 기준이 다를 수 있으므로, 유저 별 평균 평점 값을 실제 평점 값에서 빼줌)

def subtract_mean(ratings):


    mean_subtracted_ratings = np.zeros_like(ratings)    # 정규화된 평점 (sim(x,y)의 분모)
    
    
    for i in range(ratings.shape[0]):
    
        nonzero_idx = ratings[i].nonzero()[0]        # 0이 아닌 원소의 수
        
        sum_ratings = np.sum(ratings[i])
        
        num_nonzero = len(nonzero_idx)
        
        avg_rating = sum_ratings / num_nonzero      # 평점 평균을 계산
        
        if num_nonzero == 0:
        
            print("No Rating : ", i)
            
            avg_rating = 0
            
        mean_subtracted_ratings[i, nonzero_idx] = ratings[i, nonzero_idx] - avg_rating # 원본 평점에서 평균 평점을 빼서 정규화된 평균평점을 구함


    return mean_subtracted_ratings

 

3) collaborative_filtering

 

두 rating의 Pearson Correlation을 값으로 갖는 similarity matrix를 생성하여 return해주는 함수입니다.

여기서 similarity는 다음과 같이 계산됩니다.

https://www.edwith.org/bcaitech1

 

def collaborative_filtering(ratings):

    similarity = np.zeros((ratings.shape[0], ratings.shape[0]))      # 사용자수 x 사용자수 행렬
    
    num_r, num_c = ratings.shape
    
    for i in range (num_r):         # i 사용자
    
        for j in range(i+1, num_r): # j 사용자
        
            sum_i = 0           
            
            sum_j = 0
            
            dot_product = 0
            
            for k in range(num_c):   # 함꼐 본 영화 k
            
                if ratings[i,k] !=0 and ratings[j,k] != 0:  # 함께 봤을 경우 sum_i, sum_j, dot_product를 계산
                
                    sum_i += ratings[i,k]**2
                    
                    sum_j += ratings[j,k]**2
                    
                    dot_product += ratings[i,k] * ratings[j,k]
                    
                
            if dot_product!=0 : 
            
                similarity[i,j] = dot_product / sqrt(sum_i) / sqrt(sum_j)   # sim(x,y)를 계산하기 위해 prod / sum_i / sum_j 계산
                
                similarity[j,i] = similarity[i,j]                           # i, j의 유사도는 j,i의 유사도와 동일
                
            print("i:{}, j:{}".format(i,j))
            

    return similarity

 

4) predict

collaborative filtering을 통해 구한 similarity matrix와 주어진 rating matrix를 사용하여 rating을 예측하는 함수입니다.

 

주어진 유저(영화)와의 Pearson Correlation이 양수인 유저(영화) 중, 본인을 제외한 top k개의 rating을 similarity에 따라 weighted sum해주어 점수를 예측합니다.

 

def predict(ratings, similarity, k=10):


    pred = np.zeros(ratings.shape)      # 평점 데이터 크기와 동일
    

    for u in range(ratings.shape[0]):     # u는 사용자
    
        for i in range(ratings.shape[1]):   # i는 영화
        
            watched_i = ratings[:,i].nonzero()[0]                                  # 영화 i를 본 user들을 추출
            
            if u in watched_i:
            
                watched_i = np.setdiff1d(watched_i, u)                             # 본인은 제외
                
            
            similarity_u = similarity[u, watched_i]                                # 영화 i를 본 user들의 유사도를
            
            similar_idx = np.argsort(similarity_u)[::-1]                           # 높은 순으로 정렬
            
            similar_idx = similar_idx[:k]                                          # 유사도가 가장 높은 k개의 index만 추출 
            
            similar_idx = np.where(similarity_u[similar_idx] > 0)[0]               # 양수값을 갖는 유사도만 사용
            

            sum_similarity = np.sum(similarity[u, similar_idx])                    # 식의 분모를 구하기 위해 sum
            
            if sum_similarity == 0:                                                 # 0/0 = nan 문제 피하기 위해
            
                sum_similarity = 1
                

            pred[u, i] = np.sum(similarity[u, similar_idx].reshape([-1, 1]) * ratings[similar_idx, i]) / sum_similarity
            
            # np.sum(similarity[u, similar_idx].reshape([-1, 1]) * ratings[similar_idx, i]) <- 식의 분자를 계산


    return pred

 

여기서 식의 분모를 구하기 위한 코드는 다음과 같습니다.

sum_similarity = np.sum(similarity[u, similar_idx]) 

 

5) MSE

Test Score와 Predicted Score의 Mean Squared Error를 계산합니다.

def get_mse(pred, actual):

    pred = pred[actual.nonzero()].flatten()     # actual.nonzero() 평가 데이터의 평가된 원소들만 불러옴
    
    actual = actual[actual.nonzero()].flatten()
    
    return mean_squared_error(pred, actual)

 

6) recommend

특정 user와 유사한 영화를 추천합니다.

def recommend(watched_rating, pred, user_id, user_dict, movie_idx_to_name, movie_idx_to_genre):

    movies_in_order = np.argsort(pred[user_dict[user_id]])[::-1]
    
    watched_movie = watched_rating[user_dict[user_id]].nonzero()[0]
    
    cnt = 0
    
    for movie in movies_in_order:
    
        if pred[user_dict[user_id], movie] == 0:
        
            if cnt== 0:
            
                print("### Cannot Recommend a Movie : All Input Ratings Have Same Value ###")
                
            break
            
        if movie in watched_movie: continue
        
        cnt += 1 
        
        print("### Top {} Movie for User {} : {} \t Genre: {} ###".format(cnt, user_id, movie_idx_to_name[movie], movie_idx_to_genre[movie]))
        
        if cnt == 5: break

 

5️⃣ Collaborative Filtering 실행

 

제공된 데이터에 collaborative filtering을 적용해 봅니다.

train_ratings, test_ratings = train_test_split(ratings)
# 유저별로 평점을 주는 기준이 다를 수 있으므로, 유저 별 평균 평점 값을 실제 평점 값에서 빼준다

mean_subtracted_ratings = subtract_mean(train_ratings)

collaborative filtering 함수를 실행합니다.

similarity = collaborative_filtering(mean_subtracted_ratings) 

predict를 수행합니다.

predicted_ratings = predict(train_ratings, similarity)

user_id가 800일 때 recommend 받는 영화는 다음과 같습니다.

user_id = 800
recommend(train_ratings, predicted_ratings, user_id, user_dict, movie_idx_to_name, movie_idx_to_genre)



### Top 1 Movie for User 800 : Usual Suspects, The (1995) 	 Genre: Crime|Mystery|Thriller ###
### Top 2 Movie for User 800 : Schindler's List (1993) 	 Genre: Drama|War ###
### Top 3 Movie for User 800 : Jurassic Park (1993) 	 Genre: Action|Adventure|Sci-Fi|Thriller ###
### Top 4 Movie for User 800 : Fugitive, The (1993) 	 Genre: Thriller ###
### Top 5 Movie for User 800 : Beauty and the Beast (1991) 	 Genre: Animation|Children|Fantasy|Musical|Romance|IMAX ###
Comments