Offline RL for Safe Policy Learning

기존 서비스 로그에서 안전하게 최적 정책 학습

온라인 탐색이 위험한 환경(의료, 금융, 서비스)에서 기존 로그 데이터만으로 최적 정책을 학습하는 Offline RL을 다룬다. Distribution Shift 문제, Importance Sampling, Conservative Q-Learning(CQL), BCQ의 원리와 구현, Off-Policy Evaluation, 그리고 A/B → Bandit → DTR → Offline RL 진화 경로를 정리한다.

Statistics
Reinforcement Learning
저자

Kwangmin Kim

공개

2026년 03월 08일

1 Offline RL for Safe Policy Learning

1.1 왜 Offline RL인가

1.1.1 온라인 탐색의 위험

온라인 RL(Q-learning, DQN 등)은 학습 중 탐색이 필수다. 이는 일부 사용자에게 의도적으로 나쁜(또는 알 수 없는) 행동을 적용한다는 의미다.

온라인 RL의 탐색 비용:

의료: 환자에게 검증되지 않은 투약 → 생명 위험
금융: 고객에게 검증되지 않은 투자 전략 → 재산 손실
서비스: 사용자에게 나쁜 프롬프트 전략 → 이탈, 불만족

ε-greedy (ε=0.1):
  → 전체 상호작용의 10%에서 무작위 행동
  → 10,000명 중 1,000명이 무작위 전략 노출
  → 서비스 규모에서 허용 불가

1.1.2 Offline RL의 핵심 아이디어

기존에 수집된 로그 데이터만으로 최적 정책을 학습:

\[ \mathcal{D} = \{(s_i, a_i, r_i, s_i')\}_{i=1}^{N} \]

  • 데이터는 행동 정책(behavior policy) \(\mu\)로 수집됨
  • 학습 목표: \(\mu\)와 다른 목표 정책(target policy) \(\pi\)를 찾아 성능 개선
  • 환경과 추가 상호작용 없이 \(\mathcal{D}\)만 사용
기존 서비스 로그:
  6개월간 수집된 (사용자 상태, 적용된 전략, 만족도, 다음 상태)
  행동 정책 μ: 규칙 기반 시스템 (세그먼트별 고정 전략)

Offline RL 목표:
  이 로그에서 μ보다 나은 π를 학습
  → 새로운 탐색 없이, 기존 데이터에서 최대한의 정보 추출

1.2 Distribution Shift 문제

1.2.1 핵심 문제: 데이터에 없는 (s, a) 쌍

Offline RL의 가장 큰 도전은 distribution shift다.

\[ \pi(a|s) \neq \mu(a|s) \implies (s, a) \text{ 쌍의 분포가 다름} \]

행동 정책 μ의 데이터:
  SI 세그먼트 → 항상 "기본" 전략 적용
  → (SI, 공감형)이나 (SI, 단계별) 데이터가 거의 없음

문제:
  Q(SI, 공감형)을 추정할 데이터가 부족
  → Q가 부정확 → 과대 추정 가능
  → 학습된 정책이 "데이터에 없는 행동"을 자신 있게 선택
  → 실제 적용 시 실패

1.2.2 Q값 과대 추정의 메커니즘

\[ \hat{Q}(s, a) = r + \gamma \max_{a'} \hat{Q}(s', a') \]

\(\max\) 연산자가 노이즈를 양의 방향으로 증폭:

데이터에 있는 행동:  Q 추정이 비교적 정확 (데이터로 검증됨)
데이터에 없는 행동:  Q 추정이 노이즈 투성이
                    → max가 노이즈의 최대값을 선택
                    → 과대 추정
                    → 정책이 "데이터에 없는, Q가 높은" 행동을 선택
                    → 실제로는 나쁜 행동

1.3 Importance Sampling

1.3.1 기본 Off-Policy 보정

가장 단순한 접근: 행동 정책 \(\mu\)의 데이터로 목표 정책 \(\pi\)의 가치를 추정.

\[ \hat{V}^{\pi} = \frac{1}{n} \sum_{i=1}^{n} \frac{\pi(a_i | s_i)}{\mu(a_i | s_i)} r_i \]

  • \(\pi(a|s) / \mu(a|s)\): 중요도 비율(importance weight)
  • 직관: \(\mu\)가 잘 선택하지 않는 행동의 보상에 큰 가중치를 부여하여 \(\pi\) 하에서의 기대값을 복원

1.3.2 순차적 의사결정에서의 IS

에피소드 전체에 대한 importance weight:

\[ w_i = \prod_{t=0}^{T} \frac{\pi(a_t^{(i)} | s_t^{(i)})}{\mu(a_t^{(i)} | s_t^{(i)})} \]

1.3.3 높은 분산 문제

문제: 궤적이 길수록 w_i의 분산이 기하급수적으로 증가

예시 (T=8, 행동 3가지):
  π가 선택하는 행동을 μ가 33% 확률로 선택한다면:
  w = (1/0.33)^8 ≈ 6561   (한 궤적의 가중치)

  대부분의 궤적: w ≈ 0 (거의 무시됨)
  극소수 궤적: w >> 1 (극단적 가중치)
  → 추정량이 소수 궤적에 의존 → 분산 폭발

해결책: - Weighted IS (WIS): \(w_i\)를 정규화하여 분산 감소 - Per-Decision IS: 단계별로 IS 적용, 미래 가중치 분리 - 그러나 근본적 한계 존재 → 가치 함수 기반 방법(CQL, BCQ) 선호


1.4 Conservative Q-Learning (CQL)

1.4.1 핵심 아이디어

데이터에 없는 행동의 Q값을 보수적으로 낮게 추정하여 distribution shift 방지.

1.4.2 CQL 손실 함수

\[ \mathcal{L}_{\text{CQL}} = \underbrace{\alpha \left( \mathbb{E}_{s \sim \mathcal{D}} \left[ \log \sum_a \exp Q(s, a) \right] - \mathbb{E}_{(s,a) \sim \mathcal{D}} [Q(s, a)] \right)}_{\text{CQL penalty}} + \underbrace{\frac{1}{2} \mathbb{E}_{(s,a,r,s') \sim \mathcal{D}} \left[ (Q(s,a) - \hat{\mathcal{B}}^\pi Q(s,a))^2 \right]}_{\text{Bellman error}} \]

CQL penalty 분해:

\[ \underbrace{\log \sum_a \exp Q(s, a)}_{\text{모든 행동의 Q를 올리면 페널티}} - \underbrace{Q(s, a_{\text{data}})}_{\text{데이터에 있는 행동의 Q는 유지}} \]

  • \(\text{logsumexp}\): 모든 행동의 Q를 높이면 증가 → 억제
  • \(-Q(s, a_{\text{data}})\): 데이터에 있는 행동의 Q는 높여도 됨
  • 효과: 데이터에 없는 행동의 Q만 선택적으로 낮춤

1.4.3 α 하이퍼파라미터

\[ \alpha \text{ 크면:} \quad \text{매우 보수적 (데이터에 있는 행동만 선택)} \] \[ \alpha \text{ 작으면:} \quad \text{덜 보수적 (일부 외삽 허용)} \] \[ \alpha = 0: \quad \text{일반 Q-learning (Offline에서 발산 위험)} \]

실무에서 \(\alpha \in [0.1, 5.0]\) 범위로 탐색, 보통 \(\alpha \in [1.0, 2.0]\)에서 시작.


1.5 Batch Constrained Q-learning (BCQ)

1.5.1 핵심: 데이터에 있는 행동만 고려

CQL이 Q값을 낮추는 방식이라면, BCQ는 행동 선택 자체를 제한:

\[ \pi(s) = \arg\max_{a: \hat{\mu}(a|s) > \tau} Q(s, a) \]

  • \(\hat{\mu}(a|s)\): 행동 정책의 추정 (생성 모델 또는 분류기)
  • \(\tau\): 임계값 — 데이터에서 충분히 관찰된 행동만 후보
  • 효과: 데이터에 없는 행동은 아예 후보에서 제외
BCQ의 직관:
  데이터에서 "SI 세그먼트 + 공감형"이 거의 없으면:
  → μ̂(공감형|SI) < τ
  → 공감형은 후보에서 제외
  → Q(SI, 공감형)이 아무리 높아도 선택되지 않음
  → 안전한 행동만 선택

1.5.2 CQL vs BCQ 비교

측면 CQL BCQ
접근 Q값을 보수적으로 행동 후보를 제한
추가 모델 없음 행동 정책 \(\hat{\mu}\) 추정 필요
외삽 허용 약간 (α 조절) 거의 없음
성능 일반적으로 우세 보수적이지만 안전
구현 복잡도 손실 함수 추가 생성 모델 추가

1.6 PyTorch 구현: CQLAgent 전체 코드

import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
from collections import deque
import random

class QNetwork(nn.Module):
    """Q-function 신경망"""
    def __init__(self, state_dim, n_actions, hidden_dims=(256, 128)):
        super().__init__()
        layers = []
        prev_dim = state_dim
        for h in hidden_dims:
            layers.extend([nn.Linear(prev_dim, h), nn.ReLU()])
            prev_dim = h
        layers.append(nn.Linear(prev_dim, n_actions))
        self.net = nn.Sequential(*layers)

    def forward(self, x):
        return self.net(x)


class CQLAgent:
    """
    Conservative Q-Learning for Offline RL

    CQL penalty: logsumexp(Q(s, ·)) - Q(s, a_data)
    → 데이터에 없는 행동의 Q를 보수적으로 추정
    """
    def __init__(self, state_dim, n_actions, alpha=1.0, gamma=0.99,
                 lr=3e-4, tau=0.005):
        self.n_actions = n_actions
        self.alpha = alpha           # CQL 보수성 강도
        self.gamma = gamma
        self.tau = tau               # Target network soft update 비율

        # Q-network 및 Target network
        self.q_net = QNetwork(state_dim, n_actions)
        self.target_net = QNetwork(state_dim, n_actions)
        self.target_net.load_state_dict(self.q_net.state_dict())

        self.optimizer = torch.optim.Adam(self.q_net.parameters(), lr=lr)

        # 학습 통계
        self.train_steps = 0
        self.loss_history = {"bellman": [], "cql": [], "total": []}

    def select_action(self, state, greedy=True):
        """학습된 정책으로 행동 선택"""
        with torch.no_grad():
            state_t = torch.FloatTensor(state).unsqueeze(0)
            q_values = self.q_net(state_t)
            return q_values.argmax(dim=1).item()

    def train_step(self, batch):
        """CQL 학습 스텝"""
        states, actions, rewards, next_states, dones = batch

        states = torch.FloatTensor(states)
        actions = torch.LongTensor(actions)
        rewards = torch.FloatTensor(rewards)
        next_states = torch.FloatTensor(next_states)
        dones = torch.FloatTensor(dones)

        # --- Bellman Loss ---
        q_values = self.q_net(states)
        q_selected = q_values.gather(1, actions.unsqueeze(1)).squeeze(1)

        with torch.no_grad():
            next_q = self.target_net(next_states).max(1)[0]
            target = rewards + self.gamma * next_q * (1 - dones)

        bellman_loss = F.mse_loss(q_selected, target)

        # --- CQL Penalty ---
        # logsumexp(Q(s, ·)): 모든 행동의 Q를 높이면 페널티
        logsumexp_q = torch.logsumexp(q_values, dim=1).mean()

        # Q(s, a_data): 데이터에 있는 행동의 Q는 유지
        q_data = q_values.gather(1, actions.unsqueeze(1)).squeeze(1).mean()

        cql_loss = logsumexp_q - q_data

        # --- Total Loss ---
        total_loss = bellman_loss + self.alpha * cql_loss

        self.optimizer.zero_grad()
        total_loss.backward()

        # Gradient clipping for stability
        torch.nn.utils.clip_grad_norm_(self.q_net.parameters(), max_norm=1.0)
        self.optimizer.step()

        # Target network soft update
        self._soft_update()

        # 기록
        self.train_steps += 1
        self.loss_history["bellman"].append(bellman_loss.item())
        self.loss_history["cql"].append(cql_loss.item())
        self.loss_history["total"].append(total_loss.item())

        return total_loss.item()

    def _soft_update(self):
        """Target network 소프트 업데이트: θ' ← τθ + (1-τ)θ'"""
        for param, target_param in zip(
            self.q_net.parameters(), self.target_net.parameters()
        ):
            target_param.data.copy_(
                self.tau * param.data + (1 - self.tau) * target_param.data
            )


# --- 오프라인 데이터 로딩 및 학습 ---

class OfflineDataset:
    """서비스 로그를 오프라인 학습용으로 관리"""
    def __init__(self, data_path=None):
        self.buffer = []

    def load_from_logs(self, logs):
        """
        logs: list of dicts with keys:
          state, action, reward, next_state, done
        """
        for log in logs:
            self.buffer.append((
                np.array(log["state"]),
                log["action"],
                log["reward"],
                np.array(log["next_state"]),
                float(log["done"])
            ))

    def sample(self, batch_size):
        batch = random.sample(self.buffer, min(batch_size, len(self.buffer)))
        states, actions, rewards, next_states, dones = zip(*batch)
        return (
            np.array(states),
            np.array(actions),
            np.array(rewards),
            np.array(next_states),
            np.array(dones)
        )

    def __len__(self):
        return len(self.buffer)


# 학습 루프
def train_cql_offline(agent, dataset, n_epochs=500, batch_size=256,
                      eval_interval=50):
    """CQL 오프라인 학습"""
    print(f"Offline dataset size: {len(dataset)}")
    print(f"CQL alpha: {agent.alpha}")
    print("=" * 50)

    for epoch in range(1, n_epochs + 1):
        epoch_losses = []

        # 에폭당 여러 배치 학습
        n_batches = max(1, len(dataset) // batch_size)
        for _ in range(n_batches):
            batch = dataset.sample(batch_size)
            loss = agent.train_step(batch)
            epoch_losses.append(loss)

        if epoch % eval_interval == 0:
            avg_loss = np.mean(epoch_losses)
            avg_bellman = np.mean(agent.loss_history["bellman"][-n_batches:])
            avg_cql = np.mean(agent.loss_history["cql"][-n_batches:])
            print(f"Epoch {epoch:4d} | Loss: {avg_loss:.4f} | "
                  f"Bellman: {avg_bellman:.4f} | CQL: {avg_cql:.4f}")

    return agent


# 사용 예시
dataset = OfflineDataset()
# dataset.load_from_logs(service_logs)  # 실제 서비스 로그 로드

cql_agent = CQLAgent(
    state_dim=6,
    n_actions=3,
    alpha=1.0,     # CQL 보수성 (1.0 = 적당히 보수적)
    gamma=0.9,
    lr=3e-4,
    tau=0.005
)

# trained_agent = train_cql_offline(cql_agent, dataset)

1.7 정책 평가: Off-Policy Evaluation (OPE)

학습된 정책을 실제 배포하기 전에 오프라인 데이터로 성능을 추정.

1.7.1 Weighted Importance Sampling (WIS)

IS의 분산을 줄인 버전:

\[ \hat{V}_{\text{WIS}}^{\pi} = \frac{\sum_{i=1}^{n} w_i \cdot G_i}{\sum_{i=1}^{n} w_i} \]

  • \(w_i = \prod_{t=0}^{T} \frac{\pi(a_t^{(i)}|s_t^{(i)})}{\mu(a_t^{(i)}|s_t^{(i)})}\): 중요도 가중치
  • \(G_i = \sum_{t=0}^{T} \gamma^t r_t^{(i)}\): 할인 누적 보상
  • 정규화로 분산 감소, 약간의 편향 발생
def weighted_importance_sampling(episodes, pi, mu, gamma=0.9):
    """
    Weighted Importance Sampling for OPE

    episodes: list of [(s, a, r), ...]
    pi: 목표 정책 (학습된 CQL 정책)
    mu: 행동 정책 (데이터 수집 시 사용된 정책)
    """
    weights = []
    returns = []

    for episode in episodes:
        w = 1.0
        G = 0.0

        for t, (s, a, r) in enumerate(episode):
            w *= pi(a, s) / max(mu(a, s), 1e-8)  # 0 나눗셈 방지
            G += (gamma ** t) * r

        weights.append(w)
        returns.append(G)

    weights = np.array(weights)
    returns = np.array(returns)

    # 가중 평균
    if weights.sum() > 0:
        return np.sum(weights * returns) / np.sum(weights)
    return 0.0

1.7.2 Doubly Robust Estimator

IS와 가치 함수 추정을 결합하여 어느 한쪽이 맞으면 일관된 추정:

\[ \hat{V}_{\text{DR}}^{\pi} = \frac{1}{n} \sum_{i=1}^{n} \left[ \hat{V}^{\pi}(s_0^{(i)}) + \sum_{t=0}^{T} \gamma^t \rho_t^{(i)} \left( r_t^{(i)} + \gamma \hat{V}^{\pi}(s_{t+1}^{(i)}) - \hat{Q}^{\pi}(s_t^{(i)}, a_t^{(i)}) \right) \right] \]

  • \(\rho_t = \prod_{\tau=0}^{t} \frac{\pi(a_\tau|s_\tau)}{\mu(a_\tau|s_\tau)}\): 누적 IS 비율
  • \(\hat{V}^{\pi}, \hat{Q}^{\pi}\): 가치/행동-가치 함수 추정
  • 이중 안전성: IS가 부정확해도 \(\hat{Q}\)가 맞으면 OK, 반대도 OK
def doubly_robust_estimator(episodes, pi, mu, q_hat, v_hat, gamma=0.9):
    """
    Doubly Robust OPE Estimator

    q_hat: Q-function 추정 (CQL의 Q-network)
    v_hat: Value function 추정
    """
    estimates = []

    for episode in episodes:
        s0 = episode[0][0]
        dr_value = v_hat(s0)

        rho = 1.0  # 누적 IS 비율
        for t, (s, a, r) in enumerate(episode):
            rho *= pi(a, s) / max(mu(a, s), 1e-8)

            # 다음 상태의 가치
            if t + 1 < len(episode):
                next_v = v_hat(episode[t+1][0])
            else:
                next_v = 0.0

            # DR 보정항
            correction = rho * (r + gamma * next_v - q_hat(s, a))
            dr_value += (gamma ** t) * correction

        estimates.append(dr_value)

    return np.mean(estimates)

1.7.3 OPE 방법 비교

방법 편향 분산 필요 조건
IS 없음 (비편향) 매우 높음 \(\mu(a\|s) > 0\)
WIS 약간 (편향) 중간 \(\mu(a\|s) > 0\)
Direct Method 높음 (모델 의존) 낮음 \(\hat{Q}\) 추정
Doubly Robust 낮음 중간 \(\hat{Q}\) + IS

1.8 실무 예시: AI Agent 서비스 로그 → 최적 정책

1.8.1 전체 파이프라인

# Step 1: 서비스 로그 수집 (6개월)
# 행동 정책 μ: 세그먼트별 고정 전략
#   SI → 기본, MIEP → 단계별, N → 공감형

# Step 2: 데이터 전처리
def preprocess_logs(raw_logs):
    """서비스 로그 → (s, a, r, s', done) 튜플"""
    processed = []
    for user_id, sessions in raw_logs.groupby("user_id"):
        sessions = sessions.sort_values("week")
        for i in range(len(sessions) - 1):
            current = sessions.iloc[i]
            next_row = sessions.iloc[i + 1]

            state = np.array([
                current["satisfaction"] / 5.0,
                current["turn_count"] / 20.0,
                current["emotion_score"],
                current["seg_miep"],
                current["seg_n"],
                current["week"] / 8.0
            ])
            next_state = np.array([
                next_row["satisfaction"] / 5.0,
                next_row["turn_count"] / 20.0,
                next_row["emotion_score"],
                next_row["seg_miep"],
                next_row["seg_n"],
                next_row["week"] / 8.0
            ])
            processed.append({
                "state": state,
                "action": current["strategy_id"],  # 0, 1, 2
                "reward": current["satisfaction"],
                "next_state": next_state,
                "done": i == len(sessions) - 2
            })
    return processed

# Step 3: CQL 학습
dataset = OfflineDataset()
dataset.load_from_logs(preprocess_logs(raw_logs))

cql_agent = CQLAgent(state_dim=6, n_actions=3, alpha=1.5)
trained_agent = train_cql_offline(cql_agent, dataset, n_epochs=300)

# Step 4: OPE로 정책 평가 (배포 전)
behavior_policy = lambda a, s: {  # 기존 고정 정책
    0: 0.8 if s[3] < 0.5 and s[4] < 0.5 else 0.1,  # SI → 기본
    1: 0.8 if s[4] > 0.5 else 0.1,                    # N → 공감형
    2: 0.8 if s[3] > 0.5 else 0.1                     # MIEP → 단계별
}[a]

def learned_policy(a, s):
    """CQL로 학습된 정책"""
    with torch.no_grad():
        q = trained_agent.q_net(torch.FloatTensor(s).unsqueeze(0))
        best_a = q.argmax().item()
    return 1.0 if a == best_a else 0.0

# WIS로 정책 가치 추정
v_behavior = weighted_importance_sampling(
    test_episodes, behavior_policy, behavior_policy)
v_learned = weighted_importance_sampling(
    test_episodes, learned_policy, behavior_policy)

print(f"행동 정책 (기존) 추정 가치: {v_behavior:.3f}")
print(f"CQL 정책 (학습) 추정 가치: {v_learned:.3f}")
print(f"개선율: {(v_learned - v_behavior) / abs(v_behavior) * 100:.1f}%")

# Step 5: 소규모 A/B 테스트로 최종 검증
# CQL 정책 vs 기존 정책 → 5% 트래픽으로 1주 검증 후 확대

1.9 A/B → Bandit → DTR → Offline RL 진화 경로

수준 1: A/B 테스트 + 통계 모델 (LMM/GEE)
  ┌─────────────────────────────────────────┐
  │ 설계: 무작위 배정, 2~4주 관찰           │
  │ 분석: LMM으로 처치 효과 추정            │
  │ 결정: p-value < 0.05면 승자 채택        │
  │ 장점: 인과 추론 엄밀, 해석 쉬움         │
  │ 단점: 정적, 개인화 없음, 탐색 비용      │
  └─────────────┬───────────────────────────┘
                │ "사용자마다 최적 전략이 다르다"
                ▼
수준 2: Contextual Bandit (29번 파일)
  ┌─────────────────────────────────────────┐
  │ 설계: 문맥 관찰 → 실시간 행동 선택      │
  │ 알고리즘: LinUCB, Thompson Sampling     │
  │ 장점: 개인화, Regret 41~48% 감소        │
  │ 단점: 순차적 의존성 무시                 │
  └─────────────┬───────────────────────────┘
                │ "지금 행동이 미래 상태를 바꾼다"
                ▼
수준 3: DTR / Q-learning (30번 파일)
  ┌─────────────────────────────────────────┐
  │ 설계: MDP, 순차적 최적 결정             │
  │ 알고리즘: Q-learning, DQN              │
  │ 장점: 장기 보상 최적화                  │
  │ 단점: 온라인 탐색 위험                  │
  └─────────────┬───────────────────────────┘
                │ "탐색 없이 기존 데이터로 학습하고 싶다"
                ▼
수준 4: Offline RL (이 파일)
  ┌─────────────────────────────────────────┐
  │ 설계: 기존 로그만 사용, 추가 탐색 없음   │
  │ 알고리즘: CQL, BCQ                     │
  │ 검증: OPE (WIS, DR) → 소규모 A/B       │
  │ 장점: 안전, 기존 인프라 활용             │
  │ 단점: 데이터 커버리지 한계               │
  └─────────────────────────────────────────┘

1.9.1 실무 권장 경로

Phase 1: A/B 테스트로 기본 처치 효과 확인 (LMM)
  → "공감형이 N 세그먼트에 효과적" 같은 기본 지식 확보

Phase 2: Contextual Bandit으로 실시간 최적화
  → LinUCB로 세그먼트 × 문맥별 최적 전략 탐색
  → 데이터 축적

Phase 3: 축적된 데이터로 Offline RL 학습
  → CQL로 순차적 최적 정책 학습
  → OPE로 안전 검증 후 배포

Phase 4: 지속적 개선
  → 새 로그 축적 → Offline RL 재학습 → OPE → 배포
  → 필요 시 소규모 온라인 탐색 (안전 범위 내)

1.10 통계 모델 + RL 결합: CATE → 보상 함수

1.10.1 인과 추론으로 보상 함수 설계

통계 모델(인과 추론)로 조건부 평균 처치 효과(CATE)를 추정하고, 이를 RL의 보상 함수에 활용:

\[ \text{CATE}(x) = \mathbb{E}[Y(1) - Y(0) | X = x] \]

from sklearn.linear_model import LinearRegression

def estimate_cate_tlearner(df, treatment_col, outcome_col, feature_cols):
    """
    T-learner로 CATE 추정

    처치군과 대조군에 별도 모델을 학습하여 개인별 처치 효과 추정
    """
    treated = df[df[treatment_col] == 1]
    control = df[df[treatment_col] == 0]

    model_t = LinearRegression().fit(
        treated[feature_cols], treated[outcome_col]
    )
    model_c = LinearRegression().fit(
        control[feature_cols], control[outcome_col]
    )

    # 개인별 CATE
    df["cate"] = (
        model_t.predict(df[feature_cols])
        - model_c.predict(df[feature_cols])
    )
    return df


# RL 보상 함수에 CATE 통합
def cate_informed_reward(state, action, cate_model):
    """
    CATE 기반 보상 함수:
    처치 효과가 높은 사용자에게 적극적 전략 적용 시 보상 증가
    """
    base_reward = observe_satisfaction(state, action)

    if action > 0:  # 개인화 전략 적용
        cate = cate_model.predict(state.reshape(1, -1))[0]
        # CATE가 높은 사용자 → 보상 보너스
        bonus = max(0, cate) * 0.3
        return base_reward + bonus

    return base_reward

1.10.2 왜 결합하는가

통계 모델 단독:
  "이 사용자에게 공감형이 효과적" (CATE > 0)
  → 정적 결정, 순서/타이밍 고려 없음

RL 단독:
  보상 함수를 직접 설계해야 함
  → 도메인 지식 없이 만족도만 사용하면 근시안적

CATE + RL:
  1. CATE로 "어떤 사용자에게 어떤 전략이 효과적인지" 추정
  2. CATE를 RL 보상에 반영 → 처치 효과가 높은 상황에서 적극 적용
  3. RL이 순서/타이밍까지 최적화
  → 인과적 근거 있는 동적 최적화

1.11 요약

개념 핵심
Offline RL 기존 로그만으로 정책 학습, 추가 탐색 없음
Distribution Shift 행동 정책 ≠ 목표 정책 → Q 과대 추정 위험
Importance Sampling IS 비율로 off-policy 보정, 높은 분산 한계
CQL logsumexp 페널티로 데이터 밖 Q를 보수적 추정
BCQ 데이터에 있는 행동만 후보로 제한
OPE WIS, Doubly Robust로 배포 전 정책 평가
진화 경로 A/B → Bandit → DTR → Offline RL
CATE + RL 인과 추론으로 보상 함수 강화 → 근거 있는 최적화

Subscribe

Enjoy this blog? Get notified of new posts by email: