맨위로버튼이미지

 

 

nvidia의 tesla t4 16G의 중고가 약 350만원정도에 cuda 프로세의 갯수는 2560개이다. 오로지 머쉰런닝에만 사용되므로 Rtx 시리즈와는 완전히 다르다고 할 수 있습니다. 무엇보다 가격대인데요. tesla t4모델을 다른 이유로 IDC상면가를 알아보다 GPU서버 임대가를 보는데 보통 한달에 몇백이길래 햐 이건 강화학습 따위로 매매프로그램 만드는 회사는 망하겠다고 생각하고 RTX버젼과 RTX A시리즈와 TESLA버젼이 따로 임대가 되고 있길래 가격을 봤더니 TESLA H100 80G모델이 9천에 몇백 빠지는 8천대 가격이 더라고요. 사실 현재 프로그램의 이미지 원본이 500 X 700이라 이걸 돌린거랑 현재처럼 줄인 버젼의 이미지를 돌렸을 경우 어느정도의 차이가 생길까 하는 의문이 계속되고 있습니다. 사실 CUDA 코어 갯수는 현재 정도면 되고  GDDR이라고 크게 메카니즘이 다른것은 아닐거고  메모리만 늘리면 고해상도가 아니어도 충분이 돌것 같은데 왜 GPU보드는 메모리슬롯을 확장가능하게 만들지 않았을까요? ResNet의 경우는 개발자가 인공신경망을 몇겹까지 쌓을 수 있을까 하는 의문에 만들었다고 합니다.

import torch
import torch.nn as nn
import torch.nn.functional as F

def conv_block_1(in_dim, out_dim, act_fn, stride=1):
    model = nn.Sequential(
        nn.Conv2d(in_dim, out_dim, kernel_size=1, stride=stride),
        act_fn,
    )
    return model

def conv_block_3(in_dim, out_dim, act_fn):
    model = nn.Sequential(
        nn.Conv2d(in_dim, out_dim, kernel_size=3, stride=1, padding=1),
        act_fn,
    )
    return model

class BottleNeck(nn.Module):
    def __init__(self, in_dim, mid_dim, out_dim, act_fn, down=False):
        super(BottleNeck, self).__init__()
        self.act_fn = act_fn
        self.down = down
        
        if self.down:
            self.layer = nn.Sequential(
                conv_block_1(in_dim, mid_dim, act_fn, 2),
                conv_block_3(mid_dim, mid_dim,act_fn),
                conv_block_1(mid_dim, out_dim, act_fn),
            )
            self.downsample = nn.Conv2d(in_dim, out_dim, 1, 2)
        else:
            self.layer = nn.Sequential(
                conv_block_1(in_dim, mid_dim, act_fn),
                conv_block_3(mid_dim, mid_dim,act_fn),
                conv_block_1(mid_dim, out_dim, act_fn),
            )
            self.dim_equalizer = nn.Conv2d(in_dim, out_dim, kernel_size=1)
    
    def forward(self, x):
        if self.down:
            downsample = self.downsample(x)
            out = self.layer(x)
            out = out + downsample
        else:
            out = self.layer(x)
            if x.size() is not out.size():
                x = self.dim_equalizer(x)
            out = out + x
        return out

class ResNet(nn.Module):
    def __init__(self, base_dim, num_classes=2):
        super(ResNet, self).__init__()
        self.act_fn = nn.ReLU()
        self.layer_1 = nn.Sequential(
            nn.Conv2d(3,base_dim,7,2,3),
            nn.ReLU(),
            nn.MaxPool2d(3,2,1),
        )
        
        self.layer_2 = nn.Sequential(
            BottleNeck(base_dim, base_dim, base_dim*4, self.act_fn),
            BottleNeck(base_dim*4, base_dim, base_dim*4, self.act_fn),
            BottleNeck(base_dim*4,base_dim,base_dim*4,self.act_fn, down=False),
        )
        
        self.layer_3 = nn.Sequential(
            BottleNeck(base_dim*4, base_dim*2, base_dim*8, self.act_fn),
            BottleNeck(base_dim*8, base_dim*2, base_dim*8, self.act_fn),
            BottleNeck(base_dim*8, base_dim*2, base_dim*8, self.act_fn),
            BottleNeck(base_dim*8,base_dim*2,base_dim*8,self.act_fn, down=False),
        )

        self.layer_4 = nn.Sequential(
            BottleNeck(base_dim*8, base_dim*4, base_dim*16, self.act_fn),
            BottleNeck(base_dim*16, base_dim*4, base_dim*16, self.act_fn),
            BottleNeck(base_dim*16, base_dim*4, base_dim*16, self.act_fn),
            BottleNeck(base_dim*16, base_dim*4, base_dim*16, self.act_fn),
            BottleNeck(base_dim*16, base_dim*4, base_dim*16, self.act_fn),
            BottleNeck(base_dim*16,base_dim*4,base_dim*16,self.act_fn, down=False),
        )

        self.layer_5 = nn.Sequential(
            BottleNeck(base_dim*16, base_dim*8, base_dim*32, self.act_fn),
            BottleNeck(base_dim*32, base_dim*8, base_dim*32, self.act_fn),
            BottleNeck(base_dim*32, base_dim*8, base_dim*32,self.act_fn, down=False),
        )
        
        self.avgpool = nn.AvgPool2d(7, 1)
        self.fc_layer = nn.Linear(base_dim*3, num_classes)
        
    def forward(self, batch_size, x):
        out = self.layer_1(x)
        out = self.layer_2(out)
        out = self.layer_3(out)
        out = self.layer_4(out)
        out = self.layer_5(out)
        out = self.avgpool(out)
        out = out.view(batch_size, -1)
        out = self.fc_layer(out)
        return out

class ResNetRNN(nn.Module):
    def __init__(self, device, h, w, outputs, hdnsize):
        super(ResNetRNN, self).__init__()
        self.device = device
        self.hidden_size = hdnsize
        self.resnet = ResNet(h*w, hdnsize)
    
        self.i2h = nn.Linear(54 * hdnsize, hdnsize)
        self.h2h = nn.Linear(hdnsize, hdnsize)
        self.i2o = nn.Linear(hdnsize, outputs)
        self.act_fn = nn.Tanh()

    def init_hidden(self):
        return torch.zeros(1, self.hidden_size)
    
    def forward(self, batch_size, x):
        out = self.resnet(batch_size, x)
        hidden = self.init_hidden().to(self.device)
        out= self.i2h(out.view(out.size(0), -1))
        hidden = self.h2h(hidden)
        hidden = self.act_fn(out + hidden)
        return self.i2o(hidden)

위 코드는 resnet과 rnn을 합성한 코드 입니다. resnet은 책 '파이토치 첫걸음'의 소스 내용을 응용했고 rnn은 이전 cnnrnn의 소스에서 참조 했습니다. 파일 이름은 ResNet.py입니다.

import random
from collections import deque, namedtuple
from IPython.display import display, Math
from Account import Account
import math
from itertools import count
import os

import sys
import os.path as path
import plotly as plt
import torch
import torch.nn as nn
import torch.optim as optim
from Market import Market
import torchvision
import time
from ResNet import ResNetRNN
from memory import ReplayMemory, Experience
# from transformers import get_cosine_schedule_with_warmup
import transformers

action_kind = 41
max_episode = 5000
screen_height = 100
screen_width  = 140
data_size = 250
epsilon = 0.3
dis = 0.9

BATCH_SIZE = 8
# GAMMA = 0.999
GAMMA = 0.99
EPS_START = 0.9
EPS_END = 0.05
EPS_DECAY = 200
TARGET_UPDATE = 10
steps_done = 0
loss = any
WINDOW_START = 0
WINDOW_SIZE  = 1500

# for i in range(torch.cuda.device_count()):
#     print(torch.cuda.get_device_name(i))
device_str = "cuda"
device = torch.device(device_str)
    
memory = ReplayMemory(BATCH_SIZE)
converter = torchvision.transforms.ToTensor()
market = Market()
train_net = ResNetRNN(device, screen_height, screen_width, action_kind, 32).to(device)
train_net = nn.DataParallel(train_net, device_ids=[0,1]).to(device)

episode_durations = []
optimizer = optim.RMSprop(train_net.parameters(), lr=0.000001, eps=0.00000001)
# scheduler = get_cosine_schedule_with_warmup(optimizer, 5, base_lr=0.3, final_lr=0.01)
scheduler = transformers.get_cosine_schedule_with_warmup(optimizer, 
                                                         num_warmup_steps=5, 
                                                         num_training_steps=25)
def optimize_action(memory):
    if len(memory) < BATCH_SIZE:
        return None
    return loss

# def select_action(df, idx):
#     try:
#         global steps_done
#         sample = random.random()
#         eps_threshold = EPS_END + (EPS_START - EPS_END) * math.exp(-1. * steps_done / EPS_DECAY)
#         steps_done += 1
#         if sample > eps_threshold:
#             if df.loc[idx, "closemax"] == df.loc[idx, "close"]:
#                 action = 2
#                 return  action
#             elif df.loc[idx, "closemin"] == df.loc[idx, "close"]:
#                 action = 1
#                 return  action
#             else:
#                 return 0
#         else:
#             action = random.randrange(action_kind)
#             return  action
#     except Exception as ex:
#         exc_type, exc_obj, exc_tb = sys.exc_info()
#         fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
#         print("`select_action -> exception! %s : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))
#         return 0
def select_action(df, idx):
    return ((df.loc[idx, "close"] - df.loc[idx, "closemin"]) * (action_kind-1) // (df.loc[idx, "closemax"] - df.loc[idx, "closemin"]))
        
def get_chart(market, idx, max_data):
    img = market.get_chart(idx, max_data=max_data)
    if img is None:
        return None
    # img = Image.fromarray(np.uint8(cm.gist_earth(plt.io.to_image(fig, format='png')*255)))
    # im = Image.fromarray(img, bytes=True)
    # im = Image.fromarray(np.uint8(cm.gist_earth(img))/255)
    # im = Image.fromarray(np.uint8(img)/255)
    # img = img.resize((700, 500), resample=Image.BICUBIC)
    # img = Image.fromarray(cm.gist_earth(plt.io.to_image(fig, format='png'), bytes=True))
    # display(img)
    # chart = converter(img).unsqueeze(0).to(device)
    chart = converter(img).unsqueeze(0)
    return chart

def plot_durations(last_chart, curr_chart):
    plt.figure()
    # plt.subplot(1,2,1)
    img = plt.imshow(last_chart.cpu().squeeze(0).permute(1, 2, 0).numpy(), interpolation='none')
    plt.title('Example extracted screen')
    plt.figure(2)
    # plt.subplot(1,2,2)
    plt.clf()
    durations_t = torch.tensor(episode_durations, dtype=torch.float)
    plt.title('Training...')
    plt.xlabel('Episode')
    plt.ylabel('Duration')
    plt.plot(durations_t.numpy())
    # plt.show()
    # Take 100 episode averages and plot them too
    if len(durations_t) >= 100:
        means = durations_t.unfold(0, 100, 1).mean(1).view(-1)
        means = torch.cat((torch.zeros(99), means))
        plt.plot(means.numpy())

    img.set_data(curr_chart.cpu().squeeze(0).permute(1, 2, 0).numpy())
    plt.pause(0.01)  # pause a bit so that plots are updated
    display.clear_output(wait=True)
    display.display(plt.gcf())
            
def main():
    if path.exists("pt/train_resnetrnn_{}.pt".format(device_str)):
        train_net.load_state_dict(torch.load("pt/train_resnetrnn_{}.pt".format(device_str)))
        
    for _ in range(10):    
        df = market.get_data()
        # df = df.head(WINDOW_SIZE)

        for epoch in range(max_episode):
            account = Account(df, 50000000)
            account.reset()
            for idx,_ in enumerate(df.index, start=data_size):
                try:
                    since = time.time()
                    curr_chart = get_chart(market, idx, data_size)
                    reward = 0
                    num_action = select_action(df, idx)
                    reward, real_action = account.exec_action(2 if num_action == (action_kind - 1) else (1 if num_action == 0 else 0) , idx)
                    print("idx:%d==>action:%d,real_action==>%d price:%.2f"%(idx, num_action, real_action, df.loc[idx, 'close']))
                    # reward = torch.tensor([reward], device=device)
                    action = torch.tensor([[num_action]], device=device, dtype=torch.int64)
                                    
                    memory.push(curr_chart, action, reward)
                    if len(memory) >= BATCH_SIZE:
                        epsode = memory.pop(BATCH_SIZE)
                        batch = Experience(*zip(*epsode))
                        state_batch = torch.cat(batch.state)
                        action_batch = torch.cat(batch.action)
                        train_net.train()
                        state_action_values = train_net(state_batch).gather(1, action_batch)
                        # optimizer = optim.RMSprop(train_net.parameters(), 0.01)
                        
                        criterion = nn.SmoothL1Loss()
                        loss = criterion(state_action_values, action_batch)

                        # Optimize the model
                        # optimizer.zero_grad()
                        optimizer.zero_grad()
                        loss.backward()
                        for param in train_net.parameters():
                            param.grad.data.clamp_(-1, 1)
                        optimizer.step()
                        if loss is not None:
                            print("epoch[%d:%d] epsode is next loss[%.10f]" % (epoch, idx, loss.item()))

                    if idx % TARGET_UPDATE == 0:
                        torch.save(train_net.state_dict(),"pt/train_resnetrnn_{}.pt".format(device_str))
                    
                    spend = time.time() - since
                    print("idx:%d price [%.4f] unit[%.4f] used time[%.2f] agent rate:%.05f remind money:%.02f" 
                            % (idx, df.loc[idx, 'close'], account.unit, spend, account.rate, account.balance + account.unit * df.loc[idx, 'close']))
                    if account.is_bankrupt():
                        break
                    if idx == df.index.max():
                        break
                except Exception as ex:
                    exc_type, exc_obj, exc_tb = sys.exc_info()
                    fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
                    print("`recall_training -> exception! %s : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))
            scheduler.step()

        print("end training DQN")
    
    print('Complete Training')

if __name__ == "__main__":
    main()

위는 ResNet.py의 ResNetRNN클래스를 활용하여 학습을 진행하는 main_resnetrnn.py입니다. 

 

글을 마치며

훈련이 잘 되어 거의 0에 수렴한 결과를 가지고 백테스팅을 진행해 보니 매수 자체를 안하는 결과가 나왔습니다 초반의 학습률이 굉장히 낮을 때보다 결과가 좋지 않았습니다. 그래서 지금은 전체를 여러 단계로 나누고 그 중 (결과값은 인덱스 이므로 언제나 0부터 액션의 가지수 -1 만큼 발생 합니다.) 0과 가장 큰값을 매수와 매도로 설정 나머지는 매매를 안하고 관망 하게 소스를 수정 했습니다. 문제는 액션 수를 늘리니 이번에는 학습률이 굉장히 떨어지는 문제가 발생합니다. 이제 모델은 거의 다 만든것 같습니다 .지금부터는 개인의 삽질만이 방법이 겠죠?

반응형
LIST

맨위로버튼이미지

import random
from collections import deque, namedtuple
from typing import Dict, List, NamedTuple, Optional, Text, Tuple, Union
from IPython.display import display, Math
from Account import Account
import math
from itertools import count

import os.path as path
import numpy as np
import plotly as plt
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torchvision.transforms as T
from Market import Market
from PIL import Image
from plotly import express as px
import torchvision
import time
import joblib
import sys,os

action_kind = 4
max_episode = 5000
screen_height = 50
screen_width  = 70
visit_cnt = [0] * action_kind
# replay_buffer = deque()
epsilon = 0.3
dis = 0.9
data_size = 600

BATCH_SIZE = 8
GAMMA = 0.999
EPS_START = 0.9
EPS_END = 0.05
EPS_DECAY = 200
TARGET_UPDATE = 10
steps_done = 0
loss = any

# for i in range(torch.cuda.device_count()):
#     print(torch.cuda.get_device_name(i))

# if gpu is to be used
# device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# device = torch.device("cuda")
# device_str = "cpu"
device_str = "cuda"
device = torch.device(device_str)

from dqn import DQN
    
converter = torchvision.transforms.ToTensor()
market = Market()
policy_net = DQN(device, screen_height, screen_width, action_kind, 32).to(device)
policy_net = nn.DataParallel(policy_net, device_ids=[0,1]).to(device)

def get_chart(market, idx, max_data):
    # img = Image.fromarray(np.uint8(cm.gist_earth(plt.io.to_image(fig, format='png')*255)))
    # im = Image.fromarray(img, bytes=True)
    # im = Image.fromarray(np.uint8(cm.gist_earth(img))/255)
    # im = Image.fromarray(np.uint8(img)/255)
    # img = img.resize((700, 500), resample=Image.BICUBIC)
    # display(img)
    # img = Image.fromarray(cm.gist_earth(plt.io.to_image(fig, format='png'), bytes=True))
    img = market.get_chart(idx, max_data=max_data)
    img.convert("RGB")
    # print(img)
    img.thumbnail((screen_high, screen_width), Image.ANTIALIAS)
    return chart

def plot_durations(last_chart, curr_chart):
    plt.figure()
    # plt.subplot(1,2,1)
    img = plt.imshow(last_chart.cpu().squeeze(0).permute(1, 2, 0).numpy(), interpolation='none')
    plt.title('Example extracted screen')
    plt.figure(2)
    # plt.subplot(1,2,2)
    plt.clf()
    # durations_t = torch.tensor(episode_durations, dtype=torch.float)
    plt.title('Training...')
    plt.xlabel('Episode')
    plt.ylabel('Duration')
    # plt.plot(durations_t.numpy())
    # plt.show()
    # Take 100 episode averages and plot them too
    # if len(durations_t) >= 100:
    #     means = durations_t.unfold(0, 100, 1).mean(1).view(-1)
    #     means = torch.cat((torch.zeros(99), means))
    #     plt.plot(means.numpy())

    img.set_data(curr_chart.cpu().squeeze(0).permute(1, 2, 0).numpy())
    plt.pause(0.01)  # pause a bit so that plots are updated
#     if is_ipython:
    display.clear_output(wait=True)
    display.display(plt.gcf())

def select_action(df, idx):
    return ((df.loc[idx, "close"] - df.loc[idx, "closemin"]) * (action_kind-1) // (df.loc[idx, "closemax"] - df.loc[idx, "closemin"]))
            
def main():
    if path.exists("pt/train_net_{}.pt".format(device_str)):
        policy_net.load_state_dict(torch.load("pt/train_net_{}.pt".format(device_str)))
    policy_net.eval()
    
    for epoch in range(max_episode):
        df = market.get_data()
        account = Account(df, 50000000)
        # account.back_up()
        account.reset()
        count = 0
        correct = 0
        last_chart = None
        while last_chart == None:
            last_chart = get_chart(market, data_size, data_size)

        for idx, _ in enumerate(df.index, start=data_size + 1):
            try:
                since = time.time()
                curr_chart = get_chart(market, idx, data_size)
                if curr_chart is None:
                    continue

                state = curr_chart - last_chart
                curr_chart = last_chart
                dqn_action = policy_net(curr_chart)
                _,dqn_action = torch.max(dqn_action,1)
                dqn_action = dqn_action.cpu().numpy()[0]
                plc_action = select_action(df, idx)
                count += 1
                correct += 1 if dqn_action == plc_action else 0
                
                print("idx:%d==>dqn_action:%d:%d, price:%.2f, match:%.2f"%(idx, dqn_action, plc_action, df.loc[idx, 'close'], correct * 100/count))

                reward, real_action = account.exec_action(2 if dqn_action == (action_kind - 1) else (1 if dqn_action == 0 else 0) , idx)
                reward = torch.tensor([reward], device=device) 
                real_action = torch.tensor([[real_action]], device=device, dtype=torch.long)
                last_chart = curr_chart
                
                if account.is_bankrupt():
                    break
                spend = time.time() - since
                print("idx:%d price [%.4f] unit[%.4f] used time[%.2f] agent rate:%.05f remind money:%.02f" 
                        % (idx, df.loc[idx, 'close'], account.unit, spend, account.rate, account.balance + account.unit * df.loc[idx, 'close']))
            except Exception as ex:
                exc_type, exc_obj, exc_tb = sys.exc_info()
                fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
                print("`epoch loop -> exception! %s : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

        # torch.save(policy_net.state_dict(),"pt/policy_net.pt")
        # print("epoch[%d] epsode is next loss[%.05f]" % (epoch, loss.item()))
        if path.exists("pt/train_net_{}.pt".format(device_str)) and epoch % 100:
            policy_net.load_state_dict(torch.load("pt/train_net_{}.pt".format(device_str)))
            policy_net.eval()

        # if loss.item() < 0.00001 : 
        #     break
    
    print('Complete')

if __name__ == "__main__":
    main()

오늘은 강화학습으로 pt파일이 만들어졌다면 그것을 이용하여 실제 매매이전에 back test를 진행하는 main_agent.py에 대하여 말씀 드리겠습니다.
get_chart() 함수를 이용하여 torch.tensor로 변환된 PIL 이미지를 받습니다.
최초 실행시 0번 이미지를 미리 받아 두고 0번 이미지와 1번 이미지의 차를 구합니다.
이것이 강화학습에서 말하는 상태 S1이 됩니다.
시간 t1에 대한 S1이 생성되면 이것을 DQN에 넣고 행동 a1을 받습니다.
a1은 0,1,2 중 하나의 값입니다.
0은 관망 1은 매수 2는 매도를 하게 됩니다.
앞 장에서 다룬 내용중 Markget.get_data()함수의 데이터 갯수를 줄이거나 늘려서 매매 횟수를 변경할 수 있으며 epsilone 값을 변경 하면 매매프로그램이 얼마나 자주 모험적인 선택을 할지를 결정 할 수 있습니다.

학습에 사용된 pt/train_net.pt 파일은 학습 및 응용과 지금 main_agent.py에서도 공유됩니다.
공유된 DQN의 하이퍼 파라메트에 의해서 main_agent.py는 훈련 없이 바로 detection이 가능합니다.
실제 실행 되는 상태를 보고 어느 정도의 수익이 발생하는 지를 과거의 데이터로 예상할 수 있습니다.
여기 발견된 오 작동이나 문제점은 main_buroto.py 파일의 recall_history()함수에서 반영되어 새로운 history Series 를 생성하여야 합니다.
새로 생성된 history를 이용하여 학습을 진행 하여 main_agent.py를 실행하면 학습된 내용은 자동으로 반영이 되겠죠.

학습 데이터는 작은 량에서 점점 큰 량으로 변화 시키면서 과적합이 발생하는지 확인 합니다.
학습이 정상적으로 이루어 지면 다시 main_agent.py를 실행하여 detection을 진행한다.
과거의 데이터를 이용하지만 DQN은 학습된 Q(s,a)함수를 실행하는 것이지 과거의 학습된 내용을 바탕으로 한 데이터를 따라 가는 것이 아니다.
실제 history내용과 비교 해 보면 그것을 알 수 있다.
아래 그림은 왼쪽은 과거 데이터를 이용하여 학습을 진행하고 학습된 모델을 이용하여 agent가 실행되는 모습을 담은 사진입니다.
agent가 6%대의 수익을 내고 있으며 학습하는 프로세스가 loss가 적어질 수 록 agent의 수익률은 조금식 증가합니다.
다음 시간에는 실전 매매 프로그램에 DQN 학습 모델을 연동하여 실시간에서는 어떤 동작을 하는지 알아 보겠습니다.

현재 학습이 진행되어 loss가 0.0221정도로 줄어든 로그입니다.

main_agent.py를 실행하면 학습된 training_cuda.pt파일을 읽어서 매매를 실행하여 테스트를 진행하는 사진입니다.
수익률은 44.81%정도 됩니다.
차트를 그리는 시간이 2초 이상걸려서 그리 빠르지는 않습니다.
하루에 약30000건을 테스트 중입니다.
개인 PC사양에 영향을 받을 것 같습니다.

반응형
LIST

맨위로버튼이미지

 

지난차의 CNN+RNN모델에서 변경 가능한 파라메트는 hidden_dim값 일것입니다. 여기서 궁금점이 하나 생기는 것이 CNN+RNN,CNN+ISTM,CNN+GRU의 성능의 차이가 있을까? 하는것 입니다.

import torch
import torch.nn as nn
import torch.nn.functional as F

class CNNRNN2(nn.Module):
    def __init__(self, device, h, w, embedding_size, outputs, hdnsize, num_layer):
        super(CNNRNN2, self).__init__()
        self.device = device
        self.hidden_size = hdnsize
        self.conv1 = nn.Conv2d(4, h*w, kernel_size=5, stride=2)
        self.bn1 = nn.BatchNorm2d(h*w)
        self.conv2 = nn.Conv2d(h*w, hdnsize, kernel_size=5, stride=2)
        self.bn2 = nn.BatchNorm2d(hdnsize)
        self.conv3 = nn.Conv2d(hdnsize, hdnsize, kernel_size=5, stride=2)
        self.bn3 = nn.BatchNorm2d(hdnsize)
        self.embedding_size = embedding_size
        self.num_layer = num_layer

        self.encoder = nn.Embedding(54 * hdnsize, embedding_size)
        self.rnn = nn.RNN(embedding_size, hdnsize, num_layer)
        self.decoder = nn.Linear(hdnsize, outputs)
        
    def init_hidden(self):
        return torch.zeros(1, self.hidden_size)
    
    def forward(self, x):
        x = x.to(self.device)
        x = F.relu(self.bn1(self.conv1(x)))
        x = F.relu(self.bn2(self.conv2(x)))
        x = F.relu(self.bn3(self.conv3(x)))
        hidden = self.init_hidden().to(self.device)
        x, hidden = self.rnn(x, hidden)
        x = self.decoder(x.view(8, -1))
        
        return x

embedding을 포함한 cnn+rnn 모델 cnnrnn2.py 입니다.

import torch
import torch.nn as nn
import torch.nn.functional as F

class CNNRNN2(nn.Module):
    def __init__(self, device, h, w, embedding_size, outputs, hdnsize, num_layer):
        super(CNNRNN2, self).__init__()
        self.device = device
        self.hidden_size = hdnsize
        self.conv1 = nn.Conv2d(4, h*w, kernel_size=5, stride=2)
        self.bn1 = nn.BatchNorm2d(h*w)
        self.conv2 = nn.Conv2d(h*w, hdnsize, kernel_size=5, stride=2)
        self.bn2 = nn.BatchNorm2d(hdnsize)
        self.conv3 = nn.Conv2d(hdnsize, hdnsize, kernel_size=5, stride=2)
        self.bn3 = nn.BatchNorm2d(hdnsize)
        self.embedding_size = embedding_size
        self.num_layer = num_layer

        self.encoder = nn.Embedding(126 * hdnsize, embedding_size)
        self.rnn = nn.LSTM(embedding_size, hdnsize, num_layer)
        self.decoder = nn.Linear(hdnsize, outputs)
        
    def init_hidden(self):
        hidden = torch.zeros(num_layer, 8, self.hidden_size) #8 = batch_size
        cell = torch.zeros(num_layer, 8, self.hidden_size)   #8 = batch_size
        return hidden, cell
    
    def forward(self, x):
        x = x.to(self.device)
        x = F.relu(self.bn1(self.conv1(x)))
        x = F.relu(self.bn2(self.conv2(x)))
        x = F.relu(self.bn3(self.conv3(x)))
        hidden = self.init_hidden().to(self.device)
        x, (hidden, cell) = self.rnn(x, (hidden, cell))
        x = self.decoder(x.view(8, -1))
        
        return x

embedding을 포함한 cnn+lstm모델인 cnnlstm.py입니다.

import torch
import torch.nn as nn
import torch.nn.functional as F

class CNNRNN2(nn.Module):
    def __init__(self, device, h, w, embedding_size, outputs, hdnsize, num_layer):
        super(CNNRNN2, self).__init__()
        self.device = device
        self.hidden_size = hdnsize
        self.conv1 = nn.Conv2d(4, h*w, kernel_size=5, stride=2)
        self.bn1 = nn.BatchNorm2d(h*w)
        self.conv2 = nn.Conv2d(h*w, hdnsize, kernel_size=5, stride=2)
        self.bn2 = nn.BatchNorm2d(hdnsize)
        self.conv3 = nn.Conv2d(hdnsize, hdnsize, kernel_size=5, stride=2)
        self.bn3 = nn.BatchNorm2d(hdnsize)
        self.embedding_size = embedding_size
        self.num_layer = num_layer

        self.encoder = nn.Embedding(126 * hdnsize, embedding_size)
        self.rnn = nn.GRU(embedding_size, hdnsize, num_layer)
        self.decoder = nn.Linear(hdnsize, outputs)
        
    def init_hidden(self):
        return torch.zeros(1, self.hidden_size)
    
    def forward(self, x):
        x = x.to(self.device)
        x = F.relu(self.bn1(self.conv1(x)))
        x = F.relu(self.bn2(self.conv2(x)))
        x = F.relu(self.bn3(self.conv3(x)))
        hidden = self.init_hidden().to(self.device)
        x, hidden = self.rnn(x, hidden)
        x = self.decoder(x.view(8, -1))
        
        return x

embedding을 포함한 cnn+gru모델인 cnngru.py입니다.

import random
from collections import deque, namedtuple
from IPython.display import display, Math
from Account import Account
import math
from itertools import count
import os

import sys
import os.path as path
import plotly as plt
import torch
import torch.nn as nn
import torch.optim as optim
from Market import Market
import torchvision
import time
from cnnrnn2 import CNNRNN2
from memory import ReplayMemory, Experience
# from transformers import get_cosine_schedule_with_warmup
import transformers

action_kind = 3
max_episode = 5000
screen_height = 100
screen_width  = 140
data_size = 250
visit_cnt = [0] * action_kind
# replay_buffer = deque()
epsilon = 0.3
dis = 0.9

BATCH_SIZE = 8
# GAMMA = 0.999
GAMMA = 0.99
EPS_START = 0.9
EPS_END = 0.05
EPS_DECAY = 200
TARGET_UPDATE = 10
steps_done = 0
loss = any
WINDOW_START = 0
WINDOW_SIZE  = 1500

# for i in range(torch.cuda.device_count()):
#     print(torch.cuda.get_device_name(i))
device_str = "cuda"
device = torch.device(device_str)
    
memory = ReplayMemory(BATCH_SIZE)
converter = torchvision.transforms.ToTensor()
market = Market()
train_net = CNNRNN2(device, screen_height, screen_width, 10, action_kind, 32, 2).to(device)
train_net = nn.DataParallel(train_net, device_ids=[0,1]).to(device)

episode_durations = []
optimizer = optim.RMSprop(train_net.parameters(), lr=0.0001, eps=0.00000001)
# scheduler = get_cosine_schedule_with_warmup(optimizer, 5, base_lr=0.3, final_lr=0.01)
scheduler = transformers.get_cosine_schedule_with_warmup(optimizer, 
                                                         num_warmup_steps=5, 
                                                         num_training_steps=25)
def optimize_action(memory):
    if len(memory) < BATCH_SIZE:
        return None
    optimizer.zero_grad()
    epsode = memory.pop(BATCH_SIZE)
    batch = Experience(*zip(*epsode))
    state_batch = torch.cat(batch.state)
    action_batch = torch.cat(batch.action)
    state_action_values = train_net(state_batch).gather(1, action_batch)
    # optimizer = optim.RMSprop(train_net.parameters(), 0.01)
    
    criterion = nn.SmoothL1Loss()
    loss = criterion(state_action_values, action_batch)

    # Optimize the model
    # optimizer.zero_grad()
    loss.backward()
    for param in train_net.parameters():
        param.grad.data.clamp_(-1, 1)
    optimizer.step()
    return loss

# def select_action(df, idx):
#     try:
#         global steps_done
#         sample = random.random()
#         eps_threshold = EPS_END + (EPS_START - EPS_END) * math.exp(-1. * steps_done / EPS_DECAY)
#         steps_done += 1
#         if sample > eps_threshold:
#             if df.loc[idx, "closemax"] == df.loc[idx, "close"]:
#                 action = 2
#                 return  action
#             elif df.loc[idx, "closemin"] == df.loc[idx, "close"]:
#                 action = 1
#                 return  action
#             else:
#                 return 0
#         else:
#             action = random.randrange(action_kind)
#             return  action
#     except Exception as ex:
#         exc_type, exc_obj, exc_tb = sys.exc_info()
#         fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
#         print("`select_action -> exception! %s : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))
#         return 0
def select_action(df, idx):
    try:
        if df.loc[idx, "closemax"] == df.loc[idx, "close"]:
            action = 2
            return  action
        elif df.loc[idx, "closemin"] == df.loc[idx, "close"]:
            action = 1
            return  action
        else:
            return 0
    except Exception as ex:
        exc_type, exc_obj, exc_tb = sys.exc_info()
        fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
        print("`select_action -> exception! %s : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))
        return 0
        
def get_chart(market, idx, max_data):
    img = market.get_chart(idx, max_data=max_data)
    if img is None:
        return None
    # img = Image.fromarray(np.uint8(cm.gist_earth(plt.io.to_image(fig, format='png')*255)))
    # im = Image.fromarray(img, bytes=True)
    # im = Image.fromarray(np.uint8(cm.gist_earth(img))/255)
    # im = Image.fromarray(np.uint8(img)/255)
    # img = img.resize((700, 500), resample=Image.BICUBIC)
    # img = Image.fromarray(cm.gist_earth(plt.io.to_image(fig, format='png'), bytes=True))
    # display(img)
    # chart = converter(img).unsqueeze(0).to(device)
    chart = converter(img).unsqueeze(0)
    return chart

def plot_durations(last_chart, curr_chart):
    plt.figure()
    # plt.subplot(1,2,1)
    img = plt.imshow(last_chart.cpu().squeeze(0).permute(1, 2, 0).numpy(), interpolation='none')
    plt.title('Example extracted screen')
    plt.figure(2)
    # plt.subplot(1,2,2)
    plt.clf()
    durations_t = torch.tensor(episode_durations, dtype=torch.float)
    plt.title('Training...')
    plt.xlabel('Episode')
    plt.ylabel('Duration')
    plt.plot(durations_t.numpy())
    # plt.show()
    # Take 100 episode averages and plot them too
    if len(durations_t) >= 100:
        means = durations_t.unfold(0, 100, 1).mean(1).view(-1)
        means = torch.cat((torch.zeros(99), means))
        plt.plot(means.numpy())

    img.set_data(curr_chart.cpu().squeeze(0).permute(1, 2, 0).numpy())
    plt.pause(0.01)  # pause a bit so that plots are updated
    display.clear_output(wait=True)
    display.display(plt.gcf())
            
def main():
    if path.exists("pt/train_cnnrnn2_{}.pt".format(device_str)):
        train_net.load_state_dict(torch.load("pt/train_cnnrnn2_{}.pt".format(device_str)))
    train_net.train()
        
    for _ in range(10):    
        df = market.get_data()
        # df = df.head(WINDOW_SIZE)

        for epoch in range(max_episode):
            account = Account(df, 50000000)
            account.reset()
            for idx,_ in enumerate(df.index, start=data_size):
                try:
                    since = time.time()
                    curr_chart = get_chart(market, idx, data_size)
                    reward = 0
                    num_action = select_action(df, idx)
                    reward, real_action = account.exec_action(num_action, idx)
                    print("idx:%d==>action:%d, price:%.2f"%(idx, num_action, df.loc[idx, 'close']))
                    reward = torch.tensor([reward], device=device)
                    action = torch.tensor([[num_action]], device=device, dtype=torch.int64)
                                    
                    memory.push(curr_chart, action, reward)
                    while len(memory) >= BATCH_SIZE:
                        optimizer.zero_grad()
                        epsode = memory.pop(BATCH_SIZE)
                        batch = Experience(*zip(*epsode))
                        state_batch = torch.cat(batch.state)
                        action_batch = torch.cat(batch.action)
                        state_action_values = train_net(state_batch).gather(1, action_batch)
                        # optimizer = optim.RMSprop(train_net.parameters(), 0.01)
                        
                        criterion = nn.SmoothL1Loss()
                        loss = criterion(state_action_values, action_batch)

                        # Optimize the model
                        # optimizer.zero_grad()
                        loss.backward()
                        for param in train_net.parameters():
                            param.grad.data.clamp_(-1, 1)
                        optimizer.step()
                        if loss is not None:
                            print("epoch[%d:%d] epsode is next loss[%.10f]" % (epoch, idx, loss.item()))

                    if idx % TARGET_UPDATE == 0:
                        torch.save(train_net.state_dict(),"pt/train_cnnrnn2_{}.pt".format(device_str))
                    
                    spend = time.time() - since
                    print("idx:%d price [%.4f] unit[%.4f] used time[%.2f] agent rate:%.05f remind money:%.02f" 
                            % (idx, df.loc[idx, 'close'], account.unit, spend, account.rate, account.balance + account.unit * df.loc[idx, 'close']))
                    if account.is_bankrupt():
                        break
                    if idx == df.index.max():
                        break
                except Exception as ex:
                    exc_type, exc_obj, exc_tb = sys.exc_info()
                    fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
                    print("`recall_training -> exception! %s : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))
            scheduler.step()

        print("end training DQN")
    
    print('Complete Training')

if __name__ == "__main__":
    main()

위의 3가지 모델을 학습하기 위한 학습 파일인 main_cnnrnn2.py 입니다. import 부분만 변경하여 훈련이 가능 할 것으로 예상됩니다.

대표이미지 출처:https://dgkim5360.tistory.com/entry/understanding-long-short-term-memory-lstm-kr

 

Long Short-Term Memory (LSTM) 이해하기

이 글은 Christopher Olah가 2015년 8월에 쓴 글을 우리 말로 번역한 것이다. Recurrent neural network의 개념을 쉽게 설명했고, 그 중 획기적인 모델인 LSTM을 이론적으로 이해할 수 있도록 좋은 그림과 함께

dgkim5360.tistory.com

 

반응형
LIST

맨위로버튼이미지

현재 강화학습 모델은 과거 데이터로 히스토리를 만든 다음 주가차트와 보조지표가 다 있는 이미지로 env의 상태 tensor를 만들어 프로세싱한 모델이 강제로 3가지로 라벨링을 하기 때문에 비슷한 결과에도 어떤때는 보유 또는 관망이 되고 어떤때는 매매가 되므로 optimizer가 하이퍼 파라메트를 결정하기가 쉽지 않게 됩니다. 그래서 loss가 충분히 소멸하지 않고 계속 널뛰기를 하는 현상이 있습니다. DQN 내부의 CNN의 outputs값은 분류 총 값이지만 실제 비지도 학습으로 분류된 값은 약 500~800개 정도로 넓은 범위를 가지고 있습니다. 이것을 최종 nn.Linear함수를 거치면서 모든 값이 소멸하고 3가지로 축소 됩니다. 만약에 차트가 조금 상승할때 팔고 조금하락할때 사고를 반복하면 수익률은 점점 가파르게 감소하여 잔고가 0이 되게 됩니다. 실제 히스토리 함수가 매매를 자주 하지 않는 이유는 작은 변동에 매매가 발생할 경우 수익도 크지 않을뿐더러 낙폭이 크지는 경우 큰 손실이 발생한 다음은 손실을 복구하지 못하는 상황이 발생하게 됩니다. 그래서 매매를 자주 하지 않게 설계되어 있습니다. 이 부분은 실제로는 수학적 손실,손익과는 논리적으로 맞지 않기 때문에 이상적인 목표를 가진 CNN의 입장에서는 모두 loss로 인식이 되게 됩니다. 이 문제를 극복하기 위해서는 CNN자체를 upgrade하는 방법과 CNN과 RNN을 병합하는 방법을 연구해야 합니다. 저도 어느게 맞다고 말할 수 없습니다. 아직도 강화학습은 연구 대상이지 실용단계로 보기는 힘듭니다. RNN이란 말을 들어시고 뭔가 이상함을 느꼈다면 제가 앞에서 이야기한 인간이 개입한 데이터는 시계열 데이터로 보기 힘들다는 이야기 때문일 겁니다. 그러나 RNN은 주가분석같은 시계열 데이터 분석뿐만 아니라 자연어 학습에도 사용됩니다. 우리가 연구 중인 주식이나 코인의 매매 차트를 분류하면 조금 전에 말씀드린것처럼 약 800개의 패턴이 발생합니다. 그 이상을 넘어 가지는 않습니다. RNN 자연어 분석 예제를 보시면 입력값이 입력 글짜 수(여기에 차트의 비지도 학습 분류 패턴 수를 입력),hidden layer 갯수, 출력 글짜 수(여기에 매매프로그램이 사용할 행동갯수를 입력) 입니다. 이 부분을 잘 응용하면 CNN의 출력수(비지도 분류값 약 800개)를 RNN 의 입력값으로 hidden값은 init_hidden함수로 자동 계산되고 출력값은 우리가 구하려는 3가지(매도,매수,관망)일겁니다. 여기에 임베딩(GRU를 사용할 경우 encoding을 사용하여 자연어 분석시 학습률을 높일 수 있음)을 더하면 조금 더 복잡해지겠지만 단순히 차트만으로 분류하는게 아닌 순서적 흐름을 이용한 분류가 가능해 질것 입니다. 대부분의 값은 관망값을 가져야 합니다. 그렇다면 히든값은 무엇을 의미할까요? RNN에 들어갈 첫번째 값이 있다면 이 값은 시간적인 값t0의 값을 가질겁니다. 히든값은 그 다음에 올 수 있는 수 많은 가능성의 t1값의 tensor일 것입니다. 두 값의 연산에 의해서 t1이 결정되면 hidden은 가능성의값 t2의 텐서가 될것이고 그로 인해 실제 t2가 결정될것 입니다. 학습이 진행되면 정확도는 증가할 것으로 예상됩니다.

import torch
import torch.nn as nn
import torch.nn.functional as F

class CNNRNN(nn.Module):
    def __init__(self, device, h, w, outputs, hdnsize):
        super(CNNRNN, self).__init__()
        self.device = device
        self.hidden_size = hdnsize
        self.conv1 = nn.Conv2d(4, h*w, kernel_size=5, stride=2)
        self.bn1 = nn.BatchNorm2d(h*w)
        self.conv2 = nn.Conv2d(h*w, hdnsize, kernel_size=5, stride=2)
        self.bn2 = nn.BatchNorm2d(hdnsize)
        self.conv3 = nn.Conv2d(hdnsize, hdnsize, kernel_size=5, stride=2)
        self.bn3 = nn.BatchNorm2d(hdnsize)
        
        self.i2h = nn.Linear(54 * hdnsize, hdnsize)
        self.h2h = nn.Linear(hdnsize, hdnsize)
        self.i2o = nn.Linear(hdnsize, outputs)
        self.act_fn = nn.Tanh()

    def init_hidden(self):
        return torch.zeros(1, self.hidden_size)
    
    def forward(self, x):
        x = x.to(self.device)
        x = F.relu(self.bn1(self.conv1(x)))
        x = F.relu(self.bn2(self.conv2(x)))
        x = F.relu(self.bn3(self.conv3(x)))
        hidden = self.init_hidden().to(self.device)
        x= self.i2h(x.view(x.size(0), -1))
        hidden = self.h2h(hidden)
        hidden = self.act_fn(x + hidden)
        return self.i2o(hidden)

cnnrnn.py 입니다.

import random
from collections import deque, namedtuple
from IPython.display import display, Math
from Account import Account
import math
from itertools import count
import os

import sys
import os.path as path
import plotly as plt
import torch
import torch.nn as nn
import torch.optim as optim
from Market import Market
import torchvision
import time
from cnnrnn import CNNRNN
from memory import ReplayMemory, Experience

action_kind = 3
max_episode = 5000
screen_height = 100
screen_width  = 140
data_size = 250
visit_cnt = [0] * action_kind
# replay_buffer = deque()
epsilon = 0.3
dis = 0.9

BATCH_SIZE = 8
# GAMMA = 0.999
GAMMA = 0.99
EPS_START = 0.9
EPS_END = 0.05
EPS_DECAY = 200
TARGET_UPDATE = 10
steps_done = 0
loss = any
WINDOW_START = 0
WINDOW_SIZE  = 1500

# for i in range(torch.cuda.device_count()):
#     print(torch.cuda.get_device_name(i))
device_str = "cuda"
device = torch.device(device_str)
    
memory = ReplayMemory(BATCH_SIZE)
converter = torchvision.transforms.ToTensor()
market = Market()
train_net = CNNRNN(device, screen_height, screen_width, action_kind, 32).to(device)
train_net = nn.DataParallel(train_net, device_ids=[0,1]).to(device)

episode_durations = []
    
optimizer = optim.RMSprop(train_net.parameters())

def optimize_action(memory):
    if len(memory) < BATCH_SIZE:
        return None
    epsode = memory.pop(BATCH_SIZE)
    batch = Experience(*zip(*epsode))
    state_batch = torch.cat(batch.state)
    action_batch = torch.cat(batch.action)
    train_net.train()
    state_action_values = train_net(state_batch).gather(1, action_batch)
    
    criterion = nn.SmoothL1Loss()
    loss = criterion(state_action_values, action_batch)

    # Optimize the model
    optimizer.zero_grad()
    loss.backward()
    for param in train_net.parameters():
        param.grad.data.clamp_(-1, 1)
    optimizer.step()
    return loss

def select_action(df, idx):
    try:
        global steps_done
        sample = random.random()
        eps_threshold = EPS_END + (EPS_START - EPS_END) * math.exp(-1. * steps_done / EPS_DECAY)
        steps_done += 1
        if sample > eps_threshold:
            if df.loc[idx, "closemax"] == df.loc[idx, "close"]:
                action = 2
                return  action
            elif df.loc[idx, "closemin"] == df.loc[idx, "close"]:
                action = 1
                return  action
            else:
                return 0
        else:
            action = random.randrange(action_kind)
            return  action
    except Exception as ex:
        exc_type, exc_obj, exc_tb = sys.exc_info()
        fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
        print("`select_action -> exception! %s : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))
        return 0
        
def get_chart(market, idx, max_data):
    img = market.get_chart(idx, max_data=max_data)
    if img is None:
        return None
    # img = Image.fromarray(np.uint8(cm.gist_earth(plt.io.to_image(fig, format='png')*255)))
    # im = Image.fromarray(img, bytes=True)
    # im = Image.fromarray(np.uint8(cm.gist_earth(img))/255)
    # im = Image.fromarray(np.uint8(img)/255)
    # img = img.resize((700, 500), resample=Image.BICUBIC)
    # img = Image.fromarray(cm.gist_earth(plt.io.to_image(fig, format='png'), bytes=True))
    # display(img)
    chart = converter(img).unsqueeze(0).to(device)
    return chart

def plot_durations(last_chart, curr_chart):
    plt.figure()
    # plt.subplot(1,2,1)
    img = plt.imshow(last_chart.cpu().squeeze(0).permute(1, 2, 0).numpy(), interpolation='none')
    plt.title('Example extracted screen')
    plt.figure(2)
    # plt.subplot(1,2,2)
    plt.clf()
    durations_t = torch.tensor(episode_durations, dtype=torch.float)
    plt.title('Training...')
    plt.xlabel('Episode')
    plt.ylabel('Duration')
    plt.plot(durations_t.numpy())
    # plt.show()
    # Take 100 episode averages and plot them too
    if len(durations_t) >= 100:
        means = durations_t.unfold(0, 100, 1).mean(1).view(-1)
        means = torch.cat((torch.zeros(99), means))
        plt.plot(means.numpy())

    img.set_data(curr_chart.cpu().squeeze(0).permute(1, 2, 0).numpy())
    plt.pause(0.01)  # pause a bit so that plots are updated
    display.clear_output(wait=True)
    display.display(plt.gcf())
            
def main():
    if path.exists("pt/train_cnnrnn_{}.pt".format(device_str)):
        train_net.load_state_dict(torch.load("pt/train_cnnrnn_{}.pt".format(device_str)))
        
    for _ in range(10):    
        df = market.get_data()
        # df = df.head(WINDOW_SIZE)

        for epoch in range(max_episode):
            account = Account(df, 50000000)
            account.reset()

            last_chart = get_chart(market, data_size, data_size)
            account.reset()

            for idx,_ in enumerate(df.index, start=(data_size + 1)):
                try:
                    since = time.time()
                    curr_chart = get_chart(market, idx, data_size)
                    if curr_chart is not None:
                        state = curr_chart - last_chart
                    else:
                        continue
                    last_chart = curr_chart
                    reward = 0
                    num_action = select_action(df, idx)
                    reward, real_action = account.exec_action(num_action, idx)
                    print("idx:%d==>action:%d, price:%.2f"%(idx, num_action, df.loc[idx, 'close']))
                    reward = torch.tensor([reward], device=device)
                    action = torch.tensor([[num_action]], device=device, dtype=torch.int64)
                                    
                    memory.push(state, action, reward)
                    while len(memory) >= BATCH_SIZE:
                        loss = optimize_action(memory)
                        if loss is not None:
                            print("epoch[%d:%d] epsode is next loss[%.10f]" % (epoch, idx, loss.item()))

                    if idx % TARGET_UPDATE == 0:
                        torch.save(train_net.state_dict(),"pt/train_cnnrnn_{}.pt".format(device_str))
                    
                    spend = time.time() - since
                    print("idx:%d price [%.4f] unit[%.4f] used time[%.2f] agent rate:%.05f remind money:%.02f" 
                            % (idx, df.loc[idx, 'close'], account.unit, spend, account.rate, account.balance + account.unit * df.loc[idx, 'close']))
                    if account.is_bankrupt():
                        break
                    if idx == df.index.max():
                        break
                except Exception as ex:
                    exc_type, exc_obj, exc_tb = sys.exc_info()
                    fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
                    print("`recall_training -> exception! %s : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

        print("end training DQN")
    
    print('Complete Training')

if __name__ == "__main__":
    main()

main_cnnrnn.py 입니다.
CNN 단독 모델과 CNN + RNN 모델의 학습 증가율을 비교해 보시기 바랍니다.

대표이미지출처:https://blog.kakaocdn.net/dna/IFp6A/btqASygMYCA/AAAAAAAAAAAAAAAAAAAAAHPRKM8z8_i1rJvLfrqQ8vMKOITsjcC7Q0zi2JxkpFQQ/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1759244399&allow_ip=&allow_referer=&signature=mFEIH8jDzqhtbOVg9uF%2FFSmH2NY%3D
RNN 소스코드 출처 : 책 파이토치 첫걸음에서 발췌 및 응용

 

반응형
LIST

맨위로버튼이미지

import random
from collections import deque, namedtuple
from typing import Dict, List, NamedTuple, Optional, Text, Tuple, Union
# from IPython.display import display, Math
from Account import Account
import math
from itertools import count
import os

import sys
import os.path as path
import numpy as np
import plotly as plt
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torchvision.transforms as T
from Market import Market
from PIL import Image
from plotly import express as px
import torchvision
import time
import joblib
import pandas as pd
from dqn import DQN
from memory import ReplayMemory, Experience

action_kind = 21
max_episode = 5000
screen_height = 100
screen_width  = 140
data_size = 600
epsilon = 0.3
dis = 0.9

BATCH_SIZE = 8
# GAMMA = 0.999
GAMMA = 0.99
EPS_START = 0.9
EPS_END = 0.05
EPS_DECAY = 200
TARGET_UPDATE = 10
steps_done = 0
loss = any
WINDOW_START = 0
WINDOW_SIZE  = 1500

# for i in range(torch.cuda.device_count()):
#     print(torch.cuda.get_device_name(i))

# if gpu is to be used
# device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# device = torch.device("cuda")
# device_str = "cpu"
device_str = "cuda"
device = torch.device(device_str)
    
memory = ReplayMemory(BATCH_SIZE)
converter = torchvision.transforms.ToTensor()
market = Market()
train_net = DQN(device, screen_height, screen_width, action_kind, 32).to(device)
train_net = nn.DataParallel(train_net, device_ids=[0,1]).to(device)

episode_durations = []
    
optimizer = optim.RMSprop(train_net.parameters(), lr=0.000001)

# def select_action(df, idx):
#     try:
#         global steps_done
#         sample = random.random()
#         eps_threshold = EPS_END + (EPS_START - EPS_END) * math.exp(-1. * steps_done / EPS_DECAY)
#         steps_done += 1
#         clslvl = ((df.loc[idx, "close"] - df.loc[idx, "closemin"]) * 40 // (df.loc[idx, "closemax"] - df.loc[idx, "closemin"]))
#         if sample > eps_threshold:
#             if df.loc[idx, "closemax"] == df.loc[idx, "close"]:
#                 action = clslvl
#                 return  action
#             elif df.loc[idx, "closemin"] == df.loc[idx, "close"]:
#                 action = clslvl
#                 return  action
#             else:
#                 return clslvl
#         else:
#             action = random.randrange(action_kind)
#             return  action
#     except Exception as ex:
#         exc_type, exc_obj, exc_tb = sys.exc_info()
#         fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
#         print("`select_action -> exception! %s : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))
#         return 0

def select_action(df, idx):
    return ((df.loc[idx, "close"] - df.loc[idx, "closemin"]) * (action_kind-1) // (df.loc[idx, "closemax"] - df.loc[idx, "closemin"]))
        
def get_chart(market, idx, max_data):
    img = market.get_chart(idx, max_data=max_data)
    if img is None:
        return None
    # if  idx > (df.index.max() - data_size - 1):
    #     return None
    # img = Image.fromarray(np.uint8(cm.gist_earth(plt.io.to_image(fig, format='png')*255)))
    # im = Image.fromarray(img, bytes=True)
    # im = Image.fromarray(np.uint8(cm.gist_earth(img))/255)
    # im = Image.fromarray(np.uint8(img)/255)
    # img = img.resize((700, 500), resample=Image.BICUBIC)
    # display(img)
    # img = Image.fromarray(cm.gist_earth(plt.io.to_image(fig, format='png'), bytes=True))
    chart = converter(img).unsqueeze(0)
    return chart

def plot_durations(last_chart, curr_chart):
    plt.figure()
    # plt.subplot(1,2,1)
    img = plt.imshow(last_chart.cpu().squeeze(0).permute(1, 2, 0).numpy(), interpolation='none')
    plt.title('Example extracted screen')
    plt.figure(2)
    # plt.subplot(1,2,2)
    plt.clf()
    durations_t = torch.tensor(episode_durations, dtype=torch.float)
    plt.title('Training...')
    plt.xlabel('Episode')
    plt.ylabel('Duration')
    plt.plot(durations_t.numpy())
    # plt.show()
    # Take 100 episode averages and plot them too
    if len(durations_t) >= 100:
        means = durations_t.unfold(0, 100, 1).mean(1).view(-1)
        means = torch.cat((torch.zeros(99), means))
        plt.plot(means.numpy())

    img.set_data(curr_chart.cpu().squeeze(0).permute(1, 2, 0).numpy())
    plt.pause(0.01)  # pause a bit so that plots are updated
    # display.clear_output(wait=True)
    # display.display(plt.gcf())
            
def main():
    if path.exists("pt/train_net_{}.pt".format(device_str)):
        train_net.load_state_dict(torch.load("pt/train_net_{}.pt".format(device_str)))
        
    for _ in range(10):    
        df = market.get_data()
        # df = df.head(WINDOW_SIZE)

        for epoch in range(max_episode):
            account = Account(df, 50000000)
            account.reset()

            last_chart = get_chart(market, data_size, data_size)
            account.reset()

            for idx,_ in enumerate(df.index, start=(data_size + 1)):
                try:
                    since = time.time()
                    curr_chart = get_chart(market, idx, data_size)
                    if curr_chart is None or last_chart is None:
                        continue
                    else:
                        state = curr_chart - last_chart
                        
                    last_chart = curr_chart
                    reward = 0
                    num_action = select_action(df, idx)
                    reward, real_action = account.exec_action(2 if num_action == (action_kind - 1) else (1 if num_action == 0 else 0) , idx)
                    print("idx:%d==>action:%d,real_action==>%d price:%.2f"%(idx, num_action, real_action, df.loc[idx, 'close']))
                    # reward = torch.tensor([reward], device=device)
                    num_action = torch.tensor([[num_action]], device=device, dtype=torch.int64)
                                    
                    memory.push(curr_chart, num_action, reward)
                    if len(memory) >= BATCH_SIZE:
                        epsode = memory.pop(BATCH_SIZE)
                        batch = Experience(*zip(*epsode))
                        # non_final_mask = torch.tensor(tuple(map(lambda s: s is not None,
                        #                                       batch.next_state)), device=device, dtype=torch.bool)
                        # non_final_next_states = torch.cat([s for s in batch.next_state
                        #                                             if s is not None])
                        state_batch = torch.cat(batch.state)
                        action_batch = torch.cat(batch.action)
                        # reward_batch = torch.cat(batch.reward)
                        train_net.train()
                        state_action_values = train_net(state_batch).gather(1, action_batch)
                        # next_state_values = torch.zeros(BATCH_SIZE, device=device)
                        # next_state_values[non_final_mask] = train_net(non_final_next_states).max(1)[0].detach()
                        # expected_state_action_values = (next_state_values * GAMMA) + reward_batch
                        # print(action_batch)
                        # print(state_action_values)
                        optimizer.zero_grad()
                        criterion = nn.SmoothL1Loss()
                        loss = criterion(state_action_values, action_batch)
                        # print(loss)
                        # loss = criterion(state_action_values, action_batch.unsqueeze(1))
                        # loss = criterion(market_status, train_status)

                        # Optimize the model
                        loss.backward()
                        for param in train_net.parameters():
                            param.grad.data.clamp_(-1, 1)
                        optimizer.step()
                        print("epoch[%d:%d] epsode is next loss[%.10f]" % (epoch, idx, loss.item()))

                    if idx % TARGET_UPDATE == 0:
                        torch.save(train_net.state_dict(),"pt/train_net_{}.pt".format(device_str))
                    
                    spend = time.time() - since
                    print("idx:%d price [%.4f] unit[%.4f] used time[%.2f] agent rate:%.05f remind money:%.02f" 
                            % (idx, df.loc[idx, 'close'], account.unit, spend, account.rate, account.balance + account.unit * df.loc[idx, 'close']))
                    if account.is_bankrupt():
                        break
                    if idx == df.index.max():
                        break
                        # train_net.load_state_dict(train_net.state_dict())
                    # save_file_model(degreeQ)                
                except Exception as ex:
                    exc_type, exc_obj, exc_tb = sys.exc_info()
                    fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
                    print("`recall_training -> exception! %s : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

        print("end training DQN")
    
    print('Complete Training')

if __name__ == "__main__":
    main()

main_buroto.py 파일입니다.

import random
from collections import deque, namedtuple

Experience = namedtuple(
    'Experience',
    ('state', 'action', 'reward')
)

class ReplayMemory(object):
    def __init__(self, capacity):
        self.memory = deque([],maxlen=capacity)

    def push(self, *args):
        """Save a transition"""
        self.memory.append(Experience(*args))

    def pop(self, batch_size):
        arr = []
        for _ in range(batch_size):
            arr.append(self.memory.popleft())
        return arr
    
    def sample(self, batch_size):
        return random.sample(self.memory, batch_size)

    def __len__(self):
        return len(self.memory)

memory.py 부분을 분리했습니다.

import torch.nn as nn
import torch.nn.functional as F

class DQN(nn.Module):
    def __init__(self, device, h, w, outputs, qsize):
        super(DQN, self).__init__()
        self.conv1 = nn.Conv2d(4, h*w, kernel_size=5, stride=2)
        self.bn1 = nn.BatchNorm2d(h*w)
        self.conv2 = nn.Conv2d(h*w, qsize, kernel_size=5, stride=2)
        self.bn2 = nn.BatchNorm2d(qsize)
        self.device = device
        self.conv3 = nn.Conv2d(qsize, qsize, kernel_size=5, stride=2)
        self.bn3 = nn.BatchNorm2d(qsize)

        # Number of Linear input connections depends on output of conv2d layers
        # and therefore the input image size, so compute it.
        def conv2d_size_out(size, kernel_size = 5, stride = 2):
            return (size - (kernel_size - 1) - 1) // stride  + 1
        convw = conv2d_size_out(conv2d_size_out(conv2d_size_out(w)))
        convh = conv2d_size_out(conv2d_size_out(conv2d_size_out(h)))
        # print("convw[%d]  convh[%d]" % (convw, convh))
        linear_input_size = 54 * qsize
        self.head = nn.Linear(linear_input_size, outputs)

    # Called with either one element to determine next action, or a batch
    # during optimization. Returns tensor([[left0exp,right0exp]...]).
    def forward(self, x):
        x = x.to(self.device)
        x = F.relu(self.bn1(self.conv1(x)))
        x = F.relu(self.bn2(self.conv2(x)))
        x = F.relu(self.bn3(self.conv3(x)))
        return self.head(x.view(x.size(0), -1))

dqn.py를 분리했습니다.

main함수부터 설명 드리면 가장 이상적인 매매 프로세스인 저점에서 매수하고 고점에서 매도하는 방식을 rolling함수를 이용하여 구하였습니다.
그리고 이것을 전체 데이터에 적용을 하여 history에 저장합니다.
매매 길이에 따라서 수천프로이상의 수익이 발생할 것입니다.
이것은 말 그대로 이상적인 매매 프로세스 입니다.
백프로 매치되는 것은 아니지만 이상적인 grid world의 마르코프 실행 프로세서(MDP) 처럼은 우리도 이것을 MDP로 가정합니다.
이제 실행된 결과를 DQN(CNN)에 학습합니다.
optimize함수가 그 역할을 합니다.
loss함수는 nn.SmoothL1Loss를 사용하여 해당 S의 action a와 기대 이상 행동 a'를 구하여 둘의 차이를 구하여 계산합니다.
아쉽지만 windows에서는 NCCL이 적용되지 않아 pytorch함수에서 에러가 발생합니다.
NCCL은 nvidia의 GPU를 서로 연동하여 최상의 성능을 내는 Api lib입니다.
현재 ubuntu와 red hat에서만 지원하고 있습니다.
그래서 지금 저는 WSL2를 이용한 ubuntu LTS(최종안정화버젼)인 20.04.5버젼으로 이전을 하려고 하는데 windows10과 wsl2의 병행 운영은 엄청난 cpu 제원을 요구 하네요.
어쩔 수 없이 서버를 ubuntu로 다시 설치하고 노트북으로 접속하여 이용하는 방법을 실행하려고 하고 있습니다.
불행히도 제 노트북이 현재 아작이 난 상태라 알리에서 검색해서 LCD판넬을 주문한 상태입니다만 가장 빠른 날짜가 12월 15일 도착이라고 하네요.
Nvidia 홈페이지 상으로는 현재로써는 windows에 NCCL이 적용이 안되어서 여러장의 GPU카드가 있어도 한장의 효과만 있다는 거죠.
현재 소스에서 보시면 nn.DataParallel이 DQN을 감싸고 있습니다.
실제 성능탭으로 보면 그래도 nn.DataParallel을 적용하기 전에는 한개의 GPU만 사용하게 작동이 되었으나 지금은 어느 정도 반반씩 부담하여 사용하는게 보입니다.
중요한 점은

recall_history()함수에서 저장된 데이터를 recall_training 함수에서 DQN을 학습하게 됩니다. 학습된 결과는 pt/training_net.pt파일로 저장을 하게 됩니다. 저의 경우는 데이터가 너무 많은 관계로 과적합 상태를 보이나 로스차이가 그렇게 크지는 않습니다. 적당한 데이터의 크기는 5000~10000개 정도의 데이터를 반복 학습하면서 loss를 최소화 한다음 다시 데이터를 샘플링하고 적용하고 하는 반복 작업을 통하여 실제 DQN이 적은 loss로 동작하는지가 중요합니다. 

이 작업이 어느 정도 끝나면 다음은 main_agent.py 파일을 작성하여 실제 상황에서 detection을 발생하여 거래를 하게 하여 어느 정도의 수익률이 나오는지 분석을 해 보도록 하겠습니다.
저도 처음에는 강화학습이 데이터가 많이 적용되면 더욱 딕텍션이 안정될 거라 생각했지만 실제로는 데이터가 많을 수록 ai는 거래를 하지 않으려하네요.
프로그램을 조금 수정해서 pt파일을 여러 버젼으로 저장하는게 어떨까 제안 합니다.
다음 차에서 agent파일을 돌려 보면 알겠지만 DQN은 가장 안정화된 방법을 선택하려 합니다.
즉 매매를 하지 않아 수익도 손실도 없는 상태를 만들려고 합니다.
그렇지만 어떤 경우 훈련이 잘 진행되어 100%이상의 수익을 내기도 합니다.
아무런 if문 없이 수익을 낸다는 게 신기합니다만 retro.gym을 경험해 보신분들은 아실 겁니다.
사실 retro.gym을 훈련하는 과정에도 인간의 생각 판단 따위는 중요하지 않습니다.
훈련은 우연의 연속입니다.
다만 그 우연들 중에서 가장 스코어가 높은 쪽으로만 AI는 움직이려 합니다.
그렇게 하는것은 인간의 계산에서 epsilon값을 어떻게 판단하는냐에 따라 결과가 달라 질 겁니다.
epsilon을 0.5 정도로 하면 절반은 모험값으로 채워질 겁니다.
그리고 결과를 수익률 또는 남은 돈으로 정할 때 그 값이 최고가 되는 모델들만 history에 저장하고 학습을 돌린다면 엔진은 공격적인 매매를 해서 높은 수익률을 올릴 것입니다.
다음 편 실 agent 시뮬레이션 편을 기대 해 주세요.
 

 

반응형
LIST

맨위로버튼이미지

우리가 우리 생의 모든 부분을 태어날 때부터 죽을 때까지를 다 알고 있다면 우리는 과연 어떤 모습으로 살고 있을 까요?
타임루프 물 애니나 영화에서는 그런 장면이 많이 나옵니다. 자신의 절대 위기나 문제를 이미 경험해 보았기 때문에 모든 문제를 피해 갑니다. 니콜라스 케이지의 2007년 작 넥스트라는 작품이 있습니다. 실제 이 영화는 톰 크루저 주연의 마이너리티 리포트와 같은 소설을 원작으로 하고 있지만 내용은 완전히 다릅니다. 니콜라스 케이지는 한 여자를 운명적으로 좋아하지만 그녀를 만나게 되면 세상은 적어도 미국은 핵폭발로 멸망하게 됩니다. 영화는 니콜라스 케이지의 3분 전을 내다보는 예지 능력의 이야기를 다루지만 2시간 동안의 이야기의 내용은 그래서 아무 일도 안 하고 안 일어났다는 거구 니콜라스 케이지는 몇 번이나 한 여자를 만나서 사랑을 하지만 그녀는 한 번도 니콜라스를 만나지 않았다는 내용의 끝은 좀 허무한 그런 내용입니다. 단 3분 앞을 내다보지만 그 3분만으로 니콜라스 케이지는 아주 쉽게 돈을 벌고 아주 쉽게 FBI를 따 돌리고 세상을 구하지만 결말은 항상 핵폭발이라 그냥 포기하고 혼자 외로운 인생을 산다는 그런 이야기입니다. 우리도 니콜라스 케이지 정도의 예지력은 아니지만 저는 upbit의 이더리움 데이터 1년 6개월치를 가지고 있습니다. Market class의 get_data함수를 보면 아래 부분이 추가되어 있습니다. 주식이던 코인이던 가장 많은 돈을 버는 방법은 가장 고점에서 팔고 가장 저점에서 사고를 계속하면 무조건 돈을 번다는 거죠. python의 rolling함수를 이용하여 앞으로 50 뒤로 50 범위의 수중 가장 높은 값을 미리 구하고 index를 계속 돌리면서 사고팔고를 계속하고 그 중간에는 관망을 하면 어떤 일이 있을 가요? 거의 천문학적인 돈을 벌 수 있습니다. 이런 일이 안 일어나는 이유는 50개의 데이터가 예지의 영역에 있기에 실제로는 가져올 수 없는 값이라는 거죠.

        self.df['closemax'] = self.df.close.rolling(window=100, center=True).max()
        self.df['closemin'] = self.df.close.rolling(window=100, center=True).min()​

위의 내용에서 closemax와 closemin을 미리 계산합니다. 아래 select_action은 제가 말씀 드린 내용으로 매매를 하지만 epsilon값을 구하여 그 값의 하향선에서만 정책적인 매매를 하고 그 이상이 되면 random행동을 한다는 거죠. 예를 들면
섬에 표류한 한 사람이 그 섬에 있는 5가지 열매 중 가장 첫번째 먹어서 죽지 않은 음식을 구출될 때까지 계속 먹게 된다는 이야기와 같은 원리로 머쉰 러닝의 결과도 한 가지 결과 중에서 최고값이 나오면 더 이상은 다른 값을 찾아가지 않습니다. 그래서 프로그래머는 강제로 모험을 하는 확률을 넣어서 모험을 실행하는 계수만큼 모험을 하게 만들어 진짜 최적의 값이 지금 현재 머쉰 러닝이 찾아낸 값인지 계속 질문을 합니다. 아래 함수는 실제 거래에서는 불가능하지만 우리는 이미 거래의 결과를 다 알고 있기 때문에 아래의 방식으로 최고 수익을 내는 방법을 history Series에 저장할 겁니다. 아래는 그 소스입니다. 아래의 행동을 policy action(정책적인 행동)이라고 합니다. 이 부분은 DQN과는 아무 관련이 없으며 인간이 최적의 경로를 찾아가는 MDP(마르코프 실행 프로세스)를 주식이나 코인의 차트에 대입해 본 것입니다. 자 이제는 우리는 최적의 방법으로 미친 수익률을 내는 history를 찾을 수 있습니다. 현실에서는 불가능 하지만 몇천 배의 수익을 발생하게 됩니다. 그야말로 이대로만 되며 누구나 부자가 될 수 있게 죠.

def select_action(df, idx):
    try:
        global steps_done
        sample = random.random()
        eps_threshold = EPS_END + (EPS_START - EPS_END) * math.exp(-1. * steps_done / EPS_DECAY)
        steps_done += 1
        if sample > eps_threshold:
            if df.loc[idx, "closemax"] == df.loc[idx, "close"]:
                action = 2
                return  action
            elif df.loc[idx, "closemin"] == df.loc[idx, "close"]:
                action = 1
                return  action
            else:
                return 0
        else:
            action = random.randrange(action_kind)
            return  action
    except Exception as ex:
        exc_type, exc_obj, exc_tb = sys.exc_info()
        fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
        print("`recall_training -> exception! %s : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))
        return 0
  
  def get_chart(market, df, idx, max_data):
    img = market.get_chart(idx, max_data=max_data)
    if img is None:
        return None
    if  idx > (df.index.max() - 299):
        return None
    # img = Image.fromarray(np.uint8(cm.gist_earth(plt.io.to_image(fig, format='png')*255)))
    # im = Image.fromarray(img, bytes=True)
    # im = Image.fromarray(np.uint8(cm.gist_earth(img))/255)
    # im = Image.fromarray(np.uint8(img)/255)
    # img = img.resize((700, 500), resample=Image.BICUBIC)
    # display(img)
    # img = Image.fromarray(cm.gist_earth(plt.io.to_image(fig, format='png'), bytes=True))
    chart = converter(img).unsqueeze(0).to(device)
    return chart
  
 def recall_history(df, account, idx, re_action):
    max_price = -np.inf
    min_price = np.inf
    max_reward = -np.inf
    max_rew_step = 0
    min_rew_step = 0
    save_point_idx = 0
    account.reset()
        
    for _, idx in enumerate(df.index, start=idx):
        try:
            max_step = idx
            since = time.time()
            if re_action != 0:
                action = re_action
                re_action = 0
            else:
                action = select_action(df, idx)
            
            print("idx:%d==>policy_action:%d"%(idx, action))

            reward, real_action = account.exec_action(action, idx)
            history.loc[idx, "action"] = real_action
            max_reward = reward if reward > max_reward else max_reward
            history.loc[idx, 'max_rew'] = max_reward

            if df.loc[idx, 'close'] > max_price:
                max_price = df.loc[idx, 'close']  
                account.back_up()
                max_rew_step = 0
                save_point_idx = idx
            else:
                max_rew_step += 1
            
            if df.loc[idx, 'close'] < min_price:
                min_price = df.loc[idx, 'close']
                account.back_up()
                min_rew_step = 0
                save_point_idx = idx
            else:
                min_rew_step += 1
            
            # if account.unit > 0 and max_rew_step > 50:
            #     return max_step, True, save_point_idx, 2
            # elif account.unit == 0 and min_rew_step > 50:
            #     return max_step, True, save_point_idx, 1
            
            if account.is_bankrupt():
                break
            spend = time.time() - since
            print("idx:%d used time[%.2f] price[%.2f] unit[%.2f] agent rate:%.05f remind money:%.02f" 
                    % (idx, spend, df.loc[idx, 'close'], account.unit, account.rate, account.balance + account.unit * df.loc[idx, 'close']))
            # if epoch % TARGET_UPDATE == 0:
            #     target_net.load_state_dict(train_net.state_dict())
            # torch.save(train_net.state_dict(),"pt/train_net.pt")
            # save_file_model(degreeQ)                
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            print("`recall_history -> exception! %s : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))
    return max_step, False, 0, 0

다음 차수에는 드디어 DQN(CNN) 학습으로 Q(s, a) 함수를 구현해 보도록 하겠습니다. 이전 차수의 상태 값 Sn이 바로 env입니다. 이미 우리는 env를 구했습니다. 그다음은 env에 해당 행동을 적용 했을때 기대값과 실행 후의 상태 Sn을 구해야 하는 문제가 남게 되는데 그것은 Account 클래스를 통해서 구현 해 보도록 하겠습니다. 프로세스는 2개가 동시에 실행되어야 합니다. 지금 우리가 만들고 있는 프로그램은 제 기준으로 1주일에 1 epoch를 도는 굉장히 느린 프로그램입니다. 돈이 좀 더 있다면 DQN 계산 능력을 향상 시키겠지만 현재로는 한 스텝에 평균 8초 정도 걸립니다. 이 시간은 차트 Cn - C(n-1)을 해서 상태 Sn을 구하고 이것을 CNN으로 학습하고 loss를 구하여 optimize까지 실행한 결과 입니다.
방식은 위의 이상적인 history를 먼저 실행하여 실행 행동의 행렬을 구하고 그 다음은 DQN학습을 진행하는 방식입니다.
이렇게 학습된 모델은 어떤 특정 디렉터리(pt/training_net.pt) 파일로 저장이 되고 학습을 전혀 하지 않고 DQN으로 행동만을 받아서 실행하는 또 다른 프로세스에서 모델 파일을 읽어서 셋업을 한 다음 실 데이터로 거래를 하는 형식으로 구성이 될 겁니다. 처음에는 진짜 리얼 데이터가 아니라 학습 데이터로 실행을 할 것이고 이후 학습이 되는대로 실 데이터로 실전을 할 것입니다.

 

 

반응형
LIST

맨위로버튼이미지

주식이나 코인 매매 프로그램을 너무 쉽게 생각하시면 큰일 납니다. 제 경험이기도 합니다. 일단 머쉰 러닝을 배우신 분들 중에는 주식차트를 시계열 데이터로 착각하시는 분들이 많습니다. 물론 시계열 데이터이기도 합니다. 시계열 데이터의 가장 큰 특징은 무엇일까요? 정답을 아시는 분들도 있겠지만 바로 계절성입니다. 예를 들면 따릉이의 이용률을 데이터로 변환한다면 봄가을에는 많은 사람들이 이용해서 자전거가 부족하지만 여름이나 겨울에 그 수가 확실히 감소합니다. 특히 겨울에는 이용률이 크게 감소하죠. 따릉이 자전거를 자동 배치하는 AI를 개발한다면 크게 어렵지 않게 개발이 가능하겠죠. 그렇다면 시계열 데이터 방식으로 예를 들면 LSTM 방식으로 삼성전자의 주가를 예측해보신 분들은 나름 데이터가 맞는 것 같아서 신기해합니다. 하지만 그것은 예측된 데이터로 보기엔 문제가 있습니다. 인간이 개입하는 어떤 데이터도 시계열 데이터의 특징인 주기성을 보이지는 않습니다. 그저 서로를 속이는 눈속임일 뿐입니다. 아쉽게도 LSTM이나 RNN, GRU 방식 등의 방식으로 주가를 예측하는 것은 애초에 불가능 합니다.https://m.blog.naver.com/dsz08082/222042702104
위 url의 내용을 한번 참고해 보세요. 앞에서도 말씀드렸듯이 주식, 코인을 자동으로 매매하는 프로그램은 정말 쉽지 않습니다. 인터넷에 통장까지 공개하시는 분들이 많은데 이 번의 사태를 견뎠을까요? 주식 또한 마찬가지로 동일본 지진이나 리먼사태 등을 예측할 수는 없었습니다. 그렇다면 강화 학습으로 그런 문제를 극복할 수 있을까요? 사실 저도 자신이 없네요. 그래서 브레이크 장치가 필요합니다. 그래도 주식은 제도권 안에 있기에 법적인 구제 장치가 많지만 코인 시장은 그렇지 않습니다. 혹시나 인터넷이나 유튜브를 떠도는 프로그램들에는 눈도 돌리지 마시기 바랍니다. 제가 쓴 글들 중에도 몇몇 것은 참담한 실패를 맛봤습니다. 가끔 사람은 세상을 너무 만만하게 보거나 짧은 생각으로 글을 쓸 수 있습니다. 지금 우리가 하는 길이 만만하지 않음을 꼭 아셨으면 합니다. 오늘은 CNN과 상태(S)와의 관계에 대하여 생각해보겠습니다. 코인 차트는 시간(t)에 따라 C0
, C1, C2, ... , Ct-1, Ct, ... 이렇게 생성이 될 겁니다. pytorch 의 tensor데이터는 다차원의 행렬로 이루어져 있습니다. PIL이미지를 torch.tensor로 변환하는 함수를 지원합니다. 차트 이미지를 torch.tensor로 변경했다면 C2 - C1을 하면 t1의 상태(S)가 됩니다. 원래는 RGB데이터로 이미지를 저장하고자 했지만 저도 이미지 쪽은 잘 몰라 ARGB방식 즉 4차원 torch.tensor로 저장이 되었습니다. CNN은 합성곱 인공신경망의 약자입니다. 이미지의 특징을 검색하기 위해 bmp방식으로 저장하게 되면 메모리 overflow로 프로그램은 더 이상 진행하지 않고 런타임 에러가 나거나 엄청난 컴퓨터 비용을 지불해야 할 것입니다. 그래서 화면의 일부를 차원곱을 사용하여 이미지를 쭉 따라서 검색하는 방식으로 이미지를 분석합니다. 인터넷에 있는 개와 고양이를 구분하는 예제는 고양이 코 모양과 개의 코 모양이 완전히 다르다는 것을 신경망 학습이 비지도 학습으로 알아내고 클래스(분류 가짓수)를 2라고 한정했기 때문에 고양이의 코 모양을 0으로 했다면 개의 코 모양은 1로 해서 분류를 하고 학습을 반복할수록 지도 학습으로 정확도를 높이게 됩니다. 학습이 끝나면 거의 100%의 정확도로 고양이와 개를 구분합니다. 즉 이미지의 모든 부분을 이용하는 것이 아니라 특징이 다른 부분을 합성곱으로 빨리 탐색하고 분류하는 것이죠. 다시 주식차트로 오면 C2 - C1을 하게 되면 C2와 C1의 동일한 부분은 모두 0이 될 겁니다. 행렬의 차는 같은 위치의 값을 서로 빼는 것이니 당연히 같은 좌표에 있는 이미지 중에 같은 값을 가진 것들은 전부 0으로 바뀔 겁니다. 그럼 C2 - C1을 S1이라고 하면 대분은 0인 이미지가 나오겠죠. 그리고 변화가 생긴 부분만 남게 됩니다. 그래서 우리가 만드는 차트에 라벨링은 의미가 없습니다. 같은 위치에 반복적으로 쓰이는 값은 삭제될 뿐입니다. 그다음 S1을 합성곱으로 검색해서 지나가는 부분은 비지도 학습에 의해 가짓수를 CNN이 결정합니다. 다시 우리가 지정한 output개수 즉 class의 가짓수에 의해서 다시 CNN은 지도 학습을 진행하면서 각각의 클래스에 해당하는 숫자를 지정하게 됩니다. 학습을 진행하면 우리 인간은 눈치 못 챈 어떤 특징으로 인해 차트는 우리가 지정한 가지 수만큼의 output값을 지정하게 됩니다. 우리는 이 값을 행동 0,1,2로 지정합니다. 저희 프로그램은 100% 매수 또는 매도 또는 관망의 3가지 행동 뿐입니다. CNN은 우리의 의도대로 차트를 가지고 우리가 실행해야 하는 행동을 구하게 됩니다.
어렵지 않게 설명하려고 많이 노력했는데 괜찮았는지 모르겠네요. 상태 값과 CNN의 관계를 이해하셨다면 마르코프 실행 프로세스에 해당하는 부분을 설명하겠습니다. 혹시 오류를 발견하셨다면 댓글 부탁드립니다.

 

 

반응형
LIST

맨위로버튼이미지

지금은 코인을 중심으로 프로그램을 작성하지만 주식에도 그대로 적용이 가능합니다. 코인 거래소는 upbit를 이용하고 있습니다. 지금은 코인원이 수수료가 가장 싸므로 api를 조금 수정하시면 코인원식으로 변경이 가능 할 겁니다. 강화학습에서 중요한 포인트는 env와 agent입니다. 코인에서는 env는 마켓으로 agent는 account라고 저는 명명을 했습니다. 꼭 그렇게 않해도 됩니다. 여기에 게시되는 소스는 아무런 라이센스 없이 사용하셔도 됩니다. 우리가 사용할 DQN에 대하여 간단하게 설명 드리겠습니다. 그전에 참고할 만한 책을 한권 소개 드립니다. '바닥부터 배우는 강화학습'(https://www.googleadservices.com/pagead/aclk?sa=L&ai=DChcSEwj6gIbC29D7AhVy3EwCHXJfAU4YABAKGgJ0bQ&ohost=www.google.com&cid=CAESbeD2J1z4qWevI6iWwrPeahjlRAd5PHQxq3vxCnxSB-XFcQcQlPCdbI5vS31FJsWDEdYDXr7wQRya94UUUP1TeQ_sv2Gp1efEdpeYMlECKaQ1jwRuNHUqGOxGxPQPMtaqjppqY8HY39-QdAnHBII&sig=AOD64_1r3tIroFWvKdgd3IEQB1BX0mmAdg&ctype=5&q=&ved=2ahUKEwizuv_B29D7AhUVBd4KHTcyApkQ9aACKAB6BAgEEAw&adurl=)

을 한번 읽어 보시기 바랍니다. 아주 기초 이론이고 실제 강화학습은 이 책 내용과는 다를 수 있습니다. 강화학습의 그리드월드 내용을 보시면 약간 지도학습의 모습과 닮았습니다. 모든 강화학습의 방법은 책 내용데로 해서는 결과를 보기는 상당히 힘듧니다. 기본적으로 agent는 일단 인공신경망을 배제하고 env와 agent를 구성할 필요가 있습니다. 그리고 나서 실제 agent가 발전하는게 확인이 되면 그다음은 인공신경망을 이용하여 Q(s,a)함수를 Deep learning 모듈화 하는 것입니다. 사실 이 모든 것은 어떤 수학자가 주장한 모든 문제는 곡선화 할 수 있으며 곡선화를 하면 해당 답을 구할 수 있다는 일반 함수론에서 출발하여 Q-learning을 DQN을 이용하여 함수로 만들 수 있다는 개념으로 출발했습니다. 그러니 Q(s,a)함수를 먼저 구하고 그 다음 그 함수를 DQN모듈로 변환하는 것이 바로 강화학습인것이죠. 물론 DB를 사용할 수도 있을 것입니다. 아주 정밀한 값도 저장할 수 있지만 만약에 데이터가 무한하다면 DB로 그 많은 데이터를 저장할 수 있을 까요? 그래서 강화학습에서는 DQN(CNN)을 이용하여 함수화 하자는 거죠 토치의 저장기능을 사용하면 몇 byte의 값으로도 그 함수를 저장할 수 있으니까요? 다시 시작할 때는 그냥 그 모듈을 다시 읽어서 재실행하면 끝입니다. DB보다는 컴퓨터 비용이 훨씬 저렴하죠 아니 저렴한 정도가 아니라 거의 공짜 수준이라고 할 수 있죠?
일단 Market class 소스를 보시죠

반응형
from matplotlib.backend_bases import FigureCanvasBase
import py
import plotly.graph_objects as go
import plotly.subplots as ms
import plotly.express as px
import plotly as plt
import pymysql
import pandas as pd
import numpy as np
import time
import talib
from PIL import Image

ticker = 'ETH'
class Market():
    def __init__(self) -> None:
        self.df = pd.DataFrame()
    
    def add_bbands(self):
        try:
            print("볼리져 밴드 구하기:%s"  % time.ctime(time.time()))
            self.df['ma20'] = talib.SMA(np.asarray(self.df['close']), 20)
            self.df['stddev'] = self.df['close'].rolling(window=20).std() # 20일 이동표준편차
            upper, middle, lower = talib.BBANDS(np.asarray(self.df['close']), timeperiod=40, nbdevup=2.3, nbdevdn=2.3, matype=0)
            self.df['lower']=lower
            self.df['middle']=middle
            self.df['upper']=upper
            # self.df['bbands_sub']=self.df.upper - self.df.lower
            # self.df['bbands_submax'] =  self.df.bbands_sub.rolling(window=840, min_periods=10).max()
            # self.df['bbands_submin'] =  self.df.bbands_sub.rolling(window=840, min_periods=10).min()
            # self.df['bbs_deg'] = (self.df.mavg - self.df.mavg.shift()).apply(lambda x: math.degrees(math.atan2(x, 40))) 

            # self.df['pct8']=(self.df.close - self.df.lower)/(self.df.upper - self.df.lower)
            # self.df['pct8vsclose']=self.df.close - self.df.pct8
        except Exception as ex:
            print("`add_bbands -> exception! %s `" % str(ex))

    def add_relative_strength(self):
        try:
            print("상대 강도 지수 구하기:%s"  % time.ctime(time.time()))
            rsi14 = talib.RSI(np.asarray(self.df['close']), 14)
            self.df['rsi14'] = rsi14
        except Exception as ex:
            print("`add_relative_strength -> exception! %s `" % str(ex))
            
    def add_iip(self):
        try:
            #역추세전략을 위한 IIP계산
            self.df['II'] = (2*self.df['close']-self.df['high']-self.df['low'])/(self.df['high']-self.df['low'])*self.df['volume']
            self.df['IIP21'] = self.df['II'].rolling(window=21).sum()/self.df['volume'].rolling(window=21).sum()*100            
        except Exception as ex:
            print("`add_iip -> exception! %s `" % str(ex))
            
    def add_stock_cast(self):
        try:
            # 스토캐스틱 구하기
            self.df['ndays_high'] = self.df['high'].rolling(window=14, min_periods=1).max()    # 14일 중 최고가
            self.df['ndays_low'] = self.df['low'].rolling(window=14, min_periods=1).min()      # 14일 중 최저가
            self.df['fast_k'] = (self.df['close'] - self.df['ndays_low']) / (self.df['ndays_high'] - self.df['ndays_low']) * 100  # Fast %K 구하기
            self.df['slow_d'] = self.df['fast_k'].rolling(window=3).mean()    # Slow %D 구하기        except Exception as ex:
        except Exception as ex:
            print("`add_stock_cast -> exception! %s `" % str(ex))
    
    def add_mfi(self):
        try:
            # MFI 구하기
            self.df['PB'] = (self.df['close'] - self.df['lower']) / (self.df['upper'] - self.df['lower'])
            self.df['TP'] = (self.df['high'] + self.df['low'] + self.df['close']) / 3
            self.df['PMF'] = 0
            self.df['NMF'] = 0
            for i in range(len(self.df.close)-1):
                if self.df.TP.values[i] < self.df.TP.values[i+1]:
                    self.df.PMF.values[i+1] = self.df.TP.values[i+1] * self.df.volume.values[i+1]
                    self.df.NMF.values[i+1] = 0
                else:
                    self.df.NMF.values[i+1] = self.df.TP.values[i+1] * self.df.volume.values[i+1]
                    self.df.PMF.values[i+1] = 0
            self.df['MFR'] = (self.df.PMF.rolling(window=10).sum() / self.df.NMF.rolling(window=10).sum())
            self.df['MFI10'] = 100 - 100 / (1 + self.df['MFR'])    
        except Exception as ex:
            print("`add_mfi -> exception! %s `" % str(ex))

    def add_macd(self, sort, long, sig):
        try:
            print("MACD 구하기:%s, sort:%d, long:%d, sig:%d"  % (time.ctime(time.time()), sort, long, sig))
            macd, macdsignal, macdhist = talib.MACD(np.asarray(self.df['close']), sort, long, sig) 
            self.df['macd'] = macd
            # self.df['macdmax'] = self.df.macd.rolling(window=840, min_periods=100).max()
            # self.df['macdmin'] = self.df.macd.rolling(window=840, min_periods=100).min()
            # self.df['macd_max_rate'] = self.df.apply(lambda x: (x['macdmax'] - x['macd']) * 100 / (x['macdmax'] - x['macdmin']), axis=1)
            # self.df['macd_min_rate'] = self.df.apply(lambda x: (x['macd'] - x['macdmin']) * 100 / (x['macdmax'] - x['macdmin']), axis=1)
            # self.df['prv_macd_degrees'] = self.df.macd_degrees.shift()
            self.df['signal'] = macdsignal
            self.df['flag'] = macdhist
            # self.df['prv_osc_degrees'] = self.df.osc_degrees.shift()
        except Exception as ex:
            print("`add_macd -> exception! %s `" % str(ex))

    def get_data(self):
        table_name="TB_ETH_TRADE"
        sqlcon = pymysql.connect(host='192.168.xx.xx', user='xxxx', password='xxxx', db='yubank2')
        cursor = sqlcon.cursor(pymysql.cursors.DictCursor)
        str_query = """select 
            'time'
            ,`start` as open
            , high
            , low
            , close
            , volume
            , macd
            , macdmax
            , macdmin
            , `signal`
            , `flag`
            , osc_degrees
            , sma1200
            , sma1200_degrees
            , wma1200
            , wma1200_degrees
            from %s""" % table_name
        cursor.execute(str_query)
        data = cursor.fetchall()
        self.df = pd.DataFrame(data)
        sqlcon.close()
        self.df['sma3'] = talib.SMA(np.asarray(self.df['close']), 3)
        self.df['closemax'] = self.df.close.rolling(window=100, center=True).max()
        self.df['closemin'] = self.df.close.rolling(window=100, center=True).min()
        self.add_macd(12,26,9)
        self.add_bbands()
        self.add_relative_strength()
        self.add_stock_cast()
        self.add_iip()
        self.add_mfi()
        self.df.dropna(inplace=True)
        self.df.reset_index(drop=True,inplace=True)
        return self.df

    def get_test_data(self):
        table_name="TB_ETH_TRADE"
        sqlcon = pymysql.connect(host='192.168.xx.xx', user='xxxx', password='xxxx', db='yubank2')
        cursor = sqlcon.cursor(pymysql.cursors.DictCursor)
        str_query = """select 
            `start` as open
            , high
            , low
            , close
            , volume
            , macd
            , macdmax
            , macdmin
            , `signal`
            , `flag`
            , osc_degrees
            , sma1200
            , sma1200_degrees
            , wma1200
            , wma1200_degrees
            from %s""" % table_name
        cursor.execute(str_query)
        data = cursor.fetchall()
        self.df = pd.DataFrame(data)
        sqlcon.close()
        self.add_bbands()
        self.add_relative_strength()
        self.df['clsmin'] = self.df.close.rolling(window=600, min_periods=600, center=False).min()
        self.df['clsmax'] = self.df.close.rolling(window=600, min_periods=600, center=False).max()
        self.df['action'] = self.df.apply(lambda x: 1 if x['close'] > x['up'] else( 1 if x['close'] < x['dn'] else 0), axis=1)
        # self.df.drop(self.df[self.df['action'] == 0].index, inplace=True)
        self.df.dropna(inplace=True)
        self.df.reset_index(drop=True,inplace=True)
        return self.df

    def get_lstm_data(self):
        table_name="TB_ETH_TRADE"
        sqlcon = pymysql.connect(host='192.168.xx.xx', user='xxxx', password='xxxx', db='yubank2')
        cursor = sqlcon.cursor(pymysql.cursors.DictCursor)
        str_query = """select 
            `date`
            ,`start` as open
            , high
            , low
            , close
            from %s""" % table_name
        cursor.execute(str_query)
        data = cursor.fetchall()
        self.df = pd.DataFrame(data)
        sqlcon.close()
        self.df['fclose'] = self.df.close.shift(-1)
        self.df.dropna(inplace=True)
        self.df.reset_index(drop=True,inplace=True)
        return self.df

    def get_chart(self, idx, max_data:int=300):
        try:
            df = self.df.head(idx + max_data).tail(max_data)
            df.reset_index(drop=True, inplace=True)
            if df.index.max() < 299:
                return None
            
            candle = go.Candlestick(x=df.index,open=df['open'],high=df['high'],low=df['low'],close=df['close'], increasing_line_color = 'red',decreasing_line_color = 'blue', showlegend=False)
            upper = go.Scatter(x=df.index, y=df['upper'], line=dict(color='red', width=2), name='upper', showlegend=False)
            ma20 = go.Scatter(x=df.index, y=df['ma20'], line=dict(color='black', width=2), name='ma20', showlegend=False)
            lower = go.Scatter(x=df.index, y=df['lower'], line=dict(color='blue', width=2), name='lower', showlegend=False)

            volume = go.Bar(x=df.index, y=df['volume'], marker_color='red', name='volume', showlegend=False)

            MACD = go.Scatter(x=df.index, y=df['macd'], line=dict(color='blue', width=2), name='MACD', legendgroup='group2', legendgrouptitle_text='MACD')
            MACD_Signal = go.Scatter(x=df.index, y=df['signal'], line=dict(dash='dashdot', color='green', width=2), name='MACD_Signal')
            MACD_Oscil = go.Bar(x=df.index, y=df['flag'], marker_color='purple', name='MACD_Oscil')

            fast_k = go.Scatter(x=df.index, y=df['fast_k'], line=dict(color='skyblue', width=2), name='fast_k', legendgroup='group3', legendgrouptitle_text='%K %D')
            slow_d = go.Scatter(x=df.index, y=df['slow_d'], line=dict(dash='dashdot', color='black', width=2), name='slow_d')

            PB = go.Scatter(x=df.index, y=df['PB']*100, line=dict(color='blue', width=2), name='PB', legendgroup='group4', legendgrouptitle_text='PB, MFI')
            MFI10 = go.Scatter(x=df.index, y=df['MFI10'], line=dict(dash='dashdot', color='green', width=2), name='MFI10')

            RSI = go.Scatter(x=df.index, y=df['rsi14'], line=dict(color='red', width=2), name='RSI', legendgroup='group5', legendgrouptitle_text='RSI')
            
            # 스타일
            fig = ms.make_subplots(rows=5, cols=2, specs=[[{'rowspan':4},{}],[None,{}],[None,{}],[None,{}],[{},{}]], shared_xaxes=True, horizontal_spacing=0.03, vertical_spacing=0.01)

            fig.add_trace(candle,row=1,col=1)
            fig.add_trace(upper,row=1,col=1)
            fig.add_trace(ma20,row=1,col=1)
            fig.add_trace(lower,row=1,col=1)

            fig.add_trace(volume,row=5,col=1)

            fig.add_trace(candle,row=1,col=2)
            fig.add_trace(upper,row=1,col=2)
            fig.add_trace(ma20,row=1,col=2)
            fig.add_trace(lower,row=1,col=2)

            fig.add_trace(MACD,row=2,col=2)
            fig.add_trace(MACD_Signal,row=2,col=2)
            fig.add_trace(MACD_Oscil,row=2,col=2)

            fig.add_trace(fast_k,row=3,col=2)
            fig.add_trace(slow_d,row=3,col=2)

            fig.add_trace(PB,row=4,col=2)
            fig.add_trace(MFI10,row=4,col=2)

            fig.add_trace(RSI,row=5,col=2)

            # 추세추종
            # trend_fol = 0
            # trend_refol = 0
            # for i in df.index:
            #     if df['PB'][i] > 0.8 and df['MFI10'][i] > 80:
            #         trend_fol = go.Scatter(x=[df.index[i]], y=[df['close'][i]], marker_color='orange', marker_size=20, marker_symbol='triangle-up', opacity=0.7, showlegend=False)
            #         fig.add_trace(trend_fol,row=1,col=1)
            #     elif df['PB'][i] < 0.2 and df['MFI10'][i] < 20:
            #         trend_fol = go.Scatter(x=[df.index[i]], y=[df['close'][i]], marker_color='darkblue', marker_size=20, marker_symbol='triangle-down', opacity=0.7, showlegend=False)
            #         fig.add_trace(trend_fol,row=1,col=1)

            # 역추세추종
            # for i in df.index:
            #     if df['PB'][i] < 0.05 and df['IIP21'][i] > 0:
            #         trend_refol = go.Scatter(x=[df.index[i]], y=[df['close'][i]], marker_color='purple', marker_size=20, marker_symbol='triangle-up', opacity=0.7, showlegend=False)  #보라
            #         fig.add_trace(trend_refol,row=1,col=1)
            #     elif df['PB'][i] > 0.95 and df['IIP21'][i] < 0:
            #         trend_refol = go.Scatter(x=[df.index[i]], y=[df['close'][i]], marker_color='skyblue', marker_size=20, marker_symbol='triangle-down', opacity=0.7, showlegend=False)  #하늘
            #         fig.add_trace(trend_refol,row=1,col=1)    

            # fig.add_trace(trend_fol,row=1,col=1)
            # 추세추총전략을 통해 캔들차트에 표시합니다.

            # fig.add_trace(trend_refol,row=1,col=1)
            # 역추세 전략을 통해 캔들차트에 표시합니다.
            
            # fig.update_layout(autosize=True, xaxis1_rangeslider_visible=False, xaxis2_rangeslider_visible=False, margin=dict(l=50,r=50,t=50,b=50), template='seaborn', title=f'({ticker})의 날짜: ETH [추세추종전략:오↑파↓] [역추세전략:보↑하↓]')
            # fig.update_xaxes(tickformat='%y년%m월%d일', zeroline=True, zerolinewidth=1, zerolinecolor='black', showgrid=True, gridwidth=2, gridcolor='lightgray', showline=True,linewidth=2, linecolor='black', mirror=True)
            # fig.update_yaxes(tickformat=',d', zeroline=True, zerolinewidth=1, zerolinecolor='black', showgrid=True, gridwidth=2, gridcolor='lightgray',showline=True,linewidth=2, linecolor='black', mirror=True)
            # fig.update_traces(xhoverformat='%y년%m월%d일')
            # size = len(img)
            # img = plt.io.to_image(fig, format='png')
            # canvas = FigureCanvasBase(fig)
            # img = Image.frombytes(mode='RGB', size=(700, 500), data=fig.to_image().to, decoder_name='raw')
            # img = Image.frombytes('RGBA', (700, 500), plt.io.to_image(fig, format='png'), 'raw')
            # img = Image.fromarray('RGBA', (700, 500), np.array(plt.io.to_image(fig, format='png')), 'raw')
            # img = Image.fromarray(np.array(plt.io.to_image(fig, format='png')), 'RGB')
            import io
            # img = Image.fromarray(np.array(plt.io.to_image(fig, format='png')), 'L')
            img = Image.open(io.BytesIO(plt.io.to_image(fig, format='png', width=140, height=100)))
            img.convert("RGB")
            img.thumbnail((100, 140), Image.ANTIALIAS)
            # img = Image.Image(fig.to_image(), 'RGB')
            # img.show()
            return img
        except Exception as ex:
            print("`get_chart -> exception! %s `" % str(ex))
            return None

차트 그리기는 https://sjblog1.tistory.com/m/45 의 내용을 참고했습니다. rolling함수를 사용할 경우 속도 문제가 있어서 ta-lib로 변경했습니다. ta-lib의 경우 거의 모든 주식차트 함수를 포함하고 있으며 c++로 개발이 되어 있어서 속도면에서 훨씬 빠릅니다.
클래스를 선언하고 get_chart함수를 호출하면 chart가 bmp 타입(png)의 이미지 데이터로 리턴이 됩니다. 데이터는 저의 경우 DB에 3분 단위로 매일 데이터를 저장하고 있고 그 기간이 벌써 1년반정도 되었씁니다. 꼭 DB를 사용하지 않아도 pyupbit로 데이터를 가져 오면 아마 1주일치 정도의 데이터를 가져 올 수 있습니다. 물론 더 많은 데이터가 있으면 좋겠지만 일주일 정도로도 훈련이 가능합니다. 다음 차에서는 상태값 S를 구하는 것에 대하여 설명 드리겠습니다.

 

 

반응형
LIST

맨위로버튼이미지

들어가기전에
매몰비용이라는 말이 있습니다. 경제학에서는 '콩코드 효과'라는 말이 있습니다. 미국의 보잉사는 1970년대에도 항공기 제조사중 최고였다고 합니다. 이에 영국과 프랑스는 오로지 미국 보잉을 이기기 위한 싸움을 시작합니다. 그리고 태어난 비행기가 콩코드기 였습니다. 마하2의 속도를 최초로 돌파한 민간 항공기 였을 겁니다. 하지만 앞에서 보는 것보다 뒤로 깨지는 돈이 상당한 그런 사업이었나 봅니다. 사업이 계속 될 수록 투자자들에 얼굴에는 검은 먹구름이 끼고 있었죠. 그리고 콩코드 비행기에 사고가 발생합니다. 콩코드는 보잉의 탑승료보다 훨씬 비싼 비행기여서 일반은 잘 안탑니다. 사고로 유명인사들이 많이 죽게 됩니다. 이로 인해 콩코드 프로젝트는 끝이 납니다. 그 사고가 없었다면 더많은 사람이 죽었을지도 모릅니다.

일단 만약 이 글을 읽는 분이 이 프로젝트를 시작하시고자 한다면 위의 콩코드 효과를 걱정하셔야 할겁니다. 그러나 시작했다면 뒤는 돌아 보시면 안됩니다.
오늘은 개발 환경을 셋팅하도록 하겠습니다. 일단 그래픽카드가 하나 있어야 합니다. 더이상 CUDA는 RTX 10xx대 보더 미만을 지원하지 않습니다. 최소 RTX 1030보더 이상은 구매를 해야한다는 말입니다. 다행히 저가 컴구매자를 위한 저가 모델이 아직 생산되고 있습니다. 최소한 보드의 메모리는 6GB 이상이 필요합니다. 그렇지 않으면 DQN(CNN)모듈이 구동하는 순간 RUNTIME 오류가 발생하고 멈추게 될겁니다. 일단 저희 Lib는 차트를 그릴겁니다. 메모리상으로만 그리고 실제 화면으로 출력 되지는 않을 겁니다. 이 화면의 사이즈는 500 x 700 입니다만 이 사이즈의 메모리도 Pytorch는 처리가 불가합니다. 정확하게는 cuda lib입니다만 바로 memory allocation runtime error가 발생합니다. 내용을 자세히 읽어보시면 gpu 카드의 실메모리량이 부족하여 데이타를 처리할 수 없다는 내용입니다. 그 이유로 차트를 다시 100 x 140으로 축소를 합니다. 나중에는 사이즈를 키울거지만 지금은 이게 최선인것 같습니다. 먼저 ndividia에 회원 가입이 필요합니다. 구글아이디로 가입하는 것이 가장 좋을것 같습니다. ndividia 홈페이지에서 download center로 이동하여 cuda 최신 버전을 다운 받습니다. 13이나 14던 상관은 없습니다. cuda의 설치가 완료되었다면 python 버젼을 선택해야 합니다. 혹시 openAi사의 retro gym을 경험하고자 한다면 python 3.7버젼을 추천합니다. 일단 retro gym을 경험해 보시면 프로그램을 어떻게 해야하는지 감이 옵니다. 어떤 책에서는 강화학습을 env(환경)과 agent(게임을 실행하는 프로그램)으로 나누는 데 retro gym을 보면 python프로그램 자체가 agent가 되고 env는 openAi에서 제공해 준다고 보시면 됩니다. agent를 만들어 게임을 깨는 내 프로그램을 보면 대단하다고 혼자 기쁘하실지 모르지만 진짜는 현실의 상황을 env로 만드는게 진짜구나 하는 생각을 하게 됩니다. 즉 retro gym을 경험하시면 진짜는 DQN이 아니라 env를 어떻게 설계하고 만들지 입니다. 유튜브에 보시면 카이스트 출신의 혁펜다임 채널을 보시면 이론은 쉽게 설명하는 영상을 볼 수 있습니다. 보고 나시면 현실을 강화학습으로 바꾸는 과정의 진짜 중요한 부분은 env라는것을 금방 눈치 챌 수 있습니다. 그러나 다행히 의외로 주식 프로그램은 env 제작이 그렇게 어렵지 않습니다. 앞으로 돌아가서 python 3.9 나 3.10을 사용하면 안되는 이유는 retro gym은 3.9이상에는 실행조차도 안됩니다. 책은 Do it시리즈 중 '강화학습 입문'을 추천 드립니다.( https://ebook-product.kyobobook.co.kr/dig/epd/ebook/4801163032527) 3장의 Buruto force 방법을 보시면 됩니다. 일단은 agent의 개념을 이해하는게 중요합니다. cuda가 다 설치 되었다면 pip와 numpy를 업그레이드합니다

python -m pip install -U pip
python -m pip install -U numpy

torch, torchvision만 있어도 충분하지만 필요하다면 torchaudio를 설치합니다. (https://pytorch.org/get-started/previous-versions/) 이때 torch repository site에 가시면 더 많은 whl(pip install file)이 존재하는것을 보실 수 있습니다. pip에 권한 문제가 생기거나 위치 문제 또는 lib 위치 문제가 발생한다면 python -m pip로 설치 해보시기 바랍니다. 그럼 보통은 대부분 해결이 됩니다. 그리고 pip를 바로 실행할 경우 경로가 꼬여 다시 설치해야 하는 경우가 생깁니다. 그런 경우는 python -m pip install - U {모듈명}를 사용하시면 됩니다. 그리고 whl파일을 설치하는 경우 repository url을 알고 있다면 pip install {url}이렇게 해도 됩니다. torch의 경우 내가 설치한 cuda에 맞는 버젼을 찾아서 설치해야해서 꼭 repository url을 참조해서 설치 하시기 바랍니다. 다음은 ta-lib를 설치해야 합니다 windows 의 경우 구글을 검색하여 whl파일을 python 버젼과 일치하는 파일을 찾아서 설치하셔야 합니다. linux 버젼을 깔 경우는 레드햇 계열의 경우 yum(ubuntu는 apt 또는 apt-get)으로 python버젼과 동일한 python-devel 을 설치해야 하고 gmake와 gcc, g++을 설치 해주셔야 합니다. 설치할 python의 whl파일을 검색하여 버젼과 맞는 repository url을 복사하여 pip문 끝에 붙여 줍니다. 기타 pandas나 다른 util은 쉽게 설치가 되므로 python을 실행해보고 에러유형을 보고 추가하면 됩니다. 다음 장에서는 데이터 수집과 마켓 클래스에 대하여 소개해 드리겠습니다. env의 첫 관문입니다. 강화학습이 궁금하신 분은 retro gym의 예제 하나를 실행해 보고 오시죠. OpenAi는 일론 머스크 돈으로 우리같은 사람을 위해서 만든 오픈소스 플랫폼이니 많이 이용하세요.

 

==> WSL 환경으로 변경하였으니 8장 WSL환경 이용바랍니다.

반응형
LIST

맨위로버튼이미지

강화학습 매매 프로그램을 만들면서 프로그래머로써 가장 큰 도전을 견뎌야 했습니다. 학습량이랑 소스, 디버깅 량 시스템 투자 모두 역대 최대였습니다. 중간쯤 진행하다보니 포기할 수가 없었습니다. 이 글은 다른 도전자에게 대한 삽질의 최소화를 도모하고자 작성하게 되었습니다. 부디 이 글을 읽으시는 분들은 좀 더 들 고생하였으면 합니다. 개발 환경은 win10(wsl 개발환경으로 변경)에 메모리 48G CPU는 제온 2650 V3에 RTX 3060 OG 12G 2장과 화면 디스플레이용 RTX 1060한장이 들어간 시스템을 사용했습니다. 꼭 이렇게 할 필요는 없지만 DQN은 CNN을 베이스로 하므로 GPU 메모리를 굉장히 많이 사용합니다.

딥런닝 엔진으로 PYTORCH를 사용했지만 tensoflow로 변경은 그리 어렵지 않을 것으로 생각됩니다. 먼저 머쉰런닝을 공부한 경험이 있는 사람은 지도 학습과 비지도 학습에 대한 내용은 어느 정도 알고 있을 것으로 생각합니다. 지도 학습은 기존에 정재된 데이터가 이미 존재하고 데이터에 대한 결과가 분명할 경우 학습을 통해 새로운 데이타를 분류하거나 값을 예측하는 것을 말합니다. 예를 들면 한 고등학교 학생을 대상으로 방과후 학습시간을 설문 조사한 후 3번의 시험을 치고 나서 4번째 시험 결과를 예측하는 경우가 지도학습과정이라 할 수 있습니다. 비지도 학습은 데이터는 존재하지만 분류기준등이 모호할 경우 머쉰런닝을 통해서 분류값을 얻고 데이터를 분류하는 경우입니다. 예를들면 어느 들판에 있는 들꽃을 조사했는데 같은 종류지만 키가 차이가 나는것을 발견하고 머쉰런닝을 돌려서 같아보이지만 다른 종류의 꽃이 몇가지가 존재하는지 예측하는 프로그램을 만들었다면 그것은 비지도 학습입니다. 그렇다면 다음은 어느경우에 해당할까요? 한국어를 학습하는 머쉰런닝 프로그램이 있다면 프로그램은 무작위로 학습해서 주어와 동사가 있다는 것을 학습한 후 언어에는 순서가 있다는것을 배우고 그후에는 사람과 실시간 채팅을 학습합니다. 사견이지만 지도학습과 비지도학습은 머쉰런닝을 설명하기 위한 기준이지 강화학습으로 들어가면 분류 자체가 무의미해지는 것 같습니다. 강화학습을 가장 쉽게 설명하는 것은 영화일것 입니다. 그것도 타임 루프물 영화들이 그렇습니다. 톰 크루즈가 출연한 작품 중 '엣지 오브 투모로우'라는 영화가 있습니다. 이 영화는 외계 생맹체로 부터 지구가 침공 당해 거의 전멸하기 직전의 상황에서 톰형이 무슨일인지 장교에서 신분강등이 되어 전투지휘부로 끌러 옵니다. 이후 전쟁에 참여해서 외계인의 분대장격인 알파를 죽이고 그 피를 받으면서 죽으면 하루전으로 가는 능력을 가지게 되는게 이야기의 시작입니다.

처음 도입부는 약간 지겹게 느껴질 수 있습니다. 그는 계속 억울함을 주장하고 전투에 출전하고 죽고 다시 깨어나고의 반복입니다. 그 사이 톰은 무기의 사용법과 전투방법과 미래의 일어날 일을 외우고 자기 부대원에게 전투방법을 가르치며 조금씩 발전을 합니다. 그는 전쟁에 승리하기 위한 강화학습을 진행하고 있는것입니다. 그리고 그로인해 전쟁의 양상은 바뀌게 됩니다.

우리가 하고자 하는 봐도 바로 이것입니다. 우리는 쉽게 주식이나 코인의 거래데이타를 받을 수 있습니다. 그 데이터를 이용하여 수천번의 시뮬레이션을 실시해서 최고의 성적을 내는 시나리오를 얻고 이것을 CNN에 하이퍼 파라메트로 학습하여 이것을 이용하여 실전의 행동과 예상 reward 를 얻는 방식으로 진화할 수 있습니다.

CNN은 머쉰런닝 분야에서 가장 발전한 형태일 것입니다. 처음 시작은 팩스로 전송된 손글씨를 인식하여 데이터화 하는것이었습니다. 그 다음은 개와 고양이 사진을 보고 개와 고양이를 구분하는 논문이 발표됩니다. 그 후 Open AI라는 회사는 DQN이라는 인공신경망을 개발하고 아케이드 게임을 인간보다 잘하는 인공지능을 개발합니다. 이후 레트로 짐 이라는 인공지능 학습환경을 개발하여 일반에게 오픈했습니다.

우리는 그 방법들 중 한가지를 사용하여 DQN을 학습하여 그 DQN이 얼마나 사람보다 주식이나 코인 트레이딩을 더 잘하는지 아님 더 잘할때까지 계속 학습하는 방법을 의논할 것입니다. 저의 글을 보시고 다른 의견이 있으시면 답글 부탁드립니다.

 

 

반응형
LIST

+ Recent posts