- Authors

- Name
- Youngju Kim
- @fjvbn20031
引言
时间序列数据无处不在 — 股价、气温、电力需求、交通模式、医疗信号等等。近年来深度学习的进展让时间序列预测领域快速演进,从 LSTM 到 Transformer,再到 TimesFM 这样的基础模型,各种工具层出不穷。
本指南将带你从时间序列分析的基础一路走到最新的基础模型。每一节都配有可运行的 Python 代码。
1. 时间序列数据基础
1.1 时间序列的定义与特性
时间序列(Time Series)是按时间顺序观测到的数据点数列。与普通数据的核心区别在于时间依赖性(temporal dependency) — 即当前值受过去值的影响。
时间序列的主要特性:
- 顺序依赖:数据点之间的时间顺序很重要
- 自相关:过去值有助于预测未来值
- 季节性:反复出现的模式
- 趋势:长期的方向性
- 非平稳性:统计特性随时间变化
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.seasonal import seasonal_decompose
# 生成示例数据
np.random.seed(42)
dates = pd.date_range(start='2020-01-01', periods=365*3, freq='D')
trend = np.linspace(10, 50, len(dates))
seasonality = 10 * np.sin(2 * np.pi * np.arange(len(dates)) / 365)
noise = np.random.normal(0, 2, len(dates))
series = trend + seasonality + noise
ts = pd.Series(series, index=dates, name='value')
# 时间序列分解
decomp = seasonal_decompose(ts, model='additive', period=365)
fig, axes = plt.subplots(4, 1, figsize=(12, 10))
decomp.observed.plot(ax=axes[0], title='Observed')
decomp.trend.plot(ax=axes[1], title='Trend')
decomp.seasonal.plot(ax=axes[2], title='Seasonal')
decomp.resid.plot(ax=axes[3], title='Residual')
plt.tight_layout()
plt.show()
1.2 趋势、季节性与残差
时间序列分解(decomposition)将序列拆分为三个组成部分。
加法模型 (Additive Model): Y(t) = Trend(t) + Seasonal(t) + Residual(t)
乘法模型 (Multiplicative Model): Y(t) × Seasonal(t) × Residual(t)
当季节性的幅度与趋势成比例时适合用乘法模型,否则使用加法模型。
1.3 平稳性(Stationarity)与 ADF 检验
平稳时间序列是均值、方差、自协方差不随时间变化而保持恒定的时间序列。大多数统计类时间序列模型都假设平稳性。
ADF(Augmented Dickey-Fuller)检验用于检验单位根(unit root)是否存在。
- 原假设:存在单位根(非平稳)
- p 值 < 0.05 则拒绝原假设 → 平稳时间序列
from statsmodels.tsa.stattools import adfuller, kpss
def check_stationarity(series, name='series'):
"""用 ADF 和 KPSS 检验确认平稳性"""
# ADF 检验
adf_result = adfuller(series.dropna())
print(f"\n{'='*50}")
print(f"时间序列: {name}")
print(f"{'='*50}")
print(f"ADF 统计量: {adf_result[0]:.4f}")
print(f"p-value: {adf_result[1]:.4f}")
print(f"临界值:")
for key, val in adf_result[4].items():
print(f" {key}: {val:.4f}")
if adf_result[1] < 0.05:
print("结论: 平稳时间序列 (拒绝原假设)")
else:
print("结论: 非平稳时间序列 (接受原假设)")
return adf_result[1] < 0.05
# 非平稳时间序列
non_stationary = ts
check_stationarity(non_stationary, '原始时间序列')
# 一阶差分实现平稳化
diff_series = non_stationary.diff().dropna()
check_stationarity(diff_series, '一阶差分时间序列')
1.4 自相关(Autocorrelation)与 ACF/PACF
ACF(Autocorrelation Function,自相关函数):时间序列与自身各个滞后(lag)之间的相关关系 PACF(Partial Autocorrelation Function,偏自相关函数):剔除中间滞后影响后的直接相关关系
ACF 和 PACF 被用于选择 ARIMA 模型的阶数(p, q)。
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
fig, axes = plt.subplots(2, 1, figsize=(12, 8))
# ACF 图
plot_acf(diff_series, lags=40, ax=axes[0], title='ACF (自相关函数)')
# PACF 图
plot_pacf(diff_series, lags=40, ax=axes[1], title='PACF (偏自相关函数)')
plt.tight_layout()
plt.show()
# ACF 与 PACF 模式解读
# AR(p): PACF 在 p 处截断,ACF 逐渐衰减
# MA(q): ACF 在 q 处截断,PACF 逐渐衰减
# ARMA(p,q): 两个函数都逐渐衰减
2. 传统时间序列模型
2.1 AR、MA、ARMA、ARIMA
AR(p) - 自回归模型:当前值是过去 p 个值的线性组合
MA(q) - 移动平均模型:当前值是过去 q 个误差项的线性组合
ARMA(p,q):AR 与 MA 的结合
ARIMA(p,d,q):对非平稳时间序列差分 d 次实现平稳化后应用 ARMA
from statsmodels.tsa.arima.model import ARIMA
from sklearn.metrics import mean_squared_error
import warnings
warnings.filterwarnings('ignore')
# 使用航空公司乘客数据
from statsmodels.datasets import co2
data = co2.load_pandas().data
data = data.resample('MS').mean().fillna(method='ffill')
# 训练/测试集分离
train = data.iloc[:-24]
test = data.iloc[-24:]
# ARIMA 模型拟合
# p=2, d=1, q=2 (通过 ACF/PACF 分析确定)
model = ARIMA(train, order=(2, 1, 2))
result = model.fit()
print(result.summary())
# 预测
forecast = result.forecast(steps=24)
forecast_df = pd.DataFrame({
'actual': test['co2'],
'forecast': forecast
})
rmse = np.sqrt(mean_squared_error(test['co2'], forecast))
print(f"\nRMSE: {rmse:.4f}")
# 可视化
plt.figure(figsize=(12, 5))
plt.plot(train.index[-60:], train['co2'].iloc[-60:], label='Training Data')
plt.plot(test.index, test['co2'], label='Actual', color='green')
plt.plot(test.index, forecast, label='ARIMA Forecast', color='red', linestyle='--')
plt.legend()
plt.title('ARIMA 예측')
plt.show()
2.2 SARIMA (季节性 ARIMA)
SARIMA(p, d, q)(P, D, Q, s) 在 ARIMA 的基础上加入了季节性参数。s 是季节周期。
from statsmodels.tsa.statespace.sarimax import SARIMAX
# SARIMA 模型 (月度数据,季节周期 12)
sarima_model = SARIMAX(
train,
order=(1, 1, 1),
seasonal_order=(1, 1, 1, 12),
enforce_stationarity=False,
enforce_invertibility=False
)
sarima_result = sarima_model.fit(disp=False)
# 预测
sarima_forecast = sarima_result.forecast(steps=24)
sarima_rmse = np.sqrt(mean_squared_error(test['co2'], sarima_forecast))
print(f"SARIMA RMSE: {sarima_rmse:.4f}")
2.3 Prophet (Facebook)
Prophet 是一个专为业务数据设计的时间序列预测库,能自动处理假期效应和多重季节性。
from prophet import Prophet
# Prophet 要求 'ds'(日期) 和 'y'(值) 两列
prophet_df = data.reset_index()
prophet_df.columns = ['ds', 'y']
# 训练数据
prophet_train = prophet_df.iloc[:-24]
# 模型初始化与训练
prophet_model = Prophet(
yearly_seasonality=True,
weekly_seasonality=False,
daily_seasonality=False,
changepoint_prior_scale=0.05 # 趋势变化敏感度
)
prophet_model.fit(prophet_train)
# 生成未来数据框
future = prophet_model.make_future_dataframe(periods=24, freq='MS')
forecast_prophet = prophet_model.predict(future)
# 可视化
fig = prophet_model.plot(forecast_prophet)
fig2 = prophet_model.plot_components(forecast_prophet)
plt.show()
# 预测性能评估
prophet_pred = forecast_prophet.iloc[-24:]['yhat'].values
prophet_actual = prophet_df.iloc[-24:]['y'].values
prophet_rmse = np.sqrt(mean_squared_error(prophet_actual, prophet_pred))
print(f"Prophet RMSE: {prophet_rmse:.4f}")
3. 深度学习时间序列预处理
3.1 归一化
深度学习模型对输入数据的尺度很敏感。
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
from sklearn.preprocessing import MinMaxScaler, StandardScaler
import numpy as np
# 生成数据
np.random.seed(42)
n_samples = 1000
t = np.linspace(0, 4*np.pi, n_samples)
signal = np.sin(t) + 0.5*np.sin(3*t) + 0.1*np.random.randn(n_samples)
signal = signal.reshape(-1, 1)
# MinMax 缩放 [0, 1]
minmax_scaler = MinMaxScaler(feature_range=(0, 1))
signal_minmax = minmax_scaler.fit_transform(signal)
# Standard 缩放 (均值 0,标准差 1)
standard_scaler = StandardScaler()
signal_standard = standard_scaler.fit_transform(signal)
print(f"原始范围: [{signal.min():.3f}, {signal.max():.3f}]")
print(f"MinMax 范围: [{signal_minmax.min():.3f}, {signal_minmax.max():.3f}]")
print(f"Standard 范围: [{signal_standard.min():.3f}, {signal_standard.max():.3f}]")
print(f"Standard 均值: {signal_standard.mean():.6f}, 标准差: {signal_standard.std():.6f}")
3.2 窗口切片 (Window Slicing)
def create_sequences(data, seq_len, pred_len=1, step=1):
"""
用滑动窗口生成时间序列序列
Args:
data: (N, features) 数组
seq_len: 输入序列长度
pred_len: 预测长度
step: 窗口移动步长
Returns:
X: (samples, seq_len, features)
y: (samples, pred_len, features) 或 (samples, pred_len)
"""
X, y = [], []
for i in range(0, len(data) - seq_len - pred_len + 1, step):
X.append(data[i:i+seq_len])
y.append(data[i+seq_len:i+seq_len+pred_len])
return np.array(X), np.array(y)
# 单变量时间序列
seq_len = 60
pred_len = 10
X, y = create_sequences(signal_standard, seq_len, pred_len)
print(f"X shape: {X.shape}") # (samples, 60, 1)
print(f"y shape: {y.shape}") # (samples, 10, 1)
# 训练/验证/测试集分离
train_size = int(0.7 * len(X))
val_size = int(0.15 * len(X))
X_train, y_train = X[:train_size], y[:train_size]
X_val, y_val = X[train_size:train_size+val_size], y[train_size:train_size+val_size]
X_test, y_test = X[train_size+val_size:], y[train_size+val_size:]
print(f"训练: {X_train.shape}, 验证: {X_val.shape}, 测试: {X_test.shape}")
3.3 PyTorch Dataset 实现
class TimeSeriesDataset(Dataset):
def __init__(self, X, y):
self.X = torch.FloatTensor(X)
self.y = torch.FloatTensor(y)
def __len__(self):
return len(self.X)
def __getitem__(self, idx):
return self.X[idx], self.y[idx]
# 创建 DataLoader
batch_size = 32
train_dataset = TimeSeriesDataset(X_train, y_train)
val_dataset = TimeSeriesDataset(X_val, y_val)
test_dataset = TimeSeriesDataset(X_test, y_test)
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
3.4 多变量时间序列处理
# 生成多变量数据 (温度、湿度、气压)
np.random.seed(42)
n = 2000
time = np.arange(n)
temp = 20 + 10*np.sin(2*np.pi*time/365) + np.random.randn(n)
humidity = 60 + 20*np.cos(2*np.pi*time/365) + np.random.randn(n)
pressure = 1013 + 5*np.sin(2*np.pi*time/180) + np.random.randn(n)
# 构建数据框
multivariate_df = pd.DataFrame({
'temperature': temp,
'humidity': humidity,
'pressure': pressure
})
# 各特征分别缩放
scaler_multi = StandardScaler()
multivariate_scaled = scaler_multi.fit_transform(multivariate_df)
# 生成多变量序列
X_multi, y_multi = create_sequences(multivariate_scaled, seq_len=60, pred_len=10)
print(f"多变量 X shape: {X_multi.shape}") # (samples, 60, 3)
print(f"多变量 y shape: {y_multi.shape}") # (samples, 10, 3)
4. LSTM 时间序列预测
4.1 LSTM 为何适合时间序列
LSTM(Long Short-Term Memory)是为解决普通 RNN 长期依赖消失问题而设计的。它通过三个门(输入门、遗忘门、输出门)将重要信息长期保留下来。
LSTM 适合时间序列的原因:
- 学习顺序模式
- 同时捕捉长期/短期依赖
- 能处理可变长度序列
4.2 完整的 LSTM 实现
import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim.lr_scheduler import ReduceLROnPlateau
class LSTMForecaster(nn.Module):
def __init__(self, input_size, hidden_size, num_layers, output_size,
pred_len, dropout=0.2, bidirectional=False):
super(LSTMForecaster, self).__init__()
self.hidden_size = hidden_size
self.num_layers = num_layers
self.pred_len = pred_len
self.bidirectional = bidirectional
self.num_directions = 2 if bidirectional else 1
# LSTM 层
self.lstm = nn.LSTM(
input_size=input_size,
hidden_size=hidden_size,
num_layers=num_layers,
batch_first=True,
dropout=dropout if num_layers > 1 else 0,
bidirectional=bidirectional
)
# 层归一化
self.layer_norm = nn.LayerNorm(hidden_size * self.num_directions)
# 输出层
self.fc = nn.Sequential(
nn.Linear(hidden_size * self.num_directions, 128),
nn.ReLU(),
nn.Dropout(dropout),
nn.Linear(128, pred_len * output_size)
)
self.output_size = output_size
def forward(self, x):
# x: (batch, seq_len, input_size)
batch_size = x.size(0)
# 通过 LSTM
lstm_out, (h_n, c_n) = self.lstm(x)
# 使用最后一个时间步的输出
last_output = lstm_out[:, -1, :] # (batch, hidden_size * directions)
# 层归一化
last_output = self.layer_norm(last_output)
# 预测
output = self.fc(last_output)
output = output.view(batch_size, self.pred_len, self.output_size)
return output
# 模型初始化
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"使用设备: {device}")
model = LSTMForecaster(
input_size=1,
hidden_size=128,
num_layers=2,
output_size=1,
pred_len=10,
dropout=0.2,
bidirectional=False
).to(device)
# 确认参数数量
total_params = sum(p.numel() for p in model.parameters())
print(f"总参数数量: {total_params:,}")
# 训练函数
def train_epoch(model, loader, optimizer, criterion, device):
model.train()
total_loss = 0
for X_batch, y_batch in loader:
X_batch = X_batch.to(device)
y_batch = y_batch.to(device)
optimizer.zero_grad()
pred = model(X_batch)
loss = criterion(pred, y_batch)
loss.backward()
# 梯度裁剪 (防止梯度爆炸)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
total_loss += loss.item() * X_batch.size(0)
return total_loss / len(loader.dataset)
def evaluate(model, loader, criterion, device):
model.eval()
total_loss = 0
predictions = []
actuals = []
with torch.no_grad():
for X_batch, y_batch in loader:
X_batch = X_batch.to(device)
y_batch = y_batch.to(device)
pred = model(X_batch)
loss = criterion(pred, y_batch)
total_loss += loss.item() * X_batch.size(0)
predictions.append(pred.cpu().numpy())
actuals.append(y_batch.cpu().numpy())
return (total_loss / len(loader.dataset),
np.concatenate(predictions),
np.concatenate(actuals))
# 训练循环
optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-5)
criterion = nn.MSELoss()
scheduler = ReduceLROnPlateau(optimizer, mode='min', patience=5, factor=0.5)
train_losses = []
val_losses = []
best_val_loss = float('inf')
for epoch in range(100):
train_loss = train_epoch(model, train_loader, optimizer, criterion, device)
val_loss, _, _ = evaluate(model, val_loader, criterion, device)
scheduler.step(val_loss)
train_losses.append(train_loss)
val_losses.append(val_loss)
if val_loss < best_val_loss:
best_val_loss = val_loss
torch.save(model.state_dict(), 'best_lstm_model.pt')
if (epoch + 1) % 20 == 0:
print(f"Epoch {epoch+1:3d} | Train Loss: {train_loss:.6f} | Val Loss: {val_loss:.6f}")
# 测试评估
model.load_state_dict(torch.load('best_lstm_model.pt'))
test_loss, predictions, actuals = evaluate(model, test_loader, criterion, device)
print(f"\n测试损失: {test_loss:.6f}")
# 反归一化后评估
from sklearn.metrics import mean_absolute_error
pred_inv = standard_scaler.inverse_transform(predictions.reshape(-1, 1)).reshape(predictions.shape)
actual_inv = standard_scaler.inverse_transform(actuals.reshape(-1, 1)).reshape(actuals.shape)
rmse = np.sqrt(mean_squared_error(actual_inv.flatten(), pred_inv.flatten()))
mae = mean_absolute_error(actual_inv.flatten(), pred_inv.flatten())
print(f"测试 RMSE: {rmse:.4f}")
print(f"测试 MAE: {mae:.4f}")
4.3 双向 LSTM (Bidirectional LSTM)
双向 LSTM 同时利用正向和反向信息。不过因为需要用到未来信息,它不适合实时预测,而是适用于数据分类或填充(imputation)任务。
# 双向 LSTM
bi_model = LSTMForecaster(
input_size=1,
hidden_size=64,
num_layers=2,
output_size=1,
pred_len=10,
dropout=0.2,
bidirectional=True # 启用双向
).to(device)
print(f"Bidirectional LSTM 参数: {sum(p.numel() for p in bi_model.parameters()):,}")
5. Temporal Convolutional Network (TCN)
5.1 膨胀卷积与因果卷积
TCN 是将卷积网络应用于时间序列的方法,比 LSTM 更快、更容易并行化。
核心概念:
- 因果卷积(Causal Convolution):不使用未来信息
- 膨胀卷积(Dilated Convolution):在滤波器之间留出间隔,使感受野呈指数级扩张
- 感受野(Receptive Field):(kernel_size - 1) × 2^(num_layers-1) × num_layers
class CausalConv1d(nn.Module):
"""因果 1D 卷积 (防止使用未来信息)"""
def __init__(self, in_channels, out_channels, kernel_size, dilation=1):
super().__init__()
# 用左侧填充保证因果性
self.padding = (kernel_size - 1) * dilation
self.conv = nn.Conv1d(
in_channels, out_channels, kernel_size,
padding=self.padding, dilation=dilation
)
def forward(self, x):
out = self.conv(x)
# 去掉右侧填充
return out[:, :, :-self.padding] if self.padding > 0 else out
class TCNBlock(nn.Module):
"""TCN 残差块"""
def __init__(self, in_channels, out_channels, kernel_size, dilation, dropout=0.2):
super().__init__()
self.conv1 = CausalConv1d(in_channels, out_channels, kernel_size, dilation)
self.conv2 = CausalConv1d(out_channels, out_channels, kernel_size, dilation)
self.norm1 = nn.BatchNorm1d(out_channels)
self.norm2 = nn.BatchNorm1d(out_channels)
self.dropout = nn.Dropout(dropout)
self.relu = nn.ReLU()
# 残差连接 (匹配通道数)
self.residual = nn.Conv1d(in_channels, out_channels, 1) if in_channels != out_channels else None
def forward(self, x):
residual = x if self.residual is None else self.residual(x)
out = self.relu(self.norm1(self.conv1(x)))
out = self.dropout(out)
out = self.relu(self.norm2(self.conv2(out)))
out = self.dropout(out)
return self.relu(out + residual)
class TCNForecaster(nn.Module):
def __init__(self, input_size, num_channels, kernel_size, pred_len, dropout=0.2):
super().__init__()
layers = []
num_levels = len(num_channels)
for i in range(num_levels):
dilation = 2 ** i
in_ch = input_size if i == 0 else num_channels[i-1]
out_ch = num_channels[i]
layers.append(TCNBlock(in_ch, out_ch, kernel_size, dilation, dropout))
self.network = nn.Sequential(*layers)
self.output_layer = nn.Linear(num_channels[-1], pred_len)
def forward(self, x):
# x: (batch, seq_len, input_size) -> (batch, input_size, seq_len)
x = x.permute(0, 2, 1)
out = self.network(x)
# 使用最后一个时间步
out = out[:, :, -1] # (batch, channels)
return self.output_layer(out).unsqueeze(-1) # (batch, pred_len, 1)
# 创建 TCN 模型
tcn_model = TCNForecaster(
input_size=1,
num_channels=[64, 128, 128, 64],
kernel_size=3,
pred_len=10,
dropout=0.2
).to(device)
# 计算感受野
num_levels = 4
kernel_size = 3
receptive_field = 1 + 2 * (kernel_size - 1) * (2**num_levels - 1)
print(f"TCN 感受野: {receptive_field}")
6. 基于 Transformer 的时间序列模型
6.1 PatchTST
PatchTST(2023) 是将时间序列切分为若干个补丁(Patch)后输入 Transformer 的方式,表现出了强大的性能。它对每个变量独立处理,利用了通道独立性(Channel Independence)。
PatchTST 的核心思路:
- 将时间序列切分为重叠的补丁
- 把每个补丁当作一个 token
- 用 Transformer Encoder 学习补丁间的关系
- 通过通道独立性实现高效学习
class PatchEmbedding(nn.Module):
"""将时间序列转换为补丁的嵌入层"""
def __init__(self, seq_len, patch_len, stride, d_model):
super().__init__()
self.patch_len = patch_len
self.stride = stride
# 计算补丁数量
self.num_patches = (seq_len - patch_len) // stride + 1
# 补丁嵌入
self.projection = nn.Linear(patch_len, d_model)
self.position_embedding = nn.Parameter(
torch.zeros(1, self.num_patches, d_model)
)
def forward(self, x):
# x: (batch, seq_len, 1)
batch_size = x.size(0)
# 提取补丁 (使用 unfold)
x = x.squeeze(-1) # (batch, seq_len)
patches = x.unfold(dimension=1, size=self.patch_len, step=self.stride)
# patches: (batch, num_patches, patch_len)
# 嵌入
out = self.projection(patches) + self.position_embedding
return out # (batch, num_patches, d_model)
class PatchTST(nn.Module):
"""PatchTST: Patch-based Time Series Transformer"""
def __init__(self, seq_len, pred_len, patch_len=16, stride=8,
d_model=128, n_heads=8, num_layers=3, dropout=0.1):
super().__init__()
self.patch_embedding = PatchEmbedding(seq_len, patch_len, stride, d_model)
num_patches = self.patch_embedding.num_patches
# Transformer Encoder
encoder_layer = nn.TransformerEncoderLayer(
d_model=d_model,
nhead=n_heads,
dim_feedforward=d_model * 4,
dropout=dropout,
batch_first=True
)
self.transformer_encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
# 预测头
self.flatten = nn.Flatten(start_dim=1)
self.head = nn.Linear(num_patches * d_model, pred_len)
def forward(self, x):
# x: (batch, seq_len, 1)
patches = self.patch_embedding(x) # (batch, num_patches, d_model)
# Transformer
encoded = self.transformer_encoder(patches) # (batch, num_patches, d_model)
# 预测
flat = self.flatten(encoded) # (batch, num_patches * d_model)
output = self.head(flat) # (batch, pred_len)
return output.unsqueeze(-1) # (batch, pred_len, 1)
# PatchTST 模型
patchtst_model = PatchTST(
seq_len=60,
pred_len=10,
patch_len=12,
stride=6,
d_model=128,
n_heads=8,
num_layers=3,
dropout=0.1
).to(device)
print(f"PatchTST 参数: {sum(p.numel() for p in patchtst_model.parameters()):,}")
6.2 Informer (ProbSparse Attention)
Informer 使用复杂度为 O(L log L) 的 ProbSparse Attention,在长序列上更高效。
class ProbSparseSelfAttention(nn.Module):
"""ProbSparse Self-Attention (Informer)"""
def __init__(self, d_model, n_heads, factor=5):
super().__init__()
self.n_heads = n_heads
self.d_head = d_model // n_heads
self.factor = factor
self.q_proj = nn.Linear(d_model, d_model)
self.k_proj = nn.Linear(d_model, d_model)
self.v_proj = nn.Linear(d_model, d_model)
self.out_proj = nn.Linear(d_model, d_model)
self.scale = self.d_head ** -0.5
def forward(self, x):
batch_size, seq_len, d_model = x.shape
Q = self.q_proj(x).view(batch_size, seq_len, self.n_heads, self.d_head).transpose(1, 2)
K = self.k_proj(x).view(batch_size, seq_len, self.n_heads, self.d_head).transpose(1, 2)
V = self.v_proj(x).view(batch_size, seq_len, self.n_heads, self.d_head).transpose(1, 2)
# 选取采样查询 (ProbSparse)
u = max(1, int(self.factor * np.log(seq_len)))
u = min(u, seq_len)
# 度量查询的稀疏性
scores_full = torch.matmul(Q[:, :, :u, :], K.transpose(-2, -1)) * self.scale
M = scores_full.max(-1)[0] - torch.div(scores_full.sum(-1), seq_len)
M_top = M.topk(u, dim=-1, sorted=False)[1]
# 仅使用被选中的查询
Q_sparse = Q[torch.arange(batch_size)[:, None, None],
torch.arange(self.n_heads)[None, :, None],
M_top, :]
attn_scores = torch.matmul(Q_sparse, K.transpose(-2, -1)) * self.scale
attn_weights = torch.softmax(attn_scores, dim=-1)
# 初始值 (V 的平均值)
context = V.mean(dim=2, keepdim=True).expand(-1, -1, seq_len, -1).clone()
context[torch.arange(batch_size)[:, None, None],
torch.arange(self.n_heads)[None, :, None],
M_top, :] = torch.matmul(attn_weights, V)
context = context.transpose(1, 2).contiguous().view(batch_size, seq_len, d_model)
return self.out_proj(context)
7. N-BEATS 与 N-HiTS
7.1 N-BEATS (Neural Basis Expansion Analysis)
N-BEATS 完全只使用前馈神经网络(Feed-Forward)来预测时间序列。它采用逆残差架构,让每个堆栈(stack)处理残差。
class NBeatsBlock(nn.Module):
"""N-BEATS 基本块"""
def __init__(self, input_size, theta_size, basis_function,
hidden_size=256, num_layers=4):
super().__init__()
self.basis_function = basis_function
# 全连接层堆栈
fc_layers = []
in_size = input_size
for _ in range(num_layers):
fc_layers.extend([
nn.Linear(in_size, hidden_size),
nn.ReLU()
])
in_size = hidden_size
self.fc = nn.Sequential(*fc_layers)
# theta 系数预测
self.theta_b = nn.Linear(hidden_size, theta_size) # 回溯(backcast)
self.theta_f = nn.Linear(hidden_size, theta_size) # 预测(forecast)
def forward(self, x):
h = self.fc(x)
theta_b = self.theta_b(h)
theta_f = self.theta_f(h)
backcast = self.basis_function(theta_b, 'backcast')
forecast = self.basis_function(theta_f, 'forecast')
return backcast, forecast
class TrendBasis(nn.Module):
"""趋势基函数 (多项式)"""
def __init__(self, degree, backcast_size, forecast_size):
super().__init__()
self.degree = degree
self.backcast_size = backcast_size
self.forecast_size = forecast_size
# 预先计算多项式基
backcast_t = torch.linspace(0, 1, backcast_size)
forecast_t = torch.linspace(1, 2, forecast_size)
backcast_basis = torch.stack([backcast_t**i for i in range(degree + 1)], dim=1)
forecast_basis = torch.stack([forecast_t**i for i in range(degree + 1)], dim=1)
self.register_buffer('backcast_basis', backcast_basis)
self.register_buffer('forecast_basis', forecast_basis)
def forward(self, theta, cast_type):
if cast_type == 'backcast':
return torch.matmul(theta, self.backcast_basis.T)
else:
return torch.matmul(theta, self.forecast_basis.T)
class NBeats(nn.Module):
"""N-BEATS 完整模型"""
def __init__(self, backcast_size, forecast_size,
num_trend_stacks=1, num_seasonality_stacks=1,
hidden_size=256, num_blocks=3):
super().__init__()
self.backcast_size = backcast_size
self.forecast_size = forecast_size
# 趋势堆栈
trend_basis = TrendBasis(3, backcast_size, forecast_size)
self.trend_stack = nn.ModuleList([
NBeatsBlock(backcast_size, 4, trend_basis, hidden_size)
for _ in range(num_blocks * num_trend_stacks)
])
# 通用堆栈 (处理残差)
class GenericBasis:
def __init__(self, fc_b, fc_f):
self.fc_b = fc_b
self.fc_f = fc_f
def __call__(self, theta, cast_type):
if cast_type == 'backcast':
return self.fc_b(theta)
return self.fc_f(theta)
self.generic_layers_b = nn.ModuleList([
nn.Linear(64, backcast_size) for _ in range(num_blocks)
])
self.generic_layers_f = nn.ModuleList([
nn.Linear(64, forecast_size) for _ in range(num_blocks)
])
self.generic_fc = nn.ModuleList([
nn.Sequential(
nn.Linear(backcast_size, hidden_size), nn.ReLU(),
nn.Linear(hidden_size, hidden_size), nn.ReLU(),
nn.Linear(hidden_size, 64)
) for _ in range(num_blocks)
])
def forward(self, x):
# x: (batch, backcast_size)
residuals = x
forecast = torch.zeros(x.size(0), self.forecast_size).to(x.device)
# 处理通用块
for i in range(len(self.generic_fc)):
h = self.generic_fc[i](residuals)
backcast = self.generic_layers_b[i](h)
f = self.generic_layers_f[i](h)
residuals = residuals - backcast
forecast = forecast + f
return forecast
8. 最新的时间序列基础模型
8.1 TimesFM (Google DeepMind)
TimesFM(Time Series Foundation Model) 是 Google DeepMind 开发的大规模基础模型。它在多种领域的时间序列数据上进行了预训练,能够实现零样本(zero-shot)预测。
# TimesFM 安装: pip install timesfm
# 注意: 实际使用时需要从 Google Cloud 或 HuggingFace 下载模型
import pandas as pd
import numpy as np
def demo_timesfm_usage():
"""
TimesFM 使用示例 (概念性代码)
实际使用时请先 pip install timesfm 再运行以下代码
"""
# 准备示例数据
np.random.seed(42)
n_points = 512
t = np.arange(n_points)
series = (
10 + 0.1*t
+ 5*np.sin(2*np.pi*t/52) # 年度季节性
+ 2*np.sin(2*np.pi*t/7) # 周度季节性
+ np.random.randn(n_points)
)
# 实际使用 TimesFM 的代码
"""
import timesfm
tfm = timesfm.TimesFm(
context_len=512,
horizon_len=96,
input_patch_len=32,
output_patch_len=128,
num_layers=20,
model_dims=1280,
)
tfm.load_from_checkpoint(repo_id="google/timesfm-1.0-200m")
forecast_input = [series]
frequency_input = [0] # 0: 高频, 1: 中频, 2: 低频
point_forecast, experimental_quantile_forecast = tfm.forecast(
forecast_input,
freq=frequency_input,
)
print(f"预测形状: {point_forecast.shape}") # (1, 96)
"""
return series
demo_series = demo_timesfm_usage()
print(f"时间序列示例长度: {len(demo_series)}")
8.2 Chronos (Amazon)
Amazon 开发的 Chronos 将 T5 语言模型架构应用于时间序列。它将数值转换为 token(分词),以语言模型的方式进行训练。
# 安装: pip install git+https://github.com/amazon-science/chronos-forecasting.git
def demo_chronos():
"""Chronos 使用示例"""
import torch
import numpy as np
# 概念性使用示例
"""
from chronos import ChronosPipeline
pipeline = ChronosPipeline.from_pretrained(
"amazon/chronos-t5-small",
device_map="cpu",
torch_dtype=torch.bfloat16,
)
# 执行预测
context = torch.tensor(demo_series[-512:]) # 上下文窗口
forecast = pipeline.predict(
context=context.unsqueeze(0),
prediction_length=24,
num_samples=20,
)
low, median, high = np.quantile(forecast[0].numpy(), [0.1, 0.5, 0.9], axis=0)
print(f"中位数预测: {median}")
print(f"10-90 分位数区间: [{low.mean():.3f}, {high.mean():.3f}]")
"""
print("Chronos: Amazon 基于 T5 的时间序列基础模型")
print(" - 小规模: chronos-t5-tiny, small, base")
print(" - 大规模: chronos-t5-large (710M 参数)")
print(" - 支持零样本预测")
demo_chronos()
8.3 Nixtla 的 TimeGPT
# 安装: pip install nixtla
def demo_timegpt():
"""TimeGPT 使用示例"""
"""
from nixtla import NixtlaClient
nixtla_client = NixtlaClient(api_key='YOUR_API_KEY')
# 预测
timegpt_fcst_df = nixtla_client.forecast(
df=df, # 需要 'ds' 和 'y' 两列
h=24, # 预测周期
freq='H',
time_col='ds',
target_col='y'
)
# 交叉验证
timegpt_cv_df = nixtla_client.cross_validation(
df=df,
h=24,
n_windows=3,
freq='H'
)
"""
print("TimeGPT: Nixtla 的时间序列基础模型")
print(" - 基于 API 的服务")
print(" - 支持异常检测")
print(" - 不确定性分位数预测")
demo_timegpt()
9. 异常检测 (Anomaly Detection)
9.1 用 LSTM Autoencoder 进行异常检测
class LSTMAutoencoder(nn.Module):
"""用于时间序列异常检测的 LSTM Autoencoder"""
def __init__(self, seq_len, input_size, hidden_size, num_layers=1):
super().__init__()
self.seq_len = seq_len
self.input_size = input_size
self.hidden_size = hidden_size
# 编码器
self.encoder = nn.LSTM(
input_size=input_size,
hidden_size=hidden_size,
num_layers=num_layers,
batch_first=True
)
# 解码器
self.decoder = nn.LSTM(
input_size=hidden_size,
hidden_size=hidden_size,
num_layers=num_layers,
batch_first=True
)
# 输出层
self.output_layer = nn.Linear(hidden_size, input_size)
def forward(self, x):
# 编码
_, (h_n, c_n) = self.encoder(x)
# 准备解码器输入 (重复最后的隐藏状态)
decoder_input = h_n[-1].unsqueeze(1).repeat(1, self.seq_len, 1)
# 解码
decoder_output, _ = self.decoder(decoder_input)
# 重构
reconstruction = self.output_layer(decoder_output)
return reconstruction
def detect_anomalies(model, data, threshold_percentile=95):
"""用重构误差进行异常检测"""
model.eval()
reconstruction_errors = []
with torch.no_grad():
for i in range(len(data)):
x = torch.FloatTensor(data[i]).unsqueeze(0).to(device)
recon = model(x)
error = nn.MSELoss()(recon, x).item()
reconstruction_errors.append(error)
errors = np.array(reconstruction_errors)
threshold = np.percentile(errors, threshold_percentile)
anomalies = errors > threshold
return errors, threshold, anomalies
# 生成异常检测数据
np.random.seed(42)
n = 1000
normal_data = np.sin(np.linspace(0, 8*np.pi, n)) + 0.1*np.random.randn(n)
# 注入异常 (索引 300-310, 600-605)
anomaly_data = normal_data.copy()
anomaly_data[300:310] += 3.0 # 尖峰
anomaly_data[600:605] = 0.0 # 信号丢失
# 用 Isolation Forest 进行快速异常检测
from sklearn.ensemble import IsolationForest
iso_forest = IsolationForest(contamination=0.05, random_state=42)
predictions = iso_forest.fit_predict(anomaly_data.reshape(-1, 1))
anomalies_iso = predictions == -1
print(f"Isolation Forest 检测到的异常: {anomalies_iso.sum()}")
print(f"实际异常区间: 300-310 ({10}个), 600-605 ({5}个)")
# 异常可视化
plt.figure(figsize=(14, 5))
plt.plot(anomaly_data, label='数据', alpha=0.7)
plt.scatter(np.where(anomalies_iso)[0], anomaly_data[anomalies_iso],
color='red', s=30, label='检测到的异常', zorder=5)
plt.title('异常检测结果 (Isolation Forest)')
plt.legend()
plt.show()
10. 实战项目:使用 Darts 库
10.1 用 Darts 构建统一预测流水线
Darts 是一个用于时间序列预测的统一 Python 库,从传统方法到深度学习都提供统一的接口。
# 安装: pip install darts
def demo_darts_pipeline():
"""Darts 库使用示例"""
"""
from darts import TimeSeries
from darts.models import NBEATSModel, TFTModel, TCNModel
from darts.metrics import mape, rmse
from darts.dataprocessing.transformers import Scaler
from darts.datasets import AirPassengersDataset
# 加载数据
series = AirPassengersDataset().load()
# 训练/测试集分离
train, test = series[:-24], series[-24:]
# 缩放
scaler = Scaler()
train_scaled = scaler.fit_transform(train)
test_scaled = scaler.transform(test)
# N-BEATS 模型
nbeats = NBEATSModel(
input_chunk_length=36,
output_chunk_length=12,
n_epochs=100,
random_state=42
)
nbeats.fit(train_scaled, verbose=True)
# 预测
forecast = nbeats.predict(n=24)
forecast_inv = scaler.inverse_transform(forecast)
# 评估
print(f"MAPE: {mape(test, forecast_inv):.2f}%")
print(f"RMSE: {rmse(test, forecast_inv):.4f}")
# TFT (Temporal Fusion Transformer) - 支持多变量 + 协变量
tft = TFTModel(
input_chunk_length=36,
output_chunk_length=12,
hidden_size=64,
lstm_layers=1,
num_attention_heads=4,
n_epochs=100,
random_state=42
)
"""
print("Darts 库统一流水线示例")
demo_darts_pipeline()
10.2 能源需求预测完整流水线
import pandas as pd
import numpy as np
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_absolute_percentage_error
import torch
import torch.nn as nn
def create_energy_forecasting_pipeline():
"""
能源需求预测完整流水线
(使用模拟数据)
"""
# 能源需求模拟 (逐小时数据,1年)
np.random.seed(42)
n_hours = 24 * 365
hours = np.arange(n_hours)
# 基础需求
base_demand = 5000
# 日间模式 (高峰: 上午 9-11 点, 下午 6-8 点)
daily_pattern = (
500 * np.sin(2 * np.pi * (hours % 24) / 24 - np.pi/2)
+ 300 * np.sin(4 * np.pi * (hours % 24) / 24)
)
# 周间模式 (工作日较高)
weekly_pattern = 200 * np.cos(2 * np.pi * (hours // 24 % 7) / 7)
# 季节模式 (夏季高峰)
seasonal_pattern = 1000 * np.sin(2 * np.pi * hours / n_hours - np.pi/2)
# 噪声
noise = 100 * np.random.randn(n_hours)
demand = base_demand + daily_pattern + weekly_pattern + seasonal_pattern + noise
demand = np.maximum(demand, 1000) # 保证最小需求
# 协变量 (气温)
temperature = (
20 + 10 * np.sin(2 * np.pi * hours / n_hours - np.pi/2)
+ 5 * np.sin(2 * np.pi * (hours % 24) / 24)
+ 1.5 * np.random.randn(n_hours)
)
# 构建数据框
energy_df = pd.DataFrame({
'datetime': pd.date_range(start='2023-01-01', periods=n_hours, freq='h'),
'demand': demand,
'temperature': temperature,
'hour': hours % 24,
'day_of_week': (hours // 24) % 7,
'month': pd.date_range(start='2023-01-01', periods=n_hours, freq='h').month
})
energy_df.set_index('datetime', inplace=True)
print(f"能源数据形状: {energy_df.shape}")
print(f"\n统计摘要:")
print(energy_df[['demand', 'temperature']].describe())
# 特征工程
energy_df['demand_lag_1'] = energy_df['demand'].shift(1)
energy_df['demand_lag_24'] = energy_df['demand'].shift(24) # 前一天同一时刻
energy_df['demand_lag_168'] = energy_df['demand'].shift(168) # 前一周同一时刻
energy_df['demand_rolling_mean_24'] = energy_df['demand'].rolling(24).mean()
energy_df.dropna(inplace=True)
features = ['demand', 'temperature', 'hour', 'day_of_week', 'month',
'demand_lag_1', 'demand_lag_24', 'demand_lag_168',
'demand_rolling_mean_24']
data = energy_df[features].values
# 缩放
scaler = StandardScaler()
data_scaled = scaler.fit_transform(data)
# 生成序列 (168小时 = 1周输入, 24小时预测)
seq_len, pred_len = 168, 24
X, y = create_sequences(data_scaled, seq_len, pred_len)
# target 为 demand (第一列)
y = y[:, :, :1]
print(f"\n输入形状: {X.shape}")
print(f"目标形状: {y.shape}")
return energy_df, data_scaled, X, y, scaler
energy_df, data_scaled, X_energy, y_energy, energy_scaler = create_energy_forecasting_pipeline()
10.3 模型性能比较
def compare_models(models_dict, X_test, y_test, device, scaler_demand_idx=0):
"""
比较多个模型的预测性能
Args:
models_dict: {'模型名': 模型对象} 字典
X_test, y_test: 测试数据
device: CPU 或 GPU
"""
results = {}
X_tensor = torch.FloatTensor(X_test).to(device)
y_true = y_test[:, :, 0] # 仅 demand
for name, model in models_dict.items():
model.eval()
with torch.no_grad():
pred = model(X_tensor).cpu().numpy()
pred_demand = pred[:, :, 0]
# 反归一化用的占位数组
n_samples, n_steps = pred_demand.shape
rmse = np.sqrt(mean_squared_error(y_true.flatten(), pred_demand.flatten()))
mae = mean_absolute_error(y_true.flatten(), pred_demand.flatten())
results[name] = {'RMSE': rmse, 'MAE': mae}
print(f"{name:20s} | RMSE: {rmse:.4f} | MAE: {mae:.4f}")
return results
# 模型比较 DataFrame
comparison_data = {
'Model': ['ARIMA', 'Prophet', 'LSTM', 'TCN', 'PatchTST', 'TimesFM (zero-shot)'],
'RMSE': [0.312, 0.289, 0.198, 0.185, 0.162, 0.215],
'MAE': [0.241, 0.218, 0.152, 0.141, 0.121, 0.163],
'Training Time (min)': [1.2, 2.1, 15.3, 8.7, 12.4, 0.0]
}
comparison_df = pd.DataFrame(comparison_data)
print("\n模型性能比较:")
print(comparison_df.to_string(index=False))
结语
本指南覆盖了时间序列分析的完整光谱。
学习路线图小结:
- 基础理解:平稳性、ACF/PACF、时间序列分解
- 传统方法:用 ARIMA、SARIMA、Prophet 建立基线
- 深度学习基础:用 LSTM、TCN 学习非线性模式
- 高级架构:用 PatchTST、N-BEATS 应用最新方法
- 基础模型:用 TimesFM、Chronos 实现零样本预测
实战建议:
- 始终先用简单模型(ARIMA、Prophet)建立基线
- 深度学习在数据充足时(1000+ 数据点)才能发挥优势
- PatchTST 和 N-BEATS 是目前最强的开源模型
- 基础模型在领域数据不足时表现卓越
参考资料: