1. 单变量时间序列预测基础

单变量时间序列预测基础 学习卡片

任务说明

本案例使用Transformer模型对单变量时间序列(每日最高气温)进行多步预测。输入过去48天的气温观测值,预测未来7天的气温走势。通过本案例掌握时间序列数据预处理(滑窗切分、归一化)、Transformer模型构建(位置编码、编码器-解码器结构)以及训练/验证的基本流程。

数据准备与预处理

采用北京2023年1月1日至12月31日的每日最高气温(单位:℃)作为实验数据。数据以CSV格式存储,包含date和temp两列。预处理流程如下:

  1. 读取CSV,提取气温序列;
  2. 使用MinMaxScaler将数据归一化至[0,1]区间;
  3. 以48个时间步为输入窗口、7个时间步为预测目标,构造监督学习样本;
  4. 按时间顺序划分训练集(前80%)和测试集(后20%),避免随机打乱造成数据泄漏。
原始CSV date, temp 归一化 MinMaxScaler 滑窗切分 48步输入 → 7步输出 训练/测试划分 80% / 20% 时序划分 位置编码 sin/cos 函数 Transformer编码器 2层, 4头注意力 全连接输出层 7个预测值 反归一化 恢复真实温度

Transformer模型定义

模型采用编码器-解码器架构:编码器接收48个时间步的输入序列,通过多头自注意力捕捉时间依赖;解码器以编码器输出为条件,通过全连接层直接生成7个未来值。关键超参数设置如下:

  • 输入维度:1(单变量气温);
  • 模型维度 d_model=64,前馈网络维度 128;
  • 注意力头数 4,编码器层数 2;
  • Dropout=0.1,用于缓解过拟合。

完整代码实现


import numpy as np
import pandas as pd
import torch
import torch.nn as nn
import math
from torch.utils.data import Dataset, DataLoader
from sklearn.preprocessing import MinMaxScaler

# ---------- 1. 数据加载与预处理 ----------
df = pd.read_csv('beijing_temp_2023.csv')
temps = df['temp'].values.astype(np.float32).reshape(-1, 1)

scaler = MinMaxScaler(feature_range=(0, 1))
temps_scaled = scaler.fit_transform(temps)

# 滑窗参数
SEQ_LEN = 48      # 输入窗口
PRED_LEN = 7      # 预测长度

def create_sequences(data, seq_len, pred_len):
    X, y = [], []
    for i in range(len(data) - seq_len - pred_len + 1):
        X.append(data[i:i+seq_len, 0])
        y.append(data[i+seq_len:i+seq_len+pred_len, 0])
    return np.array(X), np.array(y)

X, y = create_sequences(temps_scaled, SEQ_LEN, PRED_LEN)

# 按时间顺序划分(不打乱)
split_idx = int(len(X) * 0.8)
X_train, X_test = X[:split_idx], X[split_idx:]
y_train, y_test = y[:split_idx], y[split_idx:]

# 转换为PyTorch张量
X_train = torch.FloatTensor(X_train).unsqueeze(-1)  # (batch, seq, 1)
y_train = torch.FloatTensor(y_train)
X_test = torch.FloatTensor(X_test).unsqueeze(-1)
y_test = torch.FloatTensor(y_test)

# ---------- 2. 位置编码 ----------
class PositionalEncoding(nn.Module):
    def __init__(self, d_model, max_len=5000):
        super().__init__()
        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        self.register_buffer('pe', pe.unsqueeze(0))

    def forward(self, x):
        return x + self.pe[:, :x.size(1), :]

# ---------- 3. Transformer模型 ----------
class TransformerForecaster(nn.Module):
    def __init__(self, d_model=64, nhead=4, num_layers=2, dropout=0.1):
        super().__init__()
        self.input_proj = nn.Linear(1, d_model)
        self.pos_encoder = PositionalEncoding(d_model)
        encoder_layer = nn.TransformerEncoderLayer(
            d_model=d_model, nhead=nhead, dim_feedforward=128,
            dropout=dropout, batch_first=True
        )
        self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
        self.decoder = nn.Sequential(
            nn.Linear(d_model, 32),
            nn.ReLU(),
            nn.Linear(32, PRED_LEN)
        )

    def forward(self, x):
        # x: (batch, seq_len, 1)
        x = self.input_proj(x)          # (batch, seq_len, d_model)
        x = self.pos_encoder(x)
        x = self.encoder(x)             # (batch, seq_len, d_model)
        x = x.mean(dim=1)               # 全局池化 -> (batch, d_model)
        out = self.decoder(x)           # (batch, PRED_LEN)
        return out

# ---------- 4. 训练配置 ----------
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = TransformerForecaster().to(device)
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)

batch_size = 32
train_dataset = torch.utils.data.TensorDataset(X_train, y_train)
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)

# ---------- 5. 训练循环 ----------
epochs = 50
for epoch in range(epochs):
    model.train()
    total_loss = 0.0
    for batch_X, batch_y in train_loader:
        batch_X, batch_y = batch_X.to(device), batch_y.to(device)
        optimizer.zero_grad()
        pred = model(batch_X)
        loss = criterion(pred, batch_y)
        loss.backward()
        optimizer.step()
        total_loss += loss.item() * batch_X.size(0)
    avg_loss = total_loss / len(train_dataset)
    if (epoch + 1) % 10 == 0:
        print(f'Epoch [{epoch+1}/{epochs}], Loss: {avg_loss:.6f}')

# ---------- 6. 测试与反归一化 ----------
model.eval()
with torch.no_grad():
    test_pred = model(X_test.to(device)).cpu().numpy()
    test_true = y_test.numpy()

# 反归一化恢复真实温度
pred_temp = scaler.inverse_transform(test_pred.reshape(-1, 1)).reshape(-1, PRED_LEN)
true_temp = scaler.inverse_transform(test_true.reshape(-1, 1)).reshape(-1, PRED_LEN)

# 保存预测结果
np.save('test_predictions.npy', pred_temp)
np.save('test_ground_truth.npy', true_temp)
print('预测结果已保存至 test_predictions.npy')

运行与验证

运行方式:将上述代码保存为transformer_forecast.py,与数据文件beijing_temp_2023.csv置于同一目录,执行python transformer_forecast.py。依赖库包括numpy、pandas、torch、scikit-learn。

验证方法:训练过程中每10个epoch打印一次MSE损失,损失应呈下降趋势。训练结束后,程序输出测试集预测结果文件。可通过以下方式验证模型效果:

  1. 检查test_predictions.npy与test_ground_truth.npy形状均为(样本数, 7);
  2. 计算测试集整体RMSE:rmse = np.sqrt(np.mean((pred_temp - true_temp)**2)),该值应明显小于直接使用历史均值预测的基准误差;
  3. 随机选取一个测试样本,绘制预测曲线与真实曲线对比,观察趋势是否吻合。

判定标准:训练损失收敛至0.01以下,且测试集RMSE低于3.5℃(以北京年温差约35℃为参照),即可认为模型已掌握基本的气温时序预测能力。