Appearance
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 → conv22. 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,形状不变 |
输出 attn | None | 注意力权重;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), attn5.1 注意力残差块
本块的作用
把注意力输出
new_x融入原始 tokenx,同时用残差保留原始信号。注意力只贡献增量,不替代原始表示。
步骤一 — 自注意力
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,x 和 y 同时指向 T。
y = x = norm1(x) 的 Python 引用语义
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) → gelu → dropout → y (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,非线性的"施展空间"很窄。先升维到
:
- 16 维特征被投影到更宽的空间,各维度之间有更多混合自由度
- GELU 在高维的选择性激活效果更强(不同神经元响应不同特征组合)
- 再压缩回 16,从 64 个候选表达中提炼出最有用的方向
这一 expand → activate → compress 结构来自原版 Transformer 论文(Vaswani 2017),扩展比
是经验上效果最好的设置。
步骤四 — 降维(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) → dropout → y (8,6,16)。
conv2 压缩回 d_model=16;.transpose(-1,1) 把轴顺序还原为 (B*C, patch_num, d_model),与输入格式一致。
步骤五 — 残差② + norm2
python
return self.norm2(x + y), attnx (8,6,16) 是步骤二保留的 norm1 输出 T(未被 FFN 修改);y (8,6,16) 是 FFN 的输出。x + y 做第二个残差连接,norm2 归一化后返回 (8, 6, 16)。
6. 下钻子组件
| 子组件 | 职责 | 下层文档 |
|---|---|---|
AttentionLayer(self.attention) | d_model 格式 ↔ 多头格式的桥梁;Q/K/V 投影 + 委托 FullAttention | 04B-Layer4-AttentionLayer |