ML,DL

[바닥부터 배우는 강화학습] Chapter 6 MDP를 모를 때 최고의 정책 찾기

yeeunnnn 2026. 3. 10. 23:41

6.1. 몬테카를로 컨트롤

챕터 4에서 MDP를 알 때에 최적의 정책 함수를 구하는 방법론을 몇 가지 배웠다. 그 중 정책 이터레이션은 [임의의 정책에서 시작→정책 평가→밸류 계산→계산한 밸류에 대해 그리디 정책 만듦] 과정을 계속해서 반복하는 방법이었다. 모델-프리에서는 정책 이터레이션을 그대로 사용할 수 없을까?

6.1.1. 정책 이터레이션을 그대로 사용할 수 없는 이유

위와 같이 평가 단계에서는 반복적 정책 평가를, 개선 단계에서는 그리디 정책 생성을 이용했다. 이 방법을 모델 프리 상황에서 그대로 사용하기에는 문제가 있다.

1. 평가 단계에서 반복적 정책 평가를 사용할 수 없다.

반복적 정책 평가의 핵심이 되었던 벨만 기대 방정식 2단계를 생각해보자. 여기에는 MDP를 알아야만 채울 수 있는 값이 있다. 하지만 이런 정보를 모델-프리 상황에서는 알 수가 없다.

2. 정책 개선 단계에서 그리디 정책을 만들 수 없다.

어떻게든 정책 평가를 마친 상황이라고 생각해보자. 즉, 각 상태의 밸류를 알고있다고 하자. MDP의 모든 정보를 안다면 $s_0$에서의 그리디 정책은 $a_2$임을 쉽게 알 수 있다. 하지만, MDP의 상태 전이에 대한 정보를 모른다면 더 나은 액션을 선택할 수 없다.

6.1.2. 해결 방법

위와 같이 모델 프리 상황에서 정책 이터레이션을 그대로 적용할 수는 없다. 하지만 몇 가지 변형을 통해 적용할 수 있다.

1. 평가 자리에 MC

정책 평가 단계에서 반복적 정책 평가가 불가능했다. 하지만 챕터 5에서 MDP를 모를 때 밸류를 계산하는 2가지 방법론(MC, TD)을 배웠다. 그 중 하나를 골라 정책 평가 단계에 끼워 넣으면 된다. 몬테카를로 방법론을 평가 단계에서 사용하면 벨만 기대 방정식 2단계 수식들을 사용하지 않아도 각 상태의 밸류를 평가할 수 있게 된다.

2. V 대신 Q

v(s) 대신에 상태-액션 가치 함수인 q(s,a)를 이용하자. q(s,a)를 알게 되면 MDP에 대한 정보를 몰라도 그리디 액션을 선택할 수 있다.

→ 즉, MC를 이용하여 q(s,a)를 계산하고, 평가된 q(s,a)를 이용해 새로운 그리디 정책을 만들고, 이 정책에 MC를 이용하여 q(s,a)를 계산하는 과정을 반복하는 것이다.

3. greedy 대신 ε-greedy

강화학습에서 에이전트가 최적의 해를 찾으려면 에이전트는 주어진 MDP 안의 여러 상태를 충분히 탐색해야한다. 따라서 에이전트가 다양한 공간을 탐색할 수 있도록 보장해주는 장치가 필요하다. ε-greedy는 이러한 장치 중 가장 단순하면서도 충분히 강력한 방법론이다. ε이라는 작은 확률만큼 랜덤하게 액션을 선택하고, 1-ε이라는 나머지 확률은 원래처럼 그리디 액션을 선택한다.

여기서 조금 더 좋은 방법은 ε의 값을 처음에는 높게 하다가 점점 줄여주는 것이다. 처음에는 다양한 액션들을 선택하면서 환경에 대한 정보를 충분히 얻어야하므로 높게 설정하고, 학습이 어느 정도 진행되고 나면 이미 얻은 정보를 바탕으로 조금 더 최선의 선택을 내리는 데에 집중한다. 이런 방법론을 decaying ε-greedy라고 부른다. 몬테카를로 컨트롤을 구현할 때 바로 decaying ε-greedy를 이용할 것이다.

몬테카를로 컨트롤을 그림으로 정리하면 위와 같다. 정책 평가 단계에서는 MC를 이용하여 q(s,a)를 구하고, 정책 개선 단계에서는 q(s,a)에 대한 ε-greedy 정책을 만든다. 더 이상 정책이 바뀌지 않을 때까지 이를 반복하면 결국 최적의 정책을 얻게 된다.

디테일만 달라졌을 뿐 정책 이터레이션을 그대로 사용하는 방법론이다.


MDP 모델을 모른다고 가정하니까 직접 기대값을 계산할 수는 없으므로 대신 에피소드 경험을 가지고 추정한다:

  • Step 1. 에피소드 동안 $(s, a)$ 쌍이 등장할 때마다, 그 시점부터 종료까지의 실제 리턴 $G_t$를 기록한다.
  • Step 2. 여러 번의 에피소드에서 동일한 $(s, a)$ 쌍이 나올 수 있으니, 그때마다 $G_t$를 평균 낸다.
  • Step 3. 그 평균값이 곧 $Q(s,a)$의 추정치가 된다.

즉, $Q(s,a)≈{{1}\over{N(s,a)}}∑_{i=1}^{N(s,a)}G_i$ (여기서 $N(s,a)$ 쌍이 등장한 횟수, $G_i$는 그때 얻은 리턴)

$Q(s,a)←Q(s,a)+α(G_t−Q(s,a))$

  • MC에서 상태 가치 업데이트하던 수식을 그대로 가져오되, 상태 대신 (상태, 행동) 으로 확장
  • MC Control에서는 정책 개선을 하려면 “어떤 행동이 더 좋은가?”를 알아야 해서 Q(s,a)를 사용
  • $Q(s,a)$ 는 모델 기반 계산이 아니라, 경험으로 얻은 리턴을 (s,a) 쌍별로 평균해서 추정
  • 수식으로는 $Q(s,a)←Q(s,a)+α(G_t−Q(s,a))$

6.1.3. 몬테카를로 컨트롤 구현

업그레이드된 그리드 월드 문제를 풀어보자.

수렴할 때까지 n번 반복

  • 한 에피소드의 경험을 쌓고
  • 경험한 데이터로 q(s,a) 테이블의 값을 업데이트하고 (정책 평가)
  • 업데이트된 q(s,a) 테이블을 이용하여 ε-greedy 정책을 만들고 (정책 개선)
###라이브러리 ipmort
import random
import numpy as np

###GridWorld 클래스
class GridWorld():
  def __init__(self):
    self.x=0
    self.y=0

  def step(self, a):
    #0번:왼쪽, 1번:위, 2번:오른쪽, 3번:아래쪽
    if a==0:
      self.move_left()
    elif a==1:
      self.move_up()
    elif a==2:
      self.move_right()
    elif a==3:
      self.move_down()

    reward = -1
    done = self.is_done()
    return (self.x, self.y), reward, done

  def move_left(self):
    if self.y  == 0:
      pass
    elif self.y==3 and self.x in [0,1,2]:
      pass
    elif self.y==5 and self.x in [2,3,4]:
      pass
    else:
      self.y -= 1

  def move_right(self):
    if self.y==1 and self.x in [0,1,2]:
      pass
    elif self.y==3 and self.x in [2,3,4]:
      pass
    elif self.y==6:
      pass
    else:
      self.y += 1

  def move_up(self):
    if self.x==0:
      pass
    elif self.x==3 and self.y==2:
      pass
    else:
      self.x -= 1

  def move_down(self):
    if self.x==4:
      pass
    elif self.x==1 and self.y==4:
      pass
    else:
      self.x += 1

  def is_done(self):
    if self.x == 4 and self.y == 6:
      return True
    else :
      return False

  def get_state(self):
    return (self.x, self.y)

  def reset(self):
    self.x=0
    self.y=0
    return (self.x,self.y)

###QAgent 클래스
class QAgent():
  def __init__(self):
    self.q_table = np.zeros((5,7,4))
    self.eps = 0.9
    self.alpha=0.01

  def select_action(self,s):
    #eps-greedy로 액션을 선택해준다.
    x,y = s
    coin = random.random()
    if coin < self.eps:
      action = random.randint(0,3)
    else:
      action_val = self.q_table[x,y,:]
      action = np.argmax(action_val)
    return action
  
  def update_table(self, history):
    #한 에피소드에 해당하는 history를 입력으로 받아 q 테이블의 값을 업데이트한다.
    cum_reward = 0
    for transition in history[::-1]:
      s,a,r,s_prime = transition
      x,y = s
      #몬테카를로 방식을 이용하여 업데이트
      self.q_table[x,y,a] = self.q_table[x,y,a] + self.alpha * (cum_reward - self.q_table[x,y,a])
      cum_reward = cum_reward + r

  def anneal_eps(self):
    self.eps -= 0.03
    self.eps = max(self.eps, 0.1)

  def show_table(self):
    #학습이 각 위치에서 어느 액션의 q 값이 가장 높았는지 보여주는 함수
    q_lst = self.q_table.tolist()
    data = np.zeros((5,7))
    for row_idx in range(len(q_lst)):
      row = q_lst[row_idx]
      for col_idx in range(len(row)):
        col = row[col_idx]
        action = np.argmax(col)
        data[row_idx, col_idx] = action
    print(data)

###메인 함수
def main():
  env = GridWorld()
  agent = QAgent()
  
  for n_epi in range(1000): #총 1000번의 에피소드 동안 학습
    done = False
    history = []

    s = env.reset()
    while not done: #한 에피소드가 끝날 때 까지
      a = agent.select_action(s)
      s_prime, r, done = env.step(a)
      history.append((s,a,r,s_prime))
      s = s_prime
      #한 번의 step이 진행되자마자 바로 테이블의 데이터를 업데이트 해줌
    agent.update_table(history) #히스토리를 이용하여 에이전트를 업데이트
    agent.anneal_eps()
  
  agent.show_table()

main()

출력은 다음과 같이 나오게 된다.

출력값은 ε-greedy라는 확률적 요소를 갖기 때문에 매번 학습할 때마다 달라진다. 위 코드에서 사용된 하이퍼파라미터(입실론의 초깃값, alpha 등)들은 튜닝된 값이 아니므로 최적화한다면 더 안정적으로 학습이 되도록 만들 수 있다.


6.2. TD 컨트롤1 - SARSA

정책 이터레이션의 평가 단계에서 MC대신 TD를 해도 가능하다!

6.2.1. MC 대신 TD

위와 같이 정책 평가 단계에서 MC 대신 TD를 이용해보자. 즉 TD를 이용해 q(s,a)를 구해보자.

TD를 이용해 Q를 구하는 접근법은 위의 그림에 의해 SARSA라고 부른다.

TD를 이용한 V와 Q 학습은 아래의 식과 같다.

벨만 기대 방정식 0단계를 복습해보면 다음과 같다. 이를 통해서 TD 타깃이 모두 벨만 방정식을 통해 나온 것임을 알 수 있다.

6.2.2. SARSA 구현

###QAgent 클래스
class QAgent():
  def __init__(self):
    self.q_table = np.zeros((5,7,4))
    self.eps = 0.9

  def select_action(self,s):
    x,y = s
    coin = random.random()
    if coin < self.eps:
      action = random.randint(0,3)
    else:
      action_val = self.q_table[x,y,:]
      action = np.argmax(action_val)
    return action
  
  def update_table(self, transition):
    s,a,r,s_prime = transition
    x,y = s
    next_x, next_y = s_prime
    a_prime = self.select_action(s_prime) #s'에서 선택할 액션(실제로 취한 액션이 아님)
    #SARSA 업데이트 식을 이용
    self.q_table[x,y,a] = self.q_table[x,y,a] + 0.1 * (r + self.q_table[next_x,next_y,a_prime] - self.q_table[x,y,a])

  def anneal_eps(self):
    self.eps -= 0.03
    self.eps = max(self.eps, 0.1)

  def show_table(self):
    q_lst = self.q_table.tolist()
    data = np.zeros((5,7))
    for row_idx in range(len(q_lst)):
      row = q_lst[row_idx]
      for col_idx in range(len(row)):
        col = row[col_idx]
        action = np.argmax(col)
        data[row_idx, col_idx] = action
    print(data)

###메인 함수
def main():
  env = GridWorld()
  agent = QAgent()
  
  for n_epi in range(1000):
    done = False

    s = env.reset()
    while not done: #한 에피소드가 끝날 때 까지
      a = agent.select_action(s)
      s_prime, r, done = env.step(a)
      agent.update_table((s,a,r,s_prime))
      s = s_prime
    agent.anneal_eps()
  
  agent.show_table()

main()

출력은 다음과 같이 나오게 된다.


6.3. TD 컨트롤2 - Q러닝

위에서는 TD를 이용하여 최적의 정책을 찾는 방법을 살펴 보았다. 마찬가지로 TD를 이용하여 최적의 정책을 찾는 방법을 알아보자.

그 전에 SARSA와 Q러닝 모두 TD를 이용한 컨트롤 방법인데 그 차이가 무엇인지 알아보자.

6.3.1. Off-Policy와 On-Policy

  • 타깃 정책 (target policy) : 강화하고자 하는 목표가 되는 정책
  • 행동 정책 (behavior policy) : 실제로 환경과 상호 작용하며 경험을 쌓고 있는 정책
  • On-policy : 타깃 정책과 행동 정책이 같은 경우 (직접 경험)
  • Off-policy : 타깃 정책과 행동 정책이 다른 경우 (간접 경험)

따라서 지금까지 배운 내용은 모두 On-Policy 상황이었다.

off-policy가 지도학습과 비슷하게 느껴질 수 있겠지만 헷갈리면 안된다. 지도학습에서는 지도자를 그대로 따라하는 방식으로 학습했다면, off-policy는 결과가 좋았던 것은 그대로 따라하기도 하지만, 결과가 좋지 않았던 것은 수정하기도 한다.

6.3.2. Off-Policy 학습의 장점

  1. 과거의 경험을 재사용할 수 있다. : 타깃 정책과 행동 정책이 달라도 되기 때문에 과거의 정책이 경험한 샘플들을 그 다음 정책의 업데이트에 그대로 재사용할 수 있는 건 물론이고 더 다음에 있는 정책에서도 재사용할 수 있다. 따라서 효율성 측면에서 커다란 이득을 누릴 수 있다.
  2. 사람의 데이터로부터 학습할 수 있다. : off-policy 방법론의 행동 정책에는 어떤 정책을 가져다 놔도 된다. 따라서 기존 전문가가 만들어 내는 양질의 데이터를 학습에 사용하여 학습 속도를 끌어올릴 수 있다.
  3. 일대다, 다대일 학습이 가능하다. : 동시에 여러 개의 정책을 학습시킨다고 가정하자. off-policy 학습을 이용하면 이 중에서 단 1개의 정책만 경험을 쌓게 두고, 그로부터 생성된 데이터를 이용해 동시에 여러 개의 정책을 학습시킬 수 있다. 반대로 동시에 여러 개의 정책이 겪은 데이터를 모아서 단 1개의 정책을 업데이트할 수도 있다.

6.3.3. Q러닝의 이론적 배경 - 벨만 최적 방정식

가장 좋은 정책을 따를 때의 가치는 $q_(s,a)=\max\limits_{\pi}q_\pi(s,a)$이다. $q_$를 알면 $\pi_*=\argmax\limits_{a}q_(s,a)$와 같이 주어진 MDP에서 순간마다 최적의 행동을 선택하며 움직일 수 있다.

우리의 목적은 최적의 액션 가치-함수 $q_*$를 찾는 것!

벨만 최적 방정식 2단계의 식을 떠올려보자 : $q_∗(s,a)=r^a_s+γ\Sigma_{s'∈S}P^a_{ss'}\max\limits_{a'}q_∗(s',a')$ 하지만, MDP를 모르기 때문에 위 수식을 그대로 이용할 수 없으므로 0단계 식을 이용하자. 즉, 여러 개의 샘플을 이용해 계산하고, 여러 번 경험을 쌓아 각 경험을 평균내어 기댓값을 구한다(대수의 법칙). 벨만 최적 방정식 0단계: $q_∗(s,a)=E_{s'}[r +\max\limits_{a'}q_∗(s',a')]$ 위 과정을 적용하여 TD 학습을 해보자. Q러닝과 SARSA의 밸류 업데이트 식은 아래와 같다. (빨간 글씨가 TD 타깃)

Q 러닝과 SARSA의 차이점을 떠올리기 위하여 아래 표를 보자.

행동정책 : 에이전트가 실제로 탐색을 할 때(액션을 할 때) 따르는 정책

  • Q러닝, SARSA : 실제 액션을 취할 땐 ϵ-Greedy 정책을 따름

타깃 정책 : Q값을 업데이트 하기 위해 사용하는 정책. 즉, 타깃에 사용하는 정책

  • Q 러닝 : 가장 Q값이 높은 액션을 선택하는 Greedy 정책을 따름
  • SARSA : 행동 정책과 타깃 정책 모두 ϵ값이 들어가 있는 ϵ-greedy 정책을 따름

여기서, SARSA와 Q러닝이 다른 이유는 기초가 되는 수식이 다르기 때문이다.

6.3.4. Q러닝 구현

###QAgent 클래스
class QAgent():
  def __init__(self):
    self.q_table = np.zeros((5,7,4))
    self.eps = 0.9

  def select_action(self,s):
    x,y = s
    coin = random.random()
    if coin < self.eps:
      action = random.randint(0,3)
    else:
      action_val = self.q_table[x,y,:]
      action = np.argmax(action_val)
    return action
  
  def update_table(self, transition):
    s,a,r,s_prime = transition
    x,y = s
    next_x, next_y = s_prime
    self.q_table[x,y,a] = self.q_table[x,y,a] + 0.1 * (r + np.amax(self.q_table[next_x,next_y,:]) - self.q_table[x,y,a])

  def anneal_eps(self):
    self.eps -= 0.01
    self.eps = max(self.eps, 0.2)

  def show_table(self):
    q_lst = self.q_table.tolist()
    data = np.zeros((5,7))
    for row_idx in range(len(q_lst)):
      row = q_lst[row_idx]
      for col_idx in range(len(row)):
        col = row[col_idx]
        action = np.argmax(col)
        data[row_idx, col_idx] = action
    print(data)

위 코드의 출력은 아래와 같다.


정리

MDP를 모르는 상황에서 최적의 정책을 찾기 위해 정책 이터레이션을 적용할 수 있을까??

MC control → 반복적 정책 평가의 부분을 MC로 변경, V 대신 Q를 적용

TD control → SARSA(on-policy), Q-Learning(Off-policy)