
做了三年多的时序预测手里经手过的模型少说也有七八个从最早的单变量ARIMA到后来的LSTM、Transformer系再到这两年火起来的PatchTST。说实话大部分模型在单变量场景下都能跑得不错一旦换到真正的多变量生产数据问题就接踵而来变量之间的耦合关系建模不充分、长序列上的周期特征抓不牢、模型参数太多上线成本太高。我最后把方案收敛到了TimesNet Tsmixer的组合上实测效果和部署效率都让我意外。这篇文章就把这套方案从原理到代码、从踩坑到调参完整复盘一遍。这套方法适合谁看如果你的场景是电力负荷、设备传感器、金融交易这类周期性明显、多变量相互影响的数据并且你希望在不堆显卡、不搞超大模型的前提下把预测精度再往上推一档那这篇文章正好对路。不需要你有很强的数学底子但如果你能看懂PyTorch的基本forward流程会顺手很多。1. 为什么把TimesNet和Tsmixer放在一起1.1 两个模型各自的定位先聊TimesNet。它2023年提出后很快成了时序领域的热门基线核心思路一句话就能讲清楚把一维时间序列通过周期检测变成二维张量然后用二维卷积去提取特征。听起来很绕但它的直觉非常朴素——时间序列里大量模式是周期重复的比如电力负荷每天都有早晚高峰、每周都有周末效应这种周期结构在一维卷积眼里需要很大的感受野才能覆盖但如果你把序列按周期折叠成一张“图像”那同一个周期内的局部模式就变成了图像上挨着的像素卷积核轻松就能学到。Tsmixer则是Google在2023年发的一个“反Transformer”方案结构极简没有注意力机制核心就是多层MLP在两个方向交替混合先对每个变量通道在时间维度上做线性变换再对每个时间步在变量维度上做线性变换。它的max优势是参数少、计算快而且在多变量场景下效果能逼近甚至超过Transformer。1.2 组合的核心逻辑各取所长我一开始也试过直接拿单一模型硬怼但很快发现一个问题TimesNet在周期特征上的表现确实强但它对变量通道之间交互关系的建模相对简单基本是靠二维卷积天然的多通道信息融合并不是显式地去建模“A传感器和B传感器之间的同步变化”。反过来Tsmixer的通道混合MLP天然擅长这种变量耦合但它对周期特征不敏感本质上是一系列线性映射加激活让它从原始序列里自己找周期性规律效率很低。所以组合方案的核心逻辑是先用TimesNet把原始序列转成富含周期特征的表征序列相当于做一次“特征增强”再把增强后的序列喂给Tsmixer由它负责时间维度和变量维度的混合建模最后接一个全连接层输出多步预测。两个模型各自干自己最擅长的事不打架。我拿一批业务脱敏数据做了个对比输入长度96、预测长度24验证集上的MSE相对量级大概是这样的模型验证集MSE训练耗时单卡TimesNet 线性预测头0.93约1.2hTsmixer直接输入原始序列0.86约0.8hTimesNet Tsmixer串联组合0.72约1.5h效果提升不是玄学而是两个模型的信息处理方式确实互补。如果你的数据周期不明显、变量耦合也不强那这个组合不一定比单模型好多少但多变量预测场景里同时具备两种特性的数据其实是主流。2. 读懂TimesNet一维时序的二维视角2.1 先用FFT找出周期TimesNet的第一个关键环节是周期检测。它并不是固定用某个周期长度而是对输入序列做快速傅里叶变换FFT把时域信号转换到频域找到能量最集中的几个频率分量再由频率推导出对应的周期长度。具体来说输入形状是(B, L, C)B是批次、L是序列长度、C是变量通道数。对每个变量通道做FFT后取平均幅度谱选出幅值最大的k个频率。假设某个频率是freq以周期为单位即该频率在长度为L的序列里完成了freq个周期那对应的周期p就是 L // freq。比如L96检测到freq4那意味着序列大约包含4个重复单元每个周期长度是24个时间步——如果数据是小时级这恰好就是“一天”。注意一个细节FFT返回的第一个频率分量是直流分量对应的是序列均值这个不能拿来当周期代码里要过滤掉频率为0的分量。2.2 二维化与Inception卷积拿到周期p之后序列被reshape成二维张量形状从(B, L, C)变成(B*C, p, L//p)。这里每一行是一个周期内的完整时间步列方向是不同周期之间的对应位置。很多文章喜欢说“把时间序列折叠成图像”其实就是这一步。接下来用Inception Block做二维卷积。Inception的设计是并行使用多个不同尺寸的卷积核常见配置是1x1、3x3、5x5三种它们分别能捕捉周期内的瞬时变化、短时局部模式和相对长的趋势段。通过padding保证输出尺寸一致然后相加融合。因为所有周期都共享同一套卷积权重所以模型学到的特征对所有周期都是可复用的。然后还有一个聚合步骤对于top k个周期各自会得到一份二维表征TimesNet会对它们做加权融合。权重来自一个基于频率幅值的归一化结果幅值越大的周期说明该周期越显著对应的表征权重就越大。这一步非常关键它让模型能够自适应地关注最重要的周期而不是平均分配注意力。2.3 TimesNet的局限与适用边界说实话TimesNet不是万能的。我对它最直观的感受凡是周期稳定的数据它效果都出乎意料地好但遇到强随机、无明显周期、或者周期在某段时间内发生漂移的数据FFT选出来的top k周期可能根本不靠谱这时候它的表现甚至会不如一个简单的MLP。另外还有一点TimesNet的设计重心在特征提取预测头通常只是简单的全连接层。全连接层做多步预测时对变量间复杂交互关系的建模能力有限而这正是我引入Tsmixer的直接原因。3. Tsmixer用MLP做到接近Transformer的效果3.1 时间混合与通道混合Tsmixer的核心是“两段式MLP混合”理解它的关键是要清楚两个维度分别指什么。输入张量是(B, L, C)L是时间维度C是变量维度。第一步是时间混合。把张量转置成(B, C, L)之后对每个变量通道独立地在时间维度上应用同一个MLP形状变化是(BC, L)经过MLP变成(BC, L)。因为所有变量通道共享这一个MLP所以它学到的是“时间维度的通用模式”比如一个序列在时间上相邻的值如何相互影响。第二步是通道混合。把处理完的张量再转置回(B, L, C)对每个时间步在变量维度上应用另一个MLP形状变化是(BL, C)经过MLP变成(BL, C)。通道混合学到的是“同一个时间点上不同变量之间的耦合关系”这是多变量预测最重要的信息来源之一。代码里通常还会在每步MLP前加LayerNorm和Dropout残差连接则保证信息经过反复混合后不会退化。3.2 为什么它比全连接参数少还能更强有人可能会问通道混合不就是一个全连接层吗我把整个(BLC)拉平再接全连接不也能建模所有交互问题出在参数规模上。如果直接拉平全连接层参数量是(LC)²序列一长、变量一多参数直接爆炸。Tsmixer的做法是把时间混合和通道混合拆开两个MLP共享权重总参数量大约是L² C²这个量级比(LC)²小了几个数量级。参数少带来的另一个好处是更不容易过拟合训练也更快在数据量不够大的业务场景里这一点非常重要。3.3 Tsmixer的适用边界Tsmixer的一个明显弱点是它本质上是线性映射加激活函数堆叠对时间序列的非平稳性和突变趋势处理能力弱。比如说数据在某个时间段内发生了整体抬升如果不去做差分或归一化Tsmixer的预测就会普遍滞后。这在组合方案里反而被弥补了。TimesNet在做特征提取前本身就会对序列做频域分析周期项抓走之后剩余的趋势项和噪声项再进入混合阶段Tsmixer的建模压力小了很多。我在实践中还会额外加一个RevIN可逆实例归一化针对每个样本移除均值方差做完预测再恢复非平稳数据的效果提升非常明显。4. 组合模型设计与数据准备实战4.1 数据准备与预处理多变量时间序列建模的第一步是把原始表格数据变成滑窗样本。假设原始数据是(N, C)N是时间步总数C是变量数。设定输入长度L和预测长度H滑窗得到样本形状(num_samples, L, C)标签形状(num_samples, H, C)。预处理有三件事必须做顺序不能乱。第一是缺失值处理。很多传感器数据会有零星的缺失值我踩过的坑是直接用全局均值填充结果模型在缺失段附近预测始终偏平。后来改用“相邻有效值线性插值 同周期均值插值”的组合方案缺失段短小于5个时间步用线性插值缺失段长跨度超过一个主周期用历史同期均值做兜底效果明显好一截。第二是异常值处理。时序数据里偶尔会出现剧烈的读数毛刺比如某个传感器瞬间跳到正常值的10倍。这种毛刺会严重影响FFT的频谱质量导致周期检测失真。我一般会把超过3倍标准差的值用分位数截断处理Winsorize而不是简单删除因为删除会破坏时间连续性。第三是归一化。这里有个非常隐蔽的坑如果拿全量数据做MinMax归一化再划分训练集和测试集那测试集的信息已经在归一化阶段透传给了模型验证指标会虚高。正确做法是先划分数据集然后在训练集上计算统计量用同一套统计量去处理验证集和测试集。另外变量之间的物理量纲差异较大时要逐变量独立归一化不要统一搞一个scaler。4.2 组合架构设计我最终采用的组合结构可以概括为输入形状(B, L, C)的多变量序列第一步TimesNet做周期检测与特征提取输出依然是(B, L, C)形状的增强序列第二步Tsmixer在增强序列上做堆叠的混合块每个块包含时间混合与通道混合第三步把Tsmixer输出过一个全连接层输出形状(B, H, C)的多步预测这个串联方案的好处是职责清晰。TimesNet只负责特征增强不需要关心输出维度Tsmixer只负责时序与通道混合不需要关心周期结构。两个模块可以单独调参、单独评估定位问题也更容易。如果想把两个模型完全并联起来各算各的预测头再融合我也试过效果不算差但调参压力会比较大而且并联时两个分支的预测尺度如果不匹配融合权重很难定。串联方案在绝大多数情况下更稳我建议优先尝试。4.3 输入构造与标签设计多步预测任务中通常有两种做法一种是递归预测把上一轮的预测结果作为下一轮输入这种方式实现简单但误差会逐步累积预测步数一长效果断崖下跌另一种是直接多步预测模型一次输出H个时间步的所有预测值。Tsmixer天然适合直接多步预测因为它的最后一层可以设计成输出(B, H, C)不需要递归。我在标签设计时做了一个小优化滑窗步长设为1保证每个时间点都有机会作为训练起点样本数量足够多同时为了保证样本之间的独立性不至于太强导致过拟合训练时每个epoch会随机采样一部分窗口而不是全量喂进去。5. 核心代码实现5.1 TimesNet特征提取模块先实现TimesNet的核心模块。我做了一些简化去掉了原论文里复杂的版本间跳连但保留了最关键的三件事FFT周期检测、周期折叠二维化、Inception卷积聚合。import torch import torch.nn as nn import torch.fft as fft class TimesNetBlock(nn.Module): def __init__(self, seq_len, d_model, top_k3, kernel_list(1, 3, 5)): super().__init__() self.seq_len seq_len self.d_model d_model self.top_k top_k # 多尺度二维卷积共享参数处理不同周期折叠 self.conv nn.Sequential( nn.Conv2d(d_model, d_model, kernel_size(1, kernel_list[0]), padding(0, 0)), nn.GELU(), nn.Conv2d(d_model, d_model, kernel_size(kernel_list[1], 1), padding(kernel_list[1]//2, 0)), nn.GELU(), nn.Conv2d(d_model, d_model, kernel_size(1, kernel_list[2]), padding(0, kernel_list[2]//2)), ) # 周期权重归一化用 self.softmax nn.Softmax(dim1) def _fft_periods(self, x): # x: (B, L, C) B, L, C x.shape # 对每个变量通道做FFT取平均幅度谱 x_fft fft.rfft(x, dim1) # (B, L//21, C) amplitude x_fft.abs().mean(dim2) # (B, L//21) # 去掉直流分量 amplitude amplitude[:, 1:] # 取前top_k个幅度最大的频率索引频率为0已经去掉所以1还原 topk_val, topk_idx torch.topk(amplitude, self.top_k, dim1) topk_idx topk_idx 1 # 还原真实频率 periods (L // topk_idx).long() # (B, top_k) return periods, topk_val def forward(self, x): B, L, C x.shape periods, weights self._fft_periods(x) # 把序列按变量维度展开后续reshape成2D x x.permute(0, 2, 1).unsqueeze(-1) # (B, C, L, 1) 留一个伪高度维度便于统一处理 conv_out [] for i in range(self.top_k): p periods[:, i].clamp(min2, maxL // 2) # 防止周期异常值 # 每个样本的周期可能不一样这里为了batch处理取batch内当前top k的周期 # 实际工程中更稳的是固定周期候选或者在batch内取平均 p int(p.float().mean().item()) num_periods L // p # 截断以保证整除 x_crop x[:, :, : p * num_periods, :] # (B, C, p*num_periods, 1) # 折叠成 (B, C, p, num_periods) x_2d x_crop.reshape(B, C, p, num_periods) y self.conv(x_2d) # (B, C, p, num_periods) # 拉回一维 y y.reshape(B, C, p * num_periods) # 因为可能截断过pad回原长度L if y.shape[-1] L: pad torch.zeros(B, C, L - y.shape[-1], devicey.device) y torch.cat([y, pad], dim-1) conv_out.append(y.unsqueeze(1)) conv_out torch.cat(conv_out, dim1) # (B, top_k, C, L) # 用频率幅值作为注意力权重做聚合 weight self.softmax(weights).unsqueeze(-1).unsqueeze(-1) out (conv_out * weight).sum(dim1) # (B, C, L) return out.permute(0, 2, 1) # (B, L, C)代码里有两个点要重点说。第一每个样本检测出来的周期可能不同直接按batch维度算p会不稳定。我在工程上通常做两件事一是把样本周期取整后在batch内做中位数平滑二是如果发现训练过程中周期值频繁抖动就直接关闭自适应周期检测改用固定周期列表比如小时数据固定24和168效果反而更稳。第二卷积核配置的合理性很重要太小的核感受野不足以覆盖周期内的局部模式太大的核参数多又容易过拟合我通常保持三层结构中间层负责拉大感受野。5.2 Tsmixer预测模块Tsmixer的代码比较简练核心就是两个MLP加一个反向残差控制。class TSMixerLayer(nn.Module): def __init__(self, seq_len, n_channels, hidden_dim64, residuechannel_first, dropout0.1): super().__init__() self.seq_len seq_len self.n_channels n_channels self.residue residue # 时间混合每个变量通道共享同一个MLP作用于时间维度 self.time_mlp nn.Sequential( nn.Linear(seq_len, hidden_dim), nn.GELU(), nn.Linear(hidden_dim, seq_len), nn.Dropout(dropout), ) # 通道混合每个时间步共享同一个MLP作用于变量维度 self.channel_mlp nn.Sequential( nn.Linear(n_channels, hidden_dim), nn.GELU(), nn.Linear(hidden_dim, n_channels), nn.Dropout(dropout), ) self.norm1 nn.LayerNorm(n_channels) self.norm2 nn.LayerNorm(n_channels) def forward(self, x): # x: (B, L, C) if self.residue channel_first: # 先通道混合再时间混合 x_res x x self.norm1(x) x x self.channel_mlp(x) x_t x.transpose(1, 2) # (B, C, L) x_t x_t self.time_mlp(x_t) x x_t.transpose(1, 2) x x x_res else: # 先时间混合再通道混合 x_res x x_t x.transpose(1, 2) # (B, C, L) x_t self.norm1(x_t.transpose(1, 2)).transpose(1, 2) x_t x_t self.time_mlp(x_t) x x_t.transpose(1, 2) x self.norm2(x) x x self.channel_mlp(x) x x x_res return x反向残差的设计值得单独解释一下。Tsmixer的原始论文提到如果堆叠多层时一直按“通道混合 - 时间混合”的顺序最后一层的输出会更偏向通道维度的信息如果一直按反方向又更偏向时间维度。所以规范做法是在深层堆叠时交替使用两种残差顺序让网络在宏观上对两个方向保持均衡。我在实现时会在模型初始化阶段按层数奇偶分配residue类型而不是所有层用同一个顺序。5.3 训练与评估组合模型把两个模块串起来整体forward很直观。class TimesNetTsmixer(nn.Module): def __init__(self, seq_len, pred_len, n_channels, top_k3, hidden_dim64, n_layers3): super().__init__() self.timesnet TimesNetBlock(seq_len, n_channels, top_ktop_k) self.tsmixer_layers nn.ModuleList([ TSMixerLayer(seq_len, n_channels, hidden_dimhidden_dim, residuechannel_first if i % 2 0 else time_first) for i in range(n_layers) ]) self.head nn.Linear(seq_len, pred_len) def forward(self, x): # x: (B, L, C) h self.timesnet(x) for layer in self.tsmixer_layers: h layer(h) # head对每个变量通道独立做时间维度的映射 out self.head(h.permute(0, 2, 1)) # (B, C, pred_len) return out.permute(0, 2, 1) # (B, pred_len, C)训练流程我用AdamW优化器配合余弦退火批量大小64梯度裁剪设为1.0防止偶发的梯度爆炸导致损失变成NaN。评估指标我会同时看MSE和MAE多变量场景下还会按变量分别统计指标因为很多时候总体MSE下降是由强相关变量贡献的弱相关变量可能原地踏步。有个小技巧训练结束后不直接用最后一轮的模型而是保存验证集MSE最优的那个checkpoint重新加载这在高方差训练场景里能稳定提升最终效果。6. 超参数设置与训练经验6.1 关键参数说明超参数直接决定模型能不能收敛到合理的状态我整理了一份在实际项目中相对通用的配置参数推荐值范围说明seq_len96 ~ 336至少覆盖2个完整主周期pred_len24 ~ 96建议不超过seq_len的一半top_k3 ~ 5周期数量太多会引入噪声周期d_model / n_channels16 ~ 64变量通道数不高时不用太大hidden_dim64 ~ 128Tsmixer中间层维度n_layers2 ~ 4太深容易过拟合收益不明显dropout0.1 ~ 0.3数据量小取大值learning_rate1e-3 ~ 5e-4AdamW配余弦退火batch_size32 ~ 128样本多可适当加大seq_len的选择对FFT周期检测影响最大。如果你处理的是小时级电力数据主周期是24和168那输入长度至少应该覆盖2个主周期也就是336个时间步否则FFT分辨不出周周期。我建议选seq_len时先做一个FFT频谱分析看清主要周期分布之后再定而不是拍脑袋选个128。top_k也不宜过大。我试过把top_k从3拉到8结果不仅训练变慢验证集指标反而恶化。原因很好理解FFT确实能选出几个幅值较大的频率但排在第6、第7、第8位的频率往往是噪声或主周期的谐波分量把它们当成独立周期去建模只会增加卷积层的负担让模型学到一堆无关模式。6.2 训练中的几个细节经验固定随机种子。时序模型训练过程中一旦数据划分和权重初始化不确定实验结果就很难复现。我通常会在训练脚本最前面固定Python、NumPy和PyTorch的随机种子数据加载器也把shuffle的随机种子固定。预测结果滞后严重时先别急着调模型结构。加RevIN、对目标序列做一阶差分通常比加深网络有效得多。我有一次在强趋势数据上不管怎么调参预测曲线都像平移了一个滞后窗口后来加了RevIN滞后问题立刻缓解。多变量维度很高时先做相关性子筛选。如果一个数据里有几百个变量其中大量与目标变量无关把它们都塞进模型反而会引入噪声。我会先用皮尔逊相关系数或者互信息筛选出与预测目标相关性靠前的通道把维度降到20以内再建模效果和速度都会有提升。不同变量共用一个timesnet特征提取器是合理的吗我一开始担心共享权重会导致变量间特征相互干扰实测下来共享权重并没有导致严重问题反而因为变量间共享了周期模式的知识训练更充分。如果你实在不放心也可以把不同变量分组每组用独立的TimesNetBlock参数会多一点但灵活度更高。7. 常见问题与排查7.1 周期检测不稳定这是TimesNet落地时最常遇到的问题症状是训练loss不断下降但验证集指标波动很大或者同一个模型的两次训练结果差异明显。排查思路是先把周期打印出来看。我写过一个简易的debug函数在每个epoch结束时输出当前batch的top_k周期值。如果周期在训练过程中跳来跳去比如一会儿检测到24一会儿检测到23说明FFT频谱本身就不干净。常见原因有三个原始序列存在缺口、趋势项太强压制了周期峰、输入长度太短不足以分辨主周期。对应解法先做插值与去趋势再考虑固定周期候选。如果数据业务含义明确比如明确知道有日周期和周周期就别依赖自适应检测了直接传periods[24, 168]进去省心又稳。7.2 预测曲线明显滞后多步预测的滞后是一个老大难问题症状是预测的上升和下降趋势都慢于真实值像是把真实曲线沿着时间轴“拖”了一段距离。出现滞后优先检查归一化。Tsmixer对输入序列的均值平移非常敏感如果训练集和测试集的分布存在漂移不做RevIN就会出现滞后。其次是检查损失函数如果只用MSE模型倾向于输出条件均值而条件均值在转折点处天然是平滑的可以尝试在loss里加上相邻时间步差值的L1惩罚让模型更积极地追趋势。最后再看是不是预测步数过长如果pred_len太大用直接多步输出确实很难兼顾每个步长的灵敏度可以考虑切成两段递归预测虽然理论上会累积误差但实际中后半段的跟随性反而更好。7.3 显存和速度问题多变量场景下通道混合MLP的维度是n_channels如果n_channels达到几百模型会变大但一般不至于OOM。如果是大规模传感器场景通道数上万那Tsmixer的channel_mlp就会变成显存杀手。这种极端情况下建议做两步压缩先用PCA把通道降到几百建模完再映射回去或者把通道分成多个group每个group内部做混合跨组信息通过少数几个“汇总通道”交互。我在处理一个工业设备的千通道数据时用过分组通道混合显存占用降了接近60%精度损失只在可接受范围内。8. 写在最后的体会把TimesNet和Tsmixer串在一起做多变量预测算是我最近一年试过的性价比最高的方案之一。TimesNet负责把周期结构这种“强先验知识”显式交给模型Tsmixer负责用很低成本的MLP把变量交互关系揉进去两个模块在职责上几乎不重叠串联之后每个环节都更容易解释出问题也更好排查。这不是一个需要超大算力的模型但如果你手上的数据周期性强、变量多、数据量不算海量它很可能比你在Transformer上砸算力调半天效果更好。最后再分享一个小技巧如果你的数据本身周期很稳定在验证的时候多测几次不同时间段的测试集不要只在一个固定测试窗口上评估。周期模型对“窗外”的泛化能力往往不稳定在不同季节、不同活动水平的时间段上多测几组才能看到这个方案的真正实力。