수민 '-'

플오그래밍

제가 작성하는 모든 글은 절대 상업적인 이용이 아니며, 그저 개인적인 공부 용도로만 사용하는 것임을 밝힙니다.

Q-learning과 DQN - Q값 업데이트, Gym·CartPole 실습

행동의 점수(Q-value)를 배우는 가치 기반 강화학습

Q-learning 은 각 상태에서 어떤 행동이 얼마나 좋은지 를 점수(Q-value)로 학습하는 가치 기반(Value-Based) 강화학습 알고리즘이다. 에이전트가 환경과 상호작용하며 보상을 받을 때마다 그 행동의 Q값을 조금씩 수정하고, 반복하면 좋은 행동의 점수는 올라가고 나쁜 행동의 점수는 내려가서, 결국 가장 높은 Q값을 가진 행동 을 선택하게 된다. 이 글에서는 Q-learning의 핵심 수식과 간단한 1차원 예제, 그리고 Gym(Gymnasium) 의 CartPole 환경에서 DQN(Deep Q-Network) 으로 Q값을 신경망으로 근사하는 예제까지 정리한다.

(Q-learning - 류지 프로젝트를 바탕으로 재구성했다.)


1. Q값(Q-value)이란

Q-learning의 중심 개념은 Q값(Q-value) 이다.

Q(s, a)
  • (s): 현재 상태(State)
  • (a): 행동(Action)

의미:

현재 상태 (s)에서 행동 (a)를 했을 때, 앞으로 받을 것으로 기대되는 누적 보상

즉, “이 상태에서 이 행동을 하면 얼마나 이득인가?”를 숫자로 나타낸 것이다. 에이전트는 각 상태에서 Q값이 가장 큰 행동 을 선택하면 된다(탐험을 위해 ε-greedy 등을 섞어 쓰는 경우가 많다).


2. Q-learning이 작동하는 방식

학습은 다음 흐름을 반복한다.

  1. 현재 상태 확인
  2. 가능한 행동 중 하나 선택 (예: ε-greedy)
  3. 행동 수행
  4. 보상(reward) 수령
  5. 다음 상태 로 이동
  6. 방금 한 행동의 Q값 을 업데이트

이 과정을 계속 반복하면서 Q값이 점점 진짜 기대 보상 에 가까워진다.


3. Q-learning의 핵심 수식

Q-learning은 TD(0) 형태로, “현재 보상 + 다음 상태의 최대 미래 가치”를 이용해 Q값을 수정한다.

Q(s, a) ← Q(s, a) + α [ r + γ max_{a'} Q(s', a') − Q(s, a) ]
  • (α): 학습률(learning rate)
  • (γ): 할인율(discount factor)
  • (r): 즉시 보상
  • (s'): 다음 상태
  • (\max_{a'} Q(s', a')): 다음 상태에서 가장 큰 Q값 (목표 상태이면 보통 0으로 둠)

즉,

현재 행동의 가치를 “현재 보상 + 할인된 다음 상태의 최대 Q값”으로 조금씩 보정하는 과정 이다.


4. 간단한 1차원 환경 예제

다음과 같은 1차원 환경을 생각하자.

S   .   .   .   G
0   1   2   3   4
  • 시작 상태: 0
  • 목표 상태: 4 (Goal)
  • 목표 도착 보상: +10
  • 한 칸 이동 보상: -1
  • 행동: 왼쪽(0), 오른쪽(1)

초기에는 모든 Q값을 0으로 둔다.

4-1. 예제 1: 상태 3에서 오른쪽 이동

  • 현재 상태: (s = 3)
  • 행동: (a =) 오른쪽
  • 이동 결과: 3 → 4 (Goal)
  • 보상: (r = +10)
  • 다음 상태: (s' = 4)

Goal은 종료 상태 이므로 더 이상 움직이지 않는다. 따라서 (\max Q(s', a') = 0) 으로 둔다.

  • (α = 0.1), (γ = 0.9)
  • 목표값: (r + γ \times 0 = 10 + 0 = 10)
  • 업데이트: (Q(3, \text{오른쪽}) \leftarrow 0 + 0.1 \times (10 - 0) = 1)

상태 3에서 오른쪽 행동의 Q값이 1로 증가 한다.

4-2. 예제 2: 상태 2에서 오른쪽 이동

  • (s = 2), (a =) 오른쪽 → (s' = 3), (r = -1)
  • 방금 구한 값: (Q(3, \text{오른쪽}) = 1) 이므로 (\max Q(3, a') = 1)
  • 목표값: (-1 + 0.9 \times 1 = -0.1)
  • (Q(2, \text{오른쪽}) \leftarrow 0 + 0.1 \times (-0.1 - 0) = -0.01)

이렇게 목표(Goal) 쪽 Q값이 먼저 올라가고, 그 값이 이전 상태로 전파 되면서, “목표로 가는 방향”의 Q값이 점점 학습된다.


5. Q-learning 계산 과정 프로그램 구현

같은 1차원 환경에서 ε-greedy 로 행동을 선택하고, Q-learning 수식으로 Q테이블을 업데이트하는 코드다. Q값이 어떻게 바뀌고 보상이 뒤쪽 상태로 전파되는지 확인할 수 있다.

  • 학습률 (α = 0.5), 할인율 (γ = 0.9), 탐험 확률 (ε = 0.2)
  • 확률 (ε): 랜덤 행동
  • 확률 (1-ε): Q값이 가장 큰 행동 선택
import numpy as np
import random

num_states = 5
actions = [0, 1]  # 0: 왼쪽, 1: 오른쪽
learning_rate = 0.5
discount_factor = 0.9
epsilon = 0.2
episodes = 100

q_table = np.zeros((num_states, len(actions)))

def get_action(state):
    if random.random() < epsilon:
        return random.choice(actions)
    else:
        return np.argmax(q_table[state])

def step(state, action):
    if action == 1:
        next_state = min(state + 1, num_states - 1)
    else:
        next_state = max(state - 1, 0)

    if next_state == 4:
        reward = 10
        done = True
    else:
        reward = -1
        done = False
    return next_state, reward, done

for episode in range(episodes):
    state = 0
    done = False
    while not done:
        action = get_action(state)
        next_state, reward, done = step(state, action)

        old_value = q_table[state, action]
        next_max = np.max(q_table[next_state]) if not done else 0.0
        q_table[state, action] = old_value + learning_rate * (
            reward + discount_factor * next_max - old_value
        )
        state = next_state

print("최종 Q-테이블:")
print(q_table)

에피소드를 충분히 돌리면, 목표(4) 쪽으로 갈수록 오른쪽 행동의 Q값이 더 높아지는 패턴이 나온다.


6. Gym(Gymnasium)이란

Gym(현재는 Gymnasium 이 계승)은 강화학습 알고리즘을 실험하기 위한 표준 환경(environment) 을 제공하는 파이썬 라이브러리다.

  • 에이전트가 상태 를 관찰하고 행동 을 선택하면, 환경이 다음 상태보상 을 반환한다.
  • CartPole, MountainCar, Atari 등 다양한 환경을 제공해, 알고리즘 비교·실습에 널리 쓰인다.

설치 예시:

pip install gymnasium

7. DQN: CartPole-v1에서 Q값을 신경망으로 근사

DQN(Deep Q-Network) 은 Q값 (Q(s,a)) 를 테이블이 아니라 신경망 으로 근사하는 방법이다. DQN 논문

  • CartPole-v1: 카트 위 막대가 쓰러지지 않도록 카트를 왼쪽(0)·오른쪽(1) 으로 움직이는 환경.
  • 상태: 4차원 (카트 위치, 카트 속도, 막대 각도, 막대 각속도)
  • 행동: 0(왼쪽), 1(오른쪽)
  • 에이전트는 상태 4개 값 → 신경망 → 행동 2개에 대한 Q값 을 출력하고, 그 Q값을 Q-learning 식으로 업데이트한다.

7-1. Replay Buffer

DQN에서는 경험 (s, a, r, s', done) 을 버퍼에 쌓아 두고, 랜덤하게 mini-batch 를 뽑아 학습한다. 이렇게 하면 연속된 샘플 간 상관을 줄이고 학습이 안정된다.

import collections
import random

buffer_limit = 50000
batch_size = 32

class ReplayBuffer:
    def __init__(self):
        self.buffer = collections.deque(maxlen=buffer_limit)

    def put(self, transition):
        self.buffer.append(transition)

    def sample(self, n):
        mini_batch = random.sample(self.buffer, n)
        s_lst, a_lst, r_lst, s_prime_lst, done_mask_lst = [], [], [], [], []
        for transition in mini_batch:
            s, a, r, s_prime, done_mask = transition
            s_lst.append(s)
            a_lst.append([a])
            r_lst.append([r])
            s_prime_lst.append(s_prime)
            done_mask_lst.append([done_mask])
        return (
            torch.tensor(s_lst, dtype=torch.float),
            torch.tensor(a_lst),
            torch.tensor(r_lst),
            torch.tensor(s_prime_lst, dtype=torch.float),
            torch.tensor(done_mask_lst),
        )

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

7-2. Q 네트워크와 ε-greedy 행동 선택

상태 4차원 → FC 128 → ReLU → FC 128 → ReLU → FC 2 (행동 2개 Q값).

import torch
import torch.nn as nn
import torch.nn.functional as F

class Qnet(nn.Module):
    def __init__(self):
        super(Qnet, self).__init__()
        self.fc1 = nn.Linear(4, 128)
        self.fc2 = nn.Linear(128, 128)
        self.fc3 = nn.Linear(128, 2)

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

    def sample_action(self, obs, epsilon):
        out = self.forward(obs)
        if random.random() < epsilon:
            return random.randint(0, 1)
        else:
            return out.argmax().item()

7-3. 학습: TD 타깃과 Huber Loss

Q-learning 수식을 그대로 쓰되, 타깃 네트워크 (q_{\text{target}}) 로 다음 상태의 최대 Q값을 계산해 타깃 을 만든다. 종료 상태면 미래 가치를 0으로 두기 위해 done_mask 를 곱한다.

def train(q, q_target, memory, optimizer):
    for i in range(10):
        s, a, r, s_prime, done_mask = memory.sample(batch_size)
        q_out = q(s)
        q_a = q_out.gather(1, a)
        max_q_prime = q_target(s_prime).max(1)[0].unsqueeze(1)
        target = r + gamma * max_q_prime * done_mask
        loss = F.smooth_l1_loss(q_a, target)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
  • q_out.gather(1, a): 각 샘플에서 선택한 행동 (a) 에 해당하는 Q값만 뽑는다.
  • done_mask: done이면 0, 아니면 1 → 종료 시 타깃이 (r) 만 남게 한다.

7-4. 전체 학습 루프 (Gymnasium CartPole-v1)

import gymnasium as gym
import torch.optim as optim

learning_rate = 0.0005
gamma = 0.98
print_interval = 20

def main():
    env = gym.make('CartPole-v1')
    q = Qnet()
    q_target = Qnet()
    q_target.load_state_dict(q.state_dict())
    memory = ReplayBuffer()
    score = 0.0
    optimizer = optim.Adam(q.parameters(), lr=learning_rate)

    for n_epi in range(10000):
        epsilon = max(0.01, 0.08 - 0.01 * (n_epi / 200))
        s, info = env.reset()
        done = False

        while not done:
            a = q.sample_action(torch.from_numpy(s).float(), epsilon)
            s_prime, r, done, truncated, info = env.step(a)
            done_mask = 0.0 if (done or truncated) else 1.0
            memory.put((s, a, r / 100.0, s_prime, done_mask))
            s = s_prime
            score += r
            if done or truncated:
                break

        if memory.size() > 2000:
            train(q, q_target, memory, optimizer)

        if n_epi % print_interval == 0 and n_epi != 0:
            q_target.load_state_dict(q.state_dict())
            print("n_episode: {}, score: {:.1f}, n_buffer: {}, eps: {:.1f}%".format(
                n_epi, score / print_interval, memory.size(), epsilon * 100
            ))
            score = 0.0
    env.close()

if __name__ == '__main__':
    main()
  • ε: 초반 8% 근처에서 시작해 점점 줄여 최소 1% 로 둔다 (탐험 → 활용).
  • 타깃 네트워크: 주기적으로 (q) 의 가중치를 (q_{\text{target}}) 에 복사해, 타깃이 너무 흔들리지 않게 한다.
  • Replay Buffer: 2000개 이상 쌓인 뒤부터 학습을 시작한다.

8. 정리

주제 핵심 포인트
Q(s,a) 상태 (s)에서 행동 (a)를 했을 때 기대되는 누적 보상.
Q-learning (Q(s,a) \leftarrow Q(s,a) + α[r + γ \max_{a'} Q(s',a') - Q(s,a)]). TD(0) 기반, off-policy.
ε-greedy 확률 (ε)로 랜덤 행동(탐험), (1-ε)로 최대 Q 행동(활용).
DQN Q값을 신경망으로 근사. Replay Buffer + 타깃 네트워크로 안정화.
Gym/Gymnasium 강화학습용 표준 환경. CartPole 등으로 알고리즘 실험.

마치며

  • Q-learning은 행동의 점수(Q값) 를 배우고, 그 점수가 가장 높은 행동을 선택하는 가치 기반 강화학습의 대표 알고리즘이다.
  • 1차원 같은 작은 환경에서는 Q 테이블 만으로도 동작을 확인할 수 있고, CartPole 처럼 연속 상태 공간에서는 DQN 처럼 신경망으로 Q값을 근사해야 한다.
  • Replay Buffer와 타깃 네트워크는 DQN 학습 안정성의 핵심 요소이므로, 코드에서 각각 “왜 쓰는지”를 이해해 두면 좋다.