Skip to content

Layer3 EncoderLayer

覆盖 Encoder.forward() 循环中单次 attn_layer(x, ...) 调用。EncoderLayer 由两个残差块组成:第一残差块(Self-Attention + Add & Norm)和第二残差块(FFN + Add & Norm)。注意力由 AttentionLayer 完成,细节见 [[04-Layer4-AttentionLayer]]。


§1 在父层中的位置

Encoder.forward() 循环体:

python
for attn_layer in self.attn_layers:
    x, attn = attn_layer(x, attn_mask=attn_mask, tau=tau, delta=delta)
    attns.append(attn)
  • 输入:x (3, 9, 8)
  • 输出:x (3, 9, 8)attn = None(iTransformer 不输出 attention weights)

§2 I/O 接口定义

参数shape(toy)含义
输入 x(3, 9, 8)B=3,token_count=9,d_model=8
输出 x(3, 9, 8)两轮残差更新后的 token 表示
输出 attnNoneoutput_attention=False 时为 None

§3 顺序图


§4 语义分组图


§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 宏观逻辑

用小例子(B=1,token=3,d_model=4,d_ff=8)串起来:

① 自注意力残差块
  x (1,3,4) → AttentionLayer(Q=K=V=x) → new_x (1,3,4)
  x = x + dropout(new_x)   ← 残差
  y = x = norm1(x)          ← norm1 的输出同时赋给 x 和 y(共享引用)

② FFN 残差块
  y (1,3,4)
    → y.transpose(-1,1)     (1,4,3)   ← Conv1d 需要 (B,C,L) 顺序
    → conv1(8→16, k=1)       (1,8,3)
    → gelu + dropout          (1,8,3)
    → conv2(16→8, k=1)        (1,4,3)
    → .transpose(-1,1)        (1,3,4)   ← 还原 (B,L,C) 顺序
    → dropout                 (1,3,4)   = y 新值
  return norm2(x + y), attn   ← x 仍是 norm1 后的值,加上 FFN 的 y

shape 全程 (1,3,4) 不变。

为什么 y = x = norm1(x) 看起来奇怪?

这是 Python 链式赋值:先算右边 norm1(x) 得到新 tensor,然后把 xy 同时指向它。效果是 x 变成了 norm1 后的值,y 也是这个值的引用(共享内存)。后续 FFN 在 y 上操作,不会影响 x,因为 FFN 的赋值 y = ...y 指向新的 tensor,不再与 x 共享。

y = x = expr 的引用语义

Python 链式赋值 y = x = norm1(x) 不是先让 y=x 再让 x=expr。执行顺序是:(1) 计算 norm1(x) → 新 tensor T;(2) 将 x 绑定到 T;(3) 将 y 绑定到 T。此后 xy 指向同一对象,但一旦 y = 新操作(y) 被执行,y 就指向新对象,x 不变。因此"共享开始,FFN 操作后分离"是这段代码的准确描述。


§5.2 步骤一:AttentionLayer(自注意力)

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

self.attentionAttentionLayer 实例,Q=K=V=x,全是同一输入 → Self-Attention。

  • 输入:x (3, 9, 8)
  • 输出:new_x (3, 9, 8)attn = None

语义:9 个变量 token 两两计算注意力分数,每个 token 更新为其他 token(包括自身)的加权组合。捕捉跨变量相关性。

详见 [[04-Layer4-AttentionLayer]]。


§5.3 步骤二:第一残差 + norm1

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

第一行 — 残差加法:

  • self.dropout(new_x):训练时随机置零部分元素,推理时恒等
  • x = x + dropout(new_x):残差连接,将注意力输出加回输入

输入 x (3,9,8),残差后仍 (3,9,8)

toy 数值(batch=0,token=0 即 var_0):

x[0, 0, :] 原始值:[x0, x1, ..., x7](8维,var_0 的嵌入)
new_x[0, 0, :]:AttentionLayer 输出,8维,编码了 var_0 对其他变量的关注结果
x[0, 0, :] += new_x[0, 0, :]   ← 加法在8个维度上逐元素执行

第二行 — LayerNorm:

norm1(x)=LayerNorm(dmodel=8)(x),对每个 token 的 8 维向量独立归一化:

yi=xiμσ2+εγi+βi,μ=18j=07xj,σ2=18j=07(xjμ)2

输出仍是 (3,9,8)xy 同时指向这个新 tensor。


§5.4 步骤三:FFN — Conv1d(8→16→8, kernel=1)

python
y = self.dropout(self.activation(self.conv1(y.transpose(-1, 1))))
y = self.dropout(self.conv2(y).transpose(-1, 1))
Conv1d(kernel=1) = Position-wise Linear

Conv1d(in_channels=8, out_channels=16, kernel_size=1) 对序列每个位置独立做 8→16 的线性变换,等价于 nn.Linear(8, 16) 但输入格式不同。Conv1d 要求 (B, C, L),因此需要 transpose 把 d_model 轴移到 C 位置。

y.transpose(-1, 1) 的含义:

y (3, 9, 8)transpose(-1, 1) 交换 dim=1 和 dim=-1(即 dim=2)→ (3, 8, 9)

  • 维度语义从 (B, token_count, d_model) 变为 (B, d_model, token_count)
  • token_count=9 现在在 L(长度)位置,d_model=8 在 C(通道)位置
  • Conv1d 把"通道"8 升到 16,"长度"9 不变

逐行 shape 追踪(全局 toy):

y                     (3, 9, 8)   ← norm1 输出
y.transpose(-1, 1)    (3, 8, 9)   ← Conv1d 需要 (B,C,L)
conv1(...)            (3, 16, 9)  ← Conv1d(8→16, k=1),每个 token 独立升维
activation(gelu)      (3, 16, 9)  ← 元素级 GELU
dropout               (3, 16, 9)  ← 随机置零
= y 新值

conv2(y)              (3, 8, 9)   ← Conv1d(16→8, k=1),降维回 d_model
.transpose(-1, 1)     (3, 9, 8)   ← 还原 (B, token_count, d_model)
dropout               (3, 9, 8)   ← 随机置零
= y 最终

toy 数值(batch=0,token=0):

y[0, :, 0](transpose 后)= var_0 token 的 8 维向量,作为 C 维输入
conv1 对这 8 个数做 8→16 线性变换(8组不同的权重向量各做一次点积+bias)
gelu 激活:gelu(x) = x × Φ(x),负值被压缩近零
conv2 对 16 个数做 16→8 线性变换
transpose 后 y[0, 0, :] = var_0 FFN 输出的 8 维向量

§5.5 步骤四:第二残差 + norm2

python
return self.norm2(x + y), attn
  • x:步骤二 norm1 后的值,(3, 9, 8)
  • y:FFN 输出,(3, 9, 8)
  • x + y:残差相加,(3, 9, 8)
  • self.norm2LayerNorm(8),归一化,(3, 9, 8)

toy 数值(batch=0,token=0):

x[0, 0, :] = norm1 后的 8 维向量(约零均值,近单位方差)
y[0, 0, :] = FFN 后的 8 维向量
(x + y)[0, 0, :] = 逐元素相加
norm2((x+y))[0, 0, :] = 再次归一化,作为本层最终输出

§6 初始化参数(toy)

EncoderLayer.__init__() 中的关键参数(iTransformer 传入值):

参数含义
d_model8token 维度
n_heads4注意力头数
d_ff16FFN 隐层维度(= d_model × 2,toy 中取 16)
dropout配置值Dropout 概率
activation'gelu'FFN 激活函数
conv1Conv1d(8, 16, 1)FFN 升维层
conv2Conv1d(16, 8, 1)FFN 降维层
norm1, norm2LayerNorm(8)两处归一化
attentionAttentionLayer(...)Self-Attention,见下层

§7 下钻子组件

组件输入 shape输出 shape下层文档
AttentionLayer(3, 9, 8)(3, 9, 8)[[04-Layer4-AttentionLayer]]

§8 结构图

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