Appearance
DUET 调试形参
Abstract
这篇只做一件事:
保存用于学习 DUET 代码运行流程的 PyCharm 参数,并保证模型内部关键分支与循环至少执行一次。
1. PyCharm 配置
Script path
text
D:\1sudyta\1ai-self\aistyle\TFB\scripts\run_benchmark.pyWorking directory
text
D:\1sudyta\1ai-self\aistyle\TFBEnvironment variables
text
KMP_DUPLICATE_LIB_OK=TRUE2. 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 参数
--adapter 参数Autoformer / Informer 等走
TransformerAdapter,需要传--adapter transformer_adapter。 DUET 继承自DeepForecastingModelBase,自定义了_process(),不走 Transformer 适配器, 所以命令行里不需要也不应该加--adapter。
4. 参数含义
数据与策略参数
| 参数 | 当前值 | 作用 |
|---|---|---|
--config-path | rolling_forecast_config.json | 使用 rolling forecast 策略 |
--data-name-list | ETTh1.csv | 7 通道 ETT 数据,保证走多变量路径 |
horizon | 6 | 每次预测未来 6 步,映射成 pred_len=6 |
tv_ratio | 0.8 | 前 80% 进入 train/valid 区域 |
train_ratio_in_tv | 0.75 | train/valid 区域内再切训练段 |
stride | 6 | rolling 每次往后移 6 步 |
num_rollings | 2 | 只滚 2 次,减少调试时间 |
--num-workers | 1 | 外层只开 1 个 worker,方便断点 |
--timeout | 600 | 单任务最长 600 秒 |
模型参数
| 参数 | 当前值 | 进入 DUET 后的意义 |
|---|---|---|
model-name | duet.DUET | 加载 duet/duet.py 里的 DUET 类 |
batch_size | 2 | 每个训练 batch 两条样本 |
seq_len | 24 | encoder 输入历史长度 |
horizon | 6 | Config 设置 pred_len=6 |
d_model | 8 | Linear_extractor 的输出维(self.pred_len = configs.d_model!) |
n_heads | 2 | Channel_transformer 多头数;d_keys=8//2=4 |
d_ff | 16 | Channel_transformer FFN 隐藏维(Conv1d 8→16→8) |
e_layers | 1 | Channel_transformer EncoderLayer 循环次数 |
num_experts | 3 | MoE 专家总数 |
k | 1 | 每个样本激活的专家数(top-k=1) |
noisy_gating | false | 关闭 gating 高斯噪声,走 clean logits 路径 |
moving_avg | 3 | series_decomp AvgPool1d 核大小(两端 pad 1) |
hidden_size | 16 | gate/noise encoder 的 MLP 隐藏层大小 |
CI | true | 走 channel-independent 路径(先 rearrange 展开) |
dropout | 0.0 | 关闭 attention dropout |
fc_dropout | 0.0 | 关闭 linear_head dropout |
loss | huber | HuberLoss(DUET 默认,不同于其他模型的 MSE) |
lr | 0.001 | 学习率 |
num_epochs | 1 | 只训练 1 轮,保证进入训练 forward |
patience | 100 | 关闭早停,跑满 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_mask 和 Channel_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 次。
对应地,SparseDispatcher 的 dispatch / 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,始终走 else,logits = 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 忽略
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=77. 断点顺序
第一轮只看代码流,先不要急着手算 FFT 和 Gumbel。
ts_benchmark/baselines/duet/duet.pyDUET._process(...)- 看
output, loss_importance = self.model(input)和additional_loss路径。
ts_benchmark/baselines/duet/models/duet_model.pyDUETModel.forward(...)- 看
if self.CI分支(展开 (2,24,7) → (14,24,1))和if self.n_vars > 1分支。
ts_benchmark/baselines/duet/layers/linear_extractor_cluster.pyLinear_extractor_cluster.forward(...)- 看
noisy_top_k_gating输出 gates (14,3),以及 RevIN CI rearrange。
ts_benchmark/baselines/duet/layers/linear_extractor_cluster.pyLinear_extractor_cluster.noisy_top_k_gating(...)- 当前
noisy_gating=false,看logits = clean_logits路径 → softmax → topk。
ts_benchmark/baselines/duet/layers/linear_extractor_cluster.pySparseDispatcher.__init__(...)- 看
torch.nonzero(gates).sort(0)怎样生成_expert_index、_batch_index、_part_sizes。
ts_benchmark/baselines/duet/layers/linear_extractor_cluster.pySparseDispatcher.dispatch(...)- 看
inp[self._batch_index].squeeze(1)怎样重排样本,再split(_part_sizes)。
ts_benchmark/baselines/duet/layers/linear_pattern_extractor.pyLinear_extractor.forward(...)- 看
self.decompsition(x)(⚠️ 拼写错误)和self.pred_len = configs.d_model = 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。
ts_benchmark/baselines/duet/layers/linear_extractor_cluster.pySparseDispatcher.combine(...)- 看
torch.einsum("i...,ij->i...", stitched, self._nonzero_gates)和index_add。
ts_benchmark/baselines/duet/utils/masked_attention.pyMahalanobis_mask.forward(...)- 看
rfft → |XF| (2,7,13)→ dist (2,7,7) → p ×0.99 → Gumbel-Bernoulli 采样。
ts_benchmark/baselines/duet/utils/masked_attention.pyEncoder.forward(...)(Channel_transformer)- 确认走
else分支(conv_layers=None),for attn_layer循环 1 次。
ts_benchmark/baselines/duet/utils/masked_attention.pyFullAttention.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 调试形参的关键区别
| 维度 | Autoformer | DUET |
|---|---|---|
--adapter | transformer_adapter | 不需要(直接继承 DeepForecastingModelBase) |
| 数据集选择 | cif_2016_dataset_1.csv(单变量) | ETTh1.csv(7 通道,保证多变量路径) |
| 额外损失 | 无 | MoE 负载均衡损失 L_importance(训练时) |
| loss 类型 | MSE | HuberLoss |
| decoder 输入构造 | label_len 历史 + horizon 零占位 | 无 decoder,_process 只用 input |
| 关键循环覆盖条件 | e_layers=1, d_layers=1 | e_layers=1, num_experts=3, n_vars=7>1 |
.cuda() 风险 | AutoCorrelation init_index.cuda() | 无,DUET 代码全部用 .to(device) |