DQN 에 대해
DB 건들다가 추가로 작성해둔 논문 내용들이 삭제되었습니다. 추후 논문에 대한 추가 설명을 다시 작성하여 기재할 예정입니다
26년 3월부터 운이 좋게도 KIST 의 휴머노이드연구단 랩실로 6개월간 인턴쉽을 진행하게 되었다. 나는 휴머노이드 로봇의 이족보행을 위한 Reinforcement Learning 연구에 참여하게 되었다.
연구 참여까지 2주도 남지 않았지만.. RL 에 대한 맛보기 공부를 하려고한다. DQN 논문을 시작으로, DDPG TRPO A3C PPO, 총 다섯가지 논문을 맛보고 가려고한다. DQN 논문은 Neural Network 를 RL 에 성공적으로 적용시킨 사례이다. Atari 게임들을 사람처럼 수행할 수 있는 Agent 를 학습시키기 위해서 3 channel RGB Image 를 Input 으로 하는 CNN Network 를 접목시킨 사례를 논문에서 설명하고있다.
Q-Table
DQN 논문 이전에는 Q-Table 방식을 이용했다. Q-Table 방식은 State-Action 의 조합을 Table 로 나타내는 방식이다. Q 는 State-Action 테이블 내부에 들어가는 값인데, 나중에 얻게될 미래보상에 대한 기대값 을 의미한다.
초기에는 이 Q-Table 의 값을 0 으로 초기화해놓고, 점차적으로 Update 해간다.
복잡한 State-Action 공간을 모두 채우기는 사실상 불가능하기 때문에, NN 이 접목되기 시작했다.
Q-Table 방식은 on-policy 정책 기반으로, Agent 가 경험하는 Experience 를 그대로 이용해서 Q-Table 을 업데이트한다.
- 여기서 Experience 는 과 같이 State 와 Action, Reward 의 조합을 의미한다.
코드로 구현하기
나는 gymnasium 에서 제공하는 LunarLander v-2 를 이용해 RL 실습 코드를 작성했다.
class LanderAgent:
def __init__(
self,
env: gym.Env,
learning_rate: float,
initial_epsilon: float,
epsilon_decay: float,
final_epsilon: float,
discount_factor: float = 0.95,
):
self.env = env
self.q_values = defaultdict(lambda: np.zeros(env.action_space.n))
self.lr = learning_rate
self.discount_factor = discount_factor
self.epsilon = initial_epsilon
self.epsilon_decay = epsilon_decay
self.final_epsilon = final_epsilon
self.training_error = []
def get_action(self, obs) -> int:
state = tuple(np.round(obs, decimals=3))
if np.random.random() < self.epsilon:
return self.env.action_space.sample()
else:
return int(np.argmax(self.q_values[state]))
def update(self, obs, action, reward, terminated, next_obs):
state = tuple(np.round(obs, decimals=3))
next_state = tuple(np.round(next_obs, decimals=3))
# terminated가 True 이면 future q value = 0
future_qvalue = (1 - int(terminated)) * np.max(self.q_values[next_state])
target = reward + self.discount_factor * future_qvalue
temporal_difference = target - self.q_values[state][action]
# Q-value 업데이트
self.q_values[state][action] += self.lr * temporal_difference
self.training_error.append(temporal_difference)
def decay_epsilon(self):
self.epsilon = max(self.final_epsilon, self.epsilon - self.epsilon_decay)- defaultdict : 처음보는 state 에 대해서 KeyError 일으키지않고, 값을 0 으로 만들어 dictionary 생성
- tuple : observation 은 numpy ndarray 타입으로 반환되는데, 이는 Mutable 하기 떄문에 Dictionary 의 Key 로 사용할 수 없다. 그래서 tuple Immutable 한 자료형인 tuple 를 사용한다.
update 로직을 한번 살펴보자.
Q(s,a) 는 현재 state 에서 특정한 action 을 취해 그 다음 state 인 s' 에 도착했을때, 기대되는 보상 (미래 보상을 포함, Q-value)을 의미한다.
TD_error 는 새로 알게된 정보와, 기존 정보의 차이 = Difference 를 의미한다.
새로 알게된 정보는 TD__target 인데, 이는 특정한 action 을 통해 얻은 즉각적인 보상 R 과 다음 state 인 s' 에서, 가장 큰 보상을 얻을 수 있는 a', a' 를 통해 얻을 수 있는 기대보상 (Q-value) 의 합을 의미한다. 기존 정보는 기존 테이블에 사용하던 Q(s,a) 를 의미한다.
하지만, 새로 알게된 정보를 무작정 신뢰할 수는 없기 때문에 Discount Factor 로 γ 를 사용하고 있다.
결론으로, 계산된 TD_error 를 이용해 Q-Table 을 업데이트하게 된다.
학습 결과

DQN
논문에서는 CNN 네트워크를 이용한다고 했는데, 나는 Fully Connected Layer 를 이용해서 진행해보았다.
LunarLander env 는 이미 좌표와 각도, 속도 등에 대한 정보를 return 해주기 때문에, 이미지 데이터를 사용해 CNN 네트워크를 구현하지 않았다.
논문에서는 Atari Emulator 를 이용해 이미지를 input 으로 사용한다고했는데, preprocessing 단계를 거쳐서 최종적으로 신경망에 들어가는 input 은 88x88x4 크기가 된다.
- Gray scale 변환
- Image Resize
- Image Crop
on-policy 기반으로 업데이트가 이루어진 Q-Table 방식과 달리, Deep Q-Learning (DQN) 은 off-policy 기반으로 업데이트가 이루어진다.
Agent 가 Experience 한 것과 별개로, 기존에 알고 있던 를 업데이트 할때 argmax 수식에 의해서 결정된 최대 보상을 기준으로 가 업데이트된다.
- 를 업데이트할때, 해당 time step (state s) 에서 얻은 rewards 와는 별개로 항상 argmax 수식에 의해서 계산된 최대 보상을 기준으로 의 잠재적 기대 보상을 적용한다.
Experience Replay=Buffer 를 이용해서 Agent 가 경험했던 과거 경험들을 토대로 업데이트가 진행되는데, 연속된 프레임 (state) 의 상관관계 때문에, Random 샘플링을 통해서 경험들을 뽑아온다.
코드로 구현하기
class QNetwork(nn.Module):
def __init__(self, state_dim, action_dim):
super(QNetwork,self).__init__()
self.fc1 = nn.Linear(state_dim, 128)
self.fc2 = nn.Linear(128,128)
self.fc3 = nn.Linear(128,action_dim)
def forward(self, x):
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
return self.fc3(x)
class ReplayBuffer:
def __init__(self,capacity):
self.buffer = deque(maxlen=capacity)
def push(self, state, action, reward, next_state, done):
self.buffer.append( (state, action, reward, next_state, done))
def sample(self, batch_size):
state, action, reward, next_state, done = zip(*random.sample(self.buffer, batch_size))
return np.array(state), action, reward, np.array(next_state), done
def __len__(self):
return len(self.buffer)
class DQNAgent:
def __init__(self, state_dim, action_dim):
self.q_net = QNetwork(state_dim,action_dim)
self.target_net = QNetwork(state_dim, action_dim)
self.target_net.load_state_dict(self.q_net.state_dict())
self.optimizer = optim.Adam(self.q_net.parameters(), lr=1e-3)
self.memory = ReplayBuffer(100000)
self.batch_size = 64
self.gamma = 0.99
self.epsilon = 1.0
self.epsilon_min = 0.01
self.epsilon_decay = 0.995
def get_action(self, state):
if random.random() < self.epsilon:
return random.randint(0,3)
state = torch.FloatTensor(state).unsqueeze(0)
with torch.no_grad():
return self.q_net(state).argmax().item()
def train(self):
if len(self.memory) < self.batch_size:
return
s, a, r, ns, d = self.memory.sample(self.batch_size)
s = torch.FloatTensor(s)
a = torch.LongTensor(a).unsqueeze(1)
r = torch.FloatTensor(r).unsqueeze(1)
ns = torch.FloatTensor(ns)
d = torch.FloatTensor(d).unsqueeze(1)
current_q = self.q_net(s).gather(1,a)
next_q = self.target_net(ns).max(1)[0].unsqueeze(1)
target_q = r + (self.gamma * next_q * (1-d))
loss = F.mse_loss(current_q, target_q)
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
if self.epsilon > self.epsilon_min:
self.epsilon *= self.epsilon_decay학습결과

def foo():
passdef foo2():
pass| test1 | test2 |
|---|---|
| tkdyun | hello |
| basamg | world |