저번 강의에 이어서 진행되는 내용이다. 실제 학습이 이루어지는 강화 학습을 인공지능을 만들어보자.
지난 강의에서 우리는 Grid World를 만들었다. 그리고 거기서 제멋대로 움직이는 에이전트를 개발했다. 아래 코드는 그 에이전트가 학습을 해서 이젠 올바른 경로를 찾도록 한 것이다.
import numpy as np
class GridWorld:
def __init__(self, size=4):
self.size = size
self.terminal_states = [(0, 3)]
self.actions = ['U', 'D', 'L', 'R']
self.reset()
def reset(self):
self.state = (0, 0)
return self.state
def step(self, state, action):
x, y = state
if action == 'U':
x = max(x - 1, 0)
elif action == 'D':
x = min(x + 1, self.size - 1)
elif action == 'L':
y = max(y - 1, 0)
elif action == 'R':
y = min(y + 1, self.size - 1)
next_state = (x, y)
if next_state in self.terminal_states:
return next_state, 1.0
else:
return next_state, -0.01
env = GridWorld(size=4)
gamma = 0.9
theta = 1e-4 # 수렴 기준
# 상태 집합
states = [(x, y) for x in range(env.size) for y in range(env.size)]
# 가치 함수 초기화
V = {s: 0.0 for s in states}
policy = {s: np.random.choice(env.actions) for s in states if s not in env.terminal_states}
# 정책 반복
is_policy_stable = False
while not is_policy_stable:
# 정책 평가
while True:
delta = 0
for s in states:
if s in env.terminal_states:
continue
a = policy[s]
next_s, reward = env.step(s, a)
v = V[s]
V[s] = reward + gamma * V[next_s]
delta = max(delta, abs(v - V[s]))
if delta < theta:
break
# 정책 개선
is_policy_stable = True
for s in states:
if s in env.terminal_states:
continue
old_action = policy[s]
old_value = 0
next_s, reward = env.step(s, old_action)
old_value = reward + gamma * V[next_s]
best_action = old_action
for a in env.actions:
next_s, reward = env.step(s, a)
value = reward + gamma * V[next_s]
if value > old_value:
old_value = value
best_action = a
policy[s] = best_action
if best_action != old_action:
is_policy_stable = False
print("\n== 최적 정책 π(s) ==")
arrow = {'U':'↑', 'D':'↓', 'L':'←', 'R':'→'}
for x in range(env.size):
for y in range(env.size):
s = (x, y)
if s in env.terminal_states:
print(" G ", end=" ")
else:
print(f" {arrow[policy[s]]} ", end=" ")
print()
Grid World 정의 부분까지는 저번 코드와 동일하다. 학습을 하는 코드도 우리가 눈으로 보기 위한 출력부 제외하면 몇 줄 안된다.
하나씩 살펴보자.
우선 상태(status)는 아래와 같은 배열 형태로 초기화된다.
status = [(0, 0), (0, 1), (0, 2), (0, 3), (1, 0), (1, 1), (1, 2), (1, 3), (2, 0), (2, 1), (2, 2), (2, 3), (3, 0), (3, 1), (3, 2), (3, 3)]
여기서 상태란 4x4의 격자판에서 각 위치를 의미한다.
가치(V)는 아래와 같이 각 상태(status)별로 할당되는 실수값이다.
V= {(0, 0): 0.0, (0, 1): 0.0, (0, 2): 0.0, (0, 3): 0.0, (1, 0): 0.0, (1, 1): 0.0, (1, 2): 0.0, (1, 3): 0.0, (2, 0): 0.0, (2, 1): 0.0, (2, 2): 0.0, (2, 3): 0.0, (3, 0): 0.0, (3, 1): 0.0, (3, 2): 0.0, (3, 3): 0.0}
정책(policy)은 각 상태별로 가야 할 방향을 의미한다.
policy = {(0, 0): U, (0, 1): L, (0, 2): L, (1, 0): L, (1, 1): D, (1, 2): R, (1, 3): L, (2, 0): D, (2, 1): U, (2, 2): R, (2, 3): U, (3, 0): U, (3, 1): L, (3, 2): U, (3, 3): U}
보면 알 수 있듯이 가치와 정책은 하나의 세트이다.
정책 평가
while True:
for s in states:
a = policy[s]
next_s, reward = env.step(s, a)
v = V[s]
V[s] = reward + gamma * V[next_s]
모든 상태에 대해서 수행한다. 현재 상태별 정책을 확인하고 해당 정책(위, 아래, 오른쪽, 왼쪽)대로 한 칸 이동한다.
이곳이 "다음 상태(next_state)"이다.
그리고 그곳이 목표지점이라면 1점을 얻으면서 종료되고, 목표지점이 아니라면 0.01의 감점을 당한다. 그리고 그 다음 상태의 가치또한 0.9의 비율로 더해준다.
즉 첫 사이클은 이러하다.
(0,0)을 검사한다 -> 정책은 오른쪽이다. -> 오른쪽으로 한칸 이동한 곳이 목표 지점인가?
=> 맞다면 1점, 아니라면 -0.01 + 거기다 한칸 이동한 곳의 점수를 0.9 확률로 더해준다.
(0,1)을 검사한다 -> 정책은 아래쪽이다. -> 아래쪽으로 한칸 이동한 곳이 목표 지점인가?
=> 맞다면 1점, 아니라면 -0.01 + 거기다 한칸 이동한 곳의 점수를 0.9 확률로 더해준다.
...
(3,3)을 검사한다 -> 정책은 위쪽이다. -> 위쪽으로 한칸 이동한 곳이 목표 지점인가?
=> 맞다면 1점, 아니라면 -0.01 + 거기다 한칸 이동한 곳의 점수를 0.9 확률로 더해준다.
처음 (0,0) 부터 다시 반복한다. 무한히.
왜 이렇게 하는가?
이것은 정책 평가이다. 예를 들어 "(0,1) 상태에서 아래쪽으로 가는게 타당한가?"를 보는 것이다. 아래쪽보단 왼쪽 또는 오른쪽이 더 나을 수 도 있지 않은지를 판단해야 하기 때문이다. 그러기 위해선 현재 정책(아래쪽)에 따라 이동한 다음 지점이 가기에 마땅한 지점인지를 알아야 한다.

그런데 우리는 다음 지점이 좋은 지점인지 아닌지 모른다.
그래서 다음 지점에서도 정책에 따라 이동해 보는 것이다. 여전히 우린 그 다음 지점의 다음 지점이 우리는 좋은 지점인지 모른다.
그럼 그 다음 지점에서도 이동을 해 본다. 이 지점에선 그 다음 지점이 목표 지점이다!
이제 우린 적어도 세번째 지점에선 정책이 올바르다는 것을 알 수 있다. 그리고 세번째 지점만 유일하게 1점이라는 높은 가치를 획득했다.

그렇다면 세번째 지점으로 향하는 그 이전 지점들 좋은 선택을 하는 것으로 추정할 수 있다.

그래서 반복문이 무한히 돌아가는 것이다. 처음 모든 상태에 대해 한번 씩 평가했을 땐 내가 향하는 곳이 비록 유망한 곳이더라도, 그곳에 대한 검사가 이루어지지 않을 수 있다. 하지만 순차적으로 돌 다 보면 가장 유망한 곳(목표 지점으로 올바르게 향하는 곳)은 파악이 된다. 그리고 그 다음 평가에선 그 유망한 곳으로 향하는 상태 또한 평가가 개선될 것이다.
이것을 "가치의 전파(value propagation)"라고 한다. 내가 일전에 논문으로 소개했던 구글의 페이지랭크도 이러한 가치의 반복적인 전파를 이용한 방식이다.

그렇다면 값이 계속 변할텐데 몇번이나 돌려야 하는가?
적당히 돌려도 되고, 진짜 수렴에 가까워질 때 까지 돌려도 된다. 이를 결정하는 것이 코드에서 설정된 theta = 0.0001이다. 매번 루프 때 마다 상태 가치의 변화(delta)를 모니터링하는데, 이 가치의 변화 중 가장 큰 값이 theta보다 작으면 수렴했다고 보는 것이다.
while True:
(...)
delta = max(delta, abs(v - V[s]))
if delta < theta:
break

자 이렇게 가치가 수렴할 때 까지 한번 돌고 나면, 현재 각 상태별 정책에 대한 평가가 한번 완료되는 것이다. 이번에 정책의 수정이나 개선은 없었다. 오로지 평가만 했으므로 이게 이를 기반으로 수정을 해야 한다.
정책 수정
정책 수정은 훨씬 간단하다. 상태 가치를 보고 더 좋은 상태로 향하도록 방향만 수정하면 되는 것이다.
정책 평가를 한번 거치고 나면 각 상태별 가치가 계산 된다. 즉 어떤 상태(어떤 좌표 지점)의 가치가 높은지가 1차적으로 판단이 되는 것이다. 물론 처음에는 잘못된 정책을 토대로 상태별 가치를 업데이트하므로 부정확한 부분이 많다. 하지만 이 또한 정책까지 수정해가며 재평가를 반복하면 점점 더 정확한 방향으로 수렴해나간다.
=== 정책 평가 및 개선 1번째 ===
== 현재 상태 가치 V(s) ==
-0.1000 -0.1000 -0.1000 0.0000
-0.1000 -0.1000 -0.1000 1.0000
-0.1000 -0.1000 -0.1000 -0.1000
-0.0991 -0.1000 -0.0991 -0.0991
== 현재 정책 π(s) ==
→ → ← G
→ ← ← ↑
↑ ← ← ←
↓ ↑ → →
=== 정책 평가 및 개선 2번째 ===
== 현재 상태 가치 V(s) ==
-0.1000 -0.1000 1.0000 0.0000
-0.1000 -0.1000 0.8900 1.0000
-0.0993 -0.0994 -0.0993 0.8900
-0.0993 -0.0994 -0.0993 -0.0993
== 현재 정책 π(s) ==
↑ ← → G
↑ ← → ↑
↓ ← ↓ ↑
↓ ← → →
=== 정책 평가 및 개선 3번째 ===
== 현재 상태 가치 V(s) ==
-0.1000 0.8900 1.0000 0.0000
-0.0995 0.7910 0.8900 1.0000
-0.0995 -0.0995 0.7910 0.8900
-0.0995 -0.0995 0.7019 0.7910
== 현재 정책 π(s) ==
↑ → → G
↓ → → ↑
↓ ← ↑ ↑
↓ ← → ↑
=== 정책 평가 및 개선 4번째 ===
== 현재 상태 가치 V(s) ==
0.7910 0.8900 1.0000 0.0000
0.7019 0.7910 0.8900 1.0000
-0.0996 0.7019 0.7910 0.8900
-0.0996 0.6217 0.7019 0.7910
== 현재 정책 π(s) ==
→ → → G
→ → → ↑
↓ ↑ ↑ ↑
↓ → → ↑
=== 정책 평가 및 개선 5번째 ===
== 현재 상태 가치 V(s) ==
0.7910 0.8900 1.0000 0.0000
0.7019 0.7910 0.8900 1.0000
0.6217 0.7019 0.7910 0.8900
0.5495 0.6217 0.7019 0.7910
== 현재 정책 π(s) ==
→ → → G
→ → → ↑
↑ ↑ ↑ ↑
→ → → ↑
정챙 평가 및 개선을 반복할 때 마다 상태 가치(V)와 정책(policy)을 볼 수 있도록 로그를 띄워보면 위와 같은 과정을 확인할 수 있다. 여기선 목표 지점(Goal)이 1행 4열이다. "G"라고 표시된 곳이다.
첫번째 정책은 중구난방이다. 랜덤으로 초기화를 했으므로 당연하다. 그래도 개선이 한번 이루어진 2번째와 3번째를 보면 자리를 잡아가고 있는 모습이 보인다. 목표 인접 지점은 가치가 1.0으로 높게 평가가 되었고, 멀어지면서 점 차 작아지는 양상을 보인다. 정책에 따른 방향 또한 목표 지점과 가까운 곳은 올바른 길을 찾았는데 거리가 멀수록 업데이트가 늦는 것을 볼 수 있다.
for s in states:
old_action = policy[s]
old_value = 0
next_s, reward = env.step(s, old_action)
old_value = reward + gamma * V[next_s]
best_action = old_action
for a in env.actions:
next_s, reward = env.step(s, a)
value = reward + gamma * V[next_s]
if value > old_value:
old_value = value
best_action = a
policy[s] = best_action
소스코드와 로그를 함께 보자 각 상태에서 모든 4개 방향을 테스트한다. 그리고 가장 가치가 높은 방향으로 변경한다. 아래의 2번째 상태와 정책을 보면 붉은색 네모가 쳐진 상태에서, 오른쪽 방향이 가치가 더 높음에도 지금은 왼쪽을 향하고 있는 것을 확인할 수 있다. 위 소스코드는 이런 잘못된 방향을 수정해준다.

파란색 네모의 경우 우리 직관상 오른쪽으로 향해야 함이 분명하지만, 아직은 그 오른쪽의 값이 제대로 평가가 안 된 상태이기 때문에 그대로 유지가 된다. 붉은 색 네모 안 상태의 정책이 변경되고 나면, 그 뒤에 상태 평가가 이루어질 때 그 가치가 더 높게 업데이트 될 것이기 때문에 다음 이터레이션에선 붉은색 네모 안의 상태도 방향이 바로 잡힐 것을 기대할 수 있다.
결론
이렇게 이번 시간엔 좀 더 강화 학습의 메인스트림으로 들어와서 환경이 무엇이고, 환경 내에서 에이전트가 동작하며 상태를 평가하고 정책을 업데이트 시켜 최적의 결론을 만들어내는 과정을 보았다.
하지만 아직까지는 강화 학습에 겨우 한발 정도 들여놓은 것에 불과하다. 우리는 할 수 있는 가장 쉬운 방법으로 문제를 해결해 본 것이고, 앞으로 좀 더 효율적인 알고리즘을 통해 더 어려운 문제를 더 똑똑하게 만들어볼 것이다.
'IT 이론 공부' 카테고리의 다른 글
| LLM을 직접 만들고 개선해보자 #1 - 언어 모델의 본질 이해하기 (10) | 2025.07.17 |
|---|---|
| 간단한 코드로 이해하는 강화학습 #2 Grid World-1 (에이전트, 환경, 에피소드) (1) | 2025.07.09 |
| 강화학습을 정말 간단한 코드로 이해해보자 (Python, n-슬롯 머신) (2) | 2025.07.08 |
| [데이터 시리즈 #4] 옵트인과 옵트아웃 이해하기 (3) | 2025.03.31 |
| [데이터 시리즈 #3] 데이터 활용과 개인정보 보호 (0) | 2025.03.31 |