1. 程式人生 > 其它 >使用TensorFlow實現股票價格預測深度學習模型

使用TensorFlow實現股票價格預測深度學習模型

Sebastian Heinz. A simple deep learning model for stock price prediction using TensorFlow

在最近的黑客馬拉松中,我們在STATWORX上進行協作,團隊的一些成員利用Google Finance API抓取了每分鐘的標準普爾500指數。除了標準普爾500指數以外,我們還收集了其對應的500家公司的股價。在得到了這些資料之後,我立刻想到了一點子:基於標準普爾指數觀察的500家公司的股價,用深度學習模型來預測標準普爾500指數。

把玩這些資料並用TensorFlow在其上建立深度學習模型是很有趣的,所以我決定寫下這篇文章:預測標準普爾500指數的簡易TensorFlow教程。你將看到的不是一個深入的教程,更多的是從高層次來講解TensorFlow模型的重要構成元件和概念。我編寫的Python程式碼並沒有做專門的效能優化但是可讀性還可以。

下載我使用的資料集

注意:本文只是基於TensorFlow的一個實戰教程。真正預測股價是非常具有挑戰性的,尤其在分鐘級這樣頻率較高的預測中,要考慮的因素的量是龐大的。

匯入資料集

我們的團隊將抓取到的股票資料從爬蟲伺服器上匯出為CSV格式的檔案。該資料集包含了從2017年四月到八月共計n=41266分鐘的標準普爾500指數以及500家公司的股價。

# 匯入資料
data = pd.read_csv('data_stocks.csv')
# 移除日期列
data = data.drop(['DATE'], 1)
# 資料集的維度
n = data.shape[0]
p = data.shape[1]
# 將資料集轉化為numpy陣列
data = data.values

資料是經過清洗準備好的,這意味著指數資料和股票資料是遵循LOCF(Last Observation Carried Forward)方法的,所以檔案中不包含任何的缺失值。

可以通過pyplot.plot('SP500')來快速檢視標準普爾500指數的時間序列。

Time series plot of the S&P 500 index.

注意:這裡展示的是標普500指數的領先(lead),也就是說其值是原始值在時間軸上後移一分鐘得到的。因為我們要預測的是下一分鐘的指數而不是當前的指數,所以這一操作是必不可少的。

準備訓練集和測試集資料

原始資料集被劃分為訓練集和測試集。訓練資料集包含了整個資料集的80%。注意這裡的資料集劃分不是隨機劃分得到的,而是順序切片得到的。訓練資料集是從2017年的4月到大約7月底,測試資料集則為到17年8月底的剩餘資料。

# 劃分訓練集和測試集
train_start = 0
train_end = int(np.floor(0.8*n))
test_start = train_end + 1
test_end = n
data_train = data[np.arange(train_start, train_end), :]
data_test = data[np.arange(test_start, test_end), :]

時間序列的交叉驗證方法有很多,像有無refitting或其他像time series bootstrap resampling的精細概念的滾動預測(rolling forecasts)。後者(time series bootstrap resampling)中的重複樣本是考慮時間序列的週期性分解的結果,這是為了使模擬取樣同樣具有周期性的特徵而不是單單複製取樣值。

資料縮放

大多數的神經網路都受益於輸入值的縮放(有時也有輸出值)。為什麼呢?因為大多數神經網路的激勵函式都是定義在0, 1區間或-1, 1區間,像sigmoid函式和tanh函式一樣。雖然如今線性整流單元已經被廣泛引用於無界的啟用值問題中,但是我們還是選擇將輸入輸出值做統一的縮放。縮放操作可以通過sklearn中的MinMaxScaler輕鬆實現。

# 資料縮放
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
scaler.fit(data_train)
data_train = scaler.transform(data_train)
data_test = scaler.transform(data_test)
# 構建 X and y
X_train = data_train[:, 1:]
y_train = data_train[:, 0]
X_test = data_test[:, 1:]
y_test = data_test[:, 0]

備註:應當仔細考慮好什麼資料要在什麼時候被縮放。一個常見的錯誤是在訓練集和測試集劃分前進行特徵縮放。為什麼這樣做是錯誤的呢?因為縮放的計算需要呼叫資料的統計值(像資料的最大最小值)。當你在真實生活中進行預測時你並沒有來自未來的觀測資訊,所以相應地,訓練資料特徵縮放所用的統計值應當來源於訓練集,測試集也一樣。否則,在預測時使用了包含未來資訊往往會導致效能指標向好的方向偏移。

TensorFlow簡介

TensorFlow是一個非常棒的軟體,是深度學習和神經網路計算框架中的領頭羊。它的底層後端是用C++編寫的,通常通過Python來進行控制(還有R語言版的TensorFlow,由RStudio維護)。TensorFlow用圖來描述底層的計算任務,這種方法使得使用者可以通過表徵資料,變數和操作的元素組合得到的計算圖來指定相應的數學操作。由於神經網路實際上就是資料和數學操作的圖,TensorFlow可以完美地應用於神經網路和深度學習,可以看下面給出的一個簡單例子(取自作者的博文:Deep learning introduction

A very simple graph that adds two numbers together.

上圖中兩個數字要完成加和的操作。兩個加數被儲存在兩個變數ab當中,他們的值流入了正方形節點,即代表他們完成相加操作的位置。加和的結果被儲存在另一個變數c中。事實上,abc都可以被視為佔位符。任何被填入ab的數字將在完成加和操作後存入c中。這就是TensorFlow的工作原理,使用者通過變數和佔位符來定義模型(神經網路)的抽象表示。隨後,佔位符被實際的數字填充並開始進行實際的運算。下面的程式碼實現了上面簡單的計算圖。

# 引入 TensorFlow
import tensorflow as tf

# 定義 a 和 b 為佔位符
a = tf.placeholder(dtype=tf.int8)
b = tf.placeholder(dtype=tf.int8)

# 定義加法運算
c = tf.add(a, b)

# 初始化圖
graph = tf.Session()

# 執行圖
graph.run(c, feed_dict={a: 5, b: 4})

在引入TensorFlow的庫之後,兩個佔位符可以以tf.placeholder()的方式定義,對應上面圖示中左側兩個藍色的圖形。隨後通過tf.add()來定義數學加法操作,運算的結果為c = 9。當建立佔位符之後,可以用任意的整數值a,b來執行計算圖。當然,以上的問題不過是一個簡單的示例而已,真正神經網路中的圖和運算要複雜得多。

佔位符

正如上面所說,所有的過程都從佔位符開始。為了擬合模型,我們需要定義兩個佔位符:X包含模型輸入(在T = t時刻500個成員公司的股價),Y為模型輸出(T = t + 1時刻的標普指數)。

佔位符的shape分別為[None, n_stocks][None],意味著輸入為二維矩陣,輸出為一維向量。設計出恰當的神經網路的必要條件之一就是清楚神經網路需要的輸入和輸出維度。

# 佔位符
X = tf.placeholder(dtype=tf.float32, shape=[None, n_stocks])
Y = tf.placeholder(dtype=tf.float32, shape=[None])

None值代表著我們當前不知道每個批次中流經神經網路的觀測值數量,所以為了保持該量的彈性,我們用None來填充。稍後我們將定義控制每個批次中觀測樣本數量的變數batch_size

變數

除了佔位符,TensorFlow中的另一個基本概念是變數。佔位符在圖中用來儲存輸入資料和輸出資料,變數在圖的執行過程中可以變化,是一個彈性的容器。為了在訓練中調整權重和偏置,它們被定義為變數。變數需要在訓練開始前進行初始化。變數的初始化稍後我們會單獨講解。

我們的模型包含四個層。第一層有1024個神經元,比輸入變數的兩倍還要多一點。緊接在後面的隱藏層是前面一層的一半,即後面層的神經元個數分別為512,256和128。每層中神經元數量的減少也意味著資訊量的壓縮。當然還有其他的神經網路結構,但是不在本文的討論範圍當中。

# 模型結構引數
n_stocks = 500
n_neurons_1 = 1024
n_neurons_2 = 512
n_neurons_3 = 256
n_neurons_4 = 128
n_target = 1
# 第一層 : 隱藏層權重和偏置變數
W_hidden_1 = tf.Variable(weight_initializer([n_stocks, n_neurons_1]))
bias_hidden_1 = tf.Variable(bias_initializer([n_neurons_1]))
# 第二層 : 隱藏層權重和偏置變數
W_hidden_2 = tf.Variable(weight_initializer([n_neurons_1, n_neurons_2]))
bias_hidden_2 = tf.Variable(bias_initializer([n_neurons_2]))
# 第三層: 隱藏層權重和偏置變數
W_hidden_3 = tf.Variable(weight_initializer([n_neurons_2, n_neurons_3]))
bias_hidden_3 = tf.Variable(bias_initializer([n_neurons_3]))
# 第四層: 隱藏層權重和偏置變數
W_hidden_4 = tf.Variable(weight_initializer([n_neurons_3, n_neurons_4]))
bias_hidden_4 = tf.Variable(bias_initializer([n_neurons_4]))

# 輸出層: 輸出權重和偏置變數
W_out = tf.Variable(weight_initializer([n_neurons_4, n_target]))
bias_out = tf.Variable(bias_initializer([n_target]))

清楚輸入層,隱藏層和輸出層的變數對應的維度是非常重要的。在多層感知機的經驗法則中(MLPs,本文就是按照該準則設計的網路),前一層權重的維度陣列中的第二個元素與當前層中權重維度陣列的第一個元素數值相等。聽起來可能有些複雜,但是為了使當前層的輸入作為輸入傳入下一層,這樣的法則是必要的。偏置的維度等於當前層權重維度陣列中的第二個元素,對應當前層中神經元的數量。

設計網路架構

在定義了所需的權重和偏置變數之後,網路的拓撲結構即網路的架構需要被確定下來。在TensorFlow中,即需要將佔位符(資料)和變數(權重和偏置)整合入矩陣乘法的序列當中。

除此之外,神經網路中是經過了啟用函式的轉換的。啟用函式是神經網路架構中非常的元素之一,在非線性系統中尤其如此。目前已經有很多中可供使用的啟用函式,本文中的模型選用了最常用的整流線性單元(ReLU)。

# 隱藏層
hidden_1 = tf.nn.relu(tf.add(tf.matmul(X, W_hidden_1), bias_hidden_1))
hidden_2 = tf.nn.relu(tf.add(tf.matmul(hidden_1, W_hidden_2), bias_hidden_2))
hidden_3 = tf.nn.relu(tf.add(tf.matmul(hidden_2, W_hidden_3), bias_hidden_3))
hidden_4 = tf.nn.relu(tf.add(tf.matmul(hidden_3, W_hidden_4), bias_hidden_4))

# 輸出層 (必須經過轉置)
out = tf.transpose(tf.add(tf.matmul(hidden_4, W_out), bias_out))

下面的圖形說明了網路架構。模型一共包含了三個主要的元件:輸入層,隱藏層和輸出層。圖示的結構被稱為前饋網路,前饋意味著從左側輸入的資料將徑自向右傳播。與之相對的網路結構如recurrent neural networks(RNN)允許資料流在網路結構中反向傳播。

我們使用的前饋網路架構圖展示

損失函式

網路的損失函式可以根據網路的預測值和訓練集中的實際觀測值來生成度量偏差程度的指標。在迴歸問題當中,最常用的損失函式為均方誤差(MSE)。均方誤差計算的就是預測值和目標值的誤差平方值的平均值。基本上任何可微函式都可以用於計算預測值和目標值之間的偏差程度。

# 損失函式
mse = tf.reduce_mean(tf.squared_difference(out, Y))

但是,在我們的問題中,MSE展示出了一些更有利與解決我們問題的特性。

優化器

優化器負責訓練過程中調整網路的權重和偏置的關鍵操作。這些操作中包含著梯度運算,梯度方向對應的就是訓練過程中最小化網路損失函式的方向。穩定而又高效的優化器是神經網路中深入研究的課題之一。

# 優化器
opt = tf.train.AdamOptimizer().minimize(mse)

這裡我們使用Adam優化器,目前它是深度學習中預設的優化器。Adam的全稱為Adaptive Moment Estimation,可以視為其他兩個優化器AdaGrad和RMSProp的結合。

初始化器

初始化器用於在訓練前初始化網路的權重。由於神經網路是利用數值方法進行訓練,所以優化問題的起始點是能否找到問題的最優解(或次優解)的關鍵因素之一。TensorFlow中內建了多種優化器,每個優化器使用了不同的初始化方法。這裡我使用的是預設的初始化器之一——tf.variance_scaling_initializer()

# 初始化器
sigma = 1
weight_initializer = tf.variance_scaling_initializer(mode="fan_avg", distribution="uniform", scale=sigma)
bias_initializer = tf.zeros_initializer()

注意:在TensorFlow的計算圖中,不同的變數可以定義不同的初始化函式。不過在大多數情況下統一的初始化函式就可以滿足要求了。

擬合神經網路

在定義了網路的佔位符,變數,初始化器,損失函式和優化器之後,模型需要進入正式的訓練過程。通常我們使用minibatch的方式進行訓練(小的batch size)。在這種訓練方式中,我們從訓練集中隨機抽取n = sample_size的資料樣本送入網路進行訓練。訓練集被劃分為n / batch_size個批次並按順序送入網路。這時佔位符XY參與了這一過程,它們分別儲存輸入值和目標值並作為輸入和目標送入網路。

樣本資料X將在網路中傳播直至輸出層。到達輸出層後,TensorFlow將把模型的當前預測值與當前批次的實際觀測值Y進行比較。隨後,TensorFlow將根據選擇的學習方案對網路引數進行優化更新。權重和偏置更新完畢後,下一批取樣資料將再次送入網路並重復這一過程。這一過程將一直持續至所有批次的資料都已經送入網路。所有的批次構成的一個完整訓練過程被稱為一個epoch。

當達到訓練批次數或者使用者指定的標準之後,網路的訓練停止。

# 定義會話
net = tf.Session()
# 執行初始化器
net.run(tf.global_variables_initializer())

# 設定用於展示互動的圖表
plt.ion()
fig = plt.figure()
ax1 = fig.add_subplot(111)
line1, = ax1.plot(y_test)
line2, = ax1.plot(y_test*0.5)
plt.show()

# 設定 epochs 數和每批次的資料量
epochs = 10
batch_size = 256

for e in range(epochs):

    # 打亂訓練集
    shuffle_indices = np.random.permutation(np.arange(len(y_train)))
    X_train = X_train[shuffle_indices]
    y_train = y_train[shuffle_indices]

    # Minibatch 訓練
    for i in range(0, len(y_train) // batch_size):
        start = i * batch_size
        batch_x = X_train[start:start + batch_size]
        batch_y = y_train[start:start + batch_size]
        # 在當前batch上執行優化器
        net.run(opt, feed_dict={X: batch_x, Y: batch_y})

        # 展示進度
        if np.mod(i, 5) == 0:
            # Prediction
            pred = net.run(out, feed_dict={X: X_test})
            line2.set_ydata(pred)
            plt.title('Epoch ' + str(e) + ', Batch ' + str(i))
            file_name = 'img/epoch_' + str(e) + '_batch_' + str(i) + '.jpg'
            plt.savefig(file_name)
            plt.pause(0.01)
# 展示訓練結束時最終的MSE
mse_final = net.run(mse, feed_dict={X: X_test, Y: y_test})
print(mse_final)

每隔5個批次的訓練,我們用測試集(網路沒有在這些資料上進行訓練)來評估一次模型的預測效能並進行視覺化。我們特意將每個節點的影象到處至磁碟製作了一個視訊來展示訓練的過程。可以看到模型很快習得了原始時間序列的形狀和位置並且在一定的epochs後可以達到比較準確的預測值。這真是太好了!

可以觀察到模型先是迅速習得了時間序列的大致形狀,隨後繼續學習資料中精細結構。這與Adam學習方案為了避免越過最小優化值而不斷降低學習率是相互照應的。在10個epochs後,我們完美地擬合了訓練資料!最終的MSE只有0.00078(注意到我們的資料是縮放過的,所以這個值其實已經很小了)。在測試集絕對誤差的佔比等於5.31%,表現不錯。注意:這只是測試集上的效果,並不能代表實際場景中的效能。

標普指數預測值和實際值的散點圖(縮放後)

這裡再給出一些可以進一步提升結果的方法:規劃網路層數和神經元個數,選擇不同的初始化和啟用方案,引入神經元的dropout層,early stopping等等。除此之外,換用其他型別的深度學習模型,比方說RNN也許可以在任務上達到更優的效能。在此我們不做討論,讀者可以自行嘗試。

總結與展望

TensorFlow的釋出是深度學習研究的一個里程碑。它的靈活性和良好的效能使研究者可以藉助它完成一系列複雜的網路結構以及其他機器學習演算法。不過,與Keras或Mxnet的高層級API相比,TensorFlow高度的靈活性是以增加模型建立的時間週期為代價的。儘管如此,我仍然認為TensorFlow會在神經網路和深度學習的理論研究和實際應用中走向標準化。我們的很多顧客已經開始使用TensorFlow並用它來開發專案,我們在STATWORX上的資料科學顧問也越來越頻繁地使用TensorFlow進行研究和開發。看過了Google對TensorFlow的未來規劃後,我覺得有一件事被遺忘了(從我的觀點來看),就是利用TensorFlow作為後端去設計和開發神經網路的標準使用者介面。當然,可能Google已經在做了:)

本文的程式碼可以從Github上下載