본문 바로가기
🏭 Data

MAB 알고리즘 [2/2] - MAB와 Thompson Sampling의 아키텍처 및 실전 구현 (w. Kotlin)

by kukim 2024. 10. 28.

"서비스에서 MAB 알고리즘을 사용하기 위해 시스템은 어떻게 구축하고, Thompson Sampling은 어떻게 구현/운영할까?"

 

지난 글(MAB 알고리즘 [1/2] - A/B 테스트의 한계, MAB 알고리즘과 Thompson Sampling 이해하기)에서 A/B 테스트의 한계와 이를 보완하기 위한 MAB 알고리즘, 특히 Thompson Sampling의 개념에 대해 알아보았습니다. 하지만 실제 서비스에 적용하기 위해서는 개념 이해를 넘어 전체적인 시스템 설계와 운영 방안이 필요합니다.

 

MAB 알고리즘을 서비스에 적용하기 위해서는

- 실시간으로 사용자 행동을 수집하고

- 수집된 데이터를 적절히 처리하여

- 빠른 응답 시간 내에 추천 결과를 제공해야 합니다

 

이러한 실시간 데이터 수집/처리 시스템은 A/B 테스트, 개인화 추천 등 다양한 서비스에서 필수적인 인프라입니다. 이 글에서는 시스템 아키텍처를 살펴보고, Kotlin을 활용한 Thompson Sampling의 실전 구현과 운영 방법에 대해 상세히 알아보도록 하겠습니다.


목차

 

1. 시스템 아키텍처

   - 추천 API 서버

   - 데이터 파이프라인 시스템

   - +a) 실제 운영 환경 아키텍처

 

2. Thompson Sampling 구현

   2.1 기본 구현: 레이아웃(A/B/C 버킷) 최적화

   - 핵심 로직 구현

   - 실제 적용 예시

   

   2.2 응용 구현: 아이템 순서 최적화

   - 아이템 정렬 로직

   - 실제 적용 예시

 

   2.3 성능 최적화와 운영

   - Score 계산 시기 (실시간 vs 배치)

   - 추가 고려사항

 

3. 마치며

   - Context-free MAB의 한계

   - Contextual Bandit 소개


1.  시스템 아키텍처

MAB 알고리즘 적용을 위해 두 가지 핵심 기능이 필요합니다

1) 사용자의 추천 요청에 응답

2) 사용자 행동 데이터 수집 및 처리

 

이러한 구조는 Thompson Sampling뿐만 아니라 ε-Greedy, UCB 등 다양한 MAB 알고리즘을 적용할 수 있는 기반이 됩니다.

간단한 아키텍처

1.1 추천 API 서버 (Recommendation API Server)

사용자의 추천 요청을 처리하는 서버입니다. 실시간으로 최적의 추천 결과를 제공합니다.

// 요청 예
{
    "userId": "user123",
    "targetId": "main_recommendation",
    "userContext": {
        "age": "30",
        "gender": "F"
    }
}

// 응답 예
{
    "recommendations": [
        {
            "version": "A",
            "score": 0.342,
            "items": [...]
        }
    ]
}

 

1.2 데이터 파이프라인 시스템 (Data Pipeline System)

사용자 행동 데이터를 수집하고 처리하는 시스템입니다. 

// 이벤트 수집 데이터 예시

{
    "timestamp": "2024-01-01 10:00:00",
    "userId": "user123",
    "eventType": "impression|click",
    "targetId": "main_recommendation",
    "version": "A",
    "itemId": "item789"
}

// 집계 데이터 예시

{
    "targetId": "main_recommendation",
    "version": "A",
    "segment": {
        "age": "30",
        "gender": "F"
    },
    "stats": {
        "impressions": 1000,
        "clicks": 30,
        "ctr": 0.03
    }
}

 

위 두 가지 시스템을 통해 MAB 알고리즘을 구현할 수 있습니다. MAB 알고리즘 자체는 단순하지만, 사용자의 행동(클릭, 노출 여부) 데이터를 수집하고 처리하는 아키텍처가 기반이 되어야 합니다.

 

 

1.3. +a) 실제 운영 환경 아키텍처

위에서 설명한 기본 구조는 MAB 알고리즘의 핵심 기능을 이해하기 위한 단순화된 버전입니다.

 

요구사항과 환경에 따라 다르겠지만 실제 운영 환경에서는 더 많은 컴포넌트들이 필요합니다.

 

추천 API 서버

- A/B 테스트 관리 시스템

- 비즈니스 룰 엔진

- 캐시 시스템

- API 게이트웨이

-...

 

데이터 파이프라인 시스템

- 이벤트 검증 시스템

- 다양한 저장소 (Redis, Elasticsearch 등)

- 실시간 모니터링 대시보드

- 데이터 분석/리포트 시스템

-... 

 

(데이터 파이프라인 시스템 아키텍처는 "하루 400억 건을 처리하는 데이터 파이프라인 | 라인개발실록"에 설명이 잘 되어있습니다.)

전체 아키텍처

 

이제 Thompson Sampling을 어떻게 구현하는지 자세히 살펴보도록 하겠습니다.


2. Thompson Sampling 구현

두 가지 예시로 설명드리겠습니다.

 

2.1. 메인 페이지 최상단 추천 영역 결정(A/B/C 버킷 선택)

이전 글에서 살펴본 메인 페이지 최상단의 추천 영역을 어떻게 구성할지 결정하는 상황을 생각해 봅시다.

 

- A안: "오늘의 특가" 제목과 함께 특가 상품 노출

- B안: "당신을 위한 추천" 제목과 함께 개인화 추천 상품 노출

- C안: "비슷한 연령대가 좋아하는" 제목과 함께 연령대 기반 추천 상품 노출

세 가지 버전 중 어떤 것이 가장 효과적일까요?

 

Thompson Sampling은 다음과 같이 동작합니다.

1. 각 버전별로 노출수와 클릭수를 수집

2. 베타 분포를 이용해 각 버전의 점수를 계산

3. 가장 높은 점수를 받은 버전을 선택

 

기본 적인 구현은 아래와 같습니다.

object ThompsonSamplingCalculator {
    /**
     * Thompson Sampling을 사용하여 스코어를 계산합니다.
     * @param impression 노출 수
     * @param click 클릭 수
     * @return 계산된 Thompson Sampling 스코어 (0~1 사이 값)
     */
    fun calculateScore(impression: Double, click: Double): Double {
        // 입력값 유효성 검사
        if (!isValidInput(impression, click)) {
            return getDefaultScore()
        }

        return try {
            val alpha = click + 1     // 성공(클릭) + 1
            val beta = (impression - click) + 1  // 실패(미클릭) + 1
            val betaDistribution = BetaDistribution(alpha, beta)
            betaDistribution.sample()
        } catch (e: Exception) {
            getDefaultScore()
        }
    }

    private fun isValidInput(impression: Double, click: Double): Boolean {
        return impression >= 0 && // 음수 노출 체크
               click >= 0 && // 음수 클릭 체크
               click <= impression && // 클릭이 노출보다 큰 경우 체크
               !impression.isNaN() && // NaN 체크
               !click.isNaN()
    }

    private fun getDefaultScore(): Double {
        return BetaDistribution(1.0, 1.0).sample() // 균등 분포 (Beta(1,1))
    }
}

 

 

베타 분포 계산(BetaDistribution)을 위해 Apache Commons Math 라이브러리를 사용했습니다.

implementation("org.apache.commons:commons-math3:3.6.1")

 

해당 라이브러리를 활용하여 BetaDistribution 클래스를 통해 베타 분포의 샘플링과 기댓값, 신뢰구간 등을 구할 수 있습니다.

 

이제 이 코드가 실제로 어떻게 동작하는지 살펴보면

test("기본 Thompson Sampling 테스트") {
    println("=== 메인 페이지 추천 영역 기본 테스트 ===")

    println("특가 상품 (1000번 노출, 30번 클릭, CTR: 3.0%)")
    repeat(5) {
        val score = ThompsonSamplingCalculator.calculateScore(1000.0, 30.0)
        println("샘플링 점수: %.3f".format(score))
    }

    println("\n개인화 추천 (100번 노출, 2번 클릭, CTR: 2.0%)")
    repeat(5) {
        val score = ThompsonSamplingCalculator.calculateScore(100.0, 2.0)
        println("샘플링 점수: %.3f".format(score))
    }

    println("\n연령대 기반 (10번 노출, 1번 클릭, CTR: 10.0%)")
    repeat(5) {
        val score = ThompsonSamplingCalculator.calculateScore(10.0, 1.0)
        println("샘플링 점수: %.3f".format(score))
    }

    println("\n=== 예외 케이스 테스트 (Beta(1,1) 균등 분포) ===")
    println("노출 0, 클릭 0인 경우: %.3f".format(ThompsonSamplingCalculator.calculateScore(0.0, 0.0)))
    println("클릭이 노출보다 많은 경우: %.3f".format(ThompsonSamplingCalculator.calculateScore(10.0, 20.0)))
    println("음수 노출인 경우: %.3f".format(ThompsonSamplingCalculator.calculateScore(-10.0, 5.0)))
}

 

결과

=== 메인 페이지 추천 영역 기본 테스트 ===
특가 상품 (1000번 노출, 30번 클릭, CTR: 3.0%)
샘플링 점수: 0.034
샘플링 점수: 0.027
샘플링 점수: 0.026
샘플링 점수: 0.034
샘플링 점수: 0.020

개인화 추천 (100번 노출, 2번 클릭, CTR: 2.0%)
샘플링 점수: 0.039
샘플링 점수: 0.031
샘플링 점수: 0.026
샘플링 점수: 0.019
샘플링 점수: 0.016

연령대 기반 (10번 노출, 1번 클릭, CTR: 10.0%)
샘플링 점수: 0.102
샘플링 점수: 0.236
샘플링 점수: 0.110
샘플링 점수: 0.111
샘플링 점수: 0.223

=== 예외 케이스 테스트 (Beta(1,1) 균등 분포) ===
노출 0, 클릭 0인 경우: 0.358
클릭이 노출보다 많은 경우: 0.680
음수 노출인 경우: 0.222

 

 특가 상품은 1000번이나 노출됐기 때문에 "CTR 3% 에 가까운이 정도 성과가 날 거야"라고 확신에 찬 답변(score 0.3과 가까운)을 하는 반면, 연령대 기반은 아직 데이터가 적어서 "음... 이 정도? 아니면 저 정도...?"라며 다양한 값을 시도해보고 있습니다.

 

각 버전의 분포를 자세히 살펴보면 다음과 같습니다.

 

// 확률 보기위한 기능
data class BetaStats(
    val confidenceInterval: String,  // 95% 신뢰구간
    val samples: List<Double>        // 샘플링 결과들
)

fun calculateStats(impression: Double, click: Double): BetaStats {
    val alpha = click + 1
    val beta = (impression - click) + 1
    val distribution = BetaDistribution(alpha, beta)

    // 95% 신뢰구간 계산
    val lower = distribution.inverseCumulativeProbability(0.025) * 100
    val upper = distribution.inverseCumulativeProbability(0.975) * 100

    // 5회 샘플링
    val samples = List(5) { distribution.sample() }

    return BetaStats(
        confidenceInterval = "%.1f ~ %.1f".format(lower, upper),
        samples = samples
    )
}

test("Thompson Sampling의 확률적 특성 테스트") {
    println("=== 메인 페이지 추천 영역의 확률적 특성 분석 ===")

    // 특가 상품: 1000번 노출, 30번 클릭 (CTR 3.0%)
    val special = calculateStats(1000.0, 30.0)
    println("""
    |특가 상품 (노출: 1000, 클릭: 30, CTR: 3.0%)
    |95% 신뢰구간: ${special.confidenceInterval}%  // 데이터가 많아 좁은 구간
    |샘플링 점수 5회: [${special.samples.joinToString(", ") { "%.3f".format(it) }}]
    |""".trimMargin())

    // 개인화 추천: 100번 노출, 2번 클릭 (CTR 2.0%)
    val personalized = calculateStats(100.0, 2.0)
    println("""
    |개인화 추천 (노출: 100, 클릭: 2, CTR: 2.0%)
    |95% 신뢰구간: ${personalized.confidenceInterval}%  // 중간 정도의 구간
    |샘플링 점수 5회: [${personalized.samples.joinToString(", ") { "%.3f".format(it) }}]
    |""".trimMargin())

    // 연령대 기반: 10번 노출, 1번 클릭 (CTR 10.0%)
    val ageBased = calculateStats(10.0, 1.0)
    println("""
    |연령대 기반 (노출: 10, 클릭: 1, CTR: 10.0%)
    |95% 신뢰구간: ${ageBased.confidenceInterval}%  // 데이터가 적어 넓은 구간
    |샘플링 점수 5회: [${ageBased.samples.joinToString(", ") { "%.3f".format(it) }}]
    |""".trimMargin())
}

 

결과

=== 메인 페이지 추천 영역의 확률적 특성 분석 ===
특가 상품 (노출: 1000, 클릭: 30, CTR: 3.0%)
95% 신뢰구간: 2.1 ~ 4.3%  // 데이터가 많아 좁은 구간
샘플링 점수 5회: [0.036, 0.028, 0.031, 0.026, 0.024]

개인화 추천 (노출: 100, 클릭: 2, CTR: 2.0%)
95% 신뢰구간: 0.6 ~ 7.0%  // 중간 정도의 구간
샘플링 점수 5회: [0.028, 0.014, 0.047, 0.018, 0.020]

연령대 기반 (노출: 10, 클릭: 1, CTR: 10.0%)
95% 신뢰구간: 2.3 ~ 41.3%  // 데이터가 적어 넓은 구간
샘플링 점수 5회: [0.147, 0.070, 0.284, 0.325, 0.380]

 

베타 분포

1편에서 살펴본 베타 분포의 실제 그래프와 같은 값이 나오고 있습니다. ref) https://homepage.stat.uiowa.edu/~mbognar/applets/beta.html

 

네, 사실 Thompson Sampling은 특정 확률 분포(여기서는 Beta)를 활용하여 확률 분포의 Parameter을 설정하고 sample() 하는 게 MAB의 Thompson Sampling Score를 계산하는 것입니다. 

 

- 데이터가 많은 특가 상품은 안정적으로 비슷한 값을 내놓으면서 "활용"에 집중하고

- 데이터가 적은 연령대 기반은 과감하게 다양한 값을 시도하면서 "탐색"을 하죠

- 위 로직을 통해 그 중간 어딘가에서 적당히 균형을 잡고 계산하고 있습니다.

2.2. 상품 순서 최적화

이번에는 더 세밀한 최적화를 해보겠습니다. 만약 C안(연령대 기반)이 선택되었다고 가정해 볼까요?

이제 "비슷한 연령대가 좋아하는" 영역에 어떤 트레이닝팬츠를 먼저 보여줄지 결정해야 합니다.

 

예를 들어 다음과 같은 연령대 상품들이 있다고 해봅시다.

- 상품1 (그레이 트레이닝팬츠): 1000번 노출, 50번 클릭 (CTR 5.0%)

- 상품2 (블랙 트레이닝팬츠): 500번 노출, 15번 클릭 (CTR 3.0%)

- 상품3 (라이트 그레이 팬츠): 100번 노출, 8번 클릭 (CTR 8.0%)

- 상품4 (베이지 트레이닝팬츠): 50번 노출, 2번 클릭 (CTR 4.0%)

- 상품5 (블랙 조거 팬츠): 20번 노출, 3번 클릭 (CTR 15.0%)

- 상품6 (그레이 와이드 팬츠): 10번 노출, 1번 클릭 (CTR 10.0%)

 

단순히 CTR만 보면 상품5(블랙 조거 팬츠)가 가장 좋아 보이지만, 데이터가 충분하지 않아 불확실성이 큽니다.

Thompson Sampling score을 적용하여 정렬하면 어떤 순서가 좋을까요?

무신사 메인 페이지 - 추천 결과 중 하나 (상품 CTR 데이터는 가짜 입니다)

data class Item(
    val id: String,
    val name: String,
    val impressions: Double,
    val clicks: Double
)

fun getOptimalOrder(items: List<Item>): List<Item> {
    return items.map { item ->
        val score = ThompsonSamplingCalculator.calculateScore(
            item.impressions,
            item.clicks
        )
        item to score
    }.sortedByDescending { it.second }
        .map { it.first }
}

    test("25세 남성 세그먼트 트레이닝 팬츠 순서 최적화") {
        val items = listOf(
            Item("1", "그레이 트레이닝 팬츠", 1000.0, 50.0),
            Item("2", "블랙 트레이닝 팬츠", 500.0, 15.0),
            Item("3", "라이트 그레이 팬츠", 100.0, 8.0),
            Item("4", "베이지 트레이닝 팬츠", 50.0, 2.0),
            Item("5", "블랙 조거 팬츠", 20.0, 3.0),
            Item("6", "그레이 와이드 팬츠", 10.0, 1.0)
        )

        println("=== 25세 남성 세그먼트 트레이닝 팬츠 순서 최적화 ===")

        // 각 상품별 Thompson Sampling 점수 5회 계산
        items.forEach { item ->
            val stats = calculateStats(item.impressions, item.clicks)
            println("""
        |${item.name}
        |노출: ${item.impressions.toInt()}, 클릭: ${item.clicks.toInt()}, CTR: ${"%.1f".format(item.clicks/item.impressions*100)}%
        |95% 신뢰구간: ${stats.confidenceInterval}%
        |샘플링 점수 5회: [${stats.samples.joinToString(", ") { "%.3f".format(it) }}]
        |""".trimMargin())
        }

        // 최종 순서 결정 (5회 반복)
        println("\n=== 최종 순서 결정 (5회 반복) ===")
        repeat(5) { i ->
            val order = getOptimalOrder(items)
            println("${i+1}회차 순서: ${order.joinToString(" > ") { it.name }}")
        }
    }

 

이번 click, impression 데이터는 25세 / 남성으로 세그먼트를 나누어 aggregation 하였습니다.

 

결과

=== 25세 남성 세그먼트 트레이닝 팬츠 순서 최적화 ===
그레이 트레이닝 팬츠
노출: 1000, 클릭: 50, CTR: 5.0%
95% 신뢰구간: 3.8 ~ 6.5%
샘플링 점수 5회: [0.061, 0.047, 0.048, 0.054, 0.046]

블랙 트레이닝 팬츠
노출: 500, 클릭: 15, CTR: 3.0%
95% 신뢰구간: 1.8 ~ 4.9%
샘플링 점수 5회: [0.025, 0.042, 0.053, 0.024, 0.018]

라이트 그레이 팬츠
노출: 100, 클릭: 8, CTR: 8.0%
95% 신뢰구간: 4.2 ~ 15.0%
샘플링 점수 5회: [0.058, 0.056, 0.081, 0.117, 0.101]

베이지 트레이닝 팬츠
노출: 50, 클릭: 2, CTR: 4.0%
95% 신뢰구간: 1.2 ~ 13.5%
샘플링 점수 5회: [0.080, 0.068, 0.098, 0.012, 0.058]

블랙 조거 팬츠
노출: 20, 클릭: 3, CTR: 15.0%
95% 신뢰구간: 5.4 ~ 36.3%
샘플링 점수 5회: [0.181, 0.389, 0.185, 0.194, 0.063]

그레이 와이드 팬츠
노출: 10, 클릭: 1, CTR: 10.0%
95% 신뢰구간: 2.3 ~ 41.3%
샘플링 점수 5회: [0.215, 0.238, 0.184, 0.127, 0.164]


=== 최종 순서 결정 (5회 반복) ===
1회차 순서: 그레이 와이드 팬츠 > 블랙 조거 팬츠 > 라이트 그레이 팬츠 > 그레이 트레이닝 팬츠 > 베이지 트레이닝 팬츠 > 블랙 트레이닝 팬츠
2회차 순서: 블랙 조거 팬츠 > 그레이 와이드 팬츠 > 베이지 트레이닝 팬츠 > 라이트 그레이 팬츠 > 그레이 트레이닝 팬츠 > 블랙 트레이닝 팬츠
3회차 순서: 그레이 와이드 팬츠 > 블랙 조거 팬츠 > 라이트 그레이 팬츠 > 베이지 트레이닝 팬츠 > 그레이 트레이닝 팬츠 > 블랙 트레이닝 팬츠
4회차 순서: 라이트 그레이 팬츠 > 블랙 조거 팬츠 > 그레이 와이드 팬츠 > 그레이 트레이닝 팬츠 > 베이지 트레이닝 팬츠 > 블랙 트레이닝 팬츠
5회차 순서: 블랙 조거 팬츠 > 라이트 그레이 팬츠 > 베이지 트레이닝 팬츠 > 그레이 트레이닝 팬츠 > 블랙 트레이닝 팬츠 > 그레이 와이드 팬츠

 

단순히 CTR만 봤다면 블랙 조거 팬츠(CTR 15.0%)가 압도적인 1위였을 텐데, Thompson Sampling은 다른 선택을 보여줍니다. 블랙 조거 팬츠와 그레이 와이드 팬츠가 번갈아가며 1위를 차지하고, 때로는 라이트 그레이 팬츠도 상위권에 진입하는 모습을 보여줍니다.

 

 

Thompson Sampling의 특징을 잘 보여줍니다.

- 높은 CTR을 보인 상품들에게 더 많은 기회를 주면서도

- 데이터가 적은 상품들의 가능성도 열어두고

- 실제 클릭 데이터가 쌓이면서 점점 더 현실적인 선택을 하게 됩니다

 

위 테스트는 적은 양의 데이터로 진행했지만, 실제 운영 환경(트래픽이 많은 경우)에서는 충분한 데이터가 쌓이면서 분포가 더 뾰족해지고, 각 아이템의 실제 성과를 더 정확하게 반영하는 score가 계산됩니다.

 

위 두 가지 사례를 통해 Thompson Sampling이 다양하게 활용될 수 있습니다.

1. 상위 레벨 최적화 (레이아웃/섹션 최적화)
   - "오늘의 특가", "개인화 추천", "연령대 기반 추천" 등 큰 단위의 선택
   - 어떤 추천 방식이 사용자들에게 더 효과적인지 자동으로 학습
   
2. 아이템 레벨 최적화 (상품 순서 최적화)
   - 선택된 추천 방식 내에서 개별 상품들의 순서 결정
   - 적은 데이터의 신상품부터 충분한 데이터의 베스트 상품까지 균형 있게 노출

이처럼 Thompson Sampling은 상황에 따라 유연하게 적용할 수 있습니다. 그렇다면 이런 알고리즘을 실제 서비스에 적용할 때는 어떤 점들을 고려해야 할까요?

2.3. 성능 최적화와 운영

실제 서비스에 적용할 때 고려할 점이 무엇일까요?

2.3.1. score 계산 시기 (실시간 처리 vs 배치 처리)

1. 실시간 처리

추천 요청이 올 때마다 실시간으로 score 계산합니다.

장점 

- 항상 최신 데이터 반영 가능

- 새로운 상품이 추가되면 즉시 반영

- 특정 시점의 트래픽 급증에도 정확한 score 계산

 

단점

- 매 요청마다 베타 분포 계산 필요

- 트래픽이 많을 경우 서버 부하 증가

- 응답 시간이 길어질 수 있음

 

2. 배치 처리

일정 주기(예: 5분)마다 score를 미리 계산해서 캐시에 저장

장점

- 빠른 응답 시간 (캐시 된 값 즉시 반환)

- 서버 부하 분산 가능

- 안정적인 운영 가능

 

단점

- 최신 데이터가 즉시 반영되지 않음

- 캐시 저장소 필요

- 배치 작업 실패 시 대응 방안 필요

 

안정성을 위해 배치 처리가 좋을 수 있습니다. 5분 정도의 데이터 지연은 대부분의 서비스에서 허용 가능한 수준이고, 안정적인 운영이 가능하기 때문입니다.

 

다만 다음과 같은 상황에서는 실시간 계산도 고려해 볼 수 있습니다.

- 세일처럼 짧은 시간에 집중된 트래픽이 발생하는 경우

- 새로운 상품이 자주 추가되는 경우

- 실시간성이 매우 중요한 경우

 

운영적으로 하이브리드 방식을 선택하여 기본적으로는 5분 배치로 운영하되, 특정 조건(예: 신상품, VIP 고객)에는 실시간 계산을 할 수 있을 것 같습니다.

2.3.2. 추가 고려 사항

score 계산 시기 외에도 추가 고려사항이 있습니다.

 

1. 데이터 관리

- 얼마나 오래된 데이터까지 사용할지

- 세그먼트별로 데이터를 나눌지, 통합할지

- 디바이스나 시간대별로 구분할지

 

2. 이상 징후 대응

- CTR이 갑자기 떨어지는 경우

- 특정 상품에 트래픽이 쏠리는 현상

- 자동 알림과 대응 방안

 

3. 콜드 스타트

- 신규 상품은 어떻게 노출해줄지

- 최소 노출 수는 얼마나 보장할지

 

이처럼 실제 서비스에서는 생각보다 더 많은 것들을 고려해야 합니다. 특히 시간대, 요일, 디바이스 등 다양한 컨텍스트를 동시에 고려해야 하는 경우가 많은데, 지금까지 살펴본 기본적인 Thompson Sampling으로는 이런 요구사항을 모두 충족하기 어렵죠.

 

그렇다면 이런 한계를 어떻게 극복할 수 있을까요? 


3. 마치며

지금까지 Thompson Sampling의 개념부터 실제 구현, 그리고 운영 시 고려사항까지 살펴보았습니다.

 

Thompson Sampling은

- 상위 레벨(레이아웃/섹션)부터 아이템 레벨까지 다양하게 활용 가능하고

- 데이터 양에 따라 자연스럽게 '탐색'과 '활용'의 균형을 맞추며

- 실제 서비스에서도 비교적 쉽게 적용할 수 있는 알고리즘입니다

 

하지만 실제 서비스에서는 더 복잡한 요구사항들이 있습니다.

- 시간대별로 다른 선호도

- 디바이스별로 다른 사용 패턴

- 날씨나 계절성까지 고려해야 하는 상황

 

지금까지 살펴본 MAB 알고리즘들(Thompson Sampling, ε-Greedy, UCB)은 사실 context-free 방식입니다. 즉, 상황이나 환경에 관계없이 오직 노출수와 클릭수만을 기반으로 판단하죠.

 

이런 상황에서 사용할 수 있는 것이 바로 'Contextual Bandit'입니다. 다양한 상황(컨텍스트)을 고려하여 선택하는 방법입니다.

 

예를 들어

- 아침에 접속한 사용자와 저녁에 접속한 사용자를 다르게 대응

- 모바일과 PC 사용자를 구분해서 각각 최적화

- 날씨나 계절에 따라 다른 전략 적용

의 Context를 활용할 수 있습니다.

 

Thompson Sampling을 포함한 context-free MAB도 적재적소에 활용하면 충분히 강력한 도구가 될 수 있습니다. 결국 서비스의 특성과 요구사항을 잘 파악하고, 그에 맞는 적절한 방식을 선택하는 것이 중요하겠죠.

 

앞으로 기회가 된다면 Contextual Bandit에 대해서도 자세히 다뤄보도록 하겠습니다. (미래의 제게 맡깁니다...)

 

긴 글 읽어주셔서 감사합니다. 😄


Reference

- Contextual Multi-Armed Bandits

  - https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/37042.pdf

- Wiki - Multi-armed bandit

- Multi-Arm Bandits for recommendations and A/B testing on Amazon ratings data set

- Beta Distribution 그래프 시각화 

- The Beta Distribution 설명

- 무신사 메인 페이지

  - https://www.musinsa.com/main/musinsa/recommend

 

 
 
 
 
 
 
 

댓글