CS Repository/기초 강화학습

[강화학습] DQN

조금씩 차근차근 2025. 9. 12. 19:57

본 내용은 심층강화학습 인 액션 도서를 참고하여 작성되었습니다.

 

심층 강화학습 인 액션 - 예스24

프로젝트로 배우는 심층 강화학습의 이론과 실제!이 책 『심층 강화학습 인 액션』은 환경이 제공하는 직접적인 피드백에 기반해서 환경에 적응하고 자신을 개선해 나가는 에이전트의 구현 방

www.yes24.com

 

과거에도 Q함수를 신경망으로 표현하려던 시도는 있었지만, 이는 다음과 같은 문제가 있어 해결하지 못했다.

  • 신경망 학습 연산의 비용이 감당 불가능했다.
  • catastrophic forgetting 문제가 있었다. - ER(Experience Replay)
  • 신경망 자체의 편향을 제거하기가 어려웠다. - Target Network

시대가 변화하며 GPU의 병렬 연산의 성능이 대폭 향상되었고,
구글 딥마인드에서 2013년에 발표한 "Playing Atari with Deep Reinforcement Learning" 논문은 이러한 문제들을 해결했다.

 

위 기법들에 대한 학습 시작 전에, 가장 기본적인 DQN의 설계부터 차근차근 알아보자.

DQN 구축

기본적으로 Q-Learning은 다음 함수를 통해 진행된다.


위 점화식을 통해, 결국 Q함수는 위 식의 아래 값을 향해 수렴하게 된다.


이 값을 목표 행동 가치, Target Q Value(목표 Q 값)라고 하자.

Gridworld 게임 엔진 소개

먼저 시작하기 전에, 환경으로 사용할 GridWorld 게임 엔진을 소개하겠다.

첫 번째 매개변수는 정사각 격자 한 변의 크기이다.
위의 예는 4x4크기의 격자를 만든다.

두 번째 매개변수는 격자 내 요소들의 생성 방식을 나타낸다.

  • mode='static' 선택 시, 게임판이 정적 모드로 초기화된다.
  • mode='player' 선택 시, 정적 모드와 같되 플레이어의 위치가 무작위로 설정된다.
  • mode='random' 선택 시, 플레이어와 벽을 포함한 모든 게임 객체가 무작위로 배치된다(이러면 알고리즘의 학습이 더 어려워진다).

우선 가볍게 정적 모드부터 수행해보자.

 

해당 보드를 출력(print(game.display()))하면 다음과 같이 나온다.


여기서 'W'는 벽, '-'는 게임 패배, '+'는 게임 승리, 'P'는 플레이어를 의미한다.

주어지는 보상은 다음과 같다.

  • 게임 승리('+')시, +10의 보상을 획득하고 게임을 종료한다.
  • 게임 패배('-')시, -10의 보상을 획득하고 게임을 종료한다.
  • 그 외 모든 행동 시, -1의 보상을 획득한다.

그럼 게임의 상태는 어떤식으로 표현되는지 알아보자.

게임의 상태는 신경망의 입력으로 쓰이므로 어떤 형태인지 알 필요가 있다.

이 예에서 보듯, 게임의 상태는 하나의 3차 텐서로 표현된다.

여기서 4x4 크기의 평면을 '프레임'으로 간주했을 때,

  • 첫 번째 프레임: 플레이어
  • 두 번째 프레임: 목표
  • 세 번째 프레임: 구덩이
  • 네 번째 프레임: 벽

을 나타낸다.

이와 같이 0과 1만으로 모든 데이터를 표현하는 것을 '원 핫 인코딩'이라고 한다.

 

따라서 신경망의 입력층은 적어도 64차원의 벡터를 받아야 한다.
그리고 신경망은 이 입력 데이터의 의미를 파악해, 그 의미를 보상의 최대화와 연관시키는 방법을 배워야 한다.

처음에는 학습 알고리즘이 게임에 관해 아무것도 모른다는 점을 기억하자.

Q 함수 역할을 하는 신경망

그럼 Q 함수로 사용할 신경망을 구축해 보자.


이 코드는

  1. 신경망 모델 생성
  2. 손실 함수와 학습률을 설정하고
  3. Adam Optimizer를 설정하고
  4. 하이퍼파라미터 2개(감마, 입실론)을 설정했다.

이제 강화학습에 맞는 알고리즘을 설정하자.

  1. 훈련 반복 횟수(epochs) 만큼 for루프를 돌린다.
  2. 루프 안에 또 다른 while 루프(여기서 게임이 진행된다)가 있다.
  3. while의 각 반복에서, 우선 Q 네트워크를 실행(순전파)한다.
  4. 엡실론 탐욕 접근 방식에 따라, ε의 확률로 무작위로 동작을 하나 선택하고 1-ε 의 확률로 지금까지 신경망이 산출한 최고의 Q 가치에 해당하는 동작을 선택한다.
  5. 앞 단계에서 선택된 동작 a를 수행하고, 게임의 새 상태 s_t+1 과 보상 r_t+1을 받는다.
  6. s_t+1을 입력해서 신경망의 순전파를 실행하고, 신경망의 출력으로 현재까지 최고의 Q 가치를 갱신한다.
    • 현재까지 최고의 Q Value를 maxQ로 표기하겠다.
  7. 신경망 훈련을 위한 목푯값 r_t+1 + gamma * maxQ_A(s_t+1)를 계산한다.
    • 해당 max 값을 취하는 원리는 TD/Q-Learning과 관련이 깊다.
    • 동작 A에 의해 게임이 끝나면 유효한 s_t+1은 존재하지 않으므로 gamma * maxQ_A(s_t+1) 도 유효하지 않다.
    • 이 경우에는 항을 0으로 둔다. 즉, 목푯값은 그냥 r_t+1이다.
  8. 출력 벡터의 성분이 네 개이고 방금 취한 동작과 연관된 출력만 갱신(훈련)한다고 할 때,
    Target 출력 벡터는 첫 실행의 출력 벡터에서 동작과 연관된 출력 성분 하나만 Q Learning 공식에 따라 계산한 값으로 바꾼 것에 해당한다.
  9. 이 하나의 표본으로 모델을 훈련한다. 단계 2~9를 게임이 끝날 때까지 반복한다.

Q-Learning에 대한 자세한 학습 원리에 대해서는 링크를 참고해주시길 바랍니다.

 

[강화학습] 시간차 학습, SARSA, Q-Learning

시작하기 전에, 간단하게 강화학습에서의 몬테 카를로와 DP의 특징에 대해 짚고 넘어가겠다.DP다이나믹 프로그래밍의 점화식을 통한 증분 계산을 활용한다.장점따라서 에피소드 진행 중 평가와

dev.go-gradually.me

 

from environment.GridWorld import *  
import torch  

l1 = 64  
l2 = 150  
l3 = 100  
l4 = 4  

model = torch.nn.Sequential(  
    torch.nn.Linear(l1,l2),  
    torch.nn.ReLU(),  
    torch.nn.Linear(l2,l3),  
    torch.nn.ReLU(),  
    torch.nn.Linear(l3,l4),  
)  

loss_fn = torch.nn.MSELoss()  

learning_rate = 1e-3  

optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)  

gamma = 0.9  
epsilon = 1.0  

action_set = {  
    0: 'u',  
    1: 'd',  
    2: 'l',  
    3: 'r',  
}  


epoch = 1000  
losses = []  
for e in range(epoch):  
    game = Gridworld(size=4, mode='static')  
    state_ = game.board.render_np().reshape(1, 64) + np.random.rand(1, 64)/10.0  
    state1 = torch.from_numpy(state_).float()  
    status = 1  
    while (status == 1):  
        qval = model(state1)  
        qval_ = qval.data.numpy()  
        if(random.random() < epsilon):  
            action_ = np.random.randint(0, 4)  
        else:  
            action_ = np.argmax(qval_)  

        action = action_set[action_]  
        game.makeMove(action)  
        state2 = game.board.render_np().reshape(1, 64) + np.random.rand(1, 64)/10.0  
        reward = game.reward()  
        with torch.no_grad():  
            next_q = model(state2)  
            max_next_q = next_q.max(dim=1).values.item()  

        if reward == -1:  
            y_val = reward + gamma * max_next_q  
        else:  
            y_val = float(reward)  


        target_full = qval.detach().clone()  
        target_full[0, action_] = y_val  

        loss = loss_fn(qval, target_full)  

        optimizer.zero_grad()  
        loss.backward()  
        losses.append(loss.item())  

        optimizer.step()  
        state1 = state2  
        if reward != -1:  
            status = 0  

    if epsilon > 0.1:  
        epsilon -= (1/epoch)  # 입실론 Decay 전략. 에포크 반복마다 점차 입실론을 감소시켜나간다.

여기서 특이한 점이 몇가지 있다.

입실론 감소 전략


초기에 제대로 된 환경을 학습 모델이 알지 못하기 때문에 탐험의 비중을 높이고,
점차 학습해 나갈수록 학습 모델이 환경을 정확히 알아가기 시작한다고 가정하여 입실론을 감소시켜 나간다.

with torch.no_grad():

다음 Q 함수값을 계산할 때, 계산 그래프의 생성을 막고 있다.
state2에 대한 Q Value는 그냥 훈련을 위한 목푯값을 계산하는데 필요한 것일 뿐,
계산 그래프를 통해 역전파를 수행하기 위함이 아니다.
따라서 torch.no_grad() 메소드를 이용해 계산 그래프의 생성을 막고 있다.

 

그럼 계산 그래프를 통한 역전파는 어디에 적용되고 있는걸까?

우리가 학습해야 하는 신경망의 매개변수들은 state2가 아니라 state1에 대해 훈련해야 한다.
따라서 계산 그래프를 통한 역전파는 model(state1)로 구한 Q 값에 대해서만 적용한다.

.detach() 메소드


지금 예제에서는 newQ 계산 시 torch.no_grad() 구문을 사용했으므로 꼭 이럴 필요가 없지만,
이처럼 계산 그래프에서 노드를 떼어내는(detach: 탈착) 코드는 중요하다.
노드를 제대로 떼어내지 않으면, 모델 훈련 시 버그를 유발하기 때문에, 명시적으로 detach 메소드를 호출하는 것이 중요하다.

 

이제 이 결과를 살펴보자.


손실 그래프가 다소 들쭉날쭉하지만, 그래도 이동 평균이 0으로 향한다는 추세는 뚜렷하다.

 

이제 이 모델로 Gridworld게임의 한 에피소드를 돌려보자.

def test_model(model, mode='static', display=True):  
    i = 0  
    test_game = Gridworld(size=4, mode=mode)  
    state_= test_game.board.render_np().reshape(1, 64) + np.random.rand(1, 64)/10.0  
    state = torch.from_numpy(state_).float()  

    if display:  
        print("Initial State:")  
        print(test_game.display())  

    status = 1  

    while(status == 1):  
        qval = model(state)  
        qval_ = qval.data.numpy()  
        action_ = np.argmax(qval_)  
        action = action_set[action_]  

        if display:  
            print('Move #: %s; Taking action: %s'%(i, action))  

        test_game.makeMove(action)  
        state_ = test_game.board.render_np().reshape(1, 64) + np.random.rand(1, 64)/10.0  
        state = torch.from_numpy(state_).float()  
        if display:  
            print(test_game.display())  
        reward = test_game.reward()  
        if reward != -1:  
            if reward > 0:  
                status = 2  
                if display:  
                    print("Game won! Reward: %s" % (reward, ))  
            else:  
                status = 0  
                if display:  
                    print("Game Lost. Reward: %s"% (reward, ))  
        i += 1  
        if (i>15):  
            if display:  
                print("Game Lost; too many moves.")  
            break;  
    win = True if status == 2 else False  

    return win  

print(test_model(model))


무사히 학습에 성공한것을 알 수 있었다.

 

 

사실 이는 '정적 모드'에서의 움직임을 '암기'한 것으로, 게임의 구조를 이해한 것이 아니다.
그 증명으로 게임을 random 모드로 수행하면, 손실함수 그래프는 아래와 같이 전혀 수렴하지 않는다.

이에 대한 해소는 다음 글인 ER(Experience Replay)에서 해결해볼 예정이다.