본문 바로가기

랜덤 벽 GridWorld에서 TD Learning으로 상태가치 함수 배우기

@eunyoung-study2026. 3. 13. 15:02

1. 문제 설정 – 랜덤 벽이 있는 4×4 GridWorld

이번에는 랜덤하게 벽이 생기는 GridWorld에서 TD Learning으로 상태가치 함수 (V(s))를 학습해 보려고 합니다.

환경 조건

  • 격자 크기: 4×4
  • 시작 위치: (0, 0)
  • 목표 위치: (3, 3)
  • 행동:
    • 0: 왼쪽
    • 1: 위
    • 2: 오른쪽
    • 3: 아래
  • 보상:
    • 일반 이동: −1
    • 목표 도착: 에피소드 종료 (보상은 0으로 두어도 되고, 필요에 따라 조정 가능)

랜덤 벽 규칙

  • 매 에피소드 시작마다 벽을 랜덤 생성합니다.
    • 벽 개수: 2개 또는 3개
    • 시작 위치 (0,0)에는 벽이 생기면 안 됨
    • 목표 위치 (3,3)에는 벽이 생기면 안 됨
    • 같은 위치에 벽이 중복 생성되면 안 됨
  • 이동 규칙:
    • 이동하려는 위치가 빈 칸이면 이동
    • 격자를 벗어나면 원래 위치 유지
    • 이동하려는 칸에 벽이 있으면 제자리 유지

즉, 에이전트 입장에서는 매 에피소드마다 지도가 조금씩 바뀌는 GridWorld를 만나는 셈이고,
우리는 이런 환경에서 랜덤 정책으로 움직이면서 TD Learning으로 (V(s))를 추정합니다.


2. 기본 4×4 GridWorld와 비교하기

상대적인 차이를 보기 위해 두 가지 환경을 준비했습니다.

  1. 기본 GridWorld – 벽이 전혀 없는 4×4
  2. 랜덤 벽 GridWorld – 위에서 설명한 랜덤 벽 규칙 적용

두 환경 모두:

  • 시작: (0,0)
  • 목표: (3,3)
  • 정책은 완전 랜덤 (각 방향 25% 확률)
  • TD(0) 업데이트:
    \[
    V(s) \leftarrow V(s) + \alpha \bigl( r + \gamma V(s') - V(s) \bigr)
    \]

을 사용합니다.


3. 파이썬 코드 – 환경과 TD Learning 구현

3.1 기본 4×4 GridWorld

N = 4
START = (0, 0)
GOAL = (3, 3)
ACTIONS = [0, 1, 2, 3]  # 0: left, 1: up, 2: right, 3: down

class BasicGridWorld:
    def init(self):
    self.reset()

    def reset(self):
        self.agent_pos = START
        return self.agent_pos

    def step(self, action):
        x, y = self.agent_pos

        if action == 0:      # left
            y = max(0, y - 1)
        elif action == 1:    # up
            x = max(0, x - 1)
        elif action == 2:    # right
            y = min(N - 1, y + 1)
        elif action == 3:    # down
            x = min(N - 1, x + 1)

        self.agent_pos = (x, y)

        if self.agent_pos == GOAL:
            return self.agent_pos, 0.0, True
        else:
            return self.agent_pos, -1.0, False

3.2 랜덤 벽 GridWorld

class RandomWallGridWorld:
    def __init__(self):
        self.walls = set()
        self.reset()

    def _generate_walls(self):
        self.walls.clear()
        num_walls = random.choice([2, 3])
        candidates = [(i, j) for i in range(N) for j in range(N)
                      if (i, j) not in (START, GOAL)]
        random.shuffle(candidates)
        for pos in candidates:
            if len(self.walls) >= num_walls:
                break
            self.walls.add(pos)

    def reset(self):
        self._generate_walls()
        self.agent_pos = START
        return self.agent_pos

    def step(self, action):
        x, y = self.agent_pos
        nx, ny = x, y

        if action == 0:
            ny = max(0, y - 1)
        elif action == 1:
            nx = max(0, x - 1)
        elif action == 2:
            ny = min(N - 1, y + 1)
        elif action == 3:
            nx = min(N - 1, x + 1)

        if (nx, ny) in self.walls:
            nx, ny = x, y

        self.agent_pos = (nx, ny)

        if self.agent_pos == GOAL:
            return self.agent_pos, 0.0, True
        else:
            return self.agent_pos, -1.0, False

3.3 TD(0) 학습 루프 (랜덤 정책)

from collections import defaultdict

GAMMA = 0.9
ALPHA = 0.1
EPISODES = 1000

def td_learn(env, episodes=EPISODES, alpha=ALPHA, gamma=GAMMA,
             max_steps=100, name=""):
    V = defaultdict(float)
    for ep in range(episodes):
        s = env.reset()
        done = False

        for t in range(max_steps):
            if done:
                break
            a = random.choice(ACTIONS)
            s_prime, r, done = env.step(a)

            old_v = V[s]
            target = r + (gamma * V[s_prime] if not done else r)
            V[s] = old_v + alpha * (target - old_v)

            s = s_prime

        if (ep + 1) % 100 == 0 and name:
            print(f"[{name}] episode {ep+1}/{episodes}")
    return V

def print_value_table(V, title):
    print(title)
    for i in range(N):
        row = []
        for j in range(N):
            v = V[(i, j)]
            row.append(f"{v:6.2f}")
        print(" ".join(row))
    print()
    
if __name__ == "__main__":
    random.seed(2026)

    # 기본 GridWorld 학습
    basic_env = BasicGridWorld()
    V_basic = td_learn(basic_env, name="basic")
    print_value_table(V_basic, "[기본 4x4 GridWorld 상태가치 V(s)]")

    # 랜덤 벽 GridWorld 학습
    wall_env = RandomWallGridWorld()
    V_wall  = td_learn(wall_env,  name="wall")
    print_value_table(V_wall, "[랜덤 벽 GridWorld 상태가치 V(s)]")

4. TD Learning 결과 – 일반 그리드 vs 랜덤 벽 그리드

4.1 기본 4×4 GridWorld

코드를 돌려보면 다음과 같은 상태가치 테이블이 나왔습니다.

[기본 4x4 GridWorld 상태가치 V(s)]
 -9.33  -9.12  -8.79  -8.46
 -9.20  -8.91  -8.56  -7.35
 -8.81  -8.38  -7.10  -4.28
 -8.62  -7.64  -4.59   0.00
  • (3,3)은 목표 상태이므로 V(s) = 0
  • 위쪽/왼쪽으로 갈수록 값이 점점 더 큰 음수
    → 랜덤 정책으로 움직였을 때, 목표까지 평균적으로 더 많은 -1 보상을 받는 위치
  • 대체로 “목표까지의 거리(스텝 수)”가 그대로 V(s)에 반영된 모습입니다.

4.2 랜덤 벽 GridWorld

[랜덤 벽 GridWorld 상태가치 V(s)]
 -9.51  -9.41  -9.09  -8.81
 -9.39  -9.21  -8.59  -7.40
 -9.19  -9.01  -7.34  -4.66
 -9.07  -8.55  -5.23   0.00

여기서는 값들이 전반적으로 기본 환경보다 더 나쁘게(더 큰 음수) 나옵니다.

예를 들어:

  • 시작점 (0,0)
    • 기본: -9.33
    • 벽: -9.51
  • (2,2)
    • 기본: -7.10
    • 벽: -7.34

같은 위치라도 랜덤 벽 환경에서 V(s)가 더 작게(손해 보는 방향으로) 추정되고 있습니다.

4.3 해석

  • 기본 4×4 GridWorld에서는
    “목표까지의 거리”만 고려하면 되기 때문에,
    목표에 가까워질수록 V(s)가 천천히 0에 가까워지는 전형적인 패턴이 나옵니다.
  • 랜덤 벽 GridWorld에서는 매 에피소드마다 벽이 바뀌면서
    • 어떤 에피소드에서는 길이 열려 있고
    • 어떤 에피소드에서는 막혀 있어서 빙빙 돌다가 끝나는 경우가 생깁니다.
그 결과:
같은 위치라도,
“자주 막히는 길 / 돌아가야 하는 길” 근처의 상태는
기본 그리드보다 V(s)가 더 나쁜 상태로 평가됩니다.

 

즉, TD Learning은 단순한 랜덤 정책만으로도
“이 위치는 자주 길이 막히는 위험한 상태인지, 아니면 비교적 곧장 목표로 갈 수 있는 안전한 상태인지”
상태가치 함수 V(s)에 자연스럽게 반영해 줍니다.

더보기

[ 오늘의 정리 ]

  • 일반 GridWorld와 랜덤 벽 GridWorld에서 랜덤 정책으로 움직이면서 TD(0)로 상태가치 (V(s))를 학습해 보았습니다.
  • 벽이 없는 환경에서는 목표까지의 거리(스텝 수) 가 가치에 거의 직접적으로 반영되는 반면,
    랜덤 벽 환경에서는 “막힐 가능성”까지 함께 반영되면서 값이 바뀝니다.
  • 즉, TD Learning은 단순한 예제에서도 “길이 자주 막히는 불리한 위치 vs 상대적으로 안전한 위치”를
    상태가치 함수 (V(s))로 자연스럽게 구분해 준다는 점을 확인할 수 있습니다.
eunyoung-study
@eunyoung-study :: 은영의 이해 노트

개념을 이해하고, 논문을 풀어보고, 코드로 확인하는 기록 ! 오늘도 파이팅 😉

공감하셨다면 ❤️ 구독도 환영합니다! 🤗

목차