解决CNN_M_LSTM数据维度问题:能耗与疫情数据拼接
2025-03-05 16:34:46
CNN_M_LSTM 模型输入数据拼接问题详解与解决
你在尝试将时间戳能耗数据和疫情数据输入到 CNN_M_LSTM 模型时,遇到了数据维度不匹配的问题。 简单说,模型期望的输入数据的维度,跟你处理后的数据维度对不上。下面一步步分析并解决。
一、问题分析
-
数据维度 :
- 原始能耗数据维度: (730, 2)
- 原始疫情数据维度: (730, 1)
- 你希望的窗口大小 (window_size): 96
- 批次大小 (batch_size): 128
- 模型
create_CNN_LSTM_model
期望的input1和input2是二维,但你通过tf.data.Dataset.zip
得到的是包含三维的嵌套张量.
-
windowed_dataset
函数 :
这个函数会将数据切分成窗口,每个窗口包含window_size
个时间步的数据。 你给函数的series
是 (730, 2) 和 (730, 1)。 但是,windowed_dataset
在返回的时候只取了window[-1][0]
。 也就是说输出的label只取了第二个维度。而我们希望所有的label被保留。 -
tf.data.Dataset.zip
的使用 : 这个函数会把两个数据集“拉链式”地组合。 也就是第一个数据集的第一个样本, 跟第二个数据集的第一个样本组合, 依次类推。 -
模型的输入层定义 :
create_CNN_LSTM_model
里,输入层input1
和input2
的形状定义得不够灵活,且不适应处理完后的输入。
二、解决方案
1. 修改 windowed_dataset
函数
def windowed_dataset(series, window_size=MAX_LENGTH, batch_size=BATCH_SIZE, shuffle_buffer=TRAIN.SHUFFLE_BUFFER_SIZE):
"""
切分时间序列数据为窗口,生成特征和标签。
"""
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)
dataset = dataset.map(lambda window: (window[:-1], window[-1])) # 修改这里,保留完整的标签
dataset = dataset.batch(batch_size, drop_remainder=True).cache()
return dataset
改进点:
window[-1]
改为window[-1]
: 这样输出的 label 会包含时间步的所有特征维度, 而不是仅仅第一个特征.- 在
window
的时候加入了drop_remainder=True
: 使输出的数据都是等长的,能有效解决批处理时的错误。 - 返回的数据格式是 ((batch_size, window_size, feature), (batch_size, feature)), 符合大部分情况下模型的需求。
2. 调整数据zip
和map
方式
train_energy_dataset = windowed_dataset(TRAIN.PRE_PROCESSED_SERIES)
train_covid_dataset = windowed_dataset(TRAIN.SERIES_COVID)
# 直接zip 两个数据集.
train_dataset = tf.data.Dataset.zip((train_covid_dataset, train_energy_dataset))
def combine_features_and_labels(data1, data2):
#将特征与特征结合, 标签与标签结合.
combined_features = (data1[0], data2[0]) # ((128, 96, 1) , (128, 96, 2)), 特征维度.
combined_labels = (data1[1], data2[1]) # ((128,1) , (128, 2)), label的维度
return combined_features, combined_labels
train_dataset = train_dataset.map(combine_features_and_labels)
train_dataset = train_dataset.prefetch(tf.data.AUTOTUNE)
改进点:
- 将
map
从主要处理特征,调整到组合feature
和label
. - 移除
batch
:因为在windowed_dataset
中,batch已经被执行了。 - 加入
prefetch
: 让模型在训练时, 预先准备下一批数据.
3. 修改模型 create_CNN_LSTM_model
def create_CNN_LSTM_model():
# input1 处理 covid 数据。
input1 = tf.keras.layers.Input(shape=(MAX_LENGTH, 1), name="input1")
# CNN 部分, 处理 input1.
x = tf.keras.layers.Conv1D(filters=128, kernel_size=3, activation='relu', strides=1, padding="causal")(input1)
x = tf.keras.layers.MaxPooling1D(pool_size=2)(x)
x = tf.keras.layers.Conv1D(filters=64, kernel_size=3, activation='relu', strides=1, padding="causal")(x)
x = tf.keras.layers.MaxPooling1D(pool_size=2)(x)
x = tf.keras.layers.Dropout(0.5)(x)
x = tf.keras.layers.LSTM(16, return_sequences=True)(x)
x = tf.keras.layers.LSTM(8, return_sequences=True)(x)
# 注意, 这里先不 Flatten. 因为可能需要跟 input2 的输出拼接.
x = tf.keras.layers.LSTM(8)(x) #如果只关心最后一个时间步,可移除return_sequences=True
output_lstm = tf.keras.layers.Dense(1)(x)
# input2 处理 energy 数据.
input2 = tf.keras.layers.Input(shape=(MAX_LENGTH, 2), name="input2")
#直接使用一个dense或加入一个lstm层来对时间进行降维处理。
output_dense_1 = tf.keras.layers.LSTM(8)(input2)
#可以继续处理
output_dense_2 = tf.keras.layers.Dense(1)(output_dense_1)
# 拼接两个输入对应的输出.
concatenated = tf.keras.layers.Concatenate()([output_lstm, output_dense_2])
# 添加全连接层做最终预测
x = tf.keras.layers.Dense(6, activation=tf.nn.leaky_relu)(concatenated)
#因为我们预测energy的维度是2维, 而covid是一维. 根据你想要的结果来调整最后一层的输出。
output = tf.keras.layers.Dense(2)(x) # 假定最后你想预测energy的二维.
model_final = tf.keras.Model(inputs=[input1, input2], outputs=output)
return model_final
改进点:
input1
形状改为(MAX_LENGTH, 1)
:对应疫情数据的窗口和特征维度。input2
形状改为(MAX_LENGTH, 2)
:对应能耗数据的窗口和特征维度。- 移除了CNN部分不必要的
Flatten
操作: 输出直接送入Dense
层前,可能还需要和另一个input2
做合并, 合并时机根据实际需要来。 output
的Dense的unit数量与能耗维度对应,原来是能耗维度是2,原来label的获取代码window[-1][0]
只会拿到一个,因此output层会发生问题,要修改为和能耗维度匹配。- 给
input2
(energy)的数据加入处理层。 - 根据自己对数据处理的认知来修改模型组合方式和结构, 例如对数据先各自lstm再合并,或者先合并再一起处理。
4. 编译与训练
model_cnn_m_lstm = create_CNN_LSTM_model()
model_cnn_m_lstm.compile(
loss=tf.keras.losses.Huber(),
optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),
metrics=["mse"]
)
model_cnn_m_lstm.summary()
model_cnn_m_lstm.fit(train_dataset, epochs=100)
改进:
- 去除
batch_size
参数: batching 在tf.data
处理时已经完成了。
三、进阶技巧(可选)
-
数据归一化/标准化: 强烈建议在数据输入模型前, 对能耗和疫情数据进行归一化或标准化。 可以使用
sklearn.preprocessing
中的MinMaxScaler
或StandardScaler
。 -
处理标签维度不一致问题: 在修改版的
windowed_dataset
中,能保证特征的输出,但由于能耗与疫情数据的标签维度不同,你可以:- 考虑预测其中一项,舍弃另一项。(如本例只预测能耗).
- 分别构建不同的输出头(output heads), 每个负责一个数据集的标签预测,总损失是多个输出头损失的加权和。
-
处理时间特征: 你的能耗数据包含时间戳, 考虑提取时间特征(年、月、日、星期几、小时等),并把它们作为额外的特征。
-
早停 (Early Stopping): 在训练过程中监控验证集上的性能, 如果模型性能不再提升,则提前停止训练, 防止过拟合。
-
学习率调整策略: 可以尝试
ReduceLROnPlateau
,OneCycleLR
等. -
尝试其他的模型拼接方式: 也可以使用其他拼接数据的方法,例如可以在合并后再进入LSTM.
# 先合并两个input,把两个input想象成不同的feature,在一个时间维度进行扩展后进行concat input1 = tf.keras.layers.Input(shape=(96, 1), name="input1") # 假设的输入维度 input2 = tf.keras.layers.Input(shape=(96, 2), name="input2") concatenated_inputs = tf.keras.layers.Concatenate(axis=-1)([input1, input2])#变成96,3
通过上面步骤,你应该能够成功解决数据维度不匹配的问题, 并把数据喂给 CNN_M_LSTM 模型。 记得根据你的数据特点和需求, 对代码细节进行微调。