使用LSTM模型做股票預測【基於Tensorflow】

Dragon Ice發表於2020-11-26

LSTM模型簡介

LSTM是迴圈神經網路的一種,它具有長短時記憶的能力,克服了傳統RNN在輸入序列較長時產生的遺忘問題(即梯度消失)。LSTM通過三個分別稱為遺忘門、輸入門和輸出門的結構控制資訊的輸入輸出。LSTM有兩個狀態h(隱藏狀態)和c(細胞狀態),h控制短期記憶,c控制長期記憶。
其結構示意圖為:
在這裡插入圖片描述
其各個門的數學表達為:
在這裡插入圖片描述
其中小圓圈表示哈達瑪乘積。
最後,再總結一下各個門的客觀意義:
遺忘門:控制上個細胞狀態有多少資訊被保留
輸入門:控制當前的輸入資訊有多少被保留
輸出門:控制當前輸出有多少資訊值得保留

資料集

股票資料總共有九個維度,分別是在這裡插入圖片描述
由於本人對經濟學沒有太多研究,所以這些各個維度所代表的資訊我也不是很清楚,但在我眼裡,它們就是一堆時序資料,而長短時記憶時序模型LSTM處理時序資料具有很強的優勢。
專案資料(儲存格式是excel):https://pan.baidu.com/s/1qcqCDAATaHapMOs2I_qr6A
提取碼:1346

然後我們來簡單觀察一下資料集的分佈。使用pandas庫讀取excel檔案後,將其轉換為numpy陣列,簡單剔除掉代號、日期之類的無用資料後,利用繪相簿matplotlib將各個維度的資料分佈繪製出來,程式如下:

import numpy as np
import pandas as pd
from matplotlib import pyplot as plt

#check train data and test data
train_data = pd.read_excel('train_data.xlsx',index_col=0)
train_arrs = np.array(train_data.iloc[:,:])
trains = train_arrs[:,-9:].astype('float32')

test_data = pd.read_excel('test_data.xlsx',index_col=0)
test_arrs = np.array(test_data.iloc[:,:])
tests = test_arrs[:,-9:].astype('float32')

dim_names = ['open','high','low','close','pre_close','change','pct_chg','vol','amount']

#normalize
for dim in range(trains.shape[1]):
    trains[:,dim] = (trains[:,dim] - trains[:,dim].min()) / (trains[:,dim].max() - trains[:,dim].min())

for dim in range(tests.shape[1]):
    tests[:,dim]  = (tests[:,dim] - tests[:,dim].min()) / (tests[:,dim].max() - tests[:,dim].min())

#visualization of train data
for dim in range(trains.shape[1]):
    plt.subplot(3,3,dim+1)
    plt.plot(trains[:,dim])
    plt.title('%s'%(dim_names[dim]))
plt.show()

#visualization of test data
for dim in range(tests.shape[1]):
    plt.subplot(3,3,dim+1)
    plt.plot(tests[:,dim])
    plt.title('%s'%(dim_names[dim]))
plt.show()

效果如下:
對於訓練集:
在這裡插入圖片描述
對於測試集:
在這裡插入圖片描述
可以簡單看出,各個維度並不是相互獨立的,有些分佈具有很強的相似性。

接著,我們來建立train_batches和test_batches以訓練模型。這裡用到的技巧其實不多,有一點提一下,對於資料集使用歸一化能加快模型的收斂以及抑制梯度爆炸。然後要注意這是預測模型,不是迴歸模型,模型的輸入是前n個time step的由各個維度構成的向量組,輸出的是下個time step的包含各個維度的向量,說起來有點繞,我作了一個簡單的示意圖,希望能理清你們的思路:
在這裡插入圖片描述
從shape的角度理解的話,那麼輸入資料的shape為[BATCH_SIZE,TIME_STEP,INPUT_DIM]。輸出資料的shape為[BATCH_SIZE,INPUT_DIM]
說明一下,該專案的預測只是對於開盤價,也就是’open’這個維度,所以實際的輸出資料的shape為[BATCH_SIZE,1]

建立train/test batches的程式碼如下:

import numpy as np
import pandas as pd
from matplotlib import pyplot as plt

#讀取訓練資料
train_data = pd.read_excel('train_data.xlsx',index_col=0)
train_arrs = np.array(train_data.iloc[:,:])
train_xs = train_arrs[:,-8:].astype('float32')
train_ys = (np.array(train_data['open'],dtype='float32')).reshape(-1,1)
#讀取測試資料
test_data = pd.read_excel('test_data.xlsx',index_col=0)
test_arrs = np.array(test_data.iloc[:,:])
test_xs = test_arrs[:,-8:].astype('float32')
test_ys = (np.array(test_data['open'],dtype='float32')).reshape(-1,1)

#歸一化
train_ys = (train_ys-train_ys.min()) / (train_ys.max() - train_ys.min())
test_ys  = (test_ys-test_ys.min())   / (test_ys.max()  - test_ys.min())
for dim in range(train_xs.shape[1]):
    train_xs[:,dim] = (train_xs[:,dim] - train_xs[:,dim].min()) / (train_xs[:,dim].max() - train_xs[:,dim].min())

for dim in range(test_xs.shape[1]):
    test_xs[:,dim]  = (test_xs[:,dim] - test_xs[:,dim].min()) / (test_xs[:,dim].max() - test_xs[:,dim].min())

#由於是預測任務,那麼資料的第一個維度會少掉一個time_step-1
time_step = 8
input_dim = 8

aranged_train_xs = np.zeros(shape=(train_xs.shape[0]-time_step+1,time_step,input_dim))
for idx in range(aranged_train_xs.shape[0]):
    aranged_train_xs[idx] = train_xs[idx:idx+8]

aranged_test_xs = np.zeros(shape=(test_xs.shape[0]-time_step+1,time_step,input_dim))
for idx in range(aranged_test_xs.shape[0]):
    aranged_test_xs[idx] = test_xs[idx:idx+8]

aranged_train_ys = train_ys[time_step-1:]
aranged_test_ys  =  test_ys[time_step-1:]

#儲存資料
np.save(r'train_x_batch.npy',aranged_train_xs)
np.save(r'train_y_batch.npy',aranged_train_ys)
np.save(r'test_x_batch.npy',aranged_test_xs)
np.save(r'test_y_batch.npy',aranged_test_ys)

搭建/訓練模型

在整理得到資料集後,我們就可以開始建模了。
建模流程(建議大家都多花時間提升下建模能力):
①定義超引數(batch_size、學習率、epochs、神經元數量等)
②定義待訓練引數(從什麼分佈取樣,需要做什麼正則化)
③ 定義一個load_data函式,從之前建立的資料集中讀取資料
④定義LSTM單元,注意預設的啟用函式是tanh,同時可以利用 tf.contrib.rnn.DropoutWrapper增加dropout層。
⑤定義LSTM網路,LSTM接受的是時序資料,所以需要將輸入變成一個列表,列表的長度及時間步數。然後使用列表推導技巧(官方的辦法)定義多層LSTM網路,建議都使用這個技巧,據瞭解別的方式BUG很多。最後使用tf.contrib.rnn.static_rnn得到網路輸出,注意static和dynamic的區別,前者的時間步數是固定的,而後者是可變的,對於我們創造的資料集,每個Batch的時間步數都相同,所以我們使用static(靜態)方式。
⑥訓練模型,對於Tensorflow框架,其訓練模型的方式大家都應該爛熟於心了。定義placeholder,定義預測函式、損失函式、優化函式等等。然後啟動Session(互動式或非互動式),最後迭代進行訓練。

整個過程的程式碼如下:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import tensorflow as tf
plt.rcParams['font.sans-serif']=['SimHei']#顯示中文
plt.rcParams['axes.unicode_minus']=False#顯示負號

#Hyperparams
batch_size = 128
lr = 1e-6
epochs = 600
num_neurons = [32,32,64,64,128,128]
kp = 1.0

#定義輸出層的weight和bias
w = tf.Variable(tf.random_normal([num_neurons[-1],1]))
b = tf.Variable(tf.random_normal([1]))

def load_data():
    train_x_batch = np.load(r'train_x_batch.npy',allow_pickle=True)
    train_y_batch = np.load(r'train_y_batch.npy',allow_pickle=True)
    return (train_x_batch,train_y_batch)

#定義lstm單元
def lstm_cell(units,keep_prob):
    cell = tf.contrib.rnn.BasicLSTMCell(num_units=units,forget_bias=0.9)#activation預設為tanh
    return tf.contrib.rnn.DropoutWrapper(cell, output_keep_prob=keep_prob)

#定義lstm網路
def lstm_net(x,w,b,num_neurons,keep_prob):
    #將輸入變成一個列表,列表的長度及時間步數
    inputs = tf.unstack(x,8,1)
    cells = [lstm_cell(units=n,keep_prob = keep_prob) for n in num_neurons]
    stacked_lstm_cells = tf.contrib.rnn.MultiRNNCell(cells)
    outputs,_ =  tf.contrib.rnn.static_rnn(stacked_lstm_cells,inputs,dtype=tf.float32)
    return tf.matmul(outputs[-1],w) + b


if __name__ == '__main__':

    #載入資料
    (train_x,train_y) = load_data()

    #定義placeholder
    x = tf.placeholder(shape=(None,8,8),dtype=tf.float32)
    y = tf.placeholder(shape=(None,1),dtype=tf.float32)
    keep_prob = tf.placeholder(tf.float32,[])

    #定義預測函式、損失函式、優化函式、初始函式、儲存函式
    pred = lstm_net(x,w,b,num_neurons,keep_prob)
    cost = tf.reduce_mean(tf.reshape(tf.pow((pred-y),2),[-1]))
    optimizer = tf.train.GradientDescentOptimizer(learning_rate=lr).minimize(cost)
    init  = tf.global_variables_initializer()
    saver = tf.train.Saver(tf.global_variables())

    #啟動互動式Session
    sess = tf.InteractiveSession()

    #訓練模型
    sess.run(init)
    losses = []#記錄每個epoch結束時的損失值
    for epoch in range(epochs):
        for step in range(train_x.shape[0]//batch_size+1):
            batch_x = train_x[step*batch_size:(step+1)*batch_size]
            batch_y = train_y[step*batch_size:(step+1)*batch_size]
            sess.run(optimizer,feed_dict={x:batch_x,y:batch_y,keep_prob:kp})
            
        loss = sess.run(cost,feed_dict={x:batch_x,y:batch_y,keep_prob:1.0})
        losses.append(loss)
        print('Epoch[{}/{}],Loss = {:.4f}\n'.format(epoch+1,epochs,loss))

    #視覺化訓練過程
    plt.plot(losses)
    plt.ylim(0,1.2*max(losses))
    plt.title('損失值隨迭代週期的改變')
    plt.xlabel('Epoch')
    plt.ylabel('損失值')
    plt.show()

    #儲存模型
    #saver.save(sess,r'model_data\my_model.ckpt')

    #關閉會話
    sess.close()

訓練過程的損失曲線為:
在這裡插入圖片描述

使用模型進行預測

重頭戲來了,所謂是騾子還是馬,拉出來遛一遛就知道了,你的建模能力再強,資料處理能力再好,如果模型最終效果不行,都白搭。
要做預測,就要寫個predictor指令碼,方式其實更建模差不多,區別就是不用訓練引數,而是載入已經訓練好的引數,程式碼如下:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import tensorflow as tf
plt.rcParams['font.sans-serif']=['SimHei']#顯示中文
plt.rcParams['axes.unicode_minus']=False#顯示負號

def load_data():
    test_x_batch = np.load(r'test_x_batch.npy',allow_pickle=True)
    test_y_batch = np.load(r'test_y_batch.npy',allow_pickle=True)
    return (test_x_batch,test_y_batch)

#定義lstm單元
def lstm_cell(units):
    cell = tf.contrib.rnn.BasicLSTMCell(num_units=units,forget_bias=0.0)#activation預設為tanh
    return cell

#定義lstm網路
def lstm_net(x,w,b,num_neurons):
    #將輸入變成一個列表,列表的長度及時間步數
    inputs = tf.unstack(x,8,1)
    cells = [lstm_cell(units=n) for n in num_neurons]
    stacked_lstm_cells = tf.contrib.rnn.MultiRNNCell(cells)
    outputs,_ =  tf.contrib.rnn.static_rnn(stacked_lstm_cells,inputs,dtype=tf.float32)
    return tf.matmul(outputs[-1],w) + b

#超引數
num_neurons = [32,32,64,64,128,128]

#定義輸出層的weight和bias
w = tf.Variable(tf.random_normal([num_neurons[-1],1]))
b = tf.Variable(tf.random_normal([1]))

#定義placeholder
x = tf.placeholder(shape=(None,8,8),dtype=tf.float32)

#定義pred和saver
pred = lstm_net(x,w,b,num_neurons)
saver = tf.train.Saver(tf.global_variables())

if __name__ == '__main__':

    #開啟互動式Session
    sess = tf.InteractiveSession()
    saver.restore(sess,r'D:\股票預測\model_data\my_model.ckpt')

    #載入資料
    test_x,test_y = load_data()

    #預測
    predicts = sess.run(pred,feed_dict={x:test_x})
    predicts = ((predicts.max() - predicts) / (predicts.max() - predicts.min()))#數學校準

    #視覺化
    plt.plot(predicts,'r',label='預測曲線')
    plt.plot(test_y,'g',label='真實曲線')
    plt.xlabel('第幾天/days')
    plt.ylabel('開盤價(歸一化)')
    plt.title('股票開盤價曲線預測(測試集)')
    plt.legend()
	plt.show()
    #關閉會話
    sess.close()	

最終效果如圖所示:
在這裡插入圖片描述

後話

在訓練過程中,本人遇到過兩個問題,我將它們命名為顛倒性和超前/滯後性。前者是指,最終的預測曲線的趨勢與真實曲線完完全全相反,比如真實曲線上升達到極大值,預測曲線就下降達到極小值,然而,在將預測曲線上下顛倒後,其與真實曲線又能很好吻合!在經過仔細地研究和排查後,發現這種現象貌似不是來源於程式,而是具有隨機性,或者用哲學的話來說,感覺這是時序模型的一個種屬(就像性慾之於人類)。後者是所有時序模型的通病,就是說你的預測模型會永遠多多少少比真實模型超前或者滯後。怎麼說呢,這兩點的存在,使得預測和現實總是有個無法逾越的鴻溝。希望未來能看到能夠完美預測的時序模型。

相關文章