강화학습/파이썬과 케라스로 배우는 강화학습(스터디)

[강화학습] 13 - 액터-크리틱

고집호랑이 2023. 1. 18. 20:58

개요 

저희는 이전에 몬테카를로 폴리시 그레이디언트라고 불리는 REINFORCE 알고리즘에 대해서 배웠습니다. 

 

반환값을 이용하여 에피소드가 끝날 때 인공신경망을 업데이트시키는 특징 때문에 몬테카를로라는 말이 붙었죠.

 

그렇기 때문에 REINFORCE 알고리즘도 에피소드마다만 학습할 수 있다는 단점이 있습니다.

 

또한 반환값은 분산이 크며, 에피소드의 길이가 길어질수록 특정 상태(s,a)에 대한 반환값의 변화가 커지기 때문에 학습이 잘 이루어지지 않을 수도 있죠. 

 

이러한 단점을 극복하고 매 타임스텝마다 학습할 수 있도록 한 것이 액터-크리틱(Actor-Critic)입니다.

 

액터-크리틱 이론

액터-크리틱은 REINFORCE 알고리즘의 단점을 해결하기 위해서 다이내믹 프로그래밍의 정책 이터레이션 구조를 사용합니다.

 

정책 이터레이션은 가치함수를 이용한 정책 평가와 탐욕 정책을 통한 정책 발전으로 이루어져 있었습니다. 

 

폴리시 그레이디언트에서 정책 발전정책신경망의 업데이트 과정에 해당하고, 정책 평가는 아래의 정책신경망 업데이트 식에서 $q_\pi(s,a)$가 그 역할을 합니다. 

$$θ_{t+1} = θ_t + α∇_θJ(θ) ∽ θ_t + α[∇_θlogπ_θ(a | s)q_π(s, a)]$$

 

정책신경망을 사용하면 테이블 형태의 큐함수를 저장하지 않기 때문에 $q_\pi(s,a)$ 대신 반환값 $G_t$를 사용하는 것이 REINFORCE 알고리즘이었죠.

$$θ_{t+1} ∽ θ_t + α[∇_θlogπ_θ(a | s) G_t]$$

 

액터-크리틱은 반환값이 아닌 큐함수를 사용하기 위해서 큐함수를 근사하는 인공신경망을 하나 더 만듭니다.

 

즉 인공신경망을 총 2개 만들어서 하나는 정책을 근사하여 정책 발전의 역할을 하고, 다른 하나는 큐함수를 근사하여 정책 평가의 역할을 하는 것이죠. 

 

큐함수를 근사하는 인공신경망을 가치신경망이라고 하고 정책을 평가하는 데에 사용되기 때문에 크리틱(critic)이라고도 부릅니다.  

 

정책 이터레이션과 액터-크리틱의 관계는 다음과 같습니다.

 

정책 이터레이션과 액터-크리틱 관계

 

가치신경망의 가중치를 w라고 한다면 액터-크리틱의 업데이트 식은 다음과 같습니다.

$$θ_{t+1} ∽ θ_t + α[∇_θlogπ_θ(a | s)Q_w(s, a)]$$

 

REINFORCE 알고리즘의 업데이트 식에서 반환값만 큐함수로 바뀌었을 뿐이므로 오류함수도 크로스 엔트로피와 반환값의 곱에서 반환값만 큐함수로 바뀐 크로스 엔트로피와 큐함수의 곱이 액터-크리틱의 오류함수가 됩니다.

 

오류함수 = 정책 신경망 출력의 크로스 엔트로피 × 큐함수(가치신경망 출력)

 

하지만 위의 식을 그대로 쓴다면 큐함수의 값에 따라 오류함수의 값이 많이 변하게 됩니다.

 

따라서 큐함수의 변화 정도를 줄여주기 위해서 베이스라인을 사용하게 됩니다. 가치함수는 상태마다 값이 다르지만 행동마다 다르지는 않기 때문에 큐함수의 분산을 효율적으로 줄일 수 있습니다.

 

따라서 액터-크리틱에서는 이 가치함수를 베이스라인으로 사용하죠. 큐함수에서 베이스라인인 가치함수를 뺀 것을 어드밴티지 함수라고 합니다.

 

v라는 가중치를 가지는 인공신경망으로 가치함수를 근사한다고 하면 어드밴티지 함수는 다음과 같은 수식으로 나타낼 수 있습니다.

$$A(S_t, A_t) = Q_w(S_t, A_t) - V_v(S_t)$$

 

큐함수와 가치함수를 따로 근사한다면 비효율적이기 때문에 큐함수를 가치함수로 표현한다면 가치함수를 근사하는 것만으로 어드밴티지 함수를 정의할 수 있습니다. 

 

따라서 어드밴티지 함수의 수식은 다음과 같이 표현할 수 있으며 형태가 시간차 에러와 같으므로 $δ_v$라고 정의합니다.

$$δ_v = R_{t+1} + γV_v(S_{t+1}) - V_v(S_t)$$

 

이 어드밴티지 함수를 이용한 액터 - 크리틱의 최종 업데이트 식은 다음과 같습니다.

$$θ_{t+1} ∽ θ_t + α[∇_θlogπ_θ(a | s)δ_v]$$

 

오류함수도 정책 신경망 출력의 크로스 엔트로피와 어드밴티지 함수를 곱한 것이 되겠죠. 

 

가치함수를 근사하는 가치신경망도 학습이 필요합니다. 가치신경망은 시간차 예측을 학습에 이용하여 MSE 오류함수를 최소화하도록 업데이트를 진행합니다.

 

MSE = $($정답 - 예측$)^2$ = $(R_{t+1} + γV_v(S_{t+1}) - V_v(S_t))^2$

 

액터-크리틱의 전체 구조는 다음과 같습니다.

 

액터-크리틱의 학습 구조

 

정리하자만 정책 신경망의 출력(정책)으로 크로스 엔트로피 오류함수를 계산하고 가치신경망의 출력(가치함수)으로 시간차 에러(어드밴티지 함수)를 계산합니다.

 

이후 가치신경망은 시간차 에러를 이용하여 MSE 오류함수로 신경망을 업데이트하고, 정책신경망은 크로스 엔트로피 오류함수와 어드밴티지 함수를 곱해서 만든 새로운 오류함수로 업데이트를 진행합니다.

 

이렇게 액터-크리틱에 어드밴티지가 사용되기 때문에 A2C(Advantage Actor-Critic)이라고도 부릅니다.

 

액터-크리틱 코드 설명

액터-크리틱으로 에이전트가 학습되는 과정을 한 번 살펴보면 <정책신경망의 출력을 이용해서 행동 선택 → 환경으로부터 보상과 다음 상태를 받음 → 샘플(s,a,r,s')과 가치신경망의 출력을 이용하여 시간차 에러(어드밴티지 함수)를 구함 → 시간차 에러(어드밴티지 함수)로 가치신경망과 정책신경망을 업데이트> 다음과 같습니다. 

 

이를 코드로 표현하기 위해서는 먼저 두 신경망부터 정의해야합니다.

 

<정책신경망과 가치신경망 정의>

# 정책 신경망과 가치 신경망 생성
class A2C(tf.keras.Model):
    def __init__(self, action_size):
        super(A2C, self).__init__()
        self.actor_fc = Dense(24, activation='tanh')
        self.actor_out = Dense(action_size, activation='softmax',
                               kernel_initializer=RandomUniform(-1e-3, 1e-3))
        self.critic_fc1 = Dense(24, activation='tanh')
        self.critic_fc2 = Dense(24, activation='tanh')
        self.critic_out = Dense(1,
                                kernel_initializer=RandomUniform(-1e-3, 1e-3))

    def call(self, x):
        actor_x = self.actor_fc(x)
        policy = self.actor_out(actor_x)

        critic_x = self.critic_fc1(x)
        critic_x = self.critic_fc2(critic_x)
        value = self.critic_out(critic_x)
        return policy, value

정책신경망의 역할을 하는 actor와 가치신경망의 역할을 하는 critic 인공신경망을 생성하였습니다. 

 

두 인공신경망 모두 은닉층의 활성함수로 tanh 함수를 사용했으며, RandomUniform 함수를 이용해서 신경망의 가중치를 작은 범위 내에서 초기화시켰습니다.

 

하지만 정책신경망의 출력은 각 행동을 할 확률인 정책이고 가치신경망의 출력은 가치함수이기 때문에 정책신경망의 출력층은 softmax를 활성함수로 사용하고 가치신경망의 출력층은 선형함수를 활성함수로 사용합니다.

 

상태가 입력값으로 들어오면 정책신경망과 가치신경망을 통과해 policy(정책)value(가치함수)를 return 해줍니다.

 

<행동 선택>

# 정책신경망의 출력을 받아 확률적으로 행동을 선택
def get_action(self, state):
    policy, _ = self.model(state)
    policy = np.array(policy[0])
    return np.random.choice(self.action_size, 1, p=policy)[0]

행동은 정책신경망의 출력인 policy(정책)를 이용해서 확률에 따라 행동을 선택합니다.

 

이제 환경으로부터 보상과 다음 상태를 받고 두 인공신경망을 업데이트해주어야 합니다.

 

<인공신경망 업데이트>

# 각 타임스텝마다 정책신경망과 가치신경망을 업데이트
def train_model(self, state, action, reward, next_state, done):
    model_params = self.model.trainable_variables
    with tf.GradientTape() as tape:
        policy, value = self.model(state)
        _, next_value = self.model(next_state)
        target = reward + (1 - done) * self.discount_factor * next_value[0]

        # 정책 신경망 오류 함수 구하기
        one_hot_action = tf.one_hot([action], self.action_size)
        action_prob = tf.reduce_sum(one_hot_action * policy, axis=1)
        cross_entropy = - tf.math.log(action_prob + 1e-5)
        advantage = tf.stop_gradient(target - value[0])
        actor_loss = tf.reduce_mean(cross_entropy * advantage)

        # 가치 신경망 오류 함수 구하기
        critic_loss = 0.5 * tf.square(tf.stop_gradient(target) - value[0])
        critic_loss = tf.reduce_mean(critic_loss)

        # 하나의 오류 함수로 만들기
        loss = 0.2 * actor_loss + critic_loss

    # 오류함수를 줄이는 방향으로 모델 업데이트
    grads = tape.gradient(loss, model_params)
    self.optimizer.apply_gradients(zip(grads, model_params))
    return np.array(loss)

밑의 액터-크리틱의 변형된 업데이트 식을 보면 알 수 있듯이 정책신경망의 오류함수는 크로스 엔트로피와 어드밴티지의 곱이였습니다. 

$$θ_{t+1} ∽ θ_t + α∇_θ[logπ_θ(a | s)δ_v]$$

 

따라서 REINFORCE 알고리즘에서 구한 것처럼 cross-entropy를 구해주고 아래 식대로 advantage(어드밴티지)target-value[0]로 구한 뒤 곱하여 actor_loss(정책신경망의 오류함수)를 구합니다. 

$$δ_v = R_{t+1} + γV_v(S_{t+1}) - V_v(S_t)$$

 

advantage(어드밴티지)를 구할 때 tf.stop_gradient를 사용하는 데, 이는 정책신경망의 오류함수를 구하는 과정에서 가치신경망을 업데이트하지 않기 위해서입니다.

 

가치긴경망의 오류함수는 DQN에서와 마찬가지로 학습 도중에 타깃이 학습되는 일이 없게 업데이트 목표에 tf.stop_gradient를 적용해서 아래의 식으로 오류함수를 구하면 됩니다.

 

MSE = $($정답 - 예측$)^2$ = $(R_{t+1} + γV_v(S_{t+1}) - V_v(S_t))^2$

 

이후 두 인공신경망의 학습 속도를 비슷하게 맞추기 위해서 가치신경망의 오류함수에 더 큰 가중치를 주면서 두 오류함수를 합져줍니다.(가치신경망의 학습 속도가 더 빠르기 때문)

 

# 최적화 알고리즘 설정, 미분값이 너무 커지는 현상을 막기 위해 clipnorm 설정
self.optimizer = Adam(lr=self.learning_rate, clipnorm=5.0)

이제 이 오류함수를 이용해서 optimizer로 인공신경망을 업데이트해주면 되는데, 액터-크리틱의 학습 안정성을 위해서 optimizer를 정의할 때 clipnorm이라는 조건에 5.0의 값을 넣어 업데이트하는 크기가 평균 5.0을 넘지 않게 설정합니다. 

 

액터-크리틱의 전체 코드는 아래 링크에서 확인하실 수 있습니다.

 

https://github.com/rlcode/reinforcement-learning-kr-v2

 

GitHub - rlcode/reinforcement-learning-kr-v2: [파이썬과 케라스로 배우는 강화학습] 텐서플로우 2.0 개정판

[파이썬과 케라스로 배우는 강화학습] 텐서플로우 2.0 개정판 예제. Contribute to rlcode/reinforcement-learning-kr-v2 development by creating an account on GitHub.

github.com

 

액터-크리틱 실행 및 결과

액터-크리틱 터미널 창

위는 카트폴에서 액터-크리틱을 실행시켰을 때의 터미널 창입니다. 현재 에피소드, 평균점수, 오류함수 값이 출력되는 것을 확인할 수 있습니다.

 

액터-크리틱은 REINFORCE에 비해 매 타임스텝 학습을 진행할 수 있다는 장점이 있지만, 또 다른 근사함수인 크리틱을 사용했기 때문에 학습이 편향되기 쉽습니다.

 

또한 액터-크리틱의 학습은 온폴리시로 진행되기 때문에 현재 에이전트가 경험하고 있는 상황에 따라서 에이전트의 업데이트가 계속 달라진다는 단점이 있습니다.

 

DQN과 액터-크리틱 결과 비교
DQN 알고리즘(좌)과 액터-크리틱(우)의 실행 결과

위의 그림은 카트폴에서 평균 점수가 400점에 도달할 때까지의 DQN 알고리즘(좌)과 액터-크리틱(우)의 실행 결과입니다.

 

DQN 알고리즘에 비해 액터-크리틱이 좀 더 부드러운 학습 곡선을 그리면서 빠르게 학습을 완료하는 것을 확인할 수 있습니다.  

 

지금까지 에이전트는 카트폴이라는 환경에서 왼쪽과 오른쪽의 행동만을 선택할 수 있었습니다. 하지만 실제 환경에서 에이전트가 할 수 있는 행동을 왼쪽과 오른쪽 두가지로 정의할 수 없습니다.

 

에이전트는 실제 환경에서 정면을 기준으로 30도 만큼 왼쪽으로 움직일 수도 있고, 오른쪽으로 50도 만큼 꺾어서 움직일 수도 있습니다.  

 

즉 실제 환경에서 에이전트의 행동 집합은 이산적이지 않고 연속적이죠. 이를 위해서 만든 것이 연속적 액터-크리틱입니다. 

 

다음 포스팅에서는 이 연속적 액터-크리틱에 대해서 설명하도록 하겠습니다. 감사합니다~!

 

http://www.yes24.com/Product/Goods/44136413

 

파이썬과 케라스로 배우는 강화학습 - YES24

“강화학습을 쉽게 이해하고 코드로 구현하기”강화학습의 기초부터 최근 알고리즘까지 친절하게 설명한다!‘알파고’로부터 받은 신선한 충격으로 많은 사람들이 강화학습에 관심을 가지기

www.yes24.com

※ 이 글은 위의 책 내용을 바탕으로 작성한 글입니다.