[핸즈온 추천시스템] Matrix Factorization (MF)란?

행렬분해란 무엇인지 알아보고, python을 활용해 추천시스템을 만들어 보겠습니다.

chrisjune
10 min readJun 14, 2023

용어설명

행렬분해

  • 하나의 행렬을 두개 이상의 행렬의 곱과 같도록 행렬을 분해하는 것을 의미합니다.
  • 보통 행렬분해한 후 분해된 행렬의 일부로 원본 행렬을 추정하도록 사용합니다.
  • 연관된 알고리즘은 PCA, SVD, MF 입니다.

PCA, SVD, MF

  • PCA(Principle Component Analysis) 주성분분석은 행렬의 차원축소로 기억하면 좋습니다. 단, 데이터의 분산을 가장 잘 표현하는 (데이터가 가장 많이 퍼져있는) 축을 정하고 이후에 또 분산을 잘 표현할 수 있고 이전에 정한 축에 직교가 되는 축을 만들어나가는 것을 의미합니다. 차원을 축소한다는 것은 원본 데이터를 변형하는 것인데, 분산이 큰 방향에 정사영해야 원본데이터의 손실이 최소화할 수 있게 됩니다.
  • SVD (Singular Value Decomposition) 특이값분해는 행렬을 3개의 행렬로 분해하는 방법입니다. 분해된 행렬중 특이값으로 원래 행렬을 복원할 수 있습니다. 특이값의 크기에 따라 정보량이 정해지기때문에 몇개의 값만으로도 의미있게 추론이 가능합니다.
  • MF(Matrix Factorization) 행렬 분해는 두개의 행렬로 분해하는 것을 의미하는데, 분해시 차원을 낮추기 때문에 낮아진 차원의 요소들은 원래 행렬의 핵심요소를 설명할 수 있습니다. 또한 희박한 데이터 세트에서도 잘 동작합니다.
  • SVD와 MF는 엄연히 다른 알고리즘이지만, 많이 혼용되어 사용됩니다. 본 포스팅에서는 SVD를 활용하여 행렬 분해를 하여 추천시스템을 만들어보겠습니다.

CF에서는 크게 메모리기반 방법과 모델 기반 방법이 있는데, 그중 대표적인 모델 기반 방법이 MF(Matrix Factorization)이 있습니다.

Kaggle Movie data의 평점정보를 SVD(Singular Value Decomposition)를 이용하여 행렬을 분해하고 다시 행렬곱을 통해 복원하는 과정에서 추천시스템을 만들 수 있습니다. Python과 Scikit library로 연관상품추천(상품으로 상품추천)과 개인화(유저로 상품추천)를 만드는 코드입니다.

연관상품 추천

연관상품 추천은 상품번호로 상품을 추천하는것을 의미합니다.

0.사전작업

# 라이브러리설치
!pip install kaggle pandas numpy matplotlib scikit-learn

# https://www.geeksforgeeks.org/how-to-download-kaggle-datasets-into-jupyter-notebook/
# 링크로 이동하여 1번, 2번 스탭 가이드대로 현재위치에 kaggle.json 파일을 다운로드

# kaggle 폴더 생성
!mkdir ~/.kaggle
!cp ./kaggle.json ~/.kaggle
!chmod 600 ~/.kaggle/kaggle.json

# 데이터셋 다운로드
!kaggle datasets download -d rounakbanik/the-movies-dataset

# 데이터셋 압축해제
!mkdir dataset
!mv ./the-movies-dataset.zip ./dataset
!unzip -d dataset dataset/the-movies-dataset.zipp

1.데이터 전처리

from sklearn.decomposition import TruncatedSVD
from scipy.sparse.linalg import svds

import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import numpy as np

rating_data = pd.read_csv('dataset/ratings_small.csv')
movie_data = pd.read_csv('dataset/movies_metadata.csv')

# 전처리
movie_data.rename(columns={"id": "movieId"},inplace=True)

def is_number(s):
try:
float(s)
return True
except ValueError:
return False

# 영화메타정보와 평점을 합침
movie_data = movie_data[movie_data['movieId'].apply(is_number)]
movie_data['movieId'] = movie_data.movieId.astype(np.int64)
movie_ratings = pd.merge(rating_data,movie_data, on='movieId')

user_movie_data = movie_ratings[['userId','movieId','rating','title']]
user_movie_rating = user_movie_data.pivot_table('rating', index='userId', columns='title').fillna(0)
user_movie_rating

2.학습

movie_user_rating = user_movie_rating.T

# 행렬을 분해함. TruncatedSVD는 SVD의 다른 버전으로 원본 행렬에서 축소된 행렬로 반환해준다.
svd = TruncatedSVD(n_components=50)
matrix = svd.fit_transform(movie_user_rating)

corr = np.corrcoef(matrix)
plt.figure(figsize=(16,10))
sns.heatmap(corr[:200,:200])
corr

여기서는 피어슨 상관계수를 구하여 영화간 유사도를 구하고, 이를 Heatmap으로 표현하면 아래와 같습니다.

heatmap

3.추천

title = user_movie_rating.columns
batman = list(title).index("Batman")
corr_batman = corr[batman]
list(title[(corr_batman)>=0.4])[:10]

개인화추천

개인화 추천은 유저번호로 상품을 추천하는 것을 의미합니다.

1.전처리

rating_data = pd.read_csv('dataset/ratings_small.csv')
user_rating_data = rating_data.pivot(index='userId', columns='movieId', values='rating').fillna(0)
user_rating_data.head()

movies = pd.read_csv('dataset/movies_metadata.csv')
movies.rename(columns={"id": "movieId"},inplace=True)
def is_number(s):
try:
float(s)
return True
except ValueError:
return False
movies = movies[movies['movieId'].apply(is_number)]
movies['movieId'] = movies.movieId.astype(np.int64)

# df -> np array
matrix = user_rating_data.to_numpy()

# 사용자의 평점 Bias를 줄이기 위하여 mean centric 데이터로 변환
matrix_user_mean = matrix - np.mean(matrix, axis=1).reshape(-1, 1)
matrix_user_mean

2.학습

U, S, Vt = svds(matrix_user_mean, k =12)
print(U.shape, S.shape, Vt.shape)
# (671, 12) (12,) (12, 9066)

sigma = np.diag(S)
matrix_user_mean

3.추천

# 분해된 행렬을 다시 내적하여 원본행렬을 추론
predicted_user_rating_data = np.dot(np.dot(U,sigma), Vt) + np.mean(matrix, axis=1).reshape(-1, 1)
user_rating_data_preds = pd.DataFrame(predicted_user_rating_data, columns=user_rating_data.columns)
user_rating_data_preds
user_rating_data
# 개인화 상품추천

user_id=1
sorted_score = user_rating_data_preds.iloc[user_id - 1].sort_values(ascending=False).reset_index(name='score')
top_k = 10
top_k_movie = pd.merge(movies, sorted_score, on='movieId').sort_values(['score'], ascending=False)[:10]
top_k_movie = top_k_movie[["original_title", "score"]]

4.지표평가

from sklearn.metrics import mean_squared_error, mean_absolute_error
rmse = mean_squared_error(user_rating_data, user_rating_data_preds, squared=False)
mae = mean_absolute_error(user_rating_data, user_rating_data_preds)
print(f"rmse:{rmse}, mae:{mae}")
# rmse:0.2802923976281751, mae:0.10278284752271398

지금까지 SVD를 활용하여 원본행렬을 분해하고, 분해된 행렬중 일부요소만으로 원본행렬을 추론하여 추천하는 로직을 구현해보았습니다. 행렬 분해를 이해하는데 조금이나마 도움이 되셨으면 좋겠습니다.

참고자료
https://lsjsj92.tistory.com/569

--

--