본문 바로가기
AI SCHOOL/TIL

[DAY 80] Bidirectional RNN을 통해 삼성전자 주가 예측하기

2023. 4. 19.

RNN 모델을 사용하여 삼성전자 주가 데이터를 예측하고 실제와 비교했다.

먼저 RNN과 Sequence 데이터의 개념을 이해했다.

RNN(Recurrent Neural Network, 순환신경망)

인공신경망의 한 종류로, 유닛 간의 연결이 순환적 구조를 갖는다.
결과값을 출력층 방향으로 보내면서 다시 은닉층 노드의 다음 계산의 입력으로도 보내는 특징
다양한 길이의 입력 시퀀스를 처리할 수 있는 인공신경망
시퀀스 데이터(순서가 중요한 데이터)를 처리할 때 강력

rnn
출처 : https://ko.wikipedia.org/wiki/순환_신경망

은닉층이 입력층과 이전 타임스텝의 은닉층으로부터 정보를 받는다.
타임스텝(timesteps)은 시점의 수를 의미하며 입력 시퀀스의 길이라고 표현하기도 한다.


Sequence Data(시퀀스 데이터)
연관된, 연속의 데이터로 순서(과거)의 영향을 받는 데이터
활용 분야
- NLP(자연어 처리) : 문장, 문서, 대화 등 텍스트 데이터는 일련의 단어 또는 문자
- 음성 인식 : 음성 신호는 일련의 시간적인 샘플링 값
- 시계열 분석 : 주가, 날씨, 센서 데이터 등 시간 순서대로 측정된 시계열 값
- 영상 및 음악 처리 : 비디오 및 오디오 데이터는 시간적인 차원을 가짐

RNN을 사용하여 시퀀스 데이터 중 시계열 데이터의 대표 예시인 주가 데이터를 다루어 본다.

RNN으로 주가 예측

삼성전자 주가 수집FinanceDataReader를 사용했다.

df = fdr.DataReader('005930', '2020')
df.shape

# 실행 결과
(817, 6)

FinanceDataReader를 이용해서 삼성전자(005930)의 2020년 첫 거래일부터 오늘까지 시가, 고가, 저가, 종가, 거래량, 등락률을 수집했다.

df.head(3)

첫 3개 데이터 확인

samsung1

2020년의 첫 3거래일 삼성전자 주가 정보

df.tail(3)

마지막 3개 데이터 확인

samsung2

오늘, 어제, 그저께의 삼성전자 주가 정보

문제와 답안 나누기

df_ohlcv = df.iloc[:, :-1]
dfx = df_ohlcv.drop(columns='Close')
dfy = df_ohlcv['Close']  # target : Close(종가)
dfx.shape, dfy.shape

# 실행 결과
((817, 4), (817,))

Change는 전날 종가와의 차이를 설명하기 때문에 이전 정보를 이미 포함하는 형태다. 따라서 제외하고 사용한다. 예측할 target은 Close(종가)이다.
Open, High, Low, Volume을 통해 Close를 예측한다.

스케일링

plot

Volume과 다른 변수들의 스케일 차이가 매우 큰 것을 확인할 수 있다. MinMaxScaling으로 이를 해소한다.

from sklearn.preprocessing import MinMaxScaler

mmsx = MinMaxScaler()
mmsy = MinMaxScaler()
x_mms = mmsx.fit_transform(dfx)
y_mms = mmsy.fit_transform(dfy.to_frame())

사이킷런의 MinMaxScaler를 사용해서 스케일링
예측 후 스케일링된 값을 복원할 때 편하게 하기 위해 x, y를 따로 진행한다.

pd.DataFrame(x_mms).describe()

스케일링 후 기술통계 확인

scaled

0~1 사이 값으로 MinMaxScaling이 잘 된 것이 보인다.

x, y값에 window 적용
window(윈도우) : 시계열 데이터에서 일정한 기간 동안의 관측치
현재는 윈도우를 고려하지 않은 값이다. 이전 시점의 데이터가 각 행에 들어있지 않고 해당 시점의 스냅샷만 있는 상태. 따라서 RNN 적용을 위해 윈도우를 고려해서 데이터를 만들어준다.

window
출처 : https://stackoverflow.com/questions/31947183/how-to-implement-walk-forward-testing-in-sklearn

10일 치의 OHLV 데이터를 이용하여 10일 치 다음날의 종가를 예측하는 형태로 진행한다.

# 예시
x_mms[0:10], y_mms[10]

# 실행 결과
(array([[0.27044025, 0.23380282, 0.2690678 , 0.10985591],
        [0.28092243, 0.24507042, 0.26694915, 0.13782255],
        [0.25786164, 0.22629108, 0.26059322, 0.07860503],
        [0.27463312, 0.24131455, 0.28177966, 0.0755059 ],
        [0.2851153 , 0.2600939 , 0.28813559, 0.23083931],
        [0.3312369 , 0.28262911, 0.31991525, 0.23776363],
        [0.33962264, 0.30328638, 0.33898305, 0.14447639],
        [0.35639413, 0.30892019, 0.3559322 , 0.09104179],
        [0.37316562, 0.32769953, 0.37288136, 0.15490908],
        [0.35429769, 0.30140845, 0.35169492, 0.12491213]]),
 array([0.37525773]))

x_mms에서 앞 10일 치(2020년 첫 거래일부터 10일째까지)의 OHLV 데이터를, y_mms에서 11일째의 Close 데이터를 예시로 추출했다. 이런 형태로 데이터를 구성한다.

window_size = 10 

x_data = []
y_data = []
for start in range(len(y_mms)-window_size):
    stop = start + window_size
    x_data.append(x_mms[start:stop])
    y_data.append(y_mms[stop])

전체 데이터에 대해 윈도우 사이즈만큼 슬라이싱해서 x_data, y_data를 구성했다.

print(len(y_mms))
y_mms[10], y_mms[-1]

# 실행 결과
817
(array([0.37525773]), array([0.47010309]))

윈도우 전에 817개의 데이터가 있었다면

print(len(y_data))
y_data[0], y_data[-1]

# 실행 결과
807
(array([0.37525773]), array([0.47010309]))

윈도우 적용하여 만든 데이터는 윈도우 사이즈가 10이므로 807개가 됐다.
또한 윈도우 전 10번째 데이터와 윈도우 적용 후 0번째 데이터가 같음을 이해하자.

train, test 분할

split_size = int(len(x_data) * 0.8)  # 645
X_train = np.array(x_data[:split_size])
X_test = np.array(x_data[split_size:])
y_train = np.array(y_data[:split_size])
y_test = np.array(y_data[split_size:])
X_train.shape, X_test.shape, y_train.shape, y_test.shape

# 실행 결과
((645, 10, 4), (162, 10, 4), (645, 1), (162, 1))

807개의 데이터를 80:20으로 분할하여 645개, 162개로 분할한다.
유의해야 할 점은 랜덤으로 분할하면 안 된다는 점이다. 시계열 데이터는 시점을 기준으로 분할해야 한다.

모델링
Bidirectional LSTM을 사용한다.
LSTM(Long Short-Term Memory, 장단기 메모리)는 기존 RNN의 기울기 소실 문제를 해결하기 위해 고안되었다. forget gate(망각 게이트)를 추가적으로 가져 역전파시 기울기 값이 급격하게 변화하는 문제를 방지한다.
Bidirectional(양방향) RNN은 어떤 값이 들어오기 전과 후의 정보를 모두 학습하는 방식의 알고리즘이다. 시퀀스를 양방향으로 읽고 둘의 출력값을 조합한다. LSTM과 함께 사용할 때 특히 좋은 성능을 낸다.

model = Sequential()
model.add(
    layers.Bidirectional(
        layers.LSTM(64, return_sequences=True), input_shape=X_train[0].shape)
)
model.add(layers.Bidirectional(layers.LSTM(32)))
model.add(layers.Dense(1))
model.summary()

tensorflow의 함수들을 사용한다.

summary
model summary


모델 컴파일

model.compile(loss=tf.keras.losses.MeanSquaredError(),
               optimizer=tf.keras.optimizers.Adam(),
               metrics=['mse', 'mae']
              )

주가 예측은 회귀 예측이므로 loss function을 MSE로 지정한다.

학습과 예측

history = model.fit(X_train, y_train, epochs=100)
df_history = pd.DataFrame(history.history)
df_history.loc[:, ['loss', 'mae']].plot()

100회 학습 후 loss와 mae 시각화

plot2

스케일링이 된 값을 사용해서 기본적으로 loss나 error 값이 낮다.

y_pred = model.predict(X_test)
df_test = pd.DataFrame(y_test, columns=['test'])
df_test['predict'] = y_pred
y_test_date = df.iloc[-162:].index
df_test.index = y_test_date

예측값을 y_pred에 담고 실제 값과 예측값을 데이터프레임으로 만든다. 데이터프레임의 인덱스는 0부터 시작하는 숫자로 되어 있는데, df 데이터프레임에서 날짜 값을 가져와서 바꿔준다.

df_test

데이터프레임 확인

dftest

스케일링된 값이기 때문에 직관적이지 않다. 원래 값으로 복원해야 한다.

y_predict_inverse = mmsy.inverse_transform(y_pred)
y_test_origin = df.iloc[-len(y_pred):, [-3]]
df_test['test_origin'] = y_test_origin
df_test['predict_origin'] = y_predict_inverse
df_test

MinMaxScaler의 inverse_transform을 사용하여 스케일링된 값을 역변환할 수 있다. 실제 주가는 df 데이터프레임에서 가져오고 실제값과 예측값을 위에서 만든 df_test에 변수로 추가한다.

dftest2

꽤 비슷하게 잘 예측한 것 같다. 특히 3원밖에 차이가 안 나게 예측한 날도 있었다.

df_test[['test_origin', 'predict_origin']].plot(figsize=(10, 5))

실제값과 예측값 비교 시각화

plot3

Bidirectional RNN 모델을 사용하여 2020년 1월 2일부터 2022년 8월 25일 전까지 데이터를 학습하고 2022년 8월 25일부터 오늘까지의 삼성전자 주가를 예측해 봤다.

반응형

댓글