TensorFlow/[TensorFlow 学习笔记 ] 8 时间序列 2 - 神经网络方法

TensorFlow/[TensorFlow 学习笔记 ] 8 时间序列 2 - 神经网络方法

在上一篇文章中,我们使用了统计学中的移动平均方法来预测时间序列。

在这篇文章中,我们使用普通的神经网络和 RNN 来预测时间序列。

数据预处理

首先导入包:

1
2
3
4
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
print(tf.__version__)

接着,生成序列数据:

1
2
3
dataset = tf.data.Dataset.range(10)
for val in dataset:
print(val.numpy())

输出如下:

1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9

使用 window 方法把序列分为窗口。

1
2
3
4
5
6
dataset = tf.data.Dataset.range(10)
dataset = dataset.window(5, shift=1)
for window_dataset in dataset:
for val in window_dataset:
print(val.numpy(), end=" ")
print()

输出如下:

1
2
3
4
5
6
7
8
9
10
0 1 2 3 4 
1 2 3 4 5
2 3 4 5 6
3 4 5 6 7
4 5 6 7 8
5 6 7 8 9
6 7 8 9
7 8 9
8 9
9

但是分割的数据中,后面的数据长度小于窗口长度,我们需要把这部分数据删除,设置 drop_remainder=True

1
2
3
4
5
6
dataset = tf.data.Dataset.range(10)
dataset = dataset.window(5, shift=1, drop_remainder=True)
for window_dataset in dataset:
for val in window_dataset:
print(val.numpy(), end=" ")
print()

输出如下:

1
2
3
4
5
6
0 1 2 3 4 
1 2 3 4 5
2 3 4 5 6
3 4 5 6 7
4 5 6 7 8
5 6 7 8 9

下一步,我们需要把每个窗口的数据,转换为一个 list。

1
2
3
4
5
dataset = tf.data.Dataset.range(10)
dataset = dataset.window(5, shift=1, drop_remainder=True)
dataset = dataset.flat_map(lambda window: window.batch(5))
for window in dataset:
print(window.numpy())

输出如下:

1
2
3
4
5
6
[0 1 2 3 4]
[1 2 3 4 5]
[2 3 4 5 6]
[3 4 5 6 7]
[4 5 6 7 8]
[5 6 7 8 9]

然后,把每个窗口前面的数据作为 x,后面最后一个数据作为 y:

1
2
3
4
5
6
dataset = tf.data.Dataset.range(10)
dataset = dataset.window(5, shift=1, drop_remainder=True)
dataset = dataset.flat_map(lambda window: window.batch(5))
dataset = dataset.map(lambda window: (window[:-1], window[-1:]))
for x,y in dataset:
print(x.numpy(), y.numpy())

输出如下:

1
2
3
4
5
6
[0 1 2 3] [4]
[1 2 3 4] [5]
[2 3 4 5] [6]
[3 4 5 6] [7]
[4 5 6 7] [8]
[5 6 7 8] [9]

然后进行 shuffle

1
2
3
4
5
6
7
dataset = tf.data.Dataset.range(10)
dataset = dataset.window(5, shift=1, drop_remainder=True)
dataset = dataset.flat_map(lambda window: window.batch(5))
dataset = dataset.map(lambda window: (window[:-1], window[-1:]))
dataset = dataset.shuffle(buffer_size=10)
for x,y in dataset:
print(x.numpy(), y.numpy())

输出如下:

1
2
3
4
5
6
[3 4 5 6] [7]
[0 1 2 3] [4]
[5 6 7 8] [9]
[2 3 4 5] [6]
[4 5 6 7] [8]
[1 2 3 4] [5]

最后构建 batch。

1
2
3
4
5
6
7
8
9
10
dataset = tf.data.Dataset.range(10)
dataset = dataset.window(5, shift=1, drop_remainder=True)
dataset = dataset.flat_map(lambda window: window.batch(5))
dataset = dataset.map(lambda window: (window[:-1], window[-1:]))
dataset = dataset.shuffle(buffer_size=10)
# dataset = dataset.batch(2).prefetch(2)
dataset = dataset.batch(2).prefetch(2)
for x,y in dataset:
print("x = ", x.numpy())
print("y = ", y.numpy())

输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
x =  [[4 5 6 7]
[1 2 3 4]]
y = [[8]
[5]]
x = [[5 6 7 8]
[3 4 5 6]]
y = [[9]
[7]]
x = [[0 1 2 3]
[2 3 4 5]]
y = [[4]
[6]]

一层神经网络

下面定义各种帮助的函数,并生成数据,划分训练集和验证集:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def plot_series(time, series, format="-", start=0, end=None):
plt.plot(time[start:end], series[start:end], format)
plt.xlabel("Time")
plt.ylabel("Value")
plt.grid(True)

def trend(time, slope=0):
return slope * time

def seasonal_pattern(season_time):
"""Just an arbitrary pattern, you can change it if you wish"""
return np.where(season_time < 0.4,
np.cos(season_time * 2 * np.pi),
1 / np.exp(3 * season_time))

def seasonality(time, period, amplitude=1, phase=0):
"""Repeats the same pattern at each period"""
season_time = ((time + phase) % period) / period
return amplitude * seasonal_pattern(season_time)

def noise(time, noise_level=1, seed=None):
rnd = np.random.RandomState(seed)
return rnd.randn(len(time)) * noise_level

time = np.arange(4 * 365 + 1, dtype="float32")
baseline = 10
series = trend(time, 0.1)
baseline = 10
amplitude = 40
slope = 0.05
noise_level = 5

# Create the series
series = baseline + trend(time, slope) + seasonality(time, period=365, amplitude=amplitude)
# Update with noise
series += noise(time, noise_level, seed=42)

# 划分训练集和验证集
split_time = 1000
time_train = time[:split_time]
x_train = series[:split_time]
time_valid = time[split_time:]
x_valid = series[split_time:]

# 设置超参数
window_size = 20
batch_size = 32
shuffle_buffer_size = 1000

将上面数据预处理的代码封装为一个函数:

1
2
3
4
5
6
7
def windowed_dataset(series, window_size, batch_size, shuffle_buffer):
dataset = tf.data.Dataset.from_tensor_slices(series)
dataset = dataset.window(window_size + 1, shift=1, drop_remainder=True)
dataset = dataset.flat_map(lambda window: window.batch(window_size + 1))
dataset = dataset.shuffle(shuffle_buffer).map(lambda window: (window[:-1], window[-1]))
dataset = dataset.batch(batch_size).prefetch(1)
return dataset

下面进行数据预处理,并构建一个简单的只有一层的模型进行训练:

1
2
3
4
5
6
7
8
9
10
dataset = windowed_dataset(x_train, window_size, batch_size, shuffle_buffer_size)
print(dataset)
# 这里使用 l0 向量保存层,是为了下一步能够输出这层的权重
l0 = tf.keras.layers.Dense(1, input_shape=[window_size])
model = tf.keras.models.Sequential([l0])

model.compile(loss="mse", optimizer=tf.keras.optimizers.SGD(lr=1e-6, momentum=0.9))
model.fit(dataset,epochs=100,verbose=0)

print("Layer weights {}".format(l0.get_weights()))

输出如下:

第一个 array 表示系数,第二个 array 只有一个数,表示 bias。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Layer weights [array([[-0.07050379],
[ 0.01968649],
[ 0.03672196],
[ 0.08268202],
[-0.07870597],
[-0.01931396],
[-0.04015162],
[ 0.06981006],
[ 0.03004084],
[ 0.02451279],
[-0.08928804],
[ 0.00215667],
[ 0.02927431],
[ 0.01889405],
[-0.06870657],
[ 0.17018564],
[-0.03161251],
[ 0.19858538],
[ 0.2544437 ],
[ 0.44778714]], dtype=float32), array([0.01451531], dtype=float32)]

下面对验证集进行预测:

其中,series[time:time + window_size][np.newaxis] 是在前面增加一个 batch_size 维度,把形状为 [window_size] 的数据,变为 [1, window_size]

model.predict 的结果是一个2 维的 list,第一个维度是 batch_size,第二个维度是输出神经元的个数,这里形状是 [1, 1]

因此 np.array(forecast) 是一个 3 维数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
forecast = []

for time in range(len(series) - window_size):
forecast.append(model.predict(series[time:time + window_size][np.newaxis]))

# 由于 forecast 比 series 少 window_size 个元素,因此这里取 split_time-window_size。
forecast = forecast[split_time-window_size:]
# np.array(forecast) 的形状是:[length, 1,1]
results = np.array(forecast)[:, 0, 0]


plt.figure(figsize=(10, 6))

plot_series(time_valid, x_valid)
plot_series(time_valid, results)

图显示如下:


计算 MAE 如下:

1
tf.keras.metrics.mean_absolute_error(x_valid, results).numpy()
1
5.1287417

把网络结构改为 2 层

1
2
3
4
5
6
7
8
9
10
11
dataset = windowed_dataset(x_train, window_size, batch_size, shuffle_buffer_size)


model = tf.keras.models.Sequential([
tf.keras.layers.Dense(10, input_shape=[window_size], activation="relu"),
tf.keras.layers.Dense(10, activation="relu"),
tf.keras.layers.Dense(1)
])

model.compile(loss="mse", optimizer=tf.keras.optimizers.SGD(lr=1e-6, momentum=0.9))
model.fit(dataset,epochs=100,verbose=0)

计算得到的 MAE 是 5.004041。

寻找最佳学习率

接下来,使用 LearningRateScheduler 寻找最佳学习率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dataset = windowed_dataset(x_train, window_size, batch_size, shuffle_buffer_size)


model = tf.keras.models.Sequential([
tf.keras.layers.Dense(10, input_shape=[window_size], activation="relu"),
tf.keras.layers.Dense(10, activation="relu"),
tf.keras.layers.Dense(1)
])

lr_schedule = tf.keras.callbacks.LearningRateScheduler(
lambda epoch: 1e-8 * 10**(epoch / 20))
optimizer = tf.keras.optimizers.SGD(lr=1e-8, momentum=0.9)
model.compile(loss="mse", optimizer=optimizer)
history = model.fit(dataset, epochs=100, callbacks=[lr_schedule], verbose=0)

画出 loss 曲线变化图,X 轴是学习率,Y 轴是 loss。

1
2
3
lrs = 1e-8 * (10 ** (np.arange(100) / 20))
plt.semilogx(lrs, history.history["loss"])
plt.axis([1e-8, 1e-3, 0, 300])

loss 曲线变化图如下所示:


可以看出,学习率大概在 \(8 \times 10^{-6}\) 时,比较平稳且低,因此下面是用学习率 \(8 \times 10^{-6}\) 进行训练。

1
2
3
4
5
6
7
8
9
10
11
12
window_size = 30
dataset = windowed_dataset(x_train, window_size, batch_size, shuffle_buffer_size)

model = tf.keras.models.Sequential([
tf.keras.layers.Dense(10, activation="relu", input_shape=[window_size]),
tf.keras.layers.Dense(10, activation="relu"),
tf.keras.layers.Dense(1)
])

optimizer = tf.keras.optimizers.SGD(lr=8e-6, momentum=0.9)
model.compile(loss="mse", optimizer=optimizer)
history = model.fit(dataset, epochs=500, verbose=0)

画出loss 曲线变化图

1
2
3
4
loss = history.history['loss']
epochs = range(len(loss))
plt.plot(epochs, loss, 'b', label='Training Loss')
plt.show()

loss 曲线变化图如下所示:


第一眼看上去,loss 在前面几个 epoch 之后就达到了收敛,后面的训练几乎没有下降。其实这有可能是因为前面几个 epoch 的 loss 尺度太大,导致后面 epoch 的 loss 看起来微乎其微。

我们下面看看去掉前面 10 个 epoch,再画出 loss 曲线变化图。

1
2
3
4
5
6
# Plot all but the first 10
loss = history.history['loss']
epochs = range(10, len(loss))
plot_loss = loss[10:]
plt.plot(epochs, plot_loss, 'b', label='Training Loss')
plt.show()

loss 曲线变化图如下所示:


可以看出,loss 的确是在震荡。

最后对验证集进行预测。

1
2
3
4
5
6
7
8
9
10
11
12
forecast = []
for time in range(len(series) - window_size):
forecast.append(model.predict(series[time:time + window_size][np.newaxis]))

forecast = forecast[split_time-window_size:]
results = np.array(forecast)[:, 0, 0]


plt.figure(figsize=(10, 6))

plot_series(time_valid, x_valid)
plot_series(time_valid, results)


计算 MAE。

1
tf.keras.metrics.mean_absolute_error(x_valid, results).numpy()

得到结果为 5.2556496。

RNN

下面使用 RNN 来进行预测。

简介

首先来看下 RNN 的输入形状是 [batch_size, time_step, feature_dim]

假设输入的形状是 [4,30,1],表示 batch_size 为 4,时间序列长度为 30,特征维度是 1。那么在每个时间步的输入数据形状是 [batch_size, feature_dim](4,1),输出的数据形状是 [batch_size, units](4, 3),其中 units 表示这层 RNN 的神经元个数。所有数据拼接起来的输出形状是 [batch_size, time_step, units]

在普通的 RNN 中每个时间步输入的 state H 和上一个时间步的输出 Y 是一样的。


如果设置了 return_sequences=false(这是 keras RNN 的默认参数),那么只会返回最后一个时间步的输出,形状是 [batch_size, units],每个时间序列被转换为一个长度为 units 的向量表示。

如果你有两层 RNN ,那么最后一层之前的 RNN 都需要设置 return_sequences=true,因为所有时间步的输出,都需要输入到下一层 RNN。

如果我们使用 2 层 RNN。

那么第一层 RNN 需要设置 input_shape,你可以设置 [batch_size, time_step, feature_dim],或者设置 [time_step, feature_dim]

比如下面设置 input_shape=[None,1],不包括 batch_size,表示 batch_size 可以是任意的,而 time_step 为 None,表示 time_step 也是任意的,最后的 feature_dim 为 1。

1
2
3
4
5
model = tf.keras.models.Sequential([
tf.keras.layers.SimpleRNN(32, return_sequences=True, input_shape=[None,1]),
tf.keras.layers.SimpleRNN(20),
tf.keras.layers.Dense(1),
])

网络结构图如下所示:


如果我们把第 2 层 RNN 也设置 return_sequences=True

1
2
3
4
5
model = tf.keras.models.Sequential([
tf.keras.layers.SimpleRNN(32, return_sequences=True, input_shape=[None,1]),
tf.keras.layers.SimpleRNN(20, return_sequences=True),
tf.keras.layers.Dense(1),
])

那么结构如下所示:


同一个 Dense 层会应用到每个时间步,输出的数据长度和输入的数据长度是一样的,这种模型叫做 Sequence2Sequence Model

Lambda 层

keras 提供了 Lambda 层让我们实现任意的自定义层来扩展 keras 的能力。

这里,我们使用 Lambda 层来完成 2 个数据处理。

首先,在 windowed_dataset 函数中,我们返回的数据是 2 个维度的 [batch_size, time_step],而 RNN 的输入数据形状是 3 个维度 [batch_size, time_step, feature_dim]

因此我们在 RNN 层之前,使用 Lambda 层来扩展数据的维度,其中 input_shape=[None] 表示时间序列的长度 time_step 可以是任意的(而不是指 batch_size)。

其次,我们的数据范围长度是 [-100, 100] 之间,而 RNN 层使用 Tanh 激活函数,输出的数据尺度是 [-1,1],大约相差 100 倍。因此,我们在最后添加一个 Lmabda 层,将所有数据乘以 100,这种做法有助于收敛。

1
2
3
4
5
6
7
8
model = tf.keras.models.Sequential([
tf.keras.layers.Lambda(lambda x: tf.expand_dims(x, axis=-1),
input_shape=[None]),
tf.keras.layers.SimpleRNN(32, return_sequences=True, input_shape=[None,1]),
tf.keras.layers.SimpleRNN(20, return_sequences=True),
tf.keras.layers.Dense(1),
tf.keras.layers.Lambda(lambda x: x * 100.0)
])

实战

首先定义 RNN 模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
tf.keras.backend.clear_session()
tf.random.set_seed(51)
np.random.seed(51)

train_set = windowed_dataset(x_train, window_size, batch_size=128, shuffle_buffer=shuffle_buffer_size)

model = tf.keras.models.Sequential([
tf.keras.layers.Lambda(lambda x: tf.expand_dims(x, axis=-1),
input_shape=[None]),
tf.keras.layers.SimpleRNN(40, return_sequences=True),
tf.keras.layers.SimpleRNN(40),
tf.keras.layers.Dense(1),
tf.keras.layers.Lambda(lambda x: x * 100.0)
])

然后寻找最佳学习率

1
2
3
4
5
6
7
8
9
lr_schedule = tf.keras.callbacks.LearningRateScheduler(
lambda epoch: 1e-8 * 10**(epoch / 20))
optimizer = tf.keras.optimizers.SGD(lr=1e-8, momentum=0.9)
model.compile(loss=tf.keras.losses.Huber(),
optimizer=optimizer,
metrics=["mae"])
history = model.fit(train_set, epochs=100, callbacks=[lr_schedule])
plt.semilogx(history.history["lr"], history.history["loss"])
plt.axis([1e-8, 1e-4, 0, 30])

loss 随着学习率变化图如下:


可以看出,在 学习率为 \(10^{-5}\) 时,loss 比较小且平稳,因此下面是用这个学习率来训练模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
tf.keras.backend.clear_session()
tf.random.set_seed(51)
np.random.seed(51)

tf.keras.backend.clear_session()
dataset = windowed_dataset(x_train, window_size, batch_size, shuffle_buffer_size)

model = tf.keras.models.Sequential([
tf.keras.layers.Lambda(lambda x: tf.expand_dims(x, axis=-1),
input_shape=[None]),
tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(32, return_sequences=True)),
tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(32)),
tf.keras.layers.Dense(1),
tf.keras.layers.Lambda(lambda x: x * 100.0)
])


model.compile(loss="mse", optimizer=tf.keras.optimizers.SGD(lr=1e-5, momentum=0.9),metrics=["mae"])
history = model.fit(dataset,epochs=500,verbose=0)

对验证集进行预测并画图:

1
2
3
4
5
6
7
8
9
10
11
12
forecast = []
results = []
for time in range(len(series) - window_size):
forecast.append(model.predict(series[time:time + window_size][np.newaxis]))

forecast = forecast[split_time-window_size:]
results = np.array(forecast)[:, 0, 0]

plt.figure(figsize=(10, 6))

plot_series(time_valid, x_valid)
plot_series(time_valid, results)


计算 MAE

1
tf.keras.metrics.mean_absolute_error(x_valid, results).numpy()

MAE 为 8.514286。

可以看到,结果不如之前普通的神经网络,你可以尝试添加多层 RNN,并修改超参数来看看能不能提降低 MAE。

CNN

下面,使用 CNN 来构建模型。

首先定义模型预测的函数:

1
2
3
4
5
6
7
def model_forecast(model, series, window_size):
ds = tf.data.Dataset.from_tensor_slices(series)
ds = ds.window(window_size, shift=1, drop_remainder=True)
ds = ds.flat_map(lambda w: w.batch(window_size))
ds = ds.batch(32).prefetch(1)
forecast = model.predict(ds)
return forecast

其中 input_shape=[30, 1] 表示输入的每个时间序列的形状是 [30, 1],也可以设置为 [None, 1]

注意最后一个 LSTM 层的 return_sequences=True,意味着最后输出的数据形状和 input_shape 一样,也是 [30, 1]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
tf.keras.backend.clear_session()
tf.random.set_seed(51)
np.random.seed(51)

window_size = 30
train_set = windowed_dataset(x_train, window_size, batch_size=128, shuffle_buffer=shuffle_buffer_size)
# padding="causal" 表示保持卷积后的时间序列长度不变
model = tf.keras.models.Sequential([
tf.keras.layers.Conv1D(filters=32, kernel_size=5,
strides=1, padding="causal",
activation="relu",
input_shape=[30, 1]),
tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(32, return_sequences=True)),
tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(32, return_sequences=True)),
tf.keras.layers.Dense(1),
tf.keras.layers.Lambda(lambda x: x * 200)
])
model.summary()

模型结构如下,注意最后的输出的数据形状是 (None, 30, 1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv1d (Conv1D) (None, 30, 32) 192
_________________________________________________________________
bidirectional (Bidirectional (None, 30, 64) 16640
_________________________________________________________________
bidirectional_1 (Bidirection (None, 30, 64) 24832
_________________________________________________________________
dense (Dense) (None, 30, 1) 65
_________________________________________________________________
lambda (Lambda) (None, 30, 1) 0
=================================================================
Total params: 41,729
Trainable params: 41,729
Non-trainable params: 0
_________________________________________________________________

用不同的学习率训练模型,找出最佳学习率。

1
2
3
4
5
6
7
lr_schedule = tf.keras.callbacks.LearningRateScheduler(
lambda epoch: 1e-8 * 10**(epoch / 20))
optimizer = tf.keras.optimizers.SGD(lr=1e-8, momentum=0.9)
model.compile(loss=tf.keras.losses.Huber(),
optimizer=optimizer,
metrics=["mae"])
history = model.fit(train_set, epochs=100, callbacks=[lr_schedule])

画出 loss 随着学习率变化的曲线。

1
2
plt.semilogx(history.history["lr"], history.history["loss"])
plt.axis([1e-8, 1e-4, 0, 30])


最佳学习率是 \(10^{-5}\),下面使用这个学习率进行训练。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
tf.keras.backend.clear_session()
tf.random.set_seed(51)
np.random.seed(51)
#batch_size = 16
dataset = windowed_dataset(x_train, window_size, batch_size, shuffle_buffer_size)

model = tf.keras.models.Sequential([
tf.keras.layers.Conv1D(filters=32, kernel_size=3,
strides=1, padding="causal",
activation="relu",
input_shape=[None, 1]),
tf.keras.layers.LSTM(32, return_sequences=True),
tf.keras.layers.LSTM(32, return_sequences=True),
tf.keras.layers.Dense(1),
tf.keras.layers.Lambda(lambda x: x * 200)
])

optimizer = tf.keras.optimizers.SGD(lr=1e-5, momentum=0.9)
model.compile(loss=tf.keras.losses.Huber(),
optimizer=optimizer,
metrics=["mae"])
history = model.fit(dataset,epochs=500)

使用模型进行预测

1
2
3
4
5
# 添加一个 feature_dim 的维度
rnn_forecast = model_forecast(model, series[..., np.newaxis], window_size)
# 由于模型输出的数据形状是 (1432, 30, 1),对应 [size, time_step, feature_dim]
# 因此 time_step 取最后一个,feature_dim 取第一个
rnn_forecast = rnn_forecast[split_time - window_size:-1, -1, 0]

画出验证集的真实值和预测值

1
2
3
plt.figure(figsize=(10, 6))
plot_series(time_valid, x_valid)
plot_series(time_valid, rnn_forecast)


计算 MAE:

1
tf.keras.metrics.mean_absolute_error(x_valid, rnn_forecast).numpy()

得到 MAE 是 4.963412。

真实案例

下面使用我们所学习的方法来完成 kaggle 上的太阳黑子数量预测,地址:https://www.kaggle.com/robervalt/sunspots

课程提供了数据集的下载地址:https://storage.googleapis.com/laurencemoroney-blog.appspot.com/Sunspots.csv

首先导入库:

1
2
import tensorflow as tf
print(tf.__version__)

下载数据:

1
!wget --no-check-certificate https://storage.googleapis.com/laurencemoroney-blog.appspot.com/Sunspots.csv -O sunspots.csv

读取数据并进行可视化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import csv
time_step = []
sunspots = []

with open('sunspots.csv') as csvfile:
reader = csv.reader(csvfile, delimiter=',')
next(reader)
for row in reader:
sunspots.append(float(row[2]))
time_step.append(int(row[0]))

series = np.array(sunspots)
time = np.array(time_step)
plt.figure(figsize=(10, 6))
plot_series(time, series)

如下所示:


划分训练集和验证集:

1
2
3
4
5
6
7
8
9
split_time = 3000
time_train = time[:split_time]
x_train = series[:split_time]
time_valid = time[split_time:]
x_valid = series[split_time:]

window_size = 30
batch_size = 32
shuffle_buffer_size = 1000

定义窗口划分函数和预测的工具函数。

这里把 添加一个 feature_dim 的维度 的功能放到了 windowed_dataset 函数里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def windowed_dataset(series, window_size, batch_size, shuffle_buffer):
# 这里首先添加一个 feature_dim 的维度
series = tf.expand_dims(series, axis=-1)
ds = tf.data.Dataset.from_tensor_slices(series)
ds = ds.window(window_size + 1, shift=1, drop_remainder=True)
ds = ds.flat_map(lambda w: w.batch(window_size + 1))
ds = ds.shuffle(shuffle_buffer)
ds = ds.map(lambda w: (w[:-1], w[1:]))
return ds.batch(batch_size).prefetch(1)

def model_forecast(model, series, window_size):
ds = tf.data.Dataset.from_tensor_slices(series)
ds = ds.window(window_size, shift=1, drop_remainder=True)
ds = ds.flat_map(lambda w: w.batch(window_size))
ds = ds.batch(32).prefetch(1)
forecast = model.predict(ds)
return forecast

定义模型并调整不同的学习率进行训练,这里注意 Lambda 层放大了 400 倍,因为原始数据尺度是 0 到 400。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
tf.keras.backend.clear_session()
tf.random.set_seed(51)
np.random.seed(51)
window_size = 64
batch_size = 256
train_set = windowed_dataset(x_train, window_size, batch_size, shuffle_buffer_size)
print(train_set)
print(x_train.shape)

model = tf.keras.models.Sequential([
tf.keras.layers.Conv1D(filters=32, kernel_size=5,
strides=1, padding="causal",
activation="relu",
input_shape=[None, 1]),
tf.keras.layers.LSTM(64, return_sequences=True),
tf.keras.layers.LSTM(64, return_sequences=True),
tf.keras.layers.Dense(30, activation="relu"),
tf.keras.layers.Dense(10, activation="relu"),
tf.keras.layers.Dense(1),
# 这里注意 Lambda 层放大了 400 倍,因为原始数据尺度是 0 到 400
tf.keras.layers.Lambda(lambda x: x * 400)
])

lr_schedule = tf.keras.callbacks.LearningRateScheduler(
lambda epoch: 1e-8 * 10**(epoch / 20))
optimizer = tf.keras.optimizers.SGD(lr=1e-8, momentum=0.9)
model.compile(loss=tf.keras.losses.Huber(),
optimizer=optimizer,
metrics=["mae"])
history = model.fit(train_set, epochs=100, callbacks=[lr_schedule])

画出 loss 随着学习率变化的曲线图。


最佳学习率是 \(10^{-5}\)

下面使用这个学习率进行训练:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
tf.keras.backend.clear_session()
tf.random.set_seed(51)
np.random.seed(51)
train_set = windowed_dataset(x_train, window_size=60, batch_size=100, shuffle_buffer=shuffle_buffer_size)
model = tf.keras.models.Sequential([
tf.keras.layers.Conv1D(filters=60, kernel_size=5,
strides=1, padding="causal",
activation="relu",
input_shape=[None, 1]),
tf.keras.layers.LSTM(60, return_sequences=True),
tf.keras.layers.LSTM(60, return_sequences=True),
tf.keras.layers.Dense(30, activation="relu"),
tf.keras.layers.Dense(10, activation="relu"),
tf.keras.layers.Dense(1),
tf.keras.layers.Lambda(lambda x: x * 400)
])


optimizer = tf.keras.optimizers.SGD(lr=1e-5, momentum=0.9)
model.compile(loss=tf.keras.losses.Huber(),
optimizer=optimizer,
metrics=["mae"])
history = model.fit(train_set,epochs=500)

进行预测:

1
2
rnn_forecast = model_forecast(model, series[..., np.newaxis], window_size)
rnn_forecast = rnn_forecast[split_time - window_size:-1, -1, 0]

画出验证集的真实值和预测值:

1
2
3
plt.figure(figsize=(10, 6))
plot_series(time_valid, x_valid)
plot_series(time_valid, rnn_forecast)


计算 MAE

1
tf.keras.metrics.mean_absolute_error(x_valid, rnn_forecast).numpy()

得到 MAE 是 15.40074。

评论