본문 바로가기
추천시스템

협업필터링 기본

by 장찐 2021. 10. 11.

📚 Collaborative Filtering 개념  

  기본 개념 

   협업필터링(CF)는 가장 보편적으로 많이 알려지고 사용되는 추천 알고리즘이다. 기본적인 협업 필터링은 사용자 A에게 추천을 할 때, A와 유사한 취향을 가진 이웃들을 찾고 이 사람들이 좋아하는 상품이나 서비스를 추천하는 방식으로 진행된다. 

 

기본 가정 

  - 사용자로부터 아이템에 대한 명시적/묵시적 평가를 데이터로 구할 수 있다 

  - 사용자들의 평가 데이터에서 취향이 비슷한 사람을 찾아낼 수 있고, 취향이 비슷한 사람들은 선호 패턴이 비슷하다 

  

추천이 적합한 도메인과 그렇지 않은 도메인이 존재한다. 

 추천이 잘 맞는 도메인은 사람들의 취향이 일관되게 나타나는 도메인이다. 예를 들어, 영화의 경우 한 사람의 취향이 일관되게 유지되는 경우가 많다. 하지만 음식, 의류의 경우 개인의 성향은 물론 상황에 따라서 선호가 자주 바뀌기 때문에 추천이 비교적 어려운 편이다. 또한 고 관여도 제품이거나, 예산에 영향을 많이 받는 경우 추천이 어렵다. 대표적인 예시로 자동차가 있다. 

 


유사도(similarity) 지표 

 비슷한 취향을 가진 사용자들을 분류할 때 유사도 지표가 사용된다. 대표적으로는  Euclidean / Cosine / Correlation 이 있다.

 


📌
Correlation coefficient
(상관계수)

 가장 이해하기 쉬운 지표이면서 계산이 간단하다. 

  파이썬 corr() 함수는 주어진 데이터에서 column간 유사도를 계산함 

  평가 데이터가 연속값이고 데이터가 많은 경우 잘 작동함 

 하지만 일반적으로 좋은 성능을 보장하지는 못한다. 

📌코사인 유사도 

• 협업필터링에서 연속값에 대해서 보편적으로 많이 사용되는 지표. 

각 아이템을 차원으로 보고 사용자의 평점을 좌표값으로 본다. 사용자의 평점은 벡터로 나타낼 수 있고, 벡터간의 각도를 코사인 값으로 구해서 유사도를 계산할 수 있다.

 

-1(완전 불일치) ~ 1(완전 일치) 

 사이킷런에서는 주어진 데이터에서 row간 유사도를 계산하므로 유의해야 한다

 dimension이 높은 데이터에서 잘 작동, item-based CF에서 잘 작동 

 

📌Tanimoto coefficient 

• binary 데이터의 경우 사용함 ( = Jaccard similarity 와 거의 유사)

   a = A사용자가 1인 갯수 / b = B사용자가 1인 갯수 / c = A,B 두 사용자 모두 1인 갯수 

 두 사용자가 완전히 같으면 tanimoto coefficient 는 1이 되고, 완전히 다르면 0이 됨

 


작동 방식 

 기본적인 협업필터링은 이웃(neighbor)를 특정 사용자를 제외한 나머지 모두로 본다. 이 경우에 계산 과정은 다음과 같다. 

 

① 모든 사용자간의 평가 유사도를 계산한다. 상관계수, 코사인 등 사용

② 추천 대상과 다른 사용자들의 유사도를 추출한다. 

③ 추천 대상이 평가하지 않은 모든 아이템에 대해서, 추천 대상의 예상 평가 값을 구한다. 

예상 평가값은 다른 사용자의 해당 아이템에 대한 평가와 그 사용자와의 유사도를 가중평균으로 계산한다. 

 

 weighted average 사용 → 사용자 A에 대해서, A의 이웃 사용자들이 평가한 값을 유사도로 가중 평균함 

④ 아이템 중에서 예상 평가값이 가장 높은 N개의 아이템을 추천한다. 

 


 

📚 실습 

• 실습에는 가장 유명한 데이터 셋인 MovieLens 데이터를 사용했다.

 

< 데이터 셋 불러오기 > 

import numpy as np
import pandas as pd

# u.user 데이터 불러오기 
u_cols = ['user_id', 'age', 'sex', 'occupation', 'zip_code']
users = pd.read_csv('u.user', sep='|', names=u_cols, encoding='latin-1')

# u.items 데이터 불러오기 
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('u.user', sep='|', names=i_cols, encoding='latin-1')

# movie ID와 title을 제외한 컬럼 지우기
movies = movies[['movie_id', 'title']]

# u.data 데이터 불러오기
r_cols = ['user_id', 'movie_id', 'rating', 'timestamp']
ratings = pd.read_csv('u.data', sep='\t', names=r_cols, encoding='latin-1')

# timestamp 지우기
ratings = ratings.drop('timestamp', axis=1)

• 불러온 3개의 데이터셋은 위와 같이 구성되어 있다. 각 데이터셋을 왔다갔다 하면서 작업을 실시하니 혼동하지 않도록 유의. 

 

 

< 유저 X 아이템 행렬 만들기 > 

# Rating 데이터를 test, train split 실시(stratified split)
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)

#train 셋을 full matrix로 변환
rating_matrix = x_train.pivot(values='rating', index='user_id', columns='movie_id')

rating_matrix

• rating_matrix는 사용자 X 아이템(943 X 1631) 형태의 행렬이다. 

 

 

< 사용자 유사도 행렬 계산 > 

# 유저 간 유사도 행렬 계산 : consine similarity 사용 
from sklearn.metrics.pairwise import cosine_similarity

#코사인 유사도 계산 시에는, NaN 값을 허용하지 않으므로 0으로 대체함 
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_similarity

• 코사인 유사도를 이용하여, 유저 pair에 대한 유사도 행렬을 계산한다. 사이킷런의 코사인 유사도 패키지에서는 NaN 값을 허용하지 않으므로 0으로 대체한다. 유저 X 유저 (943 X 943) 형태의 매트릭스를 확인할 수 있다. 

 

 

 

< CF 실시 >

# 모든 영화의 (movie_id) 가중평균 rating을 계산하는 함수

def cf_simple(user_id, movie_id):
    if movie_id in rating_matrix:   # 해당 movie_id가 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.dropna()
        
        # 현재 영화를 평가하지 않은 사용자의 similarity값 제거
        sim_scores = sim_scores.drop(none_rating_idx)
        
        # 현재 영화를 평가한 모든 사용자의 가중평균값 구하기
        mean_rating = np.dot(sim_scores, movie_ratings) / sim_scores.sum()
        
    else:  #해당 movie_id가 없으므로 기본값 3.0을 예측치로 돌려 줌
        mean_rating = 3.0
        
    return mean_rating


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


# score 함수 정의 : 모델을 입력값으로 받음 
def score(model):
    id_pairs = zip(x_test['user_id'], x_test['movie_id'])
    y_pred = np.array([model(user, movie) for (user, movie) in id_pairs])
    y_true = np.array(x_test['rating'])
    return RMSE(y_true, y_pred)
   
# 정확도 계산
score(cf_simple)

 

• sim_scores : 943 개  /  movie_rating : 943 개 

• mean_rating : 유사도를 가중평균을 계산한 예측치 

• score() 함수는, CF 모델을 test 셋 데이터에 대해서 적용하는 함수임. 

• 정확도를 계산해보면 1.0165 정도로 나타남 

 

 

< 특정 사용자에게 추천 실시 > 

- 한 사용자의 모든 영화에 대한 예측값 계산

- 그중에서 값이 높은 상위 n개만 추출해서 보여줌 

 

# 추천을 위한 데이터 다시 로딩 (추천을 위해서는 전체 데이터를 읽어야 함)
r_cols = ['user_id', 'movie_id', 'rating', 'timestamp']
ratings = pd.read_csv('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('u.item', sep='|', names=i_cols, encoding='latin-1')
movies = movies[['movie_id', 'title']]
movies = movies.set_index('movie_id')


# Cosine similarity 계산
rating_matrix = ratings.pivot(values='rating', index='user_id', columns='movie_id')

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):
    # 현재 사용자의 모든 아이템에 대한 예상 평점 계산
    predictions = []
    # 이미 평가한 영화의 인덱스 추출 -> 추천 시 제외해야 함 
    rated_index = rating_matrix.loc[user][rating_matrix.loc[user].notnull()].index
    # 해당 사용자가 평가하지 않은 영화만 선택 
    items = rating_matrix.loc[user].drop(rated_index)
    
    # 예상평점 계산
    for item in items.index:
        predictions.append(cf_simple(user, item))
                                   
    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(2, 10)

• 앞서 불러온 rating 데이터셋도 사용함 

• 실제 추천을 할 때는, train/test 나눌 필요 없이 모든 데이터로 하는게 더 정확하다

• rated_index 에서 해당 사용자가 이미 평가한 영화는 제외함 

 

 


 

📚 참고자료 출처 

• "Python을 이용한 개인화 추천시스템", 임일, 청람

 

댓글