Skip to content

Layer 2A — Projector(De-stationary 因子学习器)

1. 在父层中的位置

forecast() 步骤 ③④ 调用 self.tau_learner(x_raw, std_enc)self.delta_learner(x_raw, mean_enc)。两个 learner 都是同一个 Projector 类,只有 output_dim 不同(tau=1,delta=seq_len)。

2. I/O 接口定义

tau_learner(Projector with output_dim=1)

参数Shape含义
x (x_raw)(2, 12, 5)原始输入(归一化前),梯度已截断
stats (std_enc)(2, 1, 5)标准差统计量
输出(2, 1)原始 tau,经 .exp() 后为正标量

delta_learner(Projector with output_dim=seq_len=12)

参数Shape含义
x (x_raw)(2, 12, 5)原始输入(归一化前)
stats (mean_enc)(2, 1, 5)均值统计量
输出(2, 12)delta 向量,每个位置对应 key 序列一个时间步

3. 顺序图

4. 语义分组图

5. 逐步骤精读

§5.0 完整原始代码

python
class Projector(nn.Module):
    """
    MLP to learn the De-stationary factors
    Paper link: https://openreview.net/pdf?id=ucNDIDRNjjv
    """

    def __init__(
        self, enc_in, seq_len, hidden_dims, hidden_layers, output_dim, kernel_size=3
    ):
        super(Projector, self).__init__()

        padding = 1 if torch.__version__ >= "1.5.0" else 2
        self.series_conv = nn.Conv1d(
            in_channels=seq_len,
            out_channels=1,
            kernel_size=kernel_size,
            padding=padding,
            padding_mode="circular",
            bias=False,
        )

        layers = [nn.Linear(2 * enc_in, hidden_dims[0]), nn.ReLU()]
        for i in range(hidden_layers - 1):
            layers += [nn.Linear(hidden_dims[i], hidden_dims[i + 1]), nn.ReLU()]

        layers += [nn.Linear(hidden_dims[-1], output_dim, bias=False)]
        self.backbone = nn.Sequential(*layers)

    def forward(self, x, stats):
        # x:     B x S x E
        # stats: B x 1 x E
        # y:     B x O
        batch_size = x.shape[0]
        x = self.series_conv(x)  # B x 1 x E
        x = torch.cat([x, stats], dim=1)  # B x 2 x E
        x = x.view(batch_size, -1)  # B x 2E
        y = self.backbone(x)  # B x O

        return y

§5.1 宏观逻辑

核心设计意图:用一个轻量的 MLP 把"整条序列的统计摘要"转化为注意力调制因子。

有两路输入:

  1. 原始序列 x_raw(完整时序形态)
  2. 统计量 stats(均值或标准差的全局概括)

问题:MLP 的输入是固定大小向量,但 x_raw 是 (B, seq_len, enc_in) 的三维张量。需要把时序信息压缩成一个固定大小的摘要,这就是 series_conv 的作用。

小例子(B=1, seq_len=4, enc_in=2,kernel_size=3,padding=1):

x_raw: [[a0,b0], [a1,b1], [a2,b2], [a3,b3]]  →  shape (1, 4, 2)

Conv1d 视角:in_channels=4, out_channels=1, kernel_size=3, length=2
- 4个"通道"对应4个时间步
- 卷积在 enc_in=2 的"长度"轴上做 k=3 的滑窗,padding=1 后 length 保持 2
- 输出 (1, 1, 2):把4个时间步的信息"加权聚合"成1个摘要

stats: [[μ₀, μ₁]]  →  shape (1, 1, 2)

cat: (1,1,2) + (1,1,2) → (1, 2, 2)
view: (1, 4)     = (B, 2×enc_in)
MLP:  (1, 4) → hidden → (1, output_dim)

完整 shape 变化链(tau_learner,toy 全局参数)

(2,12,5) → Conv1d → (2,1,5) → cat → (2,2,5) → view → (2,10) → Linear(10,32)→ReLU→Linear(32,1) → (2,1)

完整 shape 变化链(delta_learner,toy 全局参数)

(2,12,5) → Conv1d → (2,1,5) → cat → (2,2,5) → view → (2,10) → Linear(10,32)→ReLU→Linear(32,12) → (2,12)

§5.2 __init__ — series_conv 的非常规 Conv1d 用法

python
padding = 1 if torch.__version__ >= "1.5.0" else 2
self.series_conv = nn.Conv1d(
    in_channels=seq_len,
    out_channels=1,
    kernel_size=kernel_size,
    padding=padding,
    padding_mode="circular",
    bias=False,
)

nn.Conv1d 期望输入格式为 (B, C, L),其中 C=in_channels,L=length。

这里传入的 x_raw 是 (B, seq_len, enc_in) = (2, 12, 5),PyTorch 接受时把它视作:C = seq_len = 12(12个"通道"),L = enc_in = 5(在变量维度上滑窗)。

为什么用这种非常规用法?

常规 Conv1d 用法是在时间轴(L 轴)做滑窗,提取局部时序特征。 Projector 反过来:把时间轴当作 channel,把变量轴当作 spatial,用 1D 卷积在"变量空间"里跨时间步做加权聚合。 效果:kernel_size=3 的卷积在 5 个变量上滑动,同时通过 in_channels=12 → out_channels=1 把 12 个时间步的权重合并成 1 个摘要,相当于学出了"哪些时间步对因子预测更重要"。 与 Linear(seq_len→1) 相比,Conv1d 的参数在变量维度上共享,更高效。

toy 参数下的 Conv1d 参数量:kernel_size=3,in_channels=12,out_channels=1,L=enc_in=5,padding=1(circular),bias=False。权重矩阵形状为 (out_channels=1, in_channels=12, kernel_size=3) = (1, 12, 3),总参数 36 个。

padding_mode="circular" 在 enc_in=5 的两端做循环填充:pad 后 L=7,Conv1d 输出 (L-kernel_size+2×padding+1) 这里是 (5-3+2×1) = 5,保持 enc_in 不变((2,1,5))。

§5.3 __init__ — backbone MLP 构建

python
layers = [nn.Linear(2 * enc_in, hidden_dims[0]), nn.ReLU()]
for i in range(hidden_layers - 1):
    layers += [nn.Linear(hidden_dims[i], hidden_dims[i + 1]), nn.ReLU()]
layers += [nn.Linear(hidden_dims[-1], output_dim, bias=False)]
self.backbone = nn.Sequential(*layers)

toy 参数(p_hidden_dims=[32], p_hidden_layers=1):

hidden_layers-1=0,循环不执行,直接构建:

layers = [Linear(2×5=10, 32), ReLU(), Linear(32, output_dim, bias=False)]

tau_learner 的 backbone:Linear(10,32) → ReLU → Linear(32,1, no_bias) — 参数量:10×32+32 + 32×1 = 384

delta_learner 的 backbone:Linear(10,32) → ReLU → Linear(32,12, no_bias) — 参数量:10×32+32 + 32×12 = 736

注意最后一层 bias=False:tau 和 delta 的输出不加 bias,由论文决定(保证输出纯粹由输入决定,不依赖常数偏置)。

§5.4 forward — series_conv

python
batch_size = x.shape[0]
x = self.series_conv(x)  # B x 1 x E

输入 x 为 x_raw,shape (2, 12, 5)。Conv1d 视作 (B=2, C=12, L=5),输出 (B=2, out_channels=1, L=5) = (2, 1, 5)

toy 数值追踪:x_raw[0,:,0](batch=0,变量=0 的 12 个时间步假设为 [1.0, 1.2, 0.8, 1.5, ...])。经过 Conv1d 卷积后,series_conv_out[0,0,0] 是这 12 个时间步在变量 0 位置的加权聚合,具体值由权重矩阵决定,范围约在 0.0~3.0(权重随机初始化,训练后学出"哪些时间步重要"的模式)。

§5.5 forward — cat + view

python
x = torch.cat([x, stats], dim=1)  # B x 2 x E
x = x.view(batch_size, -1)  # B x 2E

x shape (2, 1, 5),stats(std_enc 或 mean_enc)shape (2, 1, 5)。

torch.cat([...], dim=1) 在 dim=1("时间摘要"维)拼接 → (2, 2, 5)。

.view(batch_size, -1) = .view(2, -1) = .view(2, 10) = (2, 10),展平成 2E=2×enc_in=10 的向量。

展平后向量的含义:前 5 个元素是 series_conv 对序列形态的摘要(按变量),后 5 个元素是统计量(均值或标准差,按变量)。

toy 值示意:x_flat[0] = [series_摘要_var0, ..., series_摘要_var4, stat_var0, ..., stat_var4] = 一个 10 维向量。

§5.6 forward — backbone MLP

python
y = self.backbone(x)  # B x O

输入 (2, 10) → Linear(10,32) → (2, 32) → ReLU() → (2, 32) → Linear(32, output_dim) → 输出。

tau_learner:Linear(32,1) → 输出 (2, 1),调用处再 .exp() 保证正值。

delta_learner:Linear(32,12) → 输出 (2, 12)

.exp() 的作用:把任意实数映射到正实数域 (0,+)。tau 需要是正数才有物理意义(负的温度因子会让 softmax 语义混乱)。delta 不需要正数约束(可以正向或负向偏移注意力),所以没有 .exp()


6. 参数规模速查

默认(p_hidden_dims=[128,128], p_hidden_layers=2,enc_in=5, seq_len=12):

子模块参数量
series_conv:Conv1d(12,1,3,bias=False)12×3 = 36
backbone:Linear(10,128)→ReLU→Linear(128,128)→ReLU→Linear(128,out)10×128+128 + 128×128+128 + 128×out
tau_learner(out=1)36 + (1408 + 16640 + 128) = 18,212
delta_learner(out=12)36 + (1408 + 16640 + 1536) = 19,620
合计(两个 Projector)≈ 37,832

整个 Non-stationary Transformer 的主体参数(Encoder + Decoder + Embedding)远大于此,Projector 是轻量附加模块。

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