Appearance
Layer 2A — DataEmbedding 精读
父层(Layer 1)的步骤②③:
enc_embedding(x_enc, x_mark_enc)和dec_embedding(x_dec, x_mark_dec),结构完全相同,本文合并讲解。
1. 在父层中的位置
long_forecast()
├─ ② enc_embedding(x_enc, x_mark_enc) → enc_out (3,10,8) ← 本文档
├─ ③ dec_embedding(x_dec, x_mark_dec) → dec_out (3,12,8) ← 本文档(结构相同)
├─ ④ encoder(enc_out)
└─ ⑤ decoder(dec_out, enc_out)2. I/O 接口定义
python
class DataEmbedding(nn.Module):
def forward(self, x, x_mark) -> Tensor:以 encoder embedding 为例:
| shape(toy) | 含义 | |
|---|---|---|
输入 x | (3, 10, 6) = (B, seq_len, enc_in) | 原始时序值 |
输入 x_mark | (3, 10, 4) | 时间特征(timeF 下:4维连续特征) |
| 输出 | (3, 10, 8) = (B, seq_len, d_model) | 三路叠加后的 token 表示 |
3. 顺序图(具体层)
4. 语义分组图(索引层)
5. 精读
5.0 完整原始代码
python
# Embed.py:118-140
class DataEmbedding(nn.Module):
def __init__(self, c_in, d_model, embed_type="fixed", freq="h", dropout=0.1):
super(DataEmbedding, self).__init__()
self.value_embedding = TokenEmbedding(c_in=c_in, d_model=d_model)
self.position_embedding = PositionalEmbedding(d_model=d_model)
self.temporal_embedding = (
TemporalEmbedding(d_model=d_model, embed_type=embed_type, freq=freq)
if embed_type != "timeF"
else TimeFeatureEmbedding(d_model=d_model, embed_type=embed_type, freq=freq)
)
self.dropout = nn.Dropout(p=dropout)
def forward(self, x, x_mark):
if x_mark is None:
x = self.value_embedding(x) + self.position_embedding(x)
else:
x = (
self.value_embedding(x)
+ self.temporal_embedding(x_mark)
+ self.position_embedding(x)
)
return self.dropout(x)15 行代码。
__init__注册三路嵌入模块;forward三路独立计算后 element-wise 相加。以下各节逐步拆解。
5.1 宏观逻辑:三路叠加的设计意图
本节的作用
三路独立嵌入(TokenEmbedding + TimeFeatureEmbedding + PositionalEmbedding)直接相加,把"原始值 + 时间戳"融合为统一的 d_model 维 token 表示。
DataEmbedding 的目标:让每个时间步的 token 同时携带值信息(局部时序模式)、时间信息(当前是几月几日几时)、位置信息(在序列里是第几步)——三件事各自独立计算,最后叠加在同一个 d_model 空间里。
用小例子(B=1, seq_len=3, enc_in=2, d_model=4, freq="h"→d_inp=2)串起来看:
输入 x[0]:
t=0: [1.0, 2.0] ← 两个变量在 t=0 的值
t=1: [3.0, 4.0]
t=2: [5.0, 6.0]
输入 x_mark[0]:
t=0: [0.08, 0.50] ← 归一化后的时间特征(示意)
t=1: [0.17, 0.50]
t=2: [0.25, 0.50]
① value_embedding: (1,3,2) → Conv1d → (1,3,4) 捕获相邻时步的值模式
② temporal_embedding: (1,3,2) → Linear → (1,3,4) 注入时间周期性
③ position_embedding: 查表 → (1,3,4) 广播 注入绝对位置
三者相加: (1,3,4) + (1,3,4) + (1,3,4) = (1,3,4)shape 变化全链:
x: (B, seq_len, enc_in) → TokenEmbedding(Conv1d) → (B, seq_len, d_model)
x_mark: (B, seq_len, 4) → TimeFeatureEmbedding(Linear) → (B, seq_len, d_model)
→ PositionalEmbedding(查表) → (1, seq_len, d_model)
↓ element-wise + (广播)
→ (B, seq_len, d_model) → dropout → 输出为什么三路相加而不是 concat?concat 会让维度变成 3×d_model,后续所有 Attention 参数量也随之膨胀;相加保持 d_model 不变,假设三种信息在同一语义空间内可以线性叠加。
5.2 TokenEmbedding — 值嵌入
本节的作用
Conv1d(enc_in→d_model, kernel=3, padding=1, circular)对每个时间步的多变量值做局部卷积投影,kernel=3 让每个时间步能看到左右各1个邻居,提取局部时序模式。
python
# Embed.py:30-50
class TokenEmbedding(nn.Module):
def __init__(self, c_in, d_model):
self.tokenConv = nn.Conv1d(
in_channels=c_in, out_channels=d_model,
kernel_size=3, padding=1, padding_mode="circular", bias=False,
)
def forward(self, x):
x = self.tokenConv(x.permute(0, 2, 1)).transpose(1, 2)
return x注解版:
x.permute(0, 2, 1) 把 (B, seq_len, enc_in) 变成 (B, enc_in, seq_len)。Conv1d 之后再 .transpose(1, 2) 还原为 (B, seq_len, d_model)。
图解 — Conv1d 为什么必须 transpose:
Conv1d 要求输入格式: (Batch, C_in, Length)
↑ ↑
通道数 序列长
当前 x 形状: (B=3, seq_len=10, enc_in=6) ← enc_in 在最后一维,不对!
permute(0, 2, 1) 交换 dim1 和 dim2:
(3, 10, 6) → (3, 6, 10)
现在 C_in=enc_in=6 ✓,Length=seq_len=10 ✓
tokenConv: Conv1d(6→8, k=3, p=1, circular)
(3, 6, 10) → (3, 8, 10)
.transpose(1, 2) 把通道和序列维换回来:
(3, 8, 10) → (3, 10, 8) = (B, seq_len, d_model) ✓图解 — circular padding vs zero padding:
原序列: [t0, t1, t2, ..., t9]
zero padding: [ 0, t0, t1, t2, ..., t9, 0] ← 头尾补零(人工引入零值)
circular padding: [t9, t0, t1, t2, ..., t9, t0] ← 首补尾值,尾补首值(周期延拓)
Conv1d(kernel=3) 在 t=0 位置的感受野:
zero: [0, t0, t1] ← 边界 0 会稀释输出
circular: [t9, t0, t1] ← 用真实序列值填边界论文直觉 — 为什么用 circular padding:时序数据首尾两端的时间步和中间位置同等重要,zero padding 会在边界处引入人工零值,导致边界 token 的卷积输出偏低——好像那里"没有历史"。circular padding 用序列自身的头尾值填充,保持所有位置的统计分布一致。
toy 数值追踪(第0个样本,第0个变量通道):
x[0, :, 0] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] (seq_len=10 步)
permute 后: x[0, 0, :] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
circular padding(k=3, p=1) 后(对 Length=10 维操作):
填充后 = [10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1] (头补t9=10,尾补t0=1)
Conv1d(kernel=3) 在 t=0 的输出(第0输出通道,只看第0输入通道的贡献):
out_partial = W[0,0,0]×10 + W[0,0,1]×1 + W[0,0,2]×2
(实际 out 对 enc_in=6 个通道各有一组 W,全部累加)
Conv1d(6→8, k=3) 完整输出: (3, 8, 10)
.transpose(1, 2) 后: (3, 10, 8) ✓| 论文/原理描述 | 代码实现 | 关键原因 |
|---|---|---|
| "对每个时间步提取局部值特征(看左右邻居)" | Conv1d(k=3, p=1, circular) | kernel=3 感受野覆盖相邻时步;circular 避免边界零稀释 |
Conv1d 要求 (B, C, L) 格式 | permute(0,2,1) → conv → transpose(1,2) | 输入是 (B,L,C) 格式,需先换轴再换回 |
5.3 TimeFeatureEmbedding — 时间嵌入(timeF 模式)
本节的作用
Linear(time_feat_dim→d_model, bias=False)把时间戳特征(小时、星期、月等连续归一化值)直接线性投影到 d_model,注入绝对时间周期信息。
python
# Embed.py:106-115
class TimeFeatureEmbedding(nn.Module):
def __init__(self, d_model, embed_type="timeF", freq="h"):
freq_map = {"h": 4, "t": 5, "s": 6, "m": 1, "a": 1, "w": 2, "d": 3, "b": 3}
d_inp = freq_map[freq]
self.embed = nn.Linear(d_inp, d_model, bias=False)
def forward(self, x):
return self.embed(x)freq="h" → d_inp=4(4维连续特征:月份/日期/星期/小时,各自归一化到 [-0.5, 0.5])。
注解版:
nn.Linear(4, 8, bias=False) 作用在最后一维。x_mark 的 (B, seq_len, 4) 可看作 B×seq_len 条长度为 4 的行向量,每条独立乘权重矩阵 W(shape (8, 4))得到长度为 8 的输出,重组回 (B, seq_len, 8)。
公式(bias=False):
toy 数值追踪(第0个样本,第0个时间步):
x_mark[0, 0, :] = [0.08, 0.10, 0.28, 0.75]
月份 日期 星期 小时 (归一化连续值)
设 W[0, :] = [0.5, -0.3, 0.2, 0.1] ← 第0输出维度的权重
temporal_out[0, 0, 0] = 0.08×0.5 + 0.10×(-0.3) + 0.28×0.2 + 0.75×0.1
= 0.04 - 0.03 + 0.056 + 0.075 = 0.141
temporal_out[0, 0, :] shape = (8,) ← 8个维度各用自己的 W[i,:] 计算
temporal_out shape = (3, 10, 8) ✓timeF vs fixed 的区别:
embed_type="fixed" 时走 TemporalEmbedding:x_mark 是整数索引(月=1~12,日=1~31,...),每个维度分别查独立的 nn.Embedding 表得到 d_model 维向量,四路向量相加。这种方式对整数离散标签自然,但梯度在整数边界处不连续。
embed_type="timeF"(TFB 实际使用)时走 TimeFeatureEmbedding:TFB 的 time_features.py 把时间戳预处理为连续浮点特征(如小时/23 - 0.5),再统一用一个 Linear 投影,梯度连续,对时间的"距离感"更平滑。
| 论文/原理描述 | 代码实现 | 关键原因 |
|---|---|---|
| "注入绝对时间信息(周期性)" | Linear(d_inp, d_model, bias=False) | 连续时间特征直接线性投影,比整数查表更平滑 |
| "freq 决定时间特征维数" | freq_map[freq] → d_inp | 小时粒度需4维(月/日/周/时),分钟粒度需5维 |
5.4 PositionalEmbedding — 位置嵌入
本节的作用
预计算 sin/cos 位置编码表(
register_buffer,不参与梯度),forward只截取前 seq_len 步并广播到 batch;让模型感知 token 的绝对位置,弥补 Attention 置换不变的缺陷。
python
# Embed.py:8-27
class PositionalEmbedding(nn.Module):
def __init__(self, d_model, max_len=5000):
pe = torch.zeros(max_len, d_model)
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)]注解版:
__init__ 计算一张 (1, 5000, d_model) 的位置编码表,存为 buffer。forward 只用 x 的 size(1) 获取序列长度,截取前 seq_len 行,返回 (1, seq_len, d_model),在加法时广播到 batch 维。
公式:
toy 数值(d_model=8,取 pos=0 和 pos=1):
div_term(4个频率,对应偶数维度 0,2,4,6):
i=0: exp(-0 × log(10000)/8) = 1.000
i=1: exp(-2 × log(10000)/8) ≈ 0.133
i=2: exp(-4 × log(10000)/8) ≈ 0.018
i=3: exp(-6 × log(10000)/8) ≈ 0.002
pos=0: sin(0)=0.000, cos(0)=1.000 (所有频率在 pos=0 的 sin 都是 0)
pos=1:
dim 0: sin(1×1.000)=0.841, cos(1×1.000)=0.540
dim 2: sin(1×0.133)=0.133, cos(1×0.133)=0.991
dim 4: sin(1×0.018)=0.018, cos(1×0.018)=1.000
dim 6: sin(1×0.002)=0.002, cos(1×0.002)=1.000
pe[0, :] = [0.000, 1.000, 0.000, 1.000, 0.000, 1.000, 0.000, 1.000]
pe[1, :] = [0.841, 0.540, 0.133, 0.991, 0.018, 1.000, 0.002, 1.000]不同 pos 的编码向量在各维度上形成不同频率的正余弦波,模型可通过线性变换推断相对位置差。
register_buffer vs nn.Parameter:
register_buffer("pe", pe) 把张量注册为 buffer(非参数):随 .to(device) 自动移动到 GPU,出现在 model.state_dict() 里(可以保存/加载),但不出现在 model.parameters() 里,不参与梯度更新。位置编码是固定数学函数,不需要学习,用 buffer 而非 parameter 是正确选择。
论文直觉 — 为什么 Transformer 需要位置编码:Attention 是置换不变的(permutation-invariant)——把序列打乱顺序,注意力权重仍然一样。没有位置编码,模型无法区分"t=3 的值"和"t=7 的值",时序预测中位置信息至关重要。sin/cos 编码用不同频率的波形让每个位置有独特的"指纹"。
| 论文/原理描述 | 代码实现 | 关键原因 |
|---|---|---|
| "固定正弦位置编码,不参与训练" | register_buffer("pe", pe) | buffer 随模型移动到 GPU 但不计入梯度,固定即可 |
| "不同频率正余弦让位置可区分" | sin/cos + div_term 多频率 | 高频维感知近邻,低频维感知全局位置 |
5.5 三路相加 + dropout
本节的作用
三路 embedding 直接逐元素相加(不是拼接),形状均为
(B, L, d_model=8),相加不改变维度;最后 dropout 正则化。
python
if x_mark is None:
x = self.value_embedding(x) + self.position_embedding(x)
else:
x = (
self.value_embedding(x)
+ self.temporal_embedding(x_mark)
+ self.position_embedding(x)
)
return self.dropout(x)注解版:
三路输出形状相同,直接相加:
value_embedding(x): (3, 10, 8) ← TokenEmbedding 输出
temporal_embedding(m): (3, 10, 8) ← TimeFeatureEmbedding 输出
position_embedding(x): (1, 10, 8) ← PositionalEmbedding 输出(广播 batch 维)
相加: (3, 10, 8) + (3, 10, 8) + (1, 10, 8) = (3, 10, 8)toy 数值追踪(第0个样本,第0个时间步,取前4维展示):
value_embed[0, 0, :4] = [ 0.42, -0.15, 0.63, 0.08] ← Conv1d 输出(示意)
temporal_embed[0, 0, :4] = [ 0.14, 0.09, -0.21, 0.31] ← Linear 输出(示意)
position_embed[0, 0, :4] = [ 0.00, 1.00, 0.00, 1.00] ← sin/cos pos=0
x[0, 0, :4] = 0.42+0.14+0.00, -0.15+0.09+1.00, 0.63-0.21+0.00, 0.08+0.31+1.00
= [0.56, 0.94, 0.42, 1.39]
dropout(p=0.1): 随机将约 10% 的元素置零并放大剩余(仅训练时)
输出 shape: (3, 10, 8) ✓x_mark is None 分支:当没有时间戳特征时(如某些基准测试),跳过 temporal_embedding,只做值嵌入 + 位置嵌入。TFB 的 _process() 始终传入 x_mark,不走此分支。
6. 下钻子组件
TokenEmbedding / TimeFeatureEmbedding / PositionalEmbedding 的内部已在本文档完整精读,无需进一步下钻。