본문으로 바로가기

Q-러닝 입문: 초보자를 위한 튜토리얼

Python 튜토리얼로 가장 인기 있는 모델 프리 강화학습 알고리즘을 배워보세요.
업데이트됨 2026년 7월 22일  · 11분 읽다

AI로 탐색하기

ChatGPT에서 열기Claude에서 열기Perplexity에서 열기

Q Learning Header

강화학습(RL)은 에이전트가 환경과 상호작용하며 목표를 달성하기 위한 최적의 전략을 학습하는 머신 러닝 영역입니다. 데이터 입력과 처리가 필요한 지도학습과는 꽤 다릅니다. 강화학습은 별도의 데이터가 필요하지 않습니다. 대신 환경과 보상 체계로부터 학습하여 더 나은 의사결정을 내립니다.

예를 들어, 마리오 비디오게임에서 캐릭터가 임의의 행동(예: 왼쪽으로 이동)을 하면, 그 행동에 따라 보상을 받을 수 있습니다. 행동을 취한 후 에이전트(마리오)는 새로운 상태에 놓이고, 게임 캐릭터가 스테이지의 끝에 도달하거나 사망할 때까지 이 과정이 반복됩니다. 

이 에피소드는 마리오가 보상을 최대화하여 환경을 탐색하는 법을 배울 때까지 여러 번 반복됩니다. 

Reinforcement Learning

이미지: 필자 제작

강화학습은 다음 다섯 단계로 나눌 수 있습니다:

  1. 에이전트는 환경의 상태 0에 있습니다.
  2. 특정 전략에 따라 행동을 취합니다.
  3. 그 행동에 따라 보상이나 패널티를 받습니다.
  4. 이전 움직임에서 학습하고 전략을 최적화합니다. 
  5. 최적 전략을 찾을 때까지 이 과정을 반복합니다. 

강화학습 소개 튜토리얼을 읽고 더 알아보세요. 코드 예제로 강화학습이 어떻게 작동하는지 살펴봅니다. 

이 튜토리얼에서는 Q-러닝을 배우고, 왜 딥 Q-러닝이 필요한지 이해합니다. 또한 Numpy와 Gymnasium을 사용해 Q-러닝 알고리즘을 처음부터 직접 만들고 학습해 봅니다.

참고: 머신 러닝이 처음이면 Machine Learning Scientist with Python 커리어 트랙을 수강해 강화학습과 Q-러닝을 더 잘 이해하시길 권장합니다. 

Q-러닝이란?

Q-러닝은 모델 프리, 가치 기반, 오프-폴리시 알고리즘으로, 에이전트의 현재 상태를 바탕으로 최적의 행동 시퀀스를 찾습니다. 여기서 “Q”는 품질(quality)을 뜻합니다. 품질은 미래 보상을 최대화하는 데 있어 해당 행동의 가치를 나타냅니다.  

모델 기반 알고리즘은 전이 및 보상 함수를 사용해 최적 정책을 추정하고 모델을 만듭니다. 반면 모델 프리 알고리즘은 전이/보상 함수 없이 경험을 통해 행동의 결과를 학습합니다. 

가치 기반 방법은 가치 함수를 학습해 어떤 상태가 더 가치 있는지 파악하고 행동을 취합니다. 반대로 정책 기반 방법은 정책을 직접 학습하여 주어진 상태에서 어떤 행동을 취해야 하는지 배웁니다.

오프-폴리시에서는 행동을 취하는 데 사용한 정책과 다른 정책을 평가하고 업데이트합니다. 반대로 온-폴리시 알고리즘은 행동에 사용한 동일한 정책을 평가하고 개선합니다.  

Q-러닝의 핵심 용어

Q-러닝이 어떻게 작동하는지 살펴보기 전에, 기본을 이해하는 데 유용한 몇 가지 용어를 먼저 알아봅시다. 

  • 상태(s): 환경에서 에이전트의 현재 위치. 
  • 행동(a): 특정 상태에서 에이전트가 취한 한 걸음. 
  • 보상: 각 행동에 대해 에이전트가 받는 보상 또는 패널티. 
  • 에피소드: 에이전트가 더 이상 새로운 행동을 할 수 없는 스테이지의 끝. 목표 달성 또는 실패 시 발생. 
  • Q(St+1, a): 특정 상태에서 행동을 했을 때의 기대 최적 Q-값. 
  • Q(St, At): Q(St+1, a)의 현재 추정치.
  • Q-테이블: 에이전트가 상태와 행동의 집합에 대한 Q-테이블을 유지.
  • 시간차(TD): 현재/이전 상태와 행동을 사용해 Q(St+1, a)의 기대값을 추정하는 데 사용. 

Q-러닝은 어떻게 작동하나요?

얼어붙은 호수(frozen lake) 예제를 통해 Q-러닝의 작동 방식을 자세히 배워봅니다. 이 환경에서 에이전트는 구멍에 빠지지 않고 시작점에서 목표까지 얼음 호수를 건너야 합니다. 최적 전략은 최단 경로로 목표에 도달하는 것입니다. 

Q-Learning Visualization

Gif: 필자 제작

Q-테이블

에이전트는 환경의 각 상태에서 기대 보상을 바탕으로 최선의 행동을 취하기 위해 Q-테이블을 사용합니다. 간단히 말해 Q-테이블은 행동과 상태의 데이터 구조이며, Q-러닝 알고리즘을 사용해 테이블의 값을 업데이트합니다. 

Q-함수

Q-함수는 벨만 방정식을 사용하며 상태(s)와 행동(a)을 입력으로 받습니다. 이 방정식은 상태 값과 상태-행동 값 계산을 단순화합니다. Bellman Equation

이미지 출처: freecodecamp.org

Q-러닝 알고리즘

Q-Learning Process

이미지: 필자 제작

Q-테이블 초기화

먼저 Q-테이블을 초기화합니다. 열은 행동 수, 행은 상태 수를 기준으로 테이블을 구성합니다.

예시에서 캐릭터는 위, 아래, 왼쪽, 오른쪽으로 이동할 수 있습니다. 가능한 행동은 4개이고, 상태는 4개(시작, 정지, 잘못된 경로, 종료)입니다. 잘못된 경로는 구멍에 빠지는 것으로 볼 수 있습니다. Q-테이블은 0으로 초기화합니다. 

Q-Table 1

이미지: 필자 제작

행동 선택

두 번째 단계는 비교적 간단합니다. 시작할 때 에이전트는 임의의 행동(아래 또는 오른쪽)을 선택하고, 두 번째 실행부터는 업데이트된 Q-테이블을 사용해 행동을 선택합니다. 

행동 수행

행동 선택과 수행은 학습 루프가 멈출 때까지 여러 번 반복됩니다. 첫 행동과 상태는 Q-테이블을 사용해 선택합니다. 이 경우 Q-테이블의 모든 값은 0입니다. 

그 다음 에이전트는 아래로 이동하고 벨만 방정식을 사용해 Q-테이블을 업데이트합니다. 매 이동마다 Q-테이블의 값을 업데이트하고, 이를 이용해 최적의 행동 경로를 결정합니다. 

초기에는 에이전트가 탐색 모드에 있으며, 환경을 탐험하기 위해 임의의 행동을 선택합니다. 엡실론 그리디 전략은 탐색과 활용의 균형을 맞추는 간단한 방법입니다. 엡실론은 탐색을 선택할 확률을 의미하며, 탐색 확률이 낮아질수록 활용을 더 많이 합니다. 

초기에는 엡실론 값이 높아 에이전트가 탐색 모드에 있습니다. 환경을 탐색하는 동안 엡실론이 감소하고, 에이전트는 환경을 활용하기 시작합니다. 탐색 단계에서 반복할수록 에이전트는 Q-값 추정에 더 자신감을 갖게 됩니다

Q-Table 2

이미지: 필자 제작

얼어붙은 호수 예시에서 에이전트는 환경을 모르는 상태이므로 시작할 때 임의의 행동(아래로 이동)을 합니다. 위 이미지와 같이 Q-테이블은 벨만 방정식을 사용해 업데이트됩니다.

보상 측정

행동을 취한 뒤 결과와 보상을 측정합니다. 

  • 목표에 도달: +1
  • 잘못된 경로(구멍에 빠짐): 0
  • 정지 또는 얼어붙은 호수 위 이동: 0 

Q-테이블 업데이트

식을 사용해 Q(St, At) 함수를 업데이트합니다. 이전 에피소드의 추정 Q-값, 학습률, 그리고 시간차 오차를 사용합니다. 시간차 오차는 즉시 보상, 할인된 최대 기대 미래 보상, 그리고 이전 추정 Q-값으로 계산됩니다. 

이 과정은 Q-테이블이 업데이트되고 Q-값 함수가 최대화될 때까지 여러 번 반복됩니다. 

Q-learning equation

이미지: 필자 제작 | 방정식 시각화: Thomas Simonini

처음에는 에이전트가 Q-테이블을 업데이트하기 위해 환경을 탐색합니다. 그리고 Q-테이블이 준비되면 에이전트는 활용을 시작해 더 나은 결정을 내립니다. Q-Table 3

이미지: 필자 제작

얼어붙은 호수의 경우, 에이전트는 목표에 도달하기 위한 최단 경로를 학습하고 구멍으로 뛰어드는 것을 피하게 됩니다. 

Q-러닝 Python 튜토리얼 

이 섹션에서는 Gymnasium 환경, Pygame, Numpy를 사용하여 Q-러닝 모델을 처음부터 구축합니다. 이 Python 튜토리얼은 Thomas Simonini의 노트북을 수정한 버전입니다. 환경 및 Q-테이블 초기화, 그리디 정책 정의, 하이퍼파라미터 설정, 학습 루프와 평가 생성/실행, 결과 시각화를 포함합니다.   

학습 루프 생성 및 실행에 문제가 있다면 출력과 함께 제공되는 코드 소스를 확인하세요.   

환경 설정

가상 디스플레이 설정

먼저 리플레이 영상(Gif)을 생성하기 위한 모든 의존성을 설치합니다. 환경을 렌더링하고 프레임을 기록하려면 가상 화면(pyvirtualdisplay)이 필요합니다. 

참고: %%capture를 사용하면 Jupyter 셀의 출력을 숨깁니다. 

%%capture
!pip install pyglet==1.5.1
!apt install python-opengl
!apt install ffmpeg
!apt install xvfb
!pip3 install pyvirtualdisplay

# Virtual display
from pyvirtualdisplay import Display

virtual_display = Display(visible=0, size=(1400, 900))
virtual_display.start()

의존성 설치

이제 학습 루프를 생성, 실행, 평가하는 데 도움이 되는 의존성을 설치합니다. 

  • gymnasium: FrozenLake-v1 환경 초기화에 사용.
  • pygame: FrozenLake-v1 UI에 사용.
  • numPy: Q-테이블 생성 및 처리에 사용.
%%capture
!pip install gymnasium
!pip install pygame
!pip install numpy

!pip install imageio imageio_ffmpeg

패키지 임포트

이제 필요한 라이브러리를 임포트합니다. 

  • Imageio는 애니메이션 생성에 사용합니다. 
  • tqdm은 진행 표시줄에 사용합니다. 
import numpy as np
import gymnasium as gym
import random
import imageio
from tqdm.notebook import trange

Frozen lake Gymnasium 환경 

Frozen Lake gymnasium 라이브러리를 사용해 미끄럽지 않은 4x4 환경을 생성합니다. 

  • 격자 버전은 “4x4”와 “8x8” 두 가지가 있습니다.
  • is_slippery=True이면 얼어붙은 호수의 미끄러운 성질로 인해 에이전트가 의도한 방향으로 이동하지 않을 수 있습니다. 

환경을 초기화한 후 환경 분석을 진행합니다. 

env = gym.make("FrozenLake-v1",map_name="4x4",is_slippery=False)

print("Observation Space", env.observation_space)
print("Sample observation", env.observation_space.sample()) # display a random observation

환경에는 무작위 위치에 표시되는 16개의 고유 공간이 있습니다. 

Observation Space Discrete(16)
Sample observation 15

가능한 행동 수를 알아보고 임의의 행동을 표시해 봅시다. 

행동 공간:

  • 0: 왼쪽으로 이동
  • 1: 아래로 이동
  • 2: 오른쪽으로 이동
  • 3: 위로 이동

보상 함수:

  • 목표 도달: +1
  • 구멍에 빠짐: 0
  • 얼어붙은 호수 위에 머무름: 0
print("Action Space Shape", env.action_space.n)
print("Action Space Sample", env.action_space.sample())
Action Space Shape 4
Action Space Sample 1

Q-테이블 생성 및 초기화

Q-테이블은 열이 행동, 행이 상태입니다. Gymnasium으로 행동 공간과 상태 공간을 확인하고, 이 정보를 사용해 Q-테이블을 만듭니다. 

state_space = env.observation_space.n
print("There are ", state_space, " possible states")

action_space = env.action_space.n
print("There are ", action_space, " possible actions")
There are  16  possible states
There are  4  possible actions

Q-테이블 초기화를 위해 상태 공간과 행동 공간 크기의 Numpy 배열을 생성합니다. 16 X 4 배열을 만듭니다. 

def initialize_q_table(state_space, action_space):
  Qtable = np.zeros((state_space, action_space))
  return Qtable

Qtable_frozenlake = initialize_q_table(state_space, action_space)

엡실론-그리디 정책

앞서 탐색과 활용의 트레이드오프를 다루는 엡실론 그리디 전략을 배웠습니다. 확률 1 - ɛ로는 활용을, 확률 ɛ로는 탐색을 수행합니다. 

epsilon_greedy_policy에서는 다음을 수행합니다:

  1. 0과 1 사이의 난수를 생성합니다.
  2. 난수가 엡실론보다 크면 활용을 수행합니다. 즉, 주어진 상태에서 값이 가장 높은 행동을 선택합니다.
  3. 그 외에는 탐색(임의의 행동 선택)을 수행합니다. 
def epsilon_greedy_policy(Qtable, state, epsilon):
  random_int = random.uniform(0,1)
  if random_int > epsilon:
    action = np.argmax(Qtable[state])
  else:
    action = env.action_space.sample()
  return action

그리디 정책 정의

Q-러닝은 오프-폴리시 알고리즘이므로, 행동을 취하는 정책과 함수를 업데이트하는 정책이 다릅니다. 

이 예시에서는 엡실론 그리디 정책이 행동 정책이고, 그리디 정책이 업데이트 정책입니다. 

그리디 정책은 에이전트가 학습된 후의 최종 정책이기도 합니다. Q-테이블에서 가장 높은 상태-행동 값을 선택하는 데 사용됩니다.

def greedy_policy(Qtable, state):
  action = np.argmax(Qtable[state])
  return action

모델 하이퍼파라미터

이 하이퍼파라미터는 학습 루프에서 사용되며, 잘 조정하면 더 좋은 결과를 얻을 수 있습니다. 

에이전트가 좋은 가치 근사를 학습하려면 충분한 상태 공간을 탐색해야 하므로 엡실론을 점진적으로 감소시켜야 합니다. 감쇠율이 너무 높으면 충분히 탐색하지 못해 갇힐 수 있습니다.

  • 학습 에피소드는 10,000회, 평가 에피소드는 100회입니다.
  • 학습률은 0.7입니다.
  • 환경은 "FrozenLake-v1"이며 에피소드당 최대 스텝 수는 99입니다.
  • 감마(할인율)는 0.95입니다.
  • eval_seed: 환경 평가용 시드.
  • 탐색 엡실론 확률은 시작 시 1.0, 최소 확률은 0.05입니다.
  • 엡실론 확률의 지수 감쇠율은 0.0005입니다.
# Training parameters
n_training_episodes = 10000
learning_rate = 0.7        

# Evaluation parameters
n_eval_episodes = 100      

# Environment parameters
env_id = "FrozenLake-v1"   
max_steps = 99             
gamma = 0.95               
eval_seed = []             

# Exploration parameters
max_epsilon = 1.0           
min_epsilon = 0.05           
decay_rate = 0.0005           

모델 학습 

학습 루프에서는 다음을 수행합니다:

  1. 학습 에피소드 루프를 만듭니다.
  2. 엡실론을 먼저 줄입니다. 에피소드가 진행될수록 탐색은 줄이고 활용을 늘려야 합니다. 
  3. 환경을 리셋합니다.
  4. 최대 스텝 수에 대한 중첩 루프를 만듭니다.
  5. 엡실론 그리디 정책으로 행동을 선택합니다. 
  6. 행동(At)을 취하고 기대 보상(Rt+1)과 상태(St+1)를 관찰합니다.
  7. 행동(a)을 취하고 결과 상태(s')와 보상(r)을 관찰합니다.
  8. 공식을 사용해 Q-함수를 업데이트합니다. 
  9. done= True이면 에피소드를 종료하고 루프를 중단합니다.
  10. 마지막으로 현재 상태를 새로운 상태로 바꿉니다. 
  11. 모든 학습 에피소드가 끝나면 업데이트된 Q-테이블을 반환합니다. 
def train(n_training_episodes, min_epsilon, max_epsilon, decay_rate, env, max_steps, Qtable):
  for episode in trange(n_training_episodes):
 
    epsilon = min_epsilon + (max_epsilon - min_epsilon)*np.exp(-decay_rate*episode)
    # Reset the environment
    state = env.reset()
    step = 0
    done = False

    # repeat
    for step in range(max_steps):
   
      action = epsilon_greedy_policy(Qtable, state, epsilon)

   
      new_state, reward, done, info = env.step(action)

   
      Qtable[state][action] = Qtable[state][action] + learning_rate * (reward + gamma * np.max(Qtable[new_state]) - Qtable[state][action])

      # If done, finish the episode
      if done:
        break
     
      # Our state is the new state
      state = new_state
  return Qtable

10,000개의 학습 에피소드를 3초 만에 완료했습니다. 

Qtable_frozenlake = train(n_training_episodes, min_epsilon, max_epsilon, decay_rate, env, max_steps, Qtable_frozenlake)

Training Episodes Outcome

보시다시피 학습된 Q-테이블에는 값이 채워졌고, 이제 에이전트는 이 값을 사용해 환경을 탐색하고 목표를 달성합니다.  

Qtable_frozenlake
array([[0.73509189, 0.77378094, 0.77378094, 0.73509189],
      [0.73509189, 0.        , 0.81450625, 0.77378094],
      [0.77378094, 0.857375  , 0.77378094, 0.81450625],
      [0.81450625, 0.        , 0.77378094, 0.77378094],
      [0.77378094, 0.81450625, 0.        , 0.73509189],
      [0.        , 0.        , 0.        , 0.        ],
      [0.        , 0.9025    , 0.        , 0.81450625],
      [0.        , 0.        , 0.        , 0.        ],
      [0.81450625, 0.        , 0.857375  , 0.77378094],
      [0.81450625, 0.9025    , 0.9025    , 0.        ],
      [0.857375  , 0.95      , 0.        , 0.857375  ],
      [0.        , 0.        , 0.        , 0.        ],
      [0.        , 0.        , 0.        , 0.        ],
      [0.        , 0.9025    , 0.95      , 0.857375  ],
      [0.9025    , 0.95      , 1.        , 0.9025    ],
      [0.        , 0.        , 0.        , 0.        ]])

평가

evaluate_agent는 n_eval_episodes 만큼 에피소드를 실행하고 보상의 평균과 표준편차를 반환합니다. 

  1. 루프에서 먼저 평가 시드가 있는지 확인합니다. 없다면 시드 없이 환경을 리셋합니다. 
  2. 중첩 루프는 max_steps까지 실행됩니다.
  3. 에이전트는 주어진 상태에서 Q-테이블을 사용하여 기대 미래 보상이 최대인 행동을 취합니다. 
  4. 보상을 계산합니다.
  5. 상태를 변경합니다.
  6. 종료(구멍에 빠지거나 목표 달성)되면 루프를 중단합니다.
  7. 결과를 추가합니다.
  8. 마지막에 이 결과로 평균과 표준편차를 계산합니다. 
def evaluate_agent(env, max_steps, n_eval_episodes, Q, seed):

  episode_rewards = []
  for episode in range(n_eval_episodes):
    if seed:
      state = env.reset(seed=seed[episode])
    else:
      state = env.reset()
    step = 0
    done = False
    total_rewards_ep = 0
   
    for step in range(max_steps):
      # Take the action (index) that have the maximum reward
      action = np.argmax(Q[state][:])
      new_state, reward, done, info = env.step(action)
      total_rewards_ep += reward
       
      if done:
        break
      state = new_state
    episode_rewards.append(total_rewards_ep)
  mean_reward = np.mean(episode_rewards)
  std_reward = np.std(episode_rewards)

  return mean_reward, std_reward

보시다시피 표준편차 0의 만점을 받았습니다. 100개 모든 에피소드에서 에이전트가 목표에 도달했음을 의미합니다. 

# Evaluate our Agent
mean_reward, std_reward = evaluate_agent(env, max_steps, n_eval_episodes, Qtable_frozenlake, eval_seed)
print(f"Mean_reward={mean_reward:.2f} +/- {std_reward:.2f}")
Mean_reward=1.00 +/- 0.00

결과 시각화

지금까지는 숫자로만 다뤘으니, 데모를 위해 에이전트가 시작부터 목표에 도달할 때까지의 과정을 담은 애니메이션 Gif를 만들어 보겠습니다. 

  1. 먼저 0-500 사이의 임의 정수 시드로 환경을 리셋해 상태를 생성합니다. 
  2. rdb_array로 환경을 렌더링해 이미지 배열을 만듭니다. 
  3. 그런 다음 imgimages 배열에 추가합니다. 
  4. 루프에서 Q-테이블을 사용해 스텝을 진행하고, 매 스텝마다 이미지를 렌더링합니다. 
  5. 마지막에 이 배열과 imageio를 사용해 초당 한 프레임의 Gif를 생성합니다. 
def record_video(env, Qtable, out_directory, fps=1):
  images = [] 
  done = False
  state = env.reset(seed=random.randint(0,500))
  img = env.render(mode='rgb_array')
  images.append(img)
  while not done:
    # Take the action (index) that have the maximum expected future reward given that state
    action = np.argmax(Qtable[state][:])
    state, reward, done, info = env.step(action) # We directly put next_state = state for recording logic
    img = env.render(mode='rgb_array')
    images.append(img)
  imageio.mimsave(out_directory, [np.array(img) for i, img in enumerate(images)], fps=fps)

Jupyter 노트북에서는 IPython.display의 Image 함수를 사용해 Gif를 표시할 수 있습니다. 

video_path="/content/replay.gif"
video_fps=1
record_video(env, Qtable_frozenlake, video_path, video_fps)

from IPython.display import Image
Image('./replay.gif')

이제 동료나 학급 친구들과 결과를 공유하거나 소셜 미디어에 게시해 보세요.

Q-러닝 자주 묻는 질문

Q-러닝의 단점은 무엇인가요?

Q-러닝에서 학습 과정은, 특히 초기 단계에서, 에이전트에게 비용이 많이 듭니다. 왜 그럴까요? 최적 정책으로 수렴하려면 모든 상태-행동 쌍을 충분히 자주 방문해야 하기 때문입니다.

왜 Q-러닝이라고 부르나요?

Q-러닝에서 ‘Q’는 품질(quality)을 의미합니다. 이는 미래 보상을 달성하는 데 있어 주어진 행동이 얼마나 유용한지를 나타내며, 기대 보상을 최대화하기 위한 상태-행동의 지도 체계를 만드는 데 사용됩니다.

왜 Q-러닝은 오프-폴리시인가요?

Q-러닝에서는 업데이트되는 정책이 행동(행동 선택) 정책과 다르기 때문에 오프-폴리시 알고리즘이라고 합니다.

Q-러닝은 항상 수렴하나요?

네. 학습 과정에서 알고리즘은 항상 최적 정책으로 수렴합니다.

왜 딥 Q-러닝이 필요한가요?

Q-러닝은 작고 이산적인 환경을 위해 설계된 단순한 알고리즘입니다. 더 큰 환경에서는 상태와 행동에 대한 Q-테이블이 엄청나게 커져 메모리와 연산 비용이 크게 증가합니다. 반면, 딥 Q-러닝은 연속적인 행동과 상태가 포함된 대규모 환경을 다루기 위해 Q-테이블을 신경망으로 대체합니다.

주제

머신 러닝 과정

courses

Python으로 설계하는 Machine Learning 워크플로

4
12.6K
시간의 시험을 견디는 파이프라인 구축법을 익히세요.
자세히 보기Right Arrow
강좌 시작
더 보기Right Arrow