본문 바로가기
투자x개발/퀀트 투자

[주가 예측 모델링] #3. LSTM으로 주가예측 모델 만들기

by didi0di 2023. 4. 6.
728x90

 

이제 데이터를 살펴봤으니, 

LSTM 기반의 주가예측 모델을 만들어보자.

우선 지난글에서 불러온대로 데이터를 다시 확인해본다. 

 

[이전글] #1. 주식 데이터 불러오기 (yfinance)

 

[주가 예측 모델링] #1. 주식 데이터 불러오기 (yfinance)

앞서 2차전지 섹터 종목 분석에서 선정한 종목인 에코프로비엠 주가 예측 모델을 만들려고 한다. [이전글] - 2차전지 주요 종목 분석 with ChatGPT [국내주식] 2차전지 주요 종목 분석 with ChatGPT 2차전

didi-universe.tistory.com

[이전글] #2. 주가 데이터 차트 그리기(matplotlib)

 

[주가 예측 모델링] #2. 주가 데이터 차트 그리기 (matplotlib)

이전글에서 yfinance 라이브러리를 이용해 주가 데이터를 불러왔다. [이전글] #1. 주가 데이터 불러오기 (yfinance) [주가 예측 모델링] #1. 주식 데이터 불러오기 (yfinance) 앞서 2차전지 섹터 종목 분석

didi-universe.tistory.com

 

데이터 로드

import yfinance as yf
import pandas as pd

echopro_bm = yf.download("247540.KQ", interval='1h', period='1y')
echopro_bm.head(3)

>> 	Open	High	Low	Close	Adj Close	Volume
Datetime						
2022-04-06 13:00:00+09:00	412600.0	418600.0	412400.0	417100.0	417100.0	0
2022-04-06 14:00:00+09:00	417100.0	422200.0	416500.0	418800.0	418800.0	51266

 

다양한 변수들이 있는데, 그 중 종가를 예측하는 모델을 만들기 위해서

'Close' 컬럼을 예측값 y로 만들고, 종가를 제외한 나머지 'Open', 'High', 'Low', 'Volume' 을 독립변수 x로 사용하려고 한다.

 

데이터 정규화 

그 전에, 데이터들 간 범위가 너무 많이 차이나면 학습이 잘 안될 수 있기 때문에, 정규화를 해주는게 좋을 것 같다.

sklearn 라이브러리의 MinMaxScaler를 사용했다.

 

from sklearn.preprocessing import MinMaxScaler

scaler = MinMaxScaler()

# 스케일을 적용할 column을 정의
scale_cols = ['Open', 'High', 'Low', 'Close', 'Volume']
# 스케일 후 columns
scaled = scaler.fit_transform(echopro_bm[scale_cols])
# 데이터 프레임으로 만들기
s_df = pd.DataFrame(scaled, columns=scale_cols)
s_df.head(3)
>> 	Open	High	Low	Close	Volume
0	0.686291	0.691535	0.692175	0.694588	0.000000
1	0.694242	0.691962	0.697579	0.691581	0.016615
2	0.691448	0.687046	0.696282	0.690507	0.009878

스케일링 후 데이터들이 0~1 사이의 값들로 정규화 되는 모습을 확인 할 수 있다.

 

학습/테스트 데이터셋 만들기

 

이제 학습 데이터와 테스트 데이터로 데이터셋을 나눠보자.

sklearn의 train_test_split 함수를 이용하면 간단하게 데이터셋을 나눌 수 있다.

 

종가를 예측하는 모델을 만들려고 하기 때문에,

y로는 s_df['Close'] 를, x로는 Close 열을 제외한 나머지 컬럼을 사용하기 위해 s_df.drop('Close', 1) 을 넣어주었다.

(s_df.drop('Close', 1) 는 'Close' 열을 제외한 데이터를 불러온다는 의미이며, 1은 axis=1 (열 방향 드롭) 을 간략히 쓴 것)

 

train 데이터와 test 데이터의 비율은 7:3으로 적용했으며,

그 결과 학습 데이터가 1188개, 테스트 데이터가 298개로 나눠진 것을 확인할 수 있다. 

 

from sklearn.model_selection import train_test_split

x_train, x_test, y_train, y_test = train_test_split(s_df.drop('Close', 1), s_df['Close'], test_size=0.2, random_state=0, shuffle=False)
print(x_train.shape, y_train.shape, x_test.shape, y_test.shape)
>> (1188, 4) (1188,) (298, 4) (298,)

그리고 종가 예측에 사용할 데이터를 지정한 윈도우 사이즈에 맞춰 배치로 만들어 주는 함수를 만든다.

이 window_size와 batch_size는 하이퍼파라미터로, 값을 바꿔서 돌려볼 수 있다.

 

import tensorflow as tf

def windowed_dataset(series, window_size, batch_size, shuffle):
    series = tf.expand_dims(series, axis=-1)
    ds = tf.data.Dataset.from_tensor_slices(series)
    ds = ds.window(window_size + 1, shift=1, drop_remainder=True)
    ds = ds.flat_map(lambda w: w.batch(window_size + 1))
    if shuffle:
        ds = ds.shuffle(1000)
    ds = ds.map(lambda w: (w[:-1], w[-1]))
    return ds.batch(batch_size).prefetch(1)

WINDOW_SIZE=24
BATCH_SIZE=4

# trian_data는 학습용 데이터셋, test_data는 검증용 데이터셋
train_data = windowed_dataset(y_train, WINDOW_SIZE, BATCH_SIZE, True)
test_data = windowed_dataset(y_test, WINDOW_SIZE, BATCH_SIZE, False)

# 아래의 코드로 데이터셋의 구성을 확인해 볼 수 있음
# X: (batch_size, window_size, feature)
# Y: (batch_size, feature)
for data in train_data.take(1):
    print(f'데이터셋(X) 구성(batch_size, window_size, feature갯수): {data[0].shape}')
    print(f'데이터셋(Y) 구성(batch_size, window_size, feature갯수): {data[1].shape}')
    
>> 데이터셋(X) 구성(batch_size, window_size, feature갯수): (4, 24, 1)
데이터셋(Y) 구성(batch_size, window_size, feature갯수): (4, 1)

window_size의 경우 처음에 20으로 설정했는데, 

현재 데이터가 1시간 간격으로 가져온거라 

6시간 단위 (하루에 장이 열리는 시간이 09-16시까지 6시간)로 끊어주는게 좋을 것 같아서

나중엔 24, 18 등 6의 배수로 실험해봤다.

 

batch_size는 32부터 16, 8, 4 로 줄여서 돌려봤는데 4가 제일 나았다.

배치사이즈가 작아질수록 학습에 보는 데이터수의 적어져서 그런지 예측하기 직전 데이터들의 트렌드를 더 잘 학습하는 것 같다.

 

우선 과거의 종가 데이터만 가지고 다음 종가데이터를 예측하도록 해봤다.

이제 LSTM 모델을 만들어봅시다!

 

LSTM 모델 만들기

 

import os

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, LSTM, Conv1D, Lambda
from tensorflow.keras.losses import Huber
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint

model = Sequential([
    # 1차원 feature map 생성
    Conv1D(filters=32, kernel_size=5,
           padding="causal",
           activation="relu",
           input_shape=[WINDOW_SIZE, 1]),
    # LSTM
    LSTM(16, activation='tanh'),
    Dense(16, activation="relu"),
    Dense(1),
])

# Sequence 학습에 비교적 좋은 퍼포먼스를 내는 Huber()를 사용
loss = Huber()
optimizer = Adam(0.0005)
model.compile(loss=Huber(), optimizer=optimizer, metrics=['mse'])

# earlystopping은 10번 epoch통안 val_loss 개선이 없다면 학습을 멈춤
earlystopping = EarlyStopping(monitor='val_loss', patience=10)

# val_loss 기준 체크포인터도 생성
filename = os.path.join('tmp', 'ckeckpointer.ckpt')
checkpoint = ModelCheckpoint(filename, 
                             save_weights_only=True, 
                             save_best_only=True, 
                             monitor='val_loss', 
                             verbose=1)

history = model.fit(train_data, 
                    validation_data=(test_data), 
                    epochs=50, 
                    callbacks=[checkpoint, earlystopping])   

model.load_weights(filename)
pred = model.predict(test_data)

 

 

결과 시각화 

실제 값과 예측값을 시각화해봤다.

plt.figure(figsize=(12, 9))
plt.plot(np.asarray(y_test)[24:], label='actual')
plt.plot(pred, label='prediction')
plt.legend()
plt.show()

 

배치 사이즈에 따른 예측값 차이

-> 배치 사이즈가 커질수록 스무딩 되는 것 같고, 배치 사이즈가 작아지면 좀 더 변동성에 핏하게 예측하는 듯.

 

window_size= 24, batxh_size = 8 window_size= 24, batxh_size = 4
window_size= 24, batxh_size = 8
window_size= 24, batxh_size = 4
   

 

 

최종 선정 모델

window_size =20, batch_size = 4 인 모델을 최종 모델로 선정했다.

 

예측 결과 시각화

window_size= 20, batxh_size = 4

 

예측값이 후행되는게 아쉽긴 하지만,

그래도 몇번의 파라미터 튜닝 끝에 근접하게 예측하는 모델을 만든 것 같다.

 

728x90

댓글