Skip to content

Layer 1 — forecast() 主链

本层作用

本层解释 TimesNet.forward() 进入预测分支后,forecast() 如何把历史窗口 (B, seq_len, enc_in) 变成完整长度输出 (B, seq_len + pred_len, c_out)DataEmbeddingTimesBlock 只在本层说明接口,内部细节分别下钻到 [[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-Normalization

2. 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_decx_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_out

5.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 /= stdev

x_enc.mean(1, keepdim=True) 沿时间维 seq_len=8 求均值,保留时间维,得到 means (3,1,4)detach() 切断梯度,表示均值只是当前样本的尺度统计量,不作为可学习路径参与反向传播。

数学公式:

μb,1,c=1Lt=0L1xb,t,cσb,1,c=1Lt=0L1(xb,t,cμb,1,c)2+105x^b,t,c=xb,t,cμb,1,cσb,1,c
keepdim=True 的约束传递
写法means shape后续广播是否直接成立
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.12

5.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),数学形式是:

y=xWT+b

其中 W 的 shape 是 (13,8)b 的 shape 是 (13)。前面的 (3,6) 被当成 batch 维,等价于 3×6=18 条长度为 8 的 hidden 时间线共用同一个线性层。

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(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)

数学形式:

dec\_outb,t,:=enc\_outb,t,:WT+b

其中 W 的 shape 是 (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.27

5.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)
)

反归一化把模型在标准化尺度上的输出还原到原始数值尺度:

outputb,t,c=dec\_outb,t,cσb,1,c+μb,1,c

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.2373

5.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. 下钻子组件

子组件父层调用位置职责下层文档
DataEmbeddingself.enc_embedding(x_enc, x_mark_enc)数值、时间、位置三路 embedding[[03A-Layer2A-DataEmbedding]]
TimesBlockself.model[i](enc_out)FFT 周期发现、二维周期网格、Conv2d、多周期融合[[03B-Layer2B-TimesBlock]]
FFT_for_PeriodTimesBlock.forward() 内部从频域幅值选主周期[[03B1-Layer3-FFT_for_Period]]
Inception_Block_V1TimesBlock.forward() 内部多 kernel 二维卷积[[03B2-Layer3-Inception_Block_V1]]

*记录并在线阅读我的笔记*