第R4周:LSTM-火灾温度预测

前言

  • 本次任务基于火灾温度(Tem1)、一氧化碳浓度(CO 1)、烟雾浓度(Soot 1)的时序数据,使用 LSTM 模型实现未来两个时刻(第9、第10时刻)的温度预测,要求整体决定系数 R² 达到 0.83 以上。指导文章能完成单步预测(前8步预测第9步)。经过对数据构造、模型结构、训练策略和评估方式的系统性改进,最终实现了两步预测,整体 R² 达到 0.83的要求。

1. 导入依赖与读取数据

  • 本段导入必要的 Python 库(numpy, pandas, torch),并读取 woodpine2.csv 数据集,准备运行环境并加载原始数据。
python 复制代码
import torch.nn.functional as F
import numpy as np
import pandas as pd
import torch
from torch import nn

# 导入数据
data = pd.read_csv("woodpine2.csv")
data

2. 数据集可视化

  • 绘制三条时序曲线(Tem1、CO 1、Soot 1),初步观察数据变化趋势。
python 复制代码
# 数据集可视化
import matplotlib.pyplot as plt
import seaborn as sns

# 设置图像保存分辨率与显示分辨率
plt.rcParams['savefig.dpi'] = 500  # 保存图片像素
plt.rcParams['figure.dpi'] = 500   # 画布显示分辨率

# 创建1行3列的子图画布,自动调整布局,设定画布尺寸
fig, ax = plt.subplots(1,3,constrained_layout=True, figsize=(14, 3))

# 分别绘制三条时序曲线
sns.lineplot(data=data["Tem1"], ax=ax[0])
sns.lineplot(data=data["CO 1"], ax=ax[1])
sns.lineplot(data=data["Soot 1"], ax=ax[2])

# 展示图像
plt.show()

3. 数据预处理与归一化

  • 截取三列特征,使用 MinMaxScaler 将各特征分别归一化到 0,1 区间,消除量纲影响。代码在两个版本中完全一致。
python 复制代码
# 选取所有行,从第2列(索引1)开始向后的全部列,剔除第1列
dataFrame = data.iloc[:,1:]
# 查看截取后的数据
dataFrame

4. 数据集构造(做了一些改动)

  • 输入窗口从width_X = 8,预测步长 width_y = 1扩大为 width_X = 16,预测步长改为 width_y = 2,标签 y 形状变为 (样本数, 2, 1),对应未来两个时刻(第9和第10步),滑动窗口生成输入 X(形状:样本数×8×3)和标签 y(形状:样本数×1×1),但 y_ 截取长度变为 width_y,且最终 y 重塑为 (-1, width_y, 1),作用是窗口长度增加一倍以提供更多历史信息,输出从单步扩展为两步,为后续多步预测奠定数据基础。
python 复制代码
# 导入最值归一化工具
from sklearn.preprocessing import MinMaxScaler

# 截取全部行、第2列往后所有列,copy防止原数据视图警告
dataFrame = data.iloc[:,1:].copy()

# 创建归一化器,将数据映射到 [0, 1] 区间
sc = MinMaxScaler(feature_range=(0, 1)) 

# 循环对指定列单独做0-1归一化
for i in ['CO 1', 'Soot 1', 'Tem1']:
    # reshape(-1,1)把一维序列转为sklearn要求的二维格式
    dataFrame[i] = sc.fit_transform(dataFrame[i].values.reshape(-1, 1))

# 输出处理后数据集维度 (样本数, 特征数)
dataFrame.shape
python 复制代码
# 输入序列长度:利用连续8个时间步数据作为输入
width_X = 16
# 预测步长:预测未来2个时间步
width_y = 2

# 构建数据集容器
# 取前8个时间段的Tem1、CO 1、Soot 1作为X,第9个时间段的Tem1为y。
X = []
y = []

in_start = 0

# 滑动窗口遍历数据
for _, _ in data.iterrows():
    in_end = in_start + width_X
    out_end = in_end + width_y
    # 防止索引越界
    if out_end < len(dataFrame):
        # 截取窗口内全部特征作为输入样本
        X_ = np.array(dataFrame.iloc[in_start:in_end , :])
        # 截取下一个时刻第0列(Tem1)作为预测标签
        y_ = np.array(dataFrame.iloc[in_end :out_end, 0])
        
        X.append(X_)
        y.append(y_)
    # 窗口向右滑动一格
    in_start += 1

# 转为numpy数组
X = np.array(X)
# reshape适配模型输入维度 [样本数, 预测长度, 特征维度]
y = np.array(y).reshape(-1,width_y,1)

# 打印输入、标签数组形状
X.shape, y.shape

5. 检查缺失值与划分数据集

  • 检查 Xy 中是否存在 NaN(两者均无缺失),然后按前5000条为训练集、剩余为测试集,并转换为 PyTorch 张量。
python 复制代码
# 检查输入特征X中是否存在缺失值NaN
print(np.any(np.isnan(X)))
# 检查标签y中是否存在缺失值NaN
print(np.any(np.isnan(y)))
python 复制代码
# 前5000条样本划分为训练集,转换为PyTorch浮点张量
X_train = torch.tensor(np.array(X[:5000]), dtype=torch.float32)
y_train = torch.tensor(np.array(y[:5000]), dtype=torch.float32)

# 5000条之后全部划分为测试集
X_test  = torch.tensor(np.array(X[5000:]), dtype=torch.float32)
y_test  = torch.tensor(np.array(y[5000:]), dtype=torch.float32)

# 打印训练集输入、标签维度
X_train.shape, y_train.shape

6. 创建 DataLoader

  • 使用 TensorDatasetDataLoader 封装训练/测试数据,batch_size=64,不进行打乱(时序数据保持顺序)。
python 复制代码
# 导入数据集与加载器工具
from torch.utils.data import TensorDataset, DataLoader

# 构造训练数据集与训练加载器
train_dl = DataLoader(TensorDataset(X_train, y_train),
                      batch_size=64,
                      shuffle=False)

# 构造测试数据集与测试加载器
test_dl  = DataLoader(TensorDataset(X_test, y_test),
                      batch_size=64,
                      shuffle=False)

7. LSTM 模型结构

  • LSTM(Long Short-Term Memory,长短期记忆网络)是一种特殊的循环神经网络(RNN),专门用于处理和预测时间序列数据中的长期依赖关系。它由 Hochreiter & Schmidhuber 于1997年提出,是对传统 RNN 在长序列训练中容易产生梯度消失或梯度爆炸问题的有效改进。

    • 核心结构

      • LSTM 通过引入门控机制(Gates)来控制信息的流动,每个 LSTM 单元包含三个门:

        • 遗忘门(Forget Gate):决定从上一时刻的细胞状态(Cell State)中丢弃哪些信息。

        • 输入门(Input Gate):决定当前时刻的新信息有多少被存入细胞状态。

        • 输出门(Output Gate):基于当前的细胞状态决定输出什么给下一个时刻。

指导文章模型

python 复制代码
class model_lstm(nn.Module):
    def __init__(self):
        super(model_lstm, self).__init__()
        # 第一层LSTM:输入特征维度3,隐藏层320,单层,batch维度在前
        self.lstm0 = nn.LSTM(input_size=3 ,hidden_size=320,
                             num_layers=1, batch_first=True)
        # 第二层LSTM:接收上一层320维输出
        self.lstm1 = nn.LSTM(input_size=320 ,hidden_size=320,
                             num_layers=1, batch_first=True)
        # 全连接层,映射到预测输出1维
        self.fc0  = nn.Linear(320, 1)

    def forward(self, x):
        out, hidden1 = self.lstm0(x)
        # 将第一层隐藏状态传入第二层LSTM
        out, _ = self.lstm1(out, hidden1)
        out = self.fc0(out)
        # 只取序列最后一个时间步输出,实现单步预测
        return out[:, -1:, :] 
  • 两层 LSTM 堆叠,最后只取最后一个时间步的输出,经全连接层得到单步预测。

我最终调整后的模型

python 复制代码
class model_lstm(nn.Module):
    def __init__(self, input_size=3, hidden_size=320, num_layers=2, output_steps=2):
        super().__init__()
        self.output_steps = output_steps
        self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)
        self.fc = nn.Linear(hidden_size, 1)

    def forward(self, x):
        out, _ = self.lstm(x)                       # (batch, seq_len, hidden)
        out = out[:, -self.output_steps:, :]       # 取最后 output_steps 个时间步
        out = self.fc(out)                         # (batch, output_steps, 1)
        return out 
  • 简化LSTM,直接利用序列输出,截取最后 output_steps 个时刻的隐藏状态,再通过一个共享全连接层输出对应时刻的预测值。这样参数更少,且能同时学习两个时刻的时序依赖。

8. 训练与测试函数

  • traintest 函数完成前向传播、损失计算、反向传播(训练时)和损失累加。原始版本中 train 函数内包含了学习率调度(lr_scheduler.step()),最终版本保留了该参数但将调度移至外部循环,但函数主体逻辑一致。由于功能相同,此处视为未改动(仅调用方式略有调整)。
python 复制代码
# 训练循环
import copy
def train(train_dl, model, loss_fn, opt, lr_scheduler=None):
    size        = len(train_dl.dataset)
    num_batches = len(train_dl)
    train_loss  = 0  # 初始化训练损失

    for x, y in train_dl:
        x, y = x.to(device), y.to(device)

        # 前向传播,得到预测值
        pred = model(x)
        loss = loss_fn(pred, y)

        # 反向传播与参数更新
        opt.zero_grad()   # 梯度清零
        loss.backward()   # 反向传播求梯度
        opt.step()        # 更新网络权重

        # 累加批次损失
        train_loss += loss.item()

    # 学习率调度器更新
    if lr_scheduler is not None:
        lr_scheduler.step()
        print("learning rate = {:.5f}".format(opt.param_groups[0]['lr']), end=" ")

    # 计算整个epoch平均训练损失
    train_loss /= num_batches
    return train_loss
python 复制代码
def test(dataloader, model, loss_fn):
    size        = len(dataloader.dataset)  # 测试集样本总量
    num_batches = len(dataloader)           # 批次数量
    test_loss   = 0

    # 推理阶段关闭梯度计算,节省显存、加速运算
    with torch.no_grad():
        for x, y in dataloader:
            x, y = x.to(device), y.to(device)
            y_pred = model(x)
            loss = loss_fn(y_pred, y)
            test_loss += loss.item()

    # 计算测试集平均损失
    test_loss /= num_batches
    return test_loss

9. 训练配置与超参数

配置项 指导文章 我的调整 说明
优化器 SGD SGD 不变
学习率 0.1 0.01 适当降低,配合增大模型容量
动量 0.9 新增动量项加速收敛
权重衰减 1e-4 1e-4 不变
学习率调度 CosineAnnealingLR(epochs=50) CosineAnnealingLR(T_max=300) 适应更多轮数
训练轮数 50 300 想配合早停
早停 patience=50 防止过拟合
隐藏层大小 320 512 增加模型容量

最终训练循环额外打印每轮学习率,并实现早停逻辑,训练结束后加载最佳模型权重。

python 复制代码
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model_lstm(input_size=3, hidden_size=512, num_layers=2, output_steps=width_y).to(device)
loss_fn = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=1e-4)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=300)

epochs = 300
train_loss, test_loss = [], []
best_test_loss = float('inf')
patience = 50
counter = 0

for epoch in range(epochs):
    model.train()
    epoch_train_loss = train(train_dl, model, loss_fn, optimizer)
    model.eval()
    epoch_test_loss = test(test_dl, model, loss_fn)
    
    train_loss.append(epoch_train_loss)
    test_loss.append(epoch_test_loss)
    
    scheduler.step()
    lr = optimizer.param_groups[0]['lr']
    print(f'Epoch:{epoch+1:3d}, Train_loss:{epoch_train_loss:.5f}, Test_loss:{epoch_test_loss:.5f}, lr={lr:.6f}')
    
    # 早停(基于验证损失是否下降)
    if epoch_test_loss < best_test_loss:
        best_test_loss = epoch_test_loss
        torch.save(model.state_dict(), 'best_model.pth')
        counter = 0
    else:
        counter += 1
        if counter >= patience:
            print(f"Early stopping at epoch {epoch+1}")
            break

print("="*20, 'Done', "="*20)
model.load_state_dict(torch.load('best_model.pth'))
python 复制代码
# 构造模拟输入,测试模型前向传播输出维度
model(torch.rand(30, 8, 3).to(device)).shape

10. 训练过程可视化

绘制训练损失与验证损失曲线,并添加当前时间戳作为 x 轴标签。

python 复制代码
import matplotlib.pyplot as plt
from datetime import datetime
current_time = datetime.now() # 获取当前时间

plt.figure(figsize=(5, 3),dpi=120)

plt.plot(train_loss   , label='LSTM Training Loss')
plt.plot(test_loss, label='LSTM Validation Loss')

plt.title('Training and Validation Loss')
plt.xlabel(current_time)
plt.legend()
plt.show()

11. 结果可视化

最终版本新增

原始版本仅绘制单步预测对比图。最终版本分别绘制第9时刻和第10时刻的预测与真实值曲线(前2000个点),便于直观检查两步预测的拟合效果。

python 复制代码
import matplotlib.pyplot as plt
from datetime import datetime

# 预测全部两个时刻 
preds = model(X_test.to(device)).detach().cpu().numpy()   # (batch, 2, 1)
y_true = y_test.numpy()                                   # (batch, 2, 1)

# 绘制第9时刻(第一个预测步)的对比
pred_step0 = sc.inverse_transform(preds[:, 0, :].reshape(-1, 1))
true_step0  = sc.inverse_transform(y_true[:, 0, :].reshape(-1, 1))

plt.figure(figsize=(5, 3), dpi=120)
plt.plot(true_step0[:2000], color='red', label='real_temp (step 9)')
plt.plot(pred_step0[:2000], color='blue', label='prediction (step 9)')
plt.title('LSTM Temperature Prediction (first predicted step)')
plt.xlabel('Time Step')
plt.ylabel('Temperature')
plt.legend()
plt.show()

# 绘制第10时刻(第二个预测步)对比
pred_step1 = sc.inverse_transform(preds[:, 1, :].reshape(-1, 1))
true_step1  = sc.inverse_transform(y_true[:, 1, :].reshape(-1, 1))
plt.figure(figsize=(5, 3), dpi=120)
plt.plot(true_step1[:2000], color='red', label='real_temp (step 10)')
plt.plot(pred_step1[:2000], color='blue', label='prediction (step 10)')
plt.title('LSTM Temperature Prediction (second predicted step)')
plt.xlabel('Time Step')
plt.ylabel('Temperature')
plt.legend()
plt.show()

12. 模型预测与逆归一化、以及性能评估

  • 指导文章直接对单步预测结果 model(X_test).detach().numpy().reshape(-1,1) 进行逆归一化。而我的最终得到形状为 (batch, 2, 1) 的预测结果,先整体展平为 (-1,1),统一逆归一化,再 reshape 回原始形状。这样确保两个时刻的逆变换使用相同的缩放参数(因为 MinMaxScaler 基于全局数据拟合,展平后再逆变换等价于分别对每个时刻逆变换)。而且指导文章仅计算单步预测的 RMSE 和 R²。我分别计算第9时刻、第10时刻的 R²,以及整体(两个时刻合并)的 RMSE 和 R²。
python 复制代码
from sklearn import metrics

"""
RMSE : 均方根误差 -----> 对均方误差开方
R2   : 决定系数,可以简单理解为反映模型拟合优度的重要的统计量
"""

# 展平逆归一化
pred_flat = preds.reshape(-1, 1)
true_flat = y_true.reshape(-1, 1)
pred_inv = sc.inverse_transform(pred_flat).reshape(preds.shape)
true_inv = sc.inverse_transform(true_flat).reshape(y_true.shape)

# 分别计算第9和第10时刻的R2
r2_step0 = metrics.r2_score(true_inv[:, 0, 0], pred_inv[:, 0, 0])
r2_step1 = metrics.r2_score(true_inv[:, 1, 0], pred_inv[:, 1, 0])
print(f'Step 9  R2: {r2_step0:.5f}')
print(f'Step 10 R2: {r2_step1:.5f}')

# 总体R2
R2_total = metrics.r2_score(true_inv.flatten(), pred_inv.flatten())
RMSE_total = metrics.mean_squared_error(true_inv.flatten(), pred_inv.flatten()) ** 0.5
print('总均方根误差 (RMSE): %.5f' % RMSE_total)
print('总决定系数 (R2): %.5f' % R2_total)

13. 总结

本次任务从单步预测成功扩展到两步预测,并达到 R²≈0.836,达到0.83 的指标要求。主要改进点总结如下:

  1. 数据构造:将输入窗口从 8 扩大到 16,预测步长从 1 改为 2,为多步预测提供更丰富的时序上下文。
  2. 训练策略:采用 SGD + 动量(0.9),初始学习率 0.01,配合余弦退火调度,训练 300 轮并引入早停(patience=50),防止过拟合的同时充分挖掘数据规律。隐藏层维度从 320 提升至 512,提高模型容量。
  3. 评估方式:不仅计算整体 R²,还分别计算每一步的 R²,便于诊断模型在各时刻的表现,确保两步预测均达到优良水平。
相关推荐
笨蛋不要掉眼泪3 小时前
Java虚拟机:常用参数
java·开发语言·python
2501_909509103 小时前
DAY 31
python
李昊哲小课3 小时前
FastAPI 猫咖预约系统 API
人工智能·python·fastapi
你驴我3 小时前
WhatsApp 消息撤回与编辑的幂等性设计实践
java·服务器·前端·后端·python
向日的葵0064 小时前
Redis会话机制vsJWT机制深度解析
数据库·redis·python·缓存·系统架构·jwt
祉猷并茂,雯华若锦4 小时前
Win下完美解决Allure报错,生成Web自动化测试报告
android·python·selenium·自动化
cui_ruicheng4 小时前
Python数据分析(一):数据分析概述与环境搭建
开发语言·python·数据分析
aqi004 小时前
15天学会AI应用开发(十六)LangChain实现对话记忆功能
人工智能·python·大模型·ai编程·ai应用