[강화학습] Q-learning의 개념부터 Gym을 활용한 DQN 구현까지

2026. 3. 4. 18:06·개념 정리 step2/강화 학습

오늘은 강화학습의 개념 중 Q-learning에 대해서 공부해보겠습니다. Q-learning의 기본 동작 원리부터 간단한 파이썬 구현, 그리고 한 단계 더 나아가 OpenAI Gym을 활용한 DQN(Deep Q-Network) 예제까지 정리해 보겠습니다.


1. Q-learning

Q-learning은 강화학습에서 에이전트가 각 상태(State)에서 어떤 행동(Action)을 하는 것이 얼마나 좋은지를 점수, 즉 Q-value로 학습하는 방법입니다.

에이전트는 환경과 끊임없이 상호작용하며 행동을 취해보고, 그 결과로 받은 보상(Reward)을 바탕으로 해당 행동의 점수를 조금씩 수정해 나갑니다. 이 과정을 반복하면 자연스럽게 좋은 행동의 점수는 높아지고, 좋지 않은 행동의 점수는 낮아집니다. 결과적으로 에이전트는 각 상태에서 가장 높은 점수를 가진 행동을 선택하게 되며, 이러한 작동 방식 때문에 Q-learning은 대표적인 가치 기반(Value-Based) 강화학습 알고리즘으로 불립니다.

1-1. Q값 (Q-value)

💡 참고 자료: https://arxiv.org/abs/1312.5602

 

Playing Atari with Deep Reinforcement Learning

We present the first deep learning model to successfully learn control policies directly from high-dimensional sensory input using reinforcement learning. The model is a convolutional neural network, trained with a variant of Q-learning, whose input is raw

arxiv.org

Q-learning에서 가장 핵심이 되는 개념은 Q값(Q-value)입니다. 기호로는 다음과 같이 표기합니다.

$Q(s, a)$

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

간단히 말해, "현재 상태 $s$에서 특정 행동 $a$를 했을 때 앞으로 받을 것으로 기대되는 총 보상의 합"을 의미합니다.

1-2. Q-learning의 작동 방식

Q-learning은 다음 6단계의 루프를 계속해서 반복하며 에이전트를 학습시킵니다.

  1. 현재 상태를 확인합니다.
  2. 가능한 행동 중 하나를 선택합니다.
  3. 선택한 행동을 환경에서 수행합니다.
  4. 그에 따른 보상(Reward)을 받습니다.
  5. 다음 상태로 이동합니다.
  6. 방금 수행한 행동의 Q값을 수정(업데이트)합니다.

1-3. Q-learning의 핵심 수식

이 수식의 본질은 "현재 행동의 가치를 '현재 받은 보상 + 다음 상태의 최대 미래 가치'를 이용하여 수정한다"는 것입니다. 수식으로 표현하면 아래와 같습니다.

$$Q(s, a) \leftarrow Q(s, a) + \alpha \left[ r + \gamma \max_{a'} Q(s', a') - Q(s, a) \right]$$
  • $\alpha$: 학습률 (Learning Rate)
  • $\gamma$: 할인율 (Discount Factor)
  • $r$: 보상 (Reward)
  • $\max_{a'} Q(s', a')$: 다음 상태 $s'$에서 선택할 수 있는 가장 높은 Q값

1-4. 1차원 그리드 예제로 보는 Q값 계산

이해를 돕기 위해 아주 간단한 1차원 맵이 있다고 가정해 보겠습니다.

S   .   .   .   G
0   1   2   3   4
  • 설정
    • 시작 상태: 0
    • 목표(Goal) 상태: 4
    • 목표 도착 보상: +10
    • 1칸 이동 시 보상 (페널티): -1
  • 가능한 행동: 왼쪽 이동, 오른쪽 이동
  • 초기에는 모든 상태와 행동의 Q값이 0이라고 가정합니다.

[예제 1: 상태 3에서 오른쪽으로 이동하는 상황]

  • 현재 상태: $s = 3$
  • 행동: $a = \text{오른쪽}$
  • 이동 결과: $3 \rightarrow 4 (\text{Goal})$
  • 보상: $r = +10$
  • 다음 상태: $s' = 4$
  • 파라미터: $\alpha = 0.1$, $\gamma = 0.9$

Goal 상태(4)는 종료 상태이므로 더 이상 움직일 수 없습니다. 즉, 다음 상태의 최대 기대값은 $\max Q(s', a') = 0$이 됩니다.

  • 목표값(Target) 계산: $10 + (0.9 \times 0) = 10$
  • Q값 업데이트:$0 + 0.1 \times (10 - 0) = \mathbf{1}$
  • 현재 $Q(3, \text{오른쪽}) = 0$ 이므로 수식에 대입하면,

결과적으로 상태 3에서 오른쪽으로 가는 Q값은 1로 업데이트됩니다. 만약 상태 2에서 오른쪽으로 이동하여 상태 3에 도달한다면, 이 학습된 가치(1)가 할인율 $\gamma$가 적용된 채 점차 뒤쪽 상태로 전파(Propagation)되게 됩니다.


1-5. Q-learning 파이썬 구현

학습률($0.5$), 할인율($0.9$), 탐험 확률($0.2$)을 적용하여 상태 2 $\rightarrow$ 상태 3으로 넘어가는 단일 스텝을 코드로 구현해 보겠습니다. 행동 선택은 Epsilon-Greedy 방식을 사용합니다.

[구현 1] 단일 스텝 Q값 업데이트 확인

import numpy as np
import random

STATES_COUNT = 5       # 상태 0, 1, 2, 3, 4
ACTIONS = [0, 1]       # 0: 왼쪽(Left), 1: 오른쪽(Right)

alpha = 0.5            # 학습률 (Learning Rate)
gamma = 0.9            # 할인율 (Discount Factor)
epsilon = 0.2          # 탐험 확률 (Exploration Rate)

# Q-테이블 초기화 (5x2 행렬, 모두 0.00으로 시작)
Q_table = np.zeros((STATES_COUNT, len(ACTIONS)))

# [사전 조건 세팅] 이전 학습 결과로 상태 3에서 오른쪽으로 가면 1의 가치가 있다고 가정
Q_table[3, 1] = 1.0

current_state = 2

# [단계 A] 행동 선택 (Epsilon-Greedy)
if random.uniform(0, 1) < epsilon:
    action = random.choice(ACTIONS)
    print("🎲 무작위 탐험을 선택했습니다.")
else:
    action = np.argmax(Q_table[current_state])
    print("📈 가장 Q값이 높은 행동(활용)을 선택했습니다.")

# (계산 과정을 명확히 보기 위해 action을 '오른쪽(1)'으로 고정)
action = 1

# [단계 B] 환경과 상호작용
next_state = current_state + 1  # 2 -> 3
reward = -1

# [단계 C] 계산을 위한 값 추출
old_q = Q_table[current_state, action]         # 현재 Q값: 0.00
max_next_q = np.max(Q_table[next_state])       # 다음 상태(3)에서 가장 큰 Q값: 1.0

# [단계 D] Target 및 새로운 Q값 계산
target = reward + (gamma * max_next_q)         # -1 + (0.9 * 1.0) = -0.1
new_q = old_q + alpha * (target - old_q)       # 0.00 + 0.5 * (-0.1 - 0.00) = -0.05

# [단계 E] Q-테이블 업데이트
Q_table[current_state, action] = new_q

# 결과 출력
print(f"--- 이동 로그 ---")
print(f"현재 상태: {current_state}")
print(f"선택 행동: {'오른쪽' if action == 1 else '왼쪽'}")
print(f"다음 상태: {next_state}")
print(f"받은 보상: {reward}")
print(f"\n--- Q값 계산 ---")
print(f"이전 Q값: {old_q:.2f}")
print(f"Target 값: {target:.2f}")
print(f"새로운 Q값: {new_q:.2f}")

[구현 2] 전체 에피소드 반복을 통한 Q-Table 학습

import random

states = [0, 1, 2, 3, 4]
goal_state = 4
actions = ['left', 'right']
alpha = 0.5
gamma = 0.9
epsilon = 0.2
episodes = 10

Q = {s: {a: 0.0 for a in actions} for s in states}

def choose_action(state):
    if random.random() < epsilon:
        return random.choice(actions)
    else:
        if Q[state]['left'] > Q[state]['right']:
            return 'left'
        elif Q[state]['left'] < Q[state]['right']:
            return 'right'
        else:
            return random.choice(actions)

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

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

def print_q_table():
    print('상태  |   left   |   right')
    print('-' * 40)
    for s in states:
        print(f'{s:>3} | {Q[s]["left"]:>8.2f} | {Q[s]["right"]:>8.2f}')
    print('-' * 40)

for episode in range(1, episodes + 1):
    state = 0
    done = False
    print(f'---------- 에피소드 {episode} ----------')
    while not done:
        action = choose_action(state)
        next_state, reward, done = step(state, action)
        
        old_q = Q[state][action]
        max_next_q = max(Q[next_state].values())
        if next_state == goal_state:
            max_next_q = 0
            
        target = reward + gamma * max_next_q
        new_q = old_q + alpha * (target - old_q)
        Q[state][action] = new_q
        
        print(f'현재 상태: {state} | 선택 행동: {action} | 다음 상태: {next_state} | 보상: {reward}')
        print(f'Target: {reward} + {gamma} * {max_next_q:.2f} = {target:.2f}')
        print(f'새로운 Q값: {old_q:.2f} + {alpha} * ({target:.2f} - {old_q:.2f}) = {new_q:.2f}')
        print("-" * 40)
        
        state = next_state
        
print_q_table()

# 최적 정책 추출
for s in states[:-1]:
    best_action = max(Q[s], key=Q[s].get)
    print(f'상태 {s} -> 최적 행동: {best_action}')
# 출력
---------- 에피소드 10 ----------
현재 상태: 0
선택 행동: right
다음 상태: 1
보상 r: -1
이전 Q값: 3.21
target = r + gamma * maxQ(next) = -1 + 0.9 * 5.63 = 4.06
새로운 Q값: 3.21 + 0.5 * (4.06 - 3.21) = 3.63
----------------------------------------
현재 상태: 1
선택 행동: right
다음 상태: 2
보상 r: -1
이전 Q값: 5.63
target = r + gamma * maxQ(next) = -1 + 0.9 * 7.84 = 6.05
새로운 Q값: 5.63 + 0.5 * (6.05 - 5.63) = 5.84
----------------------------------------
현재 상태: 2
선택 행동: right
다음 상태: 3
보상 r: -1
이전 Q값: 7.84
target = r + gamma * maxQ(next) = -1 + 0.9 * 9.98 = 7.98
새로운 Q값: 7.84 + 0.5 * (7.98 - 7.84) = 7.91
# 출력
----------------------------------------
현재 상태: 3
선택 행동: right
다음 상태: 4
보상 r: 10
이전 Q값: 9.98
target = r + gamma * maxQ(next) = 10 + 0.9 * 0.00 = 10.00
새로운 Q값: 9.98 + 0.5 * (10.00 - 9.98) = 9.99
----------------------------------------
상태  |  left  |  right
----------------------------------------
  0 |    -2.07 |     3.63
  1 |    -1.51 |     5.84
  2 |     0.82 |     7.91
  3 |     0.53 |     9.99
  4 |     0.00 |     0.00
----------------------------------------
상태 0 -> right
상태 1 -> right
상태 2 -> right
상태 3 -> right

2. OpenAI Gym과 DQN(Deep Q-Network)

1차원 맵처럼 상태가 적을 때는 표(Q-Table)를 만들어 학습할 수 있지만, 자율주행이나 체스처럼 상태가 무한에 가까울 때는 표로 기록하는 것이 불가능합니다. 이를 해결하기 위해 신경망(Neural Network)을 결합한 것이 바로 DQN(Deep Q-Network) 입니다.

Gym (OpenAI Gym / Gymnasium)은 이러한 강화학습 알고리즘을 테스트할 수 있도록 CartPole, MountainCar, Atari 게임 등 다양한 표준 환경(Environment)을 제공하는 파이썬 라이브러리입니다.

DQN 예제: CartPole-v1 (막대 세우기)

아래는 Gym 환경에서 막대가 쓰러지지 않도록 카트를 좌우로 움직이는 DQN 파이토치(PyTorch) 구현 예제입니다. 에이전트는 상태(카트 위치, 속도, 막대 각도 등)를 신경망에 입력하여 왼쪽/오른쪽 행동의 Q값을 예측하고 학습합니다.

환경 구성을 위해 사전에 pip install gymnasium 설치가 필요합니다.

import gymnasium as gym
import collections
import random
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

learning_rate = 0.0005
gamma = 0.98
buffer_limit = 50000
batch_size = 32

# 경험 재생 버퍼 (Replay Buffer)
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)

# Q 네트워크 (신경망 모델)
class Qnet(nn.Module):
    def __init__(self):
        super(Qnet, self).__init__()
        # 입력: 카트 위치, 카트 속도, 막대 각도, 막대 각속도 (4)
        self.fc1 = nn.Linear(4, 128)
        self.fc2 = nn.Linear(128, 128)
        # 출력: 왼쪽(0), 오른쪽(1)에 대한 각각의 Q값 (2)
        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)
        coin = random.random()
        if coin < epsilon:
            return random.randint(0, 1)  # Exploration (탐험)
        else:
            return out.argmax().item()   # Exploitation (활용)

# 학습 함수
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값 예측
        q_a = q_out.gather(1, a)                       # 실제 선택한 행동의 Q값 추출
        
        # 타겟 네트워크를 이용해 다음 상태의 최대 Q값 계산
        max_q_prime = q_target(s_prime).max(1)[0].unsqueeze(1)
        target = r + gamma * max_q_prime * done_mask
        
        # Huber Loss (MSE 대신 자주 쓰임)
        loss = F.smooth_l1_loss(q_a, target)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

# 메인 실행 함수
def main():
    env = gym.make('CartPole-v1')
    q = Qnet()                   # 학습할 Q 네트워크
    q_target = Qnet()            # 안정적인 타겟을 제공할 네트워크
    q_target.load_state_dict(q.state_dict())
    
    memory = ReplayBuffer()
    print_interval = 20
    score = 0.0
    optimizer = optim.Adam(q.parameters(), lr=learning_rate)

    for n_epi in range(10000):
        # 초반에는 탐험을 많이(약 8%), 후반부로 갈수록 최소 1%까지 감소
        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 else 1.0
            # 학습 안정성을 위해 보상을 축소 (r/100.0)
            memory.put((s, a, r / 100.0, s_prime, done_mask))
            
            s = s_prime
            score += r
            if done:
                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("에피소드: {}, 평균 점수: {:.1f}, 버퍼 크기: {}, Epsilon: {:.1f}%".format(
                n_epi, score / print_interval, memory.size(), epsilon * 100))
            score = 0.0
            
    env.close()

if __name__ == '__main__':
    main()
# 출력 예시

n_episode :360, score : 287.4, n_buffer : 13213, eps : 6.2%
n_episode :380, score : 347.8, n_buffer : 20169, eps : 6.1%
n_episode :400, score : 409.9, n_buffer : 28368, eps : 6.0%
n_episode :420, score : 374.0, n_buffer : 35848, eps : 5.9%
n_episode :440, score : 287.8, n_buffer : 41603, eps : 5.8%
n_episode :460, score : 325.9, n_buffer : 48120, eps : 5.7%
n_episode :480, score : 314.4, n_buffer : 50000, eps : 5.6%
n_episode :500, score : 7004.5, n_buffer : 50000, eps : 5.5%
n_episode :520, score : 475.1, n_buffer : 50000, eps : 5.4%
n_episode :540, score : 486.4, n_buffer : 50000, eps : 5.3%
n_episode :560, score : 435.4, n_buffer : 50000, eps : 5.2%
n_episode :580, score : 337.8, n_buffer : 50000, eps : 5.1%

'개념 정리 step2 > 강화 학습' 카테고리의 다른 글

[강화학습] A3C, A2C, ACER 알고리즘 핵심 정리 및 구현  (0) 2026.03.14
[강화학습] Policy Gradient부터 Actor-Critic까지 정리  (0) 2026.03.10
[강화학습] Deep Reinforcement Learning 개념  (0) 2026.03.03
[강화학습] TD Learning (시간차 학습) 개념, 랜덤 벽 GridWorld 실습  (0) 2026.03.01
[강화학습] Monte Carlo Learning 정리  (0) 2026.02.28
'개념 정리 step2/강화 학습' 카테고리의 다른 글
  • [강화학습] A3C, A2C, ACER 알고리즘 핵심 정리 및 구현
  • [강화학습] Policy Gradient부터 Actor-Critic까지 정리
  • [강화학습] Deep Reinforcement Learning 개념
  • [강화학습] TD Learning (시간차 학습) 개념, 랜덤 벽 GridWorld 실습
고니3000원
고니3000원
공부 내용 정리, 자기발전 블로그 입니다. 기존 네이버 블로그에서 티스토리로 이전했습니다. https://blog.naver.com/pak1010pak
  • 고니3000원
    곤이의 공부 블로그
    고니3000원
  • 전체
    오늘
    어제
    • 분류 전체보기 (178) N
      • 1. AI 논문 + 모델 분석 (20)
        • AI 논문 분석 (13)
        • AI 모델 분석 (7)
      • 2. 자료구조와 알고리즘 (16)
        • 2-1 자료구조와 알고리즘 (13)
        • 2-2 강화학습 알고리즘 (3)
      • 3. 자습 & 메모(실전, 실습, 프로젝트) (25)
        • 3-1 문제 해석 (4)
        • 3-2 메모(실전, 프로젝트) (14)
        • 3-3 배포 실전 공부 (7)
      • 4. [팀] 프로젝트 및 공모전 (14)
        • 4-1 팀 프로젝트(메모, 공부) (1)
        • 4-2 Meat-A-Eye (6)
        • 4-3 RL-Tycoon-Agent (3)
        • 4-4 구조물 안정성 물리 추론 AI 경진대회(D.. (4)
      • 5. [개인] 프로젝트 및 공모전 (0)
        • 4-1 귀멸의칼날디펜스(자바스크립트 활용) (5)
        • 4-2 바탕화면 AI 펫 프로그램 (4)
        • 4-3 개인 프로젝트(기타) (3)
      • 개념 정리 step1 (32)
        • Python 기초 (7)
        • DBMS (1)
        • HTML | CSS (3)
        • Git | GitHub (1)
        • JavaScript (5)
        • Node.js (5)
        • React (1)
        • 데이터 분석 (6)
        • Python Engineering (3)
      • 개념 정리 step2 (57) N
        • Machine | Deep Learning (15)
        • 멀티모달(Multi-modal) (23)
        • 강화 학습 (10)
        • AI Agent (9) N
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • 네이버 곤이의 블로그(Naver->Tistory)
    • Github
  • 공지사항

  • 인기 글

  • 태그

    pandas
    transformer
    bottleneck
    OCR학습
    Ai
    논문 리뷰
    github
    자바스크립트
    Grad-CAM
    프로젝트
    학습
    자료구조
    EfficientNet
    ViT
    공모전
    알고리즘
    구현
    javascript
    Vision
    귀칼
    Attention Is All You Need
    데이터분석
    파이썬
    OCR
    파인튜닝
    강화학습
    Python
    paddleocr
    강화 학습
    html
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
고니3000원
[강화학습] Q-learning의 개념부터 Gym을 활용한 DQN 구현까지
상단으로

티스토리툴바