검색
← 모듈 로드맵

직접 해 보기: 그리드월드 Q-러닝 (절벽 걷기로 Q-러닝 vs SARSA)

강의 · 입문자 · KAITABASE 정리

원제 · Hands-on: Tabular Q-learning vs SARSA on Cliff Walking (Sutton · Barto 예제 6.6 · Gymnasium 참고)

NumPy로 절벽 걷기 격자를 만들어 Q-러닝과 SARSA를 직접 돌려 비교하는 실습

먼저 알아야 할 것

Q-러닝 · SARSA 갱신식ε-탐욕파이썬 · NumPy 기초할인된 반환

다루는 내용

4×12 절벽 걷기 격자 환경과 표 형식 Q-러닝 · SARSA를 NumPy 60줄 남짓으로 직접 구현하고 실제 실행 결과를 읽습니다. Q-러닝은 최단 경로(−13)를, SARSA는 안전한 먼 길(−17)을 배우지만 학습 중 점수는 SARSA가 더 좋은 이유를 확인합니다. ε 감쇠 · 걸음 크기 변경 확장 과제와 탐욕 정책이 맴도는 이상 사례까지 다룹니다.

해설

직접 해 보기: 그리드월드 Q-러닝 (절벽 걷기로 Q-러닝 vs SARSA)

앞 수업에서 손으로 계산한 Q-러닝 · SARSA 갱신식을 파이썬 60줄 남짓으로 직접 돌려 봅니다. 무대는 Sutton · Barto의 유명한 절벽 걷기(Cliff Walking) 격자입니다. 같은 경험을 하고도 Q-러닝은 절벽 바로 옆 최단 경로를, SARSA는 멀찍이 돌아가는 안전한 경로를 배우는 모습을 출력으로 확인합니다. 필요한 것은 NumPy 하나뿐입니다.

항목내용
참고 출처Sutton · Barto 『Reinforcement Learning: An Introduction』 2판 예제 6.6(절벽 걷기), Gymnasium 문서 Cliff Walking
트랙 · 수준강화학습 · 기초 (실습)
이 수업이 다루는 범위격자 환경 직접 만들기, ε-탐욕, 표 형식 Q-러닝 · SARSA 구현, 학습 곡선 숫자 읽기, 탐욕 정책 화살표 그리기, 씨앗(seed)별 비교, 확장 과제
준비물파이썬 3 · NumPy (이 페이지의 출력은 Python 3.14 · NumPy 2.5에서 실제로 돌린 결과)

들어가기 전에

한눈에 보기

  • 4×12 격자 환경을 함수 하나(step)로 직접 만듭니다.
  • Q-러닝과 SARSA가 목표값 한 줄만 다르다는 것을 코드로 확인합니다.
  • 학습한 Q-표에서 화살표 지도(탐욕 정책)를 그려, 두 알고리즘이 고른 길을 비교합니다.
  • "학습 중 받은 점수"와 "다 배운 뒤 탐욕적으로 걸은 점수"가 왜 다른지 설명할 수 있게 됩니다.
  • 확장 과제로 ε 줄이기 · 걸음 크기 바꾸기를 해 보고 결과를 해석합니다.

1. 문제: 절벽 걷기

직관(해설용 비유). 낭떠러지 옆 오솔길을 지나 반대편 집에 가야 합니다. 낭떠러지 바로 옆으로 붙어 가면 가장 빠르지만, 가끔 발을 헛디디는(무작위 행동) 사람이라면 멀찍이 돌아가는 편이 낫습니다. 강화학습 에이전트도 "가끔 헛디디는" ε-탐욕으로 움직이므로 같은 고민을 합니다.

규칙(S&B 예제 6.6 · Gymnasium 문서 기준).

항목값
격자4행 × 12열
출발 S · 목표 G맨 아래 줄 왼쪽 끝 (3, 0) · 오른쪽 끝 (3, 11)
절벽 C맨 아래 줄 (3, 1)~(3, 10)
보상한 걸음마다 −1, 절벽에 들어가면 −100 그리고 출발점으로 돌아감
행동위 · 오른쪽 · 아래 · 왼쪽 (벽에 부딪히면 제자리)
할인없음(γ=1\gamma=1), 목표에 닿으면 한 판 끝
코드
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
S C C C C C C C C C C G

최단 경로는 위로 한 칸 → 오른쪽 11칸 → 아래로 한 칸, 모두 13걸음이라 반환이 −13-13 입니다. 맨 위 줄로 돌아가면 3 + 11 + 3 = 17걸음, 반환 −17-17 입니다.

2. 두 갱신식 다시 보기

Q-러닝:Q(s,a)←Q(s,a)+α[r+γmax⁡a′Q(s′,a′)−Q(s,a)]\text{Q-러닝:}\quad Q(s,a) \leftarrow Q(s,a) + \alpha\big[r + \gamma \max_{a'} Q(s',a') - Q(s,a)\big]
SARSA:Q(s,a)←Q(s,a)+α[r+γ Q(s′,a′)−Q(s,a)],a′∼ε-탐욕\text{SARSA:}\quad Q(s,a) \leftarrow Q(s,a) + \alpha\big[r + \gamma\, Q(s',a') - Q(s,a)\big],\quad a' \sim \varepsilon\text{-탐욕}
기호뜻이번 실습 값
α\alpha걸음 크기0.5
γ\gamma할인율1
ε\varepsilon무작위 행동 확률0.1
max⁡a′Q(s′,a′)\max_{a'}Q(s',a')다음 상태에서 가장 좋은 행동의 값(실제로 그 행동을 할지는 상관없음)—
Q(s′,a′)Q(s',a')다음 상태에서 실제로 고른 행동의 값(무작위 실수까지 포함)—

왜 결과가 갈리나. 절벽 옆 칸에서 ε-탐욕은 10% 확률로 무작위 행동을 하고, 그중 1/4은 '아래'라서 절벽으로 떨어집니다. SARSA는 이 실수까지 포함한 값을 배우므로 절벽 옆 칸의 가치가 낮아지고, 멀리 돌아가는 길을 택합니다(온폴리시). Q-러닝은 "다음부터는 최선만 한다"고 가정한 값을 배우므로 절벽 옆 최단 경로의 가치를 정확히 배우지만, 실제로 걸을 때는 ε 때문에 가끔 떨어집니다(오프폴리시).

3. 코드 (전체)

아래 코드를 cliff.py 로 저장해 python cliff.py 로 실행합니다. 각 부분에 번호를 붙였습니다.

python
import sys; sys.stdout.reconfigure(encoding="utf-8")   # 윈도 콘솔에서 한글 · 화살표 출력용
import numpy as np

# 1) 환경: 4x12 절벽 걷기 (Sutton·Barto 예제 6.6, Gymnasium CliffWalking 과 같은 배치)
ROWS, COLS = 4, 12
START, GOAL = (3, 0), (3, 11)
ACTIONS = [(-1, 0), (0, 1), (1, 0), (0, -1)]   # 0=위 1=오른쪽 2=아래 3=왼쪽
ARROWS = "↑→↓←"

def step(state, a):
    r, c = state
    dr, dc = ACTIONS[a]
    r = min(max(r + dr, 0), ROWS - 1)
    c = min(max(c + dc, 0), COLS - 1)
    if r == 3 and 1 <= c <= 10:          # 절벽: -100, 출발점으로
        return START, -100, False
    if (r, c) == GOAL:
        return (r, c), -1, True
    return (r, c), -1, False

# 2) ε-탐욕 행동 선택 (동점은 무작위)
def eps_greedy(Q, s, eps, rng):
    if rng.random() < eps:
        return int(rng.integers(4))
    q = Q[s]
    return int(rng.choice(np.flatnonzero(q == q.max())))

# 3) 학습: Q-러닝과 SARSA 는 목표값 한 줄만 다르다
def train(method, episodes=500, alpha=0.5, eps=0.1, gamma=1.0, seed=0):
    rng = np.random.default_rng(seed)
    Q = np.zeros((ROWS, COLS, 4))
    returns = []
    for _ in range(episodes):
        s, G, done = START, 0, False
        a = eps_greedy(Q, s, eps, rng)
        while not done:
            s2, r, done = step(s, a)
            G += r
            a2 = eps_greedy(Q, s2, eps, rng)
            if done:
                target = r
            elif method == "q":
                target = r + gamma * Q[s2].max()        # 다음 상태의 최댓값
            else:
                target = r + gamma * Q[s2][a2]          # 실제로 고를 다음 행동
            Q[s][a] += alpha * (target - Q[s][a])
            s, a = s2, a2
        returns.append(G)
    return Q, np.array(returns)

# 4) 결과 보기: 탐욕 정책 화살표와 탐욕 경로의 반환
def show_policy(Q):
    lines = []
    for r in range(ROWS):
        row = ""
        for c in range(COLS):
            if (r, c) == START: row += "S"
            elif (r, c) == GOAL: row += "G"
            elif r == 3 and 1 <= c <= 10: row += "C"
            else: row += ARROWS[int(Q[r, c].argmax())]
        lines.append(row)
    return "\n".join(lines)

def greedy_return(Q, max_steps=100):
    s, G = START, 0
    for _ in range(max_steps):
        s, r, done = step(s, int(Q[s].argmax()))
        G += r
        if done: return G
    return None   # 목표에 못 감

for name, m in [("Q-러닝", "q"), ("SARSA", "sarsa")]:
    Q, R = train(m)
    print(f"== {name} ==")
    print("마지막 100판 평균 반환(ε=0.1 탐험 포함):", round(R[-100:].mean(), 1))
    print("탐욕 정책으로 한 판 걸었을 때 반환:", greedy_return(Q))
    print(show_policy(Q))

# 5) 씨앗 10개 평균으로 다시 비교
for name, m in [("Q-러닝", "q"), ("SARSA", "sarsa")]:
    avg = [train(m, seed=k)[1][-100:].mean() for k in range(10)]
    gr = [greedy_return(train(m, seed=k)[0]) for k in range(10)]
    print(name, "씨앗 10개: 마지막 100판 평균", round(float(np.mean(avg)), 1), "| 탐욕 경로 반환", gr)

코드를 단계별로 읽기.

  1. 환경(step): 위치와 행동을 받아 (다음 위치, 보상, 끝났는지)를 돌려줍니다. 격자 밖으로 나가려 하면 min/max 로 제자리에 묶습니다. Gymnasium의 CliffWalking-v1 을 써도 되지만, 환경을 직접 만들어 보면 "환경 = 상태 전이 + 보상 함수"라는 MDP 정의가 손에 잡힙니다.
  2. ε-탐욕: 동점일 때 항상 첫 번째 행동을 고르면 초반에 한쪽으로 쏠리므로 무작위로 깹니다.
  3. 학습: 판을 시작할 때 행동을 하나 고르고, 걸을 때마다 다음 행동 a2 를 미리 골라 둡니다. SARSA는 그 a2 를 목표값에 쓰고, Q-러닝은 a2 와 상관없이 최댓값을 씁니다. 다른 곳은 한 글자도 다르지 않습니다.
  4. 결과 보기: 칸마다 Q값이 가장 큰 행동을 화살표로 찍고, 탐욕적으로(ε=0) 한 판 걸어 반환을 잽니다.

4. 실행 결과 (이 페이지를 위해 실제로 돌린 출력, seed=0)

코드
== Q-러닝 ==
마지막 100판 평균 반환(ε=0.1 탐험 포함): -61.8
탐욕 정책으로 한 판 걸었을 때 반환: -13
↓→→→→→→→↓↓↓↓
→↓↓→↓→→↓→↓↓↓
→→→→→→→→→→→↓
SCCCCCCCCCCG
== SARSA ==
마지막 100판 평균 반환(ε=0.1 탐험 포함): -26.2
탐욕 정책으로 한 판 걸었을 때 반환: -17
→→→→→→→→→→↓↓
↑↑↑→↑↑→→↑↑→↓
↑↑↑→↑←←↑↑→→↓
SCCCCCCCCCCG
Q-러닝 씨앗 10개: 마지막 100판 평균 -49.5 | 탐욕 경로 반환 [-13, -13, -13, -13, -13, -13, -13, -13, -13, -13]
SARSA 씨앗 10개: 마지막 100판 평균 -26.2 | 탐욕 경로 반환 [-17, -17, -19, -17, -17, None, None, -19, -17, -17]

난수 생성기 · 버전이 다르면 숫자는 조금 달라질 수 있습니다. 하지만 아래 경향은 S&B 예제 6.6이 설명하는 것과 같습니다.

출력 읽기.

  • 화살표 지도. Q-러닝은 3행(절벽 바로 위 줄)을 따라 오른쪽으로 가는 최단 경로(반환 −13)를 배웠습니다. SARSA는 출발점에서 위로 올라가 맨 위 줄을 따라가는 먼 길(반환 −17)을 배웠습니다.
  • 학습 중 점수는 거꾸로입니다. ε=0.1로 실제로 걸은 마지막 100판의 평균은 SARSA(−26.2)가 Q-러닝(−61.8, 씨앗 10개 평균 −49.5)보다 훨씬 좋습니다. Q-러닝은 절벽 옆을 걷다가 무작위 행동으로 종종 떨어져 −100을 받기 때문입니다. 책도 Q-러닝이 최적 정책의 값을 배우지만 온라인 성능은 SARSA보다 나쁘다고 설명합니다.
  • Q-러닝의 출발점 Q값. 학습 뒤 Q[START] 를 찍어 보면 (위, 오른쪽, 아래, 왼쪽) = (−13, −113, −14, −14)였습니다. '위'가 −13으로 최적 반환과 정확히 같고, '오른쪽'은 절벽(−100) + 출발점에서 다시 최단 경로(−13) = −113 입니다. 같은 실행에서 SARSA의 '위'는 −20 정도로, ε 실수를 포함한 값이라 더 낮습니다.
mermaid
flowchart LR
  A[출발점 S에서<br/>ε-탐욕으로 a 선택] --> B[step: s', r 받기]
  B --> C[다음 행동 a' 미리 선택]
  C --> D{알고리즘}
  D -- Q-러닝 --> E[목표 = r + max Q s']
  D -- SARSA --> F[목표 = r + Q s', a']
  E --> G[Q s,a ← Q + α 목표 − Q]
  F --> G
  G --> H{목표 G 도착?}
  H -- 아니오 --> B
  H -- 예 --> I[반환 기록 · 다음 판]

5. 이상한 결과 하나: 탐욕 경로가 None

SARSA 씨앗 10개 중 두 번은 탐욕 경로 반환이 None(100걸음 안에 도착 못 함)이었습니다. seed=5 를 들여다보면 맨 위 줄 (0, 4)에서 탐욕 행동이 '위'(벽에 부딪혀 제자리)로 나와 같은 칸을 맴돌았습니다.

왜 이런가. 맨 위 줄에서 '위'는 ε 무작위로만 가끔 시도되므로 갱신 횟수가 아주 적습니다. 그래서 그 Q값이 실제보다 덜 깎인 채(낙관적으로) 남아 있고, 탐욕적으로 고르면 그 행동이 1등이 됩니다. 학습 중에는 ε 덕분에 빠져나오지만, ε=0으로 걸으면 갇힙니다. 표 형식 학습에서는 자주 가 보지 않은 상태-행동 쌍의 값은 믿을 수 없다는 교훈입니다(탐험 수업의 '낙관적 초깃값'이 거꾸로 작동한 경우라고 볼 수도 있습니다 — 초깃값 0이 보상 −1보다 크기 때문입니다).

6. 확장 과제

아래 결과도 같은 코드를 조금 고쳐 실제로 돌린 값입니다(씨앗 0~9).

과제 1. ε 을 점점 줄이면? eps = 0.1 * (1 - ep / episodes) 로 판마다 ε 을 0까지 줄여 보세요.

  • 결과: Q-러닝 탐욕 경로는 10개 모두 −13, 마지막 100판 평균은 −14.7로 크게 좋아졌습니다(탐험이 줄어 덜 떨어짐). SARSA는 탐욕 경로가 대부분 −17(한 번 −19), 마지막 100판 평균 −18.1이었습니다.
  • 해석: 책은 "ε 을 점점 줄이면 두 방법 모두 결국 최적 정책으로 수렴한다"고 말합니다. 500판 안에 0까지 선형으로 줄이는 이번 설정에서는 SARSA가 아직 안전한 길에 머물렀습니다. ε 이 작아진 뒤에도 그동안 배운 '절벽 옆은 위험하다'는 값이 충분히 고쳐지려면 더 많은 판이 필요합니다.

과제 2. 걸음 크기 α\alpha 를 0.1로 줄이면?

  • 결과: Q-러닝 탐욕 경로는 모두 −13, 마지막 100판 평균 −49.6. SARSA 탐욕 경로는 대부분 −15(두 번 −17), 평균 −21.5.
  • 해석: α\alpha 가 작으면 값이 천천히 변해 SARSA의 Q가 아직 덜 수렴했고, 절벽에서 두 줄 떨어진 중간 줄(반환 −15)을 고르는 경우가 많았습니다. "어느 길을 고르나"가 ε 뿐 아니라 학습이 얼마나 진행됐는지에도 달려 있음을 보여 줍니다.

과제 3(생각해 보기). Gymnasium의 CliffWalking-v1 으로 바꾸려면? env.reset() 이 상태 번호(행 × 12 + 열)를 주므로 Q-표를 (48, 4) 로 만들고, env.step(a) 의 반환값 (s2, r, terminated, truncated, info) 를 쓰면 됩니다. 나머지 학습 코드는 그대로입니다.

연습 문제

  1. 최단 경로와 맨 위 줄 경로의 반환은 각각 얼마이고, 왜 그런가요?
    • 풀이: 최단 경로는 위 1 + 오른쪽 11 + 아래 1 = 13걸음이라 −13, 맨 위 줄 경로는 위 3 + 오른쪽 11 + 아래 3 = 17걸음이라 −17 입니다.
  2. 학습이 끝난 Q-러닝에서 Q[START] 의 '오른쪽' 값이 −113인 이유는?
    • 풀이: 오른쪽은 바로 절벽이라 −100을 받고 출발점으로 돌아가며, Q-러닝은 그 뒤 최선(−13)을 가정하므로 −100 + (−13) = −113 입니다.
  3. 코드에서 Q-러닝을 SARSA로 바꾸려면 몇 줄을 고치면 되나요?
    • 풀이: 목표값 한 줄(Q[s2].max() → Q[s2][a2])입니다. 다음 행동을 미리 고르는 구조는 이미 두 방법이 함께 씁니다.
  4. ε=0(처음부터 탐욕)으로 학습하면 어떤 일이 생길까요?
    • 풀이: 초깃값 0이 보상 −1보다 크므로 아직 안 해 본 행동이 늘 더 좋아 보여 저절로 탐험합니다(낙관적 초깃값 효과). 실제로 eps=0.0 으로 씨앗 0~4를 돌려 보니 두 방법 모두 탐욕 경로 반환 −13(최단 경로)을 찾았습니다. 또 ε=0이면 '실제로 고를 다음 행동'이 곧 최댓값 행동이라 SARSA와 Q-러닝의 목표값이 사실상 같아집니다(동점 처리만 다름). 다만 이런 탐험은 초깃값과 보상 크기에 기댄 것이라, 보상이 양수인 문제에서는 통하지 않으므로 일반적으로는 ε 이나 다른 탐험 장치를 둡니다.

스스로 점검

  1. 같은 ε=0.1 환경에서 학습 중 평균 반환은 SARSA가 더 좋은데, 다 배운 탐욕 경로는 Q-러닝이 더 짧은 이유는?
    • 답: SARSA는 ε 실수까지 포함한 '실제로 걷는 방식'의 가치를 배워 안전한 길을 택하고, Q-러닝은 '최선만 한다'고 가정한 가치를 배워 최단 경로를 택하지만 학습 중에는 ε 때문에 절벽에 떨어지기 때문입니다.
  2. 탐욕 경로가 맴돌며 목표에 못 가는 경우는 왜 생기나요?
    • 답: 거의 시도되지 않은 상태-행동 쌍의 Q값이 초깃값 근처에 남아 실제보다 좋아 보이기 때문입니다.
  3. 실무에서 "학습 중 안전"이 중요하면 어느 쪽 성질이 유리한가요?
    • 답: 학습 중 실제 행동 방식의 위험을 값에 반영하는 온폴리시(SARSA) 쪽입니다. 다만 학습 뒤 최적성은 오프폴리시(Q-러닝)가 유리할 수 있습니다.

용어 정리

용어쉬운 뜻
절벽 걷기(Cliff Walking)S&B 예제 6.6의 4×12 격자 문제
표 형식(tabular) 방법상태-행동마다 칸을 하나씩 두고 값을 저장하는 방식
온폴리시(on-policy)지금 행동하는 정책 자신의 가치를 배움 (SARSA)
오프폴리시(off-policy)행동하는 정책과 다른(최적) 정책의 가치를 배움 (Q-러닝)
탐욕 정책(greedy policy)각 상태에서 Q값이 가장 큰 행동만 고르는 정책
씨앗(seed)난수 생성기의 시작값. 같으면 같은 결과가 재현됨
반환(return)한 판 동안 받은 보상의 합
ε 감쇠(ε decay)학습이 진행될수록 무작위 행동 확률을 줄이는 것

더 깊이 보려면

  • S&B 6.6절 기대 SARSA(Expected SARSA): 다음 행동 값의 '기댓값'을 쓰는 변형. 이 코드에서 목표값 한 줄을 ∑a′π(a′∣s′)Q(s′,a′)\sum_{a'}\pi(a'\mid s')Q(s',a') 로 바꿔 비교해 보세요.
  • 딥 강화학습으로 아타리 게임하기 (DQN) — 오늘의 Q-표를 신경망으로 바꾸면 무엇이 달라지는지.
  • 다음 수업 함수 근사: 표에서 신경망으로 — 칸이 너무 많아 표를 만들 수 없을 때의 방법.

다음에 볼 것

라이선스

본문과 코드는 KAITABASE가 이 수업을 위해 직접 작성했습니다(코드는 자유롭게 고쳐 써도 됩니다). 환경 설정은 Sutton · Barto 2판(PDF 판권면 CC BY-NC-ND 2.0) 예제 6.6과 Gymnasium 문서의 설명을 따랐고, 책의 그림 · 수치는 옮기지 않았습니다. 실행 결과는 이 페이지를 위해 실제로 돌린 출력입니다.

출처

AI 원리부터 이해하기 · 강화학습 모듈

강화학습 모듈 학습

이 글은 강화학습 모듈의 학습 자료입니다. 전체 31개 자료 중 4번째입니다.

AI 기초 과정은 현재 진도 저장을 지원하지 않습니다.

출처·원문

원문 보기

요약만

본문 · 코드 자체 작성 · 참고 자료 Sutton · Barto 2판(CC BY-NC-ND 2.0) · Gymnasium 문서