Skip to content

Layer 3 — ConvLayer 精读

父层(Layer 2B)Encoder.forward distilling 循环里的压缩算子([[03B-Layer2B-Encoder]])。
源文件:ts_benchmark/baselines/time_series_library/layers/Transformer_EncDec.py
__init__:第 7–18 行;forward:第 20–26 行。


1. 在父层中的位置

Encoder.forward()
  └─ for i, (attn_layer, conv_layer) in enumerate(zip(...)):
         x, attn = attn_layer(x, ...)   # EncoderLayer(见 03B1)
         x = conv_layer(x)              ← ConvLayer(本文档)seq 10→6
     x, attn = attn_layers[-1](x, ...)  # 最后一层 EncoderLayer(无 ConvLayer)

2. I/O 接口定义

shape(toy)含义
输入 x(3, 10, 8) = (B, L, d_model)EncoderLayer 0 输出
输出 x(3, 6, 8) = (B, L', d_model)distilling 后序列(L=L/2+1

3. 顺序图(具体层)


4. 语义分组图(索引层)


5. 逐步精读

5.0 完整原始代码

__init__Transformer_EncDec.py:7):

python
def __init__(self, c_in):
    super(ConvLayer, self).__init__()
    self.downConv = nn.Conv1d(
        in_channels=c_in,
        out_channels=c_in,
        kernel_size=3,
        padding=2,
        padding_mode="circular",
    )
    self.norm = nn.BatchNorm1d(c_in)
    self.activation = nn.ELU()
    self.maxPool = nn.MaxPool1d(kernel_size=3, stride=2, padding=1)

forwardTransformer_EncDec.py:20):

python
def forward(self, x):
    x = self.downConv(x.permute(0, 2, 1))
    x = self.norm(x)
    x = self.activation(x)
    x = self.maxPool(x)
    x = x.transpose(1, 2)
    return x

5.1 宏观逻辑:为什么是 Conv → BN+ELU → MaxPool?

distilling 的设计意图

Encoder 每经过一个 EncoderLayer,序列中的信息已经被注意力"聚合"过一次。此时用 Conv1d 提取局部特征,再用 MaxPool 缩减序列长度,相当于"拍一张更小分辨率的缩略图":保留主要特征,删掉冗余的 token 位置,把后续注意力矩阵从 L2 降到约 (L/2)2

用 MaxPool 而不是 AvgPool:取局部最大激活,保留最显著特征;AvgPool 会把强特征和弱特征平均掉。

整条 shape 变化链(toy,c_in=8):

(B=3, L=10, C=8)
→ permute         → (3, 8, 10)   Conv1d 需要 (B, C, L)
→ Conv1d(k=3,p=2) → (3, 8, 12)   L 先扩到 12(circular 填充再卷积)
→ BN + ELU        → (3, 8, 12)   激活后形状不变
→ MaxPool(k=3,s=2)→ (3, 8,  6)   stride=2 将 L 12→6
→ transpose(1,2)  → (3, 6,  8)   还原 (B, L', C)

5.2 步骤一:permute(0, 2, 1)

python
x = self.downConv(x.permute(0, 2, 1))

nn.Conv1d 要求输入格式 (B, C, L),而 x(B, L, C) = (3, 10, 8)

transpose 含义:
  维度 0 (B=3)   保持不动
  维度 1 (L=10)  ↔  维度 2 (C=8)
结果: (3, 8, 10)  ← C=8 现在是 channels 维

Conv1d 为什么需要 (B, C, L) 格式?

Conv1d 的卷积核 shape 是 (out_ch, in_ch, kernel_size) = (8, 8, 3),它在 L 维上滑动,在 C 维上做跨通道加权。若不 permute,C=8 在 dim=2,Conv1d 会错误地把 L 作为 channels,把 C 当作 length。


5.3 步骤二:Conv1d(k=3, padding=2, circular)

参数: in_channels=8, out_channels=8, kernel_size=3, padding=2, padding_mode="circular"

circular padding 的效果:
PyTorch 对 circular padding 先手动在两端各贴 padding=2 个循环值,再以 padding=0 调用卷积核。

原始序列(L=10): [t0, t1, t2, t3, t4, t5, t6, t7, t8, t9]
circular 填充后(L=14):
  [t8, t9, t0, t1, t2, t3, t4, t5, t6, t7, t8, t9, t0, t1]
   ← 尾部2个 →  ←————————原始 10 个————————→  ← 头部2个 →
Lout=Lpaddedks+1=1431+1=12

输出:(3, 8, 12)

为什么用 circular 而不是 zero padding?
时序数据不是图像,边界处补零会引入"假突变"。circular padding 把序列视为周期信号,边界行为更平滑,与 Autoformer 的 moving_avgTokenEmbedding 使用 circular 的理由相同。

toy 数值(batch=0, channel=0):
x[0, 0, :] = [1.0, 2.0, 1.5, 3.0, 2.5, 1.0, 2.0, 3.5, 1.0, 2.0](长度10)
circular 填充后首尾各贴2个:[1.0, 2.0, 1.0, 2.0, 1.5, 3.0, 2.5, 1.0, 2.0, 3.5, 1.0, 2.0, 1.0, 2.0]
卷积核(随机初始化,仅展示形状)在此长度14的序列上滑动12次 → (3, 8, 12)


5.4 步骤三/四:BatchNorm1d + ELU

python
x = self.norm(x)       # BatchNorm1d(8)
x = self.activation(x) # ELU()

BatchNorm1d(c_in=8):在 channel 维度(dim=1)对每个 batch 归一化,形状不变 (3, 8, 12)

ELU():激活函数,ELU(x)=xx>0α(ex1)x0α=1)。相比 ReLU,负值区域有平滑梯度,避免"dying neuron"问题。形状不变。

为什么选 ELU 不选 ReLU?
Conv1d 提取特征后立即做 MaxPool,ReLU 的硬截断可能将有意义的负特征全清零,MaxPool 取最大值就会丢失这些信息;ELU 保留负值区域的渐变,对 MaxPool 更友好。


5.5 步骤五:MaxPool1d(k=3, stride=2, padding=1)

python
x = self.maxPool(x)  # MaxPool1d(kernel_size=3, stride=2, padding=1)
Lout=Lin+2pks+1=12+2×132+1=112+1=5+1=6

输入 (3, 8, 12) → 输出 (3, 8, 6)

stride=2 是序列长度减半的核心:每2个位置取一次局部最大值,保留最显著特征。

toy 数值(batch=0, channel=0,ELU 之后的序列长=12):
x[0,0,:] = [0.3, 1.2, -0.1, 0.8, 2.1, 0.5, 1.4, 0.2, 0.9, 1.7, 0.4, 0.6]

  • zero-padding 1 on each side → [0, 0.3, 1.2, -0.1, 0.8, 2.1, 0.5, 1.4, 0.2, 0.9, 1.7, 0.4, 0.6, 0]
  • 窗口(k=3,stride=2)逐步取最大:
    • 位置 0: max(0, 0.3, 1.2) = 1.2
    • 位置 2: max(-0.1, 0.8, 2.1) = 2.1
    • 位置 4: max(0.5, 1.4, 0.2) = 1.4
    • 位置 6: max(0.9, 1.7, 0.4) = 1.7
    • 位置 8: max(0.6, 0) = 0.6 ← 右边补0
    • x[0,0,:] = [1.2, 2.1, 1.4, 1.7, 0.6, ...](6个值)

5.6 步骤六:transpose(1, 2)

python
x = x.transpose(1, 2)

(3, 8, 6) 还原为 (3, 6, 8) = (B, L', d_model) 格式,与 EncoderLayer 的输入格式一致,交回 Encoder 循环。


6. 整体效果:distilling 的几何意义

EncoderLayer 0: (3,10,8) → (3,10,8)   # 注意力矩阵 10×10
ConvLayer 0:    (3,10,8) → (3, 6,8)   # 序列压缩 ×0.6
EncoderLayer 1: (3, 6,8) → (3, 6,8)   # 注意力矩阵  6×6

102=10062=36,注意力计算量降低 64%。若 e_layers=3,则继续压到约 4,矩阵从 1026242,累计节省 84%。

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