Skip to content

Layer 2A — PatchEmbedding 精读

父层(Layer 1)forecast() 的第④步调用 self.patch_embedding(x_enc)
本文档覆盖 PatchEmbedding 的完整计算:ReplicationPad1d + unfold + channel-independent reshape + value_embedding + position_embedding。

1. 在父层中的位置

forecast(x_enc)
  ├─ ①② 归一化
  ├─ ③  permute: (B,seq_len,C) → (B,C,seq_len)
  └─ ④  self.patch_embedding(x_enc)   ← 本文档
       └─ padding_patch_layer(x)      → (B,C,T+stride)
       └─ x.unfold(...)               → (B,C,patch_num,patch_len)
       └─ reshape(B*C,...)            → (B*C,patch_num,patch_len)
       └─ value_embedding(x) + position_embedding(x)
                                      → (B*C,patch_num,d_model)

2. I/O 接口定义

python
class PatchEmbedding(nn.Module):
    def forward(self, x) -> Tuple[Tensor, int]:
shape(toy)含义
输入 x(2, 4, 12) = (B, C, T)经过 permute 后的时序,C 在 dim=1
输出 enc_out(8, 6, 16) = (B*C, patch_num, d_model)patch token 的 embedding
输出 n_vars4 = enc_in变量数,用于后续 reshape 还原

3. 顺序图(具体层)


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


5. 精读

阅读 更详细的基础函数例子

5.0 完整函数原始代码

python
# Embed.py:198-206
def forward(self, x):
    # do patching
    n_vars = x.shape[1]
    x = self.padding_patch_layer(x)
    x = x.unfold(dimension=-1, size=self.patch_len, step=self.stride)
    x = torch.reshape(x, (x.shape[0] * x.shape[1], x.shape[2], x.shape[3]))
    # Input encoding
    x = self.value_embedding(x) + self.position_embedding(x)
    return self.dropout(x), n_vars

8 行代码。以下各节逐步拆解,覆盖 n_vars 保存、padding、unfold、reshape、embedding 每一步。


5.1 宏观逻辑:三步 pipeline 的设计意图 ✨✨✨

PatchTST 的目标是:把"每个变量的一整条时间序列"切成一串 patch token,让 Transformer 在 patch 之间做 attention,且各变量独立处理

这三行代码分别完成三件事:

python
x = self.padding_patch_layer(x)          # ① 补齐时间轴
x = x.unfold(...)                         # ② 沿时间轴切 patch
x = torch.reshape(x, (B*C, ...))          # ③ 把变量当独立样本

用小例子(B=1, C=2, T=6, patch_len=3, stride=2, padding=2, d_model=4)串起来看:

① 为什么先填充?

原始序列(变量 0): [1, 2, 3, 4, 5, 6]

不填充时能切出:
  Patch 0 起点0: [1, 2, 3]
  Patch 1 起点2: [3, 4, 5]
  下一个起点4:   [5, 6, ?]  ← 不够 3 个点,切不出来

右端复制填充 padding=stride=2 步:
  [1, 2, 3, 4, 5, 6, 6, 6]   → 最后一个 patch 可以完整切出 [5, 6, 6]

填充是为了让最后一段时间也能形成完整 patch。

② 为什么 unfold 会把时间轴变成两个维度?

原来: T_padded = 8  (一条线)

切 patch 后,变成二维结构:patch_num 个 patch,每个 patch 有 patch_len 个点

patch_num = floor((8 - 3) / 2) + 1 = 3

shape: (1, 2, 8) → (1, 2, 3, 3)  即 (B, C, patch_num, patch_len)

unfold 把连续时间序列切成一串局部片段,每个片段是一个 Transformer token 的原材料。

③ 为什么把 B 和 C 合并?

unfold 后: (B=1, C=2, patch_num=3, patch_len=3)
           ↕ 合并 B 和 C
reshape 后: (B*C=2, patch_num=3, patch_len=3)

第 0 条序列 = batch 0 的变量 0:    [ [1,2,3], [3,4,5], [5,6,6] ]
第 1 条序列 = batch 0 的变量 1:    [ [10,20,30], [30,40,50], [50,60,60] ]

Transformer 看到的是 2 条互相独立的 3-token 序列,不知道它们来自同一个 batch 的两个变量。attention 只在每条序列内部的 patch 间发生——这就是 Channel-Independent。

reshape(B*C, ...) 是为了让每个变量独立进入 Transformer。

shape 变化全链

(B, C, T)       → padding →  (B, C, T+stride)
                → unfold  →  (B, C, patch_num, patch_len)
                → reshape →  (B*C, patch_num, patch_len)
                → Linear  →  (B*C, patch_num, d_model)

为什么不先把 B 和 C 合并、再 padding 和 unfold?
因为 ReplicationPad1d 要求输入是 (B, C, L) 三维格式。padding 和 unfold 都在时间轴操作,保持 (B, C, T) 格式更自然;patch 切完之后再合并变量,才是"每个变量的一串 patch = 一个独立训练样本"这个语义的自然表达。


5.2 n_vars 保存

python
# Embed.py:200
n_vars = x.shape[1]
# x: (B, C, T) = (2, 4, 12)
# x.shape[1] = C = enc_in = 4

这个值在 PatchEmbedding 返回后,由 forecast() 用于步骤⑥的 reshape(-1, n_vars, ...)B*C 还原成 (B, C, ...)


5.3 ReplicationPad1d(右端填充)✨

python
# Embed.py:187,201
self.padding_patch_layer = nn.ReplicationPad1d((0, padding))
# (左端填充, 右端填充) = (0, padding=stride=2)

x = self.padding_patch_layer(x)
# 输入: (B, C, T) = (2, 4, 12)
# 输出: (B, C, T+padding) = (2, 4, 14)

ReplicationPad1d 复制边缘值(不补零)。输入格式必须是 (B, C, L)——上一步 permute 已满足。

为什么右端填充 stride=2 步?

padding=stride 时 patch_num 公式:
  patch_num = floor((T + padding - patch_len) / stride) + 1
            = floor((12 + 2 - 4) / 2) + 1
            = floor(10/2) + 1 = 6

若 padding=0:patch_num = floor((12-4)/2)+1 = 5,少一个 patch
head_nf = d_model × patch_num 也会随之缩水

为什么复制而不是补零?
补零相当于序列末尾出现了人为的"值=0时间步",会拉低最后几个 patch 的均值,干扰局部模式。复制末尾值让边界 patch 的分布保持和内部 patch 一致。

toy 数值(以 batch=0, var=0 为贯通示例,设其归一化后值为 1~12 的整数):

填充前  x[0, 0, :] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]  (12步)
填充后  x[0, 0, :] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 12, 12]
                                                              ↑    ↑
                                                        复制最后一步 t11=12

输出 shape: (2, 4, 14)

5.4 unfold 切割 patch

python
# Embed.py:202
x = x.unfold(dimension=-1, size=self.patch_len, step=self.stride)
# toy: unfold(dimension=-1, size=4, step=2)
# 输入:  (B, C, T_padded) = (2, 4, 14)
# 输出:  (B, C, patch_num, patch_len) = (2, 4, 6, 4)

unfold(dim, size, step) 沿 dim 做滑窗:窗口大小 size,步长 step。输出在 dim 位置多出一个维度 (patch_num, patch_len)

patch_num = floor((T_padded - patch_len) / stride) + 1
          = floor((14 - 4) / 2) + 1 = 6

toy 数值(batch=0, var=0,延续上一步的具体值):

padded: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 12, 12]
         0  1  2  3  4  5  6  7  8   9  10  11  12  13  ← 下标

Patch 0 起点 0:  [1,  2,  3,  4 ]
Patch 1 起点 2:  [3,  4,  5,  6 ]
Patch 2 起点 4:  [5,  6,  7,  8 ]
Patch 3 起点 6:  [7,  8,  9,  10]
Patch 4 起点 8:  [9,  10, 11, 12]
Patch 5 起点 10: [11, 12, 12, 12]  ← 最后两步是填充复制的 12

所以 x[0, 0] 变成形状 (6, 4) 的矩阵:

x[batch=0, var=0] =
[
  [ 1,  2,  3,  4],   ← Patch 0(最旧)
  [ 3,  4,  5,  6],   ← Patch 1(与 Patch 0 重叠 2 步)
  [ 5,  6,  7,  8],
  [ 7,  8,  9, 10],
  [ 9, 10, 11, 12],
  [11, 12, 12, 12],   ← Patch 5(最新,含填充)
]

重叠:相邻 patch 共享 patch_len - stride = 4 - 2 = 2 步,保证边界处的连续性。

下面这张 SVG 把同样的切割过程换成 B=1, C=2, T=6 的具体数字例子,同时展示两个变量各自被切成哪些 patch:

论文/原理描述代码实现关键原因
"把时间序列切成局部 patch,每个 patch 是一个 token"unfold(dim=-1, size=patch_len, step=stride)滑窗产生所有 patch,stride < patch_len 时允许重叠

5.5 reshape:channel-independent 的核心✨✨✨

python
# Embed.py:203
x = torch.reshape(x, (x.shape[0] * x.shape[1], x.shape[2], x.shape[3]))
# 输入:  (B, C, patch_num, patch_len) = (2, 4, 6, 4)
# 输出:  (B*C, patch_num, patch_len) = (8, 6, 4)

B=2 个样本 × C=4 个变量 → B*C=8 条独立序列。reshape 后 dim=0 的排列顺序:

x[0] = batch=0, var=0 的 6 个 patches(即上面那个 (6,4) 矩阵)
x[1] = batch=0, var=1 的 6 个 patches
x[2] = batch=0, var=2 的 6 个 patches
x[3] = batch=0, var=3 的 6 个 patches
x[4] = batch=1, var=0 的 6 个 patches
x[5] = batch=1, var=1 的 6 个 patches
x[6] = batch=1, var=2 的 6 个 patches
x[7] = batch=1, var=3 的 6 个 patches

在小例子里,(B,C,patch_num,patch_len)=(1,2,3,3) 会被合并成 (B*C,patch_num,patch_len)=(2,3,3),也就是两条单变量 patch 序列:

从此起,Encoder 看到的是 8 条互相独立的 6-token 序列,不知道哪些来自同一个样本、哪些来自同一个变量——注意力只在每条序列内部的 6 个 patch 间计算,变量间天然隔离。

论文/原理描述代码实现关键原因
"Channel-Independent: 各变量不共享注意力"reshape(B*C, patch_num, patch_len)把 C 并入 batch,Transformer 对每条序列独立处理,变量间无信息交换

5.6 value_embedding:patch → d_model 空间✨

python
# Embed.py:190,205
self.value_embedding = nn.Linear(patch_len, d_model, bias=False)
# toy: Linear(in=4, out=16, bias=False)

x = self.value_embedding(x)
# 输入:  (B*C, patch_num, patch_len) = (8, 6, 4)
# 输出:  (B*C, patch_num, d_model)   = (8, 6, 16)

nn.Linear 只作用于最后一维。(8, 6, 4) 中 8×6=48 个长度为 4 的 patch,各自独立投影到 d_model=16 维。

公式(bias=False):

y = x @ W.T
W.shape = (d_model, patch_len) = (16, 4)   ← W 每一行是一个输出维度的投影向量
输入一个 patch: x[i] shape = (4,)
输出: y[i] = W @ x[i],shape = (16,)

toy 数值(用 Patch 0 示例,假设初始化后 W 第0行 = [0.1, 0.2, 0.3, 0.4]):

Patch 0 of x[0]: [1, 2, 3, 4]

输出维度 0: W[0] · [1,2,3,4] = 0.1×1 + 0.2×2 + 0.3×3 + 0.4×4
                              = 0.1 + 0.4 + 0.9 + 1.6 = 3.0

输出 y[0, 0, :] shape = (16,)  ← 16个维度各用自己的 W 行计算

W 的权重在训练中学习,让不同的 patch 形状映射到 d_model 空间里有意义的位置。


5.7 position_embedding:加位置信息

原始代码(Embed.py:8-27):

python
class PositionalEmbedding(nn.Module):
    def __init__(self, d_model, max_len=5000):
        super(PositionalEmbedding, self).__init__()
        pe = torch.zeros(max_len, d_model).float()
        pe.require_grad = False

        position = torch.arange(0, max_len).float().unsqueeze(1)
        div_term = (
            torch.arange(0, d_model, 2).float() * -(math.log(10000.0) / d_model)
        ).exp()

        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)

        pe = pe.unsqueeze(0)
        self.register_buffer("pe", pe)

    def forward(self, x):
        return self.pe[:, : x.size(1)]

注解版:

python
# position: [0, 1, 2, ..., 4999].unsqueeze(1) → shape (5000, 1)
# div_term: exp(- arange(0,d_model,2) × log(10000) / d_model)
#   = [10000^(0/d_model), 10000^(-2/d_model), ..., 10000^(-(d_model-2)/d_model)]^(-1)

# pe[:, 2i]   = sin(pos / 10000^(2i/d_model))  ← 偶数维度
# pe[:, 2i+1] = cos(pos / 10000^(2i/d_model))  ← 奇数维度

pe = pe.unsqueeze(0)  # (5000, d_model) → (1, 5000, d_model),备用

# forward: 只取前 patch_num 个位置
return self.pe[:, :x.size(1)]  # (1, 6, 16)(toy 中 x.size(1) = patch_num = 6)

公式含义pos = patch 的位置索引(0~5),i = embedding 维度索引(0~7,即 d_model/2 个频率)。不同维度用不同频率的正余弦,让模型可以通过线性变换捕获相对位置关系。

为什么 pe.require_grad = False:位置编码是确定性的数学函数,不需要学习,固定即可。注意这里是 require_grad(旧版写法),等价于 requires_grad=False

toy 数值(部分,d_model=16,取前 2 个 patch 位置,前 4 个维度):

div_term[0] = 10000^(0/16)  = 1.0      → 1/1.0   = 1.000
div_term[1] = 10000^(-2/16) = 10000^(-0.125) ≈ 0.562  → 实际取exp(...)
div_term[2] = 10000^(-4/16) = 10000^(-0.25) ≈ 0.316
div_term[3] = 10000^(-6/16) ≈ 0.178

pe[pos=0, dim=0] = sin(0 × 1.000) = 0.000
pe[pos=0, dim=1] = cos(0 × 1.000) = 1.000
pe[pos=0, dim=2] = sin(0 × 0.562) = 0.000
pe[pos=0, dim=3] = cos(0 × 0.562) = 1.000

pe[pos=1, dim=0] = sin(1 × 1.000) = 0.841
pe[pos=1, dim=1] = cos(1 × 1.000) = 0.540
pe[pos=1, dim=2] = sin(1 × 0.562) = 0.531
pe[pos=1, dim=3] = cos(1 × 0.562) = 0.847

register_buffer:把 pe 注册为模型的 buffer(不是 parameter),会随模型 .to(device) 自动移动到 GPU,但不出现在 model.parameters() 里,不参与梯度更新。

位置编码的意义:告诉模型每个 patch 在序列中的位置(patch 0 是最早的,patch 5 是最近的)。没有位置编码,注意力机制是置换不变的,无法区分 patch 顺序。

python
# forward 中的加法:
x = self.value_embedding(x) + self.position_embedding(x)
# value_embedding 输出: (8, 6, 16)
# position_embedding 返回 pe[:, :6, :] → (1, 6, 16)
# 广播加法: (8, 6, 16) + (1, 6, 16) = (8, 6, 16)
# 8 个序列共享同一套位置编码(广播自 batch=1)

6. 下钻子组件

PatchEmbedding 所有子组件(ReplicationPad1d、unfold、Linear、PositionalEmbedding)已在本文档完整精读,无需进一步下钻。

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