콘텐츠 기반 필터링에 PinnerSage 입히기

안녕하세요, 머신러닝 엔지니어 카터입니다. 지난 글에서 공유드린 바와 같이 라이너는 추천 시스템의 여러 꼭지 중 하나로 콘텐츠 기반 필터링을 계속해서 발전시켜 나가고 있습니다. 많은 분들이 이미 알고 계시듯 콘텐츠 기반 필터링은 Cold start 에 강점을 지니며, Sparse 한 사용자-아이템 인터랙션 매트릭스로 인한 추천 모델 학습의 어려움에서 비교적 자유롭다는 장점을 지니고 있습니다. 때문에 수천만개의 후보 문서군을 추천 시스템에서 다루어야 하는 라이너에게 콘텐츠 기반 필터링은 필히 중요하게 다루어져야 할 기술입니다.

이러한 콘텐츠 기반 필터링을 제품에 실제로 “잘” 적용하기 위해서는 수많은 물음에 답을 할 수 있어야 합니다. 이 글에서는 여러 질문 중 사용자의 여러 히스토리 중 어떤 아이템을 기준점으로 선정해 유사 아이템을 추천해 줄 것인지에 대해 답을 해보도록 하겠습니다. 최초의 라이너의 콘텐츠 기반 필터링 추론 로직에서는 사용자가 최근 하이라이트 한 n개 문서를 읽어와 임베딩을 구한 후, 평균을 내어 사용자를 대표하는 벡터로 사용하는 전략을 택했습니다.

사용자가 관심을 가진 서로 다른 세 개의 이미지 벡터를 평균내었을 때 생뚱 맞은 벡터가 나오는 모습

해당 로직에는 명확한 단점이 존재했는데, 서로 다른 문서들의 벡터들을 더한 후 평균을 구하게 됐을 때 위 그림과 같이 사용자가 관심을 가졌던 문서들과 전혀 관련 없는 주제의 벡터가 나올 수 있다는 점이었습니다. 따라서 개선된 추론 로직이 필요하다는 사실을 깨닫게 되었습니다.

위 문제를 해결하기 위해 첫 번째 안으로 kNN 클러스터링을 적용해보았습니다. 간략히 이야기하면 사용자가 최근 하이라이트 한 n개 문서를 임베딩 한 후, 문서 임베딩들에 대해 kNN 클러스터링을 적용해 동일한 클러스터로 묶인 문서 임베딩들의 평균을 구해 평균 벡터를 기준으로 유사 문서를 추출하는 방식이었습니다. 자세한 내용은 링크에서 확인하실 수 있습니다.

kNN 클러스터링의 적용을 통해 당시 발견한 문제를 일부 완화할 수는 있었지만, 여전히 문제가 남아있었습니다. 첫 번째 문제는 동일한 클러스터로 묶인 문서들에 대해 평균을 구한다고 하더라도 여전히 원치 않는 주제의 벡터가 형성될 수 있다는 점입니다. 그리고 두 번째 문제는 클러스터 갯수를 개발자가 직접 지정을 해주어야 하므로, 문서들의 자연스러운 클러스터 형성에 방해를 준다는 점입니다.

그리고 위 문제들을 해결하기 위해 Pinterest가 20년 내놓았던 PinnerSage 논문을 다시 찾게 되었습니다. PinnerSagePinSage로 임베딩을 추출할 수 있는 아이템들이 있을 때 사용자 히스토리 중 “어떤 아이템을 대표 앵커로 선정해 유사 이미지를 추천해줄 것인가”에 대해 다룬 논문입니다. 라이너가 겪고 있던 동일한 문제에 대해 해결책을 기술한 논문이었기 때문에 꼼꼼히 분석해 논문에 나온 내용들을 라이너 추천 시스템에 적용했는데요. 논문을 통해 어떤 문제를 해결할 수 있었고, 추가로 얻을 수 있었던 사이드 이펙트는 무엇이었는지 소개해보겠습니다.

Ward Clustering

먼저, Pinterest는 개발자가 정적으로 클러스터 갯수를 지정하는 방식이 추천 시스템에 바람직하지 않다고 지적합니다. 이러한 제약이 사용자를 올바르게 이해하는데 방해가 되기 때문인데요. 따라서 동적으로 클러스터 갯수가 결정될 수 있는 알고리즘의 필요성을 주장하며, Ward Clustering 을 활용하게 되었다고 이야기합니다.

Ward Clustering 을 간단히 설명하자면, 최초 모든 데이터 포인트를 개별적인 클러스터로 간주하고 반복해서 클러스터를 순회하며, 미리 설정해두었던 Distance Threshold 보다 가까이 위치한 두 클러스터를 하나의 클러스터로 묶어주는 알고리즘입니다. 더 자세한 설명은 동영상에서 확인하실 수 있습니다.

Ward Clustering 은 Centroid를 기준으로 클러스터를 형성하는 kNN 클러스터링과 비교했을 때, 아웃라이어를 배제하는데 더 효과적인 알고리즘으로 알려져있습니다. PinterestWard Clustering 을 적용해 동적으로 클러스터를 형성할 수 있도록 했으며, 라이너도 이를 따라 기존 kNN 클러스터링을 Ward Clustering 으로 변경하게 되었습니다.

Medoid-based Cluster Representation

Ward Clustering 을 통해 사용자가 하이라이트를 한 문서들에 대해 동적 클러스터링이 가능해졌습니다. 이제 클러스터 별로 추천 문서를 받아와야 할텐데 클러스터 내 어떤 문서가 기준이 되어야 할까요? 동일한 클러스터로 묶인 문서들의 임베딩의 평균을 앵커로 사용하면 기존과 동일한 문제가 발생하게 됩니다. 때문에 Pinterest는 클러스터를 대표할 수 있는 데이터 포인트 하나를 선정해 해당 아이템의 유사 아이템을 반환해야 한다고 주장합니다.

즉, 가공된 벡터를 만들어 발생할 수 있는 문제를 피하기 위해 실제로 존재하는 데이터 포인트에 대해 유사 아이템을 추천하자는 주장인 셈이죠. 그렇다면 클러스터 내 어떤 문서를 대표 문서로 설정할 수 있을까요? Pinterest는 클러스터의 Medoid를 대표 문서로 설정하자고 주장합니다.

Medoid란 클러스터 내 모든 데이터 포인트들과의 거리를 재서 더해봤을 때, 그 합이 가장 작은 데이터 포인트입니다. 즉, 클러스터를 구성하는 데이터 포인트들 중 가장 중심부에 가까운 데이터 포인트가 되겠죠. 이제 Ward Clustering 을 통해 구성된 클러스터에서 Medoid를 계산해 대표 문서까지 선정할 수 있게 되었습니다.

+) 간혹 클러스터 내 데이터 포인트가 단 두 개만 존재하는 경우가 발생하기도 합니다. 데이터 포인트가 두 개인 경우, 어느 데이터 포인트에서 거리를 재도 그 합이 동일하기 때문에 Medoid를 선정할 수 없게 됩니다. 라이너는 이 경우, 두 벡터 중 L2 Norm이 더 큰 문서를 Medoid로 임의로 선정하도록 로직을 추가했습니다. L2 Norm 기준을 택한 이유는 두 벡터 중 임의의 벡터가 아닌 표현력이 더 큰 벡터를 선정하는게 더 좋을 것이라 판단했기 때문입니다.

Cluster Importance

이제 사용자 히스토리에 대해 동적으로 클러스터를 형성한 후, 클러스터를 대표할 수 있는 데이터 포인트까지 얻을 수 있게 되었습니다. 그렇다면 문서를 추천 받는 일만 남았네요! 하지만 프로덕션 레벨은 그렇게 녹록치 않습니다. PinterestWard Clustering 을 적용했을 때, 라이트한 사용자에게서는 3-5개의 클러스터가, 헤비한 사용자에게서는 75-100개의 클러스터가 추출된다고 밝혔습니다.

3개 클러스터만 활용해도 충분히 Relevance-Diversity 를 챙길 수 있음을 보여준 그래프

Pinterest 규모에서 한 사용자를 위한 추천 요청이 75-100개로 쪼개어져 수행된다면 시스템에 엄청난 병목이 될 것입니다. 따라서 Pinterest는 얼마나 많은 클러스터가 형성되었든 가장 중요한 3개 클러스터를 선정한 후, 각 클러스터의 Medoid를 추천 시스템의 인풋으로 사용하는 것이 프로덕션 레벨에서 타당하다고 주장했습니다. 3개가 합리적인 이유는 갯수를 더 늘리더라도 DiversityRelevance 가 드라마틱하게 개선되지는 않았기 때문입니다.

그렇다면 수많은 클러스터들 중 중요한 클러스터 n개는 어떻게 결정할 수 있을까요? 바로 위 수식을 통해 결정되게 됩니다. 클러스터를 구성하는 데이터 포인트가 많을수록, 그리고 해당 히스토리가 최근에 일어났을수록 클러스터의 Importance 는 커지게 됩니다. 위 식에서 Lambda의 조정을 통해 FrequencyRecency 의 트레이드 오프를 조정할 수 있으며, Pinterest0.01을 가장 옵티멀한 값이라고 주장합니다.

라이너 역시 Lambda의 조정 실험을 통해 0.01을 최종 값으로 설정했습니다. Pinterest의 경우, 이처럼 구해진 Cluster Importance 를 통해 최대 3개의 Medoid를 선택했다고 하며, 라이너의 경우 최대 5개의 Medoid를 선택해 추천 시스템의 인풋으로 활용하도록 설정했습니다.

Inference Caching

이제 PinterestPinnerSage를 통해 제시한 모든 로직을 프로덕션에 적용할 수 있게 되었습니다. 이제 앞서 언급한 사이드 이펙트에 대해 이야기를 해보고자 하는데요. PinnerSage를 적용하게 되면 프로덕션 레벨에서 컴퓨팅을 크게 줄일 수 있는 부분이 생기게 됩니다.

기존 평균 벡터를 구해 추천 시스템의 인풋으로 사용하는 경우, 시스템에 캐시를 적용하는게 불가능해집니다. 사용자 마다 평균 벡터가 다르게 추출될 것이기 때문에 Hit Ratio 가 0%에 가깝게 기록될 것이기 때문이죠. 그러나 Medoid를 활용하게 된다면 캐시를 손쉽게, 그리고 효율적으로 적용할 수 있게 됩니다.

Medoid는 사용자 마다 발생하는 것이 아닌 독립적인 문서의 단위이기 때문에 Medoid가 된 문서 아이디를 Key 로, ANN (Approximate Nearest Neighborhoods) 추론 결과를 Value 로 설정하게 되면 Hit이 발생할 수 있는 캐시 전략을 구축할 수 있게 됩니다. 특히 특정 문서의 경우 여러 사용자 히스토리에서 Medoid로 선정될 수 있기 때문에 (a.k.a Hot Key) ANN으로 인해 발생하는 컴퓨팅 자원, 레이턴시를 크게 잡을 수 있게 됩니다. 이처럼 라이너는 PinnerSage의 적용을 통해 추론 로직상 정밀함은 물론, 컴퓨팅 자원에서의 이점까지 챙길 수 있게 되었습니다.

결과

이제 PinnerSage를 적용한 라이너 추천 시스템의 결과를 예시와 함께 간략히 소개하겠습니다.

PinnerSage를 적용한 결과, 확실히 평균 벡터를 사용했을 때 관측되었던 “입력 값과 결이 다른 문서가 추천”되지 않는 모습을 보여주었습니다. 따라서 저희가 해결하고자 했던 평균 벡터로 인해 사용자와 전혀 관련 없는 추천 경험을 제공해주는 빈도를 줄일 수 있게 되었습니다.

또한 Cluster Importance 를 구하는 과정, Medoid를 추출하는 과정 등이 추가되어 최초 호출에서 발생하는 지연 시간은 소폭 상승했지만, 이후 캐시 전략이 적용되어 반복적인 호출 시에는 지연 시간에 있어 큰 이득을 취할 수 있게 되었습니다.

마치며…

현재 시스템에 안주하지 않고, 지속적으로 문제를 파악하고 개선해나가는 일은 정말 흥미로운 일인 것 같습니다. PinnerSage를 적용한 현재 시점에서는 이제 뛰어난 임베딩 모델을 얻는게 가장 중요해지게 되었습니다. Representation 을 잘 추출해주어야 클러스터링이 잘 적용될 수 있고, Medoid 역시 클러스터를 대표할 수 있는 데이터 포인트로 잘 선정될 수 있기 때문입니다.

때문에 현재 저는 다시 임베딩을 잘 추출해줄 수 있는 모델링을 하는데 큰 관심을 가지고 연구 개발에 임하고 있습니다. 스타트업에서 일을 하는 매력이 바로 이런데 있는게 아닐까요? 자신이 직접 문제를 정의하고 해결한 후, 새로운 문제를 발견하는 Iterative Process 를 지속하며 성취감을 느낄 수 있다는게 제가 생각하는 스타트업의 가장 큰 매력인 것 같습니다.

최근 라이너는 문서 임베딩을 잘 추출해줄 수 있는 콘텐츠 모델링과 해당 Pretrained Embedding + 그래프 구조를 활용할 수 있는 그래프 기반 추천 시스템, 검색 엔진에 큰 관심을 가지고 있습니다. 그리고 Search & Discovery 기술을 통해 사용자들이 더 빠르게 똑똑해질 수 있도록 돕는 플랫폼을 만들기 위해 기술적으로 열심히 고민하고, 새로운 노하우들을 쌓아 나가고 있는 라이너에서 머신러닝 기술을 함께 발전시켜나가실 분들을 열렬히 모시고 있어요!

가볍게 이야기하는 자리도 좋으니, 언제든 편하게 연락주시면 좋겠습니다. 긴 글 읽어주셔서 감사드리며, 추천 시스템을 개발하시는 분들께 도움이 되셨으면 좋겠습니다.

👉 라이너 채용 페이지 바로가기