Appearance
Layer 3 — ConvLayer 精读
父层(Layer 2B)
Encoder.forwarddistilling 循环里的压缩算子([[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 后序列( |
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)forward(Transformer_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 x5.1 宏观逻辑:为什么是 Conv → BN+ELU → MaxPool?
distilling 的设计意图
Encoder 每经过一个 EncoderLayer,序列中的信息已经被注意力"聚合"过一次。此时用 Conv1d 提取局部特征,再用 MaxPool 缩减序列长度,相当于"拍一张更小分辨率的缩略图":保留主要特征,删掉冗余的 token 位置,把后续注意力矩阵从
降到约 。 用 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个 →输出:(3, 8, 12)
为什么用 circular 而不是 zero padding?
时序数据不是图像,边界处补零会引入"假突变"。circular padding 把序列视为周期信号,边界行为更平滑,与 Autoformer 的 moving_avg 和 TokenEmbedding 使用 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 不选 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)输入 (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个值)
- 位置 0:
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×6e_layers=3,则继续压到约 4,矩阵从