본문으로 이동
메뉴 여닫기
환경 설정 메뉴 여닫기
개인 메뉴 여닫기
로그인하지 않음
지금 편집한다면 당신의 IP 주소가 공개될 수 있습니다.
Experience replay; 경험 재생
에이전트가 겪은 전이를 버퍼에 저장해 두었다가 무작위로 다시 뽑아 학습에 쓰는 강화 학습 기법

경험 재생(experience replay)은 강화 학습 에이전트가 환경과 상호작용하며 얻은 전이 (s,a,r,s′)를 리플레이 버퍼(replay buffer, 재생 버퍼, 리플레이 메모리)에 쌓아 두고, 학습할 때 거기서 미니배치를 뽑아 쓰는 기법이다. 롱지 린(Long-Ji Lin)이 1992년 연구했고,[1] 2013~2015년 DQN이 목표 신경망과 함께 핵심 장치로 써서 널리 알려졌다.[2] 지금은 DQN 계열과 DDPG·TD3·SAC 같은 오프 폴리시 액터-크리틱의 기본 구성 요소다.

에이전트가 연속해서 겪는 전이는 서로 강하게 상관되어 있다. 이를 순서대로 바로 학습하면 확률적 경사 하강법이 가정하는 독립 동일 분포(i.i.d.) 표본과 거리가 멀어지고, 신경망은 최근에 본 상황에 과적합하고 예전 경험을 잊는다. 경험 재생은 다음 효과를 낸다.[3]

  • 상관 제거: 버퍼에서 무작위로 뽑으면 연속된 갱신이 서로 다른 시점의 전이를 쓰므로 갱신의 분산이 줄어든다.
  • 데이터 효율: 전이 하나를 여러 번 학습에 쓴다. 환경 상호작용이 비쌀수록 이득이 크다.
  • 드문 경험 보존: 가끔만 일어나는 중요한 사건(보상을 받은 순간 등)을 바로 잊지 않는다.
  • 분포 평활화: 학습 분포가 현재 정책 하나가 아니라 최근 여러 정책의 행동 분포의 평균이 되어, 정책이 바뀔 때 데이터 분포가 급변하는 피드백 고리를 완화한다.

서턴·바토는 DQN의 경험 재생이 연속 갱신 사이의 상관을 없애 분산을 줄이고, 현재 가중치에 대한 데이터 의존을 끊어 불안정 요인 하나를 없앴다고 설명한다.[3] DQN 논문의 제거 실험에서도 경험 재생과 목표 신경망은 각각 성능을 크게 높였고 함께 쓸 때 효과가 가장 컸다.[3]

오프 폴리시 조건

편집 원본 편집

버퍼에 든 전이는 과거 정책들이 만든 것이므로 경험 재생은 오프 폴리시 알고리즘에만 그대로 쓸 수 있다. Q-러닝의 타깃 r+γmaxa′Q(s′,a′)는 다음 행동을 어떤 정책이 골랐는지와 무관하므로 문제없다. 반면 REINFORCE, A2C, PPO 같은 온 폴리시 정책 경사 방법은 현재 정책의 표본이 필요해, 버퍼 대신 매번 새로 모은 롤아웃 버퍼(rollout buffer)를 쓰고 학습 후 비운다. Stable-Baselines3도 오프 폴리시용 ReplayBuffer와 온 폴리시용 RolloutBuffer를 구분한다.

기본 구현: 원형 버퍼

편집 원본 편집

가장 단순한 형태는 고정 크기 배열에 순환하며 쓰는 FIFO 버퍼와 균등 무작위 추출이다. 가득 차면 가장 오래된 전이를 덮어쓴다.

import numpy as np

class ReplayBuffer:
    """고정 크기 원형 버퍼. 가득 차면 가장 오래된 전이를 덮어쓴다."""
    def __init__(self, capacity, obs_dim, seed=0):
        self.capacity, self.idx, self.size = capacity, 0, 0
        self.obs = np.zeros((capacity, obs_dim), np.float32)
        self.next_obs = np.zeros((capacity, obs_dim), np.float32)
        self.act = np.zeros(capacity, np.int64)
        self.rew = np.zeros(capacity, np.float32)
        self.done = np.zeros(capacity, np.float32)   # terminated만 저장(시간 제한 truncated는 0)
        self.rng = np.random.default_rng(seed)

    def add(self, o, a, r, o2, terminated):
        i = self.idx
        self.obs[i], self.act[i], self.rew[i], self.next_obs[i], self.done[i] = o, a, r, o2, terminated
        self.idx = (i + 1) % self.capacity
        self.size = min(self.size + 1, self.capacity)

    def sample(self, batch_size):
        j = self.rng.integers(0, self.size, size=batch_size)   # 균등 무작위 추출
        return self.obs[j], self.act[j], self.rew[j], self.next_obs[j], self.done[j]

구현할 때 주의할 점은 다음과 같다.

  • 종료와 시간 제한 구분: Gymnasium은 진짜 종료(terminated)와 시간 제한으로 잘린 것(truncated)을 따로 돌려준다. 잘린 경우에도 다음 상태 가치로 부트스트래핑해야 하므로 버퍼의 done에는 terminated만 넣는다. SB3는 handle_timeout_termination=True(기본값)로 이를 처리하며, 2.1.0 이전에는 처리 방식이 달랐다.[4]
  • 메모리: 아타리처럼 이미지 관측이면 s와 s′를 따로 저장하면 같은 프레임이 두 번 들어간다. 다음 관측을 다음 칸에서 꺼내 쓰는 방식으로 절반을 아낄 수 있다(SB3의 optimize_memory_usage). 픽셀은 uint8로 저장한다.
  • 학습 시작 시점: 버퍼에 어느 정도 쌓이기 전에는 학습하지 않는다(SB3의 learning_starts, 기본 100 단계).

버퍼 크기와 재생 비율

편집 원본 편집

버퍼가 너무 작으면 상관 제거 효과가 약하고, 너무 크면 오래된 정책의 데이터가 많아 학습이 느려진다. 페더스 등(ICML 2020)은 버퍼 용량과 재생 비율(replay ratio, 모은 경험 한 단위당 학습 갱신 횟수)을 나눠 실험해, 용량을 키우면 효과가 큰 알고리즘과 거의 없는 알고리즘이 갈리며, 특히 보정하지 않은 n단계 이득을 함께 쓸 때 큰 버퍼의 이득이 크다고 보고했다.[5] SB3의 오프 폴리시 알고리즘 기본 버퍼 크기는 100만 전이다.

우선순위 경험 재생

편집 원본 편집

균등 추출은 이미 잘 맞히는 전이와 아직 크게 틀리는 전이를 똑같이 뽑는다. 우선순위 경험 재생(prioritized experience replay, PER)은 TD 오차가 큰, 즉 배울 것이 많은 전이를 더 자주 뽑는다. 샤울 등이 2015년 발표했고(ICLR 2016), DQN에 적용해 아타리 49개 게임 중 41개에서 균등 재생보다 좋은 성능을 냈다.[6]

전이 i의 우선순위를 pi라 할 때 추출 확률은

P(i)=piα∑kpkα

이다. α=0이면 균등 추출, 클수록 우선순위를 강하게 반영한다. 우선순위를 정하는 방식은 두 가지다.

  • 비례 방식: pi=|δi|+ϵ. 작은 양수 ϵ은 오차가 0인 전이도 가끔 뽑히게 한다.
  • 순위 방식: pi=1/rank(i). TD 오차 크기 순위만 쓰므로 이상값에 덜 민감하다.

새 전이는 아직 TD 오차를 모르므로 지금까지의 최대 우선순위로 넣어 적어도 한 번은 뽑히게 한다. 뽑힌 전이는 학습 후 새로 계산한 TD 오차로 우선순위를 갱신한다.

중요도 샘플링 보정

편집 원본 편집

우선순위로 뽑으면 학습 분포가 원래 분포와 달라져 기댓값이 편향된다. 이를 중요도 샘플링 가중치로 보정한다.

wi=(1N⋅1P(i))β,손실=1B∑iwimaxjwjδi2

β=1이면 편향을 완전히 보정한다. 학습 초반에는 편향이 덜 중요하므로 β를 β0에서 시작해 학습이 끝날 때 1이 되도록 선형으로 올린다. 논문은 8개 게임 격자 탐색으로 비례 방식 α=0.6,β0=0.4, 순위 방식 α=0.7,β0=0.5를 골랐다.[6]

합 트리 구현

편집 원본 편집

버퍼가 수십만~수백만 개일 때 매번 전체 합을 다시 구하면 느리다. 논문은 비례 방식에 합 트리(sum tree)를 썼다. 잎에 각 전이의 우선순위를 두고 내부 노드에 자식들의 합을 저장하는 이진 트리로, 이진 힙의 배열 표현과 비슷하다.[6] 우선순위 갱신과 추출 모두 O(log⁡N)이다. 추출은 [0,총합)에서 난수 u를 뽑고 루트에서부터 왼쪽 자식 합과 비교하며 내려간다. 배치 크기만큼 구간을 나눠 각 구간에서 하나씩 뽑는 층화 추출을 쓴다.

class SumTree:
    """잎에 우선순위를 두고 부모가 자식 합을 갖는 이진 트리. capacity는 2의 거듭제곱."""
    def __init__(self, capacity):
        self.capacity = capacity
        self.tree = np.zeros(2 * capacity)       # tree[1]이 전체 합, 잎은 tree[capacity:]

    def update(self, i, priority):
        k = i + self.capacity
        self.tree[k] = priority
        k //= 2
        while k >= 1:
            self.tree[k] = self.tree[2 * k] + self.tree[2 * k + 1]
            k //= 2

    def find(self, x):
        """누적합이 x를 넘는 첫 잎의 인덱스"""
        k = 1
        while k < self.capacity:
            if x <= self.tree[2 * k]:
                k = 2 * k
            else:
                x -= self.tree[2 * k]
                k = 2 * k + 1
        return k - self.capacity

    @property
    def total(self):
        return self.tree[1]

class PrioritizedReplay(ReplayBuffer):
    def __init__(self, capacity, obs_dim, alpha=0.6, eps=1e-6, seed=0):
        super().__init__(capacity, obs_dim, seed)
        self.tree, self.alpha, self.eps, self.max_p = SumTree(capacity), alpha, eps, 1.0

    def add(self, *transition):
        self.tree.update(self.idx, self.max_p)        # 새 전이는 최대 우선순위로
        super().add(*transition)

    def sample(self, batch_size, beta=0.4):
        seg = self.tree.total / batch_size            # 층화 추출
        u = (np.arange(batch_size) + self.rng.random(batch_size)) * seg
        j = np.array([min(self.tree.find(x), self.size - 1) for x in u])
        p = self.tree.tree[j + self.capacity] / self.tree.total
        w = (self.size * p) ** (-beta)
        w /= w.max()                                  # 중요도 가중치 정규화
        return (self.obs[j], self.act[j], self.rew[j], self.next_obs[j], self.done[j]), j, w

    def update_priorities(self, j, td_errors):
        pr = (np.abs(td_errors) + self.eps) ** self.alpha
        for i, q in zip(j, pr):
            self.tree.update(i, q)
        self.max_p = max(self.max_p, pr.max())

학습 루프에서는 sample()로 받은 가중치 w를 각 표본의 TD 손실에 곱하고, 계산한 TD 오차로 update_priorities()를 부른다. 위 코드는 로컬에서 실행해 우선순위가 높은 전이가 그 비율대로 더 자주 뽑히는 것을 확인했다. Stable-Baselines3 2.9 본체에는 우선순위 버퍼 클래스가 없고 ReplayBuffer, NStepReplayBuffer, HerReplayBuffer 등만 있다.

PER의 한계도 있다. TD 오차가 큰 전이는 잡음이 많은 전이일 수도 있어 확률적 환경에서는 오히려 해가 될 수 있고, 우선순위가 갱신될 때만 바뀌므로 오래된 우선순위가 남는다. 트리 연산 때문에 균등 추출보다 느리다.

기법 연도 바꾸는 것 쓰임
경험 재생 1992 과거 전이 저장·재사용[1] DQN, 오프 폴리시 전반
우선순위 경험 재생 2015 TD 오차 비례 추출 + 중요도 보정[6] Rainbow, Ape-X
n단계 재생 2017~ 전이 대신 n단계 누적 보상과 n단계 뒤 상태 저장 Rainbow, SB3 NStepReplayBuffer
사후 경험 재생(HER) 2017 실패한 궤적의 목표를 실제로 도달한 상태로 바꿔 다시 저장 보상이 드문 목표 조건 로봇 과제[7]
분산 우선순위 재생(Ape-X) 2018 많은 액터가 공유 버퍼에 쓰고 학습기 하나가 우선순위로 추출 대규모 분산 학습[8]
시퀀스 재생 2018~ 전이 대신 일정 길이 시퀀스 저장 순환 신경망 정책

사후 경험 재생(hindsight experience replay)은 "목표 위치로 물체를 옮겨라" 같은 목표 조건 과제에서 쓴다. 목표에 실패한 궤적이라도 "실제로 도착한 곳이 원래 목표였다"고 바꿔 저장하면 성공 사례가 되어 보상 0/1뿐인 환경에서도 학습 신호가 생긴다. 어떤 오프 폴리시 알고리즘과도 결합할 수 있고, 논문은 밀기, 미끄러뜨리기, 집어 옮기기 로봇 팔 과제에서 시뮬레이션으로 학습한 정책을 실제 로봇에 옮겼다.[7]

  • 오프라인 강화 학습: 환경과 더 이상 상호작용하지 않고 고정된 버퍼(데이터셋)만으로 학습하는 극단적 경우다. 버퍼에 없는 행동의 가치를 과대평가하는 문제가 새로 생긴다.
  • 연속 학습의 재생: 딥러닝의 연속 학습(continual learning)에서도 이전 과제의 표본을 저장해 섞어 학습하는 재생 기법으로 파국적 망각을 줄인다.
  • 뇌의 재생: 쥐의 해마에서 수면·휴식 중 낮에 겪은 경로의 신경 활동 순서가 다시 나타나는 현상이 관찰되어, 경험 재생과 비교하는 연구가 있다.[9]
  1. ↑ 1.0 1.1 Long-Ji Lin, 〈Self-improving reactive agents based on reinforcement learning, planning and teaching〉, Machine Learning 8, 293~321, 1992. doi:10.1007/BF00992699
  2. ↑ Volodymyr Mnih 외, 〈Human-level control through deep reinforcement learning〉, Nature 518, 529~533, 2015. doi:10.1038/nature14236
  3. ↑ 3.0 3.1 3.2 Richard S. Sutton, Andrew G. Barto, 《Reinforcement Learning: An Introduction》 2판, MIT Press, 2018, 16.5절(DQN). 저자 공개본
  4. ↑ Stable-Baselines3 문서, RL Algorithms
  5. ↑ William Fedus 외, 〈Revisiting Fundamentals of Experience Replay〉, ICML 2020. arXiv:2007.06700
  6. ↑ 6.0 6.1 6.2 6.3 Tom Schaul, John Quan, Ioannis Antonoglou, David Silver, 〈Prioritized Experience Replay〉, ICLR 2016. arXiv:1511.05952
  7. ↑ 7.0 7.1 Marcin Andrychowicz 외, 〈Hindsight Experience Replay〉, NIPS 2017. arXiv:1707.01495
  8. ↑ Dan Horgan 외, 〈Distributed Prioritized Experience Replay〉, ICLR 2018. arXiv:1803.00933
  9. ↑ Emma L. Roscow 외, 〈Learning offline: memory replay in biological and artificial reinforcement learning〉, Trends in Neurosciences, 2021. arXiv:2109.10034