Long Short-Term Memory networks(長-短期記憶網絡),簡稱 LSTMs,可用於時間序列預測。有許多類型的LSTM模型可用於每種特定類型的時間序列預測問題。本文介紹瞭如何爲一系列標準時間序列預測問題開發一套LSTM模型。本文旨在爲每種類型的時間序列問題提供所對應模型的示例,你可以依此爲模板,針對自己的業務需求進行修改。本文的主要內容爲:
- 如何開發適用於單變量時間序列預測的LSTM模型。
- 如何開發適用於多變量時間序列預測的LSTM模型。
- 如何開發適用於多步時間序列預測的LSTM模型。
代碼環境:
- python 3.7.6
- tensorflow 2.1.0
- keras 2.3.1
日期:2020/4/7
文章目錄
滑動窗口是處理時間序列數據通用的方法,之前的時間序列預測的文章中,示例代碼其實是簡單的滑動窗口。先通過兩張相關圖片來直觀理解滑動窗口是如何建模的。
-
滑動窗口在時間序列預測任務上的建模方法:
對於時間序列預測任務來講,以一定滑動窗口大小和滑動步長在原始時間序列上截取數據,以之前的滑動窗口內的數據作爲訓練樣本X,以需要預測的之後的某一時間步長內的的數據作爲樣本標籤y,即從歷史數據中學習映射關係來預測未來數據。 -
滑動窗口在時間序列分類任務上的建模方法:
在時間序列分類任務中,使用滑動窗口分割數據並獲取標籤的過程如上圖所示。傳感器信號稱爲特徵(features),傳感器信號的通道數就是特徵的尺寸;傳感器信號被一個個滑動窗口按照指定的滑動步長(time steps)截取數據,一個窗口內截取的採樣點的數據稱爲樣本。滑動窗口所截取的最後一個採樣點的數據標籤作爲樣本標籤。
前言
本文分爲四個部分,將按照如下行文順序進行介紹:
- Univariate LSTM Models
- Multivariate LSTM Models
- Multi-step LSTM Models
- Multivariate Multi-step LSTM Models
1. 單變量 LSTM 模型(Univariate LSTM Models)
LSTMs可用於單變量時間序列預測問題的建模。這些問題由單個觀測序列組成,需要一個模型從過去的觀測序列中學習,以預測序列中的下一個值。下文將演示一元時間序列預測的LSTM模型。本節分爲六個部分:
- Data Preparation
- Vanilla LSTM
- Stacked LSTM
- Bidirectional LSTM
- CNN-LSTM
- ConvLSTM
上邊的5種LSTM模型都可用於單時間步單變量時間序列預測,也可用於其他類型時間序列預測問題。
1.1 數據準備
在對單變量序列建模之前,必須做好準備。LSTM模型學習一個映射規則,該規則以過去的序列觀測值作爲輸入,然後輸出預測值。因此,觀測序列必須轉換成LSTM可以學習的多個樣本(由多個觀測值組成)。考慮一個單變量序列:
[10, 20, 30, 40, 50, 60, 70, 80, 90]
我們可以將序列分成多個輸入/輸出模式,稱爲樣本,其中三個時間步(的觀測值)作爲輸入,一個時間步(的觀測值)作爲輸出:
X, y
10, 20, 30, 40
20, 30, 40, 50
30, 40, 50, 60
...
我們可以通過寫一個函數來實現上述功能,爲了增加文章可讀性,減少重複,此處先不貼出代碼,會在完整代碼中貼出。先看下劃分結果,使用該函數對整個數據集進行劃分,將單變量序列拆分爲6個樣本(sample),其中每個樣本有3個輸入時間步(的觀測值)和1個輸出時間步(的觀測值)。
[10 20 30] 40
[20 30 40] 50
[30 40 50] 60
[40 50 60] 70
[50 60 70] 80
[60 70 80] 90
數據集構建好之後,接下來定義網絡模型,進行訓練。
1.2 Vanilla LSTM
剛開始接觸LSTM的時候,碰到“Vanilla”還以爲是“香草”的意思,心想作者也太文藝了,後來才知道是自己才疏學淺,“Vanilla”是“普通的,簡單的”意思…
Vanilla LSTM是一個LSTM模型,它有一個單隱層的LSTM單元和一個用於預測的輸出層。LSTM可以很好的完成序列預測任務。與CNN讀取整個輸入向量不同,LSTM模型一次讀取序列的一個時間步(的觀測值),並建立一個內部狀態表示,來學習上下文信息,從而進行預測。以下爲單變量時間序列預測任務定義了一個簡單的LSTM模型:
model = Sequential()
model.add(LSTM(50, activation='relu', input_shape=(n_steps, n_features)))
model.add(Dense(1))
model.compile(optimizer='adam', loss='mse')
該模型中,定義了一個有50個LSTM 單元的單隱層和一個預測單個數值的輸出層。該模型利用有效的隨機梯度下降Adam模型進行擬合,並利用均方誤差(mse)損失函數進行優化。
模型定義中的關鍵是 input_shape
參數,當初花了很長時間來理解,現在算是稍有眉目吧。如果不好理解,可以用 jupyter notebook 一步一步地看,看每個變量的shape,也可以使用IDE debug,再結合API的官方文檔,幫助理解。這裏有一點需要注意,如果查LSTM官方文檔的話,這個API中是沒有input_shape
這個參數的,那爲什麼要指定?這其實是用於構建順序模型的 Sequential()
API所要求提供的的輸入數據格式,只需要在第一層輸入數據尺寸,後面層Keras會自動計算。我們的示例中使用的是一個單變量序列,因此特徵數(features)爲1。相信很多人跟我一樣,對 n_steps
這個參數也有疑惑,其實就是 滑動窗口的寬度,也可以理解爲一個樣本中包含觀測值的數量。這個參數在 split_sequence()
函數中已經定義了,還不明白的,可以結合這個函數再理解理解,在我們的示例中,這個參數爲3。
這裏貼一下所用API的官方文檔:
模型定義完成之後,需要考慮Keras中模型擬合API fit()
的數據輸入維度要求,每個樣本的輸入形狀在單隱層的LSTM層中的 input_shape
參數中指定。因爲有多個訓練樣本,所以模型擬合API fit()
要求輸入數據的形狀爲 [樣本,時間步,特徵]([samples, timesteps, features])
。上一節中的 split_sequence()
函數輸出 X
的形狀爲 [samples,timesteps]
,因此我們可以通過 reshape()
方法來爲訓練數據集添加特徵維度。代碼實現:
n_features = 1
X = X.reshape((X.shape[0], X.shape[1], n_features))
完整代碼在該小節結束後貼出,此處只放出結果:
[10 20 30] 40
[20 30 40] 50
[30 40 50] 60
[40 50 60] 70
[50 60 70] 80
[60 70 80] 90
X:
[[[10]
[20]
[30]]
[[20]
[30]
[40]]
[[30]
[40]
[50]]
[[40]
[50]
[60]]
[[50]
[60]
[70]]
[[60]
[70]
[80]]]
y:
[40 50 60 70 80 90]
X.shape:(6, 3, 1), y.shape:(6,)
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
lstm (LSTM) (None, 50) 10400
_________________________________________________________________
dense (Dense) (None, 1) 51
=================================================================
Total params: 10,451
Trainable params: 10,451
Non-trainable params: 0
_________________________________________________________________
None
[[[70]
[80]
[90]]]
yhat: [[102.01498]]
1.3 Stacked LSTM
Stacked LSTM意即堆疊LSTM,可以將多個LSTM層堆疊在一起。LSTM層需要三維輸入,LSTM的默認輸出爲二維,可以通過設置 return_sequences=True
參數使得LSTM的輸出形狀爲三維。這樣就可以作爲下一層的輸入。代碼實現:
model = Sequential()
model.add(LSTM(50, activation='relu', return_sequences=True, input_shape=(n_steps,
n_features)))
model.add(LSTM(50, activation='relu'))
model.add(Dense(1))
model.compile(optimizer='adam', loss='mse')
1.4 Bidirectional LSTM
在一些序列預測問題上,允許LSTM模型向前和向後學習輸入序列,並將兩種解釋連接起來是有益的。這稱爲雙向LSTM。我們可以通過將第一個隱藏層包裝在一個稱爲雙向的包裝層中來實現用於單變量時間序列預測的雙向LSTM。代碼實現:
# define model
model = Sequential()
model.add(Bidirectional(LSTM(50, activation='relu'), input_shape=(n_steps, n_features)))
model.add(Dense(1))
model.compile(optimizer='adam', loss='mse')
1.5 CNN-LSTM
卷積神經網絡,簡稱CNN,是爲處理二維圖像數據而開發的一種神經網絡。CNN可以非常有效地從一維序列數據(如單變量時間序列數據)中自動提取和學習特徵。CNN模型可用於具有LSTM後端的混合模型中,其中CNN用於解釋輸入的子序列,這些子序列作爲一個序列提供給LSTM模型來解釋。這種混合模型稱爲CNN-LSTM。
第一步是將輸入序列分割成可以由CNN模型處理的子序列。例如,我們可以首先將單變量時間序列數據分成輸入/輸出樣本,其中四個步驟作爲輸入,一個步驟作爲輸出。然後,每個樣本可以分成兩個子樣本,每個子樣本有兩個時間步。CNN可以解釋兩個時間步的每個子序列,並將子序列的時間序列解釋提供給LSTM模型作爲輸入進行處理。我們可以將其參數化,並將子序列的數量定義爲 n_seq
,將每個子序列的時間步數定義爲n_steps
。然後可以對輸入數據進行整形,使其具有所需的結構:[示例、子序列、時間步、功能]([samples, subsequences, timesteps, features])
。例如:
n_steps = 4
# 劃分成樣本
X, y = split_sequence(raw_seq, n_steps)
# 重塑形狀 [samples, timesteps] 變爲 [samples, subsequences, timesteps, features]
n_features = 1
n_seq = 2
n_steps = 2
X = X.reshape((X.shape[0], n_seq, n_steps, n_features)
我們希望在分別讀取每個數據子序列時重用相同的CNN模型。這可以通過將整個CNN模型包裝在一個 Keras API TimeDistributed
包裝器中來實現,該包裝器將對每個輸入數據子序列應用一次整個模型。定義的CNN模型有一個用於讀取子序列的卷積層,該子序列需要指定filter的數目和kernel_size的大小。filter的數目是對輸入序列的讀取或解釋的數目。kernel_size是輸入序列的每個讀取操作包含的時間步數。卷積層之後是最大池化層,它將filter提取的特徵圖映射到其大小的1/4,其中包含最顯著的特徵。然後,這些結構被展平成一個一維向量,用作LSTM層的單個輸入時間步。代碼實現:
model.add(TimeDistributed(Conv1D(filters=64, kernel_size=1, activation='relu'), input_shape=(None, n_steps, n_features)))
model.add(TimeDistributed(MaxPooling1D(pool_size=2)))
model.add(TimeDistributed(Flatten()))
model.add(LSTM(50, activation='relu'))
model.add(Dense(1)
1.6 ConvLSTM
與CNN-LSTM相關的一種LSTM是ConvLSTM,它將輸入的卷積讀取直接構建到每個LSTM單元中。該方法可用於二維時空數據的讀取,也可用於單變量時間序列預測。該層期望輸入爲二維圖像序列,因此輸入數據的形狀必須是:[樣本、時間步長、行、列、特徵]( [samples, timesteps, rows, columns, features])
。
我們可以將每個樣本分割成子序列,其中 timesteps
將成爲子序列的數量,或 n seq
,而列將是每個子序列的時間步長,或 n_steps
。對於一維數據,rows
固定爲1。分析清楚後,接下來重塑樣本。代碼實現:
n_steps = 4
# 將序列數據劃分成樣本
X, y = split_sequence(raw_seq, n_steps)
# 重塑形狀 [samples, timesteps] 變爲 [samples, timesteps, rows, columns, features]
n_features = 1
n_seq = 2
n_steps = 2
X = X.reshape((X.shape[0], n_seq, 1, n_steps, n_features))
我們可以根據過濾器的數量將ConvLSTM定義爲單層,根據 (rows, columns)
將其定義爲二維 kernel_size
的大小。對於一維數據,在覈函數中,rows
總是固定爲1。在解釋和預測模型之前,模型的輸出必須被展平。
model.add(ConvLSTM2D(filters=64, kernel_size=(1,2), activation='relu', input_shape=(n_seq,
1, n_steps, n_features)))
model.add(Flatten())
以上五個模型的完整代碼:
import numpy as np
from tensorflow.keras.models import Sequential, Model
from tensorflow.keras.layers import Dense, LSTM, Flatten, Bidirectional
from tensorflow.keras.layers import Conv1D, TimeDistributed, MaxPooling1D
from tensorflow.keras.layers import ConvLSTM2D
class UnivariateModels:
'''
單變量時間序列LSTM模型
'''
def __init__(self, sequence, test_seq, n_seq, n_steps, sw_width, features, epochs_num, verbose_set, flag):
self.sequence = sequence
self.test_seq = test_seq
self.sw_width = sw_width
self.features = features
self.epochs_num = epochs_num
self.verbose_set = verbose_set
self.flag = flag
self.X, self.y = [], []
self.n_seq = n_seq
self.n_steps = n_steps
def split_sequence(self):
for i in range(len(self.sequence)):
# 找到最後一個元素的索引
end_index = i + self.sw_width
# 如果最後一個滑動窗口中的最後一個元素的索引大於序列中最後一個元素的索引則丟棄該樣本
if end_index > len(self.sequence) - 1:
break
# 實現以滑動步長爲1(因爲是for循環),窗口寬度爲self.sw_width的滑動步長取值
seq_x, seq_y = self.sequence[i:end_index], self.sequence[end_index]
self.X.append(seq_x)
self.y.append(seq_y)
self.X, self.y = np.array(self.X), np.array(self.y)
for i in range(len(self.X)):
print(self.X[i], self.y[i])
if self.flag == 1:
self.X = self.X.reshape((self.X.shape[0], self.n_seq, self.n_steps, self.features))
elif self.flag == 2:
self.X = self.X.reshape((self.X.shape[0], self.n_seq, 1, self.n_steps, self.features))
else:
self.X = self.X.reshape((self.X.shape[0], self.X.shape[1], self.features))
print('X:\n{}\ny:\n{}\n'.format(self.X, self.y))
print('X.shape:{}, y.shape:{}\n'.format(self.X.shape, self.y.shape))
return self.X, self.y
def vanilla_lstm(self):
model = Sequential()
model.add(LSTM(50, activation='relu', input_shape=(self.sw_width, self.features)))
model.add(Dense(1))
model.compile(optimizer='adam', loss='mse', metrics=['accuracy'])
print(model.summary())
history = model.fit(self.X, self.y, epochs=self.epochs_num, verbose=self.verbose_set)
print('\ntrain_acc:%s'%np.mean(history.history['accuracy']), '\ntrain_loss:%s'%np.mean(history.history['loss']))
print('yhat:%s'%(model.predict(self.test_seq)),'\n-----------------------------')
# model = Sequential()
# model.add(LSTM(50, activation='relu', input_shape=(self.sw_width, self.features),
# # 其它參數配置
# recurrent_activation='sigmoid', use_bias=True, kernel_initializer='glorot_uniform',
# recurrent_initializer='orthogonal', bias_initializer='zeros', unit_forget_bias=True, kernel_regularizer=None,
# recurrent_regularizer=None, bias_regularizer=None, kernel_constraint=None, recurrent_constraint=None,
# bias_constraint=None, dropout=0.0, recurrent_dropout=0.0, implementation=2))
# model.add(Dense(units=1,
# # 其它參數配置
# activation=None, use_bias=True, kernel_initializer='glorot_uniform', bias_initializer='zeros',
# kernel_regularizer=None, bias_regularizer=None, activity_regularizer=None, kernel_constraint=None, bias_constraint=None))
# model.compile(optimizer='adam', loss='mse',
# # 其它參數配置
# metrics=None, loss_weights=None, sample_weight_mode=None, weighted_metrics=None, target_tensors=None)
# print(model.summary())
# history = model.fit(self.X, self.y, self.epochs_num, self.verbose_set,
# # 其它參數配置
# callbacks=None, validation_split=0.0, validation_data=None, shuffle=True, class_weight=None, sample_weight=None,
# initial_epoch=0, steps_per_epoch=None, validation_steps=None, validation_freq=1, max_queue_size=10, workers=1, use_multiprocessing=False)
# model.predict(self.test_seq, verbose=self.verbose_set,
# # 其它參數配置
# steps=None, callbacks=None, max_queue_size=10, workers=1, use_multiprocessing=False)
def stacked_lstm(self):
model = Sequential()
model.add(LSTM(50, activation='relu', return_sequences=True,
input_shape=(self.sw_width, self.features)))
model.add(LSTM(50, activation='relu'))
model.add(Dense(1))
model.compile(optimizer='adam', loss='mse', metrics=['accuracy'])
history = model.fit(self.X, self.y, epochs=self.epochs_num, verbose=self.verbose_set)
print('\ntrain_acc:%s'%np.mean(history.history['accuracy']), '\ntrain_loss:%s'%np.mean(history.history['loss']))
print('yhat:%s'%(model.predict(self.test_seq)),'\n-----------------------------')
def bidirectional_lstm(self):
model = Sequential()
model.add(Bidirectional(LSTM(50, activation='relu'),
input_shape=(self.sw_width, self.features)))
model.add(Dense(1))
model.compile(optimizer='adam', loss='mse', metrics=['accuracy'])
history = model.fit(self.X, self.y, epochs=self.epochs_num, verbose=self.verbose_set)
print('\ntrain_acc:%s'%np.mean(history.history['accuracy']), '\ntrain_loss:%s'%np.mean(history.history['loss']))
print('yhat:%s'%(model.predict(self.test_seq)),'\n-----------------------------')
def cnn_lstm(self):
model = Sequential()
model.add(TimeDistributed(Conv1D(filters=64, kernel_size=1, activation='relu'),
input_shape=(None, self.n_steps, self.features)))
model.add(TimeDistributed(MaxPooling1D(pool_size=2)))
model.add(TimeDistributed(Flatten()))
model.add(LSTM(50, activation='relu'))
model.add(Dense(1))
model.compile(optimizer='adam', loss='mse',metrics=['accuracy'])
history = model.fit(self.X, self.y, epochs=self.epochs_num, verbose=self.verbose_set)
print('\ntrain_acc:%s'%np.mean(history.history['accuracy']), '\ntrain_loss:%s'%np.mean(history.history['loss']))
print('yhat:%s'%(model.predict(self.test_seq)),'\n-----------------------------')
def conv_lstm(self):
model = Sequential()
model.add(ConvLSTM2D(filters=64, kernel_size=(1,2), activation='relu',
input_shape=(self.n_seq, 1, self.n_steps, self.features)))
model.add(Flatten())
model.add(Dense(1))
model.compile(optimizer='adam', loss='mse', metrics=['accuracy'])
history = model.fit(self.X, self.y, epochs=self.epochs_num, verbose=self.verbose_set)
print('\ntrain_acc:%s'%np.mean(history.history['accuracy']), '\ntrain_loss:%s'%np.mean(history.history['loss']))
print('yhat:%s'%(model.predict(self.test_seq)),'\n-----------------------------')
前三個模型:
if __name__ == '__main__':
single_seq = [10, 20, 30, 40, 50, 60, 70, 80, 90]
sw_width = 3
features = 1
n_seq = 2
n_steps = 2
epochs = 300
verbose = 0
test_seq = np.array([70, 80, 90])
test_seq = test_seq.reshape((1, sw_width, features))
UnivariateLSTM = UnivariateModels(single_seq, test_seq, n_seq, n_steps, sw_width, features, epochs, verbose, flag=0)
UnivariateLSTM.split_sequence()
UnivariateLSTM.vanilla_lstm()
UnivariateLSTM.stacked_lstm()
UnivariateLSTM.bidirectional_lstm()
輸出:
[10 20 30] 40
[20 30 40] 50
[30 40 50] 60
[40 50 60] 70
[50 60 70] 80
[60 70 80] 90
X:
[[[10]
[20]
[30]]
[[20]
[30]
[40]]
[[30]
[40]
[50]]
[[40]
[50]
[60]]
[[50]
[60]
[70]]
[[60]
[70]
[80]]]
y:
[40 50 60 70 80 90]
X.shape:(6, 3, 1), y.shape:(6,)
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
lstm (LSTM) (None, 50) 10400
_________________________________________________________________
dense (Dense) (None, 1) 51
=================================================================
Total params: 10,451
Trainable params: 10,451
Non-trainable params: 0
_________________________________________________________________
None
train_acc:0.0
train_loss:603.0445827874324
yhat:[[100.85666]]
-----------------------------
train_acc:0.0
train_loss:337.3305027484894
yhat:[[102.78299]]
-----------------------------
train_acc:0.0
train_loss:283.14844233221066
yhat:[[100.81099]]
-----------------------------
後兩個模型:
if __name__ == '__main__':
single_seq = [10, 20, 30, 40, 50, 60, 70, 80, 90]
sw_width = 4
features = 1
n_seq = 2
n_steps = 2
epochs = 500
verbose = 0
test_seq = np.array([60, 70, 80, 90])
test_seq = test_seq.reshape((1, n_seq, n_steps, features))
UnivariateLSTM = UnivariateModels(single_seq, test_seq, n_seq, n_steps, sw_width, features, epochs, verbose, flag=1)
UnivariateLSTM.split_sequence()
UnivariateLSTM.cnn_lstm()
test_seq = test_seq.reshape((1, n_seq, 1, n_steps, features))
UnivariateLSTM = UnivariateModels(single_seq, test_seq, n_seq, n_steps, sw_width, features, epochs, verbose, flag=2)
UnivariateLSTM.split_sequence()
UnivariateLSTM.conv_lstm()
輸出:
[10 20 30 40] 50
[20 30 40 50] 60
[30 40 50 60] 70
[40 50 60 70] 80
[50 60 70 80] 90
X:
[[[[10]
[20]]
[[30]
[40]]]
[[[20]
[30]]
[[40]
[50]]]
[[[30]
[40]]
[[50]
[60]]]
[[[40]
[50]]
[[60]
[70]]]
[[[50]
[60]]
[[70]
[80]]]]
y:
[50 60 70 80 90]
X.shape:(5, 2, 2, 1), y.shape:(5,)
train_acc:0.0
train_loss:96.9693284504041
yhat:[[100.737885]]
-----------------------------
[10 20 30 40] 50
[20 30 40 50] 60
[30 40 50 60] 70
[40 50 60 70] 80
[50 60 70 80] 90
X:
[[[[[10]
[20]]]
[[[30]
[40]]]]
[[[[20]
[30]]]
[[[40]
[50]]]]
[[[[30]
[40]]]
[[[50]
[60]]]]
[[[[40]
[50]]]
[[[60]
[70]]]]
[[[[50]
[60]]]
[[[70]
[80]]]]]
y:
[50 60 70 80 90]
X.shape:(5, 2, 1, 2, 1), y.shape:(5,)
train_acc:0.0
train_loss:236.70842473191
yhat:[[103.98932]]
-----------------------------
關於多變量的LSTM模型,將在下文介紹。
參考:
https://machinelearningmastery.com/how-to-develop-lstm-models-for-time-series-forecasting/