- 🍨 本文为🔗365天深度学习训练营 中的学习记录博客
- 🍖 原作者:K同学啊
前言
- 本次任务基于火灾温度(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. 检查缺失值与划分数据集
- 检查
X和y中是否存在 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
- 使用
TensorDataset和DataLoader封装训练/测试数据,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. 训练与测试函数
train和test函数完成前向传播、损失计算、反向传播(训练时)和损失累加。原始版本中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 的指标要求。主要改进点总结如下:
- 数据构造:将输入窗口从 8 扩大到 16,预测步长从 1 改为 2,为多步预测提供更丰富的时序上下文。
- 训练策略:采用 SGD + 动量(0.9),初始学习率 0.01,配合余弦退火调度,训练 300 轮并引入早停(patience=50),防止过拟合的同时充分挖掘数据规律。隐藏层维度从 320 提升至 512,提高模型容量。
- 评估方式:不仅计算整体 R²,还分别计算每一步的 R²,便于诊断模型在各时刻的表现,确保两步预测均达到优良水平。