如何使用Keras解释RNN的输出?

3
我想使用循环神经网络(RNN)进行时间序列预测,使用96个向前的步骤来预测未来96个步骤。为此,我有以下代码:
#Import modules
import pandas as pd
import numpy as np
import tensorflow as tf
from sklearn.preprocessing import StandardScaler
from tensorflow import keras

# Define the parameters of the RNN and the training
epochs = 1
batch_size = 50
steps_backwards = 96
steps_forward = 96
split_fraction_trainingData = 0.70
split_fraction_validatinData = 0.90
randomSeedNumber = 50
helpValueStrides =  int(steps_backwards /steps_forward)

#Read dataset
df = pd.read_csv('C:/Users1/Desktop/TestValues.csv', sep=';', header=0, low_memory=False, infer_datetime_format=True, parse_dates={'datetime':[0]}, index_col=['datetime'])

# standardize data

data = df.values
indexWithYLabelsInData = 0
data_X = data[:, 0:3]
data_Y = data[:, indexWithYLabelsInData].reshape(-1, 1)


scaler_standardized_X = StandardScaler()
data_X = scaler_standardized_X.fit_transform(data_X)
data_X = pd.DataFrame(data_X)
scaler_standardized_Y = StandardScaler()
data_Y = scaler_standardized_Y.fit_transform(data_Y)
data_Y = pd.DataFrame(data_Y)


# Prepare the input data for the RNN

series_reshaped_X =  np.array([data_X[i:i + (steps_backwards+steps_forward)].copy() for i in range(len(data) - (steps_backwards+steps_forward))])
series_reshaped_Y =  np.array([data_Y[i:i + (steps_backwards+steps_forward)].copy() for i in range(len(data) - (steps_backwards+steps_forward))])


timeslot_x_train_end = int(len(series_reshaped_X)* split_fraction_trainingData)
timeslot_x_valid_end = int(len(series_reshaped_X)* split_fraction_validatinData)

X_train = series_reshaped_X[:timeslot_x_train_end, :steps_backwards] 
X_valid = series_reshaped_X[timeslot_x_train_end:timeslot_x_valid_end, :steps_backwards] 
X_test = series_reshaped_X[timeslot_x_valid_end:, :steps_backwards] 

   
Y_train = series_reshaped_Y[:timeslot_x_train_end, steps_backwards:] 
Y_valid = series_reshaped_Y[timeslot_x_train_end:timeslot_x_valid_end, steps_backwards:] 
Y_test = series_reshaped_Y[timeslot_x_valid_end:, steps_backwards:]                                
   
   
# Build the model and train it

np.random.seed(randomSeedNumber)
tf.random.set_seed(randomSeedNumber)

model = keras.models.Sequential([
keras.layers.SimpleRNN(10, return_sequences=True, input_shape=[None, 3]),
keras.layers.SimpleRNN(10, return_sequences=True),
keras.layers.Conv1D(16, helpValueStrides, strides=helpValueStrides), 
keras.layers.TimeDistributed(keras.layers.Dense(1))
])

model.compile(loss="mean_squared_error", optimizer="adam", metrics=['mean_absolute_percentage_error'])
history = model.fit(X_train, Y_train, epochs=epochs, batch_size=batch_size, validation_data=(X_valid, Y_valid))

#Predict the test data
Y_pred = model.predict(X_test)

prediction_lastValues_list=[]

for i in range (0, len(Y_pred)):
  prediction_lastValues_list.append((Y_pred[i][0][1 - 1]))

# Create thw dataframe for the whole data
wholeDataFrameWithPrediciton = pd.DataFrame((X_test[:,1]))
wholeDataFrameWithPrediciton.rename(columns = {indexWithYLabelsInData:'actual'}, inplace = True)
wholeDataFrameWithPrediciton.rename(columns = {1:'Feature 1'}, inplace = True)
wholeDataFrameWithPrediciton.rename(columns = {2:'Feature 2'}, inplace = True)
wholeDataFrameWithPrediciton['predictions'] = prediction_lastValues_list
wholeDataFrameWithPrediciton['difference'] = (wholeDataFrameWithPrediciton['predictions'] - wholeDataFrameWithPrediciton['actual']).abs()
wholeDataFrameWithPrediciton['difference_percentage'] = ((wholeDataFrameWithPrediciton['difference'])/(wholeDataFrameWithPrediciton['actual']))*100


# Inverse the scaling (traInv: transformation inversed)

data_X_traInv = scaler_standardized_X.inverse_transform(data_X)
data_Y_traInv = scaler_standardized_Y.inverse_transform(data_Y)
series_reshaped_X_notTransformed =  np.array([data_X_traInv[i:i + (steps_backwards+steps_forward)].copy() for i in range(len(data) - (steps_backwards+steps_forward))])
X_test_notTranformed = series_reshaped_X_notTransformed[timeslot_x_valid_end:, :steps_backwards] 
predictions_traInv = scaler_standardized_Y.inverse_transform(wholeDataFrameWithPrediciton['predictions'].values.reshape(-1, 1))

edictions_traInv = wholeDataFrameWithPrediciton['predictions'].values.reshape(-1, 1)

# Create thw dataframe for the inversed transformed data
wholeDataFrameWithPrediciton_traInv = pd.DataFrame((X_test_notTranformed[:,0]))
wholeDataFrameWithPrediciton_traInv.rename(columns = {indexWithYLabelsInData:'actual'}, inplace = True)
wholeDataFrameWithPrediciton_traInv.rename(columns = {1:'Feature 1'}, inplace = True)
wholeDataFrameWithPrediciton_traInv['predictions'] = predictions_traInv
wholeDataFrameWithPrediciton_traInv['difference_absolute'] = (wholeDataFrameWithPrediciton_traInv['predictions'] - wholeDataFrameWithPrediciton_traInv['actual']).abs()
wholeDataFrameWithPrediciton_traInv['difference_percentage'] = ((wholeDataFrameWithPrediciton_traInv['difference_absolute'])/(wholeDataFrameWithPrediciton_traInv['actual']))*100
wholeDataFrameWithPrediciton_traInv['difference'] = (wholeDataFrameWithPrediciton_traInv['predictions'] - wholeDataFrameWithPrediciton_traInv['actual'])

在这里,您可以获得一些测试数据(不用关心实际值,因为我是编造的,只要形状重要)下载测试数据
如何解释Y_pred数据的输出?哪些值能够预测我未来96步的数值?我已经附上了“Y_pred”数据的屏幕截图。一次使用5个输出神经元的最后一层,另一次仅使用1个输出神经元。有人能告诉我如何解释“Y_pred”数据,即RNN到底在预测什么吗?我可以使用RNN模型输出(最后一层)中的任何值。“Y_pred”数据始终具有形状(X_test的批大小,时间序列,输出神经元的数量)。我的问题针对的是最后一个维度。我认为这些可能是特征,但在我的情况下并不是这样,因为我只有1个输出特征(您可以在Y_trainY_testY_valid数据的形状中看到这一点)。

enter image description here

提醒:赏金即将到期,不幸的是我仍然没有收到任何答案。所以我想提醒您关于这个问题和赏金。我会非常感激每一个评论。

你能展示一下你的模型以及训练方法吗? - The Guy with The Hat
@TheGuywithTheHat:感谢你的评论,戴帽子的人先生。 你所说的“模型”和“如何训练它”是什么意思? 我已经发布了我的代码。 这是整个代码和几乎最小可重现的示例。 我还包括了测试数据。 因此,我认为,该模型在发布的代码中定义,从行model = keras.models.Sequential([...开始,并且训练命令在行model.compile(loss="mean_squared_error", optimizer="adam", metrics=['mean_absolute_percentage_error'])history = model.fit(X_train, Y_train, ...中定义。 - PeterBe
我的错,我不知怎么错过了那段代码块上的滚动条。 - The Guy with The Hat
@TheGuywithTheHat:没问题。你能看一下我的代码,然后告诉我一些关于我的问题的东西吗?我会非常感激每一个评论。 - PeterBe
我可能稍后能够更仔细地查看,但是在创建X_trainX_validX_test时,您应该使用:-steps_backwards而不是:steps_backwards,这种情况有可能发生吗? - The Guy with The Hat
@TheGuywithTheHat:感谢你的评论,很抱歉我回复晚了(周末我忙于其他事情)。我尝试了你建议的方法,结果完全一样。所以如果我使用 :-steps_backwards 而不是 :steps_backwards 没有任何区别。无论如何,更重要的问题(也是我提出这个问题并将奖励给你的原因)是RNN到底在预测什么?我如何解释RNN的输出结果?你能否在这个话题上给我更多的见解? - PeterBe
2个回答

2

逐步详细了解模型的输入/输出可能很有用。

当使用带return_sequences=Truekeras.layers.SimpleRNN层时,输出将返回一个3-D张量,其中0轴是批量大小,1轴是时间步长,2轴是隐藏单元数(在您的模型中都为10,因为有两个SimpleRNN层)。

Conv1D层将产生一个输出张量,其中最后一个维度成为隐藏单元数(在您的模型中为16),因为它只是与输入进行卷积。

keras.layers.TimeDistributed层提供的层(在提供的示例中为Dense(1))将独立地应用于批次中的每个时间步。因此,在96个时间步中,我们对于批次中的每条记录都有96个输出。

因此,逐步查看您的模型:

model = keras.models.Sequential([
    keras.layers.SimpleRNN(10, return_sequences=True, input_shape=[None, 3]), # output size is (BATCH_SIZE, NUMBER_OF_TIMESTEPS, 10)
    keras.layers.SimpleRNN(10, return_sequences=True), # output size is (BATCH_SIZE, NUMBER_OF_TIMESTEPS, 10)
    keras.layers.Conv1D(16, helpValueStrides, strides=helpValueStrides) # output size is (BATCH_SIZE, NUMBER_OF_TIMESTEPS, 16),
    keras.layers.TimeDistributed(keras.layers.Dense(1)) # output size is (BATCH_SIZE, NUMBER_OF_TIMESTEPS, 1)
])

回答您的问题,模型的输出张量包含每个样本未来96步的预测值。如果更容易理解的话,在只有一个输出的情况下,您可以对model.predict的结果应用np.squeeze,这将使输出变为2-D:
Y_pred = model.predict(X_test) # output size is (BATCH_SIZE, NUMBER_OF_TIMESTEPS, 1)
Y_pred_squeezed = np.squeeze(Y_pred) # output size is (BATCH_SIZE, NUMBER_OF_TIMESTEPS)

这样,您就会得到一个矩形矩阵,其中每一行对应批次中的一个样本,每一列i对应于时间步i的预测。

在预测步骤之后的循环中,除了第一个时间步的预测之外,所有时间步的预测都被丢弃:

for i in range(0, len(Y_pred)):
    prediction_lastValues_list.append((Y_pred[i][0][1 - 1]))

这意味着最终结果只是每个批次中每个样本的第一个时间步长的预测列表。如果您想要第96个时间步长的预测,可以执行以下操作:

for i in range(0, len(Y_pred)):
    prediction_lastValues_list.append((Y_pred[i][-1][1 - 1]))

请注意第二个方括号中的 -1 而不是 0 ,以确保我们获取最后一个预测时间步而不是第一个。
顺便说一句,在复制结果时,我必须对您的代码进行一些更改,具体来说是在创建 series_reshaped_X series_reshaped_Y 时。使用 np.array 从列表创建数组时,我遇到了异常: ValueError:无法将大小为192的序列复制到具有3维度的数组轴,但是看着您正在做什么(沿着新轴连接张量),我将其更改为 np.stack ,它将完成相同的目标( https://numpy.org/doc/stable/reference/generated/numpy.stack.html ):
series_reshaped_X = np.stack([data_X[i:i + (steps_backwards + steps_forward)].copy() for i in
                              range(len(data) - (steps_backwards + steps_forward))])
series_reshaped_Y = np.stack([data_Y[i:i + (steps_backwards + steps_forward)].copy() for i in
                              range(len(data) - (steps_backwards + steps_forward))])

更新

"当我只有一个目标特征时,这5个值代表什么?"

实际上,这是TensorFlow API(也是NumPy的一个特性)的广播特性。如果您对形状不同的两个张量执行算术运算,它将尝试使它们兼容。在这种情况下,如果您将输出层大小更改为 "5" 而不是 "1" (keras.layers.Dense(5)),则输出大小为(BATCH_SIZE, NUMBER_OF_TIMESTEPS, 5)而不是(BATCH_SIZE, NUMBER_OF_TIMESTEPS, 1),这意味着来自卷积层的输出进入了5个神经元而不是1个。当计算两者之间的损失(均方误差)时,标签张量的大小(BATCH_SIZE, NUMBER_OF_TIMESTEPS, 1)被广播到预测张量的大小(BATCH_SIZE, NUMBER_OF_TIMESTEPS, 5)。在本例中,广播是通过复制列来完成的。例如,如果在第一时间步的第一行中,Y_train[-1.69862224],并且在第一时间步的第一行中,Y_pred[-0.6132075 , -0.6621697 , -0.7712653 , -0.60011995, -0.48753992],则在执行减法操作时,Y_train中的条目将转换为[-1.69862224, -1.69862224, -1.69862224, -1.69862224, -1.69862224]

这5个值中哪一个是96个时间步预测的“正确”值?

以这种方式训练时没有真正的“正确”值-如上所述,这只是API的一项功能。所有输出应该收敛于时间步的单个目标值,它们都与该值进行比较,因此您可以在技术上训练这种方式,但这只会向模型添加参数和复杂性(并且您必须选择一个作为“真正”的预测)。获取96个时间步的预测的正确方法在原始答案中详细说明,但是仅重申,模型的输出包含每个批次中每个样本的未来时间步预测。可以迭代输出张量以检索每个时间步的每个样本的预测。此外,请确保最终密集层中的神经元数与您要预测的目标值的数量相匹配,否则您将遇到广播问题(并且“正确”的输出将不清楚)。

仅为详尽起见(我不建议这样做),如果您真的想在只有一个目标值的情况下并入几个输出神经元,则可以像平均结果一样做:

for i in range(0, len(Y_pred)):
    prediction_lastValues_list.append(np.mean(Y_pred[i][0]))

但是这种方法没有任何好处,所以我建议你只需坚持之前的建议。

更新2

我的模型只预测了96个时间步骤后的一个时间段吗?还是它也预测了其中的所有内容? 该模型预测了其中的所有内容。因此,在时间步骤t处的样本中,模型的输出是预测 [t + 1, t + 2, ..., t + NUMBER_OF_TIMESTEPS]。根据我的原始答案,"您的模型的输出张量包含每个样本未来96个时间步骤的预测值"。在评估代码中指定这一点,您可以执行以下操作:

Y_pred = np.squeeze(Y_pred)
predictions_for_all_samples_and_timesteps = Y_pred.tolist()

这导致一个长度为BATCH_SIZE的列表,其中列表中的每个元素的长度为NUMBER_OF_TIMESTEPS(为了更清晰,predictions_for_all_samples_and_timesteps是一个嵌套列表)。predictions_for_all_samples_and_timesteps中索引为i的元素包含了X_test中第i个样本(行)在1-96个时间步长上的预测结果。
顺便说一下,您可以省略np.squeeze,但这样你会得到一个嵌套了多层列表的列表,其中内部列表中的每个元素都是一个单独的列表(输出将类似于[[[1], [2], [3], ...], ]而不是[[1, 2, 3, ...], ])。

更新3

Y_testY_pred都是大小为(BATCH_SIZE, NUMBER_OF_TIMESTEPS, 1)的三维numpy数组。要比较它们,你可以取两者之间的绝对值(或平方差):
abs_diff = np.abs(Y_pred - Y_test)

这将导致一个相同维度的数组(BATCH_SIZE, NUMBER_OF_TIMESTEPS)。您可以遍历行并生成每行时间步误差的图表。
for diff in abs_diff:
    print(diff.shape)
    plt.plot(list(range(diff)), diff)

在此输入图片描述 如果批处理大小很大(如图所示),可能会变得有些笨重,因此您可以绘制行的子集。如果您愿意绘制误差百分比,也可以将绝对差异转换为错误百分比:

percentage_diff = abs_diff / Y_test

就像我在Pandas中看到你最初做的那样,这将是与实际值的绝对差异。这个numpy数组将具有相同的维度,因此您可以遍历它并以相同的方式生成图形。

对于未来的问题,请不要发布评论,而是开一个新问题并提供链接——我很乐意继续帮助,但我想继续从中获得声誉。


感谢您的出色更新和巨大努力。我已经点赞并接受了您的答案,并将悬赏奖励授予了您。然而,我仍然有一些后续问题(但如果您不想再回答它们,我可以理解)。1)我不明白 Y_pred = np.squeeze(Y_pred) 实际上是做什么?现在,Y_pred 是否包含每个时间段的所有预测(如果不进行 squeeze,则不包含)? - PeterBe
  1. 我想将预测值与实际值进行比较。为此,我在代码中使用数据框的两列 wholeDataFrameWithPrediciton_traInv.rename(columns = {indexWithYLabelsInData:'actual'}, inplace = True)wholeDataFrameWithPrediciton_traInv['predictions'] = predictions_traInv。然后,我计算例如两列之间的RMSE误差。这是否也考虑了所有中间的预测值,还是只考虑当前时间槽96步之外的预测时间槽?
- PeterBe
实际上我想要的是对BATCH_SIZE中的所有项目进行预测评估。因此,BATCH_SIZE中的每个项目应该有NUMBER_OF_TIMESTEPS的预测结果,并且这些预测结果应该与实际值进行比较。然后应该计算整个预测的RMSE或百分比偏差。 - PeterBe
感谢您的回答和努力,我非常感激。您说得对,我想比较“输出预测张量”和Y_test。问题是我该如何做呢?基本上,我也想在图表中打印结果。目前,我正在使用matplotlib进行此操作,但仅用于比较两个时间序列wholeDataFrameWithPrediciton_traInv['predictions']wholeDataFrameWithPrediciton_traInv ['actual']。实际上,应该有BATCH_SIZE数量的图。 - PeterBe
感谢您的出色回答,danielcahall。对我上一个评论有什么看法吗?我非常感激您的进一步评论。 - PeterBe
显示剩余13条评论

2
我不同意@danielcahall的一个观点:
输出张量从您的模型中包含每个样本96个未来步骤的预测值。
输出确实包含96个时间步长,每个输入时间步长对应一个输出。但是,这并不适合您尝试做的事情。主要原因是您使用的RNN是单向的。
x   x   x   x   x   x    # input
|   |   |   |   |   | 
x-->x-->x-->x-->x-->x    # SimpleRNN
|   |   |   |   |   | 
x-->x-->x-->x-->x-->x    # SimpleRNN
|  /|\ /|\ /|\ /|\  | 
| / | \ | \ | \ | \ |
x   x   x   x   x   x    # Conv
|   |   |   |   |   | 
x   x   x   x   x   x    # Dense -> output

第一次输出的时间索引只能看到前两个输入时间(感谢卷积),它无法看到之后的时间。第一个预测仅基于旧数据。只有最后几个输出可以看到所有输入。
“使用96个向后步骤来预测未来96个步骤。”
大多数输出都无法看到所有数据。
如果您想从每个输入时间预测1个步骤,那么这个模型是合适的。
要预测96个步骤的未来,放弃“return_sequences=True”和“Conv”层会更加合理。然后扩展密集层以进行预测:
model = keras.models.Sequential([
    keras.layers.SimpleRNN(10, return_sequences=True, input_shape=[None, 3]), # output size is (BATCH_SIZE, NUMBER_OF_TIMESTEPS, 10)
    keras.layers.SimpleRNN(10), # output size is (BATCH_SIZE, 10)
    keras.layers.Dense(96) # output size is (BATCH_SIZE, 96)
])

这样所有的96个预测都会看到所有的96个输入。
更多细节请参见https://www.tensorflow.org/tutorials/structured_data/time_series
此外,SimpleRNN 很糟糕。不要在超过几个步骤时使用它。

非常感谢mdaoust的答案。我有一些问题/评论。1) 您写道:“第一个预测仅基于旧数据。只有最后几个输出可以看到所有输入。”-->实际上,所有预测都基于旧数据。我想使用过去96个时间步来预测未来96个时间步。因此,所有预测只看到旧数据是完全有意的。2)为什么我应该在第二层中摆脱return_sequence = true,而不是在第一层中?在机器学习书籍中,提到了return_sequence =true可以带来更好的训练收敛性。 - PeterBe
我的最后一条评论或者意见有什么回应吗? - PeterBe
嗨,mdaoust。我对你提出的方法有进一步的评论:当我像你建议的那样删除卷积层并尝试通过设置“steps_backwards = 192”来改变用于预测的过去数据时,我会收到错误消息“ValueError: Dimensions must be equal, but are 192 and 96 for '{{node mean_squared_error/SquaredDifference}} = SquaredDifference[T=DT_FLOAT](sequential_11/time_distributed_11/Reshape_1, IteratorGetNext:1)' with input shapes: [?,192,1], [?,96,1].",这是由history = model.fit(...引起的。所以没有卷积层我无法更改它。 - PeterBe
我有另外一个问题想问。4)当我按照你的建议,在最后一个输出层使用keras.layers.Dense(96)时,输出是 (BATCH_SIZE, 96),而不是使用keras.layers.TimeDistributed(keras.layers.Dense(1))时的(BATCH_SIZE, NUMBER_OF_TIMESTEPS, 1)。我不明白的是标签向量Y_train仍然具有大小为(BATCH_SIZE, NUMBER_OF_TIMESTEPS, 1),而特征向量X_train则为(BATCH_SIZE, NUMBER_OF_TIMESTEPS, NUMBER_OF_FEATURES) - PeterBe
“实际上,所有的预测都是基于旧数据的”,没错。但是看看每个数据如何在网络中移动。只有前几个输入对第一个输出可见。看看 ASCII 图中的箭头。 - mdaoust
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接