Skip to content

Layer 3 — EncoderLayer 精读

父层(Layer 2B)Encoder.forward 的循环体调用 attn_layer(x, ...)
本文档只覆盖 EncoderLayer.forward 这一层(Transformer block 结构)。
子层 AttentionLayer 及以下见 04B-Layer4-AttentionLayer


1. 在父层中的位置

Encoder.forward
  └─ for attn_layer in self.attn_layers:
         x, attn = attn_layer(x, ...)   ← 本文档
              └─ self.attention(x, x, x, ...)   → 详见 Layer4 AttentionLayer
              └─ conv1 → gelu → conv2

2. I/O 接口定义

python
def forward(self, x, attn_mask=None, tau=None, delta=None):
shape(toy)含义
输入 x(8, 6, 16) = (B*C, patch_num, d_model)上一层输出的 patch token
输出 x(8, 6, 16)经过注意力 + FFN 变换后的 patch token,形状不变
输出 attnNone注意力权重;output_attention=False 时为 None

attn_mask / tau / delta 全为 None,原样透传给 AttentionLayer。


3. 顺序图(具体层)


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

两块结构完全对称:操作 → 残差 → LayerNorm(Post-norm 形式)。


5. 逐步解析

5.0 完整原始代码

python
def forward(self, x, attn_mask=None, tau=None, delta=None):
    new_x, attn = self.attention(x, x, x, attn_mask=attn_mask, tau=tau, delta=delta)
    x = x + self.dropout(new_x)

    y = x = self.norm1(x)
    y = self.dropout(self.activation(self.conv1(y.transpose(-1, 1))))
    y = self.dropout(self.conv2(y).transpose(-1, 1))

    return self.norm2(x + y), attn

5.1 注意力残差块

本块的作用

把注意力输出 new_x 融入原始 token x,同时用残差保留原始信号。注意力只贡献增量,不替代原始表示。

步骤一 — 自注意力

python
new_x, attn = self.attention(x, x, x, attn_mask=attn_mask, tau=tau, delta=delta)

三个位置参数都传同一个 x,对应形参 queries, keys, values,即自注意力(Q=K=V=x)。PatchTST 没有 Decoder,每个 patch token 只需在自己序列内部互相关注。

输入 x = (8, 6, 16),即 (B×C=8, patch_num=6, d_model=16)。返回 new_x = (8, 6, 16)attn = None

→ AttentionLayer 内部细节见 04B-Layer4-AttentionLayer

步骤二 — 残差① + norm1

python
x = x + self.dropout(new_x)
y = x = self.norm1(x)

第一行:dropout(new_x) 做正则化后加回 x,==残差跳接==保留原始信号,形状维持 (8, 6, 16)

第二行:一行两赋值。norm1(x) 产生 tensor T,xy 同时指向 T。

y = x = norm1(x) 的 Python 引用语义

后续 FFN 会把 y 重新绑定到新 tensor;x 仍然指向 T,不受干扰。

步骤五的 x + y = norm1 输出(T)+ FFN 输出,正好构成第二个残差连接。

如果写成 x = norm1(x); y = x,效果完全一样——后续 y = ... 赋值不会修改 x 所指的 T。一行写法只是更紧凑。

形状流水线:x_orig (8,6,16)x = x_orig + dropout(new_x) → (8,6,16)x = y = T = norm1(x) → (8,6,16)


5.2 FFN 残差块

步骤三 — 升维 + 激活(conv1)

python
y = self.dropout(self.activation(self.conv1(y.transpose(-1, 1))))

形状变换链:y (8,6,16).transpose(-1,1)(8,16,6)conv1 Conv1d(16→64, k=1)(8,64,6)geludropouty (8,64,6)

Conv1d 要求 (Batch, Channels, Length) 格式,当前 y = (8,6,16) 的 d_model=16 在 dim2(末尾),不在 Channels(dim1)位置。.transpose(-1, 1) 交换 dim1 和 dim2,得 (8, 16, 6):Channels=d_model=16 ✓,Length=patch_num=6 ✓。

k=1 保证每个 patch 位置独立计算,不跨位置——等价于对每个 patch 做一次 Linear(16→64),即 ==Position-wise FFN==。

为什么先升维到 64,再降回 16?

注意力机制本质上是 V 的==加权线性求和==,它不会创造新特征,只重新分配已有的 d_model=16 维表示。非线性变换能力完全来自 FFN。

但如果直接在 d_model=16 上做 GELU,非线性的"施展空间"很窄。先升维到 dff=64

  • 16 维特征被投影到更宽的空间,各维度之间有更多混合自由度
  • GELU 在高维的选择性激活效果更强(不同神经元响应不同特征组合)
  • 再压缩回 16,从 64 个候选表达中提炼出最有用的方向

这一 expand → activate → compress 结构来自原版 Transformer 论文(Vaswani 2017),扩展比 dff/dmodel=4 是经验上效果最好的设置。

步骤四 — 降维(conv2)

python
y = self.dropout(self.conv2(y).transpose(-1, 1))

形状变换链:y (8,64,6)conv2 Conv1d(64→16, k=1)(8,16,6).transpose(-1,1)(8,6,16)dropouty (8,6,16)

conv2 压缩回 d_model=16;.transpose(-1,1) 把轴顺序还原为 (B*C, patch_num, d_model),与输入格式一致。

步骤五 — 残差② + norm2

python
return self.norm2(x + y), attn

x (8,6,16) 是步骤二保留的 norm1 输出 T(未被 FFN 修改);y (8,6,16) 是 FFN 的输出。x + y 做第二个残差连接,norm2 归一化后返回 (8, 6, 16)


6. 下钻子组件

子组件职责下层文档
AttentionLayerself.attentiond_model 格式 ↔ 多头格式的桥梁;Q/K/V 投影 + 委托 FullAttention04B-Layer4-AttentionLayer

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