경험 리플레이
경험 리플레이
DQN 학습의 두 가지 문제
DQN을 학습할 때는 두 가지 문제가 있습니다.
- 목표가 계속 바뀐다: 목표값을 계산하는 데 학습 중인 신경망을 쓰므로, 학습할수록 목표도 함께 움직여 학습이 불안정해집니다.
- IID 가정이 깨진다: 딥러닝은 학습 데이터가 서로 독립이고 같은 분포에서 나왔다고(IID) 가정합니다. 그런데 행위자가 경험한 순서대로 학습하면, 바로 앞 경험과 뒤 경험이 밀접하게 연관되어 있어 이 가정이 성립하지 않습니다.
두 번째 문제를 해결하는 방법이 **경험 리플레이(experience replay)**입니다.
경험 리플레이
행위자가 롤아웃(rollout)을 하면서 얻은 경험을 곧바로 학습에 쓰지 않고 **리플레이 버퍼(replay buffer)**에 쌓아 둡니다. 버퍼에는 경험이 들어온 순서대로 저장되지만, 학습할 때는 그중 일부를 무작위로 뽑아서 씁니다.
경험을 순서대로 학습하면 서로 비슷하고 엮여 있는 경험만 연달아 학습하게 됩니다. 예를 들어 학습해야 할 함수의 한쪽 구간 데이터만 집중적으로 들어오면, 신경망은 그 구간에만 맞춰지고 나머지 구간은 망가집니다. 버퍼에서 무작위로 뽑으면 여러 구간의 경험이 고르게 섞여 들어오므로 IID 가정에 가까워지고 학습이 안정됩니다.
버퍼 크기
Stable-Baselines3의 DQN에는 경험 리플레이가 이미 적용되어 있으며, 버퍼 크기의 기본값은 100만입니다. 버퍼가 가득 차면 가장 오래된 경험부터 밀어냅니다. 정책이 많이 바뀐 뒤에는 아주 오래전 경험이 현재 정책을 학습하는 데 별로 도움이 되지 않기 때문입니다.
버퍼 크기는 buffer_size 옵션으로 조절합니다.
- 늘리면: 더 오래된 경험까지 참고합니다.
- 줄이면: 최근 경험만 참고합니다.
어느 값이 가장 좋은지는 정해져 있지 않으므로, 학습이 잘 되지 않으면 값을 바꿔 가며 시험해 봅니다.
PER
경험 리플레이를 개선하려는 아이디어 중 하나가 **우선순위 경험 리플레이(Prioritized Experience Replay, PER)**입니다. 버퍼에서 무작위로 뽑으면 학습에 별 도움이 되지 않는 경험도 똑같은 확률로 뽑힙니다. 경험마다 가치가 다르니, 가치가 높은 경험에 우선순위를 주어 더 자주 뽑자는 것입니다.
경험의 중요도는 TD 오차로 평가합니다. TD 오차는 목표값(실제로 받은 보상 + 다음 상태의 가치)과 현재 기대값의 차이입니다.
- TD 오차가 크다: 기대와 결과가 많이 다릅니다(기대 이상이든 이하든). 배울 것이 많은 경험입니다.
- TD 오차가 작다: 예상대로입니다. 새로 배울 것이 적습니다.
익숙한 프랜차이즈 식당에만 가면 늘 예상한 맛이 나오므로 새로운 경험이 쌓이지 않는 것과 비슷합니다.
다만 TD 오차가 큰 순서대로만 뽑으면 안 되고, TD 오차가 큰 경험은 뽑힐 확률을 조금 높이고 작은 경험은 조금 낮추는 식으로 샘플링합니다. 이렇게 확률을 조정하면 학습 분포가 치우치므로 **가중 중요도 샘플링(weighted importance sampling)**으로 보정해야 하고, 보정 정도도 학습 초반과 후반에 다르게 해야 하는 등 고려할 요소가 많습니다.
Stable-Baselines3에는 PER이 구현되어 있지 않습니다. 필요하면 직접 구현할 수 있지만 굳이 그럴 필요는 크지 않습니다.
보상이 드문 문제
PER보다 더 어려운 문제는 보상이 매우 드문(sparse reward) 경우입니다.
비트 플리핑 환경
비트 플리핑(bit flipping)은 이 문제를 극단적으로 만든 환경입니다.
- 동전 n개가 있고, 한 번에 동전 하나를 뒤집을 수 있습니다.
- 모든 동전이 목표 상태와 같아지면 +1, 아니면 매 단계 −1의 보상을 받습니다.
예를 들어 동전 3개가 모두 뒷면으로 시작하고 목표가 모두 앞면이라면, 세 개를 다 뒤집어야 비로소 +1을 받습니다. 하나나 두 개만 뒤집은 상태에서는 여전히 −1입니다. 게다가 행위자는 목표가 무엇인지 모르므로 마구잡이로 뒤집으면서 찾아야 합니다.
가능한 조합은 2n가지이므로 동전 3개면 8가지, 10개면 1,024가지, 20개면 약 100만 가지입니다. 동전이 20개라면 100만 번쯤 시도해야 한 번 보상을 받는 셈입니다. 강화학습은 기대에서 벗어나는 결과를 받아야 학습이 되는데, −1만 계속 받다가 아주 드물게 +1을 받으면 학습이 거의 일어나지 않습니다.
실제 강화학습 문제 중에도 이런 성격의 문제가 많습니다. 정답에 가까워질수록 보상이 서서히 커지면 그 방향으로 학습하면 되지만, 정확히 맞혔을 때만 보상이 있으면 그 전까지는 학습이 일어나지 않습니다. 예를 들어 모든 부품이 정확히 제자리에 있어야만 작동하고 하나라도 어긋나면 실패하는 설계 문제는 비트 플리핑과 같은 구조입니다.
보상 함수 재설계
한 가지 해결책은 목표에 가까워질수록 보상을 조금씩 주도록 보상 함수를 인위적으로 고치는 것입니다. 하지만 이렇게 하려면 무엇이 목표이고 현재 얼마나 가까워졌는지 계산할 수 있을 만큼 문제에 대한 배경지식이 있어야 하므로, 모든 문제에 쓸 수 있는 방법은 아닙니다.
Hindsight Experience Replay
**HER(Hindsight Experience Replay)**는 보상 함수를 다시 설계하지 않고 이 문제를 푸는 방법입니다. hindsight는 '뒤늦게 돌아본다'는 뜻으로, 우리말로는 '회고적 경험 리플레이' 정도로 옮길 수 있습니다.
아이디어
로봇이 부품을 A 지점으로 보내야 하는데 아직 학습이 덜 되어 B 지점으로 보냈다고 합시다. 원래대로라면 실패이므로 보상은 0(또는 −1)입니다. HER는 이 경험을 이렇게 다시 해석합니다.
"사실 나는 원래 B로 보내려고 했어."
그러면 이 경험은 성공이 되고, 보상을 다시 계산해서 학습합니다. 적어도 B로 보내는 방법은 배울 수 있습니다. 원래 의도한 결과는 아니지만, 실제로 일어난 결과를 목표였던 것처럼 바꿔서 학습하는 것이 HER의 아이디어입니다.
비트 플리핑에 적용하면 다음과 같습니다. 모두 뒷면인 상태에서 동전 하나를 뒤집어 −1을 받았을 때, "나는 원래 이 상태를 만들려고 했어"라고 목표를 바꾸면 +1이 됩니다. 이렇게 하면 특정 동전을 뒤집는 방법을 하나씩 배울 수 있습니다.
보상 재계산 함수
HER를 적용하려면 목표가 바뀌었을 때 보상을 다시 계산하는 함수가 있어야 합니다. CartPole 같은 환경에는 이런 함수가 없으므로 직접 구현해야 합니다.
구현 방식은 여러 가지인데, 보통 환경이 다음 두 가지를 알려 줍니다.
desired_goal: 진짜 목표achieved_goal: 실제로 달성한 상태
두 값의 차이에 노름(norm)을 적용해 거리를 구하고, 거리가 0이 아니면 −1, 0이면 0을 보상으로 주는 식입니다. 비트 플리핑 환경에는 이 함수가 구현되어 있습니다.
실습
비트 플리핑 환경
동전 5개를 최대 5번 뒤집어서 목표 상태에 도달해야 하는 환경을 만듭니다. 조합이 25=32가지라 보상을 받을 기회가 드뭅니다.
from stable_baselines3.common.envs import BitFlippingEnv
N_BITS = 5
env = BitFlippingEnv(n_bits=N_BITS, continuous=False, max_steps=N_BITS)
env.reset()
env.step(env.action_space.sample())
(OrderedDict([('observation', array([0, 0, 1, 1, 0], dtype=int8)),
('achieved_goal', array([0, 0, 1, 1, 0], dtype=int8)),
('desired_goal', array([1, 1, 1, 1, 1], dtype=int8))]),
-1.0,
False,
False,
{'is_success': False})
관찰이 숫자 배열이 아니라 observation, achieved_goal, desired_goal을 담은 딕셔너리로 나옵니다. 이런 환경에는 MlpPolicy 대신 MultiInputPolicy를 씁니다. MultiInputPolicy는 딕셔너리 형태의 관찰을 벡터로 바꿔 주는 부분이 추가된 것 말고는 MlpPolicy와 같습니다.
바닐라 ER
먼저 HER 없이 일반 경험 리플레이로 학습합니다. 학습 과정을 화면에만 출력하지 않고 CSV 파일로 저장하도록 로거를 설정합니다.
from stable_baselines3 import DQN
from stable_baselines3.common.logger import configure
# 모델
model = DQN("MultiInputPolicy", env, verbose=1)
# 학습 과정을 CSV로 저장
log_dir = "./vanilla"
new_logger = configure(log_dir, ["csv"])
model.set_logger(new_logger)
# 학습
model.learn(5000, progress_bar=True)
./vanilla/progress.csv 파일에 학습 중 화면에 출력되던 지표들이 저장됩니다. 여기서 time/total_timesteps는 지금까지 진행한 단계 수, rollout/success_rate는 목표 도달에 성공한 비율입니다.
학습 과정 시각화
import pandas as pd
import matplotlib.pyplot as plt
log_df = pd.read_csv(log_dir + "/progress.csv")
plt.plot(log_df['time/total_timesteps'], log_df['rollout/success_rate'])
5,000단계로는 학습이 부족하므로 50,000단계 정도로 늘려 보면 좋습니다. HER를 쓰지 않아도 학습이 아예 안 되지는 않습니다. 동전이 5개라 32번에 한 번쯤은 우연히 보상을 받기 때문입니다.
DQN+HER
HER를 적용하려면 replay_buffer_class를 HerReplayBuffer로 바꾸고 옵션을 지정합니다. 이 설정을 하지 않으면 기본 리플레이 버퍼가 쓰입니다.
n_sampled_goal: 경험마다 만들어 낼 가상 목표의 개수입니다. 2로 하면 실패한 경험 하나에 대해 가상 목표를 두 개 설정해 학습합니다.goal_selection_strategy: 가상 목표를 고르는 전략입니다.FINAL: 에피소드에서 마지막에 도달한 상태를 가상 목표로 삼습니다.FUTURE: 현재 단계 이후에 거쳐 간 상태 중 하나를 무작위로 골라 가상 목표로 삼습니다.EPISODE: 현재 단계 이전을 포함해 에피소드에서 거쳐 간 상태 중 하나를 무작위로 고릅니다.
어느 전략이 가장 좋은지는 정해져 있지 않습니다.
from stable_baselines3 import DQN
from stable_baselines3.common.logger import configure
from stable_baselines3 import HerReplayBuffer
from stable_baselines3.her.goal_selection_strategy import GoalSelectionStrategy
model = DQN(
"MultiInputPolicy",
env,
replay_buffer_class=HerReplayBuffer,
replay_buffer_kwargs=dict(
n_sampled_goal=1, # 행동마다 선택하는 가상 목표의 개수
goal_selection_strategy=GoalSelectionStrategy.FUTURE, # 가상 목표 선택 전략
),
verbose=1
)
# 학습 과정을 CSV로 저장
log_dir = "./her"
new_logger = configure(log_dir, ["csv"])
model.set_logger(new_logger)
# 학습
model.learn(5000, progress_bar=True)
학습 과정 시각화
log_df = pd.read_csv(log_dir + "/progress.csv")
plt.plot(log_df['time/total_timesteps'], log_df['rollout/success_rate'])
바닐라 ER과 공평하게 비교하려면 같은 단계 수(예: 50,000)로 학습합니다. 이 환경에서는 차이가 크지 않지만, HER를 적용한 쪽이 성공률이 조금 더 빨리 올라갑니다. 실패한 경험도 그냥 버리지 않고 가상 목표를 성공한 경험으로 바꿔 학습하기 때문입니다.
정리
HER가 항상 유리한 것은 아닙니다.
- HER가 도움이 되는 경우: 보상이 거의 주어지지 않거나, 완전히 성공했을 때만 보상을 주고 조금이라도 틀리면 보상이 없는 문제
- HER가 필요 없는 경우: 목표에 가까워질수록 보상이 서서히 커지는 문제