为何TensorFlow LSTM RNN在数据含目标值时仍预测失败?
我拥有一个包含约80万个时间步的时间序列数据集,尝试使用TensorFlow/Keras训练一个简单LSTM模型,但多次尝试后,无论是预测真实业务目标还是数据集本身已包含的目标值,模型都无法得到有效结果。
操作流程
1. 导入依赖与数据加载
# required imports import pandas as pd import numpy as np import seaborn as sns import tensorflow as tf import tensorflow.keras.layers as tf_layers from tensorflow.keras.models import Model, Sequential from tensorflow.keras.metrics import MAPE, MSE from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping, ReduceLROnPlateau from tensorflow.keras.preprocessing import timeseries_dataset_from_array from sklearn.preprocessing import MinMaxScaler print(df_src.shape) # >>> (833231, 19)
2. 数据归一化
mms = MinMaxScaler() temp = df_src.drop(columns=['time']) df = mms.fit_transform(temp) df = pd.DataFrame(df, index=df_src.index, columns=temp.columns) print(df.shape) # >>> (833231, 18)
注:为简化示例此处对全数据集归一化,实际应分别对训练、验证、测试集做归一化。
3. 定义目标值
选取数据集中第3列作为目标:
df['target'] = df[df.columns[3]]
4. 创建滑动窗口数据集
TEST_FRAC = 0.15 WINDOW_SIZE = 200 test_size = int(len(df) * TEST_FRAC) train_size = len(df) - 2 * test_size X = df.drop(columns=['target']) y = df['target'].shift(1 - WINDOW_SIZE) # 注释:由于timeseries_dataset_from_array从窗口头部取目标,而非尾部,所以需要shift ds_train = timeseries_dataset_from_array(X, y, sequence_length=WINDOW_SIZE, end_index=train_size) ds_val = timeseries_dataset_from_array(X, y, sequence_length=WINDOW_SIZE, start_index=train_size, end_index=train_size+test_size) ds_test = timeseries_dataset_from_array(X, y, sequence_length=WINDOW_SIZE, start_index=train_size+test_size)
5. 验证目标值对齐
验证目标值是否包含在对应窗口的最后一个时间步:
for idx, batch in enumerate(ds_train): x, y = batch test = x[:,-1,3] # 取每个窗口最后一个时间步的目标列值,理论上应等于y if not np.array_equal(test, y): print(f'wrong target at {idx}') # 无输出,说明目标值100%对齐
6. 模型定义
def create_model_v1(input_shape, rnn_units=128, classifier_units=64, dropout_rate=None, learning_rate=1e-2): inputs = tf_layers.Input(shape=input_shape) x = tf_layers.LSTM(units=rnn_units)(inputs) if dropout_rate is not None: x = tf_layers.Dropout(dropout_rate)(x) x = tf_layers.Dense(classifier_units, activation='relu')(x) if dropout_rate is not None: x = tf_layers.Dropout(dropout_rate)(x) x = tf_layers.Dense(outputs)(x) md = Model(inputs=inputs, outputs=x) md.compile(loss='MSE', optimizer=tf.optimizers.Adam(learning_rate=learning_rate), metrics==['MAPE']) return md md1 = create_model_v1(input_shape=(WINDOW_SIZE, (df.shape[1] - 1)), dropout_rate=0.2) md1.summary()
模型结构输出:
Model: "model" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= input_1 (InputLayer) [(None, 200, 18)] 0 lstm (LSTM) (None, 128) 75264 dropout (Dropout) (None, 128) 0 dense (Dense) (None, 64) 8256 dropout_1 (Dropout) (None, 64) 0 dense_1 (Dense) (None, 1) 65 ================================================================= Total params: 83,585 Trainable params: 83,585 Non-trainable params: 0
7. 模型训练
checkpoint = ModelCheckpoint('./save/md1.hdf5', verbose=0, save_best_only=True) earlystop = EarlyStopping(patience=25, restore_best_weights=True) callbacks_list = [checkpoint, earlystop] history = md1.fit(ds_train, validation_data = ds_val, epochs = 200, callbacks = callbacks_list, verbose=1 )
训练结果与问题
预期MAPE会趋近于0,但实际验证集MAPE稳定在18%左右,训练集MAPE约4%:
Epoch 1/200 4556/4556 [==============================] - 59s 13ms/step - loss: 0.0021 - MAPE: 5.0266 - val_loss: 0.0078 - val_MAPE: 18.2009 Epoch 2/200 4556/4556 [==============================] - 58s 13ms/step - loss: 8.4463e-04 - MAPE: 4.2501 - val_loss: 0.0080 - val_MAPE: 18.5296 ... Epoch 25/200 4556/4556 [==============================] - 58s 13ms/step - loss: 9.4219e-04 - MAPE: 4.4964 - val_loss: 0.0079 - val_MAPE: 18.3953
测试集预测值与真实值的分布显示模型几乎未学到任何信息,输出几乎不受输入影响。
请问这是什么原因导致的?我哪里出错了?
1. 模型定义中的语法错误
- 双等号错误:
metrics==['MAPE']使用了双等号,这是语法错误,应该改为单等号metrics=['MAPE']。双等号会返回布尔值,Keras无法识别,会导致模型训练逻辑异常。 - 未定义的
outputs变量:x = tf_layers.Dense(outputs)(x)中的outputs未定义,从模型结构看应该是1(回归任务输出单值)。这个错误会直接导致模型初始化失败,属于代码疏漏。
修正后的模型关键部分:
# 修正Dense层参数 x = tf_layers.Dense(1)(x) md = Model(inputs=inputs, outputs=x) md.compile(loss='MSE', optimizer=tf.optimizers.Adam(learning_rate=learning_rate), metrics=['MAPE']) # 改为单等号
2. 学习率过高
你设置的learning_rate=1e-2(0.01)对于LSTM模型来说过大。LSTM作为序列模型对学习率更敏感,过高的学习率会导致模型在损失函数最优值附近震荡,无法收敛到真正的最小值,表现为验证集损失居高不下。
建议将初始学习率调整为1e-4或5e-4,并添加学习率衰减回调动态调整:
# 添加学习率衰减回调 reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=5, min_lr=1e-6) callbacks_list = [checkpoint, earlystop, reduce_lr] # 模型定义时降低初始学习率 md1 = create_model_v1(input_shape=(WINDOW_SIZE, (df.shape[1] - 1)), dropout_rate=0.2, learning_rate=1e-4)
3. 目标值创建的冗余与潜在对齐问题
y = df['target'].shift(1 - WINDOW_SIZE)的写法容易出错,更直观且不易出错的方式是直接让模型预测窗口最后一个时间步的目标值,无需手动shift:
# 直接创建X和y,y是每个窗口最后一个时间步的目标值 sequence_length = WINDOW_SIZE X = df.drop(columns=['target']).values y = df['target'].values # 生成数据集,窗口[0:200]对应y[199] ds_train = timeseries_dataset_from_array( data=X[:train_size], targets=y[sequence_length-1:train_size], sequence_length=sequence_length ) # 验证集和测试集同理调整
4. Dropout过度使用
模型中LSTM之后连续使用两次0.2的Dropout,对于仅128个单元的LSTM来说,会过度抑制模型学习能力,尤其是训练初期,随机丢弃神经元会导致模型难以捕捉基础模式。
建议先去掉Dense层后的Dropout,仅保留LSTM后的Dropout(或暂时全部去掉,验证模型是否能学习):
def create_model_v1(input_shape, rnn_units=128, classifier_units=64, dropout_rate=None, learning_rate=1e-4): inputs = tf_layers.Input(shape=input_shape) x = tf_layers.LSTM(units=rnn_units)(inputs) if dropout_rate is not None: x = tf_layers.Dropout(dropout_rate)(x) x = tf_layers.Dense(classifier_units, activation='relu')(x) # 去掉此处的Dropout x = tf_layers.Dense(1)(x) md = Model(inputs=inputs, outputs=x) md.compile(loss='MSE', optimizer=tf.optimizers.Adam(learning_rate=learning_rate), metrics=['MAPE']) return md
5. 归一化的潜在问题
实际场景中必须严格按照训练集拟合Scaler,再用该Scaler转换验证集和测试集,否则测试集分布差异会导致模型失效:
# 正确的归一化流程 train_df = df.iloc[:train_size] val_df = df.iloc[train_size:train_size+test_size] test_df = df.iloc[train_size+test_size:] mms = MinMaxScaler() train_scaled = mms.fit_transform(train_df.drop(columns=['time'])) val_scaled = mms.transform(val_df.drop(columns=['time'])) test_scaled = mms.transform(test_df.drop(columns=['time'])) # 再转换为DataFrame后续处理
内容的提问来源于stack exchange,提问作者bellerofonte

