Timer : Transformers for Time Series Analysis at Scale 时序论文阅读

Timer : Transformers for Time Series Analysis at Scale

名词 :大型时间序列模型 (large time series models, LTSM ), 单序列序列 (single-series sequence, S3 ), 分位数 (Quantile)

论文解决的问题:

这篇论文试图解决的问题是在时间序列分析领域中,深度学习模型在小样本场景下性能瓶颈的问题:

  1. 构建统一时间序列数据集(Unified Time Series Dataset,UTSD):为了支持大型时间序列模型(LTSM)的研究,论文首先从公开可用的时间序列数据集中筛选和构建了一个具有层次化能力的统一时间序列数据集(UTSD)。这个数据集包含了来自不同领域的大量时间序列数据,为模型提供了丰富的预训练信息。
  2. 提出单序列序列(S3)格式:为了将异构时间序列数据统一处理,论文提出了S3格式,将不同时间序列转换为统一的token序列。这种格式保留了序列的模式,同时允许模型在预训练过程中观察到来自不同数据集和时间段的序列,增加了预训练的难度,迫使模型更加关注时间变化。
  3. 采用GPT风格的预训练目标:论文采用了类似于大型语言模型(如GPT)的预训练目标,即通过自回归下一个时间点预测来训练模型。这种目标允许模型学习时间序列的生成过程,从而在下游任务中展现出更好的泛化能力。
  4. 开发时间序列Transformer(Timer) :基于上述数据集、训练策略和模型架构,论文提出了Timer,这是一个大规模预训练的时间序列Transformer。Timer采用了与大型语言模型相似的解码器结构,通过自回归生成进行预训练,这使得它在各种时间序列分析任务中展现出显著的泛化能力、可扩展性和适用性。
  5. 统一生成式方法处理下游任务 :为了利用Timer处理不同的时间序列分析任务,论文将预测、插补和异常检测等任务统一为一个生成式任务。这样,Timer可以通过微调来适应不同的下游任务,而不需要为每个任务单独训练模型。
  6. 评估和分析 :论文通过在多个真实世界数据集上进行实验 ,验证了Timer在时间序列预测、插补和异常检测等任务中的有效性。同时,论文还分析了模型的可扩展性,包括模型大小和数据规模对性能的影响,以及不同架构对LTSMs的适用性。

模型微调(全量微调)

(1)环境配置:

bash 复制代码
conda activate Sundial
pip install -r requirements.txt

(2)准备数据集:

将下游数据集放在./dataset/文件夹下,可以使用提供的数据集微调, 也可以使用自定义的数据集进行微调,不过默认是按照7:1:2划分数据集。

(3)下载预训练模型:

将下载的预训练checkpoint放在./checkpoints文件夹下

(4)配置微调脚本:

参考scripts/forecast/Traffic.sh创建微调脚本, 利用关键参数配置:指定预测任务和特征模式(单变量还是多变量等)

(5)执行微调脚本:

bash 复制代码
bash ./scripts/forecast/ECL.sh

(6)微调的过程:

  • 加载数据:加载训练、验证和测试数据集
  • 初始化优化器:使用Adam优化器
  • 训练循环:迭代训练指定的epoch数
  • 验证和早停:每个epoch后验证并检查是否早停
  • 加载最佳模型:训练结束后加载验证集上表现最好的模型

(7)自定义数据集微调:

对于自定义数据集,使用CIDatasetBenchmarkCIAutoRegressionDatasetBenchmark数据加载器,其中CIDatasetBenchmark:用于直接多步预测,CIAutoRegressionDatasetBenchmark:用于迭代多步预测(需要label_len参数)

微调技巧
  1. 数据稀缺实验 :可以通过subset_rand_ratio参数控制使用的训练数据比例
  2. 学习率调整:建议使用较小的学习率(如3e-5)进行微调
  3. 批量大小:根据GPU内存调整batch_size
  4. IMS模式 :使用--use_ims启用迭代多步预测模式, 其中--use_ims参数表示可以按迭代多步方式评估Decoder-Only模型,未启用则按照直接多步方法评估Encoder-Only模型,Direct Multi-step (DMS) and Iterative Multi-step (IMS)

代码分析

S3格式的处理

(1)数据的处理:使用border1s和border2s来定义训练集、验证机、测试集的起止位置;ETTH、ETTm用固定的分割方式分割数据集。

python 复制代码
        if self.data_type == 'custom':
            data_len = len(df_raw)     # 训练, 验证, 测试集划分为70%, 20%, 10%
            num_train = int(data_len * 0.7)
            num_test = int(data_len * 0.2)
            num_vali = data_len - num_train - num_test
            border1s = [0, num_train - self.input_len, data_len - num_test - self.input_len]
            border2s = [num_train, num_train + num_vali, data_len]  # border1s和border2s用于定义训练集、验证集和测试集的起止位置

Pytorch中的DataLoader需要通过一维索引 来访问样本, 需要一个映射机制将一维索引 转换为二维坐标

(2)S3的思想:将每个变量视为独立的单变量时序,每次使用__getitem__调用只返回一个变量的时序片段, 而不是所有变量, 利用c_begin: c_begin+1来选择一列(一个变量)

① 计算每个变量能产生多少个窗口

② 数据集的组织

索引范围 对应内容

0, 880\] 变量0的881个窗口 \[881, 1761\] 变量1的881个窗口 \[1762, 2642\] 变量2的881个窗口 \[2643, 3523\] 变量3的881个窗口 \[3524, 4404\] 变量4的881个窗口 \[4405, 5285\] 变量5的881个窗口 \[5286, 6166\] 变量6的881个窗口

③ 索引到变量和时间的映射

整除运算c_begin = index // n_timepoint 确定变量编号

  • index = 0 到 880: 0 // 881 = 0 → 变量0
  • index = 881 到 1761: 881 // 881 = 1 → 变量1
  • index = 1762 到 2642: 1762 // 881 = 2 → 变量2

取模运算 : s_begin = index % n_timepoint 确定该变量内的时间起点:

  • index = 0: 0 % 881 = 0 → 变量0的第0个窗口
  • index = 100: 100 % 881 = 100 → 变量0的第100个窗口
  • index = 881: 881 % 881 = 0 → 变量1的第0个窗口
  • index = 1000: 1000 % 881 = 119 → 变量1的第119个窗口

④ 训练集的稀疏采样:通过将索引乘以采样间隔实现跳跃式的选择样本

index = index * self.internal 执行时,实际访问的样本索引会被放大。

具体示例 (假设 subset_rand_ratio = 0.2,internal = 5):

DataLoader 给的 index 乘以 internal 后 实际访问的样本
0 0 × 5 = 0 第0个样本
1 1 × 5 = 5 第5个样本
2 2 × 5 = 10 第10个样本
3 3 × 5 = 15 第15个样本
... ... ...

能够实现每隔5个样本取1个的效果,同时__len__方法返回的长度也适配了训练过程的采样,能够覆盖原始数据集中均匀分布的样本。

假设:

  • 原始数据集有 6167 个样本 (7个变量 × 881个时间窗口)
  • subset_rand_ratio = 0.2 设置 --subset_rand_ratio参数来决定小样本场景下训练样本的占比。
  • internal = 5

训练集的处理:

  1. __len__() 返回 int(6167 × 0.2) = 1233
  2. DataLoader 会请求索引 0, 1, 2, ..., 1232
  3. 在__getitem__中:
    • 请求 index=0 → 实际访问 0×5=0
    • 请求 index=1 → 实际访问 1×5=5
    • 请求 index=2 → 实际访问 2×5=10
    • ...
    • 请求 index=1232 → 实际访问 1232×5=6160

这样就从 6167 个样本中均匀采样了约 20% 的样本。

CIAutoRegressionDatasetBenchmarkCIDatasetBenchmark两个数据集类的区别

(1) CIDatasetBenchmark是直接预测模式, 关键在于__getitem__

python 复制代码
    def __getitem__(self, index):
        if self.set_type == 0:
            index = index * self.internal
        c_begin = index // self.n_timepoint  # select variable 选择变量
        s_begin = index % self.n_timepoint   # select start time 选择时间起点
        s_end = s_begin + self.input_len
        r_begin = s_end
        r_end = r_begin + self.pred_len
        seq_x = self.data_x[s_begin:s_end, c_begin:c_begin + 1]  # 提取单个变量的时序片段
        seq_y = self.data_y[r_begin:r_end, c_begin:c_begin + 1]  # 提取单个变量的预测目标
        seq_x_mark = self.data_stamp[s_begin:s_end]  # 输入序列的时间特征
        seq_y_mark = self.data_stamp[r_begin:r_end]  # 预测目标的时间特征

        return seq_x, seq_y, seq_x_mark, seq_y_mark

预测窗口紧接在输入窗口之后,没有重叠

目标序列长度为:pred_len

seq_y形状:[pred_len, 1], 只包含需要预测的未来值

复制代码
时间轴: [0 -------- 672] [672 -------- 768]  
         ↑ seq_x (输入) ↑  ↑ seq_y (目标) ↑  
         s_begin    s_end  r_begin    r_end  
  • seq_x: 时间步 0-672 (输入序列)
  • seq_y: 时间步 672-768 (预测目标,长度96)
  • 无重叠,直接预测未来96步

(2) CIAutoRegressionDatasetBenchmark是自回归预测模式

python 复制代码
    def __getitem__(self, index):
        if self.set_type == 0:
            index = index * self.internal
        c_begin = index // self.n_timepoint  # select variable
        s_begin = index % self.n_timepoint   # select start time
        s_end = s_begin + self.input_len
        r_begin = s_end - self.label_len
        r_end = r_begin + self.label_len + self.pred_len
        seq_x = self.data_x[s_begin:s_end, c_begin:c_begin + 1]
        seq_y = self.data_y[r_begin:r_end, c_begin:c_begin + 1]
        seq_x_mark = self.data_stamp[s_begin:s_end]
        seq_y_mark = self.data_stamp[r_begin:r_end]
        return seq_x, seq_y, seq_x_mark, seq_y_mark

预测窗口向前延伸 label_len,与输入窗口有重叠

目标序列长度为 label_len + pred_len

seq_y 形状: [label_len + pred_len, 1] - 包含历史标签和未来预测值

复制代码
时间轴: [0 -------- 672]  
                [96 ------------ 768]  
         ↑ seq_x ↑  ↑   seq_y      ↑  
         s_begin s_end r_begin  r_end  
                      (s_end-576)  
  • seq_x: 时间步 0-672 (输入序列)
  • seq_y: 时间步 96-768 (标签+预测,长度672)
    • 前576步 (96-672): 历史标签,与 seq_x 末尾重叠
    • 后96步 (672-768): 需要预测的未来值
  • 有重叠, 自回归预测
PatchEmbedding分析

PatchEmbedding 将连续的时间序列分割成固定长度的 patches,然后将每个 patch 投影到高维嵌入空间

python 复制代码
class PatchEmbedding(nn.Module):
    def __init__(self, d_model, patch_len, stride, padding, dropout, position_embedding=True):
        super(PatchEmbedding, self).__init__()
        # Patching
        self.patch_len = patch_len
        self.stride = stride
        self.padding_patch_layer = nn.ReplicationPad1d((0, padding))  # 在末尾使用复制填充

        # Backbone, Input encoding: projection of feature vectors onto a d-dim vector space
        self.value_embedding = nn.Linear(patch_len, d_model, bias=False)
        self.positioned = position_embedding

        # Positional embedding
        if position_embedding:
            self.position_embedding = PositionalEmbedding(d_model)

        # Residual dropout
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        # do patching
        n_vars = x.shape[1] # [B, M, T]
        x = self.padding_patch_layer(x)
        x = x.unfold(dimension=-1, size=self.patch_len, step=self.stride) # [B, M, N, L]
        x = torch.reshape(x, (x.shape[0] * x.shape[1], x.shape[2], x.shape[3]))
        # Input encoding
        if self.positioned:
            x = self.value_embedding(x) + self.position_embedding(x)
        else:
            x = self.value_embedding(x)
        return self.dropout(x), n_vars
  • patch_len: 每个 patch 的长度(时间步数)
  • stride : patch 之间的步长,在 Timer 中等于 patch_len(非重叠)
  • padding: 填充长度,确保序列可以被完整分割
  • d_model: 嵌入维度-1024
  • position_embedding: 是否使用位置编码

x.unfold(dimension=-1, size=self.patch_len, step=self.stride)

沿着最后一维创建滑动窗口,Timer在输入之前,对x=[551, 672, 1]做了交换维度的操作, permute之后变成了[551, 1, 672];每个窗口patch的大小是96, 步长也是 96。patchembedding之后维度是[551, 1, 7, 96],之后将batch和变量维度合并,这样做是因为Timer能够对每个变量独立处理, 将他们视为独立的样本

PatchEmbedding 通过以下步骤将时间序列转换为 patch 嵌入:

  1. 填充 : 确保序列长度可被 patch_len 整除
  2. 分割 : 使用 unfold 将序列分割成非重叠的 patches
  3. 重塑: 将 batch 和变量维度合并,独立处理每个变量
  4. 投影: 通过线性层将每个 patch 投影到高维空间
  5. 位置编码: 添加位置信息(可选)
  6. Dropout: 正则化
Timer执行的完整流程
  1. 输入归一化: [B, L, M][B, M, L]
  2. Patch embedding: [B, M, L][B*M, N, D]
  3. Transformer 处理: [B*M, N, D][B*M, N, D]
  4. 投影回时间域: [B*M, N, D][B*M, N, patch_len]
  5. 重塑和反归一化: [B*M, N, patch_len][B, L, M]
相关推荐
我要学脑机2 小时前
[脑磁图+PD综述]文献阅读记录
论文阅读
EEPI2 小时前
【论文阅读】RoboReward: General-Purpose Vision-Language Reward Models for Robotics
论文阅读
小明_GLC3 小时前
MDMLP-EIA时序论文阅读
论文阅读
m0_650108244 小时前
P2PNet:基于点的密集人群计数与定位
论文阅读·深度学习·人群计数与定位·基于头部中心点·p2pnet
m0_650108241 天前
WorldSplat:面向自动驾驶的 4D 场景生成与新颖视图合成框架
论文阅读·自动驾驶·高保真·时空一致性·4d驾驶场景合成·生成式与重建式融合·4d高斯
小明_GLC1 天前
Timer-XL: Long-Context Transformers For Unified Time Series Forecasting 时序论文阅读
论文阅读
小明_GLC1 天前
Improving Time Series Forecasting via Instance-aware Post-hoc Revision时序论文阅读
论文阅读
EEPI1 天前
【论文阅读】Steering Your Diffusion Policy with Latent Space Reinforcement Learning
论文阅读
学海一叶1 天前
论文精读-《ReAct: Synergizing Reasoning and Acting in Language Models》,2022
论文阅读·人工智能·语言模型·动态规划·agent