들어가며
LINE Timeline에 필요한 머신 러닝과 추천 모델을 만들고 있는 Data Science Dev의 조영인입니다. 1편과 2편에 이어 이번 3편에서는 Discover에 진입한 사용자에게 보여줄 콘텐츠를 어떻게 선정하고, 어떤 순서로 배치하는지에 대해 설명하겠습니다.
Discover 추천
Discover에 방문하는 사용자들이 Discover에 더 오래 머물면서 많은 포스트를 소비하고 다음에 또 방문하게 만드는 데에는 '추천'이 매우 중요한 역할을 합니다.
포스트 풀(post pool)
먼저, LINE Timeline의 수많은 공개 포스트 중에서 Discover 사용자에게 추천할 만한 후보군을 생성합니다. 이 후보군을 포스트 풀이라고 부르는데요. Discover 서비스의 특성상 시각화가 가능해야 하기 때문에 이미지나 비디오를 포함하고 있는 공개 포스트만을 대상으로 합니다. Discover에 진입한 사용자가 거부감 없이 소비할 수 있을 만한 포스트들을 보여주기 위해서, LINE Timeline에서 일정 횟수 이상 '좋아요'를 받은 포스트를 포스트 풀에 포함시키고, 2편에서 소개한 추천 콘텐츠 필터링 과정을 거치면서 콘텐츠의 품질을 다시 한 번 검증합니다. 서비스를 원활하게 운영하기 위해 특정 사용자나 포스트를 포스트 풀에 포함시키거나 제외하기도 합니다. 이런 과정을 통해 하루에 한 번 포스트 풀을 생성하며 포스트 풀에 포함되는 포스트만 사용자에게 전달됩니다.
임베딩(embeddings)
사용자에게 적절한 콘텐츠를 추천하기 위해 포스트의 특성을 벡터로 표현하는 임베딩 과정이 필요합니다. 포스트 풀에 속해 있는 포스트의 정보를 추천 작업을 위한 연산에 용이한 형태로 만드는 과정입니다. Discover 추천에 사용하고 있는 임베딩은 포스트에 포함되어 있는 콘텐츠의 정보(이미지, 비디오, 텍스트)에 대한 임베딩과 포스트를 클릭하는 사용자의 행동 패턴에 기반한 임베딩입니다. 현재 사용자 행동 기반 임베딩과 텍스트 임베딩은 포스트 풀에 속한 포스트에 대해서만 따로 생성해서 사용하고 있습니다.
텍스트
사용자가 작성한 포스트의 텍스트를 토크나이즈(tokenize)해서 작은 단위의 단어로 쪼개고 각 단어의 임베딩 벡터를 구한 후, 포스트에 등장하는 단어들의 임베딩 벡터들의 평균값을 포스트의 텍스트 임베딩 벡터로 정의했습니다. 단어에 대한 임베딩 벡터를 얻기 위해 Facebook에서 공개한 fastText를 사용하고 있습니다. 157개의 언어에 대해 common crawl과 위키피디아 데이터를 이용해서 fastText 모델을 학습한 결과인 단어들의 임베딩 벡터를 얻어올 수 있습니다. 각 언어마다 2백만 개의 단어가 300차원의 벡터로 표현됩니다. 포스트가 어떤 언어로 작성되었는지를 탐지하여 그 언어에 맞는 단어 벡터를 찾을 수 있도록 만들었습니다.
이런 방식으로 사전에 훈련시킨(pre-trained) 모델을 사용하면서 학습에 드는 리소스를 절약할 수 있었지만, LINE Timeline 데이터를 잘 설명하기에는 조금 부족하다는 느낌을 받았습니다. LINE Timeline 포스트는 격식을 갖춘 문서가 아닌 개인의 메모나 노트 혹은 친구들과의 대화 같은 성격이 짙어서 구어체나 신조어 등이 자주 등장하는데요. 기존 텍스트 임베딩은 이러한 단어들의 임베딩을 만들어 내지 못하기 때문입니다. 이런 한계를 극복하기 위해 전처리 방식의 고도화와 더불어 사전에 훈련시킨 fastText 모델에 LINE Timeline 포스트의 텍스트 데이터를 추가로 학습하는 방법을 시도해 보았습니다. 또한 포스트에 등장한 단어들에 대한 임베딩의 평균값을 포스트의 텍스트 임베딩으로 사용하던 기존 방식을, 단어의 등장 횟수와 품사를 고려한 가중 평균으로 개선했습니다. 그 결과, 임베딩을 생성할 수 있는 단어의 수가 늘어나고 단어와 텍스트의 임베딩 벡터가 더 정확해지면서 오프라인 평가와 온라인 평가에서 모두 향상된 성능을 보였습니다.
이미지와 비디오
각 포스트의 대표 이미지 혹은 대표 비디오에 대한 임베딩 벡터를 포스트의 임베딩 벡터로 삼고 있습니다. 이미지 임베딩 벡터는 Google의 이미지 분류 모델인 'Inception V3'의 마지막 히든 레이어(분류(classification) 레이어의 바로 전 레이어)에 PCA(Principal Component Analysis, 주성분 분석)를 적용한 1024차원 벡터를 사용하고 있습니다. 비디오에서 2초마다 하나씩 최대 150개의 프레임을 추출해서 각 프레임 이미지의 임베딩 벡터를 구한 후, 이 벡터들의 평균값을 비디오의 임베딩 벡터로 사용합니다. 새로운 포스트가 생성될 때마다 포스트가 담고 있는 이미지와 비디오에 대한 정보를 벡터로 만들어서 저장하고 있으며, 포스트 풀에 속해 있는 포스트에 대한 임베딩만 따로 저장하는 방식으로 임베딩을 수급하고 있습니다.
사용자 행동 기반
사용자의 행동에 기반한 임베딩 벡터를 구하기 위해 'Word2vec(이하 w2v)' 모델을 활용하고 있습니다. w2v 모델은 NLP(Natural language processing) 분야에서 문서 내 단어들의 시퀀스 구성 관계를 통해 단어의 임베딩 벡터를 학습하는 모델이지만, 최근에는 추천 분야에서도 많이 사용하고 있습니다. NLP용 w2v의 입력 데이터인 단어들의 시퀀스 대신, 사용자가 하루 동안 클릭한 포스트의 저자에 대한 시퀀스를 w2v 알고리즘을 통해 학습함으로써 포스트 저자(작성자)의 임베딩 벡터를 얻을 수 있습니다. 각 사용자별로 과거 30일 동안의 클릭 로그를 하루 단위로 나눠서 문장을 생성했습니다. 즉, 사용자 한 명당 최대 30개의 문장을 만들게 됩니다.
위 임베딩들을 기반으로 추천 로직을 구현하기 때문에 임베딩을 생성할 수 있는 콘텐츠라면 전부 Discover에서 추천할 수 있습니다. 앞으로 LINE Timeline의 포스트뿐만 아니라 그 외 LINE 내 다양한 콘텐츠를 Discover에서 다룰 수 있기를 기대하고 있습니다.
유사 포스트 탐색
Discover에서는 사용자의 과거 히스토리와 유사한 포스트를 찾아서 메인 페이지에 보여주는 것과, 사용자가 메인 페이지에서 클릭한 포스트와 유사한 포스트를 찾아서 추천 피드를 구성하는 두 가지 종류의 추천을 제공하고 있습니다. 이 두 가지 추천 모두에서 공통적으로 필요한 것은 유사한 포스트를 찾는 것입니다. 이를 위해 추천 후보군에 속하는 포스트 각각에 대해 유사한 포스트를 미리 찾아 두는 작업을 진행합니다. 이때 추천 후보군에 속하는 모든 포스트들에 대해서 유사한 포스트들을 찾는데요. 이 과정을 하나의 포스트에 대한 과정으로 간략하게 설명하겠습니다.
추천 후보군에 속하는 포스트 A와 유사한 포스트 1000개를 찾는 상황을 가정해 봅시다. A를 쿼리 포스트라고 부르겠습니다. 앞서 언급한 대로 각 포스트별로 최대 네 종류(텍스트, 이미지, 비디오, w2v)의 임베딩이 포함되어 있습니다. '최대'라고 표현한 이유는 이미지만 있고 비디오는 없는 포스트라면 비디오 임베딩은 존재하지 않을 것이고, 사용자가 어떤 글도 작성하지 않고 올린 포스트라면 텍스트 임베딩이 존재하지 않을 것이기 때문입니다. 우선 A의 임베딩 벡터와 코사인 유사도(cosine similarity)가 높은 1000개의 포스트를 찾습니다. 제한된 추천 풀에서 유사한 포스트를 찾을 때 쿼리 포스트와 유사한 정도, 즉 각 포스트 간의 '유사도 순위'가 중요한 역할을 한다고 판단했기 때문입니다. A는 네 종류의 임베딩을 갖고 있으니 각 임베딩별로 1000개씩 유사 포스트를 찾게 됩니다. 이제 이렇게 찾은 최대 4000개의 유사 포스트를 하나의 기준으로 정렬하는 과정이 필요합니다. 그런데 이미지가 매우 유사하지만 텍스트가 다른 두 포스트를 어느 정도로 유사하다고 말할 수 있을까요? 성질이 다른 네 종류의 유사도 순위 목록을 조합해 하나로 합치기 위해 RRF(Reciprocal Rank Fusion)를 도입했습니다. RRF는 아주 간단하지만 성능이 좋은 비지도(unsupervised) learning-to-rank 방법으로, 정보 검색(information retrieval) 분야에서 검색어를 입력했을 때 속성별로 다른 기준에 따라 검색되는 여러 개의 문서를 최종적으로 어떤 순서로 조합하여 사용자에게 보여줄지를 결정하는 방법입니다. 저희의 경우에선 문서=포스트, 문서의 순위를 결정하는 속성=임베딩 벡터의 종류가 됩니다. 텍스트와 이미지, 비디오, 저자의 특성까지 각 속성별로 유사한 포스트를 찾고, 사용자에게 보여줄 최종 순위를 RRF 점수를 통해 결정하는 것입니다.
위와 같은 간단한 계산식을 이용해 다양한 기준으로 선정된 순위들을 더 나은 하나의 순위로 병합할 수 있습니다. 임베딩 종류별 유사도 순위를 각 정보 검색 시스템에서 얻은 문서의 랭킹이라고 본다면, 이 RRF를 통해서 쿼리 포스트 A와 유사한 포스트들의 순위를 결합하고 병합하는 과정을 쉽게 이해할 수 있습니다. RRF를 통해서 4000개의 포스트의 순위가 결정됩니다. 쿼리 포스트 A와 유사한 포스트를 찾은 결과가 아래와 같다고 가정하겠습니다. 괄호 안의 내용은 {포스트 a: 추천 풀 내의 모든 포스트 중 A와의 코사인 유사도 순위}를 뜻합니다.
- 텍스트 임베딩: {a:1, b:2, c:3, … , N: 1000}
- 이미지 임베딩: {a:1, b:2, x:3, … , M: 1000}
- 비디오 임베딩: {b:1, a:2, y:3, … , I : 1000}
- w2v 임베딩: {c:1, a:2, z:3, … , b:100, … , J:1000}
쿼리 포스트 A에 대해 포스트 a와 b의 RRF 점수는 아래와 같습니다.
- a = 1 / (k+1) + 1 / (k+1) + 1 / (k+2) + 1 / (k+2)
- b = 1 / (k+2) + 1 / (k+2) + 1 / (k+1) + 1 / (k+100)
원래 k는 여러 시스템의 순위를 병합할 때 높은 순위의 문서에 어드밴티지를 얼마나 부여할지 혹은 낮은 순위의 문서에 패널티를 얼마나 줄지 결정하는 상수값이지만, 저희는 임베딩의 종류별로 k값을 상수가 아닌 변수로 다루고 있습니다. k값에 따라 어떤 종류의 임베딩에 더 가중치를 높게 줄 것인지를 결정할 수 있기 때문입니다.
포스트 풀에 속한 모든 포스트에 대해 모든 조합의 포스트 쌍을 대상으로 코사인 유사도를 계산해서 각 쿼리 포스트에 대해 코사인 유사도 순으로 정렬된 유사 포스트 리스트를 만드는 작업은, 추천 모델 내에서 진행하기에는 시간과 자원이 너무 많이 필요한 작업입니다. 또한, 이어서 소개할 세 종류의 추천 모델 모두 이 결과물을 공통으로 사용하기 때문에 포스트 풀이 생성되는 시점에 미리 유사 포스트를 만드는 작업을 진행하고 있습니다.
추천 모델
Discover에서 제공하고 있는 추천은 Timeline 사용자가 그동안 미처 발견하지 못했던 자신의 선호에 맞는 포스트를 찾게 해주거나, 사용자가 선호하는 포스트와 유사한 포스트를 더 많이 볼 수 있는 기회를 제공하는 것을 목표로 하고 있습니다.
Discover 메인 페이지
Discover에 진입했을 때 제일 먼저 나타나는 메인 페이지는 사용자별 개인화 추천이 제공되는 페이지입니다. 사용자가 Discover와 Timeline에서 클릭한 포스트와 유사한 포스트를 포스트 풀에서 찾아서 보여줍니다. Discover나 Timeline에서 클릭한 포스트가 하나도 없다면 개인화 추천은 불가능한데요. 이런 경우에는 사용자의 성별을 예측해서 해당 성별에서 인기를 끌고 있는 포스트를 보여주고 있습니다. 만약 성별도 예측할 수 없는 사용자라면 전체 인기 포스트를 보여줍니다.
사용자별 개인화 추천 로직에 대해 조금 더 자세히 설명하겠습니다. 사용자별 개인화 추천 로직에서는 최근 28일 간의 사용자의 Discover와 Timeline 클릭 히스토리를 참고합니다. 이때 라이크(like)는 일반 클릭보다 더 명시적인 관심의 표현이기 때문에 다른 클릭과 구분해서 사용하고 있는데요. 이번 글에선 편의상 클릭 히스토리로 통칭하겠습니다. 현재 각 사용자별로 28일 간의 클릭 히스토리를 Redis 클러스터에 저장하고 있으며, 이 결과를 모델에서 읽어 사용하고 있습니다. 이 클릭 히스토리 정보를 토대로 최대 20개까지의 시드 포스트(개인화 추천을 위해 참고할 포스트)를 지정하는데요. Discover에서 클릭된 포스트를 우선적으로 시드 포스트로 지정하고, 만약 Discover의 클릭 히스토리에서 20개의 시드 포스트를 얻지 못했다면 Timeline의 클릭 히스토리를 참고하고 있습니다. 이런 방식으로 20개의 시드 포스트를 모두 찾아낸 뒤에도, 메인 페이지에 노출할 때는 사용자가 조금이라도 새로운 취향을 발견할 수 있도록 화면의 5%를 글로벌 인기 포스트로 채웁니다. 만약 Discover와 Timeline의 히스토리를 다 참조해도 시드 포스트의 개수가 10개 미만이라면, 이 히스토리가 사용자의 취향을 충분히 설명하기에는 부족하다고 판단하고 시드 포스트의 개수에 반비례하게 인기 포스트의 비율을 늘려주는 방식으로 개인화 추천과 인기 추천의 밸런스를 맞추고 있습니다. 시드 포스트를 찾은 뒤에는 각 시드 포스트와 유사한 포스트를 50개씩 가져옵니다. 여기서 유사한 포스트란 앞서 소개한 방식에 따라 유사도 순으로 정렬한 포스트 목록의 상위 50개를 말합니다. 그리고 시드 포스트별 유사 포스트를 하나의 추천 목록으로 만들기 위해 다시 한 번 RRF 점수를 이용합니다. 사용자에게 조금이라도 더 다양한 콘텐츠를 볼 수 있게 하기 위해 유사 포스트의 RRF 점수를 정규화(normalization)하고, 그 값에 비례하게 샘플링하는 방식으로 메인 페이지 추천 목록를 만듭니다.
또한 개인화가 불가능한 사용자들에게 전체 인기 포스트나 성별에 따른 인기 포스트보다 더 나은 추천 결과를 제공하기 위해 AiRS 사용자 임베딩을 사용하고 있습니다. LINE 뉴스 소비 패턴을 기반으로 생성된 AiRS 사용자 임베딩을 클러스터링해 해당 클러스터에 속하는 사용자에게는 클러스터별 인기 포스트를 보여주는 방식입니다. LINE 뉴스 소비 패턴은 성별보다 더 많은 정보를 담고 있기 때문에 Discover나 Timeline에서 활동이 많지 않은 사용자에게도 개인의 성향에 맞춘 포스트를 제공할 수 있습니다.
Timeline의 모듈과 홈탭
Timeline 피드 내의 Explore 모듈과 홈탭에도 개인화 추천을 제공하고 있습니다. 모듈이나 홈탭은 Discover 메인 페이지보다 훨씬 적은 수의 포스트가 노출되기 때문에, 메인 페이지의 추천 결과 중 상위 몇 개만 보여주는 방식으로는 좋은 성능을 내지 못했습니다. 그래서 사용자의 성향에 맞게 추천된 포스트 중에서 전반적으로 CTR(Click Through Ratio)이 높은 포스트를 먼저 보여주기 위해 CTR 기반 확률 분포 모형을 도입했고, 그 결과 기존보다 향상된 추천 성능을 얻을 수 있었습니다.
추천 피드
사용자가 Discover 메인 페이지에서 마음에 드는 콘텐츠를 발견하여 클릭하면 추천 피드로 진입합니다. 추천 피드는 추천 후보군 중에서 사용자가 클릭한 콘텐츠와 유사한 콘텐츠를 보여주는 페이지입니다. 사용자가 클릭한 포스트와 유사한 포스트를 더 보여줘서 사용자가 관심을 가질만 하지만 미처 다 탐색할 수 없었던 콘텐츠를 발견할 수 있게 만듭니다. 추천 피드는 쿼리 포스트의 유사 포스트 목록으로 구성하는데요. 이 유사 포스트 목록은 앞서 설명한 방식으로 만든 목록입니다. 추천 피드가 동일한 저자의 포스트로 도배되는 것을 막기 위해서 이미 등장한 저자의 포스트는 점수를 조금 낮추어서 좀 더 다양한 저자의 포스트를 접할 수 있도록 만들었고, 가능한 최근에 생성된 포스트를 많이 보여주기 위해 추천 목록을 5개씩 한 단위로 두고 각 단위 내에서는 최근에 생성된 포스트 순으로 다시 정렬하는 방법을 통해 유사도 순위를 크게 해치지 않으면서 최신 포스트를 조금 더 보여줄 수 있도록 만들었습니다.
맺으며
지금까지 설명드린 Discover의 추천 방식은 많은 사람들의 고민과 노력으로 만들어졌습니다. 그렇지만 아직 개선할 만한 부분이 많이 남아 있는데요. 사용자에게 조금이라도 더 나은 추천을 제공하기 위해 성능을 개선하기 위한 노력을 지속하고 있습니다.
- 팔로우 관계 그래프를 이용한 추천 - 사용자들 간의 팔로우 관계 그래프를 이용한 추천을 준비하고 있습니다. 팔로우 관계 그래프 기반으로 사용자의 임베딩 벡터를 얻으면 이를 활용해서 다양한 형태의 추천이 가능합니다. 사용자가 팔로우한 계정 간의 관계를 바탕으로 팔로우할 만한 다른 계정을 추천하거나, 사용자가 좋아할 만한 계정의 포스트를 추천할 수도 있습니다.
- 점진적(incremental) 업데이트 - 현재는 일정 배치 간격으로 학습을 진행하고 있는데요. 새로운 사용자가 유입되거나 새로운 포스트가 생성되면 그 즉시 모델에 반영하는 점진적 업데이트를 구현하는 것이 목표입니다. 이를 위해 온라인 w2v이나 온라인 학습이 가능한 랭킹 모델 등을 준비하고 있습니다.
- 카테고리화 - 포스트의 이미지와 비디오, 텍스트를 분석하여 카테고리화하는 작업을 진행하고 있습니다. 이미지와 비디오 분류 모델은 PicCell, 텍스트 분류 작업은 일본과 태국, 대만의 각 로컬 NLP 혹은 ML 팀과 협업해 완성할 예정입니다.
- Milvus - 'Milvus'는 입력된 벡터들간의 유사도를 계산하여 서로 가까운 벡터들을 찾아주는 검색 엔진으로, 유사한 임베딩 벡터를 빠르게 찾기 위한 저희의 목적에 부합하는 오픈 소스입니다. Milvus를 도입하면 유사도 계산과 유사도 순 정렬, 가까운 벡터 찾기 등 Discover 추천에 필요한 벡터 연산의 상당 부분을 빠르게 처리할 수 있게 될 것입니다.