본문 바로가기

Deep Learning

Imitation Learning에서 DQN 으로.

흠.. Imitation Learning을 기억하는가? 그때는 Expert의 행동을 그대로 따라하는 모델을 만들었었다.

다만 그때의 문제는 Expert 의 데이터를 모으는 것이 쉽지 않다는 것이었다. 

그래서 Imitation Learning 예제에서는 간단한 Expert 함수를 만들어서 데이터를 모았었다. 

 

이번에는 Imitation Learning과 비슷하지만, 딱 한 발만 더 앞으로 전진을 해보자. 

강화학습 기술 중에 가장 먼저 듣는 얘기가 DQN 일 것이다.

Deep Q-Network 인데, 원래 있었던 Q-Learning 방법에 Deep Network을 적용한 기법이다. 

 

Q-Learning에 대해서 자세히 설명은... 다음에 하는 걸로 하고,

그냥 간단히 말해서 더 좋은 행동(action)에 대해 더 좋은 reward(Q값)을 주는 함수라고 생각을 하자. 

즉, 좋은 행동(action)을 하면 좋은 점수(더 높은 Q값)을 얻을 수 있는 함수가 있다는 뜻이다.

 

Imitation Learning에서는 Expert의 행동을 저장했다가 해당 행동을 유사하게 따라하도록 모델을 학습시켰다면 DQN에서는 Q함수를 이용하여 Expert를 대신하게 된다. Imitation Learning에서는 Expert의 행동이, DQN에서는 더 좋은 Q값을 얻을 수 있는 행동이 되는 것이다. 

 

일단 제일 간단한 DQN 코드를 먼저 보자. 

참고로 이 코드는 완전한 DQN코드가 아니라, 이해하기 쉽도록 핵심 기능을 제외하고 전체적인 흐름을 파악하기 위한 코드이다. 

이 코드를 기준으로 계속 기능을 넣어서 완전한 DQN 코드로 확장할 예정이니, 일단은 간한단 DQN코드를 보자. 

import gymnasium as gym
import torch
import torch.nn as nn
import torch.optim as optim
import random
import numpy as np
from collections import deque

# 하이퍼파라미터
learning_rate = 0.001
gamma = 0.99
epsilon = 1.0
epsilon_decay = 0.995
epsilon_min = 0.01
batch_size = 64
memory_size = 10000
episodes = 500

# 환경
env = gym.make('CartPole-v1', render_mode="human")
state_size = env.observation_space.shape[0]  # 4개
action_size = env.action_space.n             # 2개 (왼쪽, 오른쪽)

# Q-Network
class DQN(nn.Module):
    def __init__(self):
        super(DQN, self).__init__()
        self.fc1 = nn.Linear(state_size, 24)
        self.fc2 = nn.Linear(24, 24)
        self.fc3 = nn.Linear(24, action_size)

    def forward(self, x):
        x = torch.relu(self.fc1(x))
        x = torch.relu(self.fc2(x))
        return self.fc3(x)

model = DQN()
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
criterion = nn.MSELoss()

# 메모리
memory = deque(maxlen=memory_size)

# 행동 선택 함수
def get_action(state, epsilon):
    if random.random() < epsilon:
        return env.action_space.sample()
    else:
        state = torch.FloatTensor(state)
        with torch.no_grad():
            q_values = model(state)
        return q_values.argmax().item()

# 학습 함수
def train():
    if len(memory) < batch_size:
        return

    batch = random.sample(memory, batch_size)
    states, actions, rewards, next_states, dones = zip(*batch)

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

    current_q = model(states).gather(1, actions).squeeze()
    next_q = model(next_states).max(1)[0]
    target_q = rewards + gamma * next_q * (1 - dones)

    loss = criterion(current_q, target_q.detach())
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

# 메인 루프
for episode in range(episodes):
    state, _ = env.reset()
    done = False
    score = 0

    while not done:
        action = get_action(state, epsilon)
        next_state, reward, terminated, truncated, _ = env.step(action)
        done = terminated or truncated
        memory.append((state, action, reward, next_state, done))
        state = next_state
        score += reward

        train()

    # epsilon 감소
    if epsilon > epsilon_min:
        epsilon *= epsilon_decay

    print(f'Episode: {episode}, Score: {score}, Epsilon: {epsilon:.2f}')

env.close()

 

일단 위의 코드를 실행해보면 아래와 같은 cartpole 그림이 뜨고, pole이 쓰러지지 않도록 cart가 좌우로 왔다갔다 하는 것을 볼 수 있다. 

pole이 쓰러지지 않도록 cart를 움직여라.

 

이제 코드를 하나씩 보자. 

먼저 변수를 설정하고, 환경, state_size, action_size를 설정하자. 

# 환경
env = gym.make('CartPole-v1', render_mode="human")
state_size = env.observation_space.shape[0]  # 4개
action_size = env.action_space.n             # 2개 (왼쪽, 오른쪽)

환경은 gym의 CartPole-V1을 사용하고, render_mode="human"으로 해주면 움직이는 cartpole을 실제로 볼 수 있다. 해당 문구를 없애면 실시간으로 움직이는 화면을 보이지 않고 학습만 수행하게 되어 더 빠르게 실행된다. 여기서는 화면을 보여서 잘 되는지 확인용으로 render_mode를 설정해줬다. 

 

state_size는 4가 되는데, 환경에서 얻을 수 있는 값이 4개 라는 의미이다. 이 4개는 아래와 같다. 

1 cart position 카트의 좌우 위치 위치 (m)
2 cart velocity 카트의 속도 속도 (m/s)
3 pole angle 막대기의 기울어진 각도 라디안 (rad)
4 pole angular velocity 막대기의 각속도 (회전 속도) rad/s

 

action_size는 2가 되는데, 이것은 모델이 수행할 수 있는 action의 갯수이다. 여기서는 (좌,우)의 2가지 값이 된다. 

 

위의 예제는 env에서 state의 4가지값(cart position, cart velocity, pole angle, pole angular velocity)값을 가지고 와서, 모델은 2가지 action(좌측이동, 우측이동) 중 하나를 선택해서 행동을 하고, 그 행동에 따라 pole이 계속 서 있다면 +1 의 reward를 받는 형태가 된다. 

 

이제 Q-Network 모델을 정의한다. 

# Q-Network
class DQN(nn.Module):
    def __init__(self):
        super(DQN, self).__init__()
        self.fc1 = nn.Linear(state_size, 24)
        self.fc2 = nn.Linear(24, 24)
        self.fc3 = nn.Linear(24, action_size)

    def forward(self, x):
        x = torch.relu(self.fc1(x))
        x = torch.relu(self.fc2(x))
        return self.fc3(x)

model = DQN()
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
criterion = nn.MSELoss()

간단하게 모델을 정의한 후, optimizer, criterion 도 설정해준다. 

 

그리고 Imitation Learning에서 환경(state)와 행동(action)을 저장했던 것 처럼, 여기서도 state와 action을 저장하기 위한 memory를 설정해준다. 

# 메모리
memory = deque(maxlen=memory_size)

 

그 후에 행동을 선택하는 get_action 함수를 선언해준다. 

# 행동 선택 함수
def get_action(state, epsilon):
    if random.random() < epsilon:
        return env.action_space.sample()
    else:
        state = torch.FloatTensor(state)
        with torch.no_grad():
            q_values = model(state)
        return q_values.argmax().item()

이 부분이 Imitation Learning과 다른 부분인데, 일단 여기서는...

epsilon 의 확률만큼은 env.action_space.sample() 을 수행하고, 

1-epsilon 의 확률만큼은 model에서 Q값을 받아와서 action을 수행하게 된다. 

 

evn.action_space.sample()은 action 중에 랜덤하게 하나 선택하는 함수이므로, epsilon 확률만큼은 random action을 선택한다는 뜻이다. 그리고 epsilon은 처음엔 1.0 으로 설정되어 있어 처음에는 100% random action을 수행하지만 계속 학습을 진행하면서 epsilon 값을 감소시키므로 점차적으로 model 에서 계산한 action을 수행하게 된다. 

 

잠시 Imitation Learning과 비교하면, Imitation Learning에서는 state에 따른 Expert의 action을 저장했다가, 그것을 가져와서 학습에 사용했었다. DQN에서는 Expert를 쓰지 않고 random 한 action을 수행하고 그에 따른 reward를 저장했다가 사용하게 된다. 즉, DQN에서는 Expert가 필요없다는 점이 장점인데, Expert가 없기 때문에 제대로 학습이 안되거나 학습에 오랜 시간이 필요하다는 단점도 나타난다. 

 

이제 학습함수를 보자. 

# 학습 함수
def train():
    if len(memory) < batch_size:
        return

    batch = random.sample(memory, batch_size)
    states, actions, rewards, next_states, dones = zip(*batch)

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

    current_q = model(states).gather(1, actions).squeeze()
    next_q = model(next_states).max(1)[0]
    target_q = rewards + gamma * next_q * (1 - dones)

    loss = criterion(current_q, target_q.detach())
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

학습 함수는 일단 일정갯수(batch_size) 이상의 memory가 쌓여야만 학습을 수행한다. memory가 일정수준이 되지 않으면 아예 학습을 수행하지 않는다. 

 

학습을 수행할 때는...

1) memory에서 batch_size 만큼 메모리를 가져와서

2) states, actions, rewards, next_states, dones 로 각 항목을 나눈 후, 

3) Tensor로 변환하고

4) 현재 모델에 states, actions 를 이용해서 current_q값 계산하고, next_states를 이용해서 next_q값을 얻어서 target_q값을 계산한다. 

5) current_q값과 target_q값으로부터 loss를 계산한후 backward(), step()을 수행하여 모델 업데이트를 수행한다. 

 

이제 실제 메인 루프를 보면...

# 메인 루프
for episode in range(episodes):
    state, _ = env.reset()
    done = False
    score = 0

    while not done:
        action = get_action(state, epsilon)
        next_state, reward, terminated, truncated, _ = env.step(action)
        done = terminated or truncated
        memory.append((state, action, reward, next_state, done))
        state = next_state
        score += reward

        train()

    # epsilon 감소
    if epsilon > epsilon_min:
        epsilon *= epsilon_decay

    print(f'Episode: {episode}, Score: {score}, Epsilon: {epsilon:.2f}')

episode마다 환경을 초기화 시키고, 

1) state를 주고, 그에 따른 action을 가져온다. (초기에는 random action을 가져오게 됨)

2) action을 환경(env)에 전달하면, 해당 action이 적용된 이후에 대한 next_state, reward, terminated, truncated 를 가져온다. 

3) 현재 state와 그에 대응한 action, action을 수행하고 얻은 reward와 action에 의해 변화된 next_state를 memory에 저장한다. 

4) state를 next_state로 변경하고 다시 시작한다. 

 

주요 구조를 보면 앞서 봤던 Imitation Learning과 기본 구조는 동일하다. 단지 Expert의 action을 그대로 사용하느냐, 아니면 Q값을 커지게 하는 action을 사용하느냐의 차리로 볼 수 있다. 

 

자, 이제 Imitation Learning에서 DQN으로 가는 한 발을 내딛였다. 

아직 DQN이 완성된 것은 아니다. 현재 상태로는 학습이 잘 되지 않는다.

 

이제 다음 단계로 넘어가보자.

다음 단계는 replay memory이다. 

 

오늘은 여기까지. 

'Deep Learning' 카테고리의 다른 글

Policy gradient (강화학습)  (0) 2025.05.25
강화학습 on-policy vs off-policy  (0) 2025.05.23
Gymnasium (강화학습 라이브러리)  (0) 2025.05.20
Imitation Learning - gymnasium  (0) 2025.05.19
Classification with unknown  (1) 2025.05.18