使用 LSTM 预测一个简单的合成时间序列。为啥这么糟糕?
Posted
技术标签:
【中文标题】使用 LSTM 预测一个简单的合成时间序列。为啥这么糟糕?【英文标题】:Using LSTM to predict a simple synthetic time series. Why is it that bad?使用 LSTM 预测一个简单的合成时间序列。为什么这么糟糕? 【发布时间】:2018-10-01 17:10:19 【问题描述】:我刚开始在 keras 中使用 LSTM,我发现学习时间序列行为的可能性非常有趣。 我在网上阅读了几篇教程和文章,其中大部分都展示了在预测时间序列方面的令人印象深刻的能力,所以我试了一下。我注意到的第一件事是,我发现的所有文章总是以非常不公平的方式使用验证数据。我预测时间序列的想法是,我使用训练数据来构建模型,并使用训练数据的最后 N 个元素来估计序列的未来行为.为此,模型必须使用自己的预测作为输入,才能在未来向前迈进。
相反,我看到人们做的是在未来的任何时候估计测试集的准确性,使用基本事实作为估计的输入。这是非常不公平的,因为它不会产生真正的预测!
我尝试在 Keras 中编写自己的 LSTM 预测(请在下面找到代码),我从一个相对简单的案例开始,即抛物线和正弦曲线的组合。不幸的是,结果非常不令人满意。下面是几个例子,通过改变网络的参数得到:
您有什么建议可以得到更好的结果吗?如果 LSTM 无法预测这样一个“简单”的信号,如何预测复杂的行为?
谢谢你, 亚历山德罗
import os
import numpy as np
from matplotlib import pyplot as plt
import keras
# Number of vectors to consider in the time window
look_back = 50
N_datapoints = 2000
train_split = 0.8
# Generate a time signal composed of a linear function and a sinusoid
t = np.linspace(0, 200, N_datapoints)
y = t**2 + np.sin(t*2)*1000
y -= y.mean()
y /= y.std()
plt.plot(y)
# Reshape the signal into fixed windows for training
def create_blocks(y, look_back=1):
x_data, y_data = [], []
for i in range(0, len(y)-look_back-1):
x_data.append(y[i:i+look_back])
y_data.append(y[i+look_back])
return np.array(x_data), np.array(y_data)
x_data, y_data = create_blocks(y, look_back)
# Split data in training and testing
N_train = int(x_data.shape[0]*train_split)
x_train = x_data[:N_train, :, None]
y_train = y_data[:N_train, ]
x_test = x_data[N_train:-1, :, None]
y_test = y_data[N_train:-1:, ]
# Get the time vector for train and test (just to plot)
t_train = t[0:N_train-1, None]
t_test = t[N_train:-1, None]
# Network
from keras import Model, Input
from keras.layers import LSTM, Dense, Activation, BatchNormalization, Dropout
inputs = Input(shape=(look_back, 1))
net = LSTM(32, return_sequences=False)(inputs)
net = Dense(32)(net)
net = Dropout(0.25)(net)
outputs = Dense(1)(net)
model = Model(inputs=inputs, outputs=outputs)
model.compile(optimizer=keras.optimizers.rmsprop(), loss='mean_squared_error')
model.summary()
# Callback
from keras.callbacks import Callback
class PlotResuls(Callback):
def on_train_begin(self, logs=None):
self.fig = plt.figure()
def save_data(self, x_test, y, look_back, t_test):
self.x_test = x_test
self.y = y
self.t_test = t_test
self.look_back = look_back
def on_epoch_end(self, epoch, logs=None):
if epoch % 20 == 0:
plt.clf()
y_pred = self.x_test[0, ...]
for i in range(len(x_test)+1):
new_prediction = model.predict(y_pred[None, -self.look_back:, ])
y_pred = np.concatenate((y_pred, new_prediction), axis=0)
plt.plot(t, y, label='GT')
plt.plot(self.t_test, y_pred, '.-', label='Predicted')
plt.legend()
plt.pause(0.01)
plt.savefig('lstm_%d.png' % epoch)
plot_results = PlotResuls()
plot_results.save_data(x_test, y, look_back, t_test)
model.fit(x_train, y_train, validation_data=(x_test, y_test), epochs=100000, batch_size=32, callbacks=[plot_results])
【问题讨论】:
【参考方案1】:正如 Primusa 的回答中已经显示的那样,允许循环层使用 return_sequences=True
输出其隐藏状态是有帮助的,而 Bidirectional
层已被证明可以更好地捕获时间模式。此外,我认为您需要对您尝试近似的函数类型有某种直觉——尝试将其分解为多个函数并为每个函数构建一个子网络通常会加快学习过程,尤其是在使用适当的激活组合时。应用权重正则化也是相关的,因为它可以阻止由于误差累积而导致的极端分歧。另请注意,除非您使用stateful=True
,否则您将需要为网络提供足够长的时间框架来检查远程模式(即,如果时间窗口很小,抛物线很容易近似为一条线)。
具体而言,以下更改实现了 (1.0223e-04/0.0015) 的 MSE(仍在快速下降) ) 在 20 个 epoch 之后和 (2.8111e-05/3.0393e-04) 在 100 个 epoch 之后,回溯为 100(请注意,我还将您的优化器更改为 Adam 我只是更喜欢):
from keras import Model, Input
from keras.layers import (LSTM, Dense, Activation, BatchNormalization,
Dropout, Bidirectional, Add)
inputs = Input(shape=(look_back, 1))
bd_seq = Bidirectional(LSTM(128, return_sequences=True,
kernel_regularizer='l2'),
merge_mode='sum')(inputs)
bd_sin = Bidirectional(LSTM(32, return_sequences=True,
kernel_regularizer='l2'),
merge_mode='sum') (bd_seq)
bd_1 = Bidirectional(LSTM(1, activation='linear'),
merge_mode='sum')(bd_seq)
bd_2 = Bidirectional(LSTM(1, activation='tanh'),
merge_mode='sum')(bd_sin)
output = Add()([bd_1, bd_2])
model = Model(inputs=inputs, outputs=output)
model.compile(optimizer='adam', loss='mean_squared_error')
【讨论】:
我也不太了解您使用的结构。我将两个序列组合在一起的天真方法是有两个分支,具有相同的输入,一个用于正弦曲线,一个用于抛物线,我将简单地将其添加到最后一层。在您的情况下,您有一个分支是 3 个 LSTM 的级联,另一个分支是只有 2 个 LSTM 的序列。灵感来自什么? 双向层的副作用是两倍的参数增长; 1)因为您实际上是在构建图层的第二个“向后”版本,并且 2)如果您(如您所提到的)不是按元素合并两个输出(即加法或乘法),那么您将大大增加图层的宽度,然后将其送入后续图层。至于为每一层选择的单元数量,我的直觉是我们希望在不同的时间范围内捕捉不同复杂性的模式,因此单一的表示是不够的。 玩弄层堆叠并可视化每个顶层的输出以查看每个人正在学习的模式会很有趣。我假设 tanh 层是学习正弦曲线的层,而线性层是学习抛物线的层。 一般来说,重要的是要记住,与宽度相比,通过深度获得的表征能力呈指数级增长,因此使用窄、深、特定于任务的层来增强网络通常很有帮助。跨度> 这是个好问题;首先,完全连接的网络被证明能够逼近 any 函数,因此直接的答案是否定的。然而,实际上,LSTM(作为一个循环网络)能够比完全连接的网络更容易捕捉更复杂、不断发展的过程。【参考方案2】:虽然神经网络非常复杂且功能强大,但它们并不是一个神奇的盒子。很多时候,您需要微调您的网络以获得更好的结果。
我调整了你的模型,得到了以下结果:
虽然它们绝不是非常准确的,但我想说它们比您在问题中发布的结果要好得多。这个神经网络对波的频率有清晰的感知,但在确定线的总体趋势方面需要做更多的工作。你可以看到当它接近曲线的最大值时,它的预测能力是如何恶化的。
我使用的模型是:
model = Sequential()
model.add(Bidirectional(LSTM(8, return_sequences=True),input_shape=(50, 1),))
model.add(LSTM(8, return_sequences=True))
model.add(LSTM(4, return_sequences=False))
model.add(Dense(1))
model.compile(optimizer='adam', loss='mse')
我将您的回顾期从 100 缩短到 50 以缩短培训时间。我以 5 的批大小对模型进行了 50 个 epoch 的训练:
c.fit(epochs=50, batch_size=5)
这在我的笔记本电脑上花了大约 15 分钟(在 CPU 而非 GPU 上训练)。
我用来提高其准确性的主要技巧是双向 LSTM,它涉及两个 LSTMS,一个具有前向馈送的序列,另一个使用反向馈送的序列。这样做的想法是使用未来的数据来理解曲线的上下文。
请注意,仅在训练期间使用未来数据。在实际预测期间,仅使用先前的数据来预测下一个点,并且我也使用了您的“滚动预测”想法,随后将预测用作输入。
【讨论】:
以上是关于使用 LSTM 预测一个简单的合成时间序列。为啥这么糟糕?的主要内容,如果未能解决你的问题,请参考以下文章