[譯] 用長短時間記憶網絡預測股價走勢(使用 Tensorflow)

在本教程中,你將瞭解到如何使用被稱做長短時間記憶網絡(LSTM)的時間序列模型。LSTM 模型在保持長期記憶方面很是強大。閱讀這篇教程時,你將:前端

  • 明白預測股市走勢的動機;
  • 下載股票數據 — 你將使用由 Alpha Vantage 或 Kaggle 收集的股票數據;
  • 將數據劃分爲訓練集和測試集,並將其標準化;
  • 簡要討論一下爲何 LSTM 模型能夠預測將來多步的情形;
  • 使用現有數據預測股票趨勢,並將結果可視化。

注意:請不要認爲 LSTM 是一種能夠完美預測股票趨勢的可靠模型,也不要盲目使用它進行股票交易。我只是出於對機器學習的興趣作了這個實驗。在大部分狀況下,這個模型的確能發現數據中的特定規律並準確預測股票的走勢。可是否將其用於實際的股票市場取決於你本身。node

爲何要用時間序列模型?

做爲一名股民,若是你能對股票價格進行正確的建模,你就能夠經過在合適的時機買入或賣出來獲取利益。所以,你須要能經過一組歷史數據來預測將來數據的模型——時間序列模型。python

警告:股價自己因受到諸多因素影響而難以預測,這意味着你難以找到一種能完美預測股價的模型。並不僅有我一人如此認爲。普林斯頓大學的經濟學教授 Burton Malkiel 在他 1973 年出版的《A Random Walk Down Wall Street》一書中寫道:「若是股市足夠高效,以致於人們能從公開的股價中知曉影響它的所有因素,那麼人人都能像投資專業人士那樣炒股」。android

可是,請保持信心,用機器學習的方法來預測這徹底隨機的股價仍有一絲但願。咱們至少能經過建模來預測這組數據的實際走勢。換而言之,沒必要知曉股價的確切值,你只要能預測股價要漲仍是要跌就萬事大吉了。ios

# 請確保你安裝了這些包,而且能運行成功如下代碼
from pandas_datareader import data
import matplotlib.pyplot as plt
import pandas as pd
import datetime as dt
import urllib.request, json 
import os
import numpy as np
import tensorflow as tf # TensorFlow 1.6 版本下測試經過
from sklearn.preprocessing import MinMaxScaler
複製代碼

下載數據

你能夠從如下來源下載數據:git

  1. Alpha Vantage。首先,你必須從 這個網站 獲取所需的 API key。在此以後,將它的值賦給變量 api_key
  2. 這個頁面 下載並將其中的 Stocks 文件夾拷貝到你的工程目錄下。

股價中包含幾種不一樣的數據,它們是:github

  • 開盤價:一天中股票剛開盤時的價格;
  • 收盤價:一天中股票收盤時的價格;
  • 最高價:一天中股價的最大值;
  • 最低價:一天中股價的最小值。

從 Alpha Vantage 獲取數據

爲了從 Alpha Vantage 上下載美國航空公司的股價數據用於分析,你要將行情顯示代號 ticker 設置爲 "AAL"。同時,你也要定義一個 url_string 變量來獲取包含最近 20 年內的所有股價信息的 JSON 文件,以及文件保存路徑 file_to_save。別忘了用你的 ticker 變量來幫助你命名你下載下來的文件。算法

接下來,設定一個條件:若是本地沒有保存的數據文件,就從 url_string 指明的 URL 下載數據,並將其中的日期、最低價、最高價、交易量、開盤價和收盤價存入 Pandas 的 DataFrame df 中,再將其保存到 file_to_save;不然直接從本地讀取 csv 文件就行了。json

從 Kaggle 獲取數據

從 Kaggle 上找到的數據是一系列 csv 表格,你不須要對它進行任何處理就能夠直接讀入 Pandas 的 DataFrame 中。確保你正確地將 Stocks 文件夾放在項目的主目錄中。後端

讀取數據

如今,將這些數據打印到 DataFrame 中吧!因爲數據的順序在時間序列模型中相當重要,因此請確保你的數據已經按照日期排好序了。

# 按日期排序
df = df.sort_values('Date')

# 檢查結果
df.head()
複製代碼

數據可視化

看看你的數據,並從中找到伴隨時間推移而具備的不一樣規律。

plt.figure(figsize = (18,9))
plt.plot(range(df.shape[0]),(df['Low']+df['High'])/2.0)
plt.xticks(range(0,df.shape[0],500),df['Date'].loc[::500],rotation=45)
plt.xlabel('Date',fontsize=18)
plt.ylabel('Mid Price',fontsize=18)
plt.show()
複製代碼

這幅圖包含了不少信息。我特地選取了這家公司的股價圖,由於它包含了股價的多種不一樣規律。這將使你的模型更健壯,也讓它能更好地預測不一樣情形下的股價。

另外一件值得注意的事情是 2017 年的股價遠比上世紀七十年代的股價高且波動更大。所以,你要在數據標準化的過程當中,注意讓這些部分的數據落在相近的數值區間內。

將數據劃分爲訓練集和測試集

首先經過對每一天的最高和最低價的平均值來算出 mid_prices

# 首先用最高和最低價來算出中間價
high_prices = df.loc[:,'High'].as_matrix()
low_prices = df.loc[:,'Low'].as_matrix()
mid_prices = (high_prices+low_prices)/2.0
複製代碼

而後你就能夠劃分數據集了。前 11000 個數據屬於訓練集,剩下的都屬於測試集。

train_data = mid_prices[:11000] 
test_data = mid_prices[11000:]
複製代碼

接下來咱們須要一個換算器 scaler 用於標準化數據。MinMaxScalar 會將全部數據換算到 0 和 1 之間。同時,你也能夠將兩個數據集都調整爲 [data_size, num_features] 的大小。

# 將全部數據縮放到 0 和 1 之間
# 在縮放時請注意,縮放測試集數據時請使用縮放訓練集數據的參數
# 由於在測試前你是不該當知道測試集數據的
scaler = MinMaxScaler()
train_data = train_data.reshape(-1,1)
test_data = test_data.reshape(-1,1)
複製代碼

上面咱們注意到不一樣年代的股價處於不一樣的價位,若是不作特殊處理的話,在標準化後的數據中,上世紀的股價數據將很是接近於 0。這對模型的學習過程沒啥好處。因此咱們將整個時間序列劃分爲若干個區間,並在每個區間上作標準化。這裏每個區間的長度取值爲 2500。

提示:由於每個區間都被獨立地初始化,因此在兩個區間的交界處會引入一個「突變」。爲了不這個「突變」給咱們的模型帶來大麻煩,這裏的每個區間長度不要過小。

本例中會引入 4 個「突變」,鑑於數據有 11000 組,因此它們可有可無。

# 使用訓練集來訓練換算器 scaler,而且調整數據使之更平滑
smoothing_window_size = 2500
for di in range(0,10000,smoothing_window_size):
    scaler.fit(train_data[di:di+smoothing_window_size,:])
    train_data[di:di+smoothing_window_size,:] = scaler.transform(train_data[di:di+smoothing_window_size,:])

# 標準化全部的數據
scaler.fit(train_data[di+smoothing_window_size:,:])
train_data[di+smoothing_window_size:,:] = scaler.transform(train_data[di+smoothing_window_size:,:])
複製代碼

將數據矩陣調整回 [data_size] 的形狀。

# 從新調整測試集和訓練集
train_data = train_data.reshape(-1)

# 將測試集標準化
test_data = scaler.transform(test_data).reshape(-1)
複製代碼

爲了產生一條更平滑的曲線,咱們使用一種叫作指數加權平均的算法。

注意:咱們只使用訓練集來訓練換算器 scaler,不然在標準化測試集時將獲得不許確的結果。

注意:只容許對訓練集作平滑處理。

# 應用指數加權平均
# 如今數據將比之間更爲平滑
EMA = 0.0
gamma = 0.1
for ti in range(11000):
  EMA = gamma*train_data[ti] + (1-gamma)*EMA
  train_data[ti] = EMA

# 用於可視化和調試
all_mid_data = np.concatenate([train_data,test_data],axis=0)
複製代碼

評估結果

爲了評估訓練出來的模型,咱們將計算其預測值與真實值的均方偏差(MSE)。將每個預測值與真實值偏差的平方取均值,即爲這個模型的均方偏差。

股價建模中的平均值

在個人 這篇同類型文章 中,我提到了取平均值在股價建模中是一種糟糕的作法,其結論以下:

取平均值在預測單步上效果不錯,但對股市預測這種須要預測許多步的情形不適用。若是你想了解更多,請查看 這篇文章

使用 LSTM 預測將來股價走勢

長短時間記憶網絡模型是很是強大的基於時間序列的模型,它們能向後預測任意步。一個 LSTM 模塊(或者一個 LSTM 單元)使用 5 個重要的參數來對長期和短時間數據建模。

  • 單元狀態(c_{t})- 這表明了單元存儲的短時間和長期記憶;
  • 隱藏狀態(h_{t})- 這是根據當前輸入、之前的隱藏狀態和當前單元輸入計算的用於預測將來股價的輸出狀態信息 。此外,隱藏狀態還決定着是否只使用單元狀態中的記憶(短時間、長期或二者都使用)來進行下一次預測;
  • 輸入門(i_{t})- 從輸入門流入到單元狀態中的信息;
  • 遺忘門(f_{t})- 從當前輸入和前一個單元狀態流到當前單元狀態的信息;
  • 輸出門(o_{t})- 從當前單元狀態流到隱藏狀態的信息,這決定了 LSTM 接下來使用的記憶類型。

下圖展現了一個 LSTM 單元。

其中計算的算式以下:

  • i_{t} = \sigma(W_{ix} * x_{t} + W_{ih} * h_{t-1}+b_{i})
  • \tilde{c_{t}} = tanh(W_{cx} * x_{t} + W_{ch} * h_{t-1} + b_{c})
  • f_{t} = \sigma(W_{fx} * x_{t} + W_{fh} * h_{t-1}+b_{f})
  • c_{t} = f_{t} * c_{t-1} + i_{t} * \tilde{c_{t}}
  • o_{t} = \sigma(W_{ox} * x_{t} + W_{oh} * h_{t-1}+b_{o})
  • h_{t} = o_{t} * tanh(c_{t})

若是你想更學術性地瞭解 LSTM,請閱讀 這篇文章

數據生成器

最簡單的想法是將總量爲 N 的數據集,平均分割成 N/b 個序列,每一個序列包含 b 個數據點。而後咱們假想若干個指針,它們指向每個序列的第一個元素。而後咱們就能夠開始採樣生成數據了。咱們將當前段的指針指向的元素下標看成輸入,並在其後面的 1~5 個元素中隨機挑選一個做爲正確的預測值,由於模型並不老是隻預測緊靠當前時間點的後一個數據。這樣能夠有效避免過擬合。每一次取樣以後,咱們將指針的下標加一,並開始生成下一個數據點。請移步個人 另外一篇教程 來了解更多。

定義超參數

在本節中,咱們將定義若干個超參數。D 是輸入的維數。由於你使用前一天的股價來預測後面的股價,因此 D 應當是 1

num_unrollings 表示單個步驟中考慮的連續時間點個數,越大越好。

而後是 batch_size。它是在單個時間點中考慮的數據樣本數量。它越大越好,由於選取的樣本數量越大,模型能夠參考的數據也就更多。

最後是 num_nodes 決定了每一個單元中包含了多少隱藏神經元。在本例中,網絡中包含三層 LSTM。

D = 1 # 數據的維度
num_unrollings = 50 # 你想預測多遠的結果
batch_size = 500 # 一次批處理中包含的數據個數
num_nodes = [200,200,150] # 使用的深層 LSTM 網絡的每一層中的隱藏節點數
n_layers = len(num_nodes) # 層數
dropout = 0.2 # dropout 機率

tf.reset_default_graph() # 若是你想要屢次運行,這個語句相當重要
複製代碼

定義輸入和輸出

接下來定義用於輸入訓練數據和標籤的 placeholder。由於每一個 placeholder 中只包含一批一維數據,因此這並不難。對於每個優化步驟,咱們須要 num_unrollings 個 placeholder。

# 輸入數據
train_inputs, train_outputs = [],[]

# 根據時間順序展開輸入,爲每一個時間點定義一個 placeholder
for ui in range(num_unrollings):
    train_inputs.append(tf.placeholder(tf.float32, shape=[batch_size,D],name='train_inputs_%d'%ui))
    train_outputs.append(tf.placeholder(tf.float32, shape=[batch_size,1], name = 'train_outputs_%d'%ui))
複製代碼

定義 LSTM 和迴歸層的參數

您將有一個包含三層 LSTM 和一層線性迴歸層的神經網絡,分別用 wb 表示,它獲取上一個長短時間記憶單元的輸出,並輸出對下一個時間的預測。你可使用 TensorFlow 中的 MultiRNNCell 來封裝您建立的三個 LSTMCell 對象。此外,LSTM 單元上還能夠加上 dropout 來提升性能並減小過擬合。

stm_cells = [
    tf.contrib.rnn.LSTMCell(num_units=num_nodes[li],
                            state_is_tuple=True,
                            initializer= tf.contrib.layers.xavier_initializer()
                           )
 for li in range(n_layers)]

drop_lstm_cells = [tf.contrib.rnn.DropoutWrapper(
    lstm, input_keep_prob=1.0,output_keep_prob=1.0-dropout, state_keep_prob=1.0-dropout
) for lstm in lstm_cells]
drop_multi_cell = tf.contrib.rnn.MultiRNNCell(drop_lstm_cells)
multi_cell = tf.contrib.rnn.MultiRNNCell(lstm_cells)

w = tf.get_variable('w',shape=[num_nodes[-1], 1], initializer=tf.contrib.layers.xavier_initializer())
b = tf.get_variable('b',initializer=tf.random_uniform([1],-0.1,0.1))
複製代碼

計算 LSTM 輸出並將結果代入迴歸層進行預測

在本節中,首先建立 TensorFlow 張量 ch 用來保存 LSTM 單元的單元狀態和隱藏狀態。而後將 train_input 轉換爲 [num_unrollings, batch_size, D] 的形狀,這是計算 tf.nn.dynamic_rnn 函數的輸出所必需的。而後用 tf.nn.dynamic_rnn 計算 LSTM 輸出,並將輸出轉化爲一系列 num_unrolling 張量來預測和真實股價之間的損失函數。

# 建立 LSTM 的單元狀態 c 和隱藏狀態 h
c, h = [],[]
initial_state = []
for li in range(n_layers):
  c.append(tf.Variable(tf.zeros([batch_size, num_nodes[li]]), trainable=False))
  h.append(tf.Variable(tf.zeros([batch_size, num_nodes[li]]), trainable=False))
  initial_state.append(tf.contrib.rnn.LSTMStateTuple(c[li], h[li]))

# 由於 dynamic_rnn 函數須要特定的輸出格式,因此咱們對張量進行一些變換
# 請訪問 https://www.tensorflow.org/api_docs/python/tf/nn/dynamic_rnn 來了解更多
all_inputs = tf.concat([tf.expand_dims(t,0) for t in train_inputs],axis=0)

# all_outputs 張量的尺寸是 [seq_length, batch_size, num_nodes]
all_lstm_outputs, state = tf.nn.dynamic_rnn(
    drop_multi_cell, all_inputs, initial_state=tuple(initial_state),
    time_major = True, dtype=tf.float32)

all_lstm_outputs = tf.reshape(all_lstm_outputs, [batch_size*num_unrollings,num_nodes[-1]])

all_outputs = tf.nn.xw_plus_b(all_lstm_outputs,w,b)

split_outputs = tf.split(all_outputs,num_unrollings,axis=0)
複製代碼

損失函數的計算與優化

而後計算損失函數。可是在計算它時有一個值得注意的點。對於每批預測和真實輸出,計算均方偏差。而後將這些均方損失加起來(而非平均值)。最後,定義用於優化神經網絡的優化器。我推薦使用 Adam 這種最新的、性能良好的優化器。

# 在計算損失函數時,你須要注意準確的計算方法
# 由於你要同時計算全部展開步驟的損失函數
# 所以,在展開時取每批數據的平均偏差,並將它們相加獲得最終損失函數

print('Defining training Loss')
loss = 0.0
with tf.control_dependencies([tf.assign(c[li], state[li][0]) for li in range(n_layers)]+
                             [tf.assign(h[li], state[li][1]) for li in range(n_layers)]):
  for ui in range(num_unrollings):
    loss += tf.reduce_mean(0.5*(split_outputs[ui]-train_outputs[ui])**2)

print('Learning rate decay operations')
global_step = tf.Variable(0, trainable=False)
inc_gstep = tf.assign(global_step,global_step + 1)
tf_learning_rate = tf.placeholder(shape=None,dtype=tf.float32)
tf_min_learning_rate = tf.placeholder(shape=None,dtype=tf.float32)

learning_rate = tf.maximum(
    tf.train.exponential_decay(tf_learning_rate, global_step, decay_steps=1, decay_rate=0.5, staircase=True),
    tf_min_learning_rate)

# 優化器
print('TF Optimization operations')
optimizer = tf.train.AdamOptimizer(learning_rate)
gradients, v = zip(*optimizer.compute_gradients(loss))
gradients, _ = tf.clip_by_global_norm(gradients, 5.0)
optimizer = optimizer.apply_gradients(
    zip(gradients, v))

print('\tAll done')
複製代碼

這裏定義與預測相關的 TensorFlow 操做。首先,定義用於輸入的佔位符(sample_input)。而後像訓練階段那樣,定義用於預測的狀態變量(sample_csample_h)。再而後用 tf.nn.dynamic_rnn 函數計算預測值。最後經過線性迴歸層(wb)發送輸出。您還應該定義 reset_sample_state 操做用於重置單元格狀態和隱藏狀態。每次進行一系列預測時,都應該在開始時執行此操做。

print('Defining prediction related TF functions')

sample_inputs = tf.placeholder(tf.float32, shape=[1,D])

# 在預測階段更新 LSTM 狀態
sample_c, sample_h, initial_sample_state = [],[],[]
for li in range(n_layers):
  sample_c.append(tf.Variable(tf.zeros([1, num_nodes[li]]), trainable=False))
  sample_h.append(tf.Variable(tf.zeros([1, num_nodes[li]]), trainable=False))
  initial_sample_state.append(tf.contrib.rnn.LSTMStateTuple(sample_c[li],sample_h[li]))

reset_sample_states = tf.group(*[tf.assign(sample_c[li],tf.zeros([1, num_nodes[li]])) for li in range(n_layers)],
                               *[tf.assign(sample_h[li],tf.zeros([1, num_nodes[li]])) for li in range(n_layers)])

sample_outputs, sample_state = tf.nn.dynamic_rnn(multi_cell, tf.expand_dims(sample_inputs,0),
                                   initial_state=tuple(initial_sample_state),
                                   time_major = True,
                                   dtype=tf.float32)

with tf.control_dependencies([tf.assign(sample_c[li],sample_state[li][0]) for li in range(n_layers)]+
                              [tf.assign(sample_h[li],sample_state[li][1]) for li in range(n_layers)]):  
  sample_prediction = tf.nn.xw_plus_b(tf.reshape(sample_outputs,[1,-1]), w, b)

print('\tAll done')
複製代碼

運行 LSTM

在這裏,你將訓練並預測股票價格在接下來一段時間內的變更趨勢,並觀察預測是否正確。按照如下步驟操做我分享出來的 Jupyter Notebook。

★ 在時間序列上定義一系列起始點 test_points_seq 用於評估你的模型

★ 對於每個時間點

★★ 對於所有的訓練數據

★★★ 將 num_unrollings 展開

★★★ 使用展開的數據訓練神經網絡

★★ 計算訓練的平均損失函數

★★ 對於測試集中的每個起始點

★★★ 經過迭代測試點以前找到的 num_unrollings 中的數據點來更新 LSTM 狀態

★★★ 連續預測接下來的 n_predict_once 步,而後將前一次的預測做爲本次的輸入

★★★ 計算預測值和真實股價之間的均方偏差

將預測結果可視化

你能夠發現,模型的均方偏差在顯著地降低,這意味着模型確實學習到了有用的信息。你能夠經過比較神經網絡產生的均方偏差以及對股價取標準平均的均方偏差(0.004)來量化你的成果。顯然,LSTM 優於標準平均,同時你也能明白股價的標準平均能較好地反映股價地變化。

儘管並不完美,LSTM 在大部分狀況下都能正確預測接下來的股價。並且你只能預測到股票接下來是漲是跌,而非股價的確切值。

總結

希望本教程能幫到你,寫這篇教程也讓我受益不淺。在本教程中,我瞭解到創建可以正確預測股價走勢的模型是很是困難的。首先咱們探討了預測股價的動機。接下來咱們瞭解到如何去下載並處理數據。而後咱們介紹了兩種能夠向後預測一步的平均技術,這兩種方法在預測多步時並無論用。以後,咱們討論瞭如何使用 LSTM 對將來的多步進行預測。最後,結果可視化,並發現這個模型(儘管並不完美)能出色地預測股價走勢。

下面是本教程中對幾個要點:

  1. 股票價格/走勢預測是一項極其困難的任務。就我我的而言,我認爲任何股票預測模型都不徹底正確,所以它們不該該被盲目地依賴。模型並不老是正確的。

  2. 不要相信那些聲稱預測曲線與真實股價徹底重合的文章。那些取平均的方法在實踐中並無論用。更明智的作法是預測股價走勢。

  3. 模型的超參數會顯著影響訓練結果。因此最好使用一些諸如 Grid search 和 Random search 的調參技巧,下面是一系列很是重要的超參數:優化器的學習速率、網絡層數、每層中的隱藏節點個數、優化器(Adam 是最好用的)以及模型的種類(GRU / LSTM / 增長 peephole connection 的 LSTM)

  4. 在本教程中,因爲數據集過小,咱們根據測試損失函數來下降學習速率,這自己是不對的,由於這間接地將有關測試集的信息泄露到訓練過程當中。一種更好的處理方法是使用一個獨立的驗證集(與測試集不一樣),並根據驗證集的性能下降學習速率。

Jupyter Notebook:請訪問個人 GitHub 來獲取。

參考

我參照 這個 理解了怎樣使用 LSTM 來預測股價。可是實現細節與之有很大不一樣。

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索