본문으로 건너뛰기

© 2026 Molayo

Lilian헤드라인2026. 05. 14. 07:44

RNN을 사용한 주가 예측: Part 1

요약

본 문서는 Tensorflow를 사용하여 순환 신경망(RNN)을 구축하고 주가 예측 모델을 훈련하는 방법을 다루는 튜토리얼입니다. 특히 S&P500 지수의 종가를 예측하기 위해 LSTM 셀을 활용한 RNN 모델 구축 과정을 설명합니다. 이 가이드는 단순히 예측 결과 개선보다는, 실제 세계의 데이터를 사용하여 Tensorflow에서 RNN 모델을 구현하고 학습시키는 과정 자체에 초점을 맞추고 있습니다.

핵심 포인트

  • RNN은 시퀀스 데이터 처리에 탁월하며, 은닉층에 자기 루프를 통해 이전 상태를 활용하여 현재 상태를 학습합니다.
  • LSTM 셀은 RNN이 장기적인 문맥(long-term context)을 더 잘 기억하도록 돕기 위해 설계된 특별한 작동 단위입니다.
  • 본 모델 구축은 S&P500 지수의 종가 데이터를 사용하며, 예측을 위해 슬라이딩 윈도우 기법을 적용합니다.
  • 튜토리얼의 목적은 최신 Tensorflow API를 사용하여 실제 금융 시계열 데이터로 RNN 모델을 구현하는 방법을 보여주는 데 있습니다.

이것은 주식 시장 가격을 예측하기 위해 Tensorflow를 사용하여 순환 신경망 (Recurrent Neural Network, RNN)을 구축하는 방법에 대한 튜토리얼입니다. 전체 작동 코드는 github.com/lilianweng/stock-rnn에서 확인할 수 있습니다. 만약 순환 신경망 (RNN)이나 LSTM 셀 (LSTM cell)이 무엇인지 모른다면, 제 이전 포스트를 자유롭게 확인해 보세요.

제가 강조하고 싶은 한 가지는, 이 포스트를 작성하는 동기가 주가 예측 문제를 해결하는 것보다는 Tensorflow에서 RNN 모델을 구축하고 훈련하는 방법을 보여주는 데 더 중점을 두고 있기 때문에, 예측 결과를 개선하는 데 큰 노력을 기울이지 않았다는 점입니다. 여러분의 코드를 참고 자료로 활용하여 주가 예측과 관련된 더 많은 아이디어를 추가해 개선하는 것을 언제든 환영합니다. 즐겁게 학습하세요!

기존 튜토리얼 개요

인터넷에는 다음과 같은 많은 튜토리얼이 있습니다:

  • A noob’s guide to implementing RNN-LSTM using Tensorflow
  • TensorFlow RNN Tutorial
  • LSTM by Example using Tensorflow
  • How to build a Recurrent Neural Network in TensorFlow
  • RNNs in Tensorflow, a Practical Guide and Undocumented Features
  • Sequence prediction using recurrent neural networks(LSTM) with TensorFlow
  • Anyone Can Learn To Code an LSTM-RNN in Python
  • How to do time series prediction using RNNs, TensorFlow and Cloud ML Engine

이러한 기존 튜토리얼들이 있음에도 불구하고, 제가 새로운 튜토리얼을 쓰고 싶은 이유는 주로 세 가지입니다:

  • Tensorflow는 여전히 개발 중이며 API 인터페이스의 변경이 빠르게 이루어지고 있기 때문에, 초기 튜토리얼들은 더 이상 최신 버전에 대응할 수 없습니다.
  • 많은 튜토리얼이 예제에서 합성 데이터 (synthetic data)를 사용합니다. 저는 실제 세계의 데이터를 다뤄보고 싶습니다.
  • 일부 튜토리얼은 여러분이 Tensorflow API에 대해 미리 알고 있다고 가정하며, 이는 읽는 것을 다소 어렵게 만듭니다.

수많은 예제를 읽은 후, 저는 Penn Tree Bank (PTB) 데이터셋에 대한 공식 예제를 시작점으로 삼을 것을 제안하고 싶습니다. PTB 예제는 상당히 미려하고 모듈화된 디자인 패턴으로 RNN 모델을 보여주지만, 모델 구조를 쉽게 이해하는 데 방해가 될 수도 있습니다. 따라서 여기서는 매우 직관적인 방식으로 그래프를 구축하겠습니다.

목표

저는 LSTM 셀을 사용하여 S&P500 지수의 가격을 예측하는 RNN 모델을 구축하는 방법을 설명할 것입니다. 데이터셋은 Yahoo! Finance ^GSPC에서 다운로드할 수 있습니다. 다음 예제에서는 1950년 1월 3일(Yahoo! Finance가 추적할 수 있는 최대 날짜)부터 2017년 6월 23일까지의 S&P 500 데이터를 사용했습니다. 데이터셋은 하루에 여러 가격 지점을 제공합니다. 단순화를 위해, 우리는 예측을 위해 일일 **종가 (close prices)**만을 사용할 것입니다. 한편, 저는 쉽게 디버깅하고 모델을 추적하기 위해 TensorBoard를 사용하는 방법도 보여줄 것입니다.

간단히 요약하자면: 순환 신경망 (RNN)은 은닉층 (hidden layer)에 자기 루프 (self-loop)가 있는 인공 신경망의 한 종류로, 이를 통해 RNN은 새로운 입력이 주어졌을 때 은닉 뉴런 (hidden neuron)의 이전 상태를 사용하여 현재 상태를 학습할 수 있습니다. RNN은 시퀀스 데이터 (sequential data)를 처리하는 데 탁월합니다. 장단기 메모리 (LSTM) 셀은 RNN이 장기적인 문맥 (long-term context)을 더 잘 기억하도록 돕기 위해 특별히 설계된 작동 단위입니다.

더 자세한 정보는 저의 이전 포스트나 이 멋진 포스트를 읽어보시기 바랍니다.

데이터 준비

주가는 $p_0, p_1, ext{...}, p_{N-1}$로 정의되는 길이 $N$의 시계열 (time series)이며, 여기서 $p_i$는 $0 ext{ } ext{*} ext{ } i < N$인 날 $i$의 종가입니다. 고정된 크기 $w$의 슬라이딩 윈도우 (sliding window)가 있고 (나중에 이를 input_size라고 부릅니다), 윈도우를 오른쪽으로 크기 $w$만큼 이동할 때마다 모든 슬라이딩 윈도우 사이의 데이터에 중복이 없다고 가정해 봅시다.

우리가 구축하려는 RNN 모델은 LSTM 셀을 기본 은닉 단위 (hidden units)로 가집니다. 우리는 첫 번째 슬라이딩 윈도우 $W_0$의 맨 처음 값부터 시간 $t$에서의 윈도우 $W_t$까지의 값을 사용합니다:

다음 윈도우 $w_{t+1}$의 가격을 예측하기 위해서입니다:

$$ W_{t+1} = (p_{(t+1)w}, p_{(t+1)w+1}, \dots, p_{(t+2)w-1}) $$

본질적으로 우리는 근사 함수(approximation function) $f(W_0, W_1, \dots, W_t) \approx W_{t+1}$를 학습하려고 시도합니다.

시간에 따른 역전파 (Backpropagation Through Time, BPTT)가 작동하는 방식을 고려할 때, 우리는 보통 RNN을 "펼쳐진 (unrolled)" 버전으로 학습시킵니다. 이는 역전파 계산을 너무 멀리까지 수행하지 않도록 하여 학습의 복잡성을 줄이기 위함입니다.

다음은 num_steps에 대한 설명입니다.

Tensorflow 튜토리얼에 따르면:

설계상 순환 신경망 (Recurrent Neural Network, RNN)의 출력은 임의로 멀리 떨어진 입력값에 의존합니다. 불행히도 이는 역전파 (backpropagation) 계산을 어렵게 만듭니다. 학습 과정을 다룰 수 있게 만들기 위해, 고정된 수 (num_steps)의 LSTM 입력과 출력을 포함하는 네트워크의 "펼쳐진 (unrolled)" 버전을 만드는 것이 일반적인 관행입니다. 그런 다음 모델은 이 유한한 RNN 근사치 위에서 학습됩니다. 이는 한 번에 num_steps 길이의 입력을 공급하고, 각 입력 블록 이후에 역방향 패스 (backward pass)를 수행함으로써 구현될 수 있습니다.

가격 시퀀스는 먼저 겹치지 않는 작은 윈도우들로 분할됩니다. 각 윈도우는 input_size개의 숫자를 포함하며, 각각은 하나의 독립적인 입력 요소로 간주됩니다. 그런 다음 연속된 임의의 num_steps개 입력 요소들을 하나의 학습 입력으로 그룹화하여, Tensorflow에서 학습하기 위한 "펼쳐진 (un-rolled)" 버전의 RNN을 형성합니다. 그에 대응하는 레이블 (label)은 바로 다음에 오는 입력 요소입니다.

예를 들어, input_size=3이고 num_steps=2라면, 처음 몇 개의 학습 예시는 다음과 같을 것입니다:

데이터 포맷팅을 위한 핵심 부분은 다음과 같습니다:

seq = [np.array(seq[i * self.input_size: (i + 1) * self.input_size])
for i in range(len(seq) // self.input_size)]
# `num_steps` 단위로 분할
...

데이터 포맷팅의 전체 코드는 여기에 있습니다.

훈련 / 테스트 분할 (Train / Test Split)

우리는 항상 미래를 예측하고자 하므로, 데이터의 **최신 10%**를 테스트 데이터로 사용합니다.

정규화 (Normalization)

S&P 500 지수는 시간이 지남에 따라 상승하며, 이로 인해 테스트 세트의 대부분의 값이 훈련 세트의 범위를 벗어나는 문제가 발생합니다. 따라서 모델은 이전에 본 적 없는 숫자를 예측해야 하는 상황에 놓이게 됩니다. 안타깝게도, 예상대로 모델은 비극적인 결과를 보여줍니다.

이러한 범위를 벗어나는 (out-of-scale) 문제를 해결하기 위해, 저는 각 슬라이딩 윈도우 (sliding window) 내의 가격을 정규화 (Normalization) 합니다. 이제 작업은 절대값 대신 상대적인 변화율 (relative change rates)을 예측하는 것이 됩니다. 시간 $t$에서의 정규화된 슬라이딩 윈도우 $W’t$에서, 모든 값은 마지막 미지의 가격, 즉 $W{t-1}$의 마지막 가격으로 나누어집니다:

$$ W’t = (\frac{p{tw}}{p_{tw-1}}, \frac{p_{tw+1}}{p_{tw-1}}, \dots, \frac{p_{(t+1)w-1}}{p_{tw-1}}) $$

여기 2017년 7월까지 크롤링한 S&P 500 주가 데이터인 stock-data-lilianweng.tar.gz 데이터 아카이브가 있습니다. 자유롭게 활용해 보세요 :)

모델 구축 (Model Construction)

정의 (Definitions)

lstm_size : 하나의 LSTM 레이어 (layer)에 있는 유닛 (unit)의 수.
num_layers : 쌓여 있는 LSTM 레이어의 수.
keep_prob : 드롭아웃 (dropout) 연산에서 유지할 셀 유닛 (cell unit)의 비율.
init_learning_rate : 시작 학습률 (learning rate).
learning_rate_decay : 이후 학습 에포크 (epoch)에서의 감쇠 비율 (decay ratio).
init_epoch : 고정된 init_learning_rate를 사용하는 에포크 수.
max_epoch : 훈련의 총 에포크 수.
input_size : 슬라이딩 윈도우의 크기 / 하나의 훈련 데이터 포인트 크기.
batch_size : 하나의 미니 배치 (mini-batch)에 사용할 데이터 포인트의 수.

LSTM 모델은 num_layers개의 쌓인 LSTM 레이어를 가지며, 각 레이어는 lstm_size개의 LSTM 셀 (cell)을 포함합니다. 그 후, 유지 확률(keep probability)이 keep_prob인 드롭아웃 마스크 (dropout mask)가 모든 LSTM 셀의 출력에 적용됩니다. 드롭아웃의 목표는 한 차원에 대한 잠재적인 강한 의존성을 제거하여 과적합 (overfitting)을 방지하는 것입니다.

훈련에는 총 max_epoch 에포크가 필요합니다. 에포크 (epoch)란 모든 훈련 데이터 포인트를 한 번 완전히 통과하는 것을 의미합니다. 한 에포크 내에서 훈련 데이터 포인트들은 batch_size 크기의 미니 배치로 나뉩니다.

하나의 BPTT (Backpropagation Through Time) 학습을 위해 하나의 미니 배치 (mini-batch)를 모델에 전달합니다. 학습률 (learning rate)은 처음 init_epoch 에포크 (epochs) 동안 init_learning_rate로 설정되며, 그 이후 매 에포크마다 $\times$ learning_rate_decay 비율로 감소합니다.

# Configuration is wrapped in one object for easy tracking and passing.
class RNNConfig():
input_size=1
...

그래프 정의 (Define Graph)

tf.Graph는 실제 데이터에 연결되어 있지 않습니다. 이는 데이터를 처리하는 방법과 연산을 실행하는 흐름을 정의합니다. 나중에 이 그래프는 tf.session 내에서 데이터가 주입될 수 있으며, 이 시점에 실제 연산이 일어납니다.

— 코드를 하나씩 살펴보겠습니다 —

(1) 먼저 새로운 그래프를 초기화합니다.

import tensorflow as tf
tf.reset_default_graph()
lstm_graph = tf.Graph()

(2) 그래프가 어떻게 작동할지는 해당 스코프 (scope) 내에서 정의되어야 합니다.

with lstm_graph.as_default():

(3) 연산에 필요한 데이터를 정의합니다. 여기에는 세 개의 입력 변수가 필요한데, 그래프 구축 단계에서는 이 값들이 무엇인지 알 수 없으므로 모두 tf.placeholder로 정의합니다.

inputs: 훈련 데이터 $X$로, shape이 (# data examples, num_steps, input_size)인 텐서 (tensor)입니다. 데이터 예시의 개수는 알 수 없으므로 None으로 설정합니다. 우리의 경우, 훈련 세션에서의 batch_size가 될 것입니다. 혼란스럽다면 입력 형식 예시를 확인하세요. targets: 훈련 라벨 $y$로, shape이 (# data examples, input_size)인 텐서입니다. learning_rate: 단순한 float 값입니다.

# Dimension = (
# number of data examples,
# number of input in one computation step,
...

(4) 이 함수는 드롭아웃 (dropout) 연산이 포함되거나 포함되지 않은 하나의 LSTMCell을 반환합니다.

def _create_one_cell():
return tf.contrib.rnn.LSTMCell(config.lstm_size, state_is_tuple=True)
if config.keep_prob < 1.0:
...

(5) 필요한 경우 셀들을 여러 레이어 (layers)로 쌓아봅시다. MultiRNNCell은 여러 개의 단순한 셀들을 순차적으로 연결하여 하나의 셀을 구성할 수 있도록 도와줍니다.

cell = tf.contrib.rnn.MultiRNNCell(
[_create_one_cell() for _ in range(config.num_layers)],
state_is_tuple=True
...

(6) tf.nn.dynamic_rnn

cell (RNNCell)에 의해 지정된 순환 신경망 (Recurrent Neural Network)을 구축합니다. 이 함수는 (모델 출력, 상태)의 쌍을 반환하며, 여기서 출력 val의 크기는 기본적으로 (batch_size, num_steps, lstm_size)입니다. 상태 (state)는 LSTM 셀의 현재 상태를 나타내며, 여기서는 사용되지 않습니다.

val, _ = tf.nn.dynamic_rnn(cell, inputs, dtype=tf.float32)

(7) tf.transpose

출력의 차원을 (batch_size, num_steps, lstm_size)에서 (num_steps, batch_size, lstm_size)로 변환합니다. 그 후 마지막 출력을 선택합니다.

# transpose 전, val.get_shape() = (batch_size, num_steps, lstm_size)
# transpose 후, val.get_shape() = (num_steps, batch_size, lstm_size)
val = tf.transpose(val, [1, 0, 2])
...

(8) 은닉층 (Hidden layer)과 출력층 (Output layer) 사이의 가중치 (Weights) 및 편향 (Biases)을 정의합니다.

weight = tf.Variable(tf.truncated_normal([config.lstm_size, config.input_size]))
bias = tf.Variable(tf.constant(0.1, shape=[config.input_size]))
prediction = tf.matmul(last, weight) + bias

(9) 손실 지표 (Loss metric)로 평균 제곱 오차 (Mean Square Error)를 사용하며, 경사 하강법 (Gradient descent) 최적화를 위해 RMSPropOptimizer 알고리즘을 사용합니다.

loss = tf.reduce_mean(tf.square(prediction - targets))
optimizer = tf.train.RMSPropOptimizer(learning_rate)
minimize = optimizer.minimize(loss)

훈련 세션 시작 (Start Training Session)

(1) 실제 데이터로 그래프 훈련을 시작하려면, 먼저 tf.session을 시작해야 합니다.

with tf.Session(graph=lstm_graph) as sess:

(2) 정의된 변수들을 초기화합니다.

tf.global_variables_initializer().run()

(0) 훈련 에포크 (Epoch)를 위한 학습률 (Learning rates)은 사전에 계산되어 있어야 합니다. 인덱스는 에포크 인덱스를 나타냅니다.

learning_rates_to_use = [
config.init_learning_rate * (
config.learning_rate_decay ** max(float(i + 1 - config.init_epoch), 0.0)
...

(3) 아래의 각 루프는 하나의 에포크 훈련을 완료합니다.

for epoch_step in range(config.max_epoch):
    current_lr = learning_rates_to_use[epoch_step]
    # https://github.com/lilianweng/stock-rnn/blob/master/data_wrapper.py 를 확인하세요
...

(4) 마지막에 훈련된 모델을 저장하는 것을 잊지 마세요.

saver = tf.train.Saver()
saver.save(sess, "your_awesome_model_path_and_name", global_step=max_epoch_step)

전체 코드는 여기에서 확인할 수 있습니다.

TensorBoard 사용하기

시각화 없이 그래프를 구축하는 것은 어둠 속에서 그림을 그리는 것과 같아서, 매우 모호하고 오류가 발생하기 쉽습니다. TensorBoard는 그래프 구조와 학습 과정에 대한 쉬운 시각화를 제공합니다. 단 20분 정도 소요되는 이 실습 튜토리얼을 확인해 보세요. 매우 실용적이며 여러 가지 라이브 데모를 보여줍니다.

요약

  • 유사한 목표를 위해 작동하는 요소들을 함께 묶으려면
    with [tf.name_scope](https://www.tensorflow.org/api_docs/python/tf/name_scope)("your_awesome_module_name"):를 사용하세요.
  • 많은 tf.* 메서드들은 name= 인자를 허용합니다. 사용자 정의 이름을 할당하면 그래프를 읽을 때 훨씬 수월해질 수 있습니다.
  • tf.summary.scalar와 같은 메서드들

AI 자동 생성 콘텐츠

본 콘텐츠는 Lilian Weng Blog의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
1

댓글

0