【时序数据预测】transformer对时序数据分析案例

给出一个完整、详细Transformer模型进行时序数据分析(时间序列预测)的案例,包括:

  1. 数据模拟(如无法导入真实数据,则自动创建示例数据)
  2. 数据清洗特征工程
  3. 时间窗口切分(滑动窗口)
  4. Transformer 模型构建(PyTorch)
  5. 训练预测
  6. 结果评估优化思路

力求在代码层面展示可运行的端到端流程。
你可以将本案例当作模板,并在真实项目中根据数据特征、预测目标和业务需求对其进行调整和扩展。


一、场景设定与目标

  • 场景:我们假设有一组温度传感器记录,记录了每天的平均温度,同时还包含一个外部特征(例如室外湿度)或者时间特征(如星期几)。
  • 目标:用过去 14 天(seq_len=14)的一些特征数据,预测第 15 天的温度。
  • 模型:使用 PyTorch 的 Transformer(Encoder部分)来做序列回归,输出单步预测。

注意:在真实落地中,可能需要做多步预测(一次性预测未来多天)或者多变量预测,这里先以单步预测为例,便于展示基础流程。


二、自动生成示例数据

我们先以 numpy/pandas 生成一个模拟时间序列,包含:

  1. temp:温度(目标值)
  2. humidity:湿度(辅助特征)
  3. day_of_week:星期几(0~6),可以视为离散时间特征

并制造一些缺失值或异常值,展示数据清洗思路。

import numpy as np
import pandas as pd
import torch
import random
import matplotlib.pyplot as plt

def create_fake_timeseries_data(num_days=500, seed=42):
    """
    生成模拟的温度传感器数据:
      - temp(目标) + humidity + day_of_week
      - 每天一条记录, 并随机制造部分缺失和异常值
    """
    np.random.seed(seed)
    random.seed(seed)
    
    dates = pd.date_range(start="2021-01-01", periods=num_days, freq='D')
    day_of_week = dates.dayofweek  # 0=Monday, 6=Sunday
    
    # 模拟温度: baseline=20, 带周期性和噪声
    temp = 20 + 5 * np.sin(np.arange(num_days) * 2*np.pi/365) + np.random.randn(num_days)*1.5
    
    # 模拟湿度: 在30~70%之间随机波动
    humidity = 50 + 20*np.random.randn(num_days)
    humidity = np.clip(humidity, 30, 70)
    
    df = pd.DataFrame({
        'date': dates,
        'temp': temp,
        'humidity': humidity,
        'day_of_week': day_of_week
    })
    df.set_index('date', inplace=True)
    
    # 制造缺失值 (NaN)
    for _ in range(5):
        idx = np.random.randint(0, num_days)
        df.iloc[idx, 0] = np.nan  # 在 temp 列制造缺失
    for _ in range(5):
        idx = np.random.randint(0, num_days)
        df.iloc[idx, 1] = np.nan  # 在 humidity 列制造缺失
    
    # 制造异常值
    anom_idx = np.random.randint(0, num_days)
    df.iloc[anom_idx, 0] = 200  # 温度异常极高
    return df

df = create_fake_timeseries_data(num_days=500)

print("Data sample:")
print(df.head(10))

三、数据清洗与特征工程

3.1 缺失值处理

  • 缺失值 NaN 可以用前向填充(ffill)、后向填充(bfill) 或 插值(interpolate) 等方式。
  • 对时间序列,前向/后向填充相对简单,插值有时更平滑。也可视具体业务策略处理。
# 查看缺失值情况
missing_count = df.isna().sum()
print("Missing Values:\n", missing_count)

# 用前向填充,再用后向填充补充
df = df.fillna(method='ffill').fillna(method='bfill')

3.2 异常值检测与处理

我们简单检测温度是否超出合理范围,若超过 [ -10, 50 ] 则视为异常,用邻近值替代。
(实际中可以使用统计方法、箱线图、3σ法、或更高级异常检测算法)

unreasonable_mask = (df['temp'] < -10) | (df['temp'] > 50)
if unreasonable_mask.any():
    print("Anomaly found, auto-correcting by neighbor values.")
    anomaly_indices = df[unreasonable_mask].index
    for idx in anomaly_indices:
        # 用前一天或后一天均值替换
        df.loc[idx, 'temp'] = (df.loc[idx - pd.Timedelta(days=1), 'temp'] + 
                               df.loc[idx + pd.Timedelta(days=1), 'temp']) / 2

3.3 特征工程

  1. 周期性时间编码day_of_week 是离散值(0~6),可用one-hotsin/cos编码。这里简单用 one-hot。
  2. 差分特征:假如我们想捕捉温度的变化率,可做 df['temp_diff'] = df['temp'].diff().
  3. 移动平均:例如 3 天平均温度 df['temp_ma3'] = df['temp'].rolling(3).mean()

出于示例,我们就做两点演示:

  1. 一天的差分
  2. day_of_week → one-hot
df['temp_diff'] = df['temp'].diff().fillna(0)  # 对首行差分补0
# one-hot
df_week = pd.get_dummies(df['day_of_week'], prefix='dow')
df = pd.concat([df, df_week], axis=1)

df.drop(columns=['day_of_week'], inplace=True)

df.head()

3.4 归一化

使用 MinMaxScaler 或 StandardScaler 都行,这里用 MinMaxScaler。

from sklearn.preprocessing import MinMaxScaler

feature_cols = ['temp','humidity','temp_diff'] + list(df_week.columns)  
# 目标: next-day temp,用当前天这些特征

scaler = MinMaxScaler()
df_scaled = scaler.fit_transform(df[feature_cols].values)
# df_scaled shape: (num_days, len(feature_cols))
print("Scaled shape:", df_scaled.shape)

四、时间窗口划分

Transformer 需要序列输入,可把过去 14 天作为一条训练样本(seq_len=14),去预测第 15 天的 temp(只在归一化后的列里获取temp那一列即可)。

4.1 自定义数据集

import torch
from torch.utils.data import Dataset, DataLoader

class TimeSeriesDataset(Dataset):
    def __init__(self, data_array, seq_len=14, target_col=0):
        """
        :param data_array: 已归一化的 2D numpy, shape=(num_samples, num_features)
        :param seq_len: 过去多少天作为输入
        :param target_col: temp 对应列索引(这里默认为0)
        """
        self.data = data_array
        self.seq_len = seq_len
        self.target_col = target_col

    def __len__(self):
        # 用最后一天预测 => (num - seq_len)
        return len(self.data) - self.seq_len

    def __getitem__(self, idx):
        x = self.data[idx : idx + self.seq_len, :]   # (seq_len, num_features)
        y = self.data[idx + self.seq_len, self.target_col]  # 标量
        return torch.tensor(x, dtype=torch.float32), torch.tensor(y, dtype=torch.float32)

SEQ_LEN = 14
TARGET_COL_IDX = 0  # temp在feature_cols中的索引=0
dataset = TimeSeriesDataset(df_scaled, seq_len=SEQ_LEN, target_col=TARGET_COL_IDX)

# 训练集/测试集划分: 比如前 400 天训练, 后面做测试
train_size = 400 - SEQ_LEN
train_dataset = TimeSeriesDataset(df_scaled[:400], seq_len=SEQ_LEN, target_col=TARGET_COL_IDX)
test_dataset  = TimeSeriesDataset(df_scaled[400:], seq_len=SEQ_LEN, target_col=TARGET_COL_IDX)

print("Train samples:", len(train_dataset))
print("Test  samples:", len(test_dataset))

train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
test_loader  = DataLoader(test_dataset,  batch_size=32, shuffle=False)

五、构建 Transformer 模型

PyTorch 中可以使用 nn.TransformerEncodernn.TransformerDecoder,或直接 nn.Transformer
对于时序回归的简单场景,常见做法:

  1. 对输入的 (batch, seq_len, feature_dim) 做线性Embedding
  2. 叠加Position Encoding(或者直接让 Transformer 学习一个可训练的 Positional Embedding);
  3. TransformerEncoder 编码出 (batch, seq_len, d_model);
  4. 取最后一个时间步或者做平均池化映射到回归输出。

下例是一个简化的 Transformer Encoder 结构,示例性展示可训练位置编码并对齐维度,最后得到单步预测。

import torch.nn as nn
import math

class TimeSeriesTransformer(nn.Module):
    def __init__(self, num_features, d_model=32, nhead=4, num_layers=2, dim_feedforward=64, dropout=0.1):
        """
        :param num_features: 输入特征数
        :param d_model: Transformer内部词向量维度
        :param nhead: Multi-head attention 数
        :param num_layers: TransformerEncoderLayer 堆叠层数
        :param dim_feedforward: 前馈层大小
        :param dropout: dropout 概率
        """
        super(TimeSeriesTransformer, self).__init__()
        
        self.d_model = d_model

        # 1) 线性 Embedding: 将 input_dim -> d_model
        self.input_embedding = nn.Linear(num_features, d_model)

        # 2) 可训练的位置编码 (也可用正余弦位置编码)
        self.pos_embedding = nn.Embedding(5000, d_model)  # 5000 仅作最大长度上限

        # 3) Transformer Encoder
        encoder_layer = nn.TransformerEncoderLayer(d_model=d_model, nhead=nhead,
                                                   dim_feedforward=dim_feedforward,
                                                   dropout=dropout, batch_first=True)
        self.transformer_encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)

        # 4) 最终回归输出
        self.fc_out = nn.Linear(d_model, 1)

    def forward(self, x):
        """
        :param x: shape (batch, seq_len, num_features)
        :return: (batch, 1)
        """
        batch_size, seq_len, _ = x.size()
        
        # 线性变换 -> (batch, seq_len, d_model)
        x_embed = self.input_embedding(x)
        
        # 构建位置id [0,1,2,... seq_len-1]
        positions = torch.arange(seq_len, device=x.device).unsqueeze(0).expand(batch_size, seq_len)
        # 位置编码 -> (batch, seq_len, d_model)
        pos_embed = self.pos_embedding(positions)
        
        # 将 x_embed + pos_embed 合并
        x_trans_in = x_embed + pos_embed  # (batch, seq_len, d_model)

        # 通过 TransformerEncoder
        encoded = self.transformer_encoder(x_trans_in)  # (batch, seq_len, d_model)

        # 取最后时刻的 hidden state
        last_step = encoded[:, -1, :]  # (batch, d_model)

        # 映射到回归输出
        out = self.fc_out(last_step)   # (batch, 1)

        return out.squeeze(-1)         # (batch,)

六、模型训练与预测

6.1 实例化模型、设置损失和优化器

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = TimeSeriesTransformer(
    num_features=len(feature_cols),
    d_model=32,
    nhead=4,
    num_layers=2,
    dim_feedforward=64,
    dropout=0.1
).to(device)

criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)

6.2 训练循环

num_epochs = 10
model.train()

for epoch in range(num_epochs):
    epoch_loss = 0.0
    for batch_x, batch_y in train_loader:
        batch_x = batch_x.to(device)  # (batch, seq_len, feature_dim)
        batch_y = batch_y.to(device)  # (batch,)

        optimizer.zero_grad()
        pred = model(batch_x)  # (batch,)
        loss = criterion(pred, batch_y)
        loss.backward()
        optimizer.step()
        
        epoch_loss += loss.item()
    
    avg_loss = epoch_loss / len(train_loader)
    if (epoch+1) % 2 == 0:
        print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {avg_loss:.4f}")

6.3 测试集预测与可视化

model.eval()
preds = []
targets = []
with torch.no_grad():
    for batch_x, batch_y in test_loader:
        batch_x = batch_x.to(device)
        pred = model(batch_x)  # (batch,)
        preds.append(pred.cpu().numpy())
        targets.append(batch_y.numpy())

import numpy as np
preds = np.concatenate(preds)
targets = np.concatenate(targets)

# 计算MSE, MAE
mse_test = np.mean((preds - targets)**2)
mae_test = np.mean(np.abs(preds - targets))
print(f"Test MSE: {mse_test:.4f}, Test MAE: {mae_test:.4f}")

因为是归一化后的值,可以反归一化来获得真实温度范围。

from sklearn.preprocessing import MinMaxScaler

def inverse_transform(values, scaler, feature_index=0):
    # values shape=(N,)
    # 构造 dummy array: (N, feature_dim)
    dummy = np.zeros((len(values), len(feature_cols)))
    dummy[:, feature_index] = values
    inv = scaler.inverse_transform(dummy)
    return inv[:, feature_index]

preds_real = inverse_transform(preds, scaler, feature_index=0)
targets_real = inverse_transform(targets, scaler, feature_index=0)

plt.figure(figsize=(8,4))
plt.plot(targets_real, label='True Temp')
plt.plot(preds_real, label='Pred Temp', alpha=0.7)
plt.title("Transformer Time Series Prediction (Test Set)")
plt.legend()
plt.show()

如果曲线基本重合或趋势接近,说明模型学到了部分规律;若差距较大,则可能需要继续优化。


七、模型优化与分析

  1. 更多训练轮数 & 调参

    • 可以尝试增加 num_epochs、改变 lr 或使用学习率衰减 (如 StepLR、ReduceLROnPlateau)。
    • 增加 d_modelnum_layers 可能提升表达能力,但也增加过拟合风险与计算量。
    • 调整 nhead(多头数量)和 dim_feedforward 也会影响性能。
  2. 正则化

    • 在 Transformer 中添加 dropout(已演示)或权重衰减(weight decay) 等。
    • 增加早停(EarlyStopping)机制,如果验证集损失长时间不下降就停止训练。
  3. 更多特征

    • 本例只用了湿度、diff、day_of_week等。
    • 可在实际中引入更多上下文(节假日、天气预报、室内外温差、设备运行状态等)。
  4. 多步预测

    • 若要一次预测未来多天,可以将输出层设为多个时间步,或采用更复杂的 Encoder-Decoder Transformer 结构(Seq2Seq 风格)。
  5. 位置编码

    • 这里用了可训练的位置嵌入,也可尝试正余弦位置编码来更好建模时间周期性。
  6. 错误分析

    • 观测哪些日期的预测误差最严重。
    • 残差 vs. 时间分布可揭示模型在特定时段(如周末)效果不佳的可能原因。

八、小结

通过本案例,你可以看到一个从数据清洗、特征工程Transformer 模型构建,再到训练与预测完整时序分析流程。要在实际项目中取得效果,通常需更多迭代和调优,包括:

  • 更丰富的外部特征
  • 合适的窗口长度与预测步数
  • 更深度的模型结构(多层 Encoder-Decoder)
  • 更多正则化和超参数搜索

然而,以上示例已经展示了Transformer 在时序预测中的基本思路,包括位置编码多头自注意力滑动窗口样本单步回归等关键环节。你可以据此在工业应用中进一步探索多步预测、在线更新、以及 Transformer 和其他网络(如 CNN、RNN、LSTM、Attention)相结合的更多方法。

哈佛博后带小白玩转机器学习哔哩哔哩_bilibili

总课时超400+,时长75+小时

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值