Files
ts-as-modality/docs/superpowers/plans/2026-06-29-ts-as-modality-plan.md
T
张宗平 c160718934 chore(ts-as-modality): scaffold OpenSpec change & adopt reviewed design
- OpenSpec change: proposal/design/tasks + 3 delta specs
  (ts-synthesis, ts-multimodal-model, ts-evaluation; 21 requirements)
- comet state initialized (phase=build), plan recorded
- adopt existing reviewed design doc as Design Doc (+ comet frontmatter)
- existing implementation plan (+ comet frontmatter)
- workspace .gitignore
2026-06-29 22:29:31 +08:00

228 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
change: ts-as-modality
design-doc: docs/superpowers/specs/2026-06-29-ts-as-modality-design.md
base-ref: 7a5a1d033fb1ac20af25a17a571b2791694bd923
---
# 时序作为模态的多模态模型 · 实现计划
> **对应规格**`docs/superpowers/specs/2026-06-29-ts-as-modality-design.md`
> **日期**2026-06-29
> **目标**:把规格拆成有序、可验证的实现任务,按里程碑 M1→M5 推进
> **原则**:每个任务都有明确的"完成判定"(可跑一条命令/测试验证);任务间依赖清晰;先建最小可跑通骨架,再迭代填充
---
## 阶段总览
```
M1 数据管线 ──► M2 模型可跑通 ──► M3 阶段①对齐训练 ──► M4 阶段②SFT ──► M5 完整评测
(data) (model) (train stage1) (train stage2) (eval)
└─ 合成+真实 └─ TS Encoder └─ 50万 2-3ep └─ 10万 3ep └─ VUS-PR/问答/基线
```
每里程碑结束有"出口验证",通过才进下一个。首版先做"小规模冒烟"(数据 1k、训练几十步)确认管线通,再放大到全量。
---
## M1 · 数据管线
**目标**:能产出合法 JSONL 样本(合成)+ 能加载真实 benchmark 窗口。
### T1.1 项目脚手架
- 创建 `pyproject.toml`:依赖 `torch`, `transformers`, `peft`, `accelerate`, `datasets`, `numpy`, `tqdm`, `pyyaml`, `pytest`
- 创建 `src/tsmm/__init__.py``configs/``scripts/``tests/` 目录骨架
-`data/``checkpoints/` 并加 `.gitignore`
- **完成判定**`python -c "import tsmm"` 不报错;`pytest tests/` 能跑(哪怕空)
### T1.2 属性词表与采样 `data/attributes.py`
- 定义变量名词表(CPU/内存/流量/延迟/温度/请求量…)、单位、采样率档位
- `sample_attributes(n_channels)` → 返回 `[{"name","unit","freq"}, ...]`
- **完成判定**:单元测试 `tests/test_attributes.py` — 采样结果字段齐全、数量正确、可复现(设 seed)
### T1.3 成分模型与时序合成 `data/synthesis.py`
- 实现 `generate_series(T, C, seed)`:趋势+周期+基线+噪声(高斯/Student-t)叠加
- 实现 `inject_anomaly(series, types)`:尖刺/水平偏移/方差膨胀/缺失段/缓慢漂移,返回 `(series, labels)`labels 为逐点 0/1 + 段元数据
- 实现 `couple_event(series, t, kind)`:在 t 注入阶跃并返回事件文本描述
- 实现 `add_missing(series, rate)`:随机丢弃 5-10% 点(用 NaN 占位)
- **完成判定**`tests/test_synthesis.py` — 给定 seed 输出确定;异常段位置与 labels 一致;事件 t 处确有阶跃;可视化 1 条样本人工确认形态合理
### T1.4 指令与回答生成 `data/instruct.py`
- 定义 6 类指令模板(描述/异常/根因/预测/比较/事件关联)
- `build_instruction(series_meta, labels, events, category)``(instruction, answer)`answer 含 JSON 区间(异常类)或数值(统计类)或文本(解释类)
- Evol-Instruct 演化:用模板组合 + 随机改写生成多样化问法
- **完成判定**`tests/test_instruct.py` — 每类至少产 1 条,answer 的 JSON 可被 `json.loads` 解析;异常类 answer 段与 labels 吻合
### T1.5 离线生成脚本 `scripts/gen_synthetic.py`
- 多进程跑 T1.2-T1.4,产出 `data/align.jsonl`50万)+ `data/sft.jsonl`10万,Evol 演化)+ `data/eval_synth.jsonl`2k held-out,固定 seed
- 支持 `--n``--out``--seed``--workers` 参数;带 tqdm 进度
- **完成判定**`python scripts/gen_synthetic.py --n 1000` 跑通;抽样 5 条人工可视检 schema 合规(含 series/attributes/timestamps/events/instruction/answer/labels
### T1.6 真实 benchmark 加载 `data/real_bench.py`
- 加载 SMD/MSL/SMAP/SWaT/PSM(先用本地缓存或公开下载脚本;若无现成包,至少实现 SWaT/MSL 两个)
- `load_windows(name, T=512, stride=256)` → 滑窗 + 归一化 + 异常标签
- 用 LLM/模板把异常段转成问答对(评测用,不进训练),存 `data/eval_real.jsonl`
- **完成判定**:能加载至少 2 个数据集;窗口形状 `[N, T, C]`;标签形状 `[N, T]`;问答对非空
> **M1 出口验证**`gen_synthetic.py --n 1000` 产出可视检合法的 JSONL`real_bench.py` 能加载 ≥2 数据集。人工抽看 5 条合成样本确认成分/异常/事件正确。
---
## M2 · 模型可跑通
**目标**TS Encoder + Projector + 多模态拼接能在 3060 上前向+反向不 OOM。
### T2.1 TS Encoder `model/ts_encoder.py`
- `Patchify(patch=8, stride=4)`:通道独立切 patch,线性嵌入到 d=256
- `TSEncoder(d=256, layers=2, heads=4)`2 层 Transformer + 可学习位置编码
- 前向:`[B, T, C]``[B, n_patches, 256]`
- **完成判定**`tests/test_ts_encoder.py` — 随机输入 `[2, 512, 5]` 输出 `[2, 128, 256]`;参数量 ≈4M
### T2.2 Projector `model/projector.py`
- `Projector(256 → 896)`Linear + LayerNorm
- **完成判定**:输出 `[B, 128, 896]` 对齐 Qwen2.5-0.5B hidden
### T2.3 多模态拼接 `model/multimodal.py`
- 用 Qwen2.5-0.5B tokenizer 把 `[属性][时间戳][事件][问题]` 文本 tokenize
- 把 TS token[B,128,896])作为 inputs_embeds 插入对应位置:`[属性][时间戳][TS tok][事件][问题]`
- 统一构造 `inputs_embeds`(文本走 `get_input_embeddings()`TS 走 Projector 输出)+ `attention_mask` + `labels`(仅回答段非 -100
- **完成判定**`tests/test_multimodal.py` — 拼接后 seq_len ≤1024mask/labels 形状对;回答段 label 非 -100
### T2.4 训练/推理封装 `model/wrapper.py`
- `MultimodalTSModel`:组合 TS Encoder + Projector + LLM + LoRA 挂载开关
- `forward(batch)` 返回 loss`generate(batch)` 返回文本
- 支持 `freeze_llm`(阶段①)和 `enable_lora(r=16)`(阶段②)两种模式
- **完成判定**:单 batch `forward` 在 3060 不 OOM`generate` 能出文本(哪怕乱码)
### T2.5 Collator `data/collator.py`
- 把 JSONL 样本 batch 化为 `(series, attributes, timestamps, events, instruction, answer)` → 张量
- 处理变长 Cpadding + mask)、变长文本(padding + attention_mask
- **完成判定**`tests/test_collator.py` — batch=4 输出张量形状一致、无 NaN
> **M2 出口验证**:单 batch 前向+反向在 3060 跑通,loss 是有限值且下降趋势;阶段①模式峰值显存 ~5GB。
---
## M3 · 阶段① 对齐训练
**目标**50万条 2-3 epochTS token 不被忽略。
### T3.1 损失函数 `train/losses.py`
- `lm_loss(logits, labels)`:仅回答段计 LM losslabels mask
- `contrastive_loss(model, batch)`:扰动时序(加噪/平移)→ 回答 embedding 应变化(InfoNCE 或扰动一致性)
- 总 loss = `lm_loss + λ * contrastive_loss`(λ 可配,默认 0.1
- **完成判定**`tests/test_losses.py` — loss 标量、可反传、对比损失对扰动敏感
### T3.2 阶段① 训练脚本 `train/stage1.py` + `scripts/train_stage1.sh`
- 加载 `data/align.jsonl`datasets 流式)
- `freeze_llm=True`,仅训 TS Encoder + ProjectorAdamW, lr=1e-4
- 配置:bs=8, grad_accum=4, ctx=512, BF16, 梯度检查点
- 每 2000 step 存 ckpt 到 `checkpoints/stage1/`TensorBoard 记录
- **完成判定**`--max_steps 100` 冒烟跑通不 OOM;loss 下降;峰值显存 ≤6GB
### T3.3 TS-token 有效性检验
- 训练后取 held-out 样本:原时序 vs 扰动时序,对比模型回答是否变化
- "必看时序"样本:问具体数值/段位置,看模型能否答对
- **完成判定**:扰动后回答变化率 >50%;"必看时序"样本答对率显著高于纯 LLM 基线
> **M3 出口验证**:阶段① ckpt 产出;扰动检验通过;显存 ≤6GB。失败则回查对比损失权重/数据质量。
---
## M4 · 阶段② SFT
**目标**10万条 Evol-Instruct 3 epoch + LoRA6 类任务遵循指令。
### T4.1 阶段② 训练脚本 `train/stage2.py` + `scripts/train_stage2.sh`
- 加载阶段① ckptTS Encoder + Projector
- `enable_lora(r=16)` on q/v_projLLM 其余冻结
- 加载 `data/sft.jsonl`,配置:bs=4, grad_accum=8, ctx=1024, BF16, 梯度检查点
- 每 2000 step 存 ckpt(含 LoRA 适配器)到 `checkpoints/stage2/`
- **完成判定**`--max_steps 100` 冒烟跑通;峰值显存 ≤11GB;loss 下降
### T4.2 指令遵循抽检
- 6 类任务各取 10 条 held-out,人工/规则看回答是否遵循格式与语义
- 异常类:JSON 可解析、段位置合理
- 解释类:回答切题、引用事件
- **完成判定**:6 类任务回答可用率 >70%JSON 解析成功率 >80%
> **M4 出口验证**:阶段② ckpt 产出;6 类任务抽检达标;显存 ≤11GB。若 OOM 按风险对策降批量/开 CPU offload。
---
## M5 · 完整评测
**目标**:产出 VUS-PR/Aff-F1/问答分 + 4 类基线对比。
### T5.1 回答解析 `eval/parse_answer.py`
- 从模型文本回答中提取 JSON 异常段 → 逐点分数 `[T]`(段内 1.0,段外 0.0
- 解析失败 → 全 0 + 记录失败标记
- **完成判定**`tests/test_parse.py` — 多种回答格式(含噪声/嵌套)能解析;失败有兜底
### T5.2 异常检测指标 `eval/ts_metrics.py`
-`tsb_uad` 算 VUS-PR(主);自实现 Affiliation-F1(λ=0.1/0.5/1.0)、AUC-PR、Point-F1(无PA)
- PA-F1 单独算作对照
- **完成判定**`tests/test_ts_metrics.py` — 对已知段能算出合理值;trivial 基线(全报异常)VUS-PR 不虚高
### T5.3 问答评测 `eval/qa_judge.py`
- 规则解析轨:JSON/数值容差比对,产出准确率
- LLM-judge 轨:调 GPT-4o 对 (问题,时序摘要,参考答案,模型答案) 打 0-5 分(可换开源 judge 做 sanity
- 不一致样本导出供人工抽检
- **完成判定**:能对 eval 集产出 6 类任务的 {规则准确率, judge 均分, 解析成功率}
### T5.4 对照基线 `eval/baselines.py`
- 纯 LLM 数值喂文本:时序数值拼字符串喂 Qwen2.5-0.5B,同评测脚本
- Time-LLM 风格重映射:同样 0.5B 骨干(简化复现)
- ChatTS(若可复现则纳入,否则标注未复现)
- TrivialRandom / Constant / 全报异常
- 全部走同一套 `parse_answer` + `ts_metrics`,保证同口径
- **完成判定**:4 类基线均产出同口径指标表
### T5.5 评测脚本与报告 `scripts/run_eval.sh`
-`eval_synth.jsonl` + `eval_real.jsonl`,产出 markdown 报告 `reports/eval-YYYYMMDD.md`
- 报告含:每数据集×每任务的 VUS-PR/Aff-F1/AUC-PR/Point-F1/PA-F1(对照)/问答分/解析成功率/消融/基线对比
- 标注是否用 PA、含 trivial 对照
- **完成判定**:一键 `run_eval.sh` 产出完整报告;报告满足规格 §5.4 必报清单
> **M5 出口验证**:报告满足规格 §6.5 成功标准 —— 真实 benchmark VUS-PR 显著优于纯 LLM 基线;时序消融后指标明显下降;问答均分超基线;评测可复现含 trivial。
---
## 执行顺序与依赖
```
T1.1 ─► T1.2 ─► T1.3 ─► T1.4 ─► T1.5 ┐
├─► [M1 出口] ─► T2.1 ─► T2.2 ─► T2.3 ─► T2.4 ─► T2.5 ─► [M2 出口]
T1.6 ────────────────┘ │
T3.1 ─► T3.2 ─► T3.3 ─► [M3 出口]
T4.1 ─► T4.2 ─► [M4 出口]
T5.1 ─► T5.2 ┐
T5.3 ────────┼─► T5.4 ─► T5.5 ─► [M5 出口]
```
## 风险触发时的回退
- **M2 OOM**:先降 ctx 到 256 / bs 到 2;仍不行退到只验证阶段①模式
- **M3 TS token 被忽略**:提升对比损失 λ;增加"必看时序"样本占比;检查 Projector 是否饱和
- **M4 OOM**:降 bs=2/grad_accum=16;开 `optim=adamw_8bit` + CPU offloadctx 降到 768
- **M5 合成→真实泛化差**:在阶段②掺入 `real_bench` 改写样本重训;报告以真实 benchmark 为准
## 首次跑通的"冒烟路径"(先做这个)
不等全量数据,先用最小路径确认端到端通:
1. `gen_synthetic.py --n 200` 产小数据
2. T2.1-T2.4 单 batch 跑通
3. `stage1.py --max_steps 50` + `stage2.py --max_steps 50` 冒烟
4. `run_eval.sh` 在 200 样本上产出报告骨架
冒烟通过后再放大到 M1 全量数据 + 完整训练。
---
> 本计划把规格拆成 22 个任务、5 个带出口验证的里程碑。每个任务有"完成判定"可独立验证。建议从"冒烟路径"开始,确认端到端通后再放大规模。