Skip to content

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):

y=xWT,WRd_model×d_inp=R8×4

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" 时走 TemporalEmbeddingx_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 只用 xsize(1) 获取序列长度,截取前 seq_len 行,返回 (1, seq_len, d_model),在加法时广播到 batch 维。

公式

pe[pos,2i]=sin(pos100002i/d_model),pe[pos,2i+1]=cos(pos100002i/d_model)

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 的内部已在本文档完整精读,无需进一步下钻。

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