📚 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는 사용자 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)
• 코사인 유사도를 이용하여, 유저 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을 이용한 개인화 추천시스템", 임일, 청람
'추천시스템' 카테고리의 다른 글
컨텐츠 기반 추천(Content Based Filtering) (0) | 2022.01.10 |
---|---|
협업필터링 - 사용자의 평가 경향 고려 (0) | 2021.12.27 |
협업필터링 - 아이템 기반(IBCF) (0) | 2021.11.04 |
협업필터링 + KNN (0) | 2021.10.11 |
추천시스템 관련 기본 개념 (0) | 2021.10.08 |
댓글