Appearance
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_vars | 4 = 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_vars8 行代码。以下各节逐步拆解,覆盖
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 = 6toy 数值(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.847register_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)已在本文档完整精读,无需进一步下钻。