주요 컨텐츠로 이동

PySpark 및 Pandas UDF를 사용하여 SHAP 계산 확장하기

scaling-shap-blog-og

발행일: February 2, 2022

데이터 사이언스 및 MLLess than a minute

동기

머신러닝(ML), 특히 딥러닝(DL) 모델이 의사 결정에 적용되는 사례가 확산됨에 따라, 블랙박스 내부를 들여다보고 이러한 모델의 출력을 기반으로 주요 비즈니스 결정을 정당화하는 것이 더욱 중요해지고 있습니다. 예를 들어 ML 모델이 고객의 대출 요청을 거부하거나 P2P 대출에서 특정 고객에게 신용 위험을 할당하는 경우, 비즈니스 이해관계자에게 이러한 결정이 내려진 이유에 대한 설명을 제공하는 것은 모델의 도입을 장려하는 강력한 도구가 될 수 있습니다. 많은 경우 해석 가능한 ML은 비즈니스 요구사항일 뿐만 아니라 고객에게 특정 결정이나 옵션이 제공된 이유를 이해하기 위한 규제 요건이기도 합니다. SHapley Additive exPlanations (SHAP)는 설명 가능한 AI를 활용하고 비즈니스 문제 해결에 있어 ML 모델 및 신경망 결과에 대한 신뢰를 구축하는 데 도움이 되는 중요한 도구입니다.

SHAP은 게임 이론에 기반한 최신 모델 설명 프레임워크입니다. 이 접근 방식은 데이터세트의 각 데이터 포인트에 대해 모델의 특성과 모델 출력 간의 선형 관계를 찾는 것을 포함합니다. 이 프레임워크를 사용하여 모델의 출력을 전역적으로 또는 지역적으로 해석할 수 있습니다. 전역적 해석 가능성은 각 특성이 결과에 긍정적으로 또는 부정적으로 얼마나 기여하는지 이해하는 데 도움이 됩니다. 반면에 지역적 해석 가능성은 특정 관측값에 대해 각 특성의 효과를 이해하는 데 도움이 됩니다.

데이터 과학 커뮤니티에서 널리 채택된 가장 일반적인 SHAP 구현은 단일 노드 머신에서 실행됩니다. 즉, 사용 가능한 코어 수에 관계없이 모든 계산을 단일 코어에서 실행합니다. 따라서 분산 컴퓨팅 기능을 활용하지 못하고 단일 코어의 한계에 의해 제약을 받습니다.

이 게시물에서는 특히 로컬 해석 가능성을 위해 여러 머신에 걸쳐 SHAP 값 계산을 병렬화하는 간단한 방법을 보여드리겠습니다. 그런 다음 데이터세트에서 행과 열의 수가 증가함에 따라 이 솔루션이 어떻게 확장되는지 설명하겠습니다. 마지막으로 Spark로 SHAP 계산을 병렬화할 때 효과가 있었던 점과 피해야 할 점에 대한 저희의 연구 결과를 중점적으로 살펴보겠습니다.

단일 노드 SHAP

설명 가능성을 실현하기 위해 SHAP은 모델을 Explainer로 변환하고, 개별 모델 예측은 Explainer를 적용하여 설명합니다. SHAP 값 계산은 Python의 인기 있는 구현을 포함하여 다양한 프로그래밍 언어로 여러 가지가 구현되어 있습니다. 이 구현에서는 각 관측치에 대한 설명을 얻기 위해 모델에 적합한 explainer를 적용할 수 있습니다. 다음 코드 스니펫은 TreeExplainer를 랜덤 포레스트 분류기에 적용하는 방법을 보여줍니다.

이 방법은 데이터 볼륨이 작을 때는 잘 작동하지만, 수백만 개의 레코드에 대한 ML 모델의 출력을 설명할 때는 구현이 단일 노드 방식이므로 확장성이 떨어집니다. 예를 들어, 아래 그림 1의 시각화는 레코드 수가 증가함에 따라 단일 노드 머신(코어 4개 및 메모리 30.5GB)에서 SHAP 값 계산의 실행 시간이 증가하는 것을 보여줍니다. 1백만 개가 넘는 행과 50개가 넘는 열을 가진 데이터 형태의 경우 머신 메모리가 부족해졌기 때문에 해당 값들이 그림에 누락되었습니다. 보시다시피 실행 시간은 레코드 수에 따라 거의 선형적으로 증가하는데, 이는 실제 시나리오에서는 지속 가능하지 않습니다. 예를 들어, 머신 러닝 모델이 예측을 수행한 이유를 이해하기 위해 10시간을 기다리는 것은 많은 비즈니스 환경에서 비효율적이며 용납될 수 없습니다.

단일 노드 SHAP 계산 실행 시간
그림 1: 단일 노드 SHAP 계산 실행 시간

이 문제를 해결하기 위해 고려할 수 있는 한 가지 방법은 근사 계산을 사용하는 것입니다. `shap_values` 메서드에서 `approximate` 인수를 `True`로 설정할 수 있습니다. 그렇게 하면 트리의 하위 분할이 더 높은 가중치를 갖게 되며 SHAP 값이 정확한 계산과 일치한다는 보장은 없습니다. 이렇게 하면 계산 속도가 빨라지지만, 모델 출력에 대한 부정확한 설명이 나올 수 있습니다. 또한 `approximate` 인수는 `TreeExplainers`에서만 사용할 수 있습니다.

대안적인 접근 방식은 Apache Spark™와 같은 분산 처리 프레임워크를 활용하여 여러 코어에 Explainer의 적용을 병렬화하는 것입니다.

PySpark를 사용한 SHAP 계산 확장

SHAP 계산을 분산하기 위해 PySpark의 Python 구현과 Pandas UDF 를 사용하고 있습니다. 저희는 kddcup99 데이터세트를 사용하여 '침입' 또는 '공격'이라고 하는 불량 연결과 정상적인 양호한 연결을 구별할 수 있는 예측 모델인 네트워크 침입 탐지기를 구축하고 있습니다. 이 데이터세트는 침입 탐지 목적으로 사용하기에는 결함이 있는 것으로 알려져 있습니다. 하지만 이 게시물에서는 기본 ML 모델의 시맨틱이 아니라 SHAP 값 계산에만 순수하게 초점을 맞추고 있습니다.

실험을 위해 구축한 두 모델은 다양한 열 크기에 대한 솔루션의 확장성을 보여주기 위해 10개 및 50개 피처를 가진 데이터 세트에서 학습된 간단한 랜덤 포레스트 분류기입니다. 원본 데이터 세트에는 50개 미만의 열이 있으며, 원하는 데이터 볼륨에 도달하기 위해 이 열 중 일부를 복제했다는 점에 유의하시기 바랍니다. 우리가 실험한 데이터 볼륨의 범위는 4MB에서 1.85GB입니다.

코드를 자세히 살펴보기 전에 Spark Dataframe과 UDF가 어떻게 작동하는지 간략하게 알아보겠습니다. Spark Dataframes은 clusters 전체에 (행 단위로) 분산되며 각 행 그룹을 파티션이라고 합니다. 각 파티션은 (기본적으로) 1개의 코어로 연산할 수 있습니다. 이것이 Spark가 근본적으로 병렬 처리를 달성하는 방식입니다. pandas는 SHAP에 쉽게 데이터를 공급할 수 있고 성능이 뛰어나므로 Pandas UDF는 자연스러운 선택입니다. 벡터화된 UDF라고도 하는 pandas UDF는 Apache Arrow 를 사용하여 데이터 전송을 최적화함으로써 Python UDF보다 더 나은 성능을 제공합니다.

아래 코드 스니펫은 PySpark에서 Pandas UDF를 사용하여 Explainer 적용을 병렬화하는 방법을 보여줍니다. `calculate_shap`이라는 pandas UDF를 정의한 다음 이 함수를 `mapInPandas`에 전달합니다. 그런 다음 이 메서드를 사용하여 PySpark 데이터프레임에 병렬화된 메서드를 적용합니다. 이 UDF를 사용하여 SHAP 성능 테스트를 실행할 것입니다.

그림 2는 100만 개 행과 10개 열에 대해 단일 노드 머신과 크기가 각각 2, 4, 8, 16, 32, 64인 클러스터의 실행 시간을 비교합니다. 모든 클러스터의 기본 머신은 유사합니다(4개 코어 및 30.5GB 메모리). 한 가지 흥미로운 관찰은 병렬화된 코드가 클러스터의 노드 전체에 있는 모든 코어를 활용한다는 것입니다. 따라서 크기가 2인 클러스터를 사용하더라도 성능이 거의 5배 향상됩니다.

단일 노드 대 병렬 SHAP 계산 실행 시간(1백만 행, 10개 열)
그림 2: 단일 노드 vs 병렬 SHAP 계산 실행 시간(1백만 행, 10개 열)

데이터 크기 증가에 따른 확장성

SHAP이 구현된 방식 때문에 추가 행보다 추가 특성이 성능에 더 큰 영향을 미칩니다. 이제 Spark와 Pandas UDF를 사용하여 SHAP 값을 더 빠르게 계산할 수 있다는 것을 알게 되었습니다. 다음으로 추가적인 특성/열에 대한 SHAP의 성능을 살펴보겠습니다.

직관적으로 데이터 크기가 커지면 SHAP 알고리즘이 처리해야 할 계산이 더 많아진다는 것을 의미합니다. 그림 3은 다양한 행과 열 수에 대해 16개 노드 클러스터에서 SHAP 값 실행 시간을 보여줍니다. 행을 확장하면 실행 시간이 거의 정비례하여 증가하며, 즉 행 수를 두 배로 늘리면 실행 시간이 거의 두 배가 된다는 것을 확인할 수 있습니다. 열 수를 늘리는 것은 실행 시간과 비례 관계가 있으며, 열을 하나 추가하면 실행 시간이 거의 80% 증가합니다.

이러한 관찰(그림 2 및 그림 3)을 통해 데이터가 많을수록 실행 시간을 적정 수준으로 유지하기 위해 (더 많은 작업자 노드를 추가하여) 컴퓨팅을 수평적으로 확장할 수 있다는 결론을 내렸습니다.

다양한 행 및 열 수에 따른 6노드 병렬 SHAP 계산 실행 시간
그림 3: 다양한 행과 열에 대한 16-노드 병렬 SHAP 계산 실행 시간

병렬화는 언제 고려해야 할까요?

우리가 답하고 싶었던 질문은 다음과 같습니다. 병렬화는 언제 가치가 있을까요? 계산에 추가될 수 있다는 것을 알면서도 SHAP 계산을 병렬화하기 위해 PySpark을 언제 사용하기 시작해야 할까요? 클러스터 크기를 두 배로 늘리는 것이 SHAP 계산 실행 시간을 개선하는 데 미치는 효과를 측정하기 위해 실험을 설정했습니다. 이 Experiment의 목표는 문제에 더 많은 수평적 리소스(즉, 더 많은 worker 노드 추가)를 투입하는 것을 정당화하는 데이터 크기가 어느 정도인지 알아내는 것입니다.

10개 열의 데이터에 대해 행 수가 10, 100, 1000에서 최대 1,000만 개인 경우까지 SHAP 계산을 실행했습니다. 각 행 수에 대해 클러스터 크기가 2, 4, 32, 64인 경우에 대해 SHAP 계산 실행 시간을 4번 측정했습니다. 실행 시간 비율은 노드 수가 절반인 클러스터(각각 2와 32)에서 동일한 계산을 실행할 때와 비교하여, 더 큰 클러스터(4와 64)에서 SHAP 값 계산을 실행할 때의 시간 비율입니다.

그림 4는 이 Experiment의 결과를 보여줍니다. 핵심 내용은 다음과 같습니다.

  •  
    • 행 수가 적은 경우, 클러스터 크기를 두 배로 늘려도 실행 시간이 개선되지 않으며, 경우에 따라 Spark 작업 관리로 인해 추가되는 오버헤드로 인해 실행 시간이 더 나빠지기도 합니다(따라서 실행 시간 비율 > 1).
    • 행 수를 늘릴수록 클러스터 크기를 두 배로 늘리는 것이 더 효과적입니다. 1,000만 개 행 데이터의 경우 클러스터 크기를 두 배로 늘리면 실행 시간이 거의 절반으로 줄어듭니다.
    • 모든 행 수에 대해 클러스터 크기를 32에서 64로 두 배로 늘리는 것보다 2에서 4로 두 배로 늘리는 것이 더 효과적입니다(파란색 선과 주황색 선 사이의 간격 참조). 클러스터 크기가 커질수록 더 많은 노드를 추가하는 오버헤드도 커집니다. 이는 파티션당 데이터 크기가 너무 작은 파티션 크기를 사용하기 때문이며, 더 최적의 데이터/파티션 크기를 사용하는 것보다 적은 양의 데이터를 처리하기 위해 별도의 작업을 생성하는 데 더 많은 오버헤드가 추가됩니다.
데이터 볼륨별 클러스터 크기 두 배 증가가 실행 시간에 미치는 영향
그림 4: 다양한 데이터 볼륨에 대해 클러스터 크기를 두 배로 늘렸을 때 실행 시간에 미치는 영향
기술 가이드 eBook

MLOps의 Big Book

주의할 점

재분할

위에서 언급했듯이 Spark는 파티션 개념을 통해 병렬 처리를 구현합니다. 데이터는 행 청크로 분할되며 각 파티션은 default 단일 코어에서 처리됩니다. Apache Spark가 데이터를 처음 읽을 때 클러스터에서 실행하려는 연산에 최적인 파티션을 반드시 생성하는 것은 아닙니다. 특히 SHAP 값을 계산하기 위해 데이터 세트를 재분할하면 더 나은 성능을 얻을 수 있습니다.

충분히 작은 파티션을 만들면서도, 파티션 생성 오버헤드가 계산 병렬화의 이점을 능가할 정도로 너무 작게 만들지 않는 것 사이에서 균형을 맞추는 것이 중요합니다.
성능 테스트를 위해 다음 코드를 사용하여 클러스터의 모든 코어를 활용하기로 했습니다.

데이터 볼륨이 훨씬 더 큰 경우 파티션 수를 코어 수의 2배 또는 3배로 설정할 수 있습니다. 핵심은 이를 실험하여 데이터에 가장 적합한 파티셔닝 전략을 찾는 것입니다.

display() 사용

Databricks 노트북에서 작업하는 경우 실행 시간을 벤치마킹할 때 display() 함수 사용을 피하는 것이 좋습니다. display() 함수는 전체 변환에 소요되는 시간을 반드시 보여주지는 않을 수 있습니다. 이 함수에는 쿼리에 주입되는 암시적인 행 제한이 있으며, 파일에 쓰기와 같이 측정하려는 운영에 따라 결과를 driver로 다시 수집하는 데 추가적인 오버헤드가 발생합니다. 저희의 실행 시간은 "noop" 형식을 사용하는 Spark의 쓰기 메서드를 사용하여 측정되었습니다.

결론

이 블로그 게시물에서는 PySpark 및 Pandas UDF로 병렬 처리하여 SHAP 계산 속도를 높이는 솔루션을 소개했습니다. 그런 다음 데이터 볼륨 증가, 다양한 머신 유형 및 구성 변경에 따른 솔루션의 성능을 평가했습니다. 핵심 내용은 다음과 같습니다.

  •  
    •  
      • 단일 노드 SHAP 계산은 행과 열의 수에 비례하여 선형적으로 증가합니다.
      • PySpark로 SHAP 계산을 병렬 처리하면 클러스터 전체의 모든 CPU에서 연산을 실행하여 성능이 개선됩니다.
      • 데이터 볼륨이 클수록 클러스터 크기를 늘리는 것이 더 효과적입니다. 데이터가 작을 경우 이 방법은 효과적이지 않습니다.

향후 과제

수직 확장 - 이 블로그 게시물의 목적은 대규모 데이터 세트를 사용하여 수평적으로 확장하는 것이 SHAP 값 계산 성능을 어떻게 향상시킬 수 있는지 보여주는 것이었습니다. 저희는 클러스터의 각 노드가 4코어, 30.5GB를 갖추고 있다는 전제에서 시작했습니다. 앞으로는 수평 확장뿐만 아니라 수직 확장의 성능을 테스트하는 것도 흥미로울 것입니다. 예를 들어, 4개 노드로 구성된 클러스터(각각 4코어, 30.5GB)와 2개 노드로 구성된 클러스터(각각 8코어, 61GB) 간의 성능을 비교하는 것입니다.

직렬화/역직렬화 - 앞서 언급했듯이 Python UDF 대신 Pandas UDF를 사용하는 핵심 이유 중 하나는 Pandas UDF가 Apache Arrow를 사용하여 JVM과 python 프로세스 간의 데이터 직렬화/역직렬화를 개선한다는 것입니다. Spark 데이터 파티션을 Arrow 레코드 배치로 변환할 때 잠재적인 최적화가 있을 수 있으며, Arrow 배치 크기를 실험하면 성능이 더욱 향상될 수 있습니다.

분산 SHAP 구현과의 비교 - 저희 솔루션의 결과를 Shparkley와 같은 SHAP의 분산 구현과 비교해 보는 것이 흥미로울 것입니다. 이러한 비교 연구를 수행할 때 우선 두 솔루션의 출력이 비교 가능한지 확인하는 것이 중요합니다.

 

(이 글은 AI의 도움을 받아 번역되었습니다. 원문이 궁금하시다면 여기를 클릭해 주세요)

게시물을 놓치지 마세요

관심 있는 카테고리를 구독하고 최신 게시물을 받은편지함으로 받아보세요

다음은 무엇인가요?

Social Card

데이터 사이언스 및 ML

July 24, 2024/1분 이내 소요

GenAI 모델 파인튜닝을 위한 Mosaic AI Model Training 소개

Unlock-Faster-Machine-Learning-with-Graviton

데이터 사이언스 및 ML

August 16, 2024/1분 이내 소요

Graviton으로 더 빠른 머신 러닝 실현