1 Dynamic Treatment Regime (DTR)
1.1 Bandit → DTR: 순차적 의존성
1.1.1 Contextual Bandit의 가정과 한계
Contextual Bandit은 각 시점이 독립이라고 가정한다:
\[ x_t \perp a_{t-1} \quad \text{(이전 행동이 현재 상태에 영향 없음)} \]
현실에서는 이 가정이 깨진다:
Week 1: 공감형 프롬프트 → 사용자 만족도 3.2 → 4.1 (상승)
Week 2: 상승된 만족도(4.1)가 새로운 상태 → 다음 행동 선택에 영향
Week 3: 단계별 가이드 적용 → 만족도 유지 4.0 → 개인화 비율 상승
→ 전략 순서가 중요: (공감 → 단계별)과 (단계별 → 공감)의 장기 결과가 다름
DTR은 이 순차적 의존성을 명시적으로 모델링한다.
1.2 DTR 수학적 프레임워크
1.2.1 Markov Decision Process (MDP)
DTR은 MDP로 공식화된다:
\[ \mathcal{M} = (\mathcal{S}, \mathcal{A}, P, R, \gamma, T) \]
| 요소 | 정의 | AI Agent 예시 |
|---|---|---|
| \(\mathcal{S}\) | 상태 공간 | 만족도, 턴수, 감정, 세그먼트, 주차, 개인화비율 |
| \(\mathcal{A}\) | 행동 공간 | {기본, 공감형, 단계별 가이드} |
| \(P(s_{t+1} \| s_t, a_t)\) | 전이 확률 | 현재 전략이 다음 주 상태에 미치는 영향 |
| \(R(s_t, a_t)\) | 보상 함수 | 해당 주의 만족도 |
| \(\gamma \in [0, 1)\) | 할인율 | 미래 보상의 현재 가치 (보통 0.9~0.99) |
| \(T\) | 에피소드 길이 | 8주 |
1.2.2 정책 (Policy)
정책은 상태에서 행동으로의 매핑:
\[ \pi: \mathcal{S} \to \mathcal{A}, \quad a_t = \pi(s_t) \]
최적 정책은 누적 보상을 최대화:
\[ \pi^* = \arg\max_\pi \mathbb{E}\left[\sum_{t=0}^{T} \gamma^t r_t \;\middle|\; s_0, \pi\right] \]
1.2.3 가치 함수 (Value Function)
정책 \(\pi\) 하에서 상태 \(s\)의 가치:
\[ V^\pi(s) = \mathbb{E}\left[\sum_{t=0}^{T} \gamma^t r_t \;\middle|\; s_0 = s, \pi\right] \]
행동-가치 함수 (Q-function):
\[ Q^\pi(s, a) = \mathbb{E}\left[r_0 + \gamma V^\pi(s_1) \;\middle|\; s_0 = s, a_0 = a\right] \]
최적 정책과 Q-function의 관계:
\[ \pi^*(s) = \arg\max_a Q^*(s, a) \]
1.3 Q-learning for DTR
1.3.1 Bellman Equation
최적 Q-function은 Bellman 최적 방정식을 만족:
\[ Q^*(s, a) = \mathbb{E}\left[r + \gamma \max_{a'} Q^*(s', a') \;\middle|\; s, a\right] \]
직관: 지금 보상 + 다음 상태에서의 최대 미래 가치
1.3.2 선형 Q-function 근사
상태 공간이 연속적일 때, Q를 선형 함수로 근사:
\[ Q(s, a; \mathbf{w}_a) = \mathbf{w}_a^T \phi(s) \]
- \(\phi(s) \in \mathbb{R}^d\): 상태 특징 벡터
- \(\mathbf{w}_a \in \mathbb{R}^d\): arm \(a\)의 가중치
1.3.3 Temporal Difference (TD) Error
매 시점 관찰 \((s_t, a_t, r_t, s_{t+1})\)에서:
\[ \delta_t = r_t + \gamma \max_{a'} Q(s_{t+1}, a'; \mathbf{w}) - Q(s_t, a_t; \mathbf{w}) \]
가중치 업데이트:
\[ \mathbf{w}_{a_t} \leftarrow \mathbf{w}_{a_t} + \alpha \cdot \delta_t \cdot \phi(s_t) \]
- \(\alpha\): 학습률
- \(\delta_t\): TD error — 양수면 Q를 올리고, 음수면 내린다
1.4 환경 시뮬레이션: AgentPersonalizationEnv
1.4.1 상태 공간 설계
import numpy as np
class AgentPersonalizationEnv:
"""
AI Agent 개인화 시뮬레이션 환경
상태: [만족도, 턴수(정규화), 감정점수, 세그먼트_MIEP, 세그먼트_N, 주차(정규화)]
행동: 0=기본, 1=공감형, 2=단계별 가이드
에피소드: 8주 (T=8)
"""
def __init__(self, max_weeks=8):
self.max_weeks = max_weeks
self.state_dim = 6
self.n_actions = 3
self.action_names = ["기본", "공감형", "단계별"]
def reset(self):
"""새 에피소드 (새 사용자) 시작"""
self.week = 1
# 사용자 프로필 샘플링
segment = np.random.choice(["SI", "MIEP", "N"], p=[0.4, 0.35, 0.25])
self.segment = segment
self.seg_miep = 1.0 if segment == "MIEP" else 0.0
self.seg_n = 1.0 if segment == "N" else 0.0
# 초기 상태
self.satisfaction = np.random.uniform(2.5, 4.0)
self.emotion = np.random.uniform(-0.5, 0.5)
self.turn_count = np.random.randint(3, 15)
self.personalization_ratio = 0.0
return self._get_state()
def _get_state(self):
return np.array([
self.satisfaction / 5.0, # 정규화
self.turn_count / 20.0, # 정규화
self.emotion, # [-1, 1]
self.seg_miep, # 이진
self.seg_n, # 이진
self.week / self.max_weeks # 정규화
])
def step(self, action):
"""한 주 진행: 행동 → 보상 + 상태 전이"""
reward = self._compute_reward(action)
# 상태 전이: 현재 행동이 다음 상태에 영향
self._transition(action, reward)
self.week += 1
done = self.week > self.max_weeks
next_state = self._get_state() if not done else np.zeros(self.state_dim)
return next_state, reward, done
def _compute_reward(self, action):
"""세그먼트 × 전략 × 현재 상태 → 보상"""
base = self.satisfaction * 0.3 + self.emotion * 0.2
if action == 0: # 기본
effect = 0.0
elif action == 1: # 공감형
# N 세그먼트에 효과적, 감정이 낮을 때 더 효과적
effect = 0.5 * self.seg_n + 0.15 * self.seg_miep
effect += 0.2 * max(0, -self.emotion) # 부정 감정일 때 보너스
else: # 단계별 가이드
# MIEP에 효과적, 주차가 진행될수록 효과 증가
effect = 0.4 * self.seg_miep + 0.05 * self.seg_n
effect += 0.1 * (self.week / self.max_weeks) # 후반부 보너스
noise = np.random.normal(0, 0.2)
return np.clip(base + effect + noise, 0, 5)
def _transition(self, action, reward):
"""상태 전이: 행동과 보상이 다음 상태를 결정"""
# 만족도 업데이트 (지수 이동 평균)
self.satisfaction = 0.7 * self.satisfaction + 0.3 * reward
# 감정 업데이트
if reward > 3.0:
self.emotion = min(1.0, self.emotion + 0.1)
else:
self.emotion = max(-1.0, self.emotion - 0.15)
# 턴수 증가
self.turn_count += np.random.randint(2, 8)
# 개인화 비율 (공감형/단계별 사용 시 증가)
if action > 0:
self.personalization_ratio = min(
1.0, self.personalization_ratio + 0.15
)1.4.2 핵심 설계 포인트
1. 순차적 의존성:
- 만족도는 지수 이동 평균으로 업데이트 → 과거 행동의 누적 효과
- 감정은 보상에 따라 변동 → 행동 선택의 결과가 다음 상태에 반영
2. 세그먼트별 차별적 효과:
- SI: 기본 전략이 무난 (추가 효과 없음)
- MIEP: 단계별 가이드에 보너스 +0.4
- N: 공감형에 보너스 +0.5, 부정 감정일 때 추가 +0.2
3. 시간 의존 효과:
- 단계별 가이드는 후반부에 효과 증가 (주차/max_weeks 보너스)
1.5 Q-learning 학습 코드
1.5.1 전체 학습 루프
class QLearningDTR:
"""
Tabular-style Q-learning with linear function approximation
"""
def __init__(self, state_dim, n_actions,
lr=0.01, gamma=0.9, epsilon_start=1.0, epsilon_end=0.05,
epsilon_decay=0.995):
self.n_actions = n_actions
self.gamma = gamma
self.lr = lr
self.epsilon = epsilon_start
self.epsilon_end = epsilon_end
self.epsilon_decay = epsilon_decay
# 선형 Q-function: Q(s, a) = W[a] @ s
self.W = np.random.randn(n_actions, state_dim) * 0.01
def q_values(self, state):
return self.W @ state
def select_action(self, state):
if np.random.random() < self.epsilon:
return np.random.randint(self.n_actions)
return np.argmax(self.q_values(state))
def update(self, state, action, reward, next_state, done):
q_current = self.q_values(state)[action]
if done:
q_target = reward
else:
q_target = reward + self.gamma * np.max(self.q_values(next_state))
td_error = q_target - q_current
self.W[action] += self.lr * td_error * state
def decay_epsilon(self):
self.epsilon = max(self.epsilon_end, self.epsilon * self.epsilon_decay)
# 학습 실행
env = AgentPersonalizationEnv(max_weeks=8)
agent = QLearningDTR(state_dim=6, n_actions=3)
n_episodes = 10000
episode_rewards = []
for ep in range(n_episodes):
state = env.reset()
total_reward = 0
while True:
action = agent.select_action(state)
next_state, reward, done = env.step(action)
agent.update(state, action, reward, next_state, done)
total_reward += reward
if done:
break
state = next_state
agent.decay_epsilon()
episode_rewards.append(total_reward)
if (ep + 1) % 2000 == 0:
avg = np.mean(episode_rewards[-500:])
print(f"Episode {ep+1:5d} | Avg Reward (last 500): {avg:.3f} | ε: {agent.epsilon:.3f}")1.5.2 수렴 확인
import matplotlib.pyplot as plt
# 이동 평균으로 수렴 확인
window = 200
moving_avg = np.convolve(episode_rewards, np.ones(window)/window, mode="valid")
plt.figure(figsize=(10, 5))
plt.plot(moving_avg, linewidth=1.5)
plt.xlabel("Episode")
plt.ylabel("Total Reward (8-week)")
plt.title("Q-learning Training Convergence")
plt.axhline(y=np.mean(moving_avg[-1000:]), color="r", linestyle="--",
label=f"Final Avg: {np.mean(moving_avg[-1000:]):.2f}")
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()1.6 학습된 최적 정책 분석
1.6.1 세그먼트 × 주차별 최적 전략
strategy_names = ["기본", "공감형", "단계별"]
segments = {
"SI": [0.0, 0.0],
"MIEP": [1.0, 0.0],
"N": [0.0, 1.0]
}
print("학습된 최적 전략 (Q-learning)")
print("=" * 55)
print(f"{'세그먼트':>8} | {'Week':>4} | {'전략':>8} | {'Q-values':>24}")
print("-" * 55)
for seg_name, (seg_m, seg_n) in segments.items():
for week in [1, 2, 4, 6, 8]:
state = np.array([3.5/5, 5/20, 0.0, seg_m, seg_n, week/8])
q_vals = agent.q_values(state)
best_action = np.argmax(q_vals)
q_str = ", ".join(f"{q:.2f}" for q in q_vals)
print(f"{seg_name:>8} | {week:>4} | {strategy_names[best_action]:>8} | [{q_str}]")
print("-" * 55)1.6.2 기대 출력
학습된 최적 전략 (Q-learning)
=======================================================
세그먼트 | Week | 전략 | Q-values
-------------------------------------------------------
SI | 1 | 기본 | [1.23, 0.98, 0.87]
SI | 2 | 기본 | [1.31, 1.05, 0.92]
SI | 4 | 기본 | [1.45, 1.12, 1.01]
SI | 6 | 기본 | [1.52, 1.18, 1.08]
SI | 8 | 기본 | [1.55, 1.20, 1.10]
-------------------------------------------------------
MIEP | 1 | 기본 | [1.15, 1.08, 1.12]
MIEP | 2 | 단계별 | [1.22, 1.15, 1.35]
MIEP | 4 | 단계별 | [1.28, 1.20, 1.58]
MIEP | 6 | 단계별 | [1.30, 1.22, 1.72]
MIEP | 8 | 단계별 | [1.32, 1.24, 1.85]
-------------------------------------------------------
N | 1 | 기본 | [1.10, 1.05, 0.88]
N | 2 | 공감형 | [1.18, 1.42, 0.95]
N | 4 | 공감형 | [1.25, 1.68, 1.02]
N | 6 | 공감형 | [1.28, 1.80, 1.05]
N | 8 | 공감형 | [1.30, 1.88, 1.08]
-------------------------------------------------------
해석:
- SI: 전 기간 기본 전략이 최적 (추가 전략의 효과 미미)
- MIEP: 1주차 기본으로 시작 → 2주차부터 단계별 가이드가 우세 (후반부로 갈수록 Q값 격차 확대)
- N: 1주차 기본 → 2주차부터 공감형이 압도적 (부정 감정 보정 효과)
1.7 Deep Q-Network (DQN)
1.7.1 선형 근사 → 신경망 확장
상태 공간이 복잡하거나 비선형 관계가 있을 때, Q-function을 신경망으로 근사:
\[ Q(s, a; \theta) \approx Q^*(s, a) \]
import torch
import torch.nn as nn
from collections import deque
import random
class DQN(nn.Module):
def __init__(self, state_dim, n_actions):
super().__init__()
self.net = nn.Sequential(
nn.Linear(state_dim, 128),
nn.ReLU(),
nn.Linear(128, 64),
nn.ReLU(),
nn.Linear(64, n_actions)
)
def forward(self, x):
return self.net(x)
class DQNAgent:
def __init__(self, state_dim, n_actions, lr=1e-3, gamma=0.9,
buffer_size=10000, batch_size=64, target_update=100):
self.n_actions = n_actions
self.gamma = gamma
self.batch_size = batch_size
self.target_update = target_update
self.step_count = 0
self.q_net = DQN(state_dim, n_actions)
self.target_net = DQN(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.buffer = deque(maxlen=buffer_size)
self.epsilon = 1.0
def select_action(self, state):
if random.random() < self.epsilon:
return random.randint(0, self.n_actions - 1)
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 store(self, state, action, reward, next_state, done):
self.buffer.append((state, action, reward, next_state, done))
def train_step(self):
if len(self.buffer) < self.batch_size:
return 0.0
batch = random.sample(self.buffer, self.batch_size)
states, actions, rewards, next_states, dones = zip(*batch)
states = torch.FloatTensor(np.array(states))
actions = torch.LongTensor(actions)
rewards = torch.FloatTensor(rewards)
next_states = torch.FloatTensor(np.array(next_states))
dones = torch.FloatTensor(dones)
# 현재 Q
q_values = self.q_net(states).gather(1, actions.unsqueeze(1)).squeeze()
# Target Q
with torch.no_grad():
next_q = self.target_net(next_states).max(1)[0]
target = rewards + self.gamma * next_q * (1 - dones)
loss = nn.MSELoss()(q_values, target)
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
# Target network 업데이트
self.step_count += 1
if self.step_count % self.target_update == 0:
self.target_net.load_state_dict(self.q_net.state_dict())
return loss.item()1.7.2 DQN vs Linear Q-learning 비교
| 측면 | Linear Q-learning | DQN |
|---|---|---|
| 모델 | \(Q = \mathbf{w}^T \phi(s)\) | \(Q = f_\theta(s)\) (신경망) |
| 비선형 관계 | 불가 | 가능 |
| 안정성 | 높음 (수렴 보장) | Experience Replay + Target Net 필요 |
| 데이터 효율 | 좋음 | 많은 데이터 필요 |
| 해석 가능성 | \(\mathbf{w}\)에서 직접 해석 | 블랙박스 |
| 추천 상황 | 상태 차원 낮고, 관계 선형 | 상태 복잡, 비선형 관계 |
1.8 DTR과 임상시험: SMART
1.8.1 Sequential Multiple Assignment Randomized Trial
DTR을 임상시험으로 평가하는 표준 설계가 SMART:
SMART (Sequential Multiple Assignment Randomized Trial):
Stage 1: 모든 환자를 치료 A 또는 B에 무작위 배정
↓
중간 평가: 반응자(Responder) vs 비반응자(Non-responder) 분류
↓
Stage 2:
반응자 → 유지(Maintain) 또는 강화(Intensify) 무작위 배정
비반응자 → 전환(Switch) 또는 추가(Augment) 무작위 배정
1.8.2 SMART와 Q-learning의 연결
SMART 데이터에서 DTR 학습:
1. Stage 2부터 역방향 Q-learning (Backward Induction):
Q₂(s₂, a₂) = E[Y | s₂, a₂]
→ Stage 2의 최적 행동 결정
2. Stage 1 Q-learning:
Q₁(s₁, a₁) = E[r₁ + max_{a₂} Q₂(s₂, a₂) | s₁, a₁]
→ Stage 1의 최적 행동 결정 (미래 최적을 고려)
3. 최적 DTR:
π* = (π₁*, π₂*) = (argmax Q₁, argmax Q₂)
1.8.3 AI Agent 맥락의 SMART
Stage 1 (Week 1~4): 기본 vs 공감형 프롬프트 무작위 배정
↓
중간 평가 (Week 4): 만족도 3.5 이상 = 반응자
↓
Stage 2 (Week 5~8):
반응자: 현재 전략 유지 vs 단계별 가이드로 전환
비반응자: 다른 전략으로 전환 vs 현재 전략 + 추가 개인화
→ 4가지 내장 DTR 비교:
DTR 1: 기본 → (반응) 유지 / (비반응) 전환
DTR 2: 기본 → (반응) 단계별 / (비반응) 추가
DTR 3: 공감형 → (반응) 유지 / (비반응) 전환
DTR 4: 공감형 → (반응) 단계별 / (비반응) 추가
1.9 DTR의 한계와 실무 고려
1.9.1 한계
1. 환경 모델 필요:
- 전이 확률 P(s'|s,a)를 알거나 시뮬레이션할 수 있어야 함
- 실제 서비스에서는 환경이 복잡하고 비정상적
2. 온라인 탐색의 위험:
- Q-learning은 탐색 중 일부 사용자에게 나쁜 전략 적용
- 의료/금융에서는 허용 불가
3. 데이터 효율성:
- 상태 × 행동 공간이 크면 수렴까지 많은 에피소드 필요
- 8주 × 사용자 1명 = 에피소드 1개 (비쌈)
4. 비정상 환경:
- 사용자 선호가 시간에 따라 변할 수 있음
- 정적 정책이 최적이 아닐 수 있음
→ 해결: Offline RL — 기존 서비스 로그로 안전하게 학습
1.10 요약
| 개념 | 핵심 |
|---|---|
| Bandit → DTR | 현재 행동이 미래 상태에 영향 → 순차적 의존성 |
| MDP | \((S, A, P, R, \gamma)\) — 상태, 행동, 전이, 보상, 할인 |
| Q-function | \(Q(s,a) = r + \gamma \max_{a'} Q(s', a')\) |
| TD error | 현재 추정 vs 실제 관찰의 차이 → 업데이트 신호 |
| 선형 Q-learning | \(Q = \mathbf{w}^T \phi(s)\), 해석 가능, 안정적 |
| DQN | 신경망 근사, 비선형 관계, Experience Replay 필요 |
| SMART | DTR 평가를 위한 순차적 무작위 배정 임상시험 |
| 한계 | 온라인 탐색 위험 → Offline RL로 해결 |
다음 글: 31 — Offline RL for Safe Policy Learning — 기존 서비스 로그에서 안전하게 최적 정책을 학습하는 방법을 다룬다.