DQN 에 대해

Cover Image for DQN 에 대해
#Paper#RL

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 는 et=(st,at,rt,st+1,at+1)e_t = (s_t, a_t, r_t, s_{t+1}, a_{t+1}) 과 같이 State 와 Action, Reward 의 조합을 의미한다.

코드로 구현하기

나는 gymnasium 에서 제공하는 LunarLander v-2 를 이용해 RL 실습 코드를 작성했다.

Q-table.py
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)Q(s,a)+lrTDerror [Update Rule]TDerror=TDtargetQ(s,a) [Difference]TDtarget=R+γmaxaQ(s,a) [Target]Q(s,a) \leftarrow Q(s,a) + lr* TD_{error} \text{ [Update Rule]} \\ TD_{error} = TD_{target} - Q(s,a)\text{ [Difference]} \\ TD_{target} = R + \gamma \max_{a'}Q(s',a') \text{ [Target]}

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 을 업데이트하게 된다.

학습 결과

image

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 한 것과 별개로, 기존에 알고 있던 Q(s,a)Q(s,a) 를 업데이트 할때 argmax 수식에 의해서 결정된 최대 보상을 기준으로 Q(s,a)Q(s,a) 가 업데이트된다.

  • Q(s,a)Q(s,a) 를 업데이트할때, 해당 time step (state s) 에서 얻은 rewards 와는 별개로 항상 argmax 수식에 의해서 계산된 최대 보상을 기준으로 Q(s,a)Q(s,a) 의 잠재적 기대 보상을 적용한다.

Experience Replay=Buffer 를 이용해서 Agent 가 경험했던 과거 경험들을 토대로 업데이트가 진행되는데, 연속된 프레임 (state) 의 상관관계 때문에, Random 샘플링을 통해서 경험들을 뽑아온다.

코드로 구현하기

DQN.py
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

학습결과

image

poo.py
def foo():
  pass
poo2.py
def foo2():
  pass
test1 test2
tkdyun hello
basamg world

방명록

Visitor Authentication Required

안녕하세요. 방명록을 남기시려면 로그인/회원가입 부탁드립니다. 매너챗 부탁드려요.

Log In / Sign Up
This blog is based on this source code on GitHub.