이 글은 서강대 가상융합 전문대학원(구 메타버스 전문대학원) 강화학습 자료를 기반으로 작성되었습니다.
Q-learning vs SARSA: 지도 없이 길을 찾는 방법
세상은 우리가 미리 계산할 수 없는 일들로 가득 차 있다. 도로 위 자율주행차는 예기치 않은 상황을 맞닥뜨리고, 로봇은 사람의 행동을 예측해야 한다. 이처럼 완벽한 모델링이 불가능한 환경에서 인공지능이 스스로 학습하려면 어떻게 해야 할까? 그 해답 중 하나가 Q-learning이다. Q-learning은 ‘모델이 없어도 학습할 수 있는 인공지능’을 가능하게 한 획기적인 방법이다. 기존의 모델 기반 강화학습은 환경의 전이확률과 보상 함수를 알아야 했다. 하지만 현실 세계에서는 이러한 정보를 얻는 것이 불가능에 가깝다. Q-learning은 이를 정면으로 돌파했다. 오직 (상태, 행동, 보상, 다음 상태)라는 네 가지 정보만을 가지고 스스로 최적의 행동전략을 찾아간다.
Q-learning의 중심에는 행동가치함수(Action-Value Function), 즉 Q(s,a)가 있다. Q(s,a)는 “상태 s에서 행동 a를 선택했을 때 앞으로 얻을 수 있는 누적 보상의 기대값”이다. 이 값이 높을수록 그 행동은 더 유리하다는 뜻이다. Q-learning의 목표는 이 Q(s,a)를 반복적으로 개선하여 모든 상황에서 최적의 행동을 고를 수 있는 정책 π*(s)=argmax_a Q*(s,a)를 찾는 것이다. 이 과정은 벨만 최적 방정식(Bellman Optimality Equation)을 기반으로 한다. 이 방정식은 현재의 보상 R(s,a)와 다음 상태의 최대 Q값을 결합하여 현재 행동의 가치를 계산한다. 즉, Q*(s,a) = R(s,a) + γ ∑ P(s’|s,a) max_a′ Q*(s’,a′) 로 표현할 수 있다. 여기서 γ는 감쇠율(discount factor)로, 미래 보상의 가치를 현재로 환산하는 역할을 한다. 이 수식은 “지금의 행동 가치는, 즉시 얻는 보상 + 최선의 미래 가치”라는 개념을 담고 있다. Q-learning은 이 식을 근사적으로 구현한다. 실제 전이확률을 모르므로, 기대값 대신 실제 경험한 하나의 샘플 전이(s,a,r,s’)를 이용해 Q를 갱신한다. 이러한 접근이 바로 샘플 기반 근사(Bootstrapping)이며, 강화학습의 본질이기도 하다. Q-learning의 학습 규칙은 단 한 줄의 수식으로 요약된다.
Q(s,a) ← Q(s,a) + α [r + γ max_a′ Q(s’,a′) − Q(s,a)]
여기서 α는 학습률(learning rate)이며, 괄호 안의 차이는 TD오차(Temporal Difference Error)라 불린다. TD오차는 “현재 예측과 실제 경험의 차이”를 의미하며, 이 오차를 줄이는 방향으로 Q값을 업데이트하는 것이 학습의 핵심이다. 문제는 “언제 새로운 행동을 시도할 것인가”이다. 새로운 행동을 탐험하지 않으면 좋은 전략을 발견할 수 없고, 너무 자주 시도하면 이미 배운 내용을 잊게 된다. 이를 해결하는 것이 ε-greedy 정책이다. 확률 ε만큼은 무작위로 행동을 선택(탐험)하고, 1−ε의 확률로는 현재 가장 큰 Q값을 선택(활용)한다. ε은 점차 감소하여 학습 후반에는 탐욕적(greedy) 정책으로 수렴한다. 이 전략은 단순하지만 강력하다. Watkins와 Dayan(1992)은 이러한 방식이 모든 상태-행동쌍을 무한히 방문하고(탐험 조건), 학습률이 점차 줄어들면(감쇠 조건) 거의 확실히 Q*로 수렴함을 증명했다. 이를 GLIE(Greedy in the Limit with Infinite Exploration)라고 부른다.
이를 확인하기 위해 PyTorch를 이용해 Q-learning을 직접 구현해 보자. 실험 환경으로는 OpenAI Gym의 FrozenLake을 사용한다. 이 환경은 얼음 위를 이동하며 구멍(Hole)을 피해 목표 지점(Goal)에 도달해야 하는 간단한 게임이지만, 보상이 매우 희소하고 이동의 성공 여부가 확률적으로 결정되기 때문에 탐험(Exploration)의 중요성을 명확히 보여준다. 코드 구현 절차는 다음과 같다. 먼저 Q테이블을 상태×행동 차원으로 0으로 초기화한다. 이후 ε-greedy 정책을 이용해 행동을 선택하고, 선택한 행동에 따라 환경으로부터 보상을 얻은 뒤 다음 상태를 관찰한다. 그런 다음, 현재 예측과 실제 경험의 차이인 TD오차(Temporal Difference Error)를 계산하여 Q값을 갱신한다. 이 일련의 과정을 에피소드가 종료될 때까지 반복하며, 각 반복을 통해 Q값이 점점 더 정확해진다. 이 단순한 과정만으로도 학습은 놀라운 결과를 보인다. 약 500회의 학습 에피소드 후, 에이전트는 FrozenLake 환경에서 90% 이상의 성공률을 달성했다. 특히 탐험률 ε을 일정하게 유지한 경우보다, 시간에 따라 점진적으로 감소(decay)시키는 감쇠형 ε 스케줄을 적용했을 때 학습은 훨씬 더 빠르고 안정적으로 수렴했다. 이는 탐험의 초기 중요성과 점진적 활용의 균형이 Q-learning의 효율적 학습에 필수적임을 보여주는 대표적 사례라 할 수 있다. 학습률 α의 선택도 중요한 변수다. 너무 크면 Q값이 진동하고, 너무 작으면 학습 속도가 느려진다. 1/t나 1/√t 형태의 감소 스케줄은 Robbins-Monro 조건을 만족하여 수렴을 보장한다. 또한 학습률을 일정하게 유지하더라도 실험적으로는 안정적인 결과를 보일 수 있다. 탐험 전략도 여러 가지가 있다. Boltzmann 탐험은 확률적으로 좋은 행동을 선택하며, UCB(Upper Confidence Bound)는 불확실성이 큰 행동을 더 자주 시도한다. 그러나 계산 효율성과 단순성 측면에서 ε-greedy는 여전히 가장 널리 쓰이는 기본 전략으로 남아 있다.
Q-learning의 장점은 “오프정책(Off-policy)” 학습이다. 즉, 실제로 어떤 행동을 하더라도 그와 무관하게 최적정책을 학습할 수 있다. 반면 SARSA는 “온정책(On-policy)” 학습으로, 현재 정책이 실제 수행한 행동을 그대로 반영한다. 따라서 Q-learning은 더 빠르고 공격적이지만, SARSA는 더 안정적이고 위험을 회피하는 특성이 있다. Cliff Walking 환경에서는 두 알고리즘의 차이가 뚜렷하다. Q-learning은 절벽가의 최단경로를 선택해 더 빨리 목표에 도달하지만, 작은 실수로 큰 벌점을 받을 수 있다. SARSA는 상대적으로 느리지만, 절벽에서 떨어질 위험이 없는 안전한 경로를 택한다. 즉, 전자는 탐욕적, 후자는 보수적이다. 또한 Q-learning에는 과대추정 편향(Overestimation Bias) 문제가 있다. TD타깃 계산 시 max 연산을 사용하기 때문에, 노이즈가 섞인 Q값 중 큰 값이 선택되어 실제보다 높은 보상이 예측된다. 이를 해결하기 위해 등장한 것이 Double Q-learning이다. 두 개의 Q테이블(Q1, Q2)을 번갈아 사용하여 선택(selection)과 평가(evaluation)을 분리하면 편향이 크게 줄어든다. 실험 결과 Double Q-learning은 과대추정을 약 40~60% 줄였으며, 더 정확하고 안정적인 학습 곡선을 보였다.
학습을 안정화하기 위한 추가 방법으로는 낙관적 초기화(Optimistic Initialization)와 n-step Q-learning이 있다. 낙관적 초기화는 Q값을 1과 같은 양의 값으로 시작해 모든 행동을 최소 한 번은 시도하도록 유도한다. n-step Q-learning은 1-step 대신 n단계의 보상을 합산하여 단기적 편향을 줄이는 방식이다. n이 커질수록 편향은 줄지만 분산이 커지므로 적절한 n의 선택이 필요하다. 이 외에도 학습률, 감쇠율, 탐험률 등 하이퍼파라미터 튜닝이 모델의 성능을 좌우한다. 학습률이 지나치게 높으면 발산하고, 탐험이 부족하면 국소최적해에 머문다. 따라서 실험을 통해 최적의 조합을 찾는 과정이 중요하다. FrozenLake 실험에서 Q-learning은 빠르지만 불안정했고, SARSA는 느리지만 안정적이었다. Double Q-learning은 그 중간에서 균형 잡힌 성능을 보여주었다. 모두 충분한 학습 후에는 75% 이상의 성공률을 달성했다. Q-learning은 모델이 없어도 학습할 수 있다는 사실을 증명했다.
import gymnasium as gym
import numpy as np
import torch
import random
import datetime
from torch.utils.tensorboard import SummaryWriter
# ==========================================
# 1. 정책 정의 (Policy Definitions)
# ==========================================
def q_greedy_policy(state, q_table):
"""
[활용] 최적 정책 π*(s) = argmax_a Q*(s,a) 구현
가장 높은 Q값을 가진 행동(인덱스)을 정수로 반환합니다.
"""
return torch.argmax(q_table[state]).item()
def create_epsilon_greedy_policy(q_table, epsilon, env):
"""
[탐험/활용] ε-greedy 정책
무작위 행동을 통해 새로운 전략을 발견하고(탐험),
동시에 이미 배운 최선의 길을 따릅니다(활용).
"""
def policy_fn(state):
if random.random() < epsilon:
# env.action_space.sample()은 0~3 중 하나를 무작위로 추출합니다.
return env.action_space.sample()
else:
return q_greedy_policy(state, q_table)
return policy_fn
# ==========================================
# 2. 유틸리티 함수 (Utilities)
# ==========================================
def update_q_table(q_table, state, action, reward, next_state, done, alpha, gamma):
"""
[Q-Learning 학습 규칙]
TD 오차(예측과 실제의 차이)를 계산하여 Q값을 갱신합니다.
공식: Q(s,a) ← Q(s,a) + α [r + γ max Q(s',a') - Q(s,a)]
"""
current_q = q_table[state, action]
# 벨만 최적 방정식: 즉시 보상 + 미래 가치의 최댓값
# torch.max(tensor)는 해당 텐서의 최댓값 하나를 반환합니다.
next_max_q = torch.max(q_table[next_state])
target_q = reward + (gamma * next_max_q * (1 - int(done)))
# TD 오차를 반영하여 점수판(Q-table) 업데이트
q_table[state, action] = current_q + alpha * (target_q - current_q)
# ==========================================
# 3. 훈련 및 테스트 루프 (Loops)
# ==========================================
def train_agent(env, num_episodes, alpha, gamma, initial_epsilon, writer):
"""
[환경 정보 요약 - FrozenLake-v1]
1. env.observation_space.n: 상태 개수 (4x4 격자 = 16개)
- 0(시작점)부터 15(목표지점)까지의 정수로 표현됩니다.
2. env.action_space.n: 행동 개수 (4개)
- 0: 왼쪽(Left), 1: 아래(Down), 2: 오른쪽(Right), 3: 위(Up)
"""
# Q-table 초기화: 상태(16) x 행동(4) 크기의 0으로 채워진 테이블 생성
q_table = torch.zeros([env.observation_space.n, env.action_space.n])
epsilon = initial_epsilon
epsilon_decay = 0.998 # 시간에 따라 탐험을 줄이는 감쇠형 스케줄
min_epsilon = 0.01
for episode in range(num_episodes):
# 환경 초기화: 에이전트를 시작점(0번 상태)으로 보냅니다
state, _ = env.reset()
done = False
total_reward = 0
policy = create_epsilon_greedy_policy(q_table, epsilon, env)
while not done:
action = policy(state)
# 행동 수행: 환경으로부터 다음 상태와 보상을 받습니다
next_state, reward, terminated, truncated, _ = env.step(action)
done = terminated or truncated # 구멍에 빠지거나 목표 도달 시 종료
update_q_table(q_table, state, action, reward, next_state, done, alpha, gamma)
state = next_state
total_reward += reward
epsilon = max(min_epsilon, epsilon * epsilon_decay)
writer.add_scalar('Train/Reward', total_reward, episode)
writer.add_scalar('Train/Epsilon', epsilon, episode)
return q_table
def test_agent(env_name, q_table, num_tests=5):
"""학습된 Q-table을 바탕으로 에이전트의 실제 주행을 테스트합니다."""
# 시각화를 위해 render_mode를 'human'으로 설정
test_env = gym.make(env_name, is_slippery=True, render_mode="human")
for i in range(num_tests):
state, _ = test_env.reset()
done = False
print(f"\n[테스트 {i+1}] 에이전트 이동 시작...")
while not done:
# 테스트 단계에서는 오직 학습된 결과(argmax)만 사용합니다
action = q_greedy_policy(state, q_table)
state, reward, terminated, truncated, _ = test_env.step(action)
done = terminated or truncated
if reward > 0:
print("결과: 성공! 목표(Goal)에 도달했습니다.")
else:
print("결과: 실패! 구멍(Hole)에 빠졌습니다.")
test_env.close()
# ==========================================
# 4. 실행 설정 (Configuration)
# ==========================================
if __name__ == "__main__":
ENV_NAME = 'FrozenLake-v1'
# 텐서보드 로그 설정 (학습 추세 확인용)
log_dir = f"runs/{ENV_NAME}_InfoAnnotated_" + datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
writer = SummaryWriter(log_dir)
# 1. 훈련 단계 (경험으로부터 패턴 학습)
# FrozenLake는 미끄러운 바닥 때문에 많은 시행착오가 필요합니다.
train_env = gym.make(ENV_NAME, is_slippery=True)
learned_q_table = train_agent(
train_env,
num_episodes=2000,
alpha=0.1, # 학습률: 오차를 얼마나 반영할지
gamma=0.99, # 감쇠율: 미래 보상을 얼마나 중요시할지
initial_epsilon=1.0,
writer=writer
)
train_env.close()
writer.close()
# 2. 테스트 단계 (학습된 지능으로 세상 판단)
test_agent(ENV_NAME, learned_q_table)
코드의 중요 부분을 설명하면, 에이전트는 현재 상태($s$)에서 $\epsilon$-greedy 정책을 통해 수행할 행동($a$)을 결정한다. 이때 '활용(Exploitation)' 단계에서는 Q-테이블에서 가장 높은 기대 보상을 주는 행동(최대치 쌍)을 선택하여 정책으로 삼고, '탐험(Exploration)' 단계에서는 무작위 행동을 통해 새로운 전략을 탐색한다. 이렇게 결정된 행동으로 환경에서 1-Step 이동을 수행하여 즉각적인 보상($r$)과 다음 상태($s'$), 그리고 종료 여부(done)를 감지한다. 이어서 실제 경험한 데이터($(s, a, r, s')$)와 벨만 최적 방정식을 바탕으로 현재 예측치와 실제 결과의 차이인 TD 오차를 계산하며, 이를 통해 Q-테이블의 값을 실시간으로 업데이트한다.
'Reinforcement learning' 카테고리의 다른 글
| [강화학습 정복하기] 7강: DQN 성능 높이기-Dueling DQN 구조와 학습 안정화 팁 (0) | 2025.12.22 |
|---|---|
| [강화학습 정복하기] 6강: DQN의 탄생-딥러닝이 강화학습을 만났을 때 (Replay Buffer & Target Network) (0) | 2025.12.22 |
| [강화학습 정복하기] 4강: MDP와 벨만 방정식: 강화학습을 지탱하는 수학적 뼈대 (0) | 2025.12.22 |
| [강화학습 정복하기] 3강: 첫 번째 실습 CartPole: 엡실론-그리디(ε-greedy)로 균형 잡기 (0) | 2025.12.22 |
| [강화학습 정복하기] 2강: 강화학습을 위한 PyTorch 기초: 텐서부터 오토그라드까지 (0) | 2025.12.22 |