추천 시스템 2 — Matrix Factorization(MF)를 이용한 협력 필터링

김희규
8 min readDec 26, 2021
Photo by Erik Witsoe on Unsplash

이번 글에서는 Matrix Factorization(MF) 기법을 이용한 협력 필터링 기법에 대해서 알아보겠습니다.

User-Item Matrix

먼저 MovieLens100k 데이터셋의 ratings.csv 파일로 아래와 같은 user-item matrix를 만들겠습니다. 이 행렬의 행은 각 유저를 의미하고, 열은 유저가 평가한 영화를 의미하며, 원소의 값은 유저가 영화를 평가했으면 1, 평가하지 않았으면 0입니다. 여기서는 유저가 평가한 영화를 유저가 본 영화, 평가하지 않은 영화를 유저가 보지 않은 영화로 가정하겠습니다. 실제로는 그런 데이터가 아니기 때문에 오류가 있을 것입니다.

DataFrame의 pivot 메서드로 쉽게 만들 수 있고, 이 경우 평가가 없는 값은 NaN이 되는데 fillna(0)으로 0으로 바꿔줍니다. 평가가 있는 경우는 모두 1로 바꿔줍니다. 행렬의 대부분의 값이 0이고 1인 값의 적은데, 이를 희소행렬(Sparse Matrix)라고 부릅니다.

Matrix Factorization(행렬분해)

MF는 User-Item Matrix를 아래와 같이 User Matrix, Item Matrix 2개의 행렬의 곱셈으로 분해하는 것입니다.

이 때 k는 임의의 값입니다. 원본 행렬 M은 두 개의 행렬의 곱으로 나타내지며, User Matrix와 Item Matrix는 각각 k개의 feature를 갖게됩니다. 예를 들어 위 사진에서는 재미, 감동, 티켓값이라는 3개의 feature를 만들었습니다. 유저 행렬에는 각 유저가 재미, 감동, 티켓값 중 어떤 feature를 더 중요하게 여기는지를 수치화하고, 아이템 행렬에서도 영화가 재미, 감동, 티켓값 중 어떤 feature에 적합한지를 수치화한 뒤 유저 행렬과와 아이템 행렬을을 내적해서 원본 행렬을 복원하게 됩니다.

유저의 feature와 영화의 feature가 비슷하다면 영화와 유저가 잘 맞는다는 의미이고, 내적은 1이 될 것이고, 비슷하지 않다면 0이 되겠죠? 각 feature는 User-Item Matrix를 복원하기 위해 자동으로 계산되는 값이기 때문에 실제로 무슨 값인지는 알 수 없습니다. 이 feature들을 latent feature이라고 합니다.

이 때 완벽히 원본과 동일한 결과물을 얻을 수는 없습니다. 이 때 원본 행렬에는 1이 아니었던 값도 1에 가까운 값이 되어있을 수 있습니다. 이 때 그 값은 복원과정에서 “아 이것도 1일 것 같은데…”로 계산된 값이라고 판단할 수 있습니다. 그런 값들을 찾아서 추천해주는것이 바로 MF기법입니다.

위 예시에서는 k 값이 클수록 원본 행렬을 잘 복원하지만 계산량은 늘어나고, k의 값이 작을수록 원본 행렬과의 오차는 커지지만 계산량은 줄어들 것입니다.

빨간 부분이 계산된 값

Singular Value Decomposition(SVD)

SVD(특이값분해)는 행렬분해기법 중 하나로, 계산량이 다른 행렬분해 기법에 비해 적습니다. SVD는 행렬을 U, sigma, V 3개의 행렬로 분해합니다. 이 중 sigma는 대각행렬로, 이 행렬의 값을 특이치라고 부릅니다. 대각원소가 내림차순으로 정렬되어있습니다. 각 원소는 원본 행렬을 복원하는 과정에서 각 latent feature의 중요도가 됩니다. 해당 값이 클 수록, 결과를 복원하는데 미치는 영향도가 커지기 때문입니다.

https://ko.wikipedia.org/wiki/%ED%8A%B9%EC%9E%87%EA%B0%92_%EB%B6%84%ED%95%B4

그렇다면 sigma에서 모든 m개의 feature를 사용할 필요 없이 가장 영향도가 큰 k개의 feature만 사용해도 유사한 원본을 얻을 수 있을 것입니다.

코드로 구현하기

실제 SVD를 코드로 구현해보겠습니다. 전체 코드는 여기에 있습니다. Scipy의 svds 함수를 이용해서 SVD를 수행할 수 있습니다. K 값은 64로 지정했습니다.

복원한 행렬에서, 원본 행렬에서는 1이 아니지만(유저가 본 영화가 아니지만) 1에 가장 가까운 값으로 복원된(유저가 봤을 것 같은) 10개의 영화를 찾아보겠습니다.

먼저 이 유저가 본 영화들입니다. 총 37개인데 일부만 표시했습니다. 스릴러 장르가 많은 걸 알 수 있습니다.

이제 추천할 10개의 영화들입니다

스릴러 장르가 5개나 포함되어있습니다. SVD 사용 과정에서 장르에 대한 정보는 전혀 주지 않았지만 포함되어있는게 흥미롭습니다. 스릴러장르가 많은가? 하고 봤는데 전체의 20%일 뿐입니다.

개선할 점들

SVD는 간단하면서도 효과적인 방법입니다. 하지만 여기서 나온 내용들로 실제 추천 시스템을 구현하기 위해선 몇가지 과제들이 남아있습니다. 간단하게 문제점들에 대해서 알아보고 다음 글에서 이 문제들을 해결하는 방법에 대해서 알아보겠습니다.

정말 사용자가 좋아하는 영화인가…?

이 글에서는 데이터셋의 한계로 유저가 평가한 영화를 유저가 본 영화, 평가하지 않은 영화를 유저가 보지 않은 영화로 가정했습니다. 실제로는 유저가 평가한 영화 데이터입니다. 그렇다면 4점, 5점을 줘서 긍정적으로 평가한 데이터도 있고 1점, 2점을 주고 부정적으로 평가한 데이터도 있을 것입니다.

즉 실제로 추천한 영화가 사용자가 싫어하는 영화일 수 있다는 것입니다. 즉 우리는 평점 정보를 활용해 ‘유저가 좋아할 만한 영화’를 추천해줄 방법에 대해 고민해봐야합니다. 그럴러면 유저가 이 영화에 평점을 몇 점으로 줄지 예측하는 모델이 필요합니다. 높은 평점을 줄 것 같은 영화를 추천해주는게 더 좋을테니까요.

존재하지 않는 값을 계산하는 방법

그렇다면 SVD로 유저가 아직 평가하지 않은 다른 영화를 몇 점으로 평가할지 예측하려면 어떻게해야 할까요? 유저의 영화 평가를 User-Item Matrix로 만들면 아래와 같은 모양이 될 것입니다.

기존 SVD로는 이런 NaN 값을 처리할 수 없습니다. 이를 해결한게 Simon Funk가 개발한 FunkSVD입니다. Netflix Prize Contest에서 우승하면서 유명해졌다고 합니다. FunkSVD는 다음 글에서 자세히 알아보겠습니다.

새로운 User를 추가하려면?

서비스에서 새로운 사용자가 회원가입을 했고, 내가 영화를 몇 개 이미 봤다고 알려주었다고 가정해보겠습니다. 이 사용자를 위한 추천을 해주려면 SVD로는 어떻게 해야할까요? SVD를 다시 계산해야할까요?

SVD를 다시 계산하고 싶지 않다면 비교적 간단한 방법이 있습니다. 새로운 사용자 벡터를 무작위로 초기화한 후, 기존의 아이템 행렬과 곱해서 신규 사용자의 유저-아이템 행렬을 복원하도록 값을 바꿔가면 됩니다. 이 때 아이템 행렬의 값은 고정하고 사용자 벡터만 바꾼다면 이전에 학습된 다른 사용자 벡터는 그대로 유지하며, 새로운 사용자 벡터만 학습할 수 있습니다.

다른 feature를 추가하고싶다면?

이 글에서는 사용자가 영화를 봤냐 안 봤냐만을 가지고 예측을 했는데, 실제로는 더 효과적인 추천을 위해서 사용자의 다른 정보를 이용할 수 있습니다. 예를 들면 자신이 좋아하는 장르, 연령, 성별, 국가 들이 있습니다. 이런 것들도 활용해서 추천을 하려면 어떻게해야 할까요? SVD로 해결하는 방법도 있고, 딥러닝을 활용하는 방법들도 있는데, 이후 글에서 살펴보겠습니다.

이번 글에서는 MF의 개념과 SVD를 이용한 간단한 추천방법에 대해서 알아봤습니다. 짧고 쉬운 코드로 추천 시스템을 구현할 수 있는게 SVD의 장점이라고 느껴집니다. 다음 글에서는 FunkSVD를 이용해서 ‘유저의 영화 평가를 예측’해보고, 유저가 높게 평가할 만한 영화를 추천해보도록 하겠습니다.

참고

--

--

김희규

나는 최고의 선수다. 나를 최고라고 믿지 않는 사람은 최고가 될 수 없다.