大数跨境

导师冷笑:“分钟级波动、天级周期、月级趋势全混着来,你凭什么一把梭?”我:“通过Claude Code写的代码,让我第一次看懂多尺度建模”

导师冷笑:“分钟级波动、天级周期、月级趋势全混着来,你凭什么一把梭?”我:“通过Claude Code写的代码,让我第一次看懂多尺度建模” 机器学习和人工智能AI
2026-08-29
1

哈喽,大家好~

时间序列的业务中,真正难的地方不在于“它会变”,而在于它是同时按很多种节奏在变

比如有些变化是分钟级的抖动,有些变化是天级的周期,有些变化是月级的趋势,甚至还会夹杂突发异常。你如果只用一种尺度去看,就很容易把问题看扁了:短期噪声会淹没长期趋势,长期平滑又会吃掉关键的局部变化。所以,多尺度建模的核心价值,就是一句话:

把同一条时间序列,拆成不同时间尺度下的结构,再把这些结构重新组织起来分析。

这件事为什么重要?

因为现实业务里的时间序列,几乎都不是“单频率、单规律、单噪声”的理想数据。工业监控要同时看设备瞬时波动和长期老化,金融风控要同时看高频跳动和中长期趋势,能源预测要同时看日周期、周周期、季节性和节假日扰动。

不从多尺度入手,模型经常就是“平均看起来还行,但关键点全错”。

多尺度建模到底在干什么

比如一首歌中,这首歌里有鼓点、有主旋律、有和声、还有偶尔的杂音。你如果把整首歌只压成一个频段去听,那很多信息就丢了。鼓点体现短期节奏,主旋律体现中期结构,和声体现更长期的层次,杂音则是异常和噪声。

时间序列也是一样。

一条复杂时间序列里,通常会同时包含这几类东西:

  • 趋势(Trend):长期往上还是往下
  • 季节性/周期性(Seasonality):比如每天、每周、每月重复出现的模式
  • 局部波动(Local fluctuation):短时间内的小起伏
  • 异常冲击(Shock / Anomaly):突然暴涨、暴跌、故障尖峰
  • 噪声(Noise):没有明确规律的随机扰动

多尺度建模做的事情,就是把这些不同节奏的变化分开看。

比如:

  • 用短窗口看局部变化
  • 用中窗口看周期规律
  • 用长窗口看整体趋势

最后再把这些尺度的信息综合起来,做预测、分类、异常检测或者解释分析。

所以你可以把它理解成:

这里:

  •  表示原始时间序列在时刻   的取值
  •  表示短期尺度成分
  •  表示中期尺度成分
  •  表示长期尺度成分
  •  表示噪声或无法解释的残差

这不是说现实里一定能完美拆成这四块,而是告诉你一个非常重要的思考方式:复杂时间序列,不要只用一把尺子量。

多尺度建模的核心原理

多尺度建模常见有三种思路:

  1. 基于平滑/窗口的方法
  2. 基于频域分解的方法
  3. 基于深度学习多分支结构的方法

用不同窗口观察同一条序列

最朴素的思路,就是用不同长度的窗口看数据。

假设原始序列是:

我们可以构造:

  • 短窗口特征:看最近   个点
  • 中窗口特征:看最近   个点
  • 长窗口特征:看最近   个点

其中 

比如:

  • :看最近 12 个时间点,抓局部波动
  • :看最近 48 个时间点,抓中期周期
  • :看最近 96 个时间点,抓长期趋势

为什么这样做有效?因为不同长度的窗口,本质上是在用不同的“视野”看问题。

就像你看地图:

  • 放大看街区,看到的是短期局部结构
  • 缩小看整个城市,看到的是全局路线

时间序列也一样。

移动平均是最简单的多尺度分解

最经典的长期趋势提取办法,是移动平均。

长度为   的移动平均定义为:

这里:

  •  表示在时刻      阶移动平均
  •  越大,平滑效果越强,更偏向长期趋势
  •  越小,保留更多局部波动

举个非常小的数字例子。

假设最近 5 个点是:

如果做 3 点移动平均,那么最后一个位置的平滑值是:

如果做 5 点移动平均,那么最后一个位置的平滑值是:

你看出来了吧:

  • 3 点平均更敏感,保留更多短期波动
  • 5 点平均更平滑,更接近长期趋势

所以,多尺度建模里,经常会同时用多个窗口的移动平均、卷积核或者池化操作,来提取不同层次的信息。

从残差中看到局部结构

有了长期趋势后,我们还会去看原始序列减去平滑序列后的残差:

这个   通常会更突出短期变化和异常点。

比如:

  • 原始值 
  • 长窗口均值 

那么残差是:

这说明当前点明显高于长期背景,有局部增强或异常冲击。

所以在多尺度分析里,一个常见套路是:

  • 大窗口提趋势
  • 小窗口提局部
  • 原始值与趋势差值得到残差
  • 把这些信息一起喂给模型

深度学习里怎么做多尺度

到了深度学习阶段,做法就更自然了。

你可以把同一段序列,同时送进多个分支:

  • 小卷积核分支:抓短期局部模式
  • 中卷积核分支:抓中期重复结构
  • 大卷积核分支:抓长期趋势依赖

最后把这些分支的输出拼接起来,再做预测。

如果用 1D 卷积表示,一个卷积操作可以写成:

这里:

  •  是卷积核大小,也可以理解成感受野范围
  •  是卷积权重
  •  是偏置
  •  是激活函数
  •  是提取出来的特征

   不同时,模型感受到的时间尺度就不同:

  •   :关注局部细节
  •   :关注更长依赖

所以多尺度卷积网络,本质上就是让模型在不同时间视野下同时学习。

一个完整案例

这里我们自己构造一个“很像业务真实情况”的时间序列数据。每一部分规律都是可控的,可以清楚地看到模型到底学到了什么~

我们的目标是:

基于过去一段时间的数据,预测下一个时间点的值。

这个序列包含:

  • 长期上升趋势
  • 两种不同频率的周期
  • 非线性扰动
  • 随机噪声
  • 少量异常点
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

np.random.seed(42)

T = 1200
t = np.arange(T)

trend = 0.01 * t
seasonal_1 = 2.5 * np.sin(2 * np.pi * t / 24)          # 短周期
seasonal_2 = 4.0 * np.sin(2 * np.pi * t / 168)         # 长周期
nonlinear = 1.2 * np.sin(2 * np.pi * t / 24) * np.cos(2 * np.pi * t / 48)
noise = np.random.normal(00.8, T)

series = 20 + trend + seasonal_1 + seasonal_2 + nonlinear + noise

# 加一些异常点
anomaly_idx = np.random.choice(np.arange(1001100), size=20, replace=False)
series[anomaly_idx] += np.random.choice([6-68-8], size=20)

df = pd.DataFrame({
    "time": t,
    "value": series
})

这段数据你可以把它想成:

  • 一个设备监控指标
  • 或者某类平台流量
  • 或者城市区域能耗

它绝对不是简单的一条正弦线,而是多个尺度叠加的复杂结构。

可视化分析

原始时间序列全貌图:

plt.figure(figsize=(165))
plt.plot(df["time"], df["value"], color="#ff006e", linewidth=1.8, label="Original Series")
plt.scatter(anomaly_idx, series[anomaly_idx], color="#ffd60a", s=40, label="Anomalies", edgecolors="black")
plt.title("Complex Time Series with Trend, Multi-seasonality and Anomalies", fontsize=14)
plt.xlabel("Time")
plt.ylabel("Value")
plt.legend()
plt.grid(alpha=0.25)
plt.show()

这张图是总览图~

能先从整体上看到几个东西:

  • 整体是缓慢上升的,说明存在长期趋势
  • 局部有明显重复波动,说明存在周期结构
  • 有些点突然冲上去或掉下去,说明有异常冲击
  • 波动不是纯规则的,说明噪声和非线性扰动也在里面

这一步非常重要。很多人上来就建模,但根本没看懂数据长什么样。时间序列分析里,先看图,是最基本的习惯。

不同窗口移动平均,观察多尺度趋势:

df["ma_12"] = df["value"].rolling(window=12).mean()
df["ma_48"] = df["value"].rolling(window=48).mean()
df["ma_168"] = df["value"].rolling(window=168).mean()

plt.figure(figsize=(166))
plt.plot(df["time"], df["value"], color="#3a86ff", alpha=0.45, label="Original")
plt.plot(df["time"], df["ma_12"], color="#ff006e", linewidth=2, label="MA-12")
plt.plot(df["time"], df["ma_48"], color="#fb5607", linewidth=2, label="MA-48")
plt.plot(df["time"], df["ma_168"], color="#8338ec", linewidth=2.5, label="MA-168")
plt.title("Multi-scale Moving Averages", fontsize=14)
plt.xlabel("Time")
plt.ylabel("Value")
plt.legend()
plt.grid(alpha=0.25)
plt.show()

这张图就是多尺度最直观的体现。

  • MA-12 更接近原始曲线,保留了较多短期波动
  • MA-48 开始变平滑,更适合看中尺度变化
  • MA-168 非常平滑,更像长期趋势线

你从这张图就能很直观地理解:时间尺度一变,看到的规律就变了。

如果业务上你关心的是“下一小时会不会抖”,那短窗口更有价值。

如果你关心“这个月设备是否持续劣化”,那长窗口才更重要。

趋势与残差分解图:

trend_component = df["ma_168"]
residual_component = df["value"] - trend_component

plt.figure(figsize=(168))

plt.subplot(211)
plt.plot(df["time"], df["value"], color="#06d6a0", alpha=0.5, label="Original")
plt.plot(df["time"], trend_component, color="#ef476f", linewidth=2.5, label="Long-term Trend")
plt.title("Original Series and Long-term Trend", fontsize=13)
plt.legend()
plt.grid(alpha=0.25)

plt.subplot(212)
plt.plot(df["time"], residual_component, color="#118ab2", linewidth=1.5, label="Residual")
plt.axhline(0, color="black", linestyle="--", alpha=0.6)
plt.title("Residual after Removing Long-term Trend", fontsize=13)
plt.legend()
plt.grid(alpha=0.25)

plt.tight_layout()
plt.show()

这张图在告诉我们:如果把长期趋势拿掉,剩下来的是什么?

剩下来的残差里通常会有:

  • 更明显的局部周期
  • 更突出的异常点
  • 更容易识别的短期波动结构

这在异常检测、局部模式识别里特别有用。因为有时候原始序列看着波动很正常,但一旦减去长期趋势,异常会突然“跳出来”。

多尺度特征热力图

我们构造几个多尺度特征:

  • 原始值
  • 短期均值
  • 中期均值
  • 长期均值
  • 短期残差
  • 中长期差值
features = pd.DataFrame({
    "value": df["value"],
    "ma_12": df["ma_12"],
    "ma_48": df["ma_48"],
    "ma_168": df["ma_168"],
    "resid_12": df["value"] - df["ma_12"],
    "resid_48": df["value"] - df["ma_48"],
})

features = features.dropna()
corr = features.corr()

plt.figure(figsize=(107))
plt.imshow(corr, cmap="turbo", interpolation="nearest")
plt.colorbar()
plt.xticks(range(len(corr.columns)), corr.columns, rotation=45)
plt.yticks(range(len(corr.columns)), corr.columns)
plt.title("Correlation Heatmap of Multi-scale Features", fontsize=14)
plt.tight_layout()
plt.show()

热力图不是为了好看,它是在告诉你:不同尺度的特征之间,到底是相似、互补,还是冲突。

比如:

  • value  ma_12 往往相关性高,说明短期平滑还保留了原始结构
  • ma_168 更接近长期趋势,和残差类特征关系会不同
  • resid_12resid_48 对异常和局部波动更敏感

如果多个尺度特征全都高度重复,那你加再多特征也没意义。

但如果它们展示的是不同层面的信息,多尺度建模就能带来真正增益。

开始建模:构造多尺度输入样本

现在我们要把这个问题转成监督学习。

假设我们希望用过去 96 个点,预测下一个点。

同时,我们把过去 96 个点按照多尺度方式喂给模型。

这里采用:

  • 原始窗口:96
  • 下采样中尺度窗口
  • 下采样长尺度窗口

数据标准化与序列切片:

from sklearn.preprocessing import StandardScaler
import torch
from torch.utils.data import Dataset, DataLoader

values = df["value"].values.reshape(-11)

scaler = StandardScaler()
values_scaled = scaler.fit_transform(values).flatten()

seq_len = 96

def create_multiscale_sequences(data, seq_len=96):
    X_short, X_mid, X_long, y = [], [], [], []
    for i in range(len(data) - seq_len - 1):
        seq = data[i:i+seq_len]
        target = data[i+seq_len]
        
        short_scale = seq
        mid_scale = seq[::2]   # 每2个点取一个
        long_scale = seq[::4]  # 每4个点取一个
        
        X_short.append(short_scale)
        X_mid.append(mid_scale)
        X_long.append(long_scale)
        y.append(target)
    return np.array(X_short), np.array(X_mid), np.array(X_long), np.array(y)

X_short, X_mid, X_long, y = create_multiscale_sequences(values_scaled, seq_len)

train_size = int(0.8 * len(y))

X_short_train, X_short_test = X_short[:train_size], X_short[train_size:]
X_mid_train, X_mid_test = X_mid[:train_size], X_mid[train_size:]
X_long_train, X_long_test = X_long[:train_size], X_long[train_size:]
y_train, y_test = y[:train_size], y[train_size:]

这里的处理逻辑要看明白。

同一段 96 长度的序列,我们做了三种表达:

  • short_scale:完整保留,抓局部细节
  • mid_scale:隔 2 取点,压缩时间分辨率,突出中尺度模式
  • long_scale:隔 4 取点,更关注长依赖趋势

这就是一个非常实用的多尺度建模技巧:同一条时间序列,用不同采样密度表达不同时间视角。

用 PyTorch 搭一个多尺度卷积网络

class MultiScaleDataset(Dataset):
    def __init__(self, xs, xm, xl, y):
        self.xs = torch.tensor(xs, dtype=torch.float32).unsqueeze(1)
        self.xm = torch.tensor(xm, dtype=torch.float32).unsqueeze(1)
        self.xl = torch.tensor(xl, dtype=torch.float32).unsqueeze(1)
        self.y = torch.tensor(y, dtype=torch.float32)

    def __len__(self):
        return len(self.y)

    def __getitem__(self, idx):
        return self.xs[idx], self.xm[idx], self.xl[idx], self.y[idx]

train_dataset = MultiScaleDataset(X_short_train, X_mid_train, X_long_train, y_train)
test_dataset = MultiScaleDataset(X_short_test, X_mid_test, X_long_test, y_test)

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

多尺度模型结构:

import torch.nn as nn
import torch.nn.functional as F

class MultiScaleCNN(nn.Module):
    def __init__(self):
        super(MultiScaleCNN, self).__init__()
        
        self.short_branch = nn.Sequential(
            nn.Conv1d(116, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.AdaptiveAvgPool1d(16)
        )
        
        self.mid_branch = nn.Sequential(
            nn.Conv1d(116, kernel_size=5, padding=2),
            nn.ReLU(),
            nn.AdaptiveAvgPool1d(16)
        )
        
        self.long_branch = nn.Sequential(
            nn.Conv1d(116, kernel_size=7, padding=3),
            nn.ReLU(),
            nn.AdaptiveAvgPool1d(16)
        )
        
        self.fc = nn.Sequential(
            nn.Linear(16 * 16 * 364),
            nn.ReLU(),
            nn.Linear(641)
        )
        
    def forward(self, xs, xm, xl):
        fs = self.short_branch(xs)
        fm = self.mid_branch(xm)
        fl = self.long_branch(xl)
        
        fusion = torch.cat([fs.flatten(1), fm.flatten(1), fl.flatten(1)], dim=1)
        out = self.fc(fusion)
        return out.squeeze(-1)

这个网络的设计思路很清楚:

  • 短尺度分支用较小卷积核,擅长抓局部变化
  • 中尺度分支用中等卷积核,擅长抓重复结构
  • 长尺度分支用较大卷积核,擅长抓长期依赖
  • 最后把 3 个分支融合做预测

你不要把它看成很复杂的模型,本质上它就是在模拟人类观察序列时的多视角分析过程。

训练模型并评估效果:

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = MultiScaleCNN().to(device)

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

epochs = 20

for epoch in range(epochs):
    model.train()
    train_loss = 0
    
    for xs, xm, xl, target in train_loader:
        xs, xm, xl, target = xs.to(device), xm.to(device), xl.to(device), target.to(device)
        
        optimizer.zero_grad()
        pred = model(xs, xm, xl)
        loss = criterion(pred, target)
        loss.backward()
        optimizer.step()
        
        train_loss += loss.item() * xs.size(0)
    
    train_loss /= len(train_loader.dataset)
    print(f"Epoch {epoch+1}/{epochs}, Train Loss: {train_loss:.6f}")

预测测试集:

model.eval()
preds = []
targets = []

with torch.no_grad():
    for xs, xm, xl, target in test_loader:
        xs, xm, xl = xs.to(device), xm.to(device), xl.to(device)
        pred = model(xs, xm, xl)
        preds.extend(pred.cpu().numpy())
        targets.extend(target.numpy())

preds = np.array(preds).reshape(-11)
targets = np.array(targets).reshape(-11)

preds_inv = scaler.inverse_transform(preds).flatten()
targets_inv = scaler.inverse_transform(targets).flatten()

预测结果图:

from sklearn.metrics import mean_squared_error, mean_absolute_error

rmse = np.sqrt(mean_squared_error(targets_inv, preds_inv))
mae = mean_absolute_error(targets_inv, preds_inv)

plt.figure(figsize=(165))
plt.plot(targets_inv, color="#ff006e", linewidth=2, label="True")
plt.plot(preds_inv, color="#00b4d8", linewidth=2, label="Predicted")
plt.title(f"Prediction Result of Multi-scale CNN | RMSE={rmse:.3f}, MAE={mae:.3f}", fontsize=14)
plt.xlabel("Test Time Step")
plt.ylabel("Value")
plt.legend()
plt.grid(alpha=0.25)
plt.show()

这张预测图主要看两个点:

第一,模型是不是跟住了整体趋势。

第二,模型能不能在局部波动时也不掉队。

如果你的模型只能跟趋势,但局部全错,那说明它只学到了长尺度。

如果它只能跟局部抖动,但长期趋势偏掉,那说明它只学到了短尺度。

而多尺度模型的优势,就在于这两头都尽量兼顾。

尺度不是越多越好

很多人一听多尺度,就开始上 5 个、8 个、10 个窗口。最后模型变复杂了,效果反而下降。

原因很简单:如果这些尺度表达的信息高度重复,那只是给模型增加冗余输入。

比较实用的做法是:

  • 一个短尺度
  • 一个中尺度
  • 一个长尺度

先把这三个层次打透,再考虑细化。

继续升级

到这里,已经把多尺度建模的核心逻辑吃透了。

但如果你想进一步往工业级方案走,可以从下面几个方向继续升级。

第一个方向,是把简单卷积升级成 TCN、LSTM、Transformer 的多尺度结构。

比如不同尺度各自编码,再做注意力融合,这在复杂预测任务里很常见。

第二个方向,是引入更正式的分解方法。

比如 STL 分解、小波变换、EMD 分解,把趋势、季节项、残差先拆出来,再分别建模。

第三个方向,是把多尺度分析和异常检测结合。

很多工业故障并不是单点异常,而是某个尺度上的模式变化。这个时候,多尺度残差会非常有用。

第四个方向,是做多变量时间序列。

真实业务里通常不止一条序列,而是多个指标一起变化。这个时候不仅有时间尺度,还有变量间关联尺度,问题会更有挑战,也更有价值。

总结

多尺度建模的本质,不是换一个花哨名字,而是承认一个现实:复杂时间序列不是按一个节奏在变化,而是多个时间节奏叠加在一起变化。

所以分析和建模时,必须同时照顾短期波动、中期周期和长期趋势,这样你对数据的理解才完整,模型效果才稳定。

大家接下来可以继续尝试两件事:

第一,把本文的单变量案例改成多变量输入,比如加入温度、节假日、设备状态等辅助特征。

第二,把本文的多尺度 CNN 升级成多尺度 Transformer 或 TCN,对比不同模型在趋势、峰值和异常段上的表现。

【声明】内容源于网络
0
0
机器学习和人工智能AI
让我们一起期待 AI 带给我们的每一场变革!推送最新行业内最新最前沿人工智能技术!
内容 381
粉丝 0
机器学习和人工智能AI 让我们一起期待 AI 带给我们的每一场变革!推送最新行业内最新最前沿人工智能技术!
总阅读5.0k
粉丝0
内容381