Appearance
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 把"整条序列的统计摘要"转化为注意力调制因子。
有两路输入:
- 原始序列
x_raw(完整时序形态) - 统计量
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 2Ex 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() 的作用:把任意实数映射到正实数域 .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 是轻量附加模块。