Appearance
Layer 1 — forecast() 主链
本层作用
本层解释
TimesNet.forward()进入预测分支后,forecast()如何把历史窗口(B, seq_len, enc_in)变成完整长度输出(B, seq_len + pred_len, c_out)。DataEmbedding与TimesBlock只在本层说明接口,内部细节分别下钻到 [[03A-Layer2A-DataEmbedding]] 与 [[03B-Layer2B-TimesBlock]]。
1. 在父层中的位置
text
TimesNet.forward(x_enc, x_mark_enc, x_dec, x_mark_dec)
└─ self.forecast(x_enc, x_mark_enc, x_dec, x_mark_dec) ← 本文档
├─ Normalization
├─ self.enc_embedding(x_enc, x_mark_enc) → 详见 [[03A-Layer2A-DataEmbedding]]
├─ self.predict_linear(enc_out.permute(0, 2, 1))
├─ self.model[i](enc_out) → 详见 [[03B-Layer2B-TimesBlock]]
├─ self.projection(enc_out)
└─ De-Normalization2. I/O 接口定义
函数入口:
python
def forecast(self, x_enc, x_mark_enc, x_dec, x_mark_dec):全局 toy 参数:B=3, seq_len=8, pred_len=5, T=13, enc_in=c_out=4, d_model=6。
| 参数 | toy shape | 含义 | 是否真正使用 |
|---|---|---|---|
x_enc | (3,8,4) | encoder 历史数值窗口 | 是 |
x_mark_enc | (3,8,4) | encoder 时间特征 | 是 |
x_dec | (3,9,4) | adapter 构造的 decoder 输入,label_len + pred_len = 4 + 5 | 否 |
x_mark_dec | (3,9,4) | decoder 时间特征 | 否 |
forecast() 输出 dec_out | (3,13,4) | 完整长度输出,包含历史对齐段和未来段 | 是 |
forward() 最终输出 | (3,5,4) | dec_out[:, -pred_len:, :] 截取未来 5 步 | 是 |
TimesNet 与 Informer/Autoformer 的接口差异
TFB 的
transformer_adapter统一给模型传四个张量,但 TimesNet 的 forecasting 主链不依赖 decoder 输入。x_dec和x_mark_dec只是为了适配统一接口存在。
3. 顺序图(具体层)
4. 语义分组图(索引层)
5. 逐步精读
5.0 完整原始代码
位置:ts_benchmark/baselines/time_series_library/models/TimesNet.py
python
def forecast(self, x_enc, x_mark_enc, x_dec, x_mark_dec):
# Normalization from Non-stationary Transformer
means = x_enc.mean(1, keepdim=True).detach()
x_enc = x_enc - means
stdev = torch.sqrt(torch.var(x_enc, dim=1, keepdim=True, unbiased=False) + 1e-5)
x_enc /= stdev
# embedding
enc_out = self.enc_embedding(x_enc, x_mark_enc) # [B,T,C]
enc_out = self.predict_linear(enc_out.permute(0, 2, 1)).permute(
0, 2, 1
) # align temporal dimension 把时间维度T 映射到 seq + pred
# TimesNet
for i in range(self.layer):
enc_out = self.layer_norm(self.model[i](enc_out))
# porject back
dec_out = self.projection(enc_out)
# De-Normalization from Non-stationary Transformer
dec_out = dec_out * (
stdev[:, 0, :].unsqueeze(1).repeat(1, self.pred_len + self.seq_len, 1)
)
dec_out = dec_out + (
means[:, 0, :].unsqueeze(1).repeat(1, self.pred_len + self.seq_len, 1)
)
return dec_out5.1 宏观逻辑:forecast() 为什么先扩长度再做 TimesBlock
设计直觉
TimesNet 不是在最后才一次性预测未来窗口。它先把 embedding 后的 hidden 序列从
seq_len=8扩展到seq_len + pred_len = 13,然后让 TimesBlock 在“历史 + 未来占位”的完整 hidden 序列上寻找周期结构。这样Conv2d处理的不是纯历史,而是已经包含未来位置的全长度表示。
完整 shape 链:
text
x_enc: (3,8,4)
Normalization: (3,8,4)
DataEmbedding: (3,8,6)
permute(0,2,1): (3,6,8)
predict_linear 8到13: (3,6,13)
permute(0,2,1): (3,13,6)
TimesBlock: (3,13,6)
projection 6到4: (3,13,4)
De-Normalization: (3,13,4)
forward tail slice: (3,5,4)最小直觉例子只看一条 hidden 维度:
text
历史 hidden 序列长度 8:
[h0, h1, h2, h3, h4, h5, h6, h7]
predict_linear 学一个 8 到 13 的线性外推:
[z0, z1, z2, z3, z4, z5, z6, z7, z8, z9, z10, z11, z12]
TimesBlock 在长度 13 上找周期,而不是只在长度 8 上找周期。5.2 S1 — Normalization
python
means = x_enc.mean(1, keepdim=True).detach()
x_enc = x_enc - means
stdev = torch.sqrt(torch.var(x_enc, dim=1, keepdim=True, unbiased=False) + 1e-5)
x_enc /= stdevx_enc.mean(1, keepdim=True) 沿时间维 seq_len=8 求均值,保留时间维,得到 means (3,1,4)。detach() 切断梯度,表示均值只是当前样本的尺度统计量,不作为可学习路径参与反向传播。
数学公式:
keepdim=True 的约束传递
keepdim=True 的约束传递
写法 meansshape后续广播是否直接成立 mean(1, keepdim=True)(3,1,4)成立,能和 (3,8,4)广播mean(1)(3,4)不直接成立,需要 unsqueeze(1)当前代码依赖
dim=1被保留为 1,这也是后面x_enc - means能直接广播的原因。
toy 数值只看 batch=0, channel=0:
text
x_enc[0,:,0] = [10, 12, 14, 16, 18, 20, 22, 24]
mean = (10+12+14+16+18+20+22+24) / 8 = 17
var = ((-7)^2+(-5)^2+(-3)^2+(-1)^2+1^2+3^2+5^2+7^2) / 8 = 21
stdev = sqrt(21 + 1e-5) ≈ 4.5826
normalized =
[-1.5275, -1.0911, -0.6547, -0.2182, 0.2182, 0.6547, 1.0911, 1.5275]5.3 S2 — DataEmbedding
python
enc_out = self.enc_embedding(x_enc, x_mark_enc) # [B,T,C]输入:
text
x_enc: (3,8,4)
x_mark_enc: (3,8,4)输出:
text
enc_out: (3,8,6)DataEmbedding 把三种信息加到同一个 hidden 空间:
text
value_embedding(x_enc): (3,8,6)
temporal_embedding(x_mark_enc):(3,8,6)
position_embedding(x_enc): (1,8,6),广播到 (3,8,6)
相加后: (3,8,6)本层只需要记住:enc_out[b,t,d] 已经不是原始变量值,而是第 t 个时间步在 hidden 维度 d 上的表示。内部卷积、时间特征线性层、位置编码详见 [[03A-Layer2A-DataEmbedding]]。
toy 数值只看一个位置:
text
假设 batch=0, time=3, hidden=0:
value_embedding = 1.36
temporal_embedding = 0.62
position_embedding = 0.14
dropout=0.0
enc_out[0,3,0] = 1.36 + 0.62 + 0.14 = 2.125.4 S3 — predict_linear
python
enc_out = self.predict_linear(enc_out.permute(0, 2, 1)).permute(
0, 2, 1
)nn.Linear 只作用在最后一维。当前 enc_out 的时间维在中间,所以必须先把时间维换到最后。
text
enc_out: (3,8,6)
permute(0,2,1): (3,6,8)
Linear(8 到 13): (3,6,13)
permute(0,2,1): (3,13,6)self.predict_linear = nn.Linear(self.seq_len, self.pred_len + self.seq_len),数学形式是:
其中 (13,8),(13)。前面的 (3,6) 被当成 batch 维,等价于
toy 数值只追踪 batch=0, hidden=0 的一条长度 8 序列。设:
text
x = [1,2,3,4,5,6,7,8]为了手算,假设 W 的前 8 行复制输入位置,后 5 行是简单外推组合,bias=0:
text
y0 = x0 = 1
y1 = x1 = 2
y2 = x2 = 3
y3 = x3 = 4
y4 = x4 = 5
y5 = x5 = 6
y6 = x6 = 7
y7 = x7 = 8
y8 = (x5+x6+x7)/3 = (6+7+8)/3 = 7.0
y9 = (x6+x7)/2 = (7+8)/2 = 7.5
y10 = x7 = 8.0
y11 = (x0+x7)/2 = (1+8)/2 = 4.5
y12 = 0.1*(x0+x1+x2+x3+x4+x5+x6+x7) = 3.6
输出 y = [1,2,3,4,5,6,7,8,7.0,7.5,8.0,4.5,3.6]真实训练中 W 不会这么简单,它会通过梯度学习历史 hidden 序列到全长度 hidden 序列的映射。
5.5 S4 — TimesBlock + LayerNorm
python
for i in range(self.layer):
enc_out = self.layer_norm(self.model[i](enc_out))在 toy 参数里 e_layers=1,所以循环执行一次:
text
进入 TimesBlock: enc_out (3,13,6)
TimesBlock 输出: (3,13,6)
LayerNorm 输出: (3,13,6)TimesBlock 内部才是 TimesNet 的核心创新:FFT_for_Period 选周期,按周期 padding,把 (B,T,D) 折成 (B,D,周期段数,period),再交给 Inception_Block_V1 做二维卷积,最后把多个周期分支加权融合。详见 [[03B-Layer2B-TimesBlock]]。
LayerNorm 的位置
LayerNorm 的位置这里的
LayerNorm(configs.d_model)作用在最后一维d_model=6上。它不会改变 shape,只把每个时间点的 hidden 向量重新归一化,使 TimesBlock 输出尺度稳定。
5.6 S5 — projection
python
dec_out = self.projection(enc_out)self.projection = nn.Linear(configs.d_model, configs.c_out, bias=True)。
text
enc_out: (3,13,6)
Linear(6 到 4)
dec_out: (3,13,4)数学形式:
其中 (4,6)。它把每个时间步的 hidden 向量映射回 4 个原始变量。
toy 数值只看 batch=0, time=8, output_var=0。设:
text
enc_out[0,8,:] = [0.5, -0.2, 1.0, 0.0, 0.3, -0.4]
W[0,:] = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6]
b[0] = 0.05则:
text
dec_out[0,8,0]
= 0.5*0.1 + (-0.2)*0.2 + 1.0*0.3 + 0.0*0.4 + 0.3*0.5 + (-0.4)*0.6 + 0.05
= 0.275.7 S6 — De-Normalization
python
dec_out = dec_out * (
stdev[:, 0, :].unsqueeze(1).repeat(1, self.pred_len + self.seq_len, 1)
)
dec_out = dec_out + (
means[:, 0, :].unsqueeze(1).repeat(1, self.pred_len + self.seq_len, 1)
)反归一化把模型在标准化尺度上的输出还原到原始数值尺度:
shape 链:
text
stdev: (3,1,4)
stdev[:,0,:]: (3,4)
unsqueeze(1): (3,1,4)
repeat(1,13,1): (3,13,4)
means 同理: (3,13,4)
dec_out: (3,13,4)⚠️ 源码冗余写法
stdev本来已经是(3,1,4),代码先做stdev[:,0,:]变成(3,4),再unsqueeze(1)变回(3,1,4)。这是一组 squeeze/unsqueeze round-trip。等价简洁写法可以是stdev.repeat(1, self.pred_len + self.seq_len, 1)。当前写法不影响正确性。
toy 数值接续 S1 的 batch=0, channel=0:
text
mean = 17
stdev ≈ 4.5826
如果 projection 后的标准化尺度输出:
dec_out[0,8,0] = 0.27
反归一化:
output[0,8,0] = 0.27 * 4.5826 + 17 = 18.23735.8 forward 截取未来窗口
forecast() 返回完整长度 (3,13,4),但 forward() 在预测任务下只返回最后 pred_len=5 个时间步:
python
return dec_out[:, -self.pred_len :, :] # [B, L, D]shape:
text
dec_out: (3,13,4)
dec_out[:, -5:, :]: (3,5,4)toy 时间索引:
text
完整输出索引: [0,1,2,3,4,5,6,7,8,9,10,11,12]
截取未来段: [8,9,10,11,12]这就是 benchmark evaluator 最终拿去和真实未来窗口比较的预测结果。
6. 下钻子组件
| 子组件 | 父层调用位置 | 职责 | 下层文档 |
|---|---|---|---|
DataEmbedding | self.enc_embedding(x_enc, x_mark_enc) | 数值、时间、位置三路 embedding | [[03A-Layer2A-DataEmbedding]] |
TimesBlock | self.model[i](enc_out) | FFT 周期发现、二维周期网格、Conv2d、多周期融合 | [[03B-Layer2B-TimesBlock]] |
FFT_for_Period | TimesBlock.forward() 内部 | 从频域幅值选主周期 | [[03B1-Layer3-FFT_for_Period]] |
Inception_Block_V1 | TimesBlock.forward() 内部 | 多 kernel 二维卷积 | [[03B2-Layer3-Inception_Block_V1]] |