콘텐츠로 이동하기
데이터 분석

Shopify에서 실시간 ML로 소비자 검색 의도를 개선한 방법

2025년 1월 15일
Jonathan Sabbagh

Machine Learning Infrastructure Engineer, Shopify

Danny McCormick

Software Engineer, Google Cloud

Join us at Google Cloud Next

Early bird pricing available now through Feb 14th.

Register

* 본 아티클의 원문은 2024년 10월 16일 Google Cloud 블로그(영문)에 게재되었습니다. 

 

빠르게 변화하는 상거래 환경 속에서 Shopify 판매자는 잠재고객에게 관련성 높은 제품을 원활하고 안정적으로 제공할 수 있는 Shopify 플랫폼의 이점을 십분 활용하고 있습니다. 풍부하고 직관적인 검색 경험은 Shopify 서비스의 핵심입니다.

지난 한 해 동안 Shopify는 판매자의 오프라인 매장에 AI 기반 검색 기능을 통합했습니다. Shopify의 오프라인 매장 검색 기능은 소비자의 온라인 쇼핑 방식을 혁신했습니다. 특히 시맨틱 검색을 활용해 키워드 검색을 넘어 소비자 검색 이면의 의도를 더 깊이 이해하면서 가장 관련성 높은 제품을 검색 결과로 제공할 수 있었습니다.

그 결과, 판매자는 매출을 높이고 소비자는 더 나은 상호작용 경험을 누리면서 모두에게 이익이 되었습니다.

https://storage.googleapis.com/gweb-cloudblog-publish/images/1_QNT2iZy.max-800x800.jpg

실시간 임베딩을 사용한 ML 애셋 구축

비슷한 시기에 Shopify는 기반이 되는 머신러닝(ML) 애셋을 생성하는 데 투자하기 시작했습니다. 이 애셋은 ML 기본 요소의 공유 저장소 형태로 구축되며, 보다 정교한 AI 시스템을 개발하는 빌딩 블록으로 재사용됩니다. Shopify 오프라인 매장 검색은 이러한 ML 애셋을 활용한 최적의 사용 사례입니다. 이와 같은 복잡한 시스템에는 텍스트와 이미지를 모두 처리 가능한 데이터 형식으로 변환할 수 있는 기본 요소가 필요합니다.

그렇다면 어떻게 해야 할까요?

텍스트 콘텐츠와 시각적 콘텐츠를 고차원 공간에서 수치형 벡터로 변환하는 임베딩을 활용하면 됩니다. 이 변환을 통해 텍스트나 이미지 등 서로 다른 콘텐츠 간의 유사성을 측정할 수 있어 보다 정확하고 맥락적으로 관련성 높은 검색 결과를 얻을 수 있습니다.

https://storage.googleapis.com/gweb-cloudblog-publish/images/2_rQxuo0l.max-1800x1800.png

임베딩을 사용하여 검색 텍스트를 결과 코퍼스와 비교하는 간단한 예시

Shopify 오프라인 매장 검색에 사용되는 ML 기본 요소를 명확하게 이해했으니 이제 이러한 임베딩 업데이트를 시스템에 보내는 방식을 살펴보겠습니다.

ML 추론 스트리밍 파이프라인 설계

현재 Shopify는 이미지 및 텍스트 파이프라인 전반에서 초당 약 2,500개의 임베딩(하루 약 2억 1,600만 개)을 거의 실시간으로 처리하고 있습니다. 이러한 임베딩에는 판매자의 순 신규 콘텐츠와 업데이트된 콘텐츠가 포함됩니다. 데이터 플랫폼 인프라 대부분이 BigQuery에서 설계되었으므로 Google Cloud의 스트리밍 분석 서비스인 Dataflow를 사용하여 이러한 파이프라인을 구동합니다. Dataflow의 기본 스트리밍 기능, 전체 Google Cloud 생태계와의 간편한 통합, 끊임없이 진화하는 AI 및 데이터 니즈에 맞춘 확장 가능성이 Dataflow를 선택한 이유입니다.

이 설계의 전반적인 아키텍처는 꽤 단순합니다. 이미지 임베딩 스트리밍 파이프라인을 예로 들어 살펴보겠습니다.

  1. 시작 시 임베딩 모델이 로드됩니다.

  2. 파이프라인은 판매자 웹사이트에서 이미지가 생성되거나 수정되었음을 알리는 입력 이벤트 주제의 새 이벤트를 리슨합니다.

  3. 추론 실행 전에 새 이벤트가 사전 처리됩니다.

  • 이미지를 작업자 머신에 다운로드합니다.

  • 메모리에 이미지를 로드합니다.

  • 이미지 크기를 조정합니다.

  1. 다음으로 이미지에 해당하는 임베딩 벡터가 생성됩니다.

  2. 임베딩에 최종 후처리가 적용됩니다.

  3. 임베딩이 작성되는 위치는 다음과 같습니다.

  • 오프라인 분석을 위한 데이터 웨어하우스(보고서, 대시보드)

  • 다운스트림 실시간 수집을 위한 출력 이벤트 주제(Shopify 오프라인 매장 검색)
https://storage.googleapis.com/gweb-cloudblog-publish/images/3_6BpODWE.max-1200x1200.png

Shopify가 더 단순한 일괄 처리 솔루션 대신 실시간에 가까운 임베딩을 선택한 이유가 궁금하실지도 모릅니다. 왜 파이프라인의 복잡성이 커질 수 있는 선택을 했을까요? 판매자와 고객은 Shopify 플랫폼에서 매끄러운 경험을 기대합니다. 판매자는 제품을 수정하거나 새로운 이미지를 업로드하는 즉시 이러한 업데이트가 웹사이트에 반영되길 바랍니다. 게다가 Shopify의 궁극적인 목적은 판매자의 매출을 높이면서 소비자에게 만족스러운 상호작용이 이루어지는 환경을 제공하는 것입니다. Shopify에서 조사한 결과에 따르면, 스트리밍 파이프라인을 통해 최신 임베딩을 유지할 때 일괄 처리 솔루션과 비교해 복잡성은 더 커지지만 앞서 설명한 목표에 맞는 최적화가 가능합니다.

스트리밍 파이프라인 유지보수에 따른 과제

스트리밍 파이프라인의 유지보수는 실제로 꽤 까다롭습니다. 여기에 GPU 가속 추론을 추가하면 더 복잡해집니다. 결과적으로 비용, 처리량, 지연 시간 사이에서 의미 있는 균형점을 계속 찾기 위해 몇 가지 기술 관련 결정을 내려야 했습니다. 그중 몇 가지를 소개합니다.

1. 메모리 내 데이터 관리이미지 임베딩 파이프라인을 배포할 때 가장 처음 선택한 Dataflow 기반 작업자 머신은 NVIDIA T4 GPU가 탑재된 n1-standard-16이었습니다. 그러나 메모리 부족(OOM) 오류가 발생하기 시작했습니다. 이미지가 소모하는 상당량의 메모리를 고려할 때 자연스러운 일이었습니다.

첫 번째 해결책은 작업자의 메모리 확장이었습니다. 그래서 n1-highmem-16 머신으로 전환해 작업자 메모리를 60GB에서 104GB로 확장했습니다. 이 방법은 한동안 효과적이었으나 비용이 14% 증가하는 결과를 낳았습니다. 결국 보다 나은 장기적 해결책이 필요했습니다.

이후 Dataflow 내부 구조를 더 잘 파악하기 위한 여정이 시작되었습니다. Dataflow의 Python Runner는 작업자의 하드웨어 활용도를 극대화하기 위해 일반적으로 머신에서 코어당 하나의 프로세스를 생성합니다. 따라서 n1-highmem-16 머신에서 병렬 처리 수를 최대화하면 16개의 프로세스를 실행할 수 있습니다. 그뿐 아니라 동시 실행을 늘리기 위해 이러한 각 프로세스는 기본적으로 12개의 스레드를 생성하며 각 스레드에는 추론을 실행하기 위한 요소가 할당됩니다.

https://storage.googleapis.com/gweb-cloudblog-publish/images/4_wtE3mKE.max-2000x2000.png

즉, 16개의 프로세스가 실행될 때 동시에 요소를 처리하는 스레드가 16x12 = 192개가 됩니다. 이는 메모리에서 대략 192개의 이미지가 동시 실행된다는 의미로 해석됩니다. 실제로 요소가 번들로 묶이는 경우를 고려하면 조금 더 복잡해지지만 여기서는 다루지 않겠습니다.

다행히 Dataflow Runner를 사용하면 number_of_worker_harness_threads를 설정하여 스레드 수를 제어할 수 있습니다. 간단한 계산만으로도 스레드 수를 4개로 줄이면 이론상 메모리 사용량이 3배 감소한다고 예측할 수 있었습니다(예: 4x16 = 64개 스레드, 192/64 = 3). 스레드 수를 줄이면 자연스럽게 동시 실행이 적어져서 파이프라인의 전처리 단계 처리량이 감소합니다. 하지만 이 파이프라인은 이미 GPU에 의해 처리량이 제한되고 있기 때문에 GPU로 처리 가능한 수준을 초과해 이미지를 축적하고 있었으며 결과적으로 스레드 수 감소는 추론 단계의 처리량에는 큰 영향을 미치지 않았습니다. 

이 변화는 어떤 결과를 가져왔을까요? 이전에는 고메모리 머신의 최대 메모리 용량이 항상 104GB를 상회했습니다. 이 변화 이후에는 메모리 사용량이 약 2.6배 줄어든 40GB에 가까워졌습니다. 결과적으로 n1-standard-16 머신으로 다시 전환해 추가된 14%의 비용을 절감할 수 있었습니다. 

2. 메모리에서 모델 관리Apache Beam은 일괄 파이프라인 또는 스트리밍 파이프라인에서 추론을 실행하기 위한 두 가지 개념을 제공합니다.

  • ModelHandler: 추론에 사용될 ML 모델을 정의합니다.

  • RunInference: ModelHandler를 기반으로 임베딩을 생성하는 변환입니다.

기본적으로 작업자의 각 프로세스는 GPU에 임베딩 모델의 자체 인스턴스를 로드합니다. 앞서 설명한 것처럼 n1-standard-16을 사용하고 있기 때문에 모델은 GPU의 메모리에 16번 로드되고 있었습니다. 이 경우 더 많은 모델 인스턴스가 동시 로드로 실행될 수 있어 처리량은 물론 메모리 소비량도 높아지게 됩니다.

처음에는 모델이 메모리에 로드되는 방식을 제어할 수 있는지 판단하여 모델의 메모리 사용량을 줄이는 방안을 살펴봤습니다. 가장 직관적인 해결책은 ModelHandler가 프로세스 전반에 모델을 공유프로세스 전반에 모델을 공유 (share_model_across_processes)하도록 설정하는 것입니다.

로드 중...

이렇게 하면 모델이 공유 프로세스에 한 번만 로드되고 VM 기반 Pythorn 프로세스는 이 모델을 쿼리하여 추론을 수행합니다.

이 해결책은 메모리 소비량을 크게 줄이지만 병렬 처리 수 감소로 인해 처리량도 현저하게 줄어듭니다. 처리량 감소로 인해 이 접근 방식은 포기했습니다.

또 다른 해결책으로 작업자별로 생성되는 Python 프로세스 수를 제어하는 방안을 생각해 냈지만 현재로서는 실현 가능하지 않습니다. 유일한 해결책은 experiments=no_use_multiple_sdk_containers를 사용 설정하여 강제로 Dataflow Runner가 작업자당 하나의 프로세스만 생성하도록 하는 것입니다. 하지만, 이에 따라 처리량이 크게 감소하기 때문에 이 해결책을 선택하지 않았습니다.

결과적으로, 임베딩 모델의 규모는 추가적인 메모리 최적화가 필요하지 않을 만큼 작기 때문에 Dataflow의 기본 설정을 유지하기로 했습니다.

3. 일괄 처리일반적으로 CPU 활용도가 80%를 넘으면 스래싱 (thrashing)이 발생할 수 있어 피하는 것이 좋지만 GPU는 활용도가 100%에 가까울수록 이상적입니다. 처음 들었다면 놀라셨을지도 모릅니다. 이는 GPU 스트리밍 멀티 프로세서(SM)는 스레드 간 전환 비용이 거의 없기 때문입니다.

GPU의 높은 처리량으로 인해 호스트(CPU)와 기기(GPU) 간 데이터 전송이 스트리밍 데이터 파이프라인의 병목 현상을 일으키는 경우가 대부분입니다. 실제로는 GPU의 전역 메모리와 커널을 실행하는 SM 간 데이터 전송도 병목 현상을 일으키는 주요 요소이지만 이 블로그 글에서는 다루지 않습니다. 이때 일괄 처리는 작업자 GPU를 최대한 활용하는 데 중요한 역할을 합니다.

다행히도 Apache Beam은 스트리밍 파이프라인에서 데이터를 일괄 처리할 수 있는 유용한 시맨틱스를 제공합니다. ModelHandler에서 batch_element_kwargs 메서드를 재정의하여 GPU에 전송할 원하는 일괄 처리를 정의할 수 있었습니다.

로드 중...

아직 문제가 해결되기에는 이릅니다.

여전히 GPU로 전송되는 일괄 처리의 크기가 1입니다. 어떻게 된 걸까요? 

RunInference는 일괄 처리를 수행하기 위해 내부적으로 BatchElements 변환을 사용합니다. 기본적으로 데이터를 일괄 처리하는 두 가지 방법이 있습니다. 데이터 일괄 처리를 기존 번들 내에서 시도하거나 스테이트풀(Stateful) 구현을 사용해 번들 전반에서 시도할 수 있습니다.

번들은 병렬 처리를 관리하기 위한 Dataflow 개념입니다. 기본적으로 함께 처리되는 요소의 모음을 번들이라고 합니다. 번들은 Dataflow Runner에 의해 결정되며 사용자가 직접 제어할 수 없습니다.

이 사례에서는 파이프라인의 입력 주제의 급증으로 인해 요소가 번들 1에 구성되고 있었습니다. 즉 Dataflow는 의미 있는 방식으로 요소를 일괄 처리할 수 없었습니다. 한편 max_batch_duration_secs를 추가하면 스테이트풀(Stateful) 일괄 처리 시 사용자가 원하는 일괄 처리 크기를 보장하려고 하지만 지연 시간이 늘어납니다. 이 문제는 주로 Dataflow Runner가 셔플을 강제하기 때문에 발생합니다. 

결국 앞서 설명한 것처럼 Dataflow가 프로세스별로 모델 인스턴스를 로드하는 구조 때문에 GPU를 최대한 활용할 수 있습니다. 이 방식은 호스트(CPU)와 기기(GPU) 간에 데이터 전송 수가 늘어나는 것을 상쇄할 수 있을 만큼 충분한 병렬 처리를 생성합니다. 결과적으로, 지연 비용이 너무 높아 번들 내 일괄 처리 옵션을 유지하기로 했습니다. 지금도 이 분야의 권장사항을 찾기 위해 열심히 연구하는 중입니다. 

결론

Shopify의 실시간 임베딩 파이프라인은 중앙화된 ML 애셋으로 텍스트 및 이미지 임베딩을 전송하며, 혁신을 지원하는 강력한 빌딩 블록으로서 멋진 신제품을 구축하는 기반이 됩니다. 시맨틱 검색은 그중 하나의 예시일 뿐입니다. 현재 Shopify는 사내 여러 팀과 협업하면서 해결할 가치가 있는 문제와 이를 해결하기 위해 구축할 수 있는 중앙화된 ML 애셋을 계속 모색하고 있습니다.

다음 단계

Dataflow ML 사용을 시작하고 싶으신가요? 이 문서를 확인해 보세요.

게시 위치