RNN을 이용한 주가 예측: Part 2
요약
본 튜토리얼은 순환 신경망(RNN)을 활용하여 여러 종목의 주가를 예측하는 방법을 다루며, 특히 각 주식 심볼에 대한 정보를 모델에 명시적으로 제공하는 데 초점을 맞춥니다. 이를 위해 주식 심볼 임베딩 벡터를 입력으로 추가하고, 가격 시퀀스와 결합하여 LSTM 셀에 입력합니다. 이 방식은 원-핫 인코딩보다 효율적이며, 유사한 주식 간의 관계 학습을 가능하게 합니다.
핵심 포인트
- 주가 예측 모델에 여러 종목 대응 능력을 부여하기 위해 주식 심볼 임베딩 벡터를 사용한다.
- 임베딩 벡터는 원-핫 인코딩 대비 차원 축소 및 압축된 표현을 제공하며, 유사한 주식 간의 관계 학습에 유리하다.
- 입력 시퀀스는 일일 가격 값과 해당 주식 심볼의 임베딩 벡터를 결합(concatenate)하여 LSTM 셀로 입력된다.
- 모델 구축 시 `embedding_size`와 `stock_count` 설정을 통해 임베딩 행렬을 정의하고, 이를 학습 가능한 변수로 활용한다.
Part 2 튜토리얼에서는 주가 예측에 관한 주제를 계속 이어가며, Part 1에서 구축한 순환 신경망 (Recurrent Neural Network, RNN)에 여러 종목에 대응할 수 있는 능력을 부여하고자 합니다. 서로 다른 가격 시퀀스 (Price sequences)와 관련된 패턴을 구분하기 위해, 입력의 일부로 주식 심볼 임베딩 벡터 (Stock symbol embedding vectors)를 사용합니다.
데이터셋 (Dataset)
검색 과정에서 Yahoo! Finance API를 쿼리하기 위한 이 라이브러리를 발견했습니다. Yahoo가 과거 데이터 가져오기 API를 중단하지 않았다면 매우 유용했을 것입니다. 하지만 다른 정보를 쿼리하는 데에는 유용할 수 있습니다. 여기서는 과거 주가를 다운로드할 수 있는 몇 가지 무료 데이터 소스 중에서 Google Finance 링크를 선택했습니다.
데이터 가져오기 코드는 다음과 같이 간단하게 작성할 수 있습니다:
import urllib2
from datetime import datetime
BASE_URL = "https://www.google.com/finance/historical?"
...
콘텐츠를 가져올 때, 링크가 실패하거나 제공된 주식 심볼이 유효하지 않을 경우를 대비하여 try-catch 래퍼 (wrapper)를 추가하는 것을 잊지 마세요.
try:
f = urllib2.urlopen(symbol_url)
with open("GOOG.csv", 'w') as fin:
...
전체 작동 가능한 데이터 가져오기 코드는 여기에서 확인할 수 있습니다.
모델 구축 (Model Construction)
모델은 시간에 따른 서로 다른 주식의 가격 시퀀스를 학습할 것으로 기대됩니다. 기저에 깔린 패턴이 다르기 때문에, 모델에게 어떤 주식을 다루고 있는지 명시적으로 알려주고 싶습니다. 임베딩 (Embedding)이 원-핫 인코딩 (One-hot encoding)보다 선호되는 이유는 다음과 같습니다:
- 훈련 세트(Train set)에 $N$개의 주식이 포함되어 있다고 가정할 때, 원-핫 인코딩은 $N$ (또는 $N-1$)개의 추가적인 희소 특징 차원 (Sparse feature dimensions)을 도입하게 됩니다. 각 주식 심볼이 길이가 $k$인 훨씬 작은 임베딩 벡터 ($k \ll N$)로 매핑되면, 결과적으로 훨씬 더 압축된 표현을 얻게 되며 처리해야 할 데이터셋도 작아집니다.
- 임베딩 벡터는 학습 가능한 변수입니다. 유사한 주식들은 유사한 임베딩과 연관될 수 있으며, 이는 서로의 예측에 도움을 줄 수 있습니다. 예를 들어, 나중에 보게 될 "GOOG"와 "GOOGL" 같은 경우입니다.
순환 신경망 (Recurrent Neural Network, RNN)에서, 한 타임 스텝 (time step) $t$에 입력 벡터는 $i$번째 주식의 input_size ( $w$로 표시됨) 일일 가격 값인 $(p_{i, tw}, p_{i, tw+1}, ext{...}, p_{i, (t+1)w-1})$을 포함합니다. 주식 심볼 (stock symbol)은 embedding_size ( $k$로 표시됨) 길이의 벡터 $(e_{i,0}, e_{i,1}, ext{...}, e_{i,k})$로 고유하게 매핑됩니다. 그림 1에서 보여주는 바와 같이, 가격 벡터는 임베딩 벡터 (embedding vector)와 결합 (concatenate)된 후 LSTM 셀 (cell)로 입력됩니다.
또 다른 대안은 임베딩 벡터를 LSTM 셀의 마지막 상태 (last state)와 결합하고 출력층 (output layer)에서 새로운 가중치 $W$와 편향 (bias) $b$를 학습하는 것입니다. 하지만 이 방식으로는 LSTM 셀이 한 주식의 가격과 다른 주식의 가격을 구분할 수 없으며, 그 성능이 크게 제한될 것입니다. 따라서 저는 전자의 방식을 선택하기로 했습니다.
RNNConfig에 두 가지 새로운 설정이 추가되었습니다:
embedding_size는 각 임베딩 벡터의 크기를 제어하며, stock_count는 데이터셋 내의 고유한 주식 수를 나타냅니다.
이 두 값은 함께 임베딩 행렬 (embedding matrix)의 크기를 정의하며, 모델은 Part 1의 모델과 비교했을 때 embedding_size $\times$ stock_count 만큼의 추가적인 변수를 학습해야 합니다.
class RNNConfig():
# ... 기존 설정들
embedding_size = 3
...
그래프 정의 (Define the Graph)
— 코드를 살펴보겠습니다 —
(1) 튜토리얼 Part 1: 그래프 정의 (Define the Graph)에서 보여준 것과 같이, lstm_graph라는 이름의 tf.Graph()와 입력 데이터를 담기 위한 텐서 (tensor) 세트인 inputs, targets, learning_rate를 동일한 방식으로 정의하겠습니다. 하나 더 정의해야 할 플레이스홀더 (placeholder)는 입력 가격과 연관된 주식 심볼의 리스트입니다. 주식 심볼은 레이블 인코딩 (label encoding)을 통해 사전에 고유한 정수로 매핑되었습니다.
# 정수로 매핑됨. 하나의 레이블은 하나의 주식 심볼을 나타냄.
stock_labels = tf.placeholder(tf.int32, [None, 1])
(2) 그다음으로 모든 주식의 임베딩 벡터 (embedding vectors)를 포함하는 룩업 테이블 (lookup table) 역할을 할 임베딩 행렬 (embedding matrix)을 설정해야 합니다. 이 행렬은 [-1, 1] 구간의 난수로 초기화되며 학습 과정 동안 업데이트됩니다.
# NOTE: config = RNNConfig() 이며 하이퍼파라미터 (hyperparameters)를 정의합니다.
# 정수 레이블을 수치형 임베딩 벡터로 변환합니다.
embedding_matrix = tf.Variable(
...
(3) RNN의 언폴딩 (unfolded) 버전 및 학습 중 inputs 텐서의 형상 (shape)과 일치하도록 주식 레이블 (stock labels)을 num_steps 번 반복합니다.
변환 연산인 tf.tile은 베이스 텐서 (base tensor)를 받아 특정 차원을 여러 번 복제하여 새로운 텐서를 생성합니다. 정확히는 입력 텐서의 $i$번째 차원이 multiples[i] 배만큼 곱해집니다. 예를 들어, stock_labels가 [[0], [0], [2], [1]]인 경우, 이를 [1, 5]로 타일링 (tiling)하면 [[0 0 0 0 0], [0 0 0 0 0], [2 2 2 2 2], [1 1 1 1 1]]이 생성됩니다.
stacked_stock_labels = tf.tile(stock_labels, multiples=[1, config.num_steps])
(4) 그다음 룩업 테이블인 embedding_matrix에 따라 심볼을 임베딩 벡터로 매핑 (map)합니다.
# stock_label_embeds.get_shape() = (?, num_steps, embedding_size).
stock_label_embeds = tf.nn.embedding_lookup(embedding_matrix, stacked_stock_labels)
(5) 마지막으로, 가격 값 (price values)을 임베딩 벡터와 결합합니다. tf.concat 연산은 axis 차원을 따라 텐서 리스트를 연결 (concatenate)합니다. 우리의 경우, 배치 크기 (batch size)와 스텝 수 (number of steps)는 변경하지 않으면서, 길이가 input_size인 입력 벡터를 확장하여 임베딩 특징 (embedding features)을 포함시키고자 합니다.
# inputs.get_shape() = (?, num_steps, input_size)
# stock_label_embeds.get_shape() = (?, num_steps, embedding_size)
# inputs_with_embeds.get_shape() = (?, num_steps, input_size + embedding_size)
...
나머지 코드는 동적 RNN (dynamic RNN)을 실행하고, LSTM 셀의 마지막 상태 (last state)를 추출하며, 출력 레이어 (output layer)의 가중치 (weights)와 편향 (bias)을 처리합니다. 자세한 내용은 Part 1: Define the Graph를 참조하세요.
Training Session
Tensorflow에서 훈련 세션 (training session)을 실행하는 방법에 대해 아직 읽지 않으셨다면, Part 1: Start Training Session을 읽어주시기 바랍니다.
데이터를 그래프에 입력하기 전에, 주식 심볼 (stock symbols)은 레이블 인코딩 (label encoding)을 통해 고유한 정수로 변환되어야 합니다.
from sklearn.preprocessing import LabelEncoder
label_encoder = LabelEncoder()
label_encoder.fit(list_of_symbols)
훈련/테스트 분할 비율은 모든 개별 주식에 대해 훈련 90%, 테스트 10%로 동일하게 유지됩니다.
Visualize the Graph
코드에서 그래프가 정의된 후, 구성 요소들이 올바르게 구축되었는지 확인하기 위해 Tensorboard에서 시각화를 확인해 보겠습니다. 본질적으로 이는 우리의 아키텍처 삽화와 매우 유사해 보입니다.
Tensorboard는 그래프 구조를 제시하거나 시간에 따른 변수 (variables)를 추적하는 것 외에도, **임베딩 시각화 (embeddings visualization)**를 지원합니다. 임베딩 값을 Tensorboard에 전달하기 위해서는 훈련 로그 (training logs)에 적절한 추적 (tracking) 기능을 추가해야 합니다.
(0) 나의 임베딩 시각화에서는 각 주식을 해당 산업 섹터 (industry sector)별로 색상을 지정하고 싶습니다. 이 메타데이터 (metadata)는 csv 파일에 저장되어야 합니다. 이 파일은 주식 심볼과 산업 섹터라는 두 개의 열을 가집니다. csv 파일에 헤더 (header)가 있는지 여부는 상관없지만, 나열된 주식의 순서는 label_encoder.classes_와 일치해야 합니다.
import csv
embedding_metadata_path = os.path.join(your_log_file_folder, 'metadata.csv')
with open(embedding_metadata_path, 'w') as fout:
...
(1) 먼저 훈련 tf.Session 내에 서머리 라이터 (summary writer)를 설정합니다.
from tensorflow.contrib.tensorboard.plugins import projector
with tf.Session(graph=lstm_graph) as sess:
summary_writer = tf.summary.FileWriter(your_log_file_folder)
...
(2) 우리 그래프인 lstm_graph에 정의된 텐서 embedding_matrix를 프로젝터 설정 (projector config) 변수에 추가하고 메타데이터 csv 파일을 첨부합니다.
projector_config = projector.ProjectorConfig()
# 여러 개의 임베딩을 추가할 수 있습니다. 여기서는 하나만 추가합니다.
added_embedding = projector_config.embeddings.add()
...
(3) 이 라인은 your_log_file_folder 폴더 안에 projector_config.pbtxt 파일을 생성합니다.
TensorBoard는 시작 시 이 파일을 읽습니다.
projector.visualize_embeddings(summary_writer, projector_config)
결과 (Results)
모델은 S&P 500 지수에서 시가총액이 가장 큰 상위 50개 주식으로 학습됩니다.
(github.com/lilianweng/stock-rnn 내에서 다음 명령어를 실행하세요)
python main.py --stock_count=50 --embed_size=3 --input_size=3 --max_epoch=50 --train
그리고 다음과 같은 설정이 사용됩니다:
stock_count = 100
input_size = 3
embed_size = 3
...
가격 예측 (Price Prediction)
예측 품질에 대한 간략한 개요로서, 그림 3(Fig. 3)은 “KO”, “AAPL”, “GOOG” 및 “NFLX”의 테스트 데이터에 대한 예측값을 나타냅니다. 전체적인 추세는 실제 값(true values)과 예측값(predictions) 사이에서 일치했습니다. 예측 작업이 설계된 방식을 고려할 때, 모델은 오직 다음 5일(input_size)만을 예측하기 위해 모든 과거 데이터 포인트에 의존합니다. input_size가 작기 때문에 모델은 장기적인 성장 곡선(long-term growth curve)에 대해 걱정할 필요가 없습니다. 일단 input_size를 늘리면 예측은 훨씬 더 어려워질 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Lilian Weng Blog의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기