경험 재생
더 많은 작업
- Experience replay; 경험 재생
- 에이전트가 겪은 전이를 버퍼에 저장해 두었다가 무작위로 다시 뽑아 학습에 쓰는 강화 학습 기법
경험 재생(experience replay)은 강화 학습 에이전트가 환경과 상호작용하며 얻은 전이 를 리플레이 버퍼(replay buffer, 재생 버퍼, 리플레이 메모리)에 쌓아 두고, 학습할 때 거기서 미니배치를 뽑아 쓰는 기법이다. 롱지 린(Long-Ji Lin)이 1992년 연구했고,[1] 2013~2015년 DQN이 목표 신경망과 함께 핵심 장치로 써서 널리 알려졌다.[2] 지금은 DQN 계열과 DDPG·TD3·SAC 같은 오프 폴리시 액터-크리틱의 기본 구성 요소다.
에이전트가 연속해서 겪는 전이는 서로 강하게 상관되어 있다. 이를 순서대로 바로 학습하면 확률적 경사 하강법이 가정하는 독립 동일 분포(i.i.d.) 표본과 거리가 멀어지고, 신경망은 최근에 본 상황에 과적합하고 예전 경험을 잊는다. 경험 재생은 다음 효과를 낸다.[3]
- 상관 제거: 버퍼에서 무작위로 뽑으면 연속된 갱신이 서로 다른 시점의 전이를 쓰므로 갱신의 분산이 줄어든다.
- 데이터 효율: 전이 하나를 여러 번 학습에 쓴다. 환경 상호작용이 비쌀수록 이득이 크다.
- 드문 경험 보존: 가끔만 일어나는 중요한 사건(보상을 받은 순간 등)을 바로 잊지 않는다.
- 분포 평활화: 학습 분포가 현재 정책 하나가 아니라 최근 여러 정책의 행동 분포의 평균이 되어, 정책이 바뀔 때 데이터 분포가 급변하는 피드백 고리를 완화한다.
서턴·바토는 DQN의 경험 재생이 연속 갱신 사이의 상관을 없애 분산을 줄이고, 현재 가중치에 대한 데이터 의존을 끊어 불안정 요인 하나를 없앴다고 설명한다.[3] DQN 논문의 제거 실험에서도 경험 재생과 목표 신경망은 각각 성능을 크게 높였고 함께 쓸 때 효과가 가장 컸다.[3]
가장 단순한 형태는 고정 크기 배열에 순환하며 쓰는 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] - 메모리: 아타리처럼 이미지 관측이면 와 를 따로 저장하면 같은 프레임이 두 번 들어간다. 다음 관측을 다음 칸에서 꺼내 쓰는 방식으로 절반을 아낄 수 있다(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]
전이 의 우선순위를 라 할 때 추출 확률은
이다. 이면 균등 추출, 클수록 우선순위를 강하게 반영한다. 우선순위를 정하는 방식은 두 가지다.
- 비례 방식: . 작은 양수 은 오차가 0인 전이도 가끔 뽑히게 한다.
- 순위 방식: . TD 오차 크기 순위만 쓰므로 이상값에 덜 민감하다.
새 전이는 아직 TD 오차를 모르므로 지금까지의 최대 우선순위로 넣어 적어도 한 번은 뽑히게 한다. 뽑힌 전이는 학습 후 새로 계산한 TD 오차로 우선순위를 갱신한다.
우선순위로 뽑으면 학습 분포가 원래 분포와 달라져 기댓값이 편향된다. 이를 중요도 샘플링 가중치로 보정한다.
이면 편향을 완전히 보정한다. 학습 초반에는 편향이 덜 중요하므로 를 에서 시작해 학습이 끝날 때 1이 되도록 선형으로 올린다. 논문은 8개 게임 격자 탐색으로 비례 방식 , 순위 방식 를 골랐다.[6]
버퍼가 수십만~수백만 개일 때 매번 전체 합을 다시 구하면 느리다. 논문은 비례 방식에 합 트리(sum tree)를 썼다. 잎에 각 전이의 우선순위를 두고 내부 노드에 자식들의 합을 저장하는 이진 트리로, 이진 힙의 배열 표현과 비슷하다.[6] 우선순위 갱신과 추출 모두 이다. 추출은 에서 난수 를 뽑고 루트에서부터 왼쪽 자식 합과 비교하며 내려간다. 배치 크기만큼 구간을 나눠 각 구간에서 하나씩 뽑는 층화 추출을 쓴다.
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.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
- ↑ Volodymyr Mnih 외, 〈Human-level control through deep reinforcement learning〉, Nature 518, 529~533, 2015. doi:10.1038/nature14236
- ↑ 3.0 3.1 3.2 Richard S. Sutton, Andrew G. Barto, 《Reinforcement Learning: An Introduction》 2판, MIT Press, 2018, 16.5절(DQN). 저자 공개본
- ↑ Stable-Baselines3 문서, RL Algorithms
- ↑ William Fedus 외, 〈Revisiting Fundamentals of Experience Replay〉, ICML 2020. arXiv:2007.06700
- ↑ 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.0 7.1 Marcin Andrychowicz 외, 〈Hindsight Experience Replay〉, NIPS 2017. arXiv:1707.01495
- ↑ Dan Horgan 외, 〈Distributed Prioritized Experience Replay〉, ICLR 2018. arXiv:1803.00933
- ↑ Emma L. Roscow 외, 〈Learning offline: memory replay in biological and artificial reinforcement learning〉, Trends in Neurosciences, 2021. arXiv:2109.10034