Technology

A/B Testing ML Production Setup Guide — ระบบทดสอบ ML Model ใน Production

ab testing ml production setup guide
A/B Testing ML Production Setup Guide | SiamCafe Blog
2025-07-11· อ. บอม — SiamCafe.net· 11,294 คำ

A/B Testing สำหรับ ML Model คืออะไร

A/B Testing ในบริบทของ Machine Learning Production คือการทดสอบ ML Model หลายเวอร์ชันพร้อมกันโดยแบ่ง traffic จริงจากผู้ใช้ไปยังแต่ละ model แล้ววัดผลลัพธ์ด้วยวิธีทางสถิติเพื่อตัดสินใจว่า model ไหนทำงานได้ดีกว่า

ความแตกต่างจาก A/B Testing ของเว็บไซต์ทั่วไปคือ ML A/B Testing ต้องพิจารณา metrics เฉพาะทางเช่น Prediction Accuracy, Latency, Model Drift และ Business Metrics เช่น Conversion Rate หรือ Revenue per User นอกจากนี้ยังต้องจัดการกับ Data Feedback Loop ที่ผลจากการ predict อาจส่งผลกระทบต่อข้อมูลที่ใช้ train model ต่อไป

ระบบ A/B Testing สำหรับ ML Production ประกอบด้วย 4 ส่วนหลักคือ Model Registry ที่เก็บ model ทุกเวอร์ชัน, Model Serving Layer ที่ serve prediction requests, Traffic Router ที่แบ่ง traffic ระหว่าง model versions และ Metrics Collection ที่เก็บผลลัพธ์สำหรับวิเคราะห์ทางสถิติ

การออกแบบ A/B Test ที่ดีต้องกำหนด Hypothesis ให้ชัดเจน เลือก Primary Metric ที่สะท้อน business value เลือก Sample Size ที่มากพอสำหรับ Statistical Significance และกำหนดระยะเวลาที่เหมาะสมสำหรับการทดสอบ

สถาปัตยกรรมระบบ A/B Testing สำหรับ ML Production

สถาปัตยกรรมที่แนะนำใช้ Kubernetes เป็น platform หลักร่วมกับ Seldon Core สำหรับ model serving และ Istio สำหรับ traffic management

# สถาปัตยกรรมของระบบ
# Client → Istio Gateway → VirtualService (Traffic Split)
#                              ├── Model A (v1) - 80% traffic
#                              └── Model B (v2) - 20% traffic
#                                      ↓
#                              Prometheus (Metrics)
#                                      ↓
#                              Grafana (Dashboard)
#                                      ↓
#                              Statistical Analysis Job
#                                      ↓
#                              MLflow (Model Registry)

# ติดตั้ง Seldon Core บน Kubernetes
helm repo add seldon https://storage.googleapis.com/seldon-charts
helm repo update

kubectl create namespace seldon-system
helm install seldon-core seldon/seldon-core-operator \
  --namespace seldon-system \
  --set usageMetrics.enabled=true \
  --set istio.enabled=true \
  --set istio.gateway=istio-system/seldon-gateway

# ตรวจสอบการติดตั้ง
kubectl get pods -n seldon-system
# NAME                                        READY   STATUS
# seldon-controller-manager-xxx-yyy            1/1     Running

# ติดตั้ง Istio (ถ้ายังไม่มี)
istioctl install --set profile=default -y
kubectl label namespace default istio-injection=enabled

ตั้งค่า MLflow เป็น Model Registry สำหรับเก็บและจัดการ model versions

# docker-compose.yml สำหรับ MLflow
version: "3.9"
services:
  mlflow:
    image: ghcr.io/mlflow/mlflow:v2.12.1
    ports:
      - "5000:5000"
    environment:
      MLFLOW_BACKEND_STORE_URI: postgresql://mlflow:password@postgres:5432/mlflow
      MLFLOW_DEFAULT_ARTIFACT_ROOT: s3://mlflow-artifacts/
      AWS_ACCESS_KEY_ID: 
      AWS_SECRET_ACCESS_KEY: 
    command: >
      mlflow server
      --host 0.0.0.0
      --port 5000
      --backend-store-uri postgresql://mlflow:password@postgres:5432/mlflow
      --default-artifact-root s3://mlflow-artifacts/
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: mlflow
      POSTGRES_USER: mlflow
      POSTGRES_PASSWORD: password
    volumes:
      - mlflow_db:/var/lib/postgresql/data

volumes:
  mlflow_db:

# Register model version
# python3 -c "
# import mlflow
# mlflow.set_tracking_uri('http://localhost:5000')
# mlflow.register_model('runs:/RUN_ID/model', 'recommendation-model')
# "

ติดตั้ง ML Model Serving ด้วย Seldon Core

สร้าง SeldonDeployment ที่ serve model 2 เวอร์ชันพร้อมกันสำหรับ A/B Testing

# ab-test-deployment.yaml
apiVersion: machinelearning.seldon.io/v1
kind: SeldonDeployment
metadata:
  name: recommendation-ab-test
  namespace: default
spec:
  predictors:
    - name: model-a
      replicas: 3
      traffic: 80
      annotations:
        seldon.io/engine-separate-pod: "true"
      componentSpecs:
        - spec:
            containers:
              - name: model-a
                image: myregistry/recommendation-model:v1.2.0
                resources:
                  requests:
                    memory: "1Gi"
                    cpu: "500m"
                  limits:
                    memory: "2Gi"
                    cpu: "1000m"
                env:
                  - name: MODEL_VERSION
                    value: "v1.2.0"
                  - name: MLFLOW_TRACKING_URI
                    value: "http://mlflow:5000"
                readinessProbe:
                  httpGet:
                    path: /health/status
                    port: 9000
                  initialDelaySeconds: 10
      graph:
        name: model-a
        type: MODEL
        endpoint:
          type: REST

    - name: model-b
      replicas: 2
      traffic: 20
      componentSpecs:
        - spec:
            containers:
              - name: model-b
                image: myregistry/recommendation-model:v2.0.0
                resources:
                  requests:
                    memory: "1Gi"
                    cpu: "500m"
                  limits:
                    memory: "2Gi"
                    cpu: "1000m"
                env:
                  - name: MODEL_VERSION
                    value: "v2.0.0"
                  - name: MLFLOW_TRACKING_URI
                    value: "http://mlflow:5000"
      graph:
        name: model-b
        type: MODEL
        endpoint:
          type: REST

# Deploy
# kubectl apply -f ab-test-deployment.yaml

# ตรวจสอบสถานะ
# kubectl get seldondeployments
# kubectl get pods -l seldon-deployment-id=recommendation-ab-test

ทดสอบ prediction endpoint

# ส่ง prediction request
curl -X POST "http://istio-gateway/seldon/default/recommendation-ab-test/api/v1.0/predictions" \
  -H "Content-Type: application/json" \
  -d '{
    "data": {
      "ndarray": [[25, "male", "electronics", 3, 150.0]]
    }
  }'

# Response จะมี metadata บอกว่าถูก route ไป model ไหน
# {
#   "data": {"ndarray": [[0.85, 0.12, 0.03]]},
#   "meta": {
#     "routing": {"model-a": 1},
#     "requestPath": {"model-a": "myregistry/recommendation-model:v1.2.0"}
#   }
# }

# ทดสอบด้วย load test เพื่อตรวจสอบ traffic distribution
for i in $(seq 1 100); do
  curl -s -X POST "http://istio-gateway/seldon/default/recommendation-ab-test/api/v1.0/predictions" \
    -H "Content-Type: application/json" \
    -d '{"data":{"ndarray":[[25,"male","electronics",3,150.0]]}}' | \
    python3 -c "import sys, json; d=json.load(sys.stdin); print(list(d['meta']['routing'].keys())[0])"
done | sort | uniq -c
# Output:
#   81 model-a
#   19 model-b

ตั้งค่า Traffic Splitting ระหว่าง Model Version

ใช้ Istio VirtualService สำหรับ traffic management ที่ละเอียดกว่า Seldon built-in routing

# istio-virtual-service.yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: recommendation-ab-test
spec:
  hosts:
    - recommendation-ab-test.default.svc.cluster.local
  http:
    - match:
        - headers:
            x-model-version:
              exact: "v2"
      route:
        - destination:
            host: recommendation-ab-test-model-b
            port:
              number: 8000
    - route:
        - destination:
            host: recommendation-ab-test-model-a
            port:
              number: 8000
          weight: 80
        - destination:
            host: recommendation-ab-test-model-b
            port:
              number: 8000
          weight: 20
---
# DestinationRule สำหรับ circuit breaking
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: recommendation-circuit-breaker
spec:
  host: recommendation-ab-test-model-b
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 100
      http:
        h2UpgradePolicy: DEFAULT
        http1MaxPendingRequests: 50
        http2MaxRequests: 100
    outlierDetection:
      consecutive5xxErrors: 5
      interval: 30s
      baseEjectionTime: 60s
      maxEjectionPercent: 50

สร้าง Script สำหรับปรับ traffic weight แบบ gradual

#!/usr/bin/env python3
# adjust_traffic.py — ปรับ traffic split แบบ gradual
import subprocess
import json
import time
import sys

def get_current_weights():
    result = subprocess.run(
        ["kubectl", "get", "seldondeployment", "recommendation-ab-test",
         "-o", "json"],
        capture_output=True, text=True
    )
    spec = json.loads(result.stdout)
    predictors = spec["spec"]["predictors"]
    return {p["name"]: p["traffic"] for p in predictors}

def set_weights(model_a_weight, model_b_weight):
    patch = json.dumps({
        "spec": {
            "predictors": [
                {"name": "model-a", "traffic": model_a_weight},
                {"name": "model-b", "traffic": model_b_weight}
            ]
        }
    })
    subprocess.run([
        "kubectl", "patch", "seldondeployment", "recommendation-ab-test",
        "--type=merge", "-p", patch
    ])
    print(f"Updated: model-a={model_a_weight}%, model-b={model_b_weight}%")

def gradual_increase(target_b_weight, step=10, interval_minutes=30):
    current = get_current_weights()
    current_b = current.get("model-b", 20)

    while current_b < target_b_weight:
        current_b = min(current_b + step, target_b_weight)
        current_a = 100 - current_b
        set_weights(current_a, current_b)
        if current_b < target_b_weight:
            print(f"Waiting {interval_minutes} minutes before next increase...")
            time.sleep(interval_minutes * 60)

if __name__ == "__main__":
    target = int(sys.argv[1]) if len(sys.argv) > 1 else 50
    gradual_increase(target)

เก็บ Metrics และวิเคราะห์ผลด้วย Statistical Test

ตั้งค่า Prometheus เพื่อเก็บ metrics จาก Seldon Core และสร้าง Statistical Analysis Job

# prometheus-seldon-rules.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: seldon-ab-test-rules
spec:
  groups:
    - name: seldon-ab-test
      rules:
        - record: seldon_model_prediction_latency_p99
          expr: |
            histogram_quantile(0.99,
              sum(rate(seldon_api_executor_client_requests_seconds_bucket{
                deployment_name="recommendation-ab-test"
              }[5m])) by (le, predictor_name)
            )
        - record: seldon_model_error_rate
          expr: |
            sum(rate(seldon_api_executor_client_requests_seconds_count{
              deployment_name="recommendation-ab-test",
              code!="200"
            }[5m])) by (predictor_name)
            /
            sum(rate(seldon_api_executor_client_requests_seconds_count{
              deployment_name="recommendation-ab-test"
            }[5m])) by (predictor_name)

---
# statistical_analysis.py — วิเคราะห์ผล A/B Test
import numpy as np
from scipy import stats
import requests
import json

PROMETHEUS_URL = "http://prometheus:9090"

def query_prometheus(query, start, end, step="1h"):
    r = requests.get(f"{PROMETHEUS_URL}/api/v1/query_range", params={
        "query": query,
        "start": start,
        "end": end,
        "step": step
    })
    return r.json()["data"]["result"]

def get_conversion_data(model_name, days=7):
    query = f'sum(seldon_prediction_conversion{{predictor_name="{model_name}"}})'
    total_query = f'sum(seldon_prediction_total{{predictor_name="{model_name}"}})'
    # ดึงข้อมูลจาก business metrics database
    conversions = 1250  # ตัวอย่าง
    total = 5000
    return conversions, total

def run_ab_test_analysis():
    conv_a, total_a = get_conversion_data("model-a")
    conv_b, total_b = get_conversion_data("model-b")

    rate_a = conv_a / total_a
    rate_b = conv_b / total_b

    # Two-proportion z-test
    p_pool = (conv_a + conv_b) / (total_a + total_b)
    se = np.sqrt(p_pool * (1 - p_pool) * (1/total_a + 1/total_b))
    z_stat = (rate_b - rate_a) / se
    p_value = 2 * (1 - stats.norm.cdf(abs(z_stat)))

    # Effect size (Cohen's h)
    h = 2 * np.arcsin(np.sqrt(rate_b)) - 2 * np.arcsin(np.sqrt(rate_a))

    print(f"=== A/B Test Results ===")
    print(f"Model A: {rate_a:.4f} ({conv_a}/{total_a})")
    print(f"Model B: {rate_b:.4f} ({conv_b}/{total_b})")
    print(f"Lift: {((rate_b - rate_a) / rate_a * 100):.2f}%")
    print(f"Z-statistic: {z_stat:.4f}")
    print(f"P-value: {p_value:.6f}")
    print(f"Cohen's h: {h:.4f}")
    print(f"Significant (p<0.05): {'YES' if p_value < 0.05 else 'NO'}")

    return {
        "winner": "model-b" if (p_value < 0.05 and rate_b > rate_a) else "model-a",
        "p_value": p_value,
        "lift": (rate_b - rate_a) / rate_a * 100,
        "significant": p_value < 0.05
    }

if __name__ == "__main__":
    result = run_ab_test_analysis()
    if result["significant"]:
        print(f"\nWINNER: {result['winner']} with {result['lift']:.2f}% lift")
    else:
        print("\nNo significant difference detected. Continue testing.")

Automate Model Promotion ด้วย CI/CD Pipeline

สร้าง GitHub Actions Pipeline ที่ promote model อัตโนมัติเมื่อ A/B Test มี statistical significance

# .github/workflows/ml-ab-test-promote.yml
name: ML A/B Test Auto-Promote

on:
  schedule:
    - cron: '0 6 * * *'  # รันทุกวัน 6 โมงเช้า
  workflow_dispatch:

jobs:
  analyze-and-promote:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install dependencies
        run: pip install numpy scipy requests mlflow kubernetes

      - name: Run Statistical Analysis
        id: analysis
        env:
          PROMETHEUS_URL: }
          MLFLOW_TRACKING_URI: }
        run: |
          python3 statistical_analysis.py > result.json
          WINNER=$(python3 -c "import json; print(json.load(open('result.json'))['winner'])")
          SIGNIFICANT=$(python3 -c "import json; print(json.load(open('result.json'))['significant'])")
          echo "winner=$WINNER" >> $GITHUB_OUTPUT
          echo "significant=$SIGNIFICANT" >> $GITHUB_OUTPUT

      - name: Promote Winner Model
        if: steps.analysis.outputs.significant == 'True'
        env:
          KUBECONFIG_DATA: }
        run: |
          echo "$KUBECONFIG_DATA" | base64 -d > /tmp/kubeconfig
          export KUBECONFIG=/tmp/kubeconfig
          WINNER="}"
          echo "Promoting $WINNER to 100% traffic"
          kubectl patch seldondeployment recommendation-ab-test \
            --type=merge \
            -p "{\"spec\":{\"predictors\":[{\"name\":\"$WINNER\",\"traffic\":100}]}}"

      - name: Notify Slack
        if: always()
        uses: slackapi/slack-github-action@v1
        with:
          payload: |
            {
              "text": "ML A/B Test Result: Winner=}, Significant=}"
            }
        env:
          SLACK_WEBHOOK_URL: }

FAQ คำถามที่พบบ่อย

Q: A/B Testing กับ Multi-Armed Bandit ต่างกันอย่างไร?

A: A/B Testing แบ่ง traffic คงที่ตลอดการทดสอบ (เช่น 80/20) แล้วใช้ statistical test วิเคราะห์ผลตอนจบ ส่วน Multi-Armed Bandit ปรับ traffic แบบ dynamic ตาม performance ที่สังเกตได้ระหว่างทดสอบ ข้อดีของ Bandit คือลด regret (สูญเสียจากการส่ง traffic ไป model ที่แย่กว่า) แต่ A/B Testing ให้ผลทางสถิติที่ชัดเจนและตีความง่ายกว่า

Q: ต้องใช้ sample size เท่าไหร่ถึงจะเพียงพอ?

A: ขึ้นอยู่กับ Minimum Detectable Effect (MDE) ที่ต้องการตรวจจับ ถ้าต้องการตรวจจับ lift 1% ด้วย statistical power 80% และ significance level 5% จะต้องใช้ประมาณ 15,000-20,000 samples ต่อ group ใช้ online sample size calculator หรือคำนวณด้วย scipy.stats.power

Q: จะจัดการ Feature Interaction ระหว่าง A/B Test หลายตัวอย่างไร?

A: ใช้ Layered Experiment Framework แบบที่ Google อธิบายใน paper "Overlapping Experiment Infrastructure" แบ่ง traffic เป็น layers แต่ละ layer รัน experiment อิสระจากกัน หรือใช้ Factorial Design ที่ทดสอบหลาย factors พร้อมกันเพื่อวัด interaction effects

Q: ML Model A/B Testing ต่างจากการทดสอบ UI อย่างไร?

A: ML A/B Testing ต้องพิจารณา metrics เพิ่มเติมเช่น Prediction Latency, Model Drift, Feature Distribution Shift และ Feedback Loop Effects นอกจากนี้ผลลัพธ์จาก ML model อาจใช้เวลานานกว่าจะเห็นผลกระทบต่อ business metrics ทำให้ต้องรัน test นานกว่าการทดสอบ UI ทั่วไป

📖 บทความที่เกี่ยวข้อง

PHP Pest Testing Production Setup Guideอ่านบทความ → Proxmox VE Cluster Production Setup Guideอ่านบทความ → Htmx Alpine.js Production Setup Guideอ่านบทความ → Spark Structured Streaming Production Setup Guideอ่านบทความ → Azure Cosmos DB Production Setup Guideอ่านบทความ →

📚 ดูบทความทั้งหมด →