30분만에 나스닥 기업 주가 예측

주가 예측 서론

회사에서 주가 예측 과 관련된 코딩 공모전을 열었다.

제대로 인공신경망 모델을 만들기 위해서는

설계, 튜닝에 상당한 시간을 투자해야한다.

현생이 바쁘기 때문에 순위는 일찌감치 포기했다.

다만 구현 난이도가 궁금해서 인터넷 상 코드를 테스트해 보았다.

Github 가 보편화된 이후로 코딩 실력은 필요한 코드를

얼마나 빠르게 찾아 응용할 수 있는지에 달렸다.

다행히 주가 예측 에 대한 코드는 인터넷에 차고 넘친다.

코드 선택 기준

  • Tensorflow 로 작성될 것
  • VM에서 학습 가능한 복잡도
  • 논문 수준의 분석, 설계 필요없음
  • 20년도 이후 비교적 최신 코드

많은 코드들 중에 위 조건에 해당하는 코드를 탐색했다.

주가 예측에는 보편적으로 LSTM , GRU , ARIMA 등이 사용되는데

데이터셋의 종류, 형태에 따라 성능이 달라진다.

그리고 LSTM + GRU , LSTM + ARIMA , ARIMA + GRU 처럼

하이브리드로 구성될 때 성능이 향상된다.

이중에서 LSTM + GRU 에 해당하는 코드들을 찾아봤다.

둘 다 RNN이기때문에 hybrid 로 만들기 쉽고

tensorflow 로 구현하기도 쉽다.

Kaggle 원본 코드는 링크로 첨부하니 참고하길 바란다.

본론

모듈 및 데이터 임포트

코드에서 사용되는 모듈들과 설명

  • pandas : CSV를 사용하기 쉬운 dataframe , series로 변환,저장
  • numpy : 데이터 가공
  • sklearn : 학습에 필요한 utility 제공
  • tensorflow : 실질적으로 학습을 진행
  • matplotlib : plot 에 사용
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import mean_absolute_error
from sklearn.model_selection import train_test_split
import os
os.environ['TF_ENABLE_ONEDNN_OPTS']='0'
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout
from tensorflow.keras.layers import LSTM, GRU
from keras.callbacks import EarlyStopping,ModelCheckpoint
from sklearn.preprocessing import MinMaxScaler

#import data
org_df={}
dflist=['AAPL','AMD','ATVI','CSCO','EBAY','GOOGL','INTC','MSFT','MU','NTGR','NVDA','SBUX','STX','TSM','WDC']
for i in range(1,16):
    org_df[i]=pd.read_csv(f"/dataset/{dflist[i-1]}.csv")

전처리

우선 데이터를 별도로 어떻게 전처리 할것인지 정해야 한다.

sklearn 에서 제공하는 MinMaxScaler 를 사용해

0에서 1 사이의 값을 가지도록 정규화 했다.

데이터의 노이즈가 심하지 않아 필터는 적용하지않았다.

data는 6개의 column을 가지는데 원본 저자는 종가만 학습에 사용했다.

다른 데이터로 accuracy를 올릴 수도 있지만 학습,튜닝에 오랜시간이 걸린다.

또한 volume을 제외한 column 들은 종가와 유사하기때문에

accuracy를 올리는데 효율적이지 못하다.

단순하게 k개의 사전데이터로 다음 날을 예측하도록 input을 구성했다.


따라서 위 그림 대로 Input 과 Output 데이터를 가공한다.

train set과 validation set을 나누어주기 위해서는 train_test_split을 사용하거나

model.fit에서 validation_split 옵션을 사용하면 된다.

def create_dataset(dataset, time_step=1):
    dataX, dataY = [], []
    for i in range(len(dataset)-time_step-1):
        a = dataset[i:(i+time_step)]    
        dataX.append(a)
        dataY.append(dataset[i + time_step])
    return np.array(dataX), np.array(dataY)

t_step=10
scaler=MinMaxScaler(feature_range=(0,1))
scaled=scaler.fit_transform(np.array(org_df[1]['Close']).reshape(-1,1))
X_train,y_train=create_dataset(scaled,t_step)
# X_train,X_val,y_train,y_val=train_test_split(X_train,y_train,test_size=0.3,random_state=0)

신경망 구축 및 학습

def custom_loss(y_true, y_pred):
    mae = tf.keras.losses.MeanAbsoluteError()
    return mae(y_true, y_pred)/np.mean(abs(y_true))

callbacks = [EarlyStopping(monitor='val_loss',
                                           patience=15,min_delta=0.01),
             ModelCheckpoint(filepath='best_model.h5',
                                             monitor='val_loss',
                                             save_best_only=True, restore_best_weights=True)]
model=Sequential()
model.add(LSTM(32,return_sequences=True,input_shape=(t_step,1)))
model.add(LSTM(32,return_sequences=True))
model.add(Dropout(0.2))

model.add(GRU(32,return_sequences=True))
model.add(Dropout(0.2))

model.add(GRU(32,return_sequences=True))
model.add(GRU(32))
model.add(Dropout(0.2))

model.add(Dense(1))

model.compile(loss=custom_loss,optimizer='adam' , metrics = [ "mae",'mse'],run_eagerly=True)
model.summary()


history=model.fit(X_train,y_train,validation_split=0.3,epochs=100,batch_size=64,verbose=0,callbacks=callbacks)
# history=model.fit(X_train,y_train,validation_data=(X_val,y_val),epochs=100,batch_size=64,verbose=1,callbacks=callbacks)
print('done')
Model: "sequential_6"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 lstm_12 (LSTM)              (None, 10, 32)            4352      
                                                                 
 lstm_13 (LSTM)              (None, 10, 32)            8320      
                                                                 
 dropout_18 (Dropout)        (None, 10, 32)            0         
                                                                 
 gru_18 (GRU)                (None, 10, 32)            6336      
                                                                 
 dropout_19 (Dropout)        (None, 10, 32)            0         
                                                                 
 gru_19 (GRU)                (None, 10, 32)            6336      
                                                                 
 gru_20 (GRU)                (None, 32)                6336      
                                                                 
 dropout_20 (Dropout)        (None, 32)                0         
                                                                 
 dense_6 (Dense)             (None, 1)                 33        
                                                                 
=================================================================

LSTM과 GRU가 같이 배치된 하이브리드 모델이다.

학습의 속도, accuracy는 모델의 배치,shape에도 크게 영향 받으나 절대적인 법칙은 없다.

성능을 보면서 적절하게 변경하면 된다.

원본 코드와 다르게 레이어를 배치해 봤지만 원본대로 하는게 가장 정확도가 높다.

loss는 mse, mae 등 여러가지를 사용할 수 있는데 코드에 나온것처럼

새로운 loss function을 정의해도 된다.

물론, mae와 mse 를 섞는 등 다른 평가 요소를 사용하여 성능을 더 개선할 수 있다.

코드수행에 생각보다 시간이 걸리는데 EarlyStopping callback을 통해

학습을 조기 중단시킬 수 있다.

혹은 processpool executor를 사용하여 수행속도를 올릴 수 있다.

마지막으로 model.fit을 통해 학습을 수행하는데

epochs, batch_size 등 각종 parameter의 튜닝이 필요하다.

이러한 hyper parameter는 keras의 tuner를 사용해도 되고

단순한 반복문을 만들어 기록해도 된다.

verbose를 1로 두면 학습 과정을 지켜볼 수 있습니다.

epoch별 loss값을 지켜보면 학습이 정상진행되는지 알 수 있기때문에 신경망을 설계할 때 유용하다.

다음 코드를 통해 학습이 잘됐는지 plot해보자.

hh=history.history
epochs=range(len(hh['loss']))
plt.plot(epochs,hh['loss'],label='loss')
plt.plot(epochs,hh['val_loss'],label='val_loss')
plt.legend()
plt.show()

loss와 validation_loss가 둘 다 수렴하고 있으므로

학습이 어느정도 잘 이루어지고 있다.

과적합이 일어난다면 신경망의 설계,변수를 바꾸거나

정규화를 변경함으로써 해결할 수 있다.

주가 예측 값 도출

prd={}
nmae={}
day_to_predict=7
for _,i in enumerate(org_df):
    last_data=scaler[i].fit_transform(np.array(org_df[i]['Close'].iloc[-t_step:]).reshape(-1,1))
    prd_list=[]
    for j in range(day_to_predict):
        last_data=last_data[-t_step:]
        prd_temp=models[i].predict(np.array(last_data).reshape(1,t_step[i],1),verbose=0)
        prd_list.append(prd_temp[0][0])
        last_data=np.append(last_data,pd.Series(prd_temp[0][0]))
    prd[i]=scaler[i].inverse_transform(np.array(prd_list).reshape(-1,1))[:,0]
    nmae[i]=my_loss(test_df[i]['Close'][:7],prd[i])
    print(f'{dflist[i-1]} : {nmae[i]}')

print(f'avg={pd.Series(nmae).mean()}')

이제 위 코드를 통해 주가 예측 데이터를 도출한다.

루틴을 간단하게 설명하면 다음과 같습니다.

  1. 마지막 k개의 데이터를 input으로 만든다.
  2. input을 통해 다음날의 데이터를 예측한다.
  3. 예측 데이터를 input 데이터에 덧붙인다.

위 과정을 예측하고 싶은만큼 날만큼 반복하여 loss값을 기록한다.

기록된 loss값이나 평균 loss값으로 학습이 잘 이루어졌는지 확인 가능하다.

주가 예측 결론

AAPL : 0.04620344564318657
AMD : 0.11094651371240616
ATVI : 0.0061081573367118835
CSCO : 0.01842576451599598
EBAY : 0.026052556931972504
GOOGL : 0.03204019367694855
INTC : 0.03417059779167175
MSFT : 0.05612574890255928
MU : 0.008338606916368008
NTGR : 0.02389097958803177
NVDA : 0.0929316058754921
SBUX : 0.015276948921382427
STX : 0.07766019552946091
TSM : 0.02993595041334629
WDC : 0.03140253946185112
avg = 0.04063398440678914
  • 평균 오차: 주가의 4~5%
  • 소요시간: 코딩 30분 + 학습 30분 (CPU,싱글 코어 기준)

약간의 튜닝으로도 성능을 2~3%까지 개선할 수 있다.

30분짜리 코딩치고 주가 예측 결과가 나쁘지 않다.

강화학습, deep learning 등에서 이론 공부만으로 시간을 버리는 경우가 많다.

Matlab의 Tuner/Optimizer를 쓰면서 알고리즘을 다 이해하는건 아니듯이

알고리즘 이해에 너무 긴 시간을 쏟을 필요 없다.

차라리 kaggle 등에서 실용적인 코드를 찾아 직접 해보는것이 도움이 된다.

여러분들께도 올려드린 코드가 도움이 되길 바란다.

Leave a Reply