텐서플로우로 구글 클라우드 플랫폼 상에서 금융 데이터를 기계 학습하기

이 솔루션은 구글 클라우드 플랫폼(이하 GCP) 상에서 금융 시계열 데이터로 기계 학습을 한 실제 사례를 소개합니다.

시계열 데이터는 금융 분석의 핵심 분야 입니다. 오늘날, 당신은 다양한 출처로부터 더 빈번하게 배달되어 오는 더 많은 데이터를 가지게 되었습니다. 이런 출처에는 새로운 교류, 소셜 미디어 아웃렛, 그리고 뉴스를 포함합니다. 또한 10년 전 초당 수십 개의 메세지로부터 오늘날에 이르러 수십만 개로 전달 빈도가 증가했습니다. 이를 극복하기 위한 결과로써 자연적으로 다양한 분석 기법들이 등장했습니다. 현대 분석 기법들의 대부분은 통계를 기반으로 하고 있다는 면에서 새롭지는 않지만 적용 가능성이 가용한 컴퓨팅 파워의 증가 추세를 따르고 있다는 점에 주목할 필요가 있습니다. 이렇게 가용한 컴퓨팅 파워의 성장 속도가 시계열 데이터의 증가보다 더 빨라서 규모의 문제로 인해 이전에는 실용적이지 않아 불가능했던 시계열 데이터를 분석할 수 있게 되었습니다.

특히, 딥러닝과 같은 기계학습 기법은 시계열 분석에 희망적입니다. 시계열 데이터가 점점 더 조밀해지고 많은 부분에서 겹쳐지게 될수록 기계학습은 소음이 엄청나게 보일지라도 소음에서 신호를 분리해 내는 방법을 제공할 것입니다. 딥러닝은 금융의 시계열 데이터가 보여주는 무작위적 특성에서도 최적 해(best fit)를 찾을 수 있기에 엄청난 가능성을 가지고 있습니다.

이 솔루션에서는 당신은 다음과 같은 일을 수행하게 됩니다:

  • 여러 금융 시장에서 데이터 획득
  • 데이터를 쓸수 있는 형태로 분리하고 전제(premise)를 탐색하고 검증하기 위해 탐색적 데이터 분석 (EDA)를 수행
  • 텐서플로우로 금융 시장에서 일어날 일들을 예측하기 위한 여러 모델을 만들고, 학습(train)해서 평가

당신은 이 모든 일들을 클라우드 데이터랩 노트북에서 수행하게 될 것입니다.

경고: 이 솔루션은 GCP와 텐서플로우가 빠르고 상호작용적이며 반복적인 데이터 분석과 기계학습에 적절한 지 보여주기 위해 만들어 졌습니다. 따라서 이 솔루션은 금융 시장이나 매매 전략에 어떠한 조언도 제공하지 않습니다. 이 설명서에 나타난 시나리오는 설명서에 나타난 시나리오는 예제에 불과합니다. 절대 투자를 결정하는 데 있어 이 코드를 사용하지 마십시오!

선행 요건

노트: 클라우드 데이터랩 상의 이 노트북을 읽거나 가치를 끌어내기 위해서 복제 본을 수행하거나 다운로드할 필요는 없습니다. 그러나 이것을 변경시키거나 적극적으로 장려하는 자신의 목적에 맞게 시험해 보기 원한다면 다음 선행 요건을 완료해야 합니다.

전제(The premise)

여기의 전제는 명료합니다: 금융시장이 세계화되어서 만일 태양이 뜨는 순서를 따라 아시아로부터 유럽 그리고 미국 등으로 따라가면 빠른 시간대에서 얻은 정보로 늦은 시간대의 국가에서 이득을 볼 수 있을 것입니다.

다음 표는 전 세계의 많은 주식 시장과 미국 동부 시간대(EST) 기준의 폐장 시장, 그리고 뉴욕의 S&P 500의 마감 시간 간의 차이를 보여줍니다. 이 표에서는 EST를 기준 시간 대로 사용하는데 예를 들면, 호주 시장은 미국 시장이 폐장하기 15시간 전에 닫습니다. 만일 호주의 All Ords(AOI)의 마감이 주어진 날의 S&P 500 마감의 유용한 예측 변수(predictor)가 된다면, 우리는 이 정보를 우리의 거래 지침을 주는 정보로 활용할 수 있습니다. 호주의 AOI를 일례로 계속해보죠. 만일, AOI 지수가 마감되고 이후 S&P 500 또한 머지않아 마감될 것이기에 이것으로 우리는 S&P 500에 포함된 주식 (혹은 보다 그럴듯하게 S&P 500를 따르는 ETF)를 살 지를 고려해야 한다고 합시다. 사실 현실에서는 상황은 훨씬 더 복잡한데 그 이유는 수수료와 관련 세금이 추가되기 때문입니다. 그러나, 처음 근사 치로써, 우리는 이 마감 지수가 이득을 나타내고 그 반대로도 그렇다고 가정할 것입니다.

인덱스 국가 폐장시간 (EST) S&P 폐장 시간과의 차이
All Ords 호주 0100 15
Nikkei 225 일본 0200 14
Hang Seng 홍콩 0400 12
DAX 독일 1130 4.5
FTSE 100 영국 1130 4.5
NYSE Composite 미국 1600 0
Dow Jones Industrial Average 미국 1600 0
S&P 500 미국 1600 0

셋업

먼저, 필요한 라이브러리들을 불러옵시다.

불러들인 라이브러리들은 다음과 같습니다:

  • pd로 지칭(alias)된 pandas는 BSD 라이센스의 라이브러리로 고 성능의 손 쉬운 데이터 구조와 데이터 분석 도구를 제공합니다. 이 라이브러리의 auto correlation plot과 scatter matrix를 에서 도식화를 위해 활용하고 있습니다.
  • numpy 역시 BSD 라이센스의 라이브러리로 파이썬에서 과학적 컴퓨팅을 위한 기본 패키지이며 N차원 배열 연산을 손쉽게 수행할 수 있는 기능들을 제공합니다.
  • matplotlib은 2차원 차트 라이브러리를 손쉽게 그릴 수 있도록 해 줍니다.
  • gcp는 Google Cloud Platform의 기능을 데이터랩에서 손쉽게 활용할 수 있도록 제공된 라이브러리 입니다. 이 예제에서는 big query 서비스를 이용합니다. 자세한 내용은 데이터랩의 README를 참고하세요.
  • tensorflow는 딥러닝을 위해 제공되는 라이브러리입니다.
In [3]:
import StringIO

import pandas as pd
from pandas.tools.plotting import autocorrelation_plot
from pandas.tools.plotting import scatter_matrix

import numpy as np

import matplotlib.pyplot as plt

import gcp
import gcp.bigquery as bq

import tensorflow as tf

데이터 얻어오기

이 데이터에는 2010년 1월 1일부터 2015년 10월 1일까지 지난 5년 여 간의 기록을 담고 있습니다. 데이터는 S&P 500 (S&P), NYSE, Dow Jones Industrial Average (DJIA), Nikkei 225 (Nikkei), Hang Seng, FTSE 100 (FTSE), DAX, and All Ordinaries (AORD) 인덱스에서 왔습니다.

이 데이터는 공개되어 있고 편의를 위해 빅쿼리에 저장되어 있으며 클라우드 데이터랩의 빌트인 컨넥터 기능으로 Pandas 데이터프레임으로 가져올 수 있게 됩니다.

먼저, %%로 시작하는 명령은 노트북 외부의 bash command를 실행할 수 있도록 제공되는 기능입니다. 코드에서는 $market_data_table 테이블에서 SQL 문을 통해 Date와 Close 컬럼을 가져와서 market_data_query 모듈에 저장한 다음 빅쿼리 API로 각각 데이터프레임으로 저장하게 됩니다. 자세한 예제는 빅쿼리 튜토리얼 을 참고하시기 바랍니다.

In [4]:
%%sql --module market_data_query
SELECT Date, Close FROM $market_data_table
In [5]:
snp = bq.Query(market_data_query, market_data_table=bq.Table('bingo-ml-1:market_data.snp')).to_dataframe().set_index('Date')
nyse = bq.Query(market_data_query, market_data_table=bq.Table('bingo-ml-1:market_data.nyse')).to_dataframe().set_index('Date')
djia = bq.Query(market_data_query, market_data_table=bq.Table('bingo-ml-1:market_data.djia')).to_dataframe().set_index('Date')
nikkei = bq.Query(market_data_query, market_data_table=bq.Table('bingo-ml-1:market_data.nikkei')).to_dataframe().set_index('Date')
hangseng = bq.Query(market_data_query, market_data_table=bq.Table('bingo-ml-1:market_data.hangseng')).to_dataframe().set_index('Date')
ftse = bq.Query(market_data_query, market_data_table=bq.Table('bingo-ml-1:market_data.ftse')).to_dataframe().set_index('Date')
dax = bq.Query(market_data_query, market_data_table=bq.Table('bingo-ml-1:market_data.dax')).to_dataframe().set_index('Date')
aord = bq.Query(market_data_query, market_data_table=bq.Table('bingo-ml-1:market_data.aord')).to_dataframe().set_index('Date')

데이터 정리하기(Munge the data)

첫번째 시도로써 데이터를 정리하는 건 명확합니다. 종가(closing prices)가 관심사이기 때문에 편의를 위해 각각의 인덱스에서 종가를 추출해서 closing_data 라는 Pandas 데이터프레임에 저장합니다. 모든 인덱스 값들이 주로 거래소 휴장등의 이유로 값들을 가지지 않는 경우가 있기 때문에 이 차이를 미리 메꿀(forward-fill) 겁니다. 즉, 어떤 날(day N)에 값이 없다면 전날(N-1)이나 그 전전날(N-2)의 값을 채워서 최신 값이 포함되도록 합니다.

In [4]:
closing_data = pd.DataFrame()

closing_data['snp_close'] = snp['Close']
closing_data['nyse_close'] = nyse['Close']
closing_data['djia_close'] = djia['Close']
closing_data['nikkei_close'] = nikkei['Close']
closing_data['hangseng_close'] = hangseng['Close']
closing_data['ftse_close'] = ftse['Close']
closing_data['dax_close'] = dax['Close']
closing_data['aord_close'] = aord['Close']

# Pandas includes a very convenient function for filling gaps in the data.
closing_data = closing_data.fillna(method='ffill')

지금 이 순간, 이 노트북의 코드 20 여 줄로 여덟 개의 금융 인덱스들에서 각각 5년 간의 시 계열 데이터를 확보했고 관련된 데이터를 얽어서 하나의 데이터 구조로 만든 후, 같은 수의 엔트리를 갖도록 일치시켰습니다. 덧붙여, 이 모든 것을 수행하는 데 약 10초 밖에 걸리지 않습니다. 이는 감동적인 부분인데, 첫번째 iPython 노트북을 통해 파이썬의 우수함을 활용하고 두번째 GCP 서비스에 연결할 수 있는 호스트를 제공함으로써 어떻게 구글 클라우드 데이터랩이 생산성을 확대하는가에 보여줍니다. 당신이 지금 빅쿼리만 써 봤고 구글 클라우드 스토리지를 경험하지 않았지만, 점차 이런 컨넥터의 수가 증가하는 것이 눈에 보이길 기대하게 될 것입니다.

탐색적 데이터 분석(EDA)

탐색적 데이터 분석(EDA)는 기계학습으로 작업하는 것과 다른 종류의 어떤 분석들의 기초가 됩니다. EDA는 데이터를 점점 더 이해하게 되고, 데이터에 손을 더럽히게 되며, 데이터를 보고 느끼는 것을 의미합니다. 그 최종 결과는 데이터를 매우 잘 이해할 수 있게되어 모델을 세울 때 그 모델을 모호한 편견이나 가정이 아닌 현실적이고 실제적이자 구체적인 이해를 기반으로 만들 수 있게 됩니다.
물론 당신은 여전히 어떤 가정들을 만들 수 있겠지만 EDA는 당신의 가정과 이 가정들을 왜 만들게 되는 지를 이해하게 된다는 것을 의미하게 됩니다.

먼저, 다음 데이터를 살펴 봅시다.

In [5]:
closing_data.describe()
Out[5]:
snp_close nyse_close djia_close nikkei_close hangseng_close ftse_close dax_close aord_close
count 1447.000000 1447.000000 1447.000000 1447.000000 1447.000000 1447.000000 1447.000000 1447.000000
mean 1549.733275 8920.468489 14017.464990 12529.915089 22245.750485 6100.506356 7965.888030 4913.770143
std 338.278280 1420.830375 2522.948044 3646.022665 2026.412936 553.389736 1759.572713 485.052575
min 1022.580017 6434.810059 9686.480469 8160.009766 16250.269531 4805.799805 5072.330078 3927.600098
25% 1271.239990 7668.234863 11987.635254 9465.930176 20841.259765 5677.899902 6457.090088 4500.250000
50% 1433.189941 8445.769531 13323.360352 10774.150391 22437.439453 6008.899902 7435.209961 4901.100098
75% 1875.510010 10370.324707 16413.575196 15163.069824 23425.334961 6622.650147 9409.709961 5346.150147
max 2130.820068 11239.660156 18312.390625 20868.029297 28442.750000 7104.000000 12374.730469 5954.799805

다음과 같이 수의 비교(Orders of magnitude)로 다르게 한 비례를 기반으로 운영되는 다양한 인덱스를 볼 수 있습니다. 이 데이터를 비율로 맞추는 것이 좋은데 예를 들어, 하나의 엄청나게 큰 인덱스에 의해 여러 인덱스를 포함하는 동작에 과도하게 영향을 미치지 않게 되기 때문입니다.

이 데이터를 그려봅시다.

코드는 paddas의 concatenate 함수를 통해, 각 인덱스 별 종가 데이터를 연결하고 concaternated objects에 plot 함수로 데이터를 line chart로 도식화합니다. 여기서 인자 axis는 연결할 축 갯수를 지정하고 figsize는 인치 당 튜플(넓이, 높이)를 지정합니다. 반환된 값을 저장하는 _ 변수는 고의적으로 버리려는(throwaway) 목적으로 관습적 명명법에 따라 정의했습니다. 주석에 따르면 처리 결과를 보여주지 않을 목적으로 활용되었다고 합니다.

In [35]:
# N.B. A super-useful trick-ette is to assign the return value of plot to _ 
# so that you don't get text printed before the plot itself.

_ = pd.concat([closing_data['snp_close'],
  closing_data['nyse_close'],
  closing_data['djia_close'],
  closing_data['nikkei_close'],
  closing_data['hangseng_close'],
  closing_data['ftse_close'],
  closing_data['dax_close'],
  closing_data['aord_close']], axis=1).plot(figsize=(20, 15))

기대했던 것처럼, 인덱스 별로 구조가 동일하게 보이지 않습니다. 각각의 인덱스의 값들을 그 인덱스의 최대 값으로 나누고 다시 도식을 그려 봅니다. 모든 인덱스의 최대 값은 1이 될 것입니다. 코드는 이 값들을 *_close_scaled 를 postfix로 붙여 pandas 데이터프레임에 저장합니다.

In [36]:
closing_data['snp_close_scaled'] = closing_data['snp_close'] / max(closing_data['snp_close'])
closing_data['nyse_close_scaled'] = closing_data['nyse_close'] / max(closing_data['nyse_close'])
closing_data['djia_close_scaled'] = closing_data['djia_close'] / max(closing_data['djia_close'])
closing_data['nikkei_close_scaled'] = closing_data['nikkei_close'] / max(closing_data['nikkei_close'])
closing_data['hangseng_close_scaled'] = closing_data['hangseng_close'] / max(closing_data['hangseng_close'])
closing_data['ftse_close_scaled'] = closing_data['ftse_close'] / max(closing_data['ftse_close'])
closing_data['dax_close_scaled'] = closing_data['dax_close'] / max(closing_data['dax_close'])
closing_data['aord_close_scaled'] = closing_data['aord_close'] / max(closing_data['aord_close'])
In [37]:
_ = pd.concat([closing_data['snp_close_scaled'],
  closing_data['nyse_close_scaled'],
  closing_data['djia_close_scaled'],
  closing_data['nikkei_close_scaled'],
  closing_data['hangseng_close_scaled'],
  closing_data['ftse_close_scaled'],
  closing_data['dax_close_scaled'],
  closing_data['aord_close_scaled']], axis=1).plot(figsize=(20, 15))

보는 것처럼 지난 5년 동안 이 인덱스들 사이의 상관 관계가 있음을 알 수 있습니다. 국제적인 경제 사건 때문에 급락이 있었고 그 외 다른 것들로 일반적인 상승이 있었음을 보여줍니다. 다음으로 인덱스들 각각의 autocorrelations를 그려봅니다. 이것은 한 인덱스의 현재 값들과 같은 인덱스의 지체된 값들 사이의 상관관계를 결정합니다. 지연된 값들이 현재 값들의 믿을만한 척도(indicator)가 될 수 있을지 결정하는 것이 목표입니다. 만일 인덱스들이 그렇다면, 우리는 상관 관계를 파악하게 된 것입니다.

fig에는 matplotlib이 제공하는 모든 plot element를 담고 있는 모듈이 저장됩니다. pandas 라이브러리에서 가져온 autocorrelation_plot 함수는 시계열 데이터에서의 임의성(randomness)를 확인하는데 종종 쓰입니다. legend 함수는 도표의 범례를 표시할 때 활용합니다.

In [38]:
fig = plt.figure()
fig.set_figwidth(20)
fig.set_figheight(15)

_ = autocorrelation_plot(closing_data['snp_close'], label='snp_close')
_ = autocorrelation_plot(closing_data['nyse_close'], label='nyse_close')
_ = autocorrelation_plot(closing_data['djia_close'], label='djia_close')
_ = autocorrelation_plot(closing_data['nikkei_close'], label='nikkei_close')
_ = autocorrelation_plot(closing_data['hangseng_close'], label='hangseng_close')
_ = autocorrelation_plot(closing_data['ftse_close'], label='ftse_close')
_ = autocorrelation_plot(closing_data['dax_close'], label='dax_close')
_ = autocorrelation_plot(closing_data['aord_close'], label='aord_close')

_ = plt.legend(loc='upper right')

이것으로 강한 autocorrelations를 볼 수 있으며 500일 정도 지연된 때까지 양의 관계가 되며 그 이후로 음의 관계가 됩니다. 이것은 우리가 직관적으로 알고있는 사실 즉, 어떤 인덱스가 상승장(rising)일 때 계속 상승하려고 하고 하락장일 때는 하락하려고 한다는 것을 보여줍니다.

다음으로 인덱스 각각이 어떻게 연관(correlated)되어 있는지를 보기 위해, 모든 것에 대해 모든 것을 그린 것을 보여주는 scatter matrix를 봅시다.

활용한 pandas 의 scatter_matrix 함수는 diagonal plot를 함께 그려주는 데 자세한 설명은 링크를 참고하세요.

In [39]:
_ = scatter_matrix(pd.concat([closing_data['snp_close_scaled'],
  closing_data['nyse_close_scaled'],
  closing_data['djia_close_scaled'],
  closing_data['nikkei_close_scaled'],
  closing_data['hangseng_close_scaled'],
  closing_data['ftse_close_scaled'],
  closing_data['dax_close_scaled'],
  closing_data['aord_close_scaled']], axis=1), figsize=(20, 20), diagonal='kde')

이 도표로 모든 인덱스들이 강한 상관관계가 있으며, 우리의 전제가 동작하고, 한 시장이 다른 시장에 영향을 미칠 수 있다는 증거를 찾을 수 있습니다.

곁가지로, 이 점진적으로 증가하는 실험법과 진행은 가장 좋은 접근법이자 당신이 아마도 일반적으로 할 일입니다. 조금만 인내심을 가지면 우리는 더 깊은 이해를 갖게 될 것입니다.

어떤 인덱스의 실제 값은 모델링에 유용하지는 않습니다. 이것은 쓸만한 척도가 될 수 있겠지만, 핵심적인 측면에서는 평균(mean) 으로 고정시킨 시계열 데이터가 필요하고 그러므로 데이터에는 경향성(trend)이 없습니다. 이것을 하는 여러가지 방법이 있겠지만 이 모두는 절대 값을 보기 보다는 본질적으로는 값들 사이의 차이를 보는 것입니다. 시장 데이터의 경우에 기록된 결과를 다루는 보편적인 방식은 다음과 같이 어제 인덱스로 오늘 인덱스를 나눈 후 자연 로그를 취한 값을 계산하는 방법입니다:

ln(Vt/Vt-1)

백분율 반환보다 로그 반환이 선호되는 다양한 이유가 있는데 예를 들면 로그가 정규 분포를 따르고 합산이 가능하다는 점을 들 수 있습니다. 하지만, 이 작업에는 큰 상관은 없습니다. 여기서 단지 관심있는 것은 고정된 시계열 데이터를 얻는 것뿐입니다.

이 로그 반환을 계산해서 새로운 데이터프레임에 넣고 그려봅시다.

shift 함수는 데이터 프레임의 인덱스를 지정된 periods 값 만큼 이동시킵니다. 생략 시 기본 값은 1이기 때문에 배열 인덱스가 오른쪽으로 하나 증가하면서 아래와 같이 어제의 값이 됩니다.

Index 0 1 2 3 4 5
before shift D1 D2 D3 D4 D5 D6
after shift NA D1 D2 D3 D4 D5
In [40]:
log_return_data = pd.DataFrame()

log_return_data['snp_log_return'] = np.log(closing_data['snp_close']/closing_data['snp_close'].shift())
log_return_data['nyse_log_return'] = np.log(closing_data['nyse_close']/closing_data['nyse_close'].shift())
log_return_data['djia_log_return'] = np.log(closing_data['djia_close']/closing_data['djia_close'].shift())
log_return_data['nikkei_log_return'] = np.log(closing_data['nikkei_close']/closing_data['nikkei_close'].shift())
log_return_data['hangseng_log_return'] = np.log(closing_data['hangseng_close']/closing_data['hangseng_close'].shift())
log_return_data['ftse_log_return'] = np.log(closing_data['ftse_close']/closing_data['ftse_close'].shift())
log_return_data['dax_log_return'] = np.log(closing_data['dax_close']/closing_data['dax_close'].shift())
log_return_data['aord_log_return'] = np.log(closing_data['aord_close']/closing_data['aord_close'].shift())

log_return_data.describe()
Out[40]:
snp_log_return nyse_log_return djia_log_return nikkei_log_return hangseng_log_return ftse_log_return dax_log_return aord_log_return
count 1446.000000 1446.000000 1446.000000 1446.000000 1446.000000 1446.000000 1446.000000 1446.000000
mean 0.000366 0.000203 0.000297 0.000352 -0.000032 0.000068 0.000313 0.000035
std 0.010066 0.010538 0.009287 0.013698 0.011779 0.010010 0.013092 0.009145
min -0.068958 -0.073116 -0.057061 -0.111534 -0.060183 -0.047798 -0.064195 -0.042998
25% -0.004048 -0.004516 -0.003943 -0.006578 -0.005875 -0.004863 -0.005993 -0.004767
50% 0.000628 0.000551 0.000502 0.000000 0.000000 0.000208 0.000740 0.000406
75% 0.005351 0.005520 0.005018 0.008209 0.006169 0.005463 0.006807 0.005499
max 0.046317 0.051173 0.041533 0.074262 0.055187 0.050323 0.052104 0.034368

로그 반환 값을 살펴보면, 평균값, 최소값, 최대값 모두 유사한 걸 볼 수 있습니다. 더 나아가 0으로 중앙을 맞추고, 스케일(scale)을 맞춘 후, 표준 편차를 정규화(normalize)할 수 있지만 이 시점에서는 그럴 필요가 없습니다. 데이터를 도식화하면서 나아가면서 필요할 때 반복합니다. 모든 인덱스들의 로그 반환 값을 연결한 후 다시 그림을 그려 봅니다.

In [41]:
_ = pd.concat([log_return_data['snp_log_return'],
  log_return_data['nyse_log_return'],
  log_return_data['djia_log_return'],
  log_return_data['nikkei_log_return'],
  log_return_data['hangseng_log_return'],
  log_return_data['ftse_log_return'],
  log_return_data['dax_log_return'],
  log_return_data['aord_log_return']], axis=1).plot(figsize=(20, 15))

이제 인덱스들의 로그 반환 값들이 비슷한 규모로 조정되고 중앙에 정렬된 도표를 볼 수 있는데 이 데이터에서는 어떤 추세도 보이지 않습니다. 좋아 보이니 이제 autocorrelations를 봅시다.

이 코드는 앞서 그린 autocorrelation plot에 로그 반환 값들을 데이터로 바꾼 것입니다.

In [42]:
fig = plt.figure()
fig.set_figwidth(20)
fig.set_figheight(15)

_ = autocorrelation_plot(log_return_data['snp_log_return'], label='snp_log_return')
_ = autocorrelation_plot(log_return_data['nyse_log_return'], label='nyse_log_return')
_ = autocorrelation_plot(log_return_data['djia_log_return'], label='djia_log_return')
_ = autocorrelation_plot(log_return_data['nikkei_log_return'], label='nikkei_log_return')
_ = autocorrelation_plot(log_return_data['hangseng_log_return'], label='hangseng_log_return')
_ = autocorrelation_plot(log_return_data['ftse_log_return'], label='ftse_log_return')
_ = autocorrelation_plot(log_return_data['dax_log_return'], label='dax_log_return')
_ = autocorrelation_plot(log_return_data['aord_log_return'], label='aord_log_return')

_ = plt.legend(loc='upper right')

도표에 우리가 바랬던 것처럼 어떤 autocorrelation도 보이지 않습니다. 각각의 금융시장은 마코프(Markov) 프로세스이고 역사에 대한 지식이 미래를 예측하는데 도움이 되지 않습니다.

당신은 인덱스 별로 시계열 데이터와 비슷하게 중앙 정렬되고 크기 조절된 고정된 평균을 보유하게 되었습니다. 참 멋진 일이죠! 이제 S&P 500의 종가를 예측하는 시도에 대한 시그널을 살펴 봅시다.

어떻게 로그 반환 인덱스 값들이 각기 상관 관계가 있는지 보여주는 산점도(scattertplot)를 봅시다.

앞서 사용했던 scatter_matrix 함수에 로그 반환 데이터를 입력하면 됩니다.

In [43]:
_ = scatter_matrix(log_return_data, figsize=(20, 20), diagonal='kde')

이전의 로그 반환 값에 대한 산점도에 대한 이야기가 보다 미묘하고 흥미롭습니다. 예상했던 것처럼 미국의 인덱스들 간에 강한 상관 관계가 있는데 다른 인덱스들은 역시 예상처럼 덜 그렇습니다. 그러나 여기에는 어떤 구조와 신호가 있습니다. 이제 더 나아가, 우리의 모델을 위한 피처(feature)를 잡을 수 있게 정량화 해보겠습니다.

먼저 어떤 동일한 날에 S&P 500의 종가의 로그 반환 값이 가용한 다른 인덱스들의 종가에 어떤 상관 관계가 있는지 살펴 봅시다. 이는 핵심적으로 S&P 500 이전에 마감하는 미국이 아닌 시장(market)의 인덱스들이 존재하고 미국 시장은 그렇지 않다는 가정을 의미합니다.

pandas의 데이터프레임 형식인 tmp 임시 변수에 S&P(미국)의 오늘 데이터, NYSE(미국)과 다우존스(미국)의 어제 데이터, FTSE(영국), DAX(독일), Hang Seng(홍콩), NIKKEI(일본), AORD(호주)의 어제 데이터를 저장합니다. corr 함수는 모든 컬럼(column)에 대한 상관계수를 계산해 주고 iloc는 순수하게 정수 위치 기반 인덱싱(indexing)으로 위치에 따라 item을 선택하는 함수로 위치를 통한 선택 에 활용법이 자세히 기술되어 있습니다. 이 코드에서는 {시작인덱스}:{끝인덱스}로 모든 배열을 포함해 나누기(slice) 연산한 결과와 더불어 0번 인덱스 값을 선택하고 있습니다. (XXX:별 의미가 없는 것 같은데 왜 이렇게 짰는지...)

In [44]:
tmp = pd.DataFrame()
tmp['snp_0'] = log_return_data['snp_log_return']
tmp['nyse_1'] = log_return_data['nyse_log_return'].shift()
tmp['djia_1'] = log_return_data['djia_log_return'].shift()
tmp['ftse_0'] = log_return_data['ftse_log_return']
tmp['dax_0'] = log_return_data['dax_log_return']
tmp['hangseng_0'] = log_return_data['hangseng_log_return']
tmp['nikkei_0'] = log_return_data['nikkei_log_return']
tmp['aord_0'] = log_return_data['aord_log_return']
tmp.corr().iloc[:,0]
Out[44]:
snp_0         1.000000
nyse_1       -0.038903
djia_1       -0.047759
ftse_0        0.656523
dax_0         0.654757
hangseng_0    0.205776
nikkei_0      0.151892
aord_0        0.227845
Name: snp_0, dtype: float64

여기서부터 우리의 전제를 직접 다루기 시작합니다. 지금 우리는 S&P 500의 마감 이전에 S&P 500의 종가와 관련된 시그널들을 연관시키고 있습니다. S&P 500 종가가 비교적 강한 상관 관계를 보이는 유럽의 인덱스들인 FTSE 및 DAX과 약 0.65의 상관 계수를 가지며, 미국이 아닌 다른 국가의 인덱스들과 유의미한 상관관계를 갖는 아시아/오세아니아 인덱스들이 0.15-0.22의 상관 계수를 가짐을 볼 수 있습니다.
우리는 다른 인덱스들과 지역들로부터 우리의 모델을 위해 가용한 시그널들을 갖게 되었습니다.

이제 만일 지난 마감이 예측에 활용될 수 있다면 그걸 확인하기 위해 어떻게 S&P 종가의 로그 반환 값이 지난 날로부터 얻은 인덱스 값과 연관되어 있는지를 살펴 볼겁니다. 금융 시장이 마코프 프로세스라는 전재를 따르면, 역사적인 값들은 가치가 없거나 미미해야 합니다.

이번에는 임시 변수 tmp를 재정의하고 이번에는 이전 코드와 다르게 NYSE, DJIA의 그제(D-2 일) 종가 값을 할당해서 상관 계수를 구합니다.

In [45]:
tmp = pd.DataFrame()
tmp['snp_0'] = log_return_data['snp_log_return']
tmp['nyse_1'] = log_return_data['nyse_log_return'].shift(2)
tmp['djia_1'] = log_return_data['djia_log_return'].shift(2)
tmp['ftse_0'] = log_return_data['ftse_log_return'].shift()
tmp['dax_0'] = log_return_data['dax_log_return'].shift()
tmp['hangseng_0'] = log_return_data['hangseng_log_return'].shift()
tmp['nikkei_0'] = log_return_data['nikkei_log_return'].shift()
tmp['aord_0'] = log_return_data['aord_log_return'].shift()
tmp.corr().iloc[:,0]
Out[45]:
snp_0         1.000000
nyse_1        0.043572
djia_1        0.030391
ftse_0        0.012052
dax_0         0.006265
hangseng_0    0.040744
nikkei_0      0.010357
aord_0        0.021371
Name: snp_0, dtype: float64

이번 데이터에서는 상관 관계가 없다는 것이 조금 보이는데, 이것은 오늘 종가를 예측하는데 있어서 어제 종가가 도움이 되지 않는다는 걸 의미합니다. 한 걸음 더 나아가서 오늘과 그제 사이의 상관 관계를 봅시다.

이번 코드는 새로 정의한 임수 변수 tmp에다 S&P, NYSE의 그제 종가 값, FTSE, DAX, Hang Seng, Nikkei, AORD 어제 종가 값를 할당해 상관 계수를 계산합니다.

In [46]:
tmp = pd.DataFrame()
tmp['snp_0'] = log_return_data['snp_log_return']
tmp['nyse_1'] = log_return_data['nyse_log_return'].shift(3)
tmp['djia_1'] = log_return_data['djia_log_return'].shift(3)
tmp['ftse_0'] = log_return_data['ftse_log_return'].shift(2)
tmp['dax_0'] = log_return_data['dax_log_return'].shift(2)
tmp['hangseng_0'] = log_return_data['hangseng_log_return'].shift(2)
tmp['nikkei_0'] = log_return_data['nikkei_log_return'].shift(2)
tmp['aord_0'] = log_return_data['aord_log_return'].shift(2)

tmp.corr().iloc[:,0]
Out[46]:
snp_0         1.000000
nyse_1       -0.070845
djia_1       -0.071228
ftse_0        0.017085
dax_0        -0.005546
hangseng_0   -0.031368
nikkei_0     -0.015766
aord_0        0.004254
Name: snp_0, dtype: float64

결과는 다시 한번 상관 관계 없음이 조금 임을 보여줍니다.

탐색적 데이터 분석 결과 요약

지금 시점에 이르렀다면 여러분은 EDA 작업을 꽤 잘 수행해 낸 겁니다. 데이터를 시각화 했고 데이터를 더 잘 이해하게 되었습니다. 데이터를 변환해 모델링, 로그 반환 값들 그리고 인덱스들이 서로 어떻게 연관되어져 있는지 보기 쉬운 형태로 만들었습니다. 유럽의 인덱스들이 미국의 인덱스들돠 강한 연관 관계를 갖고 아시아/오세아니아의 인덱스들은 주어진 날짜의 동일한 인덱스들과 유의미하게 상관 관계가 있음을 확인했습니다. 또한, 과거의 값들을 보면, 오늘의 값과 상관 관계가 없다는 걸 확인했습니다.

다음과 같이 요약합니다:

  • 같은 날의 유럽의 인덱스들이 S&P 500 종가의 강한 예측 변수(predictor)였다.
  • 같은 날의 아시아/오세아니아 인덱스들은 s&P 500 종가의 유의미한 예측 변수 였다.
  • 다른 인덱스들의 예전 종가들은 S&P 500 종가에 좋지 않은 예측 변수였다.

지금에 이르러 무엇을 생각해야 할까요?

(갑자기 광고) 클라우드 데이터랩은 적은 코드 양 만으로도 데이터를 정리하고, 변화를 시각화하고 의사 결정을 하는데 충분합니다. 여러분은 쉽게 분석하고 반복할 수 있었습니다. 이 기능은 iPython에서 제공하는 기본 기능이기는 하지만, 클라우드 데이터랩이 당신이 단순히 몇 번의 클릭으로 사용할 수 있는 관리된 서비스라 당신의 분석에만 집중할 수 있다는 점에서 매우 이득입니다. (광고 끝)

피처 선정 (Feature selection)

이 시점에서 우리는 모델을 확인할 수 있습니다:

  • 오늘의 S&P 500 종가가 어제보다 높을 지 낮을 지 예측할 수 있을 겁니다.
  • 우리는 다음의 모든 데이터 소스를 사용할 겁니다: NYSE, DJIA, Nikkei, Hang Seng, FTSE, DAX, AORD.
  • 우리는 데이터가 있는 날 T 혹은 T-n에서 가져온 T, T-1, T-2 세 가지 데이터 포인트를 사용할 예정인데 각각 미국이 아닌 시장의 오늘 날짜 데이터와 미국 시장의 어제 데이터를 의미합니다.

S&P 500의 로그 반환 값의 부호가 +(positive) 혹은 -(negative)이 될지를 예측하는 것은 분류 문제입니다. 즉, 한정된 선택지에서 하나를 고르고 싶은데 이 경우에는 그게 + 또는 -입니다. 오직 두 가지 선택지만 가지는 분류 분제가 가장 기본인데 이를 이진 분류(binary classification) 혹은 logistic regression 이라고 합니다.

EDA를 통해 발견한 사실 즉, 특정한 날 다른 지역의 로그 반환 값이 S&P 500의 값과 강한 상관 관계를 갖는데 이 지역들이 지리적으로 시간대 측면에서 가깝다는 점입니다. 하지만, 우리의 모델은 이런 발견 외의 데이터를 사용합니다. 예를 들어, 우리는 오늘에 더해 과거 며칠 간으로부터의 데이터를 사용합니다. 이렇게 추가 데이터를 사용하는데는 두 가지 이유가 있는데, 그 첫번째는 우리가 튜토리얼 상황 밖에서 피처를 추가하는 것이 좋은 이유가 아니지만 어떻게 돌아가는지 보기 위한 이 솔루션의 목적에 따라 모델에 추가 리처를 추가한다는 점입니다. 두번째는 기계학습 모델은 데이터로부터 약한 시그널을 찾는데 매우 좋기 때문입니다.

기계 학습에서는 대부분 그렇듯이, 미묘한 트레이드오프(tradeoff)가 발생하는데 일반적으로 좋은 프레임워크보다 좋은 알고리즘이 더 낫고 그보다 더 좋은 데이터가 낫습니다. 여러분들은 이 세 가지 기둥이 다 필요하지만 그 중요성은 다음 순서입니다: 데이터 > 알고리즘 > 프레임워크

텐서플로우(TensorFlow)

TensorFlow는 구글이 시작한 오픈소스 프로젝트로써 데이터 흐름 그래프(data flow graphs)를 사용한 수치 연산을 위한 소프트웨어 라이브러리입니다. 텐서 플로우는 구글의 기계 학습 전문성을 기초로 해서 구글 내부의 번역이나 이미지 인식 등의 서비스에 사용할 차세대 프레임워크입니다. 표현력이 좋고 효율적이며 사용하기 쉬운 멋진 기계 학습 프레임워크입니다.

텐서플로우를 위한 Feature engineering

학습(training)과 실험(testing) 관점에서 보면 시계열 데이터는 쉽습니다. 학습데이터는 테스트 데이터 사건이 일어나기 전 사건에서 나오게 되고 시간적으로 인접합니다. 그렇지 않으면 모델은 적어도 테스트 데이터에 비해 "미래"인 사건에 의해 학습되어 버릴 수 있습니다. 그렇게 되면 실제에서는 성능이 형편없을 수 있는데 이유는 미래로부터의 진짜 데이터를 얻을 수 없기 때문입니다. 이는 임의 샘플링(random sampling)이나 교차 검증(cross validataion)이 시계열 데이터에 적용되지 않음을 의미합니다. 학습 대 시험 쪼개기(training-versus-testing split)에 따라 결정하고 데이터를 학습 데이터와 실험 데이터셋으로 나눕니다.

이번 사례에서는 피처를 두 개의 추가 컬럼으로 생성할 예정입니다:

  • snp_log_return_positive : S&P 500 종가의 로그 반환 값이 양일 때 1이고 그 외는 0.
  • snp_log_return_negative : S&P 500 종가의 로그 반환 값이 음일 때 1이고 그 외는 0.

이제 논리적으로 양일 때 1, 음일 때는 0을 갖는 snp_log_return 이름의 한 컬럼으로 인코딩(encoding) 할 수 있는데 이것은 분류 모델에서 텐서플로우가 동작하는 방식이 아닙니다. 텐서플로우는 분류의 일반적인 정의를 사용하는 데, 이 정의에서는 선택할 수 있는 많은 다른 가능성이 있는 값들이 있을 수 있는 원-핫-인코딩(one-hot encoding)이라 불리는 선택지의 형태나 인코딩입니다. 원-핫 인코딩은 모든 선택이 어떤 배열의 엔트리(entry)이고 한 엔트리 값이 1이면 다른 모든 값들이 0이 되는 형태입니다. 이런 인코딩은 모델의 입력을 위함인데 이로써 어떤 값이 참인지 명확히 알 수 있습니다. 이것과 유사한 변형이 출력에도 쓰이는데 이 배열의 각각의 엔트리가 선택할 응답의 확률 값을 같는 배열이 됩니다. 여러분은 가장 근사한 값을 가장 높은 확률을 갖는 값을 취해서 선택할 수 있고 더불어 다른 답들에 비해 이 답이 갖는 신뢰성에 대한 측정 값을 가질 수 있습니다.

우리는 데이터의 80%로 학습하고 20%로 실험하기로 합시다.

피처 생성을 위해 지금까지 가공한 데이터를 저장해 둔 log_return_data 데이터프레임 변수에 추가 컬럼을 구현합니다. ix함수는 label-location 기반 indexer로 데이터프레임 내에서 계층적 인덱스로 고급 인덱싱 할 수 있습니다.
이 함수를 통해 2번째 줄 코드에서 snp_log_return 컬럼 값이 양일 경우 snp_log_return_positive 를 선택해서 1을 입력하고 있습니다.

이제 학습과 실험에 쓸 데이터를 담을 traing_test_data 라는 데이터프레임 변수를 만들어 컬럼을 지정해 둔 후, for 반복문으로 log_return_data 에서 값을 가져와 컬럼을 채웁니다. 흥미로운 점은 반복문 인덱스가 7부터 시작한다는 점인데 각 인덱스의 과거 값을 D-3 까지 저장하는데 log_return_data 의 0-4번째 아이템은 버려지는 걸로 보입니다. (왜 일까?)
이후 append 함수를 통해 log_return_data 에서 추출한 값들을 각각의 컬럼에 맞춰 저장한 후 describe 함수로 통계정보를 검토합니다.

In [47]:
log_return_data['snp_log_return_positive'] = 0
log_return_data.ix[log_return_data['snp_log_return'] >= 0, 'snp_log_return_positive'] = 1
log_return_data['snp_log_return_negative'] = 0
log_return_data.ix[log_return_data['snp_log_return'] < 0, 'snp_log_return_negative'] = 1

training_test_data = pd.DataFrame(
  columns=[
    'snp_log_return_positive', 'snp_log_return_negative',
    'snp_log_return_1', 'snp_log_return_2', 'snp_log_return_3',
    'nyse_log_return_1', 'nyse_log_return_2', 'nyse_log_return_3',
    'djia_log_return_1', 'djia_log_return_2', 'djia_log_return_3',
    'nikkei_log_return_0', 'nikkei_log_return_1', 'nikkei_log_return_2',
    'hangseng_log_return_0', 'hangseng_log_return_1', 'hangseng_log_return_2',
    'ftse_log_return_0', 'ftse_log_return_1', 'ftse_log_return_2',
    'dax_log_return_0', 'dax_log_return_1', 'dax_log_return_2',
    'aord_log_return_0', 'aord_log_return_1', 'aord_log_return_2'])

for i in range(7, len(log_return_data)):
  snp_log_return_positive = log_return_data['snp_log_return_positive'].ix[i]
  snp_log_return_negative = log_return_data['snp_log_return_negative'].ix[i]
  snp_log_return_1 = log_return_data['snp_log_return'].ix[i-1]
  snp_log_return_2 = log_return_data['snp_log_return'].ix[i-2]
  snp_log_return_3 = log_return_data['snp_log_return'].ix[i-3]
  nyse_log_return_1 = log_return_data['nyse_log_return'].ix[i-1]
  nyse_log_return_2 = log_return_data['nyse_log_return'].ix[i-2]
  nyse_log_return_3 = log_return_data['nyse_log_return'].ix[i-3]
  djia_log_return_1 = log_return_data['djia_log_return'].ix[i-1]
  djia_log_return_2 = log_return_data['djia_log_return'].ix[i-2]
  djia_log_return_3 = log_return_data['djia_log_return'].ix[i-3]
  nikkei_log_return_0 = log_return_data['nikkei_log_return'].ix[i]
  nikkei_log_return_1 = log_return_data['nikkei_log_return'].ix[i-1]
  nikkei_log_return_2 = log_return_data['nikkei_log_return'].ix[i-2]
  hangseng_log_return_0 = log_return_data['hangseng_log_return'].ix[i]
  hangseng_log_return_1 = log_return_data['hangseng_log_return'].ix[i-1]
  hangseng_log_return_2 = log_return_data['hangseng_log_return'].ix[i-2]
  ftse_log_return_0 = log_return_data['ftse_log_return'].ix[i]
  ftse_log_return_1 = log_return_data['ftse_log_return'].ix[i-1]
  ftse_log_return_2 = log_return_data['ftse_log_return'].ix[i-2]
  dax_log_return_0 = log_return_data['dax_log_return'].ix[i]
  dax_log_return_1 = log_return_data['dax_log_return'].ix[i-1]
  dax_log_return_2 = log_return_data['dax_log_return'].ix[i-2]
  aord_log_return_0 = log_return_data['aord_log_return'].ix[i]
  aord_log_return_1 = log_return_data['aord_log_return'].ix[i-1]
  aord_log_return_2 = log_return_data['aord_log_return'].ix[i-2]
  training_test_data = training_test_data.append(
    {'snp_log_return_positive':snp_log_return_positive,
    'snp_log_return_negative':snp_log_return_negative,
    'snp_log_return_1':snp_log_return_1,
    'snp_log_return_2':snp_log_return_2,
    'snp_log_return_3':snp_log_return_3,
    'nyse_log_return_1':nyse_log_return_1,
    'nyse_log_return_2':nyse_log_return_2,
    'nyse_log_return_3':nyse_log_return_3,
    'djia_log_return_1':djia_log_return_1,
    'djia_log_return_2':djia_log_return_2,
    'djia_log_return_3':djia_log_return_3,
    'nikkei_log_return_0':nikkei_log_return_0,
    'nikkei_log_return_1':nikkei_log_return_1,
    'nikkei_log_return_2':nikkei_log_return_2,
    'hangseng_log_return_0':hangseng_log_return_0,
    'hangseng_log_return_1':hangseng_log_return_1,
    'hangseng_log_return_2':hangseng_log_return_2,
    'ftse_log_return_0':ftse_log_return_0,
    'ftse_log_return_1':ftse_log_return_1,
    'ftse_log_return_2':ftse_log_return_2,
    'dax_log_return_0':dax_log_return_0,
    'dax_log_return_1':dax_log_return_1,
    'dax_log_return_2':dax_log_return_2,
    'aord_log_return_0':aord_log_return_0,
    'aord_log_return_1':aord_log_return_1,
    'aord_log_return_2':aord_log_return_2},
    ignore_index=True)
  
training_test_data.describe()
Out[47]:
snp_log_return_positive snp_log_return_negative snp_log_return_1 snp_log_return_2 snp_log_return_3 nyse_log_return_1 nyse_log_return_2 nyse_log_return_3 djia_log_return_1 djia_log_return_2 ... hangseng_log_return_2 ftse_log_return_0 ftse_log_return_1 ftse_log_return_2 dax_log_return_0 dax_log_return_1 dax_log_return_2 aord_log_return_0 aord_log_return_1 aord_log_return_2
count 1440.000000 1440.000000 1440.000000 1440.000000 1440.000000 1440.000000 1440.000000 1440.000000 1440.000000 1440.000000 ... 1440.000000 1440.000000 1440.000000 1440.000000 1440.000000 1440.000000 1440.000000 1440.000000 1440.000000 1440.000000
mean 0.547222 0.452778 0.000358 0.000346 0.000347 0.000190 0.000180 0.000181 0.000294 0.000287 ... -0.000056 0.000069 0.000063 0.000046 0.000326 0.000326 0.000311 0.000029 0.000011 0.000002
std 0.497938 0.497938 0.010086 0.010074 0.010074 0.010558 0.010547 0.010548 0.009305 0.009298 ... 0.011783 0.010028 0.010030 0.010007 0.013111 0.013111 0.013099 0.009153 0.009146 0.009133
min 0.000000 0.000000 -0.068958 -0.068958 -0.068958 -0.073116 -0.073116 -0.073116 -0.057061 -0.057061 ... -0.060183 -0.047798 -0.047798 -0.047798 -0.064195 -0.064195 -0.064195 -0.042998 -0.042998 -0.042998
25% 0.000000 0.000000 -0.004068 -0.004068 -0.004068 -0.004545 -0.004545 -0.004545 -0.003962 -0.003962 ... -0.005884 -0.004865 -0.004871 -0.004871 -0.005995 -0.005995 -0.005995 -0.004774 -0.004786 -0.004786
50% 1.000000 0.000000 0.000611 0.000611 0.000611 0.000528 0.000528 0.000528 0.000502 0.000502 ... 0.000000 0.000180 0.000166 0.000166 0.000752 0.000752 0.000746 0.000398 0.000384 0.000384
75% 1.000000 1.000000 0.005383 0.005360 0.005360 0.005563 0.005534 0.005534 0.005023 0.005021 ... 0.006160 0.005472 0.005472 0.005470 0.006827 0.006827 0.006812 0.005473 0.005452 0.005452
max 1.000000 1.000000 0.046317 0.046317 0.046317 0.051173 0.051173 0.051173 0.041533 0.041533 ... 0.055187 0.050323 0.050323 0.050323 0.052104 0.052104 0.052104 0.034368 0.034368 0.034368

8 rows × 26 columns

이제 학습과 실험 데이터를 생성합니다.

traning_test_data에서 피처에 해당하는 snp_log_return_positive, snp_log_return_negative 컬럼은 classes_tf로, 그 나머지를 예측 변수인 predictor_tf 로 저장합니다.

학습 데이터는 전체 데이터셋의 크기의 80%를 잡아 계산하고 실험 데이터의 수는 그 나머지로 계산하고 그 아이템 숫자로 각각 학습용 데이터와 실험용 데이터를 나눕니다.

In [48]:
predictors_tf = training_test_data[training_test_data.columns[2:]]

classes_tf = training_test_data[training_test_data.columns[:2]]

training_set_size = int(len(training_test_data) * 0.8)
test_set_size = len(training_test_data) - training_set_size

training_predictors_tf = predictors_tf[:training_set_size]
training_classes_tf = classes_tf[:training_set_size]
test_predictors_tf = predictors_tf[training_set_size:]
test_classes_tf = classes_tf[training_set_size:]

training_predictors_tf.describe()
Out[48]:
snp_log_return_1 snp_log_return_2 snp_log_return_3 nyse_log_return_1 nyse_log_return_2 nyse_log_return_3 djia_log_return_1 djia_log_return_2 djia_log_return_3 nikkei_log_return_0 ... hangseng_log_return_2 ftse_log_return_0 ftse_log_return_1 ftse_log_return_2 dax_log_return_0 dax_log_return_1 dax_log_return_2 aord_log_return_0 aord_log_return_1 aord_log_return_2
count 1152.000000 1152.000000 1152.000000 1152.000000 1152.000000 1152.000000 1152.000000 1152.000000 1152.000000 1152.000000 ... 1152.000000 1152.000000 1152.000000 1152.000000 1152.000000 1152.000000 1152.000000 1152.000000 1152.000000 1152.000000
mean 0.000452 0.000444 0.000451 0.000314 0.000308 0.000317 0.000382 0.000376 0.000381 0.000286 ... 0.000078 0.000163 0.000148 0.000153 0.000378 0.000347 0.000350 0.000087 0.000075 0.000093
std 0.010291 0.010286 0.010285 0.010921 0.010917 0.010916 0.009341 0.009337 0.009335 0.013828 ... 0.011722 0.009920 0.009918 0.009917 0.012809 0.012807 0.012807 0.009021 0.009025 0.009020
min -0.068958 -0.068958 -0.068958 -0.073116 -0.073116 -0.073116 -0.057061 -0.057061 -0.057061 -0.111534 ... -0.058270 -0.047792 -0.047792 -0.047792 -0.064195 -0.064195 -0.064195 -0.042998 -0.042998 -0.042998
25% -0.004001 -0.004001 -0.003994 -0.004462 -0.004462 -0.004415 -0.003865 -0.003865 -0.003851 -0.006914 ... -0.005689 -0.004849 -0.004852 -0.004852 -0.005527 -0.005611 -0.005611 -0.004591 -0.004607 -0.004591
50% 0.000721 0.000721 0.000725 0.000646 0.000646 0.000655 0.000561 0.000561 0.000580 0.000000 ... 0.000000 0.000195 0.000166 0.000195 0.000700 0.000694 0.000694 0.000433 0.000422 0.000433
75% 0.005607 0.005591 0.005591 0.005922 0.005908 0.005908 0.005098 0.005071 0.005071 0.008589 ... 0.006406 0.005649 0.005637 0.005637 0.006712 0.006697 0.006697 0.005191 0.005191 0.005235
max 0.046317 0.046317 0.046317 0.051173 0.051173 0.051173 0.041533 0.041533 0.041533 0.055223 ... 0.055187 0.050323 0.050323 0.050323 0.052104 0.052104 0.052104 0.034368 0.034368 0.034368

8 rows × 24 columns

In [49]:
test_predictors_tf.describe()
Out[49]:
snp_log_return_1 snp_log_return_2 snp_log_return_3 nyse_log_return_1 nyse_log_return_2 nyse_log_return_3 djia_log_return_1 djia_log_return_2 djia_log_return_3 nikkei_log_return_0 ... hangseng_log_return_2 ftse_log_return_0 ftse_log_return_1 ftse_log_return_2 dax_log_return_0 dax_log_return_1 dax_log_return_2 aord_log_return_0 aord_log_return_1 aord_log_return_2
count 288.000000 288.000000 288.000000 288.000000 288.000000 288.000000 288.000000 288.000000 288.000000 288.000000 ... 288.000000 288.000000 288.000000 288.000000 288.000000 288.000000 288.000000 288.000000 288.000000 288.000000
mean -0.000021 -0.000047 -0.000070 -0.000302 -0.000331 -0.000361 -0.000057 -0.000068 -0.000094 0.000549 ... -0.000593 -0.000306 -0.000278 -0.000383 0.000122 0.000242 0.000155 -0.000200 -0.000246 -0.000361
std 0.009226 0.009183 0.009189 0.008960 0.008914 0.008920 0.009168 0.009152 0.009154 0.013305 ... 0.012028 0.010457 0.010473 0.010365 0.014275 0.014286 0.014230 0.009677 0.009627 0.009581
min -0.040211 -0.040211 -0.040211 -0.040610 -0.040610 -0.040610 -0.036402 -0.036402 -0.036402 -0.047151 ... -0.060183 -0.047798 -0.047798 -0.047798 -0.048165 -0.048165 -0.048165 -0.041143 -0.041143 -0.041143
25% -0.004303 -0.004303 -0.004415 -0.004667 -0.004667 -0.004724 -0.004689 -0.004689 -0.004689 -0.004337 ... -0.006437 -0.005160 -0.005160 -0.005160 -0.008112 -0.008008 -0.008008 -0.005356 -0.005356 -0.005372
50% -0.000012 -0.000012 -0.000045 0.000041 0.000041 0.000033 0.000047 0.000047 0.000023 0.000621 ... 0.000000 0.000177 0.000177 0.000104 0.000978 0.001078 0.000978 0.000138 0.000138 0.000026
75% 0.004734 0.004734 0.004734 0.004311 0.004311 0.004311 0.004477 0.004477 0.004477 0.006890 ... 0.005190 0.004720 0.004816 0.004720 0.007993 0.008057 0.007993 0.006145 0.005981 0.005939
max 0.038291 0.038291 0.038291 0.029210 0.029210 0.029210 0.038755 0.038755 0.038755 0.074262 ... 0.040211 0.034971 0.034971 0.034971 0.048521 0.048521 0.048521 0.025518 0.025518 0.025518

8 rows × 24 columns

모델을 평가하기 위한 몇 가지 지표(metric)을 정의합니다.

  • Precision - 분류기가 어떤 샘플(sample)의 틀린 값을 맞는 값으로 인식(label)하지 않을 능력
  • Recall - 분류기가 맞는 모든 샘플을 찾아낼 수 있을 능력
  • F1 Score - Precision과 Recall의 가중치가 부여된 평균으로 점수가 1이 될 수록 좋아지고, 0이 될 수록 나빠짐.
  • Accuracy - 실험 데이터에서 정확하게 예측된 비율

Precision과 Recall에 관한 지표를 모아서 보여주는 표를 confusion metrics 라고 합니다. 여기 정의된 tf_confusion_metrics 함수는 각각의 지표를 출력해 주는 목적으로 구현되어 있습니다.

텐서플로우가 제공하는 수학 관련 함수들을 간략히 살펴 보겠습니다.

  • argmax: 해당 tensor(다차원 배열) 값 중 가장 큰 값을 가지는 인덱스 값을 반환
  • ones_like: 모든 값이 1로 채워진 배열을 생성, 유사하게 zeros_like는 0으로 채워진 배열을 생성
  • reduce_sum: tensor의 모든 차원의 모든 요소들(elements)의 합을 계산
  • cast: tensor를 지정한 형식으로 형 변환
  • logical_and: 두 입력 parameter 의 AND 연산 결과를 boolean 으로 반환

텐서플로우의 특징은 tensor를 선언하거나 계산하는 시점에서가 아닌 session을 수행(run)하는 시점에서 실제 연산이 일어난다는 점입니다. 따라서 tf_confusion_metrics 함수는 인자로 session을 전달 받아 51번째 줄에서 run 함수를 호출하고 있습니다.

In [50]:
def tf_confusion_metrics(model, actual_classes, session, feed_dict):
  predictions = tf.argmax(model, 1)
  actuals = tf.argmax(actual_classes, 1)

  ones_like_actuals = tf.ones_like(actuals)
  zeros_like_actuals = tf.zeros_like(actuals)
  ones_like_predictions = tf.ones_like(predictions)
  zeros_like_predictions = tf.zeros_like(predictions)

  tp_op = tf.reduce_sum(
    tf.cast(
      tf.logical_and(
        tf.equal(actuals, ones_like_actuals), 
        tf.equal(predictions, ones_like_predictions)
      ), 
      "float"
    )
  )

  tn_op = tf.reduce_sum(
    tf.cast(
      tf.logical_and(
        tf.equal(actuals, zeros_like_actuals), 
        tf.equal(predictions, zeros_like_predictions)
      ), 
      "float"
    )
  )

  fp_op = tf.reduce_sum(
    tf.cast(
      tf.logical_and(
        tf.equal(actuals, zeros_like_actuals), 
        tf.equal(predictions, ones_like_predictions)
      ), 
      "float"
    )
  )

  fn_op = tf.reduce_sum(
    tf.cast(
      tf.logical_and(
        tf.equal(actuals, ones_like_actuals), 
        tf.equal(predictions, zeros_like_predictions)
      ), 
      "float"
    )
  )

  tp, tn, fp, fn = \
    session.run(
      [tp_op, tn_op, fp_op, fn_op], 
      feed_dict
    )

  tpr = float(tp)/(float(tp) + float(fn))
  fpr = float(fp)/(float(tp) + float(fn))

  accuracy = (float(tp) + float(tn))/(float(tp) + float(fp) + float(fn) + float(tn))

  recall = tpr
  precision = float(tp)/(float(tp) + float(fp))
  
  f1_score = (2 * (precision * recall)) / (precision + recall)
  
  print 'Precision = ', precision
  print 'Recall = ', recall
  print 'F1 Score = ', f1_score
  print 'Accuracy = ', accuracy

텐서 플로우로 이진 분류 (binary classification)

이제 tensor가 흐르게 해봅시다. 이 모델이 텐서 플로우로 표현한 이진 분류입니다.

  • Session은 텐서플로우를 수행하기 위한 클래스로 앞서 언급한 바와 같이 이 인터페이스에 작성한 모델을 전달함으로써 실제 수행이 일어납니다.

  • placeholder는 그래프를 수행하는 과정에서 활용하기 위한 배열 공간으로 학습 과정에서 필요한 메모리 공간을 미리 할당하기 위해 필요한 함수입니다.

  • Variable은 학습된 값들이 저장될 목적으로 지정한 배열 공간으로 정의된 weights, biases는 outputs = weights * inputs + biases 형태의 기본 학습 모델의 필수 parameter입니다. (단, 여기서 * 는 행렬 곱, + 는 행렬 합을 지칭합니다)
  • softmax 알고리즘을 이용해 기본 학습 모델을 만들고 cross entropy를 비용 함수(cost function)로 해서 이것을 최소화해서 최적 해를 찾아가는 adam optimizer로 학습을 수행합니다.
  • adam optimizer의 learning_rate는 수렴(convergence) 비율을 지정하는 값으로 발산하지 않으며 빠르게 학습할 수 있도록 적당한 값을 지정해야 합니다. (magic number)
  • initialize_all_variables 함수는 텐서플로우의 기본(default) 그래프를 초기화합니다.
In [51]:
sess = tf.Session()

# Define variables for the number of predictors and number of classes to remove magic numbers from our code.
num_predictors = len(training_predictors_tf.columns) # 24 in the default case
num_classes = len(training_classes_tf.columns) # 2 in the default case

# Define placeholders for the data we feed into the process - feature data and actual classes.
feature_data = tf.placeholder("float", [None, num_predictors])
actual_classes = tf.placeholder("float", [None, num_classes])

# Define a matrix of weights and initialize it with some small random values.
weights = tf.Variable(tf.truncated_normal([num_predictors, num_classes], stddev=0.0001))
biases = tf.Variable(tf.ones([num_classes]))

# Define our model...
# Here we take a softmax regression of the product of our feature data and weights.
model = tf.nn.softmax(tf.matmul(feature_data, weights) + biases)

# Define a cost function (we're using the cross entropy).
cost = -tf.reduce_sum(actual_classes*tf.log(model))

# Define a training step...
# Here we use gradient descent with a learning rate of 0.01 using the cost function we just defined.
training_step = tf.train.AdamOptimizer(learning_rate=0.0001).minimize(cost)

init = tf.initialize_all_variables()
sess.run(init)

우리는 다음 코드조각을 통해 모델을 학습합니다. 그래프 연산을 수행하는 텐서플로우의 접근법은 이 과정에서 상세히 제어할 수 있습니다. run 함수의 일부처럼 세션에 전달하는 어떤 연산이라도 수행되어 그 결과가 반환됩니다. 여러분은 다양한 연산 리스트를 전달할 수 있습니다.

매번 모든 데이터셋을 사용해 3만번의 이상의 반복을 통해 모델을 학습하게 됩니다. 매 천번 째 반복마다 진행상황을 평가하기 위해 학습데이터에 대한 모델의 정확도를 평가합니다.

  • correct_prediction은 equal 함수로 모델의 예측 값과 실제 값이 같을 경우 참(true)을 저장하고 아닐 경우 거짓(false)을 저장한 배열이 됩니다.
  • reduce_mean은 배열 값의 평균 값을 계산하는 함수로 2번째 코드에서는 correct_prediction 변수를 실수로 변환한 후 평균 값을 취해 정확도로 사용합니다.
  • run 함수의 feed_dict 인자(feed dictionary)는 학습 과정 반복 중에 활용할 변수들을 전달하는 인터페이스입니다.
In [52]:
correct_prediction = tf.equal(tf.argmax(model, 1), tf.argmax(actual_classes, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))

for i in range(1, 30001):
  sess.run(
    training_step, 
    feed_dict={
      feature_data: training_predictors_tf.values, 
      actual_classes: training_classes_tf.values.reshape(len(training_classes_tf.values), 2)
    }
  )
  if i%5000 == 0:
    print i, sess.run(
      accuracy,
      feed_dict={
        feature_data: training_predictors_tf.values, 
        actual_classes: training_classes_tf.values.reshape(len(training_classes_tf.values), 2)
      }
    )
5000 0.560764
10000 0.575521
15000 0.594618
20000 0.614583
25000 0.630208
30000 0.644965

학습 데이터로 65%의 정확도를 얻었는데 임의로 찍는 것(random)보다는 확실 나은 수치입니다.

이번에는 앞서 정의해 둔 confusion metrics 함수를 통해 지표를 확인해 봅니다.

In [53]:
feed_dict= {
  feature_data: test_predictors_tf.values,
  actual_classes: test_classes_tf.values.reshape(len(test_classes_tf.values), 2)
}

tf_confusion_metrics(model, actual_classes, sess, feed_dict)
Precision =  0.914285714286
Recall =  0.222222222222
F1 Score =  0.357541899441
Accuracy =  0.600694444444

텐서플로우 모델 중 가장 단순한 것으로 평가한 지표는 별로 인상적이지 않는데 F1 점수 0.36로는 방 안의 어떤 전구도 날려 버리지 못할 겁니다.

이런 결과는 일부는 모델의 단순함 때문이고 일부는 아직 튜닝하지 않았기 때문입니다; 하이퍼파라메터(hyperparameters)를 선정하는 것이 기계학습 모델링에서 매우 중요합니다.

2개 은닉층의 피드 포워드 신경망 (Feed-forward neural network with two hidden layers)

이제 적당한 2개의 은닉층을 갖는 피드 포워드 신경망을 만들어 봅시다.

은닉층 각각에 필요한 weights, biases parameter를 선언합니다. 사용된 텐서플로우 API들은 다음과 같습니다:

  • truncated_normal 함수는 첫번째 인자로 지정한 영역 외를 제거한 형태의 정규 분포를 갖는 임의의 값들을 생성합니다.
  • ones 함수는 모든 요소를 1로 지정한 tensor를 생성합니다.
  • matmul 함수는 두 배열을 곱합니다.
  • relu는 activation function의 하나로 신경망에서 필요한 여러 형식의 nonlinearity 함수 중 하나입니다.

앞서 softmax 함수를 이용해 학습한 코드에서 모델이 신경망으로 바뀐 것을 제외하고는 거의 유사한 코드입니다.

In [54]:
sess1 = tf.Session()

num_predictors = len(training_predictors_tf.columns)
num_classes = len(training_classes_tf.columns)

feature_data = tf.placeholder("float", [None, num_predictors])
actual_classes = tf.placeholder("float", [None, 2])

weights1 = tf.Variable(tf.truncated_normal([24, 50], stddev=0.0001))
biases1 = tf.Variable(tf.ones([50]))

weights2 = tf.Variable(tf.truncated_normal([50, 25], stddev=0.0001))
biases2 = tf.Variable(tf.ones([25]))
                     
weights3 = tf.Variable(tf.truncated_normal([25, 2], stddev=0.0001))
biases3 = tf.Variable(tf.ones([2]))

hidden_layer_1 = tf.nn.relu(tf.matmul(feature_data, weights1) + biases1)
hidden_layer_2 = tf.nn.relu(tf.matmul(hidden_layer_1, weights2) + biases2)
model = tf.nn.softmax(tf.matmul(hidden_layer_2, weights3) + biases3)

cost = -tf.reduce_sum(actual_classes*tf.log(model))

train_op1 = tf.train.AdamOptimizer(learning_rate=0.0001).minimize(cost)

init = tf.initialize_all_variables()
sess1.run(init)

이 모델을 이용해서 다시 한번 모든 데이터 셋으로 3만번 반복해서 모델을 학습시켜 봅시다.

동일하게 매 천번째 반복마다 모델이 잘 학습되었는지 평가합니다. (어디서 본 것같은 기시감이... 복붙의 냄새)

In [55]:
correct_prediction = tf.equal(tf.argmax(model, 1), tf.argmax(actual_classes, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))

for i in range(1, 30001):
  sess1.run(
    train_op1, 
    feed_dict={
      feature_data: training_predictors_tf.values, 
      actual_classes: training_classes_tf.values.reshape(len(training_classes_tf.values), 2)
    }
  )
  if i%5000 == 0:
    print i, sess1.run(
      accuracy,
      feed_dict={
        feature_data: training_predictors_tf.values, 
        actual_classes: training_classes_tf.values.reshape(len(training_classes_tf.values), 2)
      }
    )
5000 0.757812
10000 0.766493
15000 0.768229
20000 0.767361
25000 0.767361
30000 0.767361

학습데이터를 이용한 정확도 측면에서 유의미한 개선이 보이는데 이는 은닉층이 모델을 학습하는데 도움이 되었다는 걸 의미합니다.

앞서 정의한 tf_confusion_metrics 함수를 수행해서 precision, recall 과 정확도를 보면 성능 측면에서 상당한 개선을 확인할 수 있습니다만 step function은 확실히 아닙니다.

이는 상대적으로 단순한 피처 셋으로만 학습한 것에 의한 한계에 도달한 것으로 보입니다.

In [56]:
feed_dict= {
  feature_data: test_predictors_tf.values,
  actual_classes: test_classes_tf.values.reshape(len(test_classes_tf.values), 2)
}

tf_confusion_metrics(model, actual_classes, sess1, feed_dict)
Precision =  0.775862068966
Recall =  0.625
F1 Score =  0.692307692308
Accuracy =  0.722222222222

결론

여러분은 지금까지 방대한 내용을 다뤘습니다. 5년 간 축적된 금융 시계열 데이터를 얻어와서 데이터를 보다 적당한 형태로 변경했습니다. 탐색적 데이터 분석을 통해 데이터를 탐험하고 시각화 했으며 기계 학습 모델과 관련해 필요한 피처에 대해 결정했습니다. 여러분은 이 피처들을 가공해서 텐서플로우의 이진 분류기를 만들었고 성능을 분석했습니다. 또한 2개의 은닉층으로 이뤄진 피드 포워드 신경망을 텐서플로우로 만들어 성능을 분석했습니다.

이 기술에 얼마나 비용이 들었을까요? 대부분의 사람들이 이 솔루션으로부터 주스를 추출하는데 한시간 반에서 세 시간이 걸리고 이 시간에서 인프라나 소프트웨어 때문에 대기하는 시간은 거의 없습니다; 다만, 읽고 생각하는데 쓴 시간들이죠. 많은 조직 중 이런 종류의 데이터 분석을 하는데 수 일부터 수 개월이 걸리는 곳도 있는데 특정 하드웨어를 마련하는게 필요한지에 따라 다릅니다. 그리고 여러분은 인프라나 추가 소프트웨어에 대해 어떤 것도 할 필요가 없습니다. 대신에 여러분은 GCP에 직접 연결된 웹 기반 콘솔을 사용해 편의에 따라 시스템을 셋업하면 되는데 완벽히 관리되고 운영되며 분석하는데만 시간을 쓸 수 있도록 해줍니다. (아, 역시 광고는 이렇게 위대한 겁니다)

게다가 비용 대비 고효율입니다. 이 솔루션에 시간을 들인다면 3시간으로 해낼 수 있으며 비용도 몇 푼에 불과할 겁니다.

(아래는 미래를 위해 남겨두렵니다. 절대 귀찮아서 그런겁니다.)

Cloud Datalab worked admirably, too. iPython/Jupyter has always been a great platform for interactive, iterative work and a fully-managed version of that platform on GCP, with connectors to other GCP technologies such as BigQuery and Google Cloud Storage, is a force multiplier for your analysis needs. If you haven't used iPython before, this solution might have been eye opening, for you. If you're already familiar with iPython, then you'll love the connectors to other GCP technologies.

Of course, R and Matlab are popular tools in machine learning, and we've made no mention either in this solution. Neither R nor Matlab are available as managed services on GCP. Both can be hosted in GCP and accessed through a cloud-friendly, web frontend.

TensorFlow is a special piece of technology. It is expressive, performs well, and comes with the weight of Google's machine learning history and expertise to back it up and support it. We've only scratched the surface, but you can already see that within a handful of lines of code we've been able to write two models. Neither of them is cutting edge, by design, but neither of them is trivial either. With some additional tuning they would suit a whole spectrum of machine learning tasks.

Finally, how did we do with the data analysis? We did well: over 70% accuracy in predicting the close of the S&P 500 is the highest we've seen achieved on this dataset, so with few steps and a few lines of code we've produced a full-on machine learning model. The reason for the relatively modest accuracy achieved is the dataset itself; there isn't enough signal there to do significantly better. But 7 times out of 10, we were able to correctly determine if the S&P 500 index would close up or down on the day, and that's objectively good.

마무-의리

작업이 끝나면 클라우드 데이터랩이 실행되는 VM를 종료시켜서 쓸데없는 비용이 발생되지 않도록 합시다.

In [57]:
print '''
Copyright 2015, Google, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
'''
Copyright 2015, Google, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

In [2]:
print '''
Copyright 2016, Web of Think
Licensed under the Creative Commons 3.0 CC-SA

It has been translated without without warranties or conditions of any kind.
Some comments such as code explanation which added from original post.

이 문서의 번역이나 추가 기술 부분은 크리에이티브 커먼스 3.0 저작자표시 라이센스를 따릅니다.

이 한글 번역은 역자의 오역이나 의역으로 인해 왜곡될 수 있으니 자세한 사항은 원문을 참조하시기 바랍니다. 
원문에 코드 해설과 관련 링크가 추가되었습니다.

'''
Copyright 2016, Web of Think
Licensed under the Creative Commons 3.0 CC-SA

It has been translated without without warranties or conditions of any kind.
Some comments such as code explanation which added from original post.

이 문서의 번역이나 추가 기술 부분은 크리에이티브 커먼스 3.0 저작자표시 라이센스를 따릅니다.

이 한글 번역은 역자의 오역이나 의역으로 인해 왜곡될 수 있으니 자세한 사항은 원문을 참조하시기 바랍니다. 
원문에 코드 해설과 관련 링크가 추가되었습니다.