본문 바로가기
프로그래밍/딥러닝 (완)

DDPG: 연속 제어의 마법사 (87)

by 서가_ 2025. 6. 7.
반응형

DDPG: 연속 제어의 마법사, 로봇부터 자율주행까지

로봇 팔이 정확한 각도로 물체를 집거나, 자율주행차가 부드럽게 스티어링을 조작하는 모습을 본 적이 있나요? 이런 연속적이고 정밀한 제어가 가능한 이유 중 하나가 바로 DDPG(Deep Deterministic Policy Gradient) 알고리즘입니다.

2015년 DeepMind에서 발표한 DDPG는 연속 행동 공간에서의 강화학습 문제를 해결하기 위해 탄생했습니다. 오늘은 이 강력한 알고리즘의 비밀을 파헤치고, 실제 연속 제어 문제에서 어떻게 활용할 수 있는지 알아보겠습니다.

 

연속 제어의 도전: 무한한 선택의 문제

이산 vs 연속 행동 공간

이산 행동 공간 (기존 강화학습)

행동 = {위, 아래, 왼쪽, 오른쪽}  # 4개의 선택지
Q-Learning: Q(s, a)로 각 행동의 가치 계산 가능

연속 행동 공간 (DDPG의 영역)

행동 = [스티어링 각도: -30° ~ +30°]  # 무한한 실수값
기존 방법: 모든 실수값에 대해 Q값 계산 불가능!

기존 해결책의 한계

  1. 행동 공간 이산화: 연속값을 구간별로 나누기
    • 문제: 정밀도 손실, 차원의 저주
  2. 정책 기반 방법 (REINFORCE, Actor-Critic)
    • 문제: 높은 분산, 느린 수렴
  3. 진화 알고리즘
    • 문제: 샘플 비효율성, 지역 최적화

DDPG의 핵심 혁신: 4가지 기둥

1. 결정적 정책 (Deterministic Policy)

기존 확률적 정책:

# 확률적: 같은 상태에서도 다른 행동
action = policy_network(state) + noise  # 확률 분포에서 샘플링

DDPG 결정적 정책:

# 결정적: 같은 상태에서 항상 같은 행동
action = actor_network(state)  # 직접적인 행동 출력

장점:

  • 행동 일관성 확보
  • 정밀한 제어 가능
  • 학습 안정성 향상

2. Actor-Critic 아키텍처 + DQN 기법 융합

DDPG는 Actor-Critic의 구조에 DQN의 성공 요소들을 결합했습니다:

class DDPG:
    def __init__(self):
        # Actor: 상태 → 행동 (연속값)
        self.actor = ActorNetwork()
        self.actor_target = ActorNetwork()

        # Critic: (상태, 행동) → Q값
        self.critic = CriticNetwork() 
        self.critic_target = CriticNetwork()

        # DQN 기법들
        self.replay_buffer = ExperienceReplayBuffer()
        self.noise = OrnsteinUhlenbeckNoise()

3. 타겟 네트워크 (Target Networks)

DQN에서 영감을 받은 타겟 네트워크로 학습 안정성 확보:

def soft_update(target_network, main_network, tau=0.001):
    """부드러운 타겟 네트워크 업데이트"""
    for target_param, main_param in zip(target_network.parameters(), 
                                        main_network.parameters()):
        target_param.data.copy_(
            tau * main_param.data + (1.0 - tau) * target_param.data
        )

# 매 스텝마다 점진적 업데이트
soft_update(self.actor_target, self.actor)
soft_update(self.critic_target, self.critic)

4. 경험 재현 (Experience Replay)

연속 제어에서도 과거 경험을 재사용하여 샘플 효율성 향상:

class ReplayBuffer:
    def __init__(self, capacity=1000000):
        self.buffer = deque(maxlen=capacity)

    def push(self, state, action, reward, next_state, done):
        self.buffer.append((state, action, reward, next_state, done))

    def sample(self, batch_size=256):
        return random.sample(self.buffer, batch_size)

# 배치 학습으로 안정성 확보
batch = replay_buffer.sample(256)

DDPG의 학습 과정: 단계별 분석

1단계: 탐험적 행동 수집

def select_action(state, add_noise=True):
    action = actor_network(state)

    if add_noise:
        # Ornstein-Uhlenbeck 노이즈로 탐험
        noise = ou_noise.sample()
        action = action + noise

    return np.clip(action, action_low, action_high)

2단계: Critic 업데이트 (Q값 학습)

def update_critic(batch):
    states, actions, rewards, next_states, dones = batch

    # 타겟 Q값 계산
    next_actions = actor_target(next_states)
    target_q = critic_target(next_states, next_actions)
    target_q = rewards + gamma * target_q * (1 - dones)

    # 현재 Q값
    current_q = critic(states, actions)

    # Critic 손실 계산 및 업데이트
    critic_loss = F.mse_loss(current_q, target_q.detach())
    critic_optimizer.zero_grad()
    critic_loss.backward()
    critic_optimizer.step()

3단계: Actor 업데이트 (정책 개선)

def update_actor(batch):
    states, _, _, _, _ = batch

    # Actor가 선택한 행동에 대한 Q값을 최대화
    predicted_actions = actor(states)
    actor_loss = -critic(states, predicted_actions).mean()

    actor_optimizer.zero_grad()
    actor_loss.backward()
    actor_optimizer.step()

DDPG의 강력한 장점들

🎯 연속 행동 공간 특화

적용 가능 영역:

# 로봇 제어
joint_angles = [-180°, +180°] × 6  # 6축 로봇 팔
motor_speeds = [0, 1000] RPM × 4   # 4륜 구동 차량

# 금융 트레이딩
portfolio_weights = [0.0, 1.0] × N  # N개 자산 비중
position_sizes = [-1.0, +1.0]      # 롱/숏 포지션

# 게임 제어
steering_angle = [-1.0, +1.0]      # 레이싱 게임
throttle_brake = [0.0, 1.0]        # 가속/브레이크

샘플 효율성

경험 재현과 타겟 네트워크로 기존 정책 기반 방법보다 효율적:

# 성능 비교 (일반적인 경우)
REINFORCE: 100만 스텝 → 목표 성능 달성
DDPG:      10만 스텝 → 목표 성능 달성 (10배 효율적)

🎮 결정적 행동의 안정성

같은 상태에서 일관된 행동으로 예측 가능한 제어:

# 확률적 정책의 문제
state = [robot_arm_position]
action1 = policy(state) + noise1  # [45°, 30°, 60°]
action2 = policy(state) + noise2  # [50°, 25°, 65°]  # 불일치!

# DDPG 결정적 정책
state = [robot_arm_position]
action = ddpg_policy(state)  # 항상 [47°, 28°, 62°]  # 일관성!

DDPG의 도전과제와 실무 해결책

🔍 탐험 부족 문제

문제: 결정적 정책으로 인한 제한된 탐험

해결 전략:

  1. Ornstein-Uhlenbeck 노이즈
  2. class OUNoise: def __init__(self, action_dim, mu=0, theta=0.15, sigma=0.2): self.action_dim = action_dim self.mu = mu self.theta = theta self.sigma = sigma self.state = np.ones(action_dim) * mu def sample(self): dx = self.theta * (self.mu - self.state) + \ self.sigma * np.random.randn(self.action_dim) self.state += dx return self.state
  3. 적응적 노이즈 스케줄링
  4. def get_exploration_noise(episode, max_episodes): # 학습 진행에 따라 노이즈 감소 noise_scale = max(0.1, 1.0 - episode / max_episodes) return noise_scale * ou_noise.sample()
  5. 파라미터 공간 탐험
  6. # 주기적으로 네트워크 파라미터에 노이즈 추가 if episode % 100 == 0: add_parameter_noise(actor_network, noise_std=0.01)

📈 Q값 과대추정 편향

문제: Critic이 Q값을 실제보다 높게 추정

해결책:

  1. 클리핑된 더블 Q-learning (TD3에서 발전)
  2. # 두 개의 Critic 사용 q1 = critic1(state, action) q2 = critic2(state, action) target_q = min(q1, q2) # 더 보수적인 추정
  3. 타겟 정책 스무딩
  4. def target_policy_smoothing(next_states): next_actions = actor_target(next_states) # 타겟 행동에 노이즈 추가로 과신 방지 noise = torch.clamp(torch.randn_like(next_actions) * 0.2, -0.5, 0.5) next_actions = torch.clamp(next_actions + noise, -1, 1) return next_actions

⚙️ 하이퍼파라미터 민감성

핵심 파라미터들:

ddpg_config = {
    'actor_lr': 1e-4,           # Actor 학습률 (중요!)
    'critic_lr': 1e-3,          # Critic 학습률 (Actor보다 높게)
    'tau': 1e-3,                # 타겟 네트워크 업데이트 비율
    'gamma': 0.99,              # 할인 인수
    'batch_size': 256,          # 배치 크기
    'buffer_size': 1e6,         # 리플레이 버퍼 크기
    'noise_sigma': 0.2,         # 탐험 노이즈 강도
    'warmup_steps': 10000       # 초기 랜덤 탐험 스텝
}

튜닝 전략:

  1. Critic 학습률을 Actor보다 5-10배 높게 설정
  2. 타겟 네트워크 업데이트는 매우 천천히 (tau = 0.001)
  3. 충분한 워밍업 스텝으로 리플레이 버퍼 채우기

DDPG 실무 적용 사례

🤖 로보틱스: 7-DoF 로봇 팔 제어

class RobotArmEnvironment:
    def __init__(self):
        self.joint_limits = [
            (-180, 180),  # 관절 1: 베이스 회전
            (-90, 90),    # 관절 2: 어깨
            (-180, 180),  # 관절 3: 팔꿈치
            (-90, 90),    # 관절 4: 손목 1
            (-180, 180),  # 관절 5: 손목 2
            (-90, 90),    # 관절 6: 손목 3
            (0, 50)       # 관절 7: 그리퍼
        ]

    def step(self, action):
        # action: [7개 관절 각도 변화량]
        joint_velocities = np.clip(action, -10, 10)  # 안전 제한

        # 물리 시뮬레이션 실행
        new_position = self.simulate_robot_motion(joint_velocities)

        # 보상 계산: 목표 위치 도달 + 부드러운 동작
        reward = -distance_to_target(new_position) - \
                 0.1 * np.sum(np.abs(joint_velocities))  # 동작 부드러움

        return new_state, reward, done

🚗 자율주행: 차량 제어

class AutonomousDrivingEnv:
    def __init__(self):
        self.action_space = {
            'steering': (-30, 30),    # 조향각 (도)
            'throttle': (0, 1),       # 가속 (0-1)
            'brake': (0, 1)           # 브레이크 (0-1)
        }

    def step(self, action):
        steering, throttle, brake = action

        # 차량 동역학 시뮬레이션
        self.vehicle.update(steering, throttle, brake)

        # 다중 목표 보상
        reward = (
            10.0 * lane_keeping_reward() +      # 차선 유지
            5.0 * speed_maintenance_reward() +   # 속도 유지
            -100.0 * collision_penalty() +      # 충돌 회피
            -1.0 * comfort_penalty(action)       # 승차감
        )

        return next_state, reward, done

💹 알고리즘 트레이딩

class TradingEnvironment:
    def __init__(self, assets):
        self.assets = assets
        self.portfolio_size = len(assets)

    def step(self, action):
        # action: 각 자산의 목표 비중 변화 [-1, 1]
        portfolio_changes = np.clip(action, -0.1, 0.1)  # 급격한 변화 방지

        # 거래 비용 고려
        transaction_costs = self.calculate_transaction_costs(portfolio_changes)

        # 포트폴리오 수익률 계산
        returns = self.market_returns @ self.current_weights

        # 리스크 조정 수익률
        reward = returns - 0.01 * np.std(returns) - transaction_costs

        return next_market_state, reward, done

성공적인 DDPG 구현을 위한 실무 가이드

📋 단계별 구현 체크리스트

  1. 환경 설정 검증
    • 행동 공간이 연속적인가?
    • 행동 범위가 명확히 정의되었나?
    • 보상 함수가 부드럽게 변화하나?
  2. 네트워크 아키텍처
    • Actor 출력에 적절한 활성화 함수 (tanh, sigmoid)
    • Critic은 상태와 행동을 적절히 결합
    • 배치 정규화 적용 고려
  3. 학습 안정화
    • 충분한 워밍업 스텝 (10,000+)
    • 그래디언트 클리핑 적용
    • 타겟 네트워크 천천히 업데이트

🚀 성능 최적화 팁

# 1. 우선순위 경험 재현 (PER)
class PrioritizedReplayBuffer:
    def sample(self, batch_size):
        # TD 오차가 큰 경험을 우선적으로 샘플링
        indices = self.priority_sampling(batch_size)
        return self.buffer[indices]

# 2. 힌드사이트 경험 재현 (HER)
def add_hindsight_experience(episode):
    # 실패한 에피소드도 학습에 활용
    for transition in episode:
        # 도달한 위치를 새로운 목표로 설정
        new_goal = transition.next_state[:3]  # 위치 정보
        modified_reward = goal_achieved_reward(new_goal)
        replay_buffer.add(transition.state, transition.action, 
                          modified_reward, transition.next_state)

디버깅 가이드

학습이 안 될 때 체크포인트:

  1. Actor가 학습되지 않는 경우
  2. # Critic 손실이 수렴했는지 확인 if critic_loss < 0.01: print("Critic 학습 완료, Actor 학습률 증가 고려") actor_lr *= 1.5
  3. 과도한 탐험 노이즈
  4. # 노이즈 스케일 점진적 감소 noise_scale = max(0.1, 1.0 - episode / total_episodes) if episode % 1000 == 0: print(f"현재 노이즈 스케일: {noise_scale}")
  5. 학습 불안정성
  6. # 그래디언트 크기 모니터링 grad_norm = torch.nn.utils.clip_grad_norm_(actor.parameters(), max_norm=1.0) if grad_norm > 10.0: print(f"경고: 그래디언트 폭발! 크기: {grad_norm}")

마무리: DDPG의 현재 위치와 발전 방향

DDPG는 연속 제어 문제에서 강화학습의 실용성을 크게 높인 알고리즘입니다. 비록 현재는 TD3, SAC 같은 더 안정적인 후속 알고리즘들이 등장했지만, DDPG가 제시한 결정적 정책 + Actor-Critic + 경험 재현의 조합은 여전히 연속 제어 강화학습의 표준 패러다임입니다.

실무에서 DDPG를 고려해야 하는 경우:

  • 연속 행동 공간이 중요한 문제
  • 결정적이고 예측 가능한 제어가 필요한 상황
  • 비교적 안정적인 환경에서의 정밀 제어
  • 기존 연속 제어 시스템과의 통합

DDPG를 마스터하면 연속 제어의 핵심 원리를 이해할 수 있고, TD3, SAC 같은 최신 알고리즘들도 훨씬 쉽게 이해할 수 있습니다. 로봇 공학, 자율주행, 금융 등 연속 제어가 중요한 분야에서 일하고 있다면, DDPG는 반드시 익혀야 할 필수 도구입니다.

반응형