본문 바로가기

블로그/머신러닝

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

이전 포스팅에서는 유저의 영화 Rating 성향이 더욱 가까운 사람 K명을 선정하여 추천에 이용하는 트릭을 활용하였다.

추천 모델 RMSE
CF - 사용자 유사도 1.017
CF - 사용자 유사도 + 유사도 순위 1.010
CF - 사용자 유사도 + 유사도 순위 + 사용자 평가 경향 맨 아래에 공개

 

이번에는 사용자 평가의 경향을 모델에 반영하여 추천 모델의 성능(RMSE)을 높여 보자.

 

이전 포스팅 - CF - 사용자 유사도 + 유사도 순위

 

 

https://fenzhan.tistory.com/32

 

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

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

fenzhan.tistory.com

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

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

 

MovieLens 100K Dataset

 

www.kaggle.com

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

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

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

 

사용자가 콘텐츠의 평점을 결정하는 매커니즘은 유저 개개인마다 모두 다르다. 우리 데이터셋인 영화로 예를 들면, 사람1은 극중 인물의 연기력을 바탕으로 10점을 주는 반면에, 사람2는 극중 인물뿐만 아니라 내용 전개 방식과 영상미까지 모두 고려해 평점을 줄 수 있다. 

 

우리는 기존 사용자들의 평점을 데이터를 바탕으로 다른 사용자의 평점을 예측하는 태스크이다. 따라서, 평점을 보다 객관성이 있는 데이터셋으로 만들어 주는 것이 필요할 것이다.  따라서, 평점 데이터를 사용자의 평가 성향을 반영해 정규화 해준다면 더 정확한 추천을 할 수 있지 않을까?

 

평점 정규화는 다양한 방식이 있지만(e.g., 중앙값 기준으로 정규화), 본 포스팅은 평균을 기준으로 구성한 편차를 활용해보고자 한다.

 

기존 포스팅에서 아이템 i에 대한 사용자 a의 예상 평점인 선호도(P(a,i))를 구하는 방식을 생각해보자.

 

a: 사용자, u: 이웃 사용자, n: 이웃 사용자의 수
p(a,i): 아이템 i에 대한 사용자 a의 예상 평점
w(a,u): 사용자 a와 u의 유사도
r(u,i): 아이템 i에 대한 사용자 u의 평점

 

이전 포스팅의 최종 예측값 계산 코드에서 위 식을 통해 예상 평점을 구하였다.
mean_rating = np.dot(sim_scores, movie_ratings) / sim_scores.sum()

 

여기서 평가 성향을 반영하기 위해, 위 식을 평가 경향을 반영하기 위해 아래와 같이 변형시켜 보자.

 

코드에 반영하기 위한 전략은 아래와 같다. 

 

1) 우선 각 사용자의 평점평균을 계산한다 (r_u)

 

2) 각 아이템의 평점을 각 사용자의 평균에서의 차이, 즉 편차(아이템 평점 - 해당 사용자의 평점 평균)로 변환한다(r_u,i - r_u)

 

3) 편차를 사용해서 해당 사용자의 해당 아이템의 편차 예측값을 계산함. 이 때, 전 포스팅에서 적용한 것과 마찬가지로 추천 대상 사용자의 이웃을 구하고, 이웃에 해당하는 아이템에 대한 평점편차와 유사도를 가중평균함.

 

4) 이렇게 구한 편차 예측값은 평균에서의 차이를 의미하기 때문에 마지막에 실제 예측값으로 변환하기 위해 현 사용자의 평균에 이 편차 예측값을 더함.(r_a+)

 

# Bias-from-mean + KNN CF

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')

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

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

# 모든 가능한 사용자 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)

# 모든 user의 rating 평균 계산 
rating_mean = rating_matrix.mean(axis=1)

# KNN + 사용자 평가 경향 추천
def ubcf_bias_knn(user_id, movie_id, neighbor_size=20):
    import numpy as np
    # 현 user의 평균 가져오기
    user_mean = rating_mean[user_id]
    if movie_id in rating_matrix:
        # 현 user와 다른 사용자 간의 유사도 가져오기
        sim_scores = user_similarity[user_id]
        # 현 movie의 rating 가져오기
        movie_ratings = rating_matrix[movie_id]
        # 모든 사용자의 rating 평균 가져오기
        others_mean = rating_mean
        # 현 movie에 대한 rating이 없는 user 삭제
        none_rating_idx = movie_ratings[movie_ratings.isnull()].index
        movie_ratings = movie_ratings.drop(none_rating_idx)
        sim_scores = sim_scores.drop(none_rating_idx)
        others_mean = others_mean.drop(none_rating_idx)
        if neighbor_size == 0:               # Neighbor size가 지정되지 않은 경우
            # 편차로 예측치 계산
            movie_ratings = movie_ratings - others_mean
            prediction = np.dot(sim_scores, movie_ratings) / sim_scores.sum()
            # 예측값에 현 사용자의 평균 더하기
            prediction = prediction + user_mean
        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)
            others_mean = np.array(others_mean)
            # 유사도를 순서대로 정렬
            user_idx = np.argsort(sim_scores)
            # 유사도와 rating을 neighbor size만큼 받기
            sim_scores = sim_scores[user_idx][-neighbor_size:]
            movie_ratings = movie_ratings[user_idx][-neighbor_size:]
            # 사용자의 mean을 neighbor size만큼 받기
            others_mean = others_mean[user_idx][-neighbor_size:]
            # 편차로 예측치 계산
            movie_ratings = movie_ratings - others_mean
            prediction = np.dot(sim_scores, movie_ratings) / sim_scores.sum()
            # 예측값에 현 사용자의 평균 더하기
            prediction = prediction + user_mean
    else:
        prediction = user_mean
    return prediction

score(ubcf_bias_knn, 30)

Output >> 0.9434094850964859

 

많은 성능 개선을 이루어냈다 !

baseline model 1.017 -> 0.9434

 

이제 모델을 활용해 추천 목록을 반환시켜 보자. 3번 사용자에게 예상평점이 가장 높을 것으로 보이는 영화 10개를 추천하고 이때, 고려하는 이웃의 개수는 35명이다.

 

###################### 추천하기 ######################
import pandas as pd
# 추천을 위한 데이터 읽기 (추천을 위해서는 전체 데이터를 읽어야 함)
r_cols = ['user_id', 'movie_id', 'rating', 'timestamp']
ratings = pd.read_csv('C:/Recosys/Data/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')
# 사용자별 평균 구하기
rating_mean = rating_matrix.mean(axis=1)

# 영화 제목 가져오기
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(ubcf_bias_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(3, 10, 35)

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

CF 모델 성능 비교

추천 모델 RMSE
CF - 사용자 유사도 1.017
CF - 사용자 유사도 + 유사도 순위 1.010
CF - 사용자 유사도 + 유사도 순위 + 사용자 평가 경향 0.9434