给出一个完整、详细的Transformer模型进行时序数据分析(时间序列预测)的案例,包括:
- 数据模拟(如无法导入真实数据,则自动创建示例数据)
- 数据清洗与特征工程
- 时间窗口切分(滑动窗口)
- Transformer 模型构建(PyTorch)
- 训练与预测
- 结果评估及优化思路
力求在代码层面展示可运行的端到端流程。
你可以将本案例当作模板,并在真实项目中根据数据特征、预测目标和业务需求对其进行调整和扩展。
一、场景设定与目标
- 场景:我们假设有一组温度传感器记录,记录了每天的平均温度,同时还包含一个外部特征(例如室外湿度)或者时间特征(如星期几)。
- 目标:用过去 14 天(seq_len=14)的一些特征数据,预测第 15 天的温度。
- 模型:使用 PyTorch 的 Transformer(Encoder部分)来做序列回归,输出单步预测。
注意:在真实落地中,可能需要做多步预测(一次性预测未来多天)或者多变量预测,这里先以单步预测为例,便于展示基础流程。
二、自动生成示例数据
我们先以 numpy
/pandas
生成一个模拟时间序列,包含:
- temp:温度(目标值)
- humidity:湿度(辅助特征)
- 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 特征工程
- 周期性时间编码:
day_of_week
是离散值(0~6),可用one-hot或sin/cos编码。这里简单用 one-hot。 - 差分特征:假如我们想捕捉温度的变化率,可做
df['temp_diff'] = df['temp'].diff()
. - 移动平均:例如 3 天平均温度
df['temp_ma3'] = df['temp'].rolling(3).mean()
出于示例,我们就做两点演示:
- 一天的差分
- 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.TransformerEncoder
、nn.TransformerDecoder
,或直接 nn.Transformer
。
对于时序回归的简单场景,常见做法:
- 对输入的 (batch, seq_len, feature_dim) 做线性Embedding;
- 叠加Position Encoding(或者直接让 Transformer 学习一个可训练的 Positional Embedding);
- 用
TransformerEncoder
编码出 (batch, seq_len, d_model); - 取最后一个时间步或者做平均池化映射到回归输出。
下例是一个简化的 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()
如果曲线基本重合或趋势接近,说明模型学到了部分规律;若差距较大,则可能需要继续优化。
七、模型优化与分析
-
更多训练轮数 & 调参
- 可以尝试增加
num_epochs
、改变lr
或使用学习率衰减 (如 StepLR、ReduceLROnPlateau)。 - 增加
d_model
、num_layers
可能提升表达能力,但也增加过拟合风险与计算量。 - 调整
nhead
(多头数量)和dim_feedforward
也会影响性能。
- 可以尝试增加
-
正则化
- 在 Transformer 中添加 dropout(已演示)或权重衰减(weight decay) 等。
- 增加早停(EarlyStopping)机制,如果验证集损失长时间不下降就停止训练。
-
更多特征
- 本例只用了湿度、diff、day_of_week等。
- 可在实际中引入更多上下文(节假日、天气预报、室内外温差、设备运行状态等)。
-
多步预测
- 若要一次预测未来多天,可以将输出层设为多个时间步,或采用更复杂的 Encoder-Decoder Transformer 结构(Seq2Seq 风格)。
-
位置编码
- 这里用了可训练的位置嵌入,也可尝试正余弦位置编码来更好建模时间周期性。
-
错误分析
- 观测哪些日期的预测误差最严重。
- 残差 vs. 时间分布可揭示模型在特定时段(如周末)效果不佳的可能原因。
八、小结
通过本案例,你可以看到一个从数据清洗、特征工程到Transformer 模型构建,再到训练与预测的完整时序分析流程。要在实际项目中取得效果,通常需更多迭代和调优,包括:
- 更丰富的外部特征
- 合适的窗口长度与预测步数
- 更深度的模型结构(多层 Encoder-Decoder)
- 更多正则化和超参数搜索
然而,以上示例已经展示了Transformer 在时序预测中的基本思路,包括位置编码、多头自注意力、滑动窗口样本、单步回归等关键环节。你可以据此在工业应用中进一步探索多步预测、在线更新、以及 Transformer 和其他网络(如 CNN、RNN、LSTM、Attention)相结合的更多方法。
【哈佛博后带小白玩转机器学习】 哔哩哔哩_bilibili
总课时超400+,时长75+小时