【深度学习实验】—— 基于 LSTM 的 火灾温度预测
- 🍨 本文为🔗365天深度学习训练营 中的学习记录博客
- 🍖 原作者:K同学啊
文章目录
1. 简介
本项目使用 woodpine2.csv 中的连续观测数据,依据最近 8 个时刻的温度(Tem1)、一氧化碳浓度(CO 1)和烟尘浓度(Soot 1),预测下一时刻的温度。模型采用 TensorFlow/Keras 实现的 LSTM。
原始 Notebook 能够完成训练,但直接预测绝对温度时得到的 RMSE 为 17.15614,明显没有利用好序列的平滑性。优化后,模型改为预测“下一时刻相对当前温度的变化量”,并修正了数据缩放、验证集使用和指标计算方式。
| 项目 | 内容 |
|---|---|
| 模型 | 带一阶差分特征、L2 与 Dropout 的轻量单层 LSTM(残差/变化量预测) |
| 任务 | 多变量单步时间序列回归:预测下一时刻 Tem1 |
| 数据集 | woodpine2.csv,5,948 个时间点、3 个观测变量 |
| 样本构造 | 8 个连续时间步作为输入,得到 5,940 个监督学习样本 |
| 划分方式 | 前 4,992 条训练、隔离 8 个窗口、后 940 条测试;训练集尾部 15% 用作验证集 |
| 本次运行结果 | LSTM:RMSE 0.83551,R² 0.99755(固定随机种子 42) |
| 持久性基线 | RMSE 1.14854,R² 0.99536 |
这里的持久性基线(Persistence Baseline)直接把窗口最后一个温度当作下一时刻预测值。它是平滑时间序列中必须比较的强基线:优化后的 LSTM 虽然略优于它,但提升很小,说明当前数据的短期温度具有非常强的连续性。
2. 环境
- 语言环境:Python 3.12
- 编译器:Jupyter Notebook(VS Code)
- 深度学习框架:TensorFlow 2.21.0
- 主要库:pandas 3.0.5、scikit-learn 1.9.0、matplotlib 3.11.1、seaborn
- 硬件策略:自动检测 GPU;若检测到 GPU,则启用按需申请显存
3. 代码实现
3.1 前期准备
3.1.1 导入库、配置绘图与 GPU
import tensorflow as tf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from datetime import datetime
from matplotlib import font_manager
from sklearn.preprocessing import StandardScaler
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout, LSTM
from tensorflow.keras import regularizers
from tensorflow.keras import Input
from sklearn import metrics
preferred_chinese_fonts = [
'Hiragino Sans GB', 'PingFang SC', 'Microsoft YaHei', 'SimHei',
'Noto Sans CJK SC', 'Arial Unicode MS', 'WenQuanYi Zen Hei',
]
installed_fonts = {font.name for font in font_manager.fontManager.ttflist}
if not any(name in installed_fonts for name in preferred_chinese_fonts):
for font_path in font_manager.findSystemFonts():
try:
font_name = font_manager.FontProperties(fname=font_path).get_name()
if font_name in preferred_chinese_fonts:
font_manager.fontManager.addfont(font_path)
installed_fonts.add(font_name)
except (OSError, RuntimeError):
pass
chinese_font = next(
(name for name in preferred_chinese_fonts if name in installed_fonts),
'DejaVu Sans',
)
plt.rcParams['font.sans-serif'] = [chinese_font, 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
gpus = tf.config.list_physical_devices('GPU')
if gpus:
tf.config.experimental.set_memory_growth(gpus[0], True)
tf.config.set_visible_devices([gpus[0]], 'GPU')
3.1.2 读取数据与观察序列
数据包含 Time、Tem1、CO 1、Soot 1 四列。Time 为时间坐标,后三列是模型特征;其中 Tem1 同时是预测目标。
df_1 = pd.read_csv('./Data/woodpine2.csv')
dataFrame = df_1.iloc[:, 1:]
dataFrame
Tem1 CO 1 Soot 1
0 25.0 0.000000 0.000000
1 25.0 0.000000 0.000000
2 25.0 0.000000 0.000000
3 25.0 0.000000 0.000000
4 25.0 0.000000 0.000000
... ... ... ...
5943 295.0 0.000077 0.000496
5944 294.0 0.000077 0.000494
5945 292.0 0.000077 0.000491
5946 291.0 0.000076 0.000489
5947 290.0 0.000076 0.000487
5948 rows × 3 columns
数据共有 5,948 行、3 个建模变量。通过折线图可以同时观察温度、CO 和烟尘的变化趋势。
plt.rcParams['savefig.dpi'] = 500
plt.rcParams['figure.dpi'] = 500
fig, ax = plt.subplots(1, 3, constrained_layout=True, figsize=(14, 3))
sns.lineplot(data=df_1['Tem1'], ax=ax[0])
sns.lineplot(data=df_1['CO 1'], ax=ax[1])
sns.lineplot(data=df_1['Soot 1'], ax=ax[2])
plt.show()

绘图时应使用
plt.subplots()创建多个坐标轴。plt.subplot()只用于取得单个子图,向它传入(1, 3)会触发参数数量错误。
3.1.3 构造滑动时间窗口
令窗口长度 width_X=8。第 i 个输入样本由 i 至 i+7 时刻的三维观测组成,标签是第 i+8 时刻的温度。原始序列存在大量平台段,仅输入水平值容易使模型退化为“变化量恒为 0”的持久性基线,因此同时加入温度、CO、烟尘的一阶差分。LSTM 最终输入形状为 (样本数, 8, 6):3 个水平特征加 3 个差分特征。
width_X = 8
width_y = 1
values = dataFrame.to_numpy(dtype=np.float32)
# 用连续的 8 个时刻预测下一时刻温度;保留二维时间结构供 LSTM 使用。
X_level = np.asarray([values[i:i + width_X] for i in range(len(values) - width_X)])
X_delta = np.diff(X_level, axis=1, prepend=X_level[:, :1, :])
X = np.concatenate([X_level, X_delta], axis=-1)
y = values[width_X:, 0]
print(X.shape, y.shape)
(5940, 8, 6) (5940,)
3.2 数据划分与优化策略
3.2.1 按时间顺序划分并防止数据泄漏
时间序列不能像普通表格数据那样随机打乱,否则未来数据可能混入训练集。这里按时间顺序保留最后 940 条样本作为测试集。
另一个关键点是:StandardScaler 只能在实际参与参数更新的训练段拟合,再用于转换验证集与测试集。若在包含验证集或测试集的数据上 fit,未来时段的均值和标准差会泄漏到训练过程,使离线评估偏乐观。
split_index = 5000
purge_width = width_X
# 在训练、测试窗口间隔离一个完整窗口,避免任一原始观测同时出现在两侧。
X_train_raw, X_test_raw = X[:split_index - purge_width], X[split_index:]
y_train_raw, y_test = y[:split_index - purge_width], y[split_index:]
validation_ratio = 0.15
fit_size = int(len(X_train_raw) * (1 - validation_ratio))
X_fit_raw = X_train_raw[:fit_size]
# 每个传感器只使用一套缩放参数,保证同一特征在所有时间步上的尺度一致。
feature_scaler = StandardScaler().fit(X_fit_raw.reshape(-1, X_fit_raw.shape[-1]))
n_features = X_train_raw.shape[-1]
X_train = feature_scaler.transform(
X_train_raw.reshape(-1, n_features)
).reshape(-1, width_X, n_features)
X_test = feature_scaler.transform(
X_test_raw.reshape(-1, n_features)
).reshape(-1, width_X, n_features)
print(X_train.shape, X_test.shape)
(4992, 8, 6) (940, 8, 6)
3.2.2 从预测绝对温度改为预测变化量
直接让网络输出绝对温度时,标签范围较大,且网络必须同时学习长期温度水平和短期变化。由于该序列很平滑,后一项才是模型真正需要补充的信息。
因此定义:
Δ T t + 1 = T t + 1 − T t \Delta T_{t+1}=T_{t+1}-T_t ΔTt+1=Tt+1−Tt
模型学习标准化后的 Δ T t + 1 \Delta T_{t+1} ΔTt+1;推理时再与窗口最后一个温度 T t T_t Tt 相加,还原绝对温度。这个残差式目标使网络专注于下一步的增量。
last_temperature_train = X_train_raw[:, -1, 0]
last_temperature_test = X_test_raw[:, -1, 0]
y_train_delta = y_train_raw - last_temperature_train
# 与特征缩放一致:目标缩放器也不能看到验证段或测试段。
target_scaler = StandardScaler().fit(y_train_delta[:fit_size].reshape(-1, 1))
y_train = target_scaler.transform(y_train_delta.reshape(-1, 1))
X_train = X_train.astype('float32')
X_test = X_test.astype('float32')
print(y_train.shape, y_test.shape)
(4992, 8, 6)
3.3 LSTM 模型建立与训练
3.3.1 定义网络
网络由一层 32 单元的 LSTM 提取 8 个时间步的水平和差分特征,再通过一个 16 单元的 ReLU 全连接层和单输出回归层预测标准化后的温度变化量。LSTM 使用 L2 正则化,中间加入 20% Dropout,以抑制对训练时段噪声的记忆。这里使用 MSE 学习条件均值。
tf.keras.utils.set_random_seed(42)
model_lstm = Sequential([
Input(shape=(width_X, n_features)),
LSTM(32, kernel_regularizer=regularizers.l2(1e-5)),
Dropout(0.2),
Dense(16, activation='relu'),
Dense(width_y),
])
model_lstm.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3),
loss='mean_squared_error',
)
model_lstm.summary()
3.3.2 训练策略
训练最多运行 80 个 Epoch,但并不使用测试集作为训练中的验证集。代码显式保留训练序列末尾 15%(749 条样本)作为验证集;缩放器仅在前 4,243 条实际训练样本上拟合。划分始终保持时间顺序;模型不是 stateful LSTM,因此仅在拟合段内部打乱已经构造好的窗口,避免相邻阶段批次造成梯度震荡,不会把验证或测试数据混入训练。
EarlyStopping 从第 6 个 Epoch 起监控验证损失,连续 5 个 Epoch 无有效改善时恢复最优权重并停止训练;ReduceLROnPlateau 则在平台期将学习率减半。
callbacks = [
tf.keras.callbacks.EarlyStopping(
monitor='val_loss', patience=5, min_delta=1e-3,
restore_best_weights=True, start_from_epoch=5,
),
tf.keras.callbacks.ReduceLROnPlateau(
monitor='val_loss', patience=3, factor=0.5, min_lr=1e-6,
),
]
history_lstm = model_lstm.fit(
X_train[:fit_size], y_train[:fit_size],
validation_data=(X_train[fit_size:], y_train[fit_size:]),
batch_size=64,
epochs=80,
shuffle=True,
callbacks=callbacks,
verbose=1,
)
Epoch 1/20
67/67 ━━━━━━━━━━━━━━━━━━━━ 1s 4ms/step - loss: 0.8813 - val_loss: 4.4871 - learning_rate: 0.0010
Epoch 2/20
67/67 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 0.8310 - val_loss: 4.1522 - learning_rate: 0.0010
Epoch 3/20
67/67 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 0.8062 - val_loss: 3.9076 - learning_rate: 0.0010
Epoch 4/20
67/67 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 0.7917 - val_loss: 3.6557 - learning_rate: 0.0010
Epoch 5/20
67/67 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 0.7626 - val_loss: 3.5088 - learning_rate: 0.0010
Epoch 6/20
67/67 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 0.7552 - val_loss: 3.4273 - learning_rate: 0.0010
Epoch 7/20
67/67 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 0.7493 - val_loss: 3.3088 - learning_rate: 0.0010
Epoch 8/20
67/67 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 0.7347 - val_loss: 3.2885 - learning_rate: 0.0010
Epoch 9/20
67/67 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 0.7213 - val_loss: 3.1975 - learning_rate: 0.0010
Epoch 10/20
67/67 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 0.7205 - val_loss: 3.1592 - learning_rate: 0.0010
Epoch 11/20
67/67 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 0.7239 - val_loss: 3.1110 - learning_rate: 0.0010
Epoch 12/20
67/67 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 0.7113 - val_loss: 3.0958 - learning_rate: 0.0010
Epoch 13/20
67/67 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 0.7055 - val_loss: 3.0986 - learning_rate: 0.0010
Epoch 14/20
67/67 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 0.6992 - val_loss: 3.0686 - learning_rate: 0.0010
Epoch 15/20
67/67 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 0.6939 - val_loss: 3.0342 - learning_rate: 0.0010
Epoch 16/20
67/67 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 0.6947 - val_loss: 3.0925 - learning_rate: 0.0010
Epoch 17/20
67/67 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 0.6926 - val_loss: 3.1163 - learning_rate: 0.0010
Epoch 18/20
67/67 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 0.6850 - val_loss: 3.0961 - learning_rate: 0.0010
Epoch 19/20
67/67 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 0.6740 - val_loss: 3.0947 - learning_rate: 5.0000e-04
Epoch 20/20
67/67 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 0.6751 - val_loss: 3.0916 - learning_rate: 5.0000e-04
4. 模型评估
4.1 损失曲线
训练结束后绘制训练集与验证集的 MSE 曲线。实际运行时应重点观察两条曲线:若训练损失持续下降而验证损失上升,说明出现过拟合;早停会保留验证损失最小处的模型权重。图内使用 datetime.now() 标明本次生成时间。
generated_at = datetime.now()
train_losses = history_lstm.history['loss']
val_losses = history_lstm.history['val_loss']
epochs_range = range(1, len(train_losses) + 1)
best_epoch = int(np.argmin(val_losses)) + 1
fig, ax = plt.subplots(figsize=(6, 3.6), dpi=120)
ax.plot(epochs_range, train_losses, label='训练损失')
ax.plot(epochs_range, val_losses, label='验证损失')
ax.axvline(
best_epoch, color='gray', linestyle='--', linewidth=1,
label=f'验证最优:第 {best_epoch} 轮',
)
ax.scatter(best_epoch, val_losses[best_epoch - 1], color='tab:orange', s=28, zorder=3)
ax.set_title('训练与验证损失', pad=22)
ax.set_xlabel('训练轮次')
ax.set_ylabel('均方误差(MSE)')
ax.text(
0.5, 1.02, f'生成时间:{generated_at:%Y-%m-%d %H:%M:%S}',
transform=ax.transAxes, ha='center', va='bottom', fontsize=8, color='dimgray',
)
ax.legend()
ax.grid(alpha=0.25)
fig.tight_layout()
plt.show()

使用本文当前代码并固定随机种子 42 复跑时,训练在第 20 个 Epoch 停止,并恢复第 15 个 Epoch 的验证最优权重。训练损失由 0.88128 降至 0.67513,验证损失由 4.48713 降至最低 3.03419,两条曲线不再停留在常数基准线上。验证损失仍高于训练损失,主要因为验证时间段的温度变化量方差更高;轻量网络、正则化和早停可限制继续拟合训练噪声,但仍建议用多随机种子与滚动验证复核。
4.2 还原预测值并绘制对比曲线
网络输出是标准化后的变化量,必须先逆变换,再加回每个窗口的最后一个温度,才能得到最终温度预测。下面展示测试集前 1,000 个点的真实值和预测值。
predicted_delta = target_scaler.inverse_transform(
model_lstm.predict(X_test, verbose=0)
).ravel()
predicted_y_lstm = last_temperature_test + predicted_delta
plt.figure(figsize=(5, 3), dpi=120)
plt.plot(y_test[:1000], color='red', label='真实值')
plt.plot(predicted_y_lstm[:1000], color='blue', label='预测值')
plt.title('测试集温度预测')
plt.xlabel('时间步')
plt.ylabel('温度')
plt.legend()
plt.show()

4.3 指标与基线对照
回归任务中,r2_score 的第一个参数必须是真实值、第二个参数必须是预测值。
RMAE_lstm = metrics.mean_squared_error(y_test, predicted_y_lstm) ** 0.5
R2_lstm = metrics.r2_score(y_test, predicted_y_lstm)
# 强基线:直接使用窗口最后一个温度预测下一时刻。
RMAE_naive = metrics.mean_squared_error(y_test, last_temperature_test) ** 0.5
R2_naive = metrics.r2_score(y_test, last_temperature_test)
print('均方根误差:%.5f' % RMAE_lstm)
print('R2:%.5f' % R2_lstm)
print('持久性基线 — RMSE:%.5f,R2:%.5f' % (RMAE_naive, R2_naive))
均方根误差:0.83551
R2:0.99755
持久性基线 — RMSE:1.14854,R2:0.99536
复现实测结果如下:
| 模型 | RMSE | R² |
|---|---|---|
| 原始绝对温度 LSTM(旧运行输出) | 17.15614 | 旧代码的 R² 参数顺序错误,不作横向解释 |
| 持久性基线: T ^ t + 1 = T t \hat{T}_{t+1}=T_t T^t+1=Tt | 1.14854 | 0.99536 |
| 变化量 LSTM + 一阶差分特征(严格切分、当前代码、种子 42) | 0.83551 | 0.99755 |
变化量设计、显式一阶差分和无泄漏标准化使 RMSE 从旧实现的 17.15614 降至 0.83551;相对持久性基线的 RMSE 降低约 27.3%。在这类高度平滑的单步预测任务中仍必须报告该基线,并用多次实验确认差异是否稳定。
5. 总结
-
残差目标更适合平滑的单步预测:让 LSTM 预测 T t + 1 − T t T_{t+1}-T_t Tt+1−Tt,再加回当前温度,比直接回归绝对温度稳定得多。
-
时间序列评估要严格隔离未来信息:缩放器只在训练集拟合,验证集来自训练时间段,测试集只用于最终评估,避免信息泄漏。
-
本次 RMSE 为 0.83551:变化量建模与一阶差分特征使模型学到了平台跳变之外的动态信息,明显优于持久性基线。
-
强基线不可省略:持久性基线的 RMSE 为 1.14854;当前 LSTM 为 0.83551,但仍需通过重复实验和滚动验证确认改进是否稳定。
-
后续可优化方向:可以尝试多步预测、不同窗口长度、加入滑动统计特征,并使用 walk-forward validation(滚动时间验证)评估稳定性;也可以比较 GRU、线性回归或树模型,判断复杂神经网络是否确实带来稳定收益。
更多推荐

所有评论(0)