딥러닝 모델/DQN for Cloud-Edge Caching

[강화학습 기초 10] Policy Gradient와 REINFORCE 알고리즘 구현

gksyb4235 2026. 3. 4. 19:41

우리는 Policy Gradient Theorem을 통해 정책 기반 강화학습에서 policy를 직접 학습할 수 있는 방법을 알았다.

Policy Gradient Theorem은 다음과 같은 형태로 표현된다.

 

이 식은 매우 중요한 의미를 가지는데,
식을 통해 우리는 policy의 gradient를 계산하여 policy parameter θ를 직접 업데이트할 수 있다.

하지만 여기서 한 가지 현실적인 문제가 등장한다.

 

를 실제로 계산할 수 있는가?

 

일반적인 환경에서는 정확한 Action-value Qπ(s,a)를 알 수 없다.

따라서 문제를 해결하기 위해 등장한 가장 간단한 알고리즘이 바로 REINFORCE 알고리즘이다.

 

 

 

Policy Gradient Theorm과 REINFORCE 알고리즘


REINFORCE 알고리즘의 핵심 아이디어


REINFORCE 알고리즘의 핵심은 매우 단순하다.

 

즉 다음과 같이 바꾸는 것이다.

 

 

Q 대신 Return (G_t)를 사용할 수 있는 이유


왜 Qπ(s,a) 대신 G_t를 사용해도 괜찮을까?

Action-value 함수는 다음과 같이 정의된다.

 

이때 Qπ(s,a)는 return G_t의 기댓값이다.

Policy Gradient 식에는 기댓값 연산자 𝐸[⋅]가 존재하기 때문에 여기에 𝐺𝑡를 넣어도 문제가 없다.

여러 에피소드에서 얻은 𝐺𝑡들을 평균내면 결국 Qπ(s,a)로 수렴하게 된다.

 

 

왜 Return을 사용하는가?


또 하나 중요한 이유는 Qπ(s,a)를 실제로 계산하기 어렵기 때문이다.

우리가 알고 있는 것은 다음뿐이다.

 

  • 현재 policy π
  • 환경과 상호작용하면서 얻는 reward

하지만 Qπ(s,a)는 상태 s에서 행동 a를 했을 때 미래에 받을 모든 reward의 기댓값을 알아야 한다.

 

반면 Return Gt는 다음의 방법으로 쉽게 얻을 수 있다.

 

  1. policy를 환경에 적용한다
  2. episode를 끝까지 진행한다
  3. reward를 모두 더한다

따라서 REINFORCE 알고리즘은 "Q 대신 실제로 관측 가능한 Return을 사용하는 방법"이라고 볼 수 있다.

 

 

 

 

 

REINFORCE 알고리즘


REINFORCE 알고리즘의 pseudo code


 

1. Policy Network 초기화

  • 먼저 policy network를 정의하고 파라미터 θ를 랜덤하게 초기화한다.
  • 이 네트워크는 상태 s를 입력받아 행동 확률 분포 출력한다. (πθ​(a∣s))

2. 에피소드 생성

  • 현재 policy를 사용하여 환경과 상호작용한다.
    (예를 들어 다음과 같은 trajectory가 생성될 수 있다: (s0,a0,r0),(s1,a1,r1),(s2,a2,r2),...)
  • 이 과정은 episode가 끝날 때까지 계속된다.

 

3. Return 계산

  • 각 시점 t에서의 Return은 현재 시점부터 미래까지 받을 reward의 합으로 정의된다. (뒤에서부터 계산하면 효율적)


4. Policy Gradient Update

  • 각 시점에 대해 다음과 같이 파라미터를 업데이트한다.

 

이 과정을 반복하면 policy가 점점 개선된다.

 

 

 

REINFORCE 업데이트의 직관적 의미


REINFORCE의 파라미터 업데이트 식은 다음과 같다.

 

먼저 다음 부분을 보자.

 

이 값은 행동 a의 확률을 증가시키는 방향을 의미한다.

즉 이 방향으로 파라미터를 업데이트하면 πθ​(a∣s)가 증가한다.

 

 

Return이 양수일 때

만약 Return G_t가 크다면 "+G_t"가 곱해지므로 해당 행동의 확률을 더 크게 증가시킨다.

즉, 좋은 결과를 만든 행동을 더 자주 하도록 만든다.

 

 

반면, Return이 음수일 때

반대로 Return이 음수라면 업데이트 방향이 반대로 바뀐다.

즉, 해당 행동의 확률을 감소시킨다.

 

이를 한 문장으로 정리하면

REINFORCE 알고리즘은 다음 규칙을 따른다.

결과가 좋았던 에피소드의 행동은 확률을 높이고
결과가 나빴던 에피소드의 행동은 확률을 낮춘다.

즉, “좋은 행동은 더 자주, 나쁜 행동은 덜 하도록 학습한다.”

 

 

 

REINFORCE 알고리즘의 loss 함수


그러나 실제 구현에서는 위의 업데이트 식을 직접 사용하지 않는다.

딥러닝 프레임워크(PyTorch 등)는 Loss Function을 최소화하는 방식으로 학습하기 때문이다.

우리가 원하는 업데이트는 다음이다.

 

이 gradient를 얻기 위해서는 다음 값을 미분하면 된다.

 

하지만 딥러닝에서는 Loss를 최소화해야 하므로 다음과 같이 음수를 붙인다.

 

이 Loss를 최소화하면 실제로는 log⁡ πθ(s,a) * G_t최대화하는 것과 동일하다.

 

 

 

 

 

REINFORCE 알고리즘 구현


앞서 살펴본 것처럼 REINFORCE 알고리즘은 Policy Gradient Theorem을 가장 직접적으로 구현한 알고리즘이다.
이 알고리즘의 장점은 구조가 매우 단순하다는 점이다.

이전에 구현했던 DQN의 경우 Replay Buffer나 Target Network와 같은 추가 구성요소들이 필요했다.

반면 REINFORCE는 Policy Network 하나만으로 학습이 가능하기 때문에 훨씬 간단하게 구현할 수 있다.

 

라이브러리 및 하이퍼파라미터 설정


import gym
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.distributions import Categorical

#Hyperparameters
learning_rate = 0.0002
gamma         = 0.98

 

 

이번 구현에서 사용한 주요 파라미터는 learning rate 0.0002와 discount factor γ = 0.98이다.

이 값들은 이론적으로 정답이 있는 값이 아니라 실험적으로 안정적으로 동작하는 값을 선택한 것이다.

 

 

Policy Network 정의


REINFORCE 알고리즘에서는 Q Network 대신 Policy Network를 사용한다.

즉 상태를 입력받아 각 행동의 확률을 출력하는 신경망을 구성한다.

class Policy(nn.Module):
    def __init__(self):
        super(Policy, self).__init__()
        self.data = []
        
        self.fc1 = nn.Linear(4, 128)
        self.fc2 = nn.Linear(128, 2)
        self.optimizer = optim.Adam(self.parameters(), lr=learning_rate)

 

구조는 매우 단순하다.

  • 입력: 상태(state)
  • 은닉층: 128 차원 fully connected layer
  • 출력층: 행동 확률 분포

 

CartPole 환경의 상태는 4차원 벡터이다.
그리고 가능한 행동은 왼쪽 또는 오른쪽 두 가지이므로 출력 차원은 2가 된다.

이 네트워크의 출력은 다음과 같이 확률 분포가 되어야 한다.

따라서 마지막 layer에는 Softmax 함수가 적용된다.

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = F.softmax(self.fc2(x), dim=0)
        return x

 

Softmax를 사용하는 이유는 Policy는 확률 분포여야 하기 때문이다.

즉 출력값들은 다음 조건을 만족해야 한다.

  • 각 값은 0 이상
  • 전체 합은 1

그래야 행동을 확률적으로 샘플링할 수 있다.

 

 

Episode 데이터 저장


REINFORCE 알고리즘은 한 에피소드가 끝난 후에 학습을 진행한다.
따라서 에피소드 동안 생성된 데이터를 잠시 저장해 둘 공간이 필요하다.

이를 위해 다음과 같은 리스트를 사용한다.

self.data = []

 

그리고 데이터를 저장하는 함수는 다음과 같다.

def put_data(self, item):
    self.data.append(item)

 

여기서 저장되는 데이터는 다음 두 가지이다.

  • reward rr
  • 실제로 선택한 행동의 확률 πθ(s,a)

즉 (reward, prob) 형태로 저장된다.

 

 

REINFORCE 업데이트


REINFORCE 알고리즘의 핵심 학습 과정은 train_net() 함수에 구현되어 있다.

    def train_net(self):
        R = 0
        self.optimizer.zero_grad()
        for r, prob in self.data[::-1]:
            R = r + gamma * R
            loss = -torch.log(prob) * R
            loss.backward()
        self.optimizer.step()
        self.data = []

 

이 함수의 핵심은 Return GtG_t 를 계산하는 부분이다.

Return은 다음과 같이 정의된다.

 

현재 시점부터 미래에 받을 reward의 누적값이다.

 

이때 코드를 보면 데이터를 뒤에서부터 순회하고 있다.

self.data[::-1]

 

예를 들어 하나의 에피소드에서 다음과 같은 reward가 발생했다고 가정해보자.

 

그러면 각 상태에서의 Return은 다음과 같이 계산된다.

 

이처럼 앞에서부터 계산하면 매번 미래 reward를 다시 전부 더해야 한다.

하지만 식을 자세히 보면 중요한 패턴이 하나 보인다.

 

현재 Return은 다음 Return을 이용해서 계산할 수 있다.

이를 일반적으로 표현하면 다음과 같다.

 

이 성질을 이용하면 Return을 뒤에서부터 계산하는 것이 매우 효율적이라는 사실을 확인할 수 있다.

 

 

 

Loss 함수


Loss 함수는 위에서 설명한 것처럼 다음과 같이 정의한다.

 

코드에서는 다음과 같이 표현된다.

loss = -torch.log(prob) * R

 

여기서

  • prob : 선택된 행동의 확률
  • R : Return

이 식을 통해 Return이 양수이면 해당 행동 확률이 증가하고, Return이 음수이면 해당 행동 확률이 감소한다.

 

 

Environment와 상호작용


이제 실제로 환경과 상호작용하며 데이터를 생성하는 부분을 살펴보자.

env = gym.make('CartPole-v1')
pi = Policy()

 

CartPole 환경을 생성하고 policy network를 초기화한다.

그리고 각 에피소드마다 다음 과정을 반복한다.

prob = pi(torch.from_numpy(s).float())
m = Categorical(prob)
a = m.sample()

 

여기서 중요한 부분은 행동을 확률적으로 샘플링한다는 점이다.

Categorical 분포를 사용하면 policy가 출력한 확률에 따라 행동을 선택할 수 있다.

만약 action이 왼쪽과 오른쪽이 있고 policy가 각각 0.3과 0.7의 확률을 출력했다면, 30% 확률로 왼쪽, 70% 확률로 오른쪽 행동이 선택된다.

 

 

데이터 저장


환경에서 행동을 수행하면 다음 값을 얻는다.

s_prime, r, done, truncated, info = env.step(a.item())

 

이때 REINFORCE 학습에 필요한 정보는 다음 두 가지이다.

  • reward
  • 선택한 action의 확률

따라서 다음과 같이 저장한다.

pi.put_data((r, prob[a]))

 

여기서 prob[a]는 실제로 선택한 action의 확률을 의미한다.

 

 

학습 진행 과정


에피소드가 종료되면 다음 코드가 실행된다.

pi.train_net()

 

이 과정에서 아래의 순서로 학습이 이루어진다.

  1. Return 계산
  2. Loss 계산
  3. Gradient 계산
  4. Policy Network 업데이트

이 과정을 10000 episode 동안 반복한다.

 

 

 

 

전체 코드


import gym
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.distributions import Categorical

#Hyperparameters
learning_rate = 0.0002
gamma         = 0.98

class Policy(nn.Module):
    def __init__(self):
        super(Policy, self).__init__()
        self.data = []
        
        self.fc1 = nn.Linear(4, 128)
        self.fc2 = nn.Linear(128, 2)
        self.optimizer = optim.Adam(self.parameters(), lr=learning_rate)
        
    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = F.softmax(self.fc2(x), dim=0)
        return x
      
    def put_data(self, item):
        self.data.append(item)
        
    def train_net(self):
        R = 0
        self.optimizer.zero_grad()
        for r, prob in self.data[::-1]:
            R = r + gamma * R
            loss = -torch.log(prob) * R
            loss.backward()
        self.optimizer.step()
        self.data = []

def main():
    env = gym.make('CartPole-v1')
    pi = Policy()
    score = 0.0
    print_interval = 20
    
    
    for n_epi in range(10000):
        s, _ = env.reset()
        done = False
        
        while not done: # CartPole-v1 forced to terminates at 500 step.
            prob = pi(torch.from_numpy(s).float())
            m = Categorical(prob)
            a = m.sample()
            s_prime, r, done, truncated, info = env.step(a.item())
            pi.put_data((r,prob[a]))
            s = s_prime
            score += r
            
        pi.train_net()
        
        if n_epi%print_interval==0 and n_epi!=0:
            print("# of episode :{}, avg score : {}".format(n_epi, score/print_interval))
            score = 0.0
    env.close()
    
if __name__ == '__main__':
    main()

 

 

 

실행 결과


 

 

학습이 진행되면 CartPole의 점수가 점점 증가하는 것을 볼 수 있다.

초기에는 평균 점수가 약 20점 정도에서 시작하지만 학습이 진행될수록 점수가 상승한다.

하지만 학습 도중 점수가 갑자기 떨어지는 경우도 발생한다.

 

그 이유는 다음과 같다.

  • 행동 선택이 확률적이다
  • 환경의 반응도 확률적이다

즉 같은 policy라도 운에 따라 결과가 달라질 수 있다.

 

 

 

Policy 확률의 변화


이번에는 각 episode마다 출력되는 (1초마다 Policy Neural Network에 의해 출력되는) policy의 확률분포를 보자.

        while not done: # CartPole-v1 forced to terminates at 500 step.
            prob = pi(torch.from_numpy(s).float())
            if n_epi % 20 == 0:
                print("# of episode :{}, prob : {}".format(n_epi, prob)) // prob 출력!
            m = Categorical(prob)
            a = m.sample()
            s_prime, r, done, truncated, info = env.step(a.item())
            pi.put_data((r,prob[a]))
            s = s_prime
            score += r

 

 

episode가 20번밖에 진행되지 않은 학습 초기에 policy network의 출력 확률을 보면 다음과 같은 값을 확인할 수 있다.

 

거의 두 행동이 50대 50으로 동일한 확률로 선택된다.

그 결과 평균 점수도 20점대에 머무른다.

 

하지만 episode가 2500을 넘을 정도로 학습이 진행되면 다음과 같이 변화한다.

 

이 의미는 각 step마다 Return이 좋았던 행동의 확률이 점차 조정되고 있음을 뜻한다.

즉 REINFORCE 알고리즘이 실제로 좋은 행동을 강화(reinforce) 하고 있음을 확인할 수 있다.

 

 

 

 

정리


REINFORCE 알고리즘은 Policy Gradient를 가장 단순하게 구현한 알고리즘이다.

핵심 아이디어는 다음과 같다.

  1. Policy Network를 이용해 행동을 선택한다
  2. Episode를 실행해 Return을 얻는다
  3. Return을 이용해 행동 확률을 업데이트한다

좋은 결과를 만든 행동은 확률을 높이고
나쁜 결과를 만든 행동은 확률을 낮춘다.

 

이 단순한 아이디어만으로도 CartPole 문제를 성공적으로 해결할 수 있다.