본문 바로가기

Data & AI

강화학습 트레이딩 3. 강화학습

안녕하세요. 

지난 시간에 강화학습을 위한 메타데이터 수집을 완료했고 

 

이번에는 본격적인 강화학습 내용을 담으려고 합니다. 

 

결론을 이야기하자면 아직 완벽히 학습시키지는 못해서 

 

투자 자동화까지 연결되도록 MLOps를 연결하진 않았습니다. 

 

조금 더 다양한 연구 시도를 통해 학습 시행착오가 필요할 것 같습니다. 

 

 

연구 목적

 

일단 목적부터 다시 상기해 보면

 

저는 Top-Down 방식으로 시장을 접근하고 있습니다. 

 

전체 시장 자산군을 거시적인 관점에서 자산배분을 진행하고

 

경제 사이클별 적절한 주식 비중을 선택하고, 그 비중 안에서 추천알고리즘을 적용하고

 

개별 종목 안에서는 미시적인 데이터들의 흐름들을 분석해보고자 합니다. 

 

이 과정 동안 각 분야의 투자에 맞도록 AI를 만들어 투입시키고자 합니다. 

 

그 첫 번째 단계로 자산배분을 위한 AI를 만들고 있고

 

여러 자산군들의 파도들을 타고 다니는 것을 형상화하여 Dynasurf라는 이름을 가지고 있습니다. 

 

 

지난 데이터 수집 리뷰와 처리


무슨 종류의 AI 던, 데이터가 필요합니다.

 

지난번 수집한 데이터를 다시 살펴보고

 

데이터를 처리하면서 강화학습을 위한 형태로 변형해 보겠습니다. 

 

좋은 ML/DL 모델들은 문제 해결을 위한 단서들을 가진 데이터들을 가지고 만들어집니다. 

 

시장에서 문제해결을 위한 단서를 가진 데이터들은 무엇일까라는 질문에 저는 가격이라고 말하고 싶습니다. 

 

물론 대안데이터들이 중요시되고 있긴 하지만 시장 참여자들이 가장 중요한 의미를 가진 데이터라고 저는 생각합니다. 

 

가지고 있는 데이터들을 살펴보면 다양한 자산들을 추종하는 ETF의 가격데이터입니다. 

 

 

가격이 중요한 데이터이긴 하지만 AI학습을 위해선 데이터 처리가 필요합니다. 

 

아직 본격적으로 학습을 진행하는 것은 아니라서

가격이라는 인간이 필요로 하는 데이터가 아닌 가격의 변화, 모멘텀만 추가해 주었습니다. 

 

단기, 장기의 기준이 어느 정도인지는 주관적이지만 5일 거래일부터 250일 거래일까지의 모멘텀을 준비해 보았습니다. 

 

각 자산군들이 많다 보니 각 etf별 5, 10, 20, 60, 120, 250 6개의 특성이 추가되다 보니

 

feature수가 91개 정도였습니다(13개 일별 pct_change + 13 * 6)

 

 

강화학습 환경 설정

 

일반적인 ML/DL에서는 데이터를 준비하고 loss function을 최적화하면 되는데

 

강화학습에서는 환경과 Agent를 설정해주어야 합니다. 

 

모든 코드를 보여드리기보단 강화학습을 위하 시장데이터를 어떻게 처리하는지 의사코드정도로 ~보여드리고자 합니다. 

 

class DynasurfEnv(gym.Env):
    def __init__(self, data):
    	self.data = data
        self.position = {
            'IEF': 0.2,
            'TLT': 0.2,
            'XLE': 0.2,
            'XLY': 0.2,
            'GLD': 0.2
        }
        
        # ...
    
    def calculate_reward(self, action):
    	# calculate reward from data
        return reward
        
    def observation(self):
    	# return next observation state from data
        return state
        
    def step(self, action):
    	step_reward = self.calculate_reward(action)
    	step_state = self.observation()
        
    	return step_state, step_reward, self._done, info

 

openAI Gym 라이브러리를 상속받아 환경을 구현했습니다. 

 

상속을 받으면 기본적으로 구현해야 하는 함수들이 있는데 

 

필요한 함수들을 구현해 주고 학습시킬 문제에 맞게 환경을 구성해주어야 합니다. 

 

init함수에서는 position의 비율을 설정해 주었고. 환경이 처음 만들어질 때 동일 비율로 자산을 배분하는

 

어찌 보면 올웨더에서 시작한다고 보면 됩니다. 올웨더 전략으로 설정해 두었지만 강화학습을 진행하면서

 

각 시장상황을 보며 AI가 자산군의 비율을 조정해 주도록 구성해주려고 합니다. 

 

위 함수에서는 step함수가 가장 중요합니다. 

 

학습을 진행하면서 에이전트가 에피소드마다 step함수를 호출해서 환경과 상호작용하기 때문입니다. 

 

그래서 에이전트가 행동을 가했을 경우에 대한 처리이기 때문에 입력변수로 action을 받고 

 

행동을 가하고 다음 상태, 행동에 대한 보상, 에피소드 종료 여부, 부가정보를 응답으로 내려줍니다. 

 

행동에 대한 상태, 보상등을 받는 이유는 Agent가 들고 있는 딥러닝 네트워크에서 찾고자 하는 목적함수들을 최적화하기 위해서입니다. 

 

이 설명을 위해선 머신러닝, 딥러닝이 어떤 원리로 학습되는지 필요할 수 있는데

 

저에게 설명을 듣는 것보다. 3 Blue1 Brown의 아래 영상을 보는 것을 추천드립니다. 

https://www.youtube.com/watch?v=aircAruvnKk

 

 

 

강화학습 Agent  설정

위에서 환경을 구성해 주었고 이제 환경과 상호작용할 Agent들을 만들어주어야 합니다. 

 

여기서 강화학습의 갈래에 대해서 간략히 설명을 드려야 하는데요

 

강화학습은 크게 두 가지 방법이 있습니다. 

  • 가치함수 최적화 : 환경과 상호작용할 때 (상태, 행동)을 한 쌍으로 보고 이 환경에서 (상태, 행동)의 가치를 추정해 나가는 방법
  • 정책근사 : 정책함수를 근사; 정책은 주어진 상황에 어떤 행동을 선택할 확률을 나타냅니다.

각 방법들마다 가치함수 최적화는 Qlearning, DQN

 

policy gradient는 REINFORCE 알고리즘이 있습니다. 

 

여는 이 두 가지를 모두 이용하는 Actor-Critic 알고리즘을 사용할 예정입니다. 

 

Actor-Critic알고리즘은 actor, critic이라는 두 에이전트를 만들고

 

actor는 policy gradient, critic은 value function을 근사합니다. 

 

대표적인 알고리즘으론 A2C, A3C 등이 있습니다.

 

저는 풀고자 하는 문제 자체가 연속적인(자산군의 비율 조정) 문제이기 때문에

 

해당 문제에 적합한 DDPG 알고리즘을 응용해서 Agent를 생성하고자 했습니다. 

 

Agent들도 위 환경과 같이 의사코드로 필요한 부분만 작성하도록 하겠습니다. 

 

class Actor(nn.Module):
    def __init__(self):
        # ...
        self.dl_layer = DeepLearningLayer(
        input_layer=state_dimension, 
        hidden_layer=[160, 80, 40, 20] # many hidden layer ...
        output_layer=action_dimension
                       hidden_act='ReLU',
                       out_act='Softmax')


class Critic(nn.Module):
    def __init__(self):
        self.state_encoder = DeepLearningLayer(input_layer=state_dimension, [], output_layer=64,
                                 out_act = 'ReLU')

        self.action_encoder = DeepLearningLayer(input_layer=action_dimension, [], output_layer=64,
                                  out_act='ReLU')

        self.q_estimator = DeepLearningLayer(input_layer=128, [64, 32, 16], output_layer=action_dimension,
                               hidden_act='ReLU',
                               out_act='Softmax') 
    def forward(self, x, a):
        emb = torch.cat([self.state_encoder(x), self.action_encoder(a)], dim=-1)
        return self.q_estimator(emb)

Actor와 Critic agent는 우리가 딥러닝이라고 부르는 AI에 쓰이는 심층신경망을 각자 가지고 있습니다.

 

하지만 가지고 있는 신경망의 목적이 다르다는 걸 위에서 설명했었죠. 

 

그래서 입력과 출력 신경망의 구조가 다릅니다. 

 

신경망의 구조는 해당 세부분야에 깊이 있는 연구자들이 있을 정도로 공부가 필요한 부분이지만 간단하게 구현해 보았습니다. 

 

대충 각 에이전트마다 심층신경망들이 있고 Critic의 경우는 상태, 행동에 대한 쌍을 받는다고 했지만 

 

성능을 개선시킨 사례가 있는 구조로 두 텐서를 하나로 합쳐 새로운 심층신경망으로 보내주는 형식으로 참고해 구현해 보았습니다.  

 

그리고 행동을 출력으로 하는 레이어에선 출력 활성화 함수로 Softmax를 적용했습니다. 

 

Softmax 예제

Softmax는 다중분류 문제에서 특정 분류에 대한 확률을 제공하기 위해 사용되었는데

 

이를 응용해서 다양한 자산군들의 비율을 설정하도록 할 수 있겠다 싶어서 적용해 보았습니다. 

 

각 출력의 합을 1로 하는 것이 한정된 자산의 배분과 비슷하다고 생각했습니다. 

 

하지만 softmax는 최대 확률을 갖는 값을 위해 사용되었던 경험이 많아서

 

비율을 정하는데도 특화되어 있을진 연구를 더 해볼 생각입니다. 

 

에이전트가 어떤 알고리즘으로 사용될지 추가로 구현을 해야 합니다. 

 

DDPG 알고리즘을 사용하기로 했었고 이 구현 부분에서 딥러닝의 신경심층망이 학습하는 부분입니다. 

 

class DDPG(nn.Module):
	def __init__(self, critic, critic_target, actor, actor_target):
		# ...
		self.critic_opt = torch.optim.Adam(params=self.critic.parameters())
		self.actor_opt = torch.optim.Adam(params=self.actor.parameters())
        
	def update(self, state, action, reward, next_state, done):
		s, a, r, ns = state, action, reward, next_state

		with torch.no_grad():
			critic_target = r + self.gamma * self.critic_target(ns, self.actor_target(ns)) * (1 - done)
		critic_loss = self.criteria(self.critic(s, a), critic_target)

		self.critic_opt.zero_grad()
		critic_loss.backward()
		self.critic_opt.step()

		actor_loss = -self.critic(s, self.actor(s)).mean()
		self.actor_opt.zero_grad()
		actor_loss.backward()
		self.actor_opt.step()

입력으로 critic, actor만 받으면 될 줄 알았는데 

 

target이 붙은 agent들이 추가로 들어왔습니다. 

 

이유는 학습을 진행하면서 모델이 들고 있는 변수를 바꾸는데 

 

Moving Target 문제를 개선하기 위함입니다. 

 

update 함수 부분이 가장 중요한데 에이전트가 가지고 있는 목적함수(가치함수와 정책함수)를 근사하는 과정입니다. 

 

각 에이전트마다 Adam optimizer를 만들었고

 

해당옵티마이저들은 목적함수를 최소화/최대화하기 위해 고차원 함수영역을 미분해 나갑니다. 

 

 

 

학습 구현

 

이제 강화학습을 위한 준비가 완료되었습니다. 

 

학습진행 구현 부분에서도 기법들이 들어가긴 하는데 

 

일단 구현을 해보고 자세히 살펴보도록 하겠습니다. 

 

env = DynasurfEnv(data=data)
actor, actor_target = Actor(), Actor()
critic, critic_target = Critic(), Critic()

DEVICE = 'cuda'

agent = DDPG(critic=critic,
             critic_target=critic_target,
             actor=actor,
             actor_target=actor_target).to(DEVICE)

memory = ReplayMemory(memory_size)

for _epi in range(total_eps):
    s = env.reset()
    cum_r = 0
    counter = 0

    while True:
        s = to_tensor(s, size=(s.shape[0], s.shape[1])).to(DEVICE)
        a = agent.get_action(s).cpu().numpy()

        ns, r, done, info = env.step(a)

        experience = (s,
                      torch.tensor(a).view(1, action_dimension),
                      torch.tensor(r).view(1, 1),
                      torch.tensor(ns).view(1, state_dimension),
                      torch.tensor(done).view(1,1))
        memory.push(experience)

        s = ns
        cum_r += r

        if len(memory) >= sampling_only_until:
            sampled_exps = memory.sample(batch_size)
            sampled_exps = prepare_training_inputs(sampled_exps, device=DEVICE)
            agent.update(*sampled_exps)
            soft_update(agent.actor, agent.actor_target, tau)
            soft_update(agent.critic, agent.critic_target, tau)

        if done:
            break

 

학습에 필요한 환경, agent를 만들어주고

 

가지고 있는 데이터 내에서 학습을 진행시켜 줍니다. 

 

데이터를 여러 번 반복시켜 학습을 시키고, 각 에피소드마다 보상이 증가하는지 살펴볼 예정입니다. 

 

memory를 사용하는 부분은 Exploration을 제공하기 위한 기법인데

 

딥마인드가 DQN논문을 처음 공개하면서 사용한 방법으로 과거 상태값들을 가끔씩 꺼내어 에이전트에게 제공하면서 

 

과적합을 막고 에이전트에게 Exploration을 제공해 줄 수 있습니다. 

 

 

 

1차 학습 

학습의 초기에는 임의로 설정된 네트워크 파라미터대로 비중을 조절하다 보니 

 

초기 분산이 너무 컸습니다. 

 

44 에피소드를 반복해도 152의 보상인 학습이 있고 

 

7번째 에피소드에서 158의 보상을 경험하기도 합니다.

 

처음 초기화되는 값들에 따라서 결과에 영향을 많이 받기도 했었습니다. 

 

 

memory buffer에서 과거 데이터를 학습하는 것으로는 Exploration이 부족했는지

 

어느 정도 지나서는 데이터가 스스로 Local 최적화 지점에 도착했는지 함수의 변화가 거의 없는 경우도 있었습니다. 

52 에피소드와 53 에피소드 보상의 차이가 거의 차이가 없음을 볼 수 있었습니다. 

강화학습에선 (감가 된) 예상 보상의 합을 최대화하는 것이 목적인데 

 

함수가 어떤 형식일진 모르나 고차원 함수 영역에서 

 

Local 한 영역의 최댓값을 구하면 결국 그 문제를 해결하지 못하는 것이 될 수 있습니다. 

 

복잡한 영역에서 더 원활하게 탐험할 수 있도록 Exploration을 주어야겠다고 생각했습니다. 

 

 

Exploration 부여 : Ornstein-Uhlenbeck 

Ornstein-Uhlenbeck은 확률프로세스의 한 종류입니다. 

 

시간에 따른 여러 시스템의 변동을 모델링하는 데 사용되곤 합니다. 

 

페어트레이딩에서 두 자산군간의 차이, 금리 등의 평균 회귀 모델이

 

이 확률프로세스를 응용하기도 합니다. 

 

OU 프로세스는 시계열이 지남에 따라 평균회귀하는 데이터들을 만들어낼 수 있습니다.

 

연속행동구간에서 약간의 노이즈를 주면서 Exploration을 하는 기법을 적용하여 위 문제를 해결해 보려고 시도했습니다. 

 

그러기 위해선 OU 프로세스에 적절한 파라미터를 주어

 

어떤 평균값에 어느 정도로 회귀하는지 확률프로세스를 만들어주어야 합니다. 

 

먼저 Theta에 따라 어떤 변동을 나타내는지 알아보겠습니다. 

 

theta는 평균으로 회귀하는 속도를 결정합니다.

 

값이 크면 회귀속도가 빨라집니다. 노이즈가 평균과 더 멀어지기 평균회귀하기 때문에

 

적은 노이즈를 줄 때 적절합니다. 

 

반대로 작은 값의 경우 평균회귀하는 경향이 적어 노이즈가 커지고 있습니다. 

 

다음은 sigma입니다. 

sigma는 노이즈의 크기를 결정합니다. 

 

값이 클수록 노이즈도 커지는 것을 볼 수 있습니다. 

 

theta, sigma에 대한 적절한 값은 구현한 강화학습 환경에 따라 다르지만

 

decaying epsilon 알고리즘을 응용해 theta, sigma값을 시간에 따라 조절해 주어

 

학습초기에 많은 탐험을 할 수 있도록 설계했습니다. 

 

초기 theta는  0.3, sigma는 0.05

 

마지막 에피소드에는 각각 1.0, 0.01이 되도록 설정했습니다. 

 

# ...
theta = 0.3 + (0.7 / total_episode * n_episode)
sigma = 0.05 - (0.04 / total_episode * n_episode)

 

학습결과 

 

아래 사진은 에피소드를 거듭하면서 최종 보상의 집계를 나타내고 있습니다. 

 

x축은 에피소드, y축은 보상의 합입니다.

 

설계한 대로 초기에는 다양한 Exploration을 하면서 

 

보상의 변동성도 컸지만 점차 줄어가는 것을 볼 수 있습니다. 

 

정적인 비율로 투자를 했을 때 140 정도의 보상을 얻는 데이터였는데 

 

특성데이터를 가공할 때 큰 공을 들이지 않았음에도 어느 정도 성과는 보이고 있음을 볼 수 있습니다. 

 

 

 

계획

아직 더 나은 보상을 얻지 못했다고 느끼고 있어서

 

더 다양한 논문들을 읽고 특성가공기법을 더 익혀서 

 

기대하는 만큼의 보상을 얻는 에이전트를 만드려고 합니다.

 

그리고 데이터 수집과 학습, 학습된 에이전트들을 실제 투자에 투입시키기 위한 MLOps 개발도 진행해야 하고

 

할 일이 참 많은 것 같습니다.