추천 시스템 4 — Basic Retrieval Model

김희규
10 min readJan 7, 2022
Photo by Dollar Gill on Unsplash

이전 글에서는 SVD를 이용해서 추천 시스템을 만들었는데, 이번 글부터는 Tensorflow Recommenders를 이용해서 딥러닝 추천 시스템을 만드는 법에 대해서 작성하겠습니다. 전체적인 내용은 TF Recommenders(TFRS) 튜토리얼을 가져왔고, 제가 이해한 내용들과 저의 생각과 의견들을 추가로 작성했습니다.

2-Stage 컨셉

딥러닝 기반의 추천 시스템을 주로 2단계로 구분됩니다. 검색(Retrieval) 단계와 순위(Ranking) 단계입니다. 검색 단계는 수백만개가 넘는 전체 아이템 중 사용자가 클릭하거나 볼 만한 아이템들을 빠르게 추려내는 과정입니다. 핵심은 정확도보다는 속도이며, 추려진 검색 결과는 추천 단계에서 사용자에게 유용할지 보다 정확하게 판단한 뒤 최종적으로 사용자에게 제공하게 됩니다.

영화 추천을 예로 들면, 검색 단계에서는 먼저 사용자가 볼만한 영화를 전체 영화 중에서 100개정도만 추려낸 뒤, 순위단계에서 100개 영화에 대해 사용자가 평점을 얼마나 줄 지 예측하고 가장 높은 평점을 줄 것 같은 영화들을 사용자에게 추천할 수 있습니다. 이번 글에서는 SVD 글에서 했던 내용과 마찬가지로, MovieLens100k 데이터셋에서, 사용자가 평가한 영화를 `본 영화`로 가정하고 사용자가 볼 것 같은 영화를 추려주는 Retrieval Model을 만들어보겠습니다.

TFRS 로 간단한 Retrieval Model 만들기

전체 코드는 여기 구글 코랩 링크에 있습니다

먼저 tensorflow-recommenders를 설치해줍니다. 이전 글처럼 MovieLens 영화 데이터셋을 사용할건데 tensorflow-datasets 패키지를 이용해서 MovieLens100k 데이터셋을 쓰겠습니다. 여기에는 10만개의 사용자가 평가한 영화 정보와 사용자의 인구통계학적 정보가 추가로 포함되어있습니다.

데이터셋 불러오기

먼저 TFDS로 데이터셋을 불러오고 하나씩만 가져와서 출력해봅니다, ratings에는 유저 정보(ID, 나이, 직업, 우편번호 등)와 평가한 영화 ID(movie_id) 몇 점으로 평가했는지(user_rating)가 있습니다. movies는 영화 ID, 제목이 있고, 최대 4개의 장르 정수 ID(movie_genres)가 있습니다.

이 글에서는 영화 제목과 유저ID만 쓸 것이기 때문에 그것들만 남깁니다. 영화도 영화ID 대신 영화 제목을 ID로 사용합니다.

전체 유저 ID들과 영화 제목들을 가져옵니다. 1664개의 영화와 943명의 유저가 있습니다. 왜 batch로 Dataset을 받아서 concatenate로 연결해서 사용하는걸까 했는데, batch로 받아야 numpy 배열로 값이 리턴되서 텐서를 변환해주는 작업 없이 바로 concatenate와 unique 함수를 사용할 수 있어서 그런 것 같습니다.

모델 만들기

Retrieval 단계에서 사용되는 Two-Tower Model에 대해서 알아보겠습니다. 두 개의 타워란 두 개의 모델을 의미하며, 각 모델은 유저 모델과 아이템 모델입니다. 유저 모델은 유저 정보를 받아서 인코딩 후 임베딩 벡터를 출력하고, 아이템 모델도 아이템 정보를 받아서 인코딩 후 임베딩 벡터를 출력합니다. 이렇게 각 타워(모델)에서 출력된 임베딩 벡터간의 유사도가 사용자의 아이템 선호도와 일치하게 학습하는 것입니다. 이 부분은 SVD에서 유저 행렬과 아이템 행렬의 곱(내적)으로 아이템 선호도를 복원한 것과 동일합니다.

이런 방법을 사용하는 이유는, 내적 연산이 매우 빠르기 때문에 미리 아이템의 벡터들을 저장해 뒀다가, 사용자가 추천을 원할 때 사용자 벡터만 얻어와서 내적하면 빠르게 결과를 계산할 수 있기 때문입니다.

이번 모델은 굉장히 간단합니다. 유저 ID와 영화 제목만을 가지고 32차원 벡터로 인코딩합니다. 유저 ID와 영화 제목 둘 다 현재 문자열이기 때문에, StringLookup 레이어로 정수ID로 매핑합니다. 전체 문자열 사전을 vocabulary로 주면 됩니다. 사전에 없는 문자열은 별도 값(기본 1)로 매핑되기 때문에 총 사전 개수 + 1개의 정수 ID로 매핑됩니다.

이후 임베딩 레이어를 이용해서 32차원의 임베팅 벡터로 변환합니다. 즉 각 영화와 사용자는 32차원의 무작위 임베딩 벡터로 초기화된 후 학습을 통해 적절한 값으로 바뀌어갑니다.

학습 방법

모델에서 나온 유저 벡터와 아이템 임베딩 벡터의 내적이 1에 가깝다면 유저가 아이템을 볼 것 같다고 예측하고, 0에 가깝다면 보지 않을 것 같다고 예측한 것입니다. 보통 학습이라면 사용자가 본 영화(Positive Samples)와 보지 않은 영화(Negative Samples)를 이용해서 모델을 학습할텐데, 여기서는 Positive Samples밖에 없습니다.

이 경우 보지 않은 영화를 모두 Negative Samples로 넣으면 양이 너무 많아질 것입니다. 사용자가 본 영화보다는 보지않은 영화가 훨씬 많기 때문입니다. 네거티브 샘플링을 이용해서 일부만 추리는 방법 등이 있지만, 여기서는 Positive Samples만을 가지고 학습하도록 하겠습니다.

측정 지표(metric)

모델이 학습하기 위해서 필요한 Objective 함수와는 별개로, Accuracy나 F1-score 처럼 모델의 학습 결과를 우리가 알 수 있는 지표가 필요합니다. 여기서는 Top K Accuracy를 사용합니다. 예를 들어 설명해보겠습니다.

사용자가 본 영화 A가 있을 때 이 두 벡터의 내적 결과가 0.8이 나왔다고 가정해봅시다. 다른 영화 B, C, D, E들과도 내적을 했는데, 각각 0.9. 0.7. 0.6 0.5가 나왔습니다. 이 경우 내적 결과가 높은 순으로 정렬하면 영화 A는 2번째가 됩니다.

Top K Accuracy는 결과 상위 K개에 정답이 포함된 비율을 의미합니다. 사용자가 본 영화가 200개가 있고, Top 100 Accuracy가 20%라고 한다면, 사용자들이 영화를 본 확률을 예측했을 때 전체 상위 100개에 실제로 사용자들이 본 200개의 영화가 추려질 확률이 20%라는 의미가 됩니다.

전체 영화 A, B, C, D, E 가운데에서 꼭 A만이 사용자가 본 영화가 아닐 수 있습니다. B, C, D, E 도 사용자가 본 영화로 학습 데이터에 포함됬을 수 있습니다. 하지만 이 경우에는 A를 제외한 나머지는 암시적으로 Negative Sample로 가정하게 됩니다.

TFRS에는 이런 Metric을 계산하는 것과 사용자 임베딩 벡터와 아이템 임베딩 벡터를 가져와서 내적하고 loss를 계산하는 과정을 tfrs.tasks.Retrieval 클래스가 해줍니다.

결합된 모델(Combined Model)

이제 User Model, Item Model, Task 3가지를 결합한 모델을 만듭니다. 입력 데이터에서 유저 id, 영화 제목을 각각 모델에 넣어 인코딩된 벡터를 얻어서 task에 넣은 뒤 loss와 metric를 계산합니다.

이후 3 에폭을 학습합니다.

train set 학습 결과는 아래와 같습니다.

  • factorized_top_k/top_1_categorical_accuracy: 0.0029
  • factorized_top_k/top_5_categorical_accuracy: 0.0213
  • factorized_top_k/top_10_categorical_accuracy: 0.0440
  • factorized_top_k/top_50_categorical_accuracy: 0.1855
  • factorized_top_k/top_100_categorical_accuracy: 0.3135
  • loss: 66302.9609

test set 평가결과는 아래와 같습니다

  • factorized_top_k/top_1_categorical_accuracy’: 0.0007999999797903001
  • factorized_top_k/top_5_categorical_accuracy’: 0.009449999779462814
  • factorized_top_k/top_10_categorical_accuracy’: 0.022350000217556953
  • factorized_top_k/top_50_categorical_accuracy’: 0.124549999833107
  • factorized_top_k/top_100_categorical_accuracy’: 0.23270000517368317
  • loss: 28244.7734375

test set 결과가 train set 결과보다 낮은걸 알 수 있습니다.

학습된 모델을 이용해서 추천해보기

이제 학습된 모델을 이용해서 영화를 추천해보겠습니다. 먼저 TFRS의 BruteForce 클래스를 이용해서 사용자 임베딩 벡터를 전체 영화 임베딩 벡터와 비교해서 Top K를 가져오는 예제를 보겠습니다.

BruteForce 클래스 객체를 생성할 때는 유저 모델을 인자로 줘야 합니다. 이후 index_from_dataset 메서드로 영화 데이터셋과 인코딩된 영화 벡터들을 넘겨줍니다.

이후 query() 메서드 혹은 함수처럼 호출하여 추천할 사용자 데이터들을 주면 사용자 모델을 통해 사용자 벡터로 바뀐 후 비교하여 추천 결과를 반환받습니다.

k=5로 5개의 추천 별과만 받았습니다. 점수와 영화제목들이 반환되는데, 데이터프레임으로 만들어서 표시했습니다. 그러면 이제 100개의 영화를 추천해보고, 학습셋에는 없던 사용자가 평가한 영화가 몇 번째로 추천되는지 보겠습니다. 먼저 학습셋과 테스트셋에서 사용자가 평가한 영화들을 가져옵니다.

테스트셋의 유저 24의 3개의 영화가 TOP 100에 12위, 26위, 65위로 포함됬습니다. 그런데 TOP 100에는 학습셋에 포함되어있던 영화들도 있습니다. 사용자가 이미 평가한 영화는 추천 결과에서 query_with_exclusion 메서드로 제외할 수 있습니다.

3개의 영화 순위가 4, 13, 44위로 올라간 것을 확인할 수 있습니다.

--

--

김희규

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