时间序列数据必须经过变换才能用来拟合有监督的学习模型。在这种形式下,数据可以立即用于拟合有监督的机器学习算法,甚至多层感知器神经网络。为了使数据适合卷积神经网络(CNN)或长短期记忆(LSTM)神经网络,还需要进一步的转换。即监督学习数据的二维结构必须转换为三维结构,这也是在做时间序列预测时很让人头疼的问题。本文介绍了如何将时间序列数据集转换为三维结构,以便适合CNN或LSTM模型。




1 时间序列到监督问题的转化

时间序列数据在训练有监督的学习模型(如LSTM神经网络)之前需要准备。例如,一元时间序列表示为观测向量:

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

监督学习算法要求将数据作为样本集合提供,其中每个样本具有输入分量X和输出分量y。
模型将学习如何从提供的示例中将输入映射到输出。
cnn与lstm时间序列论文_LSTM

时间序列必须转换成具有输入和输出分量的样本。例如,预测输入数据X和预测输出标签y。对于一个对一步预测感兴趣的单变量时间序列问题,可以使用先前时间点的观测值作为输入,当前时间点的观测值作为输出。例如,上述10步单变量序列可以表示为一个有监督学习问题,其输入为3个时间步,输出为1个时间步,如下所示:

X,  	     y
[1, 2, 3],  [4]
[2, 3, 4],  [5]
[3, 4, 5],  [6]
...

可以编写代码来执行此转换,下面的 split_sequence() 函数实现了转换,它将一个给定的单变量序列分割成多个样本,其中每个样本都有指定数量的时间步长,输出是单个时间步长。将数据转换为适合训练监督学习模型的形式后,用行和列表示。每一列表示模型的一个特征,每一行表示一个输入样本。这两个概念需要明确,在与LSTM时间序列预测问题相关的博客还是论文中经常提及:

  • 特征(feature):转换后数据的每一列,比如降水量预测中的温度、湿度、风速、气压等特征;
  • 样本(sample):转换后数据的每一行,包含输入数据和输出标签;

如下例,3个特征,4个样本,数据集的形状(shape)为 [4,3]:

x1, x2, x3, y
1,  2,  3,  4
2,  3,  4,  5
3,  4,  5,  6
4,  5,  6,  7
5,  6,  7,  8
6,  7,  8,  9
7,  8,  9,  10

使用代码实现:

import numpy as np

def split_sequence(sequence, sliding_window_width):
    X, y = [], []
    for i in range(len(sequence)):
        # 找到最后一次滑动所截取数据中最后一个元素的索引,
        # 如果这个索引超过原序列中元素的索引则不截取;
        end_element_index = i + sliding_window_width
        if end_element_index > len(sequence) - 1: # 序列中最后一个元素的索引
            break
        sequence_x, sequence_y = sequence[i:end_element_index], sequence[end_element_index] # 取最后一个元素作为预测值y
        X.append(sequence_x)
        y.append(sequence_y)
    
    #return X,y
    return np.array(X), np.array(y)

if __name__ == '__main__':
    seq_test = [1,2,3,4,5,6,7,8,9,10]
    sw_width = 3
    seq_test_x, seq_test_y = split_sequence(seq_test, sw_width)
    print(seq_test_x.shape,seq_test_y.shape)
    for i in zip(seq_test_x,seq_test_y):
        print(i)
    for i in range(len(seq_test_x)):
        print(seq_test_x[i], seq_test_y[i])

输出:

(7, 3) (7,)
(array([1, 2, 3]), 4)
(array([2, 3, 4]), 5)
(array([3, 4, 5]), 6)
(array([4, 5, 6]), 7)
(array([5, 6, 7]), 8)
(array([6, 7, 8]), 9)
(array([7, 8, 9]), 10)
[1 2 3] 4
[2 3 4] 5
[3 4 5] 6
[4 5 6] 7
[5 6 7] 8
[6 7 8] 9
[7 8 9] 10

这种形式的数据可以直接用于训练一个简单的神经网络,比如一个多层感知器。在为CNNs和LSTMs准备数据时要求数据具有三维结构,而不是目前描述的二维结构。


2. 为 CNN/LSTM 构建三维数据输入数据

CNN和LSTM模型的输入层由网络第一隐含层的输入形状参数指定。这也会使初学者感到困惑,因为我们可能会直观地认为模型中定义的第一层是输入层,而不是隐藏层。例如,下面是一个具有一个隐藏LSTM层和一个密集输出层的网络示例。我的TensorFlow版本为 2.1.0

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense
model = Sequential()
model.add(LSTM(32))
model.add(Dense(1))

在本例中,LSTM() 层必须指定输入数据的形状(shape)。每个CNN和LSTM层的输入必须是三维的:

  • 样本(samples):一个序列(依上例,转化后,每一行)就是一个样本。输入LSTM的一个批次数据(batch_size)由一个或多个样本组成。
  • 时间步长(time steps):一个样本包含多个时间步长,即滑动窗口的宽度(依上例,时间步长是3),这里注意与滑动窗口的滑动步长作区分。
  • 特征(features):包含多少个特征,比如降水量预测中的温度、湿度、风速、气压特征,那么指定 feature 就是 4

这种输入数据可用数组表示:[samples, time steps, features]。前一节中提到的数据集是二维的,数组形状是:[samples, features]。区别是添加了时间步长维度。在时间序列预测问题中,特征是在时间步长的观测值。所以实际上是在增加特征的维度,一个单变量时间序列只有一个特征。此处可能有人要犯迷糊了,之前也理解了比较久,转化成三维之后,其实二维上是形状为[samples, time steps]的二维数组,然后第三个维度是features,想象一下立方体,每增加一个特征(features)就相当于沿着Z轴叠加二维数组。

在定义LSTM网络的输入层时,网络假设输入有一个或多个示例,并要求指定时间步数和特征数,这两个特征组成元组传入LSTM模块中的 input_shape 参数。例如,下面的模型定义了一个输入层,它需要1个或多个样本、3个时间步和1个特征。网络中的第一层实际上是第一个隐藏层,因此在本例中,32 表示第一个隐藏层中的单元数。第一个隐藏层中的单元数与输入数据中的样本数、时间步长或特征完全无关。

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense
model = Sequential()
model.add(LSTM(32, input_shape=(3, 1)))
model.add(Dense(1))

上一节中,我们将1到10的序列分割成了一个形状为 [7,3] 的二维数组,但当输入到LSTM时,需要将数据转化为三维,因此我们可以使用 numpy 中的 reshape() 函数来实现。

seq_test_x_1 = seq_test_x.reshape((7, 3, 1))
seq_test_x_1

输出:

array([[[1],
        [2],
        [3]],

       [[2],
        [3],
        [4]],

       [[3],
        [4],
        [5]],

       [[4],
        [5],
        [6]],

       [[5],
        [6],
        [7]],

       [[6],
        [7],
        [8]],

       [[7],
        [8],
        [9]]])

这种方法需要手动设置形状,需要修改代码,不够灵活;我们可以调用数组的 seq_test_x.shape 属性返回的数组的样本数和步数来编写代码。例如,seq_test_x.shape[0] 表示2D数组中的行数(样本数),seq_test_x.shape[1] 表示2D数组中的列数(特征数):

cnn与lstm时间序列论文_LSTM_02


在这种情况下,我们将使用的特征数作为时间步数。因此,整形可以写成:

seq_test_x_2 = seq_test_x.reshape((seq_test_x.shape[0], seq_test_x.shape[1], 1))
seq_test_x_2

输出结果跟上边第一个方法是相同的。最后,每个样本的输入元素被重塑为适合于拟合LSTM或CNN的三维数组,形状为[7, 3, 1],即7个样本,3个时间步,1个特征。


3. 一个实例

假设有如下业务需求:
数据文件中有两列,行数为5000,第1列是时间(间隔为1小时),第2列是销售数量,尝试预测未来时间步的销售数量。应该如何为LSTM设置此数据中的样本数、时间步长和特征?

这里有两个问题需要明确:

  • 数据形状:LSTM需要3D输入,需要重塑为[samples, time steps, features]
  • 序列长度:LSTM的输入最好不要超过200-400个时间步的序列,因此需要将数据分割成子样本。

要实现业务需求,需要分以下四步执行:

  1. 加载数据;
  2. 丢弃时间列,因为序列的顺序已经可以保证时间先后关系;
  3. 划分样本;
  4. 重塑数据序列形状;

3.1 加载数据

这里我们模拟5000组数据。代码实现:

import numpy as np

simul_data = []
n = 5000
for i in range(n):
    simul_data.append([i+1, (i+1)*10]) # 第一列是时间列,第二列是特征数据列

simul_data = np.array(simul_data)
print(simul_data[:5, :]) # 取前5组数据
print(simul_data.shape)

输出:

[[ 1 10]
 [ 2 20]
 [ 3 30]
 [ 4 40]
 [ 5 50]]
(5000, 2)

3.2 丢弃时间列

为了方便演示,这里只是使用了切片取值的方法,在实际的应用过程中,很大一部分时间都是在处理数据,所以需要考虑空值问题,可以使用pandas相关方法来处理。这里为了便于演示就不处理了:

# 删除时间列
simul_data = simul_data[:, 1]
simul_data.shape

'''
输出:(5000,)
'''

3.3 将数据划分成样本

LSTM需要处理样本,其中每个样本是一个窗口宽度大小的序列。5000个时间步的数据太长;LSTM一般在200到400个时间步之内时表现比较好。因此,需要将5000个时间步分割成多个子序列。本例中,默认不重叠,滑动的步长就是窗口宽度,这样把5000个时间步分成25个子序列,每个子序列200个时间步(即滑动窗口宽度为200,滑动步长为200,这样划分的数据就不重叠了,但是在实际业务需求中一般是需要重叠的,关于有重叠划分数据会在以后的文章中介绍)。代码如下:

samples = []
sw_steps = 200 # 滑动窗口步长

for i in range(0, n, sw_steps):
    sample = simul_data[i:i+sw_steps] # 截取 i 到 i + 200 的数据
    samples.append(sample)
print(len(samples))

输出:

25 # 将5000个采样点数据,按照不重叠、200个采样点构成一个样本的方式,划分成25个样本

3.4 重塑数组

LSTM需要格式为 [samples,timesteps,features] 的数据。我们有25个样本,每个样本200个时间步,和1个特征。首先,需要将数组列表转换成一个二维NumPy数组,其形状为 [25,200]

simul_data = np.array(samples)
print(simul_data,simul_data.shape)

输出:

[[   10    20    30 ...  1980  1990  2000]
 [ 2010  2020  2030 ...  3980  3990  4000]
 [ 4010  4020  4030 ...  5980  5990  6000]
 ...
 [44010 44020 44030 ... 45980 45990 46000]
 [46010 46020 46030 ... 47980 47990 48000]
 [48010 48020 48030 ... 49980 49990 50000]] (25, 200)

接下来,我们可以使用 reshape() 函数添加一个特征维度,并将现有列用作时间步。

simul_data = simul_data.reshape((len(samples), sw_steps, 1))
print(simul_data.shape)

输出:

(25, 200, 1)

至此,已经把业务需求实现了。再来回顾以下处理数据的四个步骤:

  1. 加载数据:可以使用pandas;
  2. 丢弃时间列,因为序列的顺序已经可以保证时间先后关系:注意空值处理,可以使用pandas.isnull(df)检查;
  3. 划分样本:本文中不涉及重叠划分数据,会在以后的文章中介绍;还包括人类行为识别,发电量预测等;
  4. 重塑数据序列形状:使用numpy实现,转换成三维数组,其shape为:[samples,timesteps,features]

参考:
https://machinelearningmastery.com/prepare-univariate-time-series-data-long-short-term-memory-networks/