본문 바로가기
머신러닝

[RL] 강화학습 - 근사 Q-러닝과 심층 Q-러닝 개념 및 Python 구현

by 프로그래밍하겠습니다 2025. 4. 18.
728x90
반응형

🎶 Q-러닝의 문제점을 해결하기 위해 등장한 근사 Q-러닝과 심층 Q-러닝에 대해 알아보고 python으로 구현해보자.

 

1. 근사 Q-러닝과 심층 Q-러닝

Q-러닝의 주요 문제점은 바로 많은 상태와 행동을 가진 대규모의 MDP에는 적용하기 어렵다는 것이다. 가능한 상태가 기하급수적으로 늘어날 경우, 모든 Q-가치에 대한 추정값을 기록할 수 있는 방법이 없기 때문이다.

해결책으로 먼저 등장한 방법으로는 어떤 상태-행동 (s, a) 쌍의 Q-가치를 근사하는 함수 Q(θ)(s, a)를 적절한 개수의 파라미터를 사용해 찾는 근사 Q-러닝이 있다. 하지만 곧이어 이 근사 Q-러닝보다 더 나은 결과를 내는 러닝 방법이 등장했는데, 그것이 바로 심층 Q-러닝이다. 

심층 Q-러닝은 Q-가치를 추정하기 위해 DNN, 내지는 심층 Q-네트워크(DQN)를 사용하는 방법으로써, 특히 복잡한 문제에서 훨씬 더 나은 성능을 보여줬다. 이제 이 심층 Q-러닝을 python으로 직접 구현해보자.

 

2. Python 구현

심층 Q-러닝을 구현하기 위해 가장 먼저 필요한 것은 바로 심층 Q-네트워크다. 이론적으로는 상태-행동 쌍을 입력받고 근사 Q-가치를 출력하는 신경망이 필요하지만, 실전에서는 상태만 입력으로 받고 가능한 모든 행동에 대한 근사 Q-가치를 각각 출력하는 것이 더욱 효율적이다.

import tensorflow as tf

input_shape = [4] # observation_space
n_outputs = 2 # action_space

# 신경망 모델 정의
model = tf.keras.Sequential([
    tf.keras.layers.Dense(32, activation="elu", input_shape=input_shape),
    tf.keras.layers.Dense(32, activation="elu"),
    tf.keras.layers.Dense(n_outputs)
])

 

에이전트가 환경을 탐험하도록 만들기 위해 앞서 살펴본 입실론-그리디 정책을 사용한다.

import numpy as np

def epsilon_greedy_policy(state, ep=0):
    if np.random.rand() < ep:
        return np.random.randint(n_outputs)
    else:
        Q_values = model.predict(state[np.newaxis], verbose=0)[0]
        return Q_values.argmax()

 

최근의 경험에만 의지해 DQN을 훈련하는 대신, 재생 버퍼(재생 메모리)에 모든 경험을 저장하고 훈련 반복마다 여기에서 랜덤한 훈련 배치를 샘플링하는 방법을 사용해 경험과 훈련 배치 사이의 상관관계를 줄어들도록 해보자.

from collections import deque

replay_buff = deque(maxlen=2000)

 

경험은 총 원소 6개로 구성되는데, 차례로 상태, 선택한 행동, 결과 보상, 다음 상태, 에피소드 종료 여부, 에피소드 중단 여부 이다.

def sample_exp(batch_size):
    indices = np.random.randint(len(replay_buff), size=batch_size)
    batch = [replay_buff[index] for index in indices]
    return[
        np.array([experience[field_index] for experience in batch])
        for field_index in range(6)
    ]
# states, actions, rewards, next_states, dones, truncateds

 

입실론 그리디 정책을 활용해 하나의 스텝을 플레이하고 반환된 경험을 재생 버퍼에 저장하는 함수를 정의하자.

def play_step(env, state, ep):
    action = epsilon_greedy_policy(state, ep)
    next_state, reward, done, truncated, info = env.step(action)
    replay_buff.append((state, action, reward, next_state, done, truncated))
    return next_state, reward, done, truncated, info

 

이제 재생 버퍼에서 경험 배치를 샘플링하고 이 배치에서 경사 하강법 한 스텝을 수행하여 DQN을 훈련시키는 함수를 정의하자.

batch_size = 32 # 배치 크기
discount = 0.95 # 할인율
optimizer = tf.keras.optimizers.Nadam(learning_rate=1e-2) # 경사하강법 최적화 알고리즘
loss_fn = tf.keras.losses.MSE # 손실함수

def training_step(batch_size):
    experiences = sample_exp(batch_size)
    states, actions, rewards, next_states, dones, truncateds = experiences
    next_Q_values = model.predict(next_states, verbose=0)
    max_next_Q_values = next_Q_values.max(axis=1)
    runs = 1.0 - (dones | truncateds) 
    target_Q_values = rewards + runs * discount * max_next_Q_values
    target_Q_values = target_Q_values.reshape(-1, 1)
    
    mask = tf.one_hot(actions, n_outputs)
    with tf.GradientTape() as tape:
        all_Q_values = model(states)
        Q_values = tf.reduce_sum(all_Q_values * mask, axis = 1, keepdims= True)
        loss = tf.reduce_mean(loss_fn(target_Q_values, Q_values)) # 모델의 손실값 계산
    
    # 경사 하강법을 활용해 모델의 파라미터 학습
    grads = tape.gradient(loss, model.trainable_variables)
    optimizer.apply_gradients(zip(grads, model.trainable_variables))

 

코드에서 사용되는 경사하강법의 최적화 알고리즘에 관련되서는 다음 블로그를 참고하길 바란다.

경사 하강법의 최적화 - 모멘텀(momentum), 네스테로프가속경사(Nesterov-accelerated gradient), AdaGrad, RMSProp 그리고 Adam

 

경사 하강법의 최적화 - 모멘텀(momentum), 네스테로프가속경사(Nesterov-accelerated gradient), AdaGrad, RMSPro

🎶 경사 하강법의 최적화 기법에는 어떤 것들이 있을까? 지난 포스트에서는 경사하강법에 의한 파라미터 업데이트가 잘 이루어지지 않는, 사라지는 기울기 문제에 대해 다뤘었다.사라지는 기

ybbbb.tistory.com

 

마지막으로 모델 훈련 코드를 짜보고 각 episode에 받는 최종 reward를 시각화해보자.

import gymnasium as gym
import matplotlib.pyplot as plt

env = gym.make("CartPole-v1")

rewards = []

for episode in range(200):
    obs, info = env.reset()
    total_reward = 0
    for step in range(50):
        ep = max(1-episode / 200, 0.01)
        obs, reward, done, truncated, info = play_step(env, obs, ep)
        total_reward += reward
        if done or truncated:
            break
    rewards.append(total_reward)
    if episode > 50:
        training_step(batch_size)



# 보상 그래프 시각화
plt.plot(range(200), rewards)
plt.show()

 

각 에피소드별로 받은 보상값을 시각화한 결과는 다음과 같다.

이처럼 최대 보상 근처에서 안정된 것처럼 보였으나 다시 점수가 떨어지는 구간이 있는데, 이것이 바로 모든 RL 알고리즘이 직면한 문제인 '최악의 망각(catastrophic forgetting) 문제'이다.

reward_graph_per_episode
[사진1] 각 episode 별 보상 그래프

강화 학습은 대부분의 훈련이 불안정하고 하이퍼파라미터 값와 랜덤 시드의 선택에 크게 민감하기 때문에 매우 어렵다. 이에 우수한 강화학습을 위해서는 적절한 하이퍼파라미터 튜닝과 함께 운이 따라줘야 한다는 우스갯소리가 있다.

728x90
반응형