유사한 사용자/아이템을 찾는 협업 필터링
1) 협업 필터링이란? (feat. 정의, 종류)
협업 필터링은 한 사용자의 구매 패턴 및 평점을 다른 사용자들의 구매 패턴 및 평점과 비교하여 아이템을 추천하는 방식이다. 즉, 해당 사용자와 비슷하게 평점을 매긴 사용자들을 찾아 이들의 구매 패턴 및 평점 정보를 통해 사용자에게 아이템을 추천하는 것이다. 그렇기 때문에 협업필터링에서는 추가적인 사용자의 개인정보나 아이템 정보가 없이도 추천이 가능하다는 장점이 있다.
협업 필터링에는 크게 최근접 이웃기반 협업 필터링과 잠재 요인기반 협업 필터링 두 가지가 있다. 먼저 최근접 이웃기반 협업 필터링은 블록 기반의 모델인 KNN을 활용하며, Netflix Prize Competition에서 우승한 알고리즘으로 유명해졌다. 이와 달리 잠재 요인기반 협업 필터링은 잠재 요인을 활용한다.
2) 협업 필터링의 기초
(1) K Nearest Neighbors(KNN)
이웃기반 협업 필터링에서 활용되는 KNN 알고리즘은, 한 사용자(혹은 한 점)와 가장 근접한 K개의 Neighbors를 계산하는 방법이다. 일례로 위의 그림에서 Pt라는 점이 새로 들어왔다고 가정하고 이 점이 어떤 Class에 속하는지를 알고자 한다. 그렇다면 이 Pt는 Class A에 두 개, Class B에 세 개, Class C에 두 개의 점이 포함되고 있으므로 세 개의 점이 포함된 Class B에 가장 근접하다고 말할 수 있으며, B를 Pt의 Class로 정할 수 있는 것이다.
(2) 데이터(Explicit Feedback)
먼저 데이터의 형태에 대해서 알아본 뒤, 협업 필터링 각각에 대해서 알아보고자 한다. 데이터의 형태에는 크게 Implicit Feedback과 Explicit Feedback 두 가지가 있다. 사용자가 아이템을 구매했는지 구매하지 않았는지에 대한 정보는 알고 있으나 이 아이템을 좋아하는지 싫어하는지에 대한 반응 정보를 모르고 있는 상태를 Implicit이라고 하고, 사용자가 아이템에 대한 자신의 선호도를 직접 표현하는 상태를 Explicit이라고 한다.
그래서 앞으로 협업 필터링 예제에서는 사용자들이 각각의 아이템에 대해서 1~10점의 점수를 매기게 되며 몇몇 아이템에 대해서는 점수가 없는 형태, 즉 아이템을 구매하지 않아 ?가 나오는 형태를 띄게 될 것이다. 이때 우리는 구매하지 않았으나 이웃기반 모델을 통해 어떤 평점을 가질지 예측하여 ?에 들어갈 값을 찾을 수 있다.
3) 최근접 이웃기반 협업 필터링(Neighborhood based Collaborative Filtering)
(1) 사용자 기반 협업 필터링과 아이템 기반 협업 필터링 기본 예제
메모리 기반 알고리즘인 최근접 이웃기반 협업 필터링은 협업 필터링을 위해서 개발된 초기 알고리즘으로서 크게 사용자 기반 협업 필터링과 아이템 기반 협업 필터링 두 가지로 나눌 수 있다. 먼저 사용자 기반 협업 필터링(User-based collaborative filtering)은 해당 사용자와 구매 패턴 및 평점이 비슷한 사용자를 찾아 이 비슷한 사용자가 본 아이템으로 추천 리스트를 생성하는 방식이다. 즉, 아이템을 추천해줄 때 “유사한 사람”을 찾는다는 것이 핵심이다. 아래의 User-based filtering 예제에서도 볼 수 있듯이, 세 번째 사용자가 첫 번째 사용자와 50%(2/4) 유사하기 때문에 세 번째 사용자와 유사한 첫 번째 사용자가 본 포도와 오렌지라는 아이템을 세 번째 사용자에게 추천해주는 것이다.
이와 달리 아이템 기반 협업 필터링(Item-based collaborative filtering)은 해당 사용자가 보고 평점을 준 아이템과 유사한 아이템을 찾아서 추천 리스트를 생성하는 방식이다. 즉 추천 리스트를 생성하기 위해 “유사한 아이템”을 찾는 것이 핵심이다. Item-based filtering 예제를 보면, 세 번째 사용자는 수박이라는 아이템을 보았고 이를 본 첫 번째 사용자와 두 번째 사용자는 모두 포도라는 아이템 역시 보았다. 따라서 첫 번째 사용자와 두 번째 사용자가 본 포도라는 아이템을 수박이라는 아이템과 유사하다고 판단하여 이를 세 번째 사용자에게 추천해주는 것이다.
(2) 사용자 기반 협업 필터링(User-based collaborative filtering)에서 ? 구하기
바로 앞에서 말한 것처럼 ?는 어떤 평점을 가질지 예측하여, 유사도를 구해 찾을 수 있다. 따라서 일단 나와 있는 표에서 ?는 무시한 채 있는 정보들을 통해서 Cosine 유사도와 Pearson 유사도를 구한다. 이때 사용자3의 아이템1에서 이것이 사용자1, 사용자2와 비슷한 선호를 보인다고 생각해 이를 통해
?를 예측해보자. 이들의 아이템1은 각각 7점과 6점이므로 ? 역시 6~7점의 높은 수준을 보일 것이고, 사용자3 역시 아이템1을 많이 볼 것이라고 예상할 수 있다.
마찬가지로 사용자3의 아이템6에서 이것이 사용자4, 사용자5와 비슷한 선호를 보인다고 생각해 이를 통해 ?를 예측해보자. 이들의 아이템6은 각각 4점과 3점이므로 ? 역시 3~4점의 낮은 수준을 보일 것이고, 사용자3 역시 아이템6을 적게 볼 것이라고 예상할 수 있다.
(3) 사용자 기반 협업 필터링(User-based collaborative filtering)의 문제점 및 해결 방안
다만 이러한 사용자 기반 협업 필터링에서도 문제가 되는 부분이 있다. 위의 표에서 사용자마다의 평균값을 계산해보면 각각 5.5, 4.8, 2.0, 2.5, 2.0이 나오는데 사용자1과 사용자2가 사용자3, 사용자4, 사용자5에 비해서 후한 평점을 주는 것을 알 수 있다. 즉, 사용자1과 사용자2가 평점을 높게 주었는데 이것이 진짜로 아이템을 좋다고 느껴서 높은 평점을 준 것인지, 아니면 후한 평점을 주는 사용자이기 때문에 평점을 높게 주어서 편차가 생긴 것인지에 대한 의구심이 들 수 있다.
따라서 이러한 편향을 제거할 필요가 존재한다. 사용자 간의 편향을 제거하기 위한 식은 아래와 같은데, 아이템의 평점에서 사용자의 평균 평점을 뺀 뒤 피어슨 유사도를 곱하여 가중평균을 구하면 편향을 제거한 평점을 구할 수 있다.
ex) 사용자3의 아이템1 평점
사용자3의 아이템1 평점 = 사용자3의 평균 평점 + [(사용자1의 아이템1 평점 - 사용자1의 평균 평점) ⨯ 사용자1의 피어슨 유사도 + (사용자2의 아이템1 평점 - 사용자2의 평균 평점) ⨯ 사용자2의 피어슨 유사도] / [사용자1의 피어슨 유사도 + 사용자2의 피어슨 유사도]
(4) 아이템 기반 협업 필터링(Item-based collaborative filtering)
아이템 기반 협업 필터링이란 말그대로 아이템 간의 유사도를 계산하는 알고리즘으로, 사용자 기반 협업 필터링과 같은 방식으로 유사도의 편향을 제거하여 가중평균을 구할 수 있다. 아이템 기반 협업 필터링이 사용자 기반 협업 필터링과 다른 점은 사용자 기반 협업 필터링이 사용자 간의 비교를 했다면, 아이템 기반 협업 필터링은 여러 사용자 묶음에서 아이템을 기준으로 하나의 아이템과 다른 아이템의 유사도를 계산하는 방식이라는 것이다.
(5) 최근접 이웃기반 협업 필터링의 장점과 단점
장점
•
접근 방식이 간단하고 직관적이기 때문에 구현 및 디버그가 쉽다.
•
비슷한 사용자를 먼저 찾은 뒤에 이 비슷한 사용자를 몇 명이나 선택할지 K를 통해 정할 수 있기 때문에 아이템 추천의 이유가 정당하다.
•
같은 이유로 아이템 기반 협업 필터링의 해석 가능성이 높아진다.
•
추천 리스트에 새로운 아이템과 사용자가 추가되더라도 상대적으로 모델이 크게 바뀌지 않아 안정적이다.
단점
•
사용자 기반 협업 필터링에서 시간과 속도, 메모리가 많이 필요하다.
•
희소성 때문에 제한 범위가 생긴다. 즉, 사람들이 많이 보는 상품은 많이 보고 적게 보는 상품은 적게 봐서 많이 보는 상품 위주로 추천이 진행될 수밖에 없으며, 어떤 아이템에 대해서 아무도 평가를 내리지 않는다면 그 아이템에 대해서는 평점 예측이 불가능하다.
따라서 컨텐츠 기반 추천 시스템을 함께 활용해야 한다!
4) 잠재 요인기반 협업 필터링(Latent Factor Collaborative Filtering)
(1) 이웃기반 협업 필터링과의 차이점으로 본 잠재 요인기반 협업 필터링의 정의와 원리
이웃기반 협업 필터링은 아이템의 벡터와 사용자 스페이스의 벡터 간의 조합을 통해 아이템 간의 유사도를 통해 아이템을 기반으로 추천하거나 사용자 간의 벡터 유사도를 통해 추천을 진행한다. 이와 달리 잠재 요인기반 협업 필터링은 사용자 간의 스페이스와 아이템 스페이스 두 가지를 만들고 이들의 곱을 통해 추천을 진행한다.
잠재 요인기반 협업 필터링은 사용자 매트릭스와 아이템 매트릭스라는 두 가지 행렬을 도입하는 알고리즘이다. 각각의 요인들이 정확하게 무엇을 의미하는지 모르기 때문에 ‘잠재 요인기반’ 협업 필터링이라고 하며 각 사용자의 latent matrix와 아이템의 latent matrix를 곱했을 때 평점 매트릭스를 복원할 수 있다.
잠재 요인기반 협업 필터링의 원리로는 넷플릭스에서 사용하는 SVD, 가중치를 주는 Weighted 등이 있으나 이번 글에서는 SGD와 ALS에 대해서 다뤄보고자 한다.
(2) SGD의 정의
SGD는 고유값 분해를 통해 행렬을 대각화하는 방식을 의미하며 기존의 평점 매트릭스와 사용자/아이템에 대한 latent matrix의 곱 간의 차이를 최소화하기 위해 나타났다. SGD를 통해 사용자 latent와 아이템 latent를 곱했을 때 평점 매트릭스를 복원하여 실제 평점과의 차이를 줄일 수 있는 U와 V를 찾는 것을 목표로 한다. U를 편미분하면 V에 대한 함수가 도출되고, V를 편미분하면 U에 대한 함수가 도출되며 U와 V가 계속 업데이트되는 과정을 거쳐 알고리즘이 계속 변동하게 된다. 구하는 방법은 위의 예시와 같다.
그리고 SGD에는 Regularization이라는 중요한 방법이 활용된다. 이는 고유값 분해와 같은 행렬을 대각화하는 방법인데, Weight 값에서 Regularization이 없으면 값이 폭발적으로 증가할 우려가 있어 이를 방지하기 위해 각각 크기의 제곱인 Regularization term을 더해준다.
(3) SGD의 장점과 단점
장점
매우 유연하며, 딥러닝의 모든 장점을 두루 가지고 있다.
단점
수렴 속도가 느리다. 다만, 좋은 딥러닝을 쓰면 수렴 속도가 어느정도 회복되며 parallelized로 분석해서 쓰면 더 빠르다고 알려져 있다.
(4) ALS의 정의와 알고리즘의 특징
ALS는 두 행렬 중 하나를 고정하고 다른 하나의 행렬을 순차적으로 반복하면서 최적화하는 방법이다. 하나의 latent를 고정하기 때문에 다른 하나는 무조건 convex한 형태이고, 무조건 행렬에 수렴한다는 것을 알 수 있다.
사용자를 고정하고 업데이트하기 때문에 아이템의 행렬을 고정하고 사용자의 행렬을 최적화하거나, 사용자의 행렬을 고정하고 아이템의 행렬을 최적화하는 방식을 반복해서 진행한다. 이 과정에서 식이 모두 convex의 형태로 바뀌기 때문에 수렴된 행렬의 정답을 찾을 수 있다. 특히 앞에서는 ?를 모두 비우고 계산을 진행했으나 ALS에서는 ?를 모두 0으로 처리하고 학습한다는 특이점이 있고 고정시키는 부분 외에는 SGD와 크게 다르지 않은 방식이다.
SGD 코드
import numpy as np
from tqdm import tqdm_notebook as tqdm
import numpy as np
# Base code : https://yamalab.tistory.com/92
class MatrixFactorization():
def __init__(self, R, k, learning_rate, reg_param, epochs, verbose=False):
"""
:param R: rating matrix
:param k: latent parameter
:param learning_rate: alpha on weight update
:param reg_param: beta on weight update
:param epochs: training epochs
:param verbose: print status
"""
self._R = R # 평점 행렬
self._num_users, self._num_items = R.shape
self._k = k # user latent와 item latent의 차원 수
self._learning_rate = learning_rate # 학습률
self._reg_param = reg_param # weight의 regularization 값
self._epochs = epochs # 전체 학습 횟수
self._verbose = verbose # 학습 과정을 출력할지 여부
def fit(self):
"""
training Matrix Factorization : Update matrix latent weight and bias
참고: self._b에 대한 설명
- global bias: input R에서 평가가 매겨진 rating의 평균값을 global bias로 사용
- 정규화 기능. 최종 rating에 음수가 들어가는 것 대신 latent feature에 음수가 포함되도록 해줌.
:return: training_process
"""
# latent matrix 초기화
self._P = np.random.normal(size=(self._num_users, self._k))
self._Q = np.random.normal(size=(self._num_items, self._k))
# bias 초기화
self._b_P = np.zeros(self._num_users)
self._b_Q = np.zeros(self._num_items)
self._b = np.mean(self._R[np.where(self._R != 0)])
# train while epochs
self._training_process = []
for epoch in range(self._epochs):
# rating이 존재하는 index를 기준으로 training
xi, yi = self._R.nonzero()
for i, j in zip(xi, yi):
self.gradient_descent(i, j, self._R[i, j])
cost = self.cost()
self._training_process.append((epoch, cost)) # epoch와 cost를 저장하는 부분
# print status
if self._verbose == True and ((epoch + 1) % 10 == 0):
print("Iteration: %d ; cost = %.4f" % (epoch + 1, cost))
def cost(self):
"""
compute root mean square error
:return: rmse cost
"""
# xi, yi: R[xi, yi]는 nonzero인 value를 의미한다.
# 참고: http://codepractice.tistory.com/90
xi, yi = self._R.nonzero()
# predicted = self.get_complete_matrix()
cost = 0
for x, y in zip(xi, yi):
cost += pow(self._R[x, y] - self.get_prediction(x, y), 2)
return np.sqrt(cost/len(xi))
def gradient(self, error, i, j):
"""
gradient of latent feature for GD
:param error: rating - prediction error
:param i: user index
:param j: item index
:return: gradient of latent feature tuple
"""
dp = (error * self._Q[j, :]) - (self._reg_param * self._P[i, :])
dq = (error * self._P[i, :]) - (self._reg_param * self._Q[j, :])
return dp, dq
# user latent matrix와 item latent matrix의 곱을 통해 평점 행렬의 값을 생성
def gradient_descent(self, i, j, rating):
"""
graident descent function
:param i: user index of matrix
:param j: item index of matrix
:param rating: rating of (i,j)
"""
# get error
prediction = self.get_prediction(i, j)
error = rating - prediction
# update biases
self._b_P[i] += self._learning_rate * (error - self._reg_param * self._b_P[i])
self._b_Q[j] += self._learning_rate * (error - self._reg_param * self._b_Q[j])
# update latent feature
dp, dq = self.gradient(error, i, j)
self._P[i, :] += self._learning_rate * dp
self._Q[j, :] += self._learning_rate * dq
def get_prediction(self, i, j):
"""
get predicted rating: user_i, item_j
:return: prediction of r_ij
"""
return self._b + self._b_P[i] + self._b_Q[j] + self._P[i, :].dot(self._Q[j, :].T)
def get_complete_matrix(self):
"""
computer complete matrix PXQ + P.bias + Q.bias + global bias
- PXQ 행렬에 b_P[:, np.newaxis]를 더하는 것은 각 열마다 bias를 더해주는 것
- b_Q[np.newaxis:, ]를 더하는 것은 각 행마다 bias를 더해주는 것
- b를 더하는 것은 각 element마다 bias를 더해주는 것
- newaxis: 차원을 추가해줌. 1차원인 Latent들로 2차원의 R에 행/열 단위 연산을 해주기위해 차원을 추가하는 것.
:return: complete matrix R^
"""
return self._b + self._b_P[:, np.newaxis] + self._b_Q[np.newaxis:, ] + self._P.dot(self._Q.T)
# run example
if __name__ == "__main__":
# rating matrix - User X Item : (7 X 5)
R = np.array([
[1, 0, 0, 1, 3],
[2, 0, 3, 1, 1],
[1, 2, 0, 5, 0],
[1, 0, 0, 4, 4],
[2, 1, 5, 4, 0],
[5, 1, 5, 4, 0],
[0, 0, 0, 1, 0],
])
# P, Q is (7 X k), (k X 5) matrix
Python
복사
%%time
factorizer = MatrixFactorization(R, k=3, learning_rate=0.01, reg_param=0.01, epochs=100, verbose=True)
factorizer.fit()
Python
복사
factorizer.get_complete_matrix()
Python
복사