카카오 테크 — 세그먼트 추천을 GDCN + MoE 개인화 랭커로 바꾼 기록

카카오 테크 — 세그먼트 추천을 GDCN + MoE 개인화 랭커로 바꾼 기록

2026, Sep 26    

카카오 기술 블로그에 9월 23일 올라온 「개인화 추천을 위한 랭킹 모델 개발기」를 읽고 정리했다. 성별·연령 세그먼트 단위로 똑같은 결과를 내려주던 추천을, 유저 개인의 시청 히스토리를 반영하는 딥러닝 랭커로 교체한 프로젝트다. 아키텍처와 라벨 설계, 세 번의 모델 실험, 배포 뒤 지표까지 한 흐름으로 공개했다. (이 글의 초안은 설치된 Hermes 에이전트가 작성했고, Claude가 검수 후 발행했다.)

TL;DR: 라벨은 시청시간에 Root-Log 변환(RLTW)을 적용해 영상 길이 편향을 줄였다. 모델은 DCNv2 → DCNv2 + MoE → GDCN + MoE 순서로 세 번 실험한 끝에 확정했다. MoE는 라이트/헤비 유저 사이의 예측력 편차를 메우려고, GDCN은 임베딩 분포가 바뀔 때 DCNv2가 흔들리는 문제를 잡으려고 넣었다. 배포 후 VTR은 +10%p(+20%) 올랐고 신규부터 헤비까지 모든 활동성 구간에서 +7~8%p씩 고르게 개선됐다. 하루 평균 추천에 쓰인 서로 다른 아이템 수도 +53.7% 늘었다. 피처 중요도 1위는 유저×크리에이터 상호작용이었다.

세그먼트 추천이 놓친 것

기존 로직은 성별·연령 같은 세그먼트로 유저를 묶고 묶음마다 같은 추천을 내려줬다. 이렇게 하면 방금 무엇을 보고 좋아요를 눌렀는지, 어떤 크리에이터를 구독하는지 같은 개인 신호가 들어갈 자리가 없다.

원문 근거
"세그먼트는 유저를 거칠게 나눈 대표값일 뿐이라, 개인이 방금 무엇을 보고 좋아요를 눌렀는지·어떤 크리에이터를 구독하는지 같은 개인화 시그널은 전혀 반영하지 못했습니다."

랭킹 시스템 구조

시스템은 네 컴포넌트로 이뤄진다.

컴포넌트 역할
피처 파이프라인 모델 학습/예측용 피처 생성
학습 파이프라인 모델 학습
GPU 추론 서버 모델 추론
서빙 API 서버 후보 생성·리랭킹 등 비즈니스 로직

실시간 스트리밍은 방금 일어난 유저 행동을 곧바로 반영한다. 배치는 Hadoop에 쌓인 로그를 정제해 Feature Store와 학습 데이터셋을 만들고 여기서 나온 유저·아이템 피처와 Two-Tower 임베딩을 MongoDB와 Redis에 올려 실시간으로 조회할 수 있게 한다. 요청이 들어오면 서빙 API 서버가 여러 Retriever와 Filter로 후보를 추리고 랭킹 모델로 점수를 매긴다. 마지막으로 리랭커가 최신성과 크리에이터 다양성을 보정해 추천 리스트를 완성한다.

학습 피처는 4개 base_type(user/item/author/user_author)에 3개 윈도우(1d/7d/30d)를 곱한 조합이다. 모델은 하루 한 번 배치로 학습한다. 학습 주기를 더 당겨 봐도 성능이 크게 달라지지 않아서 그대로 뒀다고 한다. 추론 쪽은 모든 피처를 매번 갱신하면 쓰기 부하가 커지므로 변경이 감지된 유저·아이템만 온라인 DB에 증분 반영한다. 추론 시점에는 300개로 좁힌 후보의 피처를 읽어 벡터화한 뒤 모델에 넣는다.

라벨: RLTW로 영상 길이 편향 줄이기

라벨은 시청시간(watch length)이다. 그런데 긴 영상일수록 시청시간도 자연히 길어지는 편향(video length bias)이 있다. 그래서 RLTW(Root-Log Transformed Watch time)를 적용했다.

RLTW = √(ln(1 + watch_length))

로그로 큰 값의 스케일을 한 번 누르고 제곱근으로 한 번 더 완만하게 만든다. 같은 이름의 변환은 ShareChat 연구진의 숏폼 시청 신호 논문에서 제안된 바 있다(참고 자료).

세 번의 실험으로 GDCN + MoE에 도달하기까지

실험마다 기준은 같았다. 모델이 재정렬한 결과가 기존 노출 순서(imp_ordnum)보다 평균 시청시간과 평균 시청 비율을 얼마나 끌어올리는지 봤다.

실험 아키텍처 평균 시청 시간 평균 시청 비율
1차 DCNv2 +28.6% +3.5%
2차 DCNv2 + MoE +32.9% +5.5%
3차 GDCN + MoE +35.0% +6.5%

1차, DCNv2 베이스라인. 목표는 정확도가 아니라 인사이트였다. 경량화를 위해 weight를 low-rank로 분리한 DCNv2를 쓰면서 세 가지를 확인했다. 첫째, 장기 피처가 통했다. 라이트 유저가 많은 도메인이라 7일 윈도우로 보면 User×Author 상호작용이 있는 쌍이 10%도 안 됐는데, 장기 피처를 넣자 추론 성능이 올랐다. 둘째, weight를 rank 32로 분리해도 성능이 거의 떨어지지 않았다. 이 rank 32는 3차의 GDCN까지 그대로 이어졌다. 셋째, 학습이 하루이틀 늦어지는 건 괜찮았지만 3일을 넘기면 성능이 눈에 띄게 떨어졌다.

2차, MoE 추가. 1차에서는 라이트 유저의 예측력이 헤비 유저보다 확연히 낮았다. 활동 패턴이 제각각인데 하나의 네트워크가 모두를 똑같이 학습한 탓으로 보고 Single-gate 3-Experts 구성의 MoE를 붙였다.

3차, DCNv2에서 GDCN으로. Two-Tower 임베딩 버전을 바꾸자 DCNv2 성능이 크게 흔들렸다. DCNv2는 모든 피처를 같은 비중으로 교차시키기 때문에 피처 분포가 바뀌면 그만큼 민감하게 반응한다. 그래서 어떤 피처를 얼마나 반영할지 information gate로 고르는 GDCN(Gated DCN)으로 바꿨다. 지연시간 목표를 맞추려고 레이어 수와 임베딩 차원도 함께 줄였다.

원문 근거
"원인을 파고들어보니 DCNv2는 모든 피처를 동일한 비중으로 교차시키는 구조라, 피처 분포가 바뀌면 그만큼 민감하게 반응한다는 게 문제였습니다."

원문에는 GDCN CrossNet과 MoE 출력의 의사코드도 실려 있다. Cross Layer는 low-rank 교차항(rank 32)에 sigmoid gate를 곱하고 residual을 더하는 식이고 MoE는 softmax gate가 Dense 128짜리 expert 3개의 출력을 섞는다.

후보 개수는 300개로

구조를 정한 뒤에는 최종 랭킹에 태울 후보 수를 조정했다. 유저 2만 명을 대상으로 후보 수를 늘려 가며 최종 6개를 뽑는 오프라인 테스트를 했다.

후보 개수 VTR 300개 대비
300 78.86% -
600 79.60% +0.73%p
900 79.77% +0.90%p

900개까지 늘려도 +0.9%p에 그쳤다. 이 정도 이득으로는 늘어나는 지연시간을 감당할 이유가 없다고 보고 300개로 운영하면서 실서비스 모니터링으로 조정하기로 했다.

입력 피처와 중요도

온라인/컨텍스트 피처는 요청 순간에 채워진다. 요일·시간대, 노출 위치, 세션 내 조회수, 후보를 뽑아 온 retriever 종류가 여기에 속한다. 나머지는 배치로 집계해 Feature Store에 넣어 둔 값을 읽는다.

  • Two-Tower 임베딩: 아이템 임베딩. 신규·저노출 아이템은 평균으로 대체한다.
  • 유저 / 아이템 / 작가: 시청시간·시청비율·유효재생율, 조회수·좋아요율·완시율 등을 단기·장기 윈도우로 집계한다.
  • 유저×작가 상호작용: 상호작용 지표에 마지막 시청 후 경과시간(친밀도 신호)을 더했다. 최근 상호작용한 작가 위주로만 유지한다.

최근 취향을 더 세밀하게 잡으려고 임베딩 기반 피처도 두 개 넣었다. 아이템 ID 리스트를 그대로 넣지 않고 최근 긍정 반응을 남긴 아이템과 최근 유효재생한 아이템의 Two-Tower 임베딩을 각각 평균 내 벡터 하나로 압축했다. 앞의 것은 요청 시점 값을 쓰고 뒤의 것은 배치로 미리 계산해 둔다.

학습된 모델에서 중요도가 가장 높게 나온 피처는 유저×크리에이터 상호작용이었다. 이 유저가 이 크리에이터와 얼마나 자주, 얼마나 깊게 상호작용했는지가 다음 추천을 가장 강하게 좌우했다는 얘기다. 개인화라는 프로젝트 목적과도 맞아떨어진다.

원문 근거
"실제 학습된 모델의 feature importance를 뜯어보면, 유저×크리에이터 상호작용 피처의 중요도가 가장 높게 나타났습니다. 이는 "개인화"라는 프로젝트의 목적과도 정확히 맞아떨어지는 결과였습니다."

배포 후 지표

VTR +10%p(+20%). 재생 대비 유효재생 비율(VTR)이 개인화 랭커 배포와 함께 계단식으로 올랐고 지금까지 유지되고 있다. 상승 시점은 개인화 랭커의 노출 비중 추이와 정확히 겹친다.

모든 활동성 구간에서 고르게. 신규·콜드·라이트·미들·헤비 어느 구간이든 유효재생 비율이 +7~8%p씩 올랐다. 히스토리가 거의 없는 콜드 유저에서도 효과가 났다는 점에서, 저자는 GDCN의 피처 선별과 MoE의 활동성 분기가 의도대로 작동했다고 해석한다.

원문 근거
"또 한 가지 인상적이었던 건 유저 활동성 구간(신규 · 콜드 · 라이트 · 미들 · 헤비)에 관계없이 유효재생비율이 고르게 +7~8%p씩 개선됐다는 점입니다."

추천 다양성 +53.7%. 하루 평균 추천에 쓰인 서로 다른 아이템 개수가 53.7% 늘었다. 개인 취향을 세밀하게 반영하니 노출되는 콘텐츠의 폭도 넓어진 것이다.

원문 근거
"하루 평균 추천에 쓰인 서로 다른 아이템 개수가 +53.7% 늘었습니다. 유저 개인의 취향을 세밀하게 반영하다 보니, 더 다양한 콘텐츠가 추천되는 셈입니다."

읽고 나서

눈에 띄는 건 모델을 바꾼 이유가 실험마다 구체적이었다는 점이다. MoE는 라이트/헤비 예측력 격차라는 관측에서, GDCN은 임베딩 버전 교체 뒤 성능이 흔들린 사고에서 나왔다. 둘 다 벤치마크 점수를 쫓아 붙인 게 아니다. 임베딩 모델을 주기적으로 갱신하는 시스템이라면, 하류 랭커가 입력 분포 변화에 얼마나 버티는지부터 따져 봐야 한다는 교훈으로 읽힌다.

후보 개수 튜닝도 참고할 만하다. 300개에서 900개로 늘려도 VTR은 0.9%p밖에 오르지 않았다. 후보를 늘리는 것보다 랭커가 개인 신호를 제대로 읽게 만드는 쪽의 효과가 훨씬 컸던 셈이다.

참고 자료