Skip to content

DUET 调试形参

Abstract

这篇只做一件事:

保存用于学习 DUET 代码运行流程的 PyCharm 参数,并保证模型内部关键分支与循环至少执行一次。


1. PyCharm 配置

Script path

text
D:\1sudyta\1ai-self\aistyle\TFB\scripts\run_benchmark.py

Working directory

text
D:\1sudyta\1ai-self\aistyle\TFB

Environment variables

text
KMP_DUPLICATE_LIB_OK=TRUE

2. Parameters

直接复制下面这一整行到 PyCharm 的 Parameters

text
--config-path rolling_forecast_config.json --data-name-list ETTh1.csv --model-name "duet.DUET" --model-hyper-params "{\"batch_size\":2,\"seq_len\":24,\"horizon\":6,\"d_model\":8,\"d_ff\":16,\"n_heads\":2,\"e_layers\":1,\"num_experts\":3,\"k\":1,\"noisy_gating\":false,\"moving_avg\":3,\"hidden_size\":16,\"CI\":true,\"dropout\":0.0,\"fc_dropout\":0.0,\"loss\":\"huber\",\"lr\":0.001,\"lradj\":\"type1\",\"num_epochs\":1,\"num_workers\":0,\"patience\":100}" --strategy-args "{\"horizon\":6,\"tv_ratio\":0.8,\"train_ratio_in_tv\":0.75,\"stride\":6,\"num_rollings\":2}" --num-workers 1 --timeout 600 --save-path "debug\ETTh1_DUET_rolling_min"

2.1 VSCode 调试配置

先在 VSCode 里执行:

text
Ctrl+Shift+P
-> Python: Select Interpreter
-> 选择 D:\Anaconda\envs\tfb\python.exe

然后在仓库根目录创建或修改:

text
D:\1sudyta\1ai-self\aistyle\TFB\.vscode\launch.json

加入下面配置:

json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "TFB DUET rolling debug",
      "type": "python",
      "request": "launch",
      "program": "${workspaceFolder}\\scripts\\run_benchmark.py",
      "cwd": "${workspaceFolder}",
      "console": "integratedTerminal",
      "env": {
        "KMP_DUPLICATE_LIB_OK": "TRUE"
      },
      "args": [
        "--config-path", "rolling_forecast_config.json",
        "--data-name-list", "ETTh1.csv",
        "--model-name", "duet.DUET",
        "--model-hyper-params", "{\"batch_size\":2,\"seq_len\":24,\"horizon\":6,\"d_model\":8,\"d_ff\":16,\"n_heads\":2,\"e_layers\":1,\"num_experts\":3,\"k\":1,\"noisy_gating\":false,\"moving_avg\":3,\"hidden_size\":16,\"CI\":true,\"dropout\":0.0,\"fc_dropout\":0.0,\"loss\":\"huber\",\"lr\":0.001,\"lradj\":\"type1\",\"num_epochs\":1,\"num_workers\":0,\"patience\":100}",
        "--strategy-args", "{\"horizon\":6,\"tv_ratio\":0.8,\"train_ratio_in_tv\":0.75,\"stride\":6,\"num_rollings\":2}",
        "--num-workers", "1",
        "--timeout", "600",
        "--save-path", "debug\\ETTh1_DUET_rolling_min"
      ]
    }
  ]
}

3. 这组参数的第一性

这组参数不是为了刷 benchmark 分数,而是为了读代码:

text
1. 数据小、通道多:ETTh1.csv 有 7 个通道,保证 n_vars=7>1,
   让 Mahalanobis_mask 和 Channel_transformer 路径都能被命中。

2. seq_len=24、horizon=6,shape 好看,且 freq_size=24//2+1=13
   与 Mahalanobis A 参数形状 (13,13) 直接对应。

3. num_experts=3、k=1,保证 MoE 专家列表循环跑 3 次,
   SparseDispatcher 的 dispatch/combine 都会实际执行。

4. noisy_gating=false,门控网络走确定性路径(无高斯噪声),
   gating logits 在断点里是固定值,便于比较。

5. e_layers=1,保证 Channel_transformer 的 for 循环至少执行 1 次。

6. moving_avg=3,series_decomp 的 AvgPool1d 核可手算(两端各 pad 1)。

7. dropout=0.0、fc_dropout=0.0,关闭随机性,便于比较 tensor。

8. d_model=8、n_heads=2,与精读文档的 toy 参数完全一致:
   d_keys = d_model // n_heads = 8 // 2 = 4。
DUET 没有 --adapter 参数

Autoformer / Informer 等走 TransformerAdapter,需要传 --adapter transformer_adapter。 DUET 继承自 DeepForecastingModelBase,自定义了 _process(),不走 Transformer 适配器, 所以命令行里不需要也不应该加 --adapter


4. 参数含义

数据与策略参数

参数当前值作用
--config-pathrolling_forecast_config.json使用 rolling forecast 策略
--data-name-listETTh1.csv7 通道 ETT 数据,保证走多变量路径
horizon6每次预测未来 6 步,映射成 pred_len=6
tv_ratio0.8前 80% 进入 train/valid 区域
train_ratio_in_tv0.75train/valid 区域内再切训练段
stride6rolling 每次往后移 6 步
num_rollings2只滚 2 次,减少调试时间
--num-workers1外层只开 1 个 worker,方便断点
--timeout600单任务最长 600 秒

模型参数

参数当前值进入 DUET 后的意义
model-nameduet.DUET加载 duet/duet.py 里的 DUET
batch_size2每个训练 batch 两条样本
seq_len24encoder 输入历史长度
horizon6Config 设置 pred_len=6
d_model8Linear_extractor 的输出维(self.pred_len = configs.d_model!)
n_heads2Channel_transformer 多头数;d_keys=8//2=4
d_ff16Channel_transformer FFN 隐藏维(Conv1d 8→16→8)
e_layers1Channel_transformer EncoderLayer 循环次数
num_experts3MoE 专家总数
k1每个样本激活的专家数(top-k=1)
noisy_gatingfalse关闭 gating 高斯噪声,走 clean logits 路径
moving_avg3series_decomp AvgPool1d 核大小(两端 pad 1)
hidden_size16gate/noise encoder 的 MLP 隐藏层大小
CItrue走 channel-independent 路径(先 rearrange 展开)
dropout0.0关闭 attention dropout
fc_dropout0.0关闭 linear_head dropout
losshuberHuberLoss(DUET 默认,不同于其他模型的 MSE)
lr0.001学习率
num_epochs1只训练 1 轮,保证进入训练 forward
patience100关闭早停,跑满 1 epoch

5. 为什么这次能覆盖关键分支与循环

5.1 MoE 多变量路径(n_vars > 1)

DUETModel.forward() 里有一个关键条件:

python
if self.n_vars > 1:
    changed_input = rearrange(input, "b l n -> b n l")
    channel_mask = self.mask_generator(changed_input)
    channel_group_feature, attention = self.Channel_transformer(
        x=temporal_feature, attn_mask=channel_mask
    )
    output = self.linear_head(channel_group_feature)
else:
    output = temporal_feature
    output = self.linear_head(output)

ETTh1.csv 有 7 个通道,multi_forecasting_hyper_param_tune 自动设置:

text
self.config.enc_in = 7  →  DUETModel 里 self.n_vars = 7 > 1

所以 Mahalanobis_maskChannel_transformer 一定被调用。

若使用单变量数据集(如 cif_2016_dataset_1.csv),n_vars=1,会走 else 分支, 跳过整个通道路径,Mahalanobis 和 Channel_transformer 永远不执行。

5.2 MoE 专家循环跑 3 次

Linear_extractor_cluster.forward() 里的列表推导:

python
expert_outputs = [
    self.experts[i](expert_inputs[i]) for i in range(self.num_experts)
]

当前 num_experts=3,无论每个 expert 分到多少样本,循环固定跑 3 次。

对应地,SparseDispatcherdispatch / combine 也一定执行(即使某个 expert 的 expert_inputs[i] 为空 tensor,循环仍然进入)。

5.3 CI 路径与 RevIN CI 技巧

DUETModel.forward()CI=true

python
if self.CI:
    channel_independent_input = rearrange(input, "b l n -> (b n) l 1")
    reshaped_output, L_importance = self.cluster(channel_independent_input)
    temporal_feature = rearrange(
        reshaped_output, "(b n) l 1 -> b l n", b=input.shape[0]
    )

cluster.forward() 内部的 RevIN CI 技巧:

python
if self.CI:
    x_norm = rearrange(x, "(x y) l c -> x l (y c)", y=self.n_vars)
    x_norm = self.revin(x_norm, "norm")
    x_norm = rearrange(x_norm, "x l (y c) -> (x y) l c", y=self.n_vars)

当前 CI=true,两处 rearrange + RevIN 都会执行。

5.4 Channel_transformer EncoderLayer 循环至少跑 1 次

DUETModel.__init__() 构造 Channel_transformer

python
self.Channel_transformer = Encoder(
    [
        EncoderLayer(...)
        for _ in range(config.e_layers)
    ],
    ...
)

当前 e_layers=1,所以:

text
range(1) = [0]
EncoderLayer_0 一定被创建
Encoder.forward 里的 for attn_layer 至少执行 1 次

对应代码(Encoder.forward,走无 distilling 的 else 分支):

python
for attn_layer in self.attn_layers:
    x, attn = attn_layer(x, attn_mask=attn_mask, tau=tau, delta=delta)
    attns.append(attn)

5.5 noisy_gating=false 的 gating 路径

noisy_top_k_gating() 里的两条路径:

python
if self.noisy_gating and train:
    # 加高斯噪声(复杂,难以追踪)
    raw_noise_stddev = self.noise(x)
    ...
    noisy_logits = clean_logits + (noise * noise_stddev)
    logits = noisy_logits @ self.W_h
else:
    logits = clean_logits   ← 当前走这里

当前 noisy_gating=false,始终走 elselogits = clean_logits(无噪声)。 第一次断点调试建议走这条路径,后续再改 true 观察噪声分支。

5.6 additional_loss 路径(训练时)

DUET._process() 里:

python
output, loss_importance = self.model(input)
out_loss = {"output": output}
if self.model.training:
    out_loss["additional_loss"] = loss_importance   ← 训练时执行
return out_loss

训练阶段(num_epochs=1)一定进入训练 forward,additional_loss 路径执行。 loss_importance = cv_squared(importance) + cv_squared(load) 即 MoE 负载均衡损失。


6. 当前小例子的关键 shape

ETTh1.csv 有 7 个通道(N=7),与精读文档 toy 参数完全对齐。

当前 batch 的核心输入(DUET._process 接收):

text
input / x_enc:          (2, 24, 7)    ← B=2, seq_len=24, N=7
target:                 (2, 6, 7)     ← 被 _process 忽略
input_mark:             (2, 24, time_feature_dim)  ← 被 _process 忽略
target_mark:            (2, 6, time_feature_dim)   ← 被 _process 忽略
DUET 只用 input,target / mark 全部被 _process 忽略

DUET._process(self, input, target, input_mark, target_mark) 里只有 self.model(input)。 这是 DUET 区别于 Autoformer / Informer 的重要特征——它是 encoder-only 结构,无 decoder。

进入 DUETModel.forward(input) 后的 shape 追踪:

MoE 时序路径(CI 模式):

text
input:                  (2, 24, 7)
CI rearrange:           (2*7, 24, 1) = (14, 24, 1)

gate mean:              mean(x, dim=-1) → (14, 24)
gate encoder(24→16→3):  (14, 3) logits
softmax → topk(k=1):    gates (14, 3),每行仅 1 个非零

RevIN CI rearrange 临时: (14,24,1) → (2,24,7) → norm → (2,24,7) → (14,24,1)

SparseDispatcher.dispatch: 3 个子 batch,样本总数 = 14
  expert_inputs[0]:    (n₀, 24, 1)   n₀+n₁+n₂ = 14
  expert_inputs[1]:    (n₁, 24, 1)
  expert_inputs[2]:    (n₂, 24, 1)

每个 Linear_extractor:
  series_decomp(24→24)  ← 移动均值 kernel=3,两端 pad 1
  Linear_Seasonal(24→8) ← self.pred_len = configs.d_model = 8 !
  Linear_Trend(24→8)
  output: (n_i, 8, 1)

combine → (14, 8, 1)
rearrange: (14,8,1) → (2,8,7) → rearrange(b d n -> b n d) → (2,7,8)

通道路径(Mahalanobis + Channel_transformer):

text
changed_input rearrange: (2,24,7) → (2,7,24)
Mahalanobis_mask:
  rfft(dim=-1):   (2,7,24) → (2,7,13)  ← freq_size = 24//2+1 = 13
  A 参数 shape:   (13,13)
  dist:           (2,7,7)
  p:              (2,7,7) ∈ (0, 0.99]
  mask:           (2,1,7,7)   0/1 稀疏掩码

Channel_transformer(e_layers=1):
  EncoderLayer_0:
    Q/K/V proj: (2,7,8) → (2,7,8) → .view(2,7,2,4)
    scores:     einsum → (2,2,7,7)
    × mask(2,1,7,7) 广播 → 屏蔽弱相关通道对
    V 加权:     (2,7,2,4) → .view(2,7,8)
    FFN:        transpose→Conv1d(8→16)→GELU→Conv1d(16→8)→transpose
    out:        (2,7,8)
  LayerNorm(8) → (2,7,8)

linear_head:  Linear(8→6) + Dropout → (2,7,6)
rearrange:    (2,7,6) → (2,6,7)
RevIN denorm: (2,6,7)
output:       (2,6,7)   ← B=2, pred_len=6, N=7

7. 断点顺序

第一轮只看代码流,先不要急着手算 FFT 和 Gumbel。

  1. ts_benchmark/baselines/duet/duet.py

    • DUET._process(...)
    • output, loss_importance = self.model(input)additional_loss 路径。
  2. ts_benchmark/baselines/duet/models/duet_model.py

    • DUETModel.forward(...)
    • if self.CI 分支(展开 (2,24,7) → (14,24,1))和 if self.n_vars > 1 分支。
  3. ts_benchmark/baselines/duet/layers/linear_extractor_cluster.py

    • Linear_extractor_cluster.forward(...)
    • noisy_top_k_gating 输出 gates (14,3),以及 RevIN CI rearrange。
  4. ts_benchmark/baselines/duet/layers/linear_extractor_cluster.py

    • Linear_extractor_cluster.noisy_top_k_gating(...)
    • 当前 noisy_gating=false,看 logits = clean_logits 路径 → softmax → topk。
  5. ts_benchmark/baselines/duet/layers/linear_extractor_cluster.py

    • SparseDispatcher.__init__(...)
    • torch.nonzero(gates).sort(0) 怎样生成 _expert_index_batch_index_part_sizes
  6. ts_benchmark/baselines/duet/layers/linear_extractor_cluster.py

    • SparseDispatcher.dispatch(...)
    • inp[self._batch_index].squeeze(1) 怎样重排样本,再 split(_part_sizes)
  7. ts_benchmark/baselines/duet/layers/linear_pattern_extractor.py

    • Linear_extractor.forward(...)
    • self.decompsition(x) (⚠️ 拼写错误)和 self.pred_len = configs.d_model = 8 的含义。
  8. ts_benchmark/baselines/duet/layers/Autoformer_EncDec.py(duet 目录下)

    • series_decomp.forward(...)
    • moving_avg(x) 两端 replication padding(kernel=3, pad=1)和 res = x - moving_mean
  9. ts_benchmark/baselines/duet/layers/linear_extractor_cluster.py

    • SparseDispatcher.combine(...)
    • torch.einsum("i...,ij->i...", stitched, self._nonzero_gates)index_add
  10. ts_benchmark/baselines/duet/utils/masked_attention.py

    • Mahalanobis_mask.forward(...)
    • rfft → |XF| (2,7,13) → dist (2,7,7) → p ×0.99 → Gumbel-Bernoulli 采样。
  11. ts_benchmark/baselines/duet/utils/masked_attention.py

    • Encoder.forward(...)(Channel_transformer)
    • 确认走 else 分支(conv_layers=None),for attn_layer 循环 1 次。
  12. ts_benchmark/baselines/duet/utils/masked_attention.py

    • FullAttention.forward(...)
    • 看掩码应用:scores * attn_mask + where(attn_mask==0, -23.03, 0)

8. 当前学习主线

text
run_benchmark
-> pipeline
-> eval_model
-> RollingForecast._eval_batch
-> forecast_fit
-> DUET._process
-> DUETModel.forward
   |
   ├─ [CI 路径] rearrange (2,24,7) → (14,24,1)
   |
   ├─ Linear_extractor_cluster.forward
   |    ├─ noisy_top_k_gating → gates (14,3)
   |    ├─ RevIN CI trick: (14,24,1)→(2,24,7)→norm→(14,24,1)
   |    ├─ SparseDispatcher.dispatch → 3 sub-batches
   |    ├─ 3 × Linear_extractor.forward
   |    |    └─ series_decomp + Linear(24→8)
   |    └─ SparseDispatcher.combine → (14,8,1)
   |
   ├─ rearrange (14,8,1)→(2,8,7)→(2,7,8)
   |
   ├─ Mahalanobis_mask → (2,1,7,7)
   |
   ├─ Channel_transformer (Encoder)
   |    └─ EncoderLayer × 1
   |         ├─ FullAttention(带掩码)
   |         └─ Conv1d FFN
   |
   ├─ linear_head Linear(8→6) → (2,7,6)
   ├─ rearrange → (2,6,7)
   └─ RevIN denorm → output (2,6,7)

这一轮的第一性:

先看清 DUET 怎样把输入在 MoE 时序路径(分布漂移 → 多专家路由)和通道路径(频域相似度 → Mahalanobis 掩码注意力)两条路上分别处理,最后在 Channel_transformer 处汇合,输出预测。


9. 与 Autoformer 调试形参的关键区别

维度AutoformerDUET
--adaptertransformer_adapter不需要(直接继承 DeepForecastingModelBase
数据集选择cif_2016_dataset_1.csv(单变量)ETTh1.csv(7 通道,保证多变量路径)
额外损失MoE 负载均衡损失 L_importance(训练时)
loss 类型MSEHuberLoss
decoder 输入构造label_len 历史 + horizon 零占位无 decoder_process 只用 input
关键循环覆盖条件e_layers=1, d_layers=1e_layers=1, num_experts=3, n_vars=7>1
.cuda() 风险AutoCorrelation init_index.cuda(),DUET 代码全部用 .to(device)

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